diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs index cf41bc8a4..f8af1b21b 100644 --- a/zebra-chain/src/work/difficulty.rs +++ b/zebra-chain/src/work/difficulty.rs @@ -503,6 +503,13 @@ impl std::ops::Add for Work { /// Partial work used to track relative work in non-finalized chains pub struct PartialCumulativeWork(u128); +impl PartialCumulativeWork { + /// Return the inner `u128` value. + pub fn as_u128(self) -> u128 { + self.0 + } +} + impl From for PartialCumulativeWork { fn from(work: Work) -> Self { PartialCumulativeWork(work.0) diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 283c4c514..119e9ece8 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -43,7 +43,7 @@ jsonrpc-http-server = "18.0.0" num_cpus = "1.14.0" # zebra-rpc needs the preserve_order feature in serde_json, which is a dependency of jsonrpc-core -serde_json = { version = "1.0.89", features = ["preserve_order"] } +serde_json = { version = "1.0.89", features = ["preserve_order", "arbitrary_precision"] } indexmap = { version = "1.9.2", features = ["serde"] } tokio = { version = "1.23.0", features = ["time", "rt-multi-thread", "macros", "tracing"] } diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index ccf4b2214..f3f3b4d48 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -52,6 +52,11 @@ pub mod zip317; /// > and clock time varies between nodes. const MAX_ESTIMATED_DISTANCE_TO_NETWORK_CHAIN_TIP: i32 = 100; +/// The default window size specifying how many blocks to check when estimating the chain's solution rate. +/// +/// Based on default value in zcashd. +const DEFAULT_SOLUTION_RATE_WINDOW_SIZE: usize = 120; + /// The RPC error code used by `zcashd` for when it's still downloading initial blocks. /// /// `s-nomp` mining pool expects error code `-10` when the node is not synced: @@ -132,6 +137,38 @@ pub trait GetBlockTemplateRpc { hex_data: HexData, _options: Option, ) -> BoxFuture>; + + /// Returns mining-related information. + /// + /// zcashd reference: [`getmininginfo`](https://zcash.github.io/rpc/getmininginfo.html) + #[rpc(name = "getmininginfo")] + fn get_mining_info(&self) -> BoxFuture>; + + /// Returns the estimated network solutions per second based on the last `num_blocks` before `height`. + /// If `num_blocks` is not supplied, uses 120 blocks. + /// If `height` is not supplied or is 0, uses the tip height. + /// + /// zcashd reference: [`getnetworksolps`](https://zcash.github.io/rpc/getnetworksolps.html) + #[rpc(name = "getnetworksolps")] + fn get_network_sol_ps( + &self, + num_blocks: Option, + height: Option, + ) -> BoxFuture>; + + /// Returns the estimated network solutions per second based on the last `num_blocks` before `height`. + /// If `num_blocks` is not supplied, uses 120 blocks. + /// If `height` is not supplied or is 0, uses the tip height. + /// + /// zcashd reference: [`getnetworkhashps`](https://zcash.github.io/rpc/getnetworkhashps.html) + #[rpc(name = "getnetworkhashps")] + fn get_network_hash_ps( + &self, + num_blocks: Option, + height: Option, + ) -> BoxFuture> { + self.get_network_sol_ps(num_blocks, height) + } } /// RPC method implementations. @@ -531,6 +568,56 @@ where } .boxed() } + + fn get_mining_info(&self) -> BoxFuture> { + let network = self.network; + let solution_rate_fut = self.get_network_sol_ps(None, None); + async move { + Ok(types::get_mining_info::Response::new( + network, + solution_rate_fut.await?, + )) + } + .boxed() + } + + fn get_network_sol_ps( + &self, + num_blocks: Option, + height: Option, + ) -> BoxFuture> { + let num_blocks = num_blocks + .map(|num_blocks| num_blocks.max(1)) + .unwrap_or(DEFAULT_SOLUTION_RATE_WINDOW_SIZE); + let height = height.and_then(|height| (height > 1).then_some(Height(height as u32))); + let mut state = self.state.clone(); + + async move { + let request = ReadRequest::SolutionRate { num_blocks, height }; + + let response = state + .ready() + .and_then(|service| service.call(request)) + .await + .map_err(|error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + })?; + + let solution_rate = match response { + ReadResponse::SolutionRate(solution_rate) => solution_rate.ok_or(Error { + code: ErrorCode::ServerError(0), + message: "No blocks in state".to_string(), + data: None, + })?, + _ => unreachable!("unmatched response to a solution rate request"), + }; + + Ok(solution_rate) + } + .boxed() + } } // get_block_template support methods diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs index 989599dca..4d20ad10b 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types.rs @@ -3,6 +3,7 @@ pub mod default_roots; pub mod get_block_template; pub mod get_block_template_opts; +pub mod get_mining_info; pub mod hex_data; pub mod submit_block; pub mod transaction; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_mining_info.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_mining_info.rs new file mode 100644 index 000000000..a75984489 --- /dev/null +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_mining_info.rs @@ -0,0 +1,31 @@ +//! Response type for the `getmininginfo` RPC. + +use zebra_chain::parameters::Network; + +/// Response to a `getmininginfo` RPC request. +#[derive(Debug, PartialEq, Eq, serde::Serialize)] +pub struct Response { + /// The estimated network solution rate in Sol/s. + networksolps: u128, + + /// The estimated network solution rate in Sol/s. + networkhashps: u128, + + /// Current network name as defined in BIP70 (main, test, regtest) + chain: String, + + /// If using testnet or not + testnet: bool, +} + +impl Response { + /// Creates a new `getmininginfo` response + pub fn new(network: Network, networksolps: u128) -> Self { + Self { + networksolps, + networkhashps: networksolps, + chain: network.bip70_network_name(), + testnet: network == Network::Testnet, + } + } +} diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 760c54d20..9162a9983 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -29,7 +29,9 @@ use zebra_test::mock_service::{MockService, PanicAssertion}; use crate::methods::{ get_block_template_rpcs::{ self, - types::{get_block_template::GetBlockTemplate, hex_data::HexData, submit_block}, + types::{ + get_block_template::GetBlockTemplate, get_mining_info, hex_data::HexData, submit_block, + }, }, tests::utils::fake_history_tree, GetBlockHash, GetBlockTemplateRpc, GetBlockTemplateRpcImpl, @@ -127,9 +129,22 @@ pub async fn test_responses( .get_block_hash(BLOCK_HEIGHT10) .await .expect("We should have a GetBlockHash struct"); - snapshot_rpc_getblockhash(get_block_hash, &settings); + // `getmininginfo` + let get_mining_info = get_block_template_rpc + .get_mining_info() + .await + .expect("We should have a success response"); + snapshot_rpc_getmininginfo(get_mining_info, &settings); + + // `getnetworksolps` (and `getnetworkhashps`) + let get_network_sol_ps = get_block_template_rpc + .get_network_sol_ps(None, None) + .await + .expect("We should have a success response"); + snapshot_rpc_getnetworksolps(get_network_sol_ps, &settings); + // get a new empty state let new_read_state = MockService::build().for_unit_tests(); @@ -225,3 +240,16 @@ fn snapshot_rpc_submit_block_invalid( insta::assert_json_snapshot!("snapshot_rpc_submit_block_invalid", submit_block_response) }); } + +/// Snapshot `getmininginfo` response, using `cargo insta` and JSON serialization. +fn snapshot_rpc_getmininginfo( + get_mining_info: get_mining_info::Response, + settings: &insta::Settings, +) { + settings.bind(|| insta::assert_json_snapshot!("get_mining_info", get_mining_info)); +} + +/// Snapshot `getnetworksolps` response, using `cargo insta` and JSON serialization. +fn snapshot_rpc_getnetworksolps(get_network_sol_ps: u128, settings: &insta::Settings) { + settings.bind(|| insta::assert_json_snapshot!("get_network_sol_ps", get_network_sol_ps)); +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@mainnet_10.snap new file mode 100644 index 000000000..67ffde393 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@mainnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: get_mining_info +--- +{ + "networksolps": 2, + "networkhashps": 2, + "chain": "main", + "testnet": false +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@testnet_10.snap new file mode 100644 index 000000000..fc728a854 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_mining_info@testnet_10.snap @@ -0,0 +1,10 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: get_mining_info +--- +{ + "networksolps": 0, + "networkhashps": 0, + "chain": "test", + "testnet": true +} diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@mainnet_10.snap new file mode 100644 index 000000000..edeae9e01 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@mainnet_10.snap @@ -0,0 +1,5 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: get_network_sol_ps +--- +2 diff --git a/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@testnet_10.snap new file mode 100644 index 000000000..136bc75f1 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshot/snapshots/get_network_sol_ps@testnet_10.snap @@ -0,0 +1,5 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +expression: get_network_sol_ps +--- +0 diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 89aaa1591..4136d9db1 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -785,6 +785,95 @@ async fn rpc_getblockhash() { mempool.expect_no_requests().await; } +#[cfg(feature = "getblocktemplate-rpcs")] +#[tokio::test(flavor = "multi_thread")] +async fn rpc_getmininginfo() { + let _init_guard = zebra_test::init(); + + // Create a continuous chain of mainnet blocks from genesis + let blocks: Vec> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS + .iter() + .map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap()) + .collect(); + + // Create a populated state service + let (_state, read_state, latest_chain_tip, _chain_tip_change) = + zebra_state::populated_state(blocks.clone(), Mainnet).await; + + // Init RPC + let get_block_template_rpc = get_block_template_rpcs::GetBlockTemplateRpcImpl::new( + Mainnet, + Default::default(), + Buffer::new(MockService::build().for_unit_tests(), 1), + read_state, + latest_chain_tip.clone(), + MockService::build().for_unit_tests(), + MockSyncStatus::default(), + ); + + get_block_template_rpc + .get_mining_info() + .await + .expect("get_mining_info call should succeed"); +} + +#[cfg(feature = "getblocktemplate-rpcs")] +#[tokio::test(flavor = "multi_thread")] +async fn rpc_getnetworksolps() { + let _init_guard = zebra_test::init(); + + // Create a continuous chain of mainnet blocks from genesis + let blocks: Vec> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS + .iter() + .map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap()) + .collect(); + + // Create a populated state service + let (_state, read_state, latest_chain_tip, _chain_tip_change) = + zebra_state::populated_state(blocks.clone(), Mainnet).await; + + // Init RPC + let get_block_template_rpc = get_block_template_rpcs::GetBlockTemplateRpcImpl::new( + Mainnet, + Default::default(), + Buffer::new(MockService::build().for_unit_tests(), 1), + read_state, + latest_chain_tip.clone(), + MockService::build().for_unit_tests(), + MockSyncStatus::default(), + ); + + let get_network_sol_ps_inputs = [ + (None, None), + (Some(0), None), + (Some(0), Some(0)), + (Some(0), Some(-1)), + (Some(0), Some(10)), + (Some(0), Some(i32::MAX)), + (Some(1), None), + (Some(1), Some(0)), + (Some(1), Some(-1)), + (Some(1), Some(10)), + (Some(1), Some(i32::MAX)), + (Some(usize::MAX), None), + (Some(usize::MAX), Some(0)), + (Some(usize::MAX), Some(-1)), + (Some(usize::MAX), Some(10)), + (Some(usize::MAX), Some(i32::MAX)), + ]; + + for (num_blocks_input, height_input) in get_network_sol_ps_inputs { + let get_network_sol_ps_result = get_block_template_rpc + .get_network_sol_ps(num_blocks_input, height_input) + .await; + assert!( + get_network_sol_ps_result + .is_ok(), + "get_network_sol_ps({num_blocks_input:?}, {height_input:?}) call with should be ok, got: {get_network_sol_ps_result:?}" + ); + } +} + #[cfg(feature = "getblocktemplate-rpcs")] #[tokio::test(flavor = "multi_thread")] async fn rpc_getblocktemplate() { diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 289b8d469..c7f04eb79 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -779,6 +779,17 @@ pub enum ReadRequest { /// [`zebra-state::GetBlockTemplateChainInfo`](zebra-state::GetBlockTemplateChainInfo)` structure containing /// best chain state information. ChainInfo, + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Get the average solution rate in the best chain. + /// + /// Returns [`ReadResponse::SolutionRate`] + SolutionRate { + /// Specifies over difficulty averaging window. + num_blocks: usize, + /// Optionally estimate the network speed at the time when a certain block was found + height: Option, + }, } impl ReadRequest { @@ -806,6 +817,8 @@ impl ReadRequest { ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash", #[cfg(feature = "getblocktemplate-rpcs")] ReadRequest::ChainInfo => "chain_info", + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::SolutionRate { .. } => "solution_rate", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index c1e0802e9..481d5157d 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -133,6 +133,10 @@ pub enum ReadResponse { /// Response to [`ReadRequest::ChainInfo`](crate::ReadRequest::ChainInfo) with the state /// information needed by the `getblocktemplate` RPC method. ChainInfo(GetBlockTemplateChainInfo), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Response to [`ReadRequest::SolutionRate`](crate::ReadRequest::SolutionRate) + SolutionRate(Option), } #[cfg(feature = "getblocktemplate-rpcs")] @@ -204,7 +208,7 @@ impl TryFrom for Response { Err("there is no corresponding Response for this ReadResponse") } #[cfg(feature = "getblocktemplate-rpcs")] - ReadResponse::ChainInfo(_) => { + ReadResponse::ChainInfo(_) | ReadResponse::SolutionRate(_) => { Err("there is no corresponding Response for this ReadResponse") } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 239dbc6b0..5f926baa0 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1622,6 +1622,62 @@ impl Service for ReadStateService { .map(|join_result| join_result.expect("panic in ReadRequest::ChainInfo")) .boxed() } + + // Used by getmininginfo, getnetworksolps, and getnetworkhashps RPCs. + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::SolutionRate { num_blocks, height } => { + let timer = CodeTimer::start(); + + let state = self.clone(); + + // # Performance + // + // Allow other async tasks to make progress while concurrently reading blocks from disk. + let span = Span::current(); + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let latest_non_finalized_state = state.latest_non_finalized_state(); + // # Correctness + // + // It is ok to do these lookups using multiple database calls. Finalized state updates + // can only add overlapping blocks, and block hashes are unique across all chain forks. + // + // The worst that can happen here is that the default `start_hash` will be below + // the chain tip. + let (tip_height, tip_hash) = + match read::tip(latest_non_finalized_state.best_chain(), &state.db) { + Some(tip_hash) => tip_hash, + None => return Ok(ReadResponse::SolutionRate(None)), + }; + + let start_hash = match height { + Some(height) if height < tip_height => read::hash_by_height( + latest_non_finalized_state.best_chain(), + &state.db, + height, + ), + // use the chain tip hash if height is above it or not provided. + _ => Some(tip_hash), + }; + + let solution_rate = start_hash.and_then(|start_hash| { + read::difficulty::solution_rate( + &latest_non_finalized_state, + &state.db, + num_blocks, + start_hash, + ) + }); + + // The work is done in the future. + timer.finish(module_path!(), line!(), "ReadRequest::ChainInfo"); + + Ok(ReadResponse::SolutionRate(solution_rate)) + }) + }) + .map(|join_result| join_result.expect("panic in ReadRequest::ChainInfo")) + .boxed() + } } } } diff --git a/zebra-state/src/service/read/difficulty.rs b/zebra-state/src/service/read/difficulty.rs index a18f37175..c67df238f 100644 --- a/zebra-state/src/service/read/difficulty.rs +++ b/zebra-state/src/service/read/difficulty.rs @@ -5,10 +5,10 @@ use std::sync::Arc; use chrono::{DateTime, Duration, TimeZone, Utc}; use zebra_chain::{ - block::{self, Block, Height}, + block::{self, Block, Hash, Height}, history_tree::HistoryTree, parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING}, - work::difficulty::CompactDifficulty, + work::difficulty::{CompactDifficulty, PartialCumulativeWork}, }; use crate::{ @@ -63,6 +63,58 @@ pub fn get_block_template_chain_info( )) } +/// Accepts a `non_finalized_state`, [`ZebraDb`], `num_blocks`, and a block hash to start at. +/// +/// Iterates over up to the last `num_blocks` blocks, summing up their total work. +/// Divides that total by the number of seconds between the timestamp of the +/// first block in the iteration and 1 block below the last block. +/// +/// Returns the solution rate per second for the current best chain, or `None` if +/// the `start_hash` and at least 1 block below it are not found in the chain. +pub fn solution_rate( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, + num_blocks: usize, + start_hash: Hash, +) -> Option { + // Take 1 extra block for calculating the number of seconds between when mining on the first block likely started. + // The work for the last block in this iterator is not added to `total_work`. + let mut block_iter = any_ancestor_blocks(non_finalized_state, db, start_hash) + .take(num_blocks.checked_add(1).unwrap_or(num_blocks)) + .peekable(); + + let get_work = |block: Arc| { + block + .header + .difficulty_threshold + .to_work() + .expect("work has already been validated") + }; + + let block = block_iter.next()?; + let last_block_time = block.header.time; + + let mut total_work: PartialCumulativeWork = get_work(block).into(); + + loop { + // Return `None` if the iterator doesn't yield a second item. + let block = block_iter.next()?; + + if block_iter.peek().is_some() { + // Add the block's work to `total_work` if it's not the last item in the iterator. + // The last item in the iterator is only used to estimate when mining on the first block + // in the window of `num_blocks` likely started. + total_work += get_work(block); + } else { + let first_block_time = block.header.time; + let duration_between_first_and_last_block = last_block_time - first_block_time; + return Some( + total_work.as_u128() / duration_between_first_and_last_block.num_seconds() as u128, + ); + } + } +} + /// Do a consistency check by checking the finalized tip before and after all other database queries. /// Returns and error if the tip obtained before and after is not the same. ///