From 8d777c892ffdc83cc0feb782cc52d16c0204bd3b Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Fri, 21 Oct 2022 03:01:29 -0300 Subject: [PATCH] feat(rpc): add getblockhash rpc method (#4967) * implement getblockhash rpc method * make fixes * fix some docs * rustfmt * add snapshot test * rename `Hash` to `BestChainBlockHash` * Suggestion for "add getblockhash rpc method" PR (#5428) * Always immediately return errors in get_height_from_int() * Explain why calculations can't overflow * fix for rust feature * fix some warnings * hide state functions behind feature * remove commented assert * renames * rename * fix some warnings * make zebra-rpc rpc features depend on zebra-state rpc features Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: teor --- zebra-rpc/Cargo.toml | 2 +- zebra-rpc/src/methods.rs | 16 +- zebra-rpc/src/methods/get_block_template.rs | 141 +++++++++++++++++- zebra-rpc/src/methods/tests/snapshot.rs | 24 ++- .../snapshots/get_block_hash@mainnet_10.snap | 6 + .../snapshots/get_block_hash@testnet_10.snap | 6 + zebra-rpc/src/methods/tests/vectors.rs | 64 +++++++- zebra-rpc/src/server.rs | 2 +- zebra-state/Cargo.toml | 1 + zebra-state/src/request.rs | 11 ++ zebra-state/src/response.rs | 10 ++ zebra-state/src/service.rs | 36 +++++ zebra-state/src/service/read.rs | 5 + zebra-state/src/service/read/block.rs | 22 +++ 14 files changed, 321 insertions(+), 25 deletions(-) create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_hash@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_hash@testnet_10.snap diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 67b28233f..82f18532a 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [features] default = [] proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"] -getblocktemplate-rpcs = [] +getblocktemplate-rpcs = ["zebra-state/getblocktemplate-rpcs"] [dependencies] chrono = { version = "0.4.22", default-features = false, features = ["clock", "std"] } diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index e39e5aa54..8807eda64 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -147,11 +147,11 @@ pub trait Rpc { #[rpc(name = "getblock")] fn get_block(&self, height: String, verbosity: u8) -> BoxFuture>; - /// Returns the hash of the current best blockchain tip block, as a [`GetBestBlockHash`] JSON string. + /// Returns the hash of the current best blockchain tip block, as a [`GetBlockHash`] JSON string. /// /// zcashd reference: [`getbestblockhash`](https://zcash.github.io/rpc/getbestblockhash.html) #[rpc(name = "getbestblockhash")] - fn get_best_block_hash(&self) -> Result; + fn get_best_block_hash(&self) -> Result; /// Returns all transaction ids in the memory pool, as a JSON array. /// @@ -610,10 +610,10 @@ where .boxed() } - fn get_best_block_hash(&self) -> Result { + fn get_best_block_hash(&self) -> Result { self.latest_chain_tip .best_tip_hash() - .map(GetBestBlockHash) + .map(GetBlockHash) .ok_or(Error { code: ErrorCode::ServerError(0), message: "No blocks in state".to_string(), @@ -1141,13 +1141,13 @@ pub enum GetBlock { }, } -/// Response to a `getbestblockhash` RPC request. +/// Response to a `getbestblockhash` and `getblockhash` RPC request. /// -/// Contains the hex-encoded hash of the tip block. +/// Contains the hex-encoded hash of the requested block. /// -/// Also see the notes for the [`Rpc::get_best_block_hash` method]. +/// Also see the notes for the [`Rpc::get_best_block_hash`] and `get_block_hash` methods. #[derive(Copy, Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct GetBestBlockHash(#[serde(with = "hex")] block::Hash); +pub struct GetBlockHash(#[serde(with = "hex")] block::Hash); /// Response to a `z_gettreestate` RPC request. /// diff --git a/zebra-rpc/src/methods/get_block_template.rs b/zebra-rpc/src/methods/get_block_template.rs index 7856df4c2..de7d3f459 100644 --- a/zebra-rpc/src/methods/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template.rs @@ -1,8 +1,12 @@ //! RPC methods related to mining only available with `getblocktemplate-rpcs` rust feature. -use zebra_chain::chain_tip::ChainTip; +use zebra_chain::{block::Height, chain_tip::ChainTip}; -use jsonrpc_core::{self, Error, ErrorCode, Result}; +use futures::{FutureExt, TryFutureExt}; +use jsonrpc_core::{self, BoxFuture, Error, ErrorCode, Result}; use jsonrpc_derive::rpc; +use tower::{Service, ServiceExt}; + +use crate::methods::{GetBlockHash, MISSING_BLOCK_ERROR_CODE}; /// getblocktemplate RPC method signatures. #[rpc(server)] @@ -17,31 +21,75 @@ pub trait GetBlockTemplateRpc { /// This rpc method is available only if zebra is built with `--features getblocktemplate-rpcs`. #[rpc(name = "getblockcount")] fn get_block_count(&self) -> Result; + + /// Returns the hash of the block of a given height iff the index argument correspond + /// to a block in the best chain. + /// + /// zcashd reference: [`getblockhash`](https://zcash-rpc.github.io/getblockhash.html) + /// + /// # Parameters + /// + /// - `index`: (numeric, required) The block index. + /// + /// # Notes + /// + /// - If `index` is positive then index = block height. + /// - If `index` is negative then -1 is the last known valid block. + #[rpc(name = "getblockhash")] + fn get_block_hash(&self, index: i32) -> BoxFuture>; } /// RPC method implementations. -pub struct GetBlockTemplateRpcImpl +pub struct GetBlockTemplateRpcImpl where Tip: ChainTip, + State: Service< + zebra_state::ReadRequest, + Response = zebra_state::ReadResponse, + Error = zebra_state::BoxError, + >, { // TODO: Add the other fields from the [`Rpc`] struct as-needed /// Allows efficient access to the best tip of the blockchain. latest_chain_tip: Tip, + + /// A handle to the state service. + state: State, } -impl GetBlockTemplateRpcImpl +impl GetBlockTemplateRpcImpl where Tip: ChainTip + Clone + Send + Sync + 'static, + State: Service< + zebra_state::ReadRequest, + Response = zebra_state::ReadResponse, + Error = zebra_state::BoxError, + > + Clone + + Send + + Sync + + 'static, { /// Create a new instance of the RPC handler. - pub fn new(latest_chain_tip: Tip) -> Self { - Self { latest_chain_tip } + pub fn new(latest_chain_tip: Tip, state: State) -> Self { + Self { + latest_chain_tip, + state, + } } } -impl GetBlockTemplateRpc for GetBlockTemplateRpcImpl +impl GetBlockTemplateRpc for GetBlockTemplateRpcImpl where Tip: ChainTip + Send + Sync + 'static, + State: Service< + zebra_state::ReadRequest, + Response = zebra_state::ReadResponse, + Error = zebra_state::BoxError, + > + Clone + + Send + + Sync + + 'static, + >::Future: Send, { fn get_block_count(&self) -> Result { self.latest_chain_tip @@ -53,4 +101,83 @@ where data: None, }) } + + fn get_block_hash(&self, index: i32) -> BoxFuture> { + let mut state = self.state.clone(); + + let maybe_tip_height = self.latest_chain_tip.best_tip_height(); + + async move { + let tip_height = maybe_tip_height.ok_or(Error { + code: ErrorCode::ServerError(0), + message: "No blocks in state".to_string(), + data: None, + })?; + + let height = get_height_from_int(index, tip_height)?; + + let request = zebra_state::ReadRequest::BestChainBlockHash(height); + let response = state + .ready() + .and_then(|service| service.call(request)) + .await + .map_err(|error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + })?; + + match response { + zebra_state::ReadResponse::BlockHash(Some(hash)) => Ok(GetBlockHash(hash)), + zebra_state::ReadResponse::BlockHash(None) => Err(Error { + code: MISSING_BLOCK_ERROR_CODE, + message: "Block not found".to_string(), + data: None, + }), + _ => unreachable!("unmatched response to a block request"), + } + } + .boxed() + } +} + +/// Given a potentially negative index, find the corresponding `Height`. +/// +/// This function is used to parse the integer index argument of `get_block_hash`. +fn get_height_from_int(index: i32, tip_height: Height) -> Result { + if index >= 0 { + let height = index.try_into().expect("Positive i32 always fits in u32"); + if height > tip_height.0 { + return Err(Error::invalid_params( + "Provided index is greater than the current tip", + )); + } + Ok(Height(height)) + } else { + // `index + 1` can't overflow, because `index` is always negative here. + let height = i32::try_from(tip_height.0) + .expect("tip height fits in i32, because Height::MAX fits in i32") + .checked_add(index + 1); + + let sanitized_height = match height { + None => return Err(Error::invalid_params("Provided index is not valid")), + Some(h) => { + if h < 0 { + return Err(Error::invalid_params( + "Provided negative index ends up with a negative height", + )); + } + let h: u32 = h.try_into().expect("Positive i32 always fits in u32"); + if h > tip_height.0 { + return Err(Error::invalid_params( + "Provided index is greater than the current tip", + )); + } + + h + } + }; + + Ok(Height(sanitized_height)) + } } diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index 2f860ff93..044910e6f 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -43,6 +43,8 @@ async fn test_rpc_response_data_for_network(network: Network) { #[cfg(feature = "getblocktemplate-rpcs")] let latest_chain_tip_gbt_clone = latest_chain_tip.clone(); + #[cfg(feature = "getblocktemplate-rpcs")] + let read_state_clone = read_state.clone(); // Init RPC let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new( @@ -104,7 +106,7 @@ async fn test_rpc_response_data_for_network(network: Network) { // `getbestblockhash` let get_best_block_hash = rpc .get_best_block_hash() - .expect("We should have a GetBestBlockHash struct"); + .expect("We should have a GetBlockHash struct"); snapshot_rpc_getbestblockhash(get_best_block_hash, &settings); // `getrawmempool` @@ -172,13 +174,23 @@ async fn test_rpc_response_data_for_network(network: Network) { #[cfg(feature = "getblocktemplate-rpcs")] { - let get_block_template_rpc = GetBlockTemplateRpcImpl::new(latest_chain_tip_gbt_clone); + let get_block_template_rpc = + GetBlockTemplateRpcImpl::new(latest_chain_tip_gbt_clone, read_state_clone); // `getblockcount` let get_block_count = get_block_template_rpc .get_block_count() .expect("We should have a number"); snapshot_rpc_getblockcount(get_block_count, &settings); + + // `getblockhash` + const BLOCK_HEIGHT10: i32 = 10; + let get_block_hash = get_block_template_rpc + .get_block_hash(BLOCK_HEIGHT10) + .await + .expect("We should have a GetBlockHash struct"); + + snapshot_rpc_getblockhash(get_block_hash, &settings); } } @@ -239,7 +251,7 @@ fn snapshot_rpc_getblock_verbose(block: GetBlock, settings: &insta::Settings) { } /// Snapshot `getbestblockhash` response, using `cargo insta` and JSON serialization. -fn snapshot_rpc_getbestblockhash(tip_hash: GetBestBlockHash, settings: &insta::Settings) { +fn snapshot_rpc_getbestblockhash(tip_hash: GetBlockHash, settings: &insta::Settings) { settings.bind(|| insta::assert_json_snapshot!("get_best_block_hash", tip_hash)); } @@ -274,6 +286,12 @@ fn snapshot_rpc_getblockcount(block_count: u32, settings: &insta::Settings) { settings.bind(|| insta::assert_json_snapshot!("get_block_count", block_count)); } +#[cfg(feature = "getblocktemplate-rpcs")] +/// Snapshot `getblockhash` response, using `cargo insta` and JSON serialization. +fn snapshot_rpc_getblockhash(block_hash: GetBlockHash, settings: &insta::Settings) { + settings.bind(|| insta::assert_json_snapshot!("get_block_hash", block_hash)); +} + /// Utility function to convert a `Network` to a lowercase string. fn network_string(network: Network) -> String { let mut net_suffix = network.to_string(); diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_hash@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_hash@mainnet_10.snap new file mode 100644 index 000000000..74f56f796 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_hash@mainnet_10.snap @@ -0,0 +1,6 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 270 +expression: block_hash +--- +"00074c46a4aa8172df8ae2ad1848a2e084e1b6989b7d9e6132adc938bf835b36" diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_hash@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_hash@testnet_10.snap new file mode 100644 index 000000000..3740afc40 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_hash@testnet_10.snap @@ -0,0 +1,6 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +assertion_line: 270 +expression: block_hash +--- +"079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112" diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 4d22831d0..514646b40 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -233,7 +233,7 @@ async fn rpc_getbestblockhash() { // Get the tip hash using RPC method `get_best_block_hash` let get_best_block_hash = rpc .get_best_block_hash() - .expect("We should have a GetBestBlockHash struct"); + .expect("We should have a GetBlockHash struct"); let response_hash = get_best_block_hash.0; // Check if response is equal to block 10 hash. @@ -641,13 +641,13 @@ async fn rpc_getblockcount() { Mainnet, false, Buffer::new(mempool.clone(), 1), - read_state, + read_state.clone(), latest_chain_tip.clone(), ); // Init RPC let get_block_template_rpc = - get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone()); + get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), read_state); // Get the tip height using RPC method `get_block_count` let get_block_count = get_block_template_rpc @@ -681,12 +681,12 @@ async fn rpc_getblockcount_empty_state() { Mainnet, false, Buffer::new(mempool.clone(), 1), - read_state, + read_state.clone(), latest_chain_tip.clone(), ); let get_block_template_rpc = - get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone()); + get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), read_state); // Get the tip height using RPC method `get_block_count let get_block_count = get_block_template_rpc.get_block_count(); @@ -703,3 +703,57 @@ async fn rpc_getblockcount_empty_state() { let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never(); assert!(matches!(rpc_tx_queue_task_result, None)); } + +#[cfg(feature = "getblocktemplate-rpcs")] +#[tokio::test(flavor = "multi_thread")] +async fn rpc_getblockhash() { + 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 RPCs + let _rpc = RpcImpl::new( + "RPC test", + Mainnet, + false, + Buffer::new(mempool.clone(), 1), + Buffer::new(read_state.clone(), 1), + latest_chain_tip.clone(), + ); + let get_block_template_rpc = + get_block_template::GetBlockTemplateRpcImpl::new(latest_chain_tip, read_state); + + // Query the hashes using positive indexes + for (i, block) in blocks.iter().enumerate() { + let get_block_hash = get_block_template_rpc + .get_block_hash(i.try_into().expect("usize always fits in i32")) + .await + .expect("We should have a GetBlockHash struct"); + + assert_eq!(get_block_hash, GetBlockHash(block.clone().hash())); + } + + // Query the hashes using negative indexes + for i in (-10..=-1).rev() { + let get_block_hash = get_block_template_rpc + .get_block_hash(i) + .await + .expect("We should have a GetBlockHash struct"); + + assert_eq!( + get_block_hash, + GetBlockHash(blocks[(10 + (i + 1)) as usize].hash()) + ); + } + + mempool.expect_no_requests().await; +} diff --git a/zebra-rpc/src/server.rs b/zebra-rpc/src/server.rs index 4647b72e1..7842c32be 100644 --- a/zebra-rpc/src/server.rs +++ b/zebra-rpc/src/server.rs @@ -75,7 +75,7 @@ impl RpcServer { { // Initialize the getblocktemplate rpc methods let get_block_template_rpc_impl = - GetBlockTemplateRpcImpl::new(latest_chain_tip.clone()); + GetBlockTemplateRpcImpl::new(latest_chain_tip.clone(), state.clone()); io.extend_with(get_block_template_rpc_impl.to_delegate()); } diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 1559147ba..8e77fa5d6 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [features] proptest-impl = ["proptest", "proptest-derive", "zebra-test", "zebra-chain/proptest-impl"] +getblocktemplate-rpcs = [] [dependencies] bincode = "1.3.3" diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index bca3f0140..87831c071 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -731,6 +731,15 @@ pub enum ReadRequest { /// /// Returns a type with found utxos and transaction information. UtxosByAddresses(HashSet), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Looks up a block hash by height in the current best chain. + /// + /// Returns + /// + /// * [`ReadResponse::BlockHash(Some(hash))`](ReadResponse::BlockHash) if the block is in the best chain; + /// * [`ReadResponse::BlockHash(None)`](ReadResponse::BlockHash) otherwise. + BestChainBlockHash(block::Height), } impl ReadRequest { @@ -751,6 +760,8 @@ impl ReadRequest { ReadRequest::AddressBalance { .. } => "address_balance", ReadRequest::TransactionIdsByAddresses { .. } => "transaction_ids_by_addesses", ReadRequest::UtxosByAddresses(_) => "utxos_by_addesses", + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::BestChainBlockHash(_) => "best_chain_block_hash", } } diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 7ca08b3fd..68bfafda5 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -110,6 +110,11 @@ pub enum ReadResponse { /// Response to [`ReadRequest::UtxosByAddresses`] with found utxos and transaction data. AddressUtxos(AddressUtxos), + + #[cfg(feature = "getblocktemplate-rpcs")] + /// Response to [`ReadRequest::BestChainBlockHash`](crate::ReadRequest::BestChainBlockHash) with the + /// specified block hash. + BlockHash(Option), } /// Conversion from read-only [`ReadResponse`]s to read-write [`Response`]s. @@ -144,6 +149,11 @@ impl TryFrom for Response { | ReadResponse::AddressUtxos(_) => { Err("there is no corresponding Response for this ReadResponse") } + + #[cfg(feature = "getblocktemplate-rpcs")] + ReadResponse::BlockHash(_) => { + Err("there is no corresponding Response for this ReadResponse") + } } } } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index 626b819d5..ac2ff5e1f 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1510,6 +1510,42 @@ impl Service for ReadStateService { .map(|join_result| join_result.expect("panic in ReadRequest::UtxosByAddresses")) .boxed() } + + // Used by get_block_hash RPC. + #[cfg(feature = "getblocktemplate-rpcs")] + ReadRequest::BestChainBlockHash(height) => { + metrics::counter!( + "state.requests", + 1, + "service" => "read_state", + "type" => "best_chain_block_hash", + ); + + let timer = CodeTimer::start(); + + let state = self.clone(); + + // # Performance + // + // Allow other async tasks to make progress while concurrently reading blocks from disk. + let span = Span::current(); + tokio::task::spawn_blocking(move || { + span.in_scope(move || { + let hash = state.non_finalized_state_receiver.with_watch_data( + |non_finalized_state| { + read::hash(non_finalized_state.best_chain(), &state.db, height) + }, + ); + + // The work is done in the future. + timer.finish(module_path!(), line!(), "ReadRequest::BestChainBlockHash"); + + Ok(ReadResponse::BlockHash(hash)) + }) + }) + .map(|join_result| join_result.expect("panic in ReadRequest::BestChainBlockHash")) + .boxed() + } } } } diff --git a/zebra-state/src/service/read.rs b/zebra-state/src/service/read.rs index f39e2c543..debff2fd3 100644 --- a/zebra-state/src/service/read.rs +++ b/zebra-state/src/service/read.rs @@ -27,7 +27,12 @@ pub use address::{ tx_id::transparent_tx_ids, utxo::{address_utxos, AddressUtxos, ADDRESS_HEIGHTS_FULL_RANGE}, }; + pub use block::{any_utxo, block, block_header, transaction, transaction_hashes_for_block, utxo}; + +#[cfg(feature = "getblocktemplate-rpcs")] +pub use block::hash; + pub use find::{ best_tip, block_locator, chain_contains_hash, depth, find_chain_hashes, find_chain_headers, hash_by_height, height_by_hash, tip, tip_height, diff --git a/zebra-state/src/service/read/block.rs b/zebra-state/src/service/read/block.rs index 54eb485c3..dfa9ba125 100644 --- a/zebra-state/src/service/read/block.rs +++ b/zebra-state/src/service/read/block.rs @@ -167,3 +167,25 @@ pub fn any_utxo( .any_utxo(&outpoint) .or_else(|| db.utxo(&outpoint).map(|utxo| utxo.utxo)) } + +#[cfg(feature = "getblocktemplate-rpcs")] +/// Returns the [`Hash`] given [`block::Height`](zebra_chain::block::Height), if it exists in +/// the non-finalized `chain` or finalized `db`. +pub fn hash(chain: Option, db: &ZebraDb, height: Height) -> Option +where + C: AsRef, +{ + // # Correctness + // + // The StateService commits blocks to the finalized state before updating + // the latest chain, and it can commit additional blocks after we've cloned + // this `chain` variable. + // + // Since blocks are the same in the finalized and non-finalized state, we + // check the most efficient alternative first. (`chain` is always in memory, + // but `db` stores blocks on disk, with a memory cache.) + chain + .as_ref() + .and_then(|chain| chain.as_ref().hash_by_height(height)) + .or_else(|| db.hash(height)) +}