feat(rpc): add getblockhash rpc method (#4967)

* implement getblockhash rpc method

* make fixes

* fix some docs

* rustfmt

* add snapshot test

* rename `Hash` to `BestChainBlockHash`

* Suggestion for "add getblockhash rpc method" PR (#5428)

* Always immediately return errors in get_height_from_int()

* Explain why calculations can't overflow

* fix for rust feature

* fix some warnings

* hide state functions behind feature

* remove commented assert

* renames

* rename

* fix some warnings

* make zebra-rpc rpc features depend on zebra-state rpc features

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
Alfredo Garcia 2022-10-21 03:01:29 -03:00 committed by GitHub
parent 8fcbeab946
commit 8d777c892f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 321 additions and 25 deletions

View File

@ -10,7 +10,7 @@ edition = "2021"
[features]
default = []
proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"]
getblocktemplate-rpcs = []
getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs"]
[dependencies]
chrono = { version = "0.4.22", default-features = false, features = ["clock", "std"] }

View File

@ -147,11 +147,11 @@ pub trait Rpc {
#[rpc(name = "getblock")]
fn get_block(&self, height: String, verbosity: u8) -> BoxFuture<Result<GetBlock>>;
/// Returns the hash of the current best blockchain tip block, as a [`GetBestBlockHash`] JSON string.
/// Returns the hash of the current best blockchain tip block, as a [`GetBlockHash`] JSON string.
///
/// zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html)
#[rpc(name = "getbestblockhash")]
fn get_best_block_hash(&self) -> Result<GetBestBlockHash>;
fn get_best_block_hash(&self) -> Result<GetBlockHash>;
/// Returns all transaction ids in the memory pool, as a JSON array.
///
@ -610,10 +610,10 @@ where
.boxed()
}
fn get_best_block_hash(&self) -> Result<GetBestBlockHash> {
fn get_best_block_hash(&self) -> Result<GetBlockHash> {
self.latest_chain_tip
.best_tip_hash()
.map(GetBestBlockHash)
.map(GetBlockHash)
.ok_or(Error {
code: ErrorCode::ServerError(0),
message: "No blocks in state".to_string(),
@ -1141,13 +1141,13 @@ pub enum GetBlock {
},
}
/// Response to a `getbestblockhash` RPC request.
/// Response to a `getbestblockhash` and `getblockhash` RPC request.
///
/// Contains the hex-encoded hash of the tip block.
/// Contains the hex-encoded hash of the requested block.
///
/// Also see the notes for the [`Rpc::get_best_block_hash` method].
/// Also see the notes for the [`Rpc::get_best_block_hash`] and `get_block_hash` methods.
#[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash);
pub struct GetBlockHash(#[serde(with = "hex")] block::Hash);
/// Response to a `z_gettreestate` RPC request.
///

View File

@ -1,8 +1,12 @@
//! RPC methods related to mining only available with `getblocktemplate-rpcs` rust feature.
use zebra_chain::chain_tip::ChainTip;
use zebra_chain::{block::Height, chain_tip::ChainTip};
use jsonrpc_core::{self, Error, ErrorCode, Result};
use futures::{FutureExt, TryFutureExt};
use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result};
use jsonrpc_derive::rpc;
use tower::{Service, ServiceExt};
use crate::methods::{GetBlockHash, MISSING_BLOCK_ERROR_CODE};
/// getblocktemplate RPC method signatures.
#[rpc(server)]
@ -17,31 +21,75 @@ pub trait GetBlockTemplateRpc {
/// This rpc method is available only if zebra is built with `--features getblocktemplate-rpcs`.
#[rpc(name = "getblockcount")]
fn get_block_count(&self) -> Result<u32>;
/// Returns the hash of the block of a given height iff the index argument correspond
/// to a block in the best chain.
///
/// zcashd reference: [`getblockhash`](https://zcash-rpc.github.io/getblockhash.html)
///
/// # Parameters
///
/// - `index`: (numeric, required) The block index.
///
/// # Notes
///
/// - If `index` is positive then index = block height.
/// - If `index` is negative then -1 is the last known valid block.
#[rpc(name = "getblockhash")]
fn get_block_hash(&self, index: i32) -> BoxFuture<Result<GetBlockHash>>;
}
/// RPC method implementations.
pub struct GetBlockTemplateRpcImpl<Tip>
pub struct GetBlockTemplateRpcImpl<Tip, State>
where
Tip: ChainTip,
State: Service<
zebra_state::ReadRequest,
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
>,
{
// TODO: Add the other fields from the [`Rpc`] struct as-needed
/// Allows efficient access to the best tip of the blockchain.
latest_chain_tip: Tip,
/// A handle to the state service.
state: State,
}
impl<Tip> GetBlockTemplateRpcImpl<Tip>
impl<Tip, State> GetBlockTemplateRpcImpl<Tip, State>
where
Tip: ChainTip + Clone + Send + Sync + 'static,
State: Service<
zebra_state::ReadRequest,
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
{
/// Create a new instance of the RPC handler.
pub fn new(latest_chain_tip: Tip) -> Self {
Self { latest_chain_tip }
pub fn new(latest_chain_tip: Tip, state: State) -> Self {
Self {
latest_chain_tip,
state,
}
}
}
impl<Tip> GetBlockTemplateRpc for GetBlockTemplateRpcImpl<Tip>
impl<Tip, State> GetBlockTemplateRpc for GetBlockTemplateRpcImpl<Tip, State>
where
Tip: ChainTip + Send + Sync + 'static,
State: Service<
zebra_state::ReadRequest,
Response = zebra_state::ReadResponse,
Error = zebra_state::BoxError,
> + Clone
+ Send
+ Sync
+ 'static,
<State as Service<zebra_state::ReadRequest>>::Future: Send,
{
fn get_block_count(&self) -> Result<u32> {
self.latest_chain_tip
@ -53,4 +101,83 @@ where
data: None,
})
}
fn get_block_hash(&self, index: i32) -> BoxFuture<Result<GetBlockHash>> {
let mut state = self.state.clone();
let maybe_tip_height = self.latest_chain_tip.best_tip_height();
async move {
let tip_height = maybe_tip_height.ok_or(Error {
code: ErrorCode::ServerError(0),
message: "No blocks in state".to_string(),
data: None,
})?;
let height = get_height_from_int(index, tip_height)?;
let request = zebra_state::ReadRequest::BestChainBlockHash(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,
})?;
match response {
zebra_state::ReadResponse::BlockHash(Some(hash)) => Ok(GetBlockHash(hash)),
zebra_state::ReadResponse::BlockHash(None) => Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
message: "Block not found".to_string(),
data: None,
}),
_ => unreachable!("unmatched response to a block request"),
}
}
.boxed()
}
}
/// Given a potentially negative index, find the corresponding `Height`.
///
/// This function is used to parse the integer index argument of `get_block_hash`.
fn get_height_from_int(index: i32, tip_height: Height) -> Result<Height> {
if index >= 0 {
let height = index.try_into().expect("Positive i32 always fits in u32");
if height > tip_height.0 {
return Err(Error::invalid_params(
"Provided index is greater than the current tip",
));
}
Ok(Height(height))
} else {
// `index + 1` can't overflow, because `index` is always negative here.
let height = i32::try_from(tip_height.0)
.expect("tip height fits in i32, because Height::MAX fits in i32")
.checked_add(index + 1);
let sanitized_height = match height {
None => return Err(Error::invalid_params("Provided index is not valid")),
Some(h) => {
if h < 0 {
return Err(Error::invalid_params(
"Provided negative index ends up with a negative height",
));
}
let h: u32 = h.try_into().expect("Positive i32 always fits in u32");
if h > tip_height.0 {
return Err(Error::invalid_params(
"Provided index is greater than the current tip",
));
}
h
}
};
Ok(Height(sanitized_height))
}
}

View File

@ -43,6 +43,8 @@ async fn test_rpc_response_data_for_network(network: Network) {
#[cfg(feature = "getblocktemplate-rpcs")]
let latest_chain_tip_gbt_clone = latest_chain_tip.clone();
#[cfg(feature = "getblocktemplate-rpcs")]
let read_state_clone = read_state.clone();
// Init RPC
let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new(
@ -104,7 +106,7 @@ async fn test_rpc_response_data_for_network(network: Network) {
// `getbestblockhash`
let get_best_block_hash = rpc
.get_best_block_hash()
.expect("We should have a GetBestBlockHash struct");
.expect("We should have a GetBlockHash struct");
snapshot_rpc_getbestblockhash(get_best_block_hash, &settings);
// `getrawmempool`
@ -172,13 +174,23 @@ async fn test_rpc_response_data_for_network(network: Network) {
#[cfg(feature = "getblocktemplate-rpcs")]
{
let get_block_template_rpc = GetBlockTemplateRpcImpl::new(latest_chain_tip_gbt_clone);
let get_block_template_rpc =
GetBlockTemplateRpcImpl::new(latest_chain_tip_gbt_clone, read_state_clone);
// `getblockcount`
let get_block_count = get_block_template_rpc
.get_block_count()
.expect("We should have a number");
snapshot_rpc_getblockcount(get_block_count, &settings);
// `getblockhash`
const BLOCK_HEIGHT10: i32 = 10;
let get_block_hash = get_block_template_rpc
.get_block_hash(BLOCK_HEIGHT10)
.await
.expect("We should have a GetBlockHash struct");
snapshot_rpc_getblockhash(get_block_hash, &settings);
}
}
@ -239,7 +251,7 @@ fn snapshot_rpc_getblock_verbose(block: GetBlock, settings: &insta::Settings) {
}
/// Snapshot `getbestblockhash` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getbestblockhash(tip_hash: GetBestBlockHash, settings: &insta::Settings) {
fn snapshot_rpc_getbestblockhash(tip_hash: GetBlockHash, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_best_block_hash", tip_hash));
}
@ -274,6 +286,12 @@ fn snapshot_rpc_getblockcount(block_count: u32, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_block_count", block_count));
}
#[cfg(feature = "getblocktemplate-rpcs")]
/// Snapshot `getblockhash` response, using `cargo insta` and JSON serialization.
fn snapshot_rpc_getblockhash(block_hash: GetBlockHash, settings: &insta::Settings) {
settings.bind(|| insta::assert_json_snapshot!("get_block_hash", block_hash));
}
/// Utility function to convert a `Network` to a lowercase string.
fn network_string(network: Network) -> String {
let mut net_suffix = network.to_string();

View File

@ -0,0 +1,6 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 270
expression: block_hash
---
"00074c46a4aa8172df8ae2ad1848a2e084e1b6989b7d9e6132adc938bf835b36"

View File

@ -0,0 +1,6 @@
---
source: zebra-rpc/src/methods/tests/snapshot.rs
assertion_line: 270
expression: block_hash
---
"079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112"

View File

@ -233,7 +233,7 @@ async fn rpc_getbestblockhash() {
// Get the tip hash using RPC method `get_best_block_hash`
let get_best_block_hash = rpc
.get_best_block_hash()
.expect("We should have a GetBestBlockHash struct");
.expect("We should have a GetBlockHash struct");
let response_hash = get_best_block_hash.0;
// Check if response is equal to block 10 hash.
@ -641,13 +641,13 @@ async fn rpc_getblockcount() {
Mainnet,
false,
Buffer::new(mempool.clone(), 1),
read_state,
read_state.clone(),
latest_chain_tip.clone(),
);
// Init RPC
let get_block_template_rpc =
get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone());
get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), read_state);
// Get the tip height using RPC method `get_block_count`
let get_block_count = get_block_template_rpc
@ -681,12 +681,12 @@ async fn rpc_getblockcount_empty_state() {
Mainnet,
false,
Buffer::new(mempool.clone(), 1),
read_state,
read_state.clone(),
latest_chain_tip.clone(),
);
let get_block_template_rpc =
get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone());
get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), read_state);
// Get the tip height using RPC method `get_block_count
let get_block_count = get_block_template_rpc.get_block_count();
@ -703,3 +703,57 @@ async fn rpc_getblockcount_empty_state() {
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
assert!(matches!(rpc_tx_queue_task_result, None));
}
#[cfg(feature = "getblocktemplate-rpcs")]
#[tokio::test(flavor = "multi_thread")]
async fn rpc_getblockhash() {
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();
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
// Create a populated state service
let (_state, read_state, latest_chain_tip, _chain_tip_change) =
zebra_state::populated_state(blocks.clone(), Mainnet).await;
// Init RPCs
let _rpc = RpcImpl::new(
"RPC test",
Mainnet,
false,
Buffer::new(mempool.clone(), 1),
Buffer::new(read_state.clone(), 1),
latest_chain_tip.clone(),
);
let get_block_template_rpc =
get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip, read_state);
// Query the hashes using positive indexes
for (i, block) in blocks.iter().enumerate() {
let get_block_hash = get_block_template_rpc
.get_block_hash(i.try_into().expect("usize always fits in i32"))
.await
.expect("We should have a GetBlockHash struct");
assert_eq!(get_block_hash, GetBlockHash(block.clone().hash()));
}
// Query the hashes using negative indexes
for i in (-10..=-1).rev() {
let get_block_hash = get_block_template_rpc
.get_block_hash(i)
.await
.expect("We should have a GetBlockHash struct");
assert_eq!(
get_block_hash,
GetBlockHash(blocks[(10 + (i + 1)) as usize].hash())
);
}
mempool.expect_no_requests().await;
}

View File

@ -75,7 +75,7 @@ impl RpcServer {
{
// Initialize the getblocktemplate rpc methods
let get_block_template_rpc_impl =
GetBlockTemplateRpcImpl::new(latest_chain_tip.clone());
GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), state.clone());
io.extend_with(get_block_template_rpc_impl.to_delegate());
}

View File

@ -7,6 +7,7 @@ edition = "2021"
[features]
proptest-impl = ["proptest", "proptest-derive", "zebra-test", "zebra-chain/proptest-impl"]
getblocktemplate-rpcs = []
[dependencies]
bincode = "1.3.3"

View File

@ -731,6 +731,15 @@ pub enum ReadRequest {
///
/// Returns a type with found utxos and transaction information.
UtxosByAddresses(HashSet<transparent::Address>),
#[cfg(feature = "getblocktemplate-rpcs")]
/// Looks up a block hash by height in the current best chain.
///
/// Returns
///
/// * [`ReadResponse::BlockHash(Some(hash))`](ReadResponse::BlockHash) if the block is in the best chain;
/// * [`ReadResponse::BlockHash(None)`](ReadResponse::BlockHash) otherwise.
BestChainBlockHash(block::Height),
}
impl ReadRequest {
@ -751,6 +760,8 @@ impl ReadRequest {
ReadRequest::AddressBalance { .. } => "address_balance",
ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses",
ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses",
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash",
}
}

View File

@ -110,6 +110,11 @@ pub enum ReadResponse {
/// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data.
AddressUtxos(AddressUtxos),
#[cfg(feature = "getblocktemplate-rpcs")]
/// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the
/// specified block hash.
BlockHash(Option<block::Hash>),
}
/// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s.
@ -144,6 +149,11 @@ impl TryFrom<ReadResponse> for Response {
| ReadResponse::AddressUtxos(_) => {
Err("there is no corresponding Response for this ReadResponse")
}
#[cfg(feature = "getblocktemplate-rpcs")]
ReadResponse::BlockHash(_) => {
Err("there is no corresponding Response for this ReadResponse")
}
}
}
}

View File

@ -1510,6 +1510,42 @@ impl Service<ReadRequest> for ReadStateService {
.map(|join_result| join_result.expect("panic in ReadRequest::UtxosByAddresses"))
.boxed()
}
// Used by get_block_hash RPC.
#[cfg(feature = "getblocktemplate-rpcs")]
ReadRequest::BestChainBlockHash(height) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "best_chain_block_hash",
);
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 hash = state.non_finalized_state_receiver.with_watch_data(
|non_finalized_state| {
read::hash(non_finalized_state.best_chain(), &state.db, height)
},
);
// The work is done in the future.
timer.finish(module_path!(), line!(), "ReadRequest::BestChainBlockHash");
Ok(ReadResponse::BlockHash(hash))
})
})
.map(|join_result| join_result.expect("panic in ReadRequest::BestChainBlockHash"))
.boxed()
}
}
}
}

View File

@ -27,7 +27,12 @@ pub use address::{
tx_id::transparent_tx_ids,
utxo::{address_utxos, AddressUtxos, ADDRESS_HEIGHTS_FULL_RANGE},
};
pub use block::{any_utxo, block, block_header, transaction, transaction_hashes_for_block, utxo};
#[cfg(feature = "getblocktemplate-rpcs")]
pub use block::hash;
pub use find::{
best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers,
hash_by_height, height_by_hash, tip, tip_height,

View File

@ -167,3 +167,25 @@ pub fn any_utxo(
.any_utxo(&outpoint)
.or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo))
}
#[cfg(feature = "getblocktemplate-rpcs")]
/// Returns the [`Hash`] given [`block::Height`](zebra_chain::block::Height), if it exists in
/// the non-finalized `chain` or finalized `db`.
pub fn hash<C>(chain: Option<C>, db: &ZebraDb, height: Height) -> Option<zebra_chain::block::Hash>
where
C: AsRef<Chain>,
{
// # Correctness
//
// The StateService commits blocks to the finalized state before updating
// the latest chain, and it can commit additional blocks after we've cloned
// this `chain` variable.
//
// Since blocks are the same in the finalized and non-finalized state, we
// check the most efficient alternative first. (`chain` is always in memory,
// but `db` stores blocks on disk, with a memory cache.)
chain
.as_ref()
.and_then(|chain| chain.as_ref().hash_by_height(height))
.or_else(|| db.hash(height))
}