Skip to content

Commit 32eb807

Browse files
committed
feat: contractOverrides for private-call simulation
Plumbs the existing PXE-side SimulationOverrides.contracts mechanism through wallet.simulateTx and aztec.js .simulate() options as 'contractOverrides'. This covers private-function dispatch during simulation against an upgraded class - the PXE's ACIR simulator uses the override artifact for the given address instead of its registered one. Together with the existing node-side stateOverrides, .simulate() now handles both private and public call flavors when simulating against an upgraded contract. fastForwardContractUpdate now takes a ContractArtifact and returns both override layers in one blob so callers can spread it across .simulate(): const overrides = await fastForwardContractUpdate({ instanceAddress, newArtifact, node, }); await updatedContract.methods.X().simulate({ ...overrides }); Adds e2e coverage for the private-call path (set_private_value, which only exists on the upgraded class) to demonstrate the full flow.
1 parent 7e12172 commit 32eb807

11 files changed

Lines changed: 154 additions & 61 deletions

File tree

docs/docs-developers/docs/aztec-js/how_to_test.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,21 @@ Use this to:
8989

9090
### Fast-forwarding a contract update
9191

92-
`fastForwardContractUpdate` builds the full set of overrides needed to simulate 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.
92+
`fastForwardContractUpdate` builds the override blobs needed to simulate a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. Mirrors a real `pxe.updateContract` followed by waiting out the upgrade delay.
93+
94+
It returns two coherent override layers - `stateOverrides` (node-side, redirects public-call dispatch) and `contractOverrides` (PXE-side, redirects private-call dispatch) - so a single spread covers any mix of private and public function calls on the upgraded contract.
9395

9496
```typescript
9597
import { fastForwardContractUpdate } from '@aztec/aztec.js';
9698

97-
const stateOverrides = await fastForwardContractUpdate({
99+
const overrides = await fastForwardContractUpdate({
98100
instanceAddress: contract.address,
99-
newClassId: upgradedClass.id,
101+
newArtifact: UpdatedContract.artifact,
100102
node,
101103
});
102104

103-
const result = await contract.methods.upgraded_method().simulate({ stateOverrides });
105+
const upgradedContract = UpdatedContract.at(contract.address, wallet);
106+
const result = await upgradedContract.methods.upgraded_method().simulate({ ...overrides });
104107
```
105108

106109
Use this to test code paths that only execute after an upgrade, without orchestrating the full delayed-mutable upgrade flow.

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@ const result = await contract.methods.read_balance(account).simulate({
2323

2424
The same option flows through `wallet.simulateTx` and eventually to `simulatePublicCalls` RPC on `AztecNode`.
2525

26-
A second override flavor, `contractInstances`, shadows 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 on-chain upgrade flow, use the `fastForwardContractUpdate` helper which produces a coherent set of `publicStorage` and `contractInstances` overrides:
26+
A second override flavor, `contractInstances`, shadows 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.
27+
28+
`.simulate(...)` also accepts `contractOverrides`, which replaces the (instance, artifact) pair the PXE-side ACIR simulator uses for a given address. Required to redirect private-function dispatch when simulating against an upgraded class.
29+
30+
To simulate a complete on-chain upgrade flow, use the `fastForwardContractUpdate` helper. It returns both `stateOverrides` (node-side, public dispatch) and `contractOverrides` (PXE-side, private dispatch) so a single spread covers any mix of call flavors:
2731

2832
```typescript
2933
import { fastForwardContractUpdate } from '@aztec/aztec.js';
3034

31-
const stateOverrides = await fastForwardContractUpdate({
35+
const overrides = await fastForwardContractUpdate({
3236
instanceAddress: contract.address,
33-
newClassId: upgradedClass.id,
37+
newArtifact: UpdatedContract.artifact,
3438
node,
3539
});
36-
const result = await contract.methods.upgraded_method().simulate({ stateOverrides });
40+
const upgradedContract = UpdatedContract.at(contract.address, wallet);
41+
const result = await upgradedContract.methods.upgraded_method().simulate({ ...overrides });
3742
```
3843

3944
### [PXE] `proveTx` takes an options bag

yarn-project/aztec.js/src/contract/contract.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,20 @@ describe('Contract Class', () => {
238238
from: account.getAddress(),
239239
stateOverrides: { publicStorage: [{ contract: contractAddress, slot: new Fr(1), value: new Fr(42) }] },
240240
}),
241-
).rejects.toThrow(/stateOverrides are not supported for utility/);
241+
).rejects.toThrow(/not supported for utility function/);
242+
expect(wallet.executeUtility).not.toHaveBeenCalled();
243+
});
244+
245+
it('throws when contractOverrides are passed to a utility function simulation', async () => {
246+
const fooContract = Contract.at(contractAddress, defaultArtifact, wallet);
247+
await expect(
248+
fooContract.methods.qux(123n).simulate({
249+
from: account.getAddress(),
250+
contractOverrides: {
251+
[contractAddress.toString()]: { instance: contractInstance, artifact: defaultArtifact },
252+
},
253+
}),
254+
).rejects.toThrow(/not supported for utility function/);
242255
expect(wallet.executeUtility).not.toHaveBeenCalled();
243256
});
244257

yarn-project/aztec.js/src/contract/contract_function_interaction.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,11 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
130130
// docs:end:simulate
131131
if (this.functionDao.functionType == FunctionType.UTILITY) {
132132
if (
133-
options.stateOverrides &&
134-
(options.stateOverrides.publicStorage?.length || options.stateOverrides.contractInstances?.length)
133+
(options.stateOverrides &&
134+
(options.stateOverrides.publicStorage?.length || options.stateOverrides.contractInstances?.length)) ||
135+
(options.contractOverrides && Object.keys(options.contractOverrides).length > 0)
135136
) {
136-
throw new Error('stateOverrides are not supported for utility function simulation.');
137+
throw new Error('stateOverrides and contractOverrides are not supported for utility function simulation.');
137138
}
138139
const call = await this.getFunctionCall();
139140
const scopes = [...(options.additionalScopes ?? [])];
Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { Fr } from '@aztec/foundation/curves/bn254';
22
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
3+
import type { ContractArtifact } from '@aztec/stdlib/abi';
34
import { AztecAddress } from '@aztec/stdlib/aztec-address';
4-
import { SerializableContractInstance } from '@aztec/stdlib/contract';
5+
import { SerializableContractInstance, getContractClassFromArtifact } from '@aztec/stdlib/contract';
56
import {
67
DELAYED_PUBLIC_MUTABLE_VALUES_LEN,
78
DelayedPublicMutableValuesWithHash,
89
} from '@aztec/stdlib/delayed-public-mutable';
910
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
11+
import { getTestContractArtifact, getTokenContractArtifact } from '@aztec/stdlib/testing/fixtures';
1012

1113
import { type MockProxy, mock } from 'jest-mock-extended';
1214

@@ -16,13 +18,15 @@ describe('fastForwardContractUpdate', () => {
1618
let node: MockProxy<AztecNode>;
1719
let instanceAddress: AztecAddress;
1820
let originalClassId: Fr;
21+
let newArtifact: ContractArtifact;
1922
let newClassId: Fr;
2023

2124
beforeEach(async () => {
2225
node = mock<AztecNode>();
2326
instanceAddress = await AztecAddress.random();
24-
originalClassId = Fr.random();
25-
newClassId = Fr.random();
27+
originalClassId = (await getContractClassFromArtifact(getTokenContractArtifact())).id;
28+
newArtifact = getTestContractArtifact();
29+
newClassId = (await getContractClassFromArtifact(newArtifact)).id;
2630

2731
const instance = (
2832
await SerializableContractInstance.random({
@@ -32,48 +36,47 @@ describe('fastForwardContractUpdate', () => {
3236
).withAddress(instanceAddress);
3337

3438
node.getContract.mockResolvedValue(instance);
35-
node.getContractClass.mockResolvedValue({
36-
id: newClassId,
37-
artifactHash: Fr.random(),
38-
packedBytecodeCommitments: [],
39-
privateFunctionsRoot: Fr.random(),
40-
publicBytecodeCommitment: Fr.random(),
41-
version: 1,
42-
privateFunctions: [],
43-
utilityFunctions: [],
44-
publicFunctions: [],
45-
packedBytecode: Buffer.alloc(0),
46-
} as any);
39+
node.getContractClass.mockResolvedValue({ id: newClassId } as any);
4740
});
4841

49-
it('produces overrides with bumped currentContractClassId and registry storage writes', async () => {
50-
const overrides = await fastForwardContractUpdate({ instanceAddress, newClassId, node });
42+
it('produces stateOverrides with bumped currentContractClassId and registry storage writes', async () => {
43+
const { stateOverrides } = await fastForwardContractUpdate({ instanceAddress, newArtifact, node });
5144

52-
expect(overrides.contractInstances).toHaveLength(1);
53-
expect(overrides.contractInstances![0].address).toEqual(instanceAddress);
54-
expect(overrides.contractInstances![0].currentContractClassId).toEqual(newClassId);
55-
expect(overrides.contractInstances![0].originalContractClassId).toEqual(originalClassId);
45+
expect(stateOverrides.contractInstances).toHaveLength(1);
46+
expect(stateOverrides.contractInstances![0].address).toEqual(instanceAddress);
47+
expect(stateOverrides.contractInstances![0].currentContractClassId).toEqual(newClassId);
48+
expect(stateOverrides.contractInstances![0].originalContractClassId).toEqual(originalClassId);
5649

5750
const expectedSlots = await DelayedPublicMutableValuesWithHash.getContractUpdateSlots(instanceAddress);
58-
expect(overrides.publicStorage).toHaveLength(DELAYED_PUBLIC_MUTABLE_VALUES_LEN + 1);
59-
for (const entry of overrides.publicStorage!) {
51+
expect(stateOverrides.publicStorage).toHaveLength(DELAYED_PUBLIC_MUTABLE_VALUES_LEN + 1);
52+
for (const entry of stateOverrides.publicStorage!) {
6053
expect(entry.contract).toEqual(ProtocolContractAddress.ContractInstanceRegistry);
6154
}
62-
const baseSlot = expectedSlots.delayedPublicMutableSlot;
63-
expect(overrides.publicStorage![0].slot).toEqual(baseSlot);
64-
expect(overrides.publicStorage![overrides.publicStorage!.length - 1].slot).toEqual(
55+
expect(stateOverrides.publicStorage![0].slot).toEqual(expectedSlots.delayedPublicMutableSlot);
56+
expect(stateOverrides.publicStorage![stateOverrides.publicStorage!.length - 1].slot).toEqual(
6557
expectedSlots.delayedPublicMutableHashSlot,
6658
);
6759
});
6860

61+
it('produces contractOverrides keyed by the instance address with the new artifact', async () => {
62+
const { contractOverrides } = await fastForwardContractUpdate({ instanceAddress, newArtifact, node });
63+
64+
const entry = contractOverrides[instanceAddress.toString()];
65+
expect(entry).toBeDefined();
66+
expect(entry.artifact).toBe(newArtifact);
67+
expect(entry.instance.address).toEqual(instanceAddress);
68+
expect(entry.instance.currentContractClassId).toEqual(newClassId);
69+
expect(entry.instance.originalContractClassId).toEqual(originalClassId);
70+
});
71+
6972
it('throws when the instance is not deployed', async () => {
7073
node.getContract.mockResolvedValue(undefined);
71-
await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/not deployed/);
74+
await expect(fastForwardContractUpdate({ instanceAddress, newArtifact, node })).rejects.toThrow(/not deployed/);
7275
});
7376

7477
it('throws when the new class is not registered', async () => {
7578
node.getContractClass.mockResolvedValue(undefined);
76-
await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/not registered/);
79+
await expect(fastForwardContractUpdate({ instanceAddress, newArtifact, node })).rejects.toThrow(/not registered/);
7780
});
7881

7982
it('throws when the instance is already on the target class', async () => {
@@ -85,6 +88,6 @@ describe('fastForwardContractUpdate', () => {
8588
).withAddress(instanceAddress);
8689
node.getContract.mockResolvedValue(sameClassInstance);
8790

88-
await expect(fastForwardContractUpdate({ instanceAddress, newClassId, node })).rejects.toThrow(/already on class/);
91+
await expect(fastForwardContractUpdate({ instanceAddress, newArtifact, node })).rejects.toThrow(/already on class/);
8992
});
9093
});

yarn-project/aztec.js/src/contract/fastforward_contract_update.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
import { Fr } from '@aztec/foundation/curves/bn254';
22
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
3+
import type { ContractArtifact } from '@aztec/stdlib/abi';
34
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
5+
import { getContractClassFromArtifact } from '@aztec/stdlib/contract';
46
import {
57
DelayedPublicMutableValuesWithHash,
68
ScheduledDelayChange,
79
ScheduledValueChange,
810
} from '@aztec/stdlib/delayed-public-mutable';
911
import type { AztecNode, StateOverrides } from '@aztec/stdlib/interfaces/client';
12+
import type { ContractOverrides } from '@aztec/stdlib/tx';
1013

1114
/**
12-
* Builds `StateOverrides` that simulate a contract instance having already been upgraded to a new contract class.
15+
* Builds the override blobs that simulate a contract instance having already been upgraded to a new contract class.
1316
*
14-
* Mirrors a real on-chain upgrade flow (`pxe.updateContract` followed by waiting out the delay): the contract
15-
* instance's `currentContractClassId` is bumped to `newClassId`, and the `ContractInstanceRegistry`'s delayed
16-
* public mutable storage is rewritten to look like the upgrade was scheduled in the past.
17+
* Mirrors a real on-chain upgrade flow (`pxe.updateContract` followed by waiting out the delay). Returns two
18+
* coherent override layers:
1719
*
18-
* The new class must already be registered on chain. To upgrade to an unregistered class, register it first
19-
* (or use a higher-level helper that bundles class registration with the upgrade).
20+
* - `stateOverrides` — applied node-side. Bumps `currentContractClassId` on the contract instance and rewrites
21+
* the `ContractInstanceRegistry`'s delayed-public-mutable storage to look like the upgrade was scheduled in
22+
* the past. Redirects public-call dispatch.
23+
* - `contractOverrides` — applied PXE-side. Replaces the (instance, artifact) pair the ACIR simulator uses for
24+
* the upgraded address. Redirects private-call dispatch.
25+
*
26+
* Both layers are returned so a single `simulate({ ...overrides })` spread works for any mix of private and
27+
* public function calls on the upgraded contract.
28+
*
29+
* The new class must already be registered on chain. To upgrade to an unregistered class, register it first.
2030
*
2131
* @param args.instanceAddress - Address of the deployed instance to upgrade.
22-
* @param args.newClassId - ID of the (already-registered) class to upgrade to.
32+
* @param args.newArtifact - Artifact of the class to upgrade to. Must be registered on chain.
2333
* @param args.node - Node used to fetch the existing instance and validate the class is registered.
24-
* @returns `StateOverrides` to spread into a `simulate({ stateOverrides })` call.
34+
* @returns `{ stateOverrides, contractOverrides }` to spread into a `simulate(...)` call.
2535
* @throws If the instance is not deployed, the class is not registered, or the instance is already on the target class.
2636
*/
2737
export async function fastForwardContractUpdate(args: {
2838
instanceAddress: AztecAddress;
29-
newClassId: Fr;
39+
newArtifact: ContractArtifact;
3040
node: AztecNode;
31-
}): Promise<StateOverrides> {
32-
const { instanceAddress, newClassId, node } = args;
41+
}): Promise<{ stateOverrides: StateOverrides; contractOverrides: ContractOverrides }> {
42+
const { instanceAddress, newArtifact, node } = args;
43+
44+
const newClassId = (await getContractClassFromArtifact(newArtifact)).id;
3345

3446
const instance = await node.getContract(instanceAddress);
3547
if (!instance) {
@@ -63,8 +75,17 @@ export async function fastForwardContractUpdate(args: {
6375

6476
const upgradedInstance = { ...instance, currentContractClassId: newClassId };
6577

66-
return {
78+
const stateOverrides: StateOverrides = {
6779
publicStorage,
6880
contractInstances: [upgradedInstance],
6981
};
82+
83+
const contractOverrides: ContractOverrides = {
84+
[instanceAddress.toString()]: {
85+
instance: upgradedInstance,
86+
artifact: newArtifact,
87+
},
88+
};
89+
90+
return { stateOverrides, contractOverrides };
7091
}

yarn-project/aztec.js/src/contract/interaction_options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas';
66
import type { StateOverrides } from '@aztec/stdlib/interfaces/client';
77
import {
88
type Capsule,
9+
type ContractOverrides,
910
OFFCHAIN_MESSAGE_IDENTIFIER,
1011
type OffchainEffect,
1112
type SimulationStats,
@@ -160,6 +161,12 @@ export type SimulateInteractionOptions = Omit<SendInteractionOptions, 'fee'> & {
160161
includeMetadata?: boolean;
161162
/** Pre-simulation state overrides applied to the ephemeral fork and contract DB. */
162163
stateOverrides?: StateOverrides;
164+
/**
165+
* Per-simulation contract overrides applied PXE-side. For each address, replaces the (instance, artifact)
166+
* pair the ACIR simulator uses for the duration of the simulation. Required to redirect private-function
167+
* dispatch when simulating against an upgraded class.
168+
*/
169+
contractOverrides?: ContractOverrides;
163170
};
164171

165172
/**

yarn-project/aztec.js/src/wallet/wallet.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AbiDecodedSchema, type ApiSchemaFor, optional, schemas, zodFor } from '
1919
import type { ExecutionPayload, InTx } from '@aztec/stdlib/tx';
2020
import {
2121
Capsule,
22+
ContractOverridesSchema,
2223
HashedValues,
2324
TxHash,
2425
TxProfileResult,
@@ -337,6 +338,7 @@ export const SimulateOptionsSchema = z.object({
337338
includeMetadata: optional(z.boolean()),
338339
additionalScopes: optional(z.array(schemas.AztecAddress)),
339340
stateOverrides: optional(StateOverridesSchema),
341+
contractOverrides: optional(ContractOverridesSchema),
340342
});
341343

342344
export const ProfileOptionsSchema = SimulateOptionsSchema.extend({

0 commit comments

Comments
 (0)