Skip to content

Commit a850ebc

Browse files
committed
feat: contract class/instance overrides for simulation
Adds `contractClasses?: ContractClassPublic[]` and `contractInstances?: ContractInstanceWithAddress[]` to `StateOverrides`, allowing simulation callers to shadow contracts in the simulator's contract DB without on-chain registration: ```ts type StateOverrides = { publicStorage?: PublicStorageOverride[]; contractClasses?: ContractClassPublic[]; contractInstances?: ContractInstanceWithAddress[]; }; ``` Plumbing: - `PublicContractsDB.addContracts({ contractClasses?, contractInstances? })`: typed-injection method that pushes overrides onto the contract DB checkpoint stack. Bypasses the log/event-extraction path. - `PublicContractsDB.addContractsFromLogs(ContractDeploymentData)`: log/event- based injection used by the C++ AVM contract provider during real execution. Disambiguated from the typed variant. - `PublicProcessorFactory.createContractsDB()` and `create(merkleTree, globalVars, config, contractsDB?)`: lets `simulatePublicCalls` populate the DB before constructing the processor, keeping production block-building paths untouched (they still call `create(...)` without the optional `contractsDB`). - `simulatePublicCalls` injects the contract overrides into a fresh `contractsDB` after `createContractsDB()` and before `factory.create(...)`. Overrides are ephemeral — they live in the per-call contract DB and are discarded after simulation; nothing reaches committed state.
1 parent 43d10a4 commit a850ebc

8 files changed

Lines changed: 75 additions & 17 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ Use `.simulate()` to test reverts without spending gas. The simulation will thro
7171

7272
`.simulate()` accepts a `stateOverrides` option that injects values into the simulator's ephemeral world-state fork before the call runs. The override is scoped to that single simulation; the real chain state is untouched.
7373

74+
Three override flavors are supported:
75+
76+
- `publicStorage`: write `(contract, slot, value)` into the public-data tree.
77+
- `contractClasses`: shadow contract classes in the simulator's contract DB (as if they had been published on the class registry).
78+
- `contractInstances`: shadow contract instances at specific addresses (as if they had been deployed, possibly pointing at one of the shadowed classes).
79+
7480
Override a public-storage slot:
7581

7682
```typescript
@@ -81,10 +87,23 @@ const result = await contract.methods.read_balance(account).simulate({
8187
});
8288
```
8389

90+
Override a contract's class — useful for testing against a mock implementation:
91+
92+
```typescript
93+
const result = await contract.methods.foo().simulate({
94+
stateOverrides: {
95+
contractClasses: [mockClass],
96+
contractInstances: [{ ...existingInstance, currentContractClassId: mockClass.id }],
97+
},
98+
});
99+
```
100+
84101
Use this to:
85102

86103
- Set up state preconditions without running a full setup transaction
87104
- Reproduce a bug from production by pinning storage to the values seen at a specific block
105+
- Swap a contract's bytecode for a mock implementation in tests
106+
- Simulate calls against contracts that aren't yet deployed
88107
- Test branches that depend on rare values without orchestrating the contract calls that produce them
89108

90109
## Further reading

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,20 @@ Aztec is in active development. Each version may introduce breaking changes that
1111

1212
### [Aztec.js] `simulate` accepts `stateOverrides` for fork-style testing
1313

14-
`Contract.methods.foo(...).simulate(...)` now accepts a `stateOverrides` option that injects values into the simulator's ephemeral world-state fork before the call runs. The first override flavor is `publicStorage`, which writes a `(contract, slot, value)` triple into the public-data tree as if a previous tx had set it. Overrides are scoped to the simulation; the real chain state is untouched.
14+
`Contract.methods.foo(...).simulate(...)` now accepts a `stateOverrides` option that injects values into the simulator's ephemeral world-state fork before the call runs. Three override flavors are supported:
15+
16+
- `publicStorage`: write a `(contract, slot, value)` triple into the public-data tree as if a previous tx had set it.
17+
- `contractClasses`: shadow contract classes in the simulator's contract DB as if they had been published on the class registry.
18+
- `contractInstances`: shadow contract instances at specific addresses as if they had been deployed (possibly pointing at one of the shadowed classes).
19+
20+
Overrides are scoped to the simulation; the real chain state is untouched.
1521

1622
```typescript
1723
const result = await contract.methods.read_balance(account).simulate({
1824
stateOverrides: {
1925
publicStorage: [{ contract: contract.address, slot: BALANCE_SLOT, value: new Fr(1_000_000n) }],
26+
contractClasses: [shadowedClass],
27+
contractInstances: [shadowedInstance],
2028
},
2129
});
2230
```

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
SequencerClient,
4040
type SequencerPublisher,
4141
} from '@aztec/sequencer-client';
42-
import { PublicProcessorFactory } from '@aztec/simulator/server';
42+
import { PublicContractsDB, PublicProcessorFactory } from '@aztec/simulator/server';
4343
import {
4444
AttestationsBlockWatcher,
4545
EpochPruneWatcher,
@@ -1549,7 +1549,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
15491549
maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
15501550
}),
15511551
});
1552-
const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config);
1552+
const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
1553+
if (stateOverrides) {
1554+
contractsDB.addContracts(stateOverrides);
1555+
}
1556+
const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config, contractsDB);
15531557

15541558
// REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
15551559
const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);

yarn-project/simulator/src/public/public_db_sources.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ export class PublicContractsDB implements PublicContractsDBInterface {
5555
this.log = createLogger('simulator:contracts-data-source', bindings);
5656
}
5757

58-
public addContracts(contractDeploymentData: ContractDeploymentData): void {
58+
/** Parses raw log data from the C++/NAPI bridge and inserts the resulting contracts into the current checkpoint. */
59+
public addContractsFromLogs(contractDeploymentData: ContractDeploymentData): void {
5960
const currentState = this.getCurrentState();
6061

6162
this.addContractClassesFromEvents(
@@ -71,8 +72,22 @@ export class PublicContractsDB implements PublicContractsDBInterface {
7172

7273
public addNewContracts(tx: Tx): void {
7374
const contractDeploymentData = AllContractDeploymentData.fromTx(tx);
74-
this.addContracts(contractDeploymentData.getNonRevertibleContractDeploymentData());
75-
this.addContracts(contractDeploymentData.getRevertibleContractDeploymentData());
75+
this.addContractsFromLogs(contractDeploymentData.getNonRevertibleContractDeploymentData());
76+
this.addContractsFromLogs(contractDeploymentData.getRevertibleContractDeploymentData());
77+
}
78+
79+
/** Inserts typed contract classes and instances directly into the current checkpoint. */
80+
public addContracts(contracts: {
81+
contractClasses?: ContractClassPublic[];
82+
contractInstances?: ContractInstanceWithAddress[];
83+
}): void {
84+
const currentState = this.getCurrentState();
85+
for (const contractClass of contracts.contractClasses ?? []) {
86+
currentState.addClass(contractClass.id, contractClass);
87+
}
88+
for (const instance of contracts.contractInstances ?? []) {
89+
currentState.addInstance(instance.address, instance);
90+
}
7691
}
7792

7893
/**

yarn-project/simulator/src/public/public_processor/public_processor.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,15 @@ export class PublicProcessorFactory {
7676
/**
7777
* Creates a new instance of a PublicProcessor.
7878
* @param globalVariables - The global variables for the block being processed.
79-
* @param skipFeeEnforcement - Allows disabling balance checks for fee estimations.
79+
* @param contractsDB - Optional pre-populated contracts DB; a fresh one is constructed if omitted.
8080
* @returns A new instance of a PublicProcessor.
8181
*/
8282
public create(
8383
merkleTree: MerkleTreeWriteOperations,
8484
globalVariables: GlobalVariables,
8585
config: PublicSimulatorConfig,
86+
contractsDB: PublicContractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings()),
8687
): PublicProcessor {
87-
const bindings = this.log.getBindings();
88-
const contractsDB = new PublicContractsDB(this.contractDataSource, bindings);
89-
9088
const guardedFork = new GuardedMerkleTreeOperations(merkleTree);
9189
const publicTxSimulator = this.createPublicTxSimulator(guardedFork, contractsDB, globalVariables, config);
9290

@@ -97,7 +95,7 @@ export class PublicProcessorFactory {
9795
publicTxSimulator,
9896
this.dateProvider,
9997
this.telemetryClient,
100-
createLogger('simulator:public-processor', bindings),
98+
createLogger('simulator:public-processor', this.log.getBindings()),
10199
);
102100
}
103101

yarn-project/simulator/src/public/public_tx_simulator/contract_provider_for_cpp.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export class ContractProviderForCpp implements ContractProvider {
6161
// Construct ContractDeploymentData from plain object.
6262
const contractDeploymentData = ContractDeploymentData.fromPlainObject(rawData);
6363

64-
// Add contracts to the contracts DB
65-
this.log.trace(`Calling contractsDB.addContracts`);
66-
this.contractsDB.addContracts(contractDeploymentData);
64+
this.log.trace(`Calling contractsDB.addContractsFromLogs`);
65+
this.contractsDB.addContractsFromLogs(contractDeploymentData);
6766
};
6867

6968
public getBytecodeCommitment = async (classId: string): Promise<Buffer | undefined> => {

yarn-project/simulator/src/public/public_tx_simulator/public_tx_simulator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
401401
// However, things work as expected because later calls to getters on the hintingContractsDB
402402
// will pick up the new contracts and will generate the necessary hints.
403403
// So, a consumer of the hints will always see the new contracts.
404-
this.contractsDB.addContracts(context.nonRevertibleContractDeploymentData);
404+
this.contractsDB.addContractsFromLogs(context.nonRevertibleContractDeploymentData);
405405
}
406406

407407
/**
@@ -490,7 +490,7 @@ export class PublicTxSimulator implements PublicTxSimulatorInterface {
490490
// However, things work as expected because later calls to getters on the hintingContractsDB
491491
// will pick up the new contracts and will generate the necessary hints.
492492
// So, a consumer of the hints will always see the new contracts.
493-
this.contractsDB.addContracts(context.revertibleContractDeploymentData);
493+
this.contractsDB.addContractsFromLogs(context.revertibleContractDeploymentData);
494494
}
495495

496496
private async payFee(context: PublicTxContext) {
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import { z } from 'zod';
22

3+
import { type ContractClassPublic, ContractClassPublicSchema } from '../contract/interfaces/contract_class.js';
4+
import {
5+
type ContractInstanceWithAddress,
6+
ContractInstanceWithAddressSchema,
7+
} from '../contract/interfaces/contract_instance.js';
38
import { type PublicStorageOverride, PublicStorageOverrideSchema } from './public_storage_override.js';
49

510
/**
611
* Pre-simulation state overrides. Each field is optional and additive: the simulator applies all
7-
* provided overrides to the ephemeral world-state fork (in order) before running the tx.
12+
* provided overrides to the ephemeral world-state fork in order (and contract DB) before running the tx.
13+
*
14+
* - `publicStorage`: write specific (contract, slot, value) entries in the public-data tree
15+
* - `contractClasses`: shadow contract classes in the contract DB
16+
* - `contractInstances`: shadow contract instances in the contract DB
817
*/
918
export type StateOverrides = {
1019
/** Public-storage writes to apply before simulation. */
1120
publicStorage?: PublicStorageOverride[];
21+
/** Contract classes to shadow in the contract DB for the duration of the simulation. */
22+
contractClasses?: ContractClassPublic[];
23+
/** Contract instances to shadow in the contract DB for the duration of the simulation. */
24+
contractInstances?: ContractInstanceWithAddress[];
1225
};
1326

1427
export const StateOverridesSchema = z.object({
1528
publicStorage: z.array(PublicStorageOverrideSchema).optional(),
29+
contractClasses: z.array(ContractClassPublicSchema).optional(),
30+
contractInstances: z.array(ContractInstanceWithAddressSchema).optional(),
1631
});

0 commit comments

Comments
 (0)