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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +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)).

## v0.14.11 (TBD)

Expand Down
24 changes: 23 additions & 1 deletion bin/ntx-builder/src/clients/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,28 @@ 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 Expand Up @@ -199,6 +221,6 @@ impl RpcClient {
pub enum RpcError {
#[error("RPC gRPC call failed")]
GrpcClientError(#[source] tonic::Status),
#[error("failed to deserialize subscription payload")]
#[error("failed to deserialize RPC payload")]
Deserialize(#[source] miden_protocol::utils::serde::DeserializationError),
}
76 changes: 72 additions & 4 deletions bin/ntx-builder/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,48 @@ pub enum NtxBuilderCommand {
#[arg(long = "enable-otel", default_value_t = false, env = ENV_ENABLE_OTEL, value_name = "BOOL")]
enable_otel: bool,
},

/// Bootstraps the ntx-builder database with the genesis block fetched from the node RPC.
///
/// This must be run once before `start` so that the database always contains at least the
/// genesis block.
Bootstrap {
/// Directory for the ntx-builder's persistent database.
#[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>,
},
}

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,
} => {
let database_filepath = data_directory.join("ntx-builder.sqlite3");
miden_ntx_builder::bootstrap(database_filepath, rpc_url, rpc_auth_header_value)
.await
.context("failed to bootstrap ntx-builder database")
},
}
}

async fn start(self) -> anyhow::Result<()> {
let Self::Start {
listen,
rpc_url,
Expand All @@ -127,7 +165,10 @@ impl NtxBuilderCommand {
sqlite_connection_pool_size,
data_directory,
enable_otel: _,
} = self;
} = self
else {
unreachable!("start is only called for the Start variant")
};

let listener = TcpListener::bind(listen)
.await
Expand Down Expand Up @@ -157,13 +198,18 @@ impl NtxBuilderCommand {
}

pub fn is_open_telemetry_enabled(&self) -> bool {
let Self::Start { enable_otel, .. } = self;
*enable_otel
match self {
Self::Start { enable_otel, .. } => *enable_otel,
// Bootstrap is a one-shot command and does not set up a tracing pipeline.
Self::Bootstrap { .. } => false,
}
}
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;

use clap::Parser;
use tonic::metadata::AsciiMetadataValue;

Expand All @@ -185,8 +231,30 @@ mod tests {
])
.expect("command should parse");

let NtxBuilderCommand::Start { rpc_auth_header_value, .. } = command;
let NtxBuilderCommand::Start { rpc_auth_header_value, .. } = command else {
panic!("expected the start command");
};

assert_eq!(rpc_auth_header_value, Some(AsciiMetadataValue::from_static("secret-token")));
}

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

let NtxBuilderCommand::Bootstrap { data_directory, rpc_url, .. } = 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/");
}
}
76 changes: 75 additions & 1 deletion bin/ntx-builder/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use anyhow::Context;
use miden_node_db::DatabaseError;
use miden_protocol::Word;
use miden_protocol::account::AccountId;
use miden_protocol::block::{BlockHeader, BlockNumber};
use miden_protocol::block::{BlockHeader, BlockNumber, SignedBlock};
use miden_protocol::crypto::merkle::mmr::PartialMmr;
use miden_protocol::note::{NoteId, NoteScript, Nullifier};
use miden_standards::note::AccountTargetNetworkNote;
Expand Down Expand Up @@ -71,6 +71,40 @@ impl Db {
Ok(Db { inner })
}

/// Creates and initializes the database, then seeds it with the signed genesis block.
///
/// Mirrors the store's bootstrap (`Db::bootstrap`): after this completes the singleton
/// `chain_state` row exists at [`BlockNumber::GENESIS`], so [`crate::NtxBuilderConfig::build`]
/// can assume the genesis block is always present and never has to consume it from the
/// committed-block subscription on startup.
///
/// Returns an error if the database has already been bootstrapped.
#[instrument(
target = COMPONENT,
name = "ntx_builder.database.bootstrap",
skip_all,
fields(path=%database_filepath.display()),
err,
)]
pub async fn bootstrap(
database_filepath: PathBuf,
genesis: &SignedBlock,
) -> anyhow::Result<()> {
let db = Self::setup(database_filepath).await?;

anyhow::ensure!(
db.get_chain_state().await.context("failed to read chain state")?.is_none(),
"ntx-builder database is already bootstrapped",
);

let effects = CommittedBlockEffects::from_signed_block(genesis);
db.apply_committed_block(effects, PartialMmr::default())
.await
.context("failed to insert genesis block")?;

Ok(())
}

// BLOCK APPLICATION
// ============================================================================================

Expand Down Expand Up @@ -286,3 +320,43 @@ impl LoopDb {
.await
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::mock_genesis_block;

#[tokio::test]
async fn bootstrap_seeds_genesis_chain_state() {
let dir = tempfile::tempdir().expect("failed to create temp directory");
let db_path = dir.path().join("ntx-builder.sqlite3");

Db::bootstrap(db_path.clone(), &mock_genesis_block())
.await
.expect("bootstrap should succeed on a fresh database");

let db = Db::setup(db_path).await.expect("setup should open the bootstrapped database");
let (block_num, ..) = db
.get_chain_state()
.await
.expect("query should succeed")
.expect("chain state should be present after bootstrap");

assert_eq!(block_num, BlockNumber::GENESIS);
}

#[tokio::test]
async fn bootstrap_rejects_already_bootstrapped_database() {
let dir = tempfile::tempdir().expect("failed to create temp directory");
let db_path = dir.path().join("ntx-builder.sqlite3");

Db::bootstrap(db_path.clone(), &mock_genesis_block())
.await
.expect("first bootstrap should succeed");

let err = Db::bootstrap(db_path, &mock_genesis_block())
.await
.expect_err("second bootstrap should fail");
assert!(err.to_string().contains("already bootstrapped"), "unexpected error: {err}");
}
}
Loading
Loading