Skip to content

Commit

Permalink
feat!: shared mutable configurable delays (#6104)
Browse files Browse the repository at this point in the history
Closes #5493, follow up of #6085.

This makes the delay in SharedMutable not be fixed and instead
configurable by users throughout the lifetime of the contract. This is
however more complicated than it sounds at first: because private proofs
are being created relying on the public values being stable until a
future point in time, it must not be possible to cause for a shared
value to change before some delay.

Two scenarios are particularly tricky:
- if the delay is reduced, then it is possible to schedule a value
change with a shorter delay, violating the original delay's constraints.
The solution to this is to make delay changes be scheduled actions
themselves, so that the total delay (wait time for the new delay to come
into effect plus the new reduced delay) equals the original delay. Note
that increasing a delay cna be done instantly.
- if we schedule delay changes as per the above, then we must consider a
scenario in which a delay reduction is scheduled in the near future. It
may happen that waiting for the reduction to come into effect and then
scheduling results in a shorter delay than if the scheduling were to
happen immediately - this lower 'effective delay' is the value that must
be used in private proofs.

## How

I had originally considered creating a sort of wrapper state variable
that held two SharedMutables, one for the value and one for the delay,
or alternatively two ScheduledValueChanges, but ultimately I realized
that a scheduled value change is significantly different from a
scheduled delay change. Namely:
- the notion of the 'current' delay is meaningless in private - we only
care about the 'effective' delay
 - there's no use for the block horizon of a delay change
- scheduling a delay change requires setting a delay depending on the
current and new values, not an externally defined one

Due to these differences, I introduced ScheduledDelayChange, which is
essentially a variant of the value change, but with these considerations
baked in. I think this is a reasonable way to do things, even if at
first this may seem to introduce too many concepts. It also helps with
the fact that there's so many values involved (pre, post and block of
change for value and delays, as well as current, effective, historical
values, etc.), and with language becoming weird - we need to describe
the delay for scheduling a delay change, which will later affect the
delays of scheduling value changes.

With ScheduledDelayChange, extending the functionality of SharedMutable
was relatively straightforward. The unit tests became a bit more
complicated due to there bieng more scenarios, so I also used this as an
opportunity to try to create slightly more complex Noir tests. I didn't
go too crazy here, but they seem to be right at the point where we'd
want to introduce something like a `Test` struct with custom impls for
setup, common assertions, etc.

## Problems

An uninitialized `SharedMutable` has both delay and value of 0. A zero
delay transforms `SharedMutable` into `PublicMutable`: scheduled value
changes become effective immediately, and it is not possible to read
from private since `tx.max_block_number` would equal a historical block
(i.e. an already mined one). Delay initialization is therefore required,
and this is typically fine: since the initial delay is 0 any change will
be an increase, and therefore instant.

The problem arises when we cannot have explicit initialization and
instead wish to rely on defaults. This happens e.g. when we put a
SharedMutable inside a `Map`: we can't initialize all entries for all
keys, and we run into trouble. This is a pattern followed by
`KeyRegistry` and `TokenBlacklist`: we have per-user configuration, and
cant really ask users to initialize their state before interacting with
the system.

## Solution?

A possible solution would be to have a default value for the delay, and
to store e.g. `Option<u32>` instead of plain integers and using
`unwrap_or(DEFAULT)`. We could then make this a type parameter for
SharedMutable, e.g. `registry: Map<Address, SharedMutable<Key,
DEFAULT_DELAY>>`.

This would make certain things more complicated, particularly the
effective delay and delay change block of change computations, but it
should all be containable within `ScheduledDelayChange`, which sounds
just about right.

----
I'm keeping this is a draft so we can discuss the current approach and
wether we think the above or an alternative solution would be reasonable
to attempt. Note that this PR won't pass CI as some of the contracts
won't build.

---------

Co-authored-by: Jan Beneš <janbenes1234@gmail.com>
Co-authored-by: Lasse Herskind <16536249+LHerskind@users.noreply.github.com>
  • Loading branch information
3 people committed May 9, 2024
1 parent 0c20f44 commit c191a40
Show file tree
Hide file tree
Showing 8 changed files with 1,050 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ While shared state variables are much less leaky than the assertion in public ap

The `max_block_number` transaction property will be set to a value close to the current block number plus the duration of the delay in blocks. The exact value depends on the historical block over which the private proof is constructed. For example, if the current block number is 100 and a shared state variable has a delay of 20 blocks, then transactions that read this value privately will set `max_block_number` to a value close to 120 (clients building proofs on older state will select a lower `max_block_number`). This implicitly leaks the duration of the delay.

Applications using similar delays will therefore be part of the same privacy set. It is expected for social coordination to result in small set of predetermined delays that developers choose from depending on their needs, as an example a viable set might be: 12 hours (for time-sensitive operations, such as emergency mechanisms), 5 days (for middle-of-the-road operations) and 2 weeks (for operations that require lengthy public scrutiny).
Applications using similar delays will therefore be part of the same privacy set. It is expected for social coordination to result in small set of predetermined delays that developers choose from depending on their needs, as an example a viable set might be: 12 hours (for time-sensitive operations, such as emergency mechanisms), 5 days (for middle-of-the-road operations) and 2 weeks (for operations that require lengthy public scrutiny). These delays can be changed during the contract lifetime as the application's needs evolve.

:::note
Shared state delays are currently hardcoded at compilation time and cannot be changed, but there are plans to make this a mutable value.
:::note
Additionally, users might choose to coordinate and constrain their transactions to set `max_block_number` to a value lower than would be strictly needed by the applications they interact with (if any!) using some common delay, and by doing so prevent privacy leakage.

### Choosing Epochs

Expand Down
6 changes: 6 additions & 0 deletions docs/docs/misc/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ keywords: [sandbox, cli, aztec, notes, migration, updating, upgrading]

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.

## 0.39.0

### [Aztec.nr] Mutable delays in `SharedMutable`

The type signature for `SharedMutable` changed from `SharedMutable<T, DELAY>` to `SharedMutable<T, INITIAL_DELAY>`. The behavior is the same as before, except the delay can now be changed after deployment by calling `schedule_delay_change`.

## 0.38.0

### [Aztec.nr] Emmiting encrypted logs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod shared_mutable;
mod scheduled_delay_change;
mod scheduled_value_change;
mod shared_mutable_private_getter;

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,76 @@ use dep::protocol_types::{hash::pedersen_hash, traits::FromField, address::Aztec
use crate::context::{PrivateContext, Context};
use crate::history::public_storage::public_storage_historical_read;
use crate::public_storage;
use crate::state_vars::{storage::Storage, shared_mutable::scheduled_value_change::ScheduledValueChange};
use crate::state_vars::{
storage::Storage,
shared_mutable::{scheduled_delay_change::ScheduledDelayChange, scheduled_value_change::ScheduledValueChange}
};

struct SharedMutablePrivateGetter<T, DELAY> {
struct SharedMutablePrivateGetter<T, INITIAL_DELAY> {
context: PrivateContext,
// The contract address of the contract we want to read from
other_contract_address: AztecAddress,
// The storage slot where the SharedMutable is stored on the other contract
storage_slot: Field,
// The _dummy variable forces DELAY to be interpreted as a numberic value. This is a workaround to
// The _dummy variable forces INITIAL_DELAY to be interpreted as a numberic value. This is a workaround to
// https://github.com/noir-lang/noir/issues/4633. Remove once resolved.
_dummy: [Field; DELAY],
_dummy: [Field; INITIAL_DELAY],
}

// We have this as a view-only interface to reading Shared Mutables in other contracts.
// Currently the Shared Mutable does not support this. We can adapt SharedMutable at a later date
impl<T, DELAY> SharedMutablePrivateGetter<T, DELAY> {
impl<T, INITIAL_DELAY> SharedMutablePrivateGetter<T, INITIAL_DELAY> {
pub fn new(
context: PrivateContext,
other_contract_address: AztecAddress,
storage_slot: Field
) -> Self {
assert(storage_slot != 0, "Storage slot 0 not allowed. Storage slots must start from 1.");
assert(other_contract_address.to_field() != 0, "Other contract address cannot be 0");
Self { context, other_contract_address, storage_slot, _dummy: [0; DELAY] }
Self { context, other_contract_address, storage_slot, _dummy: [0; INITIAL_DELAY] }
}

pub fn get_current_value_in_private(self) -> T where T: FromField {
let mut context = self.context;

let (scheduled_value_change, historical_block_number) = self.historical_read_from_public_storage(context);
let block_horizon = scheduled_value_change.get_block_horizon(historical_block_number, DELAY);
let (value_change, delay_change, historical_block_number) = self.historical_read_from_public_storage(context);
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.
context.set_tx_max_block_number(block_horizon);
scheduled_value_change.get_current_at(historical_block_number)
value_change.get_current_at(historical_block_number)
}

fn historical_read_from_public_storage(
self,
context: PrivateContext
) -> (ScheduledValueChange<T>, u32) where T: FromField {
let derived_slot = self.get_derived_storage_slot();

// Ideally the following would be simply public_storage::read_historical, but we can't implement that yet.
let mut raw_fields = [0; 3];
) -> (ScheduledValueChange<T>, ScheduledDelayChange<INITIAL_DELAY>, u32) where T: FromField {
let value_change_slot = self.get_value_change_storage_slot();
let mut raw_value_change_fields = [0; 3];
for i in 0..3 {
raw_fields[i] = public_storage_historical_read(
context,
derived_slot + i as Field,
self.other_contract_address
);
raw_value_change_fields[i] = public_storage_historical_read(
context,
value_change_slot + i as Field,
self.other_contract_address
);
}

let scheduled_value: ScheduledValueChange<T> = ScheduledValueChange::deserialize(raw_fields);
let delay_change_slot = self.get_delay_change_storage_slot();
let raw_delay_change_fields = [public_storage_historical_read(context, delay_change_slot, context.this_address())];

let value_change = ScheduledValueChange::deserialize(raw_value_change_fields);
let delay_change = ScheduledDelayChange::deserialize(raw_delay_change_fields);

let historical_block_number = context.historical_header.global_variables.block_number as u32;

(scheduled_value, historical_block_number)
(value_change, delay_change, historical_block_number)
}

fn get_derived_storage_slot(self) -> Field {
// Since we're actually storing three values (a ScheduledValueChange struct), we hash the storage slot to get a
// unique location in which we can safely store as much data as we need. This could be removed if we informed
// the slot allocator of how much space we need so that proper padding could be added.
// See https://github.com/AztecProtocol/aztec-packages/issues/5492
fn get_value_change_storage_slot(self) -> Field {
pedersen_hash([self.storage_slot, 0], 0)
}

fn get_delay_change_storage_slot(self) -> Field {
pedersen_hash([self.storage_slot, 1], 0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ contract Auth {
fn constructor(admin: AztecAddress) {
assert(!admin.is_zero(), "invalid admin");
storage.admin.initialize(admin);
// Note that we don't initialize authorized with any value: because storage defaults to 0 it'll have a 'post'
// value of 0 and block of change 0, meaning it is effectively autoinitialized at the zero address.
}

// docs:start:shared_mutable_schedule
Expand Down

0 comments on commit c191a40

Please sign in to comment.