From 19d2f9c7d3930f981f2a0fa70d987f106fd9ae8a Mon Sep 17 00:00:00 2001 From: BlobMaster41 <96896824+BlobMaster41@users.noreply.github.com> Date: Fri, 29 May 2026 03:44:14 -0400 Subject: [PATCH 1/3] Add contract update support in VM Introduce end-to-end support for contract updates: track updatedContracts throughout evaluation and result objects, wire updatedContracts into VMManager and VM evaluator, and persist updates to storage. Added VMManager.updateContractAtAddress and persistContractUpdate with authorization and validation checks, plus integration into evaluator lifecycle. Extended ContractEvaluation/ContractEvaluator to collect and flush updated contracts, updated EvaluatedResult and InternalContractCallParameters types, and added VMStorage.updateContractBytecode + VMMongoStorage/ContractRepository plumbing (including new updateContractBytecode and sort-by-blockHeight changes). Enabled consensus flag for contract updates and adjusted MAXIMUM_UPDATE_DEPTH to mirror the fail-fast increment behavior. --- .../rpc/thread/BitcoinRPCThread.ts | 1 + src/src/db/repositories/ContractRepository.ts | 14 ++- .../consensus/RoswellConsensus.ts | 10 +- src/src/vm/VMManager.ts | 100 ++++++++++++++++++ src/src/vm/evaluated/EvaluatedResult.ts | 1 + src/src/vm/runtime/ContractEvaluator.ts | 18 ++++ .../vm/runtime/classes/ContractEvaluation.ts | 8 ++ .../types/InternalContractCallParameters.ts | 2 + src/src/vm/storage/VMStorage.ts | 2 + .../vm/storage/databases/VMMongoStorage.ts | 7 ++ 10 files changed, 157 insertions(+), 6 deletions(-) diff --git a/src/src/blockchain-indexer/rpc/thread/BitcoinRPCThread.ts b/src/src/blockchain-indexer/rpc/thread/BitcoinRPCThread.ts index 9416ad6ec..289d8d8ab 100644 --- a/src/src/blockchain-indexer/rpc/thread/BitcoinRPCThread.ts +++ b/src/src/blockchain-indexer/rpc/thread/BitcoinRPCThread.ts @@ -148,6 +148,7 @@ export class BitcoinRPCThread extends Thread { : response.result, revert: revertData, deployedContracts: [], + updatedContracts: [], }; } else { return { diff --git a/src/src/db/repositories/ContractRepository.ts b/src/src/db/repositories/ContractRepository.ts index 24a99e2a8..0f167c0d2 100644 --- a/src/src/db/repositories/ContractRepository.ts +++ b/src/src/db/repositories/ContractRepository.ts @@ -97,7 +97,7 @@ export class ContractRepository extends BaseRepository { criteria.blockHeight = { $lte: DataConverter.toDecimal128(height) }; } - const contract = await this.queryOne(criteria, currentSession); + const contract = await this.queryOne(criteria, currentSession, { blockHeight: -1 }); if (!contract) { return; } @@ -148,6 +148,7 @@ export class ContractRepository extends BaseRepository { contractPublicKey: 1, }, currentSession, + { blockHeight: -1 }, ); if (!contract) { @@ -174,6 +175,13 @@ export class ContractRepository extends BaseRepository { await this.insert(contract.toDocument(), currentSession); } + public async updateContractBytecode( + contract: ContractInformation, + currentSession?: ClientSession, + ): Promise { + await this.insert(contract.toDocument(), currentSession); + } + public async getContractFromTweakedPubKey( contractPublicKey: string, height?: bigint, @@ -194,7 +202,7 @@ export class ContractRepository extends BaseRepository { criteria.blockHeight = { $lte: DataConverter.toDecimal128(height) }; } - const contract = await this.queryOne(criteria, currentSession); + const contract = await this.queryOne(criteria, currentSession, { blockHeight: -1 }); if (!contract) { return; } @@ -219,7 +227,7 @@ export class ContractRepository extends BaseRepository { criteria.blockHeight = { $lte: DataConverter.toDecimal128(height) }; } - const contract = await this.queryOne(criteria, currentSession); + const contract = await this.queryOne(criteria, currentSession, { blockHeight: -1 }); if (!contract) { return; } diff --git a/src/src/poc/configurations/consensus/RoswellConsensus.ts b/src/src/poc/configurations/consensus/RoswellConsensus.ts index 3acfc9f6c..1549d0b8c 100644 --- a/src/src/poc/configurations/consensus/RoswellConsensus.ts +++ b/src/src/poc/configurations/consensus/RoswellConsensus.ts @@ -14,7 +14,7 @@ import { ConsensusRules } from '../../../vm/consensus/ConsensusRules.js'; const RoswellConsensusRules: ConsensusRules = new ConsensusRules(); RoswellConsensusRules.insertFlag(ConsensusRules.UNSAFE_QUANTUM_SIGNATURES_ALLOWED); -// RoswellConsensusRules.insertFlag(ConsensusRules.CONTRACT_UPDATES_ALLOWED); +RoswellConsensusRules.insertFlag(ConsensusRules.CONTRACT_UPDATES_ALLOWED); export const RoswellConsensus: IOPNetConsensus = { /** Information about the consensus */ @@ -230,9 +230,13 @@ export const RoswellConsensus: IOPNetConsensus = { ENABLE_ACCESS_LIST: false, /** - * The maximum amount of contract updates in a single transaction + * The maximum depth counter value for contract updates. + * Each update increments the counter TWICE (fail-fast check in + * updateContractFromAddressRaw + auto-increment in the ContractEvaluation + * constructor when isUpdate=true). A value of 2 therefore permits exactly + * one update per transaction. Mirrors MAXIMUM_DEPLOYMENT_DEPTH semantics. */ - MAXIMUM_UPDATE_DEPTH: 1, + MAXIMUM_UPDATE_DEPTH: 2, }, VM: { diff --git a/src/src/vm/VMManager.ts b/src/src/vm/VMManager.ts index 7cd04487c..f92271f43 100644 --- a/src/src/vm/VMManager.ts +++ b/src/src/vm/VMManager.ts @@ -521,6 +521,7 @@ export class VMManager extends Logger { mldsaLoadCounter: new MutableNumber(), deployedContracts: deployedContracts, + updatedContracts: undefined, callStack: undefined, touchedAddresses: undefined, @@ -898,6 +899,18 @@ export class VMManager extends Logger { } } + if (!vmEvaluator && params.updatedContracts) { + const updatedContract = params.updatedContracts.get(params.contractAddress); + + if (updatedContract) { + vmEvaluator = await this.getVMEvaluatorFromParams( + params.contractAddress, + params.blockHeight, + updatedContract, + ); + } + } + if (!vmEvaluator) { vmEvaluator = params.allowCached ? await this.getVMEvaluatorFromCache( @@ -937,6 +950,7 @@ export class VMManager extends Logger { mldsaLoadCounter: params.mldsaLoadCounter, deployedContracts: params.deployedContracts, + updatedContracts: params.updatedContracts, memoryPagesUsed: params.memoryPagesUsed, touchedAddresses: params.touchedAddresses, @@ -1099,6 +1113,90 @@ export class VMManager extends Logger { }; } + private async updateContractAtAddress( + sourceAddress: Address, + evaluation: ContractEvaluation, + ): Promise<{ bytecodeLength: number } | undefined> { + if (!OPNetConsensus.allowContractUpdates()) { + throw new Error('OP_NET: Contract updates are not allowed in current consensus.'); + } + + const currentContractInfo = await this.getContractInformation( + evaluation.contractAddress, + evaluation.blockNumber, + ); + + if (!currentContractInfo) { + throw new Error('OP_NET: Contract not found for update.'); + } + + if (!evaluation.msgSender.equals(currentContractInfo.deployerAddress)) { + throw new Error('OP_NET: Only the original deployer can update this contract.'); + } + + if (sourceAddress.equals(evaluation.contractAddress)) { + throw new Error('OP_NET: Contract cannot use itself as update source.'); + } + + let sourceContractInfo: ContractInformation | undefined = + evaluation.deployedContracts.get(sourceAddress); + + if (!sourceContractInfo) { + sourceContractInfo = await this.getContractInformation( + sourceAddress, + evaluation.blockNumber, + ); + } + + if (!sourceContractInfo) { + throw new Error('OP_NET: Source contract not found.'); + } + + if (!sourceContractInfo.bytecode || sourceContractInfo.bytecode.byteLength === 0) { + throw new Error('OP_NET: Source contract has no bytecode.'); + } + + const updatedContractInfo = new ContractInformation( + evaluation.blockNumber, + currentContractInfo.contractAddress, + currentContractInfo.contractPublicKey, + sourceContractInfo.bytecode, + currentContractInfo.wasCompressed, + evaluation.transactionId || alloc(32), + evaluation.transactionHash || alloc(32), + currentContractInfo.deployerPubKey, + currentContractInfo.contractSeed, + currentContractInfo.contractSaltHash, + currentContractInfo.deployerAddress, + ); + + evaluation.addUpdatedContractInformation(updatedContractInfo); + + return { bytecodeLength: sourceContractInfo.bytecode.byteLength }; + } + + private async persistContractUpdate( + contractInformation: ContractInformation, + ): Promise { + if (this.isExecutor) { + return; + } + + if ( + !contractInformation.deployedTransactionId || + !contractInformation.deployedTransactionHash + ) { + throw new Error( + 'Transaction id or hash not found. [persistContractUpdate]', + ); + } + + this.contractCache.delete(contractInformation.contractPublicKey); + this.vmEvaluators.delete(contractInformation.contractPublicKey); + + await this.vmStorage.updateContractBytecode(contractInformation); + } + private async deployContractFromInfo(contractInformation: ContractInformation): Promise { if (this.isExecutor) { // Emulators dont deploy contracts. @@ -1141,6 +1239,8 @@ export class VMManager extends Logger { vmEvaluator.isContract = this.isContract.bind(this); vmEvaluator.callExternal = this.callExternal.bind(this); vmEvaluator.deployContractAtAddress = this.deployContractAtAddress.bind(this); + vmEvaluator.updateFromAddressJsFunction = this.updateContractAtAddress.bind(this); + vmEvaluator.persistContractUpdate = this.persistContractUpdate.bind(this); vmEvaluator.getMLDSAPublicKey = this.getMLDSAPublicKey.bind(this); vmEvaluator.deployContract = this.deployContractFromInfo.bind(this); vmEvaluator.setContractInformation(contractInformation); diff --git a/src/src/vm/evaluated/EvaluatedResult.ts b/src/src/vm/evaluated/EvaluatedResult.ts index c1b4a35e0..2e3c77fe6 100644 --- a/src/src/vm/evaluated/EvaluatedResult.ts +++ b/src/src/vm/evaluated/EvaluatedResult.ts @@ -17,6 +17,7 @@ export interface EvaluatedResult { readonly specialGasUsed: bigint; revert?: Uint8Array | undefined; readonly deployedContracts: ContractInformation[]; + readonly updatedContracts: ContractInformation[]; } export type SafeEvaluatedResult = Omit< diff --git a/src/src/vm/runtime/ContractEvaluator.ts b/src/src/vm/runtime/ContractEvaluator.ts index 77c854c85..46d5dce06 100644 --- a/src/src/vm/runtime/ContractEvaluator.ts +++ b/src/src/vm/runtime/ContractEvaluator.ts @@ -85,6 +85,10 @@ export class ContractEvaluator extends Logger { throw new Error('Method not implemented. [deployContract]'); } + public persistContractUpdate(_contract: ContractInformation): Promise { + throw new Error('Method not implemented. [persistContractUpdate]'); + } + public getStorage( _address: Address, _pointer: StoragePointer, @@ -242,6 +246,14 @@ export class ContractEvaluator extends Logger { // We deploy contract at the end of the transaction. This is on purpose, so we can revert more easily. await Promise.safeAll(deploymentPromises); + + if (evaluation.updatedContracts.size > 0) { + const updatePromises: Promise[] = []; + for (const contractInfo of evaluation.updatedContracts.values()) { + updatePromises.push(this.persistContractUpdate(contractInfo)); + } + await Promise.safeAll(updatePromises); + } } private async calculateGasCostStore(evaluation: ContractEvaluation): Promise { @@ -490,6 +502,7 @@ export class ContractEvaluator extends Logger { memoryPagesUsed: evaluation.memoryPagesUsed, deployedContracts: evaluation.deployedContracts, + updatedContracts: evaluation.updatedContracts, storage: evaluation.storage, preloadStorage: evaluation.preloadStorage, @@ -611,6 +624,11 @@ export class ContractEvaluator extends Logger { let usedGas: bigint = evaluation.gasUsed; try { + // Fail-fast depth check: the ContractEvaluation constructor will also + // increment when isUpdate=true, so MAXIMUM_UPDATE_DEPTH must be 2 to + // permit a single update. This mirrors the deployment pattern and + // rejects recursive updates before doing authorization / bytecode + // loading work. evaluation.incrementContractUpdates(); const reader = new BinaryReader(data); diff --git a/src/src/vm/runtime/classes/ContractEvaluation.ts b/src/src/vm/runtime/classes/ContractEvaluation.ts index ecc52a079..1e0c481dc 100644 --- a/src/src/vm/runtime/classes/ContractEvaluation.ts +++ b/src/src/vm/runtime/classes/ContractEvaluation.ts @@ -65,6 +65,7 @@ export class ContractEvaluation implements ExecutionParameters { public readonly storage: AddressMap; public readonly preloadStorage: AddressMap; public readonly deployedContracts: AddressMap; + public readonly updatedContracts: AddressMap; public readonly touchedAddresses: AddressMap; public callStack: AddressStack; @@ -97,6 +98,7 @@ export class ContractEvaluation implements ExecutionParameters { this.blockNumber = params.blockNumber; this.blockMedian = params.blockMedian; this.deployedContracts = params.deployedContracts || new AddressMap(); + this.updatedContracts = params.updatedContracts || new AddressMap(); this.isDeployment = params.isDeployment || false; this.isUpdate = params.isUpdate || false; this.memoryPagesUsed = params.memoryPagesUsed || 0n; @@ -351,6 +353,7 @@ export class ContractEvaluation implements ExecutionParameters { const events: AddressMap = this.revert ? new AddressMap() : this.events; const result = this.revert ? new Uint8Array(1) : this.result; const deployedContracts = this.revert ? [] : this.deployedContracts; + const updatedContracts = this.revert ? [] : this.updatedContracts; const resp: EvaluatedResult = { changedStorage: modifiedStorage, @@ -360,6 +363,7 @@ export class ContractEvaluation implements ExecutionParameters { gasUsed: this.gasUsed, specialGasUsed: this.specialGasUsed, deployedContracts: Array.from(deployedContracts.values()), + updatedContracts: Array.from(updatedContracts.values()), }; if (this._revert) { @@ -377,6 +381,10 @@ export class ContractEvaluation implements ExecutionParameters { this.deployedContracts.set(contract.contractPublicKey, contract); } + public addUpdatedContractInformation(contract: ContractInformation): void { + this.updatedContracts.set(contract.contractPublicKey, contract); + } + public deployedContract(address: Address): boolean { return this.deployedContracts.has(address); } diff --git a/src/src/vm/runtime/types/InternalContractCallParameters.ts b/src/src/vm/runtime/types/InternalContractCallParameters.ts index c281e8618..b9d4f7ff8 100644 --- a/src/src/vm/runtime/types/InternalContractCallParameters.ts +++ b/src/src/vm/runtime/types/InternalContractCallParameters.ts @@ -41,6 +41,7 @@ export interface InternalContractCallParameters { readonly preloadStorage: AddressMap; readonly deployedContracts?: AddressMap; + readonly updatedContracts?: AddressMap; readonly touchedAddresses?: AddressMap; readonly inputs: StrippedTransactionInput[]; @@ -82,6 +83,7 @@ export interface ExecutionParameters { readonly storage: AddressMap; readonly preloadStorage: AddressMap; readonly deployedContracts: AddressMap | undefined; + readonly updatedContracts: AddressMap | undefined; readonly touchedAddresses: AddressMap | undefined; readonly callStack: AddressStack | undefined; diff --git a/src/src/vm/storage/VMStorage.ts b/src/src/vm/storage/VMStorage.ts index 0cdae9d40..55e1ccd7f 100644 --- a/src/src/vm/storage/VMStorage.ts +++ b/src/src/vm/storage/VMStorage.ts @@ -142,6 +142,8 @@ export abstract class VMStorage extends Logger { public abstract setContractAt(contractData: ContractInformation): Promise; + public abstract updateContractBytecode(contractData: ContractInformation): Promise; + public abstract init(): Promise; public abstract getLatestBlock(): Promise; diff --git a/src/src/vm/storage/databases/VMMongoStorage.ts b/src/src/vm/storage/databases/VMMongoStorage.ts index b15e71670..26a68d7ed 100644 --- a/src/src/vm/storage/databases/VMMongoStorage.ts +++ b/src/src/vm/storage/databases/VMMongoStorage.ts @@ -755,6 +755,13 @@ export class VMMongoStorage extends VMStorage { await this.contractRepository.setContract(contractData); } + public async updateContractBytecode(contractData: ContractInformation): Promise { + if (!this.contractRepository) { + throw new Error('Repository not initialized'); + } + await this.contractRepository.updateContractBytecode(contractData); + } + public async getContractAt( contractAddress: string, height?: bigint, From 04b27c2c39f3308f44aa0d1d0f6bfd4f2697e71d Mon Sep 17 00:00:00 2001 From: BlobMaster41 <96896824+BlobMaster41@users.noreply.github.com> Date: Wed, 3 Jun 2026 02:23:14 -0400 Subject: [PATCH 2/3] Add blockHeight to contract indexes Make contractAddress and contractPublicKey indexes compound with blockHeight (-1) and update their names. This enforces uniqueness per (contractAddress, blockHeight) and (contractPublicKey, blockHeight), allowing multiple records for the same address/key across different block heights. The single-field blockHeight index is left unchanged. --- src/src/db/indexes/required/IndexedContracts.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/src/db/indexes/required/IndexedContracts.ts b/src/src/db/indexes/required/IndexedContracts.ts index b8867b04a..595804a95 100644 --- a/src/src/db/indexes/required/IndexedContracts.ts +++ b/src/src/db/indexes/required/IndexedContracts.ts @@ -8,11 +8,15 @@ export class IndexedContracts extends IndexedCollection Date: Fri, 5 Jun 2026 17:55:00 -0400 Subject: [PATCH 3/3] Remove rule to only allow updates from contract deployer --- src/src/vm/VMManager.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/src/vm/VMManager.ts b/src/src/vm/VMManager.ts index f92271f43..71595dece 100644 --- a/src/src/vm/VMManager.ts +++ b/src/src/vm/VMManager.ts @@ -1130,10 +1130,6 @@ export class VMManager extends Logger { throw new Error('OP_NET: Contract not found for update.'); } - if (!evaluation.msgSender.equals(currentContractInfo.deployerAddress)) { - throw new Error('OP_NET: Only the original deployer can update this contract.'); - } - if (sourceAddress.equals(evaluation.contractAddress)) { throw new Error('OP_NET: Contract cannot use itself as update source.'); } @@ -1175,9 +1171,7 @@ export class VMManager extends Logger { return { bytecodeLength: sourceContractInfo.bytecode.byteLength }; } - private async persistContractUpdate( - contractInformation: ContractInformation, - ): Promise { + private async persistContractUpdate(contractInformation: ContractInformation): Promise { if (this.isExecutor) { return; } @@ -1186,9 +1180,7 @@ export class VMManager extends Logger { !contractInformation.deployedTransactionId || !contractInformation.deployedTransactionHash ) { - throw new Error( - 'Transaction id or hash not found. [persistContractUpdate]', - ); + throw new Error('Transaction id or hash not found. [persistContractUpdate]'); } this.contractCache.delete(contractInformation.contractPublicKey);