Skip to content

Commit 23d58bd

Browse files
authored
Merge branch 'merge-train/fairies-v5' into mv/f-766-handshake-facts
2 parents a8df51f + 7cdbf3a commit 23d58bd

7 files changed

Lines changed: 77 additions & 9 deletions

File tree

docs/docs-developers/docs/aztec-nr/framework-description/calling_contracts.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,46 @@ self.enqueue(Token::at(token_address).mint_to_public(recipient, amount));
6767
:::info
6868
Public functions execute after all private execution completes. Return values are not available in the private context. Learn more about [call types](../../foundational-topics/call_types.md).
6969
:::
70+
71+
## Utility calls
72+
73+
Utility functions can call other utility functions. These calls run entirely offchain in PXE and are never proven, so no guarantees are made on the correctness of their results.
74+
75+
### Utility-to-utility calls
76+
77+
From a utility function, use `self.call()` as with other call types:
78+
79+
```rust
80+
let balance = self.call(Token::at(token_address).get_balance_of(owner));
81+
```
82+
83+
To call a utility function of your own contract, use the `self.call_self` stubs instead:
84+
85+
```rust
86+
self.call_self.my_utility_function(args)
87+
```
88+
89+
### Private-to-utility calls
90+
91+
Private functions can also call utility functions, through `self.utility`. The call executes unconstrained code, so it must be wrapped in `unsafe`, and its result is not proven: use it to inform logic, never as an input to a constrained assertion without validating it first.
92+
93+
```rust
94+
// Safety: result is unconstrained
95+
let balance = unsafe { self.utility.call(Token::at(token_address).get_balance_of(owner)) };
96+
```
97+
98+
The same-contract stubs are available here as `self.utility.call_self`:
99+
100+
```rust
101+
unsafe { self.utility.call_self.my_utility_function(args) }
102+
```
103+
104+
Public functions cannot call utility functions: they run on the sequencer, which has no access to private state or PXE.
105+
106+
### Cross-contract authorization
107+
108+
A utility call to the calling contract itself always succeeds. A call to a _different_ contract can expose that contract's private state to the caller, so PXE asks the wallet to authorize it before proceeding. If the wallet denies the call, or no `authorizeUtilityCall` hook is configured, the simulation fails with `Cross-contract utility call denied`. See [execution hooks](../../foundational-topics/pxe/execution_hooks.md#authorizeutilitycall) for how wallets handle these requests, and for authorizing targets in Noir tests with `with_authorized_utility_call_targets`.
109+
110+
### `msg_sender` in nested utility calls
111+
112+
A utility function called from another contract can read the calling contract's address via `self.msg_sender()`, mirroring private and public functions. A utility function invoked directly by an application has no caller: `self.msg_sender()` panics, and `self.context.maybe_msg_sender()` returns `Option::none()`. Within a simulation PXE takes this value from the actual call graph, but utility execution as a whole is unconstrained, so contracts must not rely on it as a security guarantee.

docs/docs-developers/docs/aztec-nr/framework-description/functions/how_to_define_functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Public functions operate on public state, similar to EVM contracts. They can wri
4949

5050
Create offchain query functions using the `#[external("utility")]` annotation with `unconstrained`.
5151

52-
Utility functions are standalone unconstrained functions that cannot be called from private or public functions. They are meant to be called by _applications_ to perform auxiliary tasks like querying contract state or processing offchain messages. Example:
52+
Utility functions are unconstrained functions that applications call to perform auxiliary tasks, like querying contract state or processing offchain messages. Their execution is never proven, so no guarantees are made on the correctness of their results. They can also be called from other utility functions and from private functions. See [utility calls](../calling_contracts.md#utility-calls). Public functions cannot call them. Example:
5353

5454
#include_code get_counter /docs/examples/contracts/counter_contract/src/main.nr rust
5555

docs/docs-developers/docs/foundational-topics/call_types.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,13 @@ Public functions can be called either directly in a public context (as shown abo
187187

188188
### Utility
189189

190-
Contract functions marked with `#[external("utility")]` cannot be called as part of a transaction. They are only invoked by applications that interact with contracts for:
190+
Contract functions marked with `#[external("utility")]` are never proven as part of a transaction, even when called from a private function. They are invoked by applications that interact with contracts for:
191191

192192
- **State queries**: Reading from both private and public state via an offchain client
193193
- **Local state management**: Modifying contract-related PXE state (e.g., processing logs in Aztec.nr)
194194

195+
Utility functions can also be called from other utility functions, and from private functions as unconstrained code. Calls that cross a contract boundary require wallet authorization. See [utility calls](../aztec-nr/framework-description/calling_contracts.md#utility-calls) for how to make them.
196+
195197
Since utility execution is unconstrained and relies heavily on oracle calls, no guarantees are made on the correctness of results. However, you can verify that the bytecode being executed is correct, since a contract's address includes a commitment to all of its utility functions.
196198

197199
### aztec.js

docs/netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@
902902
[[redirects]]
903903
# PXE: cross-contract utility call denied by execution hook
904904
from = "/errors/11"
905-
to = "/developers/docs/aztec-nr/debugging#cross-contract-utility-call-denied"
905+
to = "/developers/docs/aztec-nr/framework-description/calling_contracts#cross-contract-authorization"
906906

907907
[[redirects]]
908908
# Incompatible oracle version between test and test environment

noir-projects/aztec-nr/aztec/src/contract_self/contract_self_private.nr

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,11 +436,24 @@ pub struct PrivateUtilityCalls<CallSelf> {
436436
impl<CallSelf> PrivateUtilityCalls<CallSelf> {
437437
/// Makes a utility contract call from a private function.
438438
///
439-
/// Note: only same-contract utility calls are currently supported. See TODO(F-29).
439+
/// The call executes unconstrained code, so it must be wrapped in `unsafe`, and its result is not part of any
440+
/// circuit proof: use it to inform logic, never as an input to a constrained assertion without validating it
441+
/// first.
442+
///
443+
/// The callee observes this contract's address as its `msg_sender`: see the `msg_sender` docs on
444+
/// [`ContractSelfUtility`](crate::contract_self::ContractSelfUtility).
445+
///
446+
/// To call one of this contract's own utility functions, prefer the type-safe `self.utility.call_self` stubs.
447+
///
448+
/// ## Authorization
449+
///
450+
/// A call to this contract itself always succeeds. A call to a different contract can expose that contract's
451+
/// private state to the caller, so it requires explicit authorization. A call that is not authorized fails.
440452
///
441453
/// # Example
442454
/// ```noir
443-
/// unsafe { self.utility.call(Token::at(self.address).get_balance_of(owner)) }
455+
/// // Safety: result is unconstrained
456+
/// unsafe { self.utility.call(Token::at(address).get_balance_of(owner)) }
444457
/// ```
445458
pub unconstrained fn call<let M: u32, let N: u32, T>(_self: Self, call: UtilityCall<M, N, T>) -> T
446459
where

noir-projects/aztec-nr/aztec/src/contract_self/contract_self_utility.nr

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,17 @@ impl<Storage, CallSelf> ContractSelfUtility<Storage, CallSelf> {
7575

7676
/// Makes a utility contract call from another utility function.
7777
///
78-
/// Note: only same-contract utility calls are currently supported. See TODO(F-29).
78+
/// The call runs entirely offchain in PXE and is never proven, so no guarantees are made on the correctness of
79+
/// its result.
80+
///
81+
/// The callee observes this contract's address as its `msg_sender`: see [`ContractSelfUtility::msg_sender`].
82+
///
83+
/// To call one of this contract's own utility functions, prefer the type-safe `self.call_self` stubs.
84+
///
85+
/// ## Authorization
86+
///
87+
/// A call to this contract itself always succeeds. A call to a different contract can expose that contract's
88+
/// private state to the caller, so it requires explicit authorization. A call that is not authorized fails.
7989
///
8090
/// # Example
8191
/// ```noir

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export abstract class BaseWallet implements Wallet {
358358
if (artifact) {
359359
await this.pxe.registerContractClass(artifact);
360360
}
361-
await this.pxe.registerContract(instance);
361+
const contractAddress = await this.pxe.registerContract(instance);
362362

363363
if (secretKeyOrKeys) {
364364
// PXE never receives the account seed (from which the message-signing/fallback secret keys could be re-derived):
@@ -372,9 +372,9 @@ export abstract class BaseWallet implements Wallet {
372372
? await deriveKeys(secretKeyOrKeys)
373373
: await deriveKeysFromMasterSecretKeys(secretKeyOrKeys);
374374
const { address } = await this.pxe.registerAccount(derivedKeys, await computePartialAddress(instance));
375-
if (!address.equals(instance.address)) {
375+
if (!address.equals(contractAddress)) {
376376
throw new Error(
377-
`Registered account address ${address.toString()} does not match contract instance address ${instance.address.toString()}: the provided keys do not correspond to this account.`,
377+
`Registered account address ${address.toString()} does not match contract instance address ${contractAddress.toString()}: the provided keys do not correspond to this account.`,
378378
);
379379
}
380380
}

0 commit comments

Comments
 (0)