From f39ac48c5945224964dd2f4eafe88fdf6ef46cc8 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Fri, 11 Mar 2022 02:13:08 -0300 Subject: [PATCH] feature(rpc): Implement `getbestblockhash` method (#3754) * feature(rpc): start adding a `getblock` method * fix(rpc): replace oneshot * fix(rpc): replace a panic with error * fix(rpc): fix test * feature(rpc): add hex to response * refactor(rpc): use generic instead of alias * docs(rpc): improve docs for getblock method * test(rpc): add a test for getblock method * deps(rpc): remove non needed tower features Co-authored-by: teor * docs(rpc): add a note to getblock doc * refactor(rpc): replace alias * fix(rpc): use `zcash_serialize_to_vec()` instead of logging format * tests(rpc): add network argument to `populated_state()` * refactor(rpc): use an error for state service readiness * fix(rpc): add parameter * fix(rpc): clippy * nit(rpc): remove new line from imports * fix(rpc): remove commented code * fix(rpc): simplify error Co-authored-by: Janito Vaqueiro Ferreira Filho * Use a `SerializedBlock` type to help serializing blocks (#3725) * Create a `SerializedBlock` helper type Create a type that can be used as a byte slice, but is guaranteed to represent a valid block. * Use `into_iter` instead of `iter` There's no need to borrow the elements, they can be moved out directly. This will be necessary because `&Arc` doesn't implement `Borrow`, so a `SerializedBlock` can't be built directly from an `&Arc`. * Use `SerializedBlock` in `GetBlock` Make the type stricter to avoid storing possibly invalid values. The bytes are still serialized as a hexadecimal string, through the usage of `hex`. The `serde::Deserialize` can't be derived because `hex` requires the type to also implement `FromHex`. * feature(rpc): add suggestions from code review Co-authored-by: Janito Vaqueiro Ferreira Filho * tests(rpc): make sure mempool has no requests in get_block test * fix(rpc): change height argument type in getblock method * fix(rpc): rustfmt * fix(rpc): replace panic * fix(rpc): change getblock response * fix(rpc): fix lightwalletd test * tests(rpc): add a getblock error test * fix(rpc): try another regex * feature(rpc): add `getbestblockhash` RPC method * feature(rpc): Add a `pub struct SerializedBlockHash` type * tests(rpc): add a unit test for `getbestblockhash` method * tests(rpc): make sure no requests are sent to mempool in getbestblockhash test * tests(rpc): refactor check Co-authored-by: teor * fix(rpc): fixes after rebase * refactor(rpc): refactor `GetBestBlockHash` * fix(rpc): unused variables Co-authored-by: teor * docs(rpc): update * fix(rpc): add panic * fix(rpc): fix panic Co-authored-by: teor Co-authored-by: Janito Vaqueiro Ferreira Filho --- zebra-chain/src/block/hash.rs | 58 +++++++++++++++++++++++--- zebra-rpc/src/methods.rs | 50 +++++++++++++++++++--- zebra-rpc/src/methods/tests/vectors.rs | 48 +++++++++++++++++++-- 3 files changed, 142 insertions(+), 14 deletions(-) diff --git a/zebra-chain/src/block/hash.rs b/zebra-chain/src/block/hash.rs index 71236f6cb..a74e6817e 100644 --- a/zebra-chain/src/block/hash.rs +++ b/zebra-chain/src/block/hash.rs @@ -1,5 +1,7 @@ use std::{fmt, io}; +use hex::{FromHex, ToHex}; + #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; use serde::{Deserialize, Serialize}; @@ -22,24 +24,68 @@ use super::Header; #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] pub struct Hash(pub [u8; 32]); -impl fmt::Display for Hash { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl Hash { + /// Return the hash bytes in big-endian byte-order suitable for printing out byte by byte. + /// + /// Zebra displays transaction and block hashes in big-endian byte-order, + /// following the u256 convention set by Bitcoin and zcashd. + fn bytes_in_display_order(&self) -> [u8; 32] { let mut reversed_bytes = self.0; reversed_bytes.reverse(); - f.write_str(&hex::encode(&reversed_bytes)) + reversed_bytes + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.encode_hex::()) } } impl fmt::Debug for Hash { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut reversed_bytes = self.0; - reversed_bytes.reverse(); f.debug_tuple("block::Hash") - .field(&hex::encode(&reversed_bytes)) + .field(&self.encode_hex::()) .finish() } } +impl ToHex for &Hash { + fn encode_hex>(&self) -> T { + self.bytes_in_display_order().encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + self.bytes_in_display_order().encode_hex_upper() + } +} + +impl ToHex for Hash { + fn encode_hex>(&self) -> T { + (&self).encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + (&self).encode_hex_upper() + } +} + +impl FromHex for Hash { + type Error = <[u8; 32] as FromHex>::Error; + + fn from_hex>(hex: T) -> Result { + let hash = <[u8; 32]>::from_hex(hex)?; + + Ok(hash.into()) + } +} + +impl From<[u8; 32]> for Hash { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + impl<'a> From<&'a Header> for Hash { fn from(block_header: &'a Header) -> Self { let mut hash_writer = sha256d::Writer::default(); diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 914f1d249..f5447cf27 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -13,7 +13,7 @@ use jsonrpc_derive::rpc; use tower::{buffer::Buffer, Service, ServiceExt}; use zebra_chain::{ - block::SerializedBlock, + block::{self, SerializedBlock}, serialization::{SerializationError, ZcashDeserialize}, transaction::{self, Transaction}, }; @@ -80,10 +80,7 @@ pub trait Rpc { /// /// zcashd reference: /// - /// Result: - /// { - /// "data": String, // The block encoded as hex - /// } + /// Result: [`GetBlock`] /// /// Note 1: We only expose the `data` field as lightwalletd uses the non-verbose /// mode for all getblock calls: @@ -94,6 +91,17 @@ pub trait Rpc { /// Note 3: The `verbosity` parameter is ignored but required in the call. #[rpc(name = "getblock")] fn get_block(&self, height: String, verbosity: u8) -> BoxFuture>; + + /// getbestblockhash + /// + /// Returns the hash of the current best blockchain tip block. + /// + /// zcashd reference: + /// + /// Result: [`GetBestBlockHash`] + /// + #[rpc(name = "getbestblockhash")] + fn get_best_block_hash(&self) -> BoxFuture>; } /// RPC method implementations. @@ -248,6 +256,34 @@ where } .boxed() } + + fn get_best_block_hash(&self) -> BoxFuture> { + let mut state = self.state.clone(); + + async move { + let request = zebra_state::Request::Tip; + 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::Response::Tip(Some((_height, hash))) => Ok(GetBestBlockHash(hash)), + zebra_state::Response::Tip(None) => Err(Error { + code: ErrorCode::ServerError(0), + message: "No blocks in state".to_string(), + data: None, + }), + _ => unreachable!("unmatched response to a tip request"), + } + } + .boxed() + } } #[derive(serde::Serialize, serde::Deserialize)] @@ -273,3 +309,7 @@ pub struct SentTransactionHash(#[serde(with = "hex")] transaction::Hash); #[derive(serde::Serialize)] /// Response to a `getblock` RPC request. pub struct GetBlock(#[serde(with = "hex")] SerializedBlock); + +#[derive(Debug, PartialEq, serde::Serialize)] +/// Response to a `getbestblockhash` RPC request. +pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash); diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 3c52c90d7..1a7c413c2 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -11,6 +11,9 @@ use zebra_test::mock_service::MockService; use super::super::*; +// Number of blocks to populate state with +const NUMBER_OF_BLOCKS: u32 = 10; + #[tokio::test] async fn rpc_getinfo() { zebra_test::init(); @@ -42,9 +45,6 @@ async fn rpc_getinfo() { async fn rpc_getblock() { zebra_test::init(); - // Number of blocks to populate state with - const NUMBER_OF_BLOCKS: u32 = 10; - // Put the first `NUMBER_OF_BLOCKS` blocks in a vector let blocks: Vec> = zebra_test::vectors::MAINNET_BLOCKS .range(0..=NUMBER_OF_BLOCKS) @@ -98,3 +98,45 @@ async fn rpc_getblock_error() { mempool.expect_no_requests().await; state.expect_no_requests().await; } + +#[tokio::test] +async fn rpc_getbestblockhash() { + zebra_test::init(); + + // Put `NUMBER_OF_BLOCKS` blocks in a vector + let blocks: Vec> = zebra_test::vectors::MAINNET_BLOCKS + .range(0..=NUMBER_OF_BLOCKS) + .map(|(_, block_bytes)| block_bytes.zcash_deserialize_into::>().unwrap()) + .collect(); + + // Get the hash of the block at the tip using hardcoded block tip bytes. + // We want to test the RPC response is equal to this hash + let tip_block: Block = zebra_test::vectors::BLOCK_MAINNET_10_BYTES + .zcash_deserialize_into() + .unwrap(); + let tip_block_hash = tip_block.hash(); + + // Get a mempool handle + let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + // Create a populated state service, the tip will be in `NUMBER_OF_BLOCKS`. + let state = zebra_state::populated_state(blocks.clone(), Network::Mainnet).await; + + // Init RPC + let rpc = RpcImpl { + app_version: "Zebra version test".to_string(), + mempool: Buffer::new(mempool.clone(), 1), + state, + }; + + // Get the tip hash using RPC method `get_best_block_hash` + let get_best_block_hash = rpc + .get_best_block_hash() + .await + .expect("We should have a GetBestBlockHash struct"); + let response_hash = get_best_block_hash.0; + + // Check if response is equal to block 10 hash. + assert_eq!(response_hash, tip_block_hash); + + mempool.expect_no_requests().await; +}