//! Get context and calculate difficulty for the next block. use std::borrow::Borrow; use chrono::{DateTime, Duration, TimeZone, Utc}; use zebra_chain::{ block::{Block, Hash, Height}, parameters::{Network, NetworkUpgrade, POW_AVERAGING_WINDOW}, work::difficulty::{CompactDifficulty, ExpandedDifficulty}, }; use crate::{ service::{ any_ancestor_blocks, check::{ difficulty::{BLOCK_MAX_TIME_SINCE_MEDIAN, POW_MEDIAN_BLOCK_SPAN}, AdjustedDifficulty, }, finalized_state::ZebraDb, NonFinalizedState, }, GetBlockTemplateChainInfo, }; /// Returns : /// - The `CompactDifficulty`, for the current best chain. /// - The current system time. /// - The minimum time for a next block. /// /// Panic if we don't have enough blocks in the state. pub fn difficulty_and_time_info( non_finalized_state: &NonFinalizedState, db: &ZebraDb, tip: (Height, Hash), network: Network, ) -> GetBlockTemplateChainInfo { let relevant_chain = any_ancestor_blocks(non_finalized_state, db, tip.1); difficulty_and_time(relevant_chain, tip, network) } fn difficulty_and_time( relevant_chain: C, tip: (Height, Hash), network: Network, ) -> GetBlockTemplateChainInfo where C: IntoIterator, C::Item: Borrow, C::IntoIter: ExactSizeIterator, { const MAX_CONTEXT_BLOCKS: usize = POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN; let relevant_chain: Vec<_> = relevant_chain .into_iter() .take(MAX_CONTEXT_BLOCKS) .collect(); let relevant_data: Vec<(CompactDifficulty, DateTime)> = relevant_chain .iter() .map(|block| { ( block.borrow().header.difficulty_threshold, block.borrow().header.time, ) }) .collect(); // The getblocktemplate RPC returns an error if Zebra is not synced to the tip. // So this will never happen in production code. assert_eq!( relevant_data.len(), MAX_CONTEXT_BLOCKS, "getblocktemplate RPC called with a near-empty state: should have returned an error", ); let current_system_time = chrono::Utc::now(); // Get the median-time-past, which doesn't depend on the current system time. // // TODO: split out median-time-past into its own struct? let median_time_past = AdjustedDifficulty::new_from_header_time( current_system_time, tip.0, network, relevant_data.clone(), ) .median_time_past(); // > For each block other than the genesis block , nTime MUST be strictly greater than // > the median-time-past of that block. // https://zips.z.cash/protocol/protocol.pdf#blockheader let mut min_time = median_time_past .checked_add_signed(Duration::seconds(1)) .expect("median time plus a small constant is far below i64::MAX"); // > For each block at block height 2 or greater on Mainnet, or block height 653606 or greater on Testnet, nTime // > MUST be less than or equal to the median-time-past of that block plus 90 * 60 seconds. // // We ignore the height as we are checkpointing on Canopy or higher in Mainnet and Testnet. let max_time = median_time_past .checked_add_signed(Duration::seconds(BLOCK_MAX_TIME_SINCE_MEDIAN)) .expect("median time plus a small constant is far below i64::MAX"); let current_system_time = current_system_time .timestamp() .clamp(min_time.timestamp(), max_time.timestamp()); let mut current_system_time = Utc.timestamp_opt(current_system_time, 0).single().expect( "clamping a timestamp between two valid times can't make it invalid, and \ UTC never has ambiguous time zone conversions", ); // Now that we have a valid time, get the difficulty for that time. let mut difficulty_adjustment = AdjustedDifficulty::new_from_header_time( current_system_time, tip.0, network, relevant_data.iter().cloned(), ); // On testnet, changing the block time can also change the difficulty, // due to the minimum difficulty consensus rule: // > if the block time of a block at height height ≥ 299188 // > is greater than 6 * PoWTargetSpacing(height) seconds after that of the preceding block, // > then the block is a minimum-difficulty block. // // In this case, we adjust the min_time and cur_time to the first minimum difficulty time. // // In rare cases, this could make some testnet miners produce invalid blocks, // if they use the full 90 minute time gap in the consensus rules. // (The getblocktemplate RPC reference doesn't have a max_time field, // so there is no standard way of telling miners that the max_time is smaller.) // // But that's better than obscure failures caused by changing the time a small amount, // if that moves the block from standard to minimum difficulty. if network == Network::Testnet { let max_time_difficulty_adjustment = AdjustedDifficulty::new_from_header_time( max_time, tip.0, network, relevant_data.iter().cloned(), ); // The max time is a minimum difficulty block, // so the time range could have different difficulties. if max_time_difficulty_adjustment.expected_difficulty_threshold() == ExpandedDifficulty::target_difficulty_limit(Network::Testnet).to_compact() { let min_time_difficulty_adjustment = AdjustedDifficulty::new_from_header_time( min_time, tip.0, network, relevant_data.iter().cloned(), ); // Part of the valid range has a different difficulty. // So we need to find the minimum time that is also a minimum difficulty block. // This is the valid range for miners. if min_time_difficulty_adjustment.expected_difficulty_threshold() != max_time_difficulty_adjustment.expected_difficulty_threshold() { let preceding_block_time = relevant_data.last().expect("has at least one block").1; let minimum_difficulty_spacing = NetworkUpgrade::minimum_difficulty_spacing_for_height(network, tip.0) .expect("just checked the minimum difficulty rule is active"); // The first minimum difficulty time is strictly greater than the spacing. min_time = preceding_block_time + minimum_difficulty_spacing + Duration::seconds(1); // Update the difficulty and times to match if current_system_time < min_time { current_system_time = min_time; } difficulty_adjustment = AdjustedDifficulty::new_from_header_time( current_system_time, tip.0, network, relevant_data, ); } } } GetBlockTemplateChainInfo { tip, expected_difficulty: difficulty_adjustment.expected_difficulty_threshold(), min_time, current_system_time, max_time, } }