diff --git a/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr new file mode 100644 index 000000000000..76034299cd1f --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -0,0 +1,320 @@ +use crate::oracle::ephemeral; +use crate::protocol::traits::{Deserialize, Serialize}; + +/// A dynamically sized array that exists only during a single contract call frame. +/// +/// Ephemeral arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract +/// call frame gets its own isolated slot space of ephemeral arrays. Child simulations cannot see the parent's +/// ephemeral arrays, and vice versa. +/// +/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making ephemeral arrays significantly +/// cheaper than capsule arrays and more appropriate for transient data that is never supposed to be persisted anyway. +/// +/// ## Use Cases +/// +/// 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 EphemeralArray { + pub base_slot: Field, +} + +impl EphemeralArray { + /// Creates an ephemeral array at the given base slot. + /// + /// 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 { + ephemeral::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 _ = ephemeral::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 = ephemeral::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 = ephemeral::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(); + 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) { + 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 + /// 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 crate::test::mocks::MockStruct; + use super::EphemeralArray; + + global SLOT: Field = 1230; + global OTHER_SLOT: Field = 5670; + + #[test] + unconstrained fn empty_array() { + let _ = TestEnvironment::new(); + + let array: EphemeralArray = EphemeralArray::at(SLOT); + assert_eq(array.len(), 0); + } + + #[test(should_fail)] + unconstrained fn empty_array_read() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + let _: Field = array.get(0); + } + + #[test(should_fail)] + unconstrained fn empty_array_pop() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + let _: Field = array.pop(); + } + + #[test] + unconstrained fn array_push() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::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 = EphemeralArray::at(SLOT); + array.push(5); + + let _ = array.get(1); + } + + #[test] + unconstrained fn array_pop() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + array.push(5); + array.push(10); + + let popped: Field = array.pop(); + assert_eq(popped, 10); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + } + + #[test] + unconstrained fn array_set() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + array.push(5); + array.set(0, 99); + assert_eq(array.get(0), 99); + } + + #[test] + unconstrained fn array_remove_last() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(5); + array.remove(0); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn array_remove_some() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + } + + #[test] + unconstrained fn array_remove_all() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn for_each_called_with_all_elements() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); + array.for_each(|index, value| { called_with.push((index, value)); }); + + assert_eq(called_with.len(), 3); + assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); + assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); + assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); + } + + #[test] + unconstrained fn for_each_remove_some() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { + if index == 1 { + array.remove(index); + } + }); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 4); + assert_eq(array.get(1), 6); + } + + #[test] + unconstrained fn for_each_remove_all() { + let _ = TestEnvironment::new(); + + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { array.remove(index); }); + + assert_eq(array.len(), 0); + } + + #[test] + unconstrained fn different_slots_are_isolated() { + let _ = TestEnvironment::new(); + + let array_a = EphemeralArray::at(SLOT); + let array_b = EphemeralArray::at(OTHER_SLOT); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + } + + #[test] + unconstrained fn works_with_multi_field_type() { + let _ = TestEnvironment::new(); + + let array: EphemeralArray = EphemeralArray::at(SLOT); + + let a = MockStruct::new(5, 6); + let b = MockStruct::new(7, 8); + array.push(a); + array.push(b); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), a); + assert_eq(array.get(1), b); + + let popped: MockStruct = array.pop(); + assert_eq(popped, b); + assert_eq(array.len(), 1); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 88c9f019bf36..fa676a1da916 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 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 f7ef44db5e66..35f53b269985 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -8,16 +8,16 @@ pub mod private_notes; pub mod process_message; use crate::{ - capsules::CapsuleArray, messages::{ discovery::process_message::process_message_ciphertext, 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 +126,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,17 +146,11 @@ 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() { - 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, @@ -166,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 - // volatile array with the to-process message, not the actual persistent storage of them. - msgs.remove(i); }); } @@ -187,18 +178,14 @@ 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::ephemeral::EphemeralArray; + use crate::messages::{ + discovery::{CustomMessageHandler, do_sync_state}, + logs::note::MAX_NOTE_PACKED_LEN, + processing::{offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog}, }; use crate::protocol::address::AztecAddress; + use crate::test::helpers::test_environment::TestEnvironment; #[test] unconstrained fn do_sync_state_does_not_panic_on_empty_logs() { @@ -208,9 +195,13 @@ mod test { let contract_address = AztecAddress { inner: 0xdeadbeef }; env.utility_context_at(contract_address, |_| { - let base_slot = PENDING_TAGGED_LOG_ARRAY_BASE_SLOT; + // Mock the oracle call to return a known base slot, then populate an ephemeral + // array at that slot so do_sync_state processes a non-empty log list. + let base_slot = 42; + let mock = std::test::OracleMock::mock("aztec_utl_getPendingTaggedLogs_v2"); + let _ = mock.returns(base_slot); - let logs: CapsuleArray = CapsuleArray::at(contract_address, base_slot, scope); + let logs: EphemeralArray = EphemeralArray::at(base_slot); logs.push(PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }); assert_eq(logs.len(), 1); @@ -224,8 +215,6 @@ mod test { no_inbox_sync, scope, ); - - assert_eq(logs.len(), 0); }); } 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..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 volatile 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() { @@ -167,7 +162,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..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); @@ -30,7 +29,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..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, @@ -105,7 +104,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/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 486436587642..e139a57bfc5a 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, @@ -18,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, @@ -31,10 +32,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(), ); @@ -47,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 { @@ -58,18 +51,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 @@ -102,28 +83,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. @@ -145,25 +118,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. @@ -171,8 +136,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, @@ -182,47 +145,27 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(contract_addre MAX_EVENT_SERIALIZED_LEN as Field, scope, ); -} -/// 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, - ); + // Purge the queues after processing. + EphemeralArray::::at(NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); + EphemeralArray::::at(EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); } /// 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 -/// 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 + // 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; @@ -239,16 +182,9 @@ 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, - ); + let responses = oracle::message_processing::get_logs_by_tag(log_retrieval_requests); - CapsuleArray::at( - contract_address, - LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, - scope, - ) + log_retrieval_requests.clear(); + + responses } 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 bcc03d126b06..836ddfcbd3dc 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(context_resolution_requests); assert_eq(resolved_contexts.len(), inbox_len); @@ -194,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/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr new file mode 100644 index 000000000000..920974ad7a56 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr @@ -0,0 +1,33 @@ +/// Oracles for ephemeral arrays: in-memory arrays scoped to a single contract call frame. +/// +/// 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 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 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_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_ephemeral_set)] +pub(crate) unconstrained fn set_oracle(base_slot: Field, index: u32, values: [Field; N]) {} + +/// 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_ephemeral_remove)] +pub(crate) unconstrained fn remove_oracle(base_slot: Field, index: 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/aztec-nr/aztec/src/oracle/message_processing.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr index ac66b939dcaf..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,14 +1,19 @@ +use crate::ephemeral::EphemeralArray; +use crate::messages::processing::{ + log_retrieval_request::LogRetrievalRequest, 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 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 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); + EphemeralArray::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. @@ -40,46 +45,24 @@ 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, - ); + requests: EphemeralArray, +) -> EphemeralArray> { + let response_slot = get_logs_by_tag_v2_oracle(requests.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( - 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, - ); + requests: EphemeralArray, +) -> EphemeralArray> { + let response_slot = get_message_contexts_by_tx_hash_v2_oracle(requests.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/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 772dcf882ea8..6026df412b1a 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 ephemeral; pub mod contract_sync; pub mod public_call; pub mod tx_phase; 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/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/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"); + } +} diff --git a/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts new file mode 100644 index 000000000000..abc9bfe9ea3d --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts @@ -0,0 +1,158 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + +import { EphemeralArrayService } from './ephemeral_array_service.js'; + +describe('EphemeralArrayService', () => { + let service: EphemeralArrayService; + const slot = Fr.fromString('0x01'); + const otherSlot = Fr.fromString('0x02'); + + beforeEach(() => { + service = new EphemeralArrayService(); + }); + + 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/ephemeral_array_service.ts b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts new file mode 100644 index 000000000000..43b4d9e95e36 --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts @@ -0,0 +1,107 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + +/** In-memory array service for transient data during a single contract call frame. */ +export class EphemeralArrayService { + /** Maps base slot to array of elements, where each element is a serialized Fr[]. */ + #arrays: Map = new Map(); + + /** Returns all elements in the array, or an empty array if uninitialized. */ + readArrayAt(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.readArrayAt(baseSlot).length; + } + + /** Appends an element to the array and returns the new length. */ + push(baseSlot: Fr, elements: Fr[]): number { + const array = this.readArrayAt(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.readArrayAt(baseSlot); + if (array.length === 0) { + throw new Error(`Ephemeral 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.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}`, + ); + } + 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.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}`, + ); + } + 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.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}`, + ); + } + 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; + do { + slot = Fr.random(); + } while (this.#arrays.has(slot.toString())); + return 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); + 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.readArrayAt(srcSlot); + if (count > srcArray.length) { + throw new Error( + `Cannot copy ${count} elements from ephemeral 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/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( 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..ef99416e818e 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, @@ -134,6 +135,8 @@ export interface IUtilityExecutionOracle { logRetrievalResponsesArrayBaseSlot: Fr, scope: AztecAddress, ): Promise; + getLogsByTagV2(requestArrayBaseSlot: Fr): Promise; + getMessageContextsByTxHashV2(requestArrayBaseSlot: Fr): Promise; getMessageContextsByTxHash( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, @@ -154,6 +157,16 @@ export interface IUtilityExecutionOracle { getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise; setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; + + // 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; + 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 172deda40240..d38fb71e571b 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[], @@ -576,6 +582,20 @@ 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( + Fr.fromString(requestArrayBaseSlot), + ); + return [toACVMField(responseSlot)]; + } + // eslint-disable-next-line camelcase aztec_utl_setCapsule( [contractAddress]: ACVMField[], @@ -648,6 +668,62 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + 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_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_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_ephemeral_set([baseSlot]: ACVMField[], [index]: ACVMField[], elements: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralSet( + Fr.fromString(baseSlot), + Fr.fromString(index).toNumber(), + elements.map(Fr.fromString), + ); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + 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_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_ephemeral_copy([srcSlot]: ACVMField[], [dstSlot]: ACVMField[], [count]: ACVMField[]): Promise { + this.handlerAsUtility().ephemeralCopy( + Fr.fromString(srcSlot), + Fr.fromString(dstSlot), + Fr.fromString(count).toNumber(), + ); + 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 512ca45ab9ec..24c63699d6cb 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 @@ -35,6 +35,7 @@ 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'; @@ -77,6 +78,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra private contractLogger: Logger | undefined; private aztecnrLogger: Logger | undefined; private offchainEffects: OffchainEffect[] = []; + private readonly ephemeralArrayService = new EphemeralArrayService(); protected readonly contractAddress: AztecAddress; protected readonly authWitnesses: AuthWitness[]; @@ -494,19 +496,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 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.ephemeralArrayService.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); } /** @@ -532,25 +550,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 => @@ -582,22 +590,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( @@ -617,18 +609,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. @@ -650,6 +631,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, @@ -661,7 +653,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. @@ -705,6 +697,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 @@ -781,6 +794,38 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } + public ephemeralPush(baseSlot: Fr, elements: Fr[]): number { + return this.ephemeralArrayService.push(baseSlot, elements); + } + + public ephemeralPop(baseSlot: Fr): Fr[] { + return this.ephemeralArrayService.pop(baseSlot); + } + + public ephemeralGet(baseSlot: Fr, index: number): Fr[] { + return this.ephemeralArrayService.get(baseSlot, index); + } + + public ephemeralSet(baseSlot: Fr, index: number, elements: Fr[]): void { + this.ephemeralArrayService.set(baseSlot, index, elements); + } + + public ephemeralLen(baseSlot: Fr): number { + return this.ephemeralArrayService.len(baseSlot); + } + + public ephemeralRemove(baseSlot: Fr, index: number): void { + this.ephemeralArrayService.remove(baseSlot, index); + } + + public ephemeralCopy(srcSlot: Fr, dstSlot: Fr, count: number): void { + 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/pxe/src/logs/log_service.test.ts b/yarn-project/pxe/src/logs/log_service.test.ts index 68678dc94cfc..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, []), 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 96402003a15a..6474339fa153 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, @@ -822,6 +829,20 @@ 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); + const responseSlot = await this.handlerAsUtility().getMessageContextsByTxHashV2(requestArrayBaseSlot); + return toForeignCallResult([toSingle(responseSlot)]); + } + // eslint-disable-next-line camelcase aztec_utl_setCapsule( foreignContractAddress: ForeignCallSingle, @@ -898,6 +919,77 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_push(foreignBaseSlot: ForeignCallSingle, foreignElements: ForeignCallArray) { + const baseSlot = fromSingle(foreignBaseSlot); + const elements = fromArray(foreignElements); + const newLen = this.handlerAsUtility().ephemeralPush(baseSlot, elements); + return toForeignCallResult([toSingle(new Fr(newLen))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_pop(foreignBaseSlot: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const element = this.handlerAsUtility().ephemeralPop(baseSlot); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_get(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + const element = this.handlerAsUtility().ephemeralGet(baseSlot, index); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_set( + foreignBaseSlot: ForeignCallSingle, + foreignIndex: ForeignCallSingle, + foreignElements: ForeignCallArray, + ) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + const elements = fromArray(foreignElements); + this.handlerAsUtility().ephemeralSet(baseSlot, index, elements); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_len(foreignBaseSlot: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const len = this.handlerAsUtility().ephemeralLen(baseSlot); + return toForeignCallResult([toSingle(new Fr(len))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_remove(foreignBaseSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const baseSlot = fromSingle(foreignBaseSlot); + const index = fromSingle(foreignIndex).toNumber(); + this.handlerAsUtility().ephemeralRemove(baseSlot, index); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_ephemeral_copy( + foreignSrcSlot: ForeignCallSingle, + foreignDstSlot: ForeignCallSingle, + foreignCount: ForeignCallSingle, + ) { + const srcSlot = fromSingle(foreignSrcSlot); + const dstSlot = fromSingle(foreignDstSlot); + const count = fromSingle(foreignCount).toNumber(); + this.handlerAsUtility().ephemeralCopy(srcSlot, dstSlot, count); + 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