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