diff --git a/integration-tests/Cargo.lock b/integration-tests/Cargo.lock index 9dd8e03f5..d7659ed18 100644 --- a/integration-tests/Cargo.lock +++ b/integration-tests/Cargo.lock @@ -1372,6 +1372,7 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] @@ -1380,13 +1381,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1544,6 +1553,7 @@ dependencies = [ "minreq", "once_cell", "pool_sv2", + "serde_json", "stratum-apps", "tokio", "tokio-util", @@ -1552,6 +1562,22 @@ dependencies = [ "translator_sv2", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1611,6 +1637,16 @@ dependencies = [ "binary_sv2 5.0.1", ] +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -2266,6 +2302,38 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -2771,6 +2839,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3015,6 +3086,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3110,6 +3199,7 @@ dependencies = [ "dashmap", "hex", "hotpath", + "reqwest", "serde", "serde_json", "stratum-apps", @@ -3118,6 +3208,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -3270,12 +3366,90 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 574bb4e80..7ab4caa70 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -25,6 +25,7 @@ tokio-util = { version = "0.7", default-features = false } tracing = { version = "0.1.41", default-features = false } tracing-subscriber = { version = "0.3.19", default-features = false } hex = "0.4.3" +serde_json = "1" [lib] path = "lib/mod.rs" diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs index c70395de5..cbe2d5060 100644 --- a/integration-tests/lib/mod.rs +++ b/integration-tests/lib/mod.rs @@ -118,6 +118,23 @@ pub async fn start_pool( supported_extensions: Vec, required_extensions: Vec, enable_monitoring: bool, +) -> (PoolSv2, SocketAddr, Option) { + start_pool_with_network( + template_provider_config, + supported_extensions, + required_extensions, + enable_monitoring, + None, + ) + .await +} + +pub async fn start_pool_with_network( + template_provider_config: TemplateProviderType, + supported_extensions: Vec, + required_extensions: Vec, + enable_monitoring: bool, + network: Option, ) -> (PoolSv2, SocketAddr, Option) { use pool_sv2::config::PoolConfig; let listening_address = get_available_address(); @@ -160,7 +177,8 @@ pub async fn start_pool( monitoring_address, monitoring_cache_refresh_secs, None, // no JDS - ); + ) + .with_network(network); let pool = PoolSv2::new(config); let pool_clone = pool.clone(); tokio::spawn(async move { @@ -334,6 +352,27 @@ pub async fn start_sv2_translator( required_extensions: Vec, job_keepalive_interval_secs: Option, enable_monitoring: bool, +) -> (TranslatorSv2, SocketAddr, Option) { + start_sv2_translator_with_upstream_monitoring( + upstreams, + aggregate_channels, + supported_extensions, + required_extensions, + job_keepalive_interval_secs, + enable_monitoring, + None, + ) + .await +} + +pub async fn start_sv2_translator_with_upstream_monitoring( + upstreams: &[SocketAddr], + aggregate_channels: bool, + supported_extensions: Vec, + required_extensions: Vec, + job_keepalive_interval_secs: Option, + enable_monitoring: bool, + upstream_monitoring_url: Option, ) -> (TranslatorSv2, SocketAddr, Option) { let job_keepalive_interval_secs = job_keepalive_interval_secs.unwrap_or(60); let upstreams = upstreams @@ -391,7 +430,8 @@ pub async fn start_sv2_translator( required_extensions, monitoring_address, monitoring_cache_refresh_secs, - ); + ) + .with_upstream_monitoring_url(upstream_monitoring_url); let translator_v2 = translator_sv2::TranslatorSv2::new(config); let clone_translator_v2 = translator_v2.clone(); tokio::spawn(async move { diff --git a/integration-tests/tests/monitoring_integration.rs b/integration-tests/tests/monitoring_integration.rs index d362168ae..612875158 100644 --- a/integration-tests/tests/monitoring_integration.rs +++ b/integration-tests/tests/monitoring_integration.rs @@ -235,3 +235,61 @@ async fn block_found_detected_in_pool_metrics() { assert_uptime(&pool_metrics); assert_metric_eq(&pool_metrics, "sv2_clients_total", 1.0); } + +// --------------------------------------------------------------------------- +// 5. Pool exposes network in GlobalInfo; translator propagates it by polling. +// --------------------------------------------------------------------------- +#[tokio::test] +async fn global_info_exposes_network() { + start_tracing(); + let (_tp, tp_addr) = start_template_provider(None, DifficultyLevel::Low); + let (_pool, pool_addr, pool_monitoring) = start_pool_with_network( + sv2_tp_config(tp_addr), + vec![], + vec![], + true, + Some("regtest".to_string()), + ) + .await; + let pool_mon = pool_monitoring.expect("pool monitoring should be enabled"); + + // Pool global endpoint must report network = "regtest" + let body = fetch_api(pool_mon, "/api/v1/global").await; + let json: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!( + json["network"], "regtest", + "pool global info should expose network" + ); + + // Start translator with upstream_monitoring_url pointing at pool's monitoring server + let upstream_monitoring_url = format!("http://{}", pool_mon); + let (_tproxy, _tproxy_addr, tproxy_monitoring) = start_sv2_translator_with_upstream_monitoring( + &[pool_addr], + false, + vec![], + vec![], + None, + true, + Some(upstream_monitoring_url), + ) + .await; + let tproxy_mon = tproxy_monitoring.expect("tproxy monitoring should be enabled"); + + // Translator polls pool every 60s; first poll should happen quickly at startup. + // Wait up to 10 seconds for network to propagate. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + loop { + let body = fetch_api(tproxy_mon, "/api/v1/global").await; + let json: serde_json::Value = serde_json::from_str(&body).unwrap(); + if json["network"] == "regtest" { + break; + } + if std::time::Instant::now() >= deadline { + panic!( + "tproxy global info did not propagate network within timeout; got: {}", + json + ); + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } +} diff --git a/miner-apps/Cargo.lock b/miner-apps/Cargo.lock index f0da4ac03..62406f24e 100644 --- a/miner-apps/Cargo.lock +++ b/miner-apps/Cargo.lock @@ -1327,6 +1327,7 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] @@ -1335,13 +1336,21 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1493,6 +1502,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -2130,6 +2155,38 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ron" version = "0.8.1" @@ -2535,6 +2592,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2781,6 +2841,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2876,6 +2954,7 @@ dependencies = [ "dashmap", "hex", "hotpath", + "reqwest", "serde", "serde_json", "sha2 0.10.9", @@ -2885,6 +2964,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -3037,6 +3122,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -3056,6 +3150,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.114" diff --git a/miner-apps/translator/Cargo.toml b/miner-apps/translator/Cargo.toml index 49326f7a3..7c7a5b4f8 100644 --- a/miner-apps/translator/Cargo.toml +++ b/miner-apps/translator/Cargo.toml @@ -32,6 +32,7 @@ clap = { version = "4.5.39", features = ["derive"] } hex = "0.4.3" hotpath = "0.9" dashmap = "6.1.0" +reqwest = { version = "0.12", default-features = false, features = ["json"] } [features] default = ["monitoring"] diff --git a/miner-apps/translator/src/lib/config.rs b/miner-apps/translator/src/lib/config.rs index 5cf04c790..af0d2c85a 100644 --- a/miner-apps/translator/src/lib/config.rs +++ b/miner-apps/translator/src/lib/config.rs @@ -59,6 +59,13 @@ pub struct TranslatorConfig { monitoring_address: Option, #[serde(default)] monitoring_cache_refresh_secs: Option, + /// URL of the upstream pool's monitoring server. + /// When set, the translator polls `GET /api/v1/global` every + /// 60 seconds and propagates the pool's reported `network` value into its own + /// `GET /api/v1/global` response. + /// Example: `"http://127.0.0.1:9108"` + #[serde(default)] + upstream_monitoring_url: Option, } #[derive(Debug, Deserialize, Clone)] @@ -116,6 +123,7 @@ impl TranslatorConfig { log_file: None, monitoring_address, monitoring_cache_refresh_secs, + upstream_monitoring_url: None, } } @@ -129,6 +137,17 @@ impl TranslatorConfig { self.monitoring_cache_refresh_secs } + /// Returns the upstream pool monitoring URL (if configured). + pub fn upstream_monitoring_url(&self) -> Option { + self.upstream_monitoring_url.clone() + } + + /// Set the upstream pool monitoring URL (builder style). + pub fn with_upstream_monitoring_url(mut self, url: Option) -> Self { + self.upstream_monitoring_url = url; + self + } + pub fn set_log_dir(&mut self, log_dir: Option) { if let Some(dir) = log_dir { self.log_file = Some(dir); diff --git a/miner-apps/translator/src/lib/mod.rs b/miner-apps/translator/src/lib/mod.rs index aa65bf50d..13f6fbceb 100644 --- a/miner-apps/translator/src/lib/mod.rs +++ b/miner-apps/translator/src/lib/mod.rs @@ -195,6 +195,18 @@ impl TranslatorSv2 { .with_sv1_monitoring(sv1_server.clone()) // SV1 client connections .expect("Failed to add SV1 monitoring"); + // Obtain a handle to update network at runtime (before run() consumes the server) + let network_handle = monitoring_server.network_handle(); + + // Spawn background task to poll pool's /api/v1/global for network identity + if let Some(pool_mon_url) = self.config.upstream_monitoring_url() { + let handle = network_handle.clone(); + let poll_token = cancellation_token.clone(); + task_manager.spawn(async move { + poll_network_from_pool(pool_mon_url, handle, poll_token).await; + }); + } + // Create shutdown signal using cancellation token let cancellation_token_clone = cancellation_token.clone(); let fallback_coordinator_token = fallback_coordinator.token(); @@ -339,6 +351,16 @@ impl TranslatorSv2 { .with_sv1_monitoring(sv1_server.clone()) .expect("Failed to add SV1 monitoring"); + let network_handle = monitoring_server.network_handle(); + + if let Some(pool_mon_url) = self.config.upstream_monitoring_url() { + let handle = network_handle.clone(); + let poll_token = cancellation_token.clone(); + task_manager.spawn(async move { + poll_network_from_pool(pool_mon_url, handle, poll_token).await; + }); + } + let cancellation_token_clone = cancellation_token.clone(); let fallback_coordinator_token = fallback_coordinator.token(); let shutdown_signal = async move { @@ -619,3 +641,31 @@ impl Drop for TranslatorSv2 { self.cancellation_token.cancel(); } } + +/// Poll the pool's `/api/v1/global` endpoint every 60 seconds and write the reported +/// `network` value into `network_handle`. Retries immediately on error. Exits cleanly +/// when `cancellation_token` is cancelled. +#[cfg(feature = "monitoring")] +async fn poll_network_from_pool( + pool_mon_url: String, + network_handle: std::sync::Arc>>, + cancellation_token: tokio_util::sync::CancellationToken, +) { + let url = format!("{}/api/v1/global", pool_mon_url); + loop { + match reqwest::get(&url).await { + Ok(resp) => match resp.json::().await { + Ok(json) => { + let network = json["network"].as_str().map(|s| s.to_string()); + *network_handle.write().unwrap() = network; + } + Err(e) => warn!("Failed to parse pool global info: {}", e), + }, + Err(e) => warn!("Failed to fetch pool global info from {}: {}", url, e), + } + tokio::select! { + _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => {} + _ = cancellation_token.cancelled() => break, + } + } +} diff --git a/pool-apps/pool/src/lib/config.rs b/pool-apps/pool/src/lib/config.rs index c80c1bd37..a032f1409 100644 --- a/pool-apps/pool/src/lib/config.rs +++ b/pool-apps/pool/src/lib/config.rs @@ -50,6 +50,12 @@ pub struct PoolConfig { jds: Option, #[serde(default)] monitoring_cache_refresh_secs: Option, + /// Bitcoin network this pool is operating on. + /// Exposed via `GET /api/v1/global` in the monitoring REST API. + /// Values follow bitcoin-cli convention: `"main"`, `"test"`, `"testnet4"`, `"regtest"`, + /// `"signet"`. + #[serde(default)] + network: Option, } impl PoolConfig { @@ -90,6 +96,7 @@ impl PoolConfig { monitoring_address, monitoring_cache_refresh_secs, jds, + network: None, } } @@ -186,6 +193,17 @@ impl PoolConfig { self.monitoring_cache_refresh_secs } + /// Returns the Bitcoin network name (e.g. `"regtest"`, `"main"`). + pub fn network(&self) -> Option { + self.network.clone() + } + + /// Set the Bitcoin network (builder style). + pub fn with_network(mut self, network: Option) -> Self { + self.network = network; + self + } + /// Builds a complete [`JDSConfig`] from the partial `[jds]` TOML section /// plus shared fields inherited from Pool config. /// diff --git a/pool-apps/pool/src/lib/mod.rs b/pool-apps/pool/src/lib/mod.rs index ac9688b79..e86ea7369 100644 --- a/pool-apps/pool/src/lib/mod.rs +++ b/pool-apps/pool/src/lib/mod.rs @@ -182,7 +182,8 @@ impl PoolSv2 { self.config.monitoring_cache_refresh_secs().unwrap_or(15), ), ) - .expect("Failed to initialize monitoring server"); + .expect("Failed to initialize monitoring server") + .with_network(self.config.network()); let cancellation_token_clone = cancellation_token.clone(); let shutdown_signal = async move { diff --git a/stratum-apps/src/monitoring/http_server.rs b/stratum-apps/src/monitoring/http_server.rs index 2e65bc097..71eba16b5 100644 --- a/stratum-apps/src/monitoring/http_server.rs +++ b/stratum-apps/src/monitoring/http_server.rs @@ -25,7 +25,7 @@ use serde::Deserialize; use std::{ future::Future, net::SocketAddr, - sync::Arc, + sync::{Arc, RwLock}, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tokio::net::TcpListener; @@ -88,6 +88,7 @@ struct ServerState { cache: Arc, start_time: u64, metrics: PrometheusMetrics, + network: Arc>>, } const DEFAULT_LIMIT: usize = 25; @@ -180,10 +181,31 @@ impl MonitoringServer { cache, start_time, metrics, + network: Arc::new(RwLock::new(None)), }, }) } + /// Set the Bitcoin network this application is operating on. + /// + /// Values follow bitcoin-cli convention: `"main"`, `"test"`, `"testnet4"`, `"regtest"`, + /// `"signet"`. The value is served as-is in the `network` field of `GET /api/v1/global`. + /// + /// This is optional — if not called, `network` will be `None` in the global response. + pub fn with_network(self, network: Option) -> Self { + *self.state.network.write().unwrap() = network; + self + } + + /// Return a handle to the shared network value. + /// + /// The returned `Arc>>` can be written to by a background task + /// after `run()` has consumed `self`. This allows applications (e.g. a translator proxy) + /// to update the network field at runtime once they learn it from an upstream source. + pub fn network_handle(&self) -> Arc>> { + self.state.network.clone() + } + /// Add Sv1 clients monitoring (optional, for Translator Proxy only) /// /// This must be called before `run()` if you want SV1 monitoring. @@ -423,6 +445,7 @@ async fn handle_global(State(state): State) -> Json { sv2_clients: snapshot.sv2_clients_summary, sv1_clients: snapshot.sv1_clients_summary, uptime_secs, + network: state.network.read().unwrap().clone(), }) } @@ -1064,6 +1087,15 @@ mod tests { server: Option>, clients: Option>, sv1: Option>, + ) -> Router { + build_test_app_with_options(server, clients, sv1, None) + } + + fn build_test_app_with_options( + server: Option>, + clients: Option>, + sv1: Option>, + network: Option, ) -> Router { let cache = Arc::new(SnapshotCache::new(Duration::from_secs(60), server, clients)); @@ -1094,6 +1126,7 @@ mod tests { cache, start_time, metrics, + network: Arc::new(RwLock::new(network)), }; let api_v1 = Router::new() @@ -1241,6 +1274,25 @@ mod tests { assert!(json["server"].is_null()); assert!(json["sv2_clients"].is_null()); assert!(json["uptime_secs"].as_u64().is_some()); + assert!(json["network"].is_null()); + } + + #[tokio::test] + async fn global_endpoint_network_field() { + // Without network set, field should be null + let app = build_test_app(None, None, None); + let response = app.oneshot(make_request("/api/v1/global")).await.unwrap(); + let body = get_body(response).await; + let json: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert!(json["network"].is_null()); + + // With network set, field should reflect the configured value + let app = build_test_app_with_options(None, None, None, Some("regtest".to_string())); + let response = app.oneshot(make_request("/api/v1/global")).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = get_body(response).await; + let json: serde_json::Value = serde_json::from_str(&body).unwrap(); + assert_eq!(json["network"], "regtest"); } #[tokio::test] diff --git a/stratum-apps/src/monitoring/mod.rs b/stratum-apps/src/monitoring/mod.rs index 435639e05..4c69c77dd 100644 --- a/stratum-apps/src/monitoring/mod.rs +++ b/stratum-apps/src/monitoring/mod.rs @@ -49,4 +49,9 @@ pub struct GlobalInfo { pub sv1_clients: Option, /// Uptime in seconds since the application started pub uptime_secs: u64, + /// Bitcoin network this application is operating on. + /// `None` if the application has not been configured with a network. + /// Values follow bitcoin-cli convention: `"main"`, `"test"`, `"testnet4"`, `"regtest"`, + /// `"signet"`. + pub network: Option, }