Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 366 additions & 0 deletions noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,366 @@
use crate::oracle::ephemeral;
use crate::protocol::traits::{Deserialize, Serialize};

/// A dynamically sized array that exists only during a single contract call frame.
///
/// Ephemeral arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract
/// call frame gets its own isolated slot space of ephemeral arrays. Child simulations cannot see the parent's
/// ephemeral arrays, and vice versa.
///
/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making ephemeral arrays significantly
/// cheaper than capsule arrays.
///
/// ## Use Cases
///
/// Ephemeral arrays are designed for passing data between PXE (TypeScript) and contracts (Noir) during simulation,
/// for example, note validation requests or event validation responses. This data type is appropriate for data that
/// is not supposed to be persisted.
///
/// For data that needs to persist across simulations, contract calls, etc, use
/// [`CapsuleArray`](crate::capsules::CapsuleArray) instead.
pub struct EphemeralArray<T> {
pub slot: Field,
}

impl<T> EphemeralArray<T> {
/// 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 }
}

/// Returns the number of elements stored in the array.
pub unconstrained fn len(self) -> u32 {
ephemeral::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 _ = ephemeral::push_oracle(self.slot, serialized);
}

/// 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)
}

/// 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)
}

/// 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);
}

/// 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 (e.g. `EphemeralArray::at(slot).clear()`
/// to get a guaranteed-empty array at a given slot).
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<Env>(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));
}
}
}

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<Field> = 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<MockStruct> = 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 clear_returns_self() {
let env = TestEnvironment::new();
env.utility_context(|_| {
let array: EphemeralArray<Field> = 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<Field> = 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<Field> = EphemeralArray::at(SLOT).clear();
assert_eq(fresh.len(), 0);
fresh.push(4);
assert_eq(fresh.get(0), 4);
});
}
}
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod nullifier;
pub mod oracle;
pub mod state_vars;
pub mod capsules;
pub mod ephemeral;
pub mod event;
pub mod messages;
pub use protocol_types as protocol;
Expand Down
Loading
Loading