diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index f6a414ca1048..f0b5a83aa51c 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,31 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] `MessageContext` removed: message processing uses `ResolvedTx` + +`aztec::messages::processing::MessageContext` has been removed in favor of the new `ResolvedTx`, which carries the same `tx_hash`, `unique_note_hashes_in_tx`, and `first_nullifier_in_tx`, plus `block_number` and `block_hash`. The offchain handoff type `OffchainMessageWithContext` is likewise renamed to `OffchainMessageWithTx`. + +This affects contracts that implement a custom message handler (registered via `AztecConfig::custom_message_handler`): the handler's context parameter is now a `ResolvedTx`. + +**Migration:** + +```diff + use aztec::messages::processing::{ +- enqueue_event_for_validation, MessageContext, ++ enqueue_event_for_validation, ResolvedTx, + }; + + unconstrained fn handle_my_message( + // ... +- message_context: MessageContext, ++ resolved_tx: ResolvedTx, + scope: AztecAddress, + ) { +- // ...message_context.tx_hash... ++ // ...resolved_tx.tx_hash... + } +``` + ### [PXE] `pxe.updateContract` removed and `pxe.registerContract` no longer takes an artifact Registering classes and instances are now separate, unvalidated operations. `registerContractClass(artifact)` registers a class, `registerContract(instance)` registers an instance and no longer takes an artifact. `registerContract` does not check that PXE knows the contract's artifact: a missing artifact surfaces only when the contract is later simulated. diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index 7ea06259ab4b..80e98bc24014 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -45,6 +45,7 @@ pub mod transient; pub mod event; pub mod facts; pub mod messages; +pub(crate) mod partial_notes; pub use protocol_types as protocol; pub(crate) mod logging; pub mod utils; 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 2ff672203572..8f8de2f43aaa 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -4,10 +4,10 @@ use crate::messages::{ delivery::handshake::{get_handshake_secrets, PROVIDED_SECRETS_ARRAY_BASE_SLOT}, processing::provided_secret::ProvidedSecret, }; +use crate::partial_notes; use crate::protocol::address::AztecAddress; pub(crate) mod nonce_discovery; -pub(crate) mod partial_notes; pub(crate) mod private_events; pub mod private_notes; pub mod process_message; @@ -18,8 +18,8 @@ use crate::{ encoding::MAX_MESSAGE_CONTENT_LEN, logs::note::MAX_NOTE_PACKED_LEN, processing::{ - MessageContext, offchain::OffchainInboxSync, OffchainMessageWithContext, - pending_tagged_log::PendingTaggedLog, validate_and_store_enqueued_notes_and_events, + offchain::OffchainInboxSync, OffchainMessageWithTx, pending_tagged_log::PendingTaggedLog, ResolvedTx, + validate_and_store_enqueued_notes_and_events, }, }, oracle::message_processing, @@ -108,7 +108,7 @@ pub type CustomMessageHandler = unconstrained fn( /* msg_type_id */ u64, /* msg_metadata */ u64, /* msg_content */ BoundedVec, -/* message_context */ MessageContext, +/* resolved_tx */ ResolvedTx, /* scope */ AztecAddress); /// Custom state synchronization handler. @@ -209,14 +209,14 @@ unconstrained fn sync_state_with_secrets( if offchain_inbox_sync.is_some() { let msgs = offchain_inbox_sync.unwrap()(contract_address, scope); - msgs.for_each(|_i, msg: OffchainMessageWithContext| { + msgs.for_each(|_i, msg: OffchainMessageWithTx| { process_message_ciphertext( contract_address, compute_note_hash, compute_note_nullifier, process_custom_message, msg.message_ciphertext, - msg.message_context, + msg.resolved_tx, scope, ); }); @@ -224,7 +224,7 @@ unconstrained fn sync_state_with_secrets( // Then we process all pending partial notes, regardless of whether they were found in the current or previous // executions. - partial_notes::fetch_and_process_partial_note_completion_logs( + partial_notes::sync( contract_address, compute_note_hash, compute_note_nullifier, @@ -264,7 +264,7 @@ mod test { // 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"); + let mock = std::test::OracleMock::mock("aztec_utl_getPendingTaggedLogsV2"); let _ = mock.returns(base_slot); let logs: EphemeralArray = EphemeralArray::at(base_slot); @@ -281,6 +281,10 @@ mod test { no_inbox_sync, scope, ); + + // Guards against the mocked oracle name going stale: if sync stops calling it, the crafted empty log + // above is never consumed and the test silently stops covering the empty-payload path. + assert_eq(mock.times_called(), 1); }); } 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 deleted file mode 100644 index 44054c9504b3..000000000000 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ /dev/null @@ -1,182 +0,0 @@ -use crate::{ - capsules::CapsuleArray, - ephemeral::EphemeralArray, - messages::{ - discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, - encoding::MAX_MESSAGE_CONTENT_LEN, - logs::partial_note::{decode_partial_note_private_message, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN}, - processing::{ - enqueue_note_for_validation, - get_pending_partial_notes_completion_logs, - log_retrieval_response::{LogRetrievalResponse, MAX_LOG_CONTENT_LEN}, - }, - }, - utils::array, -}; - -use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; -use crate::protocol::{address::AztecAddress, hash::sha256_to_field, traits::{Deserialize, Serialize}}; - -/// The slot in the PXE capsules where we store a `CapsuleArray` of `DeliveredPendingPartialNote`. -pub(crate) global DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT: Field = sha256_to_field( - "AZTEC_NR::DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT".as_bytes(), -); - -/// A partial note that was delivered but is still pending completion. Contains the information necessary to find the -/// log that will complete it and lead to a note being discovered and delivered. -#[derive(Serialize, Deserialize)] -pub(crate) struct DeliveredPendingPartialNote { - pub(crate) owner: AztecAddress, - pub(crate) randomness: Field, - pub(crate) note_completion_log_tag: Field, - pub(crate) note_type_id: Field, - pub(crate) packed_private_note_content: BoundedVec, -} - -pub(crate) unconstrained fn process_partial_note_private_msg( - contract_address: AztecAddress, - msg_metadata: u64, - msg_content: BoundedVec, - tx_hash: Field, - scope: AztecAddress, -) { - let decoded = decode_partial_note_private_message(msg_metadata, msg_content); - - if decoded.is_some() { - // We store the information of the partial note we found in a persistent capsule in PXE, so that we can later - // search for the public log that will complete it. - let (owner, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) = decoded.unwrap(); - - let pending = DeliveredPendingPartialNote { - owner, - randomness, - note_completion_log_tag, - note_type_id, - packed_private_note_content, - }; - - CapsuleArray::at( - contract_address, - DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, - scope, - ) - .push(pending); - } else { - aztecnr_warn_log_format!( - "Could not decode partial note private message from tx {0}, ignoring", - )( - [tx_hash], - ); - } -} - -/// Searches for logs that would result in the completion of pending partial notes, ultimately resulting in the notes -/// being delivered to PXE if completed. -pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( - contract_address: AztecAddress, - compute_note_hash: ComputeNoteHash, - compute_note_nullifier: ComputeNoteNullifier, - scope: AztecAddress, -) { - let pending_partial_notes = CapsuleArray::at( - contract_address, - DELIVERED_PENDING_PARTIAL_NOTE_ARRAY_LENGTH_CAPSULES_SLOT, - scope, - ); - - aztecnr_debug_log_format!("{} pending partial notes")([pending_partial_notes.len() as Field]); - - // 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 completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes); - - // Each entry in the completion logs array corresponds to the entry in the pending partial notes array at the same - // index. Each inner array contains all matching LogRetrievalResponses. - assert_eq(completion_logs.len(), pending_partial_notes.len()); - - // Note: the loop runs backwards because it removes completed entries from `pending_partial_notes`, which is - // index-aligned with `completion_logs`. Going from last to first keeps the indices of not-yet-visited entries - // stable. - let mut i = completion_logs.len(); - while i > 0 { - i -= 1; - let logs_for_tag: EphemeralArray = completion_logs.get(i); - let pending_partial_note = pending_partial_notes.get(i); - let num_logs = logs_for_tag.len(); - - if num_logs == 0 { - aztecnr_debug_log_format!("Found no completion logs for partial note with tag {}")( - [pending_partial_note.note_completion_log_tag], - ); - - // Note that we're not removing the pending partial note from the capsule array, so we will continue - // searching for this tagged log when performing message discovery in the future until we either find it or - // the entry is somehow removed from the array. - } else { - assert(num_logs == 1, f"Expected at most 1 completion log per partial note, got {num_logs}"); - - aztecnr_debug_log_format!("Completion log found for partial note with tag {}")([ - pending_partial_note.note_completion_log_tag, - ]); - let log = logs_for_tag.get(0); - - // The first field in the completion log payload is the storage slot, followed by the public note - // content fields. - let storage_slot = log.log_payload.get(0); - let public_note_content: BoundedVec = array::subbvec(log.log_payload, 1); - - // Public fields are assumed to all be placed at the end of the packed representation, so we combine - // the private and public packed fields (i.e. the contents of the private message and public log - // plaintext) to get the complete packed content. - let complete_packed_note = array::append( - pending_partial_note.packed_private_note_content, - public_note_content, - ); - - let discovered_notes = attempt_note_nonce_discovery( - log.unique_note_hashes_in_tx, - log.first_nullifier_in_tx, - compute_note_hash, - compute_note_nullifier, - contract_address, - pending_partial_note.owner, - storage_slot, - pending_partial_note.randomness, - pending_partial_note.note_type_id, - complete_packed_note, - ); - - // TODO(#11627): is there anything reasonable we can do if we get a log but it doesn't result in a note - // being found? - if discovered_notes.len() == 0 { - panic( - f"A partial note's completion log did not result in any notes being found - this should never happen", - ); - } - - aztecnr_debug_log_format!("Discovered {0} notes for partial note with tag {1}")([ - discovered_notes.len() as Field, - pending_partial_note.note_completion_log_tag, - ]); - - discovered_notes.for_each(|discovered_note| { - enqueue_note_for_validation( - contract_address, - pending_partial_note.owner, - storage_slot, - pending_partial_note.randomness, - discovered_note.note_nonce, - complete_packed_note, - discovered_note.note_hash, - discovered_note.inner_nullifier, - log.tx_hash, - ); - }); - - // Because there is only a single log for a given tag, once we've processed the tagged log then we simply - // delete the pending work entry, regardless of whether it was actually completed or not. - pending_partial_notes.remove(i); - } - } -} 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 96726873e4dc..c70fcd8f2c1d 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 @@ -1,17 +1,18 @@ use crate::messages::{ discovery::{ - ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, partial_notes::process_partial_note_private_msg, - private_events::process_private_event_msg, private_notes::process_private_note_msg, + ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, private_events::process_private_event_msg, + private_notes::process_private_note_msg, }, encoding::{decode_message, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN}, encryption::{aes128::AES128, message_encryption::MessageEncryption}, msg_type::{ MIN_CUSTOM_MSG_TYPE_ID, PARTIAL_NOTE_PRIVATE_MSG_TYPE_ID, PRIVATE_EVENT_MSG_TYPE_ID, PRIVATE_NOTE_MSG_TYPE_ID, }, - processing::MessageContext, + processing::ResolvedTx, }; use crate::logging::{aztecnr_debug_log, aztecnr_warn_log_format}; +use crate::partial_notes::process_partial_note_private_msg; use crate::protocol::address::AztecAddress; /// Processes a message that can contain notes, partial notes, or events. @@ -32,7 +33,7 @@ pub unconstrained fn process_message_ciphertext( compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option, message_ciphertext: BoundedVec, - message_context: MessageContext, + resolved_tx: ResolvedTx, recipient: AztecAddress, ) { let message_plaintext_option = AES128::decrypt(message_ciphertext, recipient, contract_address); @@ -44,11 +45,11 @@ pub unconstrained fn process_message_ciphertext( compute_note_nullifier, process_custom_message, message_plaintext_option.unwrap(), - message_context, + resolved_tx, recipient, ); } else { - aztecnr_warn_log_format!("Could not decrypt message ciphertext from tx {0}, ignoring")([message_context.tx_hash]); + aztecnr_warn_log_format!("Could not decrypt message ciphertext from tx {0}, ignoring")([resolved_tx.tx_hash]); } } @@ -58,7 +59,7 @@ pub(crate) unconstrained fn process_message_plaintext( compute_note_nullifier: ComputeNoteNullifier, process_custom_message: Option, message_plaintext: BoundedVec, - message_context: MessageContext, + resolved_tx: ResolvedTx, recipient: AztecAddress, ) { // The first thing to do after decrypting the message is to determine what type of message we're processing. We @@ -75,9 +76,9 @@ pub(crate) unconstrained fn process_message_plaintext( process_private_note_msg( contract_address, - message_context.tx_hash, - message_context.unique_note_hashes_in_tx, - message_context.first_nullifier_in_tx, + resolved_tx.tx_hash, + resolved_tx.unique_note_hashes_in_tx, + resolved_tx.first_nullifier_in_tx, compute_note_hash, compute_note_nullifier, msg_metadata, @@ -90,7 +91,7 @@ pub(crate) unconstrained fn process_message_plaintext( contract_address, msg_metadata, msg_content, - message_context.tx_hash, + resolved_tx, recipient, ); } else if msg_type_id == PRIVATE_EVENT_MSG_TYPE_ID { @@ -100,7 +101,7 @@ pub(crate) unconstrained fn process_message_plaintext( contract_address, msg_metadata, msg_content, - message_context.tx_hash, + resolved_tx.tx_hash, ); } 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. @@ -117,7 +118,7 @@ pub(crate) unconstrained fn process_message_plaintext( msg_type_id, msg_metadata, msg_content, - message_context, + resolved_tx, recipient, ); } else { @@ -130,6 +131,6 @@ pub(crate) unconstrained fn process_message_plaintext( ); } } else { - aztecnr_warn_log_format!("Could not decode message plaintext from tx {0}, ignoring")([message_context.tx_hash]); + aztecnr_warn_log_format!("Could not decode message plaintext from tx {0}, ignoring")([resolved_tx.tx_hash]); } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr deleted file mode 100644 index 0ab9fc9d17e9..000000000000 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/message_context.nr +++ /dev/null @@ -1,13 +0,0 @@ -use crate::protocol::{constants::MAX_NOTE_HASHES_PER_TX, traits::{Deserialize, Serialize}}; - -/// Additional information needed to process a message. -/// -/// All messages exist in the context of a transaction, and information about that transaction is typically required in -/// order to perform validation, store results, etc. For example, messages containing notes require knowledge of note -/// hashes and the first nullifier in order to find the note's nonce. -#[derive(Serialize, Deserialize, Eq)] -pub struct MessageContext { - pub tx_hash: Field, - pub unique_note_hashes_in_tx: BoundedVec, - pub first_nullifier_in_tx: Field, -} 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 5efc5ab1e598..4b4b71564f4e 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr @@ -1,8 +1,7 @@ pub(crate) mod event_validation_request; pub mod offchain; -mod message_context; -pub use message_context::MessageContext; +pub use crate::oracle::tx_resolution::ResolvedTx; mod note_validation_request; pub use note_validation_request::NoteValidationRequest; @@ -13,16 +12,15 @@ pub(crate) mod pending_tagged_log; pub(crate) mod provided_secret; use crate::{ - capsules::CapsuleArray, ephemeral::EphemeralArray, event::EventSelector, messages::{ - discovery::partial_notes::DeliveredPendingPartialNote, encoding::MESSAGE_CIPHERTEXT_LEN, logs::{event::MAX_EVENT_SERIALIZED_LEN, note::MAX_NOTE_PACKED_LEN}, processing::{log_retrieval_request::LogRetrievalRequest, log_retrieval_response::LogRetrievalResponse}, }, oracle::message_processing, + partial_notes::DeliveredPendingPartialNote, }; use crate::protocol::{ address::AztecAddress, @@ -46,9 +44,9 @@ global LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT: Field = sha256_to_field( /// An offchain-delivered message with resolved context, ready for processing during sync. #[derive(Serialize, Deserialize)] -pub struct OffchainMessageWithContext { +pub struct OffchainMessageWithTx { pub message_ciphertext: BoundedVec, - pub message_context: MessageContext, + pub resolved_tx: ResolvedTx, } /// Enqueues a note for validation and storage by PXE. @@ -145,13 +143,13 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(scope: AztecAd 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 nested `EphemeralArray`, one inner +/// Efficiently queries the node for logs that result in the completion of all `DeliveredPendingPartialNote`s in an +/// `EphemeralArray` by performing all node communication concurrently. Returns a nested `EphemeralArray`, one inner /// array per pending partial note, each containing all matching `LogRetrievalResponse`s (which may be empty if no /// logs were found). pub(crate) unconstrained fn get_pending_partial_notes_completion_logs( contract_address: AztecAddress, - pending_partial_notes: CapsuleArray, + pending_partial_notes: EphemeralArray, ) -> EphemeralArray> { let log_retrieval_requests = EphemeralArray::at(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/mod.nr index 91a0500ae492..5bd712009923 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/mod.nr @@ -1,7 +1,7 @@ use crate::{ context::UtilityContext, ephemeral::EphemeralArray, - messages::{encoding::MESSAGE_CIPHERTEXT_LEN, processing::OffchainMessageWithContext}, + messages::{encoding::MESSAGE_CIPHERTEXT_LEN, processing::OffchainMessageWithTx}, oracle::{contract_sync::set_contract_sync_cache_invalid, tx_resolution::get_resolved_txs}, protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}, }; @@ -27,7 +27,7 @@ pub global MAX_OFFCHAIN_MESSAGES_PER_RECEIVE_CALL: u32 = 16; /// The only current implementation of an [`OffchainInboxSync`] is [`sync_inbox`], which manages an inbox with /// expiration and finality-based eviction and automatic transaction context resolution. pub type OffchainInboxSync = unconstrained fn( -/* contract_address */AztecAddress, /* scope */ AztecAddress) -> EphemeralArray; +/* contract_address */AztecAddress, /* scope */ AztecAddress) -> EphemeralArray; /// Delivers offchain messages to the given contract's offchain inbox for subsequent processing. /// @@ -60,7 +60,7 @@ pub unconstrained fn receive( pub unconstrained fn sync_inbox( contract_address: AztecAddress, scope: AztecAddress, -) -> EphemeralArray { +) -> EphemeralArray { let active_receptions = OffchainReception::load_all(contract_address, scope); // Ask PXE to resolve each message's originating tx. We pass the tx hashes in reception order, so the resolved txs @@ -69,7 +69,7 @@ pub unconstrained fn sync_inbox( reception.read_message().tx_hash.unwrap_or(0) })); - let processable_messages: EphemeralArray = EphemeralArray::empty(); + let processable_messages: EphemeralArray = EphemeralArray::empty(); let now = UtilityContext::new().timestamp(); active_receptions.for_each(|i, reception| { diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/reception.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/reception.nr index 31229750e71a..36bafe540a1e 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/reception.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/offchain/reception.nr @@ -100,7 +100,7 @@ use crate::{ delete_fact_collection, Fact, FactCollection, get_fact_collection, get_fact_collections_by_type, OriginBlock, record_non_retractable_fact, record_retractable_fact, }, - messages::processing::{MessageContext, OffchainMessageWithContext}, + messages::processing::OffchainMessageWithTx, oracle::tx_resolution::ResolvedTx, protocol::{ address::AztecAddress, @@ -206,7 +206,7 @@ impl OffchainReception { self, maybe_resolved_tx: Option, now: u64, - ) -> Option { + ) -> Option { let message = self.read_message(); let processed_fact = self.collection.facts.find(|f: Fact| f.fact_type_id == OFFCHAIN_MESSAGE_PROCESSED); @@ -214,13 +214,8 @@ impl OffchainReception { // Received -> Processed: the originating tx is onchain. let resolved = maybe_resolved_tx.unwrap(); self.mark_processed(resolved.block_number, resolved.block_hash); - let message_context = MessageContext { - tx_hash: resolved.tx_hash, - unique_note_hashes_in_tx: resolved.unique_note_hashes_in_tx, - first_nullifier_in_tx: resolved.first_nullifier_in_tx, - }; Option::some( - OffchainMessageWithContext { message_ciphertext: message.ciphertext, message_context }, + OffchainMessageWithTx { message_ciphertext: message.ciphertext, resolved_tx: resolved }, ) } else if processed_fact.is_some() { if processed_fact.unwrap().origin_block.unwrap().block_state.is_finalized() { @@ -377,7 +372,7 @@ mod test { // A resolved tx within the TTL: the message is handed off with its tx context attached. let processable = reception.step(Option::some(resolved_tx(tx_hash)), 100); assert(processable.is_some()); - assert_eq(processable.unwrap().message_context.tx_hash, tx_hash); + assert_eq(processable.unwrap().resolved_tx.tx_hash, tx_hash); // It stays active for reorg safety and is now marked processed. assert(OffchainReception::is_active(address, scope, msg), "processed reception should stay active"); diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr index 3829895d2bc2..dc0cfc0578ca 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/pending_tagged_log.nr @@ -1,4 +1,4 @@ -use crate::messages::processing::MessageContext; +use crate::messages::processing::ResolvedTx; use crate::protocol::{constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::{Deserialize, Serialize}}; /// A private log that was tagged for `recipient` and has not yet been processed, plus information related to the @@ -7,5 +7,5 @@ use crate::protocol::{constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::{Deserializ pub(crate) struct PendingTaggedLog { pub log: BoundedVec, // Context of the message included in the log. - pub context: MessageContext, + pub context: ResolvedTx, } 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 fd865ab3e1c5..f6182d59f83d 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_processing.nr @@ -22,7 +22,7 @@ pub(crate) unconstrained fn get_pending_tagged_logs( get_pending_tagged_logs_oracle(scope, provided_secrets) } -#[oracle(aztec_utl_getPendingTaggedLogs)] +#[oracle(aztec_utl_getPendingTaggedLogsV2)] unconstrained fn get_pending_tagged_logs_oracle( scope: AztecAddress, provided_secrets: EphemeralArray, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index 1565ca3ebd37..5e518976c7fe 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 = 30; -pub global ORACLE_VERSION_MINOR: Field = 4; +pub global ORACLE_VERSION_MINOR: Field = 5; /// 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/partial_notes/fsm.nr b/noir-projects/aztec-nr/aztec/src/partial_notes/fsm.nr new file mode 100644 index 000000000000..f926b1b25fcc --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/partial_notes/fsm.nr @@ -0,0 +1,499 @@ +//! The finite state machine to process a single partial note. +//! +//! A partial note arrives in two parts: a private part (delivered first, carrying the owner, randomness, completion-log +//! tag, note type, and packed private content) and a public *completion log* (storage slot + packed public content). +//! The note can only be discovered and stored once both are combined. The state chart below summarizes how a single +//! partial note's process unfolds, including how reorgs cause it to rewind. +//! +//! ```text +//! init() (partial note delivery message, including tx and block data) +//! | +//! v +//! +-----------+ completion log found +-----------+ +//! ---- | | ------------------------------> | | +//! log not found | | | | | discover note(s), +//! |--> | PENDING | | COMPLETED | ~~> enqueue for storage, etc +//! | | <------------------------------ | | +//! | | completion log block re-orged | | +//! +-----------+ +-----------+ +//! | | +//! | delivery message block re-orged | completion log block finalized +//! v v +//! +---------------------------------------------------------+ +//! | TERMINATED (process ends) | +//! +---------------------------------------------------------+ +//! ``` +//! +//! ## States +//! +//! - **Pending**: the private part has been received but the completion log has not been found yet. While in this +//! state, message discovery keeps searching for that log on each sync. +//! - **Completed**: the completion log was found, so the note was completed and enqueued for storage. Because the +//! block containing the completion log can be dropped by a reorg, this state is not terminal until the block +//! finalizes: a reorg dropping it rewinds the FSM back to `Pending` state. +//! - **Terminated**: nothing else can be done with the partial note, either because its delivery block was re-orged +//! away (so the delivery itself was reverted) or because its completion block has finalized (so the discovered note +//! is permanent and reorg-proof). This is the terminal state of this FSM. +//! +//! ## Transitions (each evaluated by [`step`](PartialNoteFsm::step)) +//! +//! - **Pending -> Completed** ([`complete_with_log`](PartialNoteFsm::complete_with_log)): the completion log is +//! found. Its public content is combined with the delivered private content, the resulting note(s) are discovered +//! and enqueued for storage. +//! - **Completed -> Pending**: the completion log block is re-orged out. The completion log is searched for again. +//! - **Pending -> Terminated**: the delivery block is re-orged out, retracting the `delivered` birth fact. The +//! delivery was reverted, so there is nothing left to do. +//! - **Completed -> Terminated**: the completion log block finalizes, making the discovered note reorg-proof, so the +//! reception is complete and its collection is deleted. +//! +//! ## Implementation +//! +//! Each pending partial note is tracked as a [fact collection](crate::facts::FactCollection) of type +//! `PARTIAL_NOTE_RECEPTION_TYPE_ID`, identified by a hash of its [`DeliveredPendingPartialNote`] content (so +//! re-delivery of the same partial note is idempotent). The collection holds: +//! +//! - a retractable `PARTIAL_NOTE_DELIVERED` birth fact tied to a delivery block (so a reorg of that block auto-prunes +//! the pending note), whose payload is the serialized [`DeliveredPendingPartialNote`]; +//! - once the completion log is found, a retractable `PARTIAL_NOTE_COMPLETED` fact tied to the completion log block. +//! +//! Status folds from the canonical fact set: +//! - `delivered` only ⇒ Pending +//! - `delivered` + `completed` ⇒ Completed +//! +//! A reorg retracting either fact folds the reception back accordingly. +use crate::ephemeral::EphemeralArray; +use crate::facts::{ + delete_fact_collection, Fact, FactCollection, get_fact_collections_by_type, OriginBlock, record_retractable_fact, + RetractableFactOrigin, +}; +use crate::logging::aztecnr_debug_log_format; +use crate::messages::{ + discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, + processing::{enqueue_note_for_validation, log_retrieval_response::{LogRetrievalResponse, MAX_LOG_CONTENT_LEN}}, +}; +use crate::protocol::{address::AztecAddress, hash::{poseidon2_hash, sha256_to_field}, traits::{Deserialize, Serialize}}; +use crate::utils::array; +use super::DeliveredPendingPartialNote; + +/// Fact-collection type id shared by every partial-note reception in the [fact store](crate::facts). +global PARTIAL_NOTE_RECEPTION_TYPE_ID: Field = sha256_to_field("AZTEC_NR::PARTIAL_NOTE_RECEPTION_TYPE_ID".as_bytes()); + +/// Fact type id of the birth fact carrying the delivered private partial note. +global PARTIAL_NOTE_DELIVERED: Field = sha256_to_field("AZTEC_NR::PARTIAL_NOTE_DELIVERED".as_bytes()); + +/// Fact type id marking a partial note as completed (its note has been discovered and enqueued). +global PARTIAL_NOTE_COMPLETED: Field = sha256_to_field("AZTEC_NR::PARTIAL_NOTE_COMPLETED".as_bytes()); + +/// A single partial note's finite state machine, backed by a [`FactCollection`](crate::facts::FactCollection). +#[derive(Deserialize, Serialize)] +pub(crate) struct PartialNoteFsm { + collection: FactCollection, +} + +impl PartialNoteFsm { + /// Initializes an FSM for a freshly discovered partial note. + /// + /// The FSM existence is tied to `origin_block`: if `origin_block` is dropped by a reorg, the FSM is transparently + /// deleted. + pub(crate) unconstrained fn init( + contract_address: AztecAddress, + scope: AztecAddress, + pending: DeliveredPendingPartialNote, + origin_block: OriginBlock, + ) { + record_retractable_fact( + contract_address, + scope, + PARTIAL_NOTE_RECEPTION_TYPE_ID, + Self::id_for(pending), + PARTIAL_NOTE_DELIVERED, + to_payload(pending), + origin_block, + ); + } + + /// Loads every active partial-note FSM for the given contract and scope. + pub(crate) unconstrained fn load_all( + contract_address: AztecAddress, + scope: AztecAddress, + ) -> EphemeralArray { + get_fact_collections_by_type(contract_address, scope, PARTIAL_NOTE_RECEPTION_TYPE_ID) + .map(|collection: FactCollection| PartialNoteFsm { collection }) + } + + /// The fact-collection id identifying a specific partial note. + fn id_for(pending: DeliveredPendingPartialNote) -> Field { + poseidon2_hash(pending.serialize()) + } + + /// Reads and decodes the delivered partial note this FSM processes. + pub(crate) unconstrained fn read_pending_partial_note(self) -> DeliveredPendingPartialNote { + let delivered = self.collection.facts.find(|f: Fact| f.fact_type_id == PARTIAL_NOTE_DELIVERED).unwrap(); + let mut fields = [0; ::N]; + for i in 0..fields.len() { + fields[i] = delivered.payload.get(i); + } + Deserialize::deserialize(fields) + } + + /// Whether the partial note FSM is in `completed` state, i.e. its note was completed and stored. + unconstrained fn is_completed(self) -> bool { + self.collection.facts.any(|f: Fact| f.fact_type_id == PARTIAL_NOTE_COMPLETED) + } + + /// Whether the partial note FSM is in `pending` state, i.e. it hasn't been completed yet. + pub(crate) unconstrained fn is_pending(self) -> bool { + !self.is_completed() + } + + /// Origin block of the partial note completion log, if available. + unconstrained fn completion_log_origin(self) -> Option { + let maybe_completed = self.collection.facts.find(|f: Fact| f.fact_type_id == PARTIAL_NOTE_COMPLETED); + if maybe_completed.is_some() { + maybe_completed.unwrap().origin_block + } else { + Option::none() + } + } + + /// Transition the partial note FSM as `completed`, conditional to `origin_block`. + unconstrained fn mark_completed(self, origin_block: OriginBlock) { + let empty_payload: EphemeralArray = EphemeralArray::empty(); + record_retractable_fact( + self.collection.contract_address, + self.collection.scope, + self.collection.fact_collection_type_id, + self.collection.fact_collection_id, + PARTIAL_NOTE_COMPLETED, + empty_payload, + origin_block, + ); + } + + /// Terminates this partial note FSM. + unconstrained fn terminate(self) { + delete_fact_collection( + self.collection.contract_address, + self.collection.scope, + self.collection.fact_collection_type_id, + self.collection.fact_collection_id, + ); + } + + /// Advances this partial-note FSM by one step. + /// + /// Partial notes can only be completed if completion logs have been found for them. Log discovery and + /// provisioning is responsibility of the caller. + /// + /// `compute_note_hash` and `compute_note_nullifier` are injected dependencies. + pub(crate) unconstrained fn step( + self, + maybe_completion_logs: Option>, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + ) { + if self.is_pending() { + self.step_pending(maybe_completion_logs, compute_note_hash, compute_note_nullifier); + } else { + self.step_completed(); + } + } + + /// Processes a partial note in pending state + /// + /// Completes the partial note if its completion log is given. If not, the partial note remains in pending state + /// and its completion log is searched for again on future syncs. + unconstrained fn step_pending( + self, + maybe_completion_logs: Option>, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + ) { + if maybe_completion_logs.is_some() { + let completion_logs = maybe_completion_logs.unwrap(); + let num_logs = completion_logs.len(); + if num_logs != 0 { + assert(num_logs == 1, f"Expected at most 1 completion log per partial note, got {num_logs}"); + self.complete_with_log(completion_logs.get(0), compute_note_hash, compute_note_nullifier); + } + } + } + + /// Advances partial note in `completed` state: terminates it once the completion log block finalizes, since the + /// discovered note is then permanent and there is nothing left to track. + unconstrained fn step_completed(self) { + if self.completion_log_origin().unwrap().block_state.is_finalized() { + self.terminate(); + } + } + + /// Completes a partial note. + /// + /// Combines the delivered private content with the log's public content, discovers the resulting note(s), enqueues + /// them for validation, and transitions the FSM to `completed` state conditional to the completion log's block. + /// + /// Because the completion fact is retractable, a reorg of the completion log's block rewinds the FSM back to + /// `pending` state (which signals that completion log search should restart). + unconstrained fn complete_with_log( + self, + log: LogRetrievalResponse, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + ) { + let pending = self.read_pending_partial_note(); + + // The first field in the completion log payload is the storage slot, followed by the public note content. + let storage_slot = log.log_payload.get(0); + let public_note_content: BoundedVec = array::subbvec(log.log_payload, 1); + + // Public fields are placed at the end of the packed representation, so we combine the private and public + // packed content to get the complete packed note. + let complete_packed_note = array::append(pending.packed_private_note_content, public_note_content); + + let discovered_notes = attempt_note_nonce_discovery( + log.unique_note_hashes_in_tx, + log.first_nullifier_in_tx, + compute_note_hash, + compute_note_nullifier, + self.collection.contract_address, + pending.owner, + storage_slot, + pending.randomness, + pending.note_type_id, + complete_packed_note, + ); + + // TODO(#11627): is there anything reasonable we can do if we get a log but it doesn't result in a note being + // found? + if discovered_notes.len() == 0 { + panic( + f"A partial note's completion log did not result in any notes being found - this should never happen", + ); + } + + aztecnr_debug_log_format!("Discovered {0} notes for partial note with tag {1}")([ + discovered_notes.len() as Field, + pending.note_completion_log_tag, + ]); + + discovered_notes.for_each(|discovered_note| { + enqueue_note_for_validation( + self.collection.contract_address, + pending.owner, + storage_slot, + pending.randomness, + discovered_note.note_nonce, + complete_packed_note, + discovered_note.note_hash, + discovered_note.inner_nullifier, + log.tx_hash, + ); + }); + + self.mark_completed(OriginBlock { block_number: log.block_number, block_hash: log.block_hash }); + } +} + +/// Serializes a [`DeliveredPendingPartialNote`] into a fact payload. +unconstrained fn to_payload(pending: DeliveredPendingPartialNote) -> EphemeralArray { + let fields = pending.serialize(); + let payload: EphemeralArray = EphemeralArray::empty(); + for i in 0..fields.len() { + payload.push(fields[i]); + } + payload +} + +mod test { + use crate::ephemeral::EphemeralArray; + use crate::facts::OriginBlock; + use crate::messages::logs::note::MAX_NOTE_PACKED_LEN; + use crate::messages::processing::log_retrieval_response::LogRetrievalResponse; + use crate::partial_notes::DeliveredPendingPartialNote; + use crate::protocol::address::AztecAddress; + use crate::test::helpers::test_environment::TestEnvironment; + use super::PartialNoteFsm; + + unconstrained fn setup() -> (TestEnvironment, AztecAddress) { + let mut env = TestEnvironment::new(); + let scope = env.create_light_account(); + (env, scope) + } + + /// A delivered partial note with simple dummy content. + unconstrained fn make_pending(owner: AztecAddress) -> DeliveredPendingPartialNote { + DeliveredPendingPartialNote { + owner, + randomness: 0x11, + note_completion_log_tag: 0x22, + note_type_id: 0x33, + packed_private_note_content: BoundedVec::new(), + } + } + + /// A low block, which TXE (proven == finalized == latest) reports as finalized. + fn finalized_block() -> OriginBlock { + OriginBlock { block_number: 1, block_hash: 0 } + } + + #[test] + unconstrained fn init_creates_a_partial_note_fsm_in_pending_state() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + let all = PartialNoteFsm::load_all(address, scope); + assert_eq(all.len(), 1); + assert(all.get(0).is_pending(), "freshly delivered reception should be pending"); + assert(all.get(0).completion_log_origin().is_none()); + }); + } + + #[test] + unconstrained fn read_pending_round_trips_the_delivered_content() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + let pending = PartialNoteFsm::load_all(address, scope).get(0).read_pending_partial_note(); + assert_eq(pending.owner, scope); + assert_eq(pending.randomness, 0x11); + assert_eq(pending.note_completion_log_tag, 0x22); + assert_eq(pending.note_type_id, 0x33); + }); + } + + #[test] + unconstrained fn mark_completed_moves_to_completed() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + PartialNoteFsm::load_all(address, scope).get(0).mark_completed(finalized_block()); + + let reloaded = PartialNoteFsm::load_all(address, scope).get(0); + assert(reloaded.is_completed(), "should be completed after mark_completed"); + assert(reloaded.completion_log_origin().unwrap().block_state.is_finalized()); + }); + } + + #[test] + unconstrained fn terminate_deletes_the_collection() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + PartialNoteFsm::load_all(address, scope).get(0).terminate(); + assert_eq(PartialNoteFsm::load_all(address, scope).len(), 0); + }); + } + + #[test] + unconstrained fn re_init_is_idempotent() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + assert_eq(PartialNoteFsm::load_all(address, scope).len(), 1); + }); + } + + #[test] + unconstrained fn step_leaves_a_pending_reception_untouched_when_it_has_no_completion_log() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + + // Neither an empty fetched-logs set (`Some(empty)`, the pass-1 "searched, found nothing" case) nor no + // fetch at all (`None`, the pass-2 case) can complete a pending reception, so it stays pending. + let no_logs: EphemeralArray = EphemeralArray::empty(); + PartialNoteFsm::load_all(address, scope).get(0).step( + Option::some(no_logs), + dummy_compute_note_hash, + dummy_compute_note_nullifier, + ); + PartialNoteFsm::load_all(address, scope).get(0).step( + Option::none(), + dummy_compute_note_hash, + dummy_compute_note_nullifier, + ); + + let all = PartialNoteFsm::load_all(address, scope); + assert_eq(all.len(), 1); + assert(all.get(0).is_pending(), "reception without a completion log should stay pending"); + }); + } + + #[test] + unconstrained fn step_terminates_a_completed_reception_once_its_block_is_finalized() { + let (env, scope) = setup(); + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + PartialNoteFsm::load_all(address, scope).get(0).mark_completed(finalized_block()); + + // Completed with a finalized completion block: the discovered note is reorg-proof, so step terminates the + // FSM by deleting its collection. + PartialNoteFsm::load_all(address, scope).get(0).step( + Option::none(), + dummy_compute_note_hash, + dummy_compute_note_nullifier, + ); + + assert_eq(PartialNoteFsm::load_all(address, scope).len(), 0); + }); + } + + #[test] + unconstrained fn step_keeps_a_completed_reception_while_its_block_is_unfinalized() { + let (env, scope) = setup(); + // A future block never finalizes here, so the completed reception must survive the step. + let unfinalized_block = OriginBlock { block_number: env.last_block_number() + 100, block_hash: 0 }; + env.private_context(|context| { + let address = context.this_address(); + PartialNoteFsm::init(address, scope, make_pending(scope), finalized_block()); + PartialNoteFsm::load_all(address, scope).get(0).mark_completed(unfinalized_block); + + // Completed but not yet finalized: step must not terminate it, since a reorg could still fold it back. + PartialNoteFsm::load_all(address, scope).get(0).step( + Option::none(), + dummy_compute_note_hash, + dummy_compute_note_nullifier, + ); + + let all = PartialNoteFsm::load_all(address, scope); + assert_eq(all.len(), 1); + assert(all.get(0).is_completed(), "unfinalized completed reception should survive step"); + }); + } + + unconstrained fn dummy_compute_note_hash( + _packed_note: BoundedVec, + _owner: AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: AztecAddress, + _randomness: Field, + ) -> Option { + Option::none() + } + + unconstrained fn dummy_compute_note_nullifier( + _unique_note_hash: Field, + _packed_note: BoundedVec, + _owner: AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: AztecAddress, + _randomness: Field, + ) -> Option { + Option::none() + } +} diff --git a/noir-projects/aztec-nr/aztec/src/partial_notes/mod.nr b/noir-projects/aztec-nr/aztec/src/partial_notes/mod.nr new file mode 100644 index 000000000000..730b70bf2b48 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/partial_notes/mod.nr @@ -0,0 +1,92 @@ +mod fsm; + +use crate::{ + facts::OriginBlock, + messages::{ + discovery::{ComputeNoteHash, ComputeNoteNullifier}, + encoding::MAX_MESSAGE_CONTENT_LEN, + logs::partial_note::{decode_partial_note_private_message, MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN}, + processing::{get_pending_partial_notes_completion_logs, ResolvedTx}, + }, +}; + +use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; +use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; +use fsm::PartialNoteFsm; + +/// A partial note that was delivered but is still pending completion. Contains the information necessary to find the +/// log that will complete it and lead to a note being discovered and delivered. +#[derive(Serialize, Deserialize)] +pub(crate) struct DeliveredPendingPartialNote { + pub(crate) owner: AztecAddress, + pub(crate) randomness: Field, + pub(crate) note_completion_log_tag: Field, + pub(crate) note_type_id: Field, + pub(crate) packed_private_note_content: BoundedVec, +} + +pub(crate) unconstrained fn process_partial_note_private_msg( + contract_address: AztecAddress, + msg_metadata: u64, + msg_content: BoundedVec, + resolved_tx: ResolvedTx, + recipient: AztecAddress, +) { + let decoded = decode_partial_note_private_message(msg_metadata, msg_content); + + if decoded.is_some() { + let (owner, randomness, note_completion_log_tag, note_type_id, packed_private_note_content) = decoded.unwrap(); + + PartialNoteFsm::init( + contract_address, + recipient, + DeliveredPendingPartialNote { + owner, + randomness, + note_completion_log_tag, + note_type_id, + packed_private_note_content, + }, + OriginBlock { block_number: resolved_tx.block_number, block_hash: resolved_tx.block_hash }, + ); + } else { + aztecnr_warn_log_format!( + "Could not decode partial note private message from tx {0}, ignoring", + )( + [resolved_tx.tx_hash], + ); + } +} + +/// Drives every active partial-note FSM forward. +pub(crate) unconstrained fn sync( + contract_address: AztecAddress, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, + scope: AztecAddress, +) { + // `load_all` is a relatively expensive oracle round-trip, so we call it once and reuse the result. + let active_partial_note_fsms = PartialNoteFsm::load_all(contract_address, scope); + + // Fetching completion logs is the dominant cost of partial note contract sync (a node round-trip each) and only + // pending notes need them, so we batch that fetch for the pending subset and then drive the FSMs in two passes: + // 1. Step each pending FSM with its freshly-fetched completion logs, giving found logs a chance to complete notes. + // 2. Step every active FSM with no logs, which is where completed-and-finalized FSMs terminate. + // Keeping both the completion and termination logic inside `PartialNoteFsm::step` is what lets the orchestrator + // stay this small, the price is the somewhat less elegant two-pass shape that the batched fetch forces on us. + let pending_partial_note_fsms = active_partial_note_fsms.filter(|fsm: PartialNoteFsm| fsm.is_pending()); + let pending_partial_notes = pending_partial_note_fsms.map(|fsm: PartialNoteFsm| fsm.read_pending_partial_note()); + + aztecnr_debug_log_format!("{} pending partial notes")([pending_partial_notes.len() as Field]); + + let completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes); + assert_eq(completion_logs.len(), pending_partial_notes.len()); + + pending_partial_note_fsms.for_each(|i, fsm: PartialNoteFsm| { + fsm.step(Option::some(completion_logs.get(i)), compute_note_hash, compute_note_nullifier); + }); + + active_partial_note_fsms.for_each(|_i, fsm: PartialNoteFsm| { + fsm.step(Option::none(), compute_note_hash, compute_note_nullifier); + }); +} 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 ef55059fb0fd..c494af854882 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 @@ -19,7 +19,7 @@ use crate::{ encoding::{MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN}, logs::{event::encode_private_event_message, note::encode_private_note_message}, offchain_messages::OFFCHAIN_MESSAGE_IDENTIFIER, - processing::{MessageContext, offchain::OffchainMessage, validate_and_store_enqueued_notes_and_events}, + processing::{offchain::OffchainMessage, ResolvedTx, validate_and_store_enqueued_notes_and_events}, }, note::{note_interface::{NoteHash, NoteType}, NoteMessage}, oracle::version::assert_compatible_oracle_version, @@ -1551,7 +1551,7 @@ impl TestEnvironment { // on a message plaintext. // First we fetch the transaction effects from the latest transaction, which are required to create the - // `MessageContext`, and might contain information about data (e.g. notes, events) contained in the message. In + // `ResolvedTx`, and might contain information about data (e.g. notes, events) contained in the message. In // a real contract this data would have either been supplied by the `process_message` caller, of retrieved // along with the message from a tagged log. let effects = txe_oracles::get_last_tx_effects(); @@ -1560,10 +1560,13 @@ impl TestEnvironment { // as events are properly scoped by recipient, but notes use a global scope instead. We therefore simply set // the zero address as the recipient if one is not supplied, which PXE accepts as a scope despite this not // being a registered account. - let message_context = MessageContext { + let resolved_tx = ResolvedTx { tx_hash: effects.tx_hash, unique_note_hashes_in_tx: effects.note_hashes, first_nullifier_in_tx: effects.nullifiers.get(0), + // Test fixture: the simulated message has no real delivery block, so anchor it to the zero block. + block_number: 0, + block_hash: 0, }; // The scope controls which PXE account can see discovered notes/events. We use the zero address as the @@ -1583,7 +1586,7 @@ impl TestEnvironment { compute_note_nullifier, process_custom_message, message_plaintext, - message_context, + resolved_tx, scope, ); diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap index 2c1cdc435e25..9193488200c3 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/amm_contract/snapshots__expanded.snap @@ -833,7 +833,7 @@ pub contract AMM { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::oracle::tx_resolution::ResolvedTx, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap index c219e2555419..64e7f25150e8 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/avm_gadgets_test_contract/snapshots__expanded.snap @@ -332,7 +332,7 @@ contract AvmGadgetsTest { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::oracle::tx_resolution::ResolvedTx, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap index 3df63f3bf259..8506d594f7bd 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/public_fns_with_emit_repro_contract/snapshots__expanded.snap @@ -308,7 +308,7 @@ pub contract PublicFnsWithEmitRepro { unconstrained fn sync_state(scope: aztec::protocol::address::AztecAddress) { let address: aztec::protocol::address::AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::oracle::tx_resolution::ResolvedTx, aztec::protocol::address::AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap index 5a84c91f96e3..0e3aad62bf15 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/storage_proof_test_contract/snapshots__expanded.snap @@ -260,7 +260,7 @@ contract StorageProofTest { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::oracle::tx_resolution::ResolvedTx, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { diff --git a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap index bd311ca39d73..5e277ac07d61 100644 --- a/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap +++ b/noir-projects/contract-snapshots/tests/snapshots/expand/token_contract/snapshots__expanded.snap @@ -1035,7 +1035,7 @@ pub contract Token { unconstrained fn sync_state(scope: AztecAddress) { let address: AztecAddress = aztec::context::UtilityContext::new().this_address(); - aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::messages::processing::MessageContext, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); + aztec::messages::discovery::do_sync_state(address, _compute_note_hash, _compute_note_nullifier, Option::, aztec::oracle::tx_resolution::ResolvedTx, AztecAddress)>::none(), Option:: aztec::unconstrained_array::UnconstrainedArray>::some(aztec::messages::processing::offchain::sync_inbox), scope); } pub struct offchain_receive_parameters { 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 e03a034f3aae..db18c41a63df 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 @@ -7,7 +7,7 @@ use ::aztec::{ encoding::MAX_MESSAGE_CONTENT_LEN, logs::event::MAX_EVENT_SERIALIZED_LEN, msg_type::custom_msg_type_id, - processing::{enqueue_event_for_validation, MessageContext}, + processing::{enqueue_event_for_validation, ResolvedTx}, }, oracle::capsules, protocol::{address::AztecAddress, hash::poseidon2_hash, traits::FromField}, @@ -31,7 +31,7 @@ unconstrained fn handle_multi_log_message( msg_type_id: u64, msg_metadata: u64, msg_content: BoundedVec, - message_context: MessageContext, + resolved_tx: ResolvedTx, scope: AztecAddress, ) { if msg_type_id == MULTI_LOG_MSG_TYPE_ID { @@ -83,7 +83,7 @@ unconstrained fn handle_multi_log_message( randomness, serialized_event, event_commitment, - message_context.tx_hash, + resolved_tx.tx_hash, ); } else { capsules::store(contract_address, slot, multi_log, scope); diff --git a/yarn-project/pxe/src/contract_function_simulator/index.ts b/yarn-project/pxe/src/contract_function_simulator/index.ts index 875e60162529..16800e82b5b0 100644 --- a/yarn-project/pxe/src/contract_function_simulator/index.ts +++ b/yarn-project/pxe/src/contract_function_simulator/index.ts @@ -25,7 +25,6 @@ export { LOG_RETRIEVAL_REQUEST, LOG_RETRIEVAL_RESPONSE, MEMBERSHIP_WITNESS, - MESSAGE_CONTEXT, NOTE_SELECTOR, NOTE_VALIDATION_REQUEST, OPTION, @@ -67,6 +66,7 @@ export { EventValidationRequest } from './noir-structs/event_validation_request. export type { LogRetrievalRequest } from './noir-structs/log_retrieval_request.js'; export type { LogRetrievalResponse } from './noir-structs/log_retrieval_response.js'; export { NoteValidationRequest } from './noir-structs/note_validation_request.js'; +export type { PendingTaggedLog } from './noir-structs/pending_tagged_log.js'; export type { TxEffectData } from './noir-structs/tx_effect_data.js'; export type { ProvidedSecret } from './noir-structs/provided_secret.js'; export { ResolvedTx } from './noir-structs/resolved_tx.js'; diff --git a/yarn-project/stdlib/src/logs/pending_tagged_log.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/pending_tagged_log.ts similarity index 77% rename from yarn-project/stdlib/src/logs/pending_tagged_log.ts rename to yarn-project/pxe/src/contract_function_simulator/noir-structs/pending_tagged_log.ts index 6d1b68711755..973ae5b4cef7 100644 --- a/yarn-project/stdlib/src/logs/pending_tagged_log.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/pending_tagged_log.ts @@ -1,6 +1,6 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; -import type { MessageContext } from './message_context.js'; +import type { ResolvedTx } from './resolved_tx.js'; /** * Represents a pending tagged log as it is stored in the pending tagged log array to which the fetchTaggedLogs oracle @@ -8,5 +8,5 @@ import type { MessageContext } from './message_context.js'; */ export type PendingTaggedLog = { log: Fr[]; - context: MessageContext; + context: ResolvedTx; }; diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/provided_secret.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/provided_secret.ts index f874e746cb2d..b90243c867a2 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/provided_secret.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/provided_secret.ts @@ -1,5 +1,5 @@ import type { Fr } from '@aztec/foundation/curves/bn254'; import type { AppTaggingSecretKind } from '@aztec/stdlib/logs'; -/** A tagging secret an app supplies explicitly to `getPendingTaggedLogs` when PXE cannot derive it internally. */ +/** A tagging secret an app supplies explicitly to `getPendingTaggedLogsV2` when PXE cannot derive it internally. */ export type ProvidedSecret = { secret: Fr; mode: AppTaggingSecretKind }; diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_registry.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_registry.ts index d70a79f0af8b..4b552f175348 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_registry.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/legacy_oracle_registry.ts @@ -1,8 +1,10 @@ /* eslint-disable camelcase */ -import { MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants'; +import { MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_CIPHERTEXT_LEN, PRIVATE_LOG_SIZE_IN_FIELDS } from '@aztec/constants'; import type { EphemeralArray } from '../noir-structs/ephemeral_array.js'; import type { LogRetrievalResponse } from '../noir-structs/log_retrieval_response.js'; +import type { PendingTaggedLog } from '../noir-structs/pending_tagged_log.js'; +import type { ResolvedTx } from '../noir-structs/resolved_tx.js'; import { type InferDeserializedParams, ORACLE_REGISTRY, @@ -26,6 +28,19 @@ const LEGACY_LOG_RETRIEVAL_RESPONSE: TypeMapping = S { name: 'firstNullifierInTx', type: FIELD }, ]); +// Block-less transaction context: the prefix of `ResolvedTx`'s wire carried by the original `getPendingTaggedLogs` +// oracle. Known as `MessageContext` in the Aztec.nr versions that call that oracle. +const LEGACY_MESSAGE_CONTEXT: TypeMapping = STRUCT([ + { name: 'txHash', type: TX_HASH }, + { name: 'uniqueNoteHashesInTx', type: FIXED_BOUNDED_VEC(FIELD, MAX_NOTE_HASHES_PER_TX) }, + { name: 'firstNullifierInTx', type: FIELD }, +]); + +const LEGACY_PENDING_TAGGED_LOG: TypeMapping = STRUCT([ + { name: 'log', type: FIXED_BOUNDED_VEC(FIELD, PRIVATE_LOG_SIZE_IN_FIELDS) }, + { name: 'context', type: LEGACY_MESSAGE_CONTEXT }, +]); + /** * Wire shapes that already-deployed contracts still call by their original oracle name, keyed by that retired name. * New Aztec.nr calls the current name in `ORACLE_REGISTRY`; old bytecode keeps calling the name compiled into it and @@ -43,6 +58,14 @@ export const LEGACY_ORACLE_REGISTRY: Record = { mapping: result => result as unknown as EphemeralArray>, }, }), + aztec_utl_getPendingTaggedLogs: legacyOracle({ + modernOracle: 'aztec_utl_getPendingTaggedLogsV2', + returnType: { + legacyType: EPHEMERAL_ARRAY(LEGACY_PENDING_TAGGED_LOG), + // We can map this directly, since `ResolvedTx` carries the legacy context's fields under the same names + mapping: result => result as unknown as EphemeralArray, + }, + }), }; type LegacyLogRetrievalResponse = Pick< @@ -50,6 +73,13 @@ type LegacyLogRetrievalResponse = Pick< 'logPayload' | 'txHash' | 'uniqueNoteHashesInTx' | 'firstNullifierInTx' >; +type LegacyMessageContext = Pick; + +type LegacyPendingTaggedLog = { + log: PendingTaggedLog['log']; + context: LegacyMessageContext; +}; + type Registry = typeof ORACLE_REGISTRY; /** The handler return value type of a modern oracle entry. */ diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts index 13303e41a9ca..d6b2bdafa36c 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_registry.ts @@ -225,7 +225,7 @@ export const ORACLE_REGISTRY = { returnType: BOUNDED_VEC(NOTE), }), - aztec_utl_getPendingTaggedLogs: makeEntry({ + aztec_utl_getPendingTaggedLogsV2: makeEntry({ params: [ { name: 'scope', type: AZTEC_ADDRESS }, { name: 'providedSecrets', type: EPHEMERAL_ARRAY(PROVIDED_SECRET) }, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts index 27e787f9f33d..661dace2fd72 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.ts @@ -31,8 +31,6 @@ import type { PublicKeys } from '@aztec/stdlib/keys'; import { type AppTaggingSecretKind, type FlatPublicLogs, - type MessageContext, - type PendingTaggedLog, PrivateLog, Tag, appTaggingSecretKindFromDeliveryMode, @@ -67,6 +65,7 @@ import type { LogRetrievalResponse } from '../noir-structs/log_retrieval_respons import type { NoteData } from '../noir-structs/note_data.js'; import { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { Option } from '../noir-structs/option.js'; +import type { PendingTaggedLog } from '../noir-structs/pending_tagged_log.js'; import type { ProvidedSecret } from '../noir-structs/provided_secret.js'; import { type ResolvedTaggingStrategy, @@ -88,7 +87,6 @@ export type { BlockHeader, ContractInstancePreimage, MembershipWitness, - MessageContext, NullifierMembershipWitness, PendingTaggedLog, PublicDataWitness, @@ -523,17 +521,6 @@ export const LOG_RETRIEVAL_RESPONSE: TypeMapping = STRUCT< { name: 'blockHash', type: BLOCK_HASH }, ]); -export const MESSAGE_CONTEXT: TypeMapping = STRUCT([ - { name: 'txHash', type: TX_HASH }, - { name: 'uniqueNoteHashesInTx', type: FIXED_BOUNDED_VEC(FIELD, MAX_NOTE_HASHES_PER_TX) }, - { name: 'firstNullifierInTx', type: FIELD }, -]); - -export const PENDING_TAGGED_LOG: TypeMapping = STRUCT([ - { name: 'log', type: FIXED_BOUNDED_VEC(FIELD, PRIVATE_LOG_SIZE_IN_FIELDS) }, - { name: 'context', type: MESSAGE_CONTEXT }, -]); - // `ResolvedTx.toFields()` packs the whole struct into a single slot: txHash, the uniqueNoteHashesInTx BoundedVec // (MAX_NOTE_HASHES_PER_TX storage fields + length), firstNullifierInTx, blockNumber and blockHash. export const RESOLVED_TX: TypeMapping = { @@ -541,6 +528,11 @@ export const RESOLVED_TX: TypeMapping = { shape: [{ len: MAX_NOTE_HASHES_PER_TX + 5 }], }; +export const PENDING_TAGGED_LOG: TypeMapping = STRUCT([ + { name: 'log', type: FIXED_BOUNDED_VEC(FIELD, PRIVATE_LOG_SIZE_IN_FIELDS) }, + { name: 'context', type: RESOLVED_TX }, +]); + export const ORIGIN_BLOCK: TypeMapping = { serialization: { fn: ob => [new Fr(ob.blockNumber), ob.blockHash] }, deserialization: { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 2e7cadf32563..e14aecdb2725 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -55,6 +55,7 @@ import type { EmbeddedCurvePoint } from '../noir-structs/embedded_curve_point.js import { EphemeralArray } from '../noir-structs/ephemeral_array.js'; import { Option } from '../noir-structs/option.js'; import type { ProvidedSecret } from '../noir-structs/provided_secret.js'; +import { ResolvedTx } from '../noir-structs/resolved_tx.js'; import { TransientArrayService } from '../transient_array_service.js'; import { UtilityExecutionOracle, type UtilityExecutionOracleArgs } from './utility_execution_oracle.js'; @@ -592,7 +593,7 @@ describe('Utility Execution test suite', () => { }); }); - describe('getPendingTaggedLogs', () => { + describe('getPendingTaggedLogsV2', () => { const service = new EphemeralArrayService(); it("uses the provided secret's delivery mode when querying pending log tags", async () => { @@ -630,7 +631,7 @@ describe('Utility Execution test suite', () => { ); }); - const result = await utilityExecutionOracle.getPendingTaggedLogs( + const result = await utilityExecutionOracle.getPendingTaggedLogsV2( owner, EphemeralArray.fromValues(service, providedSecrets), ); @@ -644,11 +645,13 @@ describe('Utility Execution test suite', () => { expect(resultLogs).toEqual([ { log: log.logData, - context: { - txHash: log.txHash, - uniqueNoteHashesInTx: log.noteHashes, - firstNullifierInTx: log.nullifiers[0], - }, + context: new ResolvedTx( + log.txHash, + log.noteHashes, + log.nullifiers[0], + log.blockNumber, + log.blockHash.toFr(), + ), }, ]); }); 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 271562805d24..8c86f505e722 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 @@ -25,7 +25,7 @@ import { siloNullifier } from '@aztec/stdlib/hash'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import type { KeyValidationRequest } from '@aztec/stdlib/kernel'; import { PublicKeys, computeAddressSecret, hashPublicKey } from '@aztec/stdlib/keys'; -import { AppTaggingSecret, FlatPublicLogs, type PendingTaggedLog, appSiloEcdhSharedSecret } from '@aztec/stdlib/logs'; +import { AppTaggingSecret, FlatPublicLogs, appSiloEcdhSharedSecret } from '@aztec/stdlib/logs'; import { getNonNullifiedL1ToL2MessageWitness } from '@aztec/stdlib/messaging'; import type { NoteStatus } from '@aztec/stdlib/note'; import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees'; @@ -67,6 +67,7 @@ import type { LogRetrievalResponse } from '../noir-structs/log_retrieval_respons import type { NoteData } from '../noir-structs/note_data.js'; import type { NoteValidationRequest } from '../noir-structs/note_validation_request.js'; import { Option } from '../noir-structs/option.js'; +import type { PendingTaggedLog } from '../noir-structs/pending_tagged_log.js'; import type { ProvidedSecret } from '../noir-structs/provided_secret.js'; import type { ResolvedTx } from '../noir-structs/resolved_tx.js'; import type { TxEffectData } from '../noir-structs/tx_effect_data.js'; @@ -572,7 +573,7 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra } /** Fetches pending tagged logs into a freshly allocated ephemeral array and returns it. */ - public async getPendingTaggedLogs( + public async getPendingTaggedLogsV2( scope: AztecAddress, providedSecrets: EphemeralArray, ): Promise> { diff --git a/yarn-project/pxe/src/logs/log_service.ts b/yarn-project/pxe/src/logs/log_service.ts index 81cc6c1b0ce9..f632741f190f 100644 --- a/yarn-project/pxe/src/logs/log_service.ts +++ b/yarn-project/pxe/src/logs/log_service.ts @@ -7,13 +7,7 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { BlockHash, L2TipsProvider } from '@aztec/stdlib/block'; import type { CompleteAddress } from '@aztec/stdlib/contract'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; -import { - AppTaggingSecret, - type LogResult, - type PendingTaggedLog, - SiloedTag, - computeSharedTaggingSecret, -} from '@aztec/stdlib/logs'; +import { AppTaggingSecret, type LogResult, SiloedTag, computeSharedTaggingSecret } from '@aztec/stdlib/logs'; import type { BlockHeader } from '@aztec/stdlib/tx'; import { @@ -21,6 +15,8 @@ import { LogSource, } from '../contract_function_simulator/noir-structs/log_retrieval_request.js'; import type { LogRetrievalResponse } from '../contract_function_simulator/noir-structs/log_retrieval_response.js'; +import type { PendingTaggedLog } from '../contract_function_simulator/noir-structs/pending_tagged_log.js'; +import { ResolvedTx } from '../contract_function_simulator/noir-structs/resolved_tx.js'; import { AddressStore } from '../storage/address_store/address_store.js'; import type { RecipientTaggingStore } from '../storage/tagging_store/recipient_tagging_store.js'; import type { TaggingSecretSourcesStore } from '../storage/tagging_store/tagging_secret_sources_store.js'; @@ -219,7 +215,7 @@ export class LogService { } return { log: log.logData, - context: { txHash: log.txHash, uniqueNoteHashesInTx: noteHashes, firstNullifierInTx: nullifiers[0] }, + context: new ResolvedTx(log.txHash, noteHashes, nullifiers[0], log.blockNumber, log.blockHash.toFr()), }; }); } diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 465664ab4a72..d462ce6132db 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 = 30; -export const ORACLE_VERSION_MINOR = 4; +export const ORACLE_VERSION_MINOR = 5; /// This hash is computed from the `ORACLE_REGISTRY` declaration (each oracle's name, ordered parameter names and /// types, and return type) and is used to detect when the oracle interface changes. When it does, you need to either: @@ -19,4 +19,4 @@ export const ORACLE_VERSION_MINOR = 4; /// - 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 = '8946d1d9cfc7ceb0e8b2d656d23258f1fc82a7faa65c43515b1f141e71de6212'; +export const ORACLE_INTERFACE_HASH = 'f71b9662ec851e74593764a87792b0a91c181e1410aac49a4507f0bba949f6be'; diff --git a/yarn-project/pxe/src/storage/fact_store/fact_store.ts b/yarn-project/pxe/src/storage/fact_store/fact_store.ts index b212c07f790f..0d919eb709d1 100644 --- a/yarn-project/pxe/src/storage/fact_store/fact_store.ts +++ b/yarn-project/pxe/src/storage/fact_store/fact_store.ts @@ -82,7 +82,7 @@ export class FactStore implements StagedStore { } /** - * Records a fact in a collection. + * Records a fact in a collection, visible under the given scope. * * The collection is created implicitly on the first fact recorded for its key: recording into an existing collection * just adds to it. diff --git a/yarn-project/stdlib/src/logs/index.ts b/yarn-project/stdlib/src/logs/index.ts index 5b5adf950c22..fdf421b16838 100644 --- a/yarn-project/stdlib/src/logs/index.ts +++ b/yarn-project/stdlib/src/logs/index.ts @@ -5,13 +5,11 @@ export * from './tagging_index_range.js'; export * from './contract_class_log.js'; export * from './public_log.js'; export * from './private_log.js'; -export * from './pending_tagged_log.js'; export * from './log_result.js'; export * from './log_cursor.js'; export * from './logs_query.js'; export * from './query_all_logs_by_tags.js'; export * from './shared_secret_derivation.js'; -export * from './message_context.js'; export * from './debug_log.js'; export * from './debug_log_store.js'; export * from './tag.js'; diff --git a/yarn-project/stdlib/src/logs/message_context.ts b/yarn-project/stdlib/src/logs/message_context.ts deleted file mode 100644 index eb2f3f1db47c..000000000000 --- a/yarn-project/stdlib/src/logs/message_context.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Fr } from '@aztec/foundation/curves/bn254'; - -import type { TxHash } from '../tx/tx_hash.js'; - -/** - * Additional information needed to process a message. - * - * All messages exist in the context of a transaction, and information about that transaction is typically required - * in order to perform validation, store results, etc. For example, messages containing notes require knowledge of note - * hashes and the first nullifier in order to find the note's nonce. - * - * A TS version of `message_context.nr`. - */ -export type MessageContext = { - txHash: TxHash; - uniqueNoteHashesInTx: Fr[]; - firstNullifierInTx: Fr; -}; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index ce11b500c27b..0f37f2f32bdf 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -582,11 +582,11 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_utl_getPendingTaggedLogs(...inputs: ForeignCallArgs) { + aztec_utl_getPendingTaggedLogsV2(...inputs: ForeignCallArgs) { return callTxeHandler({ - oracle: 'aztec_utl_getPendingTaggedLogs', + oracle: 'aztec_utl_getPendingTaggedLogsV2', inputs, - handler: ([scope, providedSecrets]) => this.handlerAsUtility().getPendingTaggedLogs(scope, providedSecrets), + handler: ([scope, providedSecrets]) => this.handlerAsUtility().getPendingTaggedLogsV2(scope, providedSecrets), }); }