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/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 { 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..71595dece 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,82 @@ 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 (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 +1231,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,