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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
- [BREAKING] Removed `--wallet-filepath` / `--counter-filepath` flags and the `MIDEN_MONITOR_WALLET_FILEPATH` / `MIDEN_MONITOR_COUNTER_FILEPATH` env vars from the network monitor. The monitor now keeps wallet and counter accounts fully in memory and regenerates them on every startup; the dashboard's counter value resets to zero on restart.
- Added `--counter-pending-unhealthy-threshold` (env `MIDEN_MONITOR_COUNTER_PENDING_UNHEALTHY_THRESHOLD`, default `5`) to the network monitor: the Network Transactions card now flips unhealthy when the gap between expected and observed counter values stays above the threshold for three consecutive polls.
- Allowed network transaction submission conditionally via the gRPC `SubmitProvenTx` and `SubmitProvenTxBatch` endpoints: the NTX builder can now send a key in the `x-miden-network-tx-auth` header that enables submitting network transactions ([#2131](https://github.com/0xMiden/node/issues/2131)).
- Added a `miden-ntx-builder bootstrap` command that initializes the ntx-builder database with the genesis block fetched from the node RPC. The `start` command now requires a bootstrapped database instead of fetching the genesis block from the committed-block subscription on first run ([#2149](https://github.com/0xMiden/node/pull/2149)).
- Added a `miden-ntx-builder bootstrap` command that initializes the ntx-builder database from a trusted genesis block file. The `start` command now requires a bootstrapped database instead of fetching the genesis block from the committed-block subscription on first run ([#2149](https://github.com/0xMiden/node/pull/2149)).

## v0.14.11 (TBD)

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bin/ntx-builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ anyhow = { workspace = true }
backon = { workspace = true }
clap = { features = ["env", "string"], workspace = true }
diesel = { features = ["numeric", "sqlite"], workspace = true }
fs-err = { workspace = true }
futures = { workspace = true }
humantime = { workspace = true }
libsqlite3-sys = { workspace = true }
Expand Down
22 changes: 0 additions & 22 deletions bin/ntx-builder/src/clients/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,28 +138,6 @@ impl RpcClient {

Ok(())
}

/// Fetches the genesis block from the node RPC, returning `None` if the node does not have it.
#[instrument(target = COMPONENT, name = "ntx.rpc.client.get_genesis_block", skip_all, err)]
pub async fn get_genesis_block(&self) -> Result<Option<SignedBlock>, RpcError> {
let request = proto::blockchain::BlockRequest {
block_num: BlockNumber::GENESIS.as_u32(),
include_proof: Some(false),
};

let response = self
.inner
.clone()
.get_block_by_number(request)
.await
.map_err(RpcError::GrpcClientError)?
.into_inner();

response
.block
.map(|bytes| SignedBlock::read_from_bytes(&bytes).map_err(RpcError::Deserialize))
.transpose()
}
}

fn decode_block_subscription_response(
Expand Down
46 changes: 22 additions & 24 deletions bin/ntx-builder/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::Context;
use clap::Parser;
use miden_node_utils::clap::duration_to_human_readable_string;
use miden_protocol::block::SignedBlock;
use miden_protocol::utils::serde::Deserializable;
use tokio::net::TcpListener;
use tonic::metadata::AsciiMetadataValue;
use url::Url;
Expand All @@ -26,6 +28,7 @@ const DEFAULT_MAX_CYCLES: u32 = 1 << 18;

#[derive(Parser)]
#[command(version, about, long_about = None)]
#[expect(clippy::large_enum_variant, reason = "CLI args are a once off")]
pub enum NtxBuilderCommand {
/// Starts the network transaction builder component.
Start {
Expand Down Expand Up @@ -112,7 +115,7 @@ pub enum NtxBuilderCommand {
enable_otel: bool,
},

/// Bootstraps the ntx-builder database with the genesis block fetched from the node RPC.
/// Bootstraps the ntx-builder database from a trusted genesis block.
///
/// This must be run once before `start` so that the database always contains at least the
/// genesis block.
Expand All @@ -121,31 +124,20 @@ pub enum NtxBuilderCommand {
#[arg(long = "data-directory", env = ENV_DATA_DIRECTORY, value_name = "DIR")]
data_directory: PathBuf,

/// The node RPC service gRPC url.
#[arg(long = "rpc.url", alias = "store.url", env = ENV_RPC_URL, value_name = "URL")]
rpc_url: Url,

/// Optional value for the fixed `x-miden-network-tx-auth` metadata header.
#[arg(
long = "rpc.auth-header-value",
env = ENV_RPC_AUTH_HEADER_VALUE,
value_name = "VALUE"
)]
rpc_auth_header_value: Option<AsciiMetadataValue>,
/// Path to the trusted, signed genesis block file.
#[arg(long, value_name = "FILE")]
genesis_block: PathBuf,
},
}

impl NtxBuilderCommand {
pub async fn handle(self) -> anyhow::Result<()> {
match self {
Self::Start { .. } => self.start().await,
Self::Bootstrap {
data_directory,
rpc_url,
rpc_auth_header_value,
} => {
Self::Bootstrap { data_directory, genesis_block } => {
let database_filepath = data_directory.join("ntx-builder.sqlite3");
miden_ntx_builder::bootstrap(database_filepath, rpc_url, rpc_auth_header_value)
let genesis = read_genesis_block(&genesis_block)?;
miden_ntx_builder::bootstrap(database_filepath, &genesis)
.await
.context("failed to bootstrap ntx-builder database")
},
Expand Down Expand Up @@ -206,6 +198,12 @@ impl NtxBuilderCommand {
}
}

/// Reads a genesis block from disk and returns the signed block.
fn read_genesis_block(genesis_block_path: &Path) -> anyhow::Result<SignedBlock> {
let bytes = fs_err::read(genesis_block_path).context("failed to read genesis block")?;
SignedBlock::read_from_bytes(&bytes).context("failed to deserialize genesis block from file")
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;
Expand Down Expand Up @@ -239,22 +237,22 @@ mod tests {
}

#[test]
fn bootstrap_command_parses_data_directory_and_rpc_url() {
fn bootstrap_command_parses_data_directory_and_genesis_block() {
let command = NtxBuilderCommand::try_parse_from([
"miden-ntx-builder",
"bootstrap",
"--data-directory",
"/tmp/miden-ntx-builder",
"--rpc.url",
"http://127.0.0.1:57291",
"--genesis-block",
"/tmp/genesis.dat",
])
.expect("command should parse");

let NtxBuilderCommand::Bootstrap { data_directory, rpc_url, .. } = command else {
let NtxBuilderCommand::Bootstrap { data_directory, genesis_block } = command else {
panic!("expected the bootstrap command");
};

assert_eq!(data_directory, PathBuf::from("/tmp/miden-ntx-builder"));
assert_eq!(rpc_url.as_str(), "http://127.0.0.1:57291/");
assert_eq!(genesis_block, PathBuf::from("/tmp/genesis.dat"));
}
}
62 changes: 39 additions & 23 deletions bin/ntx-builder/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use clients::RpcClient;
use db::Db;
use miden_node_utils::ErrorReport;
use miden_node_utils::lru_cache::LruCache;
use miden_protocol::block::{BlockNumber, SignedBlock};
use miden_remote_prover_client::RemoteTransactionProver;
use tokio::sync::mpsc;
use tonic::metadata::AsciiMetadataValue;
Expand Down Expand Up @@ -42,33 +43,48 @@ pub use builder::NetworkTransactionBuilder;

/// Bootstraps the ntx-builder database at `database_filepath` with the genesis block.
///
/// The genesis block is fetched from the node RPC with a single `GetBlockByNumber` request. After
/// this completes the singleton chain-state row exists at the genesis block number, so
/// [`NtxBuilderConfig`] startup can always resume from a persisted chain state instead of
/// consuming the genesis block from the subscription.
/// After this completes the singleton chain-state row exists at the genesis block number, so
/// [`NtxBuilderConfig`] startup can always resume from a persisted chain state instead of consuming
/// the genesis block from the subscription.
///
/// Returns an error if the node is unreachable, does not yet have the genesis block, or the
/// database has already been bootstrapped.
pub async fn bootstrap(
database_filepath: PathBuf,
rpc_url: Url,
rpc_auth_header: Option<AsciiMetadataValue>,
) -> anyhow::Result<()> {
let rpc = RpcClient::new_with_auth(
rpc_url,
rpc_auth_header,
DEFAULT_REQUEST_BACKOFF_INITIAL,
DEFAULT_REQUEST_BACKOFF_MAX,
/// Returns an error if the block is not a valid genesis block or if the database has already been
/// bootstrapped.
pub async fn bootstrap(database_filepath: PathBuf, genesis: &SignedBlock) -> anyhow::Result<()> {
validate_genesis_block(genesis).context("genesis block validation failed")?;
db::Db::bootstrap(database_filepath, genesis).await
}

fn validate_genesis_block(block: &SignedBlock) -> anyhow::Result<()> {
anyhow::ensure!(
block.header().block_num() == BlockNumber::GENESIS,
"expected genesis block number (0), got {}",
block.header().block_num(),
);

anyhow::ensure!(
block
.signature()
.verify(block.header().commitment(), block.header().validator_key()),
"genesis block signature verification failed",
);

let genesis = rpc
.get_genesis_block()
.await
.map_err(|err| anyhow::anyhow!(err))
.context("failed to fetch the genesis block from the node RPC")?
.context("the node RPC returned no genesis block")?;
Ok(())
}

db::Db::bootstrap(database_filepath, &genesis).await
#[cfg(test)]
mod bootstrap_tests {
use super::*;

#[test]
fn validate_genesis_block_rejects_invalid_signature() {
let block = crate::test_utils::mock_genesis_block();
let err = validate_genesis_block(&block).expect_err("invalid signature should fail");

assert!(
err.to_string().contains("signature verification failed"),
"unexpected error: {err}",
);
}
}

// CONSTANTS
Expand Down
Loading