use chrono::{DateTime, Duration};
use zakura_chain::{
block::{merkle::AuthDataRoot, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError},
history_tree::HistoryTree,
parameters::{
testnet::{ConfiguredActivationHeights, Parameters},
Network, NetworkUpgrade, POW_AVERAGING_WINDOW,
},
sapling,
serialization::ZcashDeserializeInto,
work::difficulty::{CompactDifficulty, ParameterDifficulty},
};
use super::super::*;
use crate::tests::FakeChainHelper;
#[test]
fn test_orphan_consensus_check() {
let _init_guard = zakura_test::init();
let height = zakura_test::vectors::BLOCK_MAINNET_347499_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.unwrap()
.coinbase_height()
.unwrap();
block_is_not_orphaned(block::Height(0), height).expect("tip is lower so it should be fine");
block_is_not_orphaned(block::Height(347498), height)
.expect("tip is lower so it should be fine");
block_is_not_orphaned(block::Height(347499), height)
.expect_err("tip is equal so it should error");
block_is_not_orphaned(block::Height(500000), height)
.expect_err("tip is higher so it should error");
}
#[test]
fn test_sequential_height_check() {
let _init_guard = zakura_test::init();
let height = zakura_test::vectors::BLOCK_MAINNET_347499_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.unwrap()
.coinbase_height()
.unwrap();
height_one_more_than_parent_height(block::Height(0), height)
.expect_err("block is much lower, should panic");
height_one_more_than_parent_height(block::Height(347497), height)
.expect_err("parent height is 2 less, should panic");
height_one_more_than_parent_height(block::Height(347498), height)
.expect("parent height is 1 less, should be good");
height_one_more_than_parent_height(block::Height(347499), height)
.expect_err("parent height is equal, should panic");
height_one_more_than_parent_height(block::Height(347500), height)
.expect_err("parent height is way more, should panic");
height_one_more_than_parent_height(block::Height(500000), height)
.expect_err("parent height is way more, should panic");
}
#[test]
fn block_commitment_uses_the_precomputed_auth_data_root() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let parent_height = 1_687_106;
let (blocks, sapling_roots) = network.block_sapling_roots_map();
let parent = Arc::new(
blocks
.get(&parent_height)
.expect("NU5 parent test vector exists")
.zcash_deserialize_into::<Block>()
.expect("NU5 parent block deserializes"),
);
let sapling_root = sapling::tree::Root::try_from(
**sapling_roots
.get(&parent_height)
.expect("NU5 parent Sapling root exists"),
)
.expect("Sapling root vector is valid");
let history_tree = HistoryTree::from_block(
&network,
parent.clone(),
&sapling_root,
&Default::default(),
&Default::default(),
)
.expect("NU5 parent builds a history tree");
let child = parent.make_fake_child();
let auth_data_root = child.auth_data_root();
let hash_block_commitments = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
&history_tree
.hash()
.expect("NU5 parent history tree has a root"),
&auth_data_root,
);
let block_commitment: [u8; 32] = hash_block_commitments.into();
let child = child.set_block_commitment(block_commitment);
block_commitment_is_valid_for_chain_history(
child.clone(),
&network,
&history_tree,
Some(auth_data_root),
)
.expect("a matching precomputed auth data root is accepted");
let forged_auth_data_root = AuthDataRoot::from([0x42; 32]);
assert_ne!(
forged_auth_data_root, auth_data_root,
"the forged root must differ from the block body root"
);
let error = block_commitment_is_valid_for_chain_history(
child,
&network,
&history_tree,
Some(forged_auth_data_root),
)
.expect_err("a forged precomputed auth data root must fail the commitment check");
let forged_hash_block_commitments = ChainHistoryBlockTxAuthCommitmentHash::from_commitments(
&history_tree
.hash()
.expect("NU5 parent history tree has a root"),
&forged_auth_data_root,
);
assert!(matches!(
error,
ValidateContextError::InvalidBlockCommitment(
CommitmentError::InvalidChainHistoryBlockTxAuthCommitment { actual, expected }
) if actual == block_commitment
&& expected == <[u8; 32]>::from(forged_hash_block_commitments)
));
}
#[test]
fn header_daa_accepts_valid_threshold_with_full_context() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let previous_block_height = block::Height(99);
let candidate_time = DateTime::from_timestamp(15_000, 0).expect("test timestamp is in-range");
let relevant_headers = daa_context(&network, previous_block_height, candidate_time);
let expected = AdjustedDifficulty::new_from_header_time(
candidate_time,
previous_block_height,
&network,
relevant_headers.clone(),
)
.expect("the test supplies the complete late-chain difficulty context")
.expected_difficulty_threshold();
let mut candidate = *zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes")
.header
.as_ref();
candidate.time = candidate_time;
candidate.difficulty_threshold = expected;
header_is_valid_for_recent_chain(
&candidate,
previous_block_height,
&network,
relevant_headers,
)
.expect("expected DAA threshold is accepted");
}
#[test]
fn difficulty_context_follows_the_candidate_height() {
let _init_guard = zakura_test::init();
const NU7: u32 = 1_000;
let network = Network::new_regtest(
zakura_chain::parameters::testnet::ConfiguredActivationHeights {
nu7: Some(NU7),
..Default::default()
}
.into(),
);
let ancestors = 0..difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN * 2;
for (candidate_height, expected) in [
(NU7 - 1, difficulty::POW_ADJUSTMENT_BLOCK_SPAN),
(NU7, difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN),
(NU7 + 1, difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN),
] {
assert_eq!(
difficulty_context(&network, block::Height(candidate_height), ancestors.clone()).len(),
expected,
"candidate height {candidate_height}",
);
}
}
#[test]
fn block_daa_enforces_the_post_nu7_context() {
let _init_guard = zakura_test::init();
let network = Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
blossom: Some(1),
nu7: Some(347_400),
..Default::default()
})
.expect("activation heights are valid")
.clear_funding_streams()
.to_network()
.expect("configured testnet is valid");
assert!(!network.disable_pow());
let candidate = zakura_test::vectors::BLOCK_MAINNET_347499_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 347499 deserializes");
let parent_height = block::Height(347_498);
let candidate_height = parent_height.next().expect("test height is valid");
assert!(
difficulty::pow_adjustment_block_span_for_height(&network, candidate_height)
> difficulty::POW_ADJUSTMENT_BLOCK_SPAN
);
let candidate_time =
DateTime::from_timestamp(2_000_000_000, 0).expect("test timestamp is in-range");
let target_bits = [0x1e0ffff0, 0x1e0e0000, 0x1e0c8000, 0x1e0b4000, 0x1e0a2000];
let time_steps = [19, 31, 23, 29, 17, 37, 21];
let compact = |bits: u32| {
CompactDifficulty::from_bytes_in_display_order(&bits.to_be_bytes())
.expect("the fixed compact target is valid")
};
let mut time = candidate_time;
let context: Vec<_> = (0..difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN)
.map(|index| {
time -= Duration::seconds(time_steps[index % time_steps.len()]);
(compact(target_bits[index % target_bits.len()]), time)
})
.collect();
let expected = compact(0x1e0cd7fd);
let relevant_headers: Vec<block::Header> = context
.iter()
.map(|(threshold, time)| {
let mut header = *candidate.header;
header.difficulty_threshold = *threshold;
header.time = *time;
header
})
.collect();
let mut candidate = (*candidate).clone();
let mut header = *candidate.header;
header.time = candidate_time;
header.difficulty_threshold = expected;
candidate.header = Arc::new(header);
block_is_valid_for_recent_chain_data(
&candidate,
candidate_height,
&network,
Some(block::Height(0)),
Some(parent_height),
relevant_headers.clone(),
)
.expect("the independently calculated post-NU7 threshold is accepted");
let mut wrong_window_candidate = candidate.clone();
let mut header = *wrong_window_candidate.header;
header.difficulty_threshold = compact(0x1e0af369);
wrong_window_candidate.header = Arc::new(header);
assert!(matches!(
block_is_valid_for_recent_chain_data(
&wrong_window_candidate,
candidate_height,
&network,
Some(block::Height(0)),
Some(parent_height),
relevant_headers,
),
Err(ValidateContextError::InvalidDifficultyThreshold {
difficulty_threshold,
expected_difficulty,
}) if difficulty_threshold == compact(0x1e0af369) && expected_difficulty == expected
));
}
#[test]
fn header_daa_rejects_bad_threshold_with_full_context() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let previous_block_height = block::Height(99);
let candidate_time = DateTime::from_timestamp(15_000, 0).expect("test timestamp is in-range");
let relevant_headers = daa_context(&network, previous_block_height, candidate_time);
let mut candidate = *zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes")
.header
.as_ref();
candidate.time = candidate_time;
candidate.difficulty_threshold = network.target_difficulty_limit().to_compact();
header_is_valid_for_recent_chain(
&candidate,
previous_block_height,
&network,
relevant_headers,
)
.expect_err("unexpected DAA threshold is rejected");
}
#[test]
fn height_one_header_skips_max_time_limit_but_later_mainnet_headers_do_not() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let genesis = zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("genesis block deserializes");
let block1 = zakura_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 1 deserializes");
let mut candidate = *block1.header;
candidate.time = genesis.header.time + Duration::hours(24);
let context = [(genesis.header.difficulty_threshold, genesis.header.time)];
header_is_valid_for_recent_chain(&candidate, block::Height(0), &network, context)
.expect("height 1 is outside the Mainnet max-time consensus rule");
let block2 = zakura_test::vectors::BLOCK_MAINNET_2_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("block 2 deserializes");
let mut candidate = *block2.header;
candidate.time = block1.header.time + Duration::hours(24);
let context = [
(block1.header.difficulty_threshold, block1.header.time),
(genesis.header.difficulty_threshold, genesis.header.time),
];
assert!(matches!(
header_is_valid_for_recent_chain(&candidate, block::Height(1), &network, context),
Err(ValidateContextError::TimeTooLate { .. })
));
}
#[test]
fn short_context_early_height_uses_pow_limit_threshold() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let genesis = zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.expect("genesis block deserializes");
let candidate_time =
genesis.header.time + NetworkUpgrade::target_spacing_for_height(&network, block::Height(1));
let context = [(genesis.header.difficulty_threshold, genesis.header.time)];
let expected = difficulty::AdjustedDifficulty::new_from_header_time(
candidate_time,
block::Height(0),
&network,
context,
)
.expect("height one requires exactly one predecessor")
.expected_difficulty_threshold();
assert_eq!(expected, network.target_difficulty_limit().to_compact());
}
#[test]
fn full_context_at_averaging_window_height_uses_pow_limit_threshold() {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let previous_block_height = block::Height(
u32::try_from(POW_AVERAGING_WINDOW - 1).expect("averaging window fits in u32"),
);
let candidate_height = previous_block_height
.next()
.expect("test candidate height is valid");
let candidate_time = DateTime::from_timestamp(10_000, 0).expect("test timestamp is in-range");
let target_spacing = NetworkUpgrade::target_spacing_for_height(&network, candidate_height);
let difficulty = network.target_difficulty_limit().to_compact();
let context = (0..POW_AVERAGING_WINDOW).map(|offset| {
let offset = i32::try_from(offset + 1).expect("test offset fits in i32");
(difficulty, candidate_time - target_spacing * offset)
});
let expected = difficulty::AdjustedDifficulty::new_from_header_time(
candidate_time,
previous_block_height,
&network,
context,
)
.expect("the test supplies the complete late-chain difficulty context")
.expected_difficulty_threshold();
assert_eq!(expected, difficulty);
}
fn daa_context(
network: &Network,
previous_block_height: block::Height,
candidate_time: DateTime<chrono::Utc>,
) -> Vec<(
zakura_chain::work::difficulty::CompactDifficulty,
DateTime<chrono::Utc>,
)> {
let candidate_height = previous_block_height
.next()
.expect("test candidate height is valid");
let target_spacing = NetworkUpgrade::target_spacing_for_height(network, candidate_height);
let difficulty = network.target_difficulty_limit().to_compact();
let context_len = usize::try_from(candidate_height.0)
.expect("test candidate height fits in usize")
.min(difficulty::MAX_POW_ADJUSTMENT_BLOCK_SPAN);
(0..context_len)
.map(|offset| {
let offset = i32::try_from(offset + 1).expect("test offset fits in i32");
(difficulty, candidate_time - target_spacing * offset)
})
.collect()
}