diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index c7363a8cff0..c5fb22efde1 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -80,7 +80,6 @@ impl Height { /// # Panics /// /// - If the current height is at its maximum. - // TODO Return an error instead of panicking #7263. pub fn next(self) -> Result { (self + 1).ok_or(HeightError::Overflow) } @@ -90,7 +89,6 @@ impl Height { /// # Panics /// /// - If the current height is at its minimum. - // TODO Return an error instead of panicking #7263. pub fn previous(self) -> Result { (self - 1).ok_or(HeightError::Underflow) } @@ -99,6 +97,11 @@ impl Height { pub fn is_min(self) -> bool { self == Self::MIN } + + /// Returns the value as a `usize`. + pub fn as_usize(self) -> usize { + self.0.try_into().expect("fits in usize") + } } /// A difference between two [`Height`]s, possibly negative. @@ -169,6 +172,14 @@ impl TryIntoHeight for String { } } +impl TryIntoHeight for i32 { + type Error = BoxError; + + fn try_into_height(&self) -> Result { + u32::try_from(*self)?.try_into().map_err(Into::into) + } +} + // We don't implement Add or Sub, because they cause type inference issues for integer constants. impl Sub for Height { diff --git a/zebra-chain/src/work/difficulty.rs b/zebra-chain/src/work/difficulty.rs index 8964ae23469..a15c1f13ce8 100644 --- a/zebra-chain/src/work/difficulty.rs +++ b/zebra-chain/src/work/difficulty.rs @@ -129,6 +129,11 @@ pub struct ExpandedDifficulty(U256); pub struct Work(u128); impl Work { + /// Returns a value representing no work. + pub fn zero() -> Self { + Self(0) + } + /// Return the inner `u128` value. pub fn as_u128(self) -> u128 { self.0 @@ -660,6 +665,11 @@ impl std::ops::Add for Work { pub struct PartialCumulativeWork(u128); impl PartialCumulativeWork { + /// Returns a value representing no work. + pub fn zero() -> Self { + Self(0) + } + /// Return the inner `u128` value. pub fn as_u128(self) -> u128 { self.0 diff --git a/zebra-rpc/src/constants.rs b/zebra-rpc/src/constants.rs index 5665f7ceb8e..e8be508595f 100644 --- a/zebra-rpc/src/constants.rs +++ b/zebra-rpc/src/constants.rs @@ -1,6 +1,6 @@ //! Constants for RPC methods and server responses. -use jsonrpc_core::ErrorCode; +use jsonrpc_core::{Error, ErrorCode}; /// The RPC error code used by `zcashd` for incorrect RPC parameters. /// @@ -17,5 +17,24 @@ pub const INVALID_PARAMETERS_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-1); /// pub const MISSING_BLOCK_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-8); +/// The RPC error code used by `zcashd` when there are no blocks in the state. +/// +/// `lightwalletd` expects error code `0` when there are no blocks in the state. +// +// TODO: find the source code that expects or generates this error +pub const NO_BLOCKS_IN_STATE_ERROR_CODE: ErrorCode = ErrorCode::ServerError(0); + +/// The RPC error used by `zcashd` when there are no blocks in the state. +// +// TODO: find the source code that expects or generates this error text, if there is any +// replace literal Error { ... } with this error +pub fn no_blocks_in_state_error() -> Error { + Error { + code: NO_BLOCKS_IN_STATE_ERROR_CODE, + message: "No blocks in state".to_string(), + data: None, + } +} + /// When logging parameter data, only log this much data. pub const MAX_PARAMS_LOG_LENGTH: usize = 100; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 4e1ae2c61bd..e99258ad78f 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -11,10 +11,10 @@ use zcash_address::{self, unified::Encoding, TryFromAddress}; use zebra_chain::{ amount::Amount, - block::{self, Block, Height}, + block::{self, Block, Height, TryIntoHeight}, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, - parameters::Network, + parameters::{Network, POW_AVERAGING_WINDOW}, primitives, serialization::ZcashDeserializeInto, transparent::{ @@ -142,27 +142,32 @@ pub trait GetBlockTemplateRpc { #[rpc(name = "getmininginfo")] fn get_mining_info(&self) -> BoxFuture>; - /// Returns the estimated network solutions per second based on the last `num_blocks` before `height`. - /// If `num_blocks` is not supplied, uses 120 blocks. - /// If `height` is not supplied or is 0, uses the tip height. + /// Returns the estimated network solutions per second based on the last `num_blocks` before + /// `height`. + /// + /// If `num_blocks` is not supplied, uses 120 blocks. If it is 0 or -1, uses the difficulty + /// averaging window. + /// If `height` is not supplied or is -1, uses the tip height. /// /// zcashd reference: [`getnetworksolps`](https://zcash.github.io/rpc/getnetworksolps.html) #[rpc(name = "getnetworksolps")] fn get_network_sol_ps( &self, - num_blocks: Option, + num_blocks: Option, height: Option, ) -> BoxFuture>; - /// Returns the estimated network solutions per second based on the last `num_blocks` before `height`. - /// If `num_blocks` is not supplied, uses 120 blocks. - /// If `height` is not supplied or is 0, uses the tip height. + /// Returns the estimated network solutions per second based on the last `num_blocks` before + /// `height`. + /// + /// This method name is deprecated, use [`getnetworksolps`](Self::get_network_sol_ps) instead. + /// See that method for details. /// /// zcashd reference: [`getnetworkhashps`](https://zcash.github.io/rpc/getnetworkhashps.html) #[rpc(name = "getnetworkhashps")] fn get_network_hash_ps( &self, - num_blocks: Option, + num_blocks: Option, height: Option, ) -> BoxFuture> { self.get_network_sol_ps(num_blocks, height) @@ -838,13 +843,22 @@ where fn get_network_sol_ps( &self, - num_blocks: Option, + num_blocks: Option, height: Option, ) -> BoxFuture> { - let num_blocks = num_blocks - .map(|num_blocks| num_blocks.max(1)) - .unwrap_or(DEFAULT_SOLUTION_RATE_WINDOW_SIZE); - let height = height.and_then(|height| (height > 1).then_some(Height(height as u32))); + // Default number of blocks is 120 if not supplied. + let mut num_blocks = num_blocks.unwrap_or(DEFAULT_SOLUTION_RATE_WINDOW_SIZE); + // But if it is 0 or negative, it uses the proof of work averaging window. + if num_blocks < 1 { + num_blocks = i32::try_from(POW_AVERAGING_WINDOW).expect("fits in i32"); + } + let num_blocks = + usize::try_from(num_blocks).expect("just checked for negatives, i32 fits in usize"); + + // Default height is the tip height if not supplied. Negative values also mean the tip + // height. Since negative values aren't valid heights, we can just use the conversion. + let height = height.and_then(|height| height.try_into_height().ok()); + let mut state = self.state.clone(); async move { @@ -861,11 +875,9 @@ where })?; let solution_rate = match response { - ReadResponse::SolutionRate(solution_rate) => solution_rate.ok_or(Error { - code: ErrorCode::ServerError(0), - message: "No blocks in state".to_string(), - data: None, - })?, + // zcashd returns a 0 rate when the calculation is invalid + ReadResponse::SolutionRate(solution_rate) => solution_rate.unwrap_or(0), + _ => unreachable!("unmatched response to a solution rate request"), }; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs b/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs index 8b6b9174a58..75b26055c8e 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/constants.rs @@ -54,7 +54,7 @@ pub const NOT_SYNCED_ERROR_CODE: ErrorCode = ErrorCode::ServerError(-10); /// The default window size specifying how many blocks to check when estimating the chain's solution rate. /// /// Based on default value in zcashd. -pub const DEFAULT_SOLUTION_RATE_WINDOW_SIZE: usize = 120; +pub const DEFAULT_SOLUTION_RATE_WINDOW_SIZE: i32 = 120; /// The funding stream order in `zcashd` RPC responses. /// diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 9d89a459c72..769d561897b 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -1148,32 +1148,39 @@ async fn rpc_getnetworksolps() { ); let get_network_sol_ps_inputs = [ - (None, None), - (Some(0), None), - (Some(0), Some(0)), - (Some(0), Some(-1)), - (Some(0), Some(10)), - (Some(0), Some(i32::MAX)), - (Some(1), None), - (Some(1), Some(0)), - (Some(1), Some(-1)), - (Some(1), Some(10)), - (Some(1), Some(i32::MAX)), - (Some(usize::MAX), None), - (Some(usize::MAX), Some(0)), - (Some(usize::MAX), Some(-1)), - (Some(usize::MAX), Some(10)), - (Some(usize::MAX), Some(i32::MAX)), + // num_blocks, height, return value + (None, None, Ok(2)), + (Some(-4), None, Ok(2)), + (Some(-3), Some(0), Ok(0)), + (Some(-2), Some(-4), Ok(2)), + (Some(-1), Some(10), Ok(2)), + (Some(-1), Some(i32::MAX), Ok(2)), + (Some(0), None, Ok(2)), + (Some(0), Some(0), Ok(0)), + (Some(0), Some(-3), Ok(2)), + (Some(0), Some(10), Ok(2)), + (Some(0), Some(i32::MAX), Ok(2)), + (Some(1), None, Ok(4096)), + (Some(1), Some(0), Ok(0)), + (Some(1), Some(-2), Ok(4096)), + (Some(1), Some(10), Ok(4096)), + (Some(1), Some(i32::MAX), Ok(4096)), + (Some(i32::MAX), None, Ok(2)), + (Some(i32::MAX), Some(0), Ok(0)), + (Some(i32::MAX), Some(-1), Ok(2)), + (Some(i32::MAX), Some(10), Ok(2)), + (Some(i32::MAX), Some(i32::MAX), Ok(2)), ]; - for (num_blocks_input, height_input) in get_network_sol_ps_inputs { + for (num_blocks_input, height_input, return_value) in get_network_sol_ps_inputs { let get_network_sol_ps_result = get_block_template_rpc .get_network_sol_ps(num_blocks_input, height_input) .await; - assert!( - get_network_sol_ps_result - .is_ok(), - "get_network_sol_ps({num_blocks_input:?}, {height_input:?}) call with should be ok, got: {get_network_sol_ps_result:?}" + assert_eq!( + get_network_sol_ps_result, return_value, + "get_network_sol_ps({num_blocks_input:?}, {height_input:?}) result\n\ + should be {return_value:?},\n\ + got: {get_network_sol_ps_result:?}" ); } } diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 700814ae1c9..f2513119e2d 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -938,9 +938,10 @@ pub enum ReadRequest { /// /// Returns [`ReadResponse::SolutionRate`] SolutionRate { - /// Specifies over difficulty averaging window. + /// The number of blocks to calculate the average difficulty for. num_blocks: usize, - /// Optionally estimate the network speed at the time when a certain block was found + /// Optionally estimate the network solution rate at the time when this height was mined. + /// Otherwise, estimate at the current tip height. height: Option, }, diff --git a/zebra-state/src/service/block_iter.rs b/zebra-state/src/service/block_iter.rs index 9fc9bc8ff3c..6cb0df903ba 100644 --- a/zebra-state/src/service/block_iter.rs +++ b/zebra-state/src/service/block_iter.rs @@ -1,102 +1,86 @@ //! Iterators for blocks in the non-finalized and finalized state. -use std::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; -use zebra_chain::block::{self, Block}; +use zebra_chain::block::{self, Block, Height}; -use crate::{service::non_finalized_state::NonFinalizedState, HashOrHeight}; +use crate::{ + service::{ + finalized_state::ZebraDb, + non_finalized_state::{Chain, NonFinalizedState}, + read, + }, + HashOrHeight, +}; -use super::finalized_state::ZebraDb; - -/// Iterator for state blocks. +/// Generic state chain iterator, which iterates by block height or hash. +/// Can be used for blocks, block headers, or any type indexed by [`HashOrHeight`]. /// -/// Starts at any block in any non-finalized or finalized chain, +/// Starts at any hash or height in any non-finalized or finalized chain, /// and iterates in reverse height order. (Towards the genesis block.) -pub(crate) struct Iter<'a> { - pub(super) non_finalized_state: &'a NonFinalizedState, - pub(super) db: &'a ZebraDb, - pub(super) state: IterState, -} - -pub(super) enum IterState { - NonFinalized(block::Hash), - Finalized(block::Height), - Finished, +#[derive(Clone, Debug)] +pub(crate) struct Iter { + /// The non-finalized chain fork we're iterating, if the iterator is in the non-finalized state. + /// + /// This is a cloned copy of a potentially out-of-date chain fork. + pub(super) chain: Option>, + + /// The finalized database we're iterating. + /// + /// This is the shared live database instance, which can concurrently write blocks. + pub(super) db: ZebraDb, + + /// The height of the item which will be yielded by `Iterator::next()`. + pub(super) height: Option, + + /// An internal marker type that tells the Rust type system what we're iterating. + iterable: PhantomData, } -impl Iter<'_> { - fn next_non_finalized_block(&mut self) -> Option> { - let Iter { - non_finalized_state, - db: _, - state, - } = self; - - let hash = match state { - IterState::NonFinalized(hash) => *hash, - IterState::Finalized(_) | IterState::Finished => unreachable!(), - }; - - if let Some(block) = non_finalized_state.any_block_by_hash(hash) { - let hash = block.header.previous_block_hash; - self.state = IterState::NonFinalized(hash); - Some(block) - } else { - None - } - } - - #[allow(clippy::unwrap_in_result)] - fn next_finalized_block(&mut self) -> Option> { - let Iter { - non_finalized_state: _, - db, - state, - } = self; - - let hash_or_height: HashOrHeight = match *state { - IterState::Finalized(height) => height.into(), - IterState::NonFinalized(hash) => hash.into(), - IterState::Finished => unreachable!(), - }; - - if let Some(block) = db.block(hash_or_height) { - let height = block - .coinbase_height() - .expect("valid blocks have a coinbase height"); - - if let Some(next_height) = height - 1 { - self.state = IterState::Finalized(next_height); +impl Iter +where + Item: ChainItem, +{ + /// Returns an item by height, and updates the iterator's internal state to point to the + /// previous height. + fn yield_by_height(&mut self) -> Option { + let current_height = self.height?; + + // TODO: + // Check if the root of the chain connects to the finalized state. Cloned chains can become + // disconnected if they are concurrently pruned by a finalized block from another chain + // fork. If that happens, the iterator is invalid and should stop returning items. + // + // Currently, we skip from the disconnected chain root to the previous height in the + // finalized state, which is usually ok, but could cause consensus or light wallet bugs. + let item = Item::read(self.chain.as_ref(), &self.db, current_height); + + // The iterator is finished if the current height is genesis. + self.height = current_height.previous().ok(); + + // Drop the chain if we've finished using it. + if let Some(chain) = self.chain.as_ref() { + if let Some(height) = self.height { + if !chain.contains_block_height(height) { + std::mem::drop(self.chain.take()); + } } else { - self.state = IterState::Finished; + std::mem::drop(self.chain.take()); } - - Some(block) - } else { - self.state = IterState::Finished; - None } - } - /// Return the height for the block at `hash` in any chain. - fn any_height_by_hash(&self, hash: block::Hash) -> Option { - self.non_finalized_state - .any_height_by_hash(hash) - .or_else(|| self.db.height(hash)) + item } } -impl Iterator for Iter<'_> { - type Item = Arc; +impl Iterator for Iter +where + Item: ChainItem, +{ + type Item = Item::Type; fn next(&mut self) -> Option { - match self.state { - IterState::NonFinalized(_) => self - .next_non_finalized_block() - .or_else(|| self.next_finalized_block()), - IterState::Finalized(_) => self.next_finalized_block(), - IterState::Finished => None, - } + self.yield_by_height() } fn size_hint(&self) -> (usize, Option) { @@ -105,34 +89,121 @@ impl Iterator for Iter<'_> { } } -impl std::iter::FusedIterator for Iter<'_> {} - -impl ExactSizeIterator for Iter<'_> { +impl ExactSizeIterator for Iter +where + Item: ChainItem, +{ fn len(&self) -> usize { - match self.state { - IterState::NonFinalized(hash) => self - .any_height_by_hash(hash) - .map(|height| (height.0 + 1) as _) - .unwrap_or(0), - IterState::Finalized(height) => (height.0 + 1) as _, - IterState::Finished => 0, - } + // Add one to the height for the genesis block. + // + // TODO: + // If the Item can skip heights, or return multiple items per block, we can't calculate + // its length using the block height. For example, subtree end height iterators, or + // transaction iterators. + // + // TODO: + // Check if the root of the chain connects to the finalized state. If that happens, the + // iterator is invalid and the length should be zero. See the comment in yield_by_height() + // for details. + self.height.map_or(0, |height| height.as_usize() + 1) } } -/// Return an iterator over the relevant chain of the block identified by -/// `hash`, in order from the largest height to the genesis block. +// TODO: +// If the Item can return None before it gets to genesis, it is not fused. For example, subtree +// end height iterators. +impl std::iter::FusedIterator for Iter where Item: ChainItem {} + +/// A trait that implements iteration for a specific chain type. +pub(crate) trait ChainItem { + type Type; + + /// Read the `Type` at `height` from the non-finalized `chain` or finalized `db`. + fn read(chain: Option<&Arc>, db: &ZebraDb, height: Height) -> Option; +} + +// Block iteration + +impl ChainItem for Block { + type Type = Arc; + + fn read(chain: Option<&Arc>, db: &ZebraDb, height: Height) -> Option { + read::block(chain, db, height.into()) + } +} + +// Block header iteration + +impl ChainItem for block::Header { + type Type = Arc; + + fn read(chain: Option<&Arc>, db: &ZebraDb, height: Height) -> Option { + read::block_header(chain, db, height.into()) + } +} + +/// Returns a block iterator over the relevant chain containing `hash`, +/// in order from the largest height to genesis. +/// +/// The block with `hash` is included in the iterator. +/// `hash` can come from any chain or `db`. +/// +/// Use [`any_chain_ancestor_iter()`] in new code. +pub(crate) fn any_ancestor_blocks( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, + hash: block::Hash, +) -> Iter { + any_chain_ancestor_iter(non_finalized_state, db, hash) +} + +/// Returns a generic chain item iterator over the relevant chain containing `hash`, +/// in order from the largest height to genesis. /// -/// The block identified by `hash` is included in the chain of blocks yielded -/// by the iterator. `hash` can come from any chain. -pub(crate) fn any_ancestor_blocks<'a>( - non_finalized_state: &'a NonFinalizedState, - db: &'a ZebraDb, +/// The item with `hash` is included in the iterator. +/// `hash` can come from any chain or `db`. +pub(crate) fn any_chain_ancestor_iter( + non_finalized_state: &NonFinalizedState, + db: &ZebraDb, hash: block::Hash, -) -> Iter<'a> { +) -> Iter +where + Item: ChainItem, +{ + // We need to look up the relevant chain, and the height for the hash. + let chain = non_finalized_state.find_chain(|chain| chain.contains_block_hash(hash)); + let height = read::height_by_hash(chain.as_ref(), db, hash); + + Iter { + chain, + db: db.clone(), + height, + iterable: PhantomData, + } +} + +/// Returns a generic chain item iterator over a `chain` containing `hash_or_height`, +/// in order from the largest height to genesis. +/// +/// The item with `hash_or_height` is included in the iterator. +/// `hash_or_height` must be in `chain` or `db`. +#[allow(dead_code)] +pub(crate) fn known_chain_ancestor_iter( + chain: Option>, + db: &ZebraDb, + hash_or_height: HashOrHeight, +) -> Iter +where + Item: ChainItem, +{ + // We need to look up the height for the hash. + let height = + hash_or_height.height_or_else(|hash| read::height_by_hash(chain.as_ref(), db, hash)); + Iter { - non_finalized_state, - db, - state: IterState::NonFinalized(hash), + chain, + db: db.clone(), + height, + iterable: PhantomData, } } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block.rs b/zebra-state/src/service/finalized_state/zebra_db/block.rs index 25745e44afb..85959849985 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block.rs @@ -97,6 +97,23 @@ impl ZebraDb { self.db.zs_get(&height_by_hash, &hash) } + /// Returns the previous block hash for the given block hash in the finalized state. + #[allow(dead_code)] + pub fn prev_block_hash_for_hash(&self, hash: block::Hash) -> Option { + let height = self.height(hash)?; + let prev_height = height.previous().ok()?; + + self.hash(prev_height) + } + + /// Returns the previous block height for the given block hash in the finalized state. + #[allow(dead_code)] + pub fn prev_block_height_for_hash(&self, hash: block::Hash) -> Option { + let height = self.height(hash)?; + + height.previous().ok() + } + /// Returns the [`block::Header`] with [`block::Hash`] or /// [`Height`], if it exists in the finalized chain. // diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index bc342e5be9a..c01c16767d9 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -436,16 +436,20 @@ impl NonFinalizedState { .any(|chain| chain.height_by_hash.contains_key(hash)) } - /// Removes and returns the first chain satisfying the given predicate. + /// Returns the first chain satisfying the given predicate. /// /// If multiple chains satisfy the predicate, returns the chain with the highest difficulty. /// (Using the tip block hash tie-breaker.) - fn find_chain

(&mut self, mut predicate: P) -> Option<&Arc> + pub fn find_chain

(&self, mut predicate: P) -> Option> where P: FnMut(&Chain) -> bool, { // Reverse the iteration order, to find highest difficulty chains first. - self.chain_set.iter().rev().find(|chain| predicate(chain)) + self.chain_set + .iter() + .rev() + .find(|chain| predicate(chain)) + .cloned() } /// Returns the [`transparent::Utxo`] pointed to by the given @@ -460,7 +464,9 @@ impl NonFinalizedState { } /// Returns the `block` with the given hash in any chain. + #[allow(dead_code)] pub fn any_block_by_hash(&self, hash: block::Hash) -> Option> { + // This performs efficiently because the number of chains is limited to 10. for chain in self.chain_set.iter().rev() { if let Some(prepared) = chain .height_by_hash @@ -474,6 +480,14 @@ impl NonFinalizedState { None } + /// Returns the previous block hash for the given block hash in any chain. + #[allow(dead_code)] + pub fn any_prev_block_hash_for_hash(&self, hash: block::Hash) -> Option { + // This performs efficiently because the blocks are in memory. + self.any_block_by_hash(hash) + .map(|block| block.header.previous_block_hash) + } + /// Returns the hash for a given `block::Height` if it is present in the best chain. #[allow(dead_code)] pub fn best_hash(&self, height: block::Height) -> Option { @@ -510,6 +524,7 @@ impl NonFinalizedState { } /// Returns the height of `hash` in any chain. + #[allow(dead_code)] pub fn any_height_by_hash(&self, hash: block::Hash) -> Option { for chain in self.chain_set.iter().rev() { if let Some(height) = chain.height_by_hash.get(&hash) { @@ -568,10 +583,7 @@ impl NonFinalizedState { /// The chain can be an existing chain in the non-finalized state, or a freshly /// created fork. #[allow(clippy::unwrap_in_result)] - fn parent_chain( - &mut self, - parent_hash: block::Hash, - ) -> Result, ValidateContextError> { + fn parent_chain(&self, parent_hash: block::Hash) -> Result, ValidateContextError> { match self.find_chain(|chain| chain.non_finalized_tip_hash() == parent_hash) { // Clone the existing Arc in the non-finalized state Some(chain) => Ok(chain.clone()), diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 2b66b17cdf1..ef1e54215e8 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -450,8 +450,28 @@ impl Chain { /// Returns true is the chain contains the given block hash. /// Returns false otherwise. - pub fn contains_block_hash(&self, hash: &block::Hash) -> bool { - self.height_by_hash.contains_key(hash) + pub fn contains_block_hash(&self, hash: block::Hash) -> bool { + self.height_by_hash.contains_key(&hash) + } + + /// Returns true is the chain contains the given block height. + /// Returns false otherwise. + pub fn contains_block_height(&self, height: Height) -> bool { + self.blocks.contains_key(&height) + } + + /// Returns true is the chain contains the given block hash or height. + /// Returns false otherwise. + #[allow(dead_code)] + pub fn contains_hash_or_height(&self, hash_or_height: impl Into) -> bool { + use HashOrHeight::*; + + let hash_or_height = hash_or_height.into(); + + match hash_or_height { + Hash(hash) => self.contains_block_hash(hash), + Height(height) => self.contains_block_height(height), + } } /// Returns the non-finalized tip block height and hash. diff --git a/zebra-state/src/service/read/difficulty.rs b/zebra-state/src/service/read/difficulty.rs index b990daa5638..cb9734892b6 100644 --- a/zebra-state/src/service/read/difficulty.rs +++ b/zebra-state/src/service/read/difficulty.rs @@ -9,12 +9,13 @@ use zebra_chain::{ history_tree::HistoryTree, parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING}, serialization::{DateTime32, Duration32}, - work::difficulty::{CompactDifficulty, PartialCumulativeWork}, + work::difficulty::{CompactDifficulty, PartialCumulativeWork, Work}, }; use crate::{ service::{ any_ancestor_blocks, + block_iter::any_chain_ancestor_iter, check::{ difficulty::{ BLOCK_MAX_TIME_SINCE_MEDIAN, POW_ADJUSTMENT_BLOCK_SPAN, POW_MEDIAN_BLOCK_SPAN, @@ -87,42 +88,55 @@ pub fn solution_rate( num_blocks: usize, start_hash: Hash, ) -> Option { - // Take 1 extra block for calculating the number of seconds between when mining on the first block likely started. - // The work for the last block in this iterator is not added to `total_work`. - let mut block_iter = any_ancestor_blocks(non_finalized_state, db, start_hash) - .take(num_blocks.checked_add(1).unwrap_or(num_blocks)) - .peekable(); - - let get_work = |block: Arc| { - block - .header + // Take 1 extra header for calculating the number of seconds between when mining on the first + // block likely started. The work for the extra header is not added to `total_work`. + // + // Since we can't take more headers than are actually in the chain, this automatically limits + // `num_blocks` to the chain length, like `zcashd` does. + let mut header_iter = + any_chain_ancestor_iter::(non_finalized_state, db, start_hash) + .take(num_blocks.checked_add(1).unwrap_or(num_blocks)) + .peekable(); + + let get_work = |header: &block::Header| { + header .difficulty_threshold .to_work() .expect("work has already been validated") }; - let block = block_iter.next()?; - let last_block_time = block.header.time; - - let mut total_work: PartialCumulativeWork = get_work(block).into(); - - loop { - // Return `None` if the iterator doesn't yield a second item. - let block = block_iter.next()?; - - if block_iter.peek().is_some() { - // Add the block's work to `total_work` if it's not the last item in the iterator. - // The last item in the iterator is only used to estimate when mining on the first block - // in the window of `num_blocks` likely started. - total_work += get_work(block); - } else { - let first_block_time = block.header.time; - let duration_between_first_and_last_block = last_block_time - first_block_time; - return Some( - total_work.as_u128() / duration_between_first_and_last_block.num_seconds() as u128, - ); - } + // If there are no blocks in the range, we can't return a useful result. + let last_header = header_iter.peek()?; + + // Initialize the cumulative variables. + let mut min_time = last_header.time; + let mut max_time = last_header.time; + + let mut last_work = Work::zero(); + let mut total_work = PartialCumulativeWork::zero(); + + for header in header_iter { + min_time = min_time.min(header.time); + max_time = max_time.max(header.time); + + last_work = get_work(&header); + total_work += last_work; } + + // We added an extra header so we could estimate when mining on the first block + // in the window of `num_blocks` likely started. But we don't want to add the work + // for that header. + total_work -= last_work; + + let work_duration = (max_time - min_time).num_seconds(); + + // Avoid division by zero errors and negative average work. + // This also handles the case where there's only one block in the range. + if work_duration <= 0 { + return None; + } + + Some(total_work.as_u128() / work_duration as u128) } /// Do a consistency check by checking the finalized tip before and after all other database diff --git a/zebra-state/src/service/read/find.rs b/zebra-state/src/service/read/find.rs index 3e1c4996d8a..44491876ec9 100644 --- a/zebra-state/src/service/read/find.rs +++ b/zebra-state/src/service/read/find.rs @@ -108,7 +108,7 @@ pub fn non_finalized_state_contains_block_hash( hash: block::Hash, ) -> Option { let mut chains_iter = non_finalized_state.chain_iter(); - let is_hash_in_chain = |chain: &Arc| chain.contains_block_hash(&hash); + let is_hash_in_chain = |chain: &Arc| chain.contains_block_hash(hash); // Equivalent to `chain_set.iter().next_back()` in `NonFinalizedState.best_chain()` method. let best_chain = chains_iter.next();