diff --git a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp index 01888967c45b..8093d2884049 100644 --- a/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp +++ b/barretenberg/cpp/src/barretenberg/crypto/merkle_tree/node_store/cached_content_addressed_tree_store.hpp @@ -867,6 +867,10 @@ void ContentAddressedCachedTreeStore::advance_finalized_block(con ReadTransactionPtr readTx = create_read_transaction(); get_meta(uncommittedMeta); get_meta(committedMeta, *readTx, false); + // do nothing if the block is already finalized + if (committedMeta.finalizedBlockHeight >= blockNumber) { + return; + } if (!dataStore_->read_block_data(blockNumber, blockPayload, *readTx)) { throw std::runtime_error(format("Unable to advance finalized block: ", blockNumber, @@ -874,10 +878,6 @@ void ContentAddressedCachedTreeStore::advance_finalized_block(con forkConstantData_.name_)); } } - // do nothing if the block is already finalized - if (committedMeta.finalizedBlockHeight >= blockNumber) { - return; - } // can currently only finalize up to the unfinalized block height if (committedMeta.finalizedBlockHeight > committedMeta.unfinalizedBlockHeight) { diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index b7d1acf80b68..f95f3dc0a86f 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,36 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] `attempt_note_discovery` now takes two separate functions instead of one + +The `attempt_note_discovery` function (and related discovery functions like `do_sync_state`, `process_message_ciphertext`) now takes separate `compute_note_hash` and `compute_note_nullifier` arguments instead of a single combined `compute_note_hash_and_nullifier`. The corresponding type aliases are now `ComputeNoteHash` and `ComputeNoteNullifier` (instead of `ComputeNoteHashAndNullifier`). + +This split improves performance during nonce discovery: the note hash only needs to be computed once, while the old combined function recomputed it for every candidate nonce. + +Most contracts are not affected, as the macro-generated `sync_state` and `process_message` functions handle this automatically. Only contracts that call `attempt_note_discovery` directly need to update. + +**Migration:** + +```diff + attempt_note_discovery( + contract_address, + tx_hash, + unique_note_hashes_in_tx, + first_nullifier_in_tx, + recipient, +- _compute_note_hash_and_nullifier, ++ _compute_note_hash, ++ _compute_note_nullifier, + owner, + storage_slot, + randomness, + note_type_id, + packed_note, + ); +``` + +**Impact**: Contracts that call `attempt_note_discovery` or related discovery functions directly with a custom `_compute_note_hash_and_nullifier` argument. The old combined function is still generated (deprecated) but is no longer used by the framework. Additionally, if you had a custom `_compute_note_hash_and_nullifier` function then compilation will now fail as you'll need to also produce the corresponding `_compute_note_hash` and `_compute_note_nullifier` functions. + ### Two separate init nullifiers for private and public Contract initialization now emits two separate nullifiers instead of one: a **private init nullifier** and a **public init nullifier**. Each nullifier gates its respective execution domain: diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr index ba866fc2fc9d..01a3640825d5 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/aztec.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec.nr @@ -1,3 +1,5 @@ +mod compute_note_hash_and_nullifier; + use crate::macros::{ calls_generation::{ external_functions::{generate_external_function_calls, generate_external_function_self_calls_structs}, @@ -6,14 +8,12 @@ use crate::macros::{ dispatch::generate_public_dispatch, emit_public_init_nullifier::generate_emit_public_init_nullifier, internals_functions_generation::{create_fn_abi_exports, process_functions}, - notes::NOTES, storage::STORAGE_LAYOUT_NAME, - utils::{ - get_trait_impl_method, is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, - module_has_storage, - }, + utils::{is_fn_contract_library_method, is_fn_external, is_fn_internal, is_fn_test, module_has_storage}, }; +use compute_note_hash_and_nullifier::generate_contract_library_methods_compute_note_hash_and_nullifier; + /// Marks a contract as an Aztec contract, generating the interfaces for its functions and notes, as well as injecting /// the `sync_state` utility function. /// @@ -34,12 +34,16 @@ pub comptime fn aztec(m: Module) -> Quoted { // We generate ABI exports for all the external functions in the contract. let fn_abi_exports = create_fn_abi_exports(m); - // We generate `_compute_note_hash_and_nullifier`, `sync_state` and `process_message` functions only if they are - // not already implemented. If they are implemented we just insert empty quotes. + // We generate `_compute_note_hash`, `_compute_note_nullifier` (and the deprecated + // `_compute_note_hash_and_nullifier` wrapper), `sync_state` and `process_message` functions only if they are not + // already implemented. If they are implemented we just insert empty quotes. let contract_library_method_compute_note_hash_and_nullifier = if !m.functions().any(|f| { + // Note that we don't test for `_compute_note_hash` or `_compute_note_nullifier` in order to make this simpler + // - users must either implement all three or none. + // Down the line we'll remove this check and use `AztecConfig`. f.name() == quote { _compute_note_hash_and_nullifier } }) { - generate_contract_library_method_compute_note_hash_and_nullifier() + generate_contract_library_methods_compute_note_hash_and_nullifier() } else { quote {} }; @@ -143,157 +147,6 @@ comptime fn generate_contract_interface(m: Module) -> Quoted { } } -/// Generates a contract library method called `_compute_note_hash_and_nullifier` which is used for note discovery (to -/// create the `aztec::messages::discovery::ComputeNoteHashAndNullifier` function) and to implement the -/// `compute_note_hash_and_nullifier` unconstrained contract function. -comptime fn generate_contract_library_method_compute_note_hash_and_nullifier() -> Quoted { - if NOTES.len() > 0 { - // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the - // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we - // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and - // compute the note hash (non-siloed) and inner nullifier (also non-siloed). - - let mut if_note_type_id_match_statements_list = @[]; - for i in 0..NOTES.len() { - let typ = NOTES.get(i); - - let get_note_type_id = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteType }, - quote { get_id }, - ); - let unpack = get_trait_impl_method( - typ, - quote { crate::protocol::traits::Packable }, - quote { unpack }, - ); - - let compute_note_hash = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteHash }, - quote { compute_note_hash }, - ); - - let compute_nullifier_unconstrained = get_trait_impl_method( - typ, - quote { crate::note::note_interface::NoteHash }, - quote { compute_nullifier_unconstrained }, - ); - - let if_or_else_if = if i == 0 { - quote { if } - } else { - quote { else if } - }; - - if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( - quote { - $if_or_else_if note_type_id == $get_note_type_id() { - // As an extra safety check we make sure that the packed_note BoundedVec has the expected - // length, since we're about to interpret its raw storage as a fixed-size array by calling the - // unpack function on it. - let expected_len = <$typ as $crate::protocol::traits::Packable>::N; - let actual_len = packed_note.len(); - if actual_len != expected_len { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", - [expected_len as Field, actual_len as Field, note_type_id], - ); - Option::none() - } else { - let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); - - let note_hash = $compute_note_hash(note, owner, storage_slot, randomness); - - // The message discovery process finds settled notes, that is, notes that were created in - // prior transactions and are therefore already part of the note hash tree. We therefore - // compute the nullification note hash by treating the note as a settled note with the - // provided note nonce. - let note_hash_for_nullification = - aztec::note::utils::compute_note_hash_for_nullification( - aztec::note::HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: - aztec::note::note_metadata::SettledNoteMetadata::new( - note_nonce, - ) - .into(), - }, - ); - - let inner_nullifier = $compute_nullifier_unconstrained( - note, - owner, - note_hash_for_nullification, - ); - - Option::some( - aztec::messages::discovery::NoteHashAndNullifier { - note_hash, - inner_nullifier, - }, - ) - } - } - }, - ); - } - - let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); - - quote { - /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. - /// - /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHashAndNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_hash_and_nullifier( - packed_note: BoundedVec, - owner: aztec::protocol::address::AztecAddress, - storage_slot: Field, - note_type_id: Field, - contract_address: aztec::protocol::address::AztecAddress, - randomness: Field, - note_nonce: Field, - ) -> Option { - $if_note_type_id_match_statements - else { - aztec::protocol::logging::warn_log_format( - "[aztec-nr] Unknown note type id {0}. Skipping note.", - [note_type_id], - ); - Option::none() - } - } - } - } else { - // Contracts with no notes still implement this function to avoid having special-casing, the implementation - // simply throws immediately. - quote { - /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. - /// - /// This function is automatically injected by the `#[aztec]` macro. - #[contract_library_method] - unconstrained fn _compute_note_hash_and_nullifier( - _packed_note: BoundedVec, - _owner: aztec::protocol::address::AztecAddress, - _storage_slot: Field, - _note_type_id: Field, - _contract_address: aztec::protocol::address::AztecAddress, - _randomness: Field, - _nonce: Field, - ) -> Option { - panic(f"This contract does not use private notes") - } - } - } -} - /// Generates the `sync_state` utility function that performs message discovery. comptime fn generate_sync_state() -> Quoted { quote { @@ -309,7 +162,8 @@ comptime fn generate_sync_state() -> Quoted { let address = aztec::context::UtilityContext::new().this_address(); aztec::messages::discovery::do_sync_state( address, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_note_nullifier, Option::some(aztec::messages::processing::offchain::sync_inbox), ); } @@ -337,7 +191,8 @@ comptime fn generate_process_message() -> Quoted { aztec::messages::discovery::process_message::process_message_ciphertext( address, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_note_nullifier, message_ciphertext, message_context, ); diff --git a/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr new file mode 100644 index 000000000000..7f8887b04315 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/macros/aztec/compute_note_hash_and_nullifier.nr @@ -0,0 +1,261 @@ +use crate::macros::{notes::NOTES, utils::get_trait_impl_method}; + +/// Generates two contract library methods called `_compute_note_hash` and `_compute_note_nullifier`, plus a +/// (deprecated) wrapper called `_compute_note_hash_and_nullifier`, which are used for note discovery (i.e. these are +/// of the `aztec::messages::discovery::ComputeNoteHash` and `aztec::messages::discovery::ComputeNoteNullifier` types). +pub(crate) comptime fn generate_contract_library_methods_compute_note_hash_and_nullifier() -> Quoted { + let compute_note_hash = generate_contract_library_method_compute_note_hash(); + let compute_note_nullifier = generate_contract_library_method_compute_note_nullifier(); + + quote { + $compute_note_hash + $compute_note_nullifier + + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed) and inner nullifier (non-siloed) assuming the note has been inserted into the note hash tree with `note_nonce`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + #[allow(dead_code)] + unconstrained fn _compute_note_hash_and_nullifier( + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + storage_slot: Field, + note_type_id: Field, + contract_address: aztec::protocol::address::AztecAddress, + randomness: Field, + note_nonce: Field, + ) -> Option { + _compute_note_hash(packed_note, owner, storage_slot, note_type_id, contract_address, randomness).map(|note_hash| { + + let siloed_note_hash = aztec::protocol::hash::compute_siloed_note_hash(contract_address, note_hash); + let unique_note_hash = aztec::protocol::hash::compute_unique_note_hash(note_nonce, siloed_note_hash); + + let inner_nullifier = _compute_note_nullifier(unique_note_hash, packed_note, owner, storage_slot, note_type_id, contract_address, randomness); + + aztec::messages::discovery::NoteHashAndNullifier { + note_hash, + inner_nullifier, + } + }) + } + } +} + +comptime fn generate_contract_library_method_compute_note_hash() -> Quoted { + if NOTES.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_hash( + _packed_note: BoundedVec, + _owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + panic(f"This contract does not use private notes") + } + } + } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the note hash (non-siloed). + + let mut if_note_type_id_match_statements_list = @[]; + for i in 0..NOTES.len() { + let typ = NOTES.get(i); + + let get_note_type_id = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteType }, + quote { get_id }, + ); + let unpack = get_trait_impl_method( + typ, + quote { crate::protocol::traits::Packable }, + quote { unpack }, + ); + + let compute_note_hash = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteHash }, + quote { compute_note_hash }, + ); + + let if_or_else_if = if i == 0 { + quote { if } + } else { + quote { else if } + }; + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( + quote { + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret its raw storage as a fixed-size array by calling the + // unpack function on it. + let expected_len = <$typ as $crate::protocol::traits::Packable>::N; + let actual_len = packed_note.len(); + if actual_len != expected_len { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", + [expected_len as Field, actual_len as Field, note_type_id], + ); + Option::none() + } else { + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + Option::some($compute_note_hash(note, owner, storage_slot, randomness)) + } + } + }, + ); + } + + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); + + quote { + /// Unpacks an array into a note corresponding to `note_type_id` and then computes its note hash (non-siloed). + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteHash` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_hash( + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + storage_slot: Field, + note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + randomness: Field, + ) -> Option { + $if_note_type_id_match_statements + else { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Unknown note type id {0}. Skipping note.", + [note_type_id], + ); + Option::none() + } + } + } + } +} + +comptime fn generate_contract_library_method_compute_note_nullifier() -> Quoted { + if NOTES.len() == 0 { + // Contracts with no notes still implement this function to avoid having special-casing, the implementation + // simply throws immediately. + quote { + /// This contract does not use private notes, so this function should never be called as it will unconditionally fail. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_nullifier( + _unique_note_hash: Field, + _packed_note: BoundedVec, + _owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + _note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + panic(f"This contract does not use private notes") + } + } + } else { + // Contracts that do define notes produce an if-else chain where `note_type_id` is matched against the + // `get_note_type_id()` function of each note type that we know of, in order to identify the note type. Once we + // know it we call we correct `unpack` method from the `Packable` trait to obtain the underlying note type, and + // compute the inner nullifier (non-siloed). + + let mut if_note_type_id_match_statements_list = @[]; + for i in 0..NOTES.len() { + let typ = NOTES.get(i); + + let get_note_type_id = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteType }, + quote { get_id }, + ); + let unpack = get_trait_impl_method( + typ, + quote { crate::protocol::traits::Packable }, + quote { unpack }, + ); + + let compute_nullifier_unconstrained = get_trait_impl_method( + typ, + quote { crate::note::note_interface::NoteHash }, + quote { compute_nullifier_unconstrained }, + ); + + let if_or_else_if = if i == 0 { + quote { if } + } else { + quote { else if } + }; + + if_note_type_id_match_statements_list = if_note_type_id_match_statements_list.push_back( + quote { + $if_or_else_if note_type_id == $get_note_type_id() { + // As an extra safety check we make sure that the packed_note BoundedVec has the expected + // length, since we're about to interpret its raw storage as a fixed-size array by calling the + // unpack function on it. + let expected_len = <$typ as $crate::protocol::traits::Packable>::N; + let actual_len = packed_note.len(); + if actual_len != expected_len { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Packed note length mismatch for note type id {2}: expected {0} fields, got {1}. Skipping note.", + [expected_len as Field, actual_len as Field, note_type_id], + ); + Option::none() + } else { + let note = $unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); + + // The message discovery process finds settled notes, that is, notes that were created in + // prior transactions and are therefore already part of the note hash tree. The note hash + // for nullification is hence the unique note hash. + $compute_nullifier_unconstrained(note, owner, unique_note_hash) + } + } + }, + ); + } + + let if_note_type_id_match_statements = if_note_type_id_match_statements_list.join(quote {}); + + quote { + /// Computes a note's inner nullifier (non-siloed) given its unique note hash, preimage and extra data. + /// + /// The signature of this function notably matches the `aztec::messages::discovery::ComputeNoteNullifier` type, and so it can be used to call functions from that module such as `do_sync_state` and `attempt_note_discovery`. + /// + /// This function is automatically injected by the `#[aztec]` macro. + #[contract_library_method] + unconstrained fn _compute_note_nullifier( + unique_note_hash: Field, + packed_note: BoundedVec, + owner: aztec::protocol::address::AztecAddress, + _storage_slot: Field, + note_type_id: Field, + _contract_address: aztec::protocol::address::AztecAddress, + _randomness: Field, + ) -> Option { + $if_note_type_id_match_statements + else { + aztec::protocol::logging::warn_log_format( + "[aztec-nr] Unknown note type id {0}. Skipping note.", + [note_type_id], + ); + Option::none() + } + } + } + } +} 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 d22099f30705..c41295f73d83 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr @@ -28,37 +28,35 @@ pub struct NoteHashAndNullifier { pub inner_nullifier: Option, } -/// A function which takes a note's packed content, address of the emitting contract, note nonce, storage slot and note -/// type ID and attempts to compute its note hash (not hashed by note nonce nor siloed by address) and inner nullifier -/// (not siloed by address). +/// A contract's way of computing note hashes. /// -/// This function must be user-provided as its implementation requires knowledge of how note type IDs are allocated in -/// a contract. The `#[aztec]` macro automatically creates such a contract library method called -/// `_compute_note_hash_and_nullifier`, which looks something like this: +/// Each contract in the network is free to compute their note's hash as they see fit - the hash function itself is not +/// enshrined or standardized. Some aztec-nr functions however do need to know the details of this computation (e.g. +/// when finding new notes), which is what this type represents. /// -/// ``` -/// |packed_note, owner, storage_slot, note_type_id, contract_address, randomness, note_nonce| { +/// This function takes a note's packed content, storage slot, note type ID, address of the emitting contract and +/// randomness, and attempts to compute its inner note hash (not siloed by address nor uniqued by nonce). +/// +/// ## Transient Notes +/// +/// This function is meant to always be used on **settled** notes, i.e. those that have been inserted into the trees +/// and for which the nonce is known. It is never invoked in the context of a transient note, as those are not involved +/// in message processing. +/// +/// ## Automatic Implementation +/// +/// The [`[#aztec]`](crate::macros::aztec::aztec) macro automatically creates a correct implementation of this function +/// for each contract by inspecting all note types in use and the storage layout. This injected function is a +/// `#[contract_library_method]` called `_compute_note_hash`, and it looks something like this: +/// +/// ```noir +/// |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { /// if note_type_id == MyNoteType::get_id() { /// if packed_note.len() != MY_NOTE_TYPE_SERIALIZATION_LENGTH { /// Option::none() /// } else { /// let note = MyNoteType::unpack(aztec::utils::array::subarray(packed_note.storage(), 0)); -/// -/// let note_hash = note.compute_note_hash(owner, storage_slot, randomness); -/// let note_hash_for_nullification = aztec::note::utils::compute_note_hash_for_nullification( -/// HintedNote { -/// note, contract_address, owner, randomness, storage_slot, -/// metadata: SettledNoteMetadata::new(note_nonce).into(), -/// }, -/// ); -/// -/// let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); -/// -/// Option::some( -/// aztec::messages::discovery::NoteHashAndNullifier { -/// note_hash, inner_nullifier -/// } -/// ) +/// Option::some(note.compute_note_hash(owner, storage_slot, randomness)) /// } /// } else if note_type_id == MyOtherNoteType::get_id() { /// ... // Similar to above but calling MyOtherNoteType::unpack @@ -67,9 +65,30 @@ pub struct NoteHashAndNullifier { /// }; /// } /// ``` -pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, /* +pub type ComputeNoteHash = unconstrained fn(/* packed_note */BoundedVec, /* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, /* -randomness */ Field, /* note nonce */ Field) -> Option; +randomness */ Field) -> Option; + +/// A contract's way of computing note nullifiers. +/// +/// Like [`ComputeNoteHash`], each contract is free to derive nullifiers as they see fit. This function takes the +/// unique note hash (used as the note hash for nullification for settled notes), plus the note's packed content and +/// metadata, and attempts to compute the inner nullifier (not siloed by address). +/// +/// ## Automatic Implementation +/// +/// The [`[#aztec]`](crate::macros::aztec::aztec) macro automatically creates a correct implementation of this function +/// for each contract called `_compute_note_nullifier`. It dispatches on `note_type_id` similarly to +/// [`ComputeNoteHash`], then calls the note's +/// [`compute_nullifier_unconstrained`](crate::note::note_interface::NoteHash::compute_nullifier_unconstrained) method. +pub type ComputeNoteNullifier = unconstrained fn(/* unique_note_hash */Field, /* packed_note */ BoundedVec, +/* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, +/* randomness */ Field) -> Option; + +/// Deprecated: use [`ComputeNoteHash`] and [`ComputeNoteNullifier`] instead. +pub type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */BoundedVec, +/* owner */ AztecAddress, /* storage_slot */ Field, /* note_type_id */ Field, /* contract_address */ AztecAddress, +/*randomness */ Field, /* note nonce */ Field) -> Option; /// Performs the state synchronization process, in which private logs 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. @@ -79,11 +98,16 @@ randomness */ Field, /* note nonce */ Field) -> Option; /// That should be close to the chain tip as block synchronization is performed before contract function simulation is /// done. /// -/// Receives the address of the contract on which discovery is performed along with its -/// `compute_note_hash_and_nullifier` function. -pub unconstrained fn do_sync_state( +/// As blocks are mined, it is possible for a contract's private state to change (e.g. with new notes being created), +/// but because these changes are private they will be invisible to most actors. This is the function that processes +/// new transactions in order to discover new notes, events, and other kinds of private state changes. +/// +/// The private state will be synchronized up to the block that will be used for private transactions (i.e. the anchor +/// block. This will typically be close to the tip of the chain. +pub unconstrained fn do_sync_state( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, offchain_inbox_sync: Option>, ) { aztecnr_debug_log!("Performing state synchronization"); @@ -102,7 +126,8 @@ pub unconstrained fn do_sync_state( process_message_ciphertext( contract_address, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, message_ciphertext, pending_tagged_log.context, ); @@ -121,7 +146,8 @@ pub unconstrained fn do_sync_state( msgs.for_each(|i, msg| { process_message_ciphertext( contract_address, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, msg.message_ciphertext, msg.message_context, ); @@ -133,7 +159,11 @@ pub unconstrained fn do_sync_state( // 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(contract_address, compute_note_hash_and_nullifier); + partial_notes::fetch_and_process_partial_note_completion_logs( + contract_address, + compute_note_hash, + compute_note_nullifier, + ); // Finally we validate all notes and events that were found as part of the previous processes, resulting in them // being added to PXE's database and retrievable via oracles (get_notes) and our TS API (PXE::getPrivateEvents). @@ -144,7 +174,7 @@ mod test { use crate::{ capsules::CapsuleArray, messages::{ - discovery::{do_sync_state, NoteHashAndNullifier}, + discovery::do_sync_state, logs::note::MAX_NOTE_PACKED_LEN, processing::{ offchain::OffchainInboxSync, pending_tagged_log::PendingTaggedLog, PENDING_TAGGED_LOG_ARRAY_BASE_SLOT, @@ -168,21 +198,37 @@ mod test { assert_eq(logs.len(), 1); let no_inbox_sync: Option> = Option::none(); - do_sync_state(contract_address, dummy_compute_nhnn, no_inbox_sync); + do_sync_state( + contract_address, + dummy_compute_note_hash, + dummy_compute_note_nullifier, + no_inbox_sync, + ); assert_eq(logs.len(), 0); }); } - unconstrained fn dummy_compute_nhnn( + 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, - _note_nonce: Field, - ) -> Option { + ) -> Option { Option::none() } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr index 7de03c3ebb45..f9b7896333db 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/nonce_discovery.nr @@ -1,6 +1,6 @@ -use crate::messages::{discovery::ComputeNoteHashAndNullifier, logs::note::MAX_NOTE_PACKED_LEN}; +use crate::messages::{discovery::{ComputeNoteHash, ComputeNoteNullifier}, logs::note::MAX_NOTE_PACKED_LEN}; -use crate::logging::aztecnr_debug_log_format; +use crate::logging::{aztecnr_debug_log_format, aztecnr_warn_log_format}; use crate::protocol::{ address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX, @@ -23,10 +23,11 @@ pub struct DiscoveredNoteInfo { /// /// Due to how nonces are computed, this function requires knowledge of the transaction in which the note was created, /// more specifically the list of all unique note hashes in it plus the value of its first nullifier. -pub unconstrained fn attempt_note_nonce_discovery( +pub(crate) unconstrained fn attempt_note_nonce_discovery( unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, contract_address: AztecAddress, owner: AztecAddress, storage_slot: Field, @@ -42,67 +43,88 @@ pub unconstrained fn attempt_note_nonce_discovery( [unique_note_hashes_in_tx.len() as Field, contract_address.to_field(), storage_slot], ); - // We need to find nonces (typically just one) that result in a note hash that, once siloed into a unique note - // hash, is one of the note hashes created by the transaction. - // The nonce is meant to be derived from the index of the note hash in the transaction effects array. However, due - // to an issue in the kernels the nonce might actually use any of the possible note hash indices - not necessarily - // the one that corresponds to the note hash. Hence, we need to try them all. - for i in 0..MAX_NOTE_HASHES_PER_TX { - let nonce_for_i = compute_note_hash_nonce(first_nullifier_in_tx, i); - - // Given note nonce, note content and metadata, we can compute the note hash and silo it to check if - // the resulting unique note matches any in the transaction. - // TODO(#11157): handle failed note_hash_and_nullifier computation - let hashes = compute_note_hash_and_nullifier( - packed_note, - owner, - storage_slot, - note_type_id, - contract_address, - randomness, - nonce_for_i, - ) - .expect(f"Failed to compute a note hash for note type {note_type_id}"); - - let siloed_note_hash_for_i = compute_siloed_note_hash(contract_address, hashes.note_hash); - let unique_note_hash_for_i = compute_unique_note_hash(nonce_for_i, siloed_note_hash_for_i); - - let matching_notes = bvec_filter( - unique_note_hashes_in_tx, - |unique_note_hash_in_tx| unique_note_hash_in_tx == unique_note_hash_for_i, + let maybe_note_hash = compute_note_hash( + packed_note, + owner, + storage_slot, + note_type_id, + contract_address, + randomness, + ); + + if maybe_note_hash.is_none() { + aztecnr_warn_log_format!( + "Unable to compute note hash for note of id {0} with packed length {1}, skipping nonce discovery", + )( + [note_type_id, packed_note.len() as Field], ); - if matching_notes.len() > 1 { - let identical_note_hashes = matching_notes.len(); - // Note that we don't actually check that the note hashes array contains unique values, only that the note - // we found is unique. We don't expect for this to ever happen (it'd indicate a malicious node or PXE, - // which - // are both assumed to be cooperative) so testing for it just in case is unnecessary, but we _do_ need to - // handle it if we find a duplicate. - panic( - f"Received {identical_note_hashes} identical note hashes for a transaction - these should all be unique", - ) - } else if matching_notes.len() == 1 { - // Note that while we did check that the note hash is the preimage of a unique note hash, we perform no - // validations on the nullifier - we fundamentally cannot, since only the application knows how to compute - // nullifiers. We simply trust it to have provided the correct one: if it hasn't, then PXE may fail to - // realize that a given note has been nullified already, and calls to the application could result in - // invalid transactions (with duplicate nullifiers). This is not a concern because an application already - // has more direct means of making a call to it fail the transaction. - discovered_notes.push( - DiscoveredNoteInfo { - note_nonce: nonce_for_i, - note_hash: hashes.note_hash, - // TODO: The None case will be handled in a followup PR. - // https://linear.app/aztec-labs/issue/F-265/store-external-notes - inner_nullifier: hashes.inner_nullifier.expect( - f"Failed to compute nullifier for note type {note_type_id}", - ), - }, + } else { + let note_hash = maybe_note_hash.unwrap(); + let siloed_note_hash = compute_siloed_note_hash(contract_address, note_hash); + + // We need to find nonces (typically just one) that result in the siloed note hash that being uniqued into one + // of the transaction's effects. + // The nonce is meant to be derived from the index of the note hash in the transaction effects array. However, + // due to an issue in the kernels the nonce might actually use any of the possible note hash indices - not + // necessarily the one that corresponds to the note hash. Hence, we need to try them all. + for i in 0..MAX_NOTE_HASHES_PER_TX { + let nonce_for_i = compute_note_hash_nonce(first_nullifier_in_tx, i); + let unique_note_hash_for_i = compute_unique_note_hash(nonce_for_i, siloed_note_hash); + + let matching_notes = bvec_filter( + unique_note_hashes_in_tx, + |unique_note_hash_in_tx| unique_note_hash_in_tx == unique_note_hash_for_i, ); + if matching_notes.len() > 1 { + let identical_note_hashes = matching_notes.len(); + // Note that we don't actually check that the note hashes array contains unique values, only that the + // note we found is unique. We don't expect for this to ever happen (it'd indicate a malicious node or + // PXE, which are both assumed to be cooperative) so testing for it just in case is unnecessary, but we + // _do_ need to handle it if we find a duplicate. + panic( + f"Received {identical_note_hashes} identical note hashes for a transaction - these should all be unique", + ) + } else if matching_notes.len() == 1 { + let maybe_inner_nullifier_for_i = compute_note_nullifier( + unique_note_hash_for_i, + packed_note, + owner, + storage_slot, + note_type_id, + contract_address, + randomness, + ); - // We don't exit the loop - it is possible (though rare) for the exact same note content to be present - // multiple times in the same transaction with different nonces. This typically doesn't happen due to notes - // containing random values in order to hide their contents. + if maybe_inner_nullifier_for_i.is_none() { + // TODO: down the line we want to be able to store notes for which we don't know their nullifier, + // e.g. notes that belong to someone that is not us (and for which we therefore don't know their + // associated app-siloed nullifer hiding secret key). + // https://linear.app/aztec-labs/issue/F-265/store-external-notes + aztecnr_warn_log_format!( + "Unable to compute nullifier of unique note {0} with note type id {1} and owner {2}, skipping PXE insertion", + )( + [unique_note_hash_for_i, note_type_id, owner.to_field()], + ); + } else { + // Note that while we did check that the note hash is the preimage of a unique note hash, we + // perform no validations on the nullifier - we fundamentally cannot, since only the application + // knows how to compute nullifiers. We simply trust it to have provided the correct one: if it + // hasn't, then PXE may fail to realize that a given note has been nullified already, and calls to + // the application could result in invalid transactions (with duplicate nullifiers). This is not a + // concern because an application already has more direct means of making a call to it fail the + // transaction. + discovered_notes.push( + DiscoveredNoteInfo { + note_nonce: nonce_for_i, + note_hash, + inner_nullifier: maybe_inner_nullifier_for_i.unwrap(), + }, + ); + } + // We don't exit the loop - it is possible (though rare) for the exact same note content to be present + // multiple times in the same transaction with different nonces. This typically doesn't happen due to + // notes containing random values in order to hide their contents. + } } } @@ -129,9 +151,8 @@ unconstrained fn bvec_filter( mod test { use crate::{ - messages::{discovery::NoteHashAndNullifier, logs::note::MAX_NOTE_PACKED_LEN}, + messages::logs::note::MAX_NOTE_PACKED_LEN, note::{ - HintedNote, note_interface::{NoteHash, NoteType}, note_metadata::SettledNoteMetadata, utils::compute_note_hash_for_nullification, @@ -151,33 +172,35 @@ mod test { // This implementation could be simpler, but this serves as a nice example of the expected flow in a real // implementation, and as a sanity check that the interface is sufficient. - unconstrained fn compute_note_hash_and_nullifier( + + unconstrained fn compute_note_hash( packed_note: BoundedVec, owner: AztecAddress, storage_slot: Field, note_type_id: Field, - contract_address: AztecAddress, + _contract_address: AztecAddress, randomness: Field, - note_nonce: Field, - ) -> Option { - if note_type_id == MockNote::get_id() { + ) -> Option { + if (note_type_id == MockNote::get_id()) & (packed_note.len() == ::N) { let note = MockNote::unpack(array::subarray(packed_note.storage(), 0)); - let note_hash = note.compute_note_hash(owner, storage_slot, randomness); - - let note_hash_for_nullification = compute_note_hash_for_nullification( - HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: SettledNoteMetadata::new(note_nonce).into(), - }, - ); - - let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); + Option::some(note.compute_note_hash(owner, storage_slot, randomness)) + } else { + Option::none() + } + } - Option::some(NoteHashAndNullifier { note_hash, inner_nullifier }) + unconstrained fn 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 { + if (note_type_id == MockNote::get_id()) & (packed_note.len() == ::N) { + let note = MockNote::unpack(array::subarray(packed_note.storage(), 0)); + note.compute_nullifier_unconstrained(owner, unique_note_hash) } else { Option::none() } @@ -198,7 +221,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -210,22 +234,47 @@ mod test { assert_eq(discovered_notes.len(), 0); } - #[test(should_fail_with = "Failed to compute a note hash")] - unconstrained fn failed_hash_computation() { + #[test] + unconstrained fn failed_hash_computation_is_ignored() { let unique_note_hashes_in_tx = BoundedVec::from_array([random()]); - let packed_note = BoundedVec::new(); - let note_type_id = 0; // This note type id is unknown to compute_note_hash_and_nullifier let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + |_, _, _, _, _, _| Option::none(), + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, RANDOMNESS, - note_type_id, - packed_note, + MockNote::get_id(), + BoundedVec::new(), + ); + + assert_eq(discovered_notes.len(), 0); + } + + #[test] + unconstrained fn failed_nullifier_computation_is_ignored() { + let note_index_in_tx = 2; + let note_and_data = construct_note(VALUE, note_index_in_tx); + + let mut unique_note_hashes_in_tx = BoundedVec::from_array([ + random(), random(), random(), random(), random(), random(), random(), + ]); + unique_note_hashes_in_tx.set(note_index_in_tx, note_and_data.unique_note_hash); + + let discovered_notes = attempt_note_nonce_discovery( + unique_note_hashes_in_tx, + FIRST_NULLIFIER_IN_TX, + compute_note_hash, + |_, _, _, _, _, _, _| Option::none(), + CONTRACT_ADDRESS, + OWNER, + STORAGE_SLOT, + RANDOMNESS, + MockNote::get_id(), + BoundedVec::from_array(note_and_data.note.pack()), ); assert_eq(discovered_notes.len(), 0); @@ -276,7 +325,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -315,7 +365,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -354,7 +405,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -387,7 +439,8 @@ mod test { let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, @@ -421,7 +474,8 @@ mod test { let _ = attempt_note_nonce_discovery( unique_note_hashes_in_tx, FIRST_NULLIFIER_IN_TX, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, CONTRACT_ADDRESS, OWNER, STORAGE_SLOT, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr index aa0b9fd2468a..f109ae046f13 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr @@ -1,7 +1,7 @@ use crate::{ capsules::CapsuleArray, messages::{ - discovery::{ComputeNoteHashAndNullifier, nonce_discovery::attempt_note_nonce_discovery}, + 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::{ @@ -72,9 +72,10 @@ pub unconstrained fn process_partial_note_private_msg( /// 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 unconstrained fn fetch_and_process_partial_note_completion_logs( +pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, ) { let pending_partial_notes = CapsuleArray::at( contract_address, @@ -130,7 +131,8 @@ pub unconstrained fn fetch_and_process_partial_note_completion_logs( let discovered_notes = attempt_note_nonce_discovery( log.unique_note_hashes_in_tx, log.first_nullifier_in_tx, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, contract_address, pending_partial_note.owner, storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr index 3bc092b09894..5df56c27cbd9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/discovery/private_notes.nr @@ -1,19 +1,20 @@ use crate::logging::aztecnr_debug_log_format; use crate::messages::{ - discovery::{ComputeNoteHashAndNullifier, nonce_discovery::attempt_note_nonce_discovery}, + discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery}, encoding::MAX_MESSAGE_CONTENT_LEN, logs::note::{decode_private_note_message, MAX_NOTE_PACKED_LEN}, processing::enqueue_note_for_validation, }; use crate::protocol::{address::AztecAddress, constants::MAX_NOTE_HASHES_PER_TX}; -pub unconstrained fn process_private_note_msg( +pub(crate) unconstrained fn process_private_note_msg( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, msg_metadata: u64, msg_content: BoundedVec, ) { @@ -28,7 +29,8 @@ pub unconstrained fn process_private_note_msg( unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, owner, storage_slot, randomness, @@ -46,13 +48,14 @@ pub unconstrained fn process_private_note_msg( /// Attempts discovery of a note given information about its contents and the transaction in which it is suspected the /// note was created. -pub unconstrained fn attempt_note_discovery( +pub unconstrained fn attempt_note_discovery( contract_address: AztecAddress, tx_hash: Field, unique_note_hashes_in_tx: BoundedVec, first_nullifier_in_tx: Field, recipient: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, owner: AztecAddress, storage_slot: Field, randomness: Field, @@ -62,7 +65,8 @@ pub unconstrained fn attempt_note_discovery( let discovered_notes = attempt_note_nonce_discovery( unique_note_hashes_in_tx, first_nullifier_in_tx, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, contract_address, owner, storage_slot, 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 33defbb50dbb..e40368cb0af6 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,6 +1,6 @@ use crate::messages::{ discovery::{ - ComputeNoteHashAndNullifier, partial_notes::process_partial_note_private_msg, + ComputeNoteHash, ComputeNoteNullifier, partial_notes::process_partial_note_private_msg, private_events::process_private_event_msg, private_notes::process_private_note_msg, }, encoding::{decode_message, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN}, @@ -16,17 +16,18 @@ use crate::protocol::address::AztecAddress; /// /// Notes result in nonce discovery being performed prior to delivery, which requires knowledge of the transaction hash /// in which the notes would've been created (typically the same transaction in which the log was emitted), along with -/// the list of unique note hashes in said transaction and the `compute_note_hash_and_nullifier` function. Once -/// discovered, the notes are enqueued for validation. +/// the list of unique note hashes in said transaction and the `compute_note_hash` and `compute_note_nullifier` +/// functions. Once discovered, the notes are enqueued for validation. /// /// Partial notes result in a pending partial note entry being stored in a PXE capsule, which will later be retrieved /// to search for the note's completion public log. /// /// Events are processed by computing an event commitment from the serialized event data and its randomness field, then /// enqueueing the event data and commitment for validation. -pub unconstrained fn process_message_ciphertext( +pub unconstrained fn process_message_ciphertext( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, message_ciphertext: BoundedVec, message_context: MessageContext, ) { @@ -35,7 +36,8 @@ pub unconstrained fn process_message_ciphertext( if message_plaintext_option.is_some() { process_message_plaintext( contract_address, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, message_plaintext_option.unwrap(), message_context, ); @@ -44,9 +46,10 @@ pub unconstrained fn process_message_ciphertext( } } -pub unconstrained fn process_message_plaintext( +pub unconstrained fn process_message_plaintext( contract_address: AztecAddress, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, message_plaintext: BoundedVec, message_context: MessageContext, ) { @@ -68,7 +71,8 @@ pub unconstrained fn process_message_plaintext( message_context.unique_note_hashes_in_tx, message_context.first_nullifier_in_tx, message_context.recipient, - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, msg_metadata, msg_content, ); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr b/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr index d1f7b81243e2..b213b73e4a7b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/block_header.nr @@ -75,11 +75,107 @@ fn constrain_get_block_header_at_internal( } mod test { + use crate::protocol::traits::Hash; use crate::test::helpers::test_environment::TestEnvironment; - use super::{constrain_get_block_header_at_internal, get_block_header_at_internal}; + use super::{constrain_get_block_header_at_internal, get_block_header_at, get_block_header_at_internal}; - #[test(should_fail_with = "Proving membership of a block in archive failed")] - unconstrained fn fetching_header_with_mismatched_block_number_should_fail() { + #[test] + unconstrained fn fetching_earliest_block_header_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + + let header = get_block_header_at_internal(1); + constrain_get_block_header_at_internal(header, 1, anchor_block_header); + + assert_eq(header.block_number(), 1); + }); + } + + #[test] + unconstrained fn fetching_past_block_header_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + let target_block_number = anchor_block_header.block_number() - 2; + + let header = get_block_header_at_internal(target_block_number); + constrain_get_block_header_at_internal(header, target_block_number, anchor_block_header); + + assert_eq(header.block_number(), target_block_number); + }); + } + + #[test] + unconstrained fn fetching_header_immediately_before_anchor_succeeds() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + // Block N-1 is the boundary case: last_archive covers exactly up to block N-1. + env.private_context(|context| { + let anchor_block_header = context.anchor_block_header; + let target_block_number = anchor_block_header.block_number() - 1; + + let header = get_block_header_at_internal(target_block_number); + constrain_get_block_header_at_internal(header, target_block_number, anchor_block_header); + + assert_eq(header.block_number(), target_block_number); + }); + } + + #[test] + unconstrained fn fetching_anchor_block_header_works() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_number = context.anchor_block_header.block_number(); + + let header = get_block_header_at(anchor_block_number, *context); + + assert_eq(header.block_number(), anchor_block_number); + assert_eq(header.hash(), context.anchor_block_header.hash()); + }); + } + + #[test(should_fail_with = "Last archive block number is smaller than the block number")] + unconstrained fn fetching_future_block_header_fails() { + let env = TestEnvironment::new(); + + env.mine_block(); + env.mine_block(); + env.mine_block(); + env.mine_block(); + + env.private_context(|context| { + let anchor_block_number = context.anchor_block_header.block_number(); + + let _header = get_block_header_at(anchor_block_number + 1, *context); + }); + } + + #[test(should_fail_with = "Block number provided is not the same as the block number from the header hint")] + unconstrained fn fetching_header_with_mismatched_block_number_fails() { let env = TestEnvironment::new(); env.mine_block(); 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 7cc47bf41813..7a34d75711eb 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 @@ -12,18 +12,12 @@ use crate::{ event::{event_interface::EventInterface, EventMessage}, hash::hash_args, messages::{ - discovery::{ComputeNoteHashAndNullifier, NoteHashAndNullifier, process_message::process_message_plaintext}, + discovery::{ComputeNoteHash, ComputeNoteNullifier, process_message::process_message_plaintext}, encoding::MESSAGE_PLAINTEXT_LEN, logs::{event::encode_private_event_message, note::encode_private_note_message}, processing::{MessageContext, validate_and_store_enqueued_notes_and_events}, }, - note::{ - HintedNote, - note_interface::{NoteHash, NoteType}, - note_metadata::SettledNoteMetadata, - NoteMessage, - utils::compute_note_hash_for_nullification, - }, + note::{note_interface::{NoteHash, NoteType}, NoteMessage}, oracle::version::assert_compatible_oracle_version, test::helpers::{txe_oracles, utils::ContractDeployment}, utils::array::subarray, @@ -780,37 +774,33 @@ impl TestEnvironment { note_message.new_note.randomness, )); - // We also need to provide an implementation for the `compute_note_hash_and_nullifier` function, which would - // typically be created by the macros for the different notes in a given contract. Here we build one - // specialized for `Note`. - let compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier<_> = |packed_note, owner, storage_slot, note_type_id, contract_address, randomness, note_nonce| { + // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` + // functions, which would typically be created by the macros for the different notes in a given contract. Here + // we build variants specialized for `Note`. + let compute_note_hash: ComputeNoteHash = |packed_note, owner, storage_slot, note_type_id, _contract_address, randomness| { assert_eq(note_type_id, Note::get_id()); assert_eq(packed_note.len(), ::N); let note = Note::unpack(subarray(packed_note.storage(), 0)); - let note_hash = note.compute_note_hash(owner, storage_slot, randomness); - let note_hash_for_nullification = compute_note_hash_for_nullification( - HintedNote { - note, - contract_address, - owner, - randomness, - storage_slot, - metadata: SettledNoteMetadata::new(note_nonce).into(), - }, - ); + Option::some(note.compute_note_hash(owner, storage_slot, randomness)) + }; - let inner_nullifier = note.compute_nullifier_unconstrained(owner, note_hash_for_nullification); + let compute_note_nullifier: ComputeNoteNullifier = |unique_note_hash, packed_note, owner, _storage_slot, note_type_id, _contract_address, _randomness| { + assert_eq(note_type_id, Note::get_id()); + assert_eq(packed_note.len(), ::N); - Option::some(NoteHashAndNullifier { note_hash, inner_nullifier }) + let note = Note::unpack(subarray(packed_note.storage(), 0)); + + note.compute_nullifier_unconstrained(owner, unique_note_hash) }; self.discover_data_in_message_plaintext( message_plaintext, opts.contract_address, Option::some(note_message.new_note.owner), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, ); } @@ -902,12 +892,18 @@ impl TestEnvironment { event_message.new_event.randomness, )); - // We also need to provide an implementation for the `compute_note_hash_and_nullifier` function, which would - // typically be created by the macros for the different notes in a given contract. Here we build an empty one, - // since it will never be invoked as this is an event and not a note message. - let compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier<_> = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness, _note_nonce| { + // We also need to provide an implementation for the `compute_note_hash` and `compute_note_nullifier` + // functions, which would typically be created by the macros for the different notes in a given contract. Here + // we build empty ones, since they will never be invoked as this is an event and not a note message. + let compute_note_hash: ComputeNoteHash = |_packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { + panic( + f"Unexpected compute_note_hash invocation in TestEnvironment::discover_event", + ) + }; + + let compute_note_nullifier: ComputeNoteNullifier = |_unique_note_hash, _packed_note, _owner, _storage_slot, _note_type_id, _contract_address, _randomness| { panic( - f"Unexpected compute_note_hash_and_nullifier invocation in TestEnvironment::discover_event", + f"Unexpected compute_note_nullifier invocation in TestEnvironment::discover_event", ) }; @@ -915,16 +911,18 @@ impl TestEnvironment { message_plaintext, opts.contract_address, Option::some(recipient), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, ); } - unconstrained fn discover_data_in_message_plaintext( + unconstrained fn discover_data_in_message_plaintext( self, message_plaintext: BoundedVec, contract_address: Option, recipient: Option, - compute_note_hash_and_nullifier: ComputeNoteHashAndNullifier, + compute_note_hash: ComputeNoteHash, + compute_note_nullifier: ComputeNoteNullifier, ) { // This function will emulate the message discovery and processing that would happen in a real contract, based // on a message plaintext. @@ -952,7 +950,8 @@ impl TestEnvironment { self.utility_context_opts(UtilityContextOptions { contract_address }, |context| { process_message_plaintext( context.this_address(), - compute_note_hash_and_nullifier, + compute_note_hash, + compute_note_nullifier, message_plaintext, message_context, ); diff --git a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr index e2b5cd42dfe9..15d680e0909c 100644 --- a/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_blacklist_contract/src/main.nr @@ -292,7 +292,8 @@ pub contract TokenBlacklist { unique_note_hashes_in_tx, first_nullifier_in_tx, recipient, - _compute_note_hash_and_nullifier, + _compute_note_hash, + _compute_note_nullifier, AztecAddress::zero(), storage_slot, TRANSPARENT_NOTE_RANDOMNESS, diff --git a/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr index f077b11497ae..abb31baf8d26 100644 --- a/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/note_hash_and_nullifier/note_hash_and_nullifier_contract/src/main.nr @@ -3,7 +3,8 @@ mod test; use aztec::macros::aztec; -/// A minimal contract used to test the macro-generated `_compute_note_hash_and_nullifier` function. +/// A minimal contract used to test the macro-generated `_compute_note_hash`, `_compute_note_nullifier` and +/// (deprecated) `_compute_note_hash_and_nullifier` functions. #[aztec] pub contract NoteHashAndNullifier { use aztec::{ diff --git a/spartan/environments/staging-public.env b/spartan/environments/staging-public.env index fd3c5b52df47..fa894ca74041 100644 --- a/spartan/environments/staging-public.env +++ b/spartan/environments/staging-public.env @@ -31,6 +31,7 @@ SEQ_MAX_TX_PER_CHECKPOINT=7 # 0.1 TPS # Build checkpoint even if block is empty. SEQ_BUILD_CHECKPOINT_IF_EMPTY=true SEQ_BLOCK_DURATION_MS=6000 +SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT=36 CREATE_ROLLUP_CONTRACTS=false P2P_TX_POOL_DELETE_TXS_AFTER_REORG=true diff --git a/spartan/scripts/deploy_network.sh b/spartan/scripts/deploy_network.sh index 9a5300e83796..2973265afd68 100755 --- a/spartan/scripts/deploy_network.sh +++ b/spartan/scripts/deploy_network.sh @@ -107,11 +107,7 @@ PROVER_FAILED_PROOF_STORE=${PROVER_FAILED_PROOF_STORE:-} SEQ_MIN_TX_PER_BLOCK=${SEQ_MIN_TX_PER_BLOCK:-1} SEQ_MAX_TX_PER_BLOCK=${SEQ_MAX_TX_PER_BLOCK:-null} SEQ_MAX_TX_PER_CHECKPOINT=${SEQ_MAX_TX_PER_CHECKPOINT:-8} -<<<<<<< HEAD -SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER=${SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER:-2} -======= SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER=${SEQ_PER_BLOCK_ALLOCATION_MULTIPLIER:-} ->>>>>>> origin/v4 SEQ_BLOCK_DURATION_MS=${SEQ_BLOCK_DURATION_MS:-} SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT=${SEQ_L1_PUBLISHING_TIME_ALLOWANCE_IN_SLOT:-} SEQ_BUILD_CHECKPOINT_IF_EMPTY=${SEQ_BUILD_CHECKPOINT_IF_EMPTY:-} diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts index 6bf2975b3c3f..40875ecec4e6 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.ts @@ -446,7 +446,7 @@ export class ArchiverDataStoreUpdater { if (validFnCount > 0) { this.log.verbose(`Storing ${validFnCount} functions for contract class ${contractClassId.toString()}`); } - return await this.store.addFunctions(contractClassId, validPrivateFns, validUtilityFns); + await this.store.addFunctions(contractClassId, validPrivateFns, validUtilityFns); } return true; } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index cb373bc7d042..30de30bf7966 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1038,7 +1038,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { referenceBlock: BlockParameter, blockHash: BlockHash, ): Promise | undefined> { - const committedDb = await this.getWorldState(referenceBlock); + // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`, + // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1). + // So we need the world state at block N-1, not block N, to produce a sibling path matching that root. + const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock); + const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1)); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined ? undefined @@ -1660,6 +1664,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return snapshot; } + /** Resolves a block parameter to a block number. */ + protected async resolveBlockNumber(block: BlockParameter): Promise { + if (block === 'latest') { + return BlockNumber(await this.blockSource.getBlockNumber()); + } + if (BlockHash.isBlockHash(block)) { + const initialBlockHash = await this.#getInitialHeaderHash(); + if (block.equals(initialBlockHash)) { + return BlockNumber.ZERO; + } + const header = await this.blockSource.getBlockHeaderByHash(block); + if (!header) { + throw new Error(`Block hash ${block.toString()} not found.`); + } + return header.getBlockNumber(); + } + return block as BlockNumber; + } + /** * Ensure we fully sync the world state * @returns A promise that fulfils once the world state is synced diff --git a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts index ae84e24204d7..aa837388211c 100644 --- a/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts +++ b/yarn-project/stdlib/src/p2p/checkpoint_proposal.ts @@ -178,29 +178,32 @@ export class CheckpointProposal extends Gossipable { blockNumber: lastBlockInfo?.blockHeader?.globalVariables.blockNumber ?? BlockNumber(0), dutyType: DutyType.CHECKPOINT_PROPOSAL, }; - const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); - if (!lastBlockInfo) { - return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature); + if (lastBlockInfo) { + // Sign block proposal before signing checkpoint proposal to ensure HA protection + const lastBlockProposal = await BlockProposal.createProposalFromSigner( + lastBlockInfo.blockHeader, + lastBlockInfo.indexWithinCheckpoint, + checkpointHeader.inHash, + archiveRoot, + lastBlockInfo.txHashes, + lastBlockInfo.txs, + payloadSigner, + ); + + const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); + + return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature, { + blockHeader: lastBlockInfo.blockHeader, + indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint, + txHashes: lastBlockInfo.txHashes, + signature: lastBlockProposal.signature, + signedTxs: lastBlockProposal.signedTxs, + }); } - const lastBlockProposal = await BlockProposal.createProposalFromSigner( - lastBlockInfo.blockHeader, - lastBlockInfo.indexWithinCheckpoint, - checkpointHeader.inHash, - archiveRoot, - lastBlockInfo.txHashes, - lastBlockInfo.txs, - payloadSigner, - ); - - return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature, { - blockHeader: lastBlockInfo.blockHeader, - indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint, - txHashes: lastBlockInfo.txHashes, - signature: lastBlockProposal.signature, - signedTxs: lastBlockProposal.signedTxs, - }); + const checkpointSignature = await payloadSigner(checkpointHash, checkpointContext); + return new CheckpointProposal(checkpointHeader, archiveRoot, feeAssetPriceModifier, checkpointSignature); } /** diff --git a/yarn-project/validator-client/src/block_proposal_handler.ts b/yarn-project/validator-client/src/block_proposal_handler.ts index 43c890bdafa8..1582c74b334c 100644 --- a/yarn-project/validator-client/src/block_proposal_handler.ts +++ b/yarn-project/validator-client/src/block_proposal_handler.ts @@ -487,7 +487,9 @@ export class BlockProposalHandler { } private getReexecuteFailureReason(err: any): BlockProposalValidationFailureReason { - if (err instanceof ReExInitialStateMismatchError) { + if (err instanceof TransactionsNotAvailableError) { + return 'txs_not_available'; + } else if (err instanceof ReExInitialStateMismatchError) { return 'initial_state_mismatch'; } else if (err instanceof ReExStateMismatchError) { return 'state_mismatch'; diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 511579f3d2e4..37d55fce1eb4 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -388,6 +388,18 @@ export class ServerWorldStateSynchronizer private async handleChainFinalized(blockNumber: BlockNumber) { this.log.verbose(`Finalized chain is now at block ${blockNumber}`); + // If the finalized block number is older than the oldest available block in world state, + // skip entirely. The finalized block number can jump backwards (e.g. when the finalization + // heuristic changes) and try to read block data that has already been pruned. When this + // happens, there is nothing useful to do — the native world state is already finalized + // past this point and pruning has already happened. + const currentSummary = await this.merkleTreeDb.getStatusSummary(); + if (blockNumber < currentSummary.oldestHistoricalBlock || blockNumber < 1) { + this.log.trace( + `Finalized block ${blockNumber} is older than the oldest available block ${currentSummary.oldestHistoricalBlock}. Skipping.`, + ); + return; + } const summary = await this.merkleTreeDb.setFinalized(blockNumber); if (this.historyToKeep === undefined) { return; @@ -421,6 +433,12 @@ export class ServerWorldStateSynchronizer } // Find the block at the start of the checkpoint and remove blocks up to this one const newHistoricBlock = historicCheckpoint.checkpoint.blocks[0]; + if (newHistoricBlock.number <= currentSummary.oldestHistoricalBlock) { + this.log.debug( + `Historic block ${newHistoricBlock.number} is not newer than oldest available ${currentSummary.oldestHistoricalBlock}. Skipping prune.`, + ); + return; + } this.log.verbose(`Pruning historic blocks to ${newHistoricBlock.number}`); const status = await this.merkleTreeDb.removeHistoricalBlocks(BlockNumber(newHistoricBlock.number)); this.log.debug(`World state summary `, status.summary); diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index 4f75871da279..fd1c096460dc 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -252,6 +252,44 @@ describe('world-state integration', () => { await awaitSync(5, 4); await expectSynchedToBlock(5, 4); }); + + it('does not throw when finalized block jumps backwards past pruned blocks', async () => { + // Create 20 blocks and sync them all + await archiver.createBlocks(MAX_CHECKPOINT_COUNT); + await synchronizer.start(); + await awaitSync(MAX_CHECKPOINT_COUNT); + await expectSynchedToBlock(MAX_CHECKPOINT_COUNT); + + // Manually finalize to block 15 and prune historical blocks up to block 10 + // to simulate world-state having pruned old data. + await db.setFinalized(BlockNumber(15)); + await db.removeHistoricalBlocks(BlockNumber(10)); + + const summary = await db.getStatusSummary(); + log.info( + `After manual finalize+prune: oldest=${summary.oldestHistoricalBlock}, finalized=${summary.finalizedBlockNumber}`, + ); + expect(summary.oldestHistoricalBlock).toBe(10); + expect(summary.finalizedBlockNumber).toBe(15); + + // Now simulate the scenario from PR #21597: finalized block jumps backwards + // to a block M that is older than oldestHistoricalBlock. + // This should NOT throw — the clamping logic should handle it. + const backwardsFinalized = BlockNumber(5); + log.info( + `Sending chain-finalized for block ${backwardsFinalized} (below oldest ${summary.oldestHistoricalBlock})`, + ); + await expect( + synchronizer.handleBlockStreamEvent({ + type: 'chain-finalized', + block: { number: backwardsFinalized, hash: '' }, + }), + ).resolves.not.toThrow(); + + // Finalized block should remain at 15 (unchanged by the backwards event) + const afterSummary = await db.getStatusSummary(); + expect(afterSummary.finalizedBlockNumber).toBe(15); + }); }); });