Skip to main content

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, min};
9
10use chrono::{DateTime, Duration, Utc};
11
12use zebra_chain::{
13    block::{self, Block},
14    parameters::{Network, NetworkUpgrade, MAX_POW_AVERAGING_WINDOW},
15    work::difficulty::{CompactDifficulty, ExpandedDifficulty, ParameterDifficulty as _, U256},
16    BoundedVec,
17};
18
19#[cfg(test)]
20mod tests;
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 largest overall block span used for adjusting Zcash block difficulty.
28///
29/// `PoWAveragingWindow(height) + PoWMedianBlockSpan` in the Zcash specification, for the height
30/// with the largest averaging window, based on
31/// > ActualTimespan(height : N) := MedianTime(height) − MedianTime(height − PoWAveragingWindow)
32///
33/// Used to size the buffers that hold difficulty adjustment context. Use
34/// [`pow_adjustment_block_span`] for the span that actually applies to a block, because [ZIP 218]
35/// makes the averaging window height-dependent.
36///
37/// [ZIP 218]: https://zips.z.cash/zip-0218
38pub const MAX_POW_ADJUSTMENT_BLOCK_SPAN: usize = MAX_POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN;
39
40/// The overall block span used for adjusting the difficulty of the block at `height` on `network`.
41///
42/// `PoWAveragingWindow(height) + PoWMedianBlockSpan` in the Zcash specification.
43pub fn pow_adjustment_block_span(network: &Network, height: block::Height) -> usize {
44    NetworkUpgrade::averaging_window_for_height(network, height) + POW_MEDIAN_BLOCK_SPAN
45}
46
47/// The damping factor for median timespan variance.
48///
49/// `PoWDampingFactor` in the Zcash specification.
50pub const POW_DAMPING_FACTOR: i32 = 4;
51
52/// The maximum upward adjustment percentage for median timespan variance.
53///
54/// `PoWMaxAdjustUp * 100` in the Zcash specification.
55pub const POW_MAX_ADJUST_UP_PERCENT: i32 = 16;
56
57/// The maximum downward adjustment percentage for median timespan variance.
58///
59/// `PoWMaxAdjustDown * 100` in the Zcash specification.
60pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32;
61
62/// The maximum number of seconds between the `median-time-past` of a block,
63/// and the block's `time` field.
64///
65/// Part of the block header consensus rules in the Zcash specification.
66pub const BLOCK_MAX_TIME_SINCE_MEDIAN: u32 = 90 * 60;
67
68/// Contains the context needed to calculate the adjusted difficulty for a block.
69pub(crate) struct AdjustedDifficulty {
70    /// The `header.time` field from the candidate block
71    candidate_time: DateTime<Utc>,
72    /// The coinbase height from the candidate block
73    ///
74    /// If we only have the header, this field is calculated from the previous
75    /// block height.
76    candidate_height: block::Height,
77    /// The configured network
78    network: Network,
79    /// The `header.difficulty_threshold`s from the previous
80    /// `PoWAveragingWindow(height) + PoWMedianBlockSpan` blocks, in reverse height
81    /// order.
82    relevant_difficulty_thresholds: BoundedVec<CompactDifficulty, 1, MAX_POW_ADJUSTMENT_BLOCK_SPAN>,
83    /// The `header.time`s from the previous
84    /// `PoWAveragingWindow(height) + PoWMedianBlockSpan` blocks, in reverse height
85    /// order.
86    ///
87    /// Only the first and last `PoWMedianBlockSpan` times are used. The times in
88    /// between the two medians are ignored.
89    relevant_times: BoundedVec<DateTime<Utc>, 1, MAX_POW_ADJUSTMENT_BLOCK_SPAN>,
90}
91
92impl AdjustedDifficulty {
93    /// Initialise and return a new `AdjustedDifficulty` using a `candidate_block`,
94    /// `network`, and a `context`.
95    ///
96    /// The `context` contains the previous
97    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) `difficulty_threshold`s and
98    /// `time`s from the relevant chain for `candidate_block`, in reverse height
99    /// order, starting with the previous block.
100    ///
101    /// Note that the `time`s might not be in reverse chronological order, because
102    /// block times are supplied by miners.
103    ///
104    /// # Panics
105    ///
106    /// This function may panic in the following cases:
107    /// - The `candidate_block` has no coinbase height (should never happen for valid blocks).
108    /// - The `candidate_block` is the genesis block, so `previous_block_height` cannot be computed.
109    /// - `AdjustedDifficulty::new_from_header_time` panics.
110    pub fn new_from_block<C>(
111        candidate_block: &Block,
112        network: &Network,
113        context: C,
114    ) -> AdjustedDifficulty
115    where
116        C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
117    {
118        let candidate_block_height = candidate_block
119            .coinbase_height()
120            .expect("semantically valid blocks have a coinbase height");
121        let previous_block_height = (candidate_block_height - 1)
122            .expect("contextual validation is never run on the genesis block");
123
124        AdjustedDifficulty::new_from_header_time(
125            candidate_block.header.time,
126            previous_block_height,
127            network,
128            context,
129        )
130    }
131
132    /// Initialise and return a new [`AdjustedDifficulty`] using a
133    /// `candidate_header_time`, `previous_block_height`, `network`, and a `context`.
134    ///
135    /// Designed for use when validating block headers, where the full block has not
136    /// been downloaded yet.
137    ///
138    /// See [`Self::new_from_block`] for detailed information about the `context`.
139    ///
140    /// # Panics
141    ///
142    /// This function may panic in the following cases:
143    /// - The next block height is invalid.
144    /// - The `context` iterator is empty, because at least one difficulty threshold
145    ///   and block time are required to construct the `Bounded` vectors.
146    /// - The context iterator is empty, because at least one difficulty threshold and block time are required.
147    pub fn new_from_header_time<C>(
148        candidate_header_time: DateTime<Utc>,
149        previous_block_height: block::Height,
150        network: &Network,
151        context: C,
152    ) -> AdjustedDifficulty
153    where
154        C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
155    {
156        let candidate_height = (previous_block_height + 1).expect("next block height is valid");
157
158        let (thresholds, times) = context
159            .into_iter()
160            .take(pow_adjustment_block_span(network, candidate_height))
161            .unzip::<_, _, Vec<_>, Vec<_>>();
162
163        let relevant_difficulty_thresholds: BoundedVec<
164            CompactDifficulty,
165            1,
166            MAX_POW_ADJUSTMENT_BLOCK_SPAN,
167        > = thresholds
168            .try_into()
169            .expect("context must provide a bounded number of difficulty thresholds");
170        let relevant_times: BoundedVec<DateTime<Utc>, 1, MAX_POW_ADJUSTMENT_BLOCK_SPAN> = times
171            .try_into()
172            .expect("context must provide a bounded number of block times");
173
174        AdjustedDifficulty {
175            candidate_time: candidate_header_time,
176            candidate_height,
177            network: network.clone(),
178            relevant_difficulty_thresholds,
179            relevant_times,
180        }
181    }
182
183    /// Returns the candidate block's height.
184    pub fn candidate_height(&self) -> block::Height {
185        self.candidate_height
186    }
187
188    /// Returns the candidate block's time field.
189    pub fn candidate_time(&self) -> DateTime<Utc> {
190        self.candidate_time
191    }
192
193    /// Returns the difficulty averaging window that applies to the candidate block.
194    ///
195    /// `PoWAveragingWindow(height)` in the Zcash specification, as redefined by [ZIP 218].
196    ///
197    /// [ZIP 218]: https://zips.z.cash/zip-0218
198    fn averaging_window(&self) -> usize {
199        NetworkUpgrade::averaging_window_for_height(&self.network, self.candidate_height)
200    }
201
202    /// Returns the configured network.
203    pub fn network(&self) -> Network {
204        self.network.clone()
205    }
206
207    /// Calculate the expected `difficulty_threshold` for a candidate block, based
208    /// on the `candidate_time`, `candidate_height`, `network`, and the
209    /// `difficulty_threshold`s and `time`s from the previous
210    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the relevant chain.
211    ///
212    /// Implements `ThresholdBits` from the Zcash specification, and the Testnet
213    /// minimum difficulty adjustment from ZIPs 205 and 208.
214    pub fn expected_difficulty_threshold(&self) -> CompactDifficulty {
215        // Regtest uses fixed minimum difficulty with no retargeting, matching
216        // zcashd's and Bitcoin's `fPowNoRetargeting`. This keeps every regtest
217        // block at the powLimit, so a zcashd sidecar following a Zebra regtest
218        // chain accepts its headers instead of rejecting them as "bad-diffbits".
219        if self.network.is_regtest() {
220            return self.network.target_difficulty_limit().to_compact();
221        }
222
223        if NetworkUpgrade::is_testnet_min_difficulty_block(
224            &self.network,
225            self.candidate_height,
226            self.candidate_time,
227            *self.relevant_times.first(),
228        ) {
229            assert!(
230                self.network.is_a_test_network(),
231                "invalid network: the minimum difficulty rule only applies on test networks"
232            );
233            self.network.target_difficulty_limit().to_compact()
234        } else {
235            self.threshold_bits()
236        }
237    }
238
239    /// Calculate the `difficulty_threshold` for a candidate block, based on the
240    /// `candidate_height`, `network`, and the relevant `difficulty_threshold`s and
241    /// `time`s.
242    ///
243    /// See [`Self::expected_difficulty_threshold`] for details.
244    ///
245    /// Implements `ThresholdBits` from the Zcash specification. (Which excludes the
246    /// Testnet minimum difficulty adjustment.)
247    fn threshold_bits(&self) -> CompactDifficulty {
248        let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
249            &self.network,
250            self.candidate_height,
251        );
252
253        let threshold = (self.mean_target_difficulty() / averaging_window_timespan.num_seconds())
254            * self.median_timespan_bounded().num_seconds();
255        let threshold = min(self.network.target_difficulty_limit(), threshold);
256
257        threshold.to_compact()
258    }
259
260    /// Calculate the arithmetic mean of the expanded `difficulty_threshold`s from
261    /// the previous `PoWAveragingWindow(height)` blocks in the relevant chain.
262    ///
263    /// Implements `MeanTarget` from the Zcash specification.
264    fn mean_target_difficulty(&self) -> ExpandedDifficulty {
265        let averaging_window = self.averaging_window();
266        let averaging_window_height = u32::try_from(averaging_window)
267            .expect("the averaging window is at most MAX_POW_AVERAGING_WINDOW");
268        if self.candidate_height.0 <= averaging_window_height
269            || self.relevant_difficulty_thresholds.len() < averaging_window
270        {
271            return self.network.target_difficulty_limit();
272        }
273
274        let averaging_window_thresholds =
275            &self.relevant_difficulty_thresholds.as_slice()[..averaging_window];
276
277        // A sum of 102 Testnet targets can overflow U256. Sum the quotients and
278        // remainders separately to preserve floor(sum(targets) / window) exactly:
279        // the quotient sum is at most U256::MAX, and the remainder sum is < window^2.
280        let divisor: U256 = averaging_window.into();
281        let (quotients, remainders) = averaging_window_thresholds.iter().fold(
282            (U256::zero(), U256::zero()),
283            |(quotients, remainders), compact| {
284                let target = compact
285                    .to_expanded()
286                    .expect("difficulty thresholds in previously verified blocks are valid");
287                let (quotient, remainder) = U256::from(target).div_mod(divisor);
288                (quotients + quotient, remainders + remainder)
289            },
290        );
291
292        (quotients + remainders / divisor).into()
293    }
294
295    /// Calculate the bounded median timespan. The median timespan is the
296    /// difference of medians of the timespan times, which are the `time`s from
297    /// the previous `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the
298    /// relevant chain.
299    ///
300    /// Uses the candidate block's `height' and `network` to calculate the
301    /// `AveragingWindowTimespan` for that block.
302    ///
303    /// The median timespan is damped by the `PoWDampingFactor`, and bounded by
304    /// `PoWMaxAdjustDown` and `PoWMaxAdjustUp`.
305    ///
306    /// Implements `ActualTimespanBounded` from the Zcash specification.
307    ///
308    /// Note: This calculation only uses `PoWMedianBlockSpan` (11) times at the
309    /// start and end of the timespan times. timespan times `[11..=16]` are ignored.
310    fn median_timespan_bounded(&self) -> Duration {
311        let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
312            &self.network,
313            self.candidate_height,
314        );
315        // This value is exact, but we need to truncate its nanoseconds component
316        let damped_variance =
317            (self.median_timespan() - averaging_window_timespan) / POW_DAMPING_FACTOR;
318        // num_seconds truncates negative values towards zero, matching the Zcash specification
319        let damped_variance = Duration::seconds(damped_variance.num_seconds());
320
321        // `ActualTimespanDamped` in the Zcash specification
322        let median_timespan_damped = averaging_window_timespan + damped_variance;
323
324        // `MinActualTimespan` and `MaxActualTimespan` in the Zcash spec
325        let min_median_timespan =
326            averaging_window_timespan * (100 - POW_MAX_ADJUST_UP_PERCENT) / 100;
327        let max_median_timespan =
328            averaging_window_timespan * (100 + POW_MAX_ADJUST_DOWN_PERCENT) / 100;
329
330        // `ActualTimespanBounded` in the Zcash specification
331        max(
332            min_median_timespan,
333            min(max_median_timespan, median_timespan_damped),
334        )
335    }
336
337    /// Calculate the median timespan. The median timespan is the difference of
338    /// medians of the timespan times, which are the `time`s from the previous
339    /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the relevant chain.
340    ///
341    /// Implements `ActualTimespan` from the Zcash specification.
342    ///
343    /// See [`Self::median_timespan_bounded`] for details.
344    fn median_timespan(&self) -> Duration {
345        let newer_median = self.median_time_past();
346
347        // MedianTime(height : N) := median([ nTime(𝑖) for 𝑖 from max(0, height − PoWMedianBlockSpan) up to max(0, height − 1) ])
348        let averaging_window = self.averaging_window();
349
350        let older_median = if self.relevant_times.len() > averaging_window {
351            let older_times: Vec<_> = self
352                .relevant_times
353                .iter()
354                .skip(averaging_window)
355                .cloned()
356                .take(POW_MEDIAN_BLOCK_SPAN)
357                .collect();
358
359            AdjustedDifficulty::median_time(older_times)
360        } else {
361            *self.relevant_times.last()
362        };
363
364        // `ActualTimespan` in the Zcash specification
365        newer_median - older_median
366    }
367
368    /// Calculate the median of the `time`s from the previous
369    /// `PoWMedianBlockSpan` (11) blocks in the relevant chain.
370    ///
371    /// Implements `median-time-past` and `MedianTime(candidate_height)` from the
372    /// Zcash specification. (These functions are identical, but they are
373    /// specified in slightly different ways.)
374    pub fn median_time_past(&self) -> DateTime<Utc> {
375        let median_times: Vec<DateTime<Utc>> = self
376            .relevant_times
377            .iter()
378            .take(POW_MEDIAN_BLOCK_SPAN)
379            .cloned()
380            .collect();
381
382        AdjustedDifficulty::median_time(median_times)
383    }
384
385    /// Calculate the median of the `median_block_span_times`: the `time`s from a
386    /// Vec of `PoWMedianBlockSpan` (11) or fewer blocks in the relevant chain.
387    ///
388    /// Implements `MedianTime` from the Zcash specification.
389    ///
390    /// # Panics
391    ///
392    /// If provided an empty Vec
393    pub(crate) fn median_time(mut median_block_span_times: Vec<DateTime<Utc>>) -> DateTime<Utc> {
394        median_block_span_times.sort_unstable();
395
396        // > median(𝑆) := sorted(𝑆)_{ceiling((length(𝑆)+1)/2)}
397        // <https://zips.z.cash/protocol/protocol.pdf>, section 7.7.3, Difficulty Adjustment (p. 132)
398        let median_idx = median_block_span_times.len() / 2;
399        median_block_span_times[median_idx]
400    }
401}