zebra_state/service/check/
difficulty.rs

1//! Block difficulty adjustment calculations for contextual validation.
2//!
3//! This module supports the following consensus rule calculations:
4//!  * `ThresholdBits` from the Zcash Specification,
5//!  * the Testnet minimum difficulty adjustment from ZIPs 205 and 208, and
6//!  * `median-time-past`.
7
8use std::{cmp::max, cmp::min};
9
10use chrono::{DateTime, Duration, Utc};
11
12use zebra_chain::{
13    block,
14    block::Block,
15    parameters::Network,
16    parameters::NetworkUpgrade,
17    parameters::POW_AVERAGING_WINDOW,
18    work::difficulty::{CompactDifficulty, U256},
19    work::difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
20};
21
22/// The median block span for time median calculations.
23///
24/// `PoWMedianBlockSpan` in the Zcash specification.
25pub const POW_MEDIAN_BLOCK_SPAN: usize = 11;
26
27/// The overall block span used for adjusting Zcash block difficulty.
28///
29/// `PoWAveragingWindow + PoWMedianBlockSpan` in the Zcash specification based on
30/// > ActualTimespan(height : N) := MedianTime(height) − MedianTime(height − PoWAveragingWindow)
31pub const POW_ADJUSTMENT_BLOCK_SPAN: usize = POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN;
32
33/// The damping factor for median timespan variance.
34///
35/// `PoWDampingFactor` in the Zcash specification.
36pub const POW_DAMPING_FACTOR: i32 = 4;
37
38/// The maximum upward adjustment percentage for median timespan variance.
39///
40/// `PoWMaxAdjustUp * 100` in the Zcash specification.
41pub const POW_MAX_ADJUST_UP_PERCENT: i32 = 16;
42
43/// The maximum downward adjustment percentage for median timespan variance.
44///
45/// `PoWMaxAdjustDown * 100` in the Zcash specification.
46pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32;
47
48/// The maximum number of seconds between the `median-time-past` of a block,
49/// and the block's `time` field.
50///
51/// Part of the block header consensus rules in the Zcash specification.
52pub const BLOCK_MAX_TIME_SINCE_MEDIAN: u32 = 90 * 60;
53
54/// Contains the context needed to calculate the adjusted difficulty for a block.
55pub(crate) struct AdjustedDifficulty {
56    /// The `header.time` field from the candidate block
57    candidate_time: DateTime<Utc>,
58    /// The coinbase height from the candidate block
59    ///
60    /// If we only have the header, this field is calculated from the previous
61    /// block height.
62    candidate_height: block::Height,
63    /// The configured network
64    network: Network,
65    /// The `header.difficulty_threshold`s from the previous
66    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks, in reverse height
67    /// order.
68    relevant_difficulty_thresholds: Vec<CompactDifficulty>,
69    /// The `header.time`s from the previous
70    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks, in reverse height
71    /// order.
72    ///
73    /// Only the first and last `PoWMedianBlockSpan` times are used. Times
74    /// `11..=16` are ignored.
75    relevant_times: Vec<DateTime<Utc>>,
76}
77
78impl AdjustedDifficulty {
79    /// Initialise and return a new `AdjustedDifficulty` using a `candidate_block`,
80    /// `network`, and a `context`.
81    ///
82    /// The `context` contains the previous
83    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) `difficulty_threshold`s and
84    /// `time`s from the relevant chain for `candidate_block`, in reverse height
85    /// order, starting with the previous block.
86    ///
87    /// Note that the `time`s might not be in reverse chronological order, because
88    /// block times are supplied by miners.
89    ///
90    /// # Panics
91    ///
92    /// If the `context` contains fewer than 28 items.
93    pub fn new_from_block<C>(
94        candidate_block: &Block,
95        network: &Network,
96        context: C,
97    ) -> AdjustedDifficulty
98    where
99        C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
100    {
101        let candidate_block_height = candidate_block
102            .coinbase_height()
103            .expect("semantically valid blocks have a coinbase height");
104        let previous_block_height = (candidate_block_height - 1)
105            .expect("contextual validation is never run on the genesis block");
106
107        AdjustedDifficulty::new_from_header_time(
108            candidate_block.header.time,
109            previous_block_height,
110            network,
111            context,
112        )
113    }
114
115    /// Initialise and return a new [`AdjustedDifficulty`] using a
116    /// `candidate_header_time`, `previous_block_height`, `network`, and a `context`.
117    ///
118    /// Designed for use when validating block headers, where the full block has not
119    /// been downloaded yet.
120    ///
121    /// See [`Self::new_from_block`] for detailed information about the `context`.
122    ///
123    /// # Panics
124    ///
125    /// If the context contains fewer than 28 items.
126    pub fn new_from_header_time<C>(
127        candidate_header_time: DateTime<Utc>,
128        previous_block_height: block::Height,
129        network: &Network,
130        context: C,
131    ) -> AdjustedDifficulty
132    where
133        C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
134    {
135        let candidate_height = (previous_block_height + 1).expect("next block height is valid");
136
137        let (relevant_difficulty_thresholds, relevant_times) = context
138            .into_iter()
139            .take(POW_ADJUSTMENT_BLOCK_SPAN)
140            .unzip::<_, _, Vec<_>, Vec<_>>();
141
142        AdjustedDifficulty {
143            candidate_time: candidate_header_time,
144            candidate_height,
145            network: network.clone(),
146            relevant_difficulty_thresholds,
147            relevant_times,
148        }
149    }
150
151    /// Returns the candidate block's height.
152    pub fn candidate_height(&self) -> block::Height {
153        self.candidate_height
154    }
155
156    /// Returns the candidate block's time field.
157    pub fn candidate_time(&self) -> DateTime<Utc> {
158        self.candidate_time
159    }
160
161    /// Returns the configured network.
162    pub fn network(&self) -> Network {
163        self.network.clone()
164    }
165
166    /// Calculate the expected `difficulty_threshold` for a candidate block, based
167    /// on the `candidate_time`, `candidate_height`, `network`, and the
168    /// `difficulty_threshold`s and `time`s from the previous
169    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the relevant chain.
170    ///
171    /// Implements `ThresholdBits` from the Zcash specification, and the Testnet
172    /// minimum difficulty adjustment from ZIPs 205 and 208.
173    pub fn expected_difficulty_threshold(&self) -> CompactDifficulty {
174        if NetworkUpgrade::is_testnet_min_difficulty_block(
175            &self.network,
176            self.candidate_height,
177            self.candidate_time,
178            self.relevant_times[0],
179        ) {
180            assert!(
181                self.network.is_a_test_network(),
182                "invalid network: the minimum difficulty rule only applies on test networks"
183            );
184            self.network.target_difficulty_limit().to_compact()
185        } else {
186            self.threshold_bits()
187        }
188    }
189
190    /// Calculate the `difficulty_threshold` for a candidate block, based on the
191    /// `candidate_height`, `network`, and the relevant `difficulty_threshold`s and
192    /// `time`s.
193    ///
194    /// See [`Self::expected_difficulty_threshold`] for details.
195    ///
196    /// Implements `ThresholdBits` from the Zcash specification. (Which excludes the
197    /// Testnet minimum difficulty adjustment.)
198    fn threshold_bits(&self) -> CompactDifficulty {
199        let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
200            &self.network,
201            self.candidate_height,
202        );
203
204        let threshold = (self.mean_target_difficulty() / averaging_window_timespan.num_seconds())
205            * self.median_timespan_bounded().num_seconds();
206        let threshold = min(self.network.target_difficulty_limit(), threshold);
207
208        threshold.to_compact()
209    }
210
211    /// Calculate the arithmetic mean of the averaging window thresholds: the
212    /// expanded `difficulty_threshold`s from the previous `PoWAveragingWindow` (17)
213    /// blocks in the relevant chain.
214    ///
215    /// Implements `MeanTarget` from the Zcash specification.
216    fn mean_target_difficulty(&self) -> ExpandedDifficulty {
217        // In Zebra, contextual validation starts after Canopy activation, so we
218        // can assume that the relevant chain contains at least 17 blocks.
219        // Therefore, the `PoWLimit` case of `MeanTarget()` from the Zcash
220        // specification is unreachable.
221
222        let averaging_window_thresholds =
223            if self.relevant_difficulty_thresholds.len() >= POW_AVERAGING_WINDOW {
224                &self.relevant_difficulty_thresholds[0..POW_AVERAGING_WINDOW]
225            } else {
226                return self.network.target_difficulty_limit();
227            };
228
229        // Since the PoWLimits are `2^251 − 1` for Testnet, and `2^243 − 1` for
230        // Mainnet, the sum of 17 `ExpandedDifficulty` will be less than or equal
231        // to: `(2^251 − 1) * 17 = 2^255 + 2^251 - 17`. Therefore, the sum can
232        // not overflow a u256 value.
233        let total: ExpandedDifficulty = averaging_window_thresholds
234            .iter()
235            .map(|compact| {
236                compact
237                    .to_expanded()
238                    .expect("difficulty thresholds in previously verified blocks are valid")
239            })
240            .sum();
241
242        let divisor: U256 = POW_AVERAGING_WINDOW.into();
243        total / divisor
244    }
245
246    /// Calculate the bounded median timespan. The median timespan is the
247    /// difference of medians of the timespan times, which are the `time`s from
248    /// the previous `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the
249    /// relevant chain.
250    ///
251    /// Uses the candidate block's `height' and `network` to calculate the
252    /// `AveragingWindowTimespan` for that block.
253    ///
254    /// The median timespan is damped by the `PoWDampingFactor`, and bounded by
255    /// `PoWMaxAdjustDown` and `PoWMaxAdjustUp`.
256    ///
257    /// Implements `ActualTimespanBounded` from the Zcash specification.
258    ///
259    /// Note: This calculation only uses `PoWMedianBlockSpan` (11) times at the
260    /// start and end of the timespan times. timespan times `[11..=16]` are ignored.
261    fn median_timespan_bounded(&self) -> Duration {
262        let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
263            &self.network,
264            self.candidate_height,
265        );
266        // This value is exact, but we need to truncate its nanoseconds component
267        let damped_variance =
268            (self.median_timespan() - averaging_window_timespan) / POW_DAMPING_FACTOR;
269        // num_seconds truncates negative values towards zero, matching the Zcash specification
270        let damped_variance = Duration::seconds(damped_variance.num_seconds());
271
272        // `ActualTimespanDamped` in the Zcash specification
273        let median_timespan_damped = averaging_window_timespan + damped_variance;
274
275        // `MinActualTimespan` and `MaxActualTimespan` in the Zcash spec
276        let min_median_timespan =
277            averaging_window_timespan * (100 - POW_MAX_ADJUST_UP_PERCENT) / 100;
278        let max_median_timespan =
279            averaging_window_timespan * (100 + POW_MAX_ADJUST_DOWN_PERCENT) / 100;
280
281        // `ActualTimespanBounded` in the Zcash specification
282        max(
283            min_median_timespan,
284            min(max_median_timespan, median_timespan_damped),
285        )
286    }
287
288    /// Calculate the median timespan. The median timespan is the difference of
289    /// medians of the timespan times, which are the `time`s from the previous
290    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the relevant chain.
291    ///
292    /// Implements `ActualTimespan` from the Zcash specification.
293    ///
294    /// See [`Self::median_timespan_bounded`] for details.
295    fn median_timespan(&self) -> Duration {
296        let newer_median = self.median_time_past();
297
298        // MedianTime(height : N) := median([ nTime(𝑖) for 𝑖 from max(0, height − PoWMedianBlockSpan) up to max(0, height − 1) ])
299        let older_median = if self.relevant_times.len() > POW_AVERAGING_WINDOW {
300            let older_times: Vec<_> = self
301                .relevant_times
302                .iter()
303                .skip(POW_AVERAGING_WINDOW)
304                .cloned()
305                .take(POW_MEDIAN_BLOCK_SPAN)
306                .collect();
307
308            AdjustedDifficulty::median_time(older_times)
309        } else {
310            self.relevant_times
311                .last()
312                .cloned()
313                .expect("there must be a Genesis block")
314        };
315
316        // `ActualTimespan` in the Zcash specification
317        newer_median - older_median
318    }
319
320    /// Calculate the median of the `time`s from the previous
321    /// `PoWMedianBlockSpan` (11) blocks in the relevant chain.
322    ///
323    /// Implements `median-time-past` and `MedianTime(candidate_height)` from the
324    /// Zcash specification. (These functions are identical, but they are
325    /// specified in slightly different ways.)
326    pub fn median_time_past(&self) -> DateTime<Utc> {
327        let median_times: Vec<DateTime<Utc>> = self
328            .relevant_times
329            .iter()
330            .take(POW_MEDIAN_BLOCK_SPAN)
331            .cloned()
332            .collect();
333
334        AdjustedDifficulty::median_time(median_times)
335    }
336
337    /// Calculate the median of the `median_block_span_times`: the `time`s from a
338    /// Vec of `PoWMedianBlockSpan` (11) or fewer blocks in the relevant chain.
339    ///
340    /// Implements `MedianTime` from the Zcash specification.
341    ///
342    /// # Panics
343    ///
344    /// If provided an empty Vec
345    pub(crate) fn median_time(mut median_block_span_times: Vec<DateTime<Utc>>) -> DateTime<Utc> {
346        median_block_span_times.sort_unstable();
347
348        // > median(𝑆) := sorted(𝑆)_{ceiling((length(𝑆)+1)/2)}
349        // <https://zips.z.cash/protocol/protocol.pdf>, section 7.7.3, Difficulty Adjustment (p. 132)
350        let median_idx = median_block_span_times.len() / 2;
351        median_block_span_times[median_idx]
352    }
353}