diff --git a/docs/docs-developers/docs/aztec-js/how_to_test.md b/docs/docs-developers/docs/aztec-js/how_to_test.md index 9b9ccfba7f95..0f5526ab25ba 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_test.md +++ b/docs/docs-developers/docs/aztec-js/how_to_test.md @@ -85,7 +85,7 @@ Use this to set up state preconditions, reproduce production bugs against pinned ### Fast-forwarding a contract update -`fastForwardContractUpdate` returns a `SimulationOverrides` object that simulates a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. The cheat mirrors a real `pxe.updateContract` followed by waiting out the upgrade delay: the instance's `currentContractClassId` is bumped, and the `ContractInstanceRegistry`'s delayed-public-mutable storage is rewritten to look like the upgrade was scheduled in the past. +`fastForwardContractUpdate` returns a `SimulationOverrides` object that simulates a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. The cheat mirrors a real onchain upgrade followed by waiting out the upgrade delay: the override instance's `currentContractClassId` is bumped, and the `ContractInstanceRegistry`'s delayed-public-mutable storage is rewritten to look like the upgrade was scheduled in the past. ```typescript import { fastForwardContractUpdate } from '@aztec/aztec.js'; diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index a35174ba279b..7e452b6da8be 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,33 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [PXE] `pxe.updateContract` removed and `pxe.registerContract` no longer takes an artifact + +Registering classes and instances are now separate, unvalidated operations. `registerContractClass(artifact)` registers a class, `registerContract(instance)` registers an instance and no longer takes an artifact. `registerContract` does not check that PXE knows the contract's artifact: a missing artifact surfaces only when the contract is later simulated. + +**Migration:** + +- `pxe.registerContract` now takes the instance directly (its address preimage) and returns the derived address. Register the class separately via `registerContractClass`: + +```diff +- await pxe.registerContract({ instance, artifact }); ++ await pxe.registerContractClass(artifact); ++ await pxe.registerContract(instance); +``` + + If you were calling it without an artifact, just drop the wrapping object: `pxe.registerContract({ instance })` becomes `pxe.registerContract(instance)`. The `wallet.registerContract(instance, artifact?, secretKeyOrKeys?)` convenience is unchanged and performs both registrations for you. + +- To make a new class's code available after an onchain upgrade, register the new artifact instead of calling `updateContract`: + +```diff +- await pxe.updateContract(address, newArtifact); ++ await pxe.registerContractClass(newArtifact); +``` + + The new class is used automatically once the upgrade takes effect on chain; no further PXE action is needed. Registering it beforehand is harmless: until the update activates, the node still resolves the contract's current class to the previous one, so it keeps running its old code. + +- `pxe.getContractInstance(address)` and `wallet.getContractMetadata(address).instance` now return the contract's **address preimage**, which no longer includes `currentContractClassId`. + ### [Aztec.js] `AccountWithSecretKey` removed, read account keys from the `AccountManager` or PXE `AccountWithSecretKey` was a thin wrapper that bundled an account's transaction signer with its master secret key, used mainly to print or export the secret. It has been removed, and `AccountManager.getAccount()` now returns the plain `Account` signer. The wrapper's extra methods are no longer available on that value: diff --git a/docs/examples/webapp-tutorial/src/fees.ts b/docs/examples/webapp-tutorial/src/fees.ts index 98b85724821d..da671010ba60 100644 --- a/docs/examples/webapp-tutorial/src/fees.ts +++ b/docs/examples/webapp-tutorial/src/fees.ts @@ -30,7 +30,8 @@ export async function getSponsoredFPCContract() { */ export async function registerSponsoredFPC(pxe: PXE) { const contract = await getSponsoredFPCContract(); - await pxe.registerContract(contract); + await pxe.registerContractClass(contract.artifact); + await pxe.registerContract(contract.instance); return contract.instance.address; } // docs:end:register-fpc diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 9609755d1d6c..9c65f669c088 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -1,10 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { - CompleteAddress, - type ContractInstanceWithAddress, - getContractClassFromArtifact, -} from '@aztec/stdlib/contract'; +import { CompleteAddress } from '@aztec/stdlib/contract'; import type { TxExecutionRequest, TxReceipt, UtilityExecutionResult } from '@aztec/stdlib/tx'; import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx'; @@ -21,7 +17,6 @@ describe('Contract Class', () => { let contractAddress: AztecAddress; let account: MockProxy; let accountAddress: CompleteAddress; - let contractInstance: ContractInstanceWithAddress; const mockTxRequest = { type: 'TxRequest' } as any as TxExecutionRequest; const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt; @@ -40,17 +35,11 @@ describe('Contract Class', () => { account = mock(); accountAddress = await CompleteAddress.random(); account.getCompleteAddress.mockReturnValue(accountAddress); - const contractClass = await getContractClassFromArtifact(testContractArtifact); - contractInstance = { - address: contractAddress, - currentContractClassId: contractClass.id, - originalContractClassId: contractClass.id, - } as ContractInstanceWithAddress; wallet = mock(); wallet.simulateTx.mockResolvedValue(mockTxSimulationResultWithAppOffset); account.createTxExecutionRequest.mockResolvedValue(mockTxRequest); - wallet.registerContract.mockResolvedValue(contractInstance); + wallet.registerContract.mockResolvedValue(undefined); wallet.sendTx.mockResolvedValue({ receipt: mockTxReceipt, offchainEffects: [], offchainMessages: [] }); wallet.executeUtility.mockResolvedValue(mockUtilityResultValue); }); diff --git a/yarn-project/aztec.js/src/contract/deploy_method.test.ts b/yarn-project/aztec.js/src/contract/deploy_method.test.ts index ae1bc9e9b531..b01bff410452 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.test.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.test.ts @@ -1,6 +1,6 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract'; +import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract'; import { Gas } from '@aztec/stdlib/gas'; import { OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect } from '@aztec/stdlib/tx'; @@ -16,7 +16,7 @@ describe('DeployMethod', () => { let wallet: MockProxy; beforeEach(() => { wallet = mock(); - wallet.registerContract.mockResolvedValue({} as ContractInstanceWithAddress); + wallet.registerContract.mockResolvedValue(undefined); wallet.getContractClassMetadata.mockResolvedValue({ isContractClassPubliclyRegistered: true } as any); wallet.getContractMetadata.mockResolvedValue({ isContractPubliclyDeployed: true } as any); }); diff --git a/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts b/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts index 40e9d19aefb1..f3b2186c9690 100644 --- a/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts +++ b/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts @@ -11,7 +11,7 @@ import { SimulationOverrides } from '@aztec/stdlib/tx'; /** * Builds `SimulationOverrides` that simulate a deployed instance as if it had already been upgraded to a - * new contract class. Mirrors a real on-chain upgrade (`pxe.updateContract` followed by waiting out the delay): + * new contract class. Mirrors a real on-chain upgrade (scheduling the new class and waiting out the delay): * * - `publicStorage` rewrites the `ContractInstanceRegistry`'s delayed-public-mutable storage so the AVM's * `UpdateCheck` resolves to the new class id. diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index ddcdc5719d6b..f17de294832d 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.test.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts @@ -7,7 +7,7 @@ import { EventSelector, FunctionCall, FunctionSelector, FunctionType } from '@az import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { ContractInstancePreimageWithAddress } from '@aztec/stdlib/contract'; import { PublicKeys } from '@aztec/stdlib/keys'; import { DroppedTxReceipt, @@ -134,29 +134,17 @@ describe('WalletSchema', () => { fileMap: {}, storageLayout: {}, }; - const mockInstance: ContractInstanceWithAddress = { + const mockInstance: ContractInstancePreimageWithAddress = { address: await AztecAddress.random(), version: 2, salt: Fr.random(), deployer: await AztecAddress.random(), - currentContractClassId: Fr.random(), originalContractClassId: Fr.random(), initializationHash: Fr.random(), immutablesHash: Fr.random(), publicKeys: PublicKeys.default(), }; - const result = await context.client.registerContract(mockInstance, mockArtifact, Fr.random()); - expect(result).toEqual({ - address: expect.any(AztecAddress), - currentContractClassId: expect.any(Fr), - deployer: expect.any(AztecAddress), - initializationHash: expect.any(Fr), - immutablesHash: expect.any(Fr), - originalContractClassId: expect.any(Fr), - publicKeys: expect.any(PublicKeys), - salt: expect.any(Fr), - version: 2, - }); + await context.client.registerContract(mockInstance, mockArtifact, Fr.random()); }); it('registerContractClass', async () => { @@ -335,12 +323,11 @@ describe('WalletSchema', () => { returnTypes: [], }); - const mockInstance: ContractInstanceWithAddress = { + const mockInstance: ContractInstancePreimageWithAddress = { address: address2, version: 2, salt: Fr.random(), deployer: await AztecAddress.random(), - currentContractClassId: Fr.random(), originalContractClassId: Fr.random(), initializationHash: Fr.random(), immutablesHash: Fr.random(), @@ -397,10 +384,7 @@ describe('WalletSchema', () => { expect(results[4]).toEqual({ name: 'registerSender', result: expect.any(AztecAddress) }); expect(results[5]).toEqual({ name: 'getAddressBook', result: expect.any(Array) }); expect(results[6]).toEqual({ name: 'getAccounts', result: expect.any(Array) }); - expect(results[7]).toEqual({ - name: 'registerContract', - result: expect.objectContaining({ address: expect.any(AztecAddress) }), - }); + expect(results[7]).toEqual({ name: 'registerContract', result: undefined }); expect(results[8]).toEqual({ name: 'simulateTx', result: expect.any(TxSimulationResultWithAppOffset) }); expect(results[9]).toEqual({ name: 'executeUtility', result: expect.any(UtilityExecutionResult) }); expect(results[10]).toEqual({ name: 'profileTx', result: expect.any(TxProfileResult) }); @@ -471,19 +455,7 @@ class MockWallet implements Wallet { return [{ alias: 'account1', item: await AztecAddress.random() }]; } - async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise { - return { - version: 2, - address: await AztecAddress.random(), - currentContractClassId: Fr.random(), - deployer: await AztecAddress.random(), - initializationHash: Fr.random(), - immutablesHash: Fr.random(), - originalContractClassId: Fr.random(), - publicKeys: await PublicKeys.random(), - salt: Fr.random(), - }; - } + async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise {} async registerContractClass(_artifact: any): Promise {} diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index b8e3ad22d074..40b1f444c132 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -11,7 +11,12 @@ import { } from '@aztec/stdlib/abi'; import { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type ContractInstanceWithAddress, ContractInstanceWithAddressSchema } from '@aztec/stdlib/contract'; +import { + type ContractInstancePreimage, + ContractInstancePreimageSchema, + type ContractInstancePreimageWithAddress, + ContractInstancePreimageWithAddressSchema, +} from '@aztec/stdlib/contract'; import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas'; import type { MasterSecretKeys } from '@aztec/stdlib/keys'; import { refineTxHashAndRange } from '@aztec/stdlib/logs'; @@ -234,8 +239,8 @@ export enum ContractInitializationStatus { * Contract metadata including deployment and registration status. */ export type ContractMetadata = { - /** The contract instance */ - instance?: ContractInstanceWithAddress; + /** The contract instance preimage and address. */ + instance?: ContractInstancePreimageWithAddress; /** Whether the contract has been initialized. */ initializationStatus: ContractInitializationStatus; /** Whether the contract instance is publicly deployed on-chain */ @@ -281,10 +286,10 @@ export type Wallet = { getAddressBook(): Promise[]>; getAccounts(): Promise[]>; registerContract( - instance: ContractInstanceWithAddress, + instance: ContractInstancePreimage, artifact?: ContractArtifact, secretKeyOrKeys?: Fr | MasterSecretKeys, - ): Promise; + ): Promise; /** * Registers a contract class artifact in the local PXE without binding it to any instance. * Useful for simulation flows that need the artifact available locally before any on-chain @@ -413,7 +418,7 @@ export const PublicEventSchema: z.ZodType> = zodFor - z.object({ + const namesAndReturns = names.map(name => { + const returnType = getSchemaReturnType(methodSchemas[name]); + return z.object({ name: z.literal(name), - result: getSchemaReturnType(methodSchemas[name]), - }), - ); + // void-returning methods serialize to a missing `result` key over JSON-RPC, so their field must be optional: + // value-returning methods keep it required so a dropped result is still caught. + result: returnType instanceof z.ZodVoid ? returnType.optional() : returnType, + }); + }); // Type assertion needed because discriminatedUnion expects a tuple type [T, T, ...T[]] // but we're building the array dynamically. The runtime behavior is correct. diff --git a/yarn-project/cli-wallet/src/cmds/check_tx.ts b/yarn-project/cli-wallet/src/cmds/check_tx.ts index 08649e61a5ef..55d354d677ab 100644 --- a/yarn-project/cli-wallet/src/cmds/check_tx.ts +++ b/yarn-project/cli-wallet/src/cmds/check_tx.ts @@ -197,21 +197,22 @@ type ContractArtifactWithClassId = ContractArtifact & { classId: Fr }; async function getKnownArtifacts(wallet: CLIWallet): Promise { const knownContractAddresses = await wallet.getContracts(); - const knownContracts = ( - await Promise.all(knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress))) - ).map(contractMetadata => contractMetadata.instance); - const classIds = [...new Set(knownContracts.map(contract => contract?.currentContractClassId))]; + const knownContracts = await Promise.all( + knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress)), + ); + const classIdFor = (metadata: (typeof knownContracts)[number]): Fr | undefined => + metadata.updatedContractClassId ?? metadata.instance?.originalContractClassId; + const classIds = [...new Set(knownContracts.map(classIdFor))]; const knownArtifacts = ( await Promise.all(classIds.map(classId => (classId ? wallet.getContractArtifact(classId) : undefined))) ).map((artifact, index) => (artifact ? { ...artifact, classId: classIds[index] } : undefined)); const map: Record = {}; - for (const instance of knownContracts) { - if (instance) { - const artifact = knownArtifacts.find(a => - a?.classId?.equals(instance.currentContractClassId), - ) as ContractArtifactWithClassId; + for (const metadata of knownContracts) { + const classId = classIdFor(metadata); + if (metadata.instance && classId) { + const artifact = knownArtifacts.find(a => a?.classId?.equals(classId)) as ContractArtifactWithClassId; if (artifact) { - map[instance.address.toString()] = artifact; + map[metadata.instance.address.toString()] = artifact; } } } diff --git a/yarn-project/cli-wallet/src/cmds/simulate.ts b/yarn-project/cli-wallet/src/cmds/simulate.ts index aec161b4ab33..4319746817da 100644 --- a/yarn-project/cli-wallet/src/cmds/simulate.ts +++ b/yarn-project/cli-wallet/src/cmds/simulate.ts @@ -44,7 +44,8 @@ export async function simulate( if (!metadata.instance) { return undefined; } - const artifact = await wallet.getContractArtifact(metadata.instance.currentContractClassId); + const classId = metadata.updatedContractClassId ?? metadata.instance.originalContractClassId; + const artifact = await wallet.getContractArtifact(classId); return artifact; }, log, diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index ffb0832a1110..865705f7038d 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -68,7 +68,8 @@ export class CLIWallet extends BaseWallet { private async registerAuthRegistry(): Promise { const { instance, artifact } = await getStandardAuthRegistry(); - await this.pxe.registerContract({ instance, artifact }); + await this.pxe.registerContractClass(artifact); + await this.pxe.registerContract(instance); } /** diff --git a/yarn-project/end-to-end/src/automine/contracts/contract_updates.parallel.test.ts b/yarn-project/end-to-end/src/automine/contracts/contract_updates.parallel.test.ts index 527a695c3366..fcd28dbfe791 100644 --- a/yarn-project/end-to-end/src/automine/contracts/contract_updates.parallel.test.ts +++ b/yarn-project/end-to-end/src/automine/contracts/contract_updates.parallel.test.ts @@ -186,12 +186,16 @@ describe('automine/contracts/contract_updates', () => { ).rejects.toThrow('New update delay is too low'); }); - // Tries to register the instance against UpdatedContract.artifact before the upgrade window passes; - // expects the PXE to reject with a class mismatch error. - it('should not allow to instantiate a contract with an updated class before the update happens', async () => { - await expect(wallet.registerContract(instance, UpdatedContract.artifact)).rejects.toThrow( - 'Could not update contract to a class different from the current one', - ); + it('permits registering the updated artifact before the update, but still runs the original class', async () => { + // Registration performs no validation, so the updated artifact can be registered before the upgrade activates. + await expect(wallet.registerContract(instance, UpdatedContract.artifact)).resolves.not.toThrow(); + + // The node still resolves the contract's current class to the original until the update is enacted, so a call + // against the updated class's (no-arg) set_public_value selector does not resolve against the deployed class. + const updatedContract = UpdatedContract.at(contract.address, wallet); + await expect( + updatedContract.methods.set_public_value().simulate({ from: defaultAccountAddress }), + ).rejects.toThrow(); }); // UpdatableContract's `set_public_value(Field)` and UpdatedContract's `set_public_value()` diff --git a/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts b/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts index 8c109ec687ae..65e8b2d6c4a7 100644 --- a/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts +++ b/yarn-project/end-to-end/src/automine/contracts/deploy/deploy_method.parallel.test.ts @@ -55,7 +55,7 @@ describe('automine/contracts/deploy/deploy_method', () => { ); // docs:start:verify_deployment const metadata = await wallet.getContractMetadata(contract.address); - const classMetadata = await wallet.getContractClassMetadata(metadata.instance!.currentContractClassId); + const classMetadata = await wallet.getContractClassMetadata(metadata.instance!.originalContractClassId); const isPublished = classMetadata.isContractClassPubliclyRegistered; // docs:end:verify_deployment expect(isPublished).toBeTrue(); diff --git a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts index 8a3fcbc5c3e2..c40fbc6db502 100644 --- a/yarn-project/end-to-end/src/spartan/block_capacity.test.ts +++ b/yarn-project/end-to-end/src/spartan/block_capacity.test.ts @@ -371,9 +371,12 @@ describe('block capacity benchmark', () => { logger.info('BenchmarkingContract deployed', { address: benchmarkContract.address.toString() }); // Register benchmark contract with all other wallets - const benchMetadata = await wallets[0].getContractMetadata(benchmarkContract.address); + const benchInstance = await wallets[0].getContractMetadata(benchmarkContract.address).then(m => ({ + ...m.instance!, + currentContractClassId: m.instance!.originalContractClassId, + })); await Promise.all( - wallets.slice(1).map(wallet => wallet.registerContract(benchMetadata.instance!, BenchmarkingContract.artifact)), + wallets.slice(1).map(wallet => wallet.registerContract(benchInstance, BenchmarkingContract.artifact)), ); logger.info('Benchmark contract registered with all wallets'); }); @@ -410,10 +413,11 @@ describe('block capacity benchmark', () => { logger.info('TokenContract deployed', { address: tokenContract.address.toString() }); // Register token contract with all other wallets - const tokenMetadata = await wallets[0].getContractMetadata(tokenContract.address); - await Promise.all( - wallets.slice(1).map(wallet => wallet.registerContract(tokenMetadata.instance!, TokenContract.artifact)), - ); + const tokenInstance = await wallets[0].getContractMetadata(tokenContract.address).then(m => ({ + ...m.instance!, + currentContractClassId: m.instance!.originalContractClassId, + })); + await Promise.all(wallets.slice(1).map(wallet => wallet.registerContract(tokenInstance, TokenContract.artifact))); logger.info('Token contract registered with all wallets'); // Mint tokens publicly to each account (enough for TX_COUNT transfers). diff --git a/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts b/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts index df93dceafde1..054e6b6d3536 100644 --- a/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/worker_wallet.ts @@ -29,7 +29,7 @@ import type { PXEConfig } from '@aztec/pxe/config'; import type { ContractArtifact, EventMetadataDefinition, FunctionCall } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { ContractInstancePreimage } from '@aztec/stdlib/contract'; import type { ExecutionPayload, TxProfileResult, UtilityExecutionResult } from '@aztec/stdlib/tx'; import { Tx } from '@aztec/stdlib/tx'; @@ -158,11 +158,7 @@ export class WorkerWallet implements Wallet { return this.call('getAccounts'); } - registerContract( - instance: ContractInstanceWithAddress, - artifact?: ContractArtifact, - secretKey?: Fr, - ): Promise { + registerContract(instance: ContractInstancePreimage, artifact?: ContractArtifact, secretKey?: Fr): Promise { return this.call('registerContract', instance, artifact, secretKey); } diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index d471834578c5..f7bffe2afae6 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -24,7 +24,8 @@ import { TxHash } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; import type { BlockSynchronizerConfig } from '../config/index.js'; -import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; +import type { ContractClassService } from '../contract/contract_class_service.js'; +import type { ContractSyncService } from '../contract/contract_sync_service.js'; import { AnchorBlockStore } from '../storage/anchor_block_store/anchor_block_store.js'; import { FactStore } from '../storage/fact_store/fact_store.js'; import { FactCollectionKey, FactCollectionTypeKey } from '../storage/fact_store/fact_store_keys.js'; @@ -50,6 +51,7 @@ describe('BlockSynchronizer', () => { let getBlock: NodeGetBlockMock; let blockStream: MockProxy; let contractSyncService: MockProxy; + let contractClassService: MockProxy; const TestSynchronizer = class extends BlockSynchronizer { protected override createBlockStream(): L2BlockStream { @@ -67,6 +69,7 @@ describe('BlockSynchronizer', () => { factStore, tipsStore, contractSyncService, + contractClassService, config, ); }; @@ -127,6 +130,7 @@ describe('BlockSynchronizer', () => { privateEventStore = new PrivateEventStore(store); factStore = new FactStore(store); contractSyncService = mock(); + contractClassService = mock(); synchronizer = createSynchronizer(); }); @@ -165,6 +169,15 @@ describe('BlockSynchronizer', () => { expect(obtainedHeader.equals(block.header)).toBe(true); }); + it('wipes the contract sync and contract class caches when the anchor block changes', async () => { + const block = await L2Block.random(BlockNumber(1)); + await serveBlockDataByHash(block); + await synchronizer.handleBlockStreamEvent(await proposedEvent(block)); + + expect(contractSyncService.wipe).toHaveBeenCalled(); + expect(contractClassService.wipe).toHaveBeenCalled(); + }); + it('updates anchor block on a reorg', async () => { const reorgBlock = await L2Block.random(BlockNumber(3)); await serveBlock(reorgBlock); @@ -796,6 +809,7 @@ describe('BlockSynchronizer', () => { factStore, tipsStore, contractSyncService, + contractClassService, { syncChainTip: 'proposed' }, ); }); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts index 5675dee647ec..5b1e2a9b9285 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts @@ -8,7 +8,8 @@ import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import type { BlockHeader } from '@aztec/stdlib/tx'; import type { BlockSynchronizerConfig } from '../config/index.js'; -import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; +import type { ContractClassService } from '../contract/contract_class_service.js'; +import type { ContractSyncService } from '../contract/contract_sync_service.js'; import type { AnchorBlockStore } from '../storage/anchor_block_store/index.js'; import type { FactStore } from '../storage/fact_store/fact_store.js'; import type { NoteStore } from '../storage/note_store/index.js'; @@ -35,6 +36,7 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { private readonly factStore: FactStore, private readonly l2TipsStore: L2TipsKVStore, private readonly contractSyncService: ContractSyncService, + private readonly contractClassService: ContractClassService, private readonly config: Partial = {}, bindings?: LoggerBindings, ) { @@ -177,6 +179,12 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { // Therefore, we clear the contract synchronization cache here such that the sync is re-triggered upon new // execution. this.contractSyncService.wipe(); + + // The contract class service keeps a per-block cache - since updating our anchor means it is very unlikely we'd + // ever re-simulate at past anchors, we wipe its cache to prevent runaway memory growth on very long-lived PXE + // instances. + this.contractClassService.wipe(); + this.log.verbose(`Updated pxe last block to ${blockHeader.getBlockNumber()}`, blockHeader.toInspect()); await this.anchorBlockStore.setHeader(blockHeader); } diff --git a/yarn-project/pxe/src/contract/contract_class_service.test.ts b/yarn-project/pxe/src/contract/contract_class_service.test.ts new file mode 100644 index 000000000000..c38fa53890ce --- /dev/null +++ b/yarn-project/pxe/src/contract/contract_class_service.test.ts @@ -0,0 +1,120 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { ProtocolContractAddress } from '@aztec/protocol-contracts'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { BlockHash } from '@aztec/stdlib/block'; +import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; +import type { BlockHeader } from '@aztec/stdlib/tx'; + +import { mock } from 'jest-mock-extended'; + +import type { ContractStore } from '../storage/contract_store/contract_store.js'; +import { ContractClassService } from './contract_class_service.js'; + +describe('ContractClassService', () => { + let node: ReturnType>; + let contractStore: ReturnType>; + let service: ContractClassService; + + const address = AztecAddress.fromBigIntUnsafe(0x1234n); + const originalClassId = new Fr(0xaaaan); + + const anchorWithHash = (hash: BlockHash): BlockHeader => { + const header = mock(); + header.hash.mockResolvedValue(hash); + return header; + }; + + beforeEach(() => { + node = mock(); + contractStore = mock(); + contractStore.getContractInstance.mockResolvedValue({ + address, + originalContractClassId: originalClassId, + } as ContractInstanceWithAddress); + service = new ContractClassService(node, contractStore); + }); + + it('resolves the node-reported current class at the anchor block', async () => { + const hash = new BlockHash(new Fr(1n)); + const currentClassId = new Fr(0xbbbbn); + node.getContract.mockResolvedValue({ currentContractClassId: currentClassId } as ContractInstanceWithAddress); + + expect(await service.getCurrentClassId(address, anchorWithHash(hash))).toEqual(currentClassId); + expect(node.getContract).toHaveBeenCalledWith(address, hash); + }); + + it('resolves the node-reported current class even when no local instance is registered', async () => { + const currentClassId = new Fr(0xbbbbn); + contractStore.getContractInstance.mockResolvedValue(undefined); + node.getContract.mockResolvedValue({ currentContractClassId: currentClassId } as ContractInstanceWithAddress); + + expect(await service.getCurrentClassId(address, anchorWithHash(new BlockHash(new Fr(1n))))).toEqual(currentClassId); + expect(contractStore.getContractInstance).not.toHaveBeenCalled(); + }); + + it('returns different classes for different anchors and does not bleed across them', async () => { + const hashA = new BlockHash(new Fr(1n)); + const hashB = new BlockHash(new Fr(2n)); + const classAtA = new Fr(0xa1n); + const classAtB = new Fr(0xb2n); + node.getContract.mockImplementation((_addr, refBlock) => + Promise.resolve({ + currentContractClassId: (refBlock as BlockHash).equals(hashA) ? classAtA : classAtB, + } as ContractInstanceWithAddress), + ); + + expect(await service.getCurrentClassId(address, anchorWithHash(hashA))).toEqual(classAtA); + expect(await service.getCurrentClassId(address, anchorWithHash(hashB))).toEqual(classAtB); + }); + + it('caches per (address, anchor) so the node is queried once per anchor', async () => { + const hash = new BlockHash(new Fr(1n)); + node.getContract.mockResolvedValue({ currentContractClassId: new Fr(7n) } as ContractInstanceWithAddress); + + await service.getCurrentClassId(address, anchorWithHash(hash)); + await service.getCurrentClassId(address, anchorWithHash(hash)); + + expect(node.getContract).toHaveBeenCalledTimes(1); + }); + + it('re-queries after wipe', async () => { + const hash = new BlockHash(new Fr(1n)); + node.getContract.mockResolvedValue({ currentContractClassId: new Fr(7n) } as ContractInstanceWithAddress); + + await service.getCurrentClassId(address, anchorWithHash(hash)); + service.wipe(); + await service.getCurrentClassId(address, anchorWithHash(hash)); + + expect(node.getContract).toHaveBeenCalledTimes(2); + }); + + it('falls back to the original class when the node does not know the instance', async () => { + node.getContract.mockResolvedValue(undefined); + + expect(await service.getCurrentClassId(address, anchorWithHash(new BlockHash(new Fr(1n))))).toEqual( + originalClassId, + ); + }); + + it('short-circuits protocol contracts to their original class without querying the node', async () => { + const protocolAddress = ProtocolContractAddress.ContractInstanceRegistry; + const protocolClassId = new Fr(0xccccn); + contractStore.getContractInstance.mockResolvedValue({ + address: protocolAddress, + originalContractClassId: protocolClassId, + } as ContractInstanceWithAddress); + + expect(await service.getCurrentClassId(protocolAddress, anchorWithHash(new BlockHash(new Fr(1n))))).toEqual( + protocolClassId, + ); + expect(node.getContract).not.toHaveBeenCalled(); + }); + + it('returns undefined when neither the node nor the store knows the instance', async () => { + contractStore.getContractInstance.mockResolvedValue(undefined); + node.getContract.mockResolvedValue(undefined); + + expect(await service.getCurrentClassId(address, anchorWithHash(new BlockHash(new Fr(1n))))).toBeUndefined(); + }); +}); diff --git a/yarn-project/pxe/src/contract/contract_class_service.ts b/yarn-project/pxe/src/contract/contract_class_service.ts new file mode 100644 index 000000000000..457f964c994e --- /dev/null +++ b/yarn-project/pxe/src/contract/contract_class_service.ts @@ -0,0 +1,83 @@ +import type { Fr } from '@aztec/foundation/curves/bn254'; +import { isProtocolContract } from '@aztec/protocol-contracts'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; +import type { BlockHeader } from '@aztec/stdlib/tx'; + +import type { ContractStore } from '../storage/contract_store/contract_store.js'; + +/** + * Resolves the contract class id that an address runs at a given anchor block, as tracked by the chain. + * + * PXE does not store a contract's current class id: it is mutable, chain-derived state that changes when a contract is + * upgraded and can be undone by a reorg. Instead this service asks the node for the current class at an anchor block, + * falling back to a local instance if no upgrades were scheduled. + */ +export class ContractClassService { + /** + * Class ids are cached per `(address, anchorHash)`. This avoid unnecessary network roundtrips in scenarios where + * multiple executions are done on the same anchor block (e.g. simulation followed by witgen), or when the same + * contract is invoked multiple times in an execution (e.g. authwit checks). + * It also means the callers don't need to worry about caching this service's return values, simplifying callsites. + */ + #cache: Map> = new Map(); + + constructor( + private node: AztecNode, + private contractStore: ContractStore, + ) {} + + /** + * Returns the class id that corresponds to `address` as of `anchorBlockHeader`, or `undefined` if no instance is + * registered for `address`. A missing instance is an absence the caller decides how to handle, not an error; genuine + * failures (e.g. the node being unreachable) still throw. + */ + async getCurrentClassId(address: AztecAddress, anchorBlockHeader: BlockHeader): Promise { + // Protocol contracts are not in the registry and cannot be upgraded, so their original class is always current. + // Short-circuiting avoids a node round-trip. + if (isProtocolContract(address)) { + const instance = await this.contractStore.getContractInstance(address); + return instance?.originalContractClassId; + } + + const key = `${address.toString()}:${(await anchorBlockHeader.hash()).toString()}`; + let promise = this.#cache.get(key); + if (!promise) { + promise = (async () => { + // The node resolves the current class from the same scheduled value change the AVM enforces against the public + // data tree. If the contract was upgraded the node returns a non-undefined instance; an undefined result means + // no upgrade happened (or the node has no record of it, e.g. it was never publicly deployed), so the original + // class is current. + + const nodeInstance = await this.node.getContract(address, await anchorBlockHeader.hash()); + + if (nodeInstance) { + return nodeInstance.currentContractClassId; + } else { + return (await this.contractStore.getContractInstance(address))?.originalContractClassId; + } + })().catch(err => { + this.#cache.delete(key); + throw err; + }); + this.#cache.set(key, promise); + } + + const classId = await promise; + if (classId === undefined) { + // Don't memoize a missing instance: it may be registered later in this PXE (e.g. via contract sync + // mid-execution), and a cached miss would then hide it. Only successful resolutions stay cached. + this.#cache.delete(key); + } + return classId; + } + + /** + * Clears the cache. + * + * This is not required for correctness, only to limit how much memory the cache uses. The cache is resilient against + * reorgs etc. as it is based on block hashes, not block numbers. */ + wipe(): void { + this.#cache.clear(); + } +} diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts b/yarn-project/pxe/src/contract/contract_sync_service.test.ts similarity index 82% rename from yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts rename to yarn-project/pxe/src/contract/contract_sync_service.test.ts index 97f4f4177f6e..f574ea9b2c0e 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts +++ b/yarn-project/pxe/src/contract/contract_sync_service.test.ts @@ -3,7 +3,6 @@ import { createLogger } from '@aztec/foundation/log'; import { executeTimeout } from '@aztec/foundation/timer'; import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { makeBlockHeader } from '@aztec/stdlib/testing'; @@ -12,11 +11,13 @@ import { mock } from 'jest-mock-extended'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; +import type { ContractClassService } from './contract_class_service.js'; import { ContractSyncService, MAX_CONCURRENT_SCOPE_SYNCS } from './contract_sync_service.js'; describe('ContractSyncService', () => { let aztecNode: ReturnType>; let contractStore: ReturnType>; + let contractClassService: ReturnType>; let noteStore: ReturnType>; let service: ContractSyncService; let utilityExecutor: jest.Mock<(call: FunctionCall, scopes: AztecAddress[]) => Promise>; @@ -48,22 +49,22 @@ describe('ContractSyncService', () => { }), ), ); - contractStore.getContractInstance.mockResolvedValue({ - currentContractClassId: classId, - originalContractClassId: classId, - address: contractAddress, - } as ContractInstanceWithAddress); + contractClassService = mock(); + contractClassService.getCurrentClassId.mockResolvedValue(classId); aztecNode = mock(); - // verifyCurrentClassId reads the instance from the node at the anchor block; returning undefined causes - // readCurrentClassId to fall back to the local originalContractClassId, which matches so verification passes. - aztecNode.getContract.mockResolvedValue(undefined); noteStore = mock(); // syncNoteNullifiers returns early when no notes noteStore.getNotes.mockResolvedValue([]); - service = new ContractSyncService(aztecNode, contractStore, noteStore, createLogger('test:contract-sync')); + service = new ContractSyncService( + aztecNode, + contractStore, + contractClassService, + noteStore, + createLogger('test:contract-sync'), + ); }); describe('ensureContractSynced', () => { @@ -251,53 +252,6 @@ describe('ContractSyncService', () => { }); }); - describe('class ID verification deduplication', () => { - const contract2 = AztecAddress.fromBigIntUnsafe(300n); - - it('verifies class ID only once per contract across scope batches', async () => { - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]); - expectVerifiedContracts(contractAddress); - }); - - it('verifies class ID separately for different contracts', async () => { - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - await service.ensureContractSynced(contract2, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectVerifiedContracts(contractAddress, contract2); - }); - - it('re-verifies class ID after wipe', async () => { - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - service.wipe(); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeB]); - expectVerifiedContracts(contractAddress, contractAddress); - }); - - it('re-verifies class ID after discardStaged', async () => { - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - await service.discardStaged(jobId); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectVerifiedContracts(contractAddress, contractAddress); - }); - - it('re-verifies class ID after verification failure', async () => { - contractStore.getContractInstance.mockRejectedValueOnce(new Error('node unavailable')); - await expect( - service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]), - ).rejects.toThrow('node unavailable'); - - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectVerifiedContracts(contractAddress, contractAddress); - }); - - it('does not re-verify class ID when only scope cache is invalidated', async () => { - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - service.invalidateContractForScopes(contractAddress, [scopeA]); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectVerifiedContracts(contractAddress); - }); - }); - describe('multi-scope sync batching', () => { it('batches nullifier sync across all unsynced scopes', async () => { await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ @@ -465,14 +419,6 @@ describe('ContractSyncService', () => { } }; - /** Asserts that class ID verification was triggered for each contract address in the given sequence. */ - const expectVerifiedContracts = (...addresses: AztecAddress[]) => { - expect(contractStore.getContractInstance).toHaveBeenCalledTimes(addresses.length); - for (let i = 0; i < addresses.length; i++) { - expect(contractStore.getContractInstance).toHaveBeenNthCalledWith(i + 1, addresses[i]); - } - }; - const expectNoSync = () => expect(utilityExecutor).not.toHaveBeenCalled(); /** Yields to the macrotask queue, draining all pending microtasks (semaphore acquires/releases) in between. */ diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts b/yarn-project/pxe/src/contract/contract_sync_service.ts similarity index 76% rename from yarn-project/pxe/src/contract_sync/contract_sync_service.ts rename to yarn-project/pxe/src/contract/contract_sync_service.ts index 6e669bb06b09..2d65336ddac9 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts +++ b/yarn-project/pxe/src/contract/contract_sync_service.ts @@ -10,7 +10,8 @@ import type { StagedStore } from '../job_coordinator/job_coordinator.js'; import { NoteService } from '../notes/note_service.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; -import { syncScope, verifyCurrentClassId } from './helpers.js'; +import type { ContractClassService } from './contract_class_service.js'; +import { syncScope } from './helpers.js'; /** * Maximum number of scope syncs running concurrently within a single sync call. Sized to trade off parallelism @@ -19,8 +20,8 @@ import { syncScope, verifyCurrentClassId } from './helpers.js'; export const MAX_CONCURRENT_SCOPE_SYNCS = 5; /** - * Service for syncing the private state of contracts and verifying that the PXE holds the current class artifact. - * It uses a cache to avoid redundant sync operations - the cache is wiped when the anchor block changes. + * Service for syncing the private state of contracts. It uses a cache to avoid redundant sync operations - the cache + * is wiped when the anchor block changes. * * TODO: The StagedStore naming is broken here. Figure out a better name. */ @@ -32,19 +33,16 @@ export class ContractSyncService implements StagedStore { // The value is a promise that resolves when the contract is synced. private syncedContracts: Map> = new Map(); - // Tracks class ID verification per contract. Keyed by contract address only (no scope), since - // class ID verification is scope-independent. Cleared on wipe/discard. - private classIdVerificationCache: Map> = new Map(); - constructor( private aztecNode: AztecNode, private contractStore: ContractStore, + private contractClassService: ContractClassService, private noteStore: NoteStore, private log: Logger, ) {} /** - * Ensures a contract's private state is synchronized and that the PXE holds the current class artifact. + * Ensures a contract's private state is synchronized. * Uses a cache to avoid redundant sync operations - the cache is wiped when the anchor block changes. * @param contractAddress - The address of the contract to sync. * @param functionToInvokeAfterSync - The function selector that will be called after sync (used to validate it's @@ -61,7 +59,15 @@ export class ContractSyncService implements StagedStore { scopes: AztecAddress[], ): Promise { this.#startSyncIfNeeded(contractAddress, scopes, anchorBlockHeader, jobId, scope => - syncScope(contractAddress, this.contractStore, functionToInvokeAfterSync, utilityExecutor, scope), + syncScope( + contractAddress, + this.contractStore, + this.contractClassService, + anchorBlockHeader, + functionToInvokeAfterSync, + utilityExecutor, + scope, + ), ); await this.#awaitSync(contractAddress, scopes); @@ -79,7 +85,6 @@ export class ContractSyncService implements StagedStore { wipe(): void { this.log.debug(`Wiping contract sync cache (${this.syncedContracts.size} entries)`); this.syncedContracts.clear(); - this.classIdVerificationCache.clear(); } commit(_jobId: string): Promise { @@ -90,15 +95,13 @@ export class ContractSyncService implements StagedStore { // We clear the synced contracts cache here because, when the job is discarded, any associated database writes from // the sync are also undone. this.syncedContracts.clear(); - this.classIdVerificationCache.clear(); return Promise.resolve(); } /** * For each unsynced scope, creates a promise that waits on: - * 1. Class ID verification (cached per contract, scope-independent). - * 2. Note nullifier sync (shared, batched across all unsynced scopes). - * 3. Per-scope sync (individual, semaphore-bounded). + * 1. Note nullifier sync (shared, batched across all unsynced scopes). + * 2. Per-scope sync (individual, semaphore-bounded). */ #startSyncIfNeeded( contractAddress: AztecAddress, @@ -114,7 +117,6 @@ export class ContractSyncService implements StagedStore { this.log.debug(`Syncing contract ${contractAddress} for ${scopesToSync.length} scope(s)`); - const verifyPromise = this.#verifyClassId(contractAddress, anchorBlockHeader); const syncNullifiersPromise = this.#syncNoteNullifiers(contractAddress, anchorBlockHeader, jobId, scopesToSync); // We build a new semaphore for each sync call, so it rate-limits the scopes within that single call. We do @@ -123,11 +125,7 @@ export class ContractSyncService implements StagedStore { for (const scope of scopesToSync) { const key = toKey(contractAddress, scope); - const promise = Promise.all([ - verifyPromise, - syncNullifiersPromise, - runBounded(syncSlot, () => syncScopeFn(scope)), - ]) + const promise = Promise.all([syncNullifiersPromise, runBounded(syncSlot, () => syncScopeFn(scope))]) .then(() => {}) .catch(err => { this.syncedContracts.delete(key); @@ -137,23 +135,6 @@ export class ContractSyncService implements StagedStore { } } - /** Verifies the local class ID matches the on-chain value (cached, evicts on failure so retries re-verify). */ - #verifyClassId(contractAddress: AztecAddress, anchorBlockHeader: BlockHeader): Promise { - const contractKey = contractAddress.toString(); - const cached = this.classIdVerificationCache.get(contractKey); - if (cached) { - return cached; - } - const promise = verifyCurrentClassId(contractAddress, this.aztecNode, this.contractStore, anchorBlockHeader).catch( - err => { - this.classIdVerificationCache.delete(contractKey); - throw err; - }, - ); - this.classIdVerificationCache.set(contractKey, promise); - return promise; - } - /** Syncs note nullifiers across all unsynced scopes in a single batched call. */ async #syncNoteNullifiers( contractAddress: AztecAddress, diff --git a/yarn-project/pxe/src/contract/helpers.ts b/yarn-project/pxe/src/contract/helpers.ts new file mode 100644 index 000000000000..c6f7871f3d40 --- /dev/null +++ b/yarn-project/pxe/src/contract/helpers.ts @@ -0,0 +1,35 @@ +import { isProtocolContract } from '@aztec/protocol-contracts'; +import type { FunctionCall, FunctionSelector } from '@aztec/stdlib/abi'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { BlockHeader } from '@aztec/stdlib/tx'; + +import type { ContractStore } from '../storage/contract_store/contract_store.js'; +import type { ContractClassService } from './contract_class_service.js'; + +export async function syncScope( + contractAddress: AztecAddress, + contractStore: ContractStore, + contractClassService: ContractClassService, + anchorBlockHeader: BlockHeader, + functionToInvokeAfterSync: FunctionSelector | null, + utilityExecutor: (privateSyncCall: FunctionCall, scopes: AztecAddress[]) => Promise, + scope: AztecAddress, +) { + // Protocol contracts don't have private state to sync + if (isProtocolContract(contractAddress)) { + return; + } + + const classId = await contractClassService.getCurrentClassId(contractAddress, anchorBlockHeader); + if (!classId) { + throw new Error(`Cannot sync contract ${contractAddress}: its instance is not registered nor published.`); + } + const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [scope], contractAddress, classId); + if (functionToInvokeAfterSync && functionToInvokeAfterSync.equals(syncStateFunctionCall.selector)) { + throw new Error( + 'Forbidden `sync_state` invocation. `sync_state` can only be invoked by PXE, manual execution can lead to inconsistencies.', + ); + } + + await utilityExecutor(syncStateFunctionCall, [scope]); +} diff --git a/yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts b/yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts new file mode 100644 index 000000000000..f3beeb5f6d4d --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts @@ -0,0 +1,74 @@ +import type { Fr } from '@aztec/foundation/curves/bn254'; +import type { FunctionArtifactWithContractName, FunctionSelector } from '@aztec/stdlib/abi'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { ContractInstancePreimageWithAddress } from '@aztec/stdlib/contract'; +import type { BlockHeader, ContractOverrides } from '@aztec/stdlib/tx'; + +import type { ContractClassService } from '../contract/contract_class_service.js'; +import type { ContractStore } from '../storage/contract_store/contract_store.js'; + +/** + * Per-run view of contract data for a single simulation, bound to its anchor block. + * + * The {@link ContractStore} is pure class-id-keyed storage and has no notion of which class an address runs. This + * class bridges that gap: it resolves an address to its current class id (via the {@link ContractClassService} + * at the run's anchor block) and then serves artifacts from the store. It is also the single place contract + * overrides are applied — when an address is overridden, both its instance and its class id come from the override + * rather than the chain, so a simulation can execute different bytecode at that address. Overrides are only set for + * `simulateTx` (which skips the kernels), so the override path never reaches proving. + */ +export class AnchoredContractData { + constructor( + private readonly store: ContractStore, + private readonly contractClassService: ContractClassService, + private readonly anchorBlockHeader: BlockHeader, + private readonly overrides?: ContractOverrides, + ) {} + + /** Returns the address preimage of the instance at `address`, from the override if any, else from storage. */ + getContractInstance(address: AztecAddress): Promise { + const override = this.overrides?.[address.toString()]; + if (override) { + return Promise.resolve(override.instance); + } + return this.store.getContractInstance(address); + } + + /** + * Resolves the class id `address` runs in this simulation: the override's class if overridden, else resolved against + * the chain at the anchor block. + */ + getCurrentClassId(address: AztecAddress): Promise { + const override = this.overrides?.[address.toString()]; + if (override) { + return Promise.resolve(override.instance.currentContractClassId); + } + return this.contractClassService.getCurrentClassId(address, this.anchorBlockHeader); + } + + async getFunctionArtifact( + address: AztecAddress, + selector: FunctionSelector, + ): Promise { + const classId = await this.getCurrentClassId(address); + return classId ? this.store.getFunctionArtifact(classId, selector) : undefined; + } + + async getFunctionArtifactWithDebugMetadata( + address: AztecAddress, + selector: FunctionSelector, + ): Promise { + const classId = await this.getCurrentClassId(address); + return classId ? this.store.getFunctionArtifactWithDebugMetadata(classId, selector) : undefined; + } + + async getDebugContractName(address: AztecAddress): Promise { + const classId = await this.getCurrentClassId(address); + return classId ? this.store.getDebugContractName(classId) : undefined; + } + + async getDebugFunctionName(address: AztecAddress, selector: FunctionSelector): Promise { + const classId = await this.getCurrentClassId(address); + return classId ? this.store.getDebugFunctionName(classId, selector) : `${address}:${selector}`; + } +} diff --git a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts index 9d4f3a1367a1..b7394b9ee147 100644 --- a/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts +++ b/yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts @@ -80,6 +80,7 @@ import { MerkleTreeId } from '@aztec/stdlib/trees'; import { BlockHeader, CallContext, + type ContractOverrides, HashedValues, type OffchainEffect, PrivateExecutionResult, @@ -90,7 +91,8 @@ import { getFinalMinRevertibleSideEffectCounter, } from '@aztec/stdlib/tx'; -import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; +import type { ContractClassService } from '../contract/contract_class_service.js'; +import type { ContractSyncService } from '../contract/contract_sync_service.js'; import type { ExecutionHooks } from '../hooks/index.js'; import type { TxResolverService } from '../messages/tx_resolver_service.js'; import type { AddressStore } from '../storage/address_store/address_store.js'; @@ -104,6 +106,7 @@ import type { PrivateEventStore } from '../storage/private_event_store/private_e import type { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import type { SenderTaggingStore } from '../storage/tagging_store/sender_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../storage/tagging_store/tagging_secret_sources_store.js'; +import { AnchoredContractData } from './anchored_contract_data.js'; import type { BenchmarkedNode } from './benchmarked_node.js'; import { ExecutionNoteCache } from './execution_note_cache.js'; import { ExecutionTaggingIndexCache } from './execution_tagging_index_cache.js'; @@ -131,6 +134,9 @@ export type ContractSimulatorRunOpts = { /** Args for ContractFunctionSimulator constructor. */ export type ContractFunctionSimulatorArgs = { contractStore: ContractStore; + contractClassService: ContractClassService; + /** Per-simulation contract overrides. */ + overrides?: ContractOverrides; noteStore: NoteStore; keyStore: KeyStore; addressStore: AddressStore; @@ -154,6 +160,8 @@ export type ContractFunctionSimulatorArgs = { export class ContractFunctionSimulator { private readonly log: Logger; private readonly contractStore: ContractStore; + private readonly contractClassService: ContractClassService; + private readonly overrides: ContractOverrides | undefined; private readonly noteStore: NoteStore; private readonly keyStore: KeyStore; private readonly addressStore: AddressStore; @@ -172,6 +180,8 @@ export class ContractFunctionSimulator { constructor(args: ContractFunctionSimulatorArgs) { this.contractStore = args.contractStore; + this.contractClassService = args.contractClassService; + this.overrides = args.overrides; this.noteStore = args.noteStore; this.keyStore = args.keyStore; this.addressStore = args.addressStore; @@ -208,10 +218,23 @@ export class ContractFunctionSimulator { const contractAddress = request.origin; - const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata( + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.contractClassService, + anchorBlockHeader, + this.overrides, + ); + + const entryPointArtifact = await anchoredContractData.getFunctionArtifactWithDebugMetadata( contractAddress, request.functionSelector, ); + if (!entryPointArtifact) { + throw new Error( + `Cannot run function ${request.functionSelector} on ${contractAddress}: the contract is not registered. ` + + `Register it via wallet.registerContract(...).`, + ); + } if (entryPointArtifact.functionType !== FunctionType.PRIVATE) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as private`); @@ -249,7 +272,7 @@ export class ContractFunctionSimulator { executionCache: HashedValuesCache.create(request.argsOfCalls), noteCache, taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -333,7 +356,20 @@ export class ContractFunctionSimulator { scopes: AztecAddress[], jobId: string, ): Promise<{ result: Fr[]; offchainEffects: OffchainEffect[] }> { - const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.contractClassService, + anchorBlockHeader, + this.overrides, + ); + + const entryPointArtifact = await anchoredContractData.getFunctionArtifactWithDebugMetadata(call.to, call.selector); + if (!entryPointArtifact) { + throw new Error( + `Cannot run function ${call.selector} on ${call.to}: the contract is not registered. ` + + `Register it via wallet.registerContract(...).`, + ); + } if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); @@ -353,7 +389,7 @@ export class ContractFunctionSimulator { authWitnesses: authwits, capsules: [], anchorBlockHeader, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts index 70518a08e4f7..4074b81d786e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts @@ -24,7 +24,7 @@ import { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { PublicDataWrite, RevertCode } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; -import type { ContractInstance, PartialAddress } from '@aztec/stdlib/contract'; +import type { ContractInstancePreimage, PartialAddress } from '@aztec/stdlib/contract'; import type { GasFees } from '@aztec/stdlib/gas'; import { KeyValidationRequest } from '@aztec/stdlib/kernel'; import type { PublicKeys } from '@aztec/stdlib/keys'; @@ -86,7 +86,7 @@ export type { AppTaggingSecretKind, BlockHash, BlockHeader, - ContractInstance, + ContractInstancePreimage, MembershipWitness, MessageContext, NullifierMembershipWitness, @@ -354,7 +354,7 @@ const PUBLIC_KEYS: TypeMapping = STRUCT([ { name: 'fbpkMHash', type: FIELD }, ]); -export const CONTRACT_INSTANCE: TypeMapping = STRUCT([ +export const CONTRACT_INSTANCE: TypeMapping = STRUCT([ { name: 'salt', type: FIELD }, { name: 'deployer', type: AZTEC_ADDRESS }, // Note that the nr side of this struct does not contain the current class, only original diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index ecc96f28037d..54e0fa1e6a74 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -14,7 +14,8 @@ import { BlockHeader, CallContext, HashedValues, TxContext, TxExecutionRequest } import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; +import type { ContractClassService } from '../../contract/contract_class_service.js'; +import type { ContractSyncService } from '../../contract/contract_sync_service.js'; import type { TxResolverService } from '../../messages/tx_resolver_service.js'; import { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; @@ -28,6 +29,7 @@ import type { PrivateEventStore } from '../../storage/private_event_store/privat import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../../storage/tagging_store/tagging_secret_sources_store.js'; +import { AnchoredContractData } from '../anchored_contract_data.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; import { TransientArrayService } from '../transient_array_service.js'; import { buildACIRCallback } from './acir_callback.js'; @@ -37,6 +39,7 @@ describe('Oracle Version Check test suite', () => { const simulator = new WASMSimulator(); let contractStore: ReturnType>; + let contractClassService: ReturnType>; let noteStore: ReturnType>; let keyStore: ReturnType>; let addressStore: ReturnType>; @@ -94,16 +97,20 @@ describe('Oracle Version Check test suite', () => { originalContractClassId: new Fr(42), address: contractAddress, } as ContractInstanceWithAddress); - contractStore.getFunctionArtifactWithDebugMetadata.mockImplementation(async (address, selector) => { - const artifact = await contractStore.getFunctionArtifact(address, selector); + contractStore.getFunctionArtifactWithDebugMetadata.mockImplementation(async (classId, selector) => { + const artifact = await contractStore.getFunctionArtifact(classId, selector); if (!artifact) { - throw new Error(`Function not found: ${selector.toString()} in contract ${address}`); + throw new Error(`Function not found: ${selector.toString()} in contract class ${classId}`); } return { ...artifact, debug: undefined }; }); + contractClassService = mock(); + contractClassService.getCurrentClassId.mockResolvedValue(new Fr(42)); + acirSimulator = new ContractFunctionSimulator({ contractStore, + contractClassService, noteStore, keyStore, addressStore, @@ -207,7 +214,7 @@ describe('Oracle Version Check test suite', () => { authWitnesses: [], capsules: [], anchorBlockHeader, - contractStore, + anchoredContractData: new AnchoredContractData(contractStore, contractClassService, anchorBlockHeader), noteStore, keyStore, addressStore, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index a17064072dd1..069354376672 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -52,8 +52,9 @@ import { jest } from '@jest/globals'; import { Matcher, type MatcherCreator, type MockProxy, mock } from 'jest-mock-extended'; import { toFunctionSelector } from 'viem'; -import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; -import { syncScope } from '../../contract_sync/helpers.js'; +import type { ContractClassService } from '../../contract/contract_class_service.js'; +import type { ContractSyncService } from '../../contract/contract_sync_service.js'; +import { syncScope } from '../../contract/helpers.js'; import type { TxResolverService } from '../../messages/tx_resolver_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; @@ -103,6 +104,7 @@ describe('Private Execution test suite', () => { const simulator = new WASMSimulator(); let contractStore: MockProxy; + let contractClassService: MockProxy; let noteStore: MockProxy; let addressStore: MockProxy; let keyStore: MockProxy; @@ -280,6 +282,12 @@ describe('Private Execution test suite', () => { ws = await NativeWorldStateService.tmp(); fork = await ws.fork(); contractStore = mock(); + contractClassService = mock(); + // No upgrades in these tests: an address resolves to a class id whose string matches the address, so the + // address-keyed `contracts` map and store mocks below keep working when keyed by the resolved class id. + contractClassService.getCurrentClassId.mockImplementation(address => + Promise.resolve(Fr.fromHexString(address.toString())), + ); noteStore = mock(); noteStore.getNotes.mockResolvedValue([]); addressStore = mock(); @@ -300,7 +308,15 @@ describe('Private Execution test suite', () => { contractSyncService.ensureContractSynced.mockImplementation( async (contractAddress, functionToInvokeAfterSync, utilityExecutor, _anchorBlockHeader, _jobId, scopes) => { for (const scope of scopes) { - await syncScope(contractAddress, contractStore, functionToInvokeAfterSync, utilityExecutor, scope); + await syncScope( + contractAddress, + contractStore, + contractClassService, + _anchorBlockHeader, + functionToInvokeAfterSync, + utilityExecutor, + scope, + ); } }, ); @@ -458,6 +474,7 @@ describe('Private Execution test suite', () => { acirSimulator = new ContractFunctionSimulator({ contractStore, + contractClassService, noteStore, keyStore, addressStore, @@ -710,7 +727,7 @@ describe('Private Execution test suite', () => { expect( contractStore.getFunctionArtifact.mock.calls.some( - ([addr, sel]) => addr.equals(childAddress) && sel.equals(childSelector), + ([classId, sel]) => classId.toString() === childAddress.toString() && sel.equals(childSelector), ), ).toBe(true); expect(result.nestedExecutionResults).toHaveLength(1); @@ -737,7 +754,12 @@ describe('Private Execution test suite', () => { contractAddress: parentAddress, }); - expect(contractStore.getFunctionCall).toHaveBeenCalledWith('sync_state', [owner], childAddress); + expect(contractStore.getFunctionCall).toHaveBeenCalledWith( + 'sync_state', + [owner], + childAddress, + expect.anything(), + ); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts index 5f325f341314..8e70f85d4175 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts @@ -7,7 +7,6 @@ import { FunctionSelector } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2TipsProvider } from '@aztec/stdlib/block'; -import { SerializableContractInstance } from '@aztec/stdlib/contract'; import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { AppTaggingSecret, AppTaggingSecretKind } from '@aztec/stdlib/logs'; @@ -16,7 +15,7 @@ import { type BlockHeader, CallContext, type Capsule, TxContext } from '@aztec/s import { jest } from '@jest/globals'; import { mock } from 'jest-mock-extended'; -import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; +import type { ContractSyncService } from '../../contract/contract_sync_service.js'; import type { ResolveCustomRequest } from '../../hooks/resolve_custom_request.js'; import type { ResolveTaggingSecretStrategy, @@ -26,7 +25,6 @@ import type { TxResolverService } from '../../messages/tx_resolver_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; -import type { ContractStore } from '../../storage/contract_store/contract_store.js'; import { FactService } from '../../storage/fact_store/index.js'; import type { FactStore } from '../../storage/fact_store/index.js'; import type { NoteStore } from '../../storage/note_store/note_store.js'; @@ -34,6 +32,7 @@ import type { PrivateEventStore } from '../../storage/private_event_store/privat import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../../storage/tagging_store/tagging_secret_sources_store.js'; +import type { AnchoredContractData } from '../anchored_contract_data.js'; import { ExecutionNoteCache } from '../execution_note_cache.js'; import { ExecutionTaggingIndexCache } from '../execution_tagging_index_cache.js'; import { HashedValuesCache } from '../hashed_values_cache.js'; @@ -119,7 +118,7 @@ describe('PrivateExecutionOracle', () => { }); it('resolves a non-interactive-handshake strategy', async () => { - const { oracle } = await makeHookedOracle({ strategy: { type: 'non-interactive-handshake' } }); + const { oracle } = makeHookedOracle({ strategy: { type: 'non-interactive-handshake' } }); await expect(oracle.resolveTaggingStrategy(sender, recipient, AppTaggingSecretKind.CONSTRAINED)).resolves.toEqual( { @@ -129,7 +128,7 @@ describe('PrivateExecutionOracle', () => { }); it('resolves an address-derived strategy to the unconstrained secret', async () => { - const { oracle } = await makeHookedOracle({ strategy: { type: 'address-derived' } }); + const { oracle } = makeHookedOracle({ strategy: { type: 'address-derived' } }); const secret = Fr.random(); jest.spyOn(oracle, 'getAppTaggingSecret').mockResolvedValue(Option.some(secret)); @@ -140,7 +139,7 @@ describe('PrivateExecutionOracle', () => { it('app-silos a raw arbitrary-secret point before handing it to the contract', async () => { const point = await Point.random(); - const { oracle } = await makeHookedOracle({ strategy: { type: 'arbitrary-secret', secret: point } }); + const { oracle } = makeHookedOracle({ strategy: { type: 'arbitrary-secret', secret: point } }); const expected = await AppTaggingSecret.computeDirectional(point, contractAddress, recipient); await expect( @@ -149,7 +148,7 @@ describe('PrivateExecutionOracle', () => { }); it('overrides a hooked non-interactive handshake on an unconstrained self-send with an address-derived secret', async () => { - const { oracle } = await makeHookedOracle({ + const { oracle } = makeHookedOracle({ strategy: { type: 'non-interactive-handshake' }, keyStore: makeKeyStore({ ownsRecipient: true }), }); @@ -162,7 +161,7 @@ describe('PrivateExecutionOracle', () => { }); it('keeps a hooked non-interactive handshake under constrained delivery even when the wallet owns the recipient', async () => { - const { oracle } = await makeHookedOracle({ + const { oracle } = makeHookedOracle({ strategy: { type: 'non-interactive-handshake' }, keyStore: makeKeyStore({ ownsRecipient: true }), }); @@ -174,7 +173,7 @@ describe('PrivateExecutionOracle', () => { it('passes the correct message context to the hook', async () => { const contractClassId = Fr.random(); - const { oracle, resolveTaggingSecretStrategy } = await makeHookedOracle({ + const { oracle, resolveTaggingSecretStrategy } = makeHookedOracle({ strategy: { type: 'non-interactive-handshake' }, contractClassId, }); @@ -190,7 +189,7 @@ describe('PrivateExecutionOracle', () => { }); }); - const makeHookedOracle = async ({ + const makeHookedOracle = ({ strategy, contractClassId = Fr.random(), keyStore = makeKeyStore({ ownsRecipient: false }), @@ -200,10 +199,9 @@ describe('PrivateExecutionOracle', () => { keyStore?: KeyStore; }) => { const resolveTaggingSecretStrategy = jest.fn().mockResolvedValue(strategy); - const oracle = makeOracle({ hooks: { resolveTaggingSecretStrategy }, keyStore }); - jest - .spyOn(oracle, 'getContractInstance') - .mockResolvedValue(await SerializableContractInstance.random({ currentContractClassId: contractClassId })); + const anchoredContractData = mock(); + anchoredContractData.getCurrentClassId.mockResolvedValue(contractClassId); + const oracle = makeOracle({ hooks: { resolveTaggingSecretStrategy }, keyStore, anchoredContractData }); return { oracle, resolveTaggingSecretStrategy }; }; @@ -218,11 +216,10 @@ describe('PrivateExecutionOracle', () => { it('relays the request to the hook with the issuing contract context and returns its result', async () => { const result = [Fr.random(), Fr.random()]; const resolveCustomRequest = jest.fn().mockResolvedValue(result); - const oracle = makeOracle({ hooks: { resolveCustomRequest } }); const contractClassId = Fr.random(); - jest - .spyOn(oracle, 'getContractInstance') - .mockResolvedValue(await SerializableContractInstance.random({ currentContractClassId: contractClassId })); + const anchoredContractData = mock(); + anchoredContractData.getCurrentClassId.mockResolvedValue(contractClassId); + const oracle = makeOracle({ hooks: { resolveCustomRequest }, anchoredContractData }); const kind = Fr.random(); const payload = [Fr.random(), Fr.random(), Fr.random()]; @@ -250,7 +247,7 @@ describe('PrivateExecutionOracle', () => { executionCache: HashedValuesCache.create([]), noteCache: new ExecutionNoteCache(Fr.ZERO), taggingIndexCache: new ExecutionTaggingIndexCache(), - contractStore: mock(), + anchoredContractData: mock(), noteStore: mock(), keyStore: mock(), addressStore: mock(), diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 7440d28e8e74..cc08e3b25e2e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -197,8 +197,8 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP if (!hook) { throw new Error('Cannot serve a request: no resolveCustomRequest hook is configured'); } - const { currentContractClassId } = await this.getContractInstance(this.contractAddress); - return hook({ contractAddress: this.contractAddress, contractClassId: currentContractClassId, kind, payload }); + const contractClassId = await this.#getCurrentContractClassId(this.contractAddress); + return hook({ contractAddress: this.contractAddress, contractClassId, kind, payload }); } /** @@ -237,16 +237,28 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP return { type: 'non-interactive-handshake' }; } - const { currentContractClassId } = await this.getContractInstance(this.contractAddress); + const contractClassId = await this.#getCurrentContractClassId(this.contractAddress); return hook({ contractAddress: this.contractAddress, - contractClassId: currentContractClassId, + contractClassId, sender, recipient, deliveryMode, }); } + /** + * Resolves the class id the given contract runs at this simulation's anchor block. The instance no longer carries + * its class id (it is chain-derived), so it is resolved via the anchored contract data instead. + */ + async #getCurrentContractClassId(address: AztecAddress): Promise { + const classId = await this.anchoredContractData.getCurrentClassId(address); + if (classId === undefined) { + throw new Error(`Cannot resolve the current contract class for ${address}`); + } + return classId; + } + /** Whether this is an unconstrained delivery to one of the wallet's own accounts (a self-send). */ #isUnconstrainedSelfSend(recipient: AztecAddress, deliveryMode: AppTaggingSecretKind) { return deliveryMode === AppTaggingSecretKind.UNCONSTRAINED @@ -641,10 +653,16 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP this.scopes, ); - const targetArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata( + const targetArtifact = await this.anchoredContractData.getFunctionArtifactWithDebugMetadata( targetContractAddress, functionSelector, ); + if (!targetArtifact) { + throw new Error( + `Cannot call ${targetContractAddress}:${functionSelector}: the contract is not registered. ` + + `Register it via wallet.registerContract(...).`, + ); + } const derivedTxContext = this.txContext.clone(); @@ -661,7 +679,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP executionCache: this.executionCache, noteCache: this.noteCache, taggingIndexCache: this.taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData: this.anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -765,7 +783,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP } public getDebugFunctionName() { - return this.contractStore.getDebugFunctionName(this.contractAddress, this.callContext.functionSelector); + return this.anchoredContractData.getDebugFunctionName(this.contractAddress, this.callContext.functionSelector); } protected override get callerContext() { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index a71b982830e6..eb8389417f53 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -33,7 +33,8 @@ import { BlockHeader, CallContext, Capsule, GlobalVariables, TxHash } from '@azt import { mock } from 'jest-mock-extended'; import type { _MockProxy } from 'jest-mock-extended/lib/Mock.js'; -import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; +import type { ContractClassService } from '../../contract/contract_class_service.js'; +import type { ContractSyncService } from '../../contract/contract_sync_service.js'; import { TxResolverService } from '../../messages/tx_resolver_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; @@ -46,6 +47,7 @@ import type { PrivateEventStore } from '../../storage/private_event_store/privat import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../../storage/tagging_store/tagging_secret_sources_store.js'; +import { AnchoredContractData } from '../anchored_contract_data.js'; import { ContractFunctionSimulator } from '../contract_function_simulator.js'; import { EphemeralArrayService } from '../ephemeral_array_service.js'; import { BoundedVec } from '../noir-structs/bounded_vec.js'; @@ -60,6 +62,7 @@ describe('Utility Execution test suite', () => { const simulator = new WASMSimulator(); let contractStore: ReturnType>; + let contractClassService: ReturnType>; let noteStore: ReturnType>; let keyStore: ReturnType>; let addressStore: ReturnType>; @@ -85,6 +88,8 @@ describe('Utility Execution test suite', () => { beforeEach(async () => { contractStore = mock(); + contractClassService = mock(); + contractClassService.getCurrentClassId.mockResolvedValue(new Fr(42)); noteStore = mock(); keyStore = mock(); addressStore = mock(); @@ -119,6 +124,7 @@ describe('Utility Execution test suite', () => { }); acirSimulator = new ContractFunctionSimulator({ contractStore, + contractClassService, noteStore, keyStore, addressStore, @@ -746,7 +752,7 @@ describe('Utility Execution test suite', () => { authWitnesses: [], capsules: [], anchorBlockHeader, - contractStore, + anchoredContractData: new AnchoredContractData(contractStore, contractClassService, anchorBlockHeader), noteStore, keyStore, addressStore, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 4fe4a8417d01..625c85470e1f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -20,7 +20,7 @@ import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash, type L2TipsProvider } from '@aztec/stdlib/block'; -import type { CompleteAddress, ContractInstance, PartialAddress } from '@aztec/stdlib/contract'; +import type { CompleteAddress, ContractInstancePreimageWithAddress, PartialAddress } from '@aztec/stdlib/contract'; import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; @@ -38,8 +38,8 @@ import { type TxHash, } from '@aztec/stdlib/tx'; +import type { ContractSyncService } from '../../contract/contract_sync_service.js'; import { createContractLogger, logContractMessage, stripAztecnrLogPrefix } from '../../contract_logging.js'; -import type { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; import { EventService } from '../../events/event_service.js'; import type { UtilityCallAuthorizationRequest } from '../../hooks/authorize_utility_call.js'; import type { ExecutionHooks } from '../../hooks/index.js'; @@ -49,13 +49,13 @@ import { NoteService } from '../../notes/note_service.js'; import { ORACLE_VERSION_MAJOR } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import type { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; -import type { ContractStore } from '../../storage/contract_store/contract_store.js'; import { FactCollectionKey, FactCollectionTypeKey, anchoredTipBlockNumbers } from '../../storage/fact_store/index.js'; import type { FactService, OriginBlock } from '../../storage/fact_store/index.js'; import type { NoteStore } from '../../storage/note_store/note_store.js'; import type { PrivateEventStore } from '../../storage/private_event_store/private_event_store.js'; import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../../storage/tagging_store/tagging_secret_sources_store.js'; +import type { AnchoredContractData } from '../anchored_contract_data.js'; import { EphemeralArrayService } from '../ephemeral_array_service.js'; import { BoundedVec } from '../noir-structs/bounded_vec.js'; import type { EmbeddedCurvePoint } from '../noir-structs/embedded_curve_point.js'; @@ -83,7 +83,7 @@ export type UtilityExecutionOracleArgs = { authWitnesses: AuthWitness[]; capsules: Capsule[]; // TODO(#12425): Rename to transientCapsules anchorBlockHeader: BlockHeader; - contractStore: ContractStore; + anchoredContractData: AnchoredContractData; noteStore: NoteStore; keyStore: KeyStore; addressStore: AddressStore; @@ -126,7 +126,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra protected readonly authWitnesses: AuthWitness[]; protected readonly capsules: Capsule[]; protected readonly anchorBlockHeader: BlockHeader; - protected readonly contractStore: ContractStore; + protected readonly anchoredContractData: AnchoredContractData; protected readonly noteStore: NoteStore; protected readonly keyStore: KeyStore; protected readonly addressStore: AddressStore; @@ -151,7 +151,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.authWitnesses = args.authWitnesses; this.capsules = args.capsules; this.anchorBlockHeader = args.anchorBlockHeader; - this.contractStore = args.contractStore; + this.anchoredContractData = args.anchoredContractData; this.noteStore = args.noteStore; this.keyStore = args.keyStore; this.addressStore = args.addressStore; @@ -379,8 +379,8 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra * @param address - Address. * @returns A contract instance. */ - public async getContractInstance(address: AztecAddress): Promise { - const instance = await this.contractStore.getContractInstance(address); + public async getContractInstance(address: AztecAddress): Promise { + const instance = await this.anchoredContractData.getContractInstance(address); if (!instance) { throw new Error(`No contract instance found for address ${address.toString()}`); } @@ -535,7 +535,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // to re-use jobId as instanceId here as executions of different PXE jobs are isolated. this.contractLogger = await createContractLogger( this.contractAddress, - addr => this.contractStore.getDebugContractName(addr), + addr => this.anchoredContractData.getDebugContractName(addr), 'user', { instanceId: this.jobId }, ); @@ -552,7 +552,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra // to re-use jobId as instanceId here as executions of different PXE jobs are isolated. this.aztecnrLogger = await createContractLogger( this.contractAddress, - addr => this.contractStore.getDebugContractName(addr), + addr => this.anchoredContractData.getDebugContractName(addr), 'aztecnr', { instanceId: this.jobId }, ); @@ -944,23 +944,35 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra functionSelector: FunctionSelector, args: Fr[], ): Promise { - const targetArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata( + const targetArtifact = await this.anchoredContractData.getFunctionArtifactWithDebugMetadata( targetContractAddress, functionSelector, ); + if (!targetArtifact) { + throw new Error( + `Cannot call ${targetContractAddress}:${functionSelector}: the contract is not registered. ` + + `Register it via wallet.registerContract(...).`, + ); + } if (!targetContractAddress.equals(this.contractAddress)) { // Standard handshake registry reads are authorized by default; every other cross-contract call needs the hook. if (!(await isStandardHandshakeRegistryUtilityRead(targetContractAddress, functionSelector))) { - const [callerInstance, targetInstance] = await Promise.all([ - this.getContractInstance(this.contractAddress), - this.getContractInstance(targetContractAddress), + const [callerClassId, targetClassId] = await Promise.all([ + this.anchoredContractData.getCurrentClassId(this.contractAddress), + this.anchoredContractData.getCurrentClassId(targetContractAddress), ]); + if (!callerClassId || !targetClassId) { + throw new Error( + `Cannot authorize utility call from ${this.contractAddress} to ${targetContractAddress}: ` + + `${!callerClassId ? this.contractAddress : targetContractAddress} is not registered.`, + ); + } const request: UtilityCallAuthorizationRequest = { caller: this.contractAddress, - callerClassId: callerInstance.currentContractClassId, + callerClassId, target: targetContractAddress, - targetClassId: targetInstance.currentContractClassId, + targetClassId, functionSelector, functionName: targetArtifact.name, args, @@ -1005,7 +1017,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra authWitnesses: this.authWitnesses, capsules: this.capsules, anchorBlockHeader: this.anchorBlockHeader, - contractStore: this.contractStore, + anchoredContractData: this.anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, diff --git a/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts b/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts deleted file mode 100644 index f7f9ce20da5f..000000000000 --- a/yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type FunctionSelector, findFunctionArtifactBySelector } from '@aztec/stdlib/abi'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractOverrides } from '@aztec/stdlib/tx'; - -import type { ContractStore } from '../storage/contract_store/contract_store.js'; - -/* - * Proxy generator for a ContractStore that allows overriding contract instances at given addresses, - * so the contract function simulator can execute different bytecode on certain addresses. An example - * use case would be overriding your own account contract so that valid signatures don't have to be - * provided while simulating. - * - * Function artifact lookups for an overridden address are routed via the override-instance's - * `currentContractClassId` rather than the underlying ContractStore's address→class mapping — - * `ContractStore.getFunctionArtifact` calls `this.getContractInstance` internally, which the proxy - * cannot intercept (the binding sees the raw target, not the proxy). The target class must be - * registered ahead of time via `pxe.registerContractClass(...)`. - */ -export class ProxiedContractStoreFactory { - static create(contractStore: ContractStore, overrides?: ContractOverrides) { - if (!overrides) { - return contractStore; - } - - return new Proxy(contractStore, { - get(target, prop: keyof ContractStore) { - if (prop === 'getContractInstance') { - return (address: AztecAddress) => { - const override = overrides[address.toString()]; - return override ? Promise.resolve(override.instance) : target.getContractInstance(address); - }; - } - if (prop === 'getFunctionArtifact' || prop === 'getFunctionArtifactWithDebugMetadata') { - return async (contractAddress: AztecAddress, selector: FunctionSelector) => { - const override = overrides[contractAddress.toString()]; - if (!override) { - return (target[prop] as Function).call(target, contractAddress, selector); - } - const artifact = await target.getContractArtifact(override.instance.currentContractClassId); - if (!artifact) { - throw new Error( - `No artifact registered for override class ${override.instance.currentContractClassId} ` + - `at ${contractAddress}. Register it via pxe.registerContractClass(...) before simulating.`, - ); - } - const fn = await findFunctionArtifactBySelector(artifact, selector); - if (!fn) { - throw new Error( - `Function with selector ${selector} not found in stub artifact for overridden contract at ${contractAddress}.`, - ); - } - return { ...fn, contractName: artifact.name }; - }; - } - const value = Reflect.get(target, prop); - return typeof value === 'function' ? value.bind(target) : value; - }, - }) satisfies ContractStore; - } -} diff --git a/yarn-project/pxe/src/contract_sync/helpers.ts b/yarn-project/pxe/src/contract_sync/helpers.ts deleted file mode 100644 index b651d210280a..000000000000 --- a/yarn-project/pxe/src/contract_sync/helpers.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { isProtocolContract } from '@aztec/protocol-contracts'; -import type { FunctionCall, FunctionSelector } from '@aztec/stdlib/abi'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { ContractInstance } from '@aztec/stdlib/contract'; -import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import type { BlockHeader } from '@aztec/stdlib/tx'; - -import type { ContractStore } from '../storage/contract_store/contract_store.js'; - -/** - * Read the current class id of a contract as of the given block, as seen by the AztecNode. The node resolves it from - * the same scheduled value change the AVM enforces against the public data tree. If the node has no record of the - * instance (e.g. it was never publicly deployed), the original class id from the local instance is used. - * @param contractAddress - The address of the contract to read the class id for. - * @param instance - The local instance of the contract, used as a fallback when the node doesn't know it. - * @param aztecNode - The Aztec node to query. - * @param header - The header of the block at which to resolve the current class id. - * @returns The current class id. - */ -export async function readCurrentClassId( - contractAddress: AztecAddress, - instance: ContractInstance, - aztecNode: AztecNode, - header: BlockHeader, -) { - const nodeInstance = await aztecNode.getContract(contractAddress, await header.hash()); - // If the contract was upgraded then the node WILL know of that and return a non-undefined instance. An undefined - // result therefore means that no upgrade has happened, and therefore that the original class is the current one. - return nodeInstance?.currentContractClassId ?? instance.originalContractClassId; -} - -export async function syncScope( - contractAddress: AztecAddress, - contractStore: ContractStore, - functionToInvokeAfterSync: FunctionSelector | null, - utilityExecutor: (privateSyncCall: FunctionCall, scopes: AztecAddress[]) => Promise, - scope: AztecAddress, -) { - // Protocol contracts don't have private state to sync - if (isProtocolContract(contractAddress)) { - return; - } - - const syncStateFunctionCall = await contractStore.getFunctionCall('sync_state', [scope], contractAddress); - if (functionToInvokeAfterSync && functionToInvokeAfterSync.equals(syncStateFunctionCall.selector)) { - throw new Error( - 'Forbidden `sync_state` invocation. `sync_state` can only be invoked by PXE, manual execution can lead to inconsistencies.', - ); - } - - await utilityExecutor(syncStateFunctionCall, [scope]); -} - -/** - * Verify that the current class id of a contract obtained from AztecNode is the same as the one in contract data - * provider (i.e. PXE's own storage). - * @param contractAddress - The address of the contract to verify. - * @param aztecNode - The Aztec node to query for storage. - * @param contractStore - The contract store to fetch the local instance from. - * @param header - The header of the block at which to verify the current class id. - */ -export async function verifyCurrentClassId( - contractAddress: AztecAddress, - aztecNode: AztecNode, - contractStore: ContractStore, - header: BlockHeader, -) { - const instance = await contractStore.getContractInstance(contractAddress); - if (!instance) { - throw new Error(`No contract instance found for address ${contractAddress.toString()}`); - } - - const currentClassId = await readCurrentClassId(contractAddress, instance, aztecNode, header); - if (!instance.currentContractClassId.equals(currentClassId)) { - throw new Error( - `Contract ${contractAddress} is outdated, current class id is ${currentClassId}, local class id is ${instance.currentContractClassId}`, - ); - } -} diff --git a/yarn-project/pxe/src/debug/pxe_debug_utils.ts b/yarn-project/pxe/src/debug/pxe_debug_utils.ts index 0ab3c64bf348..69156f484cf8 100644 --- a/yarn-project/pxe/src/debug/pxe_debug_utils.ts +++ b/yarn-project/pxe/src/debug/pxe_debug_utils.ts @@ -5,8 +5,8 @@ import type { NoteDao } from '@aztec/stdlib/note'; import type { ContractOverrides } from '@aztec/stdlib/tx'; import type { BlockSynchronizer } from '../block_synchronizer/block_synchronizer.js'; +import type { ContractSyncService } from '../contract/contract_sync_service.js'; import type { ContractFunctionSimulator } from '../contract_function_simulator/contract_function_simulator.js'; -import type { ContractSyncService } from '../contract_sync/contract_sync_service.js'; import type { NotesFilter } from '../notes_filter.js'; import type { AnchorBlockStore } from '../storage/index.js'; import type { NoteStore } from '../storage/note_store/note_store.js'; diff --git a/yarn-project/pxe/src/entrypoints/server/index.ts b/yarn-project/pxe/src/entrypoints/server/index.ts index 7fac75907e37..2403e38024cb 100644 --- a/yarn-project/pxe/src/entrypoints/server/index.ts +++ b/yarn-project/pxe/src/entrypoints/server/index.ts @@ -8,4 +8,6 @@ export { NoteService } from '../../notes/note_service.js'; export { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js'; export { type PXECreationOptions } from '../pxe_creation_options.js'; export { JobCoordinator } from '../../job_coordinator/job_coordinator.js'; -export { ContractSyncService } from '../../contract_sync/contract_sync_service.js'; +export { ContractSyncService } from '../../contract/contract_sync_service.js'; +export { ContractClassService } from '../../contract/contract_class_service.js'; +export { AnchoredContractData } from '../../contract_function_simulator/anchored_contract_data.js'; diff --git a/yarn-project/pxe/src/error_enriching.ts b/yarn-project/pxe/src/error_enriching.ts index 78343df6a213..86866d0f15f1 100644 --- a/yarn-project/pxe/src/error_enriching.ts +++ b/yarn-project/pxe/src/error_enriching.ts @@ -3,7 +3,9 @@ import { resolveAssertionMessageFromRevertData, resolveOpcodeLocations } from '@ import { FunctionSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type SimulationError, isNoirCallStackUnresolved } from '@aztec/stdlib/errors'; +import type { BlockHeader } from '@aztec/stdlib/tx'; +import type { ContractClassService } from './contract/contract_class_service.js'; import type { ContractStore } from './storage/contract_store/contract_store.js'; /** @@ -11,7 +13,13 @@ import type { ContractStore } from './storage/contract_store/contract_store.js'; * can be found in the PXE database * @param err - The error to enrich. */ -export async function enrichSimulationError(err: SimulationError, contractStore: ContractStore, logger: Logger) { +export async function enrichSimulationError( + err: SimulationError, + contractStore: ContractStore, + contractClassService: ContractClassService, + anchorHeader: BlockHeader, + logger: Logger, +) { // Maps contract addresses to the set of function selectors that were in error. // Map and Set do reference equality for their keys instead of value equality, so we store the string // representation to get e.g. different contract address objects with the same address value to match. @@ -30,13 +38,14 @@ export async function enrichSimulationError(err: SimulationError, contractStore: await Promise.all( [...mentionedFunctions.entries()].map(async ([contractAddress, fnSelectors]) => { const parsedContractAddress = AztecAddress.fromStringUnsafe(contractAddress); - const contract = await contractStore.getContract(parsedContractAddress); - if (contract) { - err.enrichWithContractName(parsedContractAddress, contract.name); + const classId = await contractClassService.getCurrentClassId(parsedContractAddress, anchorHeader); + const artifact = classId && (await contractStore.getContractArtifact(classId)); + if (artifact) { + err.enrichWithContractName(parsedContractAddress, artifact.name); // Map from function selector to function name. It uses a stringified key for the same reason as mentionedFunctions. const selectorToNameMap: Map = new Map(); await Promise.all( - contract.functions.map(async fn => { + artifact.functions.map(async fn => { const selector = await FunctionSelector.fromNameAndParameters(fn); selectorToNameMap.set(selector.toString(), fn.name); }), @@ -51,7 +60,7 @@ export async function enrichSimulationError(err: SimulationError, contractStore: ); } else { logger.warn( - `Could not find function artifact in contract ${contract.name} for function '${fnSelector}' when enriching error callstack`, + `Could not find function artifact in contract ${artifact.name} for function '${fnSelector}' when enriching error callstack`, ); } } @@ -64,7 +73,13 @@ export async function enrichSimulationError(err: SimulationError, contractStore: ); } -export async function enrichPublicSimulationError(err: SimulationError, contractStore: ContractStore, logger: Logger) { +export async function enrichPublicSimulationError( + err: SimulationError, + contractStore: ContractStore, + contractClassService: ContractClassService, + anchorHeader: BlockHeader, + logger: Logger, +) { const callStack = err.getCallStack(); const originalFailingFunction = callStack[callStack.length - 1]; @@ -72,7 +87,8 @@ export async function enrichPublicSimulationError(err: SimulationError, contract throw new Error(`Original failing function not found when enriching public simulation, missing callstack`); } - const artifact = await contractStore.getPublicFunctionArtifact(originalFailingFunction.contractAddress); + const classId = await contractClassService.getCurrentClassId(originalFailingFunction.contractAddress, anchorHeader); + const artifact = classId && (await contractStore.getPublicFunctionArtifact(classId)); if (!artifact) { throw new Error( `Artifact not found when enriching public simulation error. Contract address: ${originalFailingFunction.contractAddress}.`, @@ -84,7 +100,7 @@ export async function enrichPublicSimulationError(err: SimulationError, contract err.setOriginalMessage(err.getOriginalMessage() + `${assertionMessage}`); } - const debugInfo = await contractStore.getPublicFunctionDebugMetadata(originalFailingFunction.contractAddress); + const debugInfo = await contractStore.getPublicFunctionDebugMetadata(classId); const noirCallStack = err.getNoirCallStack(); if (debugInfo) { @@ -102,6 +118,6 @@ export async function enrichPublicSimulationError(err: SimulationError, contract ); } } - await enrichSimulationError(err, contractStore, logger); + await enrichSimulationError(err, contractStore, contractClassService, anchorHeader, logger); } } diff --git a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.test.ts b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.test.ts index 5ebbbe505cb7..62dbc74e585a 100644 --- a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.test.ts +++ b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.test.ts @@ -12,6 +12,7 @@ import { BlockHeader } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; +import type { ContractClassService } from '../contract/contract_class_service.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; import { PrivateKernelOracle } from './private_kernel_oracle.js'; @@ -21,7 +22,13 @@ describe('PrivateKernelOracle', () => { beforeEach(() => { node = mock(); - oracle = new PrivateKernelOracle(mock(), mock(), node, BlockHeader.empty()); + oracle = new PrivateKernelOracle( + mock(), + mock(), + mock(), + node, + BlockHeader.empty(), + ); }); describe('getUpdatedClassIdHints', () => { diff --git a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts index cce48341341e..80611037d46f 100644 --- a/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts +++ b/yarn-project/pxe/src/private_kernel/private_kernel_oracle.ts @@ -22,30 +22,45 @@ import type { NullifierMembershipWitness } from '@aztec/stdlib/trees'; import type { BlockHeader } from '@aztec/stdlib/tx'; import type { VerificationKeyAsFields } from '@aztec/stdlib/vks'; +import type { ContractClassService } from '../contract/contract_class_service.js'; +import { AnchoredContractData } from '../contract_function_simulator/anchored_contract_data.js'; import type { ContractStore } from '../storage/contract_store/contract_store.js'; /** * Provides functionality needed by the private kernel for interacting with our state trees. */ export class PrivateKernelOracle { + private readonly anchoredContractData: AnchoredContractData; + constructor( private contractStore: ContractStore, + contractClassService: ContractClassService, private keyStore: KeyStore, private node: AztecNode, private blockHeader: BlockHeader, - ) {} + ) { + // Kernels never use contract overrides (those are confined to simulations, which skip proving), so this view is + // built without them. + this.anchoredContractData = new AnchoredContractData(contractStore, contractClassService, blockHeader); + } /** Retrieves the preimage of a contract address from the registered contract instances db. */ public async getContractAddressPreimage( address: AztecAddress, ): Promise { - const instance = await this.contractStore.getContractInstance(address); + const instance = await this.anchoredContractData.getContractInstance(address); if (!instance) { throw new Error(`Contract instance not found when getting address preimage. Contract address: ${address}.`); } + // Local instance existence was checked above, so resolution below cannot come back empty. + const currentContractClassId = await this.anchoredContractData.getCurrentClassId(address); + if (!currentContractClassId) { + throw new Error(`Could not resolve the current class id for registered contract ${address}.`); + } return { saltedInitializationHash: await computeSaltedInitializationHash(instance), ...instance, + currentContractClassId, }; } @@ -114,8 +129,12 @@ export class PrivateKernelOracle { } /** Use debug data to get the function name corresponding to a selector. */ - public getDebugFunctionName(contractAddress: AztecAddress, selector: FunctionSelector): Promise { - return this.contractStore.getDebugFunctionName(contractAddress, selector); + public async getDebugFunctionName( + contractAddress: AztecAddress, + selector: FunctionSelector, + ): Promise { + const classId = await this.anchoredContractData.getCurrentClassId(contractAddress); + return classId ? this.contractStore.getDebugFunctionName(classId, selector) : undefined; } /** diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index cf14cbeaad43..7deac424b867 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -23,7 +23,11 @@ import { GENESIS_CHECKPOINT_HEADER_HASH, } from '@aztec/stdlib/block'; import { emptyChainConfig } from '@aztec/stdlib/config'; -import { CompleteAddress, getContractClassFromArtifact } from '@aztec/stdlib/contract'; +import { + CompleteAddress, + SerializableContractInstancePreimage, + getContractClassFromArtifact, +} from '@aztec/stdlib/contract'; import type { AztecNode, AztecNodeDebug, BlockResponse } from '@aztec/stdlib/interfaces/client'; import { randomContractArtifact, @@ -212,7 +216,7 @@ describe('PXE', () => { it('successfully adds a contract', async () => { const contracts = await Promise.all([randomDeployedContract(), randomDeployedContract()]); for (const contract of contracts) { - await pxe.registerContract(contract); + await pxe.registerContract(contract.instance); } const expectedContractAddresses = contracts.map(contract => contract.instance.address); @@ -223,7 +227,9 @@ describe('PXE', () => { it('preloads the standard multi-call entrypoint on creation', async () => { const { instance: expectedInstance, artifact: expectedArtifact } = await getStandardMultiCallEntrypoint(); const instance = await pxe.getContractInstance(STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS); - expect(instance).toEqual(expectedInstance); + expect(instance).toEqual( + new SerializableContractInstancePreimage(expectedInstance).withAddress(expectedInstance.address), + ); const artifact = await pxe.getContractArtifact(expectedInstance.currentContractClassId); expect(artifact).toEqual(expectedArtifact); @@ -238,47 +244,32 @@ describe('PXE', () => { await pxe.registerContractClass(artifact); expect(await pxe.getContractArtifact(contractClassId)).toEqual(artifact); - await pxe.registerContract({ instance }); - expect(await pxe.getContractInstance(instance.address)).toEqual(instance); - }); - - it('refuses to register a class with a mismatched address', async () => { - const artifact = randomContractArtifact(); - const contractClass = await getContractClassFromArtifact(artifact); - const contractClassId = contractClass.id; - const instance = await randomContractInstanceWithAddress({ contractClassId }); - await expect( - pxe.registerContract({ - instance: { - ...instance, - address: await AztecAddress.random(), - }, - artifact, - }), - ).rejects.toThrow(/Added a contract in which the address does not match the contract instance./); + await pxe.registerContract(instance); + expect(await pxe.getContractInstance(instance.address)).toEqual( + new SerializableContractInstancePreimage(instance).withAddress(instance.address), + ); }); - it('refuses to register a contract with a class that has not been registered', async () => { + it('registers an instance and returns its derived address without checking the class is present', async () => { + // Registration performs no validation and ignores any caller-supplied address: it derives the address from the + // preimage and stores the instance. A missing class only surfaces when the contract is later simulated. const instance = await randomContractInstanceWithAddress(); - await expect(pxe.registerContract({ instance })).rejects.toThrow(/Artifact not found when registering an instance/); + await expect(pxe.registerContract(instance)).resolves.toEqual(instance.address); + expect(await pxe.getContractInstance(instance.address)).toEqual( + new SerializableContractInstancePreimage(instance).withAddress(instance.address), + ); }); - it('refuses to register a contract with an artifact with mismatching class id', async () => { + it('does not call registerContractFunctionSignatures for classes without public functions', async () => { const artifact = randomContractArtifact(); - const instance = await randomContractInstanceWithAddress(); - await expect(pxe.registerContract({ instance, artifact })).rejects.toThrow(/Artifact does not match/i); - }); - - it('does not call registerContractFunctionSignatures for contracts without public functions', async () => { - const { artifact, instance } = await randomDeployedContract(); nodeDebug.registerContractFunctionSignatures.mockClear(); - await pxe.registerContract({ artifact, instance }); + await pxe.registerContractClass(artifact); expect(nodeDebug.registerContractFunctionSignatures).not.toHaveBeenCalled(); }); - it('calls registerContractFunctionSignatures for contracts with public functions', async () => { + it('calls registerContractFunctionSignatures for classes with public functions', async () => { const artifact = randomContractArtifact(); artifact.functions = [ { @@ -294,11 +285,9 @@ describe('PXE', () => { debugSymbols: '', }, ]; - const contractClass = await getContractClassFromArtifact(artifact); - const instance = await randomContractInstanceWithAddress({ contractClassId: contractClass.id }); nodeDebug.registerContractFunctionSignatures.mockClear(); - await pxe.registerContract({ artifact, instance }); + await pxe.registerContractClass(artifact); expect(nodeDebug.registerContractFunctionSignatures).toHaveBeenCalledWith(['my_public_fn()']); }); @@ -360,7 +349,7 @@ describe('PXE', () => { }); // Read when PXE resolves the current class id of a contract instance at the anchor block. Returning undefined - // makes readCurrentClassId fall back to the local instance's originalContractClassId. + // makes the contract class service fall back to the local instance's originalContractClassId. node.getContract.mockResolvedValue(undefined); // Used to sync private logs from the node - the return array needs to have the same length as the number of tags @@ -372,9 +361,7 @@ describe('PXE', () => { const contractClass = await getContractClassFromArtifact(TestContractArtifact); const contractClassId = contractClass.id; const contractInstance = await randomContractInstanceWithAddress({ contractClassId }); - await pxe.registerContract({ - instance: contractInstance, - }); + await pxe.registerContract(contractInstance); contractAddress = contractInstance.address; eventSelector = EventSelector.random(); diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 74ff97b6da06..a1eef7cdd11a 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -13,6 +13,7 @@ import { type ContractArtifact, EventSelector, FunctionCall, + type FunctionSelector, FunctionType, decodeFunctionSignature, } from '@aztec/stdlib/abi'; @@ -21,10 +22,11 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { GENESIS_BLOCK_HEADER_HASH, type L2TipsProvider } from '@aztec/stdlib/block'; import { CompleteAddress, + type ContractInstancePreimage, + type ContractInstancePreimageWithAddress, type ContractInstanceWithAddress, type PartialAddress, computeContractAddressFromInstance, - getContractClassFromArtifact, } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import type { AztecNode, AztecNodeDebug, PrivateKernelProver } from '@aztec/stdlib/interfaces/client'; @@ -56,15 +58,14 @@ import { inspect } from 'util'; import { BlockSynchronizer } from './block_synchronizer/index.js'; import type { PXEConfig } from './config/index.js'; +import { ContractClassService } from './contract/contract_class_service.js'; +import { ContractSyncService } from './contract/contract_sync_service.js'; import { BenchmarkedNodeFactory } from './contract_function_simulator/benchmarked_node.js'; import { ContractFunctionSimulator, generateSimulatedProvingResult, } from './contract_function_simulator/contract_function_simulator.js'; -import { ProxiedContractStoreFactory } from './contract_function_simulator/proxied_contract_data_source.js'; import { displayDebugLogs } from './contract_logging.js'; -import { ContractSyncService } from './contract_sync/contract_sync_service.js'; -import { readCurrentClassId } from './contract_sync/helpers.js'; import { PXEDebugUtils } from './debug/pxe_debug_utils.js'; import { enrichPublicSimulationError, enrichSimulationError } from './error_enriching.js'; import { PrivateEventFilterValidator } from './events/private_event_filter_validator.js'; @@ -225,6 +226,7 @@ export class PXE { private addressStore: AddressStore, private privateEventStore: PrivateEventStore, private contractSyncService: ContractSyncService, + private contractClassService: ContractClassService, private txResolver: TxResolverService, private l2TipsStore: L2TipsProvider, private simulator: CircuitSimulator, @@ -293,9 +295,11 @@ export class PXE { l2TipsStore, factStore, } = openPxeStores(store, initialBlockHash); + const contractClassService = new ContractClassService(node, contractStore); const contractSyncService = new ContractSyncService( node, contractStore, + contractClassService, noteStore, createLogger('pxe:contract_sync', bindings), ); @@ -310,6 +314,7 @@ export class PXE { factStore, l2TipsStore, contractSyncService, + contractClassService, config, bindings, ); @@ -346,6 +351,7 @@ export class PXE { addressStore, privateEventStore, contractSyncService, + contractClassService, txResolver, l2TipsStore, simulator, @@ -377,10 +383,10 @@ export class PXE { // Internal methods #getSimulatorForTx(overrides?: { contracts?: ContractOverrides }) { - const proxyContractStore = ProxiedContractStoreFactory.create(this.contractStore, overrides?.contracts); - return new ContractFunctionSimulator({ - contractStore: proxyContractStore, + contractStore: this.contractStore, + contractClassService: this.contractClassService, + overrides: overrides?.contracts, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -399,6 +405,30 @@ export class PXE { }); } + /** Best-effort debug function name for the class `address` runs at `anchorBlockHeader`. Used for log display only. */ + async #getDebugFunctionName( + address: AztecAddress, + selector: FunctionSelector, + anchorBlockHeader: BlockHeader, + ): Promise { + try { + const classId = await this.contractClassService.getCurrentClassId(address, anchorBlockHeader); + return classId ? await this.contractStore.getDebugFunctionName(classId, selector) : `${address}:${selector}`; + } catch { + return `${address}:${selector}`; + } + } + + /** Best-effort debug contract name for the class `address` runs at `anchorBlockHeader`. Used for log display only. */ + async #getDebugContractName(address: AztecAddress, anchorBlockHeader: BlockHeader): Promise { + try { + const classId = await this.contractClassService.getCurrentClassId(address, anchorBlockHeader); + return classId ? await this.contractStore.getDebugContractName(classId) : undefined; + } catch { + return undefined; + } + } + #contextualizeError(err: Error, ...context: string[]): Error { let contextStr = ''; if (context.length > 0) { @@ -462,7 +492,14 @@ export class PXE { async #registerPreloadedContracts() { const contracts = await this.preloadedContractsProvider.getPreloadedContracts(); - await Promise.all(contracts.map(({ instance, artifact }) => this.registerContract({ instance, artifact }))); + await Promise.all( + contracts.map(async ({ instance, artifact }) => { + if (artifact) { + await this.registerContractClass(artifact); + } + await this.registerContract(instance); + }), + ); this.log.verbose(`Registered preloaded contracts in pxe`, { contracts: contracts.map(({ instance }) => instance.address.toString()), }); @@ -508,7 +545,7 @@ export class PXE { return result; } catch (err) { if (err instanceof SimulationError) { - await enrichSimulationError(err, this.contractStore, this.log); + await enrichSimulationError(err, this.contractStore, this.contractClassService, anchorBlockHeader, this.log); } throw err; } @@ -543,7 +580,8 @@ export class PXE { return { result, offchainEffects }; } catch (err) { if (err instanceof SimulationError) { - await enrichSimulationError(err, this.contractStore, this.log); + const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader(); + await enrichSimulationError(err, this.contractStore, this.contractClassService, anchorBlockHeader, this.log); } throw err; } @@ -567,7 +605,14 @@ export class PXE { } catch (err) { if (err instanceof SimulationError) { try { - await enrichPublicSimulationError(err, this.contractStore, this.log); + const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader(); + await enrichPublicSimulationError( + err, + this.contractStore, + this.contractClassService, + anchorBlockHeader, + this.log, + ); } catch (enrichErr) { this.log.error(`Failed to enrich public simulation error: ${enrichErr}`); } @@ -594,7 +639,13 @@ export class PXE { anchorBlockHeader: BlockHeader, config: PrivateKernelExecutionProverConfig, ): Promise> { - const kernelOracle = new PrivateKernelOracle(this.contractStore, this.keyStore, this.node, anchorBlockHeader); + const kernelOracle = new PrivateKernelOracle( + this.contractStore, + this.contractClassService, + this.keyStore, + this.node, + anchorBlockHeader, + ); const kernelTraceProver = new PrivateKernelExecutionProver( kernelOracle, proofCreator, @@ -638,11 +689,11 @@ export class PXE { } /** - * Returns the contract instance for a given address, if it's registered in the PXE. + * Returns the address preimage of the contract instance at a given address, if it's registered in the PXE. * @param address - The contract address. - * @returns The contract instance if found, undefined otherwise. + * @returns The contract instance address preimage if found, undefined otherwise. */ - public getContractInstance(address: AztecAddress): Promise { + public getContractInstance(address: AztecAddress): Promise { return this.contractStore.getContractInstance(address); } @@ -854,98 +905,33 @@ export class PXE { */ public async registerContractClass(artifact: ContractArtifact): Promise { const contractClassId = await this.contractStore.addContractArtifact(artifact); + // Publishing the artifact's public function signatures to the node is part of "registering a class", so that + // node-side debugging works regardless of which entrypoint (registerContractClass or registerContract) was used. + const publicFunctionSignatures = artifact.functions + .filter(fn => fn.functionType === FunctionType.PUBLIC) + .map(fn => decodeFunctionSignature(fn.name, fn.parameters)); + if (publicFunctionSignatures.length > 0) { + await this.nodeDebug?.registerContractFunctionSignatures(publicFunctionSignatures); + } this.log.info(`Added contract class ${artifact.name} with id ${contractClassId}`); } /** - * Adds deployed contracts to the PXE. Deployed contract information is used to access the - * contract code when simulating local transactions. This is automatically called by aztec.js when - * deploying a contract. Dapps that wish to interact with contracts already deployed should register - * these contracts in their users' PXE through this method. + * Registers a deployed contract instance so its private state can be synced and its functions simulated. The + * artifact for the class the instance runs must be registered separately via {@link registerContractClass}, before + * or after this call; registration performs no validation, so a missing or mismatched artifact only surfaces when + * the contract is later simulated. This is automatically called by aztec.js when deploying a contract. * - * @param contract - A contract instance to register, with an optional artifact which can be omitted if the contract class has already been registered. - */ - public async registerContract(contract: { instance: ContractInstanceWithAddress; artifact?: ContractArtifact }) { - const { instance } = contract; - let { artifact } = contract; - - if (artifact) { - // If the user provides an artifact, validate it against the expected class id and register it - const contractClass = await getContractClassFromArtifact(artifact); - if (!contractClass.id.equals(instance.currentContractClassId)) { - throw new Error( - `Artifact does not match expected class id (computed ${contractClass.id} but instance refers to ${instance.currentContractClassId})`, - ); - } - const computedAddress = await computeContractAddressFromInstance(instance); - if (!computedAddress.equals(instance.address)) { - throw new Error('Added a contract in which the address does not match the contract instance.'); - } - - await this.contractStore.addContractArtifact(artifact, contractClass); - - const publicFunctionSignatures = artifact.functions - .filter(fn => fn.functionType === FunctionType.PUBLIC) - .map(fn => decodeFunctionSignature(fn.name, fn.parameters)); - if (publicFunctionSignatures.length > 0) { - await this.nodeDebug?.registerContractFunctionSignatures(publicFunctionSignatures); - } - } else { - // Otherwise, make sure there is an artifact already registered for that class id - artifact = await this.contractStore.getContractArtifact(instance.currentContractClassId); - if (!artifact) { - throw new Error( - `Artifact not found when registering an instance. Contract class: ${instance.currentContractClassId}.`, - ); - } - } - - await this.contractStore.addContractInstance(instance); - this.log.info( - `Added contract ${artifact.name} at ${instance.address.toString()} with class ${instance.currentContractClassId}`, - ); - } - - /** - * Updates a deployed contract in the PXE. This is used to update the contract artifact when - * an update has happened, so the new code can be used in the simulation of local transactions. - * This is called by aztec.js when instantiating a contract in a given address with a mismatching artifact. - * @param contractAddress - The address of the contract to update. - * @param artifact - The updated artifact for the contract. - * @throws If the artifact's contract class is not found in the PXE or if the contract class is different from - * the current one (current one from the point of view of the node to which the PXE is connected). + * @param instance - The address preimage of the contract instance to register. The address is derived from it. + * @returns The address of the registered instance. */ - public updateContract(contractAddress: AztecAddress, artifact: ContractArtifact): Promise { - // We disable concurrently updating contracts to avoid concurrently syncing with the node, or changing a contract's - // class while we're simulating it. + public registerContract(instance: ContractInstancePreimage): Promise { + // Run inside the job queue so we can't race a concurrent simulation while writing the instance to the store. return this.#putInJobQueue(async () => { - const currentInstance = await this.contractStore.getContractInstance(contractAddress); - if (!currentInstance) { - throw new Error(`Instance not found when updating a contract. Contract address: ${contractAddress}.`); - } - const contractClass = await getContractClassFromArtifact(artifact); - await this.#maybeSync(); - - const header = await this.anchorBlockStore.getBlockHeader(); - - const currentClassId = await readCurrentClassId(contractAddress, currentInstance, this.node, header); - if (!contractClass.id.equals(currentClassId)) { - throw new Error('Could not update contract to a class different from the current one.'); - } - - const publicFunctionSignatures = artifact.functions - .filter(fn => fn.functionType === FunctionType.PUBLIC) - .map(fn => decodeFunctionSignature(fn.name, fn.parameters)); - if (publicFunctionSignatures.length > 0) { - await this.nodeDebug?.registerContractFunctionSignatures(publicFunctionSignatures); - } - - currentInstance.currentContractClassId = contractClass.id; - await Promise.all([ - this.contractStore.addContractArtifact(artifact, contractClass), - this.contractStore.addContractInstance(currentInstance), - ]); - this.log.info(`Updated contract ${artifact.name} at ${contractAddress.toString()} to class ${contractClass.id}`); + const address = await computeContractAddressFromInstance(instance); + await this.contractStore.addContractInstance({ ...instance, address }); + this.log.info(`Added contract at ${address}`, { address }); + return address; }); } @@ -1207,7 +1193,7 @@ export class PXE { if (skipKernels) { ({ publicInputs, executionSteps } = await generateSimulatedProvingResult( privateExecutionResult, - (addr, sel) => this.contractStore.getDebugFunctionName(addr, sel), + (addr, sel) => this.#getDebugFunctionName(addr, sel, anchorBlockHeader), this.node, )); } else { @@ -1234,7 +1220,7 @@ export class PXE { publicOutput = await this.#simulatePublicCalls(simulatedTx, skipFeeEnforcement, overrides); publicSimulationTime = publicSimulationTimer.ms(); if (publicOutput?.debugLogs?.length) { - await displayDebugLogs(publicOutput.debugLogs, addr => this.contractStore.getDebugContractName(addr)); + await displayDebugLogs(publicOutput.debugLogs, addr => this.#getDebugContractName(addr, anchorBlockHeader)); } } diff --git a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/ContractStore.json b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/ContractStore.json index 8635b76c8b38..5e03537306b8 100644 --- a/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/ContractStore.json +++ b/yarn-project/pxe/src/storage/backwards_compatibility_tests/__snapshots__/ContractStore.json @@ -22,7 +22,7 @@ "contracts_instances": [ { "key": "utf8:0x0000000000000000000000000000000000000000000000000000000000000065", - "value": "020000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000004f00000000000000000000000000000000000000000000000000000000000000530000000000000000000000000000000000000000000000000000000000000059000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000004300000000000000000000000000000000000000000000000000000000000000470000000000000000000000000000000000000000000000000000000000000049" + "value": "020000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000004f0000000000000000000000000000000000000000000000000000000000000059000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000004300000000000000000000000000000000000000000000000000000000000000470000000000000000000000000000000000000000000000000000000000000049" } ] } diff --git a/yarn-project/pxe/src/storage/contract_store/contract_store.test.ts b/yarn-project/pxe/src/storage/contract_store/contract_store.test.ts index 4b5253b863b6..c4d36bdfb243 100644 --- a/yarn-project/pxe/src/storage/contract_store/contract_store.test.ts +++ b/yarn-project/pxe/src/storage/contract_store/contract_store.test.ts @@ -1,9 +1,13 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { BenchmarkingContractArtifact } from '@aztec/noir-test-contracts.js/Benchmarking'; import { TestContractArtifact } from '@aztec/noir-test-contracts.js/Test'; -import { FunctionType } from '@aztec/stdlib/abi'; +import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { SerializableContractInstance, getContractClassFromArtifact } from '@aztec/stdlib/contract'; +import { + SerializableContractInstance, + SerializableContractInstancePreimage, + getContractClassFromArtifact, +} from '@aztec/stdlib/contract'; import { jest } from '@jest/globals'; @@ -35,11 +39,12 @@ describe('ContractStore', () => { ); }); - it('stores a contract instance', async () => { + it('stores a contract instance as its address preimage', async () => { const address = await AztecAddress.random(); const instance = (await SerializableContractInstance.random()).withAddress(address); await contractStore.addContractInstance(instance); - await expect(contractStore.getContractInstance(address)).resolves.toEqual(instance); + const expected = new SerializableContractInstancePreimage(instance).withAddress(address); + await expect(contractStore.getContractInstance(address)).resolves.toEqual(expected); }); it('reconstructs contract class with correct preimage fields', async () => { @@ -61,6 +66,41 @@ describe('ContractStore', () => { } }); + describe('function artifact resolution', () => { + const artifact = BenchmarkingContractArtifact; + + it('returns undefined when the class artifact is not registered', async () => { + const classId = (await getContractClassFromArtifact(artifact)).id; + const selector = await FunctionSelector.fromSignature('not_a_real_function()'); + + await expect(contractStore.getFunctionArtifact(classId, selector)).resolves.toBeUndefined(); + await expect(contractStore.getFunctionArtifactWithDebugMetadata(classId, selector)).resolves.toBeUndefined(); + }); + + it('throws when the selector is absent from a registered artifact', async () => { + const classId = await contractStore.addContractArtifact(artifact); + const missingSelector = await FunctionSelector.fromSignature('not_a_real_function()'); + // Inconsistency: the artifact is present but lacks the selector, so the registered artifact does not match the + // resolved class id. That is not a normal "not found", so it throws rather than returning undefined. + await expect(contractStore.getFunctionArtifact(classId, missingSelector)).rejects.toThrow( + 'does not match the class id', + ); + await expect(contractStore.getFunctionArtifactWithDebugMetadata(classId, missingSelector)).rejects.toThrow( + 'does not match the class id', + ); + }); + + it('returns the function artifact when the selector is present', async () => { + const classId = await contractStore.addContractArtifact(artifact); + const fn = artifact.functions.find(f => f.functionType === FunctionType.PRIVATE)!; + const selector = await FunctionSelector.fromNameAndParameters(fn.name, fn.parameters); + await expect(contractStore.getFunctionArtifact(classId, selector)).resolves.toMatchObject({ + name: fn.name, + contractName: artifact.name, + }); + }); + }); + it('skips KV write on cache hit', async () => { const kvStore = await openTmpStore('contract_store_cache_test'); const store = new ContractStore(kvStore); diff --git a/yarn-project/pxe/src/storage/contract_store/contract_store.ts b/yarn-project/pxe/src/storage/contract_store/contract_store.ts index 5b3e9efdf97f..f58d88e43e45 100644 --- a/yarn-project/pxe/src/storage/contract_store/contract_store.ts +++ b/yarn-project/pxe/src/storage/contract_store/contract_store.ts @@ -6,7 +6,6 @@ import type { MembershipWitness } from '@aztec/foundation/trees'; import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store'; import { type ContractArtifact, - type FunctionAbi, type FunctionArtifactWithContractName, FunctionCall, type FunctionDebugMetadata, @@ -23,8 +22,8 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractClassIdPreimage, type ContractClassWithId, - type ContractInstanceWithAddress, - SerializableContractInstance, + type ContractInstancePreimageWithAddress, + SerializableContractInstancePreimage, getContractClassFromArtifact, } from '@aztec/stdlib/contract'; @@ -112,9 +111,6 @@ export class ContractStore { // TODO: Update it to be LRU cache so that it doesn't keep all the data all the time. #contractArtifactCache: Map = new Map(); - /** Map from contract address to contract class id (avoids KV round-trip on hot path). */ - #contractClassIdMap: Map = new Map(); - #store: AztecAsyncKVStore; #contractArtifacts: AztecAsyncMap; #contractClassData: AztecAsyncMap; @@ -131,9 +127,10 @@ export class ContractStore { /** * Registers a new contract artifact and its corresponding class data. - * IMPORTANT: This method does not verify that the provided artifact matches the class data or that the class id matches the artifact. - * It is the caller's responsibility to ensure the consistency and correctness of the provided data. - * This is done to avoid redundant, expensive contract class computations + * + * IMPORTANT: This method does not verify that the provided artifact matches the class data or that the class id + * matches the artifact. It is the caller's responsibility to ensure the consistency and correctness of the provided + * data. This is done to avoid redundant, expensive contract class computations. */ public async addContractArtifact( contract: ContractArtifact, @@ -168,31 +165,17 @@ export class ContractStore { return contractClass.id; } - async addContractInstance(contract: ContractInstanceWithAddress): Promise { + async addContractInstance(contract: ContractInstancePreimageWithAddress): Promise { await this.#store.transactionAsync(async () => { await this.#contractInstances.set( contract.address.toString(), - new SerializableContractInstance(contract).toBuffer(), + new SerializableContractInstancePreimage(contract).toBuffer(), ); }); - - this.#contractClassIdMap.set(contract.address.toString(), contract.currentContractClassId); } // Private getters - async #getContractClassId(contractAddress: AztecAddress): Promise { - const key = contractAddress.toString(); - if (!this.#contractClassIdMap.has(key)) { - const instance = await this.getContractInstance(contractAddress); - if (!instance) { - return; - } - this.#contractClassIdMap.set(key, instance.currentContractClassId); - } - return this.#contractClassIdMap.get(key); - } - async #getPrivateFunctionTreeForClassId(classId: Fr): Promise { if (!this.#privateFunctionTrees.has(classId.toString())) { const artifact = await this.getContractArtifact(classId); @@ -205,11 +188,6 @@ export class ContractStore { return this.#privateFunctionTrees.get(classId.toString())!; } - async #getArtifactByAddress(contractAddress: AztecAddress): Promise { - const classId = await this.#getContractClassId(contractAddress); - return classId && this.getContractArtifact(classId); - } - // Public getters getContractsAddresses(): Promise { @@ -219,11 +197,11 @@ export class ContractStore { }); } - /** Returns a contract instance for a given address. */ - public getContractInstance(contractAddress: AztecAddress): Promise { + /** Returns the address preimage of a given address. */ + public getContractInstance(contractAddress: AztecAddress): Promise { return this.#store.transactionAsync(async () => { const contract = await this.#contractInstances.getAsync(contractAddress.toString()); - return contract && SerializableContractInstance.fromBuffer(contract).withAddress(contractAddress); + return contract && SerializableContractInstancePreimage.fromBuffer(contract).withAddress(contractAddress); }); } @@ -262,82 +240,69 @@ export class ContractStore { return { ...classData, packedBytecode }; } - public async getContract( - address: AztecAddress, - ): Promise<(ContractInstanceWithAddress & ContractArtifact) | undefined> { - const instance = await this.getContractInstance(address); - if (!instance) { - return; - } - const artifact = await this.getContractArtifact(instance.currentContractClassId); - if (!artifact) { - return; - } - return { ...instance, ...artifact }; - } - /** - * Retrieves the artifact of a specified function within a given contract. + * Retrieves the artifact of a specified function within a given contract class. * - * @param contractAddress - The AztecAddress representing the contract containing the function. + * @param contractClassId - The id of the contract class containing the function. * @param selector - The function selector. - * @returns The corresponding function's artifact as an object. + * @returns The corresponding function's artifact as an object, or `undefined` if the class is not registered. */ public async getFunctionArtifact( - contractAddress: AztecAddress, + contractClassId: Fr, selector: FunctionSelector, ): Promise { - const artifact = await this.#getArtifactByAddress(contractAddress); + const artifact = await this.getContractArtifact(contractClassId); if (!artifact) { return undefined; } - const fn = await findFunctionArtifactBySelector(artifact, selector); - return fn && { ...fn, contractName: artifact.name }; + const fn = assertSelectorInArtifact(artifact, await findFunctionArtifactBySelector(artifact, selector), { + contractClassId, + selector, + }); + return { ...fn, contractName: artifact.name }; } + /** + * Same as {@link getFunctionArtifact} but with debug metadata attached. Returns `undefined` when the class artifact + * is not registered. + */ public async getFunctionArtifactWithDebugMetadata( - contractAddress: AztecAddress, + contractClassId: Fr, selector: FunctionSelector, - ): Promise { - const artifact = await this.getFunctionArtifact(contractAddress, selector); + ): Promise { + const artifact = await this.getContractArtifact(contractClassId); if (!artifact) { - throw new Error(`Function artifact not found for contract ${contractAddress} and selector ${selector}.`); + return undefined; } - const debug = await this.getFunctionDebugMetadata(contractAddress, selector); + const fn = assertSelectorInArtifact(artifact, await findFunctionArtifactBySelector(artifact, selector), { + contractClassId, + selector, + }); return { - ...artifact, - debug, + ...fn, + contractName: artifact.name, + debug: getFunctionDebugMetadata(artifact, fn), }; } - public async getPublicFunctionArtifact( - contractAddress: AztecAddress, - ): Promise { - const artifact = await this.#getArtifactByAddress(contractAddress); + public async getPublicFunctionArtifact(contractClassId: Fr): Promise { + const artifact = await this.getContractArtifact(contractClassId); const fn = artifact && artifact.functions.find(f => f.functionType === FunctionType.PUBLIC); return fn && { ...fn, contractName: artifact.name }; } - public async getFunctionAbi( - contractAddress: AztecAddress, - selector: FunctionSelector, - ): Promise { - const artifact = await this.#getArtifactByAddress(contractAddress); - return artifact && (await findFunctionAbiBySelector(artifact, selector)); - } - /** - * Retrieves the debug metadata of a specified function within a given contract. + * Retrieves the debug metadata of a specified function within a given contract class. * - * @param contractAddress - The AztecAddress representing the contract containing the function. + * @param contractClassId - The id of the contract class containing the function. * @param selector - The function selector. * @returns The corresponding function's debug metadata, or undefined. */ public async getFunctionDebugMetadata( - contractAddress: AztecAddress, + contractClassId: Fr, selector: FunctionSelector, ): Promise { - const artifact = await this.#getArtifactByAddress(contractAddress); + const artifact = await this.getContractArtifact(contractClassId); if (!artifact) { return undefined; } @@ -345,10 +310,8 @@ export class ContractStore { return fn && getFunctionDebugMetadata(artifact, fn); } - public async getPublicFunctionDebugMetadata( - contractAddress: AztecAddress, - ): Promise { - const artifact = await this.#getArtifactByAddress(contractAddress); + public async getPublicFunctionDebugMetadata(contractClassId: Fr): Promise { + const artifact = await this.getContractArtifact(contractClassId); const fn = artifact && artifact.functions.find(f => f.functionType === FunctionType.PUBLIC); return fn && getFunctionDebugMetadata(artifact, fn); } @@ -368,28 +331,34 @@ export class ContractStore { return tree?.getFunctionMembershipWitness(selector); } - public async getDebugContractName(contractAddress: AztecAddress) { - const artifact = await this.#getArtifactByAddress(contractAddress); + public async getDebugContractName(contractClassId: Fr) { + const artifact = await this.getContractArtifact(contractClassId); return artifact?.name; } - public async getDebugFunctionName(contractAddress: AztecAddress, selector: FunctionSelector) { - const artifact = await this.#getArtifactByAddress(contractAddress); + public async getDebugFunctionName(contractClassId: Fr, selector: FunctionSelector) { + const artifact = await this.getContractArtifact(contractClassId); const fn = artifact && (await findFunctionAbiBySelector(artifact, selector)); - return `${artifact?.name ?? contractAddress}:${fn?.name ?? selector}`; + return `${artifact?.name ?? contractClassId}:${fn?.name ?? selector}`; } - public async getFunctionCall(functionName: string, args: any[], to: AztecAddress): Promise { - const contract = await this.getContract(to); - if (!contract) { + public async getFunctionCall( + functionName: string, + args: any[], + to: AztecAddress, + contractClassId: Fr, + ): Promise { + const artifact = await this.getContractArtifact(contractClassId); + if (!artifact) { throw new Error( - `Unknown contract ${to}: register it by calling wallet.registerContract(...).\nSee docs for context: https://docs.aztec.network/errors/14`, + `No artifact registered for contract class ${contractClassId} (contract ${to}): register it by calling ` + + `wallet.registerContract(...).\nSee docs for context: https://docs.aztec.network/errors/14`, ); } - const functionDao = contract.functions.find(f => f.name === functionName); + const functionDao = artifact.functions.find(f => f.name === functionName); if (!functionDao) { - throw new Error(`Unknown function ${functionName} in contract ${contract.name}.`); + throw new Error(`Unknown function ${functionName} in contract ${artifact.name}.`); } const selector = await FunctionSelector.fromNameAndParameters(functionDao.name, functionDao.parameters); @@ -406,3 +375,27 @@ export class ContractStore { }); } } + +/** + * Narrows the result of a selector lookup against a registered artifact. A missing class artifact is reported as + * `undefined` by the callers (an absence they can handle), but a selector that is absent from an artifact we *do* have + * is exceptional and throws rather than returning undefined. We cannot tell from here why the selector is missing: it + * may be that no such function exists in the contract (a wrong selector), or that the registered artifact does not + * correspond to the resolved class id (registration performs no validation, so a mismatched artifact is possible). The + * error names both possibilities. + */ +function assertSelectorInArtifact( + artifact: ContractArtifact, + fn: T | undefined, + context: { contractClassId: Fr; selector: FunctionSelector }, +): T { + if (!fn) { + throw new Error( + `Function with selector ${context.selector} not found in the registered artifact for contract class ` + + `${context.contractClassId} (${artifact.name}). Either no function with this selector exists in the ` + + `contract, or the registered artifact does not match the class id (re-register it via ` + + `wallet.registerContract(...)).`, + ); + } + return fn; +} diff --git a/yarn-project/stdlib/src/contract/contract_address.ts b/yarn-project/stdlib/src/contract/contract_address.ts index 8511fda5cb9d..5a6ff4462b50 100644 --- a/yarn-project/stdlib/src/contract/contract_address.ts +++ b/yarn-project/stdlib/src/contract/contract_address.ts @@ -6,7 +6,7 @@ import { type FunctionAbi, FunctionSelector, encodeArguments } from '../abi/inde import type { AztecAddress } from '../aztec-address/index.js'; import { computeVarArgsHash } from '../hash/hash.js'; import { computeAddress } from '../keys/index.js'; -import type { ContractInstance } from './interfaces/contract_instance.js'; +import type { ContractInstance, ContractInstancePreimage } from './interfaces/contract_instance.js'; import type { PartialAddress } from './partial_address.js'; // TODO(@spalladino): Review all generator indices in this file @@ -22,7 +22,7 @@ import type { PartialAddress } from './partial_address.js'; */ export async function computeContractAddressFromInstance( instance: - | ContractInstance + | ContractInstancePreimage | ({ originalContractClassId: Fr; saltedInitializationHash: Fr } & Pick), ): Promise { const partialAddress = await computePartialAddress(instance); diff --git a/yarn-project/stdlib/src/contract/contract_instance.test.ts b/yarn-project/stdlib/src/contract/contract_instance.test.ts index 2fbab0a8c70b..5506dcccd59a 100644 --- a/yarn-project/stdlib/src/contract/contract_instance.test.ts +++ b/yarn-project/stdlib/src/contract/contract_instance.test.ts @@ -1,4 +1,4 @@ -import { SerializableContractInstance } from './contract_instance.js'; +import { SerializableContractInstance, SerializableContractInstancePreimage } from './contract_instance.js'; describe('ContractInstance', () => { it('can serialize and deserialize an instance', async () => { @@ -6,3 +6,11 @@ describe('ContractInstance', () => { expect(SerializableContractInstance.fromBuffer(instance.toBuffer())).toEqual(instance); }); }); + +describe('ContractInstancePreimage', () => { + it('round-trips the preimage layout', async () => { + const instance = await SerializableContractInstance.random(); + const preimage = new SerializableContractInstancePreimage(instance); + expect(SerializableContractInstancePreimage.fromBuffer(preimage.toBuffer())).toEqual(preimage); + }); +}); diff --git a/yarn-project/stdlib/src/contract/contract_instance.ts b/yarn-project/stdlib/src/contract/contract_instance.ts index 7be8330eeea5..139d5c5ef101 100644 --- a/yarn-project/stdlib/src/contract/contract_instance.ts +++ b/yarn-project/stdlib/src/contract/contract_instance.ts @@ -18,7 +18,12 @@ import { computeInitializationHash, computeInitializationHashFromEncodedArgs, } from './contract_address.js'; -import type { ContractInstance, ContractInstanceWithAddress } from './interfaces/contract_instance.js'; +import type { + ContractInstance, + ContractInstancePreimage, + ContractInstancePreimageWithAddress, + ContractInstanceWithAddress, +} from './interfaces/contract_instance.js'; const VERSION = 2 as const; @@ -115,6 +120,62 @@ export class SerializableContractInstance { } } +export class SerializableContractInstancePreimage { + public readonly version = VERSION; + public readonly salt: Fr; + public readonly deployer: AztecAddress; + public readonly originalContractClassId: Fr; + public readonly initializationHash: Fr; + public readonly immutablesHash: Fr; + public readonly publicKeys: PublicKeys; + + constructor(instance: ContractInstancePreimage) { + if (instance.version !== VERSION) { + throw new Error(`Unexpected contract class version ${instance.version}`); + } + this.salt = instance.salt; + this.deployer = instance.deployer; + this.originalContractClassId = instance.originalContractClassId; + this.initializationHash = instance.initializationHash; + this.immutablesHash = instance.immutablesHash; + this.publicKeys = instance.publicKeys; + } + + public toBuffer() { + return serializeToBuffer( + numToUInt8(this.version), + this.salt, + this.deployer, + this.originalContractClassId, + this.initializationHash, + this.immutablesHash, + this.publicKeys, + ); + } + + /** Returns a copy of this object with its address included. */ + withAddress(address: AztecAddress): ContractInstancePreimageWithAddress { + return { ...this, address }; + } + + static fromBuffer(bufferOrReader: Buffer | BufferReader): SerializableContractInstancePreimage { + const reader = BufferReader.asReader(bufferOrReader); + const version = reader.readUInt8(); + if (version !== VERSION) { + throw new Error(`Unexpected contract instance preimage version ${version}`); + } + return new SerializableContractInstancePreimage({ + version: VERSION, + salt: reader.readObject(Fr), + deployer: reader.readObject(AztecAddress), + originalContractClassId: reader.readObject(Fr), + initializationHash: reader.readObject(Fr), + immutablesHash: reader.readObject(Fr), + publicKeys: reader.readObject(PublicKeys), + }); + } +} + /** * Generates a Contract Instance from some instantiation params. * @param artifact - The account contract build artifact. diff --git a/yarn-project/stdlib/src/contract/interfaces/contract_instance.ts b/yarn-project/stdlib/src/contract/interfaces/contract_instance.ts index 2d992dbac99c..86a537abde76 100644 --- a/yarn-project/stdlib/src/contract/interfaces/contract_instance.ts +++ b/yarn-project/stdlib/src/contract/interfaces/contract_instance.ts @@ -9,19 +9,19 @@ import { schemas, zodFor } from '../../schemas/index.js'; const VERSION = 2 as const; /** - * A contract instance is a concrete deployment of a contract class. It always references a contract class, - * which dictates what code it executes when called. It has state (both private and public), as well as an - * address that acts as its identifier. It can be called into. It may have encryption and nullifying public keys. + * The address preimage of a contract instance: the immutable data that hashes to its address, i.e. everything a + * contract deployment commits to. + * + * Note that `originaContractClassId` is not necessarily the instance's _current_ class id, in case of upgrades: that + * one is not part of the preimage and is instead derived from chain state. */ -export interface ContractInstance { +export interface ContractInstancePreimage { /** Version identifier. Initially one, bumped for any changes to the contract instance struct. */ version: typeof VERSION; /** User-generated pseudorandom value for uniqueness. */ salt: Fr; /** Optional deployer address or zero if this was a universal deploy. */ deployer: AztecAddress; - /** Identifier of the contract class for this instance. */ - currentContractClassId: Fr; /** Identifier of the original (at deployment) contract class for this instance */ originalContractClassId: Fr; /** Hash of the selector and arguments to the constructor. */ @@ -32,14 +32,24 @@ export interface ContractInstance { publicKeys: PublicKeys; } +/** + * The address preimage plus the current contract class id, which is chain-tracked state derived from the + * ContractInstanceRegistry (it equals the original class id unless the contract has been upgraded). + */ +export interface ContractInstance extends ContractInstancePreimage { + /** Identifier of the contract class this instance currently runs (as of some block). */ + currentContractClassId: Fr; +} + +export type ContractInstancePreimageWithAddress = ContractInstancePreimage & { address: AztecAddress }; + export type ContractInstanceWithAddress = ContractInstance & { address: AztecAddress }; -export const ContractInstanceSchema = zodFor()( +export const ContractInstancePreimageSchema = zodFor()( z.object({ version: z.literal(VERSION), salt: schemas.Fr, deployer: schemas.AztecAddress, - currentContractClassId: schemas.Fr, originalContractClassId: schemas.Fr, initializationHash: schemas.Fr, immutablesHash: schemas.Fr, @@ -47,6 +57,14 @@ export const ContractInstanceSchema = zodFor()( }), ); +export const ContractInstanceSchema = zodFor()( + ContractInstancePreimageSchema.and(z.object({ currentContractClassId: schemas.Fr })), +); + +export const ContractInstancePreimageWithAddressSchema = zodFor()( + ContractInstancePreimageSchema.and(z.object({ address: schemas.AztecAddress })), +); + export const ContractInstanceWithAddressSchema = zodFor()( ContractInstanceSchema.and(z.object({ address: schemas.AztecAddress })), ); diff --git a/yarn-project/txe/src/oracle/txe_oracle_public_context.ts b/yarn-project/txe/src/oracle/txe_oracle_public_context.ts index 67fc7ecf6efc..0b1132b7dcad 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_public_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_public_context.ts @@ -5,7 +5,7 @@ import type { ContractStore } from '@aztec/pxe/server'; import { PublicDataWrite } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { L2Block } from '@aztec/stdlib/block'; -import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract'; +import type { ContractInstancePreimageWithAddress } from '@aztec/stdlib/contract'; import { computePublicDataTreeLeafSlot, siloNoteHash, siloNullifier } from '@aztec/stdlib/hash'; import { MerkleTreeId, @@ -130,7 +130,8 @@ export class TXEOraclePublicContext implements IAvmExecutionOracle { } getContractInstanceClassId(address: AztecAddress): Promise<{ member: Fr; exists: boolean }> { - return this.getContractInstanceMember(address, i => i.currentContractClassId); + // TXE has no contract updates, so the current class always equals the original. + return this.getContractInstanceMember(address, i => i.originalContractClassId); } getContractInstanceInitializationHash(address: AztecAddress): Promise<{ member: Fr; exists: boolean }> { @@ -143,7 +144,7 @@ export class TXEOraclePublicContext implements IAvmExecutionOracle { private async getContractInstanceMember( address: AztecAddress, - accessor: (instance: ContractInstanceWithAddress) => Fr, + accessor: (instance: ContractInstancePreimageWithAddress) => Fr, ): Promise<{ member: Fr; exists: boolean }> { const instance = await this.contractStore.getContractInstance(address); if (!instance) { diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 69e415f999d1..b087a53a2344 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -12,6 +12,7 @@ import { TestDateProvider } from '@aztec/foundation/timer'; import type { KeyStore } from '@aztec/key-store'; import { AddressStore, + AnchoredContractData, CapsuleService, CapsuleStore, type ContractStore, @@ -416,11 +417,18 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl authorizedUtilityCallTargets: AztecAddress[], gasSettings: GasSettings, ) { + const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + blockHeader, + ); + this.logger.verbose( - `Executing external function ${await this.contractStore.getDebugFunctionName(targetContractAddress, functionSelector)}@${targetContractAddress} isStaticCall=${isStaticCall}`, + `Executing external function ${await anchoredContractData.getDebugFunctionName(targetContractAddress, functionSelector)}@${targetContractAddress} isStaticCall=${isStaticCall}`, ); - const artifact = await this.contractStore.getFunctionArtifact(targetContractAddress, functionSelector); + const artifact = await anchoredContractData.getFunctionArtifact(targetContractAddress, functionSelector); if (!artifact) { const message = functionSelector.equals(await FunctionSelector.fromSignature('verify_private_authwit(Field)')) ? 'Found no account contract artifact for a private authwit check - use `create_contract_account` instead of `create_light_account` for authwit support.' @@ -435,7 +443,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl await this.executeUtilityCall(call, { scopes: execScopes, jobId }); }; - const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); await this.stateMachine.contractSyncService.ensureContractSynced( targetContractAddress, functionSelector, @@ -476,7 +483,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl executionCache: HashedValuesCache.create([new HashedValues(args, argsHash)]), noteCache, taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -547,7 +554,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl // We pass the non-zero minRevertibleSideEffectCounter to make sure the side effects are split correctly. const { publicInputs } = await generateSimulatedProvingResult( result, - (addr, sel) => this.contractStore.getDebugFunctionName(addr, sel), + (addr, sel) => anchoredContractData.getDebugFunctionName(addr, sel), this.stateMachine.node, minRevertibleSideEffectCounter, ); @@ -606,7 +613,13 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl } else if (!processedTx.revertCode.isOK()) { if (processedTx.revertReason) { try { - await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); + await enrichPublicSimulationError( + processedTx.revertReason, + this.contractStore, + this.stateMachine.contractClassService, + await this.stateMachine.anchorBlockStore.getBlockHeader(), + this.logger, + ); // eslint-disable-next-line no-empty } catch {} throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); @@ -657,16 +670,21 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl isStaticCall: boolean, gasSettings: GasSettings, ) { + const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + anchorBlockHeader, + ); + this.logger.verbose( - `Executing public function ${await this.contractStore.getDebugFunctionName(targetContractAddress, FunctionSelector.fromField(calldata[0]))}@${targetContractAddress} isStaticCall=${isStaticCall}`, + `Executing public function ${await anchoredContractData.getDebugFunctionName(targetContractAddress, FunctionSelector.fromField(calldata[0]))}@${targetContractAddress} isStaticCall=${isStaticCall}`, ); const blockNumber = await this.getNextBlockNumber(); const txContext = new TxContext(this.chainId, this.version, gasSettings); - const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); - const calldataHash = await computeCalldataHash(calldata); const calldataHashedValues = new HashedValues(calldata, calldataHash); @@ -759,7 +777,13 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl } else if (!processedTx.revertCode.isOK()) { if (processedTx.revertReason) { try { - await enrichPublicSimulationError(processedTx.revertReason, this.contractStore, this.logger); + await enrichPublicSimulationError( + processedTx.revertReason, + this.contractStore, + this.stateMachine.contractClassService, + anchorBlockHeader, + this.logger, + ); // eslint-disable-next-line no-empty } catch {} throw new Error(`Contract execution has reverted: ${processedTx.revertReason.getMessage()}`); @@ -808,13 +832,19 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl jobId: string, authorizedUtilityCallTargets: AztecAddress[], ) { - const artifact = await this.contractStore.getFunctionArtifact(targetContractAddress, functionSelector); + const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + blockHeader, + ); + + const artifact = await anchoredContractData.getFunctionArtifact(targetContractAddress, functionSelector); if (!artifact) { throw new Error(`Cannot call ${functionSelector} as there is no artifact found at ${targetContractAddress}.`); } // Sync notes before executing utility function to discover notes from previous transactions - const blockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); await this.stateMachine.contractSyncService.ensureContractSynced( targetContractAddress, functionSelector, @@ -854,7 +884,17 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl authorizedUtilityCallTargets = [], }: { from?: AztecAddress; scopes: AztecAddress[]; jobId: string; authorizedUtilityCallTargets?: AztecAddress[] }, ): Promise { - const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); + const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + anchorBlockHeader, + ); + + const entryPointArtifact = await anchoredContractData.getFunctionArtifactWithDebugMetadata(call.to, call.selector); + if (!entryPointArtifact) { + throw new Error(`Cannot run function ${call.selector} on ${call.to}: the contract is not registered.`); + } if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); } @@ -865,7 +905,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl }); try { - const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); const simulator = new WASMSimulator(); const utilityExecutor = async (syncCall: FunctionCall, execScopes: AztecAddress[]) => { await this.executeUtilityCall(syncCall, { scopes: execScopes, jobId }); @@ -880,7 +919,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl authWitnesses: [], capsules: [], anchorBlockHeader, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index 3356a0c6a5fc..4f724f3f8130 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -3,7 +3,13 @@ import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; -import { type AnchorBlockStore, type ContractStore, ContractSyncService, type NoteStore } from '@aztec/pxe/server'; +import { + type AnchorBlockStore, + ContractClassService, + type ContractStore, + ContractSyncService, + type NoteStore, +} from '@aztec/pxe/server'; import { TxResolverService } from '@aztec/pxe/simulator'; import { L2Block, type L2TipsProvider } from '@aztec/stdlib/block'; import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; @@ -29,6 +35,7 @@ export class TXEStateMachine { public archiver: TXEArchiver, public anchorBlockStore: AnchorBlockStore, public contractSyncService: ContractSyncService, + public contractClassService: ContractClassService, public txResolver: TxResolverService, ) {} @@ -68,16 +75,26 @@ export class TXEStateMachine { log, }); + const contractClassService = new ContractClassService(node, contractStore); const contractSyncService = new ContractSyncService( node, contractStore, + contractClassService, noteStore, createLogger('txe:contract_sync'), ); const txResolver = new TxResolverService(node); - return new this(node, synchronizer, archiver, anchorBlockStore, contractSyncService, txResolver); + return new this( + node, + synchronizer, + archiver, + anchorBlockStore, + contractSyncService, + contractClassService, + txResolver, + ); } /** Returns an {@link L2TipsProvider} backed by this node's chain tips. */ diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index 5b1aa064b844..b72b46b2b346 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -7,6 +7,7 @@ import { openEphemeralStore } from '@aztec/kv-store/lmdb-v2'; import { AddressStore, AnchorBlockStore, + AnchoredContractData, CapsuleService, CapsuleStore, ContractStore, @@ -731,6 +732,11 @@ export class TXESession implements TXESessionStateHandler { const utilityExecutor = this.utilityExecutorForContractSync(anchorBlock); const transientArrayService = new TransientArrayService(); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + anchorBlock!, + ); const taggingSecretStrategy = this.taggingSecretStrategy; this.oracleHandler = new TXEPrivateExecutionOracle({ argsHash: Fr.ZERO, @@ -743,7 +749,7 @@ export class TXESession implements TXESessionStateHandler { executionCache: new HashedValuesCache(), noteCache, taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -841,7 +847,11 @@ export class TXESession implements TXESessionStateHandler { authWitnesses: [], capsules: [], anchorBlockHeader, - contractStore: this.contractStore, + anchoredContractData: new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + anchorBlockHeader, + ), noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -937,7 +947,18 @@ export class TXESession implements TXESessionStateHandler { private utilityExecutorForContractSync(anchorBlock: any) { return async (call: FunctionCall, scopes: AztecAddress[]) => { - const entryPointArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata(call.to, call.selector); + const anchoredContractData = new AnchoredContractData( + this.contractStore, + this.stateMachine.contractClassService, + anchorBlock!, + ); + const entryPointArtifact = await anchoredContractData.getFunctionArtifactWithDebugMetadata( + call.to, + call.selector, + ); + if (!entryPointArtifact) { + throw new Error(`Cannot run function ${call.selector} on ${call.to}: the contract is not registered.`); + } if (entryPointArtifact.functionType !== FunctionType.UTILITY) { throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`); } @@ -954,7 +975,7 @@ export class TXESession implements TXESessionStateHandler { authWitnesses: [], capsules: [], anchorBlockHeader: anchorBlock!, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, diff --git a/yarn-project/txe/src/utils/txe_public_contract_data_source.ts b/yarn-project/txe/src/utils/txe_public_contract_data_source.ts index 1a4c5b36ca88..316a9753a561 100644 --- a/yarn-project/txe/src/utils/txe_public_contract_data_source.ts +++ b/yarn-project/txe/src/utils/txe_public_contract_data_source.ts @@ -36,7 +36,8 @@ export class TXEPublicContractDataSource implements ContractDataSource { async getContract(address: AztecAddress): Promise { const instance = await this.contractStore.getContractInstance(address); - return instance && { ...instance, address }; + // TXE has no contract updates, so the current class always equals the original. + return instance && { ...instance, address, currentContractClassId: instance.originalContractClassId }; } getContractClassIds(): Promise { @@ -44,12 +45,13 @@ export class TXEPublicContractDataSource implements ContractDataSource { } async getContractArtifact(address: AztecAddress): Promise { - const instance = await this.contractStore.getContractInstance(address); - return instance && this.contractStore.getContractArtifact(instance.currentContractClassId); + const instance = await this.getContract(address); + return instance && this.contractStore.getContractArtifact(instance.originalContractClassId); } async getDebugFunctionName(address: AztecAddress, selector: FunctionSelector): Promise { - return await this.contractStore.getDebugFunctionName(address, selector); + const instance = await this.getContract(address); + return instance && this.contractStore.getDebugFunctionName(instance.originalContractClassId, selector); } registerContractFunctionSignatures(_signatures: []): Promise { diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index 8dd8273ec61e..41ff0878ce8f 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -41,12 +41,7 @@ import { } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { - type ContractInstanceWithAddress, - type NodeInfo, - computePartialAddress, - getContractClassFromArtifact, -} from '@aztec/stdlib/contract'; +import { type ContractInstancePreimage, type NodeInfo, computePartialAddress } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import { Gas, GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { @@ -353,41 +348,21 @@ export abstract class BaseWallet implements Wallet { } async registerContract( - instance: ContractInstanceWithAddress, + instance: ContractInstancePreimage, artifact?: ContractArtifact, secretKeyOrKeys?: Fr | MasterSecretKeys, - ): Promise { - const existingInstance = await this.pxe.getContractInstance(instance.address); - - if (existingInstance) { - // Instance already registered in the wallet - if (artifact) { - const thisContractClass = await getContractClassFromArtifact(artifact); - if (!thisContractClass.id.equals(existingInstance.currentContractClassId)) { - // wallet holds an outdated version of this contract - await this.pxe.updateContract(instance.address, artifact); - instance.currentContractClassId = thisContractClass.id; - } - } - // If no artifact provided, we just use the existing registration - } else { - // Instance not registered yet - if (!artifact) { - // Try to get the artifact from the wallet's contract class storage - artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); - if (!artifact) { - throw new Error( - `Cannot register contract at ${instance.address.toString()}: artifact is required but not provided, and wallet does not have the artifact for contract class ${instance.currentContractClassId.toString()}`, - ); - } - } - await this.pxe.registerContract({ artifact, instance }); + ): Promise { + // Classes and instances are registered independently: register the artifact (if provided) then the instance. + // Neither call validates that the artifact matches the class the instance runs, a missing artifact only surfaces + // when the contract is later simulated. + if (artifact) { + await this.pxe.registerContractClass(artifact); } + await this.pxe.registerContract(instance); if (secretKeyOrKeys) { await this.pxe.registerAccount(secretKeyOrKeys, await computePartialAddress(instance)); } - return instance; } registerContractClass(artifact: ContractArtifact): Promise { @@ -566,7 +541,11 @@ export abstract class BaseWallet implements Wallet { if (!instance) { return undefined; } - const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + // Contract names are class-stable (an upgrade preserves the contract name), so the original class artifact is a + // sufficient source for the display name without resolving the current class against the node. + // TODO: if a contract were to be upgraded and its original artifact never registered, then this would fail and we'd + // want to fallback to the current class. + const artifact = await this.pxe.getContractArtifact(instance.originalContractClassId); return artifact?.name; } diff --git a/yarn-project/wallets/src/embedded/embedded_wallet.ts b/yarn-project/wallets/src/embedded/embedded_wallet.ts index 9a1d62607e83..7616c047c59e 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -26,12 +26,12 @@ import type { Logger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; import type { PXEConfig, PXECreationOptions } from '@aztec/pxe/client/lazy'; import type { PXE } from '@aztec/pxe/server'; -import type { ContractArtifact, EventMetadataDefinition, FunctionCall } from '@aztec/stdlib/abi'; +import type { EventMetadataDefinition, FunctionCall } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type ContractInstanceWithAddress, getContractClassFromArtifact } from '@aztec/stdlib/contract'; +import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; import { GasSettings } from '@aztec/stdlib/gas'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { type MasterSecretKeys, deriveSigningKey } from '@aztec/stdlib/keys'; +import { deriveSigningKey } from '@aztec/stdlib/keys'; import { type ContractOverrides, ExecutionPayload, @@ -263,17 +263,6 @@ export class EmbeddedWallet extends BaseWallet { return super.getPrivateEvents(eventDef, eventFilter); } - public override async registerContract( - instance: ContractInstanceWithAddress, - artifact?: ContractArtifact, - secretKeyOrKeys?: Fr | MasterSecretKeys, - ): Promise { - // registerContract may call pxe.updateContract under the hood, which depends on a fresh anchor - // block to verify the current class id from the node. - await this.pxe.sync(); - return super.registerContract(instance, artifact, secretKeyOrKeys); - } - /** * Hashes and registers the stub class for every supported account type with PXE, populating * stubClassIds. Called on wallet initialization.