Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Added chain tip to the block producer status ([#1419](https://github.com/0xMiden/miden-node/pull/1419)).
- The mempool's transaction capacity is now configurable ([#1433](https://github.com/0xMiden/miden-node/pull/1433)).
- Renamed card's names in the `miden-network-monitor` binary ([#1441](https://github.com/0xMiden/miden-node/pull/1441)).
- Integrated NTX Builder with validator via `SubmitProvenTransaction` RPC ([#1453](https://github.com/0xMiden/miden-node/pull/1453)).

### Changes

Expand Down
1 change: 1 addition & 0 deletions bin/node/src/commands/bundled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ impl BundledCommand {
NetworkTransactionBuilder::new(
store_ntx_builder_url,
block_producer_url,
rpc_url,
ntx_builder.tx_prover_url,
ntx_builder.ticker_interval,
checkpoint,
Expand Down
40 changes: 19 additions & 21 deletions crates/ntx-builder/src/actor/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ use tracing::{Instrument, instrument};

use crate::COMPONENT;
use crate::actor::account_state::TransactionCandidate;
use crate::block_producer::BlockProducerClient;
use crate::rpc::RpcClient;
use crate::store::StoreClient;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -75,7 +75,8 @@ type NtxResult<T> = Result<T, NtxError>;
/// Provides the context for execution [network transaction candidates](TransactionCandidate).
#[derive(Clone)]
pub struct NtxContext {
block_producer: BlockProducerClient,
/// Client for submitting transactions to the network.
rpc_client: RpcClient,

/// The prover to delegate proofs to.
///
Expand All @@ -93,17 +94,12 @@ pub struct NtxContext {
impl NtxContext {
/// Creates a new [`NtxContext`] instance.
pub fn new(
block_producer: BlockProducerClient,
rpc_client: RpcClient,
prover: Option<RemoteTransactionProver>,
store: StoreClient,
script_cache: LruCache<Word, NoteScript>,
) -> Self {
Self {
block_producer,
prover,
store,
script_cache,
}
Self { rpc_client, prover, store, script_cache }
}

/// Executes a transaction end-to-end: filtering, executing, proving, and submitted to the block
Expand Down Expand Up @@ -147,7 +143,7 @@ impl NtxContext {
.set_attribute("reference_block.number", chain_tip_header.block_num());

async move {
async move {
Box::pin(async move {
let data_store = NtxDataStore::new(
account,
chain_tip_header,
Expand All @@ -157,13 +153,15 @@ impl NtxContext {
);

let notes = notes.into_iter().map(Note::from).collect::<Vec<_>>();
let (successful, failed) = self.filter_notes(&data_store, notes).await?;
let executed = Box::pin(self.execute(&data_store, successful)).await?;
let proven = Box::pin(self.prove(executed.into())).await?;
let tx_id = proven.id();
self.submit(proven).await?;
Ok((tx_id, failed))
}
let (successful_notes, failed_notes) =
self.filter_notes(&data_store, notes).await?;
let executed_tx = Box::pin(self.execute(&data_store, successful_notes)).await?;
let tx_inputs: TransactionInputs = executed_tx.into();
let proven_tx = Box::pin(self.prove(tx_inputs.clone())).await?;
let tx_id = proven_tx.id();
self.submit(proven_tx, tx_inputs).await?;
Ok((tx_id, failed_notes))
})
.in_current_span()
.await
.inspect_err(|err| tracing::Span::current().set_error(err))
Expand Down Expand Up @@ -256,11 +254,11 @@ impl NtxContext {
.map_err(NtxError::Proving)
}

/// Submits the transaction to the block producer.
/// Submits the transaction to the RPC server with transaction inputs.
#[instrument(target = COMPONENT, name = "ntx.execute_transaction.submit", skip_all, err)]
async fn submit(&self, tx: ProvenTransaction) -> NtxResult<()> {
self.block_producer
.submit_proven_transaction(tx)
async fn submit(&self, tx: ProvenTransaction, tx_inputs: TransactionInputs) -> NtxResult<()> {
self.rpc_client
.submit_proven_transaction(tx, tx_inputs)
.await
.map_err(NtxError::Submission)
}
Expand Down
14 changes: 7 additions & 7 deletions crates/ntx-builder/src/actor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ use tokio::sync::{AcquireError, RwLock, Semaphore, mpsc};
use tokio_util::sync::CancellationToken;
use url::Url;

use crate::block_producer::BlockProducerClient;
use crate::builder::ChainState;
use crate::rpc::RpcClient;
use crate::store::StoreClient;

// ACTOR SHUTDOWN REASON
Expand Down Expand Up @@ -52,8 +52,8 @@ pub enum ActorShutdownReason {
pub struct AccountActorContext {
/// Client for interacting with the store in order to load account state.
pub store: StoreClient,
/// Address of the block producer gRPC server.
pub block_producer_url: Url,
/// Address of the RPC gRPC server.
pub rpc_url: Url,
/// Address of the remote prover. If `None`, transactions will be proven locally, which is
// undesirable due to the performance impact.
pub tx_prover_url: Option<Url>,
Expand Down Expand Up @@ -153,7 +153,7 @@ pub struct AccountActor {
mode: ActorMode,
event_rx: mpsc::Receiver<Arc<MempoolEvent>>,
cancel_token: CancellationToken,
block_producer: BlockProducerClient,
rpc_client: RpcClient,
prover: Option<RemoteTransactionProver>,
chain_state: Arc<RwLock<ChainState>>,
script_cache: LruCache<Word, NoteScript>,
Expand All @@ -168,15 +168,15 @@ impl AccountActor {
event_rx: mpsc::Receiver<Arc<MempoolEvent>>,
cancel_token: CancellationToken,
) -> Self {
let block_producer = BlockProducerClient::new(actor_context.block_producer_url.clone());
let rpc_client = RpcClient::new(actor_context.rpc_url.clone());
let prover = actor_context.tx_prover_url.clone().map(RemoteTransactionProver::new);
Self {
origin,
store: actor_context.store.clone(),
mode: ActorMode::NoViableNotes,
event_rx,
cancel_token,
block_producer,
rpc_client,
prover,
chain_state: actor_context.chain_state.clone(),
script_cache: actor_context.script_cache.clone(),
Expand Down Expand Up @@ -275,7 +275,7 @@ impl AccountActor {

// Execute the selected transaction.
let context = execute::NtxContext::new(
self.block_producer.clone(),
self.rpc_client.clone(),
self.prover.clone(),
self.store.clone(),
self.script_cache.clone(),
Expand Down
16 changes: 0 additions & 16 deletions crates/ntx-builder/src/block_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ use miden_node_proto::domain::mempool::MempoolEvent;
use miden_node_proto::generated::{self as proto};
use miden_node_utils::FlattenResult;
use miden_objects::block::BlockNumber;
use miden_objects::transaction::ProvenTransaction;
use miden_tx::utils::Serializable;
use tokio_stream::StreamExt;
use tonic::Status;
use tracing::{info, instrument};
Expand Down Expand Up @@ -41,20 +39,6 @@ impl BlockProducerClient {

Self { client: block_producer }
}
#[instrument(target = COMPONENT, name = "ntx.block_producer.client.submit_proven_transaction", skip_all, err)]
pub async fn submit_proven_transaction(
&self,
proven_tx: ProvenTransaction,
) -> Result<(), Status> {
let request = proto::transaction::ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: None,
};

self.client.clone().submit_proven_transaction(request).await?;

Ok(())
}

#[instrument(target = COMPONENT, name = "ntx.block_producer.client.subscribe_to_mempool", skip_all, err)]
pub async fn subscribe_to_mempool_with_retry(
Expand Down
6 changes: 5 additions & 1 deletion crates/ntx-builder/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub struct NetworkTransactionBuilder {
store_url: Url,
/// Address of the block producer gRPC server.
block_producer_url: Url,
/// Address of the RPC gRPC server.
rpc_url: Url,
/// Address of the remote prover. If `None`, transactions will be proven locally, which is
/// undesirable due to the performance impact.
tx_prover_url: Option<Url>,
Expand Down Expand Up @@ -101,6 +103,7 @@ impl NetworkTransactionBuilder {
pub fn new(
store_url: Url,
block_producer_url: Url,
rpc_url: Url,
tx_prover_url: Option<Url>,
ticker_interval: Duration,
bp_checkpoint: Arc<Barrier>,
Expand All @@ -110,6 +113,7 @@ impl NetworkTransactionBuilder {
Self {
store_url,
block_producer_url,
rpc_url,
tx_prover_url,
ticker_interval,
bp_checkpoint,
Expand Down Expand Up @@ -145,7 +149,7 @@ impl NetworkTransactionBuilder {
let chain_state = Arc::new(RwLock::new(ChainState::new(chain_tip_header, chain_mmr)));

let actor_context = AccountActorContext {
block_producer_url: self.block_producer_url.clone(),
rpc_url: self.rpc_url.clone(),
tx_prover_url: self.tx_prover_url.clone(),
chain_state: chain_state.clone(),
store: store.clone(),
Expand Down
1 change: 1 addition & 0 deletions crates/ntx-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod actor;
mod block_producer;
mod builder;
mod coordinator;
mod rpc;
mod store;

pub use builder::NetworkTransactionBuilder;
Expand Down
52 changes: 52 additions & 0 deletions crates/ntx-builder/src/rpc.rs
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unfortunately this won't work as the RPC rejects network transactions since these are not allowed to be sent publicly (yet).

Options:

  1. Add the validator directly to ntx -- this seems unreasonable.
  2. Open up network transactions publicly
  3. Add authentication somehow
  4. An additional internal RPC url which the ntx uses which only supports the ntx submit tx method but skips the rejection

I haven't done (3) with gRPC before, so I would automatically reach for auth via http but https://grpc.io/docs/guides/auth/ has some more info.

Copy link
Contributor

@drahnr drahnr Dec 23, 2025

Choose a reason for hiding this comment

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

Can you share some more context on the relationship of auth / network transactions beyond to give some context? (link is enough)

Copy link
Collaborator

@Mirko-von-Leipzig Mirko-von-Leipzig Dec 24, 2025

Choose a reason for hiding this comment

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

There isn't really any direct context aside from the fact that currently we only allow our own network transaction builder component to submit network transactions. Allowing external submissions would cause races with our builder and we don't want to deal with that at this stage.

This is currently enforced in the RPC component's SubmitProvenTransaction implementation by simply rejecting all transactions that target a network account (except for those which create the account). This means external clients/users can only create a network account and thereafter all transactions for that account cannot go via the public RPC endpoints.

The network tx builder circumvents this by using the block-producer's SubmitProvenTransaction directly. This works but now that we're adding additional logic (validator stuff) to the RPC's gRPC implementation it would be ideal if we only have that in a single place.

Network transactions must also be submitted to the validator, so either we

  1. duplicate the validator interaction logic within the network transaction builder as well, or
  2. keep a single coherent impl in the RPC component
    • and therefore the RPC component must somehow allow network transactions from the internal network transaction builder without allowing them in from the general public
    • one option for this is some sort of authentication to just say "hey this came from our internal network transaction builder, don't reject it"

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think option 2 is viable. Network transactions shouldn't come from the outside at this point.

Why is option 1 unreasonable? NTX builder could send the transaction directly to the validator in the same way as the RPC does. It feel like this would take the least amount of work.

My order of preference would probably be:

  • Option 3, if it is not difficult to do. We could send the API key in the headers somehow - though, I remember working with headers was not super straight-forward - but maybe now we have this solved.
  • Option 1 - just sent transactions from the NTX builder directly to the validator. As I mentioned above, this should be pretty straight-forward to implement.
  • Option 4 - though, running two separate RPCs (one for internal use and one for external use) may introduce more complexity than the other two approaches.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Summarizing: go with option 1 - ntx-builder talks directly to validator

Copy link
Collaborator Author

@sergerad sergerad Jan 6, 2026

Choose a reason for hiding this comment

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

Moved forward with Option 1 (ready for review again)

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use miden_node_proto::clients::{Builder, RpcClient as InnerRpcClient};
use miden_node_proto::generated::{self as proto};
use miden_objects::transaction::{ProvenTransaction, TransactionInputs};
use miden_tx::utils::Serializable;
use tonic::Status;
use tracing::{info, instrument};
use url::Url;

use crate::COMPONENT;

// RPC CLIENT
// ================================================================================================

/// Interface to the RPC server's gRPC API.
#[derive(Clone, Debug)]
pub struct RpcClient {
client: InnerRpcClient,
}

impl RpcClient {
/// Creates a new RPC client with a lazy connection.
pub fn new(rpc_url: Url) -> Self {
info!(target: COMPONENT, rpc_endpoint = %rpc_url, "Initializing RPC client with lazy connection");

let rpc = Builder::new(rpc_url)
.without_tls()
.without_timeout()
.without_metadata_version()
.without_metadata_genesis()
.with_otel_context_injection()
.connect_lazy::<InnerRpcClient>();

Self { client: rpc }
}

/// Submits a proven transaction with transaction inputs to the RPC server.
#[instrument(target = COMPONENT, name = "ntx.rpc.client.submit_proven_transaction", skip_all, err)]
pub async fn submit_proven_transaction(
&self,
proven_tx: ProvenTransaction,
tx_inputs: TransactionInputs,
) -> Result<(), Status> {
let request = proto::transaction::ProvenTransaction {
transaction: proven_tx.to_bytes(),
transaction_inputs: Some(tx_inputs.to_bytes()),
};

self.client.clone().submit_proven_transaction(request).await?;

Ok(())
}
}