From d8fbc3444e98b0e2850e9ee9922a49f02f225998 Mon Sep 17 00:00:00 2001 From: Martin Verzilli Date: Thu, 9 Apr 2026 16:07:53 +0200 Subject: [PATCH] refactor!: ephemeral arrays (#22162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a new type of array especially designed to use in oracle interfaces between Aztec.nr and PXE. The memory space of of these arrays is isolated by contract call frame and lives in memory. This makes them faster to work with, but it also removes the need for a lot of boilerplate to instantiate them, use them as params, and ultimately dispose of them. Closes F-136 --------- Co-authored-by: Jan Beneš --- .../aztec-nr/aztec/src/ephemeral/mod.nr | 366 ++++++++++++++++++ noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../aztec/src/messages/discovery/mod.nr | 49 +-- .../src/messages/discovery/partial_notes.nr | 8 +- .../src/messages/discovery/private_events.nr | 2 - .../src/messages/discovery/private_notes.nr | 4 - .../src/messages/discovery/process_message.nr | 2 - .../processing/event_validation_request.nr | 4 +- .../aztec/src/messages/processing/mod.nr | 145 ++----- .../processing/note_validation_request.nr | 2 +- .../aztec/src/messages/processing/offchain.nr | 47 +-- .../aztec-nr/aztec/src/oracle/ephemeral.nr | 33 ++ .../aztec/src/oracle/message_processing.nr | 89 ++--- .../aztec-nr/aztec/src/oracle/mod.nr | 1 + .../aztec-nr/aztec/src/oracle/version.nr | 2 +- .../src/test/helpers/test_environment.nr | 2 +- noir-projects/noir-contracts/Nargo.toml | 2 + .../app/token_blacklist_contract/src/main.nr | 3 +- .../aztec_sublib/src/oracle/version.nr | 2 +- .../test/custom_message_contract/src/main.nr | 1 - .../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 +++ .../ephemeral_array_service.test.ts | 158 ++++++++ .../ephemeral_array_service.ts | 110 ++++++ .../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 +- .../oracle/interfaces.ts | 19 + .../oracle/oracle.ts | 84 ++++ .../oracle/oracle_version_is_checked.test.ts | 6 +- .../oracle/utility_execution_oracle.ts | 177 +++++++-- yarn-project/pxe/src/logs/log_service.test.ts | 5 - yarn-project/pxe/src/logs/log_service.ts | 57 +-- yarn-project/pxe/src/oracle_version.ts | 4 +- yarn-project/txe/src/rpc_translator.ts | 104 +++++ 38 files changed, 1247 insertions(+), 348 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/ephemeral.nr 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 create mode 100644 yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.test.ts create mode 100644 yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts 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..9e4efde8cd5a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/ephemeral/mod.nr @@ -0,0 +1,366 @@ +use crate::oracle::ephemeral; +use crate::protocol::traits::{Deserialize, Serialize}; + +/// A dynamically sized array that exists only during a single contract call frame. +/// +/// Ephemeral arrays are backed by in-memory storage on the PXE side rather than a persistent database. Each contract +/// call frame gets its own isolated slot space of ephemeral arrays. Child simulations cannot see the parent's +/// ephemeral arrays, and vice versa. +/// +/// Each logical array operation (push, pop, get, etc.) is a single oracle call, making ephemeral arrays significantly +/// cheaper than capsule arrays. +/// +/// ## Use Cases +/// +/// Ephemeral arrays are designed for passing data between PXE (TypeScript) and contracts (Noir) during simulation, +/// for example, note validation requests or event validation responses. This data type is appropriate for data that +/// is not supposed to be persisted. +/// +/// For data that needs to persist across simulations, contract calls, etc, use +/// [`CapsuleArray`](crate::capsules::CapsuleArray) instead. +pub struct EphemeralArray { + pub slot: Field, +} + +impl EphemeralArray { + /// Returns a handle to an ephemeral array at the given slot, which may already contain data (e.g. populated + /// by an oracle). + pub unconstrained fn at(slot: Field) -> Self { + Self { slot } + } + + /// Returns the number of elements stored in the array. + pub unconstrained fn len(self) -> u32 { + ephemeral::len_oracle(self.slot) + } + + /// Stores a value at the end of the array. + pub unconstrained fn push(self, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + let _ = ephemeral::push_oracle(self.slot, serialized); + } + + /// Removes and returns the last element. Panics if the array is empty. + pub unconstrained fn pop(self) -> T + where + T: Deserialize, + { + let serialized = ephemeral::pop_oracle(self.slot); + Deserialize::deserialize(serialized) + } + + /// Retrieves the value stored at `index`. Panics if the index is out of bounds. + pub unconstrained fn get(self, index: u32) -> T + where + T: Deserialize, + { + let serialized = ephemeral::get_oracle(self.slot, index); + Deserialize::deserialize(serialized) + } + + /// Overwrites the value stored at `index`. Panics if the index is out of bounds. + pub unconstrained fn set(self, index: u32, value: T) + where + T: Serialize, + { + let serialized = value.serialize(); + ephemeral::set_oracle(self.slot, index, serialized); + } + + /// Removes the element at `index`, shifting subsequent elements backward. Panics if out of bounds. + pub unconstrained fn remove(self, index: u32) { + ephemeral::remove_oracle(self.slot, index); + } + + /// Removes all elements from the array and returns self for chaining (e.g. `EphemeralArray::at(slot).clear()` + /// to get a guaranteed-empty array at a given slot). + pub unconstrained fn clear(self) -> Self { + ephemeral::clear_oracle(self.slot); + self + } + + /// Calls a function on each element of the array. + /// + /// The function `f` is called once with each array value and its corresponding index. Iteration proceeds + /// backwards so that it is safe to remove the current element (and only the current element) inside the + /// callback. + /// + /// It is **not** safe to push new elements from inside the callback. + pub unconstrained fn for_each(self, f: unconstrained fn[Env](u32, T) -> ()) + where + T: Deserialize, + { + let mut i = self.len(); + while i > 0 { + i -= 1; + f(i, self.get(i)); + } + } +} + +mod test { + use crate::test::helpers::test_environment::TestEnvironment; + use crate::test::mocks::MockStruct; + use super::EphemeralArray; + + global SLOT: Field = 1230; + global OTHER_SLOT: Field = 5670; + + #[test] + unconstrained fn empty_array() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::at(SLOT); + assert_eq(array.len(), 0); + }); + } + + #[test(should_fail_with = "out of bounds")] + unconstrained fn empty_array_read() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + let _: Field = array.get(0); + }); + } + + #[test(should_fail_with = "is empty")] + unconstrained fn empty_array_pop() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + let _: Field = array.pop(); + }); + } + + #[test] + unconstrained fn array_push() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + array.push(5); + + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); + } + + #[test(should_fail_with = "out of bounds")] + unconstrained fn read_past_len() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + array.push(5); + + let _ = array.get(1); + }); + } + + #[test] + unconstrained fn array_pop() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + array.push(5); + array.push(10); + + let popped: Field = array.pop(); + assert_eq(popped, 10); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 5); + }); + } + + #[test] + unconstrained fn array_set() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + array.push(5); + array.set(0, 99); + assert_eq(array.get(0), 99); + }); + } + + #[test] + unconstrained fn array_remove_last() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + array.push(5); + array.remove(0); + assert_eq(array.len(), 0); + }); + } + + #[test] + unconstrained fn array_remove_some() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + assert_eq(array.len(), 3); + + array.remove(1); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 7); + assert_eq(array.get(1), 9); + }); + } + + #[test] + unconstrained fn array_remove_all() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + + array.push(7); + array.push(8); + array.push(9); + + array.remove(1); + array.remove(1); + array.remove(0); + + assert_eq(array.len(), 0); + }); + } + + #[test] + unconstrained fn for_each_called_with_all_elements() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + let called_with = &mut BoundedVec::<(u32, Field), 3>::new(); + array.for_each(|index, value| { called_with.push((index, value)); }); + + assert_eq(called_with.len(), 3); + assert(called_with.any(|(index, value)| (index == 0) & (value == 4))); + assert(called_with.any(|(index, value)| (index == 1) & (value == 5))); + assert(called_with.any(|(index, value)| (index == 2) & (value == 6))); + }); + } + + #[test] + unconstrained fn for_each_remove_some() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { + if index == 1 { + array.remove(index); + } + }); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), 4); + assert_eq(array.get(1), 6); + }); + } + + #[test] + unconstrained fn for_each_remove_all() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array = EphemeralArray::at(SLOT); + + array.push(4); + array.push(5); + array.push(6); + + array.for_each(|index, _| { array.remove(index); }); + + assert_eq(array.len(), 0); + }); + } + + #[test] + unconstrained fn different_slots_are_isolated() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array_a = EphemeralArray::at(SLOT); + let array_b = EphemeralArray::at(OTHER_SLOT); + + array_a.push(10); + array_a.push(20); + array_b.push(99); + + assert_eq(array_a.len(), 2); + assert_eq(array_a.get(0), 10); + assert_eq(array_a.get(1), 20); + + assert_eq(array_b.len(), 1); + assert_eq(array_b.get(0), 99); + }); + } + + #[test] + unconstrained fn works_with_multi_field_type() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::at(SLOT); + + let a = MockStruct::new(5, 6); + let b = MockStruct::new(7, 8); + array.push(a); + array.push(b); + + assert_eq(array.len(), 2); + assert_eq(array.get(0), a); + assert_eq(array.get(1), b); + + let popped: MockStruct = array.pop(); + assert_eq(popped, b); + assert_eq(array.len(), 1); + }); + } + + #[test] + unconstrained fn clear_returns_self() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::at(SLOT).clear(); + assert_eq(array.len(), 0); + + array.push(42); + assert_eq(array.len(), 1); + assert_eq(array.get(0), 42); + }); + } + + #[test] + unconstrained fn clear_wipes_previous_data() { + let env = TestEnvironment::new(); + env.utility_context(|_| { + let array: EphemeralArray = EphemeralArray::at(SLOT); + array.push(1); + array.push(2); + array.push(3); + assert_eq(array.len(), 3); + + // Clear the same slot, previous data should be gone. + let fresh: EphemeralArray = EphemeralArray::at(SLOT).clear(); + assert_eq(fresh.len(), 0); + fresh.push(4); + assert_eq(fresh.get(0), 4); + }); + } +} 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..19e1af14dfc4 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::message_processing, 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 = 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); }); } @@ -183,22 +174,18 @@ pub unconstrained fn do_sync_state( // Finally we validate all notes and events that were found as part of the previous processes, resulting in them // being added to PXE's database and retrievable via oracles (get_notes) and our TS API (PXE::getPrivateEvents). - validate_and_store_enqueued_notes_and_events(contract_address, scope); + validate_and_store_enqueued_notes_and_events(scope); } 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..4d47dd2228c6 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 @@ -18,7 +18,6 @@ pub(crate) unconstrained fn process_private_note_msg( compute_note_nullifier: ComputeNoteNullifier, msg_metadata: u64, msg_content: BoundedVec, - recipient: AztecAddress, ) { let decoded = decode_private_note_message(msg_metadata, msg_content); @@ -37,7 +36,6 @@ pub(crate) unconstrained fn process_private_note_msg( randomness, note_type_id, packed_note, - recipient, ); } else { aztecnr_warn_log_format!( @@ -62,7 +60,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 +102,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/discovery/process_message.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr index 026fae34ac15..55937a9ec6ea 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/process_message.nr @@ -82,7 +82,6 @@ pub(crate) unconstrained fn process_message_plaintext( compute_note_nullifier, msg_metadata, msg_content, - recipient, ); } else if msg_type_id == PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID { aztecnr_debug_log!("Processing partial note private msg"); @@ -102,7 +101,6 @@ pub(crate) unconstrained fn process_message_plaintext( msg_metadata, msg_content, message_context.tx_hash, - recipient, ); } else if msg_type_id < MIN_CUSTOM_MSG_TYPE_ID { // The message type ID falls in the range reserved for aztec.nr built-in types but wasn't matched above. 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..df34c2b8344c 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,10 +19,10 @@ 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, + oracle::message_processing, }; use crate::protocol::{ address::AztecAddress, @@ -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,58 +136,36 @@ 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, +pub unconstrained fn validate_and_store_enqueued_notes_and_events(scope: AztecAddress) { + message_processing::validate_and_store_enqueued_notes_and_events( NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, EVENT_VALIDATION_REQUESTS_ARRAY_BASE_SLOT, MAX_NOTE_PACKED_LEN as Field, 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, - ); + // Defensive clearing: purge the queues after processing to prevent double-processing if this function is called + // more than once in the same call frame. It is currently defensive because we only call this once per sync run. + let _ = EphemeralArray::::at(NOTE_VALIDATION_REQUESTS_ARRAY_BASE_SLOT).clear(); + let _ = 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,10 @@ 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 = message_processing::get_logs_by_tag(log_retrieval_requests); - CapsuleArray::at( - contract_address, - LOG_RETRIEVAL_RESPONSES_ARRAY_BASE_SLOT, - scope, - ) + // Defensive clearing: prevent stale requests if this function is called more than once in the same call frame. + let _ = 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..6fa4078de2ec 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,11 @@ 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).clear(); + let ready_to_process: EphemeralArray = + EphemeralArray::at(OFFCHAIN_READY_MESSAGES_SLOT).clear(); // Build a request list aligned with the inbox indices. let mut i = 0; @@ -168,13 +153,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 +176,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..9f2721fc53c8 --- /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_pushEphemeral)] +pub(crate) unconstrained fn push_oracle(slot: Field, values: [Field; N]) -> u32 {} + +/// Removes and returns the last serialized element from the ephemeral array. +#[oracle(aztec_utl_popEphemeral)] +pub(crate) unconstrained fn pop_oracle(slot: Field) -> [Field; N] {} + +/// Returns the serialized element at the given index. +#[oracle(aztec_utl_getEphemeral)] +pub(crate) unconstrained fn get_oracle(slot: Field, index: u32) -> [Field; N] {} + +/// Overwrites the serialized element at the given index. +#[oracle(aztec_utl_setEphemeral)] +pub(crate) unconstrained fn set_oracle(slot: Field, index: u32, values: [Field; N]) {} + +/// Returns the number of elements in the ephemeral array. +#[oracle(aztec_utl_getEphemeralLen)] +pub(crate) unconstrained fn len_oracle(slot: Field) -> u32 {} + +/// Removes the element at the given index, shifting subsequent elements backward. +#[oracle(aztec_utl_removeEphemeral)] +pub(crate) unconstrained fn remove_oracle(slot: Field, index: u32) {} + +/// Removes all elements from the ephemeral array. +#[oracle(aztec_utl_clearEphemeral)] +pub(crate) unconstrained fn clear_oracle(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..c34026410b02 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -1,85 +1,64 @@ +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. +/// Validates note/event requests stored in ephemeral arrays. pub(crate) unconstrained fn validate_and_store_enqueued_notes_and_events( - contract_address: AztecAddress, - note_validation_requests_array_base_slot: Field, - event_validation_requests_array_base_slot: Field, + note_validation_requests_array_slot: Field, + event_validation_requests_array_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, scope: AztecAddress, ) { validate_and_store_enqueued_notes_and_events_oracle( - contract_address, - note_validation_requests_array_base_slot, - event_validation_requests_array_base_slot, + note_validation_requests_array_slot, + event_validation_requests_array_slot, max_note_packed_len, max_event_serialized_len, scope, ); } -#[oracle(aztec_utl_validateAndStoreEnqueuedNotesAndEvents)] +#[oracle(aztec_utl_validateAndStoreEnqueuedNotesAndEvents_v2)] unconstrained fn validate_and_store_enqueued_notes_and_events_oracle( - contract_address: AztecAddress, - note_validation_requests_array_base_slot: Field, - event_validation_requests_array_base_slot: Field, + note_validation_requests_array_slot: Field, + event_validation_requests_array_slot: Field, max_note_packed_len: Field, max_event_serialized_len: Field, 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.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_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.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_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/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index 537a7119d27d..2ffff93dab5f 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -11,7 +11,7 @@ /// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency /// without actually using any of the new oracles then there is no reason to throw. pub global ORACLE_VERSION_MAJOR: Field = 22; -pub global ORACLE_VERSION_MINOR: Field = 0; +pub global ORACLE_VERSION_MINOR: Field = 1; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index d46924aa78d5..b2dcd79f3ea1 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -970,7 +970,7 @@ impl TestEnvironment { scope, ); - validate_and_store_enqueued_notes_and_events(context.this_address(), scope); + validate_and_store_enqueued_notes_and_events(scope); }); } } 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/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index 5fb0faa05d34..08aca0350095 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -298,11 +298,10 @@ pub contract TokenBlacklist { TRANSPARENT_NOTE_RANDOMNESS, note_type_id, packed_note, - scope, ); // At this point, the note is pending validation and storage in the database. We must call // validate_and_store_enqueued_notes_and_events to complete that process. - validate_and_store_enqueued_notes_and_events(contract_address, scope); + validate_and_store_enqueued_notes_and_events(scope); } } diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/version.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/version.nr index 537a7119d27d..2ffff93dab5f 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/version.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/oracle/version.nr @@ -11,7 +11,7 @@ /// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency /// without actually using any of the new oracles then there is no reason to throw. pub global ORACLE_VERSION_MAJOR: Field = 22; -pub global ORACLE_VERSION_MINOR: Field = 0; +pub global ORACLE_VERSION_MINOR: Field = 1; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { 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..e3c7812716c7 --- /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', () => { + 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..771f7e15552a --- /dev/null +++ b/yarn-project/pxe/src/contract_function_simulator/ephemeral_array_service.ts @@ -0,0 +1,110 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; + +/** In-memory store for ephemeral arrays scoped to a single contract call frame. */ +export class EphemeralArrayService { + /** + * Maps a slot to the elements of the array stored at that slot. Each element is a serialized representation of + * the original type. + */ + #arrays: Map = new Map(); + + /** Returns all elements in the array, or an empty array if uninitialized. */ + readArrayAt(slot: Fr): Fr[][] { + return this.#arrays.get(slot.toString()) ?? []; + } + + #setArray(slot: Fr, array: Fr[][]): void { + this.#arrays.set(slot.toString(), array); + } + + /** Returns the number of elements in the array at the given slot. */ + len(slot: Fr): number { + return this.readArrayAt(slot).length; + } + + /** Appends an element to the array and returns the new length. */ + push(slot: Fr, elements: Fr[]): number { + const array = this.readArrayAt(slot); + array.push(elements); + this.#setArray(slot, array); + return array.length; + } + + /** Removes and returns the last element. Throws if empty. */ + pop(slot: Fr): Fr[] { + const array = this.readArrayAt(slot); + if (array.length === 0) { + throw new Error(`Ephemeral array at slot ${slot} is empty`); + } + const element = array.pop()!; + this.#setArray(slot, array); + return element; + } + + /** Returns the element at the given index. Throws if out of bounds. */ + get(slot: Fr, index: number): Fr[] { + const array = this.readArrayAt(slot); + if (index < 0 || index >= array.length) { + throw new Error( + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${slot}`, + ); + } + return array[index]; + } + + /** Overwrites the element at the given index. Throws if out of bounds. */ + set(slot: Fr, index: number, value: Fr[]): void { + const array = this.readArrayAt(slot); + if (index < 0 || index >= array.length) { + throw new Error( + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${slot}`, + ); + } + array[index] = value; + } + + /** Removes the element at the given index, shifting subsequent elements backward. Throws if out of bounds. */ + remove(slot: Fr, index: number): void { + const array = this.readArrayAt(slot); + if (index < 0 || index >= array.length) { + throw new Error( + `Ephemeral array index ${index} out of bounds for array of length ${array.length} at slot ${slot}`, + ); + } + array.splice(index, 1); + } + + /** Removes all elements from the array. */ + clear(slot: Fr): void { + this.#arrays.delete(slot.toString()); + } + + /** Allocates a fresh, unused 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 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 744881d54d40..c78761d5ca57 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,15 @@ export interface IUtilityExecutionOracle { logRetrievalResponsesArrayBaseSlot: Fr, scope: AztecAddress, ): Promise; + validateAndStoreEnqueuedNotesAndEventsV2( + noteValidationRequestsArrayBaseSlot: Fr, + eventValidationRequestsArrayBaseSlot: Fr, + maxNotePackedLen: number, + maxEventSerializedLen: number, + scope: AztecAddress, + ): Promise; + getLogsByTagV2(requestArrayBaseSlot: Fr): Promise; + getMessageContextsByTxHashV2(requestArrayBaseSlot: Fr): Promise; getMessageContextsByTxHash( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, @@ -154,6 +164,15 @@ export interface IUtilityExecutionOracle { getSharedSecret(address: AztecAddress, ephPk: Point, contractAddress: AztecAddress): Promise; setContractSyncCacheInvalid(contractAddress: AztecAddress, scopes: AztecAddress[]): void; emitOffchainEffect(data: Fr[]): Promise; + + // Ephemeral array methods + pushEphemeral(slot: Fr, elements: Fr[]): number; + popEphemeral(slot: Fr): Fr[]; + getEphemeral(slot: Fr, index: number): Fr[]; + setEphemeral(slot: Fr, index: number, elements: Fr[]): void; + getEphemeralLen(slot: Fr): number; + removeEphemeral(slot: Fr, index: number): void; + clearEphemeral(slot: 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 e3d6c9f1173e..9e02490f6a23 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts @@ -579,6 +579,12 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + async aztec_utl_getPendingTaggedLogs_v2([scope]: ACVMField[]): Promise { + const slot = await this.handlerAsUtility().getPendingTaggedLogsV2(AztecAddress.fromString(scope)); + return [toACVMField(slot)]; + } + // eslint-disable-next-line camelcase async aztec_utl_validateAndStoreEnqueuedNotesAndEvents( [contractAddress]: ACVMField[], @@ -600,6 +606,24 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + async aztec_utl_validateAndStoreEnqueuedNotesAndEvents_v2( + [noteValidationRequestsArrayBaseSlot]: ACVMField[], + [eventValidationRequestsArrayBaseSlot]: ACVMField[], + [maxNotePackedLen]: ACVMField[], + [maxEventSerializedLen]: ACVMField[], + [scope]: ACVMField[], + ): Promise { + await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEventsV2( + Fr.fromString(noteValidationRequestsArrayBaseSlot), + Fr.fromString(eventValidationRequestsArrayBaseSlot), + Fr.fromString(maxNotePackedLen).toNumber(), + Fr.fromString(maxEventSerializedLen).toNumber(), + AztecAddress.fromString(scope), + ); + return []; + } + // eslint-disable-next-line camelcase async aztec_utl_getLogsByTag( [contractAddress]: ACVMField[], @@ -632,6 +656,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[], @@ -704,6 +742,52 @@ export class Oracle { return []; } + // eslint-disable-next-line camelcase + aztec_utl_pushEphemeral([slot]: ACVMField[], elements: ACVMField[]): Promise { + const newLen = this.handlerAsUtility().pushEphemeral(Fr.fromString(slot), elements.map(Fr.fromString)); + return Promise.resolve([toACVMField(newLen)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_popEphemeral([slot]: ACVMField[]): Promise { + const element = this.handlerAsUtility().popEphemeral(Fr.fromString(slot)); + return Promise.resolve([element.map(toACVMField)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_getEphemeral([slot]: ACVMField[], [index]: ACVMField[]): Promise { + const element = this.handlerAsUtility().getEphemeral(Fr.fromString(slot), Fr.fromString(index).toNumber()); + return Promise.resolve([element.map(toACVMField)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_setEphemeral([slot]: ACVMField[], [index]: ACVMField[], elements: ACVMField[]): Promise { + this.handlerAsUtility().setEphemeral( + Fr.fromString(slot), + Fr.fromString(index).toNumber(), + elements.map(Fr.fromString), + ); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_getEphemeralLen([slot]: ACVMField[]): Promise { + const len = this.handlerAsUtility().getEphemeralLen(Fr.fromString(slot)); + return Promise.resolve([toACVMField(len)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_removeEphemeral([slot]: ACVMField[], [index]: ACVMField[]): Promise { + this.handlerAsUtility().removeEphemeral(Fr.fromString(slot), Fr.fromString(index).toNumber()); + return Promise.resolve([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_clearEphemeral([slot]: ACVMField[]): Promise { + this.handlerAsUtility().clearEphemeral(Fr.fromString(slot)); + 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/oracle_version_is_checked.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts index e380abce9cbf..328950d22f56 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts @@ -247,8 +247,12 @@ describe('Oracle Version Check test suite', () => { // Build the ACIR callback and try to call a non-existent oracle const callback = new Oracle(oracle).toACIRCallback(); + const contractVersion = `${ORACLE_VERSION_MAJOR}\\.${ORACLE_VERSION_MINOR + 1}`; + const pxeVersion = `${ORACLE_VERSION_MAJOR}\\.${ORACLE_VERSION_MINOR}`; expect(() => callback['aztec_utl_someNewOracle']()).toThrow( - /Oracle 'aztec_utl_someNewOracle' not found\. This usually means the contract requires a newer private execution environment than you have\. Upgrade your private execution environment to a compatible version\. The contract was compiled with Aztec\.nr oracle version 22\.1, but this private execution environment only supports up to 22\.0\./, + new RegExp( + `Oracle 'aztec_utl_someNewOracle' not found\\. This usually means the contract requires a newer private execution environment than you have\\. Upgrade your private execution environment to a compatible version\\. The contract was compiled with Aztec\\.nr oracle version ${contractVersion}, but this private execution environment only supports up to ${pxeVersion}\\.`, + ), ); }); 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 0649754a156e..12c4bae641fb 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(); // We store oracle version to be able to show a nice error message when an oracle handler is missing. private contractOracleVersion: { major: number; minor: number } | undefined; @@ -505,31 +507,43 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra logContractMessage(logger, LogLevels[level], strippedMessage, fields); } + // Deprecated, only kept for backwards compatibility until Alpha v5 rolls out. 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); } /** - * Validates all note and event validation 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` and `getPrivateEvents`. + * Legacy: validates note/event requests stored in capsule arrays. * - * This automatically clears both validation request queues, so no further work needs to be done by the caller. - * @param contractAddress - The address of the contract that the logs are tagged for. - * @param noteValidationRequestsArrayBaseSlot - The base slot of capsule array containing note validation requests. - * @param eventValidationRequestsArrayBaseSlot - The base slot of capsule array containing event validation requests. + * Deprecated, only kept for backwards compatibility until Alpha v5 rolls out. */ public async validateAndStoreEnqueuedNotesAndEvents( contractAddress: AztecAddress, @@ -544,8 +558,6 @@ 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, @@ -564,6 +576,53 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra ) ).map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); + await this.#processValidationRequests(noteValidationRequests, eventValidationRequests, scope); + + await this.capsuleService.setCapsuleArray( + contractAddress, + noteValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); + await this.capsuleService.setCapsuleArray( + contractAddress, + eventValidationRequestsArrayBaseSlot, + [], + this.jobId, + scope, + ); + } + + public async validateAndStoreEnqueuedNotesAndEventsV2( + noteValidationRequestsArrayBaseSlot: Fr, + eventValidationRequestsArrayBaseSlot: Fr, + maxNotePackedLen: number, + maxEventSerializedLen: number, + scope: AztecAddress, + ) { + const noteValidationRequests = this.ephemeralArrayService + .readArrayAt(noteValidationRequestsArrayBaseSlot) + .map(fields => NoteValidationRequest.fromFields(fields, maxNotePackedLen)); + + const eventValidationRequests = this.ephemeralArrayService + .readArrayAt(eventValidationRequestsArrayBaseSlot) + .map(fields => EventValidationRequest.fromFields(fields, maxEventSerializedLen)); + + await this.#processValidationRequests(noteValidationRequests, eventValidationRequests, scope); + } + + /** + * Dispatches note and event validation requests to the service layer. + * + * This function is an auxiliary to support legacy (capsule backed) and new (ephemeral array backed) versions of the + * `validateAndStoreEnqueuedNotesAndEvents` oracle. + */ + async #processValidationRequests( + noteValidationRequests: NoteValidationRequest[], + eventValidationRequests: EventValidationRequest[], + scope: AztecAddress, + ) { const noteService = new NoteService(this.noteStore, this.aztecNode, this.anchorBlockHeader, this.jobId); const noteStorePromises = noteValidationRequests.map(request => noteService.validateAndStoreNote( @@ -594,22 +653,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( @@ -629,18 +672,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. @@ -662,6 +694,18 @@ 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)); + } + + // Deprecated, only kept for backwards compatibility until Alpha v5 rolls out. public async getMessageContextsByTxHash( contractAddress: AztecAddress, messageContextRequestsArrayBaseSlot: Fr, @@ -673,7 +717,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. @@ -717,6 +761,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 @@ -793,6 +858,34 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra return deriveAppSiloedSharedSecret(addressSecret, ephPk, this.contractAddress); } + public pushEphemeral(slot: Fr, elements: Fr[]): number { + return this.ephemeralArrayService.push(slot, elements); + } + + public popEphemeral(slot: Fr): Fr[] { + return this.ephemeralArrayService.pop(slot); + } + + public getEphemeral(slot: Fr, index: number): Fr[] { + return this.ephemeralArrayService.get(slot, index); + } + + public setEphemeral(slot: Fr, index: number, elements: Fr[]): void { + this.ephemeralArrayService.set(slot, index, elements); + } + + public getEphemeralLen(slot: Fr): number { + return this.ephemeralArrayService.len(slot); + } + + public removeEphemeral(slot: Fr, index: number): void { + this.ephemeralArrayService.remove(slot, index); + } + + public clearEphemeral(slot: Fr): void { + this.ephemeralArrayService.clear(slot); + } + 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 6df008e1b31c..bec6610bab69 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( @@ -197,32 +184,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/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 0d181627ad28..d4a0bfbbfc5e 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -11,7 +11,7 @@ /// if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency without actually /// using any of the new oracles then there is no reason to throw. export const ORACLE_VERSION_MAJOR = 22; -export const ORACLE_VERSION_MINOR = 0; +export const ORACLE_VERSION_MINOR = 1; /// This hash is computed from the Oracle interface and is used to detect when that interface changes. When it does, /// you need to either: @@ -19,4 +19,4 @@ export const ORACLE_VERSION_MINOR = 0; /// - increment only `ORACLE_VERSION_MINOR` if the change is additive (a new oracle was added). /// /// These constants must be kept in sync between this file and `noir-projects/aztec-nr/aztec/src/oracle/version.nr`. -export const ORACLE_INTERFACE_HASH = 'b1c0322512c67ea1e90bd6178686095acc74b5593cd543d8ffb51153168a13c6'; +export const ORACLE_INTERFACE_HASH = 'efafa0db2cc1f94e26d794d0079c8f71115261df0c3d0fa8cb5b64f17a12db92'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index c1641ef00ff8..dd535d4b8bf9 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -751,6 +751,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 slot = await this.handlerAsUtility().getPendingTaggedLogsV2(scope); + return toForeignCallResult([toSingle(slot)]); + } + // eslint-disable-next-line camelcase public async aztec_utl_validateAndStoreEnqueuedNotesAndEvents( foreignContractAddress: ForeignCallSingle, @@ -779,6 +786,31 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + public async aztec_utl_validateAndStoreEnqueuedNotesAndEvents_v2( + foreignNoteValidationRequestsArrayBaseSlot: ForeignCallSingle, + foreignEventValidationRequestsArrayBaseSlot: ForeignCallSingle, + foreignMaxNotePackedLen: ForeignCallSingle, + foreignMaxEventSerializedLen: ForeignCallSingle, + foreignScope: ForeignCallSingle, + ) { + const noteValidationRequestsArrayBaseSlot = fromSingle(foreignNoteValidationRequestsArrayBaseSlot); + const eventValidationRequestsArrayBaseSlot = fromSingle(foreignEventValidationRequestsArrayBaseSlot); + const maxNotePackedLen = fromSingle(foreignMaxNotePackedLen).toNumber(); + const maxEventSerializedLen = fromSingle(foreignMaxEventSerializedLen).toNumber(); + const scope = AztecAddress.fromField(fromSingle(foreignScope)); + + await this.handlerAsUtility().validateAndStoreEnqueuedNotesAndEventsV2( + noteValidationRequestsArrayBaseSlot, + eventValidationRequestsArrayBaseSlot, + maxNotePackedLen, + maxEventSerializedLen, + scope, + ); + + return toForeignCallResult([]); + } + // eslint-disable-next-line camelcase public async aztec_utl_getLogsByTag( foreignContractAddress: ForeignCallSingle, @@ -823,6 +855,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, @@ -899,6 +945,64 @@ export class RPCTranslator { return toForeignCallResult([]); } + // eslint-disable-next-line camelcase + aztec_utl_pushEphemeral(foreignSlot: ForeignCallSingle, foreignElements: ForeignCallArray) { + const slot = fromSingle(foreignSlot); + const elements = fromArray(foreignElements); + const newLen = this.handlerAsUtility().pushEphemeral(slot, elements); + return toForeignCallResult([toSingle(new Fr(newLen))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_popEphemeral(foreignSlot: ForeignCallSingle) { + const slot = fromSingle(foreignSlot); + const element = this.handlerAsUtility().popEphemeral(slot); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_getEphemeral(foreignSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const slot = fromSingle(foreignSlot); + const index = fromSingle(foreignIndex).toNumber(); + const element = this.handlerAsUtility().getEphemeral(slot, index); + return toForeignCallResult([toArray(element)]); + } + + // eslint-disable-next-line camelcase + aztec_utl_setEphemeral( + foreignSlot: ForeignCallSingle, + foreignIndex: ForeignCallSingle, + foreignElements: ForeignCallArray, + ) { + const slot = fromSingle(foreignSlot); + const index = fromSingle(foreignIndex).toNumber(); + const elements = fromArray(foreignElements); + this.handlerAsUtility().setEphemeral(slot, index, elements); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_getEphemeralLen(foreignSlot: ForeignCallSingle) { + const slot = fromSingle(foreignSlot); + const len = this.handlerAsUtility().getEphemeralLen(slot); + return toForeignCallResult([toSingle(new Fr(len))]); + } + + // eslint-disable-next-line camelcase + aztec_utl_removeEphemeral(foreignSlot: ForeignCallSingle, foreignIndex: ForeignCallSingle) { + const slot = fromSingle(foreignSlot); + const index = fromSingle(foreignIndex).toNumber(); + this.handlerAsUtility().removeEphemeral(slot, index); + return toForeignCallResult([]); + } + + // eslint-disable-next-line camelcase + aztec_utl_clearEphemeral(foreignSlot: ForeignCallSingle) { + const slot = fromSingle(foreignSlot); + this.handlerAsUtility().clearEphemeral(slot); + 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