-
Notifications
You must be signed in to change notification settings - Fork 980
feat: expose stratum mining stats via Owner API #3897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,9 +22,12 @@ use crate::handlers::server_api::StatusHandler; | |
| use crate::p2p::types::PeerInfoDisplay; | ||
| use crate::p2p::{self, PeerData}; | ||
| use crate::rest::*; | ||
| use crate::types::Status; | ||
| use crate::types::{MiningStatus, Status}; | ||
| use std::net::SocketAddr; | ||
| use std::sync::Weak; | ||
| use std::sync::{Arc, Weak}; | ||
|
|
||
| /// Callback that returns a snapshot of stratum mining stats for the Owner API. | ||
| pub type MiningStatsProvider = Arc<dyn Fn() -> MiningStatus + Send + Sync>; | ||
|
|
||
| /// Main interface into all node API functions. | ||
| /// Node APIs are split into two separate blocks of functionality | ||
|
|
@@ -37,6 +40,8 @@ pub struct Owner { | |
| pub chain: Weak<Chain>, | ||
| pub peers: Weak<p2p::Peers>, | ||
| pub sync_state: Weak<SyncState>, | ||
| /// Optional provider of live stratum mining stats (set by the server process). | ||
| pub mining_stats: Option<MiningStatsProvider>, | ||
| } | ||
|
|
||
| impl Owner { | ||
|
|
@@ -58,6 +63,22 @@ impl Owner { | |
| chain, | ||
| peers, | ||
| sync_state, | ||
| mining_stats: None, | ||
| } | ||
| } | ||
|
|
||
| /// Create a new Owner API instance with a mining stats provider. | ||
| pub fn with_mining_stats( | ||
| chain: Weak<Chain>, | ||
| peers: Weak<p2p::Peers>, | ||
| sync_state: Weak<SyncState>, | ||
| mining_stats: Option<MiningStatsProvider>, | ||
| ) -> Self { | ||
| Owner { | ||
| chain, | ||
| peers, | ||
| sync_state, | ||
| mining_stats, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -78,6 +99,25 @@ impl Owner { | |
| status_handler.get_status() | ||
| } | ||
|
|
||
| /// Returns current stratum mining statistics (enabled/running, workers, difficulty, etc.). | ||
| /// Useful when running headless without the TUI mining tab. | ||
| /// | ||
| /// When stratum is disabled or no stats provider is wired up, returns a default | ||
| /// (disabled) [`MiningStatus`](types/struct.MiningStatus.html). | ||
| /// | ||
| /// # Returns | ||
| /// * Result Containing: | ||
| /// * A [`MiningStatus`](types/struct.MiningStatus.html) | ||
| /// * or [`Error`](struct.Error.html) if an error is encountered. | ||
| /// | ||
|
|
||
| pub fn get_mining_status(&self) -> Result<MiningStatus, Error> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we trim the empty doc line and shorten the returns section to match the neighboring methods? |
||
| match &self.mining_stats { | ||
| Some(provider) => Ok(provider()), | ||
| None => Ok(MiningStatus::default()), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A disabled server already has a provider returning is_enabled, false. Could a missing provider return an internal error instead of silently looking disabled? |
||
| } | ||
| } | ||
|
|
||
| /// Trigger a validation of the chain state. | ||
| /// | ||
| /// # Arguments | ||
|
|
@@ -201,3 +241,47 @@ impl Owner { | |
| peer_handler.unban_peer(addr) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod test { | ||
| use super::*; | ||
| use crate::types::{MiningStatus, WorkerInfo}; | ||
| use std::sync::Arc; | ||
|
|
||
| #[test] | ||
| fn get_mining_status_without_provider_returns_default() { | ||
| let owner = Owner::new(Weak::new(), Weak::new(), Weak::new()); | ||
| let status = owner.get_mining_status().unwrap(); | ||
| assert_eq!(status, MiningStatus::default()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn get_mining_status_with_provider() { | ||
| let expected = MiningStatus { | ||
| is_enabled: true, | ||
| is_running: true, | ||
| num_workers: 2, | ||
| block_height: 50, | ||
| network_difficulty: 10, | ||
| edge_bits: 29, | ||
| blocks_found: 1, | ||
| network_hashrate: 0.5, | ||
| minimum_share_difficulty: 1, | ||
| worker_stats: vec![WorkerInfo { | ||
| id: "w0".into(), | ||
| is_connected: true, | ||
| last_seen: 100, | ||
| initial_block_height: 40, | ||
| pow_difficulty: 1, | ||
| num_accepted: 3, | ||
| num_rejected: 0, | ||
| num_stale: 0, | ||
| num_blocks_found: 0, | ||
| }], | ||
| }; | ||
| let expected_clone = expected.clone(); | ||
| let provider: MiningStatsProvider = Arc::new(move || expected_clone.clone()); | ||
| let owner = Owner::with_mining_stats(Weak::new(), Weak::new(), Weak::new(), Some(provider)); | ||
| assert_eq!(owner.get_mining_status().unwrap(), expected); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -104,6 +104,72 @@ impl Status { | |
| } | ||
| } | ||
|
|
||
| /// Stratum worker statistics exposed via the Owner mining API. | ||
| #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | ||
| pub struct WorkerInfo { | ||
| /// Unique ID for this worker | ||
| pub id: String, | ||
| /// Whether the stratum worker is currently connected | ||
| pub is_connected: bool, | ||
| /// Unix timestamp (seconds) of most recent communication with this worker | ||
| pub last_seen: u64, | ||
| /// Block height the worker started mining at | ||
| pub initial_block_height: u64, | ||
| /// PoW difficulty this worker is using | ||
| pub pow_difficulty: u64, | ||
| /// Number of valid shares submitted | ||
| pub num_accepted: u64, | ||
| /// Number of invalid shares submitted | ||
| pub num_rejected: u64, | ||
| /// Number of shares submitted too late | ||
| pub num_stale: u64, | ||
| /// Number of valid blocks found by this worker | ||
| pub num_blocks_found: u64, | ||
| } | ||
|
|
||
| /// Mining / stratum status available via the Owner API (and `grin client miningstatus`). | ||
| /// Mirrors the stats shown on the TUI mining tab so headless operators can query them. | ||
| #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] | ||
| pub struct MiningStatus { | ||
| /// Whether the stratum server is enabled in config | ||
| pub is_enabled: bool, | ||
| /// Whether the stratum server is currently running | ||
| pub is_running: bool, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The source flag is set on startup but never cleared on listener failure or shutdown. Is “currently running” too strong for the API contract? |
||
| /// Number of currently connected workers | ||
| pub num_workers: usize, | ||
| /// Block height currently being mined | ||
| pub block_height: u64, | ||
| /// Current network difficulty | ||
| pub network_difficulty: u64, | ||
| /// Edge bits (cuckoo size) for the current network target | ||
| pub edge_bits: u16, | ||
| /// Total blocks found by all workers | ||
| pub blocks_found: u16, | ||
| /// Estimated network hashrate for the current edge_bits | ||
| pub network_hashrate: f64, | ||
| /// Minimum share difficulty requested from miners | ||
| pub minimum_share_difficulty: u64, | ||
| /// Per-worker status | ||
| pub worker_stats: Vec<WorkerInfo>, | ||
| } | ||
|
|
||
| impl Default for MiningStatus { | ||
| fn default() -> MiningStatus { | ||
| MiningStatus { | ||
| is_enabled: false, | ||
| is_running: false, | ||
| num_workers: 0, | ||
| block_height: 0, | ||
| network_difficulty: 0, | ||
| edge_bits: 32, | ||
| blocks_found: 0, | ||
| network_hashrate: 0.0, | ||
| minimum_share_difficulty: 1, | ||
| worker_stats: Vec::new(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// TxHashSet | ||
| #[derive(Serialize, Deserialize, Debug, Clone)] | ||
| pub struct TxHashSet { | ||
|
|
@@ -781,4 +847,45 @@ mod test { | |
| let serialized = serde_json::to_string(&deserialized).unwrap(); | ||
| assert_eq!(serialized, hex_commit); | ||
| } | ||
|
|
||
| #[test] | ||
| fn mining_status_default_is_disabled() { | ||
| let status = MiningStatus::default(); | ||
| assert!(!status.is_enabled); | ||
| assert!(!status.is_running); | ||
| assert_eq!(status.num_workers, 0); | ||
| assert!(status.worker_stats.is_empty()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn serialize_mining_status_roundtrip() { | ||
| let status = MiningStatus { | ||
| is_enabled: true, | ||
| is_running: true, | ||
| num_workers: 1, | ||
| block_height: 1000, | ||
| network_difficulty: 42, | ||
| edge_bits: 29, | ||
| blocks_found: 3, | ||
| network_hashrate: 1.5, | ||
| minimum_share_difficulty: 1, | ||
| worker_stats: vec![WorkerInfo { | ||
| id: "0".into(), | ||
| is_connected: true, | ||
| last_seen: 1609459200, | ||
| initial_block_height: 990, | ||
| pow_difficulty: 1, | ||
| num_accepted: 10, | ||
| num_rejected: 0, | ||
| num_stale: 0, | ||
| num_blocks_found: 1, | ||
| }], | ||
| }; | ||
| let json = serde_json::to_string(&status).unwrap(); | ||
| let back: MiningStatus = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(back, status); | ||
| assert!(json.contains("\"is_enabled\":true")); | ||
| assert!(json.contains("\"num_workers\":1")); | ||
| assert!(json.contains("\"num_accepted\":10")); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node_apis is publicly re-exported, so this required argument breaks external callers. Could we preserve the existing wrapper and add a provider-aware variant?