Skip to content

Commit c5f9cb8

Browse files
committed
feat(pxe,nr): allow simulation overrides to sync without exclusions
Mirrors the real-account note storage (signing_public_key) on the simulated Schnorr/ECDSA account stubs and removes the manual sync_state() override so the #[aztec] macro generates a sync_state matching the real contract's selector. With matching note storage, the macro-generated _compute_note_hash can decode the real account's signing_public_key note during simulation sync. Removes the PXE setExcludedFromSync plumbing that previously skipped overridden contracts from sync. Required for fastForwardContractUpdate's private-call simulation to discover notes through the override.
1 parent 41916d2 commit c5f9cb8

7 files changed

Lines changed: 38 additions & 93 deletions

File tree

noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/Nargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ type = "contract"
66

77
[dependencies]
88
aztec = { path = "../../../../aztec-nr/aztec" }
9+
ecdsa_public_key_note = { path = "../../libs/ecdsa_public_key_note" }

noir-projects/noir-contracts/contracts/account/simulated_ecdsa_account_contract/src/main.nr

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@ use aztec::macros::aztec;
33
// Stub account contract for ECDSA accounts (both secp256k1 and secp256r1) used during simulation.
44
// Matches the constructor signature of EcdsaKAccount / EcdsaRAccount so that deployment
55
// simulations using this stub as an override do not fail on selector lookup.
6-
// See simulated_account_contract for the base stub without a constructor.
6+
// Mirrors the EcdsaKAccount/EcdsaRAccount storage layout so simulation sync can decode
7+
// the real account's signing_public_key note.
78
#[aztec]
89
pub contract SimulatedEcdsaAccount {
910
use aztec::{
1011
authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload},
1112
context::PrivateContext,
12-
macros::functions::{allow_phase_change, external, view},
13+
macros::{functions::{allow_phase_change, external, view}, storage::storage},
1314
messages::encoding::MESSAGE_CIPHERTEXT_LEN,
1415
oracle::random::random,
16+
state_vars::SinglePrivateImmutable,
1517
};
1618

19+
use ecdsa_public_key_note::EcdsaPublicKeyNote;
20+
21+
#[storage]
22+
struct Storage<Context> {
23+
signing_public_key: SinglePrivateImmutable<EcdsaPublicKeyNote, Context>,
24+
}
25+
1726
// Stub constructor matching the EcdsaKAccount / EcdsaRAccount constructor signature.
1827
// Does NOT use #[initializer] so that the macro does not inject
1928
// assert_initialization_matches_address_preimage_private, which would fail during kernelless
@@ -67,9 +76,4 @@ pub contract SimulatedEcdsaAccount {
6776
fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool {
6877
true
6978
}
70-
71-
#[external("utility")]
72-
unconstrained fn sync_state() {
73-
assert(false, "BUG ALERT: sync_state on a simulated account contract should never be triggered.");
74-
}
7579
}

noir-projects/noir-contracts/contracts/account/simulated_schnorr_account_contract/src/main.nr

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
1+
mod public_key_note;
2+
13
use aztec::macros::aztec;
24

35
// Stub account contract for Schnorr accounts used during simulation.
46
// Matches the constructor signature of SchnorrAccount so that deployment
57
// simulations using this stub as an override do not fail on selector lookup.
6-
// See simulated_account_contract for the base stub without a constructor.
8+
// Mirrors the SchnorrAccount storage layout so simulation sync can decode
9+
// the real account's signing_public_key note.
710
#[aztec]
811
pub contract SimulatedSchnorrAccount {
912
use aztec::{
1013
authwit::{account::AccountActions, auth::IS_VALID_SELECTOR, entrypoint::app::AppPayload},
1114
context::PrivateContext,
12-
macros::functions::{allow_phase_change, external, view},
15+
macros::{functions::{allow_phase_change, external, view}, storage::storage},
1316
messages::encoding::MESSAGE_CIPHERTEXT_LEN,
1417
oracle::random::random,
18+
state_vars::SinglePrivateImmutable,
1519
};
1620

21+
use crate::public_key_note::PublicKeyNote;
22+
23+
#[storage]
24+
struct Storage<Context> {
25+
signing_public_key: SinglePrivateImmutable<PublicKeyNote, Context>,
26+
}
27+
1728
// Stub constructor matching the SchnorrAccount constructor signature.
1829
// Does NOT use #[initializer] so that the macro does not inject
1930
// assert_initialization_matches_address_preimage_private, which would fail during kernelless
@@ -67,9 +78,4 @@ pub contract SimulatedSchnorrAccount {
6778
fn is_valid_impl(_context: &mut PrivateContext, _outer_hash: Field) -> bool {
6879
true
6980
}
70-
71-
#[external("utility")]
72-
unconstrained fn sync_state() {
73-
assert(false, "BUG ALERT: sync_state on a simulated account contract should never be triggered.");
74-
}
7581
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use aztec::{macros::notes::note, protocol::traits::Packable};
2+
3+
// Mirrors PublicKeyNote in schnorr_account_contract so the stub's auto-generated _compute_note_hash
4+
// can decode the real account's signing_public_key note during simulation sync.
5+
#[derive(Eq, Packable)]
6+
#[note]
7+
pub struct PublicKeyNote {
8+
pub x: Field,
9+
pub y: Field,
10+
}

yarn-project/pxe/src/contract_sync/contract_sync_service.test.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,6 @@ describe('ContractSyncService', () => {
115115
expectSyncedScopes([scopeA], [scopeB]);
116116
});
117117

118-
it('skips sync for excluded contract in the same job', async () => {
119-
service.setExcludedFromSync(jobId, new Set([contractAddress.toString()]));
120-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
121-
expectNoSync();
122-
});
123-
124-
it('does not skip sync for excluded contract in a different job', async () => {
125-
service.setExcludedFromSync('other-job', new Set([contractAddress.toString()]));
126-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
127-
expectSyncedScopes([scopeA]);
128-
});
129-
130118
it('concurrent calls for same contract+scope share one sync promise', async () => {
131119
const p1 = service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [
132120
scopeA,
@@ -170,16 +158,6 @@ describe('ContractSyncService', () => {
170158
});
171159

172160
describe('commit', () => {
173-
it('clears exclusions for the given job', async () => {
174-
service.setExcludedFromSync(jobId, new Set([contractAddress.toString()]));
175-
await service.commit(jobId);
176-
177-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
178-
// When exclusions are set, contract sync is skipped. We verify the exclusions were cleared by confirming that sync
179-
// was actually triggered.
180-
expectSyncedScopes([scopeA]);
181-
});
182-
183161
it('does not clear sync cache', async () => {
184162
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
185163
await service.commit(jobId);
@@ -197,32 +175,6 @@ describe('ContractSyncService', () => {
197175
// We check that the sync cache was cleared by checking that the sync was triggered twice.
198176
expectSyncedScopes([scopeA], [scopeA]);
199177
});
200-
201-
it('clears exclusions for the given job', async () => {
202-
service.setExcludedFromSync(jobId, new Set([contractAddress.toString()]));
203-
await service.discardStaged(jobId);
204-
205-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
206-
// When exclusions are set, contract sync is skipped. We verify the exclusions were cleared by confirming that sync
207-
// was actually triggered.
208-
expectSyncedScopes([scopeA]);
209-
});
210-
211-
it('preserves exclusions for other jobs', async () => {
212-
service.setExcludedFromSync(jobId, new Set([contractAddress.toString()]));
213-
service.setExcludedFromSync('other-job', new Set([contractAddress.toString()]));
214-
await service.discardStaged(jobId);
215-
216-
// jobId exclusion cleared, sync proceeds
217-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, jobId, [scopeA]);
218-
expectSyncedScopes([scopeA]);
219-
220-
// other-job exclusion still active, sync skipped
221-
await service.ensureContractSynced(contractAddress, null, utilityExecutor, anchorBlockHeader, 'other-job', [
222-
scopeA,
223-
]);
224-
expectSyncedScopes([scopeA]);
225-
});
226178
});
227179

228180
describe('class ID verification deduplication', () => {

yarn-project/pxe/src/contract_sync/contract_sync_service.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ export class ContractSyncService implements StagedStore {
3131
// class ID verification is scope-independent. Cleared on wipe/discard.
3232
private verifiedClassIds: Map<string, Promise<void>> = new Map();
3333

34-
// Per-job excluded contract addresses - these contracts should not be synced.
35-
private excludedFromSync: Map<string, Set<string>> = new Map();
36-
3734
// Bounds the number of scope syncs running concurrently. Scopes beyond this limit queue here. Sized to trade off
3835
// parallelism on non-ACIR work (node RPC, note store reads) against memory pressure from concurrent circuit
3936
// execution.
@@ -46,11 +43,6 @@ export class ContractSyncService implements StagedStore {
4643
private log: Logger,
4744
) {}
4845

49-
/** Sets contracts that should be skipped during sync for a specific job. */
50-
setExcludedFromSync(jobId: string, addresses: Set<string>): void {
51-
this.excludedFromSync.set(jobId, addresses);
52-
}
53-
5446
/**
5547
* Ensures a contract's private state is synchronized and that the PXE holds the current class artifact.
5648
* 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 {
6860
jobId: string,
6961
scopes: AztecAddress[],
7062
): Promise<void> {
71-
if (this.#shouldSkipSync(jobId, contractAddress)) {
72-
return;
73-
}
74-
7563
this.#startSyncIfNeeded(
7664
contractAddress,
7765
scopes,
@@ -108,24 +96,17 @@ export class ContractSyncService implements StagedStore {
10896
this.verifiedClassIds.clear();
10997
}
11098

111-
commit(jobId: string): Promise<void> {
112-
// Clear excluded contracts for this job
113-
this.excludedFromSync.delete(jobId);
99+
commit(_jobId: string): Promise<void> {
114100
return Promise.resolve();
115101
}
116102

117-
discardStaged(jobId: string): Promise<void> {
103+
discardStaged(_jobId: string): Promise<void> {
118104
// We clear the synced contracts cache here because, when the job is discarded, any associated database writes from
119105
// the sync are also undone.
120106
this.syncedContracts.clear();
121107
this.verifiedClassIds.clear();
122-
this.excludedFromSync.delete(jobId);
123108
return Promise.resolve();
124109
}
125-
/** Returns true if sync should be skipped for this contract */
126-
#shouldSkipSync(jobId: string, contractAddress: AztecAddress): boolean {
127-
return !!this.excludedFromSync.get(jobId)?.has(contractAddress.toString());
128-
}
129110

130111
/**
131112
* If there are unsynced scopes, starts one sync per scope (bounded by #syncSlot) and stores each promise in the

yarn-project/pxe/src/pxe.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,22 +1002,13 @@ export class PXE {
10021002
const anchorBlockHeader = await this.anchorBlockStore.getBlockHeader();
10031003
const syncTime = syncTimer.ms();
10041004

1005-
const overriddenContracts = overrides?.contracts ? new Set(Object.keys(overrides.contracts)) : undefined;
1006-
const hasOverriddenContracts = overriddenContracts !== undefined && overriddenContracts.size > 0;
1007-
1008-
if (hasOverriddenContracts && !skipKernels) {
1005+
if (overrides?.contracts && Object.keys(overrides.contracts).length > 0 && !skipKernels) {
10091006
throw new Error(
10101007
'Simulating with overridden contracts is not compatible with kernel execution. Please set skipKernels to true when simulating with overridden contracts.',
10111008
);
10121009
}
10131010
const contractFunctionSimulator = this.#getSimulatorForTx(overrides);
10141011

1015-
if (hasOverriddenContracts) {
1016-
// Overridden contracts don't have a sync function, so calling sync on them would fail.
1017-
// We exclude them so the sync service skips them entirely.
1018-
this.contractSyncService.setExcludedFromSync(jobId, overriddenContracts);
1019-
}
1020-
10211012
// Execution of private functions only; no proving, and no kernel logic.
10221013
const privateExecutionResult = await this.#executePrivate({
10231014
contractFunctionSimulator,

0 commit comments

Comments
 (0)