diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 2b64fe54f4a3..8a666c3366da 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,6 +1,7 @@ -use crate::oracle::ephemeral; +use crate::oracle::ephemeral_oracles; use crate::protocol::traits::{Deserialize, Serialize}; use crate::protocol::utils::{reader::Reader, writer::Writer}; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// A dynamically sized array that exists only during a single contract call frame. /// @@ -23,98 +24,43 @@ use crate::protocol::utils::{reader::Reader, writer::Writer}; /// For data that must be shared across all frames of the same contract (private and utility) within one top-level PXE /// call (transaction simulation or utility call) but not persisted, use /// [`TransientArray`](crate::transient::TransientArray). -pub struct EphemeralArray { - pub slot: Field, -} +pub type EphemeralArray = UnconstrainedArray; -impl EphemeralArray { - /// Returns a handle to an ephemeral array at the given slot, which may already contain data (e.g. populated - /// by an oracle). - pub unconstrained fn at(slot: Field) -> Self { - Self { slot } - } +pub struct EphemeralOracles {} - /// Returns an empty ephemeral array at the given slot, clearing any pre-existing data. - pub unconstrained fn empty_at(slot: Field) -> Self { - Self::at(slot).clear() +impl ArrayOracles for EphemeralOracles { + unconstrained fn len_oracle(slot: Field) -> u32 { + ephemeral_oracles::len_oracle(slot) } - /// Returns the number of elements stored in the array. - pub unconstrained fn len(self) -> u32 { - ephemeral::len_oracle(self.slot) + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { + ephemeral_oracles::push_oracle(slot, values) } - /// Stores a value at the end of the array. - pub unconstrained fn push(self, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - let _ = ephemeral::push_oracle(self.slot, serialized); + unconstrained fn pop_oracle(slot: Field) -> [Field; N] { + ephemeral_oracles::pop_oracle(slot) } - /// Removes and returns the last element. Panics if the array is empty. - pub unconstrained fn pop(self) -> T - where - T: Deserialize, - { - let serialized = ephemeral::pop_oracle(self.slot); - Deserialize::deserialize(serialized) + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { + ephemeral_oracles::get_oracle(slot, index) } - /// Retrieves the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn get(self, index: u32) -> T - where - T: Deserialize, - { - let serialized = ephemeral::get_oracle(self.slot, index); - Deserialize::deserialize(serialized) + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { + ephemeral_oracles::set_oracle(slot, index, values) } - /// Overwrites the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn set(self, index: u32, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - ephemeral::set_oracle(self.slot, index, serialized); + unconstrained fn remove_oracle(slot: Field, index: u32) { + ephemeral_oracles::remove_oracle(slot, index) } - /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. - pub unconstrained fn remove(self, index: u32) { - ephemeral::remove_oracle(self.slot, index); - } - - /// Removes all elements from the array and returns self for chaining. - /// - /// Prefer [`EphemeralArray::empty_at`] when the intent is to start with a fresh array. - pub unconstrained fn clear(self) -> Self { - ephemeral::clear_oracle(self.slot); - self - } - - /// Calls a function on each element of the array. - /// - /// The function `f` is called once with each array value and its corresponding index. Iteration proceeds - /// backwards so that it is safe to remove the current element (and only the current element) inside the - /// callback. - /// - /// It is **not** safe to push new elements from inside the callback. - pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) - where - T: Deserialize, - { - let mut i = self.len(); - while i > 0 { - i -= 1; - f(i, self.get(i)); - } + unconstrained fn clear_oracle(slot: Field) { + ephemeral_oracles::clear_oracle(slot) } } -/// Serializes an `EphemeralArray` as its slot identifier, allowing oracle function signatures to use -/// `EphemeralArray` instead of opaque `Field` slots. -impl Serialize for EphemeralArray { +/// Serializes an `EphemeralArray` as its slot, allowing oracle function signatures to use ephemeral array types +/// instead of opaque `Field` slots. +impl Serialize for UnconstrainedArray { let N: u32 = 1; fn serialize(self) -> [Field; Self::N] { @@ -127,8 +73,7 @@ impl Serialize for EphemeralArray { } /// Deserializes a single Field into an `EphemeralArray` handle, treating the field value as the slot identifier. -/// This is the inverse of [`Serialize`]. -impl Deserialize for EphemeralArray { +impl Deserialize for UnconstrainedArray { let N: u32 = 1; fn deserialize(fields: [Field; Self::N]) -> Self { @@ -140,279 +85,5 @@ impl Deserialize for EphemeralArray { } } -mod test { - use crate::test::helpers::test_environment::TestEnvironment; - use crate::test::mocks::MockStruct; - use super::EphemeralArray; - - global SLOT: Field = 1230; - global OTHER_SLOT: Field = 5670; - - #[test] - unconstrained fn empty_array() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - assert_eq(array.len(), 0); - }); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn empty_array_read() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - let _: Field = array.get(0); - }); - } - - #[test(should_fail_with = "is empty")] - unconstrained fn empty_array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - let _: Field = array.pop(); - }); - } - - #[test] - unconstrained fn array_push() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn read_past_len() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - - let _ = array.get(1); - }); - } - - #[test] - unconstrained fn array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.push(10); - - let popped: Field = array.pop(); - assert_eq(popped, 10); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); - } - - #[test] - unconstrained fn array_set() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.set(0, 99); - assert_eq(array.get(0), 99); - }); - } - - #[test] - unconstrained fn array_remove_last() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - array.push(5); - array.remove(0); - assert_eq(array.len(), 0); - }); - } - - #[test] - unconstrained fn array_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - assert_eq(array.len(), 3); - - array.remove(1); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 7); - assert_eq(array.get(1), 9); - }); - } - - #[test] - unconstrained fn array_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - array.remove(1); - array.remove(1); - array.remove(0); - - assert_eq(array.len(), 0); - }); - } - - #[test] - unconstrained fn for_each_called_with_all_elements() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); - array.for_each(|index, value| { called_with.push((index, value)); }); - - assert_eq(called_with.len(), 3); - assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); - assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); - assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); - }); - } - - #[test] - unconstrained fn for_each_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { - if index == 1 { - array.remove(index); - } - }); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 4); - assert_eq(array.get(1), 6); - }); - } - - #[test] - unconstrained fn for_each_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = EphemeralArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { array.remove(index); }); - - assert_eq(array.len(), 0); - }); - } - - #[test] - unconstrained fn different_slots_are_isolated() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array_a = EphemeralArray::at(SLOT); - let array_b = EphemeralArray::at(OTHER_SLOT); - - array_a.push(10); - array_a.push(20); - array_b.push(99); - - assert_eq(array_a.len(), 2); - assert_eq(array_a.get(0), 10); - assert_eq(array_a.get(1), 20); - - assert_eq(array_b.len(), 1); - assert_eq(array_b.get(0), 99); - }); - } - - #[test] - unconstrained fn works_with_multi_field_type() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - - let a = MockStruct::new(5, 6); - let b = MockStruct::new(7, 8); - array.push(a); - array.push(b); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), a); - assert_eq(array.get(1), b); - - let popped: MockStruct = array.pop(); - assert_eq(popped, b); - assert_eq(array.len(), 1); - }); - } - - #[test] - unconstrained fn empty_at_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - array.push(1); - assert_eq(array.len(), 1); - - let fresh: EphemeralArray = EphemeralArray::empty_at(SLOT); - assert_eq(fresh.len(), 0); - }); - } - - #[test] - unconstrained fn clear_returns_self() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT).clear(); - assert_eq(array.len(), 0); - - array.push(42); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); - } - - #[test] - unconstrained fn clear_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: EphemeralArray = EphemeralArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - assert_eq(array.len(), 3); - - // Clear the same slot, previous data should be gone. - let fresh: EphemeralArray = EphemeralArray::at(SLOT).clear(); - assert_eq(fresh.len(), 0); - fresh.push(4); - assert_eq(fresh.get(0), 4); - }); - } -} +#[crate::unconstrained_array::test_suite::unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })] +mod test {} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 51d250108832..387f820d22da 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -39,6 +39,7 @@ pub mod nullifier; pub mod oracle; pub mod state_vars; pub mod capsules; +pub mod unconstrained_array; pub mod ephemeral; pub mod transient; pub mod event; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr similarity index 84% rename from noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr rename to noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr index 9f2721fc53c8..cb053b599841 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral_oracles.nr @@ -8,15 +8,15 @@ #[oracle(aztec_utl_pushEphemeral)] pub(crate) unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 {} -/// Removes and returns the last serialized element from the ephemeral array. +/// Removes and returns the last serialized element from the ephemeral array. Panics if the array is empty. #[oracle(aztec_utl_popEphemeral)] pub(crate) unconstrained fn pop_oracle(slot: Field) -> [Field; N] {} -/// Returns the serialized element at the given index. +/// Returns the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_getEphemeral)] pub(crate) unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] {} -/// Overwrites the serialized element at the given index. +/// Overwrites the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_setEphemeral)] pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) {} @@ -24,7 +24,7 @@ pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, valu #[oracle(aztec_utl_getEphemeralLen)] pub(crate) unconstrained fn len_oracle(slot: Field) -> u32 {} -/// Removes the element at the given index, shifting subsequent elements backward. +/// Removes the element at the given index, shifting subsequent elements backward. Panics if `index` is out of bounds. #[oracle(aztec_utl_removeEphemeral)] pub(crate) unconstrained fn remove_oracle(slot: Field, index: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 85953142230d..6d677346a72f 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -7,8 +7,8 @@ pub mod block_header; pub mod call_private_function; pub mod call_utility_function; pub mod capsules; -pub mod ephemeral; -pub mod transient; +pub(crate) mod ephemeral_oracles; +pub(crate) mod transient_oracles; pub mod contract_sync; pub mod public_call; pub mod tx_phase; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/transient.nr b/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr similarity index 84% rename from noir-projects/aztec-nr/aztec/src/oracle/transient.nr rename to noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr index c7db29c51b2d..9c76dca0e635 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/transient.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/transient_oracles.nr @@ -9,15 +9,15 @@ #[oracle(aztec_utl_pushTransient)] pub(crate) unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 {} -/// Removes and returns the last serialized element from the transient array. +/// Removes and returns the last serialized element from the transient array. Panics if the array is empty. #[oracle(aztec_utl_popTransient)] pub(crate) unconstrained fn pop_oracle(slot: Field) -> [Field; N] {} -/// Returns the serialized element at the given index. +/// Returns the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_getTransient)] pub(crate) unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] {} -/// Overwrites the serialized element at the given index. +/// Overwrites the serialized element at the given index. Panics if `index` is out of bounds. #[oracle(aztec_utl_setTransient)] pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) {} @@ -25,7 +25,7 @@ pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, valu #[oracle(aztec_utl_getTransientLen)] pub(crate) unconstrained fn len_oracle(slot: Field) -> u32 {} -/// Removes the element at the given index, shifting subsequent elements backward. +/// Removes the element at the given index, shifting subsequent elements backward. Panics if `index` is out of bounds. #[oracle(aztec_utl_removeTransient)] pub(crate) unconstrained fn remove_oracle(slot: Field, index: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/transient/mod.nr b/noir-projects/aztec-nr/aztec/src/transient/mod.nr index d143b0c294ed..569f0731e252 100644 --- a/noir-projects/aztec-nr/aztec/src/transient/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/transient/mod.nr @@ -1,6 +1,6 @@ -use crate::oracle::transient; +use crate::oracle::transient_oracles; use crate::protocol::traits::{Deserialize, Serialize}; -use crate::protocol::utils::{reader::Reader, writer::Writer}; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; /// A dynamically sized array that lives for the duration of a single top-level PXE call. /// @@ -29,424 +29,113 @@ use crate::protocol::utils::{reader::Reader, writer::Writer}; /// Use this to pass not-to-be-persisted data between a contract's own frames (private or utility) within one top-level /// PXE call. For data confined to a single call frame, prefer [`EphemeralArray`](crate::ephemeral::EphemeralArray). /// For data that must persist indefinitely, use [`CapsuleArray`](crate::capsules::CapsuleArray). -pub struct TransientArray { - pub slot: Field, +pub type TransientArray = UnconstrainedArray; + +/// A single value that lives for the duration of a single top-level PXE call. +/// +/// Shared across all call frames of the same contract (private and utility alike) within one top-level PXE call. +/// A contract can only ever access its own transient values; other contracts called within the same top-level PXE call +/// cannot see them. +/// +/// Values share the slot space with [`TransientArray`]s: a value and an array at the same slot alias each other, so +/// use distinct slots. +pub struct TransientValue { + slot: Field, } -impl TransientArray { - /// Returns a handle to a transient array at the given slot, which may already contain data pushed by an earlier - /// frame of the same contract in this top-level PXE call. +impl TransientValue { + /// Returns a handle to the value at the given slot, which may already hold data stored by an earlier frame. pub unconstrained fn at(slot: Field) -> Self { Self { slot } } - /// Returns the number of elements stored in the array. - pub unconstrained fn len(self) -> u32 { - transient::len_oracle(self.slot) - } - - /// Stores a value at the end of the array. - pub unconstrained fn push(self, value: T) + /// Stores a value at the given slot. + pub unconstrained fn store(self, value: T) where T: Serialize, { - let serialized = value.serialize(); - let _ = transient::push_oracle(self.slot, serialized); + transient_oracles::clear_oracle(self.slot); + let _ = transient_oracles::push_oracle(self.slot, value.serialize()); } - /// Removes and returns the last element. Panics if the array is empty. - pub unconstrained fn pop(self) -> T + /// Returns the value previously stored at the slot with [`TransientValue::store`], or `Option::none()` if the + /// slot holds no value. + pub unconstrained fn read(self) -> Option where T: Deserialize, { - let serialized = transient::pop_oracle(self.slot); - Deserialize::deserialize(serialized) + if transient_oracles::len_oracle(self.slot) == 0 { + Option::none() + } else { + let serialized = transient_oracles::get_oracle(self.slot, 0); + Option::some(Deserialize::deserialize(serialized)) + } } - /// Retrieves the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn get(self, index: u32) -> T - where - T: Deserialize, - { - let serialized = transient::get_oracle(self.slot, index); - Deserialize::deserialize(serialized) + /// Deletes the stored value. Does nothing if the slot holds no value. + pub unconstrained fn delete(self) { + transient_oracles::clear_oracle(self.slot) } +} - /// Overwrites the value stored at `index`. Panics if the index is out of bounds. - pub unconstrained fn set(self, index: u32, value: T) - where - T: Serialize, - { - let serialized = value.serialize(); - transient::set_oracle(self.slot, index, serialized); - } +pub struct TransientOracles {} - /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. - pub unconstrained fn remove(self, index: u32) { - transient::remove_oracle(self.slot, index); +impl ArrayOracles for TransientOracles { + unconstrained fn len_oracle(slot: Field) -> u32 { + transient_oracles::len_oracle(slot) } - /// Removes all elements from the array and returns self for chaining (e.g. `TransientArray::at(slot).clear()` - /// to get a guaranteed-empty array at a given slot). - pub unconstrained fn clear(self) -> Self { - transient::clear_oracle(self.slot); - self + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 { + transient_oracles::push_oracle(slot, values) } - /// Calls a function on each element of the array. - /// - /// Iteration proceeds backwards so it is safe to remove the current element (and only the current element) inside - /// the callback. It is **not** safe to push new elements from inside the callback. - pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) - where - T: Deserialize, - { - let mut i = self.len(); - while i > 0 { - i -= 1; - f(i, self.get(i)); - } + unconstrained fn pop_oracle(slot: Field) -> [Field; N] { + transient_oracles::pop_oracle(slot) } -} -/// Serializes a `TransientArray` as its slot identifier, allowing oracle function signatures to use `TransientArray` -/// instead of opaque `Field` slots. -impl Serialize for TransientArray { - let N: u32 = 1; - - fn serialize(self) -> [Field; Self::N] { - [self.slot] + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] { + transient_oracles::get_oracle(slot, index) } - fn stream_serialize(self, writer: &mut Writer) { - writer.write(self.slot); + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) { + transient_oracles::set_oracle(slot, index, values) } -} -/// Deserializes a single Field into a `TransientArray` handle, treating the field value as the slot identifier. -/// This is the inverse of [`Serialize`]. -impl Deserialize for TransientArray { - let N: u32 = 1; - - fn deserialize(fields: [Field; Self::N]) -> Self { - Self { slot: fields[0] } - } - - fn stream_deserialize(reader: &mut Reader) -> Self { - Self { slot: reader.read() } + unconstrained fn remove_oracle(slot: Field, index: u32) { + transient_oracles::remove_oracle(slot, index) } -} - -/// Stores a single value at `slot`, overwriting any value previously stored there. -pub unconstrained fn store(slot: Field, value: T) -where - T: Serialize, -{ - let array: TransientArray = TransientArray::at(slot); - array.clear().push(value); -} -/// Returns the value previously stored at `slot` with [`store`], or `Option::none()` if the slot holds no value. -pub unconstrained fn load(slot: Field) -> Option -where - T: Deserialize, -{ - let array: TransientArray = TransientArray::at(slot); - if array.len() == 0 { - Option::none() - } else { - Option::some(array.get(0)) + unconstrained fn clear_oracle(slot: Field) { + transient_oracles::clear_oracle(slot) } } -/// Deletes the value stored at `slot`. Does nothing if the slot is already empty. -pub unconstrained fn delete(slot: Field) { - transient::clear_oracle(slot); -} +#[crate::unconstrained_array::test_suite::unconstrained_array_tests(quote { crate::transient::TransientOracles })] +mod test {} -mod test { +mod value_test { use crate::test::helpers::test_environment::TestEnvironment; use crate::test::mocks::MockStruct; - use super::{delete, load, store, TransientArray}; - - global SLOT: Field = 1230; - global OTHER_SLOT: Field = 5670; - - #[test] - unconstrained fn empty_array() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - assert_eq(array.len(), 0); - }); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn empty_array_read() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - let _: Field = array.get(0); - }); - } - - #[test(should_fail_with = "is empty")] - unconstrained fn empty_array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - let _: Field = array.pop(); - }); - } - - #[test] - unconstrained fn array_push() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); - } - - #[test(should_fail_with = "out of bounds")] - unconstrained fn read_past_len() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - - let _ = array.get(1); - }); - } - - #[test] - unconstrained fn set_and_remove() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(7); - array.push(8); - array.push(9); - array.set(0, 99); - assert_eq(array.get(0), 99); - array.remove(1); - assert_eq(array.len(), 2); - assert_eq(array.get(0), 99); - assert_eq(array.get(1), 9); - }); - } - - #[test] - unconstrained fn array_pop() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - array.push(10); - - let popped: Field = array.pop(); - assert_eq(popped, 10); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 5); - }); - } - - #[test] - unconstrained fn array_remove_last() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - array.push(5); - array.remove(0); - assert_eq(array.len(), 0); - }); - } - - #[test] - unconstrained fn array_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - assert_eq(array.len(), 3); - - array.remove(1); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 7); - assert_eq(array.get(1), 9); - }); - } - - #[test] - unconstrained fn array_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(7); - array.push(8); - array.push(9); - - array.remove(1); - array.remove(1); - array.remove(0); - - assert_eq(array.len(), 0); - }); - } - - #[test] - unconstrained fn for_each_called_with_all_elements() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); - array.for_each(|index, value| { called_with.push((index, value)); }); - - assert_eq(called_with.len(), 3); - assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); - assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); - assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); - }); - } - - #[test] - unconstrained fn for_each_remove_some() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { - if index == 1 { - array.remove(index); - } - }); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), 4); - assert_eq(array.get(1), 6); - }); - } - - #[test] - unconstrained fn for_each_remove_all() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array = TransientArray::at(SLOT); - - array.push(4); - array.push(5); - array.push(6); - - array.for_each(|index, _| { array.remove(index); }); + use super::{TransientArray, TransientValue}; - assert_eq(array.len(), 0); - }); - } + global SLOT: Field = 9870; #[test] - unconstrained fn different_slots_are_isolated() { + unconstrained fn read_empty_returns_none() { let env = TestEnvironment::new(); env.utility_context(|_| { - let array_a = TransientArray::at(SLOT); - let array_b = TransientArray::at(OTHER_SLOT); - - array_a.push(10); - array_a.push(20); - array_b.push(99); - - assert_eq(array_a.len(), 2); - assert_eq(array_a.get(0), 10); - assert_eq(array_a.get(1), 20); - - assert_eq(array_b.len(), 1); - assert_eq(array_b.get(0), 99); + let value: TransientValue = TransientValue::at(SLOT); + assert_eq(value.read(), Option::none()); }); } #[test] - unconstrained fn works_with_multi_field_type() { + unconstrained fn store_and_read_roundtrips() { let env = TestEnvironment::new(); env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - - let a = MockStruct::new(5, 6); - let b = MockStruct::new(7, 8); - array.push(a); - array.push(b); - - assert_eq(array.len(), 2); - assert_eq(array.get(0), a); - assert_eq(array.get(1), b); - - let popped: MockStruct = array.pop(); - assert_eq(popped, b); - assert_eq(array.len(), 1); - }); - } - - #[test] - unconstrained fn clear_returns_self() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT).clear(); - assert_eq(array.len(), 0); - - array.push(42); - assert_eq(array.len(), 1); - assert_eq(array.get(0), 42); - }); - } - - #[test] - unconstrained fn clear_wipes_previous_data() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let array: TransientArray = TransientArray::at(SLOT); - array.push(1); - array.push(2); - array.push(3); - assert_eq(array.len(), 3); - - let fresh: TransientArray = TransientArray::at(SLOT).clear(); - assert_eq(fresh.len(), 0); - fresh.push(4); - assert_eq(fresh.get(0), 4); - }); - } - - #[test] - unconstrained fn store_and_load() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - store(SLOT, 42); - assert_eq(load(SLOT), Option::some(42)); - }); - } - - #[test] - unconstrained fn load_empty_returns_none() { - let env = TestEnvironment::new(); - env.utility_context(|_| { - let value: Option = load(SLOT); - assert_eq(value, Option::none()); + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + assert_eq(value.read(), Option::some(42)); }); } @@ -454,9 +143,10 @@ mod test { unconstrained fn store_overwrites() { let env = TestEnvironment::new(); env.utility_context(|_| { - store(SLOT, 1); - store(SLOT, 2); - assert_eq(load(SLOT), Option::some(2)); + let value: TransientValue = TransientValue::at(SLOT); + value.store(1); + value.store(2); + assert_eq(value.read(), Option::some(2)); }); } @@ -464,10 +154,10 @@ mod test { unconstrained fn delete_removes_value() { let env = TestEnvironment::new(); env.utility_context(|_| { - store(SLOT, 42); - delete(SLOT); - let value: Option = load(SLOT); - assert_eq(value, Option::none()); + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + value.delete(); + assert_eq(value.read(), Option::none()); }); } @@ -475,19 +165,20 @@ mod test { unconstrained fn delete_empty_is_noop() { let env = TestEnvironment::new(); env.utility_context(|_| { - delete(SLOT); - let value: Option = load(SLOT); - assert_eq(value, Option::none()); + let value: TransientValue = TransientValue::at(SLOT); + value.delete(); + assert_eq(value.read(), Option::none()); }); } #[test] - unconstrained fn store_and_load_multi_field_type() { + unconstrained fn works_with_multi_field_type() { let env = TestEnvironment::new(); env.utility_context(|_| { - let value = MockStruct::new(5, 6); - store(SLOT, value); - assert_eq(load(SLOT), Option::some(value)); + let value: TransientValue = TransientValue::at(SLOT); + let stored = MockStruct::new(5, 6); + value.store(stored); + assert_eq(value.read(), Option::some(stored)); }); } @@ -495,7 +186,9 @@ mod test { unconstrained fn stored_value_is_visible_as_a_length_one_array() { let env = TestEnvironment::new(); env.utility_context(|_| { - store(SLOT, 42); + let value: TransientValue = TransientValue::at(SLOT); + value.store(42); + let array: TransientArray = TransientArray::at(SLOT); assert_eq(array.len(), 1); assert_eq(array.get(0), 42); diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr new file mode 100644 index 000000000000..139d80bb42da --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr @@ -0,0 +1,212 @@ +pub(crate) mod test_helpers; +pub(crate) mod test_suite; + +use crate::oracle::random::random; +use crate::protocol::traits::{Deserialize, Serialize}; + +/// Oracle backend for an [`UnconstrainedArray`]: the set of PXE-side operations that implement its storage. +/// +/// Each implementor routes these operations to a distinct family of oracles, and the oracle family determines the +/// array's lifetime and visibility (e.g. [`EphemeralArray`](crate::ephemeral::EphemeralArray) arrays live for one +/// contract call frame, while [`TransientArray`](crate::transient::TransientArray) arrays are shared across all frames +/// of the same contract within one top-level PXE call). +pub(crate) trait ArrayOracles { + /// Returns the number of elements in the array at `slot`. + unconstrained fn len_oracle(slot: Field) -> u32; + + /// Appends a serialized element to the array at `slot` and returns the new length. + unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32; + + /// Removes and returns the last serialized element of the array at `slot`. Implementors must panic if the + /// array is empty. + unconstrained fn pop_oracle(slot: Field) -> [Field; N]; + + /// Returns the serialized element at the given index of the array at `slot`. Implementors must panic if `index` + /// is out of bounds. + unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N]; + + /// Overwrites the serialized element at the given index of the array at `slot`. Implementors must panic if + /// `index` is out of bounds. + unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]); + + /// Removes the element at the given index of the array at `slot`, shifting subsequent elements backward. + /// Implementors must panic if `index` is out of bounds. + unconstrained fn remove_oracle(slot: Field, index: u32); + + /// Removes all elements from the array at `slot`. + unconstrained fn clear_oracle(slot: Field); +} + +/// A dynamically sized array backed by PXE-side in-memory storage via an [`ArrayOracles`] backend. +/// +/// Arrays are identified by a slot, and each logical operation (push, pop, get, etc.) is a single oracle call. The +/// `Oracle` backend determines the array's lifetime and visibility; contracts should not use this type directly but +/// rather one of its aliases: [`EphemeralArray`](crate::ephemeral::EphemeralArray) (scoped to a single contract call +/// frame) or [`TransientArray`](crate::transient::TransientArray) (shared across all frames of the same contract +/// within one top-level PXE call). +pub struct UnconstrainedArray { + pub(crate) slot: Field, +} + +impl UnconstrainedArray +where + Oracle: ArrayOracles, +{ + /// Returns a handle to the array at the given slot, which may already contain data (e.g. populated by an oracle + /// or by an earlier frame, depending on the backend's visibility). + pub unconstrained fn at(slot: Field) -> Self { + Self { slot } + } + + /// Returns an empty array at the given slot, clearing any pre-existing data. + /// + /// For backends whose arrays are visible beyond a single call frame (e.g. transient arrays), this wipes data + /// other frames of the same contract may have written at the slot. + pub unconstrained fn empty_at(slot: Field) -> Self { + Self::at(slot).clear() + } + + /// Returns an empty array at a fresh, randomly allocated slot. + /// + /// Use this when the caller does not need a specific slot: the random slot is isolated from every other array of + /// the same backend with overwhelming probability. Prefer [`UnconstrainedArray::empty_at`] when the slot must be a + /// known value (e.g. one shared with an oracle or another call frame). + pub unconstrained fn empty() -> Self { + Self::at(random()) + } + + /// Returns the number of elements stored in the array. + pub unconstrained fn len(self) -> u32 { + Oracle::len_oracle(self.slot) + } + + /// Stores a value at the end of the array. + pub unconstrained fn push(self, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + let _ = Oracle::push_oracle(self.slot, serialized); + } + + /// Removes and returns the last element. Implementors are required to panic if the array is empty. + pub unconstrained fn pop(self) -> T + where + T: Deserialize, + { + let serialized = Oracle::pop_oracle(self.slot); + Deserialize::deserialize(serialized) + } + + /// Retrieves the value stored at `index`. Implementors are required to panic if the index is out of bounds. + pub unconstrained fn get(self, index: u32) -> T + where + T: Deserialize, + { + let serialized = Oracle::get_oracle(self.slot, index); + Deserialize::deserialize(serialized) + } + + /// Overwrites the value stored at `index`. Implementors are required to panic if the index is out of bounds. + pub unconstrained fn set(self, index: u32, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + Oracle::set_oracle(self.slot, index, serialized); + } + + /// Removes the element at `index`, shifting subsequent elements backward. Implementors are required to panic if + /// the index is out of bounds. + pub unconstrained fn remove(self, index: u32) { + Oracle::remove_oracle(self.slot, index); + } + + /// Removes all elements from the array and returns self for chaining. + pub unconstrained fn clear(self) -> Self { + Oracle::clear_oracle(self.slot); + self + } + + /// Calls a function on each element of the array. + /// + /// The function `f` is called once with each array value and its corresponding index. Iteration proceeds + /// backwards so that it is safe to remove the current element (and only the current element) inside the + /// callback. + /// + /// It is **not** safe to push new elements from inside the callback. + pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) + where + T: Deserialize, + { + let mut i = self.len(); + while i > 0 { + i -= 1; + f(i, self.get(i)); + } + } + + /// Applies `f` to every element and collects the results into a fresh array. + pub unconstrained fn map(self, f: unconstrained fn[Env](T) -> U) -> UnconstrainedArray + where + T: Deserialize, + U: Serialize, + { + let dest: UnconstrainedArray = UnconstrainedArray::empty(); + let n = self.len(); + for i in 0..n { + dest.push(f(self.get(i))); + } + dest + } + + /// Collects every element satisfying the predicate `f` into a fresh array. + pub unconstrained fn filter(self, f: unconstrained fn[Env](T) -> bool) -> Self + where + T: Serialize + Deserialize, + { + let dest: Self = UnconstrainedArray::empty(); + let n = self.len(); + for i in 0..n { + let value = self.get(i); + if f(value) { + dest.push(value); + } + } + dest + } + + /// Returns `true` if at least one element satisfies the predicate `f`. + pub unconstrained fn any(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() != 0 + } + + /// Returns `true` if every element satisfies the predicate `f` (vacuously `true` for an empty array). + pub unconstrained fn all(self, f: unconstrained fn[Env](T) -> bool) -> bool + where + T: Serialize + Deserialize, + { + self.filter(f).len() == self.len() + } + + /// Returns the first element satisfying the predicate `f`, or `Option::none` if none do. + pub unconstrained fn find(self, f: unconstrained fn[Env](T) -> bool) -> Option + where + T: Deserialize, + { + let n = self.len(); + let mut result: Option = Option::none(); + let mut i = 0; + while (i < n) & result.is_none() { + let value = self.get(i); + if f(value) { + result = Option::some(value); + } + i += 1; + } + result + } +} diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr new file mode 100644 index 000000000000..cc7c96e753fd --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_helpers.nr @@ -0,0 +1,646 @@ +//! Shared test suite for [`UnconstrainedArray`] backends. +//! +//! Every function in this module must be an unconstrained test helper, generic over the [`ArrayOracles`] backend. +//! The [`unconstrained_array_tests`](crate::unconstrained_array::test_suite::unconstrained_array_tests) macro turns +//! each helper into a `#[test]` for every backend. +//! +//! To add a test, write a new unconstrained helper here. If it is expected to fail, mark it with +//! [`should_fail_test`](crate::unconstrained_array::test_suite::should_fail_test) and the expected message; otherwise +//! the generated test expects it to pass. + +use crate::test::helpers::test_environment::TestEnvironment; +use crate::test::mocks::MockStruct; +use crate::unconstrained_array::{ArrayOracles, UnconstrainedArray}; + +pub(crate) global SLOT: Field = 1230; +pub(crate) global OTHER_SLOT: Field = 5670; + +pub(crate) unconstrained fn empty_array() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + assert_eq(array.len(), 0); + }); +} + +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "out of bounds" })] +pub(crate) unconstrained fn should_fail_empty_array_read() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let _: Field = array.get(0); + }); +} + +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "is empty" })] +pub(crate) unconstrained fn should_fail_empty_array_pop() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let _: Field = array.pop(); + }); +} + +pub(crate) unconstrained fn array_push() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); +} + +#[crate::unconstrained_array::test_suite::should_fail_test(quote { "out of bounds" })] +pub(crate) unconstrained fn should_fail_read_past_len() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(5); + + let _ = array.get(1); + }); +} + +pub(crate) unconstrained fn array_pop() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(5); + array.push(10); + + let popped: Field = array.pop(); + assert_eq(popped, 10); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); +} + +pub(crate) unconstrained fn array_set() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(5); + array.set(0, 99); + assert_eq(array.get(0), 99); + }); +} + +pub(crate) unconstrained fn array_remove_last() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(5); + array.remove(0); + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn array_remove_some() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + }); +} + +pub(crate) unconstrained fn array_remove_all() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn for_each_called_with_all_elements() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); + array.for_each(|index, value| { called_with.push((index, value)); }); + + assert_eq(called_with.len(), 3); + assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); + assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); + assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); + }); +} + +pub(crate) unconstrained fn for_each_remove_some() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { + if index == 1 { + array.remove(index); + } + }); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 4); + assert_eq(array.get(1), 6); + }); +} + +pub(crate) unconstrained fn for_each_remove_all() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { array.remove(index); }); + + assert_eq(array.len(), 0); + }); +} + +pub(crate) unconstrained fn different_slots_are_isolated() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array_a: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let array_b: UnconstrainedArray = UnconstrainedArray::at(OTHER_SLOT); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + }); +} + +pub(crate) unconstrained fn works_with_multi_field_type() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + + let a = MockStruct::new(5, 6); + let b = MockStruct::new(7, 8); + array.push(a); + array.push(b); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), a); + assert_eq(array.get(1), b); + + let popped: MockStruct = array.pop(); + assert_eq(popped, b); + assert_eq(array.len(), 1); + }); +} + +pub(crate) unconstrained fn clear_returns_self() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT).clear(); + assert_eq(array.len(), 0); + + array.push(42); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 42); + }); +} + +pub(crate) unconstrained fn clear_wipes_previous_data() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + assert_eq(array.len(), 3); + + // Clear the same slot, previous data should be gone. + let fresh: UnconstrainedArray = UnconstrainedArray::at(SLOT).clear(); + assert_eq(fresh.len(), 0); + fresh.push(4); + assert_eq(fresh.get(0), 4); + }); +} + +pub(crate) unconstrained fn empty_allocates_distinct_slots() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let a: UnconstrainedArray = UnconstrainedArray::empty(); + let b: UnconstrainedArray = UnconstrainedArray::empty(); + + assert(a.slot != b.slot, "empty() should allocate a fresh slot each time"); + + a.push(1); + b.push(2); + assert_eq(a.len(), 1); + assert_eq(a.get(0), 1); + assert_eq(b.len(), 1); + assert_eq(b.get(0), 2); + }); +} + +pub(crate) unconstrained fn map_transforms_each_element() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let doubled: UnconstrainedArray = source.map(|x| x * 2); + + assert_eq(doubled.len(), 3); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(1), 4); + assert_eq(doubled.get(2), 6); + + // The source array is left untouched. + assert_eq(source.len(), 3); + assert_eq(source.get(0), 1); + }); +} + +pub(crate) unconstrained fn map_empty_source_gives_empty_dest() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let mapped: UnconstrainedArray = source.map(|x| x * 2); + assert_eq(mapped.len(), 0); + }); +} + +pub(crate) unconstrained fn map_to_different_type() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(5); + source.push(7); + + let structs: UnconstrainedArray = source.map(|x| MockStruct::new(x, x + 1)); + + assert_eq(structs.len(), 2); + assert_eq(structs.get(0), MockStruct::new(5, 6)); + assert_eq(structs.get(1), MockStruct::new(7, 8)); + }); +} + +pub(crate) unconstrained fn map_results_are_isolated() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + // Each map allocates its own fresh slot, so two maps of the same source do not clobber each other. + let doubled: UnconstrainedArray = source.map(|x| x * 2); + let tripled: UnconstrainedArray = source.map(|x| x * 3); + + assert(doubled.slot != tripled.slot, "each map should land in a distinct slot"); + assert_eq(doubled.get(0), 2); + assert_eq(doubled.get(2), 6); + assert_eq(tripled.get(0), 3); + assert_eq(tripled.get(2), 9); + }); +} + +pub(crate) unconstrained fn filter_keeps_matching_elements_in_order() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(2); + source.push(5); + + let kept: UnconstrainedArray = source.filter(|x| x != 2); + + assert_eq(kept.len(), 3); + assert_eq(kept.get(0), 1); + assert_eq(kept.get(1), 3); + assert_eq(kept.get(2), 5); + + // The source array is left untouched. + assert_eq(source.len(), 5); + assert_eq(source.get(1), 2); + }); +} + +pub(crate) unconstrained fn filter_empty_source_gives_empty_dest() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + let kept: UnconstrainedArray = source.filter(|_| true); + assert_eq(kept.len(), 0); + }); +} + +pub(crate) unconstrained fn filter_none_match_gives_empty_dest() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + + let kept: UnconstrainedArray = source.filter(|_| false); + assert_eq(kept.len(), 0); + }); +} + +pub(crate) unconstrained fn filter_works_with_multi_field_type() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(MockStruct::new(1, 10)); + source.push(MockStruct::new(2, 20)); + source.push(MockStruct::new(3, 30)); + + let kept: UnconstrainedArray = source.filter(|s: MockStruct| s.a != 2); + + assert_eq(kept.len(), 2); + assert_eq(kept.get(0), MockStruct::new(1, 10)); + assert_eq(kept.get(1), MockStruct::new(3, 30)); + }); +} + +pub(crate) unconstrained fn filter_results_are_isolated() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let source: UnconstrainedArray = UnconstrainedArray::at(SLOT); + source.push(1); + source.push(2); + source.push(3); + source.push(4); + + // Each filter allocates its own fresh slot, so two filters of the same source do not clobber each other. + let odds: UnconstrainedArray = source.filter(|x| (x != 2) & (x != 4)); + let evens: UnconstrainedArray = source.filter(|x| (x != 1) & (x != 3)); + + assert(odds.slot != evens.slot, "each filter should land in a distinct slot"); + assert_eq(odds.len(), 2); + assert_eq(odds.get(0), 1); + assert_eq(odds.get(1), 3); + assert_eq(evens.len(), 2); + assert_eq(evens.get(0), 2); + assert_eq(evens.get(1), 4); + }); +} + +pub(crate) unconstrained fn any_is_true_when_an_element_matches() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.any(|x| x == 2)); + }); +} + +pub(crate) unconstrained fn any_is_false_when_no_element_matches() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.any(|x| x == 9)); + }); +} + +pub(crate) unconstrained fn any_on_empty_array_is_false() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + assert(!array.any(|_| true)); + }); +} + +pub(crate) unconstrained fn all_is_true_when_every_element_matches() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.all(|x| x != 0)); + }); +} + +pub(crate) unconstrained fn all_is_false_when_one_element_fails() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(!array.all(|x| x != 2)); + }); +} + +pub(crate) unconstrained fn all_on_empty_array_is_true() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + assert(array.all(|_| false)); + }); +} + +pub(crate) unconstrained fn find_returns_first_matching_element() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + array.push(4); + + // Both 2 and 4 match; find must return the first one in order. + let found = array.find(|x| (x == 2) | (x == 4)); + assert(found.is_some()); + assert_eq(found.unwrap(), 2); + }); +} + +pub(crate) unconstrained fn find_returns_none_when_no_element_matches() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + + assert(array.find(|x| x == 9).is_none()); + }); +} + +pub(crate) unconstrained fn find_on_empty_array_is_none() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + assert(array.find(|_| true).is_none()); + }); +} + +pub(crate) unconstrained fn empty_at_wipes_previous_data() +where + Oracle: ArrayOracles, +{ + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: UnconstrainedArray = UnconstrainedArray::at(SLOT); + array.push(1); + assert_eq(array.len(), 1); + + let fresh: UnconstrainedArray = UnconstrainedArray::empty_at(SLOT); + assert_eq(fresh.len(), 0); + }); +} diff --git a/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr new file mode 100644 index 000000000000..b4f1eea93f87 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/unconstrained_array/test_suite.nr @@ -0,0 +1,46 @@ +//! Generates a shared [`UnconstrainedArray`](crate::unconstrained_array::UnconstrainedArray) +//! test suite from the helpers in [`test_helpers`](crate::unconstrained_array::test_helpers). + +use crate::utils::cmap::CHashMap; + +/// Maps test helpers marked with [`should_fail_test`] to their expected failure message. +pub(crate) comptime mut global EXPECTED_FAILURES: CHashMap = CHashMap::new(); + +/// Marks a test helper as expected to fail with the given message, passed as a quoted string literal: +/// `#[should_fail_test(quote { "out of bounds" })]`. The tests generated from the helper get +/// `#[test(should_fail_with = ...)]` instead of plain `#[test]`. +pub(crate) comptime fn should_fail_test(f: FunctionDefinition, message: Quoted) { + EXPECTED_FAILURES.insert(f, message); +} + +/// Generates a shared [`UnconstrainedArray`](crate::unconstrained_array::UnconstrainedArray) test suite against the +/// given backend. Apply to a test module, passing the backend's oracle as a quoted path: +/// `#[unconstrained_array_tests(quote { crate::ephemeral::EphemeralOracles })]`. +pub(crate) comptime fn unconstrained_array_tests(_m: Module, oracle: Quoted) -> Quoted { + let helpers = quote { crate::unconstrained_array::test_helpers }.as_module().unwrap(); + let mut tests = quote {}; + for f in helpers.functions() { + let name = f.name(); + if f.is_unconstrained() { + let fail = EXPECTED_FAILURES.get(f); + let attr = if fail.is_some() { + let msg = fail.unwrap(); + quote { #[test(should_fail_with = $msg)] } + } else { + quote { #[test] } + }; + tests = quote { + $tests + $attr + unconstrained fn $name() { + crate::unconstrained_array::test_helpers::$name::<$oracle>(); + } + }; + } else { + panic( + f"unexpected non-unconstrained function `{name}` in test_helpers: every test helper must be unconstrained", + ) + } + } + tests +} diff --git a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap index b660c95f3ef1..9075cf02946b 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/compile_failure/invalid_note/snapshots__stderr.snap @@ -15,8 +15,8 @@ error: InvalidNote has a packed length of 9 fields, which exceeds the maximum al at /noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr:: 3: sync_state_with_secrets at /noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr:: - 4: EphemeralArray::for_each - at /noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr:: + 4: UnconstrainedArray::for_each + at /noir-projects/aztec-nr/aztec/src/unconstrained_array/mod.nr:: 5: sync_state_with_secrets at /noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr:: 6: process_message_ciphertext diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap index c44ddd355c2f..da5b739dff4e 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -833,7 +832,7 @@ pub contract AMM { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap index c7f2843131a6..32db6054f5b6 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -332,7 +331,7 @@ contract AvmGadgetsTest { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap index cdfa8a6786b4..b04644838350 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_test_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -1718,7 +1717,7 @@ pub contract AvmTest { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap index 445edf3cc082..88d4e8960623 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -308,7 +307,7 @@ pub contract PublicFnsWithEmitRepro { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap index 0f8dae5d076b..ea4669fed6f3 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -267,7 +266,7 @@ contract StorageProofTest { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap index 61239ae02de8..8a9801c1afcd 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap @@ -2,7 +2,6 @@ source: tests/snapshots.rs expression: stdout --- - use aztec::macros::aztec; use aztec::macros::aztec; @@ -1043,7 +1042,7 @@ pub contract Token { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::ephemeral::EphemeralArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters {