Skip to content

Commit 8db20fd

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/spartan
2 parents 3cc5efd + 2fa2623 commit 8db20fd

31 files changed

Lines changed: 1943 additions & 816 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ It is also possible to create public functions that can _only_ be invoked by pri
132132

133133
A common pattern is to enqueue public calls to check some validity condition on public state, e.g. that a deadline has not expired or that some public value is set.
134134

135-
#include_code enqueueing /noir-projects/noir-contracts/contracts/protocol/public_checks_contract/src/utils.nr rust
135+
#include_code enqueueing /noir-projects/aztec-nr/aztec/src/public_checks.nr rust
136136

137137
Note that this reveals what public function is being called on what contract, and perhaps more importantly which contract enqueued the call during private execution.
138138
To prevent this you can enqueue a call to a public function using `self.enqueue_incognito` that behaves the same as `self.enqueue` but conceals the message sender.
@@ -146,7 +146,7 @@ An example of how a deadline can be checked using the `PublicChecks` contract fo
146146

147147
`privately_check_timestamp` and `privately_check_block_number` are helper functions around the call to the `PublicChecks` contract:
148148

149-
#include_code helper_public_checks_functions /noir-projects/noir-contracts/contracts/protocol/public_checks_contract/src/utils.nr rust
149+
#include_code helper_public_checks_functions /noir-projects/aztec-nr/aztec/src/public_checks.nr rust
150150

151151
This is what the implementation of the check timestamp functionality looks like:
152152

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

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

1010
## TBD
1111

12+
### [Aztec.nr] `public_checks` helpers moved to `aztec-nr`
13+
14+
The `privately_check_timestamp`, `privately_check_block_number`, and related caller helpers previously in `noir-contracts/contracts/protocol/public_checks_contract/src/utils.nr` are now in `aztec-nr/aztec/src/public_checks.nr`. Consumer contracts should update their imports:
15+
16+
```diff
17+
- use public_checks::utils::privately_check_timestamp;
18+
+ use aztec::public_checks::privately_check_timestamp;
19+
```
20+
1221
### [Aztec.js] `AccountManager.create` takes an options bag
1322

1423
`AccountManager.create` no longer takes `salt` as a positional argument. The trailing `salt?: Salt` parameter has been folded into a new `AccountManagerCreateOptions` bag alongside `immutablesHash` and `deployer`:

noir-projects/aztec-nr/aztec/src/lib.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub use protocol_types as protocol;
4646
pub(crate) mod logging;
4747
pub mod utils;
4848
pub mod authwit;
49+
pub mod public_checks;
4950
pub mod macros;
5051

5152
pub mod test;

noir-projects/aztec-nr/aztec/src/macros/dispatch.nr

Lines changed: 189 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,40 @@ use super::functions::initialization_utils::EMIT_PUBLIC_INIT_NULLIFIER_FN_NAME;
55
use super::utils::compute_fn_selector;
66
use std::panic;
77

8+
/// Minimum number of public functions that must share a parameter-type signature before we extract a shared
9+
/// unpack helper. See [compute_unpack_prelude] for the rationale behind the chosen value.
10+
global EXTRACTION_THRESHOLD: u32 = 4;
11+
812
/// Generates a `public_dispatch` function for an Aztec contract module `m`.
913
///
1014
/// The generated function dispatches public calls based on selector to the appropriate contract function. If
1115
/// `generate_emit_public_init_nullifier` is true, it also handles dispatch to the macro-generated
1216
/// `__emit_public_init_nullifier` function.
17+
///
18+
/// Alongside `public_dispatch`, this also emits one `__aztec_nr_internals__unpack_arguments_<N>` helper per
19+
/// parameter-type signature shared by enough public functions; see [compute_unpack_prelude] for the extraction
20+
/// criterion.
1321
pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nullifier: bool) -> Quoted {
1422
let functions = get_public_functions(m);
1523

1624
let unit = get_type::<()>();
1725

26+
// Count how many public functions share each parameter-type signature, so we can decide which signatures are
27+
// worth extracting into helpers.
28+
let signature_counts = &mut CHashMap::<Quoted, u32>::new();
29+
for function in functions {
30+
let parameters = function.parameters();
31+
if parameters.len() != 0 {
32+
let key = signature_key(parameters);
33+
let prior = signature_counts.get(key).unwrap_or(0);
34+
signature_counts.insert(key, prior + 1);
35+
}
36+
}
37+
1838
let seen_selectors = &mut CHashMap::<Field, Quoted>::new();
39+
let signature_to_helper_idx = &mut CHashMap::<Quoted, u32>::new();
40+
// The helper function definitions, in the order they were created.
41+
let unpack_helpers: &mut [Quoted] = &mut @[];
1942

2043
let mut ifs = functions.map(|function: FunctionDefinition| {
2144
let parameters = function.parameters();
@@ -35,48 +58,19 @@ pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nu
3558
}
3659
seen_selectors.insert(selector, fn_name);
3760

38-
let params_len_quote = get_params_len_quote(parameters);
39-
40-
let initial_read = if parameters.len() == 0 {
41-
quote {}
42-
} else {
43-
// The initial calldata_copy offset is 1 to skip the Field selector The expected calldata is the
44-
// serialization of
45-
// - FunctionSelector: the selector of the function intended to dispatch
46-
// - Parameters: the parameters of the function intended to dispatch That is, exactly what is expected for
47-
// a call to the target function, but with a selector added at the beginning.
48-
quote {
49-
let input_calldata: [Field; $params_len_quote] = aztec::oracle::avm::calldata_copy(1, $params_len_quote);
50-
let mut reader = aztec::protocol::utils::reader::Reader::new(input_calldata);
51-
}
52-
};
53-
54-
let parameter_index: &mut u32 = &mut 0;
55-
let reads = parameters.map(|param: (Quoted, Type)| {
56-
let parameter_index_value = *parameter_index;
57-
let param_name = f"arg{parameter_index_value}".quoted_contents();
58-
let param_type = param.1;
59-
let read = quote {
60-
let $param_name: $param_type = aztec::protocol::traits::Deserialize::stream_deserialize(&mut reader);
61-
};
62-
*parameter_index += 1;
63-
quote { $read }
64-
});
65-
let read = reads.join(quote { });
66-
67-
let mut args = @[];
68-
for parameter_index in 0..parameters.len() {
69-
let param_name = f"arg{parameter_index}".quoted_contents();
70-
args = args.push_back(quote { $param_name });
71-
}
61+
let (unpack_prelude, call_args) = compute_unpack_prelude(
62+
parameters,
63+
signature_counts,
64+
signature_to_helper_idx,
65+
unpack_helpers,
66+
);
7267

7368
// We call a function whose name is prefixed with `__aztec_nr_internals__`. This is necessary because the
7469
// original function is intentionally made uncallable, preventing direct invocation within the contract.
7570
// Instead, a new function with the same name, but prefixed by `__aztec_nr_internals__`, has been generated to
7671
// be called here. For more details see the `process_functions` function.
7772
let name = f"__aztec_nr_internals__{fn_name}".quoted_contents();
78-
let args = args.join(quote { , });
79-
let call = quote { $name($args) };
73+
let call = quote { $name($call_args) };
8074

8175
let return_code = if return_type == unit {
8276
quote {
@@ -93,8 +87,7 @@ pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nu
9387

9488
let if_ = quote {
9589
if selector == $selector {
96-
$initial_read
97-
$read
90+
$unpack_prelude
9891
$return_code
9992
}
10093
};
@@ -124,7 +117,11 @@ pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nu
124117
let ifs = ifs.push_back(quote { panic(f"Unknown selector {selector}") });
125118
let dispatch = ifs.join(quote { });
126119

120+
let helpers = (*unpack_helpers).join(quote { });
121+
127122
let body = quote {
123+
$helpers
124+
128125
// We mark this as public because our whole system depends on public functions having this attribute.
129126
#[aztec::macros::internals_functions_generation::abi_attributes::abi_public]
130127
pub unconstrained fn public_dispatch(selector: Field) {
@@ -136,6 +133,160 @@ pub comptime fn generate_public_dispatch(m: Module, generate_emit_public_init_nu
136133
}
137134
}
138135

136+
/// Canonical Quoted representation of a parameter list's types, used as the deduplication key for unpack helpers.
137+
comptime fn signature_key(parameters: [(Quoted, Type)]) -> Quoted {
138+
parameters
139+
.map(|param: (Quoted, Type)| {
140+
let param_type = param.1;
141+
quote { $param_type }
142+
})
143+
.join(quote { , })
144+
}
145+
146+
/// Builds the dispatch-arm prelude for a public function and the comma-separated arg list to pass through to the call.
147+
///
148+
/// If the function's signature reaches `EXTRACTION_THRESHOLD`, the prelude reuses (or creates) a shared
149+
/// `__aztec_nr_internals__unpack_arguments_<N>` helper. Otherwise the calldata read is inlined, matching the
150+
/// pre-extraction shape; this avoids paying the helper-call overhead on signatures that would not benefit from the
151+
/// shared body.
152+
///
153+
/// The real break-even depends on the size of the inlined boilerplate at each call site (which scales with the
154+
/// parameter types' `stream_deserialize` cost), not just the share count: a signature with one `Field` arg inlines
155+
/// to a few opcodes per site, while a signature like `(AztecAddress, U128, PartialUintNote)` inlines into many. For
156+
/// now we approximate that with a single share-count threshold, which is the simplest knob that keeps the macro
157+
/// readable. If we ever want to be more precise we can size each helper against its per-site savings.
158+
comptime fn compute_unpack_prelude(
159+
parameters: [(Quoted, Type)],
160+
signature_counts: &mut CHashMap<Quoted, u32>,
161+
signature_to_helper_idx: &mut CHashMap<Quoted, u32>,
162+
unpack_helpers: &mut [Quoted],
163+
) -> (Quoted, Quoted) {
164+
if parameters.len() == 0 {
165+
(quote {}, quote {})
166+
} else {
167+
let sig_key = signature_key(parameters);
168+
let count = signature_counts.get(sig_key).unwrap_or(0);
169+
let shared = count >= EXTRACTION_THRESHOLD;
170+
171+
let mut arg_names: [Quoted] = @[];
172+
for parameter_index in 0..parameters.len() {
173+
let arg_name = f"arg{parameter_index}".quoted_contents();
174+
arg_names = arg_names.push_back(arg_name);
175+
}
176+
let args = arg_names.join(quote { , });
177+
178+
let prelude = if shared {
179+
let existing_idx = signature_to_helper_idx.get(sig_key);
180+
let helper_idx = if existing_idx.is_some() {
181+
existing_idx.unwrap()
182+
} else {
183+
let new_idx = (*unpack_helpers).len();
184+
signature_to_helper_idx.insert(sig_key, new_idx);
185+
let helper_def = build_unpack_helper(new_idx, parameters);
186+
*unpack_helpers = (*unpack_helpers).push_back(helper_def);
187+
new_idx
188+
};
189+
let helper_name = f"__aztec_nr_internals__unpack_arguments_{helper_idx}".quoted_contents();
190+
if parameters.len() == 1 {
191+
quote { let arg0 = $helper_name(); }
192+
} else {
193+
quote { let ($args) = $helper_name(); }
194+
}
195+
} else {
196+
inline_unpack(parameters)
197+
};
198+
199+
(prelude, args)
200+
}
201+
}
202+
203+
/// Inlined calldata read + per-parameter `stream_deserialize` for signatures that are not worth extracting.
204+
comptime fn inline_unpack(parameters: [(Quoted, Type)]) -> Quoted {
205+
let params_len_quote = get_params_len_quote(parameters);
206+
let initial_read = quote {
207+
let input_calldata: [Field; $params_len_quote] = aztec::oracle::avm::calldata_copy(1, $params_len_quote);
208+
let mut reader = aztec::protocol::utils::reader::Reader::new(input_calldata);
209+
};
210+
211+
let mut read_quotes: [Quoted] = @[];
212+
for parameter_index in 0..parameters.len() {
213+
let arg_name = f"arg{parameter_index}".quoted_contents();
214+
let param_type = parameters[parameter_index].1;
215+
read_quotes = read_quotes.push_back(
216+
quote {
217+
let $arg_name: $param_type = aztec::protocol::traits::Deserialize::stream_deserialize(&mut reader);
218+
},
219+
);
220+
}
221+
let reads = read_quotes.join(quote { });
222+
223+
quote {
224+
$initial_read
225+
$reads
226+
}
227+
}
228+
229+
/// Emits the `#[inline_never]` helper that reads calldata for one specific parameter-type signature.
230+
///
231+
/// Returns a single value when there is only one parameter, and a tuple when there are several. Marked
232+
/// `#[inline_never]` (and therefore `unconstrained`) so each entry point can call into the same compiled body.
233+
comptime fn build_unpack_helper(idx: u32, parameters: [(Quoted, Type)]) -> Quoted {
234+
let helper_name = f"__aztec_nr_internals__unpack_arguments_{idx}".quoted_contents();
235+
let params_len_quote = get_params_len_quote(parameters);
236+
237+
// The initial calldata_copy offset is 1 to skip the Field selector. The expected calldata is the serialization
238+
// of:
239+
// - FunctionSelector: the selector of the function intended to dispatch
240+
// - Parameters: the parameters of the function intended to dispatch
241+
// That is, exactly what is expected for a call to the target function, but with a selector added at the
242+
// beginning.
243+
let initial_read = quote {
244+
let input_calldata: [Field; $params_len_quote] = aztec::oracle::avm::calldata_copy(1, $params_len_quote);
245+
let mut reader = aztec::protocol::utils::reader::Reader::new(input_calldata);
246+
};
247+
248+
let mut read_quotes: [Quoted] = @[];
249+
let mut arg_quotes: [Quoted] = @[];
250+
let mut type_quotes: [Quoted] = @[];
251+
for parameter_index in 0..parameters.len() {
252+
let arg_name = f"arg{parameter_index}".quoted_contents();
253+
let param_type = parameters[parameter_index].1;
254+
read_quotes = read_quotes.push_back(
255+
quote {
256+
let $arg_name: $param_type = aztec::protocol::traits::Deserialize::stream_deserialize(&mut reader);
257+
},
258+
);
259+
arg_quotes = arg_quotes.push_back(arg_name);
260+
type_quotes = type_quotes.push_back(quote { $param_type });
261+
}
262+
let reads = read_quotes.join(quote { });
263+
let return_args = arg_quotes.join(quote { , });
264+
265+
if parameters.len() == 1 {
266+
let only_type = parameters[0].1;
267+
quote {
268+
#[inline_never]
269+
#[contract_library_method]
270+
unconstrained fn $helper_name() -> $only_type {
271+
$initial_read
272+
$reads
273+
arg0
274+
}
275+
}
276+
} else {
277+
let return_types = type_quotes.join(quote { , });
278+
quote {
279+
#[inline_never]
280+
#[contract_library_method]
281+
unconstrained fn $helper_name() -> ($return_types) {
282+
$initial_read
283+
$reads
284+
($return_args)
285+
}
286+
}
287+
}
288+
}
289+
139290
comptime fn get_type<T>() -> Type {
140291
let t: T = std::mem::zeroed();
141292
std::meta::type_of(t)

noir-projects/noir-contracts/contracts/protocol/public_checks_contract/src/utils.nr renamed to noir-projects/aztec-nr/aztec/src/public_checks.nr

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use crate::PublicChecks;
2-
use aztec::context::PrivateContext;
3-
use aztec::protocol::constants::PUBLIC_CHECKS_ADDRESS;
1+
use crate::context::calls::PublicStaticCall;
2+
use crate::context::PrivateContext;
3+
use crate::protocol::abis::function_selector::FunctionSelector;
4+
use crate::protocol::constants::PUBLIC_CHECKS_ADDRESS;
45

56
// docs:start:helper_public_checks_functions
67
/// Asserts that the current timestamp in the enqueued public call enqueued by `check_timestamp` satisfies
@@ -9,7 +10,14 @@ use aztec::protocol::constants::PUBLIC_CHECKS_ADDRESS;
910
/// This conceals an address of the calling contract by setting `context.msg_sender` to the public checks contract
1011
/// address.
1112
pub fn privately_check_timestamp(operation: u8, value: u64, context: &mut PrivateContext) {
12-
PublicChecks::at(PUBLIC_CHECKS_ADDRESS).check_timestamp(operation, value).enqueue_view_incognito(context);
13+
let selector = comptime { FunctionSelector::from_signature("check_timestamp(u8,u64)") };
14+
PublicStaticCall::<15, 2, ()>::new(
15+
PUBLIC_CHECKS_ADDRESS,
16+
selector,
17+
"check_timestamp",
18+
[operation as Field, value as Field],
19+
)
20+
.enqueue_view_incognito(context);
1321
}
1422

1523
/// Asserts that the current block number in the enqueued public call enqueued by `check_block_number` satisfies
@@ -19,7 +27,14 @@ pub fn privately_check_timestamp(operation: u8, value: u64, context: &mut Privat
1927
/// address.
2028
pub fn privately_check_block_number(operation: u8, value: u32, context: &mut PrivateContext) {
2129
// docs:start:enqueueing
22-
PublicChecks::at(PUBLIC_CHECKS_ADDRESS).check_block_number(operation, value).enqueue_view_incognito(context);
30+
let selector = comptime { FunctionSelector::from_signature("check_block_number(u8,u32)") };
31+
PublicStaticCall::<18, 2, ()>::new(
32+
PUBLIC_CHECKS_ADDRESS,
33+
selector,
34+
"check_block_number",
35+
[operation as Field, value as Field],
36+
)
37+
.enqueue_view_incognito(context);
2338
// docs:end:enqueueing
2439
}
2540
// docs:end:helper_public_checks_functions

noir-projects/contract-snapshots/tests/snapshots/compile_failure/public_function_selector_collision/snapshots__stderr.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ error: Public function selector collision detected between functions 'fn_selecto
1414
2: aztec
1515
at <repo>/noir-projects/aztec-nr/aztec/src/macros/aztec.nr:169:27
1616
3: generate_public_dispatch
17-
at <repo>/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr:20:19
17+
at <repo>/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr:52:19
1818
4: [T]::map
1919
at std/vector.nr:67:33
2020
5: generate_public_dispatch
21-
at <repo>/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr:32:13
21+
at <repo>/noir-projects/aztec-nr/aztec/src/macros/dispatch.nr:64:13
2222

2323
Aborting due to 1 previous error

noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
source: tests/snapshots.rs
33
expression: stdout
44
---
5-
65
use aztec::macros::aztec;
76
use aztec::macros::aztec;
87

noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
source: tests/snapshots.rs
33
expression: stdout
44
---
5-
65
use aztec::macros::aztec;
76
use aztec::macros::aztec;
87

0 commit comments

Comments
 (0)