change(rpc): Adds `getmininginfo`, `getnetworksolps` and `getnetworkhashps` methods (#5808)

* adds type and stub

* adds:
- SolutionRate state request

- getnetworksolps, getnetworkhashps, & getmininginfo RPCs

- vectors tests

* adds snapshot tests

updates ReadRequest::SolutionRate doc link

* removes random slash in doc comment

moves snapshot tests up where it can use the populated state service

* adds snapshots

* updates doc comments

* applies `num_blocks` default in RPC instead of `solution_rate`

* adds # Correctness comment

* Add testnet field to getmininginfo response

* use PartialCumulativeWork instead of u128

* document why `solution_rate` takes an extra block

* add comment explaining why the work for the last block in the iterator is not added to `total_work`

* use `as_u128` method instead of deref for PartialCumulativeWork

* Updates `chain` field of getmininginfo response

* Updates snapshots

Adds "arbitrary_precision" feature to serde_json in zebra-rpc
This commit is contained in:
Arya 2022-12-08 14:56:14 -05:00 committed by GitHub
parent accc8ccbe5
commit 77b85cf767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 404 additions and 6 deletions

View File

@ -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<Work> for PartialCumulativeWork {
fn from(work: Work) -> Self {
PartialCumulativeWork(work.0)

View File

@ -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"] }

View File

@ -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<submit_block::JsonParameters>,
) -> BoxFuture<Result<submit_block::Response>>;
/// Returns mining-related information.
///
/// zcashd reference: [`getmininginfo`](https://zcash.github.io/rpc/getmininginfo.html)
#[rpc(name = "getmininginfo")]
fn get_mining_info(&self) -> BoxFuture<Result<types::get_mining_info::Response>>;
/// 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<usize>,
height: Option<i32>,
) -> BoxFuture<Result<u128>>;
/// 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<usize>,
height: Option<i32>,
) -> BoxFuture<Result<u128>> {
self.get_network_sol_ps(num_blocks, height)
}
}
/// RPC method implementations.
@ -531,6 +568,56 @@ where
}
.boxed()
}
fn get_mining_info(&self) -> BoxFuture<Result<types::get_mining_info::Response>> {
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<usize>,
height: Option<i32>,
) -> BoxFuture<Result<u128>> {
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

View File

@ -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;

View File

@ -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,
}
}
}

View File

@ -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<State, ReadState>(
.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));
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
---
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
expression: get_network_sol_ps
---
2

View File

@ -0,0 +1,5 @@
---
source: zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs
expression: get_network_sol_ps
---
0

View File

@ -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<Arc<Block>> = 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<Arc<Block>> = 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() {

View File

@ -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<block::Height>,
},
}
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",
}
}

View File

@ -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<u128>),
}
#[cfg(feature = "getblocktemplate-rpcs")]
@ -204,7 +208,7 @@ impl TryFrom<ReadResponse> 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")
}
}

View File

@ -1622,6 +1622,62 @@ impl Service<ReadRequest> 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()
}
}
}
}

View File

@ -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<u128> {
// 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>| {
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.
///