Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

- Fixed network monitor faucet test failing to parse `/get_metadata` response due to field type mismatches ([#1612](https://github.com/0xMiden/miden-node/pull/1612)).

### Changes

- Refactored NTX Builder startup and introduced `NtxBuilderConfig` with configurable parameters ([#1610](https://github.com/0xMiden/miden-node/pull/1610)).

## v0.13.2 (2026-01-27)

- Network transaction builder no longer creates conflicting transactions by consuming the same notes twice ([#1597](https://github.com/0xMiden/miden-node/issues/1597)).
Expand Down
35 changes: 18 additions & 17 deletions bin/node/src/commands/bundled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::time::Duration;

use anyhow::Context;
use miden_node_block_producer::BlockProducer;
use miden_node_ntx_builder::NetworkTransactionBuilder;
use miden_node_rpc::Rpc;
use miden_node_store::Store;
use miden_node_utils::grpc::UrlExt;
Expand Down Expand Up @@ -304,27 +303,29 @@ impl BundledCommand {
]);

// Start network transaction builder. The endpoint is available after loading completes.
let store_ntx_builder_url = Url::parse(&format!("http://{store_ntx_builder_address}"))
.context("Failed to parse URL")?;

if should_start_ntx_builder {
let store_ntx_builder_url = Url::parse(&format!("http://{store_ntx_builder_address}"))
.context("Failed to parse URL")?;
let validator_url = Url::parse(&format!("http://{validator_address}"))
.context("Failed to parse URL")?;
let block_producer_url = Url::parse(&format!("http://{block_producer_address}"))
.context("Failed to parse URL")?;

let builder_config = ntx_builder.into_builder_config(
store_ntx_builder_url,
block_producer_url,
validator_url,
);

let id = join_set
.spawn(async move {
let block_producer_url =
Url::parse(&format!("http://{block_producer_address}"))
.context("Failed to parse URL")?;
NetworkTransactionBuilder::new(
store_ntx_builder_url,
block_producer_url,
validator_url,
ntx_builder.tx_prover_url,
ntx_builder.script_cache_size,
)
.run()
.await
.context("failed while serving ntx builder component")
builder_config
.build()
.await
.context("failed to initialize ntx builder")?
.run()
.await
.context("failed while serving ntx builder component")
})
.id();
component_ids.insert(id, "ntx-builder");
Expand Down
17 changes: 17 additions & 0 deletions bin/node/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ pub struct NtxBuilderConfig {
)]
pub ticker_interval: Duration,

/// Number of note scripts to cache locally.
///
/// Note scripts not in cache must first be retrieved from the store.
#[arg(
long = "ntx-builder.script-cache-size",
env = ENV_NTX_SCRIPT_CACHE_SIZE,
Expand All @@ -77,6 +80,20 @@ pub struct NtxBuilderConfig {
pub script_cache_size: NonZeroUsize,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more a thought experiment, but I wonder if we ever want to set this to zero? Seems unlikely, but that would effectively be the same as disabling it.

I think NonZero is used because that's what LRU requires, so we would have to do struct MyLru<T>(Option<Lru<T>>) which is a bit more work but not too much.

I kind of like the idea that we can disable caching e.g. for performance testing. wdyt? As a separate issue though, if anything. Could also easily be a YAGNI thing so maybe we just leave as is until we do need it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to test performance of the underlaying implementation? Otherwise I don't see why we need that

}

impl NtxBuilderConfig {
/// Converts this CLI config into the ntx-builder's internal config.
pub fn into_builder_config(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit confusing with the stuttering builder/builder naming, but that's not this PRs fault. We should come up with something better than network transaction builder..

Maybe network transaction..

  • producer
  • service
  • component (bleh)
  • handler
  • .. hm.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree. We tend to use a lot the builder pattern so it is a bit confusing. I like both "producer" and "service", and even "consumer". I'll create a separate issue to discuss and rename the component since it touches too many files for this PR

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #1620

self,
store_url: Url,
block_producer_url: Url,
validator_url: Url,
) -> miden_node_ntx_builder::NtxBuilderConfig {
miden_node_ntx_builder::NtxBuilderConfig::new(store_url, block_producer_url, validator_url)
.with_tx_prover_url(self.tx_prover_url)
.with_script_cache_size(self.script_cache_size)
}
}

/// Configuration for the Block Producer component
#[derive(clap::Args)]
pub struct BlockProducerConfig {
Expand Down
12 changes: 8 additions & 4 deletions crates/ntx-builder/src/actor/account_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ pub struct NetworkAccountState {
}

impl NetworkAccountState {
/// Maximum number of attempts to execute a network note.
const MAX_NOTE_ATTEMPTS: usize = 30;

/// Load's all available network notes from the store, along with the required account states.
#[instrument(target = COMPONENT, name = "ntx.state.load", skip_all)]
pub async fn load(
Expand Down Expand Up @@ -110,14 +107,21 @@ impl NetworkAccountState {
}

/// Selects the next candidate network transaction.
///
/// # Parameters
///
/// - `limit`: Maximum number of notes to include in the transaction.
/// - `max_note_attempts`: Maximum number of execution attempts before a note is dropped.
/// - `chain_state`: Current chain state for the transaction.
#[instrument(target = COMPONENT, name = "ntx.state.select_candidate", skip_all)]
pub fn select_candidate(
&mut self,
limit: NonZeroUsize,
max_note_attempts: usize,
chain_state: ChainState,
) -> Option<TransactionCandidate> {
// Remove notes that have failed too many times.
self.account.drop_failing_notes(Self::MAX_NOTE_ATTEMPTS);
self.account.drop_failing_notes(max_note_attempts);

// Skip empty accounts, and prune them.
// This is how we keep the number of accounts bounded.
Expand Down
17 changes: 16 additions & 1 deletion crates/ntx-builder/src/actor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod execute;
mod inflight_note;
mod note_state;

use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;

Expand Down Expand Up @@ -66,6 +67,10 @@ pub struct AccountActorContext {
/// Shared LRU cache for storing retrieved note scripts to avoid repeated store calls.
/// This cache is shared across all account actors to maximize cache efficiency.
pub script_cache: LruCache<Word, NoteScript>,
/// Maximum number of notes per transaction.
pub max_notes_per_tx: NonZeroUsize,
/// Maximum number of note execution attempts before dropping a note.
pub max_note_attempts: usize,
}

// ACCOUNT ORIGIN
Expand Down Expand Up @@ -161,6 +166,10 @@ pub struct AccountActor {
prover: Option<RemoteTransactionProver>,
chain_state: Arc<RwLock<ChainState>>,
script_cache: LruCache<Word, NoteScript>,
/// Maximum number of notes per transaction.
max_notes_per_tx: NonZeroUsize,
/// Maximum number of note execution attempts before dropping a note.
max_note_attempts: usize,
}

impl AccountActor {
Expand Down Expand Up @@ -192,6 +201,8 @@ impl AccountActor {
prover,
chain_state: actor_context.chain_state.clone(),
script_cache: actor_context.script_cache.clone(),
max_notes_per_tx: actor_context.max_notes_per_tx,
max_note_attempts: actor_context.max_note_attempts,
}
}

Expand Down Expand Up @@ -258,7 +269,11 @@ impl AccountActor {
// Read the chain state.
let chain_state = self.chain_state.read().await.clone();
// Find a candidate transaction and execute it.
if let Some(tx_candidate) = state.select_candidate(crate::MAX_NOTES_PER_TX, chain_state) {
if let Some(tx_candidate) = state.select_candidate(
self.max_notes_per_tx,
self.max_note_attempts,
chain_state,
) {
self.execute_transactions(&mut state, tx_candidate).await;
} else {
// No transactions to execute, wait for events.
Expand Down
4 changes: 2 additions & 2 deletions crates/ntx-builder/src/block_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl BlockProducerClient {
pub async fn subscribe_to_mempool_with_retry(
&self,
chain_tip: BlockNumber,
) -> Result<impl TryStream<Ok = MempoolEvent, Error = Status>, Status> {
) -> Result<impl TryStream<Ok = MempoolEvent, Error = Status> + Send + 'static, Status> {
let mut retry_counter = 0;
loop {
match self.subscribe_to_mempool(chain_tip).await {
Expand Down Expand Up @@ -90,7 +90,7 @@ impl BlockProducerClient {
async fn subscribe_to_mempool(
&self,
chain_tip: BlockNumber,
) -> Result<impl TryStream<Ok = MempoolEvent, Error = Status>, Status> {
) -> Result<impl TryStream<Ok = MempoolEvent, Error = Status> + Send + 'static, Status> {
let request =
proto::block_producer::MempoolSubscriptionRequest { chain_tip: chain_tip.as_u32() };
let stream = self.client.clone().mempool_subscription(request).await?;
Expand Down
Loading