Skip to content

Commit eeea6a6

Browse files
committed
feat: public data tree overrides for simulation (plumbing)
Thread publicDataOverrides through SimulateInteractionOptions, SimulateOptions, SimulateOptionsSchema, SimulateViaEntrypointOptions, PXE.simulateTx, and wallet-sdk's simulateViaNode path so overrides set in ContractFunctionInteraction.simulate flow down to the node. Re-export PublicDataTreeOverride and PublicDataTreeOverrideSchema from @aztec/aztec.js. Add two e2e tests in e2e_avm_simulator that verify: - overriding an unwritten slot returns the override value during simulation while real storage remains untouched - overriding a previously-written slot returns the override during simulation while the committed value is preserved
1 parent 8911d41 commit eeea6a6

7 files changed

Lines changed: 76 additions & 6 deletions

File tree

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

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

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

81+
export { type PublicDataTreeOverride, PublicDataTreeOverrideSchema } from '@aztec/stdlib/interfaces/client';
82+
8183
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
@@ -3,6 +3,7 @@ import type { FieldsOf } from '@aztec/foundation/types';
33
import type { AuthWitness } from '@aztec/stdlib/auth-witness';
44
import { AztecAddress } from '@aztec/stdlib/aztec-address';
55
import type { GasSettings, ManaUsageEstimate } from '@aztec/stdlib/gas';
6+
import type { PublicDataTreeOverride } from '@aztec/stdlib/interfaces/client';
67
import {
78
type Capsule,
89
OFFCHAIN_MESSAGE_IDENTIFIER,
@@ -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+
/** Public state overrides injected into the ephemeral world-state fork before simulation. */
162+
publicDataOverrides?: PublicDataTreeOverride[];
160163
};
161164

162165
/**

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { AuthWitness } from '@aztec/stdlib/auth-witness';
1313
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
1414
import { type ContractInstanceWithAddress, ContractInstanceWithAddressSchema } from '@aztec/stdlib/contract';
1515
import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas';
16+
import { type PublicDataTreeOverride, PublicDataTreeOverrideSchema } from '@aztec/stdlib/interfaces/client';
1617
import { LogId } from '@aztec/stdlib/logs';
1718
import { AbiDecodedSchema, type ApiSchemaFor, optional, schemas, zodFor } from '@aztec/stdlib/schemas';
1819
import type { ExecutionPayload, InTx } from '@aztec/stdlib/tx';
@@ -335,6 +336,7 @@ export const SimulateOptionsSchema = z.object({
335336
skipFeeEnforcement: optional(z.boolean()),
336337
includeMetadata: optional(z.boolean()),
337338
additionalScopes: optional(z.array(schemas.AztecAddress)),
339+
publicDataOverrides: optional(z.array(PublicDataTreeOverrideSchema)),
338340
});
339341

340342
export const ProfileOptionsSchema = SimulateOptionsSchema.extend({

yarn-project/end-to-end/src/e2e_avm_simulator.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { AztecAddress } from '@aztec/aztec.js/addresses';
22
import { BatchCall, type ContractInstanceWithAddress } from '@aztec/aztec.js/contracts';
33
import { Fr } from '@aztec/aztec.js/fields';
4+
import type { AztecNode } from '@aztec/aztec.js/node';
45
import { TxExecutionResult } from '@aztec/aztec.js/tx';
6+
import type { PublicDataTreeOverride } from '@aztec/aztec.js/wallet';
57
import type { Wallet } from '@aztec/aztec.js/wallet';
68
import { AvmInitializerTestContract } from '@aztec/noir-test-contracts.js/AvmInitializerTest';
79
import { AvmTestContract } from '@aztec/noir-test-contracts.js/AvmTest';
@@ -16,13 +18,15 @@ describe('e2e_avm_simulator', () => {
1618
jest.setTimeout(TIMEOUT);
1719

1820
let wallet: Wallet;
21+
let aztecNode: AztecNode;
1922
let defaultAccountAddress: AztecAddress;
2023
let teardown: () => Promise<void>;
2124

2225
beforeAll(async () => {
2326
({
2427
teardown,
2528
wallet,
29+
aztecNode,
2630
accounts: [defaultAccountAddress],
2731
} = await setup(1));
2832
await ensureAccountContractsPublished(wallet, [defaultAccountAddress]);
@@ -249,6 +253,51 @@ describe('e2e_avm_simulator', () => {
249253
});
250254
});
251255

256+
describe('publicDataOverrides', () => {
257+
// AvmTestContract: `single` is the first storage variable and lives at raw slot 1.
258+
const SINGLE_SLOT = new Fr(1n);
259+
let avmContract: AvmTestContract;
260+
261+
beforeEach(async () => {
262+
({ contract: avmContract } = await AvmTestContract.deploy(wallet).send({ from: defaultAccountAddress }));
263+
});
264+
265+
it('simulated read of an unwritten slot returns the override; real storage is untouched', async () => {
266+
const overrideValue = new Fr(0xdeadbeefn);
267+
const publicDataOverrides: PublicDataTreeOverride[] = [
268+
{ contract: avmContract.address, slot: SINGLE_SLOT, value: overrideValue },
269+
];
270+
271+
const simResult = await avmContract.methods
272+
.read_storage_single()
273+
.simulate({ from: defaultAccountAddress, publicDataOverrides });
274+
expect(simResult.result).toEqual(overrideValue.toBigInt());
275+
276+
// Real state is untouched — the slot was never written.
277+
const realValue = await aztecNode.getPublicStorageAt('latest', avmContract.address, SINGLE_SLOT);
278+
expect(realValue.toBigInt()).toEqual(0n);
279+
});
280+
281+
it('simulated read returns the override when a slot was previously written by a real tx', async () => {
282+
const realValue = new Fr(100n);
283+
await avmContract.methods.set_storage_single(realValue).send({ from: defaultAccountAddress });
284+
285+
const overrideValue = new Fr(999n);
286+
const publicDataOverrides: PublicDataTreeOverride[] = [
287+
{ contract: avmContract.address, slot: SINGLE_SLOT, value: overrideValue },
288+
];
289+
290+
const simResult = await avmContract.methods
291+
.read_storage_single()
292+
.simulate({ from: defaultAccountAddress, publicDataOverrides });
293+
expect(simResult.result).toEqual(overrideValue.toBigInt());
294+
295+
// Real storage still holds the original written value.
296+
const storedValue = await aztecNode.getPublicStorageAt('latest', avmContract.address, SINGLE_SLOT);
297+
expect(storedValue.toBigInt()).toEqual(realValue.toBigInt());
298+
});
299+
});
300+
252301
describe('AvmInitializerTestContract', () => {
253302
let avmContract: AvmInitializerTestContract;
254303

yarn-project/pxe/src/pxe.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
getContractClassFromArtifact,
2828
} from '@aztec/stdlib/contract';
2929
import { SimulationError } from '@aztec/stdlib/errors';
30-
import type { AztecNode, PrivateKernelProver } from '@aztec/stdlib/interfaces/client';
30+
import type { AztecNode, PrivateKernelProver, PublicDataTreeOverride } from '@aztec/stdlib/interfaces/client';
3131
import type {
3232
PrivateExecutionStep,
3333
PrivateKernelExecutionProofOutput,
@@ -125,6 +125,8 @@ export type SimulateTxOpts = {
125125
scopes: AztecAddress[];
126126
/** Sender address used to derive discovery tags for private messages (notes, events, logs) this tx emits. */
127127
senderForTags?: AztecAddress;
128+
/** Public state overrides injected into the ephemeral world-state fork before simulation. */
129+
publicDataOverrides?: PublicDataTreeOverride[];
128130
};
129131

130132
/** Options for PXE.executeUtility. */
@@ -467,11 +469,11 @@ export class PXE {
467469
* It can also be used for estimating gas in the future.
468470
* @param tx - The transaction to be simulated.
469471
*/
470-
async #simulatePublicCalls(tx: Tx, skipFeeEnforcement: boolean) {
472+
async #simulatePublicCalls(tx: Tx, skipFeeEnforcement: boolean, publicDataOverrides?: PublicDataTreeOverride[]) {
471473
// Simulating public calls can throw if the TX fails in a phase that doesn't allow reverts (setup)
472474
// Or return as reverted if it fails in a phase that allows reverts (app logic, teardown)
473475
try {
474-
const result = await this.node.simulatePublicCalls(tx, skipFeeEnforcement);
476+
const result = await this.node.simulatePublicCalls(tx, skipFeeEnforcement, publicDataOverrides);
475477
if (result.revertReason) {
476478
throw result.revertReason;
477479
}
@@ -956,6 +958,7 @@ export class PXE {
956958
overrides,
957959
scopes,
958960
senderForTags,
961+
publicDataOverrides,
959962
}: SimulateTxOpts,
960963
): Promise<TxSimulationResult> {
961964
// We disable concurrent simulations since those might execute oracles which read and write to the PXE stores (e.g.
@@ -1037,7 +1040,7 @@ export class PXE {
10371040
let publicOutput: PublicSimulationOutput | undefined;
10381041
if (simulatePublic && publicInputs.forPublic) {
10391042
const publicSimulationTimer = new Timer();
1040-
publicOutput = await this.#simulatePublicCalls(simulatedTx, skipFeeEnforcement);
1043+
publicOutput = await this.#simulatePublicCalls(simulatedTx, skipFeeEnforcement, publicDataOverrides);
10411044
publicSimulationTime = publicSimulationTimer.ms();
10421045
if (publicOutput?.debugLogs?.length) {
10431046
await displayDebugLogs(publicOutput.debugLogs, addr => this.contractStore.getDebugContractName(addr));

yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export type FeeOptions = {
8585
/** Options for `simulateViaEntrypoint`. */
8686
export type SimulateViaEntrypointOptions = Pick<
8787
SimulateOptions,
88-
'from' | 'additionalScopes' | 'skipTxValidation' | 'skipFeeEnforcement' | 'sendMessagesAs'
88+
'from' | 'additionalScopes' | 'skipTxValidation' | 'skipFeeEnforcement' | 'sendMessagesAs' | 'publicDataOverrides'
8989
> & {
9090
/** Fee options for the entrypoint */
9191
feeOptions: FeeOptions;
@@ -365,6 +365,7 @@ export abstract class BaseWallet implements Wallet {
365365
skipFeeEnforcement: opts.skipFeeEnforcement,
366366
scopes: this.scopesFrom(opts.from, opts.additionalScopes),
367367
senderForTags: this.senderForTagsFrom(opts.from, opts.sendMessagesAs),
368+
publicDataOverrides: opts.publicDataOverrides,
368369
});
369370
const appCallOffset = await this.computeAppCallOffset(opts.from, opts.feeOptions);
370371
return TxSimulationResultWithAppOffset.fromResultAndOffset(result, appCallOffset);
@@ -428,6 +429,7 @@ export abstract class BaseWallet implements Wallet {
428429
blockHeader,
429430
opts.skipFeeEnforcement ?? true,
430431
this.getContractName.bind(this),
432+
opts.publicDataOverrides,
431433
)
432434
: Promise.resolve([]),
433435
remainingCalls.length > 0
@@ -438,6 +440,7 @@ export abstract class BaseWallet implements Wallet {
438440
skipTxValidation: opts.skipTxValidation,
439441
skipFeeEnforcement: opts.skipFeeEnforcement ?? true,
440442
sendMessagesAs: opts.sendMessagesAs,
443+
publicDataOverrides: opts.publicDataOverrides,
441444
})
442445
: Promise.resolve(null),
443446
]);

yarn-project/wallet-sdk/src/base-wallet/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { generateSimulatedProvingResult } from '@aztec/pxe/simulator';
1111
import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi';
1212
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
1313
import type { GasSettings } from '@aztec/stdlib/gas';
14+
import type { PublicDataTreeOverride } from '@aztec/stdlib/interfaces/client';
1415
import {
1516
ClaimedLengthArray,
1617
CountedPublicCallRequest,
@@ -65,6 +66,8 @@ export function extractOptimizablePublicStaticCalls(payload: ExecutionPayload):
6566
* @param gasSettings - Gas settings for the transaction.
6667
* @param blockHeader - Block header to use as anchor.
6768
* @param skipFeeEnforcement - Whether to skip fee enforcement during simulation.
69+
* @param getContractName - Resolver for contract names (used for debug log display).
70+
* @param publicDataOverrides - Optional public state overrides injected before simulation.
6871
* @returns TxSimulationResult with public return values.
6972
*/
7073
async function simulateBatchViaNode(
@@ -76,6 +79,7 @@ async function simulateBatchViaNode(
7679
blockHeader: BlockHeader,
7780
skipFeeEnforcement: boolean,
7881
getContractName: ContractNameResolver,
82+
publicDataOverrides?: PublicDataTreeOverride[],
7983
): Promise<TxSimulationResult> {
8084
const txContext = new TxContext(chainInfo.chainId, chainInfo.version, gasSettings);
8185

@@ -143,7 +147,7 @@ async function simulateBatchViaNode(
143147
publicFunctionCalldata: publicFunctionCalldata,
144148
});
145149

146-
const publicOutput = await node.simulatePublicCalls(tx, skipFeeEnforcement);
150+
const publicOutput = await node.simulatePublicCalls(tx, skipFeeEnforcement, publicDataOverrides);
147151

148152
if (publicOutput.revertReason) {
149153
throw publicOutput.revertReason;
@@ -166,6 +170,8 @@ async function simulateBatchViaNode(
166170
* @param gasSettings - Gas settings for the transaction.
167171
* @param blockHeader - Block header to use as anchor.
168172
* @param skipFeeEnforcement - Whether to skip fee enforcement during simulation.
173+
* @param getContractName - Resolver for contract names (used for debug log display).
174+
* @param publicDataOverrides - Optional public state overrides injected before simulation.
169175
* @returns Array of TxSimulationResult, one per batch.
170176
*/
171177
export async function simulateViaNode(
@@ -177,6 +183,7 @@ export async function simulateViaNode(
177183
blockHeader: BlockHeader,
178184
skipFeeEnforcement: boolean = true,
179185
getContractName: ContractNameResolver,
186+
publicDataOverrides?: PublicDataTreeOverride[],
180187
): Promise<TxSimulationResult[]> {
181188
const batches: FunctionCall[][] = [];
182189

@@ -196,6 +203,7 @@ export async function simulateViaNode(
196203
blockHeader,
197204
skipFeeEnforcement,
198205
getContractName,
206+
publicDataOverrides,
199207
);
200208
results.push(result);
201209
}

0 commit comments

Comments
 (0)