Skip to content

Commit 999138f

Browse files
authored
feat: merge-train/fairies (#23074)
BEGIN_COMMIT_OVERRIDE refactor: drop artifact field from SimulationOverrides (#22957) feat: public data tree overrides for simulation (#22830) feat: expose wallet.registerContractClass (prep for simulating contract upgrades) (#22932) END_COMMIT_OVERRIDE
2 parents f23aa82 + 2ea329a commit 999138f

25 files changed

Lines changed: 518 additions & 149 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ Test that invalid operations revert as expected:
6767

6868
Use `.simulate()` to test reverts without spending gas. The simulation will throw if the transaction would fail onchain.
6969

70+
## Simulating with overrides
71+
72+
`.simulate()` accepts an `overrides` option that injects values into the simulator's (ephemeral) world-state fork and contract DB before the call runs. The override is scoped to that single simulation and thrown away afterwards.
73+
74+
Override a public-storage slot:
75+
76+
```typescript
77+
const result = await contract.methods.read_balance(account).simulate({
78+
overrides: {
79+
publicStorage: [{ contract: contract.address, slot: BALANCE_SLOT, value: new Fr(1_000_000n) }],
80+
},
81+
});
82+
```
83+
84+
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.
85+
7086
## Further reading
7187

7288
- [How to read contract data](./how_to_read_data.md)

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,40 @@ If you relied on a bundled bare-name binary for general use:
151151

152152
If you set `Noir: Nargo Path` in the VS Code Noir extension to `$HOME/.aztec/current/bin/nargo`, change it to `$HOME/.aztec/current/bin/aztec-nargo` (the symlink is a drop-in for `nargo`). See the [Noir VSCode Extension guide](../aztec-nr/installation.md) for details.
153153

154+
### [Stdlib] `SimulationOverrides.contracts` entries no longer carry an artifact
155+
156+
`ContractOverrides` entries are now `{ instance }` only. To override a contract's artifact, pre-register the target class via `pxe.registerContractClass(artifact)` and set the override instance's `currentContractClassId` to that class id:
157+
158+
```diff
159+
- const instance = await getContractInstanceFromInstantiationParams(stubArtifact, { salt: Fr.random() });
160+
+ const instance = await pxe.getContractInstance(addr);
161+
+ await pxe.registerContractClass(stubArtifact);
162+
+ const stubClassId = (await getContractClassFromArtifact(stubArtifact)).id;
163+
- overrides = { contracts: { [addr.toString()]: { instance, artifact: stubArtifact } } };
164+
+ overrides = { contracts: { [addr.toString()]: { instance: { ...instance, currentContractClassId: stubClassId } } } };
165+
```
166+
167+
### [Aztec.js] `simulate` accepts `overrides` for testing "what if storage value was X?"
168+
169+
`Contract.methods.foo(...).simulate(...)` now accepts an `overrides` option that injects values into the simulator's (ephemeral) world-state fork and contract DB before the call runs. The supported field is `publicStorage`, which writes a `(contract, slot, value)` into the public-data tree as if a previous tx had set it. Overrides are thrown away after simulation completes.
170+
171+
```typescript
172+
const result = await contract.methods.read_balance(account).simulate({
173+
overrides: {
174+
publicStorage: [{ contract: contract.address, slot: BALANCE_SLOT, value: new Fr(1_000_000n) }],
175+
},
176+
});
177+
```
178+
179+
The same option flows through `wallet.simulateTx` and eventually to `simulatePublicCalls` RPC on `AztecNode`.
180+
181+
Direct callers of the `SimulationOverrides` constructor must switch from a positional `contracts` argument to an options bag:
182+
183+
```diff
184+
- new SimulationOverrides(contracts);
185+
+ new SimulationOverrides({ contracts });
186+
```
187+
154188
### [PXE] `proveTx` takes an options bag
155189

156190
`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:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Fr } from '@aztec/foundation/curves/bn254';
2+
import { PublicDataWrite } from '@aztec/stdlib/avm';
3+
import { AztecAddress } from '@aztec/stdlib/aztec-address';
4+
import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
5+
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
6+
import { MerkleTreeId } from '@aztec/stdlib/trees';
7+
8+
import { type MockProxy, mock } from 'jest-mock-extended';
9+
10+
import { applyPublicDataOverrides } from './public_data_overrides.js';
11+
12+
describe('applyPublicDataOverrides', () => {
13+
let fork: MockProxy<MerkleTreeWriteOperations>;
14+
15+
beforeEach(() => {
16+
fork = mock<MerkleTreeWriteOperations>();
17+
fork.sequentialInsert.mockResolvedValue({ lowLeavesWitnesses: [], insertionWitnesses: [] } as any);
18+
});
19+
20+
it('does nothing when overrides is undefined', async () => {
21+
await applyPublicDataOverrides(fork, undefined);
22+
expect(fork.sequentialInsert).not.toHaveBeenCalled();
23+
});
24+
25+
it('does nothing when overrides is empty', async () => {
26+
await applyPublicDataOverrides(fork, []);
27+
expect(fork.sequentialInsert).not.toHaveBeenCalled();
28+
});
29+
30+
it('inserts a single override at the correct leaf slot', async () => {
31+
const contract = await AztecAddress.random();
32+
const slot = Fr.random();
33+
const value = Fr.random();
34+
35+
await applyPublicDataOverrides(fork, [{ contract, slot, value }]);
36+
37+
const expectedLeafSlot = await computePublicDataTreeLeafSlot(contract, slot);
38+
const expectedWrite = new PublicDataWrite(expectedLeafSlot, value);
39+
40+
expect(fork.sequentialInsert).toHaveBeenCalledTimes(1);
41+
expect(fork.sequentialInsert).toHaveBeenCalledWith(MerkleTreeId.PUBLIC_DATA_TREE, [expectedWrite.toBuffer()]);
42+
});
43+
44+
it('inserts multiple overrides in a single batch call', async () => {
45+
const contract = await AztecAddress.random();
46+
const overrides = [
47+
{ contract, slot: Fr.random(), value: Fr.random() },
48+
{ contract, slot: Fr.random(), value: Fr.random() },
49+
];
50+
51+
await applyPublicDataOverrides(fork, overrides);
52+
53+
const expectedWrites = await Promise.all(
54+
overrides.map(async o => {
55+
const leafSlot = await computePublicDataTreeLeafSlot(o.contract, o.slot);
56+
return new PublicDataWrite(leafSlot, o.value);
57+
}),
58+
);
59+
60+
expect(fork.sequentialInsert).toHaveBeenCalledTimes(1);
61+
expect(fork.sequentialInsert).toHaveBeenCalledWith(
62+
MerkleTreeId.PUBLIC_DATA_TREE,
63+
expectedWrites.map(w => w.toBuffer()),
64+
);
65+
});
66+
67+
it('passes duplicate (contract, slot) writes through — last write wins via tree semantics', async () => {
68+
const contract = await AztecAddress.random();
69+
const slot = Fr.random();
70+
const firstValue = Fr.random();
71+
const secondValue = Fr.random();
72+
73+
await applyPublicDataOverrides(fork, [
74+
{ contract, slot, value: firstValue },
75+
{ contract, slot, value: secondValue },
76+
]);
77+
78+
// Both writes are passed to sequentialInsert; the tree handles last-wins ordering.
79+
expect(fork.sequentialInsert).toHaveBeenCalledTimes(1);
80+
const [treeId, leaves] = fork.sequentialInsert.mock.calls[0] as [MerkleTreeId, Buffer[]];
81+
expect(treeId).toBe(MerkleTreeId.PUBLIC_DATA_TREE);
82+
expect(leaves).toHaveLength(2);
83+
});
84+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { PublicDataWrite } from '@aztec/stdlib/avm';
2+
import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
3+
import type { PublicStorageOverride } from '@aztec/stdlib/interfaces/client';
4+
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
5+
import { MerkleTreeId } from '@aztec/stdlib/trees';
6+
7+
/**
8+
* Injects public-state overrides into an (ephemeral) world-state fork before simulation.
9+
*
10+
* Each override is written via the same `sequentialInsert` path the public processor
11+
* uses during real transaction execution, so low-leaf updates and root coherence are
12+
* handled identically for both simulation and proof generation.
13+
*
14+
* Writes never reach committed world state — the fork is thrown away after simulation.
15+
*/
16+
export async function applyPublicDataOverrides(
17+
fork: MerkleTreeWriteOperations,
18+
publicStorage: PublicStorageOverride[] | undefined,
19+
): Promise<void> {
20+
if (!publicStorage?.length) {
21+
return;
22+
}
23+
24+
const writes = await Promise.all(
25+
publicStorage.map(async o => {
26+
const leafSlot = await computePublicDataTreeLeafSlot(o.contract, o.slot);
27+
return new PublicDataWrite(leafSlot, o.value);
28+
}),
29+
);
30+
31+
await fork.sequentialInsert(
32+
MerkleTreeId.PUBLIC_DATA_TREE,
33+
writes.map(w => w.toBuffer()),
34+
);
35+
}

yarn-project/aztec-node/src/aztec-node/server.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import {
106106
type GlobalVariableBuilder as GlobalVariableBuilderInterface,
107107
type IndexedTxEffect,
108108
PublicSimulationOutput,
109+
type SimulationOverrides,
109110
Tx,
110111
type TxHash,
111112
TxReceipt,
@@ -146,6 +147,7 @@ import {
146147
} from './block_response_helpers.js';
147148
import { type AztecNodeConfig, createKeyStoreForValidator } from './config.js';
148149
import { NodeMetrics } from './node_metrics.js';
150+
import { applyPublicDataOverrides } from './public_data_overrides.js';
149151

150152
/**
151153
* The aztec node.
@@ -1440,11 +1442,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
14401442
/**
14411443
* Simulates the public part of a transaction with the current state.
14421444
* @param tx - The transaction to simulate.
1445+
* @param skipFeeEnforcement - If true, fee enforcement is skipped.
1446+
* @param overrides - Optional pre-simulation overrides applied to the ephemeral fork and contract DB.
14431447
**/
14441448
@trackSpan('AztecNodeService.simulatePublicCalls', (tx: Tx) => ({
14451449
[Attributes.TX_HASH]: tx.getTxHash().toString(),
14461450
}))
1447-
public async simulatePublicCalls(tx: Tx, skipFeeEnforcement = false): Promise<PublicSimulationOutput> {
1451+
public async simulatePublicCalls(
1452+
tx: Tx,
1453+
skipFeeEnforcement = false,
1454+
overrides?: SimulationOverrides,
1455+
): Promise<PublicSimulationOutput> {
14481456
// Check total gas limit for simulation
14491457
const gasSettings = tx.data.constants.txContext.gasSettings;
14501458
const txGasLimit = gasSettings.gasLimits.l2Gas;
@@ -1489,6 +1497,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
14891497
await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
14901498
const merkleTreeFork = await this.worldStateSynchronizer.fork();
14911499
try {
1500+
await applyPublicDataOverrides(merkleTreeFork, overrides?.publicStorage);
14921501
const config = PublicSimulatorConfig.from({
14931502
skipFeeEnforcement,
14941503
collectDebugLogs: true,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,7 @@ export { AccountManager } from '../wallet/account_manager.js';
7878

7979
export { TxSimulationResultWithAppOffset } from '../wallet/tx_simulation_result_with_app_offset.js';
8080

81+
export { type PublicStorageOverride, PublicStorageOverrideSchema } from '@aztec/stdlib/interfaces/client';
82+
export { SimulationOverrides } from '@aztec/stdlib/tx';
83+
8184
export { type DeployAccountOptions, DeployAccountMethod } from '../wallet/deploy_account_method.js';

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type Capsule,
88
OFFCHAIN_MESSAGE_IDENTIFIER,
99
type OffchainEffect,
10+
type SimulationOverrides,
1011
type SimulationStats,
1112
type TxHash,
1213
type TxReceipt,
@@ -157,6 +158,8 @@ export type SimulateInteractionOptions = Omit<SendInteractionOptions, 'fee'> & {
157158
/** Whether to include metadata such as performance statistics (e.g. timing information of the different circuits and oracles) and gas estimation
158159
* in the simulation result, in addition to the return value and offchain effects */
159160
includeMetadata?: boolean;
161+
/** Pre-simulation overrides applied to the ephemeral fork and contract DB (publicStorage writes, contract instance overrides). */
162+
overrides?: SimulationOverrides;
160163
};
161164

162165
/**

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,25 @@ export interface ContractsCapability {
138138
export interface GrantedContractsCapability extends ContractsCapability {}
139139

140140
/**
141-
* Contract class capability - for querying contract class metadata.
141+
* Contract class capability - for querying contract class meatadata and registering contract classes.
142142
*
143143
* Maps to wallet methods:
144-
* - getContractClassMetadata
144+
* - getContractClassMetadata (when canGetMetadata: true)
145+
* - registerContractClass (when canRegister: true)
145146
*
146147
* Contract classes are identified by their class ID (Fr), not by contract address.
147148
* Multiple contract instances can share the same class. This capability grants
148-
* permission to query metadata for specific contract classes.
149+
* permission to query metadata for, and register, specific contract classes.
149150
*
150151
* Apps typically acquire this permission automatically when registering a contract
151152
* with an artifact (the wallet auto-grants permission for that contract's class ID).
152153
*
153154
* @example
154-
* // Query specific contract classes
155+
* // Register and query a specific contract class
155156
* \{
156157
* type: 'contractClasses',
157-
* classes: [classId1, classId2],
158+
* classes: [classId1],
159+
* canRegister: true,
158160
* canGetMetadata: true
159161
* \}
160162
*
@@ -177,6 +179,9 @@ export interface ContractClassesCapability {
177179
*/
178180
classes: '*' | Fr[];
179181

182+
/** Can register a contract class artifact in the local PXE. Maps to: registerContractClass */
183+
canRegister?: boolean;
184+
180185
/** Can query contract class metadata. Maps to: getContractClassMetadata */
181186
canGetMetadata: boolean;
182187
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,19 @@ describe('WalletSchema', () => {
157157
});
158158
});
159159

160+
it('registerContractClass', async () => {
161+
const mockArtifact: ContractArtifact = {
162+
name: 'TestContract',
163+
aztecVersion: DEV_VERSION,
164+
functions: [],
165+
nonDispatchPublicFunctions: [],
166+
outputs: { structs: {}, globals: {} },
167+
fileMap: {},
168+
storageLayout: {},
169+
};
170+
await context.client.registerContractClass(mockArtifact);
171+
});
172+
160173
it('simulateTx', async () => {
161174
const exec: ExecutionPayload = {
162175
calls: [],
@@ -451,6 +464,8 @@ class MockWallet implements Wallet {
451464
};
452465
}
453466

467+
async registerContractClass(_artifact: any): Promise<void> {}
468+
454469
async simulateTx(_exec: ExecutionPayload, _opts: SimulateOptions): Promise<TxSimulationResultWithAppOffset> {
455470
return TxSimulationResultWithAppOffset.fromResultAndOffset(await TxSimulationResult.random(), 0);
456471
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { ExecutionPayload, InTx } from '@aztec/stdlib/tx';
1919
import {
2020
Capsule,
2121
HashedValues,
22+
SimulationOverrides,
2223
TxHash,
2324
TxProfileResult,
2425
TxReceipt,
@@ -271,6 +272,12 @@ export type Wallet = {
271272
artifact?: ContractArtifact,
272273
secretKey?: Fr,
273274
): Promise<ContractInstanceWithAddress>;
275+
/**
276+
* Registers a contract class artifact in the local PXE without binding it to any instance.
277+
* Useful for simulation flows that need the artifact available locally before any on-chain
278+
* upgrade has taken effect. No chain check.
279+
*/
280+
registerContractClass(artifact: ContractArtifact): Promise<void>;
274281
simulateTx(exec: ExecutionPayload, opts: SimulateOptions): Promise<TxSimulationResultWithAppOffset>;
275282
executeUtility(call: FunctionCall, opts: ExecuteUtilityOptions): Promise<UtilityExecutionResult>;
276283
profileTx(exec: ExecutionPayload, opts: ProfileOptions): Promise<TxProfileResult>;
@@ -335,6 +342,7 @@ export const SimulateOptionsSchema = z.object({
335342
skipFeeEnforcement: optional(z.boolean()),
336343
includeMetadata: optional(z.boolean()),
337344
additionalScopes: optional(z.array(schemas.AztecAddress)),
345+
overrides: optional(SimulationOverrides.schema),
338346
});
339347

340348
export const ProfileOptionsSchema = SimulateOptionsSchema.extend({
@@ -427,6 +435,7 @@ export const GrantedContractsCapabilitySchema = ContractsCapabilitySchema;
427435
export const ContractClassesCapabilitySchema = z.object({
428436
type: z.literal('contractClasses'),
429437
classes: z.union([z.literal('*'), z.array(schemas.Fr)]),
438+
canRegister: optional(z.boolean()),
430439
canGetMetadata: z.boolean(),
431440
});
432441

@@ -557,6 +566,7 @@ const WalletMethodSchemas = {
557566
.function()
558567
.args(ContractInstanceWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr))
559568
.returns(ContractInstanceWithAddressSchema),
569+
registerContractClass: z.function().args(ContractArtifactSchema).returns(z.void()),
560570
simulateTx: z
561571
.function()
562572
.args(ExecutionPayloadSchema, SimulateOptionsSchema)

0 commit comments

Comments
 (0)