Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions zaino-fetch/src/jsonrpsee/connector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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::{
Expand Down Expand Up @@ -609,6 +610,15 @@
.await
}

// TODO: use correct error
pub async fn get_blockhash(
&self,
block_index: BlockSelector,
) -> Result<zebra_rpc::methods::GetBlockHash, RpcRequestError<Infallible>> {
let params = [serde_json::to_value(block_index).map_err(RpcRequestError::JsonRpc)?];
self.send_request("getblockhash", params).await

Check failure on line 619 in zaino-fetch/src/jsonrpsee/connector.rs

View workflow job for this annotation

GitHub Actions / clippy

the trait bound `zebra_rpc::client::GetBlockHashResponse: jsonrpsee::connector::ResponseToError` is not satisfied

error[E0277]: the trait bound `zebra_rpc::client::GetBlockHashResponse: jsonrpsee::connector::ResponseToError` is not satisfied --> zaino-fetch/src/jsonrpsee/connector.rs:619:14 | 619 | self.send_request("getblockhash", params).await | ^^^^^^^^^^^^ the trait `jsonrpsee::connector::ResponseToError` is not implemented for `zebra_rpc::client::GetBlockHashResponse` | = help: the following other types implement trait `jsonrpsee::connector::ResponseToError`: jsonrpsee::response::BlockObject jsonrpsee::response::ChainBalance jsonrpsee::response::ChainWork jsonrpsee::response::ErrorsTimestamp jsonrpsee::response::GetBalanceResponse jsonrpsee::response::GetBlockCountResponse jsonrpsee::response::GetBlockHash jsonrpsee::response::GetBlockResponse and 21 others note: required by a bound in `jsonrpsee::connector::JsonRpSeeConnector::send_request` --> zaino-fetch/src/jsonrpsee/connector.rs:283:58 | 281 | async fn send_request< | ------------ required by a bound in this associated function 282 | T: std::fmt::Debug + Serialize, 283 | R: std::fmt::Debug + for<'de> Deserialize<'de> + ResponseToError, | ^^^^^^^^^^^^^^^ required by this bound in `JsonRpSeeConnector::send_request`

Check failure on line 619 in zaino-fetch/src/jsonrpsee/connector.rs

View workflow job for this annotation

GitHub Actions / clippy

the trait bound `zebra_rpc::client::GetBlockHashResponse: jsonrpsee::connector::ResponseToError` is not satisfied

error[E0277]: the trait bound `zebra_rpc::client::GetBlockHashResponse: jsonrpsee::connector::ResponseToError` is not satisfied --> zaino-fetch/src/jsonrpsee/connector.rs:619:14 | 619 | self.send_request("getblockhash", params).await | ^^^^^^^^^^^^ the trait `jsonrpsee::connector::ResponseToError` is not implemented for `zebra_rpc::client::GetBlockHashResponse` | = help: the following other types implement trait `jsonrpsee::connector::ResponseToError`: jsonrpsee::response::BlockObject jsonrpsee::response::ChainBalance jsonrpsee::response::ChainWork jsonrpsee::response::ErrorsTimestamp jsonrpsee::response::GetBalanceResponse jsonrpsee::response::GetBlockCountResponse jsonrpsee::response::GetBlockHash jsonrpsee::response::GetBlockResponse and 21 others note: required by a bound in `jsonrpsee::connector::JsonRpSeeConnector::send_request` --> zaino-fetch/src/jsonrpsee/connector.rs:283:58 | 281 | async fn send_request< | ------------ required by a bound in this associated function 282 | T: std::fmt::Debug + Serialize, 283 | R: std::fmt::Debug + for<'de> Deserialize<'de> + ResponseToError, | ^^^^^^^^^^^^^^^ required by this bound in `JsonRpSeeConnector::send_request`
}

/// 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).
///
Expand Down
1 change: 1 addition & 0 deletions zaino-fetch/src/jsonrpsee/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
231 changes: 231 additions & 0 deletions zaino-fetch/src/jsonrpsee/response/block_hash.rs
Original file line number Diff line number Diff line change
@@ -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<Height> {
match self {
BlockSelector::Tip => None,
BlockSelector::Height(h) => Some(h),
}
}
}

impl<'de> Deserialize<'de> for BlockSelector {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: i64) -> Result<Self::Value, E>
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<E>(self, v: u64) -> Result<Self::Value, E>
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<E>(self, s: &str) -> Result<Self::Value, E>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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::<BlockSelector>("-2").is_err());
assert!(serde_json::from_str::<BlockSelector>("9223372036854775807").is_err());

// Strings: invalid negative, too large, and malformed
assert!(serde_json::from_str::<BlockSelector>(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::<BlockSelector>(&json_string_exceeding_u32_maximum).is_err());

assert!(serde_json::from_str::<BlockSelector>(r#""nope""#).is_err());
assert!(serde_json::from_str::<BlockSelector>(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)));
}
}
7 changes: 6 additions & 1 deletion zaino-state/src/backends/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
};
Expand Down Expand Up @@ -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<GetBlockHash, Self::Error> {
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)
Expand Down
19 changes: 19 additions & 0 deletions zaino-state/src/backends/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1374,6 +1375,24 @@ impl ZcashIndexer for StateServiceSubscriber {
}
}

async fn get_blockhash(&self, block_index: BlockSelector) -> Result<GetBlockHash, Self::Error> {
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)
Expand Down
8 changes: 7 additions & 1 deletion zaino-state/src/indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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::{
Expand Down Expand Up @@ -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<GetBlockHash, Self::Error>;

async fn get_blockhash(&self, block_index: BlockSelector) -> Result<GetBlockHash, Self::Error>;

/// Returns all transaction ids in the memory pool, as a JSON array.
///
/// zcashd reference: [`getrawmempool`](https://zcash.github.io/rpc/getrawmempool.html)
Expand Down
Loading