diff --git a/crates/ntx-builder/src/actor/account_state.rs b/crates/ntx-builder/src/actor/candidate.rs similarity index 95% rename from crates/ntx-builder/src/actor/account_state.rs rename to crates/ntx-builder/src/actor/candidate.rs index 753dfee8a..a5429a660 100644 --- a/crates/ntx-builder/src/actor/account_state.rs +++ b/crates/ntx-builder/src/actor/candidate.rs @@ -4,7 +4,7 @@ use miden_protocol::account::Account; use miden_protocol::block::BlockHeader; use miden_protocol::transaction::PartialBlockchain; -use crate::actor::inflight_note::InflightNetworkNote; +use crate::inflight_note::InflightNetworkNote; // TRANSACTION CANDIDATE // ================================================================================================ diff --git a/crates/ntx-builder/src/actor/execute.rs b/crates/ntx-builder/src/actor/execute.rs index 09658cd23..1529a95eb 100644 --- a/crates/ntx-builder/src/actor/execute.rs +++ b/crates/ntx-builder/src/actor/execute.rs @@ -53,10 +53,9 @@ use tokio::task::JoinError; use tracing::{Instrument, instrument}; use crate::COMPONENT; -use crate::actor::account_state::TransactionCandidate; -use crate::block_producer::BlockProducerClient; +use crate::actor::candidate::TransactionCandidate; +use crate::clients::{BlockProducerClient, StoreClient}; use crate::db::Db; -use crate::store::StoreClient; #[derive(Debug, thiserror::Error)] pub enum NtxError { diff --git a/crates/ntx-builder/src/actor/inflight_note.rs b/crates/ntx-builder/src/actor/inflight_note.rs deleted file mode 100644 index 4cc080862..000000000 --- a/crates/ntx-builder/src/actor/inflight_note.rs +++ /dev/null @@ -1,75 +0,0 @@ -use miden_node_proto::domain::note::SingleTargetNetworkNote; -use miden_protocol::block::BlockNumber; -use miden_protocol::note::Note; - -use crate::actor::has_backoff_passed; - -// INFLIGHT NETWORK NOTE -// ================================================================================================ - -/// An unconsumed network note that may have failed to execute. -/// -/// The block number at which the network note was attempted are approximate and may not -/// reflect the exact block number for which the execution attempt failed. The actual block -/// will likely be soon after the number that is recorded here. -#[derive(Debug, Clone)] -pub struct InflightNetworkNote { - note: SingleTargetNetworkNote, - attempt_count: usize, - last_attempt: Option, -} - -impl InflightNetworkNote { - /// Creates a new inflight network note. - pub fn new(note: SingleTargetNetworkNote) -> Self { - Self { - note, - attempt_count: 0, - last_attempt: None, - } - } - - /// Reconstructs an inflight network note from its constituent parts (e.g., from DB rows). - pub fn from_parts( - note: SingleTargetNetworkNote, - attempt_count: usize, - last_attempt: Option, - ) -> Self { - Self { note, attempt_count, last_attempt } - } - - /// Consumes the inflight network note and returns the inner network note. - pub fn into_inner(self) -> SingleTargetNetworkNote { - self.note - } - - /// Returns a reference to the inner network note. - pub fn to_inner(&self) -> &SingleTargetNetworkNote { - &self.note - } - - /// Returns the number of attempts made to execute the network note. - pub fn attempt_count(&self) -> usize { - self.attempt_count - } - - /// Checks if the network note is available for execution. - /// - /// The note is available if the backoff period has passed. - pub fn is_available(&self, block_num: BlockNumber) -> bool { - self.note.can_be_consumed(block_num).unwrap_or(true) - && has_backoff_passed(block_num, self.last_attempt, self.attempt_count) - } - - /// Registers a failed attempt to execute the network note at the specified block number. - pub fn fail(&mut self, block_num: BlockNumber) { - self.last_attempt = Some(block_num); - self.attempt_count += 1; - } -} - -impl From for Note { - fn from(value: InflightNetworkNote) -> Self { - value.into_inner().into() - } -} diff --git a/crates/ntx-builder/src/actor/mod.rs b/crates/ntx-builder/src/actor/mod.rs index cac7fcb3d..691fb8ee6 100644 --- a/crates/ntx-builder/src/actor/mod.rs +++ b/crates/ntx-builder/src/actor/mod.rs @@ -1,13 +1,11 @@ -pub(crate) mod account_effect; -pub mod account_state; +pub mod candidate; mod execute; -pub(crate) mod inflight_note; use std::num::NonZeroUsize; use std::sync::Arc; use std::time::Duration; -use account_state::TransactionCandidate; +use candidate::TransactionCandidate; use futures::FutureExt; use miden_node_proto::clients::{Builder, ValidatorClient}; use miden_node_proto::domain::account::NetworkAccountId; @@ -23,10 +21,9 @@ use tokio::sync::{AcquireError, Notify, RwLock, Semaphore, mpsc}; use tokio_util::sync::CancellationToken; use url::Url; -use crate::block_producer::BlockProducerClient; -use crate::builder::ChainState; +use crate::chain_state::ChainState; +use crate::clients::{BlockProducerClient, StoreClient}; use crate::db::Db; -use crate::store::StoreClient; /// Converts a database result into an `ActorShutdownReason` error, logging the error on failure. fn db_query( @@ -446,69 +443,3 @@ impl AccountActor { let _ = ack_rx.await; } } - -// HELPERS -// ================================================================================================ - -/// Checks if the backoff block period has passed. -/// -/// The number of blocks passed since the last attempt must be greater than or equal to -/// e^(0.25 * `attempt_count`) rounded to the nearest integer. -/// -/// This evaluates to the following: -/// - After 1 attempt, the backoff period is 1 block. -/// - After 3 attempts, the backoff period is 2 blocks. -/// - After 10 attempts, the backoff period is 12 blocks. -/// - After 20 attempts, the backoff period is 148 blocks. -/// - etc... -#[expect(clippy::cast_precision_loss, clippy::cast_sign_loss)] -fn has_backoff_passed( - chain_tip: BlockNumber, - last_attempt: Option, - attempts: usize, -) -> bool { - if attempts == 0 { - return true; - } - // Compute the number of blocks passed since the last attempt. - let blocks_passed = last_attempt - .and_then(|last| chain_tip.checked_sub(last.as_u32())) - .unwrap_or_default(); - - // Compute the exponential backoff threshold: Δ = e^(0.25 * n). - let backoff_threshold = (0.25 * attempts as f64).exp().round() as usize; - - // Check if the backoff period has passed. - blocks_passed.as_usize() > backoff_threshold -} - -#[cfg(test)] -mod tests { - use miden_protocol::block::BlockNumber; - - use super::has_backoff_passed; - - #[rstest::rstest] - #[test] - #[case::all_zero(Some(BlockNumber::GENESIS), BlockNumber::GENESIS, 0, true)] - #[case::no_attempts(None, BlockNumber::GENESIS, 0, true)] - #[case::one_attempt(Some(BlockNumber::GENESIS), BlockNumber::from(2), 1, true)] - #[case::three_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(3), 3, true)] - #[case::ten_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(13), 10, true)] - #[case::twenty_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(149), 20, true)] - #[case::one_attempt_false(Some(BlockNumber::GENESIS), BlockNumber::from(1), 1, false)] - #[case::three_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(2), 3, false)] - #[case::ten_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(12), 10, false)] - #[case::twenty_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(148), 20, false)] - fn backoff_has_passed( - #[case] last_attempt_block_num: Option, - #[case] current_block_num: BlockNumber, - #[case] attempt_count: usize, - #[case] backoff_should_have_passed: bool, - ) { - assert_eq!( - backoff_should_have_passed, - has_backoff_passed(current_block_num, last_attempt_block_num, attempt_count) - ); - } -} diff --git a/crates/ntx-builder/src/builder.rs b/crates/ntx-builder/src/builder.rs index 8e857b733..6707cb730 100644 --- a/crates/ntx-builder/src/builder.rs +++ b/crates/ntx-builder/src/builder.rs @@ -7,61 +7,16 @@ use miden_node_proto::domain::account::NetworkAccountId; use miden_node_proto::domain::mempool::MempoolEvent; use miden_protocol::account::delta::AccountUpdateDetails; use miden_protocol::block::BlockHeader; -use miden_protocol::crypto::merkle::mmr::PartialMmr; -use miden_protocol::transaction::PartialBlockchain; use tokio::sync::{RwLock, mpsc}; use tokio_stream::StreamExt; use tonic::Status; use crate::NtxBuilderConfig; use crate::actor::{AccountActorContext, AccountOrigin, ActorRequest}; +use crate::chain_state::ChainState; +use crate::clients::StoreClient; use crate::coordinator::Coordinator; use crate::db::Db; -use crate::store::StoreClient; - -// CHAIN STATE -// ================================================================================================ - -/// Contains information about the chain that is relevant to the [`NetworkTransactionBuilder`] and -/// all account actors managed by the [`Coordinator`]. -/// -/// The chain MMR stored here contains: -/// - The MMR peaks. -/// - Block headers and authentication paths for the last [`NtxBuilderConfig::max_block_count`] -/// blocks. -/// -/// Authentication paths for older blocks are pruned because the NTX builder executes all notes as -/// "unauthenticated" (see [`InputNotes::from_unauthenticated_notes`]) and therefore does not need -/// to prove that input notes were created in specific past blocks. -#[derive(Debug, Clone)] -pub struct ChainState { - /// The current tip of the chain. - pub chain_tip_header: BlockHeader, - /// A partial representation of the chain MMR. - /// - /// Contains block headers and authentication paths for the last - /// [`NtxBuilderConfig::max_block_count`] blocks only, since all notes are executed as - /// unauthenticated. - pub chain_mmr: Arc, -} - -impl ChainState { - /// Constructs a new instance of [`ChainState`]. - pub(crate) fn new(chain_tip_header: BlockHeader, chain_mmr: PartialMmr) -> Self { - let chain_mmr = PartialBlockchain::new(chain_mmr, []) - .expect("partial blockchain should build from partial mmr"); - Self { - chain_tip_header, - chain_mmr: Arc::new(chain_mmr), - } - } - - /// Consumes the chain state and returns the chain tip header and the partial blockchain as a - /// tuple. - pub fn into_parts(self) -> (BlockHeader, Arc) { - (self.chain_tip_header, self.chain_mmr) - } -} // NETWORK TRANSACTION BUILDER // ================================================================================================ diff --git a/crates/ntx-builder/src/chain_state.rs b/crates/ntx-builder/src/chain_state.rs new file mode 100644 index 000000000..287c0ba29 --- /dev/null +++ b/crates/ntx-builder/src/chain_state.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use miden_protocol::block::BlockHeader; +use miden_protocol::crypto::merkle::mmr::PartialMmr; +use miden_protocol::transaction::PartialBlockchain; + +// CHAIN STATE +// ================================================================================================ + +/// Contains information about the chain that is relevant to the [`NetworkTransactionBuilder`] and +/// all account actors managed by the [`Coordinator`]. +/// +/// The chain MMR stored here contains: +/// - The MMR peaks. +/// - Block headers and authentication paths for the last +/// [`NtxBuilderConfig::max_block_count`](crate::NtxBuilderConfig::max_block_count) blocks. +/// +/// Authentication paths for older blocks are pruned because the NTX builder executes all notes as +/// "unauthenticated" (see [`InputNotes::from_unauthenticated_notes`]) and therefore does not need +/// to prove that input notes were created in specific past blocks. +#[derive(Debug, Clone)] +pub struct ChainState { + /// The current tip of the chain. + pub chain_tip_header: BlockHeader, + /// A partial representation of the chain MMR. + /// + /// Contains block headers and authentication paths for the last + /// [`NtxBuilderConfig::max_block_count`](crate::NtxBuilderConfig::max_block_count) blocks + /// only, since all notes are executed as unauthenticated. + pub chain_mmr: Arc, +} + +impl ChainState { + /// Constructs a new instance of [`ChainState`]. + pub(crate) fn new(chain_tip_header: BlockHeader, chain_mmr: PartialMmr) -> Self { + let chain_mmr = PartialBlockchain::new(chain_mmr, []) + .expect("partial blockchain should build from partial mmr"); + Self { + chain_tip_header, + chain_mmr: Arc::new(chain_mmr), + } + } + + /// Consumes the chain state and returns the chain tip header and the partial blockchain as a + /// tuple. + pub fn into_parts(self) -> (BlockHeader, Arc) { + (self.chain_tip_header, self.chain_mmr) + } +} diff --git a/crates/ntx-builder/src/block_producer.rs b/crates/ntx-builder/src/clients/block_producer.rs similarity index 100% rename from crates/ntx-builder/src/block_producer.rs rename to crates/ntx-builder/src/clients/block_producer.rs diff --git a/crates/ntx-builder/src/clients/mod.rs b/crates/ntx-builder/src/clients/mod.rs new file mode 100644 index 000000000..fd8b5ea0f --- /dev/null +++ b/crates/ntx-builder/src/clients/mod.rs @@ -0,0 +1,5 @@ +mod block_producer; +mod store; + +pub use block_producer::BlockProducerClient; +pub use store::StoreClient; diff --git a/crates/ntx-builder/src/store.rs b/crates/ntx-builder/src/clients/store.rs similarity index 100% rename from crates/ntx-builder/src/store.rs rename to crates/ntx-builder/src/clients/store.rs diff --git a/crates/ntx-builder/src/db/mod.rs b/crates/ntx-builder/src/db/mod.rs index 4cae5cac6..aff084241 100644 --- a/crates/ntx-builder/src/db/mod.rs +++ b/crates/ntx-builder/src/db/mod.rs @@ -13,9 +13,9 @@ use miden_protocol::transaction::TransactionId; use tracing::{info, instrument}; use crate::COMPONENT; -use crate::actor::inflight_note::InflightNetworkNote; use crate::db::migrations::apply_migrations; use crate::db::models::queries; +use crate::inflight_note::InflightNetworkNote; pub(crate) mod models; diff --git a/crates/ntx-builder/src/actor/account_effect.rs b/crates/ntx-builder/src/db/models/account_effect.rs similarity index 100% rename from crates/ntx-builder/src/actor/account_effect.rs rename to crates/ntx-builder/src/db/models/account_effect.rs diff --git a/crates/ntx-builder/src/db/models/mod.rs b/crates/ntx-builder/src/db/models/mod.rs index 405fe0814..8279142b6 100644 --- a/crates/ntx-builder/src/db/models/mod.rs +++ b/crates/ntx-builder/src/db/models/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod account_effect; pub(crate) mod conv; pub mod queries; diff --git a/crates/ntx-builder/src/db/models/queries/mod.rs b/crates/ntx-builder/src/db/models/queries/mod.rs index 2ee11ee28..da1d07c4b 100644 --- a/crates/ntx-builder/src/db/models/queries/mod.rs +++ b/crates/ntx-builder/src/db/models/queries/mod.rs @@ -9,7 +9,7 @@ use miden_protocol::block::{BlockHeader, BlockNumber}; use miden_protocol::note::Nullifier; use miden_protocol::transaction::TransactionId; -use crate::actor::account_effect::NetworkAccountEffect; +use super::account_effect::NetworkAccountEffect; use crate::db::models::conv as conversions; use crate::db::schema; diff --git a/crates/ntx-builder/src/db/models/queries/notes.rs b/crates/ntx-builder/src/db/models/queries/notes.rs index 1c0145a9b..0e8ed8493 100644 --- a/crates/ntx-builder/src/db/models/queries/notes.rs +++ b/crates/ntx-builder/src/db/models/queries/notes.rs @@ -7,9 +7,9 @@ use miden_node_proto::domain::note::SingleTargetNetworkNote; use miden_protocol::block::BlockNumber; use miden_protocol::note::Nullifier; -use crate::actor::inflight_note::InflightNetworkNote; use crate::db::models::conv as conversions; use crate::db::schema; +use crate::inflight_note::InflightNetworkNote; // MODELS // ================================================================================================ diff --git a/crates/ntx-builder/src/inflight_note.rs b/crates/ntx-builder/src/inflight_note.rs new file mode 100644 index 000000000..7c66a6bb7 --- /dev/null +++ b/crates/ntx-builder/src/inflight_note.rs @@ -0,0 +1,139 @@ +use miden_node_proto::domain::note::SingleTargetNetworkNote; +use miden_protocol::block::BlockNumber; +use miden_protocol::note::Note; + +// INFLIGHT NETWORK NOTE +// ================================================================================================ + +/// An unconsumed network note that may have failed to execute. +/// +/// The block number at which the network note was attempted are approximate and may not +/// reflect the exact block number for which the execution attempt failed. The actual block +/// will likely be soon after the number that is recorded here. +#[derive(Debug, Clone)] +pub struct InflightNetworkNote { + note: SingleTargetNetworkNote, + attempt_count: usize, + last_attempt: Option, +} + +impl InflightNetworkNote { + /// Creates a new inflight network note. + pub fn new(note: SingleTargetNetworkNote) -> Self { + Self { + note, + attempt_count: 0, + last_attempt: None, + } + } + + /// Reconstructs an inflight network note from its constituent parts (e.g., from DB rows). + pub fn from_parts( + note: SingleTargetNetworkNote, + attempt_count: usize, + last_attempt: Option, + ) -> Self { + Self { note, attempt_count, last_attempt } + } + + /// Consumes the inflight network note and returns the inner network note. + pub fn into_inner(self) -> SingleTargetNetworkNote { + self.note + } + + /// Returns a reference to the inner network note. + pub fn to_inner(&self) -> &SingleTargetNetworkNote { + &self.note + } + + /// Returns the number of attempts made to execute the network note. + pub fn attempt_count(&self) -> usize { + self.attempt_count + } + + /// Checks if the network note is available for execution. + /// + /// The note is available if the backoff period has passed. + pub fn is_available(&self, block_num: BlockNumber) -> bool { + self.note.can_be_consumed(block_num).unwrap_or(true) + && has_backoff_passed(block_num, self.last_attempt, self.attempt_count) + } + + /// Registers a failed attempt to execute the network note at the specified block number. + pub fn fail(&mut self, block_num: BlockNumber) { + self.last_attempt = Some(block_num); + self.attempt_count += 1; + } +} + +impl From for Note { + fn from(value: InflightNetworkNote) -> Self { + value.into_inner().into() + } +} + +// HELPERS +// ================================================================================================ + +/// Checks if the backoff block period has passed. +/// +/// The number of blocks passed since the last attempt must be greater than or equal to +/// e^(0.25 * `attempt_count`) rounded to the nearest integer. +/// +/// This evaluates to the following: +/// - After 1 attempt, the backoff period is 1 block. +/// - After 3 attempts, the backoff period is 2 blocks. +/// - After 10 attempts, the backoff period is 12 blocks. +/// - After 20 attempts, the backoff period is 148 blocks. +/// - etc... +#[expect(clippy::cast_precision_loss, clippy::cast_sign_loss)] +fn has_backoff_passed( + chain_tip: BlockNumber, + last_attempt: Option, + attempts: usize, +) -> bool { + if attempts == 0 { + return true; + } + // Compute the number of blocks passed since the last attempt. + let blocks_passed = last_attempt + .and_then(|last| chain_tip.checked_sub(last.as_u32())) + .unwrap_or_default(); + + // Compute the exponential backoff threshold: Δ = e^(0.25 * n). + let backoff_threshold = (0.25 * attempts as f64).exp().round() as usize; + + // Check if the backoff period has passed. + blocks_passed.as_usize() > backoff_threshold +} + +#[cfg(test)] +mod tests { + use miden_protocol::block::BlockNumber; + + use super::has_backoff_passed; + + #[rstest::rstest] + #[test] + #[case::all_zero(Some(BlockNumber::GENESIS), BlockNumber::GENESIS, 0, true)] + #[case::no_attempts(None, BlockNumber::GENESIS, 0, true)] + #[case::one_attempt(Some(BlockNumber::GENESIS), BlockNumber::from(2), 1, true)] + #[case::three_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(3), 3, true)] + #[case::ten_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(13), 10, true)] + #[case::twenty_attempts(Some(BlockNumber::GENESIS), BlockNumber::from(149), 20, true)] + #[case::one_attempt_false(Some(BlockNumber::GENESIS), BlockNumber::from(1), 1, false)] + #[case::three_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(2), 3, false)] + #[case::ten_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(12), 10, false)] + #[case::twenty_attempts_false(Some(BlockNumber::GENESIS), BlockNumber::from(148), 20, false)] + fn backoff_has_passed( + #[case] last_attempt_block_num: Option, + #[case] current_block_num: BlockNumber, + #[case] attempt_count: usize, + #[case] backoff_should_have_passed: bool, + ) { + assert_eq!( + backoff_should_have_passed, + has_backoff_passed(current_block_num, last_attempt_block_num, attempt_count) + ); + } +} diff --git a/crates/ntx-builder/src/lib.rs b/crates/ntx-builder/src/lib.rs index bf8772f0b..ca88ec50a 100644 --- a/crates/ntx-builder/src/lib.rs +++ b/crates/ntx-builder/src/lib.rs @@ -4,22 +4,23 @@ use std::sync::Arc; use actor::AccountActorContext; use anyhow::Context; -use block_producer::BlockProducerClient; -use builder::{ChainState, MempoolEventStream}; +use builder::MempoolEventStream; +use chain_state::ChainState; +use clients::{BlockProducerClient, StoreClient}; use coordinator::Coordinator; use db::Db; use futures::TryStreamExt; use miden_node_utils::lru_cache::LruCache; -use store::StoreClient; use tokio::sync::{RwLock, mpsc}; use url::Url; mod actor; -mod block_producer; mod builder; +mod chain_state; +mod clients; mod coordinator; pub(crate) mod db; -mod store; +pub(crate) mod inflight_note; pub use builder::NetworkTransactionBuilder;