diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b100be8..6e7b0dbc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.13.5 (TBD) - OpenTelemetry traces are now flushed before program termination on panic ([#1643](https://github.com/0xMiden/miden-node/pull/1643)). +- Added support for the note transport layer in the network monitor ([#1660](https://github.com/0xMiden/miden-node/pull/1660)). ## v0.13.4 (2026-02-04) diff --git a/Cargo.lock b/Cargo.lock index ea5d48b2b..2af2b9672 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2746,6 +2746,7 @@ dependencies = [ "sha2", "tokio", "tonic", + "tonic-health", "tracing", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 10e156772..51b16fd08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ tokio = { features = ["rt-multi-thread"], version = "1.46" } tokio-stream = { version = "0.1" } toml = { version = "0.9" } tonic = { default-features = false, version = "0.14" } +tonic-health = { version = "0.14" } tonic-prost = { version = "0.14" } tonic-prost-build = { version = "0.14" } tonic-reflection = { version = "0.14" } diff --git a/bin/network-monitor/.env b/bin/network-monitor/.env index ad861da56..2c37c1978 100644 --- a/bin/network-monitor/.env +++ b/bin/network-monitor/.env @@ -20,3 +20,5 @@ MIDEN_MONITOR_COUNTER_INCREMENT_INTERVAL=30s MIDEN_MONITOR_COUNTER_LATENCY_TIMEOUT=2m # explorer checks MIDEN_MONITOR_EXPLORER_URL=https://scan-backend-devnet-miden.eu-central-8.gateway.fm/graphql +# note transport checks +MIDEN_MONITOR_NOTE_TRANSPORT_URL=https://transport.miden.io diff --git a/bin/network-monitor/Cargo.toml b/bin/network-monitor/Cargo.toml index 64a1f19e1..11c2b1905 100644 --- a/bin/network-monitor/Cargo.toml +++ b/bin/network-monitor/Cargo.toml @@ -34,5 +34,6 @@ serde_json = { version = "1.0" } sha2 = { version = "0.10" } tokio = { features = ["full"], workspace = true } tonic = { features = ["codegen", "tls-native-roots", "transport"], workspace = true } +tonic-health = { workspace = true } tracing = { workspace = true } url = { features = ["serde"], workspace = true } diff --git a/bin/network-monitor/README.md b/bin/network-monitor/README.md index 2affde86d..8a0dc71c8 100644 --- a/bin/network-monitor/README.md +++ b/bin/network-monitor/README.md @@ -31,6 +31,7 @@ miden-network-monitor start --faucet-url http://localhost:8080 --enable-otel - `--remote-prover-urls`: Comma-separated list of remote prover URLs. If omitted or empty, prover tasks are disabled. - `--faucet-url`: Faucet service URL for testing. If omitted, faucet testing is disabled. - `--explorer-url`: Explorer service GraphQL endpoint. If omitted, explorer checks are disabled. +- `--note-transport-url`: Note transport service URL for health checking. If omitted, note transport checks are disabled. - `--disable-ntx-service`: Disable the network transaction service checks (enabled by default). The network transaction service consists of two components: counter increment (sending increment transactions) and counter tracking (monitoring counter value changes). - `--remote-prover-test-interval`: Interval at which to test the remote provers services (default: `2m`) - `--faucet-test-interval`: Interval at which to test the faucet services (default: `2m`) @@ -54,6 +55,7 @@ If command-line arguments are not provided, the application falls back to enviro - `MIDEN_MONITOR_REMOTE_PROVER_URLS`: Comma-separated list of remote prover URLs. If unset or empty, prover tasks are disabled. - `MIDEN_MONITOR_FAUCET_URL`: Faucet service URL for testing. If unset, faucet testing is disabled. - `MIDEN_MONITOR_EXPLORER_URL`: Explorer service GraphQL endpoint. If unset, explorer checks are disabled. +- `MIDEN_MONITOR_NOTE_TRANSPORT_URL`: Note transport service URL for health checking. If unset, note transport checks are disabled. - `MIDEN_MONITOR_DISABLE_NTX_SERVICE`: Set to `true` to disable the network transaction service checks (enabled by default). This affects both counter increment and tracking components. - `MIDEN_MONITOR_REMOTE_PROVER_TEST_INTERVAL`: Interval at which to test the remote provers services - `MIDEN_MONITOR_FAUCET_TEST_INTERVAL`: Interval at which to test the faucet services @@ -78,6 +80,7 @@ Starts the network monitoring service with the web dashboard. RPC status is alwa - Prover checks/tests: enabled when `--remote-prover-urls` (or `MIDEN_MONITOR_REMOTE_PROVER_URLS`) is provided - Faucet testing: enabled when `--faucet-url` (or `MIDEN_MONITOR_FAUCET_URL`) is provided - Network transaction service: enabled when `--disable-ntx-service=false` or unset (or `MIDEN_MONITOR_DISABLE_NTX_SERVICE=false` or unset) +- Note transport checks: enabled when `--note-transport-url` (or `MIDEN_MONITOR_NOTE_TRANSPORT_URL`) is provided ```bash # Start with default configuration (RPC only) @@ -205,6 +208,12 @@ The monitor application provides real-time status monitoring for the following M - Pending notes: How many transactions are queued/unprocessed - Last updated timestamp +### Note Transport +- **Service Health**: Checks the note transport service via the standard gRPC Health Checking Protocol +- **Metrics**: + - Service URL + - gRPC serving status (Serving, NotServing, Unknown) + ## User Interface The web dashboard provides a clean, responsive interface with the following features: diff --git a/bin/network-monitor/assets/index.js b/bin/network-monitor/assets/index.js index 6695d232e..e8340c449 100644 --- a/bin/network-monitor/assets/index.js +++ b/bin/network-monitor/assets/index.js @@ -411,6 +411,9 @@ function updateDisplay() { detailsHtml = `
${details.RpcStatus ? ` + ${details.RpcStatus.url ? ` +
URL: ${details.RpcStatus.url}${renderCopyButton(details.RpcStatus.url, 'URL')}
+ ` : ''}
Version: ${details.RpcStatus.version}
${details.RpcStatus.genesis_commitment ? `
@@ -471,31 +474,31 @@ function updateDisplay() { ` : ''} ` : ''} ${details.RemoteProverStatus ? ` -
- Prover Status (${details.RemoteProverStatus.url}): -
Version: ${details.RemoteProverStatus.version}
+
URL: ${details.RemoteProverStatus.url}${renderCopyButton(details.RemoteProverStatus.url, 'URL')}
+
Version: ${details.RemoteProverStatus.version}
+
Proof Type: ${details.RemoteProverStatus.supported_proof_type}
+ ${renderGrpcWebProbeSection(details.RemoteProverStatus.url)} + ${details.RemoteProverStatus.workers && details.RemoteProverStatus.workers.length > 0 ? `
- Supported Proof Type: ${details.RemoteProverStatus.supported_proof_type} + Workers (${details.RemoteProverStatus.workers.length}): + ${details.RemoteProverStatus.workers.map(worker => ` +
+ ${worker.name} - + ${worker.version} - + ${worker.status} +
+ `).join('')}
- ${details.RemoteProverStatus.workers && details.RemoteProverStatus.workers.length > 0 ? ` -
- Workers (${details.RemoteProverStatus.workers.length}): - ${details.RemoteProverStatus.workers.map(worker => ` -
- ${worker.name} - - ${worker.version} - - ${worker.status} -
- `).join('')} -
- ` : ''} - ${renderGrpcWebProbeSection(details.RemoteProverStatus.url)} -
+ ` : ''} ` : ''} ${details.FaucetTest ? `
Faucet:
+
+ URL: + ${details.FaucetTest.url}${renderCopyButton(details.FaucetTest.url, 'URL')} +
Success Rate: ${formatSuccessRate(details.FaucetTest.success_count, details.FaucetTest.failure_count)} @@ -616,6 +619,21 @@ function updateDisplay() {
` : ''} + ${details.NoteTransportStatus ? ` +
+ Note Transport: +
+
+ URL: + ${details.NoteTransportStatus.url}${renderCopyButton(details.NoteTransportStatus.url, 'URL')} +
+
+ Serving Status: + ${details.NoteTransportStatus.serving_status} +
+
+
+ ` : ''} ${service.testDetails ? `
Proof Generation Testing (${service.testDetails.proof_type}): diff --git a/bin/network-monitor/src/commands/start.rs b/bin/network-monitor/src/commands/start.rs index 4262db445..75cd8f6e8 100644 --- a/bin/network-monitor/src/commands/start.rs +++ b/bin/network-monitor/src/commands/start.rs @@ -48,6 +48,13 @@ pub async fn start_monitor(config: MonitorConfig) -> Result<()> { None }; + // Initialize the note transport status checker task. + let note_transport_rx = if config.note_transport_url.is_some() { + Some(tasks.spawn_note_transport_checker(&config).await?) + } else { + None + }; + // Initialize the prover checkers & tests tasks, only if URLs were provided. let prover_rxs = if config.remote_prover_urls.is_empty() { debug!(target: COMPONENT, "No remote prover URLs configured, skipping prover tasks"); @@ -85,6 +92,7 @@ pub async fn start_monitor(config: MonitorConfig) -> Result<()> { ntx_increment: ntx_increment_rx, ntx_tracking: ntx_tracking_rx, explorer: explorer_rx, + note_transport: note_transport_rx, }; tasks.spawn_http_server(server_state, &config); diff --git a/bin/network-monitor/src/config.rs b/bin/network-monitor/src/config.rs index 7443b759f..8ae07fa27 100644 --- a/bin/network-monitor/src/config.rs +++ b/bin/network-monitor/src/config.rs @@ -166,6 +166,14 @@ pub struct MonitorConfig { )] pub explorer_url: Option, + /// The URL of the note transport service. + #[arg( + long = "note-transport-url", + env = "MIDEN_MONITOR_NOTE_TRANSPORT_URL", + help = "The URL of the note transport service" + )] + pub note_transport_url: Option, + /// Maximum time without a chain tip update before marking RPC as unhealthy. /// /// If the chain tip does not increment within this duration, the RPC service will be diff --git a/bin/network-monitor/src/faucet.rs b/bin/network-monitor/src/faucet.rs index bfef177a0..370d7bb10 100644 --- a/bin/network-monitor/src/faucet.rs +++ b/bin/network-monitor/src/faucet.rs @@ -34,6 +34,7 @@ const MINT_AMOUNT: u64 = 1_000_000; // 1 token with 6 decimals /// Details of a faucet test. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FaucetTestDetails { + pub url: String, pub test_duration_ms: u64, pub success_count: u64, pub failure_count: u64, @@ -129,6 +130,7 @@ pub async fn run_faucet_test_task( let test_duration_ms = start_time.elapsed().as_millis() as u64; let test_details = FaucetTestDetails { + url: faucet_url.to_string(), test_duration_ms, success_count, failure_count, diff --git a/bin/network-monitor/src/frontend.rs b/bin/network-monitor/src/frontend.rs index 035db669c..ecc08d26f 100644 --- a/bin/network-monitor/src/frontend.rs +++ b/bin/network-monitor/src/frontend.rs @@ -26,6 +26,7 @@ pub struct ServerState { pub ntx_increment: Option>, pub ntx_tracking: Option>, pub explorer: Option>, + pub note_transport: Option>, } /// Runs the frontend server. @@ -77,9 +78,9 @@ async fn get_status( // Collect RPC status services.push(server_state.rpc.borrow().clone()); - // Collect explorer status if available - if let Some(explorer_rx) = &server_state.explorer { - services.push(explorer_rx.borrow().clone()); + // Collect faucet status if available + if let Some(faucet_rx) = &server_state.faucet { + services.push(faucet_rx.borrow().clone()); } // Collect all remote prover statuses @@ -88,9 +89,9 @@ async fn get_status( services.push(prover_test_rx.borrow().clone()); } - // Collect faucet status if available - if let Some(faucet_rx) = &server_state.faucet { - services.push(faucet_rx.borrow().clone()); + // Collect explorer status if available + if let Some(explorer_rx) = &server_state.explorer { + services.push(explorer_rx.borrow().clone()); } // Collect counter increment status if enabled @@ -103,6 +104,11 @@ async fn get_status( services.push(ntx_tracking_rx.borrow().clone()); } + // Collect note transport status if available + if let Some(note_transport_rx) = &server_state.note_transport { + services.push(note_transport_rx.borrow().clone()); + } + let network_status = NetworkStatus { services, last_updated: current_time }; axum::response::Json(network_status) diff --git a/bin/network-monitor/src/main.rs b/bin/network-monitor/src/main.rs index ed0f08cba..80244a47a 100644 --- a/bin/network-monitor/src/main.rs +++ b/bin/network-monitor/src/main.rs @@ -16,6 +16,7 @@ pub mod explorer; pub mod faucet; pub mod frontend; mod monitor; +pub mod note_transport; pub mod remote_prover; pub mod status; diff --git a/bin/network-monitor/src/monitor/tasks.rs b/bin/network-monitor/src/monitor/tasks.rs index c5b773dc3..57f5ce395 100644 --- a/bin/network-monitor/src/monitor/tasks.rs +++ b/bin/network-monitor/src/monitor/tasks.rs @@ -23,6 +23,7 @@ use crate::deploy::ensure_accounts_exist; use crate::explorer::{initial_explorer_status, run_explorer_status_task}; use crate::faucet::run_faucet_test_task; use crate::frontend::{ServerState, serve}; +use crate::note_transport::{initial_note_transport_status, run_note_transport_status_task}; use crate::remote_prover::{ProofType, generate_prover_test_payload, run_remote_prover_test_task}; use crate::status::{ ServiceStatus, @@ -142,6 +143,39 @@ impl Tasks { Ok(explorer_status_rx) } + /// Spawn the note transport status checker task. + #[instrument(target = COMPONENT, name = "tasks.spawn-note-transport-checker", skip_all)] + pub async fn spawn_note_transport_checker( + &mut self, + config: &MonitorConfig, + ) -> Result> { + let note_transport_url = + config.note_transport_url.clone().expect("Note transport URL exists"); + let name = "Note Transport".to_string(); + let status_check_interval = config.status_check_interval; + let request_timeout = config.request_timeout; + let (tx, rx) = watch::channel(initial_note_transport_status()); + + let id = self + .handles + .spawn(async move { + run_note_transport_status_task( + note_transport_url, + name, + tx, + status_check_interval, + request_timeout, + ) + .await; + }) + .id(); + self.names.insert(id, "note-transport-checker".to_string()); + + println!("Spawned note transport status checker task"); + + Ok(rx) + } + /// Spawn prover status and test tasks for all configured provers. #[instrument( parent = None, @@ -287,6 +321,7 @@ impl Tasks { last_checked: current_time, error: None, details: crate::status::ServiceDetails::FaucetTest(crate::faucet::FaucetTestDetails { + url: config.faucet_url.as_ref().expect("faucet URL exists").to_string(), test_duration_ms: 0, success_count: 0, failure_count: 0, diff --git a/bin/network-monitor/src/note_transport.rs b/bin/network-monitor/src/note_transport.rs new file mode 100644 index 000000000..4556f8bf1 --- /dev/null +++ b/bin/network-monitor/src/note_transport.rs @@ -0,0 +1,119 @@ +// NOTE TRANSPORT STATUS CHECKER +// ================================================================================================ + +use std::time::Duration; + +use tokio::sync::watch; +use tokio::time::MissedTickBehavior; +use tonic::transport::{Channel, ClientTlsConfig}; +use tonic_health::pb::health_client::HealthClient; +use tonic_health::pb::{HealthCheckRequest, health_check_response}; +use tracing::{info, instrument}; +use url::Url; + +use crate::status::{NoteTransportStatusDetails, ServiceDetails, ServiceStatus, Status}; +use crate::{COMPONENT, current_unix_timestamp_secs}; + +/// Creates a `tonic` channel for the given URL, enabling TLS for `https` schemes. +fn create_channel(url: &Url, timeout: Duration) -> Result { + let mut endpoint = Channel::from_shared(url.to_string()).expect("valid URL").timeout(timeout); + + if url.scheme() == "https" { + endpoint = endpoint.tls_config(ClientTlsConfig::new().with_native_roots())?; + } + + Ok(endpoint.connect_lazy()) +} + +/// Runs a task that continuously checks note transport health and updates a watch channel. +pub async fn run_note_transport_status_task( + url: Url, + name: String, + status_sender: watch::Sender, + status_check_interval: Duration, + request_timeout: Duration, +) { + let channel = create_channel(&url, request_timeout).expect("failed to create channel"); + let mut health_client = HealthClient::new(channel); + + let mut interval = tokio::time::interval(status_check_interval); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + interval.tick().await; + + let current_time = current_unix_timestamp_secs(); + + let status = check_note_transport_status( + &mut health_client, + url.to_string(), + name.clone(), + current_time, + ) + .await; + + if status_sender.send(status).is_err() { + info!("No receivers for note transport status updates, shutting down"); + return; + } + } +} + +/// Checks the health of the note transport service via the standard gRPC Health Checking Protocol. +#[instrument( + target = COMPONENT, + name = "check-status.note-transport", + skip_all, + ret(level = "info") +)] +pub(crate) async fn check_note_transport_status( + health_client: &mut HealthClient, + url: String, + name: String, + current_time: u64, +) -> ServiceStatus { + let request = HealthCheckRequest { service: String::new() }; + + match health_client.check(request).await { + Ok(response) => { + let serving_status = response.into_inner().status(); + let is_serving = serving_status == health_check_response::ServingStatus::Serving; + + let status = if is_serving { Status::Healthy } else { Status::Unhealthy }; + let serving_status_str = format!("{serving_status:?}"); + + ServiceStatus { + name, + status, + last_checked: current_time, + error: None, + details: ServiceDetails::NoteTransportStatus(NoteTransportStatusDetails { + url, + serving_status: serving_status_str, + }), + } + }, + Err(e) => unhealthy(&name, current_time, &e), + } +} + +/// Returns an unhealthy service status. +fn unhealthy(name: &str, current_time: u64, err: &impl ToString) -> ServiceStatus { + ServiceStatus { + name: name.to_owned(), + status: Status::Unhealthy, + last_checked: current_time, + error: Some(err.to_string()), + details: ServiceDetails::Error, + } +} + +pub(crate) fn initial_note_transport_status() -> ServiceStatus { + ServiceStatus { + name: "Note Transport".to_string(), + status: Status::Unknown, + last_checked: current_unix_timestamp_secs(), + error: None, + details: ServiceDetails::NoteTransportStatus(NoteTransportStatusDetails::default()), + } +} diff --git a/bin/network-monitor/src/status.rs b/bin/network-monitor/src/status.rs index 759fb0ed9..419b837b0 100644 --- a/bin/network-monitor/src/status.rs +++ b/bin/network-monitor/src/status.rs @@ -170,6 +170,13 @@ pub struct ExplorerStatusDetails { pub proof_commitment: String, } +/// Details of the note transport service. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct NoteTransportStatusDetails { + pub url: String, + pub serving_status: String, +} + /// Details of a service. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ServiceDetails { @@ -180,6 +187,7 @@ pub enum ServiceDetails { NtxIncrement(IncrementDetails), NtxTracking(CounterTrackingDetails), ExplorerStatus(ExplorerStatusDetails), + NoteTransportStatus(NoteTransportStatusDetails), Error, }