Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix minimum difficulty bugs #1256

Merged
merged 7 commits into from
Nov 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 119 additions & 99 deletions Cargo.lock

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions zebra-chain/src/parameters/network_upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::parameters::{Network, Network::*};
use std::collections::{BTreeMap, HashMap};
use std::ops::Bound::*;

use chrono::Duration;

/// A Zcash network upgrade.
///
/// Network upgrades can change the Zcash network protocol or consensus rules in
Expand Down Expand Up @@ -96,6 +98,23 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] =
(Canopy, ConsensusBranchId(0xe9ff75a6)),
];

/// The target block spacing before Blossom.
const PRE_BLOSSOM_POW_TARGET_SPACING: i64 = 150;

/// The target block spacing after Blossom activation.
const POST_BLOSSOM_POW_TARGET_SPACING: i64 = 75;

/// The multiplier used to derive the testnet minimum difficulty block time gap
/// threshold.
///
/// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
const TESTNET_MINIMUM_DIFFICULTY_GAP_MULTIPLIER: i32 = 6;

/// The start height for the testnet minimum difficulty consensus rule.
///
/// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
const TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT: block::Height = block::Height(299_188);

impl NetworkUpgrade {
/// Returns a BTreeMap of activation heights and network upgrades for
/// `network`.
Expand Down Expand Up @@ -163,6 +182,47 @@ impl NetworkUpgrade {
pub fn branch_id(&self) -> Option<ConsensusBranchId> {
NetworkUpgrade::branch_id_list().get(&self).cloned()
}

/// Returns the target block spacing for the network upgrade.
///
/// Based on `PRE_BLOSSOM_POW_TARGET_SPACING` and
/// `POST_BLOSSOM_POW_TARGET_SPACING` from the Zcash specification.
pub fn target_spacing(&self) -> Duration {
let spacing_seconds = match self {
Genesis | BeforeOverwinter | Overwinter | Sapling => PRE_BLOSSOM_POW_TARGET_SPACING,
Blossom | Heartwood | Canopy => POST_BLOSSOM_POW_TARGET_SPACING,
};

Duration::seconds(spacing_seconds)
}

/// Returns the target block spacing for `network` and `height`.
///
/// See `target_spacing` for details.
pub fn target_spacing_for_height(network: Network, height: block::Height) -> Duration {
NetworkUpgrade::current(network, height).target_spacing()
}

/// Returns the minimum difficulty block spacing for `network` and `height`.
/// Returns `None` if the testnet minimum difficulty consensus rule is not active.
///
/// Based on https://zips.z.cash/zip-0208#minimum-difficulty-blocks-on-the-test-network
///
/// `zcashd` requires a gap that's strictly greater than 6 times the target
/// threshold, but ZIP-205 and ZIP-208 are ambiguous. See bug #1276.
pub fn minimum_difficulty_spacing_for_height(
network: Network,
height: block::Height,
) -> Option<Duration> {
match (network, height) {
(Network::Testnet, height) if height < TESTNET_MINIMUM_DIFFICULTY_START_HEIGHT => None,
(Network::Mainnet, _) => None,
(Network::Testnet, _) => {
let network_upgrade = NetworkUpgrade::current(network, height);
Some(network_upgrade.target_spacing() * TESTNET_MINIMUM_DIFFICULTY_GAP_MULTIPLIER)
}
}
}
}

impl ConsensusBranchId {
Expand Down
13 changes: 11 additions & 2 deletions zebra-chain/src/work/difficulty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl fmt::Debug for ExpandedDifficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut buf = [0; 32];
// Use the same byte order as block::Hash
self.0.to_little_endian(&mut buf);
self.0.to_big_endian(&mut buf);
f.debug_tuple("ExpandedDifficulty")
.field(&hex::encode(&buf))
.finish()
Expand Down Expand Up @@ -280,7 +280,16 @@ impl ExpandedDifficulty {
Network::Testnet => (U256::one() << 251) - 1,
};

limit.into()
// `zcashd` converts the PoWLimit into a compact representation before
// using it to perform difficulty filter checks.
//
// The Zcash specification converts to compact for the default difficulty
// filter, but not for testnet minimum difficulty blocks. (ZIP 205 and
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
// ZIP 208 don't specify this conversion either.) See #1277 for details.
ExpandedDifficulty(limit)
.to_compact()
.to_expanded()
.expect("difficulty limits are valid expanded values")
}

/// Calculate the CompactDifficulty for an expanded difficulty.
Expand Down
191 changes: 176 additions & 15 deletions zebra-chain/src/work/difficulty/tests/vectors.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use color_eyre::eyre::eyre;
use color_eyre::eyre::Report;
use std::sync::Arc;

use crate::block::Block;
use crate::serialization::ZcashDeserialize;
use crate::{block::Block, parameters::NetworkUpgrade};

use super::super::*;

Expand Down Expand Up @@ -36,14 +36,15 @@ fn debug_format() {
);
assert_eq!(
format!("{:?}", ExpandedDifficulty(U256::one())),
"ExpandedDifficulty(\"0100000000000000000000000000000000000000000000000000000000000000\")"
"ExpandedDifficulty(\"0000000000000000000000000000000000000000000000000000000000000001\")"
);
assert_eq!(
format!("{:?}", ExpandedDifficulty(U256::MAX)),
"ExpandedDifficulty(\"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\")"
);

assert_eq!(format!("{:?}", Work(0)), "Work(0x0, 0, -inf)");
assert_eq!(format!("{:?}", Work(1)), "Work(0x1, 1, 0.00000)");
assert_eq!(
format!("{:?}", Work(u8::MAX as u128)),
"Work(0xff, 255, 7.99435)"
Expand Down Expand Up @@ -247,16 +248,21 @@ fn compact_bitcoin_test_vectors() {

/// Test blocks using CompactDifficulty.
#[test]
#[spandoc::spandoc]
fn block_difficulty() -> Result<(), Report> {
block_difficulty_for_network(Network::Mainnet)?;
block_difficulty_for_network(Network::Testnet)?;

Ok(())
}

#[spandoc::spandoc]
fn block_difficulty_for_network(network: Network) -> Result<(), Report> {
zebra_test::init();

let mut blockchain = Vec::new();
for b in zebra_test::vectors::BLOCKS.iter() {
let block = Arc::<Block>::zcash_deserialize(*b)?;
let hash = block.hash();
blockchain.push((block.clone(), block.coinbase_height().unwrap(), hash));
}
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};

let diff_zero = ExpandedDifficulty(U256::zero());
let diff_one = ExpandedDifficulty(U256::one());
Expand All @@ -267,15 +273,20 @@ fn block_difficulty() -> Result<(), Report> {

let mut cumulative_work = PartialCumulativeWork::default();
let mut previous_cumulative_work = PartialCumulativeWork::default();
for (block, height, hash) in blockchain {
/// SPANDOC: Calculate the threshold for block {?height}

for (&height, block) in block_iter {
let block =
Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
let hash = block.hash();

/// SPANDOC: Calculate the threshold for block {?height, ?network}
let threshold = block
.header
.difficulty_threshold
.to_expanded()
.expect("Chain blocks have valid difficulty thresholds.");

/// SPANDOC: Check the difficulty for block {?height, ?threshold, ?hash}
/// SPANDOC: Check the difficulty for block {?height, ?network, ?threshold, ?hash}
{
assert!(hash <= threshold);
// also check the comparison operators work
Expand All @@ -284,15 +295,23 @@ fn block_difficulty() -> Result<(), Report> {
assert!(hash < diff_max);
}

/// SPANDOC: Check compact round-trip for block {?height}
/// SPANDOC: Check the PoWLimit for block {?height, ?network, ?threshold, ?hash}
{
// the consensus rule
assert!(threshold <= ExpandedDifficulty::target_difficulty_limit(network));
// check that ordering is transitive, we checked `hash <= threshold` above
assert!(hash <= ExpandedDifficulty::target_difficulty_limit(network));
}

/// SPANDOC: Check compact round-trip for block {?height, ?network}
{
let canonical_compact = threshold.to_compact();

assert_eq!(block.header.difficulty_threshold,
canonical_compact);
}

/// SPANDOC: Check the work for block {?height}
/// SPANDOC: Check the work for block {?height, ?network}
{
let work = block
.header
Expand All @@ -317,6 +336,148 @@ fn block_difficulty() -> Result<(), Report> {
Ok(())
}

/// Test that the genesis block threshold is PowLimit
#[test]
fn genesis_block_difficulty() -> Result<(), Report> {
genesis_block_difficulty_for_network(Network::Mainnet)?;
genesis_block_difficulty_for_network(Network::Testnet)?;

Ok(())
}

#[spandoc::spandoc]
fn genesis_block_difficulty_for_network(network: Network) -> Result<(), Report> {
zebra_test::init();

let block = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.get(&0),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.get(&0),
};

let block = block.expect("test vectors contain the genesis block");
let block = Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
let hash = block.hash();

/// SPANDOC: Calculate the threshold for the genesis block {?network}
let threshold = block
.header
.difficulty_threshold
.to_expanded()
.expect("Chain blocks have valid difficulty thresholds.");

/// SPANDOC: Check the genesis PoWLimit {?network, ?threshold, ?hash}
{
assert_eq!(threshold, ExpandedDifficulty::target_difficulty_limit(network),
"genesis block difficulty thresholds must be equal to the PoWLimit");
}

Ok(())
}

/// Test that testnet minimum-difficulty blocks are valid
#[test]
#[spandoc::spandoc]
fn testnet_minimum_difficulty() -> Result<(), Report> {
const MINIMUM_DIFFICULTY_HEIGHTS: &[block::Height] = &[
// block time gaps greater than 15 minutes (pre-Blossom)
block::Height(299_188),
block::Height(299_189),
block::Height(299_202),
// block time gaps greater than 7.5 minutes (Blossom and later)
block::Height(584_000),
// these 3 blocks have gaps greater than 7.5 minutes and less than 15 minutes
block::Height(903_800),
block::Height(903_801),
block::Height(1_028_500),
];

for (&height, _block) in zebra_test::vectors::TESTNET_BLOCKS.iter() {
let height = block::Height(height);

/// SPANDOC: Do minimum difficulty checks for testnet block {?height}
if MINIMUM_DIFFICULTY_HEIGHTS.contains(&height) {
check_testnet_minimum_difficulty_block(height)?;
} else {
assert!(check_testnet_minimum_difficulty_block(height).is_err(),
"all testnet minimum difficulty block test vectors must be tested by the unit tests. Hint: add the failing block to MINIMUM_DIFFICULTY_HEIGHTS");
}
}

Ok(())
}

/// Check that the testnet block at `height` is a testnet minimum difficulty
/// block.
#[spandoc::spandoc]
fn check_testnet_minimum_difficulty_block(height: block::Height) -> Result<(), Report> {
let block = zebra_test::vectors::TESTNET_BLOCKS
.get(&height.0)
.expect("test vectors contain the specified minimum difficulty block height");
let block = Block::zcash_deserialize(&block[..]).expect("block test vector should deserialize");
let hash = block.hash();

/// SPANDOC: Check the testnet minimum difficulty start height {?height, ?hash}
if height < block::Height(299_188) {
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
Err(eyre!(
"the testnet minimum difficulty rule starts at block 299188"
))?;
}
teor2345 marked this conversation as resolved.
Show resolved Hide resolved

/// SPANDOC: Make sure testnet minimum difficulty blocks have large time gaps {?height, ?hash}
{
let previous_block = zebra_test::vectors::TESTNET_BLOCKS.get(&(height.0 - 1));
if previous_block.is_none() {
Err(eyre!(
"test vectors should contain the previous block for each minimum difficulty block"
))?;
}

let previous_block = previous_block.unwrap();
let previous_block = Block::zcash_deserialize(&previous_block[..])
.expect("block test vector should deserialize");
let time_gap = block
.header
.time
.signed_duration_since(previous_block.header.time);

// zcashd requires a gap that's strictly greater than 6 times the target
// threshold, but ZIP-205 and ZIP-208 are ambiguous. See bug #1276.
match NetworkUpgrade::minimum_difficulty_spacing_for_height(Network::Testnet, height) {
None => Err(eyre!("the minimum difficulty rule is not active"))?,
Some(spacing) if (time_gap <= spacing) => Err(eyre!(
"minimum difficulty block times must be more than 6 target spacing intervals apart"
))?,
_ => {}
};
}

// At this point, the current block has passed all the consensus rules that allow
// minimum-difficulty blocks. So it is *allowed* to be a minimum-difficulty block, but not
// *required* to be one. But at the moment, all test vectors with large gaps are minimum-difficulty
// blocks.

/// SPANDOC: Calculate the threshold for testnet block {?height, ?hash}
let threshold = block
.header
.difficulty_threshold
.to_expanded()
.expect("Chain blocks have valid difficulty thresholds.");

/// SPANDOC: Check that the testnet minimum difficulty is the PoWLimit {?height, ?threshold, ?hash}
{
assert_eq!(threshold, ExpandedDifficulty::target_difficulty_limit(Network::Testnet),
"testnet minimum difficulty thresholds should be equal to the PoWLimit. Hint: Blocks with large gaps are allowed to have the minimum difficulty, but it's not required.");
// all blocks pass the minimum difficulty threshold, even if they aren't minimum
// difficulty blocks, because it's the lowest permitted difficulty
assert!(
hash <= ExpandedDifficulty::target_difficulty_limit(Network::Testnet),
"testnet minimum difficulty hashes must be less than the PoWLimit"
);
}

Ok(())
}

/// Test ExpandedDifficulty ordering
#[test]
#[spandoc::spandoc]
Expand Down
9 changes: 8 additions & 1 deletion zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,19 @@ pub fn difficulty_is_valid(
))?;
}

// Difficulty filter
// The difficulty filter is also context-free.
//
// ZIP 205 and ZIP 208 incorrectly describe testnet minimum difficulty blocks
// as a change to the difficulty filter. But in `zcashd`, it is implemented
// as a change to the difficulty adjustment algorithm. So we don't need to
// do anything special for testnet here.
// For details, see https://github.com/zcash/zips/issues/416
if hash > &difficulty_threshold {
Err(BlockError::DifficultyFilter(
*height,
*hash,
difficulty_threshold,
network,
))?;
}

Expand Down
3 changes: 2 additions & 1 deletion zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ fn difficulty_validation_failure() -> Result<(), Report> {
// Validate the block
let result = check::difficulty_is_valid(&block.header, Network::Mainnet, &height, &bad_hash)
.unwrap_err();
let expected = BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold);
let expected =
BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold, Network::Mainnet);
assert_eq!(expected, result);

Ok(())
Expand Down
Loading