Skip to content

Commit 07b8a25

Browse files
mverzillibenesjan
authored andcommitted
refactor!: ephemeral arrays (#22162)
Implements a new type of array especially designed to use in oracle interfaces between Aztec.nr and PXE. The memory space of of these arrays is isolated by contract call frame and lives in memory. This makes them faster to work with, but it also removes the need for a lot of boilerplate to instantiate them, use them as params, and ultimately dispose of them. Closes F-136 --------- Co-authored-by: Jan Beneš <janbenes1234@gmail.com>
1 parent 5c55805 commit 07b8a25

38 files changed

Lines changed: 1247 additions & 348 deletions

File tree

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
use crate::oracle::ephemeral;
2+
use crate::protocol::traits::{Deserialize, Serialize};
3+
4+
/// A dynamically sized array that exists only during a single contract call frame.
5+
///
6+
/// Ephemeral arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract
7+
/// call frame gets its own isolated slot space of ephemeral arrays. Child simulations cannot see the parent's
8+
/// ephemeral arrays, and vice versa.
9+
///
10+
/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making ephemeral arrays significantly
11+
/// cheaper than capsule arrays.
12+
///
13+
/// ## Use Cases
14+
///
15+
/// Ephemeral arrays are designed for passing data between PXE (TypeScript) and contracts (Noir) during simulation,
16+
/// for example, note validation requests or event validation responses. This data type is appropriate for data that
17+
/// is not supposed to be persisted.
18+
///
19+
/// For data that needs to persist across simulations, contract calls, etc, use
20+
/// [`CapsuleArray`](crate::capsules::CapsuleArray) instead.
21+
pub struct EphemeralArray<T> {
22+
pub slot: Field,
23+
}
24+
25+
impl<T> EphemeralArray<T> {
26+
/// Returns a handle to an ephemeral array at the given slot, which may already contain data (e.g. populated
27+
/// by an oracle).
28+
pub unconstrained fn at(slot: Field) -> Self {
29+
Self { slot }
30+
}
31+
32+
/// Returns the number of elements stored in the array.
33+
pub unconstrained fn len(self) -> u32 {
34+
ephemeral::len_oracle(self.slot)
35+
}
36+
37+
/// Stores a value at the end of the array.
38+
pub unconstrained fn push(self, value: T)
39+
where
40+
T: Serialize,
41+
{
42+
let serialized = value.serialize();
43+
let _ = ephemeral::push_oracle(self.slot, serialized);
44+
}
45+
46+
/// Removes and returns the last element. Panics if the array is empty.
47+
pub unconstrained fn pop(self) -> T
48+
where
49+
T: Deserialize,
50+
{
51+
let serialized = ephemeral::pop_oracle(self.slot);
52+
Deserialize::deserialize(serialized)
53+
}
54+
55+
/// Retrieves the value stored at `index`. Panics if the index is out of bounds.
56+
pub unconstrained fn get(self, index: u32) -> T
57+
where
58+
T: Deserialize,
59+
{
60+
let serialized = ephemeral::get_oracle(self.slot, index);
61+
Deserialize::deserialize(serialized)
62+
}
63+
64+
/// Overwrites the value stored at `index`. Panics if the index is out of bounds.
65+
pub unconstrained fn set(self, index: u32, value: T)
66+
where
67+
T: Serialize,
68+
{
69+
let serialized = value.serialize();
70+
ephemeral::set_oracle(self.slot, index, serialized);
71+
}
72+
73+
/// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds.
74+
pub unconstrained fn remove(self, index: u32) {
75+
ephemeral::remove_oracle(self.slot, index);
76+
}
77+
78+
/// Removes all elements from the array and returns self for chaining (e.g. `EphemeralArray::at(slot).clear()`
79+
/// to get a guaranteed-empty array at a given slot).
80+
pub unconstrained fn clear(self) -> Self {
81+
ephemeral::clear_oracle(self.slot);
82+
self
83+
}
84+
85+
/// Calls a function on each element of the array.
86+
///
87+
/// The function `f` is called once with each array value and its corresponding index. Iteration proceeds
88+
/// backwards so that it is safe to remove the current element (and only the current element) inside the
89+
/// callback.
90+
///
91+
/// It is **not** safe to push new elements from inside the callback.
92+
pub unconstrained fn for_each<Env>(self, f: unconstrained fn[Env](u32, T) -> ())
93+
where
94+
T: Deserialize,
95+
{
96+
let mut i = self.len();
97+
while i > 0 {
98+
i -= 1;
99+
f(i, self.get(i));
100+
}
101+
}
102+
}
103+
104+
mod test {
105+
use crate::test::helpers::test_environment::TestEnvironment;
106+
use crate::test::mocks::MockStruct;
107+
use super::EphemeralArray;
108+
109+
global SLOT: Field = 1230;
110+
global OTHER_SLOT: Field = 5670;
111+
112+
#[test]
113+
unconstrained fn empty_array() {
114+
let env = TestEnvironment::new();
115+
env.utility_context(|_| {
116+
let array: EphemeralArray<Field> = EphemeralArray::at(SLOT);
117+
assert_eq(array.len(), 0);
118+
});
119+
}
120+
121+
#[test(should_fail_with = "out of bounds")]
122+
unconstrained fn empty_array_read() {
123+
let env = TestEnvironment::new();
124+
env.utility_context(|_| {
125+
let array = EphemeralArray::at(SLOT);
126+
let _: Field = array.get(0);
127+
});
128+
}
129+
130+
#[test(should_fail_with = "is empty")]
131+
unconstrained fn empty_array_pop() {
132+
let env = TestEnvironment::new();
133+
env.utility_context(|_| {
134+
let array = EphemeralArray::at(SLOT);
135+
let _: Field = array.pop();
136+
});
137+
}
138+
139+
#[test]
140+
unconstrained fn array_push() {
141+
let env = TestEnvironment::new();
142+
env.utility_context(|_| {
143+
let array = EphemeralArray::at(SLOT);
144+
array.push(5);
145+
146+
assert_eq(array.len(), 1);
147+
assert_eq(array.get(0), 5);
148+
});
149+
}
150+
151+
#[test(should_fail_with = "out of bounds")]
152+
unconstrained fn read_past_len() {
153+
let env = TestEnvironment::new();
154+
env.utility_context(|_| {
155+
let array = EphemeralArray::at(SLOT);
156+
array.push(5);
157+
158+
let _ = array.get(1);
159+
});
160+
}
161+
162+
#[test]
163+
unconstrained fn array_pop() {
164+
let env = TestEnvironment::new();
165+
env.utility_context(|_| {
166+
let array = EphemeralArray::at(SLOT);
167+
array.push(5);
168+
array.push(10);
169+
170+
let popped: Field = array.pop();
171+
assert_eq(popped, 10);
172+
assert_eq(array.len(), 1);
173+
assert_eq(array.get(0), 5);
174+
});
175+
}
176+
177+
#[test]
178+
unconstrained fn array_set() {
179+
let env = TestEnvironment::new();
180+
env.utility_context(|_| {
181+
let array = EphemeralArray::at(SLOT);
182+
array.push(5);
183+
array.set(0, 99);
184+
assert_eq(array.get(0), 99);
185+
});
186+
}
187+
188+
#[test]
189+
unconstrained fn array_remove_last() {
190+
let env = TestEnvironment::new();
191+
env.utility_context(|_| {
192+
let array = EphemeralArray::at(SLOT);
193+
array.push(5);
194+
array.remove(0);
195+
assert_eq(array.len(), 0);
196+
});
197+
}
198+
199+
#[test]
200+
unconstrained fn array_remove_some() {
201+
let env = TestEnvironment::new();
202+
env.utility_context(|_| {
203+
let array = EphemeralArray::at(SLOT);
204+
205+
array.push(7);
206+
array.push(8);
207+
array.push(9);
208+
209+
assert_eq(array.len(), 3);
210+
211+
array.remove(1);
212+
213+
assert_eq(array.len(), 2);
214+
assert_eq(array.get(0), 7);
215+
assert_eq(array.get(1), 9);
216+
});
217+
}
218+
219+
#[test]
220+
unconstrained fn array_remove_all() {
221+
let env = TestEnvironment::new();
222+
env.utility_context(|_| {
223+
let array = EphemeralArray::at(SLOT);
224+
225+
array.push(7);
226+
array.push(8);
227+
array.push(9);
228+
229+
array.remove(1);
230+
array.remove(1);
231+
array.remove(0);
232+
233+
assert_eq(array.len(), 0);
234+
});
235+
}
236+
237+
#[test]
238+
unconstrained fn for_each_called_with_all_elements() {
239+
let env = TestEnvironment::new();
240+
env.utility_context(|_| {
241+
let array = EphemeralArray::at(SLOT);
242+
243+
array.push(4);
244+
array.push(5);
245+
array.push(6);
246+
247+
let called_with = &mut BoundedVec::<(u32, Field), 3>::new();
248+
array.for_each(|index, value| { called_with.push((index, value)); });
249+
250+
assert_eq(called_with.len(), 3);
251+
assert(called_with.any(|(index, value)| (index == 0) & (value == 4)));
252+
assert(called_with.any(|(index, value)| (index == 1) & (value == 5)));
253+
assert(called_with.any(|(index, value)| (index == 2) & (value == 6)));
254+
});
255+
}
256+
257+
#[test]
258+
unconstrained fn for_each_remove_some() {
259+
let env = TestEnvironment::new();
260+
env.utility_context(|_| {
261+
let array = EphemeralArray::at(SLOT);
262+
263+
array.push(4);
264+
array.push(5);
265+
array.push(6);
266+
267+
array.for_each(|index, _| {
268+
if index == 1 {
269+
array.remove(index);
270+
}
271+
});
272+
273+
assert_eq(array.len(), 2);
274+
assert_eq(array.get(0), 4);
275+
assert_eq(array.get(1), 6);
276+
});
277+
}
278+
279+
#[test]
280+
unconstrained fn for_each_remove_all() {
281+
let env = TestEnvironment::new();
282+
env.utility_context(|_| {
283+
let array = EphemeralArray::at(SLOT);
284+
285+
array.push(4);
286+
array.push(5);
287+
array.push(6);
288+
289+
array.for_each(|index, _| { array.remove(index); });
290+
291+
assert_eq(array.len(), 0);
292+
});
293+
}
294+
295+
#[test]
296+
unconstrained fn different_slots_are_isolated() {
297+
let env = TestEnvironment::new();
298+
env.utility_context(|_| {
299+
let array_a = EphemeralArray::at(SLOT);
300+
let array_b = EphemeralArray::at(OTHER_SLOT);
301+
302+
array_a.push(10);
303+
array_a.push(20);
304+
array_b.push(99);
305+
306+
assert_eq(array_a.len(), 2);
307+
assert_eq(array_a.get(0), 10);
308+
assert_eq(array_a.get(1), 20);
309+
310+
assert_eq(array_b.len(), 1);
311+
assert_eq(array_b.get(0), 99);
312+
});
313+
}
314+
315+
#[test]
316+
unconstrained fn works_with_multi_field_type() {
317+
let env = TestEnvironment::new();
318+
env.utility_context(|_| {
319+
let array: EphemeralArray<MockStruct> = EphemeralArray::at(SLOT);
320+
321+
let a = MockStruct::new(5, 6);
322+
let b = MockStruct::new(7, 8);
323+
array.push(a);
324+
array.push(b);
325+
326+
assert_eq(array.len(), 2);
327+
assert_eq(array.get(0), a);
328+
assert_eq(array.get(1), b);
329+
330+
let popped: MockStruct = array.pop();
331+
assert_eq(popped, b);
332+
assert_eq(array.len(), 1);
333+
});
334+
}
335+
336+
#[test]
337+
unconstrained fn clear_returns_self() {
338+
let env = TestEnvironment::new();
339+
env.utility_context(|_| {
340+
let array: EphemeralArray<Field> = EphemeralArray::at(SLOT).clear();
341+
assert_eq(array.len(), 0);
342+
343+
array.push(42);
344+
assert_eq(array.len(), 1);
345+
assert_eq(array.get(0), 42);
346+
});
347+
}
348+
349+
#[test]
350+
unconstrained fn clear_wipes_previous_data() {
351+
let env = TestEnvironment::new();
352+
env.utility_context(|_| {
353+
let array: EphemeralArray<Field> = EphemeralArray::at(SLOT);
354+
array.push(1);
355+
array.push(2);
356+
array.push(3);
357+
assert_eq(array.len(), 3);
358+
359+
// Clear the same slot, previous data should be gone.
360+
let fresh: EphemeralArray<Field> = EphemeralArray::at(SLOT).clear();
361+
assert_eq(fresh.len(), 0);
362+
fresh.push(4);
363+
assert_eq(fresh.get(0), 4);
364+
});
365+
}
366+
}

noir-projects/aztec-nr/aztec/src/lib.nr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub mod nullifier;
3939
pub mod oracle;
4040
pub mod state_vars;
4141
pub mod capsules;
42+
pub mod ephemeral;
4243
pub mod event;
4344
pub mod messages;
4445
pub use protocol_types as protocol;

0 commit comments

Comments
 (0)