diff --git a/zaino-fetch/src/jsonrpsee/connector.rs b/zaino-fetch/src/jsonrpsee/connector.rs index 13067f7c3..98cb365a8 100644 --- a/zaino-fetch/src/jsonrpsee/connector.rs +++ b/zaino-fetch/src/jsonrpsee/connector.rs @@ -23,6 +23,7 @@ use tracing::error; use zebra_rpc::client::ValidateAddressResponse; use crate::jsonrpsee::response::address_deltas::GetAddressDeltasError; +use crate::jsonrpsee::response::block_hash::BlockSelector; use crate::jsonrpsee::{ error::{JsonRpcError, TransportError}, response::{ @@ -609,6 +610,15 @@ impl JsonRpSeeConnector { .await } + // TODO: use correct error + pub async fn get_blockhash( + &self, + block_index: BlockSelector, + ) -> Result> { + let params = [serde_json::to_value(block_index).map_err(RpcRequestError::JsonRpc)?]; + self.send_request("getblockhash", params).await + } + /// Returns the height of the most recent block in the best valid block chain /// (equivalently, the number of blocks in this chain excluding the genesis block). /// diff --git a/zaino-fetch/src/jsonrpsee/response.rs b/zaino-fetch/src/jsonrpsee/response.rs index 6a4794835..716db066c 100644 --- a/zaino-fetch/src/jsonrpsee/response.rs +++ b/zaino-fetch/src/jsonrpsee/response.rs @@ -4,6 +4,7 @@ //! to prevent locking consumers into a zebra_rpc version pub mod address_deltas; +pub mod block_hash; pub mod block_header; pub mod block_subsidy; pub mod common; diff --git a/zaino-fetch/src/jsonrpsee/response/block_hash.rs b/zaino-fetch/src/jsonrpsee/response/block_hash.rs new file mode 100644 index 000000000..3561694ce --- /dev/null +++ b/zaino-fetch/src/jsonrpsee/response/block_hash.rs @@ -0,0 +1,231 @@ +use core::fmt; + +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; +use zebra_chain::block::Height; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BlockSelector { + Tip, + Height(Height), +} + +impl BlockSelector { + /// Resolve to a concrete height given the current tip. + #[inline] + pub fn resolve(self, tip: Height) -> Height { + match self { + BlockSelector::Tip => tip, + BlockSelector::Height(h) => h, + } + } + + /// Convenience: returns `Some(h)` if absolute, else `None`. + #[inline] + pub fn height(self) -> Option { + match self { + BlockSelector::Tip => None, + BlockSelector::Height(h) => Some(h), + } + } +} + +impl<'de> Deserialize<'de> for BlockSelector { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct SelVisitor; + + impl<'de> Visitor<'de> for SelVisitor { + type Value = BlockSelector; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "an integer height ≥ 0, -1 for tip, or a string like \"tip\"/\"-1\"/\"42\"" + ) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + if v == -1 { + Ok(BlockSelector::Tip) + } else if v >= 0 && v <= u32::MAX as i64 { + Ok(BlockSelector::Height(Height(v as u32))) + } else { + Err(E::custom("block height out of range")) + } + } + + fn visit_u64(self, v: u64) -> Result + where + E: serde::de::Error, + { + if v <= u32::MAX as u64 { + Ok(BlockSelector::Height(Height(v as u32))) + } else { + Err(E::custom("block height out of range")) + } + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let s = s.trim(); + if s.eq_ignore_ascii_case("tip") { + return Ok(BlockSelector::Tip); + } + let v: i64 = s + .parse() + .map_err(|_| E::custom("invalid block index string"))?; + self.visit_i64(v) + } + } + + deserializer.deserialize_any(SelVisitor) + } +} + +impl Serialize for BlockSelector { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + BlockSelector::Tip => serializer.serialize_i64(-1), // mirrors zcashd “-1 = tip” + BlockSelector::Height(h) => serializer.serialize_u64(h.0 as u64), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::{self, json}; + + #[test] + fn deserialize_numbers_succeeds() { + // JSON numbers + let selector_from_negative_one: BlockSelector = serde_json::from_str("-1").unwrap(); + assert_eq!(selector_from_negative_one, BlockSelector::Tip); + + let selector_from_spaced_negative_one: BlockSelector = + serde_json::from_str(" -1 ").unwrap(); + assert_eq!(selector_from_spaced_negative_one, BlockSelector::Tip); + + let selector_from_zero: BlockSelector = serde_json::from_str("0").unwrap(); + assert_eq!(selector_from_zero, BlockSelector::Height(Height(0))); + + let selector_from_forty_two: BlockSelector = serde_json::from_str("42").unwrap(); + assert_eq!(selector_from_forty_two, BlockSelector::Height(Height(42))); + + let selector_from_max_u32: BlockSelector = + serde_json::from_str(&u32::MAX.to_string()).unwrap(); + assert_eq!( + selector_from_max_u32, + BlockSelector::Height(Height(u32::MAX)) + ); + } + + #[test] + fn deserialize_strings_succeeds() { + // JSON strings + let selector_from_tip_literal: BlockSelector = serde_json::from_str(r#""tip""#).unwrap(); + assert_eq!(selector_from_tip_literal, BlockSelector::Tip); + + let selector_from_case_insensitive_tip: BlockSelector = + serde_json::from_str(r#"" TIP ""#).unwrap(); + assert_eq!(selector_from_case_insensitive_tip, BlockSelector::Tip); + + let selector_from_negative_one_string: BlockSelector = + serde_json::from_str(r#""-1""#).unwrap(); + assert_eq!(selector_from_negative_one_string, BlockSelector::Tip); + + let selector_from_numeric_string: BlockSelector = serde_json::from_str(r#""42""#).unwrap(); + assert_eq!( + selector_from_numeric_string, + BlockSelector::Height(Height(42)) + ); + + let selector_from_spaced_numeric_string: BlockSelector = + serde_json::from_str(r#"" 17 ""#).unwrap(); + assert_eq!( + selector_from_spaced_numeric_string, + BlockSelector::Height(Height(17)) + ); + } + + #[test] + fn deserialize_with_invalid_inputs_fails() { + // Numbers: invalid negative and too large + assert!(serde_json::from_str::("-2").is_err()); + assert!(serde_json::from_str::("9223372036854775807").is_err()); + + // Strings: invalid negative, too large, and malformed + assert!(serde_json::from_str::(r#""-2""#).is_err()); + + let value_exceeding_u32_maximum = (u32::MAX as u64 + 1).to_string(); + let json_string_exceeding_u32_maximum = format!(r#""{}""#, value_exceeding_u32_maximum); + assert!(serde_json::from_str::(&json_string_exceeding_u32_maximum).is_err()); + + assert!(serde_json::from_str::(r#""nope""#).is_err()); + assert!(serde_json::from_str::(r#""""#).is_err()); + } + + #[test] + fn serialize_values_match_expected_representations() { + let json_value_for_tip = serde_json::to_value(BlockSelector::Tip).unwrap(); + assert_eq!(json_value_for_tip, json!(-1)); + + let json_value_for_zero_height = + serde_json::to_value(BlockSelector::Height(Height(0))).unwrap(); + assert_eq!(json_value_for_zero_height, json!(0)); + + let json_value_for_specific_height = + serde_json::to_value(BlockSelector::Height(Height(42))).unwrap(); + assert_eq!(json_value_for_specific_height, json!(42)); + + let json_value_for_maximum_height = + serde_json::to_value(BlockSelector::Height(Height(u32::MAX))).unwrap(); + assert_eq!(json_value_for_maximum_height, json!(u32::MAX as u64)); + } + + #[test] + fn json_round_trip_preserves_value() { + let test_cases = [ + BlockSelector::Tip, + BlockSelector::Height(Height(0)), + BlockSelector::Height(Height(1)), + BlockSelector::Height(Height(42)), + BlockSelector::Height(Height(u32::MAX)), + ]; + + for test_case in test_cases { + let serialized_json_string = serde_json::to_string(&test_case).unwrap(); + let round_tripped_selector: BlockSelector = + serde_json::from_str(&serialized_json_string).unwrap(); + assert_eq!( + round_tripped_selector, test_case, + "Round trip failed for {test_case:?} via {serialized_json_string}" + ); + } + } + + #[test] + fn resolve_and_helper_methods_work_as_expected() { + let tip_height = Height(100); + + // Tip resolves to the current tip height + let selector_tip = BlockSelector::Tip; + assert_eq!(selector_tip.resolve(tip_height), tip_height); + assert_eq!(selector_tip.height(), None); + + // Absolute height resolves to itself + let selector_absolute_height = BlockSelector::Height(Height(90)); + assert_eq!(selector_absolute_height.resolve(tip_height), Height(90)); + assert_eq!(selector_absolute_height.height(), Some(Height(90))); + } +} diff --git a/zaino-state/src/backends/fetch.rs b/zaino-state/src/backends/fetch.rs index b1bdcf17d..3e6ce7917 100644 --- a/zaino-state/src/backends/fetch.rs +++ b/zaino-state/src/backends/fetch.rs @@ -24,11 +24,12 @@ use zaino_fetch::{ connector::{JsonRpSeeConnector, RpcError}, response::{ address_deltas::{GetAddressDeltasParams, GetAddressDeltasResponse}, + block_hash::BlockSelector, block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, peer_info::GetPeerInfo, - GetMempoolInfoResponse, GetNetworkSolPsResponse, + GetBlockHash, GetMempoolInfoResponse, GetNetworkSolPsResponse, }, }, }; @@ -452,6 +453,10 @@ impl ZcashIndexer for FetchServiceSubscriber { Ok(self.fetcher.get_best_blockhash().await?.into()) } + async fn get_blockhash(&self, block_index: BlockSelector) -> Result { + Ok(self.fetcher.get_blockhash(block_index).await?.into()) + } + /// Returns the current block count in the best valid block chain. /// /// zcashd reference: [`getblockcount`](https://zcash.github.io/rpc/getblockcount.html) diff --git a/zaino-state/src/backends/state.rs b/zaino-state/src/backends/state.rs index c505c6777..8a12e7e17 100644 --- a/zaino-state/src/backends/state.rs +++ b/zaino-state/src/backends/state.rs @@ -29,6 +29,7 @@ use zaino_fetch::{ connector::{JsonRpSeeConnector, RpcError}, response::{ address_deltas::{BlockInfo, GetAddressDeltasParams, GetAddressDeltasResponse}, + block_hash::BlockSelector, block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, @@ -1374,6 +1375,24 @@ impl ZcashIndexer for StateServiceSubscriber { } } + async fn get_blockhash(&self, block_index: BlockSelector) -> Result { + let (tip, _hash) = self.read_state_service.best_tip().unwrap(); + + let selected_block_height = block_index.resolve(tip); + + let block = self + .z_get_block(selected_block_height.0.to_string(), Some(1)) + .await + .unwrap(); + + let block_hash = match block { + GetBlock::Raw(serialized_block) => todo!(), + GetBlock::Object(block_object) => block_object.hash(), + }; + + Ok(GetBlockHash::new(block_hash)) + } + /// Returns the current block count in the best valid block chain. /// /// zcashd reference: [`getblockcount`](https://zcash.github.io/rpc/getblockcount.html) diff --git a/zaino-state/src/indexer.rs b/zaino-state/src/indexer.rs index d30c334f1..e93e388df 100644 --- a/zaino-state/src/indexer.rs +++ b/zaino-state/src/indexer.rs @@ -6,6 +6,7 @@ use tokio::{sync::mpsc, time::timeout}; use tracing::warn; use zaino_fetch::jsonrpsee::response::{ address_deltas::{GetAddressDeltasParams, GetAddressDeltasResponse}, + block_hash::BlockSelector, block_header::GetBlockHeader, block_subsidy::GetBlockSubsidy, mining_info::GetMiningInfoWire, @@ -21,7 +22,10 @@ use zaino_proto::proto::{ TxFilter, }, }; -use zebra_chain::{block::Height, subtree::NoteCommitmentSubtreeIndex}; +use zebra_chain::{ + block::{Height, TryIntoHeight}, + subtree::NoteCommitmentSubtreeIndex, +}; use zebra_rpc::{ client::{GetSubtreesByIndexResponse, GetTreestateResponse, ValidateAddressResponse}, methods::{ @@ -352,6 +356,8 @@ pub trait ZcashIndexer: Send + Sync + 'static { /// where `return chainActive.Tip()->GetBlockHash().GetHex();` is the [return expression](https://github.com/zcash/zcash/blob/654a8be2274aa98144c80c1ac459400eaf0eacbe/src/rpc/blockchain.cpp#L339) returning a `std::string` async fn get_best_blockhash(&self) -> Result; + async fn get_blockhash(&self, block_index: BlockSelector) -> Result; + /// Returns all transaction ids in the memory pool, as a JSON array. /// /// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)