Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/src/blockchain-indexer/rpc/thread/BitcoinRPCThread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class BitcoinRPCThread extends Thread<ThreadTypes.RPC> {
: response.result,
revert: revertData,
deployedContracts: [],
updatedContracts: [],
};
} else {
return {
Expand Down
10 changes: 7 additions & 3 deletions src/src/db/indexes/required/IndexedContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ export class IndexedContracts extends IndexedCollection<OPNetCollections.Contrac

public getIndexes(): IndexDescription[] {
return [
{ key: { contractAddress: 1 }, name: 'contractAddress_1', unique: true },
{
key: { contractAddress: 1, blockHeight: -1 },
name: 'contractAddress_blockHeight',
unique: true,
},
{ key: { p2trAddress: 1 }, name: 'p2trAddress_1' },
{
key: { contractPublicKey: 1 },
name: 'contractPublicKey_1',
key: { contractPublicKey: 1, blockHeight: -1 },
name: 'contractPublicKey_blockHeight',
unique: true,
},
{ key: { blockHeight: 1 }, name: 'blockHeight_1' },
Expand Down
14 changes: 11 additions & 3 deletions src/src/db/repositories/ContractRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class ContractRepository extends BaseRepository<IContractDocument> {
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;
}
Expand Down Expand Up @@ -148,6 +148,7 @@ export class ContractRepository extends BaseRepository<IContractDocument> {
contractPublicKey: 1,
},
currentSession,
{ blockHeight: -1 },
);

if (!contract) {
Expand All @@ -174,6 +175,13 @@ export class ContractRepository extends BaseRepository<IContractDocument> {
await this.insert(contract.toDocument(), currentSession);
}

public async updateContractBytecode(
contract: ContractInformation,
currentSession?: ClientSession,
): Promise<void> {
await this.insert(contract.toDocument(), currentSession);
}

public async getContractFromTweakedPubKey(
contractPublicKey: string,
height?: bigint,
Expand All @@ -194,7 +202,7 @@ export class ContractRepository extends BaseRepository<IContractDocument> {
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;
}
Expand All @@ -219,7 +227,7 @@ export class ContractRepository extends BaseRepository<IContractDocument> {
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;
}
Expand Down
10 changes: 7 additions & 3 deletions src/src/poc/configurations/consensus/RoswellConsensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Consensus.Roswell> = {
/** Information about the consensus */
Expand Down Expand Up @@ -230,9 +230,13 @@ export const RoswellConsensus: IOPNetConsensus<Consensus.Roswell> = {
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: {
Expand Down
92 changes: 92 additions & 0 deletions src/src/vm/VMManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ export class VMManager extends Logger {
mldsaLoadCounter: new MutableNumber(),

deployedContracts: deployedContracts,
updatedContracts: undefined,
callStack: undefined,
touchedAddresses: undefined,

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -937,6 +950,7 @@ export class VMManager extends Logger {
mldsaLoadCounter: params.mldsaLoadCounter,

deployedContracts: params.deployedContracts,
updatedContracts: params.updatedContracts,
memoryPagesUsed: params.memoryPagesUsed,
touchedAddresses: params.touchedAddresses,

Expand Down Expand Up @@ -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<void> {
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<void> {
if (this.isExecutor) {
// Emulators dont deploy contracts.
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/src/vm/evaluated/EvaluatedResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface EvaluatedResult {
readonly specialGasUsed: bigint;
revert?: Uint8Array | undefined;
readonly deployedContracts: ContractInformation[];
readonly updatedContracts: ContractInformation[];
}

export type SafeEvaluatedResult = Omit<
Expand Down
18 changes: 18 additions & 0 deletions src/src/vm/runtime/ContractEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class ContractEvaluator extends Logger {
throw new Error('Method not implemented. [deployContract]');
}

public persistContractUpdate(_contract: ContractInformation): Promise<void> {
throw new Error('Method not implemented. [persistContractUpdate]');
}

public getStorage(
_address: Address,
_pointer: StoragePointer,
Expand Down Expand Up @@ -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<void>[] = [];
for (const contractInfo of evaluation.updatedContracts.values()) {
updatePromises.push(this.persistContractUpdate(contractInfo));
}
await Promise.safeAll(updatePromises);
}
}

private async calculateGasCostStore(evaluation: ContractEvaluation): Promise<void> {
Expand Down Expand Up @@ -490,6 +502,7 @@ export class ContractEvaluator extends Logger {
memoryPagesUsed: evaluation.memoryPagesUsed,

deployedContracts: evaluation.deployedContracts,
updatedContracts: evaluation.updatedContracts,
storage: evaluation.storage,
preloadStorage: evaluation.preloadStorage,

Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/src/vm/runtime/classes/ContractEvaluation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class ContractEvaluation implements ExecutionParameters {
public readonly storage: AddressMap<PointerStorage>;
public readonly preloadStorage: AddressMap<PointerStorage>;
public readonly deployedContracts: AddressMap<ContractInformation>;
public readonly updatedContracts: AddressMap<ContractInformation>;
public readonly touchedAddresses: AddressMap<boolean>;

public callStack: AddressStack;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -351,6 +353,7 @@ export class ContractEvaluation implements ExecutionParameters {
const events: AddressMap<NetEvent[]> = 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,
Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions src/src/vm/runtime/types/InternalContractCallParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface InternalContractCallParameters {
readonly preloadStorage: AddressMap<PointerStorage>;

readonly deployedContracts?: AddressMap<ContractInformation>;
readonly updatedContracts?: AddressMap<ContractInformation>;
readonly touchedAddresses?: AddressMap<boolean>;

readonly inputs: StrippedTransactionInput[];
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface ExecutionParameters {
readonly storage: AddressMap<PointerStorage>;
readonly preloadStorage: AddressMap<PointerStorage>;
readonly deployedContracts: AddressMap<ContractInformation> | undefined;
readonly updatedContracts: AddressMap<ContractInformation> | undefined;

readonly touchedAddresses: AddressMap<boolean> | undefined;
readonly callStack: AddressStack | undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/src/vm/storage/VMStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ export abstract class VMStorage extends Logger {

public abstract setContractAt(contractData: ContractInformation): Promise<void>;

public abstract updateContractBytecode(contractData: ContractInformation): Promise<void>;

public abstract init(): Promise<void>;

public abstract getLatestBlock(): Promise<BlockHeaderAPIBlockDocument | undefined>;
Expand Down
7 changes: 7 additions & 0 deletions src/src/vm/storage/databases/VMMongoStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,13 @@ export class VMMongoStorage extends VMStorage {
await this.contractRepository.setContract(contractData);
}

public async updateContractBytecode(contractData: ContractInformation): Promise<void> {
if (!this.contractRepository) {
throw new Error('Repository not initialized');
}
await this.contractRepository.updateContractBytecode(contractData);
}

public async getContractAt(
contractAddress: string,
height?: bigint,
Expand Down