Skip to content

Commit dc7ec48

Browse files
authoredJan 31, 2025
feat!: improve storage slot allocation (#10320)
1 parent e333f29 commit dc7ec48

File tree

17 files changed

+114
-52
lines changed

17 files changed

+114
-52
lines changed
 

‎docs/docs/migration_notes.md

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ Aztec is in full-speed development. Literally every version breaks compatibility
88

99
## TBD
1010

11+
### [Aztec.nr] Improved storage slot allocation
12+
State variables are no longer assumed to be generic over a type that implements the `Serialize` trait: instead, they must implement the `Storage` trait with an `N` value equal to the number of slots they need to reserve.
13+
14+
For the vast majority of state variables, this simply means binding the serialization length to this trait:
15+
16+
```diff
17+
+ impl<T, let N: u32> Storage<N> for MyStateVar<T> where T: Serialize<N> { };
18+
```
19+
1120
### [Aztec.nr] Introduction of `Packable` trait
1221
We have introduced a `Packable` trait that allows types to be serialized and deserialized with a focus on minimizing the size of the resulting Field array.
1322
This is in contrast to the `Serialize` and `Deserialize` traits, which follows Noir's intrinsic serialization format.

‎noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr

+6-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,12 @@ pub comptime fn transform_unconstrained(f: FunctionDefinition) {
332332
let module_has_storage = module_has_storage(f.module());
333333

334334
let storage_init = if module_has_storage {
335-
quote { let storage = Storage::init(context); }
335+
quote {
336+
// Some functions don't access storage, but it'd be quite difficult to only inject this variable if it is
337+
// referenced. We instead ignore 'unused variable' warnings for it.
338+
#[allow(unused_variables)]
339+
let storage = Storage::init(context);
340+
}
336341
} else {
337342
quote {}
338343
};

‎noir-projects/aztec-nr/aztec/src/macros/storage/mod.nr

+10-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{collections::umap::UHashMap, hash::{BuildHasherDefault, poseidon2::Poseidon2Hasher}};
22

3-
use super::utils::get_packed_size;
3+
use super::utils::get_storage_size;
44
use super::utils::is_note;
55

66
/// Stores a map from a module to the name of the struct that describes its storage layout.
@@ -30,22 +30,22 @@ pub comptime fn storage(s: StructDefinition) -> Quoted {
3030
for field in s.fields_as_written() {
3131
// FIXME: This doesn't handle field types with generics
3232
let (name, typ) = field;
33-
let (storage_field_constructor, serialized_size) =
33+
let (storage_field_constructor, storage_size) =
3434
generate_storage_field_constructor(typ, quote { $slot }, false);
3535
storage_vars_constructors =
3636
storage_vars_constructors.push_back(quote { $name: $storage_field_constructor });
3737
// We have `Storable` in a separate `.nr` file instead of defining it in the last quote of this function
3838
// because that way a dev gets a more reasonable error if he defines a struct with the same name in
3939
// a contract.
4040
storage_layout_fields =
41-
storage_layout_fields.push_back(quote { $name: dep::aztec::prelude::Storable });
41+
storage_layout_fields.push_back(quote { pub $name: dep::aztec::prelude::Storable });
4242
storage_layout_constructors = storage_layout_constructors.push_back(
4343
quote { $name: dep::aztec::prelude::Storable { slot: $slot } },
4444
);
4545
//let with_context_generic = add_context_generic(typ, context_generic);
4646
//println(with_context_generic);
4747
//new_storage_fields = new_storage_fields.push_back((name, with_context_generic ));
48-
slot += serialized_size;
48+
slot += storage_size;
4949
}
5050

5151
//s.set_fields(new_storage_fields);
@@ -119,28 +119,25 @@ comptime fn generate_storage_field_constructor(
119119
generate_storage_field_constructor(generics[1], quote { slot }, true);
120120
(quote { $struct_name::new(context, $slot, | context, slot | { $value_constructor }) }, 1)
121121
} else {
122-
let (container_struct, container_struct_generics) = typ.as_struct().unwrap();
123-
let container_struct_name = container_struct.name();
124-
125-
let serialized_size = if parent_is_map {
122+
let storage_size = if parent_is_map {
126123
// Variables inside a map do not require contiguous slots since the map slot derivation is assumed to result
127124
// in slots very far away from one another.
128125
1
129126
} else {
127+
let (_, container_struct_generics) = typ.as_struct().unwrap();
130128
let stored_struct = container_struct_generics[0];
131-
if is_note(stored_struct) & (container_struct_name != quote { PublicMutable }) {
129+
130+
if is_note(stored_struct) {
132131
// Private notes always occupy a single slot, since the slot is only used as a state variable
133132
// identifier.
134-
// Someone could store a Note in PublicMutable for whatever reason though.
135-
// TODO(#8659): remove the PublicMutable exception above
136133
1
137134
} else {
138-
get_packed_size(stored_struct)
135+
get_storage_size(typ)
139136
}
140137
};
141138

142139
// We assume below that all state variables implement `fn new<Context>(context: Context, slot: Field) -> Self`.
143-
(quote { $struct_name::new(context, $slot)}, serialized_size)
140+
(quote { $struct_name::new(context, $slot)}, storage_size)
144141
}
145142
}
146143

‎noir-projects/aztec-nr/aztec/src/macros/utils.nr

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::meta::{typ::fresh_type_variable, unquote};
1+
use std::meta::unquote;
22

33
pub(crate) comptime fn get_fn_visibility(f: FunctionDefinition) -> Quoted {
44
if f.has_named_attribute("private") {
@@ -219,17 +219,18 @@ pub(crate) comptime fn compute_event_selector(s: StructDefinition) -> Field {
219219
unquote!(computation_quote)
220220
}
221221

222-
pub(crate) comptime fn get_packed_size(typ: Type) -> u32 {
223-
let any = fresh_type_variable();
224-
let maybe_packed_impl =
225-
typ.get_trait_impl(quote { protocol_types::traits::Packable<$any> }.as_trait_constraint());
222+
/// Returns how many storage slots a type needs to reserve for itself. State variables must implement the Storage trait
223+
/// for slots to be allocated for them.
224+
pub(crate) comptime fn get_storage_size(typ: Type) -> u32 {
225+
// We create a type variable for the storage size. We can't simply read the value used in the implementation because
226+
// it may not be a constant (e.g. N + 1). We then bind it to the implementation of the Storage trait.
227+
let storage_size = std::meta::typ::fresh_type_variable();
228+
assert(
229+
typ.implements(quote { crate::state_vars::Storage<$storage_size> }.as_trait_constraint()),
230+
f"Attempted to fetch storage size, but {typ} does not implement the Storage trait",
231+
);
226232

227-
maybe_packed_impl
228-
.expect(f"Attempted to fetch packed length, but {typ} does not implement the Packable trait"
229-
)
230-
.trait_generic_args()[0]
231-
.as_constant()
232-
.unwrap()
233+
storage_size.as_constant().unwrap()
233234
}
234235

235236
pub(crate) comptime fn module_has_storage(m: Module) -> bool {

‎noir-projects/aztec-nr/aztec/src/state_vars/map.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub struct Map<K, V, Context> {
99
}
1010
// docs:end:map
1111

12-
impl<K, T, Context, let N: u32> Storage<T, N> for Map<K, T, Context>
12+
impl<K, T, Context, let N: u32> Storage<N> for Map<K, T, Context>
1313
where
1414
T: Packable<N>,
1515
{

‎noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct PrivateImmutable<Note, Context> {
2121
}
2222
// docs:end:struct
2323

24-
impl<T, Context, let N: u32> Storage<T, N> for PrivateImmutable<T, Context>
24+
impl<T, Context, let N: u32> Storage<N> for PrivateImmutable<T, Context>
2525
where
2626
T: Packable<N>,
2727
{

‎noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ pub struct PrivateMutable<Note, Context> {
2323

2424
mod test;
2525

26-
impl<T, Context, let N: u32> Storage<T, N> for PrivateMutable<T, Context>
26+
impl<T, Context, let N: u32> Storage<N> for PrivateMutable<T, Context>
2727
where
2828
T: Packable<N>,
2929
{

‎noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct PrivateSet<Note, Context> {
2222
}
2323
// docs:end:struct
2424

25-
impl<T, Context, let N: u32> Storage<T, N> for PrivateSet<T, Context>
25+
impl<T, Context, let N: u32> Storage<N> for PrivateSet<T, Context>
2626
where
2727
T: Packable<N>,
2828
{

‎noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct PublicImmutable<T, Context> {
1414
}
1515
// docs:end:public_immutable_struct
1616

17-
impl<T, Context, let N: u32> Storage<T, N> for PublicImmutable<T, Context>
17+
impl<T, Context, let N: u32> Storage<N> for PublicImmutable<T, Context>
1818
where
1919
T: Packable<N>,
2020
{

‎noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub struct PublicMutable<T, Context> {
99
}
1010
// docs:end:public_mutable_struct
1111

12-
impl<T, Context, let N: u32> Storage<T, N> for PublicMutable<T, Context>
12+
impl<T, Context, let N: u32> Storage<N> for PublicMutable<T, Context>
1313
where
1414
T: Packable<N>,
1515
{

‎noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ global HASH_SEPARATOR: u32 = 2;
3636
//
3737
// TODO https://github.com/AztecProtocol/aztec-packages/issues/5736: change the storage allocation scheme so that we
3838
// can actually use it here
39-
impl<T, let INITIAL_DELAY: u32, Context, let N: u32> Storage<T, N> for SharedMutable<T, INITIAL_DELAY, Context>
39+
impl<T, let INITIAL_DELAY: u32, Context, let N: u32> Storage<N> for SharedMutable<T, INITIAL_DELAY, Context>
4040
where
4141
T: Packable<N>,
4242
{

‎noir-projects/aztec-nr/aztec/src/state_vars/storage.nr

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
use dep::protocol_types::traits::Packable;
2-
3-
pub trait Storage<T, let N: u32>
4-
where
5-
T: Packable<N>,
6-
{
1+
/// State variables must implement this trait in order to placed in the storage struct (i.e. the one marked with the
2+
/// #[storage] attribute). The `N` value determines how many storage slots will be reserved for the state variable.
3+
pub trait Storage<let N: u32> {
74
fn get_storage_slot(self) -> Field;
85
}
96

‎noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use dep::protocol_types::traits::{Deserialize, Packable, Serialize};
22

3-
pub(crate) struct MockStruct {
4-
pub(crate) a: Field,
5-
pub(crate) b: Field,
3+
pub struct MockStruct {
4+
pub a: Field,
5+
pub b: Field,
66
}
77

88
impl MockStruct {
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
pub(crate) mod mock_note;
2-
pub(crate) mod mock_struct;
1+
mod mock_note;
2+
mod mock_struct;

‎noir-projects/noir-contracts/contracts/test_contract/src/main.nr

+17-8
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ contract Test {
1313
event::encode_and_encrypt_event_unconstrained, note::encode_and_encrypt_note,
1414
};
1515
use dep::aztec::prelude::{
16-
AztecAddress, EthAddress, FunctionSelector, NoteGetterOptions, NoteViewerOptions,
16+
AztecAddress, EthAddress, FunctionSelector, Map, NoteGetterOptions, NoteViewerOptions,
1717
PrivateImmutable, PrivateSet,
1818
};
1919

2020
use dep::aztec::protocol_types::{
2121
constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, PRIVATE_LOG_SIZE_IN_FIELDS},
2222
point::Point,
23-
traits::Serialize,
23+
traits::{Hash, Packable, Serialize},
2424
utils::arrays::array_concat,
2525
};
2626

@@ -61,10 +61,24 @@ contract Test {
6161
value4: Field,
6262
}
6363

64+
#[derive(Packable)]
65+
struct ExampleStruct {
66+
value0: Field,
67+
value1: Field,
68+
value2: Field,
69+
value3: Field,
70+
value4: Field,
71+
}
72+
73+
// This struct is used to test the storage slot allocation mechanism - if modified the test_storage_slot_allocation
74+
// test function must also be updated accordingly.
6475
#[storage]
6576
struct Storage<Context> {
6677
example_constant: PrivateImmutable<TestNote, Context>,
6778
example_set: PrivateSet<TestNote, Context>,
79+
example_struct: PrivateImmutable<ExampleStruct, Context>,
80+
example_struct_in_map: Map<AztecAddress, PrivateImmutable<ExampleStruct, Context>, Context>,
81+
another_example_struct: PrivateImmutable<ExampleStruct, Context>,
6882
}
6983

7084
#[private]
@@ -482,6 +496,7 @@ contract Test {
482496
assert_eq(get_public_keys(address).npk_m.inner, public_nullifying_key);
483497
}
484498

499+
#[derive(Serialize)]
485500
pub struct DummyNote {
486501
amount: Field,
487502
secret_hash: Field,
@@ -497,12 +512,6 @@ contract Test {
497512
}
498513
}
499514

500-
impl Serialize<2> for DummyNote {
501-
fn serialize(self) -> [Field; 2] {
502-
[self.amount, self.secret_hash]
503-
}
504-
}
505-
506515
pub struct DeepStruct {
507516
a_field: Field,
508517
a_bool: bool,

‎noir-projects/noir-contracts/contracts/test_contract/src/test.nr

+45
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::Test;
12
use crate::test_note::TestNote;
23
use dep::uint_note::uint_note::UintNote;
34
use dep::value_note::value_note::ValueNote;
@@ -10,3 +11,47 @@ unconstrained fn test_note_type_id() {
1011
assert_eq(ValueNote::get_note_type_id(), 1, "ValueNote type id should be 1");
1112
assert_eq(TestNote::get_note_type_id(), 2, "TestNote type id should be 2");
1213
}
14+
15+
#[test]
16+
unconstrained fn test_storage_slot_allocation() {
17+
// This tests that sufficient storage slots are assigned to each state variable so that they do not interfere with
18+
// one another. The space a state variable needs is determined by the N value in its implementation of the Storage
19+
// trait. Most state variables bind N to the packed length of the type they hold.
20+
//
21+
// This is the storage declaration:
22+
//
23+
// #[storage]
24+
// struct Storage<Context> {
25+
// example_constant: PrivateImmutable<TestNote, Context>,
26+
// example_set: PrivateSet<TestNote, Context>,
27+
// example_struct: PrivateImmutable<ExampleStruct, Context>,
28+
// example_struct_in_map: Map<AztecAddress, PrivateImmutable<ExampleStruct, Context>, Context>,
29+
// another_example_struct: PrivateImmutable<ExampleStruct, Context>,
30+
// }
31+
32+
// We can't directly see how many slots are allocated to each variable, but we can look at the slot increments for
33+
// each and deduct the allocation size based off of that. In other words, given a struct with two members a and b,
34+
// the number of slots allocated to a will be b.storage_slot - a.storage_slot.
35+
36+
// The first slot is always 1.
37+
let mut expected_slot = 1;
38+
assert_eq(Test::storage_layout().example_constant.slot, expected_slot);
39+
40+
// Even though example_constant holds TestNote, which packs to a length larger than 1, notes always reserve a
41+
// single slot.
42+
expected_slot += 1;
43+
assert_eq(Test::storage_layout().example_set.slot, expected_slot);
44+
45+
// example_set also held a note, so it should have only allocated a single slot.
46+
expected_slot += 1;
47+
assert_eq(Test::storage_layout().example_struct.slot, expected_slot);
48+
49+
// example_struct allocates 5 slots because it is not a note and it's packed length is 5.
50+
expected_slot += 5;
51+
assert_eq(Test::storage_layout().example_struct_in_map.slot, expected_slot);
52+
53+
// example_struct_in_map should allocate a single note because it's a map, regardless of whatever it holds. The Map
54+
// type is going to deal with its own dynamic allocation based on keys
55+
expected_slot += 1;
56+
assert_eq(Test::storage_layout().another_example_struct.slot, expected_slot);
57+
}

‎noir-projects/noir-contracts/contracts/token_contract/src/test/transfer.nr

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::test::utils;
22
use crate::Token;
3-
use dep::authwit::cheatcodes as authwit_cheatcodes;
43
use dep::aztec::test::helpers::cheatcodes;
54

65
#[test]

0 commit comments

Comments
 (0)