diff --git a/Cargo.lock b/Cargo.lock index bf9b8cc33..e6ebcc693 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3253,6 +3253,7 @@ dependencies = [ name = "zebra-state" version = "3.0.0-alpha.0" dependencies = [ + "chrono", "color-eyre", "dirs", "displaydoc", diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 5f1e5da34..c16d04f1d 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -26,6 +26,7 @@ tokio = { version = "0.3", features = ["sync"] } displaydoc = "0.1.7" rocksdb = "0.15.0" tempdir = "0.3.7" +chrono = "0.4.19" [dev-dependencies] zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] } diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index bb368eaf7..01ed6d8c5 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -6,6 +6,7 @@ use std::{ time::{Duration, Instant}, }; +use check::difficulty::{POW_AVERAGING_WINDOW, POW_MEDIAN_BLOCK_SPAN}; use futures::future::FutureExt; use non_finalized_state::{NonFinalizedState, QueuedBlocks}; use tokio::sync::oneshot; @@ -184,11 +185,15 @@ impl StateService { &mut self, prepared: &PreparedBlock, ) -> Result<(), ValidateContextError> { + let relevant_chain = self.chain(prepared.block.header.previous_block_hash); + assert!(relevant_chain.len() >= POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN, + "contextual validation requires at least 28 (POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN) blocks"); + check::block_is_contextually_valid( prepared, self.network, self.disk.finalized_tip_height(), - self.chain(prepared.block.header.previous_block_hash), + relevant_chain, )?; Ok(()) diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index 817cff37e..23618d657 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -1,14 +1,20 @@ //! Consensus critical contextual checks +use std::borrow::Borrow; + use zebra_chain::{ block::{self, Block}, parameters::Network, + work::difficulty::CompactDifficulty, }; use crate::{PreparedBlock, ValidateContextError}; use super::check; +pub mod difficulty; +use difficulty::{AdjustedDifficulty, POW_AVERAGING_WINDOW, POW_MEDIAN_BLOCK_SPAN}; + /// Check that `block` is contextually valid for `network`, based on the /// `finalized_tip_height` and `relevant_chain`. /// @@ -16,6 +22,9 @@ use super::check; /// with its parent block. /// /// Panics if the finalized state is empty. +/// +/// Skips the difficulty adjustment check if the state contains less than 28 +/// (`POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN`) blocks. #[tracing::instrument( name = "contextual_validation", fields(?network), @@ -29,30 +38,51 @@ pub(crate) fn block_is_contextually_valid( ) -> Result<(), ValidateContextError> where C: IntoIterator, - C::Item: AsRef, + C::Item: Borrow, + C::IntoIter: ExactSizeIterator, { let finalized_tip_height = finalized_tip_height .expect("finalized state must contain at least one block to do contextual validation"); check::block_is_not_orphaned(finalized_tip_height, prepared.height)?; - let mut relevant_chain = relevant_chain.into_iter(); + // Peek at the first block + let mut relevant_chain = relevant_chain.into_iter().peekable(); let parent_block = relevant_chain - .next() + .peek() .expect("state must contain parent block to do contextual validation"); - let parent_block = parent_block.as_ref(); + let parent_block = parent_block.borrow(); let parent_height = parent_block .coinbase_height() .expect("valid blocks have a coinbase height"); check::height_one_more_than_parent_height(parent_height, prepared.height)?; - // TODO: validate difficulty adjustment - // TODO: other contextual validation design and implelentation + // Note: the difficulty check reads the first 28 blocks from the relevant + // chain iterator. If you want to use those blocks in other checks, you'll + // need to clone them here. + + if relevant_chain.len() >= POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN { + let relevant_data = relevant_chain.map(|block| { + ( + block.borrow().header.difficulty_threshold, + block.borrow().header.time, + ) + }); + // Reads the first 28 blocks from the iterator + let expected_difficulty = + AdjustedDifficulty::new_from_block(&prepared.block, network, relevant_data); + check::difficulty_threshold_is_valid( + prepared.block.header.difficulty_threshold, + expected_difficulty, + )?; + } + + // TODO: other contextual validation design and implementation Ok(()) } /// Returns `ValidateContextError::OrphanedBlock` if the height of the given /// block is less than or equal to the finalized tip height. -pub(super) fn block_is_not_orphaned( +fn block_is_not_orphaned( finalized_tip_height: block::Height, height: block::Height, ) -> Result<(), ValidateContextError> { @@ -65,7 +95,7 @@ pub(super) fn block_is_not_orphaned( /// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't /// equal to the parent_height+1. -pub(super) fn height_one_more_than_parent_height( +fn height_one_more_than_parent_height( parent_height: block::Height, height: block::Height, ) -> Result<(), ValidateContextError> { @@ -76,6 +106,19 @@ pub(super) fn height_one_more_than_parent_height( } } +/// Validate the `difficulty_threshold` from a candidate block's header, based +/// on an `expected_difficulty` for that block. +/// +/// Uses `expected_difficulty` to calculate the expected `ToCompact(Threshold())` +/// value, then compares that value to the `difficulty_threshold`. +/// Returns `Ok(())` if the values are equal. +fn difficulty_threshold_is_valid( + _difficulty_threshold: CompactDifficulty, + _expected_difficulty: AdjustedDifficulty, +) -> Result<(), ValidateContextError> { + Ok(()) +} + #[cfg(test)] mod tests { use std::sync::Arc; diff --git a/zebra-state/src/service/check/difficulty.rs b/zebra-state/src/service/check/difficulty.rs new file mode 100644 index 000000000..5aac11ea7 --- /dev/null +++ b/zebra-state/src/service/check/difficulty.rs @@ -0,0 +1,127 @@ +//! Block difficulty adjustment calculations for contextual validation. + +use chrono::{DateTime, Utc}; + +use std::convert::TryInto; + +use zebra_chain::{block, block::Block, parameters::Network, work::difficulty::CompactDifficulty}; + +/// The averaging window for difficulty threshold arithmetic mean calculations. +/// +/// `PoWAveragingWindow` in the Zcash specification. +pub const POW_AVERAGING_WINDOW: usize = 17; + +/// The median block span for time median calculations. +/// +/// `PoWMedianBlockSpan` in the Zcash specification. +pub const POW_MEDIAN_BLOCK_SPAN: usize = 11; + +/// Contains the context needed to calculate the adjusted difficulty for a block. +#[allow(dead_code)] +pub(super) struct AdjustedDifficulty { + /// The `header.time` field from the candidate block + candidate_time: DateTime, + /// The coinbase height from the candidate block + /// + /// If we only have the header, this field is calculated from the previous + /// block height. + candidate_height: block::Height, + /// The configured network + network: Network, + /// The `header.difficulty_threshold`s from the previous + /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks, in reverse height + /// order. + relevant_difficulty_thresholds: [CompactDifficulty; 28], + /// The `header.time`s from the previous + /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) blocks, in reverse height + /// order. + /// + /// Only the first and last `PoWMedianBlockSpan` times are used. Times + /// `11..=16` are ignored. + relevant_times: [DateTime; 28], +} + +impl AdjustedDifficulty { + /// Initialise and return a new `AdjustedDifficulty` using a `candidate_block`, + /// `network`, and a `context`. + /// + /// The `context` contains the previous + /// `PoWAveragingWindow + PoWMedianBlockSpan` (28) `difficulty_threshold`s and + /// `time`s from the relevant chain for `candidate_block`, in reverse height + /// order, starting with the previous block. + /// + /// Note that the `time`s might not be in reverse chronological order, because + /// block times are supplied by miners. + /// + /// Panics: + /// If the `context` contains fewer than 28 items. + pub fn new_from_block( + candidate_block: &Block, + network: Network, + context: C, + ) -> AdjustedDifficulty + where + C: IntoIterator)>, + { + let candidate_block_height = candidate_block + .coinbase_height() + .expect("semantically valid blocks have a coinbase height"); + let previous_block_height = (candidate_block_height - 1) + .expect("contextual validation is never run on the genesis block"); + + AdjustedDifficulty::new_from_header( + &candidate_block.header, + previous_block_height, + network, + context, + ) + } + + /// Initialise and return a new `AdjustedDifficulty` using a + /// `candidate_header`, `previous_block_height`, `network`, and a `context`. + /// + /// Designed for use when validating block headers, where the full block has not + /// been downloaded yet. + /// + /// See `new_from_block` for detailed information about the `context`. + /// + /// Panics: + /// If the context contains fewer than 28 items. + pub fn new_from_header( + candidate_header: &block::Header, + previous_block_height: block::Height, + network: Network, + context: C, + ) -> AdjustedDifficulty + where + C: IntoIterator)>, + { + let candidate_height = (previous_block_height + 1).expect("next block height is valid"); + + // unzip would be a lot nicer here, but we can't satisfy its trait bounds + let context: Vec<_> = context + .into_iter() + .take(POW_AVERAGING_WINDOW + POW_MEDIAN_BLOCK_SPAN) + .collect(); + let relevant_difficulty_thresholds = context + .iter() + .map(|pair| pair.0) + .collect::>() + .try_into() + .expect("not enough context: difficulty adjustment needs at least 28 (PoWAveragingWindow + PoWMedianBlockSpan) headers"); + let relevant_times = context + .iter() + .map(|pair| pair.1) + .collect::>() + .try_into() + .expect("not enough context: difficulty adjustment needs at least 28 (PoWAveragingWindow + PoWMedianBlockSpan) headers"); + + AdjustedDifficulty { + candidate_time: candidate_header.time, + candidate_height, + network, + relevant_difficulty_thresholds, + relevant_times, + } + } +}