diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index 7a15d1c76..017beab08 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -715,6 +715,68 @@ async fn fetch_service_get_block(validator: &ValidatorKind) { test_manager.close().await; } +async fn fetch_service_get_block_header(validator: &ValidatorKind) { + let (test_manager, _fetch_service, fetch_service_subscriber) = + create_test_manager_and_fetch_service(validator, None, true, true, true).await; + + const BLOCK_LIMIT: u32 = 10; + + for i in 0..BLOCK_LIMIT { + test_manager.local_net.generate_blocks(1).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let block = fetch_service_subscriber + .z_get_block(i.to_string(), Some(1)) + .await + .unwrap(); + + let block_hash = match block { + GetBlock::Object(block) => block.hash(), + GetBlock::Raw(_) => panic!("Expected block object"), + }; + + let fetch_service_get_block_header = fetch_service_subscriber + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + + let jsonrpc_client = JsonRpSeeConnector::new_with_basic_auth( + test_node_and_return_url( + test_manager.full_node_rpc_listen_address, + None, + Some("xxxxxx".to_string()), + Some("xxxxxx".to_string()), + ) + .await + .unwrap(), + "xxxxxx".to_string(), + "xxxxxx".to_string(), + ) + .unwrap(); + + let rpc_block_header_response = jsonrpc_client + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + + let fetch_service_get_block_header_verbose = fetch_service_subscriber + .get_block_header(block_hash.to_string(), true) + .await + .unwrap(); + + let rpc_block_header_response_verbose = jsonrpc_client + .get_block_header(block_hash.to_string(), true) + .await + .unwrap(); + + assert_eq!(fetch_service_get_block_header, rpc_block_header_response); + assert_eq!( + fetch_service_get_block_header_verbose, + rpc_block_header_response_verbose + ); + } +} + async fn fetch_service_get_best_blockhash(validator: &ValidatorKind) { let (mut test_manager, _fetch_service, fetch_service_subscriber) = create_test_manager_and_fetch_service(validator, None, true, true, true).await; @@ -1557,6 +1619,11 @@ mod zcashd { fetch_service_get_block(&ValidatorKind::Zcashd).await; } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + pub(crate) async fn block_header() { + fetch_service_get_block_header(&ValidatorKind::Zcashd).await; + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub(crate) async fn difficulty() { assert_fetch_service_difficulty_matches_rpc(&ValidatorKind::Zcashd).await; @@ -1772,6 +1839,11 @@ mod zebrad { fetch_service_get_block(&ValidatorKind::Zebrad).await; } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + pub(crate) async fn block_header() { + fetch_service_get_block_header(&ValidatorKind::Zebrad).await; + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] pub(crate) async fn difficulty() { assert_fetch_service_difficulty_matches_rpc(&ValidatorKind::Zebrad).await; diff --git a/integration-tests/tests/json_server.rs b/integration-tests/tests/json_server.rs index 737d3003f..154f41dbf 100644 --- a/integration-tests/tests/json_server.rs +++ b/integration-tests/tests/json_server.rs @@ -660,6 +660,8 @@ mod zcashd { use super::*; pub(crate) mod zcash_indexer { + use zebra_rpc::methods::GetBlock; + use super::*; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -789,6 +791,45 @@ mod zcashd { z_get_block_inner().await; } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn get_block_header() { + let ( + test_manager, + _zcashd_service, + zcashd_subscriber, + _zaino_service, + zaino_subscriber, + ) = create_test_manager_and_fetch_services(false).await; + + const BLOCK_LIMIT: u32 = 10; + + for i in 0..BLOCK_LIMIT { + test_manager.local_net.generate_blocks(1).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let block = zcashd_subscriber + .z_get_block(i.to_string(), Some(1)) + .await + .unwrap(); + + let block_hash = match block { + GetBlock::Object(block) => block.hash(), + GetBlock::Raw(_) => panic!("Expected block object"), + }; + + let zcashd_get_block_header = zcashd_subscriber + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + + let zainod_block_header_response = zaino_subscriber + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + assert_eq!(zcashd_get_block_header, zainod_block_header_response); + } + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_raw_mempool() { get_raw_mempool_inner().await; diff --git a/integration-tests/tests/state_service.rs b/integration-tests/tests/state_service.rs index 006e9234f..db7696bb0 100644 --- a/integration-tests/tests/state_service.rs +++ b/integration-tests/tests/state_service.rs @@ -1531,7 +1531,7 @@ mod zebrad { use zaino_proto::proto::service::{ AddressList, BlockId, BlockRange, GetAddressUtxosArg, GetSubtreeRootsArg, TxFilter, }; - use zebra_rpc::methods::GetAddressTxIdsRequest; + use zebra_rpc::methods::{GetAddressTxIdsRequest, GetBlock}; use super::*; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -1607,6 +1607,55 @@ mod zebrad { assert_eq!(state_service_block_by_hash, state_service_block_by_height) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn get_block_header() { + let ( + test_manager, + _fetch_service, + fetch_service_subscriber, + _state_service, + state_service_subscriber, + ) = create_test_manager_and_services( + &ValidatorKind::Zebrad, + None, + false, + false, + Some(NetworkKind::Regtest), + ) + .await; + + const BLOCK_LIMIT: u32 = 10; + + for i in 0..BLOCK_LIMIT { + test_manager.local_net.generate_blocks(1).await.unwrap(); + tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; + + let block = fetch_service_subscriber + .z_get_block(i.to_string(), Some(1)) + .await + .unwrap(); + + let block_hash = match block { + GetBlock::Object(block) => block.hash(), + GetBlock::Raw(_) => panic!("Expected block object"), + }; + + let fetch_service_get_block_header = fetch_service_subscriber + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + + let state_service_block_header_response = state_service_subscriber + .get_block_header(block_hash.to_string(), false) + .await + .unwrap(); + assert_eq!( + fetch_service_get_block_header, + state_service_block_header_response + ); + } + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_tree_state() { let ( diff --git a/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index 43835f833..83a296fa3 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -25,7 +25,10 @@ use zebra_rpc::client::ValidateAddressResponse; use crate::jsonrpsee::{ error::{JsonRpcError, TransportError}, response::{ - block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, + block_header::{GetBlockHeader, GetBlockHeaderError}, + block_subsidy::GetBlockSubsidy, + mining_info::GetMiningInfoWire, + peer_info::GetPeerInfo, GetBalanceError, GetBalanceResponse, GetBlockCountResponse, GetBlockError, GetBlockHash, GetBlockResponse, GetBlockchainInfoResponse, GetInfoResponse, GetMempoolInfoResponse, GetSubtreesError, GetSubtreesResponse, GetTransactionResponse, GetTreestateError, @@ -541,6 +544,29 @@ impl JsonRpSeeConnector { } } + /// If verbose is false, returns a string that is serialized, hex-encoded data for blockheader `hash`. + /// If verbose is true, returns an Object with information about blockheader `hash`. + /// + /// # Parameters + /// + /// - hash: (string, required) The block hash + /// - verbose: (boolean, optional, default=true) true for a json object, false for the hex encoded data + /// + /// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html) + /// method: post + /// tags: blockchain + pub async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result> { + let params = [ + serde_json::to_value(hash).map_err(RpcRequestError::JsonRpc)?, + serde_json::to_value(verbose).map_err(RpcRequestError::JsonRpc)?, + ]; + self.send_request("getblockheader", params).await + } + /// Returns the hash of the best block (tip) of the longest chain. /// zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html) /// method: post diff --git a/zaino-fetch/src/jsonrpsee/response.rs b/zaino-fetch/src/jsonrpsee/response.rs index bc13abc97..e2d9778a0 100644 --- a/zaino-fetch/src/jsonrpsee/response.rs +++ b/zaino-fetch/src/jsonrpsee/response.rs @@ -3,8 +3,9 @@ //! These types are redefined rather than imported from zebra_rpc //! to prevent locking consumers into a zebra_rpc version +pub mod block_header; pub mod block_subsidy; -mod common; +pub mod common; pub mod mining_info; pub mod peer_info; diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs new file mode 100644 index 000000000..2edeb23de --- /dev/null +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -0,0 +1,396 @@ +//! Types associated with the `getblockheader` RPC request. + +use serde::{Deserialize, Serialize}; + +use zebra_rpc::methods::opthex; + +use crate::jsonrpsee::connector::{ResponseToError, RpcError}; + +/// Response to a `getblockheader` RPC request. +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockHeader { + /// The verbose variant of the response. Returned when `verbose` is set to `true`. + Verbose(VerboseBlockHeader), + + /// The compact variant of the response. Returned when `verbose` is set to `false`. + Compact(String), + + /// An unknown response shape. + Unknown(serde_json::Value), +} + +/// Error type for the `getblockheader` RPC request. +#[derive(Debug, thiserror::Error)] +pub enum GetBlockHeaderError { + /// Verbosity not valid + #[error("Invalid verbosity: {0}")] + InvalidVerbosity(i8), + + /// The requested block hash or height could not be found + #[error("Block not found: {0}")] + MissingBlock(String), +} + +/// Verbose response to a `getblockheader` RPC request. +/// +/// See the notes for the `get_block_header` method. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VerboseBlockHeader { + /// The hash of the requested block. + #[serde(with = "hex")] + pub hash: zebra_chain::block::Hash, + + /// The number of confirmations of this block in the best chain, + /// or -1 if it is not in the best chain. + pub confirmations: i64, + + /// The height of the requested block. + pub height: u32, + + /// The version field of the requested block. + pub version: u32, + + /// The merkle root of the requesteed block. + #[serde(with = "hex", rename = "merkleroot")] + pub merkle_root: zebra_chain::block::merkle::Root, + + /// The blockcommitments field of the requested block. Its interpretation changes + /// depending on the network and height. + /// + /// This field is only present in Zebra. It was added [here](https://github.com/ZcashFoundation/zebra/pull/9217). + #[serde( + with = "opthex", + rename = "blockcommitments", + default, + skip_serializing_if = "Option::is_none" + )] + pub block_commitments: Option<[u8; 32]>, + + /// The root of the Sapling commitment tree after applying this block. + #[serde(with = "opthex", rename = "finalsaplingroot")] + #[serde(skip_serializing_if = "Option::is_none")] + pub final_sapling_root: Option<[u8; 32]>, + + /// The block time of the requested block header in non-leap seconds since Jan 1 1970 GMT. + pub time: i64, + + /// The nonce of the requested block header. + pub nonce: String, + + /// The Equihash solution in the requested block header. + pub solution: String, + + /// The difficulty threshold of the requested block header displayed in compact form. + pub bits: String, + + /// Floating point number that represents the difficulty limit for this block as a multiple + /// of the minimum difficulty for the network. + pub difficulty: f64, + + /// Cumulative chain work for this block (hex). + /// + /// Present in zcashd, omitted by Zebra. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chainwork: Option, + + /// The previous block hash of the requested block header. + #[serde( + rename = "previousblockhash", + default, + skip_serializing_if = "Option::is_none" + )] + pub previous_block_hash: Option, + + /// The next block hash after the requested block header. + #[serde( + rename = "nextblockhash", + default, + skip_serializing_if = "Option::is_none" + )] + pub next_block_hash: Option, +} + +impl ResponseToError for GetBlockHeader { + type RpcError = GetBlockHeaderError; +} + +impl TryFrom for GetBlockHeaderError { + type Error = RpcError; + + fn try_from(value: RpcError) -> Result { + // If the block is not in Zebra's state, returns + // [error code `-8`.](https://github.com/zcash/zcash/issues/5758) + if value.code == -8 { + Ok(Self::MissingBlock(value.message)) + } else { + Err(value) + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use hex::FromHex; + use serde_json::{json, Value}; + use zebra_chain::block; + + /// Zcashd verbose response. + fn zcashd_verbose_json() -> &'static str { + r#"{ + "hash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "confirmations": 10, + "height": 123456, + "version": 4, + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "time": 1700000000, + "nonce": "11nonce", + "solution": "22solution", + "bits": "1d00ffff", + "difficulty": 123456.789, + "chainwork": "0000000000000000000000000000000000000000000000000000000000001234", + "previousblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "nextblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + }"# + } + + // Zebra verbose response + fn zebra_verbose_json() -> &'static str { + r#"{ + "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", + "confirmations": 3, + "height": 42, + "version": 5, + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "blockcommitments": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "time": 1699999999, + "nonce": "33nonce", + "solution": "44solution", + "bits": "1c654321", + "difficulty": 7890.123, + "previousblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + }"# + } + + #[test] + fn deserialize_verbose_zcashd_includes_chainwork() { + match serde_json::from_str::(zcashd_verbose_json()) { + Ok(block_header) => { + assert_eq!( + block_header.hash, + block::Hash::from_str( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + assert_eq!(block_header.confirmations, 10); + assert_eq!(block_header.height, 123_456); + assert_eq!(block_header.version, 4); + assert_eq!( + block_header.merkle_root, + block::merkle::Root::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + assert_eq!( + block_header.final_sapling_root.unwrap(), + <[u8; 32]>::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + assert_eq!(block_header.time, 1_700_000_000); + assert_eq!(block_header.nonce, "11nonce"); + assert_eq!(block_header.solution, "22solution"); + assert_eq!(block_header.bits, "1d00ffff"); + assert!((block_header.difficulty - 123_456.789).abs() < f64::EPSILON); + + assert_eq!( + block_header.chainwork.as_deref(), + Some("0000000000000000000000000000000000000000000000000000000000001234") + ); + + assert_eq!( + block_header.previous_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); + assert_eq!( + block_header.next_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); + } + Err(e) => { + panic!( + "VerboseBlockHeader failed at {}:{} — {}", + e.line(), + e.column(), + e + ); + } + } + } + + #[test] + fn deserialize_verbose_zebra_includes_blockcommitments_and_omits_chainwork() { + match serde_json::from_str::(zebra_verbose_json()) { + Ok(block_header) => { + assert_eq!( + block_header.hash, + block::Hash::from_str( + "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1" + ) + .unwrap() + ); + assert_eq!(block_header.confirmations, 3); + assert_eq!(block_header.height, 42); + assert_eq!(block_header.version, 5); + assert_eq!( + block_header.merkle_root, + block::merkle::Root::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + + assert_eq!( + block_header.block_commitments.unwrap(), + <[u8; 32]>::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + + assert_eq!( + block_header.final_sapling_root.unwrap(), + <[u8; 32]>::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() + ); + + assert_eq!(block_header.time, 1_699_999_999); + assert_eq!(block_header.nonce, "33nonce"); + assert_eq!(block_header.solution, "44solution"); + assert_eq!(block_header.bits, "1c654321"); + assert!((block_header.difficulty - 7890.123).abs() < f64::EPSILON); + + assert!(block_header.chainwork.is_none()); + + // Zebra always sets previous + assert_eq!( + block_header.previous_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); + assert!(block_header.next_block_hash.is_none()); + } + Err(e) => { + panic!( + "VerboseBlockHeader failed at {}:{} — {}", + e.line(), + e.column(), + e + ); + } + } + } + + #[test] + fn compact_header_is_hex_string() { + let s = r#""040102deadbeef""#; + let block_header: GetBlockHeader = serde_json::from_str(s).unwrap(); + match block_header.clone() { + GetBlockHeader::Compact(hex) => assert_eq!(hex, "040102deadbeef"), + _ => panic!("expected Compact variant"), + } + + // Roundtrip + let out = serde_json::to_string(&block_header).unwrap(); + assert_eq!(out, s); + } + + #[test] + fn unknown_shape_falls_back_to_unknown_variant() { + let weird = r#"{ "weird": 1, "unexpected": ["a","b","c"] }"#; + let block_header: GetBlockHeader = serde_json::from_str(weird).unwrap(); + match block_header { + GetBlockHeader::Unknown(v) => { + assert_eq!(v["weird"], json!(1)); + assert_eq!(v["unexpected"], json!(["a", "b", "c"])); + } + _ => panic!("expected Unknown variant"), + } + } + + #[test] + fn zebra_roundtrip_does_not_inject_chainwork_field() { + let block_header: GetBlockHeader = serde_json::from_str(zebra_verbose_json()).unwrap(); + let header_value: Value = serde_json::to_value(&block_header).unwrap(); + + let header_object = header_value + .as_object() + .expect("verbose should serialize to object"); + assert!(!header_object.contains_key("chainwork")); + + assert_eq!( + header_object.get("blockcommitments"), + Some(&json!( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + )) + ); + } + + #[test] + fn zcashd_roundtrip_preserves_chainwork() { + let block_header: GetBlockHeader = serde_json::from_str(zcashd_verbose_json()).unwrap(); + let header_value: Value = serde_json::to_value(&block_header).unwrap(); + let header_object = header_value.as_object().unwrap(); + + assert_eq!( + header_object.get("chainwork"), + Some(&json!( + "0000000000000000000000000000000000000000000000000000000000001234" + )) + ); + } + + #[test] + fn previous_and_next_optional_edges() { + // Simulate genesis + let genesis_like = r#"{ + "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", + "confirmations": 1, + "height": 0, + "version": 4, + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "time": 1477641369, + "nonce": "nonce", + "solution": "solution", + "bits": "1d00ffff", + "difficulty": 1.0 + }"#; + + match serde_json::from_str::(genesis_like) { + Ok(block_header) => { + assert!(block_header.previous_block_hash.is_none()); + assert!(block_header.next_block_hash.is_none()); + } + Err(e) => { + panic!( + "VerboseBlockHeader failed at {}:{} — {}", + e.line(), + e.column(), + e + ); + } + } + } +} diff --git a/zaino-fetch/src/jsonrpsee/response/common.rs b/zaino-fetch/src/jsonrpsee/response/common.rs index 2fa0ab920..bb198dd82 100644 --- a/zaino-fetch/src/jsonrpsee/response/common.rs +++ b/zaino-fetch/src/jsonrpsee/response/common.rs @@ -1,13 +1,17 @@ +//! Common types used across jsonrpsee responses + pub mod amount; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +/// The identifier for a Zcash node. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct NodeId(pub i64); +/// The height of a Zcash block. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct BlockHeight(pub u32); @@ -18,6 +22,7 @@ impl From for BlockHeight { } } +/// The height of a Zcash block, or None if unknown. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MaybeHeight(pub Option); @@ -46,10 +51,13 @@ impl<'de> Deserialize<'de> for MaybeHeight { } } +/// Unix timestamp. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct UnixTime(pub i64); + impl UnixTime { + /// Converts to a [`SystemTime`]. pub fn as_system_time(self) -> SystemTime { if self.0 >= 0 { UNIX_EPOCH + Duration::from_secs(self.0 as u64) @@ -59,23 +67,29 @@ impl UnixTime { } } +/// Duration in seconds. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(transparent)] pub struct SecondsF64(pub f64); + impl SecondsF64 { + /// Converts to a [`Duration`]. pub fn as_duration(self) -> Duration { Duration::from_secs_f64(self.0) } } +/// Protocol version. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct ProtocolVersion(pub i64); +/// A byte array. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct Bytes(pub u64); +/// Time offset in seconds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(transparent)] pub struct TimeOffsetSeconds(pub i64); diff --git a/zaino-fetch/src/jsonrpsee/response/common/amount.rs b/zaino-fetch/src/jsonrpsee/response/common/amount.rs index dea677751..cdd03ca1e 100644 --- a/zaino-fetch/src/jsonrpsee/response/common/amount.rs +++ b/zaino-fetch/src/jsonrpsee/response/common/amount.rs @@ -2,6 +2,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; +/// Zatoshis per ZEC. pub const ZATS_PER_ZEC: u64 = 100_000_000; /// Represents an amount in Zatoshis. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] @@ -41,6 +42,7 @@ impl<'de> Deserialize<'de> for Zatoshis { pub struct ZecAmount(u64); impl ZecAmount { + /// Returns the amount in zatoshis. pub fn as_zatoshis(self) -> u64 { self.0 } diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index 77a07032c..d5bfa42f9 100644 --- a/zaino-serve/src/rpc/jsonrpc/service.rs +++ b/zaino-serve/src/rpc/jsonrpc/service.rs @@ -1,5 +1,6 @@ //! Zcash RPC implementations. +use zaino_fetch::jsonrpsee::response::block_header::GetBlockHeader; use zaino_fetch::jsonrpsee::response::block_subsidy::GetBlockSubsidy; use zaino_fetch::jsonrpsee::response::mining_info::GetMiningInfoWire; use zaino_fetch::jsonrpsee::response::peer_info::GetPeerInfo; @@ -218,6 +219,26 @@ pub trait ZcashIndexerRpc { verbosity: Option, ) -> Result; + /// If verbose is false, returns a string that is serialized, hex-encoded data for blockheader `hash`. + /// If verbose is true, returns an Object with information about blockheader `hash`. + /// + /// # Parameters + /// + /// - hash: (string, required) The block hash + /// - verbose: (boolean, optional, default=true) true for a json object, false for the hex encoded data + /// + /// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html) + /// zcashd implementation [here](https://github.com/zcash/zcash/blob/16ac743764a513e41dafb2cd79c2417c5bb41e81/src/rpc/blockchain.cpp#L668) + /// + /// method: post + /// tags: blockchain + #[method(name = "getblockheader")] + async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result; + /// Returns all transaction ids in the memory pool, as a JSON array. /// /// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html) @@ -559,6 +580,24 @@ impl ZcashIndexerRpcServer for JsonR }) } + async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result { + self.service_subscriber + .inner_ref() + .get_block_header(hash, verbose) + .await + .map_err(|e| { + ErrorObjectOwned::owned( + ErrorCode::InvalidParams.code(), + "Internal server error", + Some(e.to_string()), + ) + }) + } + async fn get_raw_mempool(&self) -> Result, ErrorObjectOwned> { self.service_subscriber .inner_ref() diff --git a/zaino-state/src/backends/fetch.rs b/zaino-state/src/backends/fetch.rs index 714fcf798..230652ba3 100644 --- a/zaino-state/src/backends/fetch.rs +++ b/zaino-state/src/backends/fetch.rs @@ -23,10 +23,9 @@ use zaino_fetch::{ jsonrpsee::{ connector::{JsonRpSeeConnector, RpcError}, response::{ - block_subsidy::GetBlockSubsidy, - mining_info::GetMiningInfoWire, - peer_info::GetPeerInfo, - {GetMempoolInfoResponse, GetNetworkSolPsResponse}, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, + mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, GetMempoolInfoResponse, + GetNetworkSolPsResponse, }, }, }; @@ -389,6 +388,14 @@ impl ZcashIndexer for FetchServiceSubscriber { .try_into()?) } + async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result { + Ok(self.fetcher.get_block_header(hash, verbose).await?) + } + async fn get_mining_info(&self) -> Result { Ok(self.fetcher.get_mining_info().await?) } diff --git a/zaino-state/src/backends/state.rs b/zaino-state/src/backends/state.rs index 5b7c12164..e1bdcedc8 100644 --- a/zaino-state/src/backends/state.rs +++ b/zaino-state/src/backends/state.rs @@ -28,8 +28,9 @@ use zaino_fetch::{ jsonrpsee::{ connector::{JsonRpSeeConnector, RpcError}, response::{ - block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, - GetMempoolInfoResponse, GetNetworkSolPsResponse, GetSubtreesResponse, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, + mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, GetMempoolInfoResponse, + GetNetworkSolPsResponse, GetSubtreesResponse, }, }, }; @@ -57,10 +58,10 @@ use zebra_rpc::{ }, methods::{ chain_tip_difficulty, AddressBalance, AddressStrings, ConsensusBranchIdHex, - GetAddressTxIdsRequest, GetAddressUtxos, GetBlock, GetBlockHash, GetBlockHeader, - GetBlockHeaderObject, GetBlockTransaction, GetBlockTrees, GetBlockchainInfoResponse, - GetInfo, GetRawTransaction, NetworkUpgradeInfo, NetworkUpgradeStatus, SentTransactionHash, - TipConsensusBranch, + GetAddressTxIdsRequest, GetAddressUtxos, GetBlock, GetBlockHash, + GetBlockHeader as GetBlockHeaderZebra, GetBlockHeaderObject, GetBlockTransaction, + GetBlockTrees, GetBlockchainInfoResponse, GetInfo, GetRawTransaction, NetworkUpgradeInfo, + NetworkUpgradeStatus, SentTransactionHash, TipConsensusBranch, }, server::error::LegacyCode, sync::init_read_state_with_syncer, @@ -412,12 +413,12 @@ impl StateServiceSubscriber { /// /// This rpc is used by get_block(verbose), there is currently no /// plan to offer this RPC publicly. - async fn get_block_header( + async fn get_block_header_inner( state: &ReadStateService, network: &Network, hash_or_height: HashOrHeight, verbose: Option, - ) -> Result { + ) -> Result { let mut state = state.clone(); let verbose = verbose.unwrap_or(true); let network = network.clone(); @@ -448,7 +449,7 @@ impl StateServiceSubscriber { }; let response = if !verbose { - GetBlockHeader::Raw(HexData(header.zcash_serialize_to_vec()?)) + GetBlockHeaderZebra::Raw(HexData(header.zcash_serialize_to_vec()?)) } else { let zebra_state::ReadResponse::SaplingTree(sapling_tree) = state .ready() @@ -527,7 +528,7 @@ impl StateServiceSubscriber { next_block_hash, ); - GetBlockHeader::Object(Box::new(block_header)) + GetBlockHeaderZebra::Object(Box::new(block_header)) }; Ok(response) @@ -755,7 +756,7 @@ impl StateServiceSubscriber { let (fullblock, orchard_tree_response, header, block_info) = futures::join!( blockandsize_future, orchard_future, - StateServiceSubscriber::get_block_header( + StateServiceSubscriber::get_block_header_inner( &state_3, network, hash_or_height, @@ -765,10 +766,10 @@ impl StateServiceSubscriber { ); let header_obj = match header? { - GetBlockHeader::Raw(_hex_data) => unreachable!( + GetBlockHeaderZebra::Raw(_hex_data) => unreachable!( "`true` was passed to get_block_header, an object should be returned" ), - GetBlockHeader::Object(get_block_header_object) => get_block_header_object, + GetBlockHeaderZebra::Object(get_block_header_object) => get_block_header_object, }; let (transactions_response, size, block_info): (Vec, _, _) = @@ -1109,6 +1110,17 @@ impl ZcashIndexer for StateServiceSubscriber { .map_err(Into::into) } + async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result { + self.rpc_client + .get_block_header(hash, verbose) + .await + .map_err(|e| StateServiceError::Custom(e.to_string())) + } + async fn z_get_block( &self, hash_or_height_string: String, diff --git a/zaino-state/src/indexer.rs b/zaino-state/src/indexer.rs index 96aa0403a..c62d17201 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -5,10 +5,8 @@ use async_trait::async_trait; use tokio::{sync::mpsc, time::timeout}; use tracing::warn; use zaino_fetch::jsonrpsee::response::{ - block_subsidy::GetBlockSubsidy, - mining_info::GetMiningInfoWire, - peer_info::GetPeerInfo, - {GetMempoolInfoResponse, GetNetworkSolPsResponse}, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, + peer_info::GetPeerInfo, GetMempoolInfoResponse, GetNetworkSolPsResponse, }; use zaino_proto::proto::{ compact_formats::CompactBlock, @@ -252,6 +250,23 @@ pub trait ZcashIndexer: Send + Sync + 'static { raw_transaction_hex: String, ) -> Result; + /// If verbose is false, returns a string that is serialized, hex-encoded data for blockheader `hash`. + /// If verbose is true, returns an Object with information about blockheader `hash`. + /// + /// # Parameters + /// + /// - hash: (string, required) The block hash + /// - verbose: (boolean, optional, default=true) true for a json object, false for the hex encoded data + /// + /// zcashd reference: [`getblockheader`](https://zcash.github.io/rpc/getblockheader.html) + /// method: post + /// tags: blockchain + async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result; + /// Returns the requested block by hash or height, as a [`GetBlock`] JSON string. /// If the block is not in Zebra's state, returns /// [error code `-8`.](https://github.com/zcash/zcash/issues/5758) if a height was