From 81a8e1c82d1eea1266dadd91ab4edb67cc369494 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Wed, 15 Oct 2025 15:00:04 -0300 Subject: [PATCH 01/16] chore(`getblockheader`): initial passthrough --- zaino-fetch/src/jsonrpsee/connector.rs | 25 +++-- zaino-fetch/src/jsonrpsee/response.rs | 3 +- .../src/jsonrpsee/response/block_header.rs | 100 ++++++++++++++++++ zaino-serve/src/rpc/jsonrpc/service.rs | 26 +++++ zaino-state/src/backends/fetch.rs | 13 ++- zaino-state/src/backends/state.rs | 37 ++++--- zaino-state/src/indexer.rs | 11 +- 7 files changed, 189 insertions(+), 26 deletions(-) create mode 100644 zaino-fetch/src/jsonrpsee/response/block_header.rs diff --git a/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index 46ce0c999..631051421 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -25,12 +25,12 @@ use zebra_rpc::client::ValidateAddressResponse; use crate::jsonrpsee::{ error::{JsonRpcError, TransportError}, response::{ - block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, GetBalanceError, - GetBalanceResponse, GetBlockCountResponse, GetBlockError, GetBlockHash, GetBlockResponse, - GetBlockchainInfoResponse, GetInfoResponse, GetMempoolInfoResponse, GetSubtreesError, - GetSubtreesResponse, GetTransactionResponse, GetTreestateError, GetTreestateResponse, - GetUtxosError, GetUtxosResponse, SendTransactionError, SendTransactionResponse, TxidsError, - TxidsResponse, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, + GetBalanceError, GetBalanceResponse, GetBlockCountResponse, GetBlockError, GetBlockHash, + GetBlockResponse, GetBlockchainInfoResponse, GetInfoResponse, GetMempoolInfoResponse, + GetSubtreesError, GetSubtreesResponse, GetTransactionResponse, GetTreestateError, + GetTreestateResponse, GetUtxosError, GetUtxosResponse, SendTransactionError, + SendTransactionResponse, TxidsError, TxidsResponse, }, }; @@ -546,6 +546,19 @@ impl JsonRpSeeConnector { } } + // TODO: handle error cases + pub async fn get_block_header( + &self, + hash: String, + verbose: bool, + ) -> Result> { + let params = [ + serde_json::to_value(hash).unwrap(), + serde_json::to_value(verbose).unwrap(), + ]; + 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 6f07e09ff..0f1290825 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 peer_info; use std::{convert::Infallible, num::ParseIntError}; 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..4d34bcc8f --- /dev/null +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -0,0 +1,100 @@ +//! Types associated with the `getblockheader` RPC request. + +use std::{collections::BTreeMap, convert::Infallible}; + +use serde::{Deserialize, Serialize}; +use zebra_rpc::client::BlockHeaderObject; + +use crate::jsonrpsee::connector::ResponseToError; + +type z = BlockHeaderObject; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum GetBlockHeader { + Verbose(VerboseBlockHeader), + Compact(String), + Unknown(serde_json::Value), +} + +/// Verbose response to a `getblockheader` RPC request. +/// +/// See the notes for the `get_block_header` method. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct VerboseBlockHeader { + /// The hash of the requested block. + pub hash: String, + + /// 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(rename = "merkleroot")] + pub merkle_root: String, + + /// The blockcommitments field of the requested block. Its interpretation changes + /// depending on the network and height. + #[serde( + rename = "blockcommitments", + default, + skip_serializing_if = "Option::is_none" + )] + pub block_commitments: Option, + + /// The root of the Sapling commitment tree after applying this block. + #[serde(rename = "finalsaplingroot")] + pub final_sapling_root: String, + + /// 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, + + /// Catch-all for any extra/undocumented fields. + #[serde(flatten)] + pub extra: BTreeMap, +} + +impl ResponseToError for GetBlockHeader { + type RpcError = Infallible; +} diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index d816d335a..1e44eb72d 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::peer_info::GetPeerInfo; use zaino_fetch::jsonrpsee::response::{GetMempoolInfoResponse, GetNetworkSolPsResponse}; @@ -211,6 +212,13 @@ pub trait ZcashIndexerRpc { verbosity: Option, ) -> Result; + #[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) @@ -537,6 +545,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 2bafd9d78..3a6f56422 100644 --- a/zaino-state/src/backends/fetch.rs +++ b/zaino-state/src/backends/fetch.rs @@ -23,9 +23,8 @@ use zaino_fetch::{ jsonrpsee::{ connector::{JsonRpSeeConnector, RpcError}, response::{ - block_subsidy::GetBlockSubsidy, - peer_info::GetPeerInfo, - {GetMempoolInfoResponse, GetNetworkSolPsResponse}, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, + GetMempoolInfoResponse, GetNetworkSolPsResponse, }, }, }; @@ -372,6 +371,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?.into()) + } + /// Returns the hash of the best block (tip) of the longest chain. /// online zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html) /// The zcashd doc reference above says there are no parameters and the result is a "hex" (string) of the block hash hex encoded. diff --git a/zaino-state/src/backends/state.rs b/zaino-state/src/backends/state.rs index 871ef13e3..d2628790e 100644 --- a/zaino-state/src/backends/state.rs +++ b/zaino-state/src/backends/state.rs @@ -27,8 +27,8 @@ use zaino_fetch::{ jsonrpsee::{ connector::{JsonRpSeeConnector, RpcError}, response::{ - block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, GetMempoolInfoResponse, - GetNetworkSolPsResponse, GetSubtreesResponse, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, + GetMempoolInfoResponse, GetNetworkSolPsResponse, GetSubtreesResponse, }, }, }; @@ -56,10 +56,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, @@ -390,12 +390,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(); @@ -426,7 +426,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() @@ -505,7 +505,7 @@ impl StateServiceSubscriber { next_block_hash, ); - GetBlockHeader::Object(Box::new(block_header)) + GetBlockHeaderZebra::Object(Box::new(block_header)) }; Ok(response) @@ -733,7 +733,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, @@ -743,10 +743,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, _, _) = @@ -1085,6 +1085,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 dabe5ea5a..3438876da 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -5,9 +5,8 @@ use async_trait::async_trait; use tokio::{sync::mpsc, time::timeout}; use tracing::warn; use zaino_fetch::jsonrpsee::response::{ - block_subsidy::GetBlockSubsidy, - peer_info::GetPeerInfo, - {GetMempoolInfoResponse, GetNetworkSolPsResponse}, + block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, + GetMempoolInfoResponse, GetNetworkSolPsResponse, }; use zaino_proto::proto::{ compact_formats::CompactBlock, @@ -251,6 +250,12 @@ pub trait ZcashIndexer: Send + Sync + 'static { raw_transaction_hex: String, ) -> Result; + 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 From a5dfd9e47182acab2e8a2290f57633c4eed865e3 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Wed, 15 Oct 2025 16:16:52 -0300 Subject: [PATCH 02/16] chore(`getblockheader`): add `fetch_service` and unit tests --- integration-tests/tests/fetch_service.rs | 58 +++++ .../src/jsonrpsee/response/block_header.rs | 209 +++++++++++++++++- 2 files changed, 264 insertions(+), 3 deletions(-) diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index 5dae4355a..48e68ba64 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -700,6 +700,54 @@ 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, 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.zebrad_rpc_listen_address, + false, + 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(); + assert_eq!(fetch_service_get_block_header, rpc_block_header_response); + } +} + 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, true).await; @@ -1543,6 +1591,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; @@ -1753,6 +1806,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/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 4d34bcc8f..0469b8b6f 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -3,12 +3,9 @@ use std::{collections::BTreeMap, convert::Infallible}; use serde::{Deserialize, Serialize}; -use zebra_rpc::client::BlockHeaderObject; use crate::jsonrpsee::connector::ResponseToError; -type z = BlockHeaderObject; - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum GetBlockHeader { @@ -98,3 +95,209 @@ pub struct VerboseBlockHeader { impl ResponseToError for GetBlockHeader { type RpcError = Infallible; } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{json, Value}; + + /// Zcashd verbose response. + fn zcashd_verbose_json() -> &'static str { + r#"{ + "hash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "confirmations": 10, + "height": 123456, + "version": 4, + "merkleroot": "aa11merkle", + "finalsaplingroot": "bb22sapling", + "time": 1700000000, + "nonce": "11nonce", + "solution": "22solution", + "bits": "1d00ffff", + "difficulty": 123456.789, + "chainwork": "0000000000000000000000000000000000000000000000000000000000001234", + "previousblockhash": "prevhash0001", + "nextblockhash": "nexthash0001", + "mediantime": 1700000500, + "nTx": 12 + }"# + } + + // Zebra verbose response + fn zebra_verbose_json() -> &'static str { + r#"{ + "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", + "confirmations": 3, + "height": 42, + "version": 5, + "merkleroot": "bb33merkle", + "blockcommitments": "cc44blockcommitments", + "finalsaplingroot": "dd55sapling", + "time": 1699999999, + "nonce": "33nonce", + "solution": "44solution", + "bits": "1c654321", + "difficulty": 7890.123, + "previousblockhash": "prevhash0042" + }"# + } + + #[test] + fn deserialize_verbose_zcashd_includes_chainwork_and_extra() { + let block_header: GetBlockHeader = serde_json::from_str(zcashd_verbose_json()).unwrap(); + match block_header { + GetBlockHeader::Verbose(v) => { + assert_eq!( + v.hash, + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ); + assert_eq!(v.confirmations, 10); + assert_eq!(v.height, 123_456); + assert_eq!(v.version, 4); + assert_eq!(v.merkle_root, "aa11merkle"); + assert_eq!(v.final_sapling_root, "bb22sapling"); + assert_eq!(v.time, 1_700_000_000); + assert_eq!(v.nonce, "11nonce"); + assert_eq!(v.solution, "22solution"); + assert_eq!(v.bits, "1d00ffff"); + assert!((v.difficulty - 123_456.789).abs() < f64::EPSILON); + + assert_eq!( + v.chainwork.as_deref(), + Some("0000000000000000000000000000000000000000000000000000000000001234") + ); + + assert_eq!(v.previous_block_hash.as_deref(), Some("prevhash0001")); + assert_eq!(v.next_block_hash.as_deref(), Some("nexthash0001")); + + // Extras + assert_eq!(v.extra.get("mediantime"), Some(&json!(1_700_000_500))); + assert_eq!(v.extra.get("nTx"), Some(&json!(12))); + } + _ => panic!("expected Verbose variant"), + } + } + + #[test] + fn deserialize_verbose_zebra_includes_blockcommitments_and_omits_chainwork() { + let block_header: GetBlockHeader = serde_json::from_str(zebra_verbose_json()).unwrap(); + match block_header { + GetBlockHeader::Verbose(v) => { + assert_eq!( + v.hash, + "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1" + ); + assert_eq!(v.confirmations, 3); + assert_eq!(v.height, 42); + assert_eq!(v.version, 5); + assert_eq!(v.merkle_root, "bb33merkle"); + + assert_eq!(v.block_commitments.as_deref(), Some("cc44blockcommitments")); + + assert_eq!(v.final_sapling_root, "dd55sapling"); + assert_eq!(v.time, 1_699_999_999); + assert_eq!(v.nonce, "33nonce"); + assert_eq!(v.solution, "44solution"); + assert_eq!(v.bits, "1c654321"); + assert!((v.difficulty - 7890.123).abs() < f64::EPSILON); + + assert!(v.chainwork.is_none()); + + // Zebra always sets previous + assert_eq!(v.previous_block_hash.as_deref(), Some("prevhash0042")); + assert!(v.next_block_hash.is_none()); + + // No extras + assert!(v.extra.is_empty()); + } + _ => panic!("expected Verbose variant"), + } + } + + #[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!("cc44blockcommitments")) + ); + } + + #[test] + fn zcashd_roundtrip_preserves_chainwork_and_extras() { + 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" + )) + ); + + assert_eq!(header_object.get("mediantime"), Some(&json!(1_700_000_500))); + assert_eq!(header_object.get("nTx"), Some(&json!(12))); + } + + #[test] + fn previous_and_next_optional_edges() { + // Simulate genesis + let genesis_like = r#"{ + "hash": "genesis-hash", + "confirmations": 1, + "height": 0, + "version": 4, + "merkleroot": "root", + "finalsaplingroot": "saproot", + "time": 1477641369, + "nonce": "nonce", + "solution": "solution", + "bits": "1d00ffff", + "difficulty": 1.0 + }"#; + + let block_header: GetBlockHeader = serde_json::from_str(genesis_like).unwrap(); + match block_header { + GetBlockHeader::Verbose(v) => { + assert!(v.previous_block_hash.is_none()); + assert!(v.next_block_hash.is_none()); + } + _ => panic!("expected Verbose variant"), + } + } +} From 30c83cf15ee191a1e3f9985311a19806a5ecbd2d Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 16 Oct 2025 17:12:15 -0300 Subject: [PATCH 03/16] chore(`getblockheader`): add `state_service` test --- integration-tests/tests/state_service.rs | 51 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/integration-tests/tests/state_service.rs b/integration-tests/tests/state_service.rs index 94f546627..e3795e967 100644 --- a/integration-tests/tests/state_service.rs +++ b/integration-tests/tests/state_service.rs @@ -1495,7 +1495,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)] @@ -1571,6 +1571,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 ( From 63f0c553dc52580d9eb8bca83d9637c862bcb8c3 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 16 Oct 2025 20:12:24 -0300 Subject: [PATCH 04/16] chore(`getblockheader`): use `zebra_rpc` types --- .../src/jsonrpsee/response/block_header.rs | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 0469b8b6f..323f001f2 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -4,13 +4,21 @@ use std::{collections::BTreeMap, convert::Infallible}; use serde::{Deserialize, Serialize}; +use zebra_rpc::methods::opthex; + use crate::jsonrpsee::connector::ResponseToError; +/// Response to a `getblockheader` RPC request. #[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), } @@ -20,7 +28,8 @@ pub enum GetBlockHeader { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct VerboseBlockHeader { /// The hash of the requested block. - pub hash: String, + #[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. @@ -34,20 +43,27 @@ pub struct VerboseBlockHeader { /// The merkle root of the requesteed block. #[serde(rename = "merkleroot")] - pub merkle_root: String, + pub merkle_root: zebra_chain::block::merkle::Root, /// The blockcommitments field of the requested block. Its interpretation changes /// depending on the network and height. #[serde( + with = "opthex", rename = "blockcommitments", default, skip_serializing_if = "Option::is_none" )] - pub block_commitments: Option, + pub block_commitments: Option<[u8; 32]>, /// The root of the Sapling commitment tree after applying this block. - #[serde(rename = "finalsaplingroot")] - pub final_sapling_root: String, + #[serde(with = "opthex", rename = "finalsaplingroot")] + #[serde(skip_serializing_if = "Option::is_none")] + pub final_sapling_root: Option<[u8; 32]>, + + /// The root of the Orchard commitment tree after applying this block. + #[serde(with = "opthex", rename = "finalorchardroot")] + #[serde(skip_serializing_if = "Option::is_none")] + final_orchard_root: Option<[u8; 32]>, /// The block time of the requested block header in non-leap seconds since Jan 1 1970 GMT. pub time: i64, @@ -98,8 +114,12 @@ impl ResponseToError for GetBlockHeader { #[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 { @@ -124,6 +144,7 @@ mod tests { } // Zebra verbose response + // TODO: Add finalorchardroot fn zebra_verbose_json() -> &'static str { r#"{ "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", @@ -149,13 +170,19 @@ mod tests { GetBlockHeader::Verbose(v) => { assert_eq!( v.hash, - "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + block::Hash::from_str( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() ); assert_eq!(v.confirmations, 10); assert_eq!(v.height, 123_456); assert_eq!(v.version, 4); - assert_eq!(v.merkle_root, "aa11merkle"); - assert_eq!(v.final_sapling_root, "bb22sapling"); + assert_eq!( + v.merkle_root, + block::merkle::Root::from_hex("aa11merkle").unwrap() + ); + assert_eq!(v.final_sapling_root.unwrap(), "bb22sapling".as_bytes()); assert_eq!(v.time, 1_700_000_000); assert_eq!(v.nonce, "11nonce"); assert_eq!(v.solution, "22solution"); @@ -185,16 +212,25 @@ mod tests { GetBlockHeader::Verbose(v) => { assert_eq!( v.hash, - "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1" + block::Hash::from_str( + "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1" + ) + .unwrap() ); assert_eq!(v.confirmations, 3); assert_eq!(v.height, 42); assert_eq!(v.version, 5); - assert_eq!(v.merkle_root, "bb33merkle"); + assert_eq!( + v.merkle_root, + block::merkle::Root::from_hex("bb33merkle").unwrap() + ); - assert_eq!(v.block_commitments.as_deref(), Some("cc44blockcommitments")); + assert_eq!( + v.block_commitments.unwrap(), + "cc44blockcommitments".as_bytes() + ); - assert_eq!(v.final_sapling_root, "dd55sapling"); + assert_eq!(v.final_sapling_root.unwrap(), "dd55sapling".as_bytes()); assert_eq!(v.time, 1_699_999_999); assert_eq!(v.nonce, "33nonce"); assert_eq!(v.solution, "44solution"); @@ -278,12 +314,13 @@ mod tests { fn previous_and_next_optional_edges() { // Simulate genesis let genesis_like = r#"{ - "hash": "genesis-hash", + "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", "confirmations": 1, "height": 0, "version": 4, - "merkleroot": "root", - "finalsaplingroot": "saproot", + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalorchardroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1477641369, "nonce": "nonce", "solution": "solution", @@ -297,7 +334,8 @@ mod tests { assert!(v.previous_block_hash.is_none()); assert!(v.next_block_hash.is_none()); } - _ => panic!("expected Verbose variant"), + GetBlockHeader::Compact(_) => panic!("expected Verbose variant, got Compact"), + GetBlockHeader::Unknown(_) => panic!("expected Verbose variant, got Unknown"), } } } From 2eaec5556446556358e0e1e1f54bd4e64d289e81 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 16 Oct 2025 21:56:28 -0300 Subject: [PATCH 05/16] chore(`getblockheader`): fix `block_header` test --- .../src/jsonrpsee/response/block_header.rs | 113 ++++++++++++------ 1 file changed, 76 insertions(+), 37 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 323f001f2..d7d783919 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -42,7 +42,7 @@ pub struct VerboseBlockHeader { pub version: u32, /// The merkle root of the requesteed block. - #[serde(rename = "merkleroot")] + #[serde(with = "hex", rename = "merkleroot")] pub merkle_root: zebra_chain::block::merkle::Root, /// The blockcommitments field of the requested block. Its interpretation changes @@ -61,8 +61,12 @@ pub struct VerboseBlockHeader { pub final_sapling_root: Option<[u8; 32]>, /// The root of the Orchard commitment tree after applying this block. - #[serde(with = "opthex", rename = "finalorchardroot")] - #[serde(skip_serializing_if = "Option::is_none")] + #[serde( + with = "opthex", + rename = "finalorchardroot", + default, + skip_serializing_if = "Option::is_none" + )] final_orchard_root: Option<[u8; 32]>, /// The block time of the requested block header in non-leap seconds since Jan 1 1970 GMT. @@ -128,16 +132,16 @@ mod tests { "confirmations": 10, "height": 123456, "version": 4, - "merkleroot": "aa11merkle", - "finalsaplingroot": "bb22sapling", + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1700000000, "nonce": "11nonce", "solution": "22solution", "bits": "1d00ffff", "difficulty": 123456.789, "chainwork": "0000000000000000000000000000000000000000000000000000000000001234", - "previousblockhash": "prevhash0001", - "nextblockhash": "nexthash0001", + "previousblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "nextblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "mediantime": 1700000500, "nTx": 12 }"# @@ -165,43 +169,67 @@ mod tests { #[test] fn deserialize_verbose_zcashd_includes_chainwork_and_extra() { - let block_header: GetBlockHeader = serde_json::from_str(zcashd_verbose_json()).unwrap(); - match block_header { - GetBlockHeader::Verbose(v) => { + match serde_json::from_str::(zcashd_verbose_json()) { + Ok(block_header) => { assert_eq!( - v.hash, + block_header.hash, block::Hash::from_str( "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" ) .unwrap() ); - assert_eq!(v.confirmations, 10); - assert_eq!(v.height, 123_456); - assert_eq!(v.version, 4); + assert_eq!(block_header.confirmations, 10); + assert_eq!(block_header.height, 123_456); + assert_eq!(block_header.version, 4); assert_eq!( - v.merkle_root, - block::merkle::Root::from_hex("aa11merkle").unwrap() + 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!(v.final_sapling_root.unwrap(), "bb22sapling".as_bytes()); - assert_eq!(v.time, 1_700_000_000); - assert_eq!(v.nonce, "11nonce"); - assert_eq!(v.solution, "22solution"); - assert_eq!(v.bits, "1d00ffff"); - assert!((v.difficulty - 123_456.789).abs() < f64::EPSILON); + 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!( - v.chainwork.as_deref(), + block_header.chainwork.as_deref(), Some("0000000000000000000000000000000000000000000000000000000000001234") ); - assert_eq!(v.previous_block_hash.as_deref(), Some("prevhash0001")); - assert_eq!(v.next_block_hash.as_deref(), Some("nexthash0001")); + assert_eq!( + block_header.previous_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); + assert_eq!( + block_header.next_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); // Extras - assert_eq!(v.extra.get("mediantime"), Some(&json!(1_700_000_500))); - assert_eq!(v.extra.get("nTx"), Some(&json!(12))); + assert_eq!( + block_header.extra.get("mediantime"), + Some(&json!(1_700_000_500)) + ); + assert_eq!(block_header.extra.get("nTx"), Some(&json!(12))); + } + Err(e) => { + panic!( + "VerboseBlockHeader failed at {}:{} — {}", + e.line(), + e.column(), + e + ); } - _ => panic!("expected Verbose variant"), } } @@ -222,7 +250,10 @@ mod tests { assert_eq!(v.version, 5); assert_eq!( v.merkle_root, - block::merkle::Root::from_hex("bb33merkle").unwrap() + block::merkle::Root::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() ); assert_eq!( @@ -230,7 +261,10 @@ mod tests { "cc44blockcommitments".as_bytes() ); - assert_eq!(v.final_sapling_root.unwrap(), "dd55sapling".as_bytes()); + assert_eq!( + v.final_sapling_root.unwrap(), + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f".as_bytes() + ); assert_eq!(v.time, 1_699_999_999); assert_eq!(v.nonce, "33nonce"); assert_eq!(v.solution, "44solution"); @@ -328,14 +362,19 @@ mod tests { "difficulty": 1.0 }"#; - let block_header: GetBlockHeader = serde_json::from_str(genesis_like).unwrap(); - match block_header { - GetBlockHeader::Verbose(v) => { - assert!(v.previous_block_hash.is_none()); - assert!(v.next_block_hash.is_none()); + 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 + ); } - GetBlockHeader::Compact(_) => panic!("expected Verbose variant, got Compact"), - GetBlockHeader::Unknown(_) => panic!("expected Verbose variant, got Unknown"), } } } From 54dae0e3cf6fe6ecd4c3dc86b9e7c957b7332b1e Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 16 Oct 2025 22:58:02 -0300 Subject: [PATCH 06/16] chore(`getblockheader`): fix `block_header` tests --- .../src/jsonrpsee/response/block_header.rs | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index d7d783919..09acd7306 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -155,15 +155,15 @@ mod tests { "confirmations": 3, "height": 42, "version": 5, - "merkleroot": "bb33merkle", - "blockcommitments": "cc44blockcommitments", - "finalsaplingroot": "dd55sapling", + "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "blockcommitments": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1699999999, "nonce": "33nonce", "solution": "44solution", "bits": "1c654321", "difficulty": 7890.123, - "previousblockhash": "prevhash0042" + "previousblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" }"# } @@ -235,21 +235,20 @@ mod tests { #[test] fn deserialize_verbose_zebra_includes_blockcommitments_and_omits_chainwork() { - let block_header: GetBlockHeader = serde_json::from_str(zebra_verbose_json()).unwrap(); - match block_header { - GetBlockHeader::Verbose(v) => { + match serde_json::from_str::(zebra_verbose_json()) { + Ok(block_header) => { assert_eq!( - v.hash, + block_header.hash, block::Hash::from_str( "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1" ) .unwrap() ); - assert_eq!(v.confirmations, 3); - assert_eq!(v.height, 42); - assert_eq!(v.version, 5); + assert_eq!(block_header.confirmations, 3); + assert_eq!(block_header.height, 42); + assert_eq!(block_header.version, 5); assert_eq!( - v.merkle_root, + block_header.merkle_root, block::merkle::Root::from_hex( "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" ) @@ -257,30 +256,46 @@ mod tests { ); assert_eq!( - v.block_commitments.unwrap(), - "cc44blockcommitments".as_bytes() + block_header.block_commitments.unwrap(), + <[u8; 32]>::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() ); assert_eq!( - v.final_sapling_root.unwrap(), - "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f".as_bytes() + block_header.final_sapling_root.unwrap(), + <[u8; 32]>::from_hex( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + ) + .unwrap() ); - assert_eq!(v.time, 1_699_999_999); - assert_eq!(v.nonce, "33nonce"); - assert_eq!(v.solution, "44solution"); - assert_eq!(v.bits, "1c654321"); - assert!((v.difficulty - 7890.123).abs() < f64::EPSILON); + 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!(v.chainwork.is_none()); + assert!(block_header.chainwork.is_none()); // Zebra always sets previous - assert_eq!(v.previous_block_hash.as_deref(), Some("prevhash0042")); - assert!(v.next_block_hash.is_none()); + assert_eq!( + block_header.previous_block_hash.as_deref(), + Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") + ); + assert!(block_header.next_block_hash.is_none()); // No extras - assert!(v.extra.is_empty()); + assert!(block_header.extra.is_empty()); + } + Err(e) => { + panic!( + "VerboseBlockHeader failed at {}:{} — {}", + e.line(), + e.column(), + e + ); } - _ => panic!("expected Verbose variant"), } } @@ -323,7 +338,9 @@ mod tests { assert_eq!( header_object.get("blockcommitments"), - Some(&json!("cc44blockcommitments")) + Some(&json!( + "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" + )) ); } From 529ec8e66ceb93b9bd5f6dccc3588984972fd513 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 17 Oct 2025 17:35:29 -0300 Subject: [PATCH 07/16] chore(`getblockheader`): expose endpoint & add `json_server` test --- integration-tests/tests/json_server.rs | 41 +++++++++++++++++++ zaino-fetch/src/jsonrpsee/connector.rs | 22 +++++++--- .../src/jsonrpsee/response/block_header.rs | 40 ++++++++++++++++-- zaino-serve/src/rpc/jsonrpc/service.rs | 11 +++++ zaino-state/src/indexer.rs | 11 +++++ 5 files changed, 117 insertions(+), 8 deletions(-) diff --git a/integration-tests/tests/json_server.rs b/integration-tests/tests/json_server.rs index dd0c03b2d..bf69fbf23 100644 --- a/integration-tests/tests/json_server.rs +++ b/integration-tests/tests/json_server.rs @@ -671,6 +671,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)] @@ -776,6 +778,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, 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/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index 631051421..d595614ac 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -25,7 +25,9 @@ use zebra_rpc::client::ValidateAddressResponse; use crate::jsonrpsee::{ error::{JsonRpcError, TransportError}, response::{ - block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, peer_info::GetPeerInfo, + block_header::{GetBlockHeader, GetBlockHeaderError}, + block_subsidy::GetBlockSubsidy, + peer_info::GetPeerInfo, GetBalanceError, GetBalanceResponse, GetBlockCountResponse, GetBlockError, GetBlockHash, GetBlockResponse, GetBlockchainInfoResponse, GetInfoResponse, GetMempoolInfoResponse, GetSubtreesError, GetSubtreesResponse, GetTransactionResponse, GetTreestateError, @@ -546,15 +548,25 @@ impl JsonRpSeeConnector { } } - // TODO: handle error cases + /// 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 . + /// + /// # 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> { + ) -> Result> { let params = [ - serde_json::to_value(hash).unwrap(), - serde_json::to_value(verbose).unwrap(), + serde_json::to_value(hash).map_err(RpcRequestError::JsonRpc)?, + serde_json::to_value(verbose).map_err(RpcRequestError::JsonRpc)?, ]; self.send_request("getblockheader", params).await } diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 09acd7306..08d48f377 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use zebra_rpc::methods::opthex; -use crate::jsonrpsee::connector::ResponseToError; +use crate::jsonrpsee::connector::{ResponseToError, RpcError}; /// Response to a `getblockheader` RPC request. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -22,6 +22,18 @@ pub enum GetBlockHeader { 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. @@ -113,7 +125,21 @@ pub struct VerboseBlockHeader { } impl ResponseToError for GetBlockHeader { - type RpcError = Infallible; + 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)] @@ -148,7 +174,6 @@ mod tests { } // Zebra verbose response - // TODO: Add finalorchardroot fn zebra_verbose_json() -> &'static str { r#"{ "hash": "00000000001b76b932f31289beccd3988d098ec3c8c6e4a0c7bcaf52e9bdead1", @@ -158,6 +183,7 @@ mod tests { "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "blockcommitments": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", + "finalorchardroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1699999999, "nonce": "33nonce", "solution": "44solution", @@ -270,6 +296,14 @@ mod tests { ) .unwrap() ); + + assert_eq!( + block_header.final_orchard_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"); diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index 1e44eb72d..eeda5ea6a 100644 --- a/zaino-serve/src/rpc/jsonrpc/service.rs +++ b/zaino-serve/src/rpc/jsonrpc/service.rs @@ -212,6 +212,17 @@ 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 . + /// + /// # 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 #[method(name = "getblockheader")] async fn get_block_header( &self, diff --git a/zaino-state/src/indexer.rs b/zaino-state/src/indexer.rs index 3438876da..90416aef8 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -250,6 +250,17 @@ 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 . + /// + /// # 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, From d737092f7eb632d4fb0288733f7d180bf457b969 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 17 Oct 2025 17:36:53 -0300 Subject: [PATCH 08/16] chore(`getblockheader`): run clippy --- zaino-fetch/src/jsonrpsee/response/block_header.rs | 2 +- zaino-state/src/backends/fetch.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 08d48f377..34977f218 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -1,6 +1,6 @@ //! Types associated with the `getblockheader` RPC request. -use std::{collections::BTreeMap, convert::Infallible}; +use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; diff --git a/zaino-state/src/backends/fetch.rs b/zaino-state/src/backends/fetch.rs index 3a6f56422..bb8fc6dd1 100644 --- a/zaino-state/src/backends/fetch.rs +++ b/zaino-state/src/backends/fetch.rs @@ -376,7 +376,7 @@ impl ZcashIndexer for FetchServiceSubscriber { hash: String, verbose: bool, ) -> Result { - Ok(self.fetcher.get_block_header(hash, verbose).await?.into()) + Ok(self.fetcher.get_block_header(hash, verbose).await?) } /// Returns the hash of the best block (tip) of the longest chain. From 3cd901ed6a8cbabfc0625c876f1d77dc9bf95f68 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 17 Oct 2025 18:00:41 -0300 Subject: [PATCH 09/16] chore(`getblockheader`): re-export surface types --- zaino-fetch/src/jsonrpsee/response/block_header.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index 34977f218..bbbd070e9 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -9,6 +9,7 @@ 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 { From 55e6e53a559e72d03315b263cccdce8cbc811cd1 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Fri, 17 Oct 2025 18:26:20 -0300 Subject: [PATCH 10/16] chore(`getblockheader`): fix doc comments --- zaino-fetch/src/jsonrpsee/connector.rs | 4 ++-- zaino-serve/src/rpc/jsonrpc/service.rs | 4 ++-- zaino-state/src/indexer.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index d595614ac..ad7d7c60a 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -548,8 +548,8 @@ 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 . + /// 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 /// diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index eeda5ea6a..dd250949f 100644 --- a/zaino-serve/src/rpc/jsonrpc/service.rs +++ b/zaino-serve/src/rpc/jsonrpc/service.rs @@ -212,8 +212,8 @@ 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 . + /// 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 /// diff --git a/zaino-state/src/indexer.rs b/zaino-state/src/indexer.rs index 90416aef8..298fa1940 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -250,8 +250,8 @@ 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 . + /// 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 /// From ca4a2808866124a324be4713139bddc1f6cf6ec5 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Mon, 20 Oct 2025 18:07:41 -0300 Subject: [PATCH 11/16] docs(`getblockheader`): add doc comments --- zaino-fetch/src/jsonrpsee/response/common.rs | 14 ++++++++++++++ .../src/jsonrpsee/response/common/amount.rs | 2 ++ 2 files changed, 16 insertions(+) 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 } From ff8df887f9722d65e21703fe27f95290e1ff3c1d Mon Sep 17 00:00:00 2001 From: al amoda Date: Thu, 30 Oct 2025 06:28:26 -0400 Subject: [PATCH 12/16] resolve postmerge breakage --- integration-tests/tests/fetch_service.rs | 5 ++--- integration-tests/tests/json_server.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index 05b5aefb3..b39f9f2e1 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -715,7 +715,7 @@ async fn fetch_service_get_block(validator: &ValidatorKind) { 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, true).await; + create_test_manager_and_fetch_service(validator, None, true, true, true).await; const BLOCK_LIMIT: u32 = 10; @@ -740,8 +740,7 @@ async fn fetch_service_get_block_header(validator: &ValidatorKind) { let jsonrpc_client = JsonRpSeeConnector::new_with_basic_auth( test_node_and_return_url( - test_manager.zebrad_rpc_listen_address, - false, + test_manager.full_node_rpc_listen_address, None, Some("xxxxxx".to_string()), Some("xxxxxx".to_string()), diff --git a/integration-tests/tests/json_server.rs b/integration-tests/tests/json_server.rs index beb0b1202..6588d8a7a 100644 --- a/integration-tests/tests/json_server.rs +++ b/integration-tests/tests/json_server.rs @@ -796,7 +796,7 @@ mod zcashd { zcashd_subscriber, _zaino_service, zaino_subscriber, - ) = create_test_manager_and_fetch_services(false, false).await; + ) = create_test_manager_and_fetch_services(false).await; const BLOCK_LIMIT: u32 = 10; From 497a0303e57e78216b9e8848086b7812db3ad734 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 30 Oct 2025 22:19:34 -0300 Subject: [PATCH 13/16] chore(`getblockheader`): more tests & comments --- integration-tests/tests/fetch_service.rs | 15 +++++++++++++++ zaino-serve/src/rpc/jsonrpc/service.rs | 2 ++ 2 files changed, 17 insertions(+) diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index 44d5bb914..dfb8a2024 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -758,7 +758,22 @@ async fn fetch_service_get_block_header(validator: &ValidatorKind) { .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(), false) + .await + .unwrap(); + + let rpc_block_header_response_verbose = jsonrpc_client + .get_block_header(block_hash.to_string(), false) + .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 + ); } } diff --git a/zaino-serve/src/rpc/jsonrpc/service.rs b/zaino-serve/src/rpc/jsonrpc/service.rs index 7c492dd51..d5bfa42f9 100644 --- a/zaino-serve/src/rpc/jsonrpc/service.rs +++ b/zaino-serve/src/rpc/jsonrpc/service.rs @@ -228,6 +228,8 @@ pub trait ZcashIndexerRpc { /// - 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")] From 8dc98d14e8ea2ce4ac87b0fd7135df9c30067dbf Mon Sep 17 00:00:00 2001 From: dorianvp Date: Thu, 30 Oct 2025 22:22:46 -0300 Subject: [PATCH 14/16] chore(`getblockheader`): fix test --- integration-tests/tests/fetch_service.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/tests/fetch_service.rs b/integration-tests/tests/fetch_service.rs index dfb8a2024..017beab08 100644 --- a/integration-tests/tests/fetch_service.rs +++ b/integration-tests/tests/fetch_service.rs @@ -760,12 +760,12 @@ async fn fetch_service_get_block_header(validator: &ValidatorKind) { .unwrap(); let fetch_service_get_block_header_verbose = fetch_service_subscriber - .get_block_header(block_hash.to_string(), false) + .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(), false) + .get_block_header(block_hash.to_string(), true) .await .unwrap(); From 97113f9831ed835945d9c15d6b12084e6f425bf7 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Sat, 1 Nov 2025 23:36:23 -0300 Subject: [PATCH 15/16] chore(`getblockheader`): remove `extra` fields --- .../src/jsonrpsee/response/block_header.rs | 28 +++---------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index bbbd070e9..d88b17608 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -1,7 +1,5 @@ //! Types associated with the `getblockheader` RPC request. -use std::collections::BTreeMap; - use serde::{Deserialize, Serialize}; use zebra_rpc::methods::opthex; @@ -39,6 +37,7 @@ pub enum GetBlockHeaderError { /// /// 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")] @@ -119,10 +118,6 @@ pub struct VerboseBlockHeader { skip_serializing_if = "Option::is_none" )] pub next_block_hash: Option, - - /// Catch-all for any extra/undocumented fields. - #[serde(flatten)] - pub extra: BTreeMap, } impl ResponseToError for GetBlockHeader { @@ -168,9 +163,7 @@ mod tests { "difficulty": 123456.789, "chainwork": "0000000000000000000000000000000000000000000000000000000000001234", "previousblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", - "nextblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", - "mediantime": 1700000500, - "nTx": 12 + "nextblockhash": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f" }"# } @@ -195,7 +188,7 @@ mod tests { } #[test] - fn deserialize_verbose_zcashd_includes_chainwork_and_extra() { + fn deserialize_verbose_zcashd_includes_chainwork() { match serde_json::from_str::(zcashd_verbose_json()) { Ok(block_header) => { assert_eq!( @@ -241,13 +234,6 @@ mod tests { block_header.next_block_hash.as_deref(), Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") ); - - // Extras - assert_eq!( - block_header.extra.get("mediantime"), - Some(&json!(1_700_000_500)) - ); - assert_eq!(block_header.extra.get("nTx"), Some(&json!(12))); } Err(e) => { panic!( @@ -319,9 +305,6 @@ mod tests { Some("000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f") ); assert!(block_header.next_block_hash.is_none()); - - // No extras - assert!(block_header.extra.is_empty()); } Err(e) => { panic!( @@ -380,7 +363,7 @@ mod tests { } #[test] - fn zcashd_roundtrip_preserves_chainwork_and_extras() { + 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(); @@ -391,9 +374,6 @@ mod tests { "0000000000000000000000000000000000000000000000000000000000001234" )) ); - - assert_eq!(header_object.get("mediantime"), Some(&json!(1_700_000_500))); - assert_eq!(header_object.get("nTx"), Some(&json!(12))); } #[test] From 7a6de96d6dc820abce9e7837f865532cc40d89a5 Mon Sep 17 00:00:00 2001 From: dorianvp Date: Sun, 2 Nov 2025 01:07:38 -0300 Subject: [PATCH 16/16] chore(`getblockheader`): remove invalid `finalorchardroot` field --- .../src/jsonrpsee/response/block_header.rs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/zaino-fetch/src/jsonrpsee/response/block_header.rs b/zaino-fetch/src/jsonrpsee/response/block_header.rs index d88b17608..2edeb23de 100644 --- a/zaino-fetch/src/jsonrpsee/response/block_header.rs +++ b/zaino-fetch/src/jsonrpsee/response/block_header.rs @@ -59,6 +59,8 @@ pub struct VerboseBlockHeader { /// 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", @@ -72,15 +74,6 @@ pub struct VerboseBlockHeader { #[serde(skip_serializing_if = "Option::is_none")] pub final_sapling_root: Option<[u8; 32]>, - /// The root of the Orchard commitment tree after applying this block. - #[serde( - with = "opthex", - rename = "finalorchardroot", - default, - skip_serializing_if = "Option::is_none" - )] - final_orchard_root: Option<[u8; 32]>, - /// The block time of the requested block header in non-leap seconds since Jan 1 1970 GMT. pub time: i64, @@ -177,7 +170,6 @@ mod tests { "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "blockcommitments": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", - "finalorchardroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1699999999, "nonce": "33nonce", "solution": "44solution", @@ -284,13 +276,6 @@ mod tests { .unwrap() ); - assert_eq!( - block_header.final_orchard_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"); @@ -386,7 +371,6 @@ mod tests { "version": 4, "merkleroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "finalsaplingroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", - "finalorchardroot": "000000000053d2771290ff1b57181bd067ae0e55a367ba8ddee2d961ea27a14f", "time": 1477641369, "nonce": "nonce", "solution": "solution",