From 807665c44e2b090317a13cb0d5121c2799595c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 24 Jun 2026 21:14:28 +0000 Subject: [PATCH 01/11] feat!: make contract classes dynamic --- .../docs/aztec-js/how_to_test.md | 2 +- .../docs/resources/migration_notes.md | 27 +++ .../contract/fastforward_contract_update.ts | 2 +- yarn-project/aztec.js/src/wallet/wallet.ts | 13 +- yarn-project/cli-wallet/src/cmds/check_tx.ts | 21 +- yarn-project/cli-wallet/src/cmds/simulate.ts | 3 +- yarn-project/cli-wallet/src/utils/wallet.ts | 3 +- .../src/e2e_contract_updates.test.ts | 14 +- .../e2e_deploy_contract/deploy_method.test.ts | 2 +- .../src/spartan/block_capacity.test.ts | 16 +- .../block_synchronizer.test.ts | 16 +- .../block_synchronizer/block_synchronizer.ts | 10 +- .../contract/contract_class_service.test.ts | 120 +++++++++++ .../src/contract/contract_class_service.ts | 83 ++++++++ .../contract_sync_service.test.ts | 76 +------ .../contract_sync_service.ts | 55 ++--- yarn-project/pxe/src/contract/helpers.ts | 35 ++++ .../anchored_contract_data.ts | 74 +++++++ .../contract_function_simulator.ts | 46 ++++- .../oracle/oracle_type_mappings.ts | 4 +- .../oracle/oracle_version_is_checked.test.ts | 17 +- .../oracle/private_execution.test.ts | 32 ++- .../oracle/private_execution_oracle.test.ts | 6 +- .../oracle/private_execution_oracle.ts | 12 +- .../oracle/utility_execution.test.ts | 10 +- .../oracle/utility_execution_oracle.ts | 46 +++-- .../proxied_contract_data_source.ts | 60 ------ yarn-project/pxe/src/contract_sync/helpers.ts | 79 -------- yarn-project/pxe/src/debug/pxe_debug_utils.ts | 2 +- .../pxe/src/entrypoints/server/index.ts | 4 +- yarn-project/pxe/src/error_enriching.ts | 36 +++- .../private_kernel_oracle.test.ts | 9 +- .../private_kernel/private_kernel_oracle.ts | 27 ++- yarn-project/pxe/src/pxe.test.ts | 61 ++---- yarn-project/pxe/src/pxe.ts | 190 ++++++++---------- .../__snapshots__/ContractStore.json | 2 +- .../contract_store/contract_store.test.ts | 48 ++++- .../storage/contract_store/contract_store.ts | 177 ++++++++-------- .../stdlib/src/contract/contract_address.ts | 4 +- .../src/contract/contract_instance.test.ts | 10 +- .../stdlib/src/contract/contract_instance.ts | 63 +++++- .../contract/interfaces/contract_instance.ts | 34 +++- .../src/oracle/txe_oracle_public_context.ts | 7 +- .../oracle/txe_oracle_top_level_context.ts | 69 +++++-- yarn-project/txe/src/state_machine/index.ts | 21 +- yarn-project/txe/src/txe_session.ts | 29 ++- .../utils/txe_public_contract_data_source.ts | 10 +- .../wallet-sdk/src/base-wallet/base_wallet.ts | 44 ++-- .../wallets/src/embedded/embedded_wallet.ts | 15 +- 49 files changed, 1093 insertions(+), 653 deletions(-) create mode 100644 yarn-project/pxe/src/contract/contract_class_service.test.ts create mode 100644 yarn-project/pxe/src/contract/contract_class_service.ts rename yarn-project/pxe/src/{contract_sync => contract}/contract_sync_service.test.ts (82%) rename yarn-project/pxe/src/{contract_sync => contract}/contract_sync_service.ts (76%) create mode 100644 yarn-project/pxe/src/contract/helpers.ts create mode 100644 yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts delete mode 100644 yarn-project/pxe/src/contract_function_simulator/proxied_contract_data_source.ts delete mode 100644 yarn-project/pxe/src/contract_sync/helpers.ts 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..32f7fcbe01d8 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 on-chain 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 4a46a11478ce..b636623805d6 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?, secretKey?)` convenience is unchanged and performs both registrations for you. + +- To make a new class's code available after an on-chain 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] Unchecked `AztecAddress` constructors renamed with an `Unsafe` suffix The synchronous `AztecAddress` constructors that build an address from a raw value do not verify that the value is a valid address (the x-coordinate of a point on the Grumpkin curve, which is what allows it to be encrypted to). An invalid value is accepted silently and only fails later, when a transaction is sent. To make this obvious at the call site, they now carry an `Unsafe` suffix: 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.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index c73a28604506..1d242360002f 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 ContractInstancePreimageWithAddress, + ContractInstancePreimageWithAddressSchema, + type ContractInstanceWithAddress, + ContractInstanceWithAddressSchema, +} from '@aztec/stdlib/contract'; import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { LogCursor, refineTxHashAndRange } from '@aztec/stdlib/logs'; import { @@ -229,8 +234,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 */ @@ -410,7 +415,7 @@ export const PublicEventSchema: z.ZodType> = zodFor { 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/e2e_contract_updates.test.ts b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts index 0780a440151c..9404331a8885 100644 --- a/yarn-project/end-to-end/src/e2e_contract_updates.test.ts +++ b/yarn-project/end-to-end/src/e2e_contract_updates.test.ts @@ -174,10 +174,16 @@ describe('e2e_contract_updates', () => { ).rejects.toThrow('New update delay is too low'); }); - 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.toBeDefined(); + + // 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/e2e_deploy_contract/deploy_method.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts index c194d6677265..55baa1c69a17 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract/deploy_method.test.ts @@ -52,7 +52,7 @@ describe('e2e_deploy_contract 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 26f6eb711f9f..7de00bd8615e 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 @@ -369,9 +369,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'); }); @@ -408,10 +411,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/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 9f8783e6881d..8fb422093f9a 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 { NoteStore } from '../storage/note_store/note_store.js'; import { PrivateEventStore } from '../storage/private_event_store/private_event_store.js'; @@ -47,6 +48,7 @@ describe('BlockSynchronizer', () => { let getBlock: NodeGetBlockMock; let blockStream: MockProxy; let contractSyncService: MockProxy; + let contractClassService: MockProxy; const TestSynchronizer = class extends BlockSynchronizer { protected override createBlockStream(): L2BlockStream { @@ -63,6 +65,7 @@ describe('BlockSynchronizer', () => { privateEventStore, tipsStore, contractSyncService, + contractClassService, config, ); }; @@ -110,6 +113,7 @@ describe('BlockSynchronizer', () => { noteStore = new NoteStore(store); privateEventStore = new PrivateEventStore(store); contractSyncService = mock(); + contractClassService = mock(); synchronizer = createSynchronizer(); }); @@ -148,6 +152,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)); const reorgResponse = await blockResponse(reorgBlock); @@ -628,6 +641,7 @@ describe('BlockSynchronizer', () => { privateEventStore, 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 839344854f52..0b0a27a46546 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 { NoteStore } from '../storage/note_store/index.js'; import type { PrivateEventStore } from '../storage/private_event_store/private_event_store.js'; @@ -33,6 +34,7 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { private privateEventStore: PrivateEventStore, private l2TipsStore: L2TipsKVStore, private contractSyncService: ContractSyncService, + private contractClassService: ContractClassService, private config: Partial = {}, bindings?: LoggerBindings, ) { @@ -174,6 +176,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..b371df8278ea --- /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 exeuctions 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..81ff6875a077 --- /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 store: ContractStore, + private contractClassService: ContractClassService, + private anchorBlockHeader: BlockHeader, + private 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 56de5e8b8778..56fe48a24871 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 { MessageContextService } from '../messages/message_context_service.js'; import type { AddressStore } from '../storage/address_store/address_store.js'; @@ -102,6 +104,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'; @@ -129,6 +132,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; @@ -151,6 +157,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; @@ -168,6 +176,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; @@ -203,10 +213,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`); @@ -244,7 +267,7 @@ export class ContractFunctionSimulator { executionCache: HashedValuesCache.create(request.argsOfCalls), noteCache, taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -327,7 +350,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`); @@ -347,7 +383,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 9eb32083939b..ffd71428e408 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 @@ -20,7 +20,7 @@ import { FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { PublicDataWrite } 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 { KeyValidationRequest } from '@aztec/stdlib/kernel'; import type { PublicKeys } from '@aztec/stdlib/keys'; import { @@ -241,7 +241,7 @@ export const KEY_VALIDATION_REQUEST: TypeMapping = { shape: ['scalar', 'scalar'], }; -export const CONTRACT_INSTANCE: TypeMapping = { +export const CONTRACT_INSTANCE: TypeMapping = { serialization: { fn: v => [ v.salt, 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 1d805f7e78cc..67d1bd6d4385 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 { MessageContextService } from '../../messages/message_context_service.js'; import { ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR } from '../../oracle_version.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; @@ -26,6 +27,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'; @@ -35,6 +37,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>; @@ -90,16 +93,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, @@ -202,7 +209,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 7546ea8dbb3e..d94dc7d589a0 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 { MessageContextService } from '../../messages/message_context_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import type { CapsuleStore } from '../../storage/capsule_store/capsule_store.js'; @@ -102,6 +103,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; @@ -278,6 +280,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(); @@ -296,7 +304,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, + ); } }, ); @@ -449,6 +465,7 @@ describe('Private Execution test suite', () => { acirSimulator = new ContractFunctionSimulator({ contractStore, + contractClassService, noteStore, keyStore, addressStore, @@ -700,7 +717,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); @@ -727,7 +744,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 f1fd82f3815e..2ab14984ddf2 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 @@ -12,17 +12,17 @@ import { type BlockHeader, CallContext, type Capsule, TxContext } from '@aztec/s 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 { MessageContextService } from '../../messages/message_context_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 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 { 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'; @@ -81,7 +81,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 bf666fe23e8c..b6171c865187 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 @@ -530,10 +530,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(); @@ -550,7 +556,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, @@ -653,7 +659,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 b7d36f6f0264..618f67123414 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 @@ -42,7 +42,8 @@ import { 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 { MessageContextService } from '../../messages/message_context_service.js'; import type { AddressStore } from '../../storage/address_store/address_store.js'; import { CapsuleService } from '../../storage/capsule_store/capsule_service.js'; @@ -53,6 +54,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'; @@ -65,6 +67,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>; @@ -89,6 +92,8 @@ describe('Utility Execution test suite', () => { beforeEach(async () => { contractStore = mock(); + contractClassService = mock(); + contractClassService.getCurrentClassId.mockResolvedValue(new Fr(42)); noteStore = mock(); keyStore = mock(); addressStore = mock(); @@ -122,6 +127,7 @@ describe('Utility Execution test suite', () => { }); acirSimulator = new ContractFunctionSimulator({ contractStore, + contractClassService, noteStore, keyStore, addressStore, @@ -697,7 +703,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 e372dff03ed6..bf5d07c71a4f 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'; @@ -44,8 +44,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'; @@ -55,11 +55,11 @@ 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 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 { EphemeralArray } from '../noir-structs/ephemeral_array.js'; @@ -84,7 +84,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; @@ -150,7 +150,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; @@ -373,8 +373,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()}`); } @@ -529,7 +529,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 }, ); @@ -546,7 +546,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 }, ); @@ -840,22 +840,34 @@ 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)) { 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, @@ -900,7 +912,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 5597cdf7efd2..f1009067a715 100644 --- a/yarn-project/pxe/src/entrypoints/server/index.ts +++ b/yarn-project/pxe/src/entrypoints/server/index.ts @@ -9,4 +9,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 01b6b7e56fdc..943e06ca3e6b 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -22,7 +22,7 @@ import { GENESIS_CHECKPOINT_HEADER_HASH, } from '@aztec/stdlib/block'; import { emptyChainConfig } from '@aztec/stdlib/config'; -import { getContractClassFromArtifact } from '@aztec/stdlib/contract'; +import { SerializableContractInstancePreimage, getContractClassFromArtifact } from '@aztec/stdlib/contract'; import type { AztecNode, AztecNodeDebug, BlockResponse } from '@aztec/stdlib/interfaces/client'; import { randomContractArtifact, @@ -146,7 +146,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); @@ -157,7 +157,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); @@ -172,47 +174,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); + await pxe.registerContract(instance); + expect(await pxe.getContractInstance(instance.address)).toEqual( + new SerializableContractInstancePreimage(instance).withAddress(instance.address), + ); }); - 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./); - }); - - 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 = [ { @@ -228,11 +215,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()']); }); @@ -294,7 +279,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 @@ -306,9 +291,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 8ee37ca591b0..1f531bcc9261 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'; @@ -55,15 +57,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'; @@ -209,6 +210,7 @@ export class PXE { private addressStore: AddressStore, private privateEventStore: PrivateEventStore, private contractSyncService: ContractSyncService, + private contractClassService: ContractClassService, private messageContextService: MessageContextService, private l2TipsStore: L2TipsProvider, private simulator: CircuitSimulator, @@ -276,9 +278,11 @@ export class PXE { keyStore, l2TipsStore, } = openPxeStores(store, initialBlockHash); + const contractClassService = new ContractClassService(node, contractStore); const contractSyncService = new ContractSyncService( node, contractStore, + contractClassService, noteStore, createLogger('pxe:contract_sync', bindings), ); @@ -292,6 +296,7 @@ export class PXE { privateEventStore, l2TipsStore, contractSyncService, + contractClassService, config, bindings, ); @@ -326,6 +331,7 @@ export class PXE { addressStore, privateEventStore, contractSyncService, + contractClassService, messageContextService, l2TipsStore, simulator, @@ -357,10 +363,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, @@ -378,6 +384,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) { @@ -441,7 +471,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()), }); @@ -487,7 +524,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; } @@ -522,7 +559,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; } @@ -546,7 +584,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}`); } @@ -573,7 +618,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, @@ -617,11 +668,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); } @@ -783,98 +834,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. + * @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 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). - */ - 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; }); } @@ -1136,7 +1122,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 { @@ -1163,7 +1149,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..83142c13cf1b 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": "030000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000004f0000000000000000000000000000000000000000000000000000000000000059000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000004300000000000000000000000000000000000000000000000000000000000000470000000000000000000000000000000000000000000000000000000000000049" } ] } 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..84655b1c91bf 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 resolved class id', + ); + await expect(contractStore.getFunctionArtifactWithDebugMetadata(classId, missingSelector)).rejects.toThrow( + 'does not match the resolved 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 a5c36107b344..bfd278db2950 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'; // TODO(@spalladino): Review all generator indices in this file @@ -21,7 +21,7 @@ import type { ContractInstance } from './interfaces/contract_instance.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 73af8b5f4329..bff504c646d6 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 584e0fe3b011..a9a4027bf66e 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 @@ -11,6 +11,7 @@ import { TestDateProvider } from '@aztec/foundation/timer'; import type { KeyStore } from '@aztec/key-store'; import { AddressStore, + AnchoredContractData, CapsuleService, CapsuleStore, type ContractStore, @@ -383,11 +384,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.' @@ -402,7 +410,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, @@ -442,7 +449,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, @@ -509,7 +516,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, ); @@ -568,7 +575,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()}`); @@ -619,16 +632,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); @@ -721,7 +739,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()}`); @@ -770,13 +794,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, @@ -816,7 +846,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`); } @@ -827,7 +867,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 }); @@ -842,7 +881,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 dcf23c46ad18..1b7b170a2e7a 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 { MessageContextService } 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 messageContextService: MessageContextService, ) {} @@ -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 messageContextService = new MessageContextService(node); - return new this(node, synchronizer, archiver, anchorBlockStore, contractSyncService, messageContextService); + return new this( + node, + synchronizer, + archiver, + anchorBlockStore, + contractSyncService, + contractClassService, + messageContextService, + ); } /** 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 54edc95bea7f..41263c5c5d8d 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, @@ -704,6 +705,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!, + ); this.oracleHandler = new TXEPrivateExecutionOracle({ argsHash: Fr.ZERO, txContext: new TxContext(this.chainId, this.version, gasSettings), @@ -715,7 +721,7 @@ export class TXESession implements TXESessionStateHandler { executionCache: new HashedValuesCache(), noteCache, taggingIndexCache, - contractStore: this.contractStore, + anchoredContractData, noteStore: this.noteStore, keyStore: this.keyStore, addressStore: this.addressStore, @@ -809,7 +815,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, @@ -899,7 +909,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`); } @@ -916,7 +937,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..3a2a2ea3b68c 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 { @@ -45,11 +46,14 @@ export class TXEPublicContractDataSource implements ContractDataSource { async getContractArtifact(address: AztecAddress): Promise { const instance = await this.contractStore.getContractInstance(address); - return instance && this.contractStore.getContractArtifact(instance.currentContractClassId); + // TXE has no contract updates, so the current class always equals the original. + 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.contractStore.getContractInstance(address); + // TXE has no contract updates, so the current class always equals the original. + 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 7e9179b7ee72..84b0b52e2df6 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 ContractInstanceWithAddress, type NodeInfo, computePartialAddress } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import { Gas, GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { @@ -355,32 +350,13 @@ export abstract class BaseWallet implements Wallet { artifact?: ContractArtifact, secretKey?: Fr, ): 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 }); + // 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 (secretKey) { await this.pxe.registerAccount(secretKey, await computePartialAddress(instance)); @@ -564,7 +540,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 79f9eb32db24..5084c54f4194 100644 --- a/yarn-project/wallets/src/embedded/embedded_wallet.ts +++ b/yarn-project/wallets/src/embedded/embedded_wallet.ts @@ -26,9 +26,9 @@ 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 { deriveSigningKey } from '@aztec/stdlib/keys'; @@ -261,17 +261,6 @@ export class EmbeddedWallet extends BaseWallet { return super.getPrivateEvents(eventDef, eventFilter); } - public override async registerContract( - instance: ContractInstanceWithAddress, - artifact?: ContractArtifact, - secretKey?: Fr, - ): 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, secretKey); - } - /** * Hashes and registers the stub class for every supported account type with PXE, populating * stubClassIds. Called on wallet initialization. From 6a610e20ca123fb5fbceebbffba4f90eaa9a50ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 25 Jun 2026 17:23:29 +0000 Subject: [PATCH 02/11] Fix wallet iface --- yarn-project/aztec.js/src/wallet/wallet.test.ts | 16 ++++++++-------- yarn-project/aztec.js/src/wallet/wallet.ts | 10 ++++------ .../wallet-sdk/src/base-wallet/base_wallet.ts | 6 +++--- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index ddcdc5719d6b..3189090de0e0 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,12 +134,11 @@ 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(), @@ -148,7 +147,6 @@ describe('WalletSchema', () => { 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), @@ -335,12 +333,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(), @@ -471,11 +468,14 @@ class MockWallet implements Wallet { return [{ alias: 'account1', item: await AztecAddress.random() }]; } - async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise { + 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(), diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index 1d242360002f..e148cd5c989e 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -14,8 +14,6 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractInstancePreimageWithAddress, ContractInstancePreimageWithAddressSchema, - type ContractInstanceWithAddress, - ContractInstanceWithAddressSchema, } from '@aztec/stdlib/contract'; import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { LogCursor, refineTxHashAndRange } from '@aztec/stdlib/logs'; @@ -281,10 +279,10 @@ export type Wallet = { getAddressBook(): Promise[]>; getAccounts(): Promise[]>; registerContract( - instance: ContractInstanceWithAddress, + instance: ContractInstancePreimageWithAddress, artifact?: ContractArtifact, secretKey?: Fr, - ): 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 @@ -583,8 +581,8 @@ const WalletMethodSchemas = { output: z.array(z.object({ alias: z.string(), item: schemas.AztecAddress })), }), registerContract: z.function({ - input: z.tuple([ContractInstanceWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]), - output: ContractInstanceWithAddressSchema, + input: z.tuple([ContractInstancePreimageWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]), + output: ContractInstancePreimageWithAddressSchema, }), registerContractClass: z.function({ input: z.tuple([ContractArtifactSchema]), output: z.void() }), simulateTx: z.function({ 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 84b0b52e2df6..e0ff2adc6452 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -41,7 +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 } from '@aztec/stdlib/contract'; +import { type ContractInstancePreimageWithAddress, type NodeInfo, computePartialAddress } from '@aztec/stdlib/contract'; import { SimulationError } from '@aztec/stdlib/errors'; import { Gas, GasFees, GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas'; import { @@ -346,10 +346,10 @@ export abstract class BaseWallet implements Wallet { } async registerContract( - instance: ContractInstanceWithAddress, + instance: ContractInstancePreimageWithAddress, artifact?: ContractArtifact, secretKey?: Fr, - ): Promise { + ): 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. From c43ef751b9c7cf352dc806c71f5e9cd7716ba1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 25 Jun 2026 19:44:55 -0300 Subject: [PATCH 03/11] Update yarn-project/pxe/src/contract/contract_class_service.ts Co-authored-by: Nicolas Chamo --- yarn-project/pxe/src/contract/contract_class_service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/contract/contract_class_service.ts b/yarn-project/pxe/src/contract/contract_class_service.ts index b371df8278ea..457f964c994e 100644 --- a/yarn-project/pxe/src/contract/contract_class_service.ts +++ b/yarn-project/pxe/src/contract/contract_class_service.ts @@ -16,7 +16,7 @@ import type { ContractStore } from '../storage/contract_store/contract_store.js' export class ContractClassService { /** * Class ids are cached per `(address, anchorHash)`. This avoid unnecessary network roundtrips in scenarios where - * multiple exeuctions are done on the same anchor block (e.g. simulation followed by witgen), or when the same + * 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. */ From 5eccf3bb2904191c8c4a3ad5f3469dd4c4853457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 26 Jun 2026 20:36:47 +0000 Subject: [PATCH 04/11] docs: update webapp tutorial for split registerContract API registerContract no longer takes an artifact; register the class separately via registerContractClass. --- docs/examples/webapp-tutorial/src/fees.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 5503c4ffb6e3632e0a51f24c2554b1509d71a715 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 26 Jun 2026 21:22:23 +0000 Subject: [PATCH 05/11] simplify txe instance retrieval --- .../txe/src/utils/txe_public_contract_data_source.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 3a2a2ea3b68c..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 @@ -45,14 +45,12 @@ export class TXEPublicContractDataSource implements ContractDataSource { } async getContractArtifact(address: AztecAddress): Promise { - const instance = await this.contractStore.getContractInstance(address); - // TXE has no contract updates, so the current class always equals the original. + const instance = await this.getContract(address); return instance && this.contractStore.getContractArtifact(instance.originalContractClassId); } async getDebugFunctionName(address: AztecAddress, selector: FunctionSelector): Promise { - const instance = await this.contractStore.getContractInstance(address); - // TXE has no contract updates, so the current class always equals the original. + const instance = await this.getContract(address); return instance && this.contractStore.getDebugFunctionName(instance.originalContractClassId, selector); } From fdc38ba6e9b9ebc87a63f9758db77ccc7f8ea5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 26 Jun 2026 21:23:02 +0000 Subject: [PATCH 06/11] remove extra fetch --- yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a9a4027bf66e..55574bf24c6a 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 @@ -743,7 +743,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl processedTx.revertReason, this.contractStore, this.stateMachine.contractClassService, - await this.stateMachine.anchorBlockStore.getBlockHeader(), + anchorBlockHeader, this.logger, ); // eslint-disable-next-line no-empty From 0419fbd1f72184b41eb174e1ecccde12cc9d91ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 26 Jun 2026 21:23:48 +0000 Subject: [PATCH 07/11] make props readonly --- .../contract_function_simulator/anchored_contract_data.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 81ff6875a077..f3beeb5f6d4d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts +++ b/yarn-project/pxe/src/contract_function_simulator/anchored_contract_data.ts @@ -19,10 +19,10 @@ import type { ContractStore } from '../storage/contract_store/contract_store.js' */ export class AnchoredContractData { constructor( - private store: ContractStore, - private contractClassService: ContractClassService, - private anchorBlockHeader: BlockHeader, - private overrides?: ContractOverrides, + 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. */ From 9dfb1d30a94cb3b13d6fd1285c2457c63933cc73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Fri, 26 Jun 2026 21:40:50 +0000 Subject: [PATCH 08/11] change wallet instance type for preimage --- .../aztec.js/src/contract/contract.test.ts | 15 ++------ .../src/contract/deploy_method.test.ts | 4 +-- .../aztec.js/src/wallet/wallet.test.ts | 34 ++----------------- yarn-project/aztec.js/src/wallet/wallet.ts | 25 +++++++------- .../src/test-wallet/worker_wallet.ts | 8 ++--- .../wallet-sdk/src/base-wallet/base_wallet.ts | 7 ++-- 6 files changed, 25 insertions(+), 68 deletions(-) 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/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index 3189090de0e0..f17de294832d 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.test.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts @@ -144,17 +144,7 @@ describe('WalletSchema', () => { immutablesHash: Fr.random(), publicKeys: PublicKeys.default(), }; - const result = await context.client.registerContract(mockInstance, mockArtifact, Fr.random()); - expect(result).toEqual({ - address: expect.any(AztecAddress), - 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 () => { @@ -394,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) }); @@ -468,22 +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(), - 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 e148cd5c989e..fa82f24b0f19 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -12,6 +12,8 @@ import { import { AuthWitness } from '@aztec/stdlib/auth-witness'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + type ContractInstancePreimage, + ContractInstancePreimageSchema, type ContractInstancePreimageWithAddress, ContractInstancePreimageWithAddressSchema, } from '@aztec/stdlib/contract'; @@ -278,11 +280,7 @@ export type Wallet = { registerSender(address: AztecAddress, alias?: string): Promise; getAddressBook(): Promise[]>; getAccounts(): Promise[]>; - registerContract( - instance: ContractInstancePreimageWithAddress, - artifact?: ContractArtifact, - secretKey?: Fr, - ): Promise; + registerContract(instance: ContractInstancePreimage, artifact?: ContractArtifact, secretKey?: Fr): 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 @@ -581,8 +579,8 @@ const WalletMethodSchemas = { output: z.array(z.object({ alias: z.string(), item: schemas.AztecAddress })), }), registerContract: z.function({ - input: z.tuple([ContractInstancePreimageWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]), - output: ContractInstancePreimageWithAddressSchema, + input: z.tuple([ContractInstancePreimageSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]), + output: z.void(), }), registerContractClass: z.function({ input: z.tuple([ContractArtifactSchema]), output: z.void() }), simulateTx: z.function({ @@ -634,12 +632,15 @@ function createBatchSchemas - 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/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/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index e0ff2adc6452..0d7cadb578af 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -41,7 +41,7 @@ import { } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type ContractInstancePreimageWithAddress, type NodeInfo, computePartialAddress } 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 { @@ -346,10 +346,10 @@ export abstract class BaseWallet implements Wallet { } async registerContract( - instance: ContractInstancePreimageWithAddress, + instance: ContractInstancePreimage, artifact?: ContractArtifact, secretKey?: Fr, - ): Promise { + ): 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. @@ -361,7 +361,6 @@ export abstract class BaseWallet implements Wallet { if (secretKey) { await this.pxe.registerAccount(secretKey, await computePartialAddress(instance)); } - return instance; } registerContractClass(artifact: ContractArtifact): Promise { From e44560edade5127f85f5d4524fe3ac45392c8c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Wed, 1 Jul 2026 21:38:34 +0000 Subject: [PATCH 09/11] fix build --- docs/docs-developers/docs/aztec-js/how_to_test.md | 2 +- .../docs/resources/migration_notes.md | 2 +- .../oracle/private_execution_oracle.test.ts | 15 +++++++-------- .../oracle/utility_execution_oracle.ts | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) 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 32f7fcbe01d8..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 on-chain 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. +`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 8001e05678ec..7e452b6da8be 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -25,7 +25,7 @@ Registering classes and instances are now separate, unvalidated operations. `reg 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 on-chain upgrade, register the new artifact instead of calling `updateContract`: +- 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); 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 9faf7f111513..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 @@ -25,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'; @@ -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 }), 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 8751e19de8fd..868a4a33d893 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 @@ -49,7 +49,6 @@ 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 } 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'; From 7bfd3fb3fd29fd12e1b773f84e15f6465c1d1e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 2 Jul 2026 13:00:46 +0000 Subject: [PATCH 10/11] fix: align contract store test with actual error message --- .../pxe/src/storage/contract_store/contract_store.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 84655b1c91bf..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 @@ -83,10 +83,10 @@ describe('ContractStore', () => { // 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 resolved class id', + 'does not match the class id', ); await expect(contractStore.getFunctionArtifactWithDebugMetadata(classId, missingSelector)).rejects.toThrow( - 'does not match the resolved class id', + 'does not match the class id', ); }); From 0752c0992de5844278227886b3f618bf2125b961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Venturo?= Date: Thu, 2 Jul 2026 20:19:42 +0000 Subject: [PATCH 11/11] fix snapshot test --- .../__snapshots__/ContractStore.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 83142c13cf1b..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": "030000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000004f0000000000000000000000000000000000000000000000000000000000000059000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000004300000000000000000000000000000000000000000000000000000000000000470000000000000000000000000000000000000000000000000000000000000049" + "value": "020000000000000000000000000000000000000000000000000000000000000049000000000000000000000000000000000000000000000000000000000000004f0000000000000000000000000000000000000000000000000000000000000059000000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000670000000000000000000000000000000000000000000000000000000000000029000000000000000000000000000000000000000000000000000000000000002f0000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000004300000000000000000000000000000000000000000000000000000000000000470000000000000000000000000000000000000000000000000000000000000049" } ] }