Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: rename SharedMutable methods #10165

Merged
merged 1 commit into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ Aztec is in full-speed development. Literally every version breaks compatibility

## TBD

### [aztec.nr] SharedMutable renamings

`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.

```diff
// In private
- let value = storage.my_var.get_current_value_in_private();
+ let value = storage.my_var.get_current_value();

// In public
- let value = storage.my_var.get_current_value_in_public();
+ let value = storage.my_var.get_current_value();
```

### [aztec.js] Random addresses are now valid

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,17 @@ If one wishes to schedule a value change from private, simply enqueue a public c
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.
:::

### `get_current_value_in_public`
### `get_current_value`

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.
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to dev rel, do we want to standardize oxford comma ?


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

### `get_current_value_in_private`

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.

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).
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).

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

### `get_scheduled_value_in_public`
### `get_scheduled_value`

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).

Expand Down
277 changes: 275 additions & 2 deletions noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,278 @@
pub mod shared_mutable;
use dep::protocol_types::{
address::AztecAddress,
hash::{poseidon2_hash, poseidon2_hash_with_separator},
traits::{Deserialize, FromField, Serialize, ToField},
utils::arrays::array_concat,
};

use crate::context::{PrivateContext, PublicContext, UnconstrainedContext};
use crate::oracle::storage::storage_read;
use crate::state_vars::{
shared_mutable::{
scheduled_delay_change::ScheduledDelayChange, scheduled_value_change::ScheduledValueChange,
},
storage::Storage,
};
use dep::std::mem::zeroed;

pub(crate) mod scheduled_delay_change;
pub(crate) mod scheduled_value_change;
mod test;

pub struct SharedMutable<T, let INITIAL_DELAY: u32, Context> {
context: Context,
storage_slot: Field,
}

// Separators separating storage slot of different values within the same state variable
global VALUE_CHANGE_SEPARATOR: u32 = 0;
global DELAY_CHANGE_SEPARATOR: u32 = 1;
global HASH_SEPARATOR: u32 = 2;

// This will make the Aztec macros require that T implements the Serialize<N> trait, and allocate N storage slots to
// this state variable. This is incorrect, since what we actually store is:
// - a ScheduledValueChange<T>, which requires 1 + 2 * M storage slots, where M is the serialization length of T
// - a ScheduledDelayChange, which requires another storage slot
//
// TODO https://github.com/AztecProtocol/aztec-packages/issues/5736: change the storage allocation scheme so that we
// can actually use it here
impl<T, let INITIAL_DELAY: u32, Context, let N: u32> Storage<T, N> for SharedMutable<T, INITIAL_DELAY, Context>
where
T: Serialize<N> + Deserialize<N>,
{}

// SharedMutable<T> stores a value of type T that is:
// - publicly known (i.e. unencrypted)
// - mutable in public
// - readable in private with no contention (i.e. multiple parties can all read the same value without blocking one
// another nor needing to coordinate)
// This is famously a hard problem to solve. SharedMutable makes it work by introducing a delay to public mutation:
// the value is not changed immediately but rather a value change is scheduled to happen in the future after some delay
// measured in blocks. Reads in private are only valid as long as they are included in a block not too far into the
// future, so that they can guarantee the value will not have possibly changed by then (because of the delay).
// The delay for changing a value is initially equal to INITIAL_DELAY, but can be changed by calling
// `schedule_delay_change`.
impl<T, let INITIAL_DELAY: u32, Context> SharedMutable<T, INITIAL_DELAY, Context>
where
T: ToField + FromField + Eq,
{
pub fn new(context: Context, storage_slot: Field) -> Self {
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
Self { context, storage_slot }
}

// Since we can't rely on the native storage allocation scheme, we hash the storage slot to get a unique location in
// which we can safely store as much data as we need.
// See https://github.com/AztecProtocol/aztec-packages/issues/5492 and
// https://github.com/AztecProtocol/aztec-packages/issues/5736
// We store three things in public storage:
// - a ScheduledValueChange
// - a ScheduledDelaChange
// - the hash of both of these (via `hash_scheduled_data`)
fn get_value_change_storage_slot(self) -> Field {
poseidon2_hash_with_separator([self.storage_slot], VALUE_CHANGE_SEPARATOR)
}

fn get_delay_change_storage_slot(self) -> Field {
poseidon2_hash_with_separator([self.storage_slot], DELAY_CHANGE_SEPARATOR)
}

fn get_hash_storage_slot(self) -> Field {
poseidon2_hash_with_separator([self.storage_slot], HASH_SEPARATOR)
}
}

impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, &mut PublicContext>
where
T: ToField + FromField + Eq,
{

pub fn schedule_value_change(self, new_value: T) {
let mut value_change = self.read_value_change();
let delay_change = self.read_delay_change();

let block_number = self.context.block_number() as u32;
let current_delay = delay_change.get_current(block_number);

// TODO: make this configurable
// https://github.com/AztecProtocol/aztec-packages/issues/5501
let block_of_change = block_number + current_delay;
value_change.schedule_change(new_value, block_number, current_delay, block_of_change);

self.write(value_change, delay_change);
}

pub fn schedule_delay_change(self, new_delay: u32) {
let mut delay_change = self.read_delay_change();

let block_number = self.context.block_number() as u32;

delay_change.schedule_change(new_delay, block_number);

self.write(self.read_value_change(), delay_change);
}

pub fn get_current_value(self) -> T {
let block_number = self.context.block_number() as u32;
self.read_value_change().get_current_at(block_number)
}

pub fn get_current_delay(self) -> u32 {
let block_number = self.context.block_number() as u32;
self.read_delay_change().get_current(block_number)
}

pub fn get_scheduled_value(self) -> (T, u32) {
self.read_value_change().get_scheduled()
}

pub fn get_scheduled_delay(self) -> (u32, u32) {
self.read_delay_change().get_scheduled()
}

fn read_value_change(self) -> ScheduledValueChange<T> {
self.context.storage_read(self.get_value_change_storage_slot())
}

fn read_delay_change(self) -> ScheduledDelayChange<INITIAL_DELAY> {
self.context.storage_read(self.get_delay_change_storage_slot())
}

fn write(
self,
value_change: ScheduledValueChange<T>,
delay_change: ScheduledDelayChange<INITIAL_DELAY>,
) {
// Whenever we write to public storage, we write both the value change and delay change as well as the hash of
// them both. This guarantees that the hash is always kept up to date.
// While this makes for more costly writes, it also makes private proofs much simpler because they only need to
// produce a historical proof for the hash, which results in a single inclusion proof (as opposed to 4 in the
// best case scenario in which T is a single field). Private shared mutable reads are assumed to be much more
// frequent than public writes, so this tradeoff makes sense.
self.context.storage_write(self.get_value_change_storage_slot(), value_change);
self.context.storage_write(self.get_delay_change_storage_slot(), delay_change);
self.context.storage_write(
self.get_hash_storage_slot(),
SharedMutable::hash_scheduled_data(value_change, delay_change),
);
}
}

impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, &mut PrivateContext>
where
T: ToField + FromField + Eq,
{
pub fn get_current_value(self) -> T {
// When reading the current value in private we construct a historical state proof for the public value.
// However, since this value might change, we must constrain the maximum transaction block number as this proof
// will only be valid for however many blocks we can ensure the value will not change, which will depend on the
// current delay and any scheduled delay changes.
let (value_change, delay_change, historical_block_number) =
self.historical_read_from_public_storage();

// We use the effective minimum delay as opposed to the current delay at the historical block as this one also
// takes into consideration any scheduled delay changes.
// For example, consider a scenario in which at block 200 the current delay was 50. We may naively think that
// the earliest we could change the value would be at block 251 by scheduling immediately after the historical
// block, i.e. at block 201. But if there was a delay change scheduled for block 210 to reduce the delay to 20
// blocks, then if a value change was scheduled at block 210 it would go into effect at block 230, which is
// earlier than what we'd expect if we only considered the current delay.
let effective_minimum_delay =
delay_change.get_effective_minimum_delay_at(historical_block_number);
let block_horizon =
value_change.get_block_horizon(historical_block_number, effective_minimum_delay);

// We prevent this transaction from being included in any block after the block horizon, ensuring that the
// historical public value matches the current one, since it can only change after the horizon.
self.context.set_tx_max_block_number(block_horizon);
value_change.get_current_at(historical_block_number)
}

fn historical_read_from_public_storage(
self,
) -> (ScheduledValueChange<T>, ScheduledDelayChange<INITIAL_DELAY>, u32) {
let header = self.context.get_header();
let address = self.context.this_address();

let historical_block_number = header.global_variables.block_number as u32;

// We could simply produce historical inclusion proofs for both the ScheduledValueChange and
// ScheduledDelayChange, but that'd require one full sibling path per storage slot (since due to kernel siloing
// the storage is not contiguous), and in the best case in which T is a single field that'd be 4 slots.
// Instead, we get an oracle to provide us the correct values for both the value and delay changes, and instead
// prove inclusion of their hash, which is both a much smaller proof (a single slot), and also independent of
// the size of T.
let (value_change_hint, delay_change_hint) = unsafe {
get_public_storage_hints(address, self.storage_slot, historical_block_number)
};

// Ideally the following would be simply public_storage::read_historical, but we can't implement that yet.
let hash = header.public_storage_historical_read(self.get_hash_storage_slot(), address);

if hash != 0 {
assert_eq(
hash,
SharedMutable::hash_scheduled_data(value_change_hint, delay_change_hint),
"Hint values do not match hash",
);
} else {
// The hash slot can only hold a zero if it is uninitialized, meaning no value or delay change was ever
// scheduled. Therefore, the hints must then correspond to uninitialized scheduled changes.
assert_eq(
value_change_hint,
ScheduledValueChange::deserialize(zeroed()),
"Non-zero value change for zero hash",
);
assert_eq(
delay_change_hint,
ScheduledDelayChange::deserialize(zeroed()),
"Non-zero delay change for zero hash",
);
};

(value_change_hint, delay_change_hint, historical_block_number)
}

fn hash_scheduled_data(
value_change: ScheduledValueChange<T>,
delay_change: ScheduledDelayChange<INITIAL_DELAY>,
) -> Field {
let concatenated: [Field; 4] =
array_concat(value_change.serialize(), delay_change.serialize());
poseidon2_hash(concatenated)
}
}

impl<T, let INITIAL_DELAY: u32> SharedMutable<T, INITIAL_DELAY, UnconstrainedContext>
where
T: ToField + FromField + Eq,
{
pub unconstrained fn get_current_value(self) -> T {
let block_number = self.context.block_number() as u32;
self.read_value_change().get_current_at(block_number)
}

unconstrained fn read_value_change(self) -> ScheduledValueChange<T> {
self.context.storage_read(self.get_value_change_storage_slot())
}
}

unconstrained fn get_public_storage_hints<T, let INITIAL_DELAY: u32>(
address: AztecAddress,
storage_slot: Field,
block_number: u32,
) -> (ScheduledValueChange<T>, ScheduledDelayChange<INITIAL_DELAY>)
where
T: ToField + FromField + Eq,
{
// This function cannot be part of the &mut PrivateContext impl because that'd mean that by passing `self` we'd also
// be passing a mutable reference to an unconstrained function, which is not allowed. We therefore create a dummy
// state variable here so that we can access the methods to compute storage slots. This will all be removed in the
// future once we do proper storage slot allocation (#5492).
let dummy: SharedMutable<T, INITIAL_DELAY, ()> = SharedMutable::new((), storage_slot);

pub use shared_mutable::SharedMutable;
(
storage_read(address, dummy.get_value_change_storage_slot(), block_number),
storage_read(address, dummy.get_delay_change_storage_slot(), block_number),
)
}
Loading