diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 67d17f27f..e7425bf9b 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -43,6 +43,14 @@ pub enum ValidateContextError { #[non_exhaustive] NonSequentialBlock, + /// block.header.time is less than or equal to the median-time-past for the block + #[non_exhaustive] + TimeTooEarly, + + /// block.header.time is greater than the median-time-past for the block plus 90 minutes + #[non_exhaustive] + TimeTooLate, + /// block.header.difficulty_threshold is not equal to the adjusted difficulty for the block #[non_exhaustive] InvalidDifficultyThreshold, diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index 103ae1c7a..1e6b22250 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -2,6 +2,7 @@ use std::borrow::Borrow; +use chrono::Duration; use zebra_chain::{ block::{self, Block}, parameters::Network, @@ -74,11 +75,11 @@ where block.borrow().header.time, ) }); - let expected_difficulty = + let difficulty_adjustment = AdjustedDifficulty::new_from_block(&prepared.block, network, relevant_data); check::difficulty_threshold_is_valid( prepared.block.header.difficulty_threshold, - expected_difficulty, + difficulty_adjustment, )?; // TODO: other contextual validation design and implementation @@ -111,22 +112,37 @@ 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. +/// Validate the `time` and `difficulty_threshold` from a candidate block's +/// header. /// -/// Uses `expected_difficulty` to calculate the expected difficulty value, then -/// compares that value to the `difficulty_threshold`. +/// Uses the `difficulty_adjustment` context for the block to: +/// * check that the the `time` field is within the valid range, and +/// * check that the expected difficulty is equal to the block's +/// `difficulty_threshold`. /// -/// The check passes if the values are equal. +/// These checks are performed together, because the time field is used to +/// calculate the expected difficulty adjustment. fn difficulty_threshold_is_valid( difficulty_threshold: CompactDifficulty, - expected_difficulty: AdjustedDifficulty, + difficulty_adjustment: AdjustedDifficulty, ) -> Result<(), ValidateContextError> { - if difficulty_threshold == expected_difficulty.expected_difficulty_threshold() { - Ok(()) - } else { - Err(ValidateContextError::InvalidDifficultyThreshold) + // Check the block header time consensus rules from the Zcash specification + let median_time_past = difficulty_adjustment.median_time_past(); + let block_max_time_since_median = Duration::seconds(difficulty::BLOCK_MAX_TIME_SINCE_MEDIAN); + if difficulty_adjustment.candidate_time() <= median_time_past { + Err(ValidateContextError::TimeTooEarly)? + } else if difficulty_adjustment.candidate_time() + > median_time_past + block_max_time_since_median + { + Err(ValidateContextError::TimeTooLate)? } + + let expected_difficulty = difficulty_adjustment.expected_difficulty_threshold(); + if difficulty_threshold != expected_difficulty { + Err(ValidateContextError::InvalidDifficultyThreshold)? + } + + Ok(()) } #[cfg(test)] diff --git a/zebra-state/src/service/check/difficulty.rs b/zebra-state/src/service/check/difficulty.rs index 94875450d..617acfec8 100644 --- a/zebra-state/src/service/check/difficulty.rs +++ b/zebra-state/src/service/check/difficulty.rs @@ -36,6 +36,12 @@ pub const POW_MAX_ADJUST_UP_PERCENT: i32 = 16; /// `PoWMaxAdjustDown * 100` in the Zcash specification. pub const POW_MAX_ADJUST_DOWN_PERCENT: i32 = 32; +/// The maximum number of seconds between the `meadian-time-past` of a block, +/// and the block's `time` field. +/// +/// Part of the block header consensus rules in the Zcash specification. +pub const BLOCK_MAX_TIME_SINCE_MEDIAN: i64 = 90 * 60; + /// Contains the context needed to calculate the adjusted difficulty for a block. pub(super) struct AdjustedDifficulty { /// The `header.time` field from the candidate block @@ -141,6 +147,11 @@ impl AdjustedDifficulty { } } + /// Returns the candidate block's time field. + pub fn candidate_time(&self) -> DateTime { + self.candidate_time + } + /// Calculate the expected `difficulty_threshold` for a candidate block, based /// on the `candidate_time`, `candidate_height`, `network`, and the /// `difficulty_threshold`s and `time`s from the previous