Estimate network chain tip height based on local node time and current best tip (#3492)

* Remove redundant documentation

The documentation was exactly the same as the documentation from the
trait.

* Calculate a mock time block delta for tests

Simulate a block being added to the chain with a random block time based
on the previous block time and the target spacing time.

* Add a `time` field to `ChainTipBlock`

Store the block time so that it's ready for a future chain that allows
obtaining the chain tip's block time.

* Add `ChainTip::best_tip_block_time` method

Allow obtaining the bes chain tip's block time.

* Add method to obtain both height and block time

Prevent any data races by returning both values so that they refer to
the same chain tip.

* Add `NetworkUpgrade::all_target_spacings` method

Returns all the target spacings defined for a network.

* Create a `NetworkChainTipEstimator` helper type

Isolate the code to calculate the height estimation in a new type, so
that it's easier to understand and doesn't decrease the readability of
the `chain_tip.rs` file.

* Add `ChainTip::estimate_network_chain_tip_height`

This is more of an extension method than a trait method. It uses the
`NetworkChainTipHeightEstimator` to actually perform the estimation, but
obtains the initial information from the current best chain tip.

* Fix typo in documentation

There was an extra closing bracket in the summary line.

* Refactor `MockChainTipSender` into a separate type

Prepare to allow mocking the block time of the best tip as well as the
block height.

* Allow sending mock best tip block times

Add a separate `watch` channel to send the best tip block times from a
`MockChainTipSender` to a `MockChainTip`.

The `best_tip_height_and_block_time` implementation will only return a
value if there's a height and a block time value for the best tip.

* Fix off-by-one height estimation error

Use Euclidean division to force the division result to round down
instead of rounding towards zero. This fixes an off-by-one error when
estimating a height that is lower than the current height, because the
fractionary result was being discarded, and it should have forced the
height to go one block back.

* Fix panics on local times very far in the past

Detect situations that might cause the block height estimate to
underflow, and return the genesis height instead.

* Fix another off-by-one height estimation error

The implementation of `chrono::Duration::num_seconds` adds one to the
number of seconds if it's negative. This breaks the division
calculation, so it has to be compensated for.

* Test network chain tip height estimation

Generate pairs of block heights and check that it's possible to estimate
the larger height from the smaller height and a displaced time
difference.
This commit is contained in:
Janito Vaqueiro Ferreira Filho 2022-02-10 22:27:02 -03:00 committed by GitHub
parent 683b88c819
commit eb98b7a4b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 403 additions and 44 deletions

View File

@ -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<block::Hash>;
/// Return the block time of the best chain tip.
fn best_tip_block_time(&self) -> Option<DateTime<Utc>>;
/// 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<Utc>)>;
/// 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<Utc>,
) -> Option<block::Height> {
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<DateTime<Utc>> {
None
}
fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime<Utc>)> {
None
}
fn best_tip_mined_transaction_ids(&self) -> Arc<[transaction::Hash]> {
Arc::new([])
}

View File

@ -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<Option<block::Height>>;
/// 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<Option<block::Height>>,
/// A sender that sets the `best_tip_block_time` of a [`MockChainTip`].
best_tip_block_time: watch::Sender<Option<DateTime<Utc>>>,
}
/// 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<Option<block::Height>>,
/// A mocked `best_tip_height` value set by the [`MockChainTipSender`].
best_tip_block_time: watch::Receiver<Option<DateTime<Utc>>>,
}
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<DateTime<Utc>> {
*self.best_tip_block_time.borrow()
}
fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime<Utc>)> {
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<Option<block::Height>>) {
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<Option<DateTime<Utc>>>) {
self.best_tip_block_time
.send(block_time.into())
.expect("attempt to send a best tip block time to a dropped `MockChainTip`");
}
}

View File

@ -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<Utc>,
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<Utc>,
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::<Vec<_>>().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<Utc>) -> 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<Utc>,
) -> 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")
}
}
}

View File

@ -0,0 +1 @@
mod prop;

View File

@ -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::<Network>(),
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)
}

View File

@ -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<Item = (block::Height, Duration)> {
[
(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.
///

View File

@ -12,9 +12,9 @@ mod prop;
impl MinimumPeerVersion<MockChainTip> {
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)
}
}

View File

@ -11,12 +11,10 @@ proptest! {
network in any::<Network>(),
block_height in any::<Option<block::Height>>(),
) {
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::<Network>(),
block_heights in any::<Vec<Option<block::Height>>>(),
) {
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::<Network>(),
block_height_updates in any::<Vec<Option<Option<block::Height>>>>(),
) {
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);

View File

@ -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,

View File

@ -54,6 +54,7 @@ impl From<PreparedBlock> for ChainTipBlock {
Self {
hash,
height,
time: block.header.time,
transaction_hashes,
previous_block_hash: block.header.previous_block_hash,
}

View File

@ -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<Utc>,
/// 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<ContextuallyValidBlock> 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<FinalizedBlock> 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<block::Height> {
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<block::Hash> {
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<DateTime<Utc>> {
self.with_chain_tip_block(|block| block.time)
}
#[instrument(skip(self))]
fn best_tip_height_and_block_time(&self) -> Option<(block::Height, DateTime<Utc>)> {
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())

View File

@ -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(),
}