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