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:
parent
683b88c819
commit
eb98b7a4b2
|
@ -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([])
|
||||
}
|
||||
|
|
|
@ -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`");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
mod prop;
|
|
@ -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)
|
||||
}
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue