Implement AdjustedDifficulty creation
Also: * call the difficulty check from `block_is_contextually_valid` * add a stub `difficulty_threshold_is_valid` function
This commit is contained in:
parent
fa03b83351
commit
939c2b97a6
|
|
@ -3253,6 +3253,7 @@ dependencies = [
|
||||||
name = "zebra-state"
|
name = "zebra-state"
|
||||||
version = "3.0.0-alpha.0"
|
version = "3.0.0-alpha.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"dirs",
|
"dirs",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ tokio = { version = "0.3", features = ["sync"] }
|
||||||
displaydoc = "0.1.7"
|
displaydoc = "0.1.7"
|
||||||
rocksdb = "0.15.0"
|
rocksdb = "0.15.0"
|
||||||
tempdir = "0.3.7"
|
tempdir = "0.3.7"
|
||||||
|
chrono = "0.4.19"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
|
zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use check::difficulty::{POW_AVERAGING_WINDOW, POW_MEDIAN_BLOCK_SPAN};
|
||||||
use futures::future::FutureExt;
|
use futures::future::FutureExt;
|
||||||
use non_finalized_state::{NonFinalizedState, QueuedBlocks};
|
use non_finalized_state::{NonFinalizedState, QueuedBlocks};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
@ -184,11 +185,15 @@ impl StateService {
|
||||||
&mut self,
|
&mut self,
|
||||||
prepared: &PreparedBlock,
|
prepared: &PreparedBlock,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> 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(
|
check::block_is_contextually_valid(
|
||||||
prepared,
|
prepared,
|
||||||
self.network,
|
self.network,
|
||||||
self.disk.finalized_tip_height(),
|
self.disk.finalized_tip_height(),
|
||||||
self.chain(prepared.block.header.previous_block_hash),
|
relevant_chain,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
//! Consensus critical contextual checks
|
//! Consensus critical contextual checks
|
||||||
|
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
block::{self, Block},
|
block::{self, Block},
|
||||||
parameters::Network,
|
parameters::Network,
|
||||||
|
work::difficulty::CompactDifficulty,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{PreparedBlock, ValidateContextError};
|
use crate::{PreparedBlock, ValidateContextError};
|
||||||
|
|
||||||
use super::check;
|
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
|
/// Check that `block` is contextually valid for `network`, based on the
|
||||||
/// `finalized_tip_height` and `relevant_chain`.
|
/// `finalized_tip_height` and `relevant_chain`.
|
||||||
///
|
///
|
||||||
|
|
@ -16,6 +22,9 @@ use super::check;
|
||||||
/// with its parent block.
|
/// with its parent block.
|
||||||
///
|
///
|
||||||
/// Panics if the finalized state is empty.
|
/// 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(
|
#[tracing::instrument(
|
||||||
name = "contextual_validation",
|
name = "contextual_validation",
|
||||||
fields(?network),
|
fields(?network),
|
||||||
|
|
@ -29,30 +38,51 @@ pub(crate) fn block_is_contextually_valid<C>(
|
||||||
) -> Result<(), ValidateContextError>
|
) -> Result<(), ValidateContextError>
|
||||||
where
|
where
|
||||||
C: IntoIterator,
|
C: IntoIterator,
|
||||||
C::Item: AsRef<Block>,
|
C::Item: Borrow<Block>,
|
||||||
|
C::IntoIter: ExactSizeIterator,
|
||||||
{
|
{
|
||||||
let finalized_tip_height = finalized_tip_height
|
let finalized_tip_height = finalized_tip_height
|
||||||
.expect("finalized state must contain at least one block to do contextual validation");
|
.expect("finalized state must contain at least one block to do contextual validation");
|
||||||
check::block_is_not_orphaned(finalized_tip_height, prepared.height)?;
|
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
|
let parent_block = relevant_chain
|
||||||
.next()
|
.peek()
|
||||||
.expect("state must contain parent block to do contextual validation");
|
.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
|
let parent_height = parent_block
|
||||||
.coinbase_height()
|
.coinbase_height()
|
||||||
.expect("valid blocks have a coinbase height");
|
.expect("valid blocks have a coinbase height");
|
||||||
check::height_one_more_than_parent_height(parent_height, prepared.height)?;
|
check::height_one_more_than_parent_height(parent_height, prepared.height)?;
|
||||||
|
|
||||||
// TODO: validate difficulty adjustment
|
// Note: the difficulty check reads the first 28 blocks from the relevant
|
||||||
// TODO: other contextual validation design and implelentation
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
|
/// Returns `ValidateContextError::OrphanedBlock` if the height of the given
|
||||||
/// block is less than or equal to the finalized tip height.
|
/// 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,
|
finalized_tip_height: block::Height,
|
||||||
height: block::Height,
|
height: block::Height,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> Result<(), ValidateContextError> {
|
||||||
|
|
@ -65,7 +95,7 @@ pub(super) fn block_is_not_orphaned(
|
||||||
|
|
||||||
/// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't
|
/// Returns `ValidateContextError::NonSequentialBlock` if the block height isn't
|
||||||
/// equal to the parent_height+1.
|
/// 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,
|
parent_height: block::Height,
|
||||||
height: block::Height,
|
height: block::Height,
|
||||||
) -> Result<(), ValidateContextError> {
|
) -> 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -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<Utc>,
|
||||||
|
/// 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<Utc>; 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<C>(
|
||||||
|
candidate_block: &Block,
|
||||||
|
network: Network,
|
||||||
|
context: C,
|
||||||
|
) -> AdjustedDifficulty
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
|
||||||
|
{
|
||||||
|
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<C>(
|
||||||
|
candidate_header: &block::Header,
|
||||||
|
previous_block_height: block::Height,
|
||||||
|
network: Network,
|
||||||
|
context: C,
|
||||||
|
) -> AdjustedDifficulty
|
||||||
|
where
|
||||||
|
C: IntoIterator<Item = (CompactDifficulty, DateTime<Utc>)>,
|
||||||
|
{
|
||||||
|
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::<Vec<_>>()
|
||||||
|
.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::<Vec<_>>()
|
||||||
|
.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue