Skip to content

Commit f23aa82

Browse files
authored
feat: merge-train/fairies (#23055)
BEGIN_COMMIT_OVERRIDE feat: allow setting additional scopes in nr tests (#22968) feat(pxe): add execution hooks for authorizing cross-contract utility calls (#23007) END_COMMIT_OVERRIDE
2 parents a248d30 + 149b3b7 commit f23aa82

29 files changed

Lines changed: 534 additions & 93 deletions

File tree

docs/docs-developers/docs/aztec-nr/debugging.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,33 @@ LOG_LEVEL=verbose aztec start --local-network
3535
| `No public key registered for address` | Call `wallet.registerSender(...)` |
3636
| `Direct invocation of ... functions is not supported` | Use `self.call()`, `self.view()`, or `self.enqueue()` to [call contract functions](framework-description/calling_contracts.md) |
3737
| `Failed to solve brillig function` | Check function parameters and note validity |
38+
| `Cross-contract utility call denied` | Configure an `authorizeUtilityCall` [execution hook](#cross-contract-utility-call-denied) on your PXE |
39+
40+
#### Cross-contract utility call denied
41+
42+
When a contract executes a utility function that calls into a different contract, PXE asks an **execution hook** whether the call should be allowed. If no hook is configured, or the hook denies the request, you will see:
43+
44+
```
45+
Cross-contract utility call denied: <reason>. <caller> attempted to call <target>:<selector> (<name>).
46+
```
47+
48+
To fix this, pass an `authorizeUtilityCall` hook when creating your PXE:
49+
50+
```typescript
51+
import { PXE } from "@aztec/pxe/server";
52+
53+
const pxe = await PXE.create({
54+
// ...other options
55+
hooks: {
56+
authorizeUtilityCall: async (request) => {
57+
// Inspect request.caller, request.target, request.functionSelector, etc.
58+
return { authorized: true };
59+
},
60+
},
61+
});
62+
```
63+
64+
The hook receives a `UtilityCallAuthorizationRequest` with the caller address, target address, function selector, function name, arguments, and caller context (`'private'` or `'utility'`). Return `{ authorized: true }` to allow or `{ authorized: false, reason: '...' }` to deny with a message.
3865

3966
### Circuit Errors
4067

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.nr] TXE `call_public_incognito` no longer takes a `from` parameter
13+
14+
`TestEnvironment::call_public_incognito` previously accepted a `from` address that was silently ignored (the function always uses a null `msg_sender`). The `from` parameter has been removed.
15+
16+
```diff
17+
- env.call_public_incognito(sender, SampleContract::at(addr).some_function());
18+
+ env.call_public_incognito(SampleContract::at(addr).some_function());
19+
```
20+
21+
If you need to call a public function *with* a sender, use `call_public` instead.
22+
23+
### [Aztec.nr] TXE `view_public_incognito` is deprecated
24+
25+
`TestEnvironment::view_public_incognito` is now deprecated in favor of `view_public`, which has the same behavior (null `msg_sender`, static call).
26+
27+
```diff
28+
- env.view_public_incognito(SampleContract::at(addr).some_view());
29+
+ env.view_public(SampleContract::at(addr).some_view());
30+
```
31+
1232
### [Aztec.js] `DeployMethod` address-affecting parameters move to construction time
1333

1434
Salt, deployer, and public keys are now passed when the `DeployMethod` is constructed, not on every call to `send` / `simulate` / `request` / `getInstance`. This locks the contract address once it is determined and prevents the silent salt-cache poisoning bug where the address could change between calls.

docs/netlify.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,3 +813,8 @@
813813
# PXE: capsule operation attempted with a scope not in the allowed scopes list
814814
from = "/errors/10"
815815
to = "/developers/docs/aztec-nr/framework-description/advanced/how_to_use_capsules"
816+
817+
[[redirects]]
818+
# PXE: cross-contract utility call denied by execution hook
819+
from = "/errors/11"
820+
to = "/developers/docs/aztec-nr/debugging#cross-contract-utility-call-denied"

noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,80 @@ struct UtilityContextOptions {
125125
contract_address: Option<AztecAddress>,
126126
}
127127

128+
/// Configuration for [`TestEnvironment::call_private_opts`].
129+
///
130+
/// Constructed by calling [`CallPrivateOptions::new`] and then chaining methods setting each value:
131+
///
132+
/// ```noir
133+
/// env.call_private_opts(from, CallPrivateOptions::new().with_additional_scopes([other]), call);
134+
/// ```
135+
pub struct CallPrivateOptions<let N: u32> {
136+
additional_scopes: [AztecAddress; N],
137+
}
138+
139+
impl CallPrivateOptions<0> {
140+
/// Creates default `CallPrivateOptions`.
141+
///
142+
/// The default values are the same as if using [`TestEnvironment::call_private`] instead of
143+
/// [`TestEnvironment::call_private_opts`].
144+
pub fn new() -> Self {
145+
CallPrivateOptions { additional_scopes: [] }
146+
}
147+
}
148+
149+
impl<let N: u32> CallPrivateOptions<N> {
150+
/// Grants access to secrets of additional accounts.
151+
///
152+
/// By default only the secrets that belong to the `from` account can be accessed: this function lets contracts
153+
/// retrieve private information from other accounts during execution.
154+
///
155+
/// Example usage includes accounts withdrawing from an escrow, which may require accessing the escrow's own notes
156+
/// and secrets.
157+
pub fn with_additional_scopes<let N_2: u32>(
158+
_self: Self,
159+
additional_scopes: [AztecAddress; N_2],
160+
) -> CallPrivateOptions<N_2> {
161+
CallPrivateOptions { additional_scopes }
162+
}
163+
}
164+
165+
/// Configuration for [`TestEnvironment::view_private_opts`].
166+
///
167+
/// Constructed by calling [`ViewPrivateOptions::new`] and then chaining methods setting each value:
168+
///
169+
/// ```noir
170+
/// env.view_private_opts(from, ViewPrivateOptions::new().with_additional_scopes([other]), call);
171+
/// ```
172+
pub struct ViewPrivateOptions<let S: u32> {
173+
additional_scopes: [AztecAddress; S],
174+
}
175+
176+
impl ViewPrivateOptions<0> {
177+
/// Creates default `ViewPrivateOptions`.
178+
///
179+
/// The default values are the same as if using [`TestEnvironment::view_private`] instead of
180+
/// [`TestEnvironment::view_private_opts`].
181+
pub fn new() -> Self {
182+
ViewPrivateOptions { additional_scopes: [] }
183+
}
184+
}
185+
186+
impl<let S: u32> ViewPrivateOptions<S> {
187+
/// Grants access to secrets of additional accounts.
188+
///
189+
/// By default only the secrets that belong to the `from` account can be accessed: this function lets contracts
190+
/// retrieve private information from other accounts during execution.
191+
///
192+
/// Example usage includes accounts querying an escrow's balance, which may require accessing the escrow's own
193+
/// notes.
194+
pub fn with_additional_scopes<let S2: u32>(
195+
_self: Self,
196+
additional_scopes: [AztecAddress; S2],
197+
) -> ViewPrivateOptions<S2> {
198+
ViewPrivateOptions { additional_scopes }
199+
}
200+
}
201+
128202
struct NoteDiscoveryOptions {
129203
contract_address: Option<AztecAddress>,
130204
}
@@ -544,20 +618,34 @@ impl TestEnvironment {
544618
/// let return_value = env.call_private(caller, SampleContract::at(contract_addr).sample_private_function());
545619
/// ```
546620
pub unconstrained fn call_private<let M: u32, let N: u32, T>(
621+
self: Self,
622+
from: AztecAddress,
623+
call: PrivateCall<M, N, T>,
624+
) -> T
625+
where
626+
T: Deserialize,
627+
{
628+
self.call_private_opts(from, CallPrivateOptions::new(), call)
629+
}
630+
631+
/// Variant of `call_private` which allows specifying multiple configuration values via `CallPrivateOptions`.
632+
pub unconstrained fn call_private_opts<let M: u32, let N: u32, let S: u32, T>(
547633
_self: Self,
548634
from: AztecAddress,
635+
opts: CallPrivateOptions<S>,
549636
call: PrivateCall<M, N, T>,
550637
) -> T
551638
where
552639
T: Deserialize,
553640
{
554641
let serialized_return_values = txe_oracles::private_call_new_flow(
555-
from,
642+
Option::some(from),
556643
call.target_contract,
557644
call.selector,
558645
call.args,
559646
hash_args(call.args),
560647
/*is_static=*/ false,
648+
opts.additional_scopes,
561649
);
562650

563651
T::deserialize(serialized_return_values)
@@ -571,20 +659,34 @@ impl TestEnvironment {
571659
/// The `from` parameter specifies the account from whose perspective the view is executed. This affects
572660
/// scope isolation - only notes scoped to this account will be visible during execution.
573661
pub unconstrained fn view_private<let M: u32, let N: u32, T>(
662+
self: Self,
663+
from: AztecAddress,
664+
call: PrivateStaticCall<M, N, T>,
665+
) -> T
666+
where
667+
T: Deserialize,
668+
{
669+
self.view_private_opts(from, ViewPrivateOptions::new(), call)
670+
}
671+
672+
/// Variant of `view_private` which allows specifying multiple configuration values via `ViewPrivateOptions`.
673+
pub unconstrained fn view_private_opts<let M: u32, let N: u32, let S: u32, T>(
574674
_self: Self,
575675
from: AztecAddress,
676+
opts: ViewPrivateOptions<S>,
576677
call: PrivateStaticCall<M, N, T>,
577678
) -> T
578679
where
579680
T: Deserialize,
580681
{
581682
let serialized_return_values = txe_oracles::private_call_new_flow(
582-
from,
683+
Option::some(from),
583684
call.target_contract,
584685
call.selector,
585686
call.args,
586687
hash_args(call.args),
587688
/*is_static=*/ true,
689+
opts.additional_scopes,
588690
);
589691

590692
T::deserialize(serialized_return_values)
@@ -705,16 +807,12 @@ impl TestEnvironment {
705807
/// Performs a public contract function call, including the processing of any nested public calls. Returns the
706808
/// result of the called function. Variant of `call_public`, but the `from` address (`msg_sender`) is set to
707809
/// "null".
708-
pub unconstrained fn call_public_incognito<let M: u32, let N: u32, T>(
709-
_self: Self,
710-
from: AztecAddress,
711-
call: PublicCall<M, N, T>,
712-
) -> T
810+
pub unconstrained fn call_public_incognito<let M: u32, let N: u32, T>(_self: Self, call: PublicCall<M, N, T>) -> T
713811
where
714812
T: Deserialize,
715813
{
716814
let serialized_return_values = txe_oracles::public_call_new_flow(
717-
Option::some(from),
815+
Option::none(),
718816
call.target_contract,
719817
call.selector,
720818
call.args,
@@ -727,13 +825,14 @@ impl TestEnvironment {
727825
/// Variant of `call_public` for public `#[view]` functions.
728826
///
729827
/// Unlike `call_public`, no transaction is created and no block is mined (since `#[view]` functions are only
730-
/// executable in a static context, and these produce no side effects).
828+
/// executable in a static context, and these produce no side effects). The `msg_sender` is set to "null", since
829+
/// view calls have no real caller.
731830
pub unconstrained fn view_public<let M: u32, let N: u32, T>(_self: Self, call: PublicStaticCall<M, N, T>) -> T
732831
where
733832
T: Deserialize,
734833
{
735834
let serialized_return_values = txe_oracles::public_call_new_flow(
736-
Option::some(AztecAddress::zero()),
835+
Option::none(),
737836
call.target_contract,
738837
call.selector,
739838
call.args,
@@ -743,26 +842,15 @@ impl TestEnvironment {
743842
T::deserialize(serialized_return_values)
744843
}
745844

746-
/// Variant of `view_public`, but the `from` address (`msg_sender`) is set to "null"
747-
///
748-
/// Unlike `call_public`, no transaction is created and no block is mined (since `#[view]` functions are only
749-
/// executable in a static context, and these produce no side effects).
845+
#[deprecated("use `TestEnvironment::view_public` instead")]
750846
pub unconstrained fn view_public_incognito<let M: u32, let N: u32, T>(
751-
_self: Self,
847+
self: Self,
752848
call: PublicStaticCall<M, N, T>,
753849
) -> T
754850
where
755851
T: Deserialize,
756852
{
757-
let serialized_return_values = txe_oracles::public_call_new_flow(
758-
Option::none(),
759-
call.target_contract,
760-
call.selector,
761-
call.args,
762-
true,
763-
);
764-
765-
T::deserialize(serialized_return_values)
853+
self.view_public(call)
766854
}
767855

768856
/// Discovers a note from a [`NoteMessage`], which is expected to have been created in the last transaction

noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::protocol::{
88
address::AztecAddress,
99
constants::{
1010
CONTRACT_INSTANCE_LENGTH, MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, MAX_PRIVATE_LOGS_PER_TX,
11-
NULL_MSG_SENDER_CONTRACT_ADDRESS, PRIVATE_LOG_SIZE_IN_FIELDS,
11+
PRIVATE_LOG_SIZE_IN_FIELDS,
1212
},
1313
contract_instance::ContractInstance,
1414
traits::{Deserialize, ToField},
@@ -36,13 +36,14 @@ pub unconstrained fn deploy<let M: u32, let N: u32, let P: u32>(
3636
ContractInstance::deserialize(instance_fields)
3737
}
3838

39-
pub unconstrained fn private_call_new_flow<let M: u32, let N: u32>(
40-
from: AztecAddress,
39+
pub unconstrained fn private_call_new_flow<let M: u32, let N: u32, let S: u32>(
40+
from: Option<AztecAddress>,
4141
contract_address: AztecAddress,
4242
function_selector: FunctionSelector,
4343
args: [Field; M],
4444
args_hash: Field,
4545
is_static_call: bool,
46+
additional_scopes: [AztecAddress; S],
4647
) -> [Field; N] {
4748
private_call_new_flow_oracle(
4849
from,
@@ -51,6 +52,7 @@ pub unconstrained fn private_call_new_flow<let M: u32, let N: u32>(
5152
args,
5253
args_hash,
5354
is_static_call,
55+
additional_scopes,
5456
)
5557
}
5658

@@ -63,8 +65,6 @@ pub unconstrained fn public_call_new_flow<let M: u32, let N: u32>(
6365
) -> [Field; N] {
6466
let calldata = [function_selector.to_field()].concat(args);
6567

66-
let from = from.unwrap_or(NULL_MSG_SENDER_CONTRACT_ADDRESS);
67-
6868
public_call_new_flow_oracle(from, contract_address, calldata, is_static_call)
6969
}
7070

@@ -209,18 +209,19 @@ pub unconstrained fn add_account(secret: Field) -> TestAccount {}
209209
pub unconstrained fn add_authwit(address: AztecAddress, message_hash: Field) {}
210210

211211
#[oracle(aztec_txe_privateCallNewFlow)]
212-
unconstrained fn private_call_new_flow_oracle<let M: u32, let N: u32>(
213-
_from: AztecAddress,
212+
unconstrained fn private_call_new_flow_oracle<let M: u32, let N: u32, let S: u32>(
213+
_from: Option<AztecAddress>,
214214
_contract_address: AztecAddress,
215215
_function_selector: FunctionSelector,
216216
_args: [Field; M],
217217
_args_hash: Field,
218218
_is_static_call: bool,
219+
_additional_scopes: [AztecAddress; S],
219220
) -> [Field; N] {}
220221

221222
#[oracle(aztec_txe_publicCallNewFlow)]
222223
unconstrained fn public_call_new_flow_oracle<let M: u32, let N: u32>(
223-
from: AztecAddress,
224+
from: Option<AztecAddress>,
224225
contract_address: AztecAddress,
225226
calldata: [Field; M],
226227
is_static_call: bool,

0 commit comments

Comments
 (0)