diff --git a/zebra-chain/src/block/hash.rs b/zebra-chain/src/block/hash.rs index 71236f6c..a74e6817 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 914f1d24..f5447cf2 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 3c52c90d..1a7c413c 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; +}