Skip to content

Commit 6c15604

Browse files
authored
feat!: introduce WithHash<T> + use it in PublicImmutable (#8022)
1 parent 2fd08ba commit 6c15604

File tree

17 files changed

+307
-35
lines changed

17 files changed

+307
-35
lines changed

docs/docs/migration_notes.md

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ keywords: [sandbox, aztec, notes, migration, updating, upgrading]
66

77
Aztec is in full-speed development. Literally every version breaks compatibility with the previous ones. This page attempts to target errors and difficulties you might encounter when upgrading, and how to resolve them.
88

9+
### TBD
10+
11+
### [Aztec.nr] Introduction of `WithHash<T>`
12+
`WithHash<T>` is a struct that allows for efficient reading of value `T` from public storage in private.
13+
This is achieved by storing the value with its hash, then obtaining the values via an oracle and verifying them against the hash.
14+
This results in in a fewer tree inclusion proofs for values `T` that are packed into more than a single field.
15+
16+
`WithHash<T>` is leveraged by state variables like `PublicImmutable`.
17+
This is a breaking change because now we require values stored in `PublicImmutable` and `SharedMutable` to implement the `Eq` trait.
18+
19+
To implement the `Eq` trait you can use the `#[derive(Eq)]` macro:
20+
21+
```diff
22+
+ use std::meta::derive;
23+
24+
+ #[derive(Eq)]
25+
pub struct YourType {
26+
...
27+
}
28+
```
29+
930
## 0.73.0
1031

1132
### [Token, FPC] Moving fee-related complexity from the Token to the FPC
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
use crate::{
22
context::{PrivateContext, PublicContext, UnconstrainedContext},
3-
history::public_storage::PublicStorageHistoricalRead,
43
state_vars::storage::Storage,
4+
utils::with_hash::WithHash,
55
};
66
use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::Packable};
77

88
/// Stores an immutable value in public state which can be read from public, private and unconstrained execution
99
/// contexts.
10+
///
11+
/// Leverages `WithHash<T>` to enable efficient private reads of public storage. `WithHash` wrapper allows for
12+
/// efficient reads by verifying large values through a single hash check and then proving inclusion only of the hash
13+
/// in the public storage. This reduces the number of required tree inclusion proofs from O(M) to O(1).
14+
///
15+
/// This is valuable when T packs to multiple fields, as it maintains "almost constant" verification overhead
16+
/// regardless of the original data size.
1017
// docs:start:public_immutable_struct
1118
pub struct PublicImmutable<T, Context> {
1219
context: Context,
1320
storage_slot: Field,
1421
}
1522
// docs:end:public_immutable_struct
1623

17-
impl<T, Context, let N: u32> Storage<N> for PublicImmutable<T, Context>
24+
/// `WithHash<T>` stores both the packed value (using N fields) and its hash (1 field), requiring N = M + 1 total
25+
/// fields.
26+
impl<T, Context, let M: u32, let N: u32> Storage<N> for PublicImmutable<T, Context>
1827
where
19-
T: Packable<N>,
28+
WithHash<T, M>: Packable<N>,
2029
{
2130
fn get_storage_slot(self) -> Field {
2231
self.storage_slot
@@ -38,7 +47,7 @@ impl<T, Context> PublicImmutable<T, Context> {
3847

3948
impl<T, let T_PACKED_LEN: u32> PublicImmutable<T, &mut PublicContext>
4049
where
41-
T: Packable<T_PACKED_LEN>,
50+
T: Packable<T_PACKED_LEN> + Eq,
4251
{
4352
// docs:start:public_immutable_struct_write
4453
pub fn initialize(self, value: T) {
@@ -49,41 +58,36 @@ where
4958

5059
// We populate the initialization slot with a non-zero value to indicate that the struct is initialized
5160
self.context.storage_write(initialization_slot, 0xdead);
52-
self.context.storage_write(self.storage_slot, value);
61+
self.context.storage_write(self.storage_slot, WithHash::new(value));
5362
}
5463
// docs:end:public_immutable_struct_write
5564

5665
// Note that we don't access the context, but we do call oracles that are only available in public
5766
// docs:start:public_immutable_struct_read
5867
pub fn read(self) -> T {
59-
self.context.storage_read(self.storage_slot)
68+
WithHash::public_storage_read(*self.context, self.storage_slot)
6069
}
6170
// docs:end:public_immutable_struct_read
6271
}
6372

6473
impl<T, let T_PACKED_LEN: u32> PublicImmutable<T, UnconstrainedContext>
6574
where
66-
T: Packable<T_PACKED_LEN>,
75+
T: Packable<T_PACKED_LEN> + Eq,
6776
{
6877
pub unconstrained fn read(self) -> T {
69-
self.context.storage_read(self.storage_slot)
78+
WithHash::unconstrained_public_storage_read(self.context, self.storage_slot)
7079
}
7180
}
7281

7382
impl<T, let T_PACKED_LEN: u32> PublicImmutable<T, &mut PrivateContext>
7483
where
75-
T: Packable<T_PACKED_LEN>,
84+
T: Packable<T_PACKED_LEN> + Eq,
7685
{
7786
pub fn read(self) -> T {
78-
let header = self.context.get_block_header();
79-
let mut fields = [0; T_PACKED_LEN];
80-
81-
for i in 0..fields.len() {
82-
fields[i] = header.public_storage_historical_read(
83-
self.storage_slot + i as Field,
84-
(*self.context).this_address(),
85-
);
86-
}
87-
T::unpack(fields)
87+
WithHash::historical_public_storage_read(
88+
self.context.get_block_header(),
89+
self.context.this_address(),
90+
self.storage_slot,
91+
)
8892
}
8993
}

noir-projects/aztec-nr/aztec/src/utils/mod.nr

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pub mod field;
55
pub mod point;
66
pub mod to_bytes;
77
pub mod secrets;
8+
pub mod with_hash;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use crate::{
2+
context::{PublicContext, UnconstrainedContext},
3+
history::public_storage::PublicStorageHistoricalRead,
4+
oracle,
5+
};
6+
use dep::protocol_types::{
7+
address::AztecAddress, block_header::BlockHeader, hash::poseidon2_hash, traits::Packable,
8+
};
9+
10+
/// A struct that allows for efficient reading of value `T` from public storage in private.
11+
///
12+
/// The efficient reads are achieved by verifying large values through a single hash check
13+
/// and then proving inclusion only of the hash in public storage. This reduces the number
14+
/// of required tree inclusion proofs from `N` to 1.
15+
///
16+
/// # Type Parameters
17+
/// - `T`: The underlying type being wrapped, must implement `Packable<N>`
18+
/// - `N`: The number of field elements required to pack values of type `T`
19+
pub struct WithHash<T, let N: u32> {
20+
value: T,
21+
packed: [Field; N],
22+
hash: Field,
23+
}
24+
25+
impl<T, let N: u32> WithHash<T, N>
26+
where
27+
T: Packable<N> + Eq,
28+
{
29+
pub fn new(value: T) -> Self {
30+
let packed = value.pack();
31+
Self { value, packed, hash: poseidon2_hash(packed) }
32+
}
33+
34+
pub fn get_value(self) -> T {
35+
self.value
36+
}
37+
38+
pub fn get_hash(self) -> Field {
39+
self.hash
40+
}
41+
42+
pub fn public_storage_read(context: PublicContext, storage_slot: Field) -> T {
43+
context.storage_read(storage_slot)
44+
}
45+
46+
pub unconstrained fn unconstrained_public_storage_read(
47+
context: UnconstrainedContext,
48+
storage_slot: Field,
49+
) -> T {
50+
context.storage_read(storage_slot)
51+
}
52+
53+
pub fn historical_public_storage_read(
54+
header: BlockHeader,
55+
address: AztecAddress,
56+
storage_slot: Field,
57+
) -> T {
58+
let historical_block_number = header.global_variables.block_number as u32;
59+
60+
// We could simply produce historical inclusion proofs for each field in `packed`, but that would require one
61+
// full sibling path per storage slot (since due to kernel siloing the storage is not contiguous). Instead, we
62+
// get an oracle to provide us the values, and instead we prove inclusion of their hash, which is both a much
63+
// smaller proof (a single slot), and also independent of the size of T (except in that we need to pack and hash T).
64+
let hint = WithHash::new(
65+
/// Safety: We verify that a hash of the hint/packed data matches the stored hash.
66+
unsafe {
67+
oracle::storage::storage_read(address, storage_slot, historical_block_number)
68+
},
69+
);
70+
71+
let hash = header.public_storage_historical_read(storage_slot + N as Field, address);
72+
73+
if hash != 0 {
74+
assert_eq(hash, hint.get_hash(), "Hint values do not match hash");
75+
} else {
76+
// The hash slot can only hold a zero if it is uninitialized. Therefore, the hints must then be zero
77+
// (i.e. the default value for public storage) as well.
78+
assert_eq(
79+
hint.get_value(),
80+
T::unpack(std::mem::zeroed()),
81+
"Non-zero hint for zero hash",
82+
);
83+
};
84+
85+
hint.get_value()
86+
}
87+
}
88+
89+
impl<T, let N: u32> Packable<N + 1> for WithHash<T, N>
90+
where
91+
T: Packable<N>,
92+
{
93+
fn pack(self) -> [Field; N + 1] {
94+
let mut result: [Field; N + 1] = std::mem::zeroed();
95+
for i in 0..N {
96+
result[i] = self.packed[i];
97+
}
98+
result[N] = self.hash;
99+
100+
result
101+
}
102+
103+
fn unpack(packed: [Field; N + 1]) -> Self {
104+
let mut value_packed: [Field; N] = std::mem::zeroed();
105+
for i in 0..N {
106+
value_packed[i] = packed[i];
107+
}
108+
let hash = packed[N];
109+
110+
Self { value: T::unpack(value_packed), packed: value_packed, hash }
111+
}
112+
}
113+
114+
mod test {
115+
use crate::{
116+
oracle::random::random,
117+
test::{
118+
helpers::{cheatcodes, test_environment::TestEnvironment},
119+
mocks::mock_struct::MockStruct,
120+
},
121+
utils::with_hash::WithHash,
122+
};
123+
use dep::protocol_types::hash::poseidon2_hash;
124+
use dep::std::{mem, test::OracleMock};
125+
126+
global storage_slot: Field = 47;
127+
128+
#[test]
129+
unconstrained fn create_and_recover() {
130+
let value = MockStruct { a: 5, b: 3 };
131+
let value_with_hash = WithHash::new(value);
132+
let recovered = WithHash::unpack(value_with_hash.pack());
133+
134+
assert_eq(recovered.value, value);
135+
assert_eq(recovered.packed, value.pack());
136+
assert_eq(recovered.hash, poseidon2_hash(value.pack()));
137+
}
138+
139+
#[test]
140+
unconstrained fn read_uninitialized_value() {
141+
let mut env = TestEnvironment::new();
142+
143+
let block_header = env.private().historical_header;
144+
let address = env.contract_address();
145+
146+
let result = WithHash::<MockStruct, _>::historical_public_storage_read(
147+
block_header,
148+
address,
149+
storage_slot,
150+
);
151+
152+
// We should get zeroed value
153+
let expected: MockStruct = mem::zeroed();
154+
assert_eq(result, expected);
155+
}
156+
157+
#[test]
158+
unconstrained fn read_initialized_value() {
159+
let mut env = TestEnvironment::new();
160+
161+
let value = MockStruct { a: 5, b: 3 };
162+
let value_with_hash = WithHash::new(value);
163+
164+
// We write the value with hash to storage
165+
cheatcodes::direct_storage_write(
166+
env.contract_address(),
167+
storage_slot,
168+
value_with_hash.pack(),
169+
);
170+
171+
// We advance block by 1 because env.private() currently returns context at latest_block - 1
172+
env.advance_block_by(1);
173+
174+
let result = WithHash::<MockStruct, _>::historical_public_storage_read(
175+
env.private().historical_header,
176+
env.contract_address(),
177+
storage_slot,
178+
);
179+
180+
assert_eq(result, value);
181+
}
182+
183+
#[test(should_fail_with = "Non-zero hint for zero hash")]
184+
unconstrained fn test_bad_hint_uninitialized_value() {
185+
let mut env = TestEnvironment::new();
186+
187+
env.advance_block_to(6);
188+
189+
let value_packed = MockStruct { a: 1, b: 1 }.pack();
190+
191+
let block_header = env.private().historical_header;
192+
let address = env.contract_address();
193+
194+
// Mock the oracle to return a non-zero hint/packed value
195+
let _ = OracleMock::mock("storageRead")
196+
.with_params((
197+
address.to_field(), storage_slot, block_header.global_variables.block_number as u32,
198+
value_packed.len(),
199+
))
200+
.returns(value_packed)
201+
.times(1);
202+
203+
// This should revert because the hint value is non-zero and the hash is zero (default value of storage)
204+
let _ = WithHash::<MockStruct, _>::historical_public_storage_read(
205+
block_header,
206+
address,
207+
storage_slot,
208+
);
209+
}
210+
211+
#[test(should_fail_with = "Hint values do not match hash")]
212+
unconstrained fn test_bad_hint_initialized_value() {
213+
let mut env = TestEnvironment::new();
214+
215+
let value_packed = MockStruct { a: 5, b: 3 }.pack();
216+
217+
// We write the value to storage
218+
cheatcodes::direct_storage_write(env.contract_address(), storage_slot, value_packed);
219+
220+
// Now we write incorrect hash to the hash storage slot
221+
let incorrect_hash = random();
222+
let hash_storage_slot = storage_slot + (value_packed.len() as Field);
223+
cheatcodes::direct_storage_write(
224+
env.contract_address(),
225+
hash_storage_slot,
226+
[incorrect_hash],
227+
);
228+
229+
// We advance block by 1 because env.private() currently returns context at latest_block - 1
230+
env.advance_block_by(1);
231+
232+
let _ = WithHash::<MockStruct, _>::historical_public_storage_read(
233+
env.private().historical_header,
234+
env.contract_address(),
235+
storage_slot,
236+
);
237+
}
238+
}

noir-projects/aztec-nr/compressed-string/src/field_compressed_string.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::meta::derive;
66

77
// A Fixedsize Compressed String.
88
// Essentially a special version of Compressed String for practical use.
9-
#[derive(Deserialize, Packable, Serialize)]
9+
#[derive(Deserialize, Eq, Packable, Serialize)]
1010
pub struct FieldCompressedString {
1111
value: Field,
1212
}

noir-projects/noir-contracts/contracts/amm_contract/src/config.nr

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::meta::derive;
44
/// We store the tokens of the pool in a struct such that to load it from SharedImmutable asserts only a single
55
/// merkle proof.
66
/// (Once we actually do the optimization. WIP in https://github.com/AztecProtocol/aztec-packages/pull/8022).
7-
#[derive(Deserialize, Packable, Serialize)]
7+
#[derive(Deserialize, Eq, Packable, Serialize)]
88
pub struct Config {
99
pub token0: AztecAddress,
1010
pub token1: AztecAddress,

noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub contract AppSubscription {
1818
use router::utils::privately_check_block_number;
1919
use token::Token;
2020

21+
// TODO: This can be optimized by storing the values in Config struct in 1 PublicImmutable (less merkle proofs).
2122
#[storage]
2223
struct Storage<Context> {
2324
target_address: PublicImmutable<AztecAddress, Context>,

0 commit comments

Comments
 (0)