From 2769578fa9db43d543b2c29d274cd1da8c086cac Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 30 Mar 2026 12:44:08 +0000 Subject: [PATCH 01/10] initial exploration --- noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec-nr/aztec/src/oracle/mod.nr | 1 + .../aztec-nr/aztec/src/oracle/volatile.nr | 33 ++ .../aztec-nr/aztec/src/volatile/mod.nr | 316 ++++++++++++++++++ .../oracle/interfaces.ts | 9 + .../oracle/oracle.ts | 50 +++ .../oracle/utility_execution_oracle.ts | 30 ++ .../volatile_array_service.test.ts | 158 +++++++++ .../volatile_array_service.ts | 85 +++++ yarn-project/txe/src/rpc_translator.ts | 64 ++++ 10 files changed, 747 insertions(+) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/volatile.nr create mode 100644 noir-projects/aztec-nr/aztec/src/volatile/mod.nr create mode 100644 yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts create mode 100644 yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 88c9f019bf36..da3f5893faad 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 volatile; pub mod event; pub mod messages; pub use protocol_types as protocol; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 772dcf882ea8..671876486516 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -10,6 +10,7 @@ pub mod auth_witness; pub mod block_header; pub mod call_private_function; pub mod capsules; +pub mod volatile; pub mod contract_sync; pub mod public_call; pub mod tx_phase; diff --git a/noir-projects/aztec-nr/aztec/src/oracle/volatile.nr b/noir-projects/aztec-nr/aztec/src/oracle/volatile.nr new file mode 100644 index 000000000000..36d5ccf87d4a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/volatile.nr @@ -0,0 +1,33 @@ +/// Oracles for volatile arrays: in-memory arrays scoped to a single contract call frame. +/// +/// Unlike capsule oracles, volatile oracles operate on arrays (not individual slots) and each oracle call performs a +/// complete logical operation. This reduces the number of oracle round-trips compared to building array semantics on +/// top of slot-level oracles. + +/// Appends a serialized element to the volatile array and returns the new length. +#[oracle(aztec_utl_volatile_push)] +pub(crate) unconstrained fn push_oracle(base_slot: Field, values: [Field; N]) -> u32 {} + +/// Removes and returns the last serialized element from the volatile array. +#[oracle(aztec_utl_volatile_pop)] +pub(crate) unconstrained fn pop_oracle(base_slot: Field) -> [Field; N] {} + +/// Returns the serialized element at the given index. +#[oracle(aztec_utl_volatile_get)] +pub(crate) unconstrained fn get_oracle(base_slot: Field, index: u32) -> [Field; N] {} + +/// Overwrites the serialized element at the given index. +#[oracle(aztec_utl_volatile_set)] +pub(crate) unconstrained fn set_oracle(base_slot: Field, index: u32, values: [Field; N]) {} + +/// Returns the number of elements in the volatile array. +#[oracle(aztec_utl_volatile_len)] +pub(crate) unconstrained fn len_oracle(base_slot: Field) -> u32 {} + +/// Removes the element at the given index, shifting subsequent elements backward. +#[oracle(aztec_utl_volatile_remove)] +pub(crate) unconstrained fn remove_oracle(base_slot: Field, index: u32) {} + +/// Copies `count` elements from the source array to the destination array (overwrites destination). +#[oracle(aztec_utl_volatile_copy)] +pub(crate) unconstrained fn copy_oracle(src_slot: Field, dst_slot: Field, count: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/volatile/mod.nr b/noir-projects/aztec-nr/aztec/src/volatile/mod.nr new file mode 100644 index 000000000000..6f3da8fc63b0 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/volatile/mod.nr @@ -0,0 +1,316 @@ +use crate::oracle::volatile; +use crate::protocol::traits::{Deserialize, Serialize}; + +/// A dynamically sized array that exists only during a single contract call frame. +/// +/// Volatile arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract +/// call frame gets its own isolated set of volatile arrays child simulations cannot see the parent's volatile arrays, +/// and vice versa. +/// +/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making volatile arrays significantly +/// cheaper than capsule arrays and more appropriate for transient data. +/// +/// ## Use Cases +/// +/// Volatile arrays are designed for transient communication between PXE (TypeScript) and contracts (Noir) during +/// simulation, for example, note validation requests or event validation responses. +/// +/// For data that needs to persist across simulations, contract calls, etc, use +/// [`CapsuleArray`](crate::capsules::CapsuleArray) instead. +pub struct VolatileArray { + base_slot: Field, +} + +impl VolatileArray { + /// Creates a volatile array at the given base slot. + /// + /// Multiple volatile arrays can coexist within the same call frame by using different base slots. + pub unconstrained fn at(base_slot: Field) -> Self { + Self { base_slot } + } + + /// Returns the number of elements stored in the array. + pub unconstrained fn len(self) -> u32 { + volatile::len_oracle(self.base_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 _ = volatile::push_oracle(self.base_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 = volatile::pop_oracle(self.base_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 = volatile::get_oracle(self.base_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(); + volatile::set_oracle(self.base_slot, index, serialized); + } + + /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. + pub unconstrained fn remove(self, index: u32) { + volatile::remove_oracle(self.base_slot, index); + } + + /// 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)); + } + } +} + +mod test { + use crate::test::helpers::test_environment::TestEnvironment; + use super::VolatileArray; + + global SLOT: Field = 1230; + global OTHER_SLOT: Field = 5670; + + #[test] + unconstrained fn empty_array() { + let _ = TestEnvironment::new(); + + let array: VolatileArray = VolatileArray::at(SLOT); + assert_eq(array.len(), 0); + } + + #[test(should_fail)] + unconstrained fn empty_array_read() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + let _: Field = array.get(0); + } + + #[test(should_fail)] + unconstrained fn empty_array_pop() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + let _: Field = array.pop(); + } + + #[test] + unconstrained fn array_push() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + } + + #[test(should_fail)] + unconstrained fn read_past_len() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + array.push(5); + + let _ = array.get(1); + } + + #[test] + unconstrained fn array_pop() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + array.push(5); + array.set(0, 99); + assert_eq(array.get(0), 99); + } + + #[test] + unconstrained fn array_remove_last() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::at(SLOT); + + array.push(5); + array.remove(0); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn array_remove_some() { + let _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array = VolatileArray::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 _ = TestEnvironment::new(); + + let array_a = VolatileArray::at(SLOT); + let array_b = VolatileArray::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 _ = TestEnvironment::new(); + + use crate::test::mocks::MockStruct; + + let array = VolatileArray::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); + } +} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index d9c9eb9c3e58..11154d31291d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -154,6 +154,15 @@ export interface IUtilityExecutionOracle { getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise; setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; + + // Volatile array methods — in-memory per-call-frame arrays for transient data. + volatilePush(baseSlot: Fr, elements: Fr[]): number; + volatilePop(baseSlot: Fr): Fr[]; + volatileGet(baseSlot: Fr, index: number): Fr[]; + volatileSet(baseSlot: Fr, index: number, elements: Fr[]): void; + volatileLen(baseSlot: Fr): number; + volatileRemove(baseSlot: Fr, index: number): void; + volatileCopy(srcSlot: Fr, dstSlot: Fr, count: number): void; } /** diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 172deda40240..9b833fe0f095 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -648,6 +648,56 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + aztec_utl_volatile_push([baseSlot]: ACVMField[], elements: ACVMField[]): Promise { + const newLen = this.handlerAsUtility().volatilePush(Fr.fromString(baseSlot), elements.map(Fr.fromString)); + return Promise.resolve([toACVMField(newLen)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_pop([baseSlot]: ACVMField[]): Promise { + const element = this.handlerAsUtility().volatilePop(Fr.fromString(baseSlot)); + return Promise.resolve([element.map(toACVMField)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_get([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { + const element = this.handlerAsUtility().volatileGet(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); + return Promise.resolve([element.map(toACVMField)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_set([baseSlot]: ACVMField[], [index]: ACVMField[], elements: ACVMField[]): Promise { + this.handlerAsUtility().volatileSet( + Fr.fromString(baseSlot), + Fr.fromString(index).toNumber(), + elements.map(Fr.fromString), + ); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_len([baseSlot]: ACVMField[]): Promise { + const len = this.handlerAsUtility().volatileLen(Fr.fromString(baseSlot)); + return Promise.resolve([toACVMField(len)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_remove([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { + this.handlerAsUtility().volatileRemove(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_copy([srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [count]: ACVMField[]): Promise { + this.handlerAsUtility().volatileCopy( + Fr.fromString(srcSlot), + Fr.fromString(dstSlot), + Fr.fromString(count).toNumber(), + ); + return Promise.resolve([]); + } + // eslint-disable-next-line camelcase async aztec_utl_decryptAes128( ciphertextBVecStorage: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 6b7370186f54..21b257184ef7 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -42,6 +42,7 @@ import { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js' import { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { UtilityContext } from '../noir-structs/utility_context.js'; import { pickNotes } from '../pick_notes.js'; +import { VolatileArrayService } from '../volatile_array_service.js'; import type { IMiscOracle, IUtilityExecutionOracle, NoteData } from './interfaces.js'; import { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; @@ -78,6 +79,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra private contractLogger: Logger | undefined; private aztecnrLogger: Logger | undefined; private offchainEffects: OffchainEffect[] = []; + private readonly volatileArrayService = new VolatileArrayService(); protected readonly contractAddress: AztecAddress; protected readonly authWitnesses: AuthWitness[]; @@ -785,6 +787,34 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } + public volatilePush(baseSlot: Fr, elements: Fr[]): number { + return this.volatileArrayService.push(baseSlot, elements); + } + + public volatilePop(baseSlot: Fr): Fr[] { + return this.volatileArrayService.pop(baseSlot); + } + + public volatileGet(baseSlot: Fr, index: number): Fr[] { + return this.volatileArrayService.get(baseSlot, index); + } + + public volatileSet(baseSlot: Fr, index: number, elements: Fr[]): void { + this.volatileArrayService.set(baseSlot, index, elements); + } + + public volatileLen(baseSlot: Fr): number { + return this.volatileArrayService.len(baseSlot); + } + + public volatileRemove(baseSlot: Fr, index: number): void { + this.volatileArrayService.remove(baseSlot, index); + } + + public volatileCopy(srcSlot: Fr, dstSlot: Fr, count: number): void { + this.volatileArrayService.copy(srcSlot, dstSlot, count); + } + public emitOffchainEffect(data: Fr[]): Promise { this.offchainEffects.push({ data, contractAddress: this.contractAddress }); return Promise.resolve(); diff --git a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts new file mode 100644 index 000000000000..192509af2ad8 --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts @@ -0,0 +1,158 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + +import { VolatileArrayService } from './volatile_array_service.js'; + +describe('VolatileArrayService', () => { + let service: VolatileArrayService; + const slot = Fr.fromString('0x01'); + const otherSlot = Fr.fromString('0x02'); + + beforeEach(() => { + service = new VolatileArrayService(); + }); + + describe('len', () => { + it('returns 0 for uninitialized array', () => { + expect(service.len(slot)).toBe(0); + }); + }); + + describe('push', () => { + it('appends element and returns new length', () => { + const newLen = service.push(slot, [new Fr(5), new Fr(6)]); + expect(newLen).toBe(1); + expect(service.len(slot)).toBe(1); + }); + + it('appends multiple elements sequentially', () => { + service.push(slot, [new Fr(5)]); + service.push(slot, [new Fr(6)]); + expect(service.len(slot)).toBe(2); + }); + }); + + describe('get', () => { + it('retrieves pushed element', () => { + service.push(slot, [new Fr(5), new Fr(6)]); + const result = service.get(slot, 0); + expect(result).toEqual([new Fr(5), new Fr(6)]); + }); + + it('retrieves elements at different indices', () => { + service.push(slot, [new Fr(1)]); + service.push(slot, [new Fr(2)]); + service.push(slot, [new Fr(3)]); + expect(service.get(slot, 0)).toEqual([new Fr(1)]); + expect(service.get(slot, 1)).toEqual([new Fr(2)]); + expect(service.get(slot, 2)).toEqual([new Fr(3)]); + }); + + it('throws on out of bounds index', () => { + expect(() => service.get(slot, 0)).toThrow('out of bounds'); + }); + + it('throws on index equal to length', () => { + service.push(slot, [new Fr(1)]); + expect(() => service.get(slot, 1)).toThrow('out of bounds'); + }); + }); + + describe('set', () => { + it('overwrites element at index', () => { + service.push(slot, [new Fr(1)]); + service.set(slot, 0, [new Fr(99)]); + expect(service.get(slot, 0)).toEqual([new Fr(99)]); + }); + + it('throws on out of bounds index', () => { + expect(() => service.set(slot, 0, [new Fr(1)])).toThrow('out of bounds'); + }); + }); + + describe('pop', () => { + it('removes and returns last element', () => { + service.push(slot, [new Fr(1)]); + service.push(slot, [new Fr(2)]); + const popped = service.pop(slot); + expect(popped).toEqual([new Fr(2)]); + expect(service.len(slot)).toBe(1); + }); + + it('throws on empty array', () => { + expect(() => service.pop(slot)).toThrow('empty'); + }); + }); + + describe('remove', () => { + it('removes last element without shifting', () => { + service.push(slot, [new Fr(1)]); + service.push(slot, [new Fr(2)]); + service.remove(slot, 1); + expect(service.len(slot)).toBe(1); + expect(service.get(slot, 0)).toEqual([new Fr(1)]); + }); + + it('removes middle element and shifts remaining', () => { + service.push(slot, [new Fr(7)]); + service.push(slot, [new Fr(8)]); + service.push(slot, [new Fr(9)]); + service.remove(slot, 1); + expect(service.len(slot)).toBe(2); + expect(service.get(slot, 0)).toEqual([new Fr(7)]); + expect(service.get(slot, 1)).toEqual([new Fr(9)]); + }); + + it('removes first element and shifts all', () => { + service.push(slot, [new Fr(7)]); + service.push(slot, [new Fr(8)]); + service.push(slot, [new Fr(9)]); + service.remove(slot, 0); + expect(service.len(slot)).toBe(2); + expect(service.get(slot, 0)).toEqual([new Fr(8)]); + expect(service.get(slot, 1)).toEqual([new Fr(9)]); + }); + + it('throws on out of bounds index', () => { + expect(() => service.remove(slot, 0)).toThrow('out of bounds'); + }); + }); + + describe('copy', () => { + it('copies elements to a different slot', () => { + service.push(slot, [new Fr(1)]); + service.push(slot, [new Fr(2)]); + service.push(slot, [new Fr(3)]); + service.copy(slot, otherSlot, 3); + expect(service.len(otherSlot)).toBe(3); + expect(service.get(otherSlot, 0)).toEqual([new Fr(1)]); + expect(service.get(otherSlot, 1)).toEqual([new Fr(2)]); + expect(service.get(otherSlot, 2)).toEqual([new Fr(3)]); + }); + + it('copies partial elements', () => { + service.push(slot, [new Fr(1)]); + service.push(slot, [new Fr(2)]); + service.push(slot, [new Fr(3)]); + service.copy(slot, otherSlot, 2); + expect(service.len(otherSlot)).toBe(2); + expect(service.get(otherSlot, 0)).toEqual([new Fr(1)]); + expect(service.get(otherSlot, 1)).toEqual([new Fr(2)]); + }); + + it('throws when count exceeds source length', () => { + service.push(slot, [new Fr(1)]); + expect(() => service.copy(slot, otherSlot, 2)).toThrow(); + }); + }); + + describe('slot isolation', () => { + it('different slots are independent', () => { + service.push(slot, [new Fr(10)]); + service.push(otherSlot, [new Fr(20)]); + expect(service.len(slot)).toBe(1); + expect(service.len(otherSlot)).toBe(1); + expect(service.get(slot, 0)).toEqual([new Fr(10)]); + expect(service.get(otherSlot, 0)).toEqual([new Fr(20)]); + }); + }); +}); diff --git a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts new file mode 100644 index 000000000000..00176fbb0d66 --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts @@ -0,0 +1,85 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + +/** In-memory array service for transient data during a single contract call frame. */ +export class VolatileArrayService { + /** Maps base slot to array of elements, where each element is a serialized Fr[]. */ + #arrays: Map = new Map(); + + #getArray(baseSlot: Fr): Fr[][] { + return this.#arrays.get(baseSlot.toString()) ?? []; + } + + #setArray(baseSlot: Fr, array: Fr[][]): void { + this.#arrays.set(baseSlot.toString(), array); + } + + /** Returns the number of elements in the array at the given slot. */ + len(baseSlot: Fr): number { + return this.#getArray(baseSlot).length; + } + + /** Appends an element to the array and returns the new length. */ + push(baseSlot: Fr, elements: Fr[]): number { + const array = this.#getArray(baseSlot); + array.push(elements); + this.#setArray(baseSlot, array); + return array.length; + } + + /** Removes and returns the last element. Throws if empty. */ + pop(baseSlot: Fr): Fr[] { + const array = this.#getArray(baseSlot); + if (array.length === 0) { + throw new Error(`Volatile array at slot ${baseSlot} is empty`); + } + const element = array.pop()!; + this.#setArray(baseSlot, array); + return element; + } + + /** Returns the element at the given index. Throws if out of bounds. */ + get(baseSlot: Fr, index: number): Fr[] { + const array = this.#getArray(baseSlot); + if (index < 0 || index >= array.length) { + throw new Error( + `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + ); + } + return array[index]; + } + + /** Overwrites the element at the given index. Throws if out of bounds. */ + set(baseSlot: Fr, index: number, value: Fr[]): void { + const array = this.#getArray(baseSlot); + if (index < 0 || index >= array.length) { + throw new Error( + `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + ); + } + array[index] = value; + } + + /** Removes the element at the given index, shifting subsequent elements backward. Throws if out of bounds. */ + remove(baseSlot: Fr, index: number): void { + const array = this.#getArray(baseSlot); + if (index < 0 || index >= array.length) { + throw new Error( + `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + ); + } + array.splice(index, 1); + } + + /** Copies `count` elements from the source array to the destination array (overwrites destination). */ + copy(srcSlot: Fr, dstSlot: Fr, count: number): void { + const srcArray = this.#getArray(srcSlot); + if (count > srcArray.length) { + throw new Error( + `Cannot copy ${count} elements from volatile array of length ${srcArray.length} at slot ${srcSlot}`, + ); + } + // Deep copy the elements to avoid aliasing + const copied = srcArray.slice(0, count).map(el => [...el]); + this.#setArray(dstSlot, copied); + } +} diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 96402003a15a..04c401f5c9bc 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -898,6 +898,70 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + aztec_utl_volatile_push(foreignBaseSlot: ForeignCallSingle, foreignElements: ForeignCallArray) { + const baseSlot = fromSingle(foreignBaseSlot); + const elements = fromArray(foreignElements); + const newLen = this.handlerAsUtility().volatilePush(baseSlot, elements); + return toForeignCallResult([toSingle(new Fr(newLen))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_pop(foreignBaseSlot: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const element = this.handlerAsUtility().volatilePop(baseSlot); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_get(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + const element = this.handlerAsUtility().volatileGet(baseSlot, index); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_set( + foreignBaseSlot: ForeignCallSingle, + foreignIndex: ForeignCallSingle, + foreignElements: ForeignCallArray, + ) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + const elements = fromArray(foreignElements); + this.handlerAsUtility().volatileSet(baseSlot, index, elements); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_len(foreignBaseSlot: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const len = this.handlerAsUtility().volatileLen(baseSlot); + return toForeignCallResult([toSingle(new Fr(len))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_remove(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + this.handlerAsUtility().volatileRemove(baseSlot, index); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_volatile_copy( + foreignSrcSlot: ForeignCallSingle, + foreignDstSlot: ForeignCallSingle, + foreignCount: ForeignCallSingle, + ) { + const srcSlot = fromSingle(foreignSrcSlot); + const dstSlot = fromSingle(foreignDstSlot); + const count = fromSingle(foreignCount).toNumber(); + this.handlerAsUtility().volatileCopy(srcSlot, dstSlot, count); + return toForeignCallResult([]); + } + // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. // The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need // to implement this function here. Isn't there a way to programmatically identify that this is missing, given the From 88392099262d8e05654a04e69b083e0a9a8a5fc4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 30 Mar 2026 17:14:43 +0000 Subject: [PATCH 02/10] replace fetch_pending_tagged_logs impl with volatile arrays --- .../aztec/src/messages/discovery/mod.nr | 38 ++++--------- .../aztec/src/messages/processing/mod.nr | 16 ------ .../aztec/src/oracle/message_processing.nr | 16 +++--- .../oracle/interfaces.ts | 1 + .../oracle/oracle.ts | 6 ++ .../oracle/utility_execution_oracle.ts | 37 ++++++------ .../volatile_array_service.ts | 16 ++++++ yarn-project/pxe/src/logs/log_service.test.ts | 5 -- yarn-project/pxe/src/logs/log_service.ts | 57 +++---------------- yarn-project/txe/src/rpc_translator.ts | 7 +++ 10 files changed, 80 insertions(+), 119 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 9cbfae253c62..3e4f9be74b7a 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -14,10 +14,11 @@ use crate::{ encoding::MAX_MESSAGE_CONTENT_LEN, logs::note::MAX_NOTE_PACKED_LEN, processing::{ - get_private_logs, MessageContext, offchain::OffchainInboxSync, OffchainMessageWithContext, + MessageContext, offchain::OffchainInboxSync, OffchainMessageWithContext, pending_tagged_log::PendingTaggedLog, validate_and_store_enqueued_notes_and_events, }, }, + oracle, utils::array, }; @@ -126,8 +127,8 @@ pub unconstrained fn do_sync_state( // First we process all private logs, which can contain different kinds of messages e.g. private notes, partial // notes, private events, etc. - let logs = get_private_logs(contract_address, scope); - logs.for_each(|i, pending_tagged_log: PendingTaggedLog| { + let logs = oracle::message_processing::get_pending_tagged_logs(scope); + logs.for_each(|_i, pending_tagged_log: PendingTaggedLog| { if pending_tagged_log.log.len() == 0 { aztecnr_warn_log_format!("Skipping empty log from tx {0}")([pending_tagged_log.context.tx_hash]); } else { @@ -146,12 +147,6 @@ pub unconstrained fn do_sync_state( scope, ); } - - // We need to delete each log from the array so that we won't process them again. `CapsuleArray::for_each` - // allows deletion of the current element during iteration, so this is safe. - // Note that this (and all other database changes) will only be committed if contract execution succeeds, - // including any enqueued validation requests. - logs.remove(i); }); if offchain_inbox_sync.is_some() { @@ -187,18 +182,13 @@ pub unconstrained fn do_sync_state( } mod test { - use crate::{ - capsules::CapsuleArray, - messages::{ - discovery::{CustomMessageHandler, do_sync_state}, - logs::note::MAX_NOTE_PACKED_LEN, - processing::{ - offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, - }, - }, - test::helpers::test_environment::TestEnvironment, + use crate::messages::{ + discovery::{CustomMessageHandler, do_sync_state}, + logs::note::MAX_NOTE_PACKED_LEN, + processing::offchain::OffchainInboxSync, }; use crate::protocol::address::AztecAddress; + use crate::test::helpers::test_environment::TestEnvironment; global SCOPE: AztecAddress = AztecAddress { inner: 0xcafe }; @@ -209,11 +199,9 @@ mod test { let contract_address = AztecAddress { inner: 0xdeadbeef }; env.utility_context_at(contract_address, |_| { - let base_slot = PENDING_TAGGED_LOG_ARRAY_BASE_SLOT; - - let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot, SCOPE); - logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); - assert_eq(logs.len(), 1); + // Mock the oracle call to return a base slot pointing to an empty volatile array. + let mock = std::test::OracleMock::mock("aztec_utl_getPendingTaggedLogs_v2"); + let _ = mock.returns(42); let no_handler: Option> = Option::none(); let no_inbox_sync: Option> = Option::none(); @@ -225,8 +213,6 @@ mod test { no_inbox_sync, SCOPE, ); - - assert_eq(logs.len(), 0); }); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 486436587642..b700b33f8ffc 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -31,10 +31,6 @@ use crate::protocol::{ }; use event_validation_request::EventValidationRequest; -// Base slot for the pending tagged log array to which the fetch_tagged_logs oracle inserts found private logs. -pub(crate) global PENDING_TAGGED_LOG_ARRAY_BASE_SLOT: Field = - sha256_to_field("AZTEC_NR::PENDING_TAGGED_LOG_ARRAY_BASE_SLOT".as_bytes()); - global NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT: Field = sha256_to_field( "AZTEC_NR::NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT".as_bytes(), ); @@ -58,18 +54,6 @@ pub struct OffchainMessageWithContext { pub message_context: MessageContext, } -/// Searches for private logs emitted by `contract_address` that might contain messages for the given `scope`. -pub(crate) unconstrained fn get_private_logs( - contract_address: AztecAddress, - scope: AztecAddress, -) -> CapsuleArray { - // We will eventually perform log discovery via tagging here, but for now we simply call the `fetchTaggedLogs` - // oracle. This makes PXE synchronize tags, download logs and store the pending tagged logs in a capsule array. - oracle::message_processing::fetch_tagged_logs(PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope); - - CapsuleArray::at(contract_address, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, scope) -} - /// Enqueues a note for validation and storage by PXE. /// /// Once validated, the note becomes retrievable via the `get_notes` oracle. The note will be scoped to diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index ac66b939dcaf..967d307f481f 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,14 +1,16 @@ +use crate::messages::processing::pending_tagged_log::PendingTaggedLog; use crate::protocol::address::AztecAddress; +use crate::volatile::VolatileArray; -/// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and makes -/// them available for later processing in Noir by storing them in a capsule array. -// TODO(F-498): review naming consistency -pub unconstrained fn fetch_tagged_logs(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) { - get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot, scope); +/// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and +/// returns them in a volatile array with an oracle-allocated base slot. +pub(crate) unconstrained fn get_pending_tagged_logs(scope: AztecAddress) -> VolatileArray { + let result_slot = get_pending_tagged_logs_oracle(scope); + VolatileArray::at(result_slot) } -#[oracle(aztec_utl_getPendingTaggedLogs)] -unconstrained fn get_pending_tagged_logs_oracle(pending_tagged_log_array_base_slot: Field, scope: AztecAddress) {} +#[oracle(aztec_utl_getPendingTaggedLogs_v2)] +unconstrained fn get_pending_tagged_logs_oracle(scope: AztecAddress) -> Field {} // This must be a single oracle and not one for notes and one for events because the entire point is to validate all // notes and events in one go, minimizing node round-trips. diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 11154d31291d..91517c07db72 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -120,6 +120,7 @@ export interface IUtilityExecutionOracle { numberOfElements: number, ): Promise; getPendingTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress): Promise; + getPendingTaggedLogsV2(scope: AztecAddress): Promise; validateAndStoreEnqueuedNotesAndEvents( contractAddress: AztecAddress, noteValidationRequestsArrayBaseSlot: Fr, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 9b833fe0f095..e543d6ce353e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -523,6 +523,12 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + async aztec_utl_getPendingTaggedLogs_v2([scope]: ACVMField[]): Promise { + const baseSlot = await this.handlerAsUtility().getPendingTaggedLogsV2(AztecAddress.fromString(scope)); + return [toACVMField(baseSlot)]; + } + // eslint-disable-next-line camelcase async aztec_utl_validateAndStoreEnqueuedNotesAndEvents( [contractAddress]: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 21b257184ef7..d7e3df72c507 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -500,19 +500,35 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } public async getPendingTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr, scope: AztecAddress) { - const logService = new LogService( + const logService = this.#createLogService(); + const logs = await logService.fetchTaggedLogs(this.contractAddress, scope); + await this.capsuleService.appendToCapsuleArray( + this.contractAddress, + pendingTaggedLogArrayBaseSlot, + logs.map(log => log.toFields()), + this.jobId, + scope, + ); + } + + /** Fetches pending tagged logs into a freshly allocated volatile array and returns its base slot. */ + public async getPendingTaggedLogsV2(scope: AztecAddress): Promise { + const logService = this.#createLogService(); + const logs = await logService.fetchTaggedLogs(this.contractAddress, scope); + return this.volatileArrayService.newArray(logs.map(log => log.toFields())); + } + + #createLogService(): LogService { + return new LogService( this.aztecNode, this.anchorBlockHeader, this.keyStore, - this.capsuleService, this.recipientTaggingStore, this.senderAddressBookStore, this.addressStore, this.jobId, this.logger.getBindings(), ); - - await logService.fetchTaggedLogs(this.contractAddress, pendingTaggedLogArrayBaseSlot, scope); } /** @@ -623,18 +639,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra await this.capsuleService.readCapsuleArray(contractAddress, logRetrievalRequestsArrayBaseSlot, this.jobId, scope) ).map(LogRetrievalRequest.fromFields); - const logService = new LogService( - this.aztecNode, - this.anchorBlockHeader, - this.keyStore, - this.capsuleService, - this.recipientTaggingStore, - this.senderAddressBookStore, - this.addressStore, - this.jobId, - this.logger.getBindings(), - ); - + const logService = this.#createLogService(); const maybeLogRetrievalResponses = await logService.fetchLogsByTag(contractAddress, logRetrievalRequests); // Requests are cleared once we're done. diff --git a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts index 00176fbb0d66..7303a2cdf212 100644 --- a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts +++ b/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts @@ -70,6 +70,22 @@ export class VolatileArrayService { array.splice(index, 1); } + /** Allocates a fresh, unused base slot for a new volatile array. */ + allocateSlot(): Fr { + let slot: Fr; + do { + slot = Fr.random(); + } while (this.#arrays.has(slot.toString())); + return slot; + } + + /** Creates a new volatile array pre-populated with the given elements and returns its base slot. */ + newArray(elements: Fr[][]): Fr { + const slot = this.allocateSlot(); + this.#setArray(slot, elements); + return slot; + } + /** Copies `count` elements from the source array to the destination array (overwrites destination). */ copy(srcSlot: Fr, dstSlot: Fr, count: number): void { const srcArray = this.#getArray(srcSlot); diff --git a/yarn-project/pxe/src/logs/log_service.test.ts b/yarn-project/pxe/src/logs/log_service.test.ts index a801f48c948a..7983d5c2af15 100644 --- a/yarn-project/pxe/src/logs/log_service.test.ts +++ b/yarn-project/pxe/src/logs/log_service.test.ts @@ -13,8 +13,6 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { AddressStore } from '../storage/address_store/address_store.js'; -import { CapsuleService } from '../storage/capsule_store/capsule_service.js'; -import { CapsuleStore } from '../storage/capsule_store/capsule_store.js'; import { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import { SenderAddressBookStore } from '../storage/tagging_store/sender_address_book_store.js'; import { LogService } from './log_service.js'; @@ -23,7 +21,6 @@ describe('LogService', () => { let contractAddress: AztecAddress; let aztecNode: MockProxy; let keyStore: KeyStore; - let capsuleStore: CapsuleStore; let recipientTaggingStore: RecipientTaggingStore; let addressStore: AddressStore; let senderAddressBookStore: SenderAddressBookStore; @@ -36,7 +33,6 @@ describe('LogService', () => { // Set up contract address contractAddress = await AztecAddress.random(); keyStore = new KeyStore(await openTmpStore('test')); - capsuleStore = new CapsuleStore(await openTmpStore('test')); recipientTaggingStore = new RecipientTaggingStore(await openTmpStore('test')); senderAddressBookStore = new SenderAddressBookStore(await openTmpStore('test')); addressStore = new AddressStore(await openTmpStore('test')); @@ -54,7 +50,6 @@ describe('LogService', () => { aztecNode, anchorBlockHeader, keyStore, - new CapsuleService(capsuleStore, 'ALL_SCOPES'), recipientTaggingStore, senderAddressBookStore, addressStore, diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index 0f830e018b3e..832904e77d78 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -1,21 +1,13 @@ -import type { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; import type { KeyStore } from '@aztec/key-store'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { - ExtendedDirectionalAppTaggingSecret, - PendingTaggedLog, - SiloedTag, - Tag, - TxScopedL2Log, -} from '@aztec/stdlib/logs'; +import { ExtendedDirectionalAppTaggingSecret, PendingTaggedLog, SiloedTag, Tag } from '@aztec/stdlib/logs'; import type { BlockHeader } from '@aztec/stdlib/tx'; import type { LogRetrievalRequest } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../contract_function_simulator/noir-structs/log_retrieval_response.js'; import { AddressStore } from '../storage/address_store/address_store.js'; -import type { CapsuleService } from '../storage/capsule_store/capsule_service.js'; import type { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import type { SenderAddressBookStore } from '../storage/tagging_store/sender_address_book_store.js'; import { @@ -31,7 +23,6 @@ export class LogService { private readonly aztecNode: AztecNode, private readonly anchorBlockHeader: BlockHeader, private readonly keyStore: KeyStore, - private readonly capsuleService: CapsuleService, private readonly recipientTaggingStore: RecipientTaggingStore, private readonly senderAddressBookStore: SenderAddressBookStore, private readonly addressStore: AddressStore, @@ -120,11 +111,7 @@ export class LogService { ); } - public async fetchTaggedLogs( - contractAddress: AztecAddress, - pendingTaggedLogArrayBaseSlot: Fr, - recipient: AztecAddress, - ) { + public async fetchTaggedLogs(contractAddress: AztecAddress, recipient: AztecAddress): Promise { this.log.verbose(`Fetching tagged logs for ${contractAddress.toString()}`); // We only load logs from block up to and including the anchor block number @@ -148,12 +135,12 @@ export class LogService { ), ); - // Flatten all logs from all secrets - const allLogs = logArrays.flat(); - - if (allLogs.length > 0) { - await this.#storePendingTaggedLogs(contractAddress, pendingTaggedLogArrayBaseSlot, recipient, allLogs); - } + return logArrays + .flat() + .map( + scopedLog => + new PendingTaggedLog(scopedLog.logData, scopedLog.txHash, scopedLog.noteHashes, scopedLog.firstNullifier), + ); } async #getSecretsForSenders( @@ -187,32 +174,4 @@ export class LogService { }), ); } - - #storePendingTaggedLogs( - contractAddress: AztecAddress, - capsuleArrayBaseSlot: Fr, - recipient: AztecAddress, - privateLogs: TxScopedL2Log[], - ) { - // Build all pending tagged logs from the scoped logs - const pendingTaggedLogs = privateLogs.map(scopedLog => { - const pendingTaggedLog = new PendingTaggedLog( - scopedLog.logData, - scopedLog.txHash, - scopedLog.noteHashes, - scopedLog.firstNullifier, - ); - - return pendingTaggedLog.toFields(); - }); - - // TODO: This looks like it could belong more at the oracle interface level - return this.capsuleService.appendToCapsuleArray( - contractAddress, - capsuleArrayBaseSlot, - pendingTaggedLogs, - this.jobId, - recipient, - ); - } } diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 04c401f5c9bc..ea5476fc17d0 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -750,6 +750,13 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + async aztec_utl_getPendingTaggedLogs_v2(foreignScope: ForeignCallSingle) { + const scope = AztecAddress.fromField(fromSingle(foreignScope)); + const baseSlot = await this.handlerAsUtility().getPendingTaggedLogsV2(scope); + return toForeignCallResult([toSingle(baseSlot)]); + } + // eslint-disable-next-line camelcase public async aztec_utl_validateAndStoreEnqueuedNotesAndEvents( foreignContractAddress: ForeignCallSingle, From 0b5a2a8c7ef5706d354155d5dfcf994ff47cafcb Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 30 Mar 2026 17:24:08 +0000 Subject: [PATCH 03/10] volatile -> ephemeral --- .../aztec/src/{volatile => ephemeral}/mod.nr | 64 +++++++++---------- noir-projects/aztec-nr/aztec/src/lib.nr | 2 +- .../aztec/src/messages/discovery/mod.nr | 4 +- .../src/messages/discovery/partial_notes.nr | 2 +- .../src/oracle/{volatile.nr => ephemeral.nr} | 24 +++---- .../aztec/src/oracle/message_processing.nr | 8 +-- .../aztec-nr/aztec/src/oracle/mod.nr | 2 +- ...est.ts => ephemeral_array_service.test.ts} | 8 +-- ..._service.ts => ephemeral_array_service.ts} | 16 ++--- .../oracle/interfaces.ts | 16 ++--- .../oracle/oracle.ts | 28 ++++---- .../oracle/utility_execution_oracle.ts | 38 +++++------ yarn-project/txe/src/rpc_translator.ts | 28 ++++---- 13 files changed, 120 insertions(+), 120 deletions(-) rename noir-projects/aztec-nr/aztec/src/{volatile => ephemeral}/mod.nr (78%) rename noir-projects/aztec-nr/aztec/src/oracle/{volatile.nr => ephemeral.nr} (61%) rename yarn-project/pxe/src/contract_function_simulator/{volatile_array_service.test.ts => ephemeral_array_service.test.ts} (96%) rename yarn-project/pxe/src/contract_function_simulator/{volatile_array_service.ts => ephemeral_array_service.ts} (79%) diff --git a/noir-projects/aztec-nr/aztec/src/volatile/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr similarity index 78% rename from noir-projects/aztec-nr/aztec/src/volatile/mod.nr rename to noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 6f3da8fc63b0..77ca7f34b29f 100644 --- a/noir-projects/aztec-nr/aztec/src/volatile/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -1,37 +1,37 @@ -use crate::oracle::volatile; +use crate::oracle::ephemeral; use crate::protocol::traits::{Deserialize, Serialize}; /// A dynamically sized array that exists only during a single contract call frame. /// -/// Volatile arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract -/// call frame gets its own isolated set of volatile arrays child simulations cannot see the parent's volatile arrays, +/// 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 set 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 volatile arrays significantly +/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making ephemeral arrays significantly /// cheaper than capsule arrays and more appropriate for transient data. /// /// ## Use Cases /// -/// Volatile arrays are designed for transient communication between PXE (TypeScript) and contracts (Noir) during +/// Ephemeral arrays are designed for transient communication between PXE (TypeScript) and contracts (Noir) during /// simulation, for example, note validation requests or event validation responses. /// /// For data that needs to persist across simulations, contract calls, etc, use /// [`CapsuleArray`](crate::capsules::CapsuleArray) instead. -pub struct VolatileArray { +pub struct EphemeralArray { base_slot: Field, } -impl VolatileArray { - /// Creates a volatile array at the given base slot. +impl EphemeralArray { + /// Creates an ephemeral array at the given base slot. /// - /// Multiple volatile arrays can coexist within the same call frame by using different base slots. + /// Multiple ephemeral arrays can coexist within the same call frame by using different base slots. pub unconstrained fn at(base_slot: Field) -> Self { Self { base_slot } } /// Returns the number of elements stored in the array. pub unconstrained fn len(self) -> u32 { - volatile::len_oracle(self.base_slot) + ephemeral::len_oracle(self.base_slot) } /// Stores a value at the end of the array. @@ -40,7 +40,7 @@ impl VolatileArray { T: Serialize, { let serialized = value.serialize(); - let _ = volatile::push_oracle(self.base_slot, serialized); + let _ = ephemeral::push_oracle(self.base_slot, serialized); } /// Removes and returns the last element. Panics if the array is empty. @@ -48,7 +48,7 @@ impl VolatileArray { where T: Deserialize, { - let serialized = volatile::pop_oracle(self.base_slot); + let serialized = ephemeral::pop_oracle(self.base_slot); Deserialize::deserialize(serialized) } @@ -57,7 +57,7 @@ impl VolatileArray { where T: Deserialize, { - let serialized = volatile::get_oracle(self.base_slot, index); + let serialized = ephemeral::get_oracle(self.base_slot, index); Deserialize::deserialize(serialized) } @@ -67,12 +67,12 @@ impl VolatileArray { T: Serialize, { let serialized = value.serialize(); - volatile::set_oracle(self.base_slot, index, serialized); + ephemeral::set_oracle(self.base_slot, index, serialized); } /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. pub unconstrained fn remove(self, index: u32) { - volatile::remove_oracle(self.base_slot, index); + ephemeral::remove_oracle(self.base_slot, index); } /// Calls a function on each element of the array. @@ -96,7 +96,7 @@ impl VolatileArray { mod test { use crate::test::helpers::test_environment::TestEnvironment; - use super::VolatileArray; + use super::EphemeralArray; global SLOT: Field = 1230; global OTHER_SLOT: Field = 5670; @@ -105,7 +105,7 @@ mod test { unconstrained fn empty_array() { let _ = TestEnvironment::new(); - let array: VolatileArray = VolatileArray::at(SLOT); + let array: EphemeralArray = EphemeralArray::at(SLOT); assert_eq(array.len(), 0); } @@ -113,7 +113,7 @@ mod test { unconstrained fn empty_array_read() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); let _: Field = array.get(0); } @@ -121,7 +121,7 @@ mod test { unconstrained fn empty_array_pop() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); let _: Field = array.pop(); } @@ -129,7 +129,7 @@ mod test { unconstrained fn array_push() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(5); assert_eq(array.len(), 1); @@ -140,7 +140,7 @@ mod test { unconstrained fn read_past_len() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(5); let _ = array.get(1); @@ -150,7 +150,7 @@ mod test { unconstrained fn array_pop() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(5); array.push(10); @@ -164,7 +164,7 @@ mod test { unconstrained fn array_set() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(5); array.set(0, 99); assert_eq(array.get(0), 99); @@ -174,7 +174,7 @@ mod test { unconstrained fn array_remove_last() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(5); array.remove(0); @@ -186,7 +186,7 @@ mod test { unconstrained fn array_remove_some() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(7); array.push(8); @@ -205,7 +205,7 @@ mod test { unconstrained fn array_remove_all() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(7); array.push(8); @@ -222,7 +222,7 @@ mod test { unconstrained fn for_each_called_with_all_elements() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(4); array.push(5); @@ -241,7 +241,7 @@ mod test { unconstrained fn for_each_remove_some() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(4); array.push(5); @@ -262,7 +262,7 @@ mod test { unconstrained fn for_each_remove_all() { let _ = TestEnvironment::new(); - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); array.push(4); array.push(5); @@ -277,8 +277,8 @@ mod test { unconstrained fn different_slots_are_isolated() { let _ = TestEnvironment::new(); - let array_a = VolatileArray::at(SLOT); - let array_b = VolatileArray::at(OTHER_SLOT); + let array_a = EphemeralArray::at(SLOT); + let array_b = EphemeralArray::at(OTHER_SLOT); array_a.push(10); array_a.push(20); @@ -298,7 +298,7 @@ mod test { use crate::test::mocks::MockStruct; - let array = VolatileArray::at(SLOT); + let array = EphemeralArray::at(SLOT); let a = MockStruct::new(5, 6); let b = MockStruct::new(7, 8); diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index da3f5893faad..fa676a1da916 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -39,7 +39,7 @@ pub mod nullifier; pub mod oracle; pub mod state_vars; pub mod capsules; -pub mod volatile; +pub mod ephemeral; pub mod event; pub mod messages; pub use protocol_types as protocol; diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 3e4f9be74b7a..8a54dc32239c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -162,7 +162,7 @@ pub unconstrained fn do_sync_state( scope, ); // The inbox sync returns _a copy_ of messages to process, so we clear them as we do so. This is a - // volatile array with the to-process message, not the actual persistent storage of them. + // ephemeral array with the to-process message, not the actual persistent storage of them. msgs.remove(i); }); } @@ -199,7 +199,7 @@ mod test { let contract_address = AztecAddress { inner: 0xdeadbeef }; env.utility_context_at(contract_address, |_| { - // Mock the oracle call to return a base slot pointing to an empty volatile array. + // Mock the oracle call to return a base slot pointing to an empty ephemeral array. let mock = std::test::OracleMock::mock("aztec_utl_getPendingTaggedLogs_v2"); let _ = mock.returns(42); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index 4ef8255e6539..744fda089ca7 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -98,7 +98,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( maybe_completion_logs.for_each(|i, maybe_log: Option| { // We clear the completion logs as we read them so that the array is empty by the time we next query it. - // TODO(#14943): use volatile arrays to avoid having to manually clear this. + // TODO(#14943): use ephemeral arrays to avoid having to manually clear this. maybe_completion_logs.remove(i); let pending_partial_note = pending_partial_notes.get(i); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/volatile.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr similarity index 61% rename from noir-projects/aztec-nr/aztec/src/oracle/volatile.nr rename to noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr index 36d5ccf87d4a..07444708d4aa 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/volatile.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr @@ -1,33 +1,33 @@ -/// Oracles for volatile arrays: in-memory arrays scoped to a single contract call frame. +/// Oracles for ephemeral arrays: in-memory arrays scoped to a single contract call frame. /// -/// Unlike capsule oracles, volatile oracles operate on arrays (not individual slots) and each oracle call performs a +/// Unlike capsule oracles, ephemeral oracles operate on arrays (not individual slots) and each oracle call performs a /// complete logical operation. This reduces the number of oracle round-trips compared to building array semantics on /// top of slot-level oracles. -/// Appends a serialized element to the volatile array and returns the new length. -#[oracle(aztec_utl_volatile_push)] +/// Appends a serialized element to the ephemeral array and returns the new length. +#[oracle(aztec_utl_ephemeral_push)] pub(crate) unconstrained fn push_oracle(base_slot: Field, values: [Field; N]) -> u32 {} -/// Removes and returns the last serialized element from the volatile array. -#[oracle(aztec_utl_volatile_pop)] +/// Removes and returns the last serialized element from the ephemeral array. +#[oracle(aztec_utl_ephemeral_pop)] pub(crate) unconstrained fn pop_oracle(base_slot: Field) -> [Field; N] {} /// Returns the serialized element at the given index. -#[oracle(aztec_utl_volatile_get)] +#[oracle(aztec_utl_ephemeral_get)] pub(crate) unconstrained fn get_oracle(base_slot: Field, index: u32) -> [Field; N] {} /// Overwrites the serialized element at the given index. -#[oracle(aztec_utl_volatile_set)] +#[oracle(aztec_utl_ephemeral_set)] pub(crate) unconstrained fn set_oracle(base_slot: Field, index: u32, values: [Field; N]) {} -/// Returns the number of elements in the volatile array. -#[oracle(aztec_utl_volatile_len)] +/// Returns the number of elements in the ephemeral array. +#[oracle(aztec_utl_ephemeral_len)] pub(crate) unconstrained fn len_oracle(base_slot: Field) -> u32 {} /// Removes the element at the given index, shifting subsequent elements backward. -#[oracle(aztec_utl_volatile_remove)] +#[oracle(aztec_utl_ephemeral_remove)] pub(crate) unconstrained fn remove_oracle(base_slot: Field, index: u32) {} /// Copies `count` elements from the source array to the destination array (overwrites destination). -#[oracle(aztec_utl_volatile_copy)] +#[oracle(aztec_utl_ephemeral_copy)] pub(crate) unconstrained fn copy_oracle(src_slot: Field, dst_slot: Field, count: u32) {} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 967d307f481f..0d49c8d6b1d2 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,12 +1,12 @@ +use crate::ephemeral::EphemeralArray; use crate::messages::processing::pending_tagged_log::PendingTaggedLog; use crate::protocol::address::AztecAddress; -use crate::volatile::VolatileArray; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and -/// returns them in a volatile array with an oracle-allocated base slot. -pub(crate) unconstrained fn get_pending_tagged_logs(scope: AztecAddress) -> VolatileArray { +/// returns them in an ephemeral array with an oracle-allocated base slot. +pub(crate) unconstrained fn get_pending_tagged_logs(scope: AztecAddress) -> EphemeralArray { let result_slot = get_pending_tagged_logs_oracle(scope); - VolatileArray::at(result_slot) + EphemeralArray::at(result_slot) } #[oracle(aztec_utl_getPendingTaggedLogs_v2)] diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 671876486516..6026df412b1a 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -10,7 +10,7 @@ pub mod auth_witness; pub mod block_header; pub mod call_private_function; pub mod capsules; -pub mod volatile; +pub mod ephemeral; pub mod contract_sync; pub mod public_call; pub mod tx_phase; diff --git a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts similarity index 96% rename from yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts rename to yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts index 192509af2ad8..abc9bfe9ea3d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts @@ -1,14 +1,14 @@ import { Fr } from '@aztec/foundation/curves/bn254'; -import { VolatileArrayService } from './volatile_array_service.js'; +import { EphemeralArrayService } from './ephemeral_array_service.js'; -describe('VolatileArrayService', () => { - let service: VolatileArrayService; +describe('EphemeralArrayService', () => { + let service: EphemeralArrayService; const slot = Fr.fromString('0x01'); const otherSlot = Fr.fromString('0x02'); beforeEach(() => { - service = new VolatileArrayService(); + service = new EphemeralArrayService(); }); describe('len', () => { diff --git a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts similarity index 79% rename from yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts rename to yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts index 7303a2cdf212..beaec85f9e1d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/volatile_array_service.ts +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts @@ -1,7 +1,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; /** In-memory array service for transient data during a single contract call frame. */ -export class VolatileArrayService { +export class EphemeralArrayService { /** Maps base slot to array of elements, where each element is a serialized Fr[]. */ #arrays: Map = new Map(); @@ -30,7 +30,7 @@ export class VolatileArrayService { pop(baseSlot: Fr): Fr[] { const array = this.#getArray(baseSlot); if (array.length === 0) { - throw new Error(`Volatile array at slot ${baseSlot} is empty`); + throw new Error(`Ephemeral array at slot ${baseSlot} is empty`); } const element = array.pop()!; this.#setArray(baseSlot, array); @@ -42,7 +42,7 @@ export class VolatileArrayService { const array = this.#getArray(baseSlot); if (index < 0 || index >= array.length) { throw new Error( - `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, ); } return array[index]; @@ -53,7 +53,7 @@ export class VolatileArrayService { const array = this.#getArray(baseSlot); if (index < 0 || index >= array.length) { throw new Error( - `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, ); } array[index] = value; @@ -64,13 +64,13 @@ export class VolatileArrayService { const array = this.#getArray(baseSlot); if (index < 0 || index >= array.length) { throw new Error( - `Volatile array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, ); } array.splice(index, 1); } - /** Allocates a fresh, unused base slot for a new volatile array. */ + /** Allocates a fresh, unused base slot for a new ephemeral array. */ allocateSlot(): Fr { let slot: Fr; do { @@ -79,7 +79,7 @@ export class VolatileArrayService { return slot; } - /** Creates a new volatile array pre-populated with the given elements and returns its base slot. */ + /** Creates a new ephemeral array pre-populated with the given elements and returns its base slot. */ newArray(elements: Fr[][]): Fr { const slot = this.allocateSlot(); this.#setArray(slot, elements); @@ -91,7 +91,7 @@ export class VolatileArrayService { const srcArray = this.#getArray(srcSlot); if (count > srcArray.length) { throw new Error( - `Cannot copy ${count} elements from volatile array of length ${srcArray.length} at slot ${srcSlot}`, + `Cannot copy ${count} elements from ephemeral array of length ${srcArray.length} at slot ${srcSlot}`, ); } // Deep copy the elements to avoid aliasing diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 91517c07db72..7a23000fb4a9 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -156,14 +156,14 @@ export interface IUtilityExecutionOracle { setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; - // Volatile array methods — in-memory per-call-frame arrays for transient data. - volatilePush(baseSlot: Fr, elements: Fr[]): number; - volatilePop(baseSlot: Fr): Fr[]; - volatileGet(baseSlot: Fr, index: number): Fr[]; - volatileSet(baseSlot: Fr, index: number, elements: Fr[]): void; - volatileLen(baseSlot: Fr): number; - volatileRemove(baseSlot: Fr, index: number): void; - volatileCopy(srcSlot: Fr, dstSlot: Fr, count: number): void; + // Ephemeral array methods — in-memory per-call-frame arrays for transient data. + ephemeralPush(baseSlot: Fr, elements: Fr[]): number; + ephemeralPop(baseSlot: Fr): Fr[]; + ephemeralGet(baseSlot: Fr, index: number): Fr[]; + ephemeralSet(baseSlot: Fr, index: number, elements: Fr[]): void; + ephemeralLen(baseSlot: Fr): number; + ephemeralRemove(baseSlot: Fr, index: number): void; + ephemeralCopy(srcSlot: Fr, dstSlot: Fr, count: number): void; } /** diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index e543d6ce353e..7b0a6d3adc75 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -655,26 +655,26 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_utl_volatile_push([baseSlot]: ACVMField[], elements: ACVMField[]): Promise { - const newLen = this.handlerAsUtility().volatilePush(Fr.fromString(baseSlot), elements.map(Fr.fromString)); + aztec_utl_ephemeral_push([baseSlot]: ACVMField[], elements: ACVMField[]): Promise { + const newLen = this.handlerAsUtility().ephemeralPush(Fr.fromString(baseSlot), elements.map(Fr.fromString)); return Promise.resolve([toACVMField(newLen)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_pop([baseSlot]: ACVMField[]): Promise { - const element = this.handlerAsUtility().volatilePop(Fr.fromString(baseSlot)); + aztec_utl_ephemeral_pop([baseSlot]: ACVMField[]): Promise { + const element = this.handlerAsUtility().ephemeralPop(Fr.fromString(baseSlot)); return Promise.resolve([element.map(toACVMField)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_get([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { - const element = this.handlerAsUtility().volatileGet(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); + aztec_utl_ephemeral_get([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { + const element = this.handlerAsUtility().ephemeralGet(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); return Promise.resolve([element.map(toACVMField)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_set([baseSlot]: ACVMField[], [index]: ACVMField[], elements: ACVMField[]): Promise { - this.handlerAsUtility().volatileSet( + aztec_utl_ephemeral_set([baseSlot]: ACVMField[], [index]: ACVMField[], elements: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralSet( Fr.fromString(baseSlot), Fr.fromString(index).toNumber(), elements.map(Fr.fromString), @@ -683,20 +683,20 @@ export class Oracle { } // eslint-disable-next-line camelcase - aztec_utl_volatile_len([baseSlot]: ACVMField[]): Promise { - const len = this.handlerAsUtility().volatileLen(Fr.fromString(baseSlot)); + aztec_utl_ephemeral_len([baseSlot]: ACVMField[]): Promise { + const len = this.handlerAsUtility().ephemeralLen(Fr.fromString(baseSlot)); return Promise.resolve([toACVMField(len)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_remove([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { - this.handlerAsUtility().volatileRemove(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); + aztec_utl_ephemeral_remove([baseSlot]: ACVMField[], [index]: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralRemove(Fr.fromString(baseSlot), Fr.fromString(index).toNumber()); return Promise.resolve([]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_copy([srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [count]: ACVMField[]): Promise { - this.handlerAsUtility().volatileCopy( + aztec_utl_ephemeral_copy([srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [count]: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralCopy( Fr.fromString(srcSlot), Fr.fromString(dstSlot), Fr.fromString(count).toNumber(), diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index d7e3df72c507..a6070279ea4e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -36,13 +36,13 @@ import type { NoteStore } from '../../storage/note_store/note_store.js'; import type { PrivateEventStore } from '../../storage/private_event_store/private_event_store.js'; import type { RecipientTaggingStore } from '../../storage/tagging_store/recipient_tagging_store.js'; import type { SenderAddressBookStore } from '../../storage/tagging_store/sender_address_book_store.js'; +import { EphemeralArrayService } from '../ephemeral_array_service.js'; import { EventValidationRequest } from '../noir-structs/event_validation_request.js'; import { LogRetrievalRequest } from '../noir-structs/log_retrieval_request.js'; import { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js'; import { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { UtilityContext } from '../noir-structs/utility_context.js'; import { pickNotes } from '../pick_notes.js'; -import { VolatileArrayService } from '../volatile_array_service.js'; import type { IMiscOracle, IUtilityExecutionOracle, NoteData } from './interfaces.js'; import { MessageLoadOracleInputs } from './message_load_oracle_inputs.js'; @@ -79,7 +79,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra private contractLogger: Logger | undefined; private aztecnrLogger: Logger | undefined; private offchainEffects: OffchainEffect[] = []; - private readonly volatileArrayService = new VolatileArrayService(); + private readonly ephemeralArrayService = new EphemeralArrayService(); protected readonly contractAddress: AztecAddress; protected readonly authWitnesses: AuthWitness[]; @@ -511,11 +511,11 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ); } - /** Fetches pending tagged logs into a freshly allocated volatile array and returns its base slot. */ + /** Fetches pending tagged logs into a freshly allocated ephemeral array and returns its base slot. */ public async getPendingTaggedLogsV2(scope: AztecAddress): Promise { const logService = this.#createLogService(); const logs = await logService.fetchTaggedLogs(this.contractAddress, scope); - return this.volatileArrayService.newArray(logs.map(log => log.toFields())); + return this.ephemeralArrayService.newArray(logs.map(log => log.toFields())); } #createLogService(): LogService { @@ -672,7 +672,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra throw new Error(`Got a message context request from ${contractAddress}, expected ${this.contractAddress}`); } - // TODO(@mverzilli): this is a prime example of where using a volatile array would make much more sense, we don't + // TODO(@mverzilli): this is a prime example of where using an ephemeral array would make much more sense, we don't // need scopes here, we just need a bit of shared memory to cross boundaries between Noir and TS. // At the same time, we don't want to allow any global scope access other than where backwards compatibility // forces us to. Hence we need the scope here to be artificial. @@ -792,32 +792,32 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } - public volatilePush(baseSlot: Fr, elements: Fr[]): number { - return this.volatileArrayService.push(baseSlot, elements); + public ephemeralPush(baseSlot: Fr, elements: Fr[]): number { + return this.ephemeralArrayService.push(baseSlot, elements); } - public volatilePop(baseSlot: Fr): Fr[] { - return this.volatileArrayService.pop(baseSlot); + public ephemeralPop(baseSlot: Fr): Fr[] { + return this.ephemeralArrayService.pop(baseSlot); } - public volatileGet(baseSlot: Fr, index: number): Fr[] { - return this.volatileArrayService.get(baseSlot, index); + public ephemeralGet(baseSlot: Fr, index: number): Fr[] { + return this.ephemeralArrayService.get(baseSlot, index); } - public volatileSet(baseSlot: Fr, index: number, elements: Fr[]): void { - this.volatileArrayService.set(baseSlot, index, elements); + public ephemeralSet(baseSlot: Fr, index: number, elements: Fr[]): void { + this.ephemeralArrayService.set(baseSlot, index, elements); } - public volatileLen(baseSlot: Fr): number { - return this.volatileArrayService.len(baseSlot); + public ephemeralLen(baseSlot: Fr): number { + return this.ephemeralArrayService.len(baseSlot); } - public volatileRemove(baseSlot: Fr, index: number): void { - this.volatileArrayService.remove(baseSlot, index); + public ephemeralRemove(baseSlot: Fr, index: number): void { + this.ephemeralArrayService.remove(baseSlot, index); } - public volatileCopy(srcSlot: Fr, dstSlot: Fr, count: number): void { - this.volatileArrayService.copy(srcSlot, dstSlot, count); + public ephemeralCopy(srcSlot: Fr, dstSlot: Fr, count: number): void { + this.ephemeralArrayService.copy(srcSlot, dstSlot, count); } public emitOffchainEffect(data: Fr[]): Promise { diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index ea5476fc17d0..85f90d9319f0 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -906,30 +906,30 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_utl_volatile_push(foreignBaseSlot: ForeignCallSingle, foreignElements: ForeignCallArray) { + aztec_utl_ephemeral_push(foreignBaseSlot: ForeignCallSingle, foreignElements: ForeignCallArray) { const baseSlot = fromSingle(foreignBaseSlot); const elements = fromArray(foreignElements); - const newLen = this.handlerAsUtility().volatilePush(baseSlot, elements); + const newLen = this.handlerAsUtility().ephemeralPush(baseSlot, elements); return toForeignCallResult([toSingle(new Fr(newLen))]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_pop(foreignBaseSlot: ForeignCallSingle) { + aztec_utl_ephemeral_pop(foreignBaseSlot: ForeignCallSingle) { const baseSlot = fromSingle(foreignBaseSlot); - const element = this.handlerAsUtility().volatilePop(baseSlot); + const element = this.handlerAsUtility().ephemeralPop(baseSlot); return toForeignCallResult([toArray(element)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_get(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + aztec_utl_ephemeral_get(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { const baseSlot = fromSingle(foreignBaseSlot); const index = fromSingle(foreignIndex).toNumber(); - const element = this.handlerAsUtility().volatileGet(baseSlot, index); + const element = this.handlerAsUtility().ephemeralGet(baseSlot, index); return toForeignCallResult([toArray(element)]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_set( + aztec_utl_ephemeral_set( foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle, foreignElements: ForeignCallArray, @@ -937,27 +937,27 @@ export class RPCTranslator { const baseSlot = fromSingle(foreignBaseSlot); const index = fromSingle(foreignIndex).toNumber(); const elements = fromArray(foreignElements); - this.handlerAsUtility().volatileSet(baseSlot, index, elements); + this.handlerAsUtility().ephemeralSet(baseSlot, index, elements); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_len(foreignBaseSlot: ForeignCallSingle) { + aztec_utl_ephemeral_len(foreignBaseSlot: ForeignCallSingle) { const baseSlot = fromSingle(foreignBaseSlot); - const len = this.handlerAsUtility().volatileLen(baseSlot); + const len = this.handlerAsUtility().ephemeralLen(baseSlot); return toForeignCallResult([toSingle(new Fr(len))]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_remove(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + aztec_utl_ephemeral_remove(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { const baseSlot = fromSingle(foreignBaseSlot); const index = fromSingle(foreignIndex).toNumber(); - this.handlerAsUtility().volatileRemove(baseSlot, index); + this.handlerAsUtility().ephemeralRemove(baseSlot, index); return toForeignCallResult([]); } // eslint-disable-next-line camelcase - aztec_utl_volatile_copy( + aztec_utl_ephemeral_copy( foreignSrcSlot: ForeignCallSingle, foreignDstSlot: ForeignCallSingle, foreignCount: ForeignCallSingle, @@ -965,7 +965,7 @@ export class RPCTranslator { const srcSlot = fromSingle(foreignSrcSlot); const dstSlot = fromSingle(foreignDstSlot); const count = fromSingle(foreignCount).toNumber(); - this.handlerAsUtility().volatileCopy(srcSlot, dstSlot, count); + this.handlerAsUtility().ephemeralCopy(srcSlot, dstSlot, count); return toForeignCallResult([]); } From be3ff2c7c2bd46af57d8be03904fb6f6b2e1115f Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 10:42:29 +0000 Subject: [PATCH 04/10] use ephemeral for notes and events validation and storage --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 5 ++ .../src/messages/discovery/partial_notes.nr | 1 - .../src/messages/discovery/private_events.nr | 1 - .../src/messages/discovery/private_notes.nr | 1 - .../aztec/src/messages/processing/mod.nr | 65 ++++++++----------- .../aztec-nr/aztec/src/oracle/ephemeral.nr | 4 ++ .../test/custom_message_contract/src/main.nr | 1 - .../ephemeral_array_service.ts | 22 ++++--- .../oracle/interfaces.ts | 1 + .../oracle/oracle.ts | 6 ++ .../oracle/utility_execution_oracle.ts | 46 ++++--------- yarn-project/txe/src/rpc_translator.ts | 7 ++ 12 files changed, 75 insertions(+), 85 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 77ca7f34b29f..b13ac7a9835d 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -75,6 +75,11 @@ impl EphemeralArray { ephemeral::remove_oracle(self.base_slot, index); } + /// Removes all elements from the array. + pub unconstrained fn clear(self) { + ephemeral::clear_oracle(self.base_slot); + } + /// 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 diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index 744fda089ca7..c37b35b42173 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -167,7 +167,6 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( discovered_note.note_hash, discovered_note.inner_nullifier, log.tx_hash, - scope, ); }); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr index 5737768786ab..1b2faa186a87 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr @@ -30,7 +30,6 @@ pub(crate) unconstrained fn process_private_event_msg( serialized_event, event_commitment, tx_hash, - recipient, ); } else { aztecnr_warn_log_format!( diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index e56c798c7cca..3af6245c71f1 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -105,7 +105,6 @@ pub unconstrained fn attempt_note_discovery( discovered_note.note_hash, discovered_note.inner_nullifier, tx_hash, - recipient, ); }); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index b700b33f8ffc..0177959d4814 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -11,6 +11,7 @@ pub(crate) mod pending_tagged_log; use crate::{ capsules::CapsuleArray, + ephemeral::EphemeralArray, event::EventSelector, messages::{ discovery::partial_notes::DeliveredPendingPartialNote, @@ -86,28 +87,20 @@ pub unconstrained fn enqueue_note_for_validation( note_hash: Field, nullifier: Field, tx_hash: Field, - scope: AztecAddress, ) { - // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the - // Noir `NoteValidationRequest` - CapsuleArray::at( - contract_address, - NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, - scope, + EphemeralArray::at(NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( + NoteValidationRequest { + contract_address, + owner, + storage_slot, + randomness, + note_nonce, + packed_note, + note_hash, + nullifier, + tx_hash, + }, ) - .push( - NoteValidationRequest { - contract_address, - owner, - storage_slot, - randomness, - note_nonce, - packed_note, - note_hash, - nullifier, - tx_hash, - }, - ) } /// Enqueues an event for validation and storage by PXE. @@ -129,25 +122,17 @@ pub unconstrained fn enqueue_event_for_validation( serialized_event: BoundedVec, event_commitment: Field, tx_hash: Field, - scope: AztecAddress, ) { - // We store requests in a `CapsuleArray`, which PXE will later read from and deserialize into its version of the - // Noir `EventValidationRequest` - CapsuleArray::at( - contract_address, - EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, - scope, + EphemeralArray::at(EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).push( + EventValidationRequest { + contract_address, + event_type_id, + randomness, + serialized_event, + event_commitment, + tx_hash, + }, ) - .push( - EventValidationRequest { - contract_address, - event_type_id, - randomness, - serialized_event, - event_commitment, - tx_hash, - }, - ) } /// Validates and stores all enqueued notes and events. @@ -155,8 +140,6 @@ pub unconstrained fn enqueue_event_for_validation( /// Processes all requests enqueued via [`enqueue_note_for_validation`] and [`enqueue_event_for_validation`], inserting /// them into the note database and event store respectively, making them queryable via `get_notes` oracle and our TS /// API (PXE::getPrivateEvents). -/// -/// This automatically clears both validation request queues, so no further work needs to be done by the caller. pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_address: AztecAddress, scope: AztecAddress) { oracle::message_processing::validate_and_store_enqueued_notes_and_events( contract_address, @@ -166,6 +149,10 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre MAX_EVENT_SERIALIZED_LEN as Field, scope, ); + + // Purge the queues after processing. + EphemeralArray::::at(NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); + EphemeralArray::::at(EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); } /// Resolves message contexts for a list of tx hashes stored in a CapsuleArray. diff --git a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr index 07444708d4aa..faeddad1b659 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr @@ -31,3 +31,7 @@ pub(crate) unconstrained fn remove_oracle(base_slot: Field, index: u32) {} /// Copies `count` elements from the source array to the destination array (overwrites destination). #[oracle(aztec_utl_ephemeral_copy)] pub(crate) unconstrained fn copy_oracle(src_slot: Field, dst_slot: Field, count: u32) {} + +/// Removes all elements from the ephemeral array. +#[oracle(aztec_utl_ephemeral_clear)] +pub(crate) unconstrained fn clear_oracle(base_slot: Field) {} diff --git a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr index 6ec9821e5192..c984fddc3e75 100644 --- a/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/custom_message_contract/src/main.nr @@ -91,7 +91,6 @@ unconstrained fn handle_multi_log_message( serialized_event, event_commitment, message_context.tx_hash, - scope, ); } else { capsules::store(contract_address, slot, multi_log, scope); diff --git a/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts index beaec85f9e1d..43b4d9e95e36 100644 --- a/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts @@ -5,7 +5,8 @@ export class EphemeralArrayService { /** Maps base slot to array of elements, where each element is a serialized Fr[]. */ #arrays: Map = new Map(); - #getArray(baseSlot: Fr): Fr[][] { + /** Returns all elements in the array, or an empty array if uninitialized. */ + readArrayAt(baseSlot: Fr): Fr[][] { return this.#arrays.get(baseSlot.toString()) ?? []; } @@ -15,12 +16,12 @@ export class EphemeralArrayService { /** Returns the number of elements in the array at the given slot. */ len(baseSlot: Fr): number { - return this.#getArray(baseSlot).length; + return this.readArrayAt(baseSlot).length; } /** Appends an element to the array and returns the new length. */ push(baseSlot: Fr, elements: Fr[]): number { - const array = this.#getArray(baseSlot); + const array = this.readArrayAt(baseSlot); array.push(elements); this.#setArray(baseSlot, array); return array.length; @@ -28,7 +29,7 @@ export class EphemeralArrayService { /** Removes and returns the last element. Throws if empty. */ pop(baseSlot: Fr): Fr[] { - const array = this.#getArray(baseSlot); + const array = this.readArrayAt(baseSlot); if (array.length === 0) { throw new Error(`Ephemeral array at slot ${baseSlot} is empty`); } @@ -39,7 +40,7 @@ export class EphemeralArrayService { /** Returns the element at the given index. Throws if out of bounds. */ get(baseSlot: Fr, index: number): Fr[] { - const array = this.#getArray(baseSlot); + const array = this.readArrayAt(baseSlot); if (index < 0 || index >= array.length) { throw new Error( `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, @@ -50,7 +51,7 @@ export class EphemeralArrayService { /** Overwrites the element at the given index. Throws if out of bounds. */ set(baseSlot: Fr, index: number, value: Fr[]): void { - const array = this.#getArray(baseSlot); + const array = this.readArrayAt(baseSlot); if (index < 0 || index >= array.length) { throw new Error( `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, @@ -61,7 +62,7 @@ export class EphemeralArrayService { /** Removes the element at the given index, shifting subsequent elements backward. Throws if out of bounds. */ remove(baseSlot: Fr, index: number): void { - const array = this.#getArray(baseSlot); + const array = this.readArrayAt(baseSlot); if (index < 0 || index >= array.length) { throw new Error( `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${baseSlot}`, @@ -70,6 +71,11 @@ export class EphemeralArrayService { array.splice(index, 1); } + /** Removes all elements from the array. */ + clear(baseSlot: Fr): void { + this.#arrays.delete(baseSlot.toString()); + } + /** Allocates a fresh, unused base slot for a new ephemeral array. */ allocateSlot(): Fr { let slot: Fr; @@ -88,7 +94,7 @@ export class EphemeralArrayService { /** Copies `count` elements from the source array to the destination array (overwrites destination). */ copy(srcSlot: Fr, dstSlot: Fr, count: number): void { - const srcArray = this.#getArray(srcSlot); + const srcArray = this.readArrayAt(srcSlot); if (count > srcArray.length) { throw new Error( `Cannot copy ${count} elements from ephemeral array of length ${srcArray.length} at slot ${srcSlot}`, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 7a23000fb4a9..12fe5e17c89f 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -164,6 +164,7 @@ export interface IUtilityExecutionOracle { ephemeralLen(baseSlot: Fr): number; ephemeralRemove(baseSlot: Fr, index: number): void; ephemeralCopy(srcSlot: Fr, dstSlot: Fr, count: number): void; + ephemeralClear(baseSlot: Fr): void; } /** diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 7b0a6d3adc75..5493dca53c4d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -704,6 +704,12 @@ export class Oracle { return Promise.resolve([]); } + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_clear([baseSlot]: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralClear(Fr.fromString(baseSlot)); + return Promise.resolve([]); + } + // eslint-disable-next-line camelcase async aztec_utl_decryptAes128( ciphertextBVecStorage: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index a6070279ea4e..2ad1117ddf5e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -554,25 +554,15 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra throw new Error(`Got a note validation request from ${contractAddress}, expected ${this.contractAddress}`); } - // We read all note and event validation requests and process them all concurrently. This makes the process much - // faster as we don't need to wait for the network round-trip. - const noteValidationRequests = ( - await this.capsuleService.readCapsuleArray( - contractAddress, - noteValidationRequestsArrayBaseSlot, - this.jobId, - scope, - ) - ).map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); + // We read all note and event validation requests from ephemeral arrays and process them all concurrently. This + // makes the process much faster as we don't need to wait for the network round-trip. + const noteValidationRequests = this.ephemeralArrayService + .readArrayAt(noteValidationRequestsArrayBaseSlot) + .map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); - const eventValidationRequests = ( - await this.capsuleService.readCapsuleArray( - contractAddress, - eventValidationRequestsArrayBaseSlot, - this.jobId, - scope, - ) - ).map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); + const eventValidationRequests = this.ephemeralArrayService + .readArrayAt(eventValidationRequestsArrayBaseSlot) + .map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); const noteStorePromises = noteValidationRequests.map(request => @@ -604,22 +594,6 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ); await Promise.all([...noteStorePromises, ...eventStorePromises]); - - // Requests are cleared once we're done. - await this.capsuleService.setCapsuleArray( - contractAddress, - noteValidationRequestsArrayBaseSlot, - [], - this.jobId, - scope, - ); - await this.capsuleService.setCapsuleArray( - contractAddress, - eventValidationRequestsArrayBaseSlot, - [], - this.jobId, - scope, - ); } public async getLogsByTag( @@ -820,6 +794,10 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra this.ephemeralArrayService.copy(srcSlot, dstSlot, count); } + public ephemeralClear(baseSlot: Fr): void { + this.ephemeralArrayService.clear(baseSlot); + } + public emitOffchainEffect(data: Fr[]): Promise { this.offchainEffects.push({ data, contractAddress: this.contractAddress }); return Promise.resolve(); diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 85f90d9319f0..d16b1f16d61a 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -969,6 +969,13 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_clear(foreignBaseSlot: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + this.handlerAsUtility().ephemeralClear(baseSlot); + return toForeignCallResult([]); + } + // TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. // The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need // to implement this function here. Isn't there a way to programmatically identify that this is missing, given the From 7ae06bfa02e3f1a8db2fe95f70b8b88297af7e47 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 11:08:03 +0000 Subject: [PATCH 05/10] port message context resolution to ephemeral arrays --- .../aztec/src/messages/discovery/mod.nr | 8 +--- .../processing/event_validation_request.nr | 4 +- .../aztec/src/messages/processing/mod.nr | 19 -------- .../aztec/src/messages/processing/offchain.nr | 43 ++++++------------- .../aztec/src/oracle/message_processing.nr | 27 ++++-------- .../oracle/interfaces.ts | 1 + .../oracle/oracle.ts | 8 ++++ .../oracle/utility_execution_oracle.ts | 21 +++++++++ yarn-project/txe/src/rpc_translator.ts | 7 +++ 9 files changed, 61 insertions(+), 77 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr index 8a54dc32239c..75eb468bb1b4 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -8,7 +8,6 @@ pub mod private_notes; pub mod process_message; use crate::{ - capsules::CapsuleArray, messages::{ discovery::process_message::process_message_ciphertext, encoding::MAX_MESSAGE_CONTENT_LEN, @@ -150,8 +149,8 @@ pub unconstrained fn do_sync_state( }); if offchain_inbox_sync.is_some() { - let msgs: CapsuleArray = offchain_inbox_sync.unwrap()(contract_address, scope); - msgs.for_each(|i, msg| { + let msgs = offchain_inbox_sync.unwrap()(contract_address, scope); + msgs.for_each(|_i, msg: OffchainMessageWithContext| { process_message_ciphertext( contract_address, compute_note_hash, @@ -161,9 +160,6 @@ pub unconstrained fn do_sync_state( msg.message_context, scope, ); - // The inbox sync returns _a copy_ of messages to process, so we clear them as we do so. This is a - // ephemeral array with the to-process message, not the actual persistent storage of them. - msgs.remove(i); }); } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr index 98ef074b84e5..4c373e368dae 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/event_validation_request.nr @@ -2,8 +2,8 @@ use crate::{event::EventSelector, messages::logs::event::MAX_EVENT_SERIALIZED_LE use crate::protocol::{address::AztecAddress, traits::Serialize}; /// Intermediate struct used to perform batch event validation by PXE. The -/// `aztec_utl_validateAndStoreEnqueuedNotesAndEvents` oracle expects for values of this type to be stored in a -/// `CapsuleArray` at the given `base_slot`. +/// `aztec_utl_validateAndStoreEnqueuedNotesAndEvents` oracle expects for values of this type to be stored in an +/// `EphemeralArray` at the given `base_slot`. #[derive(Serialize)] pub(crate) struct EventValidationRequest { pub contract_address: AztecAddress, diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 0177959d4814..f69815a84d13 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -155,25 +155,6 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre EphemeralArray::::at(EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); } -/// Resolves message contexts for a list of tx hashes stored in a CapsuleArray. -/// -/// The `message_context_requests_array_base_slot` must point to a CapsuleArray containing tx hashes. -/// PXE will store `Option` values into the responses array at -/// `message_context_responses_array_base_slot`. -pub unconstrained fn get_message_contexts_by_tx_hash( - contract_address: AztecAddress, - message_context_requests_array_base_slot: Field, - message_context_responses_array_base_slot: Field, - scope: AztecAddress, -) { - oracle::message_processing::get_message_contexts_by_tx_hash( - contract_address, - message_context_requests_array_base_slot, - message_context_responses_array_base_slot, - scope, - ); -} - /// Efficiently queries the node for logs that result in the completion of all `DeliveredPendingPartialNote`s stored in /// a `CapsuleArray` by performing all node communication concurrently. Returns a second `CapsuleArray` with Options /// for the responses that correspond to the pending partial notes at the same index. diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index 8e62cbfc2b27..5f29717e0ee4 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -1,10 +1,8 @@ use crate::{ capsules::CapsuleArray, context::UtilityContext, - messages::{ - encoding::MESSAGE_CIPHERTEXT_LEN, - processing::{get_message_contexts_by_tx_hash, MessageContext, OffchainMessageWithContext}, - }, + ephemeral::EphemeralArray, + messages::{encoding::MESSAGE_CIPHERTEXT_LEN, processing::OffchainMessageWithContext}, oracle::contract_sync::set_contract_sync_cache_invalid, protocol::{ address::AztecAddress, @@ -19,13 +17,10 @@ use crate::{ /// This is the slot where we accumulate messages received through [`receive`]. global OFFCHAIN_INBOX_SLOT: Field = sha256_to_field("AZTEC_NR::OFFCHAIN_INBOX_SLOT".as_bytes()); -/// Capsule array slot used by [`sync_inbox`] to pass tx hash resolution requests to PXE. +/// Ephemeral array slot used by [`sync_inbox`] to pass tx hash resolution requests to PXE. global OFFCHAIN_CONTEXT_REQUESTS_SLOT: Field = sha256_to_field("AZTEC_NR::OFFCHAIN_CONTEXT_REQUESTS_SLOT".as_bytes()); -/// Capsule array slot used by [`sync_inbox`] to read tx context responses from PXE. -global OFFCHAIN_CONTEXT_RESPONSES_SLOT: Field = sha256_to_field("AZTEC_NR::OFFCHAIN_CONTEXT_RESPONSES_SLOT".as_bytes()); - -/// Capsule array slot used by [`sync_inbox`] to collect messages ready for processing. +/// Ephemeral array slot used by [`sync_inbox`] to collect messages ready for processing. global OFFCHAIN_READY_MESSAGES_SLOT: Field = sha256_to_field("AZTEC_NR::OFFCHAIN_READY_MESSAGES_SLOT".as_bytes()); /// Maximum number of offchain messages accepted by `offchain_receive` in a single call. @@ -54,7 +49,7 @@ global MAX_MSG_TTL: u64 = MAX_TX_LIFETIME + TX_EXPIRATION_TOLERANCE; /// The only current implementation of an `OffchainInboxSync` is [`sync_inbox`], which manages an inbox with expiration /// based eviction and automatic transaction context resolution. pub(crate) type OffchainInboxSync = unconstrained fn[Env]( -/* contract_address */AztecAddress, /* scope */ AztecAddress) -> CapsuleArray; +/* contract_address */AztecAddress, /* scope */ AztecAddress) -> EphemeralArray; /// A message delivered via the `offchain_receive` utility function. pub struct OffchainMessage { @@ -143,21 +138,10 @@ pub unconstrained fn receive( pub unconstrained fn sync_inbox( contract_address: AztecAddress, scope: AztecAddress, -) -> CapsuleArray { +) -> EphemeralArray { let inbox: CapsuleArray = CapsuleArray::at(contract_address, OFFCHAIN_INBOX_SLOT, scope); - let context_resolution_requests: CapsuleArray = - CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_REQUESTS_SLOT, scope); - let resolved_contexts: CapsuleArray> = - CapsuleArray::at(contract_address, OFFCHAIN_CONTEXT_RESPONSES_SLOT, scope); - let ready_to_process: CapsuleArray = - CapsuleArray::at(contract_address, OFFCHAIN_READY_MESSAGES_SLOT, scope); - - // Clear any stale ready messages from a previous run. - ready_to_process.for_each(|i, _| { ready_to_process.remove(i); }); - - // Clear any stale context resolution requests/responses from a previous run. - context_resolution_requests.for_each(|i, _| { context_resolution_requests.remove(i); }); - resolved_contexts.for_each(|i, _| { resolved_contexts.remove(i); }); + let context_resolution_requests: EphemeralArray = EphemeralArray::at(OFFCHAIN_CONTEXT_REQUESTS_SLOT); + let ready_to_process: EphemeralArray = EphemeralArray::at(OFFCHAIN_READY_MESSAGES_SLOT); // Build a request list aligned with the inbox indices. let mut i = 0; @@ -168,13 +152,10 @@ pub unconstrained fn sync_inbox( i += 1; } - // Ask PXE to resolve contexts for all requested tx hashes. - get_message_contexts_by_tx_hash( - contract_address, - OFFCHAIN_CONTEXT_REQUESTS_SLOT, - OFFCHAIN_CONTEXT_RESPONSES_SLOT, - scope, - ); + // Ask PXE to resolve contexts for all requested tx hashes. The oracle returns responses in a new + // ephemeral array. + let resolved_contexts = + crate::oracle::message_processing::get_message_contexts_by_tx_hash(OFFCHAIN_CONTEXT_REQUESTS_SLOT); assert_eq(resolved_contexts.len(), inbox_len); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 0d49c8d6b1d2..25665fc2153c 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,5 +1,5 @@ use crate::ephemeral::EphemeralArray; -use crate::messages::processing::pending_tagged_log::PendingTaggedLog; +use crate::messages::processing::{MessageContext, pending_tagged_log::PendingTaggedLog}; use crate::protocol::address::AztecAddress; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and @@ -64,24 +64,13 @@ unconstrained fn get_logs_by_tag_oracle( scope: AztecAddress, ) {} +/// Resolves message contexts for tx hashes in an ephemeral request array and returns a response ephemeral array. pub(crate) unconstrained fn get_message_contexts_by_tx_hash( - contract_address: AztecAddress, - message_context_requests_array_base_slot: Field, - message_context_responses_array_base_slot: Field, - scope: AztecAddress, -) { - get_message_contexts_by_tx_hash_oracle( - contract_address, - message_context_requests_array_base_slot, - message_context_responses_array_base_slot, - scope, - ); + request_array_base_slot: Field, +) -> EphemeralArray> { + let response_slot = get_message_contexts_by_tx_hash_v2_oracle(request_array_base_slot); + EphemeralArray::at(response_slot) } -#[oracle(aztec_utl_getMessageContextsByTxHash)] -unconstrained fn get_message_contexts_by_tx_hash_oracle( - contract_address: AztecAddress, - message_context_requests_array_base_slot: Field, - message_context_responses_array_base_slot: Field, - scope: AztecAddress, -) {} +#[oracle(aztec_utl_getMessageContextsByTxHash_v2)] +unconstrained fn get_message_contexts_by_tx_hash_v2_oracle(request_array_base_slot: Field) -> Field {} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 12fe5e17c89f..96916ceafa79 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -135,6 +135,7 @@ export interface IUtilityExecutionOracle { logRetrievalResponsesArrayBaseSlot: Fr, scope: AztecAddress, ): Promise; + getMessageContextsByTxHashV2(requestArrayBaseSlot: Fr): Promise; getMessageContextsByTxHash( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index 5493dca53c4d..ec4768dcdaf8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -582,6 +582,14 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + async aztec_utl_getMessageContextsByTxHash_v2([requestArrayBaseSlot]: ACVMField[]): Promise { + const responseSlot = await this.handlerAsUtility().getMessageContextsByTxHashV2( + Fr.fromString(requestArrayBaseSlot), + ); + return [toACVMField(responseSlot)]; + } + // eslint-disable-next-line camelcase aztec_utl_setCapsule( [contractAddress]: ACVMField[], diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 2ad1117ddf5e..d1e3752067cd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -690,6 +690,27 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } } + /** Reads tx hash requests from an ephemeral array, resolves their contexts, and returns the response slot. */ + public async getMessageContextsByTxHashV2(requestArrayBaseSlot: Fr): Promise { + const requestFields = this.ephemeralArrayService.readArrayAt(requestArrayBaseSlot); + + const txHashes = requestFields.map((fields, i) => { + if (fields.length !== 1) { + throw new Error( + `Malformed message context request at index ${i}: expected 1 field (tx hash), got ${fields.length}`, + ); + } + return fields[0]; + }); + + const maybeMessageContexts = await this.messageContextService.getMessageContextsByTxHash( + txHashes, + this.anchorBlockHeader.getBlockNumber(), + ); + + return this.ephemeralArrayService.newArray(maybeMessageContexts.map(MessageContext.toSerializedOption)); + } + public setCapsule(contractAddress: AztecAddress, slot: Fr, capsule: Fr[], scope: AztecAddress): void { if (!contractAddress.equals(this.contractAddress)) { // TODO(#10727): instead of this check that this.contractAddress is allowed to access the external DB diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index d16b1f16d61a..d0f67d60290f 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -829,6 +829,13 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + async aztec_utl_getMessageContextsByTxHash_v2(foreignRequestArrayBaseSlot: ForeignCallSingle) { + const requestArrayBaseSlot = fromSingle(foreignRequestArrayBaseSlot); + const responseSlot = await this.handlerAsUtility().getMessageContextsByTxHashV2(requestArrayBaseSlot); + return toForeignCallResult([toSingle(responseSlot)]); + } + // eslint-disable-next-line camelcase aztec_utl_setCapsule( foreignContractAddress: ForeignCallSingle, From 952813a1e5d8304d5a6f2405bdd0471cec056af3 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 12:27:59 +0000 Subject: [PATCH 06/10] port pending partial notes request arrays to ephemeral --- .../src/messages/discovery/partial_notes.nr | 7 +---- .../aztec/src/messages/processing/mod.nr | 28 +++--------------- .../aztec/src/oracle/message_processing.nr | 29 +++++++------------ .../oracle/interfaces.ts | 1 + .../oracle/oracle.ts | 6 ++++ .../oracle/utility_execution_oracle.ts | 11 +++++++ yarn-project/txe/src/rpc_translator.ts | 7 +++++ 7 files changed, 40 insertions(+), 49 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index c37b35b42173..ff61bfc6b69b 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -88,8 +88,7 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( // Each of the pending partial notes might get completed by a log containing its public values. For performance // reasons, we fetch all of these logs concurrently and then process them one by one, minimizing the amount of time // waiting for the node roundtrip. - let maybe_completion_logs = - get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes, scope); + let maybe_completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes); // Each entry in the maybe completion logs array corresponds to the entry in the pending partial notes array at the // same index. This means we can use the same index as we iterate through the responses to get both the partial @@ -97,10 +96,6 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( assert_eq(maybe_completion_logs.len(), pending_partial_notes.len()); maybe_completion_logs.for_each(|i, maybe_log: Option| { - // We clear the completion logs as we read them so that the array is empty by the time we next query it. - // TODO(#14943): use ephemeral arrays to avoid having to manually clear this. - maybe_completion_logs.remove(i); - let pending_partial_note = pending_partial_notes.get(i); if maybe_log.is_none() { diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index f69815a84d13..1e57c5f350f1 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -44,10 +44,6 @@ global LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT: Field = sha256_to_field( "AZTEC_NR::LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT".as_bytes(), ); -global LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT: Field = sha256_to_field( - "AZTEC_NR::LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT".as_bytes(), -); - /// An offchain-delivered message with resolved context, ready for processing during sync. #[derive(Serialize, Deserialize)] pub struct OffchainMessageWithContext { @@ -160,18 +156,13 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre /// for the responses that correspond to the pending partial notes at the same index. /// /// For example, given an array with pending partial notes `[ p1, p2, p3 ]`, where `p1` and `p3` have corresponding -/// completion logs but `p2` does not, the returned `CapsuleArray` will have contents `[some(p1_log), none(), +/// completion logs but `p2` does not, the returned `EphemeralArray` will have contents `[some(p1_log), none(), /// some(p3_log)]`. pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( contract_address: AztecAddress, pending_partial_notes: CapsuleArray, - scope: AztecAddress, -) -> CapsuleArray> { - let log_retrieval_requests = CapsuleArray::at( - contract_address, - LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, - scope, - ); +) -> EphemeralArray> { + let log_retrieval_requests = EphemeralArray::at(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT); // We create a LogRetrievalRequest for each PendingPartialNote in the CapsuleArray. Because we need the indices in // the request array to match the indices in the partial note array, we can't use CapsuleArray::for_each, as that @@ -191,16 +182,5 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( i += 1; } - oracle::message_processing::get_logs_by_tag( - contract_address, - LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT, - LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, - scope, - ); - - CapsuleArray::at( - contract_address, - LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, - scope, - ) + oracle::message_processing::get_logs_by_tag(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT) } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 25665fc2153c..0d22343b437b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,5 +1,7 @@ use crate::ephemeral::EphemeralArray; -use crate::messages::processing::{MessageContext, pending_tagged_log::PendingTaggedLog}; +use crate::messages::processing::{ + log_retrieval_response::LogRetrievalResponse, MessageContext, pending_tagged_log::PendingTaggedLog, +}; use crate::protocol::address::AztecAddress; /// Finds new private logs that may have been sent to all registered accounts in PXE in the current contract and @@ -42,27 +44,16 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( scope: AztecAddress, ) {} +/// Fetches logs by tag from an ephemeral request array and returns a response ephemeral array. pub(crate) unconstrained fn get_logs_by_tag( - contract_address: AztecAddress, - log_retrieval_requests_array_base_slot: Field, - log_retrieval_responses_array_base_slot: Field, - scope: AztecAddress, -) { - get_logs_by_tag_oracle( - contract_address, - log_retrieval_requests_array_base_slot, - log_retrieval_responses_array_base_slot, - scope, - ); + request_array_base_slot: Field, +) -> EphemeralArray> { + let response_slot = get_logs_by_tag_v2_oracle(request_array_base_slot); + EphemeralArray::at(response_slot) } -#[oracle(aztec_utl_getLogsByTag)] -unconstrained fn get_logs_by_tag_oracle( - contract_address: AztecAddress, - log_retrieval_requests_array_base_slot: Field, - log_retrieval_responses_array_base_slot: Field, - scope: AztecAddress, -) {} +#[oracle(aztec_utl_getLogsByTag_v2)] +unconstrained fn get_logs_by_tag_v2_oracle(request_array_base_slot: Field) -> Field {} /// Resolves message contexts for tx hashes in an ephemeral request array and returns a response ephemeral array. pub(crate) unconstrained fn get_message_contexts_by_tx_hash( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts index 96916ceafa79..ef99416e818e 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts @@ -135,6 +135,7 @@ export interface IUtilityExecutionOracle { logRetrievalResponsesArrayBaseSlot: Fr, scope: AztecAddress, ): Promise; + getLogsByTagV2(requestArrayBaseSlot: Fr): Promise; getMessageContextsByTxHashV2(requestArrayBaseSlot: Fr): Promise; getMessageContextsByTxHash( contractAddress: AztecAddress, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts index ec4768dcdaf8..d38fb71e571b 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -582,6 +582,12 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + async aztec_utl_getLogsByTag_v2([requestArrayBaseSlot]: ACVMField[]): Promise { + const responseSlot = await this.handlerAsUtility().getLogsByTagV2(Fr.fromString(requestArrayBaseSlot)); + return [toACVMField(responseSlot)]; + } + // eslint-disable-next-line camelcase async aztec_utl_getMessageContextsByTxHash_v2([requestArrayBaseSlot]: ACVMField[]): Promise { const responseSlot = await this.handlerAsUtility().getMessageContextsByTxHashV2( diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index d1e3752067cd..4650a2b2c234 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -635,6 +635,17 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ); } + public async getLogsByTagV2(requestArrayBaseSlot: Fr): Promise { + const logRetrievalRequests = this.ephemeralArrayService + .readArrayAt(requestArrayBaseSlot) + .map(LogRetrievalRequest.fromFields); + const logService = this.#createLogService(); + + const maybeLogRetrievalResponses = await logService.fetchLogsByTag(this.contractAddress, logRetrievalRequests); + + return this.ephemeralArrayService.newArray(maybeLogRetrievalResponses.map(LogRetrievalResponse.toSerializedOption)); + } + public async getMessageContextsByTxHash( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index d0f67d60290f..6474339fa153 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -829,6 +829,13 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + async aztec_utl_getLogsByTag_v2(foreignRequestArrayBaseSlot: ForeignCallSingle) { + const requestArrayBaseSlot = fromSingle(foreignRequestArrayBaseSlot); + const responseSlot = await this.handlerAsUtility().getLogsByTagV2(requestArrayBaseSlot); + return toForeignCallResult([toSingle(responseSlot)]); + } + // eslint-disable-next-line camelcase async aztec_utl_getMessageContextsByTxHash_v2(foreignRequestArrayBaseSlot: ForeignCallSingle) { const requestArrayBaseSlot = fromSingle(foreignRequestArrayBaseSlot); From 5e031a89532460146b8667170b5612ed7a55fec1 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 12:39:58 +0000 Subject: [PATCH 07/10] simplify signatures thanks to ephemeral arrays --- noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr | 2 +- .../aztec-nr/aztec/src/messages/processing/mod.nr | 10 +++++++--- .../aztec/src/messages/processing/offchain.nr | 2 +- .../aztec-nr/aztec/src/oracle/message_processing.nr | 11 ++++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index b13ac7a9835d..1441601f0346 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -18,7 +18,7 @@ use crate::protocol::traits::{Deserialize, Serialize}; /// For data that needs to persist across simulations, contract calls, etc, use /// [`CapsuleArray`](crate::capsules::CapsuleArray) instead. pub struct EphemeralArray { - base_slot: Field, + pub base_slot: Field, } impl EphemeralArray { diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 1e57c5f350f1..6c1060b330ee 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -164,8 +164,8 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( ) -> EphemeralArray> { let log_retrieval_requests = EphemeralArray::at(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT); - // We create a LogRetrievalRequest for each PendingPartialNote in the CapsuleArray. Because we need the indices in - // the request array to match the indices in the partial note array, we can't use CapsuleArray::for_each, as that + // We create a LogRetrievalRequest for each PendingPartialNote in the EphemeralArray. Because we need the indices in + // the request array to match the indices in the partial note array, we can't use EphemeralArray::for_each, as that // function has arbitrary iteration order. Instead, we manually iterate the array from the beginning and push into // the requests array, which we expect to be empty. let mut i = 0; @@ -182,5 +182,9 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( i += 1; } - oracle::message_processing::get_logs_by_tag(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT) + let responses = oracle::message_processing::get_logs_by_tag(log_retrieval_requests); + + log_retrieval_requests.clear(); + + responses } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index 5f29717e0ee4..dbe84b4edbc5 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -155,7 +155,7 @@ pub unconstrained fn sync_inbox( // Ask PXE to resolve contexts for all requested tx hashes. The oracle returns responses in a new // ephemeral array. let resolved_contexts = - crate::oracle::message_processing::get_message_contexts_by_tx_hash(OFFCHAIN_CONTEXT_REQUESTS_SLOT); + crate::oracle::message_processing::get_message_contexts_by_tx_hash(context_resolution_requests); assert_eq(resolved_contexts.len(), inbox_len); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index 0d22343b437b..7bc158d2f365 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,6 +1,7 @@ use crate::ephemeral::EphemeralArray; use crate::messages::processing::{ - log_retrieval_response::LogRetrievalResponse, MessageContext, pending_tagged_log::PendingTaggedLog, + log_retrieval_request::LogRetrievalRequest, log_retrieval_response::LogRetrievalResponse, MessageContext, + pending_tagged_log::PendingTaggedLog, }; use crate::protocol::address::AztecAddress; @@ -46,9 +47,9 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( /// Fetches logs by tag from an ephemeral request array and returns a response ephemeral array. pub(crate) unconstrained fn get_logs_by_tag( - request_array_base_slot: Field, + requests: EphemeralArray, ) -> EphemeralArray> { - let response_slot = get_logs_by_tag_v2_oracle(request_array_base_slot); + let response_slot = get_logs_by_tag_v2_oracle(requests.base_slot); EphemeralArray::at(response_slot) } @@ -57,9 +58,9 @@ unconstrained fn get_logs_by_tag_v2_oracle(request_array_base_slot: Field) -> Fi /// Resolves message contexts for tx hashes in an ephemeral request array and returns a response ephemeral array. pub(crate) unconstrained fn get_message_contexts_by_tx_hash( - request_array_base_slot: Field, + requests: EphemeralArray, ) -> EphemeralArray> { - let response_slot = get_message_contexts_by_tx_hash_v2_oracle(request_array_base_slot); + let response_slot = get_message_contexts_by_tx_hash_v2_oracle(requests.base_slot); EphemeralArray::at(response_slot) } From d5eac0bd440dce4fdd4dc826fea4771dcc769ff8 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 13:07:23 +0000 Subject: [PATCH 08/10] minutiae --- noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr | 4 ++-- .../aztec/src/messages/processing/note_validation_request.nr | 2 +- .../aztec-nr/aztec/src/messages/processing/offchain.nr | 3 ++- .../noir-structs/event_validation_request.ts | 2 +- .../noir-structs/log_retrieval_request.ts | 2 +- .../noir-structs/log_retrieval_response.ts | 2 +- .../noir-structs/note_validation_request.ts | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr index 6c1060b330ee..e139a57bfc5a 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -19,7 +19,7 @@ use crate::{ logs::{event::MAX_EVENT_SERIALIZED_LEN, note::MAX_NOTE_PACKED_LEN}, processing::{ log_retrieval_request::LogRetrievalRequest, log_retrieval_response::LogRetrievalResponse, - note_validation_request::NoteValidationRequest, pending_tagged_log::PendingTaggedLog, + note_validation_request::NoteValidationRequest, }, }, oracle, @@ -152,7 +152,7 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre } /// Efficiently queries the node for logs that result in the completion of all `DeliveredPendingPartialNote`s stored in -/// a `CapsuleArray` by performing all node communication concurrently. Returns a second `CapsuleArray` with Options +/// a `CapsuleArray` by performing all node communication concurrently. Returns an `EphemeralArray` with Options /// for the responses that correspond to the pending partial notes at the same index. /// /// For example, given an array with pending partial notes `[ p1, p2, p3 ]`, where `p1` and `p3` have corresponding diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr index 0d7c101eef38..24dd806948d7 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/note_validation_request.nr @@ -3,7 +3,7 @@ use crate::protocol::{address::AztecAddress, traits::Serialize}; /// Intermediate struct used to perform batch note validation by PXE. The /// `aztec_utl_validateAndStoreEnqueuedNotesAndEvents` oracle expects for values of this type to be stored in a -/// `CapsuleArray`. +/// `EphemeralArray`. #[derive(Serialize)] pub(crate) struct NoteValidationRequest { pub contract_address: AztecAddress, diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr index dbe84b4edbc5..db165562a892 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain.nr @@ -175,7 +175,8 @@ pub unconstrained fn sync_inbox( // sit in the inbox until it expires. // // 3. The TX that emitted this message has been found by PXE. That gives us all the information needed to - // process the message. We add the message to the `ready_to_process` CapsuleArray so that the `sync_state` loop + // process the message. We add the message to the `ready_to_process` EphemeralArray so that the `sync_state` + // loop // processes it. // // In all cases, if the message has expired (i.e. `now > anchor_block_timestamp + MAX_MSG_TTL`), we remove it diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts index 992afadc74b5..734ae8268678 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/event_validation_request.ts @@ -6,7 +6,7 @@ import { TxHash } from '@aztec/stdlib/tx'; /** * Intermediate struct used to perform batch event validation by PXE. The `utilityValidateAndStoreEnqueuedNotesAndEvents` oracle - * expects for values of this type to be stored in a `CapsuleArray`. + * expects for values of this type to be stored in a `EphemeralArray`. */ export class EventValidationRequest { constructor( diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts index 51dc571f4b97..377213a23106 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_request.ts @@ -5,7 +5,7 @@ import { Tag } from '@aztec/stdlib/logs'; /** * Intermediate struct used to perform batch log retrieval by PXE. The `utilityBulkRetrieveLogs` oracle expects values of this - * type to be stored in a `CapsuleArray`. + * type to be stored in a `EphemeralArray`. */ export class LogRetrievalRequest { constructor( diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts index baa4b3c58e39..590fd28c1c84 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/log_retrieval_response.ts @@ -7,7 +7,7 @@ const MAX_LOG_CONTENT_LEN = PRIVATE_LOG_CIPHERTEXT_LEN; /** * Intermediate struct used to perform batch log retrieval by PXE. The `utilityBulkRetrieveLogs` oracle stores values of this - * type in a `CapsuleArray`. + * type in a `EphemeralArray`. */ export class LogRetrievalResponse { constructor( diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts index 355a6a03a858..2bbd45b94094 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/note_validation_request.ts @@ -5,7 +5,7 @@ import { TxHash } from '@aztec/stdlib/tx'; /** * Intermediate struct used to perform batch note validation by PXE. The `utilityValidateAndStoreEnqueuedNotesAndEvents` oracle - * expects for values of this type to be stored in a `CapsuleArray`. + * expects for values of this type to be stored in a `EphemeralArray`. */ export class NoteValidationRequest { constructor( From 50073ec6a0ce7aada2facdba60a7e55320b074cc Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 14:13:38 +0000 Subject: [PATCH 09/10] some initial tests of ephemeral array isolation --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 5 +- noir-projects/noir-contracts/Nargo.toml | 2 + .../test/ephemeral_child_contract/Nargo.toml | 8 +++ .../test/ephemeral_child_contract/src/main.nr | 20 ++++++ .../test/ephemeral_parent_contract/Nargo.toml | 9 +++ .../ephemeral_parent_contract/src/main.nr | 61 +++++++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/src/main.nr diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 1441601f0346..790d8e46548d 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -101,6 +101,7 @@ impl EphemeralArray { mod test { use crate::test::helpers::test_environment::TestEnvironment; + use crate::test::mocks::MockStruct; use super::EphemeralArray; global SLOT: Field = 1230; @@ -301,9 +302,7 @@ mod test { unconstrained fn works_with_multi_field_type() { let _ = TestEnvironment::new(); - use crate::test::mocks::MockStruct; - - let array = EphemeralArray::at(SLOT); + let array: EphemeralArray = EphemeralArray::at(SLOT); let a = MockStruct::new(5, 6); let b = MockStruct::new(7, 8); diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 8bc900840e8a..0f876dd56034 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -43,6 +43,8 @@ members = [ "contracts/test/child_contract", "contracts/test/counter/counter_contract", "contracts/test/custom_message_contract", + "contracts/test/ephemeral_child_contract", + "contracts/test/ephemeral_parent_contract", "contracts/test/event_only_contract", "contracts/test/large_public_event_contract", "contracts/test/import_test_contract", diff --git a/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/Nargo.toml new file mode 100644 index 000000000000..25f635a16650 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ephemeral_child_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/src/main.nr new file mode 100644 index 000000000000..f2ec6798ff14 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/ephemeral_child_contract/src/main.nr @@ -0,0 +1,20 @@ +// A contract used along with `EphemeralParent` to test ephemeral array isolation across nested call frames. +use aztec::macros::aztec; + +#[aztec] +pub contract EphemeralChild { + use aztec::{ephemeral::EphemeralArray, macros::functions::external}; + + /// Pushes different values to an ephemeral array at the same slot used by the parent. + /// If isolation is broken, this would overwrite the parent's data. + #[external("private")] + fn use_ephemeral_at_slot(slot: Field) { + // Safety: these ephemeral array operations are unconstrained oracle calls used for testing isolation. + unsafe { + let array: EphemeralArray = EphemeralArray::at(slot); + array.push(999); + array.push(888); + array.push(777); + } + } +} diff --git a/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/Nargo.toml new file mode 100644 index 000000000000..2ab65dcf036b --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/Nargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ephemeral_parent_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } +ephemeral_child_contract = { path = "../ephemeral_child_contract" } diff --git a/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/src/main.nr new file mode 100644 index 000000000000..3854dcb8aaf8 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/ephemeral_parent_contract/src/main.nr @@ -0,0 +1,61 @@ +// A contract used along with `EphemeralChild` to test ephemeral array isolation across nested call frames. +use aztec::macros::aztec; + +#[aztec] +pub contract EphemeralParent { + use aztec::{ + ephemeral::EphemeralArray, + macros::functions::external, + protocol::{abis::function_selector::FunctionSelector, address::AztecAddress}, + }; + + global EPHEMERAL_SLOT: Field = 42; + + /// Populates an ephemeral array, calls the child contract (which writes to the same slot in its own + /// frame), then verifies that the parent's data is untouched. + #[external("private")] + fn test_isolation(child_address: AztecAddress) -> pub [Field; 3] { + // Safety: ephemeral array operations are unconstrained oracle calls used for testing isolation. + unsafe { + let array: EphemeralArray = EphemeralArray::at(EPHEMERAL_SLOT); + array.push(100); + array.push(200); + } + + // Call the child's private function, which pushes to the same slot in its own frame. + let child_selector = + comptime { FunctionSelector::from_signature("use_ephemeral_at_slot(Field)") }; + let _ = self.context.call_private_function(child_address, child_selector, [EPHEMERAL_SLOT]); + + // Safety: reading back from the parent's ephemeral array to verify isolation. + unsafe { + let array: EphemeralArray = EphemeralArray::at(EPHEMERAL_SLOT); + [array.len() as Field, array.get(0), array.get(1)] + } + } +} + +mod test { + use crate::EphemeralParent; + use aztec::test::helpers::test_environment::TestEnvironment; + + #[test] + unconstrained fn ephemeral_arrays_are_isolated_across_nested_private_calls() { + let mut env = TestEnvironment::new(); + let caller = env.create_light_account(); + + let parent_address = env.deploy("EphemeralParent").without_initializer(); + let child_address = + env.deploy("@ephemeral_child_contract/EphemeralChild").without_initializer(); + + let result: [Field; 3] = env.call_private( + caller, + EphemeralParent::at(parent_address).test_isolation(child_address), + ); + + // The parent should still see its own data: length 2, values [100, 200]. + assert_eq(result[0], 2, "parent ephemeral array length should be 2"); + assert_eq(result[1], 100, "parent ephemeral array[0] should be 100"); + assert_eq(result[2], 200, "parent ephemeral array[1] should be 200"); + } +} From 578e87401feadcf8be337457e896d4c40dae86b9 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Tue, 31 Mar 2026 14:22:11 +0000 Subject: [PATCH 10/10] minutiae --- noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr | 6 +++--- .../aztec-nr/aztec/src/messages/discovery/private_events.nr | 1 - .../aztec-nr/aztec/src/messages/discovery/private_notes.nr | 1 - noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr | 4 ---- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr index 790d8e46548d..76034299cd1f 100644 --- a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -4,11 +4,11 @@ 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 set of ephemeral arrays child simulations cannot see the parent's ephemeral arrays, -/// and vice versa. +/// 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 and more appropriate for transient data. +/// cheaper than capsule arrays and more appropriate for transient data that is never supposed to be persisted anyway. /// /// ## Use Cases /// diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr index 1b2faa186a87..a52be3fa214b 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_events.nr @@ -13,7 +13,6 @@ pub(crate) unconstrained fn process_private_event_msg( msg_metadata: u64, msg_content: BoundedVec, tx_hash: Field, - recipient: AztecAddress, ) { let decoded = decode_private_event_message(msg_metadata, msg_content); diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index 3af6245c71f1..51be3e3ed05d 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -62,7 +62,6 @@ pub unconstrained fn attempt_note_discovery( randomness: Field, note_type_id: Field, packed_note: BoundedVec, - recipient: AztecAddress, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr index faeddad1b659..920974ad7a56 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr @@ -28,10 +28,6 @@ pub(crate) unconstrained fn len_oracle(base_slot: Field) -> u32 {} #[oracle(aztec_utl_ephemeral_remove)] pub(crate) unconstrained fn remove_oracle(base_slot: Field, index: u32) {} -/// Copies `count` elements from the source array to the destination array (overwrites destination). -#[oracle(aztec_utl_ephemeral_copy)] -pub(crate) unconstrained fn copy_oracle(src_slot: Field, dst_slot: Field, count: u32) {} - /// Removes all elements from the ephemeral array. #[oracle(aztec_utl_ephemeral_clear)] pub(crate) unconstrained fn clear_oracle(base_slot: Field) {}