Skip to content

Commit e8522ce

Browse files
nchamoaztec-bot
authored andcommitted
fix(pxe): sync target contract before cross-contract utility call (#23225)
1 parent 3cddecc commit e8522ce

10 files changed

Lines changed: 120 additions & 73 deletions

File tree

noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/main.nr

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@
33

44
use aztec::macros::aztec;
55

6+
mod pow_note;
67
mod test;
78

89
#[aztec]
910
pub contract NestedUtility {
10-
use aztec::macros::functions::external;
11-
use aztec::protocol::address::AztecAddress;
11+
use aztec::macros::{functions::external, storage::storage};
12+
use aztec::{
13+
messages::message_delivery::MessageDelivery,
14+
protocol::address::AztecAddress,
15+
state_vars::{Owned, PrivateMutable},
16+
};
17+
use crate::pow_note::PowNote;
18+
19+
#[storage]
20+
struct Storage<Context> {
21+
pow_args: Owned<PrivateMutable<PowNote, Context>, Context>,
22+
}
1223

1324
#[external("utility")]
1425
unconstrained fn pow_utility(x: Field, n: u32) -> Field {
@@ -24,6 +35,31 @@ pub contract NestedUtility {
2435
self.call(NestedUtility::at(target).pow_utility(x, n))
2536
}
2637

38+
/// Stores the base and exponent for pow in a private note.
39+
#[external("private")]
40+
fn set_pow_args(x: Field, n: Field) {
41+
let owner = self.msg_sender();
42+
self.storage.pow_args.at(owner).initialize_or_replace(|_| PowNote { x, n }).deliver(
43+
MessageDelivery.ONCHAIN_CONSTRAINED,
44+
);
45+
}
46+
47+
/// Reads x and n from storage and computes x^n.
48+
#[external("utility")]
49+
unconstrained fn pow_from_storage(owner: AztecAddress) -> Field {
50+
let note = self.storage.pow_args.at(owner).view_note();
51+
self.call_self.pow_utility(note.x, note.n as u32)
52+
}
53+
54+
/// Cross-contract version: calls pow_from_storage on the target contract.
55+
#[external("utility")]
56+
unconstrained fn delegate_pow_from_storage(
57+
target: AztecAddress,
58+
owner: AztecAddress,
59+
) -> Field {
60+
self.call(NestedUtility::at(target).pow_from_storage(owner))
61+
}
62+
2763
#[external("private")]
2864
fn pow_private(x: Field, n: u32) -> Field {
2965
// Safety: this is a test contract; the unconstrained result is returned directly
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use aztec::{macros::notes::note, protocol::traits::{Deserialize, Packable, Serialize}};
2+
3+
#[derive(Deserialize, Eq, Packable, Serialize)]
4+
#[note]
5+
pub struct PowNote {
6+
pub x: Field,
7+
pub n: Field,
8+
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,18 @@ describe('authorizeUtilityCall hook', () => {
153153
callerContext: 'private',
154154
});
155155
});
156+
157+
it('syncs target contract notes on cross-contract utility call', async () => {
158+
hookAllows = true;
159+
160+
// Store x=2, n=10 as private notes on contract B.
161+
await contractB.methods.set_pow_args(2n, 10n).send({ from: defaultAccountAddress });
162+
163+
// Cross-contract call from A → B: B must be synced before the nested utility call
164+
// so that B's notes (set above) are discovered.
165+
const { result: crossContractResult } = await contractA.methods
166+
.delegate_pow_from_storage(contractB.address, defaultAccountAddress)
167+
.simulate({ from: defaultAccountAddress });
168+
expect(crossContractResult).toEqual(2n ** 10n);
169+
});
156170
});

yarn-project/pxe/src/contract_function_simulator/contract_function_simulator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,10 @@ export class ContractFunctionSimulator {
337337
throw new Error(`Cannot run ${entryPointArtifact.functionType} function as utility`);
338338
}
339339

340+
const utilityExecutor = async (syncCall: FunctionCall, execScopes: AztecAddress[]) => {
341+
await this.runUtility(syncCall, [], anchorBlockHeader, execScopes, jobId);
342+
};
343+
340344
const oracle = new UtilityExecutionOracle({
341345
contractAddress: call.to,
342346
authWitnesses: authwits,
@@ -358,6 +362,7 @@ export class ContractFunctionSimulator {
358362
scopes,
359363
simulator: this.simulator,
360364
hooks: this.hooks,
365+
utilityExecutor,
361366
});
362367

363368
try {

yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ describe('Oracle Version Check test suite', () => {
214214
scopes: [],
215215
l2TipsStore,
216216
simulator,
217+
utilityExecutor: () => Promise.resolve(),
217218
});
218219
});
219220

yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { toACVMWitness } from '@aztec/simulator/client';
66
import {
77
type FunctionAbi,
88
type FunctionArtifact,
9-
type FunctionCall,
109
FunctionSelector,
1110
type NoteSelector,
1211
countArgumentsSize,
@@ -41,8 +40,6 @@ export type PrivateExecutionOracleArgs = Omit<UtilityExecutionOracleArgs, 'contr
4140
argsHash: Fr;
4241
txContext: TxContext;
4342
callContext: CallContext;
44-
/** Needed to trigger contract synchronization before nested calls */
45-
utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<void>;
4643
executionCache: HashedValuesCache;
4744
noteCache: ExecutionNoteCache;
4845
taggingIndexCache: ExecutionTaggingIndexCache;
@@ -74,7 +71,6 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
7471
private readonly argsHash: Fr;
7572
private readonly txContext: TxContext;
7673
private readonly callContext: CallContext;
77-
private readonly utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<void>;
7874
private readonly executionCache: HashedValuesCache;
7975
private readonly noteCache: ExecutionNoteCache;
8076
private readonly taggingIndexCache: ExecutionTaggingIndexCache;
@@ -95,7 +91,6 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP
9591
this.argsHash = args.argsHash;
9692
this.txContext = args.txContext;
9793
this.callContext = args.callContext;
98-
this.utilityExecutor = args.utilityExecutor;
9994
this.executionCache = args.executionCache;
10095
this.noteCache = args.noteCache;
10196
this.taggingIndexCache = args.taggingIndexCache;

yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts

Lines changed: 33 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import type { RecipientTaggingStore } from '../../storage/tagging_store/recipien
3535
import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_address_book_store.js';
3636
import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js';
3737
import { ContractFunctionSimulator } from '../contract_function_simulator.js';
38-
import { UtilityExecutionOracle } from './utility_execution_oracle.js';
38+
import { UtilityExecutionOracle, type UtilityExecutionOracleArgs } from './utility_execution_oracle.js';
3939

4040
describe('Utility Execution test suite', () => {
4141
const simulator = new WASMSimulator();
@@ -328,27 +328,7 @@ describe('Utility Execution test suite', () => {
328328

329329
scope = await AztecAddress.random();
330330

331-
utilityExecutionOracle = new UtilityExecutionOracle({
332-
contractAddress,
333-
authWitnesses: [],
334-
capsules: [],
335-
anchorBlockHeader,
336-
contractStore,
337-
noteStore,
338-
keyStore,
339-
addressStore,
340-
aztecNode,
341-
recipientTaggingStore,
342-
senderAddressBookStore,
343-
capsuleService: new CapsuleService(capsuleStore, [scope]),
344-
privateEventStore,
345-
messageContextService,
346-
contractSyncService,
347-
jobId: 'test-job-id',
348-
scopes: [scope],
349-
l2TipsStore,
350-
simulator,
351-
});
331+
utilityExecutionOracle = makeOracle({ contractAddress, scopes: [scope] });
352332
});
353333

354334
describe('Respects synced block number', () => {
@@ -393,29 +373,13 @@ describe('Utility Execution test suite', () => {
393373
const transientScoped = [Fr.random()];
394374
const persisted = [Fr.random()];
395375

396-
utilityExecutionOracle = new UtilityExecutionOracle({
376+
utilityExecutionOracle = makeOracle({
397377
contractAddress,
398-
authWitnesses: [],
378+
scopes: [scope],
399379
capsules: [
400380
new Capsule(contractAddress, slot, transientGlobal),
401381
new Capsule(contractAddress, slot, transientScoped, scope),
402382
],
403-
anchorBlockHeader,
404-
contractStore,
405-
noteStore,
406-
keyStore,
407-
addressStore,
408-
aztecNode,
409-
recipientTaggingStore,
410-
senderAddressBookStore,
411-
capsuleService: new CapsuleService(capsuleStore, [scope]),
412-
privateEventStore,
413-
messageContextService,
414-
contractSyncService,
415-
jobId: 'test-job-id',
416-
scopes: [scope],
417-
l2TipsStore,
418-
simulator,
419383
});
420384

421385
capsuleStore.getCapsule.mockResolvedValueOnce(persisted);
@@ -577,31 +541,8 @@ describe('Utility Execution test suite', () => {
577541
const contractAddressA = await AztecAddress.random();
578542
const contractAddressB = await AztecAddress.random();
579543

580-
const makeOracle = (addr: AztecAddress) =>
581-
new UtilityExecutionOracle({
582-
contractAddress: addr,
583-
authWitnesses: [],
584-
capsules: [],
585-
anchorBlockHeader,
586-
contractStore,
587-
noteStore,
588-
keyStore,
589-
addressStore,
590-
aztecNode,
591-
recipientTaggingStore,
592-
senderAddressBookStore,
593-
capsuleService: new CapsuleService(capsuleStore, []),
594-
privateEventStore,
595-
messageContextService,
596-
contractSyncService,
597-
jobId: 'test-job-id',
598-
scopes: [],
599-
l2TipsStore,
600-
simulator,
601-
});
602-
603-
const oracleA = makeOracle(contractAddressA);
604-
const oracleB = makeOracle(contractAddressB);
544+
const oracleA = makeOracle({ contractAddress: contractAddressA });
545+
const oracleB = makeOracle({ contractAddress: contractAddressB });
605546

606547
const secretA = await oracleA.getSharedSecret(owner, ephPk, contractAddressA);
607548
const secretB = await oracleB.getSharedSecret(owner, ephPk, contractAddressB);
@@ -622,5 +563,32 @@ describe('Utility Execution test suite', () => {
622563
await expect(utilityExecutionOracle.getSharedSecret(owner, ephPk, wrongAddress)).rejects.toThrow(/expected/);
623564
});
624565
});
566+
567+
const makeOracle = (overrides?: Partial<UtilityExecutionOracleArgs>) => {
568+
const scopes = overrides?.scopes ?? [];
569+
return new UtilityExecutionOracle({
570+
contractAddress,
571+
authWitnesses: [],
572+
capsules: [],
573+
anchorBlockHeader,
574+
contractStore,
575+
noteStore,
576+
keyStore,
577+
addressStore,
578+
aztecNode,
579+
recipientTaggingStore,
580+
senderAddressBookStore,
581+
capsuleService: new CapsuleService(capsuleStore, scopes),
582+
privateEventStore,
583+
messageContextService,
584+
contractSyncService,
585+
jobId: 'test-job-id',
586+
scopes,
587+
l2TipsStore,
588+
simulator,
589+
utilityExecutor: () => Promise.resolve(),
590+
...overrides,
591+
});
592+
};
625593
});
626594
});

yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
toACVMWitness,
1717
witnessMapToFields,
1818
} from '@aztec/simulator/client';
19-
import { FunctionSelector } from '@aztec/stdlib/abi';
19+
import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi';
2020
import type { AuthWitness } from '@aztec/stdlib/auth-witness';
2121
import { AztecAddress } from '@aztec/stdlib/aztec-address';
2222
import { BlockHash, type L2TipsProvider } from '@aztec/stdlib/block';
@@ -81,6 +81,8 @@ export type UtilityExecutionOracleArgs = {
8181
scopes: AztecAddress[];
8282
simulator: CircuitSimulator;
8383
hooks?: ExecutionHooks;
84+
/** Needed to trigger contract synchronization before nested cross-contract calls. */
85+
utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<void>;
8486
};
8587

8688
/**
@@ -119,6 +121,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
119121
protected readonly scopes: AztecAddress[];
120122
protected readonly simulator: CircuitSimulator;
121123
protected readonly hooks: ExecutionHooks | undefined;
124+
protected readonly utilityExecutor: (call: FunctionCall, scopes: AztecAddress[]) => Promise<void>;
122125

123126
constructor(args: UtilityExecutionOracleArgs) {
124127
this.contractAddress = args.contractAddress;
@@ -142,6 +145,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
142145
this.scopes = args.scopes;
143146
this.simulator = args.simulator;
144147
this.hooks = args.hooks;
148+
this.utilityExecutor = args.utilityExecutor;
145149
}
146150

147151
public assertCompatibleOracleVersion(major: number, minor: number): void {
@@ -949,6 +953,15 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
949953
`See https://docs.aztec.network/errors/11`,
950954
);
951955
}
956+
957+
await this.contractSyncService.ensureContractSynced(
958+
targetContractAddress,
959+
functionSelector,
960+
this.utilityExecutor,
961+
this.anchorBlockHeader,
962+
this.jobId,
963+
this.scopes,
964+
);
952965
}
953966

954967
this.logger.debug(
@@ -976,6 +989,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra
976989
scopes: this.scopes,
977990
simulator: this.simulator,
978991
hooks: this.hooks,
992+
utilityExecutor: this.utilityExecutor,
979993
log: this.logger,
980994
});
981995

yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,9 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
771771
try {
772772
const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader();
773773
const simulator = new WASMSimulator();
774+
const utilityExecutor = async (syncCall: FunctionCall, execScopes: AztecAddress[]) => {
775+
await this.executeUtilityCall(syncCall, execScopes, jobId);
776+
};
774777
const oracle = new UtilityExecutionOracle({
775778
contractAddress: call.to,
776779
authWitnesses: [],
@@ -794,6 +797,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
794797
hooks: composeHooks({
795798
authorizeUtilityCall: this.buildAuthorizeUtilityCallHook('utility', authorizedUtilityCallTargets),
796799
}),
800+
utilityExecutor,
797801
});
798802
const acirExecutionResult = await simulator
799803
.executeUserCircuit(toACVMWitness(0, call.args), entryPointArtifact, new Oracle(oracle).toACIRCallback())

yarn-project/txe/src/txe_session.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ export class TXESession implements TXESessionStateHandler {
562562
jobId: this.currentJobId,
563563
scopes: await this.keyStore.getAccounts(),
564564
simulator: new WASMSimulator(),
565+
utilityExecutor: this.utilityExecutorForContractSync(anchorBlockHeader),
565566
});
566567

567568
this.state = { name: 'UTILITY' };
@@ -661,6 +662,7 @@ export class TXESession implements TXESessionStateHandler {
661662
jobId: this.currentJobId,
662663
scopes,
663664
simulator,
665+
utilityExecutor: this.utilityExecutorForContractSync(anchorBlock),
664666
});
665667
await simulator
666668
.executeUserCircuit(toACVMWitness(0, call.args), entryPointArtifact, new Oracle(oracle).toACIRCallback())

0 commit comments

Comments
 (0)