diff --git a/zebra-chain/src/sapling/tree.rs b/zebra-chain/src/sapling/tree.rs index 519d7deeeb9..a532f7dfcda 100644 --- a/zebra-chain/src/sapling/tree.rs +++ b/zebra-chain/src/sapling/tree.rs @@ -147,6 +147,26 @@ impl TryFrom<[u8; 32]> for Root { } } +impl ToHex for &Root { + fn encode_hex>(&self) -> T { + <[u8; 32]>::from(*self).encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + <[u8; 32]>::from(*self).encode_hex_upper() + } +} + +impl ToHex for Root { + fn encode_hex>(&self) -> T { + (&self).encode_hex() + } + + fn encode_hex_upper>(&self) -> T { + (&self).encode_hex_upper() + } +} + impl ZcashSerialize for Root { fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { writer.write_all(&<[u8; 32]>::from(*self)[..])?; diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 990172e90ca..0249a4201fe 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -998,7 +998,7 @@ where height, version: header.version, merkle_root: header.merkle_root, - final_sapling_root: sapling_tree, + final_sapling_root: sapling_tree.root(), time: header.time.timestamp(), nonce: *header.nonce, bits: header.difficulty_threshold, @@ -1756,15 +1756,18 @@ pub struct GetBlockHeaderObject { pub version: u32, /// The merkle root of the requesteed block. + #[serde(with = "hex")] pub merkle_root: block::merkle::Root, /// The root of the Sapling commitment tree after applying this block. - pub final_sapling_root: Arc, + #[serde(with = "hex")] + pub final_sapling_root: zebra_chain::sapling::tree::Root, /// 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 + #[serde(with = "hex")] pub nonce: [u8; 32], /// The difficulty threshold of the requested block header displayed in compact form. diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index f4d7804088e..12c4b4df162 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -338,6 +338,34 @@ async fn test_rpc_response_data_for_network(network: &Network) { .expect("We should have a GetBlock struct"); snapshot_rpc_getblock_verbose("hash_verbosity_default", get_block, &settings); + // `getblockheader(hash, verbose = false)` + let get_block_header = rpc + .get_block_header(block_hash.to_string(), None) + .await + .expect("We should have a GetBlock struct"); + snapshot_rpc_getblockheader("hash", get_block_header, &settings); + + // `getblockheader(height, verbose = false)` + let get_block_header = rpc + .get_block_header(BLOCK_HEIGHT.to_string(), None) + .await + .expect("We should have a GetBlock struct"); + snapshot_rpc_getblockheader("height", get_block_header, &settings); + + // `getblockheader(hash, verbose = true)` + let get_block_header = rpc + .get_block_header(block_hash.to_string(), Some(true)) + .await + .expect("We should have a GetBlock struct"); + snapshot_rpc_getblockheader("hash", get_block_header, &settings); + + // `getblockheader(height, verbose = true)` + let get_block_header = rpc + .get_block_header(BLOCK_HEIGHT.to_string(), Some(true)) + .await + .expect("We should have a GetBlock struct"); + snapshot_rpc_getblockheader("height", get_block_header, &settings); + // `getbestblockhash` let get_best_block_hash = rpc .get_best_block_hash() @@ -608,6 +636,15 @@ fn snapshot_rpc_getblock_verbose( settings.bind(|| insta::assert_json_snapshot!(format!("get_block_verbose_{variant}"), block)); } +/// Check valid `getblockheader` response using `cargo insta`. +fn snapshot_rpc_getblockheader( + variant: &'static str, + block: GetBlockHeader, + settings: &insta::Settings, +) { + settings.bind(|| insta::assert_json_snapshot!(format!("get_block_header_{variant}"), block)); +} + /// Check invalid height `getblock` response using `cargo insta`. fn snapshot_rpc_getblock_invalid( variant: &'static str, diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@mainnet_10.snap new file mode 100644 index 00000000000..4658e46999a --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@mainnet_10.snap @@ -0,0 +1,18 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "height": 1, + "version": 4, + "merkle_root": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "final_sapling_root": "fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e", + "time": 1477671596, + "nonce": "7534e8cf161ff2e49d54bdb3bfbcde8cdbf2fc5963c9ec7d86aed4a67e975790", + "bits": "1f07ffff", + "difficulty": "0007ffff00000000000000000000000000000000000000000000000000000000", + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@testnet_10.snap new file mode 100644 index 00000000000..86c6179e681 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_header_hash@testnet_10.snap @@ -0,0 +1,18 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "height": 1, + "version": 4, + "merkle_root": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "final_sapling_root": "fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e", + "time": 1477674473, + "nonce": "000056c2264c31261d597c6fcea7c5e00160cf6be1cd89ca96a0389473e50000", + "bits": "2007ffff", + "difficulty": "07ffff0000000000000000000000000000000000000000000000000000000000", + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@mainnet_10.snap new file mode 100644 index 00000000000..4658e46999a --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@mainnet_10.snap @@ -0,0 +1,18 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "height": 1, + "version": 4, + "merkle_root": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "final_sapling_root": "fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e", + "time": 1477671596, + "nonce": "7534e8cf161ff2e49d54bdb3bfbcde8cdbf2fc5963c9ec7d86aed4a67e975790", + "bits": "1f07ffff", + "difficulty": "0007ffff00000000000000000000000000000000000000000000000000000000", + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@testnet_10.snap new file mode 100644 index 00000000000..86c6179e681 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_header_height@testnet_10.snap @@ -0,0 +1,18 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "height": 1, + "version": 4, + "merkle_root": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "final_sapling_root": "fbc2f4300c01f0b7820d00e3347c8da4ee614674376cbc45359daa54f9b5493e", + "time": 1477674473, + "nonce": "000056c2264c31261d597c6fcea7c5e00160cf6be1cd89ca96a0389473e50000", + "bits": "2007ffff", + "difficulty": "07ffff0000000000000000000000000000000000000000000000000000000000", + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index ab9665c5da6..9e98f3af9ec 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -374,6 +374,100 @@ async fn rpc_getblock_missing_error() { assert!(rpc_tx_queue_task_result.is_none()); } +#[tokio::test(flavor = "multi_thread")] +async fn rpc_getblockheader() { + let _init_guard = zebra_test::init(); + + // Create a continuous chain of mainnet blocks from genesis + let blocks: Vec> = zebra_test::vectors::CONTINUOUS_MAINNET_BLOCKS + .iter() + .map(|(_height, block_bytes)| block_bytes.zcash_deserialize_into().unwrap()) + .collect(); + + let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + // Create a populated state service + let (_state, read_state, latest_chain_tip, _chain_tip_change) = + zebra_state::populated_state(blocks.clone(), &Mainnet).await; + + // Init RPC + let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new( + "RPC test", + "RPC test", + Mainnet, + false, + true, + Buffer::new(mempool.clone(), 1), + read_state.clone(), + latest_chain_tip, + ); + + // Make height calls with verbose=false and check response + for (i, block) in blocks.iter().enumerate() { + let expected_result = GetBlockHeader::Raw(HexData( + block + .header + .clone() + .zcash_serialize_to_vec() + .expect("test block header should serialize"), + )); + + let hash = block.hash(); + let height = Height(i as u32); + + for hash_or_height in [HashOrHeight::from(height), hash.into()] { + let get_block_header = rpc + .get_block_header(hash_or_height.to_string(), None) + .await + .expect("we should have a GetBlockHeader struct"); + assert_eq!(get_block_header, expected_result); + } + + let zebra_state::ReadResponse::SaplingTree(sapling_tree) = read_state + .clone() + .oneshot(zebra_state::ReadRequest::SaplingTree(height.into())) + .await + .expect("should have sapling tree for block hash") + else { + panic!("unexpected response to SaplingTree request") + }; + + let expected_result = GetBlockHeader::Object(Box::new(GetBlockHeaderObject { + hash: GetBlockHash(hash), + confirmations: 11 - i as i64, + height, + version: 4, + merkle_root: block.header.merkle_root, + final_sapling_root: sapling_tree + .expect("should always have sapling root") + .root(), + time: block.header.time.timestamp(), + nonce: *block.header.nonce, + bits: block.header.difficulty_threshold, + difficulty: block + .header + .difficulty_threshold + .to_expanded() + .expect("should have valid difficulty"), + previous_block_hash: GetBlockHash(block.header.previous_block_hash), + next_block_hash: blocks.get(i + 1).map(|b| GetBlockHash(b.hash())), + })); + + for hash_or_height in [HashOrHeight::from(Height(i as u32)), block.hash().into()] { + let get_block_header = rpc + .get_block_header(hash_or_height.to_string(), Some(true)) + .await + .expect("we should have a GetBlockHeader struct"); + assert_eq!(get_block_header, expected_result); + } + } + + mempool.expect_no_requests().await; + + // The queue task should continue without errors or panics + let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); + assert!(rpc_tx_queue_task_result.is_none()); +} + #[tokio::test(flavor = "multi_thread")] async fn rpc_getbestblockhash() { let _init_guard = zebra_test::init(); diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..5894b7da55a 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -90,6 +90,15 @@ impl HashOrHeight { } } +impl std::fmt::Display for HashOrHeight { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + HashOrHeight::Hash(hash) => write!(f, "{hash}"), + HashOrHeight::Height(height) => write!(f, "{}", height.0), + } + } +} + impl From for HashOrHeight { fn from(hash: block::Hash) -> Self { Self::Hash(hash)