diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index 08355b18c22c..6f2553564636 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -9,12 +9,13 @@ pub mod nonce_discovery; /// We reserve two fields in the note private log that are not part of the note content: one for the storage slot, and /// one for the combined log and note type ID. -global NOTE_PRIVATE_LOG_RESERVED_FIELDS: u32 = 2; +global PRIVATE_LOG_EXPANDED_METADATA_LEN: u32 = 1; -/// The maximum length of the packed representation of a note's contents. This is limited by private log size, encryption -/// overhead and extra fields in the log (e.g. the combined log and note type ID). -pub global MAX_NOTE_PACKED_LEN: u32 = - PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS - NOTE_PRIVATE_LOG_RESERVED_FIELDS; +/// The maximum length of the log's content, i.e. after log type ID and metadata extraction. +pub global MAX_LOG_CONTENT_LEN: u32 = + PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS - PRIVATE_LOG_EXPANDED_METADATA_LEN; + +use private_notes::MAX_NOTE_PACKED_LEN; pub struct NoteHashAndNullifier { /// The result of NoteHash::compute_note_hash @@ -32,9 +33,9 @@ pub struct NoteHashAndNullifier { /// `_compute_note_hash_and_nullifier`, which looks something like this: /// /// ``` -/// |packed_note_content, contract_address, nonce, storage_slot, note_type_id| { +/// |packed_note, contract_address, nonce, storage_slot, note_type_id| { /// if note_type_id == MyNoteType::get_id() { -/// assert(packed_note_content.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); +/// assert(packed_note.len() == MY_NOTE_TYPE_SERIALIZATION_LENGTH); /// /// let note = MyNoteType::unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); /// @@ -58,7 +59,7 @@ pub struct NoteHashAndNullifier { /// }; /// } /// ``` -type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note_content */BoundedVec, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* nonce */ Field) -> Option; +type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* nonce */ Field) -> Option; /// Performs the message discovery process, in which private are downloaded and inspected to find new private notes, /// partial notes and events, etc., and pending partial notes are processed to search for their completion logs. diff --git a/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr index 271472a38a9d..b3dfcba60b3d 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/nonce_discovery.nr @@ -1,4 +1,7 @@ -use crate::{discovery::{ComputeNoteHashAndNullifier, MAX_NOTE_PACKED_LEN}, utils::array}; +use crate::{ + discovery::{ComputeNoteHashAndNullifier, private_notes::MAX_NOTE_PACKED_LEN}, + utils::array, +}; use dep::protocol_types::{ address::AztecAddress, @@ -30,7 +33,7 @@ pub unconstrained fn attempt_note_nonce_discovery( contract_address: AztecAddress, storage_slot: Field, note_type_id: Field, - packed_note_content: BoundedVec, + packed_note: BoundedVec, ) -> BoundedVec { let discovered_notes = &mut BoundedVec::new(); @@ -52,7 +55,7 @@ pub unconstrained fn attempt_note_nonce_discovery( // the note hash at the array index we're currently processing. // TODO(#11157): handle failed note_hash_and_nullifier computation let hashes = compute_note_hash_and_nullifier( - packed_note_content, + packed_note, storage_slot, note_type_id, contract_address, diff --git a/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr index 8d950e33d1eb..f9f4dc597f2b 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/partial_notes.nr @@ -2,7 +2,7 @@ use crate::{ capsules::CapsuleArray, discovery::{ ComputeNoteHashAndNullifier, - MAX_NOTE_PACKED_LEN, + MAX_LOG_CONTENT_LEN, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, }, oracle::message_discovery::{deliver_note, get_log_by_tag}, @@ -16,11 +16,12 @@ use dep::protocol_types::{ traits::{Deserialize, Serialize, ToField}, }; -pub global PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN: u32 = 1; -/// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (i.e. -/// the note completion log tag). +global PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN: u32 = 2; + +/// Partial notes have a maximum packed length of their private fields bound by extra content in their private log (e.g. +/// the storage slot, note completion log tag, etc.). pub global MAX_PARTIAL_NOTE_PRIVATE_PACKED_LEN: u32 = - MAX_NOTE_PACKED_LEN - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN; + MAX_LOG_CONTENT_LEN - PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN; /// The slot in the PXE capsules where we store a `CapsuleArray` of `DeliveredPendingPartialNote`. // TODO(#11630): come up with some sort of slot allocation scheme. @@ -50,23 +51,20 @@ pub(crate) struct DeliveredPendingPartialNote { pub unconstrained fn process_partial_note_private_log( contract_address: AztecAddress, - storage_slot: Field, - note_type_id: Field, - log_payload: BoundedVec, recipient: AztecAddress, + log_metadata: u64, + log_content: BoundedVec, ) { - // We store the information of the partial note we found so that we can later search for the public log that will - // complete it. The tag is the first value in the payload, with the packed note content taking up the rest of it. - std::static_assert( - PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN == 1, - "unexpected value for PARTIAL_NOTE_COMPLETION_LOG_TAG_LEN", - ); + let (note_type_id, storage_slot, note_completion_log_tag, packed_private_note_content) = + decode_partial_note_private_log(log_metadata, log_content); + // 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 pending = DeliveredPendingPartialNote { - note_completion_log_tag: log_payload.get(0), + note_completion_log_tag, storage_slot, note_type_id, - packed_private_note_content: array::subbvec(log_payload, 1), + packed_private_note_content, recipient, }; @@ -128,7 +126,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( // complete packed content. let packed_public_note_content: BoundedVec<_, MAX_PUBLIC_PARTIAL_NOTE_PACKED_CONTENT_LENGTH> = array::subbvec(log.log_content, NON_PACKED_CONTENT_FIELDS_IN_PUBLIC_LOG); - let complete_packed_note_content = array::append( + let complete_packed_note = array::append( pending_partial_note.packed_private_note_content, packed_public_note_content, ); @@ -140,7 +138,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( contract_address, pending_partial_note.storage_slot, pending_partial_note.note_type_id, - complete_packed_note_content, + complete_packed_note, ); debug_log_format( @@ -158,7 +156,7 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( contract_address, pending_partial_note.storage_slot, discovered_note.nonce, - complete_packed_note_content, + complete_packed_note, discovered_note.note_hash, discovered_note.inner_nullifier, log.tx_hash, @@ -180,3 +178,31 @@ pub unconstrained fn fetch_and_process_public_partial_note_completion_logs( } } } + +fn decode_partial_note_private_log( + log_metadata: u64, + log_content: BoundedVec, +) -> (Field, Field, Field, BoundedVec) { + let note_type_id = log_metadata as Field; // TODO: make note type id not be a full field + + assert( + log_content.len() > PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN, + f"Invalid private note log: all partial note private logs must have at least {PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN} fields", + ); + + // If PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN is changed, causing the assertion below to fail, then the + // destructuring of the partial note private log encoding below must be updated as well. + std::static_assert( + PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN == 2, + "unexpected value for PARTIAL_NOTE_PRIVATE_LOG_CONTENT_NON_NOTE_FIELDS_LEN", + ); + + // We currently have two fields that are not the partial note's packed representation, which are the storage slot + // and the note completion log tag. + let storage_slot = log_content.get(0); + let note_completion_log_tag = log_content.get(1); + + let packed_private_note_content = array::subbvec(log_content, 2); + + (note_type_id, storage_slot, note_completion_log_tag, packed_private_note_content) +} diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index e9543650f43a..d87e47208e64 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -1,5 +1,3 @@ -use std::static_assert; - use crate::{oracle::message_discovery::sync_notes, utils::array}; use dep::protocol_types::{ @@ -9,8 +7,9 @@ use dep::protocol_types::{ }; use crate::discovery::{ - ComputeNoteHashAndNullifier, MAX_NOTE_PACKED_LEN, NOTE_PRIVATE_LOG_RESERVED_FIELDS, - partial_notes::process_partial_note_private_log, private_notes::process_private_note_log, + ComputeNoteHashAndNullifier, MAX_LOG_CONTENT_LEN, + partial_notes::process_partial_note_private_log, PRIVATE_LOG_EXPANDED_METADATA_LEN, + private_notes::process_private_note_log, }; use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::decrypt_log; // TODO(#12750): don't make this value assume we're using AES. @@ -54,8 +53,7 @@ pub unconstrained fn do_process_log( // currently just have two log types: 0 for private notes and 1 for partial notes. This will likely be expanded and // improved upon in the future to also handle events, etc. - let (storage_slot, note_type_id, log_type_id, log_payload) = - destructure_log_plaintext(log_plaintext); + let (log_type_id, log_metadata, log_content) = decode_log_plaintext(log_plaintext); if log_type_id == 0 { debug_log("Processing private note log"); @@ -67,53 +65,66 @@ pub unconstrained fn do_process_log( first_nullifier_in_tx, recipient, compute_note_hash_and_nullifier, - storage_slot, - note_type_id, - log_payload, + log_metadata, + log_content, ); } else if log_type_id == 1 { debug_log("Processing partial note private log"); - process_partial_note_private_log( - contract_address, - storage_slot, - note_type_id, - log_payload, - recipient, - ); + process_partial_note_private_log(contract_address, recipient, log_metadata, log_content); } else { // TODO(#11569): handle events debug_log_format( "Unknown log type id {0} (probably belonging to an event log)", - [log_type_id], + [log_type_id as Field], ); } } -unconstrained fn destructure_log_plaintext( +/// Decodes a log's plaintext following aztec-nr's standard log encoding. +/// +/// The standard private log layout is composed of: +/// - an initial field called the 'expanded metadata' +/// - an arbitrary number of fields following that called the 'log content' +/// +/// ``` +/// log_plainext: [ log_expanded_metadata, ...log_content ] +/// ``` +/// +/// The expanded metadata itself is (currently) interpreted as a u64, of which: +/// - the upper 57 bits are the log type id +/// - the remaining 7 bits are called the 'log metadata' +/// +/// ``` +/// log_expanded_metadata: [ log_type_id | log_metadata ] +/// <--- 57 bits --->|<--- 7 bits ---> +/// ``` +/// +/// The meaning of the log metadata and log content depend on the value of the log type id. Note that there is +/// nothing special about the log metadata, it _can_ be considered part of the content. It just has a different name +/// to make it distinct from the log content given that it is not a full field. +unconstrained fn decode_log_plaintext( log_plaintext: BoundedVec, -) -> (Field, Field, Field, BoundedVec) { - assert(log_plaintext.len() >= NOTE_PRIVATE_LOG_RESERVED_FIELDS); +) -> (u64, u64, BoundedVec) { + assert( + log_plaintext.len() >= PRIVATE_LOG_EXPANDED_METADATA_LEN, + f"Invalid log plaintext: all logs must be decrypted into at least {PRIVATE_LOG_EXPANDED_METADATA_LEN} fields", + ); - // If NOTE_PRIVATE_LOG_RESERVED_FIELDS is changed, causing the assertion below to fail, then the declarations for - // `storage_slot` and `note_type_id` must be updated as well. - static_assert( - NOTE_PRIVATE_LOG_RESERVED_FIELDS == 2, - "unexpected value for NOTE_PRIVATE_LOG_RESERVED_FIELDS", + // If PRIVATE_LOG_EXPANDED_METADATA_LEN is changed, causing the assertion below to fail, then the destructuring of + // the log encoding below must be updated as well. + std::static_assert( + PRIVATE_LOG_EXPANDED_METADATA_LEN == 1, + "unexpected value for PRIVATE_LOG_EXPANDED_METADATA_LEN", ); - let storage_slot = log_plaintext.get(0); - // We currently identify log types by packing the log type ID and note type ID into a single field, called the - // combined type ID. We can do this because the note type ID is only 7 bits long, and so use an 8th bit to - // distinguish private note logs and partial note logs. - // This abuses the fact that the encoding of both of these logs is extremely similar, and will need improving and - // more formalization once we introduce other dissimilar log types, such as events. Ideally we'd be able to - // leverage enums and tagged unions to achieve this goal. - let combined_type_id = log_plaintext.get(1); - let note_type_id = ((combined_type_id as u64) % 128) as Field; - let log_type_id = ((combined_type_id as u64) / 128) as Field; + // See the documentation of this function for a description of the log layout + let expanded_log_metadata = log_plaintext.get(0); + + let log_type_id = ((expanded_log_metadata as u64) / 128); + let log_metadata = ((expanded_log_metadata as u64) % 128); - let log_payload = array::subbvec(log_plaintext, NOTE_PRIVATE_LOG_RESERVED_FIELDS); + let log_content = array::subbvec(log_plaintext, PRIVATE_LOG_EXPANDED_METADATA_LEN); - (storage_slot, note_type_id, log_type_id, log_payload) + (log_type_id, log_metadata, log_content) } diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_notes.nr index 6ab67303b951..c6c8216842df 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_notes.nr @@ -1,7 +1,7 @@ use crate::{ discovery::{ ComputeNoteHashAndNullifier, - MAX_NOTE_PACKED_LEN, + MAX_LOG_CONTENT_LEN, nonce_discovery::{attempt_note_nonce_discovery, DiscoveredNoteInfo}, }, oracle, @@ -11,6 +11,14 @@ use protocol_types::{ address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, debug_log::debug_log_format, }; +/// The number of fields in a private note log content that are not the note's packed representation. +global PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN: u32 = 1; + +/// The maximum length of the packed representation of a note's contents. This is limited by private log size, +/// encryption overhead and extra fields in the log (e.g. log type id, storage slot, etc.). +pub global MAX_NOTE_PACKED_LEN: u32 = + MAX_LOG_CONTENT_LEN - PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN; + pub unconstrained fn process_private_note_log( contract_address: AztecAddress, tx_hash: Field, @@ -18,13 +26,11 @@ pub unconstrained fn process_private_note_log( first_nullifier_in_tx: Field, recipient: AztecAddress, compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, - storage_slot: Field, - note_type_id: Field, - packed_note_content: BoundedVec, + log_metadata: u64, + log_content: BoundedVec, ) { - // Currently a placeholder, since we want to both have a 'process log kind' fn and keep 'attempt_note_discovery'. - // This is where we'll soon extract note type id, storage slot and packed note content from the log metadata and log - // content. + let (note_type_id, storage_slot, packed_note) = + decode_private_note_log(log_metadata, log_content); attempt_note_discovery( contract_address, @@ -35,7 +41,7 @@ pub unconstrained fn process_private_note_log( compute_note_hash_and_nullifier, storage_slot, note_type_id, - packed_note_content, + packed_note, ); } @@ -50,7 +56,7 @@ pub unconstrained fn attempt_note_discovery( compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, storage_slot: Field, note_type_id: Field, - packed_note_content: BoundedVec, + packed_note: BoundedVec, ) { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, @@ -59,7 +65,7 @@ pub unconstrained fn attempt_note_discovery( contract_address, storage_slot, note_type_id, - packed_note_content, + packed_note, ); debug_log_format( @@ -77,7 +83,7 @@ pub unconstrained fn attempt_note_discovery( contract_address, storage_slot, discovered_note.nonce, - packed_note_content, + packed_note, discovered_note.note_hash, discovered_note.inner_nullifier, tx_hash, @@ -88,3 +94,28 @@ pub unconstrained fn attempt_note_discovery( }, ); } + +fn decode_private_note_log( + log_metadata: u64, + log_content: BoundedVec, +) -> (Field, Field, BoundedVec) { + let note_type_id = log_metadata as Field; // TODO: make note type id not be a full field + + assert( + log_content.len() > PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN, + f"Invalid private note log: all private note logs must have at least {PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN} fields", + ); + + // If PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN is changed, causing the assertion below to fail, then the destructuring of + // the private note log encoding below must be updated as well. + std::static_assert( + PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN == 1, + "unexpected value for PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN", + ); + + // We currently have a single field that is not the note's packed representation, which is the storage slot. + let storage_slot = log_content.get(0); + let packed_note = array::subbvec(log_content, PRIVATE_NOTE_LOG_CONTENT_NON_NOTE_FIELDS_LEN); + + (note_type_id, storage_slot, packed_note) +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr index 2bec51d69339..2c46d6f0922a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr @@ -125,8 +125,16 @@ where let packed_note = note.pack(); let mut fields = [0; N + 2]; - fields[0] = storage_slot; - fields[1] = Note::get_id(); // Note that the note id only uses 7 bits of this field. + + // Note that we're almost accidentally following the standard log encoding here: because the note type id only uses + // 7 bits, it just barely fits in the log metadata, and the log type id is implicitly 0 (i.e. a private note log). + // Partial notes modify `get_id` to have it set the 8th bit, resulting in a log type id of 1 (i.e. a partial note + // private log). Fields 1 to len are the note content, which we here hardcode to be the storage slot in the first + // field, and the packed note following after. + // Ideally we'd know if this is a private note or a partial note, and call a function that'd be the opposite of + // discovery::private_notes::decode_private_note_log (or partial accordingly). + fields[0] = Note::get_id(); + fields[1] = storage_slot; for i in 0..packed_note.len() { fields[i + 2] = packed_note[i]; } diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index bda965d605e0..56a0fb57c477 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -1,5 +1,5 @@ use crate::{ - discovery::MAX_NOTE_PACKED_LEN, + discovery::private_notes::MAX_NOTE_PACKED_LEN, macros::{ dispatch::generate_public_dispatch, functions::{ @@ -222,7 +222,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] unconstrained fn _compute_note_hash_and_nullifier( - packed_note: BoundedVec, + packed_note: BoundedVec, storage_slot: Field, note_type_id: Field, contract_address: aztec::protocol_types::address::AztecAddress, @@ -244,7 +244,7 @@ comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() - /// This function is automatically injected by the `#[aztec]` macro. #[contract_library_method] unconstrained fn _compute_note_hash_and_nullifier( - _packed_note: BoundedVec, + _packed_note: BoundedVec, _storage_slot: Field, _note_type_id: Field, _contract_address: aztec::protocol_types::address::AztecAddress, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/message_discovery.nr b/noir-projects/aztec-nr/aztec/src/oracle/message_discovery.nr index 75bc0a93f86f..9bb616a3d5f3 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/message_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/message_discovery.nr @@ -1,4 +1,4 @@ -use crate::discovery::MAX_NOTE_PACKED_LEN; +use crate::discovery::private_notes::MAX_NOTE_PACKED_LEN; use dep::protocol_types::{ address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PUBLIC_LOG_DATA_SIZE_IN_FIELDS},