use std::{
collections::{HashMap, HashSet},
io::{Cursor, Write},
sync::Arc,
};
use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc};
use halo2::pasta::pallas;
use crate::{
amount::{Amount, NonNegative, MAX_MONEY},
block::{
serialize::MAX_BLOCK_BYTES, Block, BlockTimeError, Commitment::*, Hash, Header, Height,
},
ironwood, orchard,
parameters::{Network, NetworkUpgrade::*},
sapling,
serialization::{
sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
},
transaction::{arbitrary::v5_transactions, LockTime, Transaction},
transparent,
};
use super::generate;
#[test]
fn blockheaderhash_debug() {
let _init_guard = zakura_test::init();
let preimage = b"foo bar baz";
let mut sha_writer = sha256d::Writer::default();
let _ = sha_writer.write_all(preimage);
let hash = Hash(sha_writer.finish());
assert_eq!(
format!("{hash:?}"),
"block::Hash(\"3166411bd5343e0b284a108f39a929fbbb62619784f8c6dafe520703b5b446bf\")"
);
}
#[test]
fn blockheaderhash_from_blockheader() {
let _init_guard = zakura_test::init();
let (blockheader, _blockheader_bytes) = generate::block_header();
let hash = Hash::from(&blockheader);
assert_eq!(
format!("{hash:?}"),
"block::Hash(\"d1d6974bbe1d4d127c889119b2fc05724c67588dc72708839727586b8c2bc939\")"
);
let mut bytes = Cursor::new(Vec::new());
blockheader
.zcash_serialize(&mut bytes)
.expect("these bytes to serialize from a blockheader without issue");
bytes.set_position(0);
let other_header = bytes
.zcash_deserialize_into()
.expect("these bytes to deserialize into a blockheader without issue");
assert_eq!(blockheader, other_header);
}
#[test]
fn chain_value_pool_change_propagates_transaction_value_balance_errors() {
let _init_guard = zakura_test::init();
let max_money: Amount<NonNegative> = MAX_MONEY.try_into().expect("MAX_MONEY is a valid amount");
let coinbase = Transaction::V1 {
inputs: vec![transparent::Input::Coinbase {
height: Height(1),
data: vec![],
sequence: 0xFFFF_FFFF,
}],
outputs: vec![
transparent::Output::new(max_money, transparent::Script::new(&[])),
transparent::Output::new(max_money, transparent::Script::new(&[])),
],
lock_time: LockTime::unlocked(),
};
let utxos = HashMap::new();
assert!(
coinbase.value_balance(&utxos).is_err(),
"transaction-level value balance should reject an output sum above MAX_MONEY"
);
let header: Header = zakura_test::vectors::DUMMY_HEADER
.zcash_deserialize_into()
.expect("dummy header should deserialize");
let block = Block {
header: Arc::new(header),
transactions: vec![Arc::new(coinbase)],
};
assert!(
block.chain_value_pool_change(&utxos, None).is_err(),
"block-level aggregation should propagate transaction value-balance errors"
);
}
#[test]
fn ironwood_block_accessors_preserve_pool_and_wire_order() {
let _init_guard = zakura_test::init();
let mut orchard_data = Network::iter()
.flat_map(|network| v5_transactions(network.block_iter()))
.find_map(|transaction| transaction.orchard_shielded_data().cloned())
.expect("test vectors include an Orchard transaction");
let mut ironwood_data = orchard_data.clone();
let mut orchard_actions = orchard_data.actions.as_slice().to_vec();
orchard_actions[0].action.nullifier =
orchard::Nullifier::try_from([0; 32]).expect("zero is a valid Pallas base field");
orchard_actions[0].action.cm_x = pallas::Base::from(0);
orchard_data.actions = orchard_actions
.try_into()
.expect("the test bundle has at least one action");
let mut one = [0; 32];
one[0] = 1;
let mut ironwood_actions = ironwood_data.actions.as_slice().to_vec();
ironwood_actions[0].action.nullifier =
ironwood::Nullifier::try_from(one).expect("one is a valid Pallas base field");
ironwood_actions[0].action.cm_x = pallas::Base::from(1);
ironwood_data.actions = ironwood_actions
.try_into()
.expect("the test bundle has at least one action");
let make_transaction =
|orchard_shielded_data: orchard::ShieldedData,
ironwood_shielded_data: ironwood::ShieldedData| {
Arc::new(Transaction::V6 {
network_upgrade: Nu6_3,
lock_time: LockTime::unlocked(),
expiry_height: Height(1),
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
orchard_shielded_data: Some(orchard_shielded_data),
ironwood_shielded_data: Some(ironwood_shielded_data),
})
};
let header: Header = zakura_test::vectors::DUMMY_HEADER
.zcash_deserialize_into()
.expect("dummy header should deserialize");
let block = Block {
header: Arc::new(header),
transactions: vec![
make_transaction(orchard_data.clone(), ironwood_data.clone()),
make_transaction(ironwood_data, orchard_data),
],
};
let expected_orchard_nullifiers: Vec<_> = block
.transactions
.iter()
.flat_map(|transaction| transaction.orchard_nullifiers().copied())
.collect();
let expected_ironwood_nullifiers: Vec<_> = block
.transactions
.iter()
.flat_map(|transaction| transaction.ironwood_nullifiers().copied())
.collect();
assert_ne!(expected_orchard_nullifiers, expected_ironwood_nullifiers);
assert_eq!(
block.orchard_nullifiers().copied().collect::<Vec<_>>(),
expected_orchard_nullifiers
);
assert_eq!(
block.ironwood_nullifiers().copied().collect::<Vec<_>>(),
expected_ironwood_nullifiers
);
let expected_orchard_commitments: Vec<_> = block
.transactions
.iter()
.flat_map(|transaction| transaction.orchard_note_commitments().copied())
.collect();
let expected_ironwood_commitments: Vec<_> = block
.transactions
.iter()
.flat_map(|transaction| transaction.ironwood_note_commitments().copied())
.collect();
assert_ne!(expected_orchard_commitments, expected_ironwood_commitments);
assert_eq!(
block
.orchard_note_commitments()
.copied()
.collect::<Vec<_>>(),
expected_orchard_commitments
);
assert_eq!(
block
.ironwood_note_commitments()
.copied()
.collect::<Vec<_>>(),
expected_ironwood_commitments
);
}
#[test]
fn ironwood_transaction_count_is_pool_specific_and_counts_bundles() {
let _init_guard = zakura_test::init();
let orchard_data = Network::iter()
.flat_map(|network| v5_transactions(network.block_iter()))
.find_map(|transaction| transaction.orchard_shielded_data().cloned())
.expect("test vectors include an Orchard transaction");
let mut ironwood_data = orchard_data.clone();
let mut ironwood_actions = ironwood_data.actions.as_slice().to_vec();
ironwood_actions.push(ironwood_actions[0].clone());
ironwood_data.actions = ironwood_actions
.try_into()
.expect("duplicating an action keeps the bundle non-empty");
let ironwood_action_count = ironwood_data.actions.len();
let make_transaction =
|orchard_shielded_data: Option<orchard::ShieldedData>,
ironwood_shielded_data: Option<ironwood::ShieldedData>| {
Arc::new(Transaction::V6 {
network_upgrade: Nu6_3,
lock_time: LockTime::unlocked(),
expiry_height: Height(1),
inputs: Vec::new(),
outputs: Vec::new(),
sapling_shielded_data: None,
orchard_shielded_data,
ironwood_shielded_data,
})
};
let header: Header = zakura_test::vectors::DUMMY_HEADER
.zcash_deserialize_into()
.expect("dummy header should deserialize");
let block = Block {
header: Arc::new(header),
transactions: vec![
make_transaction(Some(orchard_data.clone()), None),
make_transaction(Some(orchard_data), None),
make_transaction(None, Some(ironwood_data)),
],
};
assert_eq!(block.orchard_transactions_count(), 2);
assert_eq!(block.ironwood_transactions_count(), 1);
assert!(ironwood_action_count > 1);
assert_eq!(
block
.transactions
.iter()
.flat_map(|transaction| transaction.ironwood_actions())
.count(),
ironwood_action_count,
"the history field counts a multi-action bundle as one transaction"
);
}
#[test]
fn nu6_3_uses_post_nu5_block_commitment_format() {
let _init_guard = zakura_test::init();
let regtest = Network::new_regtest(
crate::parameters::testnet::ConfiguredActivationHeights {
nu6_3: Some(1),
..Default::default()
}
.into(),
);
let commitment_bytes = [0x5a; 32];
for network in [Network::Mainnet, Network::new_default_testnet(), regtest] {
let activation_height = Nu6_3
.activation_height(&network)
.expect("NU6.3 is configured on this test network");
let commitment =
super::super::Commitment::from_bytes(commitment_bytes, &network, activation_height)
.expect("post-NU5 commitment bytes have no structural restriction");
let ChainHistoryBlockTxAuthCommitment(commitment) = commitment else {
panic!("NU6.3 must retain the post-NU5 block commitment format");
};
assert_eq!(<[u8; 32]>::from(commitment), commitment_bytes);
}
}
#[test]
fn attributed_memory_size_counts_capacity_and_nested_transparent_allocations() {
fn allocation_bytes<T>(capacity: usize) -> u64 {
u64::try_from(capacity).unwrap() * u64::try_from(std::mem::size_of::<T>()).unwrap()
}
fn block_with_capacities(
transaction_capacity: usize,
input_capacity: usize,
coinbase_data_capacity: usize,
output_capacity: usize,
) -> (Block, u64) {
let coinbase_data = Vec::with_capacity(coinbase_data_capacity);
let coinbase_data_bytes = allocation_bytes::<u8>(coinbase_data.capacity());
let mut inputs = Vec::with_capacity(input_capacity);
inputs.push(transparent::Input::Coinbase {
height: Height(1),
data: coinbase_data,
sequence: u32::MAX,
});
let input_bytes = allocation_bytes::<transparent::Input>(inputs.capacity());
let mut outputs = Vec::with_capacity(output_capacity);
outputs.push(transparent::Output::new(
Amount::zero(),
transparent::Script::new(&[]),
));
let output_bytes = allocation_bytes::<transparent::Output>(outputs.capacity());
let transaction = Transaction::V1 {
inputs,
outputs,
lock_time: LockTime::unlocked(),
};
let mut transactions = Vec::with_capacity(transaction_capacity);
transactions.push(Arc::new(transaction));
let transaction_pointer_bytes =
allocation_bytes::<Arc<Transaction>>(transactions.capacity());
let header: Header = zakura_test::vectors::DUMMY_HEADER
.zcash_deserialize_into()
.expect("dummy header should deserialize");
let block = Block {
header: Arc::new(header),
transactions,
};
let expected = allocation_bytes::<Block>(1)
+ allocation_bytes::<Header>(1)
+ transaction_pointer_bytes
+ allocation_bytes::<Transaction>(1)
+ input_bytes
+ coinbase_data_bytes
+ output_bytes;
(block, expected)
}
let (compact, compact_expected) = block_with_capacities(1, 1, 0, 1);
let (reserved, reserved_expected) = block_with_capacities(4, 3, 17, 5);
assert_eq!(compact, reserved);
assert_eq!(compact.attributed_memory_size_bytes(), compact_expected);
assert_eq!(reserved.attributed_memory_size_bytes(), reserved_expected);
assert!(reserved.attributed_memory_size_bytes() > compact.attributed_memory_size_bytes());
}
#[test]
fn blockheader_serialization() {
let _init_guard = zakura_test::init();
const BLOCK_HEADER_LENGTH: usize = crate::work::equihash::Solution::INPUT_LENGTH
+ 32
+ 3
+ crate::work::equihash::SOLUTION_SIZE;
for block in zakura_test::vectors::BLOCKS.iter() {
let header_bytes = &block[..BLOCK_HEADER_LENGTH];
let mut header = header_bytes
.zcash_deserialize_into::<Header>()
.expect("blockheader test vector should deserialize");
let _serialized_header = header
.zcash_serialize_to_vec()
.expect("blockheader test vector should serialize");
let header_bytes = [&[255; 4], &header_bytes[4..]].concat();
let deserialization_err = header_bytes
.zcash_deserialize_into::<Header>()
.expect_err("blockheader test vector should fail to deserialize");
let SerializationError::Parse(err_msg) = deserialization_err else {
panic!("SerializationError variant should be Parse")
};
assert_eq!(err_msg, "high bit was set in version field");
let header_bytes = [&[0; 4], &header_bytes[4..]].concat();
let deserialization_err = header_bytes
.zcash_deserialize_into::<Header>()
.expect_err("blockheader test vector should fail to deserialize");
let SerializationError::Parse(err_msg) = deserialization_err else {
panic!("SerializationError variant should be Parse")
};
assert_eq!(err_msg, "version must be at least 4");
header.version = u32::MAX;
let serialization_err = header
.zcash_serialize_to_vec()
.expect_err("blockheader test vector with modified version should fail to serialize");
assert_eq!(
serialization_err.kind(),
std::io::ErrorKind::Other,
"error kind should be Other"
);
let err_msg = serialization_err
.into_inner()
.expect("there should be an inner error");
assert_eq!(err_msg.to_string(), "high bit was set in version field");
}
}
#[test]
fn round_trip_blocks() {
let _init_guard = zakura_test::init();
zakura_test::vectors::BLOCK_MAINNET_434873_BYTES
.zcash_deserialize_into::<Block>()
.expect("bad version block test vector should deserialize");
for block_bytes in zakura_test::vectors::BLOCKS.iter() {
let block = block_bytes
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
let round_trip_bytes = block
.zcash_serialize_to_vec()
.expect("vec serialization is infallible");
assert_eq!(&round_trip_bytes[..], *block_bytes);
}
}
#[test]
fn coinbase_parsing_rejects_above_0x80() {
let _init_guard = zakura_test::init();
zakura_test::vectors::BAD_BLOCK_MAINNET_202_BYTES
.zcash_deserialize_into::<Block>()
.expect_err("parsing fails");
}
#[test]
fn block_test_vectors_unique() {
let _init_guard = zakura_test::init();
let block_count = zakura_test::vectors::BLOCKS.len();
let block_hashes: HashSet<_> = zakura_test::vectors::BLOCKS
.iter()
.map(|b| {
b.zcash_deserialize_into::<Block>()
.expect("block is structurally valid")
.hash()
})
.collect();
assert_eq!(
block_count,
block_hashes.len(),
"block test vectors must be unique"
);
}
#[test]
fn block_test_vectors() {
let _init_guard = zakura_test::init();
for net in Network::iter() {
let sapling_anchors = net.sapling_anchors();
let orchard_anchors = net.orchard_anchors();
for (&height, block) in net.block_iter() {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
assert_eq!(
block.coinbase_height().expect("block height is valid").0,
height,
"deserialized height must match BTreeMap key height"
);
if height
>= Sapling
.activation_height(&net)
.expect("activation height")
.0
{
assert!(
sapling_anchors.contains_key(&height),
"post-sapling block test vectors must have matching sapling root test vectors: \
missing {net} {height}"
);
}
if height >= Nu5.activation_height(&net).expect("activation height").0 {
assert!(
orchard_anchors.contains_key(&height),
"post-nu5 block test vectors must have matching orchard root test vectors: \
missing {net} {height}"
);
}
}
}
}
#[test]
fn block_commitment() {
let _init_guard = zakura_test::init();
for net in Network::iter() {
let sapling_anchors = net.sapling_anchors();
for (height, block) in net.block_iter() {
if let FinalSaplingRoot(anchor) = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid")
.commitment(&net)
.expect("unexpected structurally invalid block commitment at {net} {height}")
{
let expected_anchor = *sapling_anchors
.get(height)
.expect("unexpected missing final sapling root test vector");
assert_eq!(
anchor,
sapling::tree::Root::try_from(*expected_anchor).unwrap(),
"unexpected invalid final sapling root commitment at {net} {height}"
);
}
}
}
}
#[test]
fn block_limits_multi_tx() {
let _init_guard = zakura_test::init();
let mut block = generate::large_multi_transaction_block();
let mut data = Vec::new();
block
.zcash_serialize(&mut data)
.expect("block should serialize as we are not limiting generation yet");
assert!(data.len() <= MAX_BLOCK_BYTES as usize);
let block2 = Block::zcash_deserialize(&data[..])
.expect("block should deserialize as we are just below limit");
assert_eq!(block, block2);
block = generate::oversized_multi_transaction_block();
let mut data = Vec::new();
block
.zcash_serialize(&mut data)
.expect("block should serialize as we are not limiting generation yet");
assert!(data.len() > MAX_BLOCK_BYTES as usize);
Block::zcash_deserialize(&data[..]).expect_err("block should not deserialize");
}
#[test]
fn block_limits_single_tx() {
let _init_guard = zakura_test::init();
let mut block = generate::large_single_transaction_block_many_inputs();
let mut data = Vec::new();
block
.zcash_serialize(&mut data)
.expect("block should serialize as we are not limiting generation yet");
assert!(data.len() <= MAX_BLOCK_BYTES as usize);
Block::zcash_deserialize(&data[..])
.expect("block should deserialize as we are just below limit");
block = generate::oversized_single_transaction_block_many_inputs();
let mut data = Vec::new();
block
.zcash_serialize(&mut data)
.expect("block should serialize as we are not limiting generation yet");
assert!(data.len() > MAX_BLOCK_BYTES as usize);
Block::zcash_deserialize(&data[..]).expect_err("block should not deserialize");
}
fn node_time_check(
block_header_time: DateTime<Utc>,
now: DateTime<Utc>,
) -> Result<(), BlockTimeError> {
let (mut header, _header_bytes) = generate::block_header();
header.time = block_header_time;
header.time_is_valid_at(now, &Height(0), &Hash([0; 32]))
}
#[test]
fn time_check_now() {
let _init_guard = zakura_test::init();
let now = Utc::now();
let three_hours_in_the_past = now - Duration::hours(3);
let two_hours_in_the_future = now + Duration::hours(2);
let two_hours_and_one_second_in_the_future = now + Duration::hours(2) + Duration::seconds(1);
node_time_check(now, now).expect("the current time should be valid as a block header time");
node_time_check(three_hours_in_the_past, now)
.expect("a past time should be valid as a block header time");
node_time_check(two_hours_in_the_future, now)
.expect("2 hours in the future should be valid as a block header time");
node_time_check(two_hours_and_one_second_in_the_future, now)
.expect_err("2 hours and 1 second in the future should be invalid as a block header time");
node_time_check(now, three_hours_in_the_past)
.expect_err("3 hours in the future should be invalid as a block header time");
node_time_check(now, two_hours_in_the_future)
.expect("2 hours in the past should be valid as a block header time");
node_time_check(now, two_hours_and_one_second_in_the_future)
.expect("2 hours and 1 second in the past should be valid as a block header time");
}
static BLOCK_HEADER_VALID_TIMESTAMPS: &[i64] = &[
i64::MIN,
i64::MIN + 1,
(i32::MIN as i64) - 1,
(i32::MIN as i64),
(i32::MIN as i64) + 1,
-1,
0,
1,
LockTime::MIN_TIMESTAMP - 1,
LockTime::MIN_TIMESTAMP,
LockTime::MIN_TIMESTAMP + 1,
];
static BLOCK_HEADER_INVALID_TIMESTAMPS: &[i64] = &[
(i32::MAX as i64) - 1,
(i32::MAX as i64),
(i32::MAX as i64) + 1,
LockTime::MAX_TIMESTAMP - 1,
LockTime::MAX_TIMESTAMP,
LockTime::MAX_TIMESTAMP + 1,
i64::MAX - 1,
i64::MAX,
];
#[test]
fn time_check_fixed() {
let _init_guard = zakura_test::init();
let now = Utc::now();
for valid_timestamp in BLOCK_HEADER_VALID_TIMESTAMPS {
let block_header_time = match Utc.timestamp_opt(*valid_timestamp, 0) {
LocalResult::Single(time) => time,
LocalResult::None => {
continue;
}
LocalResult::Ambiguous(_, _) => {
unreachable!();
}
};
node_time_check(block_header_time, now)
.expect("the time should be valid as a block header time");
node_time_check(now, block_header_time)
.expect_err("the inverse comparison should be invalid");
}
for invalid_timestamp in BLOCK_HEADER_INVALID_TIMESTAMPS {
let block_header_time = match Utc.timestamp_opt(*invalid_timestamp, 0) {
LocalResult::Single(time) => time,
LocalResult::None => {
continue;
}
LocalResult::Ambiguous(_, _) => {
unreachable!();
}
};
node_time_check(block_header_time, now)
.expect_err("the time should be invalid as a block header time");
node_time_check(now, block_header_time).expect("the inverse comparison should be valid");
}
}