zebra_state/service/read/
difficulty.rs

1//! Get context and calculate difficulty for the next block.
2
3use std::sync::Arc;
4
5use chrono::{DateTime, Utc};
6
7use zebra_chain::{
8    block::{self, Block, Hash, Height},
9    history_tree::HistoryTree,
10    parameters::{Network, NetworkUpgrade, POST_BLOSSOM_POW_TARGET_SPACING},
11    serialization::{DateTime32, Duration32},
12    work::difficulty::{CompactDifficulty, PartialCumulativeWork, Work},
13};
14
15use crate::{
16    service::{
17        any_ancestor_blocks,
18        block_iter::any_chain_ancestor_iter,
19        check::{
20            difficulty::{
21                BLOCK_MAX_TIME_SINCE_MEDIAN, POW_ADJUSTMENT_BLOCK_SPAN, POW_MEDIAN_BLOCK_SPAN,
22            },
23            AdjustedDifficulty,
24        },
25        finalized_state::ZebraDb,
26        read::{
27            self, find::calculate_median_time_past, tree::history_tree,
28            FINALIZED_STATE_QUERY_RETRIES,
29        },
30        NonFinalizedState,
31    },
32    BoxError, GetBlockTemplateChainInfo,
33};
34
35/// The amount of extra time we allow for a miner to mine a standard difficulty block on testnet.
36///
37/// This is a Zebra-specific standard rule.
38pub const EXTRA_TIME_TO_MINE_A_BLOCK: u32 = POST_BLOSSOM_POW_TARGET_SPACING * 2;
39
40/// Returns the [`GetBlockTemplateChainInfo`] for the current best chain.
41///
42/// # Panics
43///
44/// - If we don't have enough blocks in the state.
45pub fn get_block_template_chain_info(
46    non_finalized_state: &NonFinalizedState,
47    db: &ZebraDb,
48    network: &Network,
49) -> Result<GetBlockTemplateChainInfo, BoxError> {
50    let mut best_relevant_chain_and_history_tree_result =
51        best_relevant_chain_and_history_tree(non_finalized_state, db);
52
53    // Retry the finalized state query if it was interrupted by a finalizing block.
54    //
55    // TODO: refactor this into a generic retry(finalized_closure, process_and_check_closure) fn
56    for _ in 0..FINALIZED_STATE_QUERY_RETRIES {
57        if best_relevant_chain_and_history_tree_result.is_ok() {
58            break;
59        }
60
61        best_relevant_chain_and_history_tree_result =
62            best_relevant_chain_and_history_tree(non_finalized_state, db);
63    }
64
65    let (best_tip_height, best_tip_hash, best_relevant_chain, best_tip_history_tree) =
66        best_relevant_chain_and_history_tree_result?;
67
68    Ok(difficulty_time_and_history_tree(
69        best_relevant_chain,
70        best_tip_height,
71        best_tip_hash,
72        network,
73        best_tip_history_tree,
74    ))
75}
76
77/// Accepts a `non_finalized_state`, [`ZebraDb`], `num_blocks`, and a block hash to start at.
78///
79/// Iterates over up to the last `num_blocks` blocks, summing up their total work.
80/// Divides that total by the number of seconds between the timestamp of the
81/// first block in the iteration and 1 block below the last block.
82///
83/// Returns the solution rate per second for the current best chain, or `None` if
84/// the `start_hash` and at least 1 block below it are not found in the chain.
85#[allow(unused)]
86pub fn solution_rate(
87    non_finalized_state: &NonFinalizedState,
88    db: &ZebraDb,
89    num_blocks: usize,
90    start_hash: Hash,
91) -> Option<u128> {
92    // Take 1 extra header for calculating the number of seconds between when mining on the first
93    // block likely started. The work for the extra header is not added to `total_work`.
94    //
95    // Since we can't take more headers than are actually in the chain, this automatically limits
96    // `num_blocks` to the chain length, like `zcashd` does.
97    let mut header_iter =
98        any_chain_ancestor_iter::<block::Header>(non_finalized_state, db, start_hash)
99            .take(num_blocks.checked_add(1).unwrap_or(num_blocks))
100            .peekable();
101
102    let get_work = |header: &block::Header| {
103        header
104            .difficulty_threshold
105            .to_work()
106            .expect("work has already been validated")
107    };
108
109    // If there are no blocks in the range, we can't return a useful result.
110    let last_header = header_iter.peek()?;
111
112    // Initialize the cumulative variables.
113    let mut min_time = last_header.time;
114    let mut max_time = last_header.time;
115
116    let mut last_work = Work::zero();
117    let mut total_work = PartialCumulativeWork::zero();
118
119    for header in header_iter {
120        min_time = min_time.min(header.time);
121        max_time = max_time.max(header.time);
122
123        last_work = get_work(&header);
124        total_work += last_work;
125    }
126
127    // We added an extra header so we could estimate when mining on the first block
128    // in the window of `num_blocks` likely started. But we don't want to add the work
129    // for that header.
130    total_work -= last_work;
131
132    let work_duration = (max_time - min_time).num_seconds();
133
134    // Avoid division by zero errors and negative average work.
135    // This also handles the case where there's only one block in the range.
136    if work_duration <= 0 {
137        return None;
138    }
139
140    Some(total_work.as_u128() / work_duration as u128)
141}
142
143/// Do a consistency check by checking the finalized tip before and after all other database
144/// queries.
145///
146/// Returns the best chain tip, recent blocks in reverse height order from the tip,
147/// and the tip history tree.
148/// Returns an error if the tip obtained before and after is not the same.
149///
150/// # Panics
151///
152/// - If we don't have enough blocks in the state.
153fn best_relevant_chain_and_history_tree(
154    non_finalized_state: &NonFinalizedState,
155    db: &ZebraDb,
156) -> Result<(Height, block::Hash, Vec<Arc<Block>>, Arc<HistoryTree>), BoxError> {
157    let state_tip_before_queries = read::best_tip(non_finalized_state, db).ok_or_else(|| {
158        BoxError::from("Zebra's state is empty, wait until it syncs to the chain tip")
159    })?;
160
161    let best_relevant_chain =
162        any_ancestor_blocks(non_finalized_state, db, state_tip_before_queries.1);
163    let best_relevant_chain: Vec<_> = best_relevant_chain
164        .into_iter()
165        .take(POW_ADJUSTMENT_BLOCK_SPAN)
166        .collect();
167
168    if best_relevant_chain.is_empty() {
169        return Err("missing genesis block, wait until it is committed".into());
170    };
171
172    let history_tree = history_tree(
173        non_finalized_state.best_chain(),
174        db,
175        state_tip_before_queries.into(),
176    )
177    .expect("tip hash should exist in the chain");
178
179    let state_tip_after_queries =
180        read::best_tip(non_finalized_state, db).expect("already checked for an empty tip");
181
182    if state_tip_before_queries != state_tip_after_queries {
183        return Err("Zebra is committing too many blocks to the state, \
184                    wait until it syncs to the chain tip"
185            .into());
186    }
187
188    Ok((
189        state_tip_before_queries.0,
190        state_tip_before_queries.1,
191        best_relevant_chain,
192        history_tree,
193    ))
194}
195
196/// Returns the [`GetBlockTemplateChainInfo`] for the supplied `relevant_chain`, tip, `network`,
197/// and `history_tree`.
198///
199/// The `relevant_chain` has recent blocks in reverse height order from the tip.
200///
201/// See [`get_block_template_chain_info()`] for details.
202fn difficulty_time_and_history_tree(
203    relevant_chain: Vec<Arc<Block>>,
204    tip_height: Height,
205    tip_hash: block::Hash,
206    network: &Network,
207    history_tree: Arc<HistoryTree>,
208) -> GetBlockTemplateChainInfo {
209    let relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)> = relevant_chain
210        .iter()
211        .map(|block| (block.header.difficulty_threshold, block.header.time))
212        .collect();
213
214    let cur_time = DateTime32::now();
215
216    // > For each block other than the genesis block , nTime MUST be strictly greater than
217    // > the median-time-past of that block.
218    // https://zips.z.cash/protocol/protocol.pdf#blockheader
219    let median_time_past = calculate_median_time_past(
220        relevant_chain
221            .iter()
222            .take(POW_MEDIAN_BLOCK_SPAN)
223            .cloned()
224            .collect(),
225    );
226
227    let min_time = median_time_past
228        .checked_add(Duration32::from_seconds(1))
229        .expect("a valid block time plus a small constant is in-range");
230
231    // > For each block at block height 2 or greater on Mainnet, or block height 653606 or greater on Testnet, nTime
232    // > MUST be less than or equal to the median-time-past of that block plus 90 * 60 seconds.
233    //
234    // We ignore the height as we are checkpointing on Canopy or higher in Mainnet and Testnet.
235    let max_time = median_time_past
236        .checked_add(Duration32::from_seconds(BLOCK_MAX_TIME_SINCE_MEDIAN))
237        .expect("a valid block time plus a small constant is in-range");
238
239    let cur_time = cur_time.clamp(min_time, max_time);
240
241    // Now that we have a valid time, get the difficulty for that time.
242    let difficulty_adjustment = AdjustedDifficulty::new_from_header_time(
243        cur_time.into(),
244        tip_height,
245        network,
246        relevant_data.iter().cloned(),
247    );
248    let expected_difficulty = difficulty_adjustment.expected_difficulty_threshold();
249
250    let mut result = GetBlockTemplateChainInfo {
251        tip_hash,
252        tip_height,
253        chain_history_root: history_tree.hash(),
254        expected_difficulty,
255        cur_time,
256        min_time,
257        max_time,
258    };
259
260    adjust_difficulty_and_time_for_testnet(&mut result, network, tip_height, relevant_data);
261
262    result
263}
264
265/// Adjust the difficulty and time for the testnet minimum difficulty rule.
266///
267/// The `relevant_data` has recent block difficulties and times in reverse order from the tip.
268fn adjust_difficulty_and_time_for_testnet(
269    result: &mut GetBlockTemplateChainInfo,
270    network: &Network,
271    previous_block_height: Height,
272    relevant_data: Vec<(CompactDifficulty, DateTime<Utc>)>,
273) {
274    if network == &Network::Mainnet {
275        return;
276    }
277
278    // On testnet, changing the block time can also change the difficulty,
279    // due to the minimum difficulty consensus rule:
280    // > if the block time of a block at height `height ≥ 299188`
281    // > is greater than 6 * PoWTargetSpacing(height) seconds after that of the preceding block,
282    // > then the block is a minimum-difficulty block.
283    //
284    // The max time is always a minimum difficulty block, because the minimum difficulty
285    // gap is 7.5 minutes, but the maximum gap is 90 minutes. This means that testnet blocks
286    // have two valid time ranges with different difficulties:
287    // * 1s - 7m30s: standard difficulty
288    // * 7m31s - 90m: minimum difficulty
289    //
290    // In rare cases, this could make some testnet miners produce invalid blocks,
291    // if they use the full 90 minute time gap in the consensus rules.
292    // (The zcashd getblocktemplate RPC reference doesn't have a max_time field,
293    // so there is no standard way of telling miners that the max_time is smaller.)
294    //
295    // So Zebra adjusts the min or max times to produce a valid time range for the difficulty.
296    // There is still a small chance that miners will produce an invalid block, if they are
297    // just below the max time, and don't check it.
298
299    // The tip is the first relevant data block, because they are in reverse order.
300    let previous_block_time = relevant_data.first().expect("has at least one block").1;
301    let previous_block_time: DateTime32 = previous_block_time
302        .try_into()
303        .expect("valid blocks have in-range times");
304
305    let Some(minimum_difficulty_spacing) =
306        NetworkUpgrade::minimum_difficulty_spacing_for_height(network, previous_block_height)
307    else {
308        // Returns early if the testnet minimum difficulty consensus rule is not active
309        return;
310    };
311
312    let minimum_difficulty_spacing: Duration32 = minimum_difficulty_spacing
313        .try_into()
314        .expect("small positive values are in-range");
315
316    // The first minimum difficulty time is strictly greater than the spacing.
317    let std_difficulty_max_time = previous_block_time
318        .checked_add(minimum_difficulty_spacing)
319        .expect("a valid block time plus a small constant is in-range");
320    let min_difficulty_min_time = std_difficulty_max_time
321        .checked_add(Duration32::from_seconds(1))
322        .expect("a valid block time plus a small constant is in-range");
323
324    // If a miner is likely to find a block with the cur_time and standard difficulty
325    // within a target block interval or two, keep the original difficulty.
326    // Otherwise, try to use the minimum difficulty.
327    //
328    // This is a Zebra-specific standard rule.
329    //
330    // We don't need to undo the clamping here:
331    // - if cur_time is clamped to min_time, then we're more likely to have a minimum
332    //    difficulty block, which makes mining easier;
333    // - if cur_time gets clamped to max_time, this is almost always a minimum difficulty block.
334    let local_std_difficulty_limit = std_difficulty_max_time
335        .checked_sub(Duration32::from_seconds(EXTRA_TIME_TO_MINE_A_BLOCK))
336        .expect("a valid block time minus a small constant is in-range");
337
338    if result.cur_time <= local_std_difficulty_limit {
339        // Standard difficulty: the cur and max time need to exclude min difficulty blocks
340
341        // The maximum time can only be decreased, and only as far as min_time.
342        // The old minimum is still required by other consensus rules.
343        result.max_time = std_difficulty_max_time.clamp(result.min_time, result.max_time);
344
345        // The current time only needs to be decreased if the max_time decreased past it.
346        // Decreasing the current time can't change the difficulty.
347        result.cur_time = result.cur_time.clamp(result.min_time, result.max_time);
348    } else {
349        // Minimum difficulty: the min and cur time need to exclude std difficulty blocks
350
351        // The minimum time can only be increased, and only as far as max_time.
352        // The old maximum is still required by other consensus rules.
353        result.min_time = min_difficulty_min_time.clamp(result.min_time, result.max_time);
354
355        // The current time only needs to be increased if the min_time increased past it.
356        result.cur_time = result.cur_time.clamp(result.min_time, result.max_time);
357
358        // And then the difficulty needs to be updated for cur_time.
359        result.expected_difficulty = AdjustedDifficulty::new_from_header_time(
360            result.cur_time.into(),
361            previous_block_height,
362            network,
363            relevant_data.iter().cloned(),
364        )
365        .expected_difficulty_threshold();
366    }
367}