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 if NetworkUpgrade::is_testnet_min_difficulty_block(
190 &self.network,
191 self.candidate_height,
192 self.candidate_time,
193 *self.relevant_times.first(),
194 ) {
195 assert!(
196 self.network.is_a_test_network(),
197 "invalid network: the minimum difficulty rule only applies on test networks"
198 );
199 self.network.target_difficulty_limit().to_compact()
200 } else {
201 self.threshold_bits()
202 }
203 }
204
205 /// Calculate the `difficulty_threshold` for a candidate block, based on the
206 /// `candidate_height`, `network`, and the relevant `difficulty_threshold`s and
207 /// `time`s.
208 ///
209 /// See [`Self::expected_difficulty_threshold`] for details.
210 ///
211 /// Implements `ThresholdBits` from the Zcash specification. (Which excludes the
212 /// Testnet minimum difficulty adjustment.)
213 fn threshold_bits(&self) -> CompactDifficulty {
214 let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
215 &self.network,
216 self.candidate_height,
217 );
218
219 let threshold = (self.mean_target_difficulty() / averaging_window_timespan.num_seconds())
220 * self.median_timespan_bounded().num_seconds();
221 let threshold = min(self.network.target_difficulty_limit(), threshold);
222
223 threshold.to_compact()
224 }
225
226 /// Calculate the arithmetic mean of the averaging window thresholds: the
227 /// expanded `difficulty_threshold`s from the previous `PoWAveragingWindow` (17)
228 /// blocks in the relevant chain.
229 ///
230 /// Implements `MeanTarget` from the Zcash specification.
231 fn mean_target_difficulty(&self) -> ExpandedDifficulty {
232 // In Zebra, contextual validation starts after Canopy activation, so we
233 // can assume that the relevant chain contains at least 17 blocks.
234 // Therefore, the `PoWLimit` case of `MeanTarget()` from the Zcash
235 // specification is unreachable.
236
237 let averaging_window_thresholds =
238 if self.relevant_difficulty_thresholds.len() >= POW_AVERAGING_WINDOW {
239 &self.relevant_difficulty_thresholds.as_slice()[0..POW_AVERAGING_WINDOW]
240 } else {
241 return self.network.target_difficulty_limit();
242 };
243
244 // Since the PoWLimits are `2^251 − 1` for Testnet, and `2^243 − 1` for
245 // Mainnet, the sum of 17 `ExpandedDifficulty` will be less than or equal
246 // to: `(2^251 − 1) * 17 = 2^255 + 2^251 - 17`. Therefore, the sum can
247 // not overflow a u256 value.
248 let total: ExpandedDifficulty = averaging_window_thresholds
249 .iter()
250 .map(|compact| {
251 compact
252 .to_expanded()
253 .expect("difficulty thresholds in previously verified blocks are valid")
254 })
255 .sum();
256
257 let divisor: U256 = POW_AVERAGING_WINDOW.into();
258 total / divisor
259 }
260
261 /// Calculate the bounded median timespan. The median timespan is the
262 /// difference of medians of the timespan times, which are the `time`s from
263 /// the previous `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the
264 /// relevant chain.
265 ///
266 /// Uses the candidate block's `height' and `network` to calculate the
267 /// `AveragingWindowTimespan` for that block.
268 ///
269 /// The median timespan is damped by the `PoWDampingFactor`, and bounded by
270 /// `PoWMaxAdjustDown` and `PoWMaxAdjustUp`.
271 ///
272 /// Implements `ActualTimespanBounded` from the Zcash specification.
273 ///
274 /// Note: This calculation only uses `PoWMedianBlockSpan` (11) times at the
275 /// start and end of the timespan times. timespan times `[11..=16]` are ignored.
276 fn median_timespan_bounded(&self) -> Duration {
277 let averaging_window_timespan = NetworkUpgrade::averaging_window_timespan_for_height(
278 &self.network,
279 self.candidate_height,
280 );
281 // This value is exact, but we need to truncate its nanoseconds component
282 let damped_variance =
283 (self.median_timespan() - averaging_window_timespan) / POW_DAMPING_FACTOR;
284 // num_seconds truncates negative values towards zero, matching the Zcash specification
285 let damped_variance = Duration::seconds(damped_variance.num_seconds());
286
287 // `ActualTimespanDamped` in the Zcash specification
288 let median_timespan_damped = averaging_window_timespan + damped_variance;
289
290 // `MinActualTimespan` and `MaxActualTimespan` in the Zcash spec
291 let min_median_timespan =
292 averaging_window_timespan * (100 - POW_MAX_ADJUST_UP_PERCENT) / 100;
293 let max_median_timespan =
294 averaging_window_timespan * (100 + POW_MAX_ADJUST_DOWN_PERCENT) / 100;
295
296 // `ActualTimespanBounded` in the Zcash specification
297 max(
298 min_median_timespan,
299 min(max_median_timespan, median_timespan_damped),
300 )
301 }
302
303 /// Calculate the median timespan. The median timespan is the difference of
304 /// medians of the timespan times, which are the `time`s from the previous
305 /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks in the relevant chain.
306 ///
307 /// Implements `ActualTimespan` from the Zcash specification.
308 ///
309 /// See [`Self::median_timespan_bounded`] for details.
310 fn median_timespan(&self) -> Duration {
311 let newer_median = self.median_time_past();
312
313 // MedianTime(height : N) := median([ nTime(𝑖) for 𝑖 from max(0, height − PoWMedianBlockSpan) up to max(0, height − 1) ])
314 let older_median = if self.relevant_times.len() > POW_AVERAGING_WINDOW {
315 let older_times: Vec<_> = self
316 .relevant_times
317 .iter()
318 .skip(POW_AVERAGING_WINDOW)
319 .cloned()
320 .take(POW_MEDIAN_BLOCK_SPAN)
321 .collect();
322
323 AdjustedDifficulty::median_time(older_times)
324 } else {
325 *self.relevant_times.last()
326 };
327
328 // `ActualTimespan` in the Zcash specification
329 newer_median - older_median
330 }
331
332 /// Calculate the median of the `time`s from the previous
333 /// `PoWMedianBlockSpan` (11) blocks in the relevant chain.
334 ///
335 /// Implements `median-time-past` and `MedianTime(candidate_height)` from the
336 /// Zcash specification. (These functions are identical, but they are
337 /// specified in slightly different ways.)
338 pub fn median_time_past(&self) -> DateTime<Utc> {
339 let median_times: Vec<DateTime<Utc>> = self
340 .relevant_times
341 .iter()
342 .take(POW_MEDIAN_BLOCK_SPAN)
343 .cloned()
344 .collect();
345
346 AdjustedDifficulty::median_time(median_times)
347 }
348
349 /// Calculate the median of the `median_block_span_times`: the `time`s from a
350 /// Vec of `PoWMedianBlockSpan` (11) or fewer blocks in the relevant chain.
351 ///
352 /// Implements `MedianTime` from the Zcash specification.
353 ///
354 /// # Panics
355 ///
356 /// If provided an empty Vec
357 pub(crate) fn median_time(mut median_block_span_times: Vec<DateTime<Utc>>) -> DateTime<Utc> {
358 median_block_span_times.sort_unstable();
359
360 // > median(𝑆) := sorted(𝑆)_{ceiling((length(𝑆)+1)/2)}
361 // <https://zips.z.cash/protocol/protocol.pdf>, section 7.7.3, Difficulty Adjustment (p. 132)
362 let median_idx = median_block_span_times.len() / 2;
363 median_block_span_times[median_idx]
364 }
365}