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 1ae388802997..9b9ccfba7f95 100644 --- a/docs/docs-developers/docs/aztec-js/how_to_test.md +++ b/docs/docs-developers/docs/aztec-js/how_to_test.md @@ -83,6 +83,24 @@ const result = await contract.methods.read_balance(account).simulate({ Use this to set up state preconditions, reproduce production bugs against pinned storage, or exercise rare value branches without orchestrating the contract calls that produce them. +### 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. + +```typescript +import { fastForwardContractUpdate } from '@aztec/aztec.js'; + +const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: upgradedClass.id, + node, +}); + +const result = await contract.methods.upgraded_method().simulate({ overrides }); +``` + +Use this to test code paths that only execute after an upgrade, without orchestrating the full delayed-mutable upgrade flow. + ## Further reading - [How to read contract data](./how_to_read_data.md) diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index f1333f13d158..c2f354b031be 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -185,6 +185,19 @@ Direct callers of the `SimulationOverrides` constructor must switch from a posit + new SimulationOverrides({ contracts }); ``` +`overrides.contracts` swaps contract instances in the simulator's contract DB — useful for simulating a contract being on a different class than the one it was deployed with. To simulate a complete onchain upgrade flow, use the `fastForwardContractUpdate` helper which returns a `SimulationOverrides` covering both registry storage rewrites and the upgraded instance entry: + +```typescript +import { fastForwardContractUpdate } from '@aztec/aztec.js'; + +const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: upgradedClass.id, + node, +}); +const result = await contract.methods.upgraded_method().simulate({ overrides }); +``` + ### [PXE] `proveTx` takes an options bag `PXE.proveTx` used to accept `scopes` as a positional argument; it now takes an options bag consistent with `simulateTx` and `profileTx`, and adds an optional `senderForTags` field. Update direct callers: diff --git a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml index 71998b27abbb..a84b6fa07296 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml @@ -6,3 +6,4 @@ type = "contract" [dependencies] aztec = { path = "../../../../aztec-nr/aztec" } +ecdsa_public_key_note = { path = "../../libs/ecdsa_public_key_note" } diff --git a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr index ae48e7a9595f..aa497522c81d 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr @@ -3,17 +3,26 @@ use aztec::macros::aztec; // Stub account contract for ECDSA accounts (both secp256k1 and secp256r1) used during simulation. // Matches the constructor signature of EcdsaKAccount / EcdsaRAccount so that deployment // simulations using this stub as an override do not fail on selector lookup. -// See simulated_account_contract for the base stub without a constructor. +// Mirrors the EcdsaKAccount/EcdsaRAccount storage layout so simulation sync can decode +// the real account's signing_public_key note. #[aztec] pub contract SimulatedEcdsaAccount { use aztec::{ authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, context::PrivateContext, - macros::functions::{allow_phase_change, external, view}, + macros::{functions::{allow_phase_change, external, view}, storage::storage}, messages::encoding::MESSAGE_CIPHERTEXT_LEN, oracle::random::random, + state_vars::SinglePrivateImmutable, }; + use ecdsa_public_key_note::EcdsaPublicKeyNote; + + #[storage] + struct Storage { + signing_public_key: SinglePrivateImmutable, + } + // Stub constructor matching the EcdsaKAccount / EcdsaRAccount constructor signature. // Does NOT use #[initializer] so that the macro does not inject // assert_initialization_matches_address_preimage_private, which would fail during kernelless @@ -67,9 +76,4 @@ pub contract SimulatedEcdsaAccount { fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool { true } - - #[external("utility")] - unconstrained fn sync_state() { - assert(false, "BUG ALERT: sync_state on a simulated account contract should never be triggered."); - } } diff --git a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr index 9d570e78bc50..daa631c90a1b 100644 --- a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr @@ -1,19 +1,30 @@ +mod public_key_note; + use aztec::macros::aztec; // Stub account contract for Schnorr accounts used during simulation. // Matches the constructor signature of SchnorrAccount so that deployment // simulations using this stub as an override do not fail on selector lookup. -// See simulated_account_contract for the base stub without a constructor. +// Mirrors the SchnorrAccount storage layout so simulation sync can decode +// the real account's signing_public_key note. #[aztec] pub contract SimulatedSchnorrAccount { use aztec::{ authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload}, context::PrivateContext, - macros::functions::{allow_phase_change, external, view}, + macros::{functions::{allow_phase_change, external, view}, storage::storage}, messages::encoding::MESSAGE_CIPHERTEXT_LEN, oracle::random::random, + state_vars::SinglePrivateImmutable, }; + use crate::public_key_note::PublicKeyNote; + + #[storage] + struct Storage { + signing_public_key: SinglePrivateImmutable, + } + // Stub constructor matching the SchnorrAccount constructor signature. // Does NOT use #[initializer] so that the macro does not inject // assert_initialization_matches_address_preimage_private, which would fail during kernelless @@ -67,9 +78,4 @@ pub contract SimulatedSchnorrAccount { fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool { true } - - #[external("utility")] - unconstrained fn sync_state() { - assert(false, "BUG ALERT: sync_state on a simulated account contract should never be triggered."); - } } diff --git a/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/public_key_note.nr b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/public_key_note.nr new file mode 100644 index 000000000000..d1acbb62f73c --- /dev/null +++ b/noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/public_key_note.nr @@ -0,0 +1,10 @@ +use aztec::{macros::notes::note, protocol::traits::Packable}; + +// Mirrors PublicKeyNote in schnorr_account_contract so the stub's auto-generated _compute_note_hash +// can decode the real account's signing_public_key note during simulation sync. +#[derive(Eq, Packable)] +#[note] +pub struct PublicKeyNote { + pub x: Field, + pub y: Field, +} diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index a396db829f3d..6a122aa453d0 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -39,7 +39,7 @@ import { SequencerClient, type SequencerPublisher, } from '@aztec/sequencer-client'; -import { PublicProcessorFactory } from '@aztec/simulator/server'; +import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server'; import { AttestationsBlockWatcher, EpochPruneWatcher, @@ -1508,7 +1508,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads, }), }); - const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config); + const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()); + if (overrides?.contracts) { + contractsDB.addContracts(Object.values(overrides.contracts).map(({ instance }) => instance)); + } + const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config, contractsDB); // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]); diff --git a/yarn-project/aztec.js/src/api/contract.ts b/yarn-project/aztec.js/src/api/contract.ts index d351abf2b665..dfb1a861ac27 100644 --- a/yarn-project/aztec.js/src/api/contract.ts +++ b/yarn-project/aztec.js/src/api/contract.ts @@ -74,6 +74,7 @@ export { } from '../contract/deploy_method.js'; export { waitForProven, type WaitForProvenOpts, DefaultWaitForProvenOpts } from '../contract/wait_for_proven.js'; export { getGasLimits } from '../contract/get_gas_limits.js'; +export { fastForwardContractUpdate } from '../contract/fastforward_contract_update.js'; export { type PartialAddress, diff --git a/yarn-project/aztec.js/src/contract/contract.test.ts b/yarn-project/aztec.js/src/contract/contract.test.ts index 09ecad876a02..9609755d1d6c 100644 --- a/yarn-project/aztec.js/src/contract/contract.test.ts +++ b/yarn-project/aztec.js/src/contract/contract.test.ts @@ -76,6 +76,17 @@ describe('Contract Class', () => { expect(result).toBe(42n); }); + it('throws when overrides are passed to a utility function simulation', async () => { + const fooContract = Contract.at(contractAddress, testContractArtifact, wallet); + await expect( + fooContract.methods.qux(123n).simulate({ + from: account.getAddress(), + overrides: { publicStorage: [{ contract: contractAddress, slot: new Fr(1), value: new Fr(42) }] }, + }), + ).rejects.toThrow(/not supported for utility/); + expect(wallet.executeUtility).not.toHaveBeenCalled(); + }); + it('should extract offchain messages with anchor block timestamp on simulate', async () => { const recipient = await AztecAddress.random(); const msgPayload = [Fr.random(), Fr.random()]; diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index d4966bf76db2..563eb58f7424 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -129,6 +129,9 @@ export class ContractFunctionInteraction extends BaseContractInteraction { ): Promise { // docs:end:simulate if (this.functionDao.functionType == FunctionType.UTILITY) { + if (options.overrides?.publicStorage?.length || options.overrides?.contracts) { + throw new Error('overrides are not supported for utility function simulation.'); + } const call = await this.getFunctionCall(); const scopes = [...(options.additionalScopes ?? [])]; const utilityResult = await this.wallet.executeUtility(call, { diff --git a/yarn-project/aztec.js/src/contract/fastforward_contract_update.test.ts b/yarn-project/aztec.js/src/contract/fastforward_contract_update.test.ts new file mode 100644 index 000000000000..7b5d2b65f305 --- /dev/null +++ b/yarn-project/aztec.js/src/contract/fastforward_contract_update.test.ts @@ -0,0 +1,91 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { ProtocolContractAddress } from '@aztec/protocol-contracts'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { SerializableContractInstance } from '@aztec/stdlib/contract'; +import { + DELAYED_PUBLIC_MUTABLE_VALUES_LEN, + DelayedPublicMutableValuesWithHash, +} from '@aztec/stdlib/delayed-public-mutable'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; + +import { type MockProxy, mock } from 'jest-mock-extended'; + +import { fastForwardContractUpdate } from './fastforward_contract_update.js'; + +describe('fastForwardContractUpdate', () => { + let node: MockProxy; + let instanceAddress: AztecAddress; + let originalClassId: Fr; + let newClassId: Fr; + + beforeEach(async () => { + node = mock(); + instanceAddress = await AztecAddress.random(); + originalClassId = Fr.random(); + newClassId = Fr.random(); + + const instance = ( + await SerializableContractInstance.random({ + currentContractClassId: originalClassId, + originalContractClassId: originalClassId, + }) + ).withAddress(instanceAddress); + + node.getContract.mockResolvedValue(instance); + node.getContractClass.mockResolvedValue({ + id: newClassId, + artifactHash: Fr.random(), + packedBytecodeCommitments: [], + privateFunctionsRoot: Fr.random(), + publicBytecodeCommitment: Fr.random(), + version: 1, + privateFunctions: [], + utilityFunctions: [], + publicFunctions: [], + packedBytecode: Buffer.alloc(0), + } as any); + }); + + it('produces overrides with bumped currentContractClassId and registry storage writes', async () => { + const overrides = await fastForwardContractUpdate({ instanceAddress, newClassId, node }); + + const upgraded = overrides.contracts?.[instanceAddress.toString()]; + expect(upgraded).toBeDefined(); + expect(upgraded!.instance.address).toEqual(instanceAddress); + expect(upgraded!.instance.currentContractClassId).toEqual(newClassId); + expect(upgraded!.instance.originalContractClassId).toEqual(originalClassId); + + const expectedSlots = await DelayedPublicMutableValuesWithHash.getContractUpdateSlots(instanceAddress); + expect(overrides.publicStorage).toHaveLength(DELAYED_PUBLIC_MUTABLE_VALUES_LEN + 1); + for (const entry of overrides.publicStorage!) { + expect(entry.contract).toEqual(ProtocolContractAddress.ContractInstanceRegistry); + } + const baseSlot = expectedSlots.delayedPublicMutableSlot; + expect(overrides.publicStorage![0].slot).toEqual(baseSlot); + expect(overrides.publicStorage![overrides.publicStorage!.length - 1].slot).toEqual( + expectedSlots.delayedPublicMutableHashSlot, + ); + }); + + it('throws when the instance is not deployed', async () => { + node.getContract.mockResolvedValue(undefined); + await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/not deployed/); + }); + + it('throws when the new class is not registered', async () => { + node.getContractClass.mockResolvedValue(undefined); + await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/not registered/); + }); + + it('throws when the instance is already on the target class', async () => { + const sameClassInstance = ( + await SerializableContractInstance.random({ + currentContractClassId: newClassId, + originalContractClassId: originalClassId, + }) + ).withAddress(instanceAddress); + node.getContract.mockResolvedValue(sameClassInstance); + + await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/already on class/); + }); +}); diff --git a/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts b/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts new file mode 100644 index 000000000000..40e9d19aefb1 --- /dev/null +++ b/yarn-project/aztec.js/src/contract/fastforward_contract_update.ts @@ -0,0 +1,71 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { ProtocolContractAddress } from '@aztec/protocol-contracts'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { + DelayedPublicMutableValuesWithHash, + ScheduledDelayChange, + ScheduledValueChange, +} from '@aztec/stdlib/delayed-public-mutable'; +import type { AztecNode } from '@aztec/stdlib/interfaces/client'; +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): + * + * - `publicStorage` rewrites the `ContractInstanceRegistry`'s delayed-public-mutable storage so the AVM's + * `UpdateCheck` resolves to the new class id. + * - `contracts` swaps the deployed instance for one whose `currentContractClassId` is bumped to the new class. + * + * The new class must already be registered on chain. + * + * @throws If the instance is not deployed, the class is not registered on chain, or the instance is already on the target class. + */ +export async function fastForwardContractUpdate(args: { + /** Address of the deployed instance to upgrade. */ + instanceAddress: AztecAddress; + /** ID of the (already-registered) class to upgrade to. */ + newClassId: Fr; + /** Node used to fetch the existing instance and validate the class is registered. */ + node: AztecNode; +}): Promise { + const { instanceAddress, newClassId, node } = args; + + const instance = await node.getContract(instanceAddress); + if (!instance) { + throw new Error(`Instance not deployed at ${instanceAddress}. Deploy it before fast-forwarding an update.`); + } + + const klass = await node.getContractClass(newClassId); + if (!klass) { + throw new Error( + `Contract class ${newClassId} is not registered on chain. Publish it before fast-forwarding to it.`, + ); + } + + if (instance.currentContractClassId.equals(newClassId)) { + throw new Error(`Instance ${instanceAddress} is already on class ${newClassId}. Nothing to fast-forward.`); + } + + // Build the SVC the same way `ContractInstanceRegistry::update` would have, but with a timestamp_of_change + // safely in the past so the AVM's UpdateCheck resolves to the post-upgrade class id at any sim timestamp. + const svc = new ScheduledValueChange(/*previous=*/ [new Fr(0)], /*post=*/ [newClassId], /*timestampOfChange=*/ 1n); + const sdc = ScheduledDelayChange.empty(); + const dpmv = new DelayedPublicMutableValuesWithHash(svc, sdc); + + const { delayedPublicMutableSlot } = await DelayedPublicMutableValuesWithHash.getContractUpdateSlots(instanceAddress); + const fields = await dpmv.toFields(); + + const publicStorage = fields.map((value, i) => ({ + contract: ProtocolContractAddress.ContractInstanceRegistry, + slot: delayedPublicMutableSlot.add(new Fr(i)), + value, + })); + + const upgradedInstance = { ...instance, currentContractClassId: newClassId }; + + return new SimulationOverrides({ + publicStorage, + contracts: { [instanceAddress.toString()]: { instance: upgradedInstance } }, + }); +} diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index a8b507dd21d7..7e82f7eb9b68 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -3,8 +3,7 @@ import { BatchCall, type ContractInstanceWithAddress } from '@aztec/aztec.js/con import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; import { TxExecutionResult } from '@aztec/aztec.js/tx'; -import type { PublicStorageOverride } from '@aztec/aztec.js/wallet'; -import type { Wallet } from '@aztec/aztec.js/wallet'; +import type { PublicStorageOverride, Wallet } from '@aztec/aztec.js/wallet'; import { AvmInitializerTestContract } from '@aztec/noir-test-contracts.js/AvmInitializerTest'; import { AvmTestContract } from '@aztec/noir-test-contracts.js/AvmTest'; 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 192930eb8888..1e5bfd82bbf8 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 @@ -1,5 +1,5 @@ import { getSchnorrAccountContractAddress } from '@aztec/accounts/schnorr'; -import { getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; +import { fastForwardContractUpdate, getContractClassFromArtifact } from '@aztec/aztec.js/contracts'; import { publishContractClass } from '@aztec/aztec.js/deployment'; import { Fr } from '@aztec/aztec.js/fields'; import type { AztecNode } from '@aztec/aztec.js/node'; @@ -177,4 +177,59 @@ describe('e2e_contract_updates', () => { 'Could not update contract to a class different from the current one', ); }); + + // UpdatableContract's `set_public_value(Field)` and UpdatedContract's `set_public_value()` + // have different function selectors. Without an upgrade, only the deployed Updatable's + // (Field) selector exists; with a fastForwardContractUpdate override, the AVM dispatches + // against UpdatedContract's bytecode and the no-args selector resolves. + it('fastForwardContractUpdate enables simulation of post-upgrade public calls', async () => { + // Local construction with the new artifact - no PXE/wallet side effect, no chain mutation. + const updatedContract = UpdatedContract.at(contract.address, wallet); + + // Without overrides, UpdatedContract's no-args selector doesn't match the deployed class. + await expect( + updatedContract.methods.set_public_value().simulate({ from: defaultAccountAddress }), + ).rejects.toThrow(); + + // With the fastForwardContractUpdate overrides, the AVM dispatches against UpdatedContract's + // bytecode and the call simulates successfully. + const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: updatedContractClassId, + node: aztecNode, + }); + await expect( + updatedContract.methods.set_public_value().simulate({ from: defaultAccountAddress, overrides }), + ).resolves.toBeDefined(); + + // Chain state is untouched: the original Updatable's set_public_value(Field) still simulates fine. + await expect( + contract.methods.set_public_value(5678n).simulate({ from: defaultAccountAddress }), + ).resolves.toBeDefined(); + }); + + // UpdatedContract.set_private_value is a private function that doesn't exist on UpdatableContract. + // For PXE-side ACIR dispatch to find it, the artifact must be registered locally first via + // wallet.registerContractClass; the helper itself only takes the class id. + it('fastForwardContractUpdate enables simulation of post-upgrade private calls', async () => { + const updatedContract = UpdatedContract.at(contract.address, wallet); + + // Without overrides (and without local artifact registration), the new private function isn't + // available on the deployed class. + await expect( + updatedContract.methods.set_private_value().simulate({ from: defaultAccountAddress }), + ).rejects.toThrow(); + + // Register the new artifact in the local PXE so the ACIR simulator can find its private functions. + await wallet.registerContractClass(UpdatedContract.artifact); + + const overrides = await fastForwardContractUpdate({ + instanceAddress: contract.address, + newClassId: updatedContractClassId, + node: aztecNode, + }); + await expect( + updatedContract.methods.set_private_value().simulate({ from: defaultAccountAddress, overrides }), + ).resolves.toBeDefined(); + }); }); diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts index ac7e194201c2..015764cb7ed9 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts @@ -115,18 +115,6 @@ describe('ContractSyncService', () => { expectSyncedScopes([scopeA], [scopeB]); }); - it('skips sync for excluded contract in the same job', async () => { - service.setExcludedFromSync(jobId, new Set([contractAddress.toString()])); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectNoSync(); - }); - - it('does not skip sync for excluded contract in a different job', async () => { - service.setExcludedFromSync('other-job', new Set([contractAddress.toString()])); - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes([scopeA]); - }); - it('concurrent calls for same contract+scope share one sync promise', async () => { const p1 = service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [ scopeA, @@ -170,16 +158,6 @@ describe('ContractSyncService', () => { }); describe('commit', () => { - it('clears exclusions for the given job', async () => { - service.setExcludedFromSync(jobId, new Set([contractAddress.toString()])); - await service.commit(jobId); - - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - // When exclusions are set, contract sync is skipped. We verify the exclusions were cleared by confirming that sync - // was actually triggered. - expectSyncedScopes([scopeA]); - }); - it('does not clear sync cache', async () => { await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); await service.commit(jobId); @@ -197,32 +175,6 @@ describe('ContractSyncService', () => { // We check that the sync cache was cleared by checking that the sync was triggered twice. expectSyncedScopes([scopeA], [scopeA]); }); - - it('clears exclusions for the given job', async () => { - service.setExcludedFromSync(jobId, new Set([contractAddress.toString()])); - await service.discardStaged(jobId); - - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - // When exclusions are set, contract sync is skipped. We verify the exclusions were cleared by confirming that sync - // was actually triggered. - expectSyncedScopes([scopeA]); - }); - - it('preserves exclusions for other jobs', async () => { - service.setExcludedFromSync(jobId, new Set([contractAddress.toString()])); - service.setExcludedFromSync('other-job', new Set([contractAddress.toString()])); - await service.discardStaged(jobId); - - // jobId exclusion cleared, sync proceeds - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]); - expectSyncedScopes([scopeA]); - - // other-job exclusion still active, sync skipped - await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, 'other-job', [ - scopeA, - ]); - expectSyncedScopes([scopeA]); - }); }); describe('class ID verification deduplication', () => { diff --git a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts index 39b29ee3cc92..6db0a2d11e3c 100644 --- a/yarn-project/pxe/src/contract_sync/contract_sync_service.ts +++ b/yarn-project/pxe/src/contract_sync/contract_sync_service.ts @@ -31,9 +31,6 @@ export class ContractSyncService implements StagedStore { // class ID verification is scope-independent. Cleared on wipe/discard. private verifiedClassIds: Map> = new Map(); - // Per-job excluded contract addresses - these contracts should not be synced. - private excludedFromSync: Map> = new Map(); - // Bounds the number of scope syncs running concurrently. Scopes beyond this limit queue here. Sized to trade off // parallelism on non-ACIR work (node RPC, note store reads) against memory pressure from concurrent circuit // execution. @@ -46,11 +43,6 @@ export class ContractSyncService implements StagedStore { private log: Logger, ) {} - /** Sets contracts that should be skipped during sync for a specific job. */ - setExcludedFromSync(jobId: string, addresses: Set): void { - this.excludedFromSync.set(jobId, addresses); - } - /** * Ensures a contract's private state is synchronized and that the PXE holds the current class artifact. * Uses a cache to avoid redundant sync operations - the cache is wiped when the anchor block changes. @@ -68,10 +60,6 @@ export class ContractSyncService implements StagedStore { jobId: string, scopes: AztecAddress[], ): Promise { - if (this.#shouldSkipSync(jobId, contractAddress)) { - return; - } - this.#startSyncIfNeeded( contractAddress, scopes, @@ -108,24 +96,17 @@ export class ContractSyncService implements StagedStore { this.verifiedClassIds.clear(); } - commit(jobId: string): Promise { - // Clear excluded contracts for this job - this.excludedFromSync.delete(jobId); + commit(_jobId: string): Promise { return Promise.resolve(); } - discardStaged(jobId: string): Promise { + discardStaged(_jobId: string): Promise { // 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.verifiedClassIds.clear(); - this.excludedFromSync.delete(jobId); return Promise.resolve(); } - /** Returns true if sync should be skipped for this contract */ - #shouldSkipSync(jobId: string, contractAddress: AztecAddress): boolean { - return !!this.excludedFromSync.get(jobId)?.has(contractAddress.toString()); - } /** * If there are unsynced scopes, starts one sync per scope (bounded by #syncSlot) and stores each promise in the diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 5c678f8cacfe..71a08d0a83f3 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -1002,22 +1002,13 @@ export class PXE { const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader(); const syncTime = syncTimer.ms(); - const overriddenContracts = overrides?.contracts ? new Set(Object.keys(overrides.contracts)) : undefined; - const hasOverriddenContracts = overriddenContracts !== undefined && overriddenContracts.size > 0; - - if (hasOverriddenContracts && !skipKernels) { + if (overrides?.contracts && Object.keys(overrides.contracts).length > 0 && !skipKernels) { throw new Error( 'Simulating with overridden contracts is not compatible with kernel execution. Please set skipKernels to true when simulating with overridden contracts.', ); } const contractFunctionSimulator = this.#getSimulatorForTx(overrides); - if (hasOverriddenContracts) { - // Overridden contracts don't have a sync function, so calling sync on them would fail. - // We exclude them so the sync service skips them entirely. - this.contractSyncService.setExcludedFromSync(jobId, overriddenContracts); - } - // Execution of private functions only; no proving, and no kernel logic. const privateExecutionResult = await this.#executePrivate({ contractFunctionSimulator, diff --git a/yarn-project/simulator/src/public/public_db_sources.ts b/yarn-project/simulator/src/public/public_db_sources.ts index e85dce90c632..6852bf23473d 100644 --- a/yarn-project/simulator/src/public/public_db_sources.ts +++ b/yarn-project/simulator/src/public/public_db_sources.ts @@ -55,7 +55,8 @@ export class PublicContractsDB implements PublicContractsDBInterface { this.log = createLogger('simulator:contracts-data-source', bindings); } - public addContracts(contractDeploymentData: ContractDeploymentData): void { + /** Parses raw log data from the C++/NAPI bridge and inserts the resulting contracts into the current checkpoint. */ + public addContractsFromLogs(contractDeploymentData: ContractDeploymentData): void { const currentState = this.getCurrentState(); this.addContractClassesFromEvents( @@ -71,8 +72,16 @@ export class PublicContractsDB implements PublicContractsDBInterface { public addNewContracts(tx: Tx): void { const contractDeploymentData = AllContractDeploymentData.fromTx(tx); - this.addContracts(contractDeploymentData.getNonRevertibleContractDeploymentData()); - this.addContracts(contractDeploymentData.getRevertibleContractDeploymentData()); + this.addContractsFromLogs(contractDeploymentData.getNonRevertibleContractDeploymentData()); + this.addContractsFromLogs(contractDeploymentData.getRevertibleContractDeploymentData()); + } + + /** Inserts typed contract instances directly into the current checkpoint. */ + public addContracts(contractInstances?: ContractInstanceWithAddress[]): void { + const currentState = this.getCurrentState(); + for (const instance of contractInstances ?? []) { + currentState.addInstance(instance.address, instance); + } } /** diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index f1922dab36cd..bd1932792bc4 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -76,17 +76,15 @@ export class PublicProcessorFactory { /** * Creates a new instance of a PublicProcessor. * @param globalVariables - The global variables for the block being processed. - * @param skipFeeEnforcement - Allows disabling balance checks for fee estimations. + * @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted. * @returns A new instance of a PublicProcessor. */ public create( merkleTree: MerkleTreeWriteOperations, globalVariables: GlobalVariables, config: PublicSimulatorConfig, + contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()), ): PublicProcessor { - const bindings = this.log.getBindings(); - const contractsDB = new PublicContractsDB(this.contractDataSource, bindings); - const guardedFork = new GuardedMerkleTreeOperations(merkleTree); const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config); @@ -97,7 +95,7 @@ export class PublicProcessorFactory { publicTxSimulator, this.dateProvider, this.telemetryClient, - createLogger('simulator:public-processor', bindings), + createLogger('simulator:public-processor', this.log.getBindings()), ); } diff --git a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts b/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts index 9077b8676c1e..21b195484d66 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts @@ -62,8 +62,8 @@ export class ContractProviderForCpp implements ContractProvider { const contractDeploymentData = ContractDeploymentData.fromPlainObject(rawData); // Add contracts to the contracts DB - this.log.trace(`Calling contractsDB.addContracts`); - this.contractsDB.addContracts(contractDeploymentData); + this.log.trace(`Calling contractsDB.addContractsFromLogs`); + this.contractsDB.addContractsFromLogs(contractDeploymentData); }; public getBytecodeCommitment = async (classId: string): Promise => { diff --git a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts index 9756205ae9ab..85fe0129fd04 100644 --- a/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts +++ b/yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts @@ -401,7 +401,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface { // However, things work as expected because later calls to getters on the hintingContractsDB // will pick up the new contracts and will generate the necessary hints. // So, a consumer of the hints will always see the new contracts. - this.contractsDB.addContracts(context.nonRevertibleContractDeploymentData); + this.contractsDB.addContractsFromLogs(context.nonRevertibleContractDeploymentData); } /** @@ -490,7 +490,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface { // However, things work as expected because later calls to getters on the hintingContractsDB // will pick up the new contracts and will generate the necessary hints. // So, a consumer of the hints will always see the new contracts. - this.contractsDB.addContracts(context.revertibleContractDeploymentData); + this.contractsDB.addContractsFromLogs(context.revertibleContractDeploymentData); } private async payFee(context: PublicTxContext) {