Skip to content
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

feat(rpc): add getblockhash rpc method #4967

Merged
merged 19 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
106 changes: 99 additions & 7 deletions zebra-rpc/src/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub trait Rpc {
///
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
/// 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.
///
Expand Down Expand Up @@ -230,6 +230,22 @@ pub trait Rpc {
&self,
address_strings: AddressStrings,
) -> BoxFuture<Result<Vec<GetAddressUtxos>>>;

/// 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.
Expand Down Expand Up @@ -567,10 +583,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(),
Expand Down Expand Up @@ -938,6 +954,44 @@ where
}
.boxed()
}

fn get_block_hash(&self, index: i32) -> BoxFuture<Result<GetBlockHash>> {
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
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::Hash(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::Hash(Some(hash)) => Ok(GetBlockHash(hash)),
zebra_state::ReadResponse::Hash(None) => Err(Error {
code: MISSING_BLOCK_ERROR_CODE,
message: "Block not found".to_string(),
data: None,
}),
_ => unreachable!("unmatched response to a block request"),
}
}
.boxed()
}
}

/// Response to a `getinfo` RPC request.
Expand Down Expand Up @@ -1098,13 +1152,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 [`Rpc::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.
///
Expand Down Expand Up @@ -1269,3 +1323,41 @@ fn check_height_range(start: Height, end: Height, chain_height: Height) -> Resul

Ok(())
}

/// 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 {
let height = (tip_height.0 as i32).checked_add(index + 1);
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved

let sanitized_height = match height {
None => 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",
));
}

Ok(h)
}
};

Ok(Height(sanitized_height?))
}
}
4 changes: 2 additions & 2 deletions zebra-rpc/src/methods/tests/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,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`
Expand Down Expand Up @@ -224,7 +224,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));
}

teor2345 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
55 changes: 54 additions & 1 deletion zebra-rpc/src/methods/tests/vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,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.
Expand Down Expand Up @@ -602,3 +602,56 @@ async fn rpc_getaddressutxos_response() {

mempool.expect_no_requests().await;
}

#[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 RPC
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Buffer::new(mempool.clone(), 1),
read_state,
latest_chain_tip,
Mainnet,
);

// Query the hashes using positive indexes
for (i, block) in blocks.iter().enumerate() {
let get_block_hash = rpc
.get_block_hash(i.try_into().expect("usize always fits in i32"))
.await
.expect("We should have a GetBestBlock struct");
teor2345 marked this conversation as resolved.
Show resolved Hide resolved

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 = rpc
.get_block_hash(i)
.await
.expect("We should have a GetBestBlock struct");
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
get_block_hash,
GetBlockHash(blocks[(10 + (i + 1)) as usize].hash())
);
}

mempool.expect_no_requests().await;

// The queue task should continue without errors or panics
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
assert!(matches!(rpc_tx_queue_task_result, None));
}
11 changes: 10 additions & 1 deletion zebra-state/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::{

use zebra_chain::{
amount::NegativeAllowed,
block::{self, Block},
block::{self, Block, Height},
serialization::SerializationError,
transaction,
transparent::{self, utxos_from_ordered_utxos},
Expand Down Expand Up @@ -503,4 +503,13 @@ pub enum ReadRequest {
///
/// Returns a type with found utxos and transaction information.
UtxosByAddresses(HashSet<transparent::Address>),

/// Looks up a block hash by height in the current best chain.
///
/// Returns
///
/// * [`Response::Hash(Some(Hash))`](Response::Hash) if the block is in the best chain;
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
/// * [`Response::Hash(None)`](Response::Hash) otherwise.
///
Hash(Height),
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 4 additions & 0 deletions zebra-state/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,8 @@ pub enum ReadResponse {
/// [`ReadRequest::UtxosByAddresses`](crate::ReadRequest::UtxosByAddresses)
/// with found utxos and transaction data.
Utxos(AddressUtxos),

/// Response to [`ReadRequest::Hash`](crate::ReadRequest::Hash) with the
/// specified block hash.
Hash(Option<block::Hash>),
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
}
33 changes: 33 additions & 0 deletions zebra-state/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,39 @@ impl Service<ReadRequest> for ReadStateService {
.map(|join_result| join_result.expect("panic in ReadRequest::UtxosByAddresses"))
.boxed()
}

// Used by get_block_hash RPC.
ReadRequest::Hash(height) => {
metrics::counter!(
"state.requests",
1,
"service" => "read_state",
"type" => "block_hash",
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
);

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.best_chain_receiver.with_watch_data(|best_chain| {
read::hash(best_chain, &state.db, height)
});

// The work is done in the future.
timer.finish(module_path!(), line!(), "ReadRequest::Block");
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved

Ok(ReadResponse::Hash(hash))
})
})
.map(|join_result| join_result.expect("panic in ReadRequest::Block"))
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
.boxed()
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion zebra-state/src/service/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub use address::{
tx_id::transparent_tx_ids,
utxo::{transparent_utxos, AddressUtxos, ADDRESS_HEIGHTS_FULL_RANGE},
};
pub use block::{block, block_header, transaction};
pub use block::{block, block_header, hash, transaction};
pub use find::{
chain_contains_hash, find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash,
tip_height,
Expand Down
21 changes: 21 additions & 0 deletions zebra-state/src/service/read/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,24 @@ where
})
.or_else(|| db.transaction(hash))
}

/// 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>
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
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))
}