Skip to content

Commit 4fd70e8

Browse files
authored
feat!: rename SharedMutable methods (#10165)
Same as #10164 but for `SharedMutable`. Here I also moved the main module outside of `mod.nr` into its own `shared_mutable.nr` file, which was not possible back when the module was created. I think this structure is cleaner as it avoids the `mod.nr` files.
1 parent ee00d1c commit 4fd70e8

File tree

7 files changed

+356
-349
lines changed

7 files changed

+356
-349
lines changed

docs/docs/migration_notes.md

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

99
## TBD
1010

11+
### [aztec.nr] SharedMutable renamings
12+
13+
`SharedMutable` getters (`get_current_value_in_public`, etc.) were renamed by dropping the `_in<public|private|unconstrained>` suffix, since only one of these versions is ever available depending on the current context.
14+
15+
```diff
16+
// In private
17+
- let value = storage.my_var.get_current_value_in_private();
18+
+ let value = storage.my_var.get_current_value();
19+
20+
// In public
21+
- let value = storage.my_var.get_current_value_in_public();
22+
+ let value = storage.my_var.get_current_value();
23+
```
24+
1125
### [aztec.js] Random addresses are now valid
1226

1327
The `AztecAddress.random()` function now returns valid addresses, i.e. addresses that can receive encrypted messages and therefore have notes be sent to them. `AztecAddress.isValid()` was also added to check for validity of an address.

docs/docs/reference/developer_references/smart_contract_reference/storage/shared_state.md

+4-8
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,17 @@ If one wishes to schedule a value change from private, simply enqueue a public c
8080
A `SharedMutable`'s storage **must** only be mutated via `schedule_value_change`. Attempting to override this by manually accessing the underlying storage slots breaks all properties of the data structure, rendering it useless.
8181
:::
8282

83-
### `get_current_value_in_public`
83+
### `get_current_value`
8484

85-
Returns the current value in a public execution context. Once a value change is scheduled via `schedule_value_change` and a number of blocks equal to the delay passes, this automatically returns the new value.
85+
Returns the current value in a public, private or unconstrained execution context. Once a value change is scheduled via `schedule_value_change` and a number of blocks equal to the delay passes, this automatically returns the new value.
8686

8787
#include_code shared_mutable_get_current_public /noir-projects/noir-contracts/contracts/auth_contract/src/main.nr rust
8888

89-
### `get_current_value_in_private`
90-
91-
Returns the current value in a private execution context. Once a value change is scheduled via `schedule_value_change` and a number of blocks equal to the delay passes, this automatically returns the new value.
92-
93-
Calling this function will set the `max_block_number` property of the transaction request, introducing a new validity condition to the entire transaction: it cannot be included in any block with a block number larger than `max_block_number`. This could [potentially leak some privacy](#privacy-considerations).
89+
Calling this function in a private execution context will set the `max_block_number` property of the transaction request, introducing a new validity condition to the entire transaction: it cannot be included in any block with a block number larger than `max_block_number`. This could [potentially leak some privacy](#privacy-considerations).
9490

9591
#include_code shared_mutable_get_current_private /noir-projects/noir-contracts/contracts/auth_contract/src/main.nr rust
9692

97-
### `get_scheduled_value_in_public`
93+
### `get_scheduled_value`
9894

9995
Returns the last scheduled value change, along with the block number at which the scheduled value becomes the current value. This may either be a pending change, if the block number is in the future, or the last executed scheduled change if the block number is in the past (in which case there are no pending changes).
10096

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,278 @@
1-
pub mod shared_mutable;
1+
use dep::protocol_types::{
2+
address::AztecAddress,
3+
hash::{poseidon2_hash, poseidon2_hash_with_separator},
4+
traits::{Deserialize, FromField, Serialize, ToField},
5+
utils::arrays::array_concat,
6+
};
7+
8+
use crate::context::{PrivateContext, PublicContext, UnconstrainedContext};
9+
use crate::oracle::storage::storage_read;
10+
use crate::state_vars::{
11+
shared_mutable::{
12+
scheduled_delay_change::ScheduledDelayChange, scheduled_value_change::ScheduledValueChange,
13+
},
14+
storage::Storage,
15+
};
16+
use dep::std::mem::zeroed;
17+
218
pub(crate) mod scheduled_delay_change;
319
pub(crate) mod scheduled_value_change;
20+
mod test;
21+
22+
pub struct SharedMutable<T, let INITIAL_DELAY: u32, Context> {
23+
context: Context,
24+
storage_slot: Field,
25+
}
26+
27+
// Separators separating storage slot of different values within the same state variable
28+
global VALUE_CHANGE_SEPARATOR: u32 = 0;
29+
global DELAY_CHANGE_SEPARATOR: u32 = 1;
30+
global HASH_SEPARATOR: u32 = 2;
31+
32+
// This will make the Aztec macros require that T implements the Serialize<N> trait, and allocate N storage slots to
33+
// this state variable. This is incorrect, since what we actually store is:
34+
// - a ScheduledValueChange<T>, which requires 1 + 2 * M storage slots, where M is the serialization length of T
35+
// - a ScheduledDelayChange, which requires another storage slot
36+
//
37+
// TODO https://github.com/AztecProtocol/aztec-packages/issues/5736: change the storage allocation scheme so that we
38+
// 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>
40+
where
41+
T: Serialize<N> + Deserialize<N>,
42+
{}
43+
44+
// SharedMutable<T> stores a value of type T that is:
45+
// - publicly known (i.e. unencrypted)
46+
// - mutable in public
47+
// - readable in private with no contention (i.e. multiple parties can all read the same value without blocking one
48+
// another nor needing to coordinate)
49+
// This is famously a hard problem to solve. SharedMutable makes it work by introducing a delay to public mutation:
50+
// the value is not changed immediately but rather a value change is scheduled to happen in the future after some delay
51+
// measured in blocks. Reads in private are only valid as long as they are included in a block not too far into the
52+
// future, so that they can guarantee the value will not have possibly changed by then (because of the delay).
53+
// The delay for changing a value is initially equal to INITIAL_DELAY, but can be changed by calling
54+
// `schedule_delay_change`.
55+
impl<T, let INITIAL_DELAY: u32, Context> SharedMutable<T, INITIAL_DELAY, Context>
56+
where
57+
T: ToField + FromField + Eq,
58+
{
59+
pub fn new(context: Context, storage_slot: Field) -> Self {
60+
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
61+
Self { context, storage_slot }
62+
}
63+
64+
// Since we can't rely on the native storage allocation scheme, we hash the storage slot to get a unique location in
65+
// which we can safely store as much data as we need.
66+
// See https://github.com/AztecProtocol/aztec-packages/issues/5492 and
67+
// https://github.com/AztecProtocol/aztec-packages/issues/5736
68+
// We store three things in public storage:
69+
// - a ScheduledValueChange
70+
// - a ScheduledDelaChange
71+
// - the hash of both of these (via `hash_scheduled_data`)
72+
fn get_value_change_storage_slot(self) -> Field {
73+
poseidon2_hash_with_separator([self.storage_slot], VALUE_CHANGE_SEPARATOR)
74+
}
75+
76+
fn get_delay_change_storage_slot(self) -> Field {
77+
poseidon2_hash_with_separator([self.storage_slot], DELAY_CHANGE_SEPARATOR)
78+
}
79+
80+
fn get_hash_storage_slot(self) -> Field {
81+
poseidon2_hash_with_separator([self.storage_slot], HASH_SEPARATOR)
82+
}
83+
}
84+
85+
impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, &mut PublicContext>
86+
where
87+
T: ToField + FromField + Eq,
88+
{
89+
90+
pub fn schedule_value_change(self, new_value: T) {
91+
let mut value_change = self.read_value_change();
92+
let delay_change = self.read_delay_change();
93+
94+
let block_number = self.context.block_number() as u32;
95+
let current_delay = delay_change.get_current(block_number);
96+
97+
// TODO: make this configurable
98+
// https://github.com/AztecProtocol/aztec-packages/issues/5501
99+
let block_of_change = block_number + current_delay;
100+
value_change.schedule_change(new_value, block_number, current_delay, block_of_change);
101+
102+
self.write(value_change, delay_change);
103+
}
104+
105+
pub fn schedule_delay_change(self, new_delay: u32) {
106+
let mut delay_change = self.read_delay_change();
107+
108+
let block_number = self.context.block_number() as u32;
109+
110+
delay_change.schedule_change(new_delay, block_number);
111+
112+
self.write(self.read_value_change(), delay_change);
113+
}
114+
115+
pub fn get_current_value(self) -> T {
116+
let block_number = self.context.block_number() as u32;
117+
self.read_value_change().get_current_at(block_number)
118+
}
119+
120+
pub fn get_current_delay(self) -> u32 {
121+
let block_number = self.context.block_number() as u32;
122+
self.read_delay_change().get_current(block_number)
123+
}
124+
125+
pub fn get_scheduled_value(self) -> (T, u32) {
126+
self.read_value_change().get_scheduled()
127+
}
128+
129+
pub fn get_scheduled_delay(self) -> (u32, u32) {
130+
self.read_delay_change().get_scheduled()
131+
}
132+
133+
fn read_value_change(self) -> ScheduledValueChange<T> {
134+
self.context.storage_read(self.get_value_change_storage_slot())
135+
}
136+
137+
fn read_delay_change(self) -> ScheduledDelayChange<INITIAL_DELAY> {
138+
self.context.storage_read(self.get_delay_change_storage_slot())
139+
}
140+
141+
fn write(
142+
self,
143+
value_change: ScheduledValueChange<T>,
144+
delay_change: ScheduledDelayChange<INITIAL_DELAY>,
145+
) {
146+
// Whenever we write to public storage, we write both the value change and delay change as well as the hash of
147+
// them both. This guarantees that the hash is always kept up to date.
148+
// While this makes for more costly writes, it also makes private proofs much simpler because they only need to
149+
// produce a historical proof for the hash, which results in a single inclusion proof (as opposed to 4 in the
150+
// best case scenario in which T is a single field). Private shared mutable reads are assumed to be much more
151+
// frequent than public writes, so this tradeoff makes sense.
152+
self.context.storage_write(self.get_value_change_storage_slot(), value_change);
153+
self.context.storage_write(self.get_delay_change_storage_slot(), delay_change);
154+
self.context.storage_write(
155+
self.get_hash_storage_slot(),
156+
SharedMutable::hash_scheduled_data(value_change, delay_change),
157+
);
158+
}
159+
}
160+
161+
impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, &mut PrivateContext>
162+
where
163+
T: ToField + FromField + Eq,
164+
{
165+
pub fn get_current_value(self) -> T {
166+
// When reading the current value in private we construct a historical state proof for the public value.
167+
// However, since this value might change, we must constrain the maximum transaction block number as this proof
168+
// will only be valid for however many blocks we can ensure the value will not change, which will depend on the
169+
// current delay and any scheduled delay changes.
170+
let (value_change, delay_change, historical_block_number) =
171+
self.historical_read_from_public_storage();
172+
173+
// We use the effective minimum delay as opposed to the current delay at the historical block as this one also
174+
// takes into consideration any scheduled delay changes.
175+
// For example, consider a scenario in which at block 200 the current delay was 50. We may naively think that
176+
// the earliest we could change the value would be at block 251 by scheduling immediately after the historical
177+
// block, i.e. at block 201. But if there was a delay change scheduled for block 210 to reduce the delay to 20
178+
// blocks, then if a value change was scheduled at block 210 it would go into effect at block 230, which is
179+
// earlier than what we'd expect if we only considered the current delay.
180+
let effective_minimum_delay =
181+
delay_change.get_effective_minimum_delay_at(historical_block_number);
182+
let block_horizon =
183+
value_change.get_block_horizon(historical_block_number, effective_minimum_delay);
184+
185+
// We prevent this transaction from being included in any block after the block horizon, ensuring that the
186+
// historical public value matches the current one, since it can only change after the horizon.
187+
self.context.set_tx_max_block_number(block_horizon);
188+
value_change.get_current_at(historical_block_number)
189+
}
190+
191+
fn historical_read_from_public_storage(
192+
self,
193+
) -> (ScheduledValueChange<T>, ScheduledDelayChange<INITIAL_DELAY>, u32) {
194+
let header = self.context.get_header();
195+
let address = self.context.this_address();
196+
197+
let historical_block_number = header.global_variables.block_number as u32;
198+
199+
// We could simply produce historical inclusion proofs for both the ScheduledValueChange and
200+
// ScheduledDelayChange, but that'd require one full sibling path per storage slot (since due to kernel siloing
201+
// the storage is not contiguous), and in the best case in which T is a single field that'd be 4 slots.
202+
// Instead, we get an oracle to provide us the correct values for both the value and delay changes, and instead
203+
// prove inclusion of their hash, which is both a much smaller proof (a single slot), and also independent of
204+
// the size of T.
205+
let (value_change_hint, delay_change_hint) = unsafe {
206+
get_public_storage_hints(address, self.storage_slot, historical_block_number)
207+
};
208+
209+
// Ideally the following would be simply public_storage::read_historical, but we can't implement that yet.
210+
let hash = header.public_storage_historical_read(self.get_hash_storage_slot(), address);
211+
212+
if hash != 0 {
213+
assert_eq(
214+
hash,
215+
SharedMutable::hash_scheduled_data(value_change_hint, delay_change_hint),
216+
"Hint values do not match hash",
217+
);
218+
} else {
219+
// The hash slot can only hold a zero if it is uninitialized, meaning no value or delay change was ever
220+
// scheduled. Therefore, the hints must then correspond to uninitialized scheduled changes.
221+
assert_eq(
222+
value_change_hint,
223+
ScheduledValueChange::deserialize(zeroed()),
224+
"Non-zero value change for zero hash",
225+
);
226+
assert_eq(
227+
delay_change_hint,
228+
ScheduledDelayChange::deserialize(zeroed()),
229+
"Non-zero delay change for zero hash",
230+
);
231+
};
232+
233+
(value_change_hint, delay_change_hint, historical_block_number)
234+
}
235+
236+
fn hash_scheduled_data(
237+
value_change: ScheduledValueChange<T>,
238+
delay_change: ScheduledDelayChange<INITIAL_DELAY>,
239+
) -> Field {
240+
let concatenated: [Field; 4] =
241+
array_concat(value_change.serialize(), delay_change.serialize());
242+
poseidon2_hash(concatenated)
243+
}
244+
}
245+
246+
impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, UnconstrainedContext>
247+
where
248+
T: ToField + FromField + Eq,
249+
{
250+
pub unconstrained fn get_current_value(self) -> T {
251+
let block_number = self.context.block_number() as u32;
252+
self.read_value_change().get_current_at(block_number)
253+
}
254+
255+
unconstrained fn read_value_change(self) -> ScheduledValueChange<T> {
256+
self.context.storage_read(self.get_value_change_storage_slot())
257+
}
258+
}
259+
260+
unconstrained fn get_public_storage_hints<T, let INITIAL_DELAY: u32>(
261+
address: AztecAddress,
262+
storage_slot: Field,
263+
block_number: u32,
264+
) -> (ScheduledValueChange<T>, ScheduledDelayChange<INITIAL_DELAY>)
265+
where
266+
T: ToField + FromField + Eq,
267+
{
268+
// This function cannot be part of the &mut PrivateContext impl because that'd mean that by passing `self` we'd also
269+
// be passing a mutable reference to an unconstrained function, which is not allowed. We therefore create a dummy
270+
// state variable here so that we can access the methods to compute storage slots. This will all be removed in the
271+
// future once we do proper storage slot allocation (#5492).
272+
let dummy: SharedMutable<T, INITIAL_DELAY, ()> = SharedMutable::new((), storage_slot);
4273

5-
pub use shared_mutable::SharedMutable;
274+
(
275+
storage_read(address, dummy.get_value_change_storage_slot(), block_number),
276+
storage_read(address, dummy.get_delay_change_storage_slot(), block_number),
277+
)
278+
}

0 commit comments

Comments
 (0)