zebra_chain/chain_tip/network_chain_tip_height_estimator.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
//! A module with helper code to estimate the network chain tip's height.
use std::vec;
use chrono::{DateTime, Duration, Utc};
use crate::{
block::{self, HeightDiff},
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 = 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 += 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;
}
// 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.
let block_difference: HeightDiff =
time_difference_seconds.div_euclid(self.current_target_spacing.num_seconds());
let current_height_as_diff = HeightDiff::from(self.current_height.0);
if let Some(height_estimate) = self.current_height + block_difference {
height_estimate
} else if current_height_as_diff + block_difference < 0 {
// 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 {
// Gracefully handle attempting to estimate a block at a very large height. This can
// happen if the local time is set incorrectly to a time too far in the future.
block::Height::MAX
}
}
}