Skip to content
Open
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
16 changes: 13 additions & 3 deletions api/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::auth::{
use crate::chain::{Chain, SyncState};
use crate::foreign::Foreign;
use crate::foreign_rpc::ForeignRpc;
use crate::owner::Owner;
use crate::owner::{MiningStatsProvider, Owner};
use crate::owner_rpc::OwnerRpc;
use crate::pool;
use crate::pool::{BlockChain, PoolAdapter};
Expand Down Expand Up @@ -62,6 +62,7 @@ pub fn node_apis<B, P>(
tls_config: Option<TLSConfig>,
api_chan: &'static mut (oneshot::Sender<()>, oneshot::Receiver<()>),
stop_state: Arc<StopState>,
mining_stats: Option<MiningStatsProvider>,

Copy link
Copy Markdown
Contributor

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?

) -> Result<(), Error>
where
B: BlockChain + 'static,
Expand All @@ -85,6 +86,7 @@ where
Arc::downgrade(&chain),
Arc::downgrade(&peers),
Arc::downgrade(&sync_state),
mining_stats,
);
router.add_route("/v2/owner", Arc::new(api_handler))?;

Expand Down Expand Up @@ -142,25 +144,33 @@ pub struct OwnerAPIHandlerV2 {
pub chain: Weak<Chain>,
pub peers: Weak<p2p::Peers>,
pub sync_state: Weak<SyncState>,
pub mining_stats: Option<MiningStatsProvider>,
}

impl OwnerAPIHandlerV2 {
/// Create a new owner API handler for GET methods
pub fn new(chain: Weak<Chain>, peers: Weak<p2p::Peers>, sync_state: Weak<SyncState>) -> Self {
pub fn new(
chain: Weak<Chain>,
peers: Weak<p2p::Peers>,
sync_state: Weak<SyncState>,
mining_stats: Option<MiningStatsProvider>,
) -> Self {
OwnerAPIHandlerV2 {
chain,
peers,
sync_state,
mining_stats,
}
}
}

impl crate::router::Handler for OwnerAPIHandlerV2 {
fn post(&self, req: Request<Incoming>) -> ResponseFuture {
let api = Owner::new(
let api = Owner::with_mining_stats(
self.chain.clone(),
self.peers.clone(),
self.sync_state.clone(),
self.mining_stats.clone(),
);

Box::pin(async move {
Expand Down
2 changes: 1 addition & 1 deletion api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub use crate::auth::{
pub use crate::foreign::Foreign;
pub use crate::foreign_rpc::ForeignRpc;
pub use crate::handlers::node_apis;
pub use crate::owner::Owner;
pub use crate::owner::{MiningStatsProvider, Owner};
pub use crate::owner_rpc::OwnerRpc;
pub use crate::rest::*;
pub use crate::router::*;
Expand Down
88 changes: 86 additions & 2 deletions api/src/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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,
}
}

Expand All @@ -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> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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()),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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);
}
}
62 changes: 61 additions & 1 deletion api/src/owner_rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::owner::Owner;
use crate::p2p::types::PeerInfoDisplay;
use crate::p2p::PeerData;
use crate::rest::Error;
use crate::types::Status;
use crate::types::{MiningStatus, Status};
use std::net::SocketAddr;

/// Public definition used to generate Node jsonrpc api.
Expand Down Expand Up @@ -73,6 +73,62 @@ pub trait OwnerRpc: Sync + Send {
*/
fn get_status(&self) -> Result<Status, Error>;

/**
Networked version of [Owner::get_mining_status](struct.Owner.html#method.get_mining_status).

Returns the same mining / stratum information shown on the TUI mining tab so
headless operators can query connected workers and share stats.

# Json rpc example

```
# grin_api::doctest_helper_json_rpc_owner_assert_response!(
# r#"
{
"jsonrpc": "2.0",
"method": "get_mining_status",
"params": [],
"id": 1
}
# "#
# ,
# r#"
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"Ok": {
"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": [
{
"id": "0",
"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
}
]
}
}
}
# "#
# );
```
*/
fn get_mining_status(&self) -> Result<MiningStatus, Error>;

/**
Networked version of [Owner::validate_chain](struct.Owner.html#method.validate_chain).

Expand Down Expand Up @@ -364,6 +420,10 @@ impl OwnerRpc for Owner {
Owner::get_status(self)
}

fn get_mining_status(&self) -> Result<MiningStatus, Error> {
Owner::get_mining_status(self)
}

fn validate_chain(&self, assume_valid_rangeproofs_kernels: bool) -> Result<(), Error> {
Owner::validate_chain(self, assume_valid_rangeproofs_kernels)
}
Expand Down
107 changes: 107 additions & 0 deletions api/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 {
Expand Down Expand Up @@ -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"));
}
}
Loading
Loading