From 94530f881193a577329ad2aa1522b4442ef4f3b7 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:43:49 -0300 Subject: [PATCH 1/3] feat(rpc)!: add `getblockheader` verbosity and extract header formatting Implement verbosity parameter for `getblockheader` RPC with both raw and verbose modes. Extract `get_block_header_inner()` helper method to retrieve headers by hash, consolidating previously duplicated logic. Introduce `get_block_header_verbose_inner()` for consistent header formatting across RPC methods. Expose the verbosity optional parameter in the FlorestaRPC trait's `get_block_header` method, allowing clients to choose between raw (hex string) and verbose (JSON object) response formats. Both `getblockheader` and `getblock` now reuse the same header formatting logic, reducing code duplication and improving maintainability. Closes: #606 --- bin/floresta-cli/src/main.rs | 9 +- .../floresta-node/src/json_rpc/blockchain.rs | 128 ++++++++++++------ crates/floresta-node/src/json_rpc/res.rs | 31 +++++ crates/floresta-node/src/json_rpc/server.rs | 7 +- crates/floresta-rpc/src/lib.rs | 10 +- crates/floresta-rpc/src/rpc.rs | 20 ++- crates/floresta-rpc/src/rpc_types.rs | 13 ++ 7 files changed, 168 insertions(+), 50 deletions(-) diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 92ae76f97..6c646da70 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -85,8 +85,8 @@ fn do_request(cmd: &Cli, client: Client) -> anyhow::Result { Methods::SendRawTransaction { tx } => { serde_json::to_string_pretty(&client.send_raw_transaction(tx)?)? } - Methods::GetBlockHeader { hash } => { - serde_json::to_string_pretty(&client.get_block_header(hash)?)? + Methods::GetBlockHeader { hash, verbosity } => { + serde_json::to_string_pretty(&client.get_block_header(hash, verbosity)?)? } Methods::LoadDescriptor { desc } => { serde_json::to_string_pretty(&client.load_descriptor(desc)?)? @@ -263,7 +263,10 @@ pub enum Methods { /// Returns the block header for the given block hash #[command(name = "getblockheader")] - GetBlockHeader { hash: BlockHash }, + GetBlockHeader { + hash: BlockHash, + verbosity: Option, + }, /// Loads a new descriptor to the watch only wallet #[doc = include_str!("../../../doc/rpc/loaddescriptor.md")] diff --git a/crates/floresta-node/src/json_rpc/blockchain.rs b/crates/floresta-node/src/json_rpc/blockchain.rs index a33e75484..8254e2f90 100644 --- a/crates/floresta-node/src/json_rpc/blockchain.rs +++ b/crates/floresta-node/src/json_rpc/blockchain.rs @@ -15,6 +15,7 @@ use bitcoin::ScriptBuf; use bitcoin::Txid; use bitcoin::VarInt; use corepc_types::v29::GetTxOut; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use corepc_types::ScriptPubkey; use floresta_chain::extensions::HeaderExt; @@ -24,6 +25,7 @@ use serde_json::json; use serde_json::Value; use tracing::debug; +use super::res::GetBlockHeaderRes; use super::res::GetBlockchainInfoRes; use super::res::GetTxOutProof; use super::res::JsonRpcError; @@ -106,7 +108,7 @@ impl RpcImpl { at: u32, ) -> Result { let hash = provider.get_block_hash(at)?; - let block = provider.get_block_header(hash)?; + let block = provider.get_block_header_inner(hash)?; Ok(block.time) } @@ -182,20 +184,7 @@ impl RpcImpl { return Ok(GetBlockRes::Zero(hex)); } if verbosity == 1 { - let header = &block.header; - let height = header.get_height(&self.chain)?; - let median_time = header.calculate_median_time_past(&self.chain)?; - let chain_work = header.calculate_chain_work(&self.chain)?.to_string_hex(); - let confirmations = header.get_confirmations(&self.chain)? as i64; - let version_hex = header.get_version_hex(); - - let next_block_hash = header - .get_next_block_hash(&self.chain)? - .map(|h| h.to_string()); - - let bits = header.get_bits_hex(); - let difficulty = header.get_difficulty(); - let target = header.get_target_hex(); + let header_fields = self.get_block_header_verbose_inner(&block)?; // Stripped size is the size of the block without witness data // Header + VarInt for number of transactions + sum of base sizes of each transaction @@ -203,10 +192,7 @@ impl RpcImpl { let total_tx_base_size: usize = block.txdata.iter().map(|tx| tx.base_size()).sum(); let stripped_size_bytes = Header::SIZE + tx_count_varint_size + total_tx_base_size; - let stripped_size = Some(stripped_size_bytes as i64); - - let previous_block_hash = (header.prev_blockhash != BlockHash::all_zeros()) - .then_some(header.prev_blockhash.to_string()); + let stripped_size = Some(stripped_size_bytes.try_into()?); let tx = block .txdata @@ -215,26 +201,26 @@ impl RpcImpl { .collect(); let block = GetBlockVerboseOne { - bits, - chain_work, - confirmations, - difficulty, - hash: header.block_hash().to_string(), - height: height as i64, - merkle_root: header.merkle_root.to_string(), - nonce: header.nonce as i64, - previous_block_hash, - size: block.total_size() as i64, - time: header.time as i64, + bits: header_fields.bits, + chain_work: header_fields.chain_work, + confirmations: header_fields.confirmations, + difficulty: header_fields.difficulty, + hash: header_fields.hash, + height: header_fields.height, + merkle_root: header_fields.merkle_root, + nonce: header_fields.nonce, + previous_block_hash: header_fields.previous_block_hash, + size: block.total_size().try_into()?, + time: header_fields.time, tx, - version: header.version.to_consensus(), - version_hex, + version: header_fields.version, + version_hex: header_fields.version_hex, weight: block.weight().to_wu(), - median_time: Some(median_time as i64), - n_tx: block.txdata.len() as i64, - next_block_hash, + median_time: Some(header_fields.median_time), + n_tx: header_fields.n_tx.into(), + next_block_hash: header_fields.next_block_hash, stripped_size, - target, + target: header_fields.target, }; return Ok(GetBlockRes::One(Box::new(block))); @@ -302,10 +288,23 @@ impl RpcImpl { } // getblockheader - pub(super) fn get_block_header(&self, hash: BlockHash) -> Result { - self.chain - .get_block_header(&hash) - .map_err(|_| JsonRpcError::BlockNotFound) + pub(super) async fn get_block_header( + &self, + hash: BlockHash, + verbosity: bool, + ) -> Result { + let header = self.get_block_header_inner(hash)?; + + if !verbosity { + let hex = serialize_hex(&header); + return Ok(GetBlockHeaderRes::Raw(hex)); + } + + let block = self.get_block_inner(hash).await?; + + let get_block_header = self.get_block_header_verbose_inner(&block)?; + + Ok(GetBlockHeaderRes::Verbose(Box::new(get_block_header))) } // getblockstats @@ -320,6 +319,55 @@ impl RpcImpl { // getmempoolinfo // getrawmempool + /// Same as `get_block_header_inner` but verbose. + fn get_block_header_verbose_inner( + &self, + block: &Block, + ) -> Result { + let header = &block.header; + let height = header.get_height(&self.chain)?; + let median_time = header.calculate_median_time_past(&self.chain)?; + let chain_work = header.calculate_chain_work(&self.chain)?.to_string_hex(); + let confirmations = header.get_confirmations(&self.chain)?; + let version_hex = header.get_version_hex(); + + let next_block_hash = header + .get_next_block_hash(&self.chain)? + .map(|h| h.to_string()); + + let bits = header.get_bits_hex(); + let difficulty = header.get_difficulty(); + let target = header.get_target_hex(); + let previous_block_hash = (header.prev_blockhash != BlockHash::all_zeros()) + .then_some(header.prev_blockhash.to_string()); + + Ok(GetBlockHeaderVerbose { + bits, + chain_work, + confirmations: confirmations.into(), + difficulty, + hash: header.block_hash().to_string(), + height: height.into(), + median_time: median_time.into(), + next_block_hash, + version: header.version.to_consensus(), + version_hex, + previous_block_hash, + merkle_root: header.merkle_root.to_string(), + time: header.time.into(), + target, + nonce: header.nonce.into(), + n_tx: block.txdata.len().try_into()?, + }) + } + + /// Helper method to get a block header by its hash, used by multiple rpcs. + fn get_block_header_inner(&self, hash: BlockHash) -> Result { + self.chain + .get_block_header(&hash) + .map_err(|_| JsonRpcError::BlockNotFound) + } + /// Check if the script is anchor type fn is_anchor_type(script: &Script) -> bool { script.as_bytes().starts_with(&[0x51, 0x02, 0x4e, 0x73]) diff --git a/crates/floresta-node/src/json_rpc/res.rs b/crates/floresta-node/src/json_rpc/res.rs index d62faf411..5710cc1d2 100644 --- a/crates/floresta-node/src/json_rpc/res.rs +++ b/crates/floresta-node/src/json_rpc/res.rs @@ -4,8 +4,11 @@ use core::fmt; use core::fmt::Debug; use core::fmt::Display; use core::fmt::Formatter; +use core::num::TryFromIntError; +use std::convert::Infallible; use axum::response::IntoResponse; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use floresta_chain::extensions::HeaderExtError; use floresta_common::impl_error_from; @@ -131,6 +134,18 @@ pub enum GetBlockRes { One(Box), } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +/// The response for `getblockheader`, which can be either a raw hex-encoded block header or a verbose +/// one with all the fields parsed and decoded. +pub enum GetBlockHeaderRes { + /// The raw hex-encoded block header, as returned by `getblockheader` with verbosity false + Raw(String), + + /// A verbose block header, as returned by `getblockheader` with verbosity true + Verbose(Box), +} + #[derive(Debug, Deserialize, Serialize)] pub struct RpcError { pub code: i32, @@ -226,6 +241,9 @@ pub enum JsonRpcError { /// Something went wrong when attempting to publish a transaction to mempool MempoolAccept(MempoolError), + + /// A numeric conversion overflows, e.g., u64 to u32 + ConversionOverflow(String), } impl_error_from!(JsonRpcError, MempoolError, MempoolAccept); @@ -260,6 +278,7 @@ impl Display for JsonRpcError { JsonRpcError::InvalidDisconnectNodeCommand => write!(f, "Invalid disconnectnode command"), JsonRpcError::PeerNotFound => write!(f, "Peer not found in the peer list"), JsonRpcError::MempoolAccept(e) => write!(f, "Could not send transaction to mempool due to {e}"), + JsonRpcError::ConversionOverflow(e) => write!(f, "Numeric conversion overflow: {e}"), } } } @@ -289,6 +308,18 @@ impl From for JsonRpcError { } } +impl From for JsonRpcError { + fn from(e: TryFromIntError) -> Self { + JsonRpcError::ConversionOverflow(e.to_string()) + } +} + +impl From for JsonRpcError { + fn from(e: Infallible) -> Self { + JsonRpcError::ConversionOverflow(e.to_string()) + } +} + impl_error_from!(JsonRpcError, DescriptorError, InvalidDescriptor); impl From> for JsonRpcError { diff --git a/crates/floresta-node/src/json_rpc/server.rs b/crates/floresta-node/src/json_rpc/server.rs index 32c87c25b..2fd8ed4c6 100644 --- a/crates/floresta-node/src/json_rpc/server.rs +++ b/crates/floresta-node/src/json_rpc/server.rs @@ -255,8 +255,11 @@ async fn handle_json_rpc_request( "getblockheader" => { let hash = get_hash(¶ms, 0, "block_hash")?; + let verbosity = get_optional_field(¶ms, 1, "verbosity", get_bool)?.unwrap_or(true); + state - .get_block_header(hash) + .get_block_header(hash, verbosity) + .await .map(|h| serde_json::to_value(h).unwrap()) } @@ -440,6 +443,7 @@ fn get_http_error_code(err: &JsonRpcError) -> u16 { | JsonRpcError::InvalidParameterType(_) | JsonRpcError::MissingParameter(_) | JsonRpcError::ChainWorkOverflow + | JsonRpcError::ConversionOverflow(_) | JsonRpcError::MempoolAccept(_) | JsonRpcError::Wallet(_) => 400, @@ -480,6 +484,7 @@ fn get_json_rpc_error_code(err: &JsonRpcError) -> i32 { | JsonRpcError::InvalidRescanVal | JsonRpcError::NoAddressesToRescan | JsonRpcError::ChainWorkOverflow + | JsonRpcError::ConversionOverflow(_) | JsonRpcError::Wallet(_) | JsonRpcError::MempoolAccept(_) => -32600, diff --git a/crates/floresta-rpc/src/lib.rs b/crates/floresta-rpc/src/lib.rs index b42368e82..fafe3e015 100644 --- a/crates/floresta-rpc/src/lib.rs +++ b/crates/floresta-rpc/src/lib.rs @@ -43,6 +43,7 @@ mod tests { use crate::jsonrpc_client::Client; use crate::rpc::FlorestaRPC; + use crate::rpc_types::GetBlockHeaderRes; use crate::rpc_types::GetBlockRes; struct Florestad { @@ -226,9 +227,14 @@ mod tests { let (_proc, client) = start_florestad(); let blockhash = client.get_block_hash(0).expect("rpc not working"); - let block_header = client.get_block_header(blockhash).expect("rpc not working"); + let block_header = client + .get_block_header(blockhash, Some(true)) + .expect("rpc not working"); + let GetBlockHeaderRes::Verbose(block_header) = block_header else { + panic!("Expected verbose block header"); + }; - assert_eq!(block_header.block_hash(), blockhash); + assert_eq!(block_header.hash, blockhash.to_string()); } #[test] diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index 6c419af32..c39176be2 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use core::fmt::Debug; +use std::vec; -use bitcoin::block::Header as BlockHeader; use bitcoin::BlockHash; use bitcoin::Txid; use corepc_types::v29::GetTxOut; @@ -42,7 +42,11 @@ pub trait FlorestaRPC { /// in the Bitcoin protocol specification. A header contains the block's version, /// the previous block hash, the merkle root, the timestamp, the difficulty target, /// and the nonce. - fn get_block_header(&self, hash: BlockHash) -> Result; + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result; /// Gets a transaction from the blockchain /// /// This method returns a transaction that's cached in our wallet. If the verbosity flag is @@ -323,8 +327,16 @@ impl FlorestaRPC for T { self.call("getblockfilter", &[Value::Number(Number::from(height))]) } - fn get_block_header(&self, hash: BlockHash) -> Result { - self.call("getblockheader", &[Value::String(hash.to_string())]) + fn get_block_header( + &self, + hash: BlockHash, + verbosity: Option, + ) -> Result { + let mut params = vec![Value::String(hash.to_string())]; + if let Some(verbosity) = verbosity { + params.push(Value::Bool(verbosity)); + } + self.call("getblockheader", ¶ms) } fn get_blockchain_info(&self) -> Result { diff --git a/crates/floresta-rpc/src/rpc_types.rs b/crates/floresta-rpc/src/rpc_types.rs index 1ebce51d7..02377eb1b 100644 --- a/crates/floresta-rpc/src/rpc_types.rs +++ b/crates/floresta-rpc/src/rpc_types.rs @@ -5,6 +5,7 @@ use core::fmt; use core::fmt::Display; use core::fmt::Formatter; +use corepc_types::v30::GetBlockHeaderVerbose; use corepc_types::v30::GetBlockVerboseOne; use serde::Deserialize; use serde::Serialize; @@ -196,6 +197,18 @@ pub enum GetBlockRes { One(Box), } +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +/// The response for getblockheader, which can be either a raw hex-encoded block header or a verbose +/// one with all the fields parsed and decoded. +pub enum GetBlockHeaderRes { + /// The raw hex-encoded block header, as returned by getblockheader with verbosity false + Raw(String), + + /// A verbose block header, as returned by getblockheader with verbosity true + Verbose(Box), +} + /// A confidence enum to auxiliate rescan timestamp values. /// /// Tells how much confidence you need for this rescan request. That is, the how conservative you want floresta to be when determining which block to start the rescan. From 9b27552174f088ea291ac46463457235d2b5ef49 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:38:54 -0300 Subject: [PATCH 2/3] docs(rpc): add documentation for getblockheader and update command attributes --- bin/floresta-cli/src/main.rs | 8 +++- crates/floresta-rpc/src/rpc.rs | 1 + doc/rpc/getblockheader.md | 67 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 doc/rpc/getblockheader.md diff --git a/bin/floresta-cli/src/main.rs b/bin/floresta-cli/src/main.rs index 6c646da70..f274f9348 100644 --- a/bin/floresta-cli/src/main.rs +++ b/bin/floresta-cli/src/main.rs @@ -262,7 +262,13 @@ pub enum Methods { SendRawTransaction { tx: String }, /// Returns the block header for the given block hash - #[command(name = "getblockheader")] + #[doc = include_str!("../../../doc/rpc/getblockheader.md")] + #[command( + name = "getblockheader", + about = "Returns the block header for the given block hash", + long_about = Some(include_str!("../../../doc/rpc/getblockheader.md")), + disable_help_subcommand = true + )] GetBlockHeader { hash: BlockHash, verbosity: Option, diff --git a/crates/floresta-rpc/src/rpc.rs b/crates/floresta-rpc/src/rpc.rs index c39176be2..3bd951644 100644 --- a/crates/floresta-rpc/src/rpc.rs +++ b/crates/floresta-rpc/src/rpc.rs @@ -42,6 +42,7 @@ pub trait FlorestaRPC { /// in the Bitcoin protocol specification. A header contains the block's version, /// the previous block hash, the merkle root, the timestamp, the difficulty target, /// and the nonce. + #[doc = include_str!("../../../doc/rpc/getblockheader.md")] fn get_block_header( &self, hash: BlockHash, diff --git a/doc/rpc/getblockheader.md b/doc/rpc/getblockheader.md new file mode 100644 index 000000000..a22933475 --- /dev/null +++ b/doc/rpc/getblockheader.md @@ -0,0 +1,67 @@ +# `getblockheader` + +Retrieve information about a specific block header by its hash. The verbosity parameter determines the format of the returned data. + +## Usage + +### Synopsis + +```bash +floresta-cli getblockheader [verbosity] +``` + +### Examples + +```bash +# Returns a JSON object with detailed block header information (default verbosity = true) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" + +# Returns a serialized, hex-encoded string of the block header data (verbosity = false) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" false + +# Returns a JSON object with detailed block header information (verbosity = true) +floresta-cli getblockheader "000000000000000000007ae6247b184396b8a1a292b8435508f448669ead45a6" true +``` + +## Arguments + +- `blockhash` - (string, required) The block hash. +- `verbosity` - (bool, optional, default=true) + - `false`: Returns a serialized, hex-encoded string of the block header data. + - `true`: Returns a JSON object with detailed block header information. + +## Returns + +### Ok Response (for verbosity = false) + +- `"hex"` - (string) A serialized, hex-encoded string of the block header data. + +### Ok Response (for verbosity = true) + +Return JSON object +- `confirmations` - (numeric) The number of confirmations. +- `height` - (numeric) The block height or index. +- `version` - (numeric) The block version. +- `versionHex` - (string) The block version formatted in hexadecimal. +- `merkleroot` - (string) The merkle root. +- `time` - (numeric) The block time expressed in UNIX epoch time. +- `mediantime` - (numeric) The median block time expressed in UNIX epoch time. +- `nonce` - (numeric) The nonce. +- `bits` - (string) Compact representation of the block difficulty target. +- `target` - (string) The difficulty target. +- `difficulty` - (numeric) The difficulty. +- `chainwork` - (string) Expected number of hashes required to produce the chain up to this block (in hex). +- `nTx` - (numeric) The number of transactions in the block. +- `previousblockhash` - (string, optional) The hash of the previous block. +- `nextblockhash` - (string, optional) The hash of the next block. + +### Error Enum + +* `JsonRpcError::ChainWorkOverflow` - Overflow occurred while calculating accumulated chain work +* `JsonRpcError::BlockNotFound` - The requested block hash was not found in the blockchain +* `JsonRpcError::Chain` - If there's an error accessing blockchain data + +## Notes + +- To retrieve block hashes, you can use the `getblockhash` RPC to obtain the hash of a specific block by its height, or the `getbestblockhash` RPC to get the hash of the latest known block. These hashes can then be used with the `getblockheader` RPC to retrieve detailed block information. +- **In regtest**, the difficulty value may not match real-world conditions due to easier mining. \ No newline at end of file From 44fffb5f96889dff6e645dc0782357641083945b Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:11:48 -0300 Subject: [PATCH 3/3] test(integration):enhance getblockheader test coverage Improve the getblockheader integration test to validate responses across multiple block scenarios: genesis block, a random block from the chain, and the tip block. Test both verbosity modes (false and true) to ensure correct formatting in all cases. Rename helper method to validate_headers_match for better clarity. --- tests/floresta-cli/getblockheader.py | 137 ++++++++++++++++++++++----- tests/test_framework/rpc/base.py | 8 +- 2 files changed, 120 insertions(+), 25 deletions(-) diff --git a/tests/floresta-cli/getblockheader.py b/tests/floresta-cli/getblockheader.py index ec3e0df79..b770f247a 100644 --- a/tests/floresta-cli/getblockheader.py +++ b/tests/floresta-cli/getblockheader.py @@ -6,28 +6,119 @@ This functional test cli utility to interact with a Floresta node with `getblockheader` """ +import time +import random +from typing import Any import pytest +from requests.exceptions import HTTPError -from test_framework.constants import GENESIS_BLOCK_HASH - - -@pytest.mark.rpc -def test_get_block_header(florestad_node): - """ - Test `getblockheader` to get the genesis block header. - """ - - result = florestad_node.rpc.get_blockheader(GENESIS_BLOCK_HASH) - - assert result["version"] == 1 - assert ( - result["prev_blockhash"] - == "0000000000000000000000000000000000000000000000000000000000000000" - ) - assert ( - result["merkle_root"] - == "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" - ) - assert result["time"] == 1296688602 - assert result["bits"] == 545259519 - assert result["nonce"] == 2 +TIMEOUT_SECONDS = 20 + + +class TestGetBlockheader: + """Functional tests for the getblockheader RPC, comparing Florestad vs Bitcoin Core.""" + + # define attributes at class level to avoid "defined outside __init__" warnings + florestad: Any = None + bitcoind: Any = None + log: Any = None + node_manager: Any = None + + @pytest.mark.rpc + def test_get_blockheader( + self, setup_logging, node_manager, florestad_node, bitcoind_node + ): + """ + Test the getblockheader RPC command. Verifies that Florestad's getblockheader RPC responses + are compliant with Bitcoin Core's getblockheader behavior and values. + """ + self.log = setup_logging + self.node_manager = node_manager + self.florestad = florestad_node + self.bitcoind = bitcoind_node + + self.log.info("Testing getblockheader with non-existent hash") + + invalid_hash = ( + "000000000000000000015abb3038f926d74fcdc171bf6c8aadc20a9a75310ffa" + ) + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash) + + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash, False) + + with pytest.raises(HTTPError): + self.florestad.rpc.get_blockheader(invalid_hash, True) + + self.bitcoind.rpc.generate_block(2017) + # Sleep is required to ensure blocks have different timestamps. In regtest, blocks are mined + # almost instantaneously, so without this sleep, block timestamps would be nearly identical. + # We need different timestamps to cause the median time of recent blocks to be different + # from earlier blocks, which is necessary for proper testing. + time.sleep(1) + self.bitcoind.rpc.generate_block(5) + + self.node_manager.connect_nodes(self.florestad, self.bitcoind) + + block_count = self.bitcoind.rpc.get_block_count() + start = time.time() + while time.time() - start < TIMEOUT_SECONDS: + florestad_count = self.florestad.rpc.get_block_count() + if florestad_count == block_count: + break + time.sleep(0.5) + + assert florestad_count == block_count + + self.log.info("Testing getblockheader RPC in the genesis block") + self.validate_block_header(0) + + random_block = random.randint(1, block_count) + self.log.info(f"Testing getblockheader RPC in block {random_block}") + self.validate_block_header(random_block) + + self.log.info(f"Testing getblockheader RPC in block {block_count}") + self.validate_block_header(block_count) + + def validate_block_header(self, height: int): + """ + Compare a block header at given height between Florestad and Bitcoin Core for several + verbosity levels. + """ + block_hash = self.bitcoind.rpc.get_blockhash(height) + self.log.info( + f"Comparing block header {block_hash} between florestad and bitcoind" + ) + + self.log.info("Fetching request without verbosity") + florestad_header = self.florestad.rpc.get_blockheader(block_hash) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash) + self.validate_headers_match(florestad_header, bitcoind_header) + + verbosity = False + self.log.info(f"Fetching request with verbosity {verbosity}") + florestad_header = self.florestad.rpc.get_blockheader(block_hash, verbosity) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash, verbosity) + assert florestad_header == bitcoind_header + + verbosity = True + self.log.info(f"Fetching request with verbosity {verbosity}") + florestad_header = self.florestad.rpc.get_blockheader(block_hash, verbosity) + bitcoind_header = self.bitcoind.rpc.get_blockheader(block_hash, verbosity) + + self.validate_headers_match(florestad_header, bitcoind_header) + + def validate_headers_match(self, florestad_header: dict, bitcoind_header: dict): + """ + Compare two block headers and assert that they match. + """ + for key, bval in bitcoind_header.items(): + fval = florestad_header[key] + + self.log.info(f"Comparing {key} field: florestad={fval} bitcoind={bval}") + if key == "difficulty": + # Allow small differences in floating point representation + assert round(fval, 3) == round(bval, 3) + else: + assert fval == bval diff --git a/tests/test_framework/rpc/base.py b/tests/test_framework/rpc/base.py index 34b79dc13..4be48cdab 100644 --- a/tests/test_framework/rpc/base.py +++ b/tests/test_framework/rpc/base.py @@ -259,14 +259,18 @@ def get_block_count(self) -> int: """ return self.perform_request("getblockcount") - def get_blockheader(self, blockhash: str) -> dict: + def get_blockheader(self, blockhash: str, verbosity: bool | None = None) -> dict: """ Get the header of a block """ if not bool(re.fullmatch(r"^[a-f0-9]{64}$", blockhash)): raise ValueError(f"Invalid blockhash '{blockhash}'.") - return self.perform_request("getblockheader", params=[blockhash]) + params = [blockhash] + if verbosity is not None: + params.append(verbosity) + + return self.perform_request("getblockheader", params=params) def get_block(self, blockhash: str, verbosity: int = 1): """