194 lines
7.1 KiB
Rust
194 lines
7.1 KiB
Rust
//! 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<C>(
|
|
relevant_chain: C,
|
|
tip: (Height, Hash),
|
|
network: Network,
|
|
) -> GetBlockTemplateChainInfo
|
|
where
|
|
C: IntoIterator,
|
|
C::Item: Borrow<Block>,
|
|
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<Utc>)> = 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,
|
|
}
|
|
}
|