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: shared mutable storage #5490

Merged
merged 29 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9e8e40c
Begin sketching ScheduledValue
nventuro Mar 20, 2024
a52933a
Add shared mutable, noir tests
nventuro Mar 27, 2024
0c3495f
Split storage lib
nventuro Mar 27, 2024
cd334e0
Add e2e test
nventuro Mar 27, 2024
2165fc9
Add docs
nventuro Mar 27, 2024
3e28aef
Comment out failing assertions
nventuro Mar 27, 2024
a0d94f4
Rename variable
nventuro Mar 27, 2024
60d57f1
Properly skip tests
nventuro Mar 27, 2024
92853d2
Remove unused import
nventuro Mar 27, 2024
3740ac4
Add padding notes
nventuro Mar 27, 2024
8c85c60
Add to list of exmaples
nventuro Mar 27, 2024
2eec474
Add issue link
nventuro Mar 27, 2024
367235f
Fix scheduled value change, add comments
nventuro Mar 28, 2024
cbb4821
Add extra test assertion
nventuro Mar 28, 2024
4a8fd2c
Make tests more readable
nventuro Mar 28, 2024
6da0b4b
Remove unused imports
nventuro Mar 28, 2024
5eff481
Add some clarifying comments on block horizon and friends
nventuro Mar 28, 2024
55ecfac
Expand max_block_number tests
nventuro Mar 28, 2024
f567507
Only emit address
nventuro Mar 28, 2024
dc75345
Merge branch 'master' into nv/shared-mutable-storage
nventuro Mar 28, 2024
c987857
Fix diagram
nventuro Mar 28, 2024
8517458
Merge branch 'master' into nv/shared-mutable-storage
nventuro Apr 10, 2024
92646b0
Apply suggestions from code review
nventuro Apr 10, 2024
ac96461
Update noir-projects/noir-contracts/contracts/auth_contract/src/main.nr
nventuro Apr 10, 2024
8e95c72
Update noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/sha…
nventuro Apr 10, 2024
39aab8e
Add struct tag
nventuro Apr 10, 2024
bbeefc6
Specialize min for u32
nventuro Apr 10, 2024
65b5605
fix imports
nventuro Apr 10, 2024
74dc42c
Merge branch 'master' into nv/shared-mutable-storage
nventuro Apr 10, 2024
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
10 changes: 10 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,16 @@ jobs:
aztec_manifest_key: end-to-end
<<: *defaults_e2e_test

e2e-auth:
steps:
- *checkout
- *setup_env
- run:
name: "Test"
command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_auth_contract.test.ts
aztec_manifest_key: end-to-end
<<: *defaults_e2e_test

e2e-note-getter:
steps:
- *checkout
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ mod note;
mod oracle;
mod state_vars;
mod prelude;
mod public_storage;
use dep::protocol_types;
68 changes: 68 additions & 0 deletions noir-projects/aztec-nr/aztec/src/public_storage.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use dep::protocol_types::traits::{Deserialize, Serialize};
use crate::oracle::storage::{storage_read, storage_write};

pub fn read<T, N>(storage_slot: Field) -> T where T: Deserialize<N> {
T::deserialize(storage_read(storage_slot))
}

pub fn write<T, N>(storage_slot: Field, value: T) where T: Serialize<N> {
storage_write(storage_slot, value.serialize());
}

// Ideally we'd do the following, but we cannot because of https://github.com/noir-lang/noir/issues/4633
// pub fn read_historical<T, N>(
// storage_slot: Field,
// context: PrivateContext
// ) -> T where T: Deserialize<N> {
// let mut fields = [0; N];
// for i in 0..N {
// fields[i] = public_storage_historical_read(
// context,
// storage_slot + i as Field,
// context.this_address()
// );
// }
// T::deserialize(fields)
// }

mod tests {
use dep::std::test::OracleMock;
use dep::protocol_types::traits::{Deserialize, Serialize};
use crate::public_storage;

struct TestStruct {
a: Field,
b: Field,
}

impl Deserialize<2> for TestStruct {
fn deserialize(fields: [Field; 2]) -> TestStruct {
TestStruct { a: fields[0], b: fields[1] }
}
}

impl Serialize<2> for TestStruct {
fn serialize(self) -> [Field; 2] {
[self.a, self.b]
}
}

#[test]
fn test_read() {
let slot = 7;
let written = TestStruct { a: 13, b: 42 };

OracleMock::mock("storageRead").with_params((slot, 2)).returns(written.serialize());

let read: TestStruct = public_storage::read(slot);
assert_eq(read.a, 13);
assert_eq(read.b, 42);
}

#[test]
fn test_write() {
// Here we'd want to test that what is written to storage is deserialized to the same struct, but the current
// oracle mocks lack these capabilities.
// TODO: implement this once https://github.com/noir-lang/noir/issues/4652 is closed
nventuro marked this conversation as resolved.
Show resolved Hide resolved
}
}
2 changes: 2 additions & 0 deletions noir-projects/aztec-nr/aztec/src/state_vars.nr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod public_immutable;
mod public_mutable;
mod private_set;
mod shared_immutable;
mod shared_mutable;
mod storage;

use crate::state_vars::map::Map;
Expand All @@ -14,4 +15,5 @@ use crate::state_vars::public_immutable::PublicImmutable;
use crate::state_vars::public_mutable::PublicMutable;
use crate::state_vars::private_set::PrivateSet;
use crate::state_vars::shared_immutable::SharedImmutable;
use crate::state_vars::shared_mutable::SharedMutable;
use crate::state_vars::storage::Storage;
4 changes: 4 additions & 0 deletions noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod shared_mutable;
mod scheduled_value_change;

use shared_mutable::SharedMutable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
use dep::protocol_types::traits::{Serialize, Deserialize, FromField, ToField};

// This data structure is used by SharedMutable to represent a value that changes from `pre` to `post` at some block
// called the `block_of_change`. The value can only be made to change by scheduling a change event at some future block
// of change after some delay measured in blocks has elapsed. This means that at any given block number we know both the
// current value and the smallest block number at which the value might change - this is called the 'block horizon'.
//
// The delay being a type parameter instead of a struct field is an implementation detail, and is due to a number of
// reasons:
// - we want to serialize and deserialize this object in order to store it in public storage, but we don't want to
// include the delay there because it is immutable
// - because of how aztec-nr state variables are declared, having a type with some immutable property is better
// expressed via types, since they are always constructed with the same `::new(context, storage_slot)` function.
struct ScheduledValueChange<T, DELAY> {
pre: T,
post: T,
block_of_change: u32,
// The _dummy variable forces DELAY to be interpreted as a numberic value. This is a workaround to
// https://github.com/noir-lang/noir/issues/4633. Remove once resolved.
nventuro marked this conversation as resolved.
Show resolved Hide resolved
_dummy: [Field; DELAY],
}

impl<T, DELAY> ScheduledValueChange<T, DELAY> {
pub fn new(pre: T, post: T, block_of_change: u32) -> Self {
Self { pre, post, block_of_change, _dummy: [0; DELAY] }
}

pub fn get_current_at(self, historical_block_number: u32) -> T {
Copy link
Contributor

Choose a reason for hiding this comment

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

Something feels weird here, that if I read really far back, the "pre" might not be the actual value.

Like. At block 10 value might be (a,b,c) and then later it becomes (b,d,e) but If I then say read at 10, I would read the pre as b.

It makes sense, was just from the naming that it is what sprung to mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I follow your example, could you provide concrete values for c and e?

But yes, values do move from post to pre when a new change is scheduled. Clients that have not seen the new scheduled change will read post, while clients that have seen the change will read pre. The important thing is that these values will be the same, and all reads will be valid (as long as max_block_number is properly constrained).

This is what the assert_block_horizon_invariants helper function tests: current_value returns the same value at the historical block number and at the block horizon, even if a new change is scheduled as early as possible (i.e. in the block following the historical block).

// The post value becomes the current one at the block of change. This means different things in each realm:
// - in public, any transaction that is included in the block of change will use the post value
// - in private, any transaction that includes the block of change as part of the historical state will use the
// post value (barring any follow-up changes)

if historical_block_number < self.block_of_change {
self.pre
} else {
self.post
}
}

pub fn get_scheduled(self) -> (T, u32) {
(self.post, self.block_of_change)
}

pub fn get_block_horizon(self, historical_block_number: u32) -> u32 {
// The block horizon is the very last block in which the current value is known. Any block past the horizon
// (i.e. with a block number larger than the block horizon) may have a different current value. Reading the
// current value in private typically requires constraining the maximum valid block number to be equal to the
// block horizon.

if historical_block_number >= self.block_of_change {
// Once the block of change has been mined, the current vaue (post) will not change unless a new value
nventuro marked this conversation as resolved.
Show resolved Hide resolved
// change is scheduled. This did not happen at the historical block number (or else it would not be
// greater or equal to the block of change), and therefore could only happen after the historical block
// number. The earliest would be the immediate next block, and so the smallest possible next block of change
// equals `historical_block_number + 1 + DELAY`. Our block horizon is simple the previous block to that one.
nventuro marked this conversation as resolved.
Show resolved Hide resolved
//
// block of historical
// change block block horizon
// =======|=============N===================H===========>
// ^ ^
// ---------------------
// delay

historical_block_number + DELAY
} else {
// If the block of change has not yet been mined however, then there's two possible scenarios.
nventuro marked this conversation as resolved.
Show resolved Hide resolved
// a) It could be so far into the future that the block horizon is actually determined by the delay,
// because a new change could be scheduled and take place _before_ the currently scheduled one. This is
// similar to the scenario where the block of change is in the past: the time horizon is the block
// prior to the earliest one in which a new block of change might land.
//
// historical
// block block horizon block of change
// =====N=================================H=================|=========>
// ^ ^
// | |
// -----------------------------------
// delay
//
// b) It could be fewer than `delay` blocks away from the historical block number, in which case it would
// become the limiting factor for the time horizon, which would be the block right before the block of
// change (since by definition the value changes at the block of change).
//
// historical block horizon
// block block of change if not scheduled
// =======N=============|===================H=================>
// ^ ^ ^
// | actual horizon |
// ----------------------------------
// delay
//
// Note that the current implementation does not allow the caller to set the block of change to an arbitrary
// value, and therefore scenario a) is not currently possible. However implementing #5501 would allow for
// this to happen.

// Because historical_block_number < self.block_of_change, then block_of_change > 0 and we can safely
// subtract 1.
min(self.block_of_change - 1, historical_block_number + DELAY)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just thinking about the -1. So we are having the -1 to make sure that the block horizon ends before the change occurs.

But probably be nicce to have some better clarity on why it is not needed for the historical_block_number + DELAY.

Like, If I m just thinking of an execution where you havae

get_block_horizon(current_block_number) <-- horizon would be current + delay
schedule_change(new, current_block_number) <-- block_of_change would be current + delay

Which sounds weird?

But as we are constraining the functions in the shared_mutable to only use the get_block_horizon in private, you cannot really get this case, as the current cannot be used.


Rambly rambler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

why it is not needed for the historical_block_number + DELAY.

This is what the comments and the diagram above try to convey: the earliest a new block of change could be is historical + 1 + DELAY, so the missing -1 is because it cancels out the +1.

Re. get_block_horizon, yes, its meant to be a value consumed in private with some historical block number. If you pass the current block in public then you'll get an invalid horizon, because as you point out you could immediately schedule a new change in the same block.

I'll add some comments and do some light renamings to make it clearer that get_current_at() can be called in both private and public, while get_block_horizon() should be private only (unless you know what you're doing).

Regardless, this is ultimately a helper data structure that would likely only be used by SharedMutable and not an end user, so I'm not too worried.

}
}

pub fn schedule_change(&mut self, new_value: T, current_block_number: u32) {
self.pre = self.get_current_at(current_block_number);
self.post = new_value;
// TODO: make this configurable
// https://github.com/AztecProtocol/aztec-packages/issues/5501
self.block_of_change = current_block_number + DELAY;
}
}

impl<T, DELAY> Serialize<3> for ScheduledValueChange<T, DELAY> {
fn serialize(self) -> [Field; 3] where T: ToField {
[self.pre.to_field(), self.post.to_field(), self.block_of_change.to_field()]
}
}

impl<T, DELAY> Deserialize<3> for ScheduledValueChange<T, DELAY> {
fn deserialize(input: [Field; 3]) -> Self where T: FromField {
Self {
pre: FromField::from_field(input[0]),
post: FromField::from_field(input[1]),
block_of_change: FromField::from_field(input[2]),
_dummy: [0; DELAY]
nventuro marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

fn min<T>(lhs: T, rhs: T) -> T where T: Ord {
if lhs < rhs { lhs } else { rhs }
}

#[test]
fn test_min() {
assert(min(3, 5) == 3);
assert(min(5, 3) == 3);
assert(min(3, 3) == 3);
}

mod test {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would likely be nice to test a case with larger structs for the pre and post to ensure working as expected, looks mainly to be single fields here?

It might be too disgusting to get working before we can do N*2+1 as the size of the serialization though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah ScheduledValueChange by itself would be fine since it doesn't use T for anything, the problem will be accesing storage due to incomplete support for numeric generics in Noir, as mentioned in #5492.

use crate::state_vars::shared_mutable::scheduled_value_change::ScheduledValueChange;

global TEST_DELAY = 200;

#[test]
fn test_get_current_at() {
let pre = 1;
let post = 2;
let block_of_change = 50;

let value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(pre, post, block_of_change);

assert_eq(value.get_current_at(0), pre);
assert_eq(value.get_current_at(block_of_change - 1), pre);
assert_eq(value.get_current_at(block_of_change), post);
assert_eq(value.get_current_at(block_of_change + 1), post);
}

#[test]
fn test_get_scheduled() {
let pre = 1;
let post = 2;
let block_of_change = 50;

let value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(pre, post, block_of_change);

assert_eq(value.get_scheduled(), (post, block_of_change));
}

fn assert_block_horizon_invariants(
value: &mut ScheduledValueChange<Field, TEST_DELAY>,
historical_block_number: u32,
block_horizon: u32
) {
// The current value should not change at the block horizon (but it might later).
let current_at_historical = value.get_current_at(historical_block_number);
assert_eq(current_at_historical, value.get_current_at(block_horizon));

// The earliest a new change could be scheduled in would be the immediate next block to the historical one. This
// should result in the new block of change landing *after* the block horizon, and the current value still not
// changing at the previously determined block_horizon.

let new = value.pre + value.post; // Make sure it's different to both pre and post
value.schedule_change(new, historical_block_number + 1);

assert(value.block_of_change > block_horizon);
assert_eq(current_at_historical, value.get_current_at(block_horizon));
}

#[test]
fn test_get_block_horizon_change_in_past() {
let historical_block_number = 100;
let block_of_change = 50;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(1, 2, block_of_change);

let block_horizon = value.get_block_horizon(historical_block_number);
assert_eq(block_horizon, historical_block_number + TEST_DELAY);

assert_block_horizon_invariants(&mut value, historical_block_number, block_horizon);
}

#[test]
fn test_get_block_horizon_change_in_immediate_past() {
let historical_block_number = 100;
let block_of_change = 100;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(1, 2, block_of_change);

let block_horizon = value.get_block_horizon(historical_block_number);
assert_eq(block_horizon, historical_block_number + TEST_DELAY);

assert_block_horizon_invariants(&mut value, historical_block_number, block_horizon);
}

#[test]
fn test_get_block_horizon_change_in_near_future() {
let historical_block_number = 100;
let block_of_change = 120;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(1, 2, block_of_change);

// Note that this is the only scenario in which the block of change informs the block horizon.
// This may result in privacy leaks when interacting with applications that have a scheduled change
// in the near future.
let block_horizon = value.get_block_horizon(historical_block_number);
assert_eq(block_horizon, block_of_change - 1);

assert_block_horizon_invariants(&mut value, historical_block_number, block_horizon);
}

#[test]
fn test_get_block_horizon_change_in_far_future() {
let historical_block_number = 100;
let block_of_change = 500;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(1, 2, block_of_change);

let block_horizon = value.get_block_horizon(historical_block_number);
assert_eq(block_horizon, historical_block_number + TEST_DELAY);

assert_block_horizon_invariants(&mut value, historical_block_number, block_horizon);
}

#[test]
fn test_schedule_change_before_prior_change() {
let pre = 1;
let post = 2;
let block_of_change = 500;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(pre, post, block_of_change);

let new = 42;
let current_block_number = block_of_change - 50;
value.schedule_change(new, current_block_number);

// Because we re-schedule before the last scheduled change takes effect, the old `post` value is lost.
assert_eq(value.pre, pre);
assert_eq(value.post, new);
assert_eq(value.block_of_change, current_block_number + TEST_DELAY);
}

#[test]
fn test_schedule_change_after_prior_change() {
let pre = 1;
let post = 2;
let block_of_change = 500;

let mut value: ScheduledValueChange<Field, TEST_DELAY> = ScheduledValueChange::new(pre, post, block_of_change);

let new = 42;
let current_block_number = block_of_change + 50;
value.schedule_change(new, current_block_number);

assert_eq(value.pre, post);
assert_eq(value.post, new);
assert_eq(value.block_of_change, current_block_number + TEST_DELAY);
}
}
Loading
Loading