diff --git a/.circleci/config.yml b/.circleci/config.yml index 8203c0dc322..739f1b542fc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index e7554afb29d..0510373d3e0 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -11,4 +11,5 @@ mod note; mod oracle; mod state_vars; mod prelude; +mod public_storage; use dep::protocol_types; diff --git a/noir-projects/aztec-nr/aztec/src/public_storage.nr b/noir-projects/aztec-nr/aztec/src/public_storage.nr new file mode 100644 index 00000000000..7375ab91e5a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/public_storage.nr @@ -0,0 +1,68 @@ +use dep::protocol_types::traits::{Deserialize, Serialize}; +use crate::oracle::storage::{storage_read, storage_write}; + +pub fn read(storage_slot: Field) -> T where T: Deserialize { + T::deserialize(storage_read(storage_slot)) +} + +pub fn write(storage_slot: Field, value: T) where T: Serialize { + 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( +// storage_slot: Field, +// context: PrivateContext +// ) -> T where T: Deserialize { +// 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 + } +} diff --git a/noir-projects/aztec-nr/aztec/src/state_vars.nr b/noir-projects/aztec-nr/aztec/src/state_vars.nr index f10b2ac487e..cc6c48f14d8 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars.nr @@ -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; @@ -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; diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr new file mode 100644 index 00000000000..fa61113bc78 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable.nr @@ -0,0 +1,4 @@ +mod shared_mutable; +mod scheduled_value_change; + +use shared_mutable::SharedMutable; diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/scheduled_value_change.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/scheduled_value_change.nr new file mode 100644 index 00000000000..482bbf0d3f7 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/scheduled_value_change.nr @@ -0,0 +1,296 @@ +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 { + 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. + _dummy: [Field; DELAY], +} + +impl ScheduledValueChange { + pub fn new(pre: T, post: T, block_of_change: u32) -> Self { + Self { pre, post, block_of_change, _dummy: [0; DELAY] } + } + + /// Returns the value stored in the data structure at a given block. This function can be called both in public + /// (where `block_number` is simply the current block number, i.e. the number of the block in which the current + /// transaction will be included) and in private (where `block_number` is the historical block number that is used + /// to construct the proof). + /// Reading in private is only safe if the transaction's `max_block_number` property is set to a value lower or + /// equal to the block horizon (see `get_block_horizon()`). + pub fn get_current_at(self, block_number: u32) -> T { + // 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 block_number < self.block_of_change { + self.pre + } else { + self.post + } + } + + /// Returns the scheduled change, i.e. the post-change value and the block at which it will become the current + /// value. Note that this block may be in the past if the change has already taken place. + /// Additionally, further changes might be later scheduled, potentially canceling the one returned by this function. + pub fn get_scheduled(self) -> (T, u32) { + (self.post, self.block_of_change) + } + + /// Returns the largest block number at which the value returned by `get_current_at` is known to remain the current + /// value. This value is only meaningful in private when constructing a proof at some `historical_block_number`, + /// since due to its asynchronous nature private execution cannot know about any later scheduled changes. + /// The value returned by `get_current_at` in private when called with a historical block number is only safe to use + /// if the transaction's `max_block_number` property is set to a value lower or equal to the block horizon computed + /// using the same historical block number. + 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 value (post) will not change unless a new value + // 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 simply the previous block to that one. + // + // 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 are two possible scenarios. + // 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) + } + } + + /// Mutates a scheduled value change by scheduling a change at the current block number. This function is only + /// meaningful when called in public with the current block number. + 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 Serialize<3> for ScheduledValueChange { + fn serialize(self) -> [Field; 3] where T: ToField { + [self.pre.to_field(), self.post.to_field(), self.block_of_change.to_field()] + } +} + +impl Deserialize<3> for ScheduledValueChange { + 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] + } + } +} + +fn min(lhs: u32, rhs: u32) -> u32 { + 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 { + 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 = 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 = ScheduledValueChange::new(pre, post, block_of_change); + + assert_eq(value.get_scheduled(), (post, block_of_change)); + } + + fn assert_block_horizon_invariants( + value: &mut ScheduledValueChange, + 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 = 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 = 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 = 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 = 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 = 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 = 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); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/shared_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/shared_mutable.nr new file mode 100644 index 00000000000..8a795b43680 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/shared_mutable.nr @@ -0,0 +1,198 @@ +use dep::protocol_types::{hash::pedersen_hash, traits::FromField}; + +use crate::context::{PrivateContext, PublicContext, 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}; + +struct SharedMutable { + context: Context, + storage_slot: Field, +} + +impl Storage for SharedMutable {} + +// SharedMutable 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). +impl SharedMutable { + 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 } + } + + pub fn schedule_value_change(self, new_value: T) { + let context = self.context.public.unwrap(); + let mut scheduled_value_change: ScheduledValueChange = public_storage::read(self.get_derived_storage_slot()); + + scheduled_value_change.schedule_change(new_value, context.block_number() as u32); + + public_storage::write(self.get_derived_storage_slot(), scheduled_value_change); + } + + pub fn get_current_value_in_public(self) -> T { + let scheduled_value_change: ScheduledValueChange = public_storage::read(self.get_derived_storage_slot()); + + let block_number = self.context.public.unwrap().block_number() as u32; + scheduled_value_change.get_current_at(block_number) + } + + pub fn get_scheduled_value_in_public(self) -> (T, u32) { + let scheduled_value_change: ScheduledValueChange = public_storage::read(self.get_derived_storage_slot()); + scheduled_value_change.get_scheduled() + } + + pub fn get_current_value_in_private(self) -> T where T: FromField { + let mut context = self.context.private.unwrap(); + + 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); + + // Prevent this transaction from being included in any block after the `block_horizon` + context.request_max_block_number(block_horizon); + scheduled_value_change.get_current_at(historical_block_number) + } + + fn historical_read_from_public_storage( + self, + context: PrivateContext + ) -> (ScheduledValueChange, 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]; + for i in 0..3 { + raw_fields[i] = public_storage_historical_read( + context, + derived_slot + i as Field, + context.this_address() + ); + } + + let scheduled_value: ScheduledValueChange = ScheduledValueChange::deserialize(raw_fields); + let historical_block_number = context.historical_header.global_variables.block_number as u32; + + (scheduled_value, 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 + pedersen_hash([self.storage_slot, 0], 0) + } +} + +mod test { + use dep::std::{unsafe, merkle::compute_merkle_root, test::OracleMock}; + + use crate::{ + context::{PublicContext, PrivateContext, Context}, + state_vars::shared_mutable::shared_mutable::SharedMutable, + oracle::get_public_data_witness::PublicDataWitness + }; + + use dep::protocol_types::{ + constants::{GENERATOR_INDEX__PUBLIC_LEAF_INDEX, PUBLIC_DATA_TREE_HEIGHT}, hash::pedersen_hash, + address::AztecAddress, public_data_tree_leaf_preimage::PublicDataTreeLeafPreimage + }; + + fn setup(private: bool) -> (SharedMutable, Field) { + let block_number = 40; + let context = create_context(block_number, private); + + let storage_slot = 57; + let state_var: SharedMutable = SharedMutable::new(context, storage_slot); + + (state_var, block_number) + } + + fn create_context(block_number: Field, private: bool) -> Context { + if private { + let mut private_context: PrivateContext = unsafe::zeroed(); + private_context.historical_header.global_variables.block_number = block_number; + Context::private(&mut private_context) + } else { + let mut public_context: PublicContext = unsafe::zeroed(); + public_context.inputs.public_global_variables.block_number = block_number; + Context::public(&mut public_context) + } + } + + global TEST_DELAY = 20; + + #[test] + fn test_get_current_value_in_public_before_change() { + let (state_var, block_number) = setup(false); + + let slot = state_var.get_derived_storage_slot(); + let (pre, post) = (13, 17); + + // Change in the future + OracleMock::mock("storageRead").with_params((slot, 3)).returns([pre, post, block_number + 1]); + assert_eq(state_var.get_current_value_in_public(), pre); + } + + #[test] + fn test_get_current_value_in_public_at_change() { + let (state_var, block_number) = setup(false); + + let slot = state_var.get_derived_storage_slot(); + let (pre, post) = (13, 17); + + // Change in the current block + OracleMock::mock("storageRead").with_params((slot, 3)).returns([pre, post, block_number]); + assert_eq(state_var.get_current_value_in_public(), post); + } + + #[test] + fn test_get_current_value_in_public_after_change() { + let (state_var, block_number ) = setup(false); + + let slot = state_var.get_derived_storage_slot(); + let (pre, post) = (13, 17); + + // Change in the past + OracleMock::mock("storageRead").with_params((slot, 3)).returns([pre, post, block_number - 1]); + assert_eq(state_var.get_current_value_in_public(), post); + } + + #[test] + fn test_schedule_value_change_before_change() { + let (state_var, block_number) = setup(false); + + let slot = state_var.get_derived_storage_slot(); + let (pre, post) = (13, 17); + + let slot = state_var.get_derived_storage_slot(); + + OracleMock::mock("storageRead").with_params((slot, 3)).returns([pre, post, block_number + 1]); + + let new_value = 42; + // Here we want to assert that the `storageWrite` oracle is called with a certain set of values, but the current + // oracle mocks don't have those capabilities. + // TODO: implement this once https://github.com/noir-lang/noir/issues/4652 is closed + // OracleMock::mock("storageWrite").expect_call((slot, [pre, new_value, block_number + DELAY])); + // state_var.schedule_value_change(new_value); + } + + #[test] + fn test_get_current_value_in_private_before_change() { + // Here we'd want to test that the private getter returns the correct value and sets max_block_number in the + // context to the expected block horizon, in all the possible scenarios (long before change, before near change, + // after change). + // However, this requires mocking the getPublicDataTreeWitness oracle so that we can convince the circuit that + // it got a valid historical proof. Because we can set the tree root to whatever we want in the context, this is + // trivial for a single historical value (we add a leaf and compute the root with any random path), but is quite + // hard if we're reading more than one value for the same root (as SharedMutable does): we essentially need to + // create an actual indexed tree and compute the correct path for each of the inserted values. + // TODO: implement an actual tree and use it here https://github.com/AztecProtocol/aztec-packages/issues/5494 + } +} diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 34f00b4cd61..7fc9a841cf2 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "contracts/app_subscription_contract", + "contracts/auth_contract", "contracts/avm_initializer_test_contract", "contracts/avm_test_contract", "contracts/fpc_contract", diff --git a/noir-projects/noir-contracts/contracts/auth_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/auth_contract/Nargo.toml new file mode 100644 index 00000000000..57453502f0e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/auth_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "auth_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/auth_contract/src/main.nr b/noir-projects/noir-contracts/contracts/auth_contract/src/main.nr new file mode 100644 index 00000000000..a8025208c60 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/auth_contract/src/main.nr @@ -0,0 +1,64 @@ +// Test contract showing basic public access control that can be used in private. It uses a SharedMutable state variable to +// publicly store the address of an authorized account that can call private functions. +contract Auth { + use dep::aztec::protocol_types::address::AztecAddress; + use dep::aztec::{ + log::{emit_unencrypted_log, emit_unencrypted_log_from_private}, + state_vars::{PublicImmutable, SharedMutable} + }; + + // Authorizing a new address has a certain block delay before it goes into effect. + global CHANGE_AUTHORIZED_DELAY_BLOCKS = 5; + + #[aztec(storage)] + struct Storage { + // Admin can change the value of the authorized address via set_authorized() + admin: PublicImmutable, + authorized: SharedMutable, + } + + #[aztec(public)] + #[aztec(initializer)] + 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 adddress. + } + + #[aztec(public)] + fn set_authorized(authorized: AztecAddress) { + assert_eq(storage.admin.read(), context.msg_sender(), "caller is not admin"); + storage.authorized.schedule_value_change(authorized); + } + + #[aztec(public)] + fn get_authorized() { + // We emit logs because we cannot otherwise return these values + emit_unencrypted_log( + &mut context, + storage.authorized.get_current_value_in_public() + ); + } + + #[aztec(public)] + fn get_scheduled_authorized() { + // We emit logs because we cannot otherwise return these values + emit_unencrypted_log( + &mut context, + storage.authorized.get_scheduled_value_in_public().0 + ); + } + + #[aztec(private)] + fn do_private_authorized_thing(value: Field) { + // Reading a value from authorized in private automatically adds an extra validity condition: the base rollup + // circuit will reject this tx if included in a block past the block horizon, which is as far as the circuit can + // guarantee the value will not change from some historical value (due to CHANGE_AUTHORIZED_DELAY_BLOCKS). + let authorized = storage.authorized.get_current_value_in_private(); + assert_eq(authorized, context.msg_sender(), "caller is not authorized"); + + // We emit logs because we cannot otherwise return these values + emit_unencrypted_log_from_private(&mut context, value); + } +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr index aab51f65593..605b291b108 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/address/aztec_address.nr @@ -2,7 +2,7 @@ use crate::{ crate::address::{eth_address::EthAddress, partial_address::PartialAddress, public_keys_hash::PublicKeysHash}, constants::{AZTEC_ADDRESS_LENGTH, GENERATOR_INDEX__CONTRACT_ADDRESS}, contract_class_id::ContractClassId, hash::pedersen_hash, grumpkin_point::GrumpkinPoint, - traits::{Empty, ToField, Serialize, Deserialize}, utils + traits::{Empty, FromField, ToField, Serialize, Deserialize}, utils }; // Aztec address @@ -30,6 +30,12 @@ impl ToField for AztecAddress { } } +impl FromField for AztecAddress { + fn from_field(value: Field) -> AztecAddress { + AztecAddress { inner: value } + } +} + impl Serialize for AztecAddress { fn serialize(self: Self) -> [Field; AZTEC_ADDRESS_LENGTH] { [self.to_field()] @@ -38,7 +44,7 @@ impl Serialize for AztecAddress { impl Deserialize for AztecAddress { fn deserialize(fields: [Field; AZTEC_ADDRESS_LENGTH]) -> Self { - AztecAddress::from_field(fields[0]) + FromField::from_field(fields[0]) } } @@ -47,10 +53,6 @@ impl AztecAddress { Self { inner: 0 } } - pub fn from_field(field: Field) -> Self { - Self { inner: field } - } - pub fn compute_from_public_key( pub_key: GrumpkinPoint, contract_class_id: ContractClassId, @@ -125,3 +127,17 @@ fn compute_address_from_partial_and_pubkey() { let expected_computed_address_from_partial_and_pubkey = 0x0447f893197175723deb223696e2e96dbba1e707ee8507766373558877e74197; assert(address.to_field() == expected_computed_address_from_partial_and_pubkey); } + +#[test] +fn from_field_to_field() { + let address = AztecAddress { inner: 37 }; + // TODO: uncomment this test once https://github.com/noir-lang/noir/issues/4635 is fixed + // assert_eq(FromField::from_field(address.to_field()), address); +} + +#[test] +fn serde() { + let address = AztecAddress { inner: 37 }; + // TODO: uncomment this test once https://github.com/noir-lang/noir/issues/4635 is fixed + // assert_eq(Deserialize::deserialize(address.serialize()), address); +} diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index 03f5f1de00d..3b0a8e20400 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -97,6 +97,7 @@ Rollup Address: 0x0dcd1bf9a1b36ce34237eeafef220932846bcd82 // docs:start:example-contracts % aztec-cli example-contracts AppSubscriptionContractArtifact +AuthContractArtifact BenchmarkingContractArtifact CardGameContractArtifact ChildContractArtifact diff --git a/yarn-project/end-to-end/src/e2e_auth_contract.test.ts b/yarn-project/end-to-end/src/e2e_auth_contract.test.ts new file mode 100644 index 00000000000..a5a3da14d14 --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_auth_contract.test.ts @@ -0,0 +1,125 @@ +import { type AccountWallet, AztecAddress, type ContractFunctionInteraction, Fr, type PXE } from '@aztec/aztec.js'; +import { AuthContract } from '@aztec/noir-contracts.js'; + +import { jest } from '@jest/globals'; + +import { publicDeployAccounts, setup } from './fixtures/utils.js'; + +describe('e2e_auth_contract', () => { + const TIMEOUT = 120_000; + jest.setTimeout(TIMEOUT); + + let teardown: () => Promise; + + let admin: AccountWallet; + let authorized: AccountWallet; + let other: AccountWallet; + + let pxe: PXE; + + let contract: AuthContract; + + const VALUE = 3; + const DELAY = 5; + + beforeAll(async () => { + ({ + teardown, + wallets: [admin, authorized, other], + pxe, + } = await setup(3)); + + await publicDeployAccounts(admin, [admin, authorized, other]); + + const deployTx = AuthContract.deploy(admin, admin.getAddress()).send({}); + const receipt = await deployTx.wait(); + contract = receipt.contract; + }); + + afterAll(() => teardown()); + + async function mineBlock() { + await contract.methods.get_authorized().send().wait(); + } + + async function mineBlocks(amount: number) { + for (let i = 0; i < amount; ++i) { + await mineBlock(); + } + } + + async function assertLoggedAddress(interaction: ContractFunctionInteraction, address: AztecAddress) { + const logs = await pxe.getUnencryptedLogs({ txHash: (await interaction.send().wait()).txHash }); + expect(AztecAddress.fromBuffer(logs.logs[0].log.data)).toEqual(address); + } + + async function assertLoggedNumber(interaction: ContractFunctionInteraction, value: number) { + const logs = await pxe.getUnencryptedLogs({ txHash: (await interaction.send().wait()).txHash }); + expect(Fr.fromBuffer(logs.logs[0].log.data)).toEqual(new Fr(value)); + } + + it('authorized is unset initially', async () => { + await assertLoggedAddress(contract.methods.get_authorized(), AztecAddress.ZERO); + }); + + it('admin sets authorized', async () => { + await contract.withWallet(admin).methods.set_authorized(authorized.getAddress()).send().wait(); + + await assertLoggedAddress(contract.methods.get_scheduled_authorized(), authorized.getAddress()); + }); + + it('authorized is not yet set, cannot use permission', async () => { + await assertLoggedAddress(contract.methods.get_authorized(), AztecAddress.ZERO); + + await expect( + contract.withWallet(authorized).methods.do_private_authorized_thing(VALUE).send().wait(), + ).rejects.toThrow('caller is not authorized'); + }); + + it('after a while the scheduled change is effective and can be used with max block restriction', async () => { + await mineBlocks(DELAY); // This gets us past the block of change + + await assertLoggedAddress(contract.methods.get_authorized(), authorized.getAddress()); + + const interaction = contract.withWallet(authorized).methods.do_private_authorized_thing(VALUE); + + const tx = await interaction.simulate(); + + const lastBlockNumber = await pxe.getBlockNumber(); + // In the last block there was no scheduled value change, so the earliest one could be scheduled is in the next + // block. Because of the delay, the block of change would be lastBlockNumber + 1 + DELAY. Therefore the block + // horizon should be the block preceding that one. + const expectedMaxBlockNumber = lastBlockNumber + DELAY; + + expect(tx.data.rollupValidationRequests.maxBlockNumber.isSome).toEqual(true); + expect(tx.data.rollupValidationRequests.maxBlockNumber.value).toEqual(new Fr(expectedMaxBlockNumber)); + + await assertLoggedNumber(interaction, VALUE); + }); + + it('a new authorized address is set but not immediately effective, the previous one retains permissions', async () => { + await contract.withWallet(admin).methods.set_authorized(other.getAddress()).send().wait(); + + await assertLoggedAddress(contract.methods.get_authorized(), authorized.getAddress()); + + await assertLoggedAddress(contract.methods.get_scheduled_authorized(), other.getAddress()); + + await expect(contract.withWallet(other).methods.do_private_authorized_thing(VALUE).send().wait()).rejects.toThrow( + 'caller is not authorized', + ); + + await assertLoggedNumber(contract.withWallet(authorized).methods.do_private_authorized_thing(VALUE), VALUE); + }); + + it('after some time the scheduled change is made effective', async () => { + await mineBlocks(DELAY); // This gets us past the block of change + + await assertLoggedAddress(contract.methods.get_authorized(), other.getAddress()); + + await expect( + contract.withWallet(authorized).methods.do_private_authorized_thing(VALUE).send().wait(), + ).rejects.toThrow('caller is not authorized'); + + await assertLoggedNumber(contract.withWallet(other).methods.do_private_authorized_thing(VALUE), VALUE); + }); +}); diff --git a/yarn-project/end-to-end/src/e2e_max_block_number.test.ts b/yarn-project/end-to-end/src/e2e_max_block_number.test.ts index 0b8307f1fb4..b7c78ff4dde 100644 --- a/yarn-project/end-to-end/src/e2e_max_block_number.test.ts +++ b/yarn-project/end-to-end/src/e2e_max_block_number.test.ts @@ -1,4 +1,4 @@ -import { type PXE, type Wallet } from '@aztec/aztec.js'; +import { Fr, type PXE, type Wallet } from '@aztec/aztec.js'; import { TestContract } from '@aztec/noir-contracts.js'; import { setup } from './fixtures/utils.js'; @@ -27,6 +27,12 @@ describe('e2e_max_block_number', () => { describe('with no enqueued public calls', () => { const enqueuePublicCall = false; + it('sets the max block number', async () => { + const tx = await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).simulate(); + expect(tx.data.rollupValidationRequests.maxBlockNumber.isSome).toEqual(true); + expect(tx.data.rollupValidationRequests.maxBlockNumber.value).toEqual(new Fr(maxBlockNumber)); + }); + it('does not invalidate the transaction', async () => { await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).send().wait(); }); @@ -35,6 +41,12 @@ describe('e2e_max_block_number', () => { describe('with an enqueued public call', () => { const enqueuePublicCall = true; + it('sets the max block number', async () => { + const tx = await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).simulate(); + expect(tx.data.rollupValidationRequests.maxBlockNumber.isSome).toEqual(true); + expect(tx.data.rollupValidationRequests.maxBlockNumber.value).toEqual(new Fr(maxBlockNumber)); + }); + it('does not invalidate the transaction', async () => { await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).send().wait(); }); @@ -51,6 +63,12 @@ describe('e2e_max_block_number', () => { describe('with no enqueued public calls', () => { const enqueuePublicCall = false; + it('sets the max block number', async () => { + const tx = await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).simulate(); + expect(tx.data.rollupValidationRequests.maxBlockNumber.isSome).toEqual(true); + expect(tx.data.rollupValidationRequests.maxBlockNumber.value).toEqual(new Fr(maxBlockNumber)); + }); + it('invalidates the transaction', async () => { await expect( contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).send().wait(), @@ -61,6 +79,12 @@ describe('e2e_max_block_number', () => { describe('with an enqueued public call', () => { const enqueuePublicCall = true; + it('sets the max block number', async () => { + const tx = await contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).simulate(); + expect(tx.data.rollupValidationRequests.maxBlockNumber.isSome).toEqual(true); + expect(tx.data.rollupValidationRequests.maxBlockNumber.value).toEqual(new Fr(maxBlockNumber)); + }); + it('invalidates the transaction', async () => { await expect( contract.methods.request_max_block_number(maxBlockNumber, enqueuePublicCall).send().wait(),