diff --git a/zebra-chain/src/chain_tip.rs b/zebra-chain/src/chain_tip.rs index 3ad4624d3..61c6b6849 100644 --- a/zebra-chain/src/chain_tip.rs +++ b/zebra-chain/src/chain_tip.rs @@ -2,10 +2,16 @@ use std::sync::Arc; -use crate::{block, transaction}; +use chrono::{DateTime, Utc}; + +use self::network_chain_tip_height_estimator::NetworkChainTipHeightEstimator; +use crate::{block, parameters::Network, transaction}; #[cfg(any(test, feature = "proptest-impl"))] pub mod mock; +mod network_chain_tip_height_estimator; +#[cfg(test)] +mod tests; /// An interface for querying the chain tip. /// @@ -19,11 +25,36 @@ pub trait ChainTip { /// Return the block hash of the best chain tip. fn best_tip_hash(&self) -> Option; + /// Return the block time of the best chain tip. + fn best_tip_block_time(&self) -> Option>; + + /// Return the height and the block time of the best chain tip. + /// + /// Returning both values at the same time guarantees that they refer to the same chain tip. + fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime)>; + /// Return the mined transaction IDs of the transactions in the best chain tip block. /// /// All transactions with these mined IDs should be rejected from the mempool, /// even if their authorizing data is different. fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]>; + + /// Return an estimate of the network chain tip's height. + /// + /// The estimate is calculated based on the current local time, the block time of the best tip + /// and the height of the best tip. + fn estimate_network_chain_tip_height( + &self, + network: Network, + now: DateTime, + ) -> Option { + let (current_height, current_block_time) = self.best_tip_height_and_block_time()?; + + let estimator = + NetworkChainTipHeightEstimator::new(current_block_time, current_height, network); + + Some(estimator.estimate_height_at(now)) + } } /// A chain tip that is always empty. @@ -39,6 +70,14 @@ impl ChainTip for NoChainTip { None } + fn best_tip_block_time(&self) -> Option> { + None + } + + fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime)> { + None + } + fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> { Arc::new([]) } diff --git a/zebra-chain/src/chain_tip/mock.rs b/zebra-chain/src/chain_tip/mock.rs index fe84362c3..839a39a53 100644 --- a/zebra-chain/src/chain_tip/mock.rs +++ b/zebra-chain/src/chain_tip/mock.rs @@ -2,17 +2,28 @@ use std::sync::Arc; +use chrono::{DateTime, Utc}; use tokio::sync::watch; use crate::{block, chain_tip::ChainTip, transaction}; -/// A sender that sets the `best_tip_height` of a [`MockChainTip`].] -pub type MockChainTipSender = watch::Sender>; +/// A sender to sets the values read by a [`MockChainTip`]. +pub struct MockChainTipSender { + /// A sender that sets the `best_tip_height` of a [`MockChainTip`]. + best_tip_height: watch::Sender>, + + /// A sender that sets the `best_tip_block_time` of a [`MockChainTip`]. + best_tip_block_time: watch::Sender>>, +} /// A mock [`ChainTip`] implementation that allows setting the `best_tip_height` externally. #[derive(Clone, Debug)] pub struct MockChainTip { + /// A mocked `best_tip_height` value set by the [`MockChainTipSender`]. best_tip_height: watch::Receiver>, + + /// A mocked `best_tip_height` value set by the [`MockChainTipSender`]. + best_tip_block_time: watch::Receiver>>, } impl MockChainTip { @@ -23,13 +34,20 @@ impl MockChainTip { /// /// Initially, the best tip height is [`None`]. pub fn new() -> (Self, MockChainTipSender) { - let (sender, receiver) = watch::channel(None); + let (height_sender, height_receiver) = watch::channel(None); + let (time_sender, time_receiver) = watch::channel(None); let mock_chain_tip = MockChainTip { - best_tip_height: receiver, + best_tip_height: height_receiver, + best_tip_block_time: time_receiver, }; - (mock_chain_tip, sender) + let mock_chain_tip_sender = MockChainTipSender { + best_tip_height: height_sender, + best_tip_block_time: time_sender, + }; + + (mock_chain_tip, mock_chain_tip_sender) } } @@ -42,7 +60,34 @@ impl ChainTip for MockChainTip { unreachable!("Method not used in tests"); } + fn best_tip_block_time(&self) -> Option> { + *self.best_tip_block_time.borrow() + } + + fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime)> { + let height = (*self.best_tip_height.borrow())?; + let block_time = (*self.best_tip_block_time.borrow())?; + + Some((height, block_time)) + } + fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> { unreachable!("Method not used in tests"); } } + +impl MockChainTipSender { + /// Send a new best tip height to the [`MockChainTip`]. + pub fn send_best_tip_height(&self, height: impl Into>) { + self.best_tip_height + .send(height.into()) + .expect("attempt to send a best tip height to a dropped `MockChainTip`"); + } + + /// Send a new best tip block time to the [`MockChainTip`]. + pub fn send_best_tip_block_time(&self, block_time: impl Into>>) { + self.best_tip_block_time + .send(block_time.into()) + .expect("attempt to send a best tip block time to a dropped `MockChainTip`"); + } +} diff --git a/zebra-chain/src/chain_tip/network_chain_tip_height_estimator.rs b/zebra-chain/src/chain_tip/network_chain_tip_height_estimator.rs new file mode 100644 index 000000000..3a310524d --- /dev/null +++ b/zebra-chain/src/chain_tip/network_chain_tip_height_estimator.rs @@ -0,0 +1,139 @@ +//! A module with helper code to estimate the network chain tip's height. + +use std::vec; + +use chrono::{DateTime, Duration, Utc}; + +use crate::{ + block, + parameters::{Network, NetworkUpgrade}, +}; + +/// A type used to estimate the chain tip height at a given time. +/// +/// The estimation is based on a known block time and height for a block. The estimator will then +/// handle any target spacing changes to extrapolate the provided information into a target time +/// and obtain an estimation for the height at that time. +/// +/// # Usage +/// +/// 1. Use [`NetworkChainTipHeightEstimator::new`] to create and initialize a new instance with the +/// information about a known block. +/// 2. Use [`NetworkChainTipHeightEstimator::estimate_height_at`] to obtain a height estimation for +/// a given instant. +#[derive(Debug)] +pub struct NetworkChainTipHeightEstimator { + current_block_time: DateTime, + current_height: block::Height, + current_target_spacing: Duration, + next_target_spacings: vec::IntoIter<(block::Height, Duration)>, +} + +impl NetworkChainTipHeightEstimator { + /// Create a [`NetworkChainTipHeightEstimator`] and initialize it with the information to use + /// for calculating a chain height estimate. + /// + /// The provided information (`current_block_time`, `current_height` and `network`) **must** + /// refer to the same block. + /// + /// # Implementation details + /// + /// The `network` is used to obtain a list of target spacings used in different sections of the + /// block chain. The first section is used as a starting point. + pub fn new( + current_block_time: DateTime, + current_height: block::Height, + network: Network, + ) -> Self { + let mut target_spacings = NetworkUpgrade::target_spacings(network); + let (_genesis_height, initial_target_spacing) = + target_spacings.next().expect("No target spacings were set"); + + NetworkChainTipHeightEstimator { + current_block_time, + current_height, + current_target_spacing: initial_target_spacing, + // TODO: Remove the `Vec` allocation once existential `impl Trait`s are available. + next_target_spacings: target_spacings.collect::>().into_iter(), + } + } + + /// Estimate the network chain tip height at the provided `target_time`. + /// + /// # Implementation details + /// + /// The `current_block_time` and the `current_height` is advanced to the end of each section + /// that has a different target spacing time. Once the `current_block_time` passes the + /// `target_time`, the last active target spacing time is used to calculate the final height + /// estimation. + pub fn estimate_height_at(mut self, target_time: DateTime) -> block::Height { + while let Some((change_height, next_target_spacing)) = self.next_target_spacings.next() { + self.estimate_up_to(change_height); + + if self.current_block_time >= target_time { + break; + } + + self.current_target_spacing = next_target_spacing; + } + + self.estimate_height_at_with_current_target_spacing(target_time) + } + + /// Advance the `current_block_time` and `current_height` to the next change in target spacing + /// time. + /// + /// The `current_height` is advanced to `max_height` (if it's not already past that height). + /// The amount of blocks advanced is then used to extrapolate the amount to advance the + /// `current_block_time`. + fn estimate_up_to(&mut self, max_height: block::Height) { + let remaining_blocks = i64::from(max_height - self.current_height); + + if remaining_blocks > 0 { + let target_spacing_seconds = self.current_target_spacing.num_seconds(); + let time_to_activation = Duration::seconds(remaining_blocks * target_spacing_seconds); + + self.current_block_time = self.current_block_time + time_to_activation; + self.current_height = max_height; + } + } + + /// Calculate an estimate for the chain height using the `current_target_spacing`. + /// + /// Using the difference between the `target_time` and the `current_block_time` and the + /// `current_target_spacing`, the number of blocks to reach the `target_time` from the + /// `current_block_time` is calculated. The value is added to the `current_height` to calculate + /// the final estimate. + fn estimate_height_at_with_current_target_spacing( + self, + target_time: DateTime, + ) -> block::Height { + let time_difference = target_time - self.current_block_time; + let mut time_difference_seconds = time_difference.num_seconds(); + + if time_difference_seconds < 0 { + // Undo the rounding towards negative infinity done by `chrono::Duration`, which yields + // an incorrect value for the dividend of the division. + // + // (See https://docs.rs/time/0.1.44/src/time/duration.rs.html#166-173) + time_difference_seconds -= 1; + } + + let block_difference = i32::try_from( + // Euclidean division is used so that the number is rounded towards negative infinity, + // so that fractionary values always round down to the previous height when going back + // in time (i.e., when the dividend is negative). This works because the divisor (the + // target spacing) is always positive. + time_difference_seconds.div_euclid(self.current_target_spacing.num_seconds()), + ) + .expect("time difference is too large"); + + if -(block_difference as i64) > self.current_height.0 as i64 { + // Gracefully handle attempting to estimate a block before genesis. This can happen if + // the local time is set incorrectly to a time too far in the past. + block::Height(0) + } else { + (self.current_height + block_difference).expect("block difference is too large") + } + } +} diff --git a/zebra-chain/src/chain_tip/tests.rs b/zebra-chain/src/chain_tip/tests.rs new file mode 100644 index 000000000..2bf82ef4e --- /dev/null +++ b/zebra-chain/src/chain_tip/tests.rs @@ -0,0 +1 @@ +mod prop; diff --git a/zebra-chain/src/chain_tip/tests/prop.rs b/zebra-chain/src/chain_tip/tests/prop.rs new file mode 100644 index 000000000..1543e9c12 --- /dev/null +++ b/zebra-chain/src/chain_tip/tests/prop.rs @@ -0,0 +1,97 @@ +use chrono::Duration; +use proptest::prelude::*; + +use crate::{ + block, + chain_tip::{mock::MockChainTip, ChainTip}, + parameters::{Network, NetworkUpgrade}, + serialization::arbitrary::datetime_u32, +}; + +const NU_BEFORE_BLOSSOM: NetworkUpgrade = NetworkUpgrade::Sapling; + +proptest! { + /// Test network chain tip height estimation. + /// + /// Given a pair of block heights, estimate the time difference and use it with the lowest + /// height to check if the estimation of the height is correct. + #[test] + fn network_chain_tip_height_estimation_is_correct( + network in any::(), + mut block_heights in any::<[block::Height; 2]>(), + current_block_time in datetime_u32(), + time_displacement_factor in 0.0..1.0_f64, + ) { + let (chain_tip, mock_chain_tip_sender) = MockChainTip::new(); + let blossom_activation_height = NetworkUpgrade::Blossom + .activation_height(network) + .expect("Blossom activation height is missing"); + + block_heights.sort(); + let current_height = block_heights[0]; + let network_height = block_heights[1]; + + mock_chain_tip_sender.send_best_tip_height(current_height); + mock_chain_tip_sender.send_best_tip_block_time(current_block_time); + + let estimated_time_difference = + // Estimate time difference for heights before Blossom activation. + estimate_time_difference( + current_height.min(blossom_activation_height), + network_height.min(blossom_activation_height), + NU_BEFORE_BLOSSOM, + ) + // Estimate time difference for heights after Blossom activation. + + estimate_time_difference( + current_height.max(blossom_activation_height), + network_height.max(blossom_activation_height), + NetworkUpgrade::Blossom, + ); + + let time_displacement = calculate_time_displacement( + time_displacement_factor, + NetworkUpgrade::current(network, network_height), + ); + + let mock_local_time = current_block_time + estimated_time_difference + time_displacement; + + assert_eq!( + chain_tip.estimate_network_chain_tip_height(network, mock_local_time), + Some(network_height) + ); + } +} + +/// Estimate the time necessary for the chain to progress from `start_height` to `end_height`, +/// assuming each block is produced at exactly the number of seconds of the target spacing for the +/// `active_network_upgrade`. +fn estimate_time_difference( + start_height: block::Height, + end_height: block::Height, + active_network_upgrade: NetworkUpgrade, +) -> Duration { + let spacing_seconds = active_network_upgrade.target_spacing().num_seconds(); + + let height_difference = i64::from(end_height - start_height); + + Duration::seconds(height_difference * spacing_seconds) +} + +/// Use `displacement` to get a displacement duration between zero and the target spacing of the +/// specified `network_upgrade`. +/// +/// This is used to "displace" the time used in the test so that the test inputs aren't exact +/// multiples of the target spacing. +fn calculate_time_displacement(displacement: f64, network_upgrade: NetworkUpgrade) -> Duration { + let target_spacing = network_upgrade.target_spacing(); + + let nanoseconds = target_spacing + .num_nanoseconds() + .expect("Target spacing nanoseconds fit in a i64"); + + let displaced_nanoseconds = (displacement * nanoseconds as f64) + .round() + .clamp(i64::MIN as f64, i64::MAX as f64) as i64; + + Duration::nanoseconds(displaced_nanoseconds) +} diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 4182b989f..fced0c58a 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -259,6 +259,24 @@ impl NetworkUpgrade { NetworkUpgrade::current(network, height).target_spacing() } + /// Returns all the target block spacings for `network` and the heights where they start. + pub fn target_spacings(network: Network) -> impl Iterator { + [ + (NetworkUpgrade::Genesis, PRE_BLOSSOM_POW_TARGET_SPACING), + (NetworkUpgrade::Blossom, POST_BLOSSOM_POW_TARGET_SPACING), + ] + .into_iter() + .map(move |(upgrade, spacing_seconds)| { + let activation_height = upgrade + .activation_height(network) + .expect("missing activation height for target spacing change"); + + let target_spacing = Duration::seconds(spacing_seconds); + + (activation_height, target_spacing) + }) + } + /// Returns the minimum difficulty block spacing for `network` and `height`. /// Returns `None` if the testnet minimum difficulty consensus rule is not active. /// diff --git a/zebra-network/src/peer/minimum_peer_version/tests.rs b/zebra-network/src/peer/minimum_peer_version/tests.rs index ad9fdd22a..83700f5f1 100644 --- a/zebra-network/src/peer/minimum_peer_version/tests.rs +++ b/zebra-network/src/peer/minimum_peer_version/tests.rs @@ -12,9 +12,9 @@ mod prop; impl MinimumPeerVersion { pub fn with_mock_chain_tip(network: Network) -> (Self, MockChainTipSender) { - let (chain_tip, best_tip_height) = MockChainTip::new(); + let (chain_tip, best_tip) = MockChainTip::new(); let minimum_peer_version = MinimumPeerVersion::new(chain_tip, network); - (minimum_peer_version, best_tip_height) + (minimum_peer_version, best_tip) } } diff --git a/zebra-network/src/peer/minimum_peer_version/tests/prop.rs b/zebra-network/src/peer/minimum_peer_version/tests/prop.rs index f3884d2cf..cab53748d 100644 --- a/zebra-network/src/peer/minimum_peer_version/tests/prop.rs +++ b/zebra-network/src/peer/minimum_peer_version/tests/prop.rs @@ -11,12 +11,10 @@ proptest! { network in any::(), block_height in any::>(), ) { - let (mut minimum_peer_version, best_tip_height) = + let (mut minimum_peer_version, best_tip) = MinimumPeerVersion::with_mock_chain_tip(network); - best_tip_height - .send(block_height) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip.send_best_tip_height(block_height); let expected_minimum_version = Version::min_remote_for_height(network, block_height); @@ -29,13 +27,11 @@ proptest! { network in any::(), block_heights in any::>>(), ) { - let (mut minimum_peer_version, best_tip_height) = + let (mut minimum_peer_version, best_tip) = MinimumPeerVersion::with_mock_chain_tip(network); for block_height in block_heights { - best_tip_height - .send(block_height) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip.send_best_tip_height(block_height); let expected_minimum_version = Version::min_remote_for_height(network, block_height); @@ -49,7 +45,7 @@ proptest! { network in any::(), block_height_updates in any::>>>(), ) { - let (mut minimum_peer_version, best_tip_height) = + let (mut minimum_peer_version, best_tip) = MinimumPeerVersion::with_mock_chain_tip(network); let mut current_minimum_version = Version::min_remote_for_height(network, None); @@ -59,9 +55,7 @@ proptest! { for update in block_height_updates { if let Some(new_block_height) = update { - best_tip_height - .send(new_block_height) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip.send_best_tip_height(new_block_height); let new_minimum_version = Version::min_remote_for_height(network, new_block_height); diff --git a/zebra-network/src/peer_set/set/tests/prop.rs b/zebra-network/src/peer_set/set/tests/prop.rs index f7ab9e128..aa48a457b 100644 --- a/zebra-network/src/peer_set/set/tests/prop.rs +++ b/zebra-network/src/peer_set/set/tests/prop.rs @@ -30,9 +30,7 @@ proptest! { let (mut minimum_peer_version, best_tip_height) = MinimumPeerVersion::with_mock_chain_tip(network); - best_tip_height - .send(Some(block_height)) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip_height.send_best_tip_height(block_height); let current_minimum_version = minimum_peer_version.current(); @@ -64,9 +62,7 @@ proptest! { let (mut minimum_peer_version, best_tip_height) = MinimumPeerVersion::with_mock_chain_tip(block_heights.network); - best_tip_height - .send(Some(block_heights.before_upgrade)) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip_height.send_best_tip_height(block_heights.before_upgrade); runtime.block_on(async move { let (discovered_peers, mut harnesses) = peer_versions.mock_peer_discovery(); @@ -81,9 +77,7 @@ proptest! { minimum_peer_version.current(), )?; - best_tip_height - .send(Some(block_heights.after_upgrade)) - .expect("receiving endpoint lives as long as `minimum_peer_version`"); + best_tip_height.send_best_tip_height(block_heights.after_upgrade); check_if_only_up_to_date_peers_are_live( &mut peer_set, diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 968c572e1..6e382c642 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -54,6 +54,7 @@ impl From for ChainTipBlock { Self { hash, height, + time: block.header.time, transaction_hashes, previous_block_hash: block.header.previous_block_hash, } diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 0174675b1..68ff60c44 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -7,12 +7,15 @@ use std::sync::Arc; +use chrono::{DateTime, Utc}; use tokio::sync::watch; use tracing::instrument; #[cfg(any(test, feature = "proptest-impl"))] use proptest_derive::Arbitrary; +#[cfg(any(test, feature = "proptest-impl"))] +use zebra_chain::serialization::arbitrary::datetime_full; use zebra_chain::{ block, chain_tip::ChainTip, @@ -44,6 +47,13 @@ pub struct ChainTipBlock { /// The height of the best chain tip block. pub height: block::Height, + /// The network block time of the best chain tip block. + #[cfg_attr( + any(test, feature = "proptest-impl"), + proptest(strategy = "datetime_full()") + )] + pub time: DateTime, + /// The mined transaction IDs of the transactions in `block`, /// in the same order as `block.transactions`. pub transaction_hashes: Arc<[transaction::Hash]>, @@ -71,6 +81,7 @@ impl From for ChainTipBlock { Self { hash, height, + time: block.header.time, transaction_hashes, previous_block_hash: block.header.previous_block_hash, } @@ -89,6 +100,7 @@ impl From for ChainTipBlock { Self { hash, height, + time: block.header.time, transaction_hashes, previous_block_hash: block.header.previous_block_hash, } @@ -301,22 +313,26 @@ impl LatestChainTip { } impl ChainTip for LatestChainTip { - /// Return the height of the best chain tip. #[instrument(skip(self))] fn best_tip_height(&self) -> Option { self.with_chain_tip_block(|block| block.height) } - /// Return the block hash of the best chain tip. #[instrument(skip(self))] fn best_tip_hash(&self) -> Option { self.with_chain_tip_block(|block| block.hash) } - /// Return the mined transaction IDs of the transactions in the best chain tip block. - /// - /// All transactions with these mined IDs should be rejected from the mempool, - /// even if their authorizing data is different. + #[instrument(skip(self))] + fn best_tip_block_time(&self) -> Option> { + self.with_chain_tip_block(|block| block.time) + } + + #[instrument(skip(self))] + fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime)> { + self.with_chain_tip_block(|block| (block.height, block.time)) + } + #[instrument(skip(self))] fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> { self.with_chain_tip_block(|block| block.transaction_hashes.clone()) diff --git a/zebrad/src/components/mempool/tests/prop.rs b/zebrad/src/components/mempool/tests/prop.rs index 998c7cfe0..3a82cc454 100644 --- a/zebrad/src/components/mempool/tests/prop.rs +++ b/zebrad/src/components/mempool/tests/prop.rs @@ -4,10 +4,15 @@ use proptest::collection::vec; use proptest::prelude::*; use proptest_derive::Arbitrary; +use chrono::Duration; use tokio::time; use tower::{buffer::Buffer, util::BoxService}; -use zebra_chain::{block, parameters::Network, transaction::VerifiedUnminedTx}; +use zebra_chain::{ + block, + parameters::{Network, NetworkUpgrade}, + transaction::VerifiedUnminedTx, +}; use zebra_consensus::{error::TransactionError, transaction as tx}; use zebra_network as zn; use zebra_state::{self as zs, ChainTipBlock, ChainTipSender}; @@ -112,7 +117,7 @@ proptest! { for (fake_chain_tip, transaction) in fake_chain_tips.iter().zip(transactions.iter_mut()) { // Obtain a new chain tip based on the previous one. - let chain_tip = fake_chain_tip.to_chain_tip_block(&previous_chain_tip); + let chain_tip = fake_chain_tip.to_chain_tip_block(&previous_chain_tip, network); // Adjust the transaction expiry height based on the new chain // tip height so that the mempool does not evict the transaction @@ -269,14 +274,24 @@ impl FakeChainTip { /// Returns a new [`ChainTipBlock`] placed on top of the previous block if /// the chain is supposed to grow. Otherwise returns a [`ChainTipBlock`] /// that does not reference the previous one. - fn to_chain_tip_block(&self, previous: &ChainTipBlock) -> ChainTipBlock { + fn to_chain_tip_block(&self, previous: &ChainTipBlock, network: Network) -> ChainTipBlock { match self { - Self::Grow(chain_tip_block) => ChainTipBlock { - hash: chain_tip_block.hash, - height: block::Height(previous.height.0 + 1), - transaction_hashes: chain_tip_block.transaction_hashes.clone(), - previous_block_hash: previous.hash, - }, + Self::Grow(chain_tip_block) => { + let height = block::Height(previous.height.0 + 1); + let target_spacing = NetworkUpgrade::target_spacing_for_height(network, height); + + let mock_block_time_delta = Duration::seconds( + previous.time.timestamp() % (2 * target_spacing.num_seconds()), + ); + + ChainTipBlock { + hash: chain_tip_block.hash, + height, + time: previous.time + mock_block_time_delta, + transaction_hashes: chain_tip_block.transaction_hashes.clone(), + previous_block_hash: previous.hash, + } + } Self::Reset(chain_tip_block) => chain_tip_block.clone(), }