#![allow(clippy::unwrap_in_result)]
use color_eyre::eyre::{eyre, Report};
use once_cell::sync::Lazy;
use tower::{buffer::Buffer, util::BoxService, ServiceExt};
use zakura_chain::{amount::NegativeAllowed, ironwood};
use zakura_chain::{
amount::{DeferredPoolBalanceChange, MAX_MONEY},
block::{
tests::generate::{
large_multi_transaction_block, large_single_transaction_block_many_inputs,
},
Block, Height,
},
local_genesis::generate_local_testnet_with_funded_keys,
orchard,
parameters::{
subsidy::block_subsidy,
testnet::{ConfiguredActivationHeights, Parameters},
NetworkUpgrade,
},
primitives::Halo2Proof,
serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::{
arbitrary::{transaction_to_fake_v5, v5_transactions},
LockTime, Transaction,
},
work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY},
};
use zakura_script::Sigops;
use zakura_test::transcript::{ExpectedTranscriptError, Transcript};
use crate::{block::check::subsidy_is_valid, transaction};
use super::*;
static VALID_BLOCK_TRANSCRIPT: Lazy<Vec<(Request, Result<block::Hash, ExpectedTranscriptError>)>> =
Lazy::new(|| {
let block: Arc<_> =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..])
.unwrap()
.into();
let hash = Ok(block.as_ref().into());
vec![(Request::Commit(block), hash)]
});
static INVALID_TIME_BLOCK_TRANSCRIPT: Lazy<
Vec<(Request, Result<block::Hash, ExpectedTranscriptError>)>,
> = Lazy::new(|| {
let mut block: Block =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]).unwrap();
let three_hours_in_the_future = Utc::now()
.checked_add_signed(chrono::Duration::hours(3))
.ok_or_else(|| eyre!("overflow when calculating 3 hours in the future"))
.unwrap();
Arc::make_mut(&mut block.header).time = three_hours_in_the_future;
vec![(
Request::Commit(Arc::new(block)),
Err(ExpectedTranscriptError::Any),
)]
});
static INVALID_HEADER_SOLUTION_TRANSCRIPT: Lazy<
Vec<(Request, Result<block::Hash, ExpectedTranscriptError>)>,
> = Lazy::new(|| {
let mut block: Block =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]).unwrap();
Arc::make_mut(&mut block.header).nonce = [0; 32].into();
vec![(
Request::Commit(Arc::new(block)),
Err(ExpectedTranscriptError::Any),
)]
});
static INVALID_COINBASE_TRANSCRIPT: Lazy<
Vec<(Request, Result<block::Hash, ExpectedTranscriptError>)>,
> = Lazy::new(|| {
let header = block::Header::zcash_deserialize(&zakura_test::vectors::DUMMY_HEADER[..]).unwrap();
let block1 = Block {
header: header.into(),
transactions: Vec::new(),
};
let mut transactions = Vec::new();
let tx = zakura_test::vectors::DUMMY_TX1
.zcash_deserialize_into()
.unwrap();
transactions.push(tx);
let block2 = Block {
header: header.into(),
transactions,
};
let mut block3 =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]).unwrap();
assert_eq!(block3.transactions.len(), 1);
let coinbase_transaction = block3.transactions.first().unwrap().clone();
block3.transactions.push(coinbase_transaction);
assert_eq!(block3.transactions.len(), 2);
vec![
(
Request::Commit(Arc::new(block1)),
Err(ExpectedTranscriptError::Any),
),
(
Request::Commit(Arc::new(block2)),
Err(ExpectedTranscriptError::Any),
),
(
Request::Commit(Arc::new(block3)),
Err(ExpectedTranscriptError::Any),
),
]
});
#[allow(dead_code)]
async fn check_transcripts_test() -> Result<(), Report> {
check_transcripts().await
}
#[allow(dead_code)]
#[spandoc::spandoc]
async fn check_transcripts() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let state_service = zakura_state::init_test(&network).await;
let transaction = transaction::Verifier::new_for_tests(&network, state_service.clone());
let transaction = Buffer::new(BoxService::new(transaction), 1);
let block_verifier = Buffer::new(
SemanticBlockVerifier::new(&network, state_service.clone(), transaction),
1,
);
for transcript_data in &[
&VALID_BLOCK_TRANSCRIPT,
&INVALID_TIME_BLOCK_TRANSCRIPT,
&INVALID_HEADER_SOLUTION_TRANSCRIPT,
&INVALID_COINBASE_TRANSCRIPT,
] {
let transcript = Transcript::from(transcript_data.iter().cloned());
transcript.check(block_verifier.clone()).await.unwrap();
}
Ok(())
}
#[test]
fn coinbase_is_first_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let block_iter = zakura_test::vectors::BLOCKS.iter();
for block in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
check::coinbase_is_first(&block)
.expect("the coinbase in a historical block should be valid");
}
Ok(())
}
#[test]
fn difficulty_is_valid_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
difficulty_is_valid_for_network(network)?;
}
Ok(())
}
fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (&height, block) in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
check::difficulty_is_valid(&block.header, &network, &Height(height), &block.hash())
.expect("the difficulty from a historical block should be valid");
}
Ok(())
}
#[test]
fn difficulty_validation_failure() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_415000_BYTES[..])
.expect("block should deserialize");
let mut block = Arc::try_unwrap(block).expect("block should unwrap");
let height = block.coinbase_height().unwrap();
let hash = block.hash();
Arc::make_mut(&mut block.header).difficulty_threshold = INVALID_COMPACT_DIFFICULTY;
let result =
check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err();
let expected = BlockError::InvalidDifficulty(height, hash);
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_TESTNET_925483_BYTES[..])
.expect("block should deserialize");
let block = Arc::try_unwrap(block).expect("block should unwrap");
let height = block.coinbase_height().unwrap();
let hash = block.hash();
let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap();
let result =
check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err();
let expected = BlockError::TargetDifficultyLimit(
height,
hash,
difficulty_threshold,
Network::Mainnet,
Network::Mainnet.target_difficulty_limit(),
);
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_415000_BYTES[..])
.expect("block should deserialize");
let block = Arc::try_unwrap(block).expect("block should unwrap");
let height = block.coinbase_height().unwrap();
let bad_hash = block::Hash([0xff; 32]);
let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap();
let result = check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &bad_hash)
.unwrap_err();
let expected =
BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold, Network::Mainnet);
assert_eq!(expected, result);
Ok(())
}
#[test]
fn equihash_is_valid_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let block_iter = zakura_test::vectors::BLOCKS.iter();
for block in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
check::equihash_solution_is_valid(&block.header, &Network::Mainnet)
.expect("the equihash solution from a historical block should be valid");
}
Ok(())
}
#[test]
fn subsidy_is_valid_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
subsidy_is_valid_for_network(network)?;
}
Ok(())
}
fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (&height, block) in block_iter {
let height = block::Height(height);
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
let canopy_activation_height = NetworkUpgrade::Canopy
.activation_height(&network)
.expect("Canopy activation height is known");
if height >= canopy_activation_height {
let expected_block_subsidy =
zakura_chain::parameters::subsidy::block_subsidy(height, &network)
.expect("valid block subsidy");
check::subsidy_is_valid(&block, &network, expected_block_subsidy)
.expect("subsidies should pass for this block");
}
}
Ok(())
}
#[test]
fn local_genesis_nu6_3_activation_has_satisfiable_lockbox_rule() -> Result<(), Report> {
let generated = generate_local_testnet_with_funded_keys(
vec!["alice".to_string(), "bob".to_string()],
Default::default(),
)
.map_err(|error| eyre!(error.to_string()))?;
let network = generated.network;
let height = NetworkUpgrade::Nu6_3
.activation_height(&network)
.expect("the default local network activates NU6.3");
let lockbox_disbursements = network.lockbox_disbursements(height);
let [(lockbox_address, lockbox_amount)] = lockbox_disbursements.as_slice() else {
panic!("the local network configures one lockbox marker output");
};
let coinbase = Transaction::V5 {
network_upgrade: NetworkUpgrade::Nu6_3,
lock_time: LockTime::unlocked(),
expiry_height: height,
inputs: vec![zakura_chain::transparent::Input::Coinbase {
height,
data: b"local activation".to_vec(),
sequence: 0,
}],
outputs: vec![zakura_chain::transparent::Output::new(
*lockbox_amount,
lockbox_address.script(),
)],
sapling_shielded_data: None,
orchard_shielded_data: None,
};
let block = Block {
header: generated
.blocks
.last()
.expect("the generated chain contains genesis")
.header
.clone(),
transactions: vec![Arc::new(coinbase)],
};
let expected_block_subsidy =
block_subsidy(height, &network).expect("the local activation height has a block subsidy");
assert!(!expected_block_subsidy.is_zero());
assert_eq!(
subsidy_is_valid(&block, &network, expected_block_subsidy)?,
DeferredPoolBalanceChange::zero()
);
Ok(())
}
#[test]
fn coinbase_validation_failure() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_1046400_BYTES[..])
.expect("block should deserialize");
let mut block = Arc::try_unwrap(block).expect("block should unwrap");
let expected_block_subsidy = zakura_chain::parameters::subsidy::block_subsidy(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
)
.expect("valid block subsidy");
block.transactions.remove(0);
let result = check::coinbase_is_first(&block).unwrap_err();
let expected = BlockError::NoTransactions;
assert_eq!(expected, result);
let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase));
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_1046401_BYTES[..])
.expect("block should deserialize");
let mut block = Arc::try_unwrap(block).expect("block should unwrap");
let expected_block_subsidy = zakura_chain::parameters::subsidy::block_subsidy(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
)
.expect("valid block subsidy");
block.transactions.remove(0);
let result = check::coinbase_is_first(&block).unwrap_err();
let expected = BlockError::Transaction(TransactionError::CoinbasePosition);
assert_eq!(expected, result);
let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase));
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_1180900_BYTES[..])
.expect("block should deserialize");
let mut block = Arc::try_unwrap(block).expect("block should unwrap");
block.transactions.push(
block
.transactions
.first()
.expect("block has coinbase")
.clone(),
);
let result = check::coinbase_is_first(&block).unwrap_err();
let expected = BlockError::Transaction(TransactionError::CoinbaseAfterFirst);
assert_eq!(expected, result);
let expected_block_subsidy = zakura_chain::parameters::subsidy::block_subsidy(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
)
.expect("valid block subsidy");
check::subsidy_is_valid(&block, &network, expected_block_subsidy)
.expect("subsidy does not check for extra coinbase transactions");
Ok(())
}
#[test]
fn funding_stream_validation() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
funding_stream_validation_for_network(network)?;
}
Ok(())
}
fn funding_stream_validation_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
let canopy_activation_height = NetworkUpgrade::Canopy
.activation_height(&network)
.expect("Canopy activation height is known");
for (&height, block) in block_iter {
let height = Height(height);
if height >= canopy_activation_height {
let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize");
let expected_block_subsidy =
zakura_chain::parameters::subsidy::block_subsidy(height, &network)
.expect("valid block subsidy");
let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy);
assert!(result.is_ok());
}
}
Ok(())
}
#[test]
fn funding_stream_validation_failure() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let block =
Arc::<Block>::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_1046400_BYTES[..])
.expect("block should deserialize");
let tx = block
.transactions
.first()
.map(|transaction| {
let mut output = transaction.outputs()[0].clone();
output.value = Amount::try_from(i32::MAX).unwrap();
Transaction::V4 {
inputs: transaction.inputs().to_vec(),
outputs: vec![output],
lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked),
expiry_height: Height(0),
joinsplit_data: None,
sapling_shielded_data: None,
}
})
.unwrap();
let transactions: Vec<Arc<zakura_chain::transaction::Transaction>> = vec![Arc::new(tx)];
let block = Block {
header: block.header.clone(),
transactions,
};
let expected_block_subsidy = zakura_chain::parameters::subsidy::block_subsidy(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
)
.expect("valid block subsidy");
let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy);
let expected = Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::FundingStreamNotFound,
)));
assert_eq!(expected, result);
Ok(())
}
#[test]
fn miner_fees_validation_success() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
miner_fees_validation_for_network(network)?;
}
Ok(())
}
fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (&height, block) in block_iter {
let height = Height(height);
if height > network.slow_start_shift() {
let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize");
let coinbase_tx = check::coinbase_is_first(&block)?;
let expected_block_subsidy = block_subsidy(height, &network)?;
let deferred_pool_balance_change =
match NetworkUpgrade::Canopy.activation_height(&network) {
Some(activation_height) if height >= activation_height => {
subsidy_is_valid(&block, &network, expected_block_subsidy)?
}
_other => DeferredPoolBalanceChange::zero(),
};
assert!(check::miner_fees_are_valid(
&coinbase_tx,
height,
Amount::try_from(MAX_MONEY / 2).unwrap(),
expected_block_subsidy,
deferred_pool_balance_change,
&network,
)
.is_ok(),);
}
}
Ok(())
}
#[test]
fn miner_fees_validation_failure() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let block = Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_347499_BYTES[..])
.expect("block should deserialize");
let height = block.coinbase_height().expect("valid coinbase height");
let expected_block_subsidy = block_subsidy(height, &network)?;
let deferred_pool_balance_change = match NetworkUpgrade::Canopy.activation_height(&network) {
Some(activation_height) if height >= activation_height => {
subsidy_is_valid(&block, &network, expected_block_subsidy)?
}
_other => DeferredPoolBalanceChange::zero(),
};
assert_eq!(
check::miner_fees_are_valid(
check::coinbase_is_first(&block)?.as_ref(),
height,
Amount::zero(),
expected_block_subsidy,
deferred_pool_balance_change,
&network
),
Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees,
)))
);
Ok(())
}
#[derive(Copy, Clone)]
struct LibrustzcashConversionFailure {
name: &'static str,
network_upgrade: NetworkUpgrade,
rk: Option<[u8; 32]>,
}
#[tokio::test]
async fn block_rejects_transactions_failing_librustzcash_conversion() {
let _init_guard = zakura_test::init();
let cases = [
LibrustzcashConversionFailure {
name: "unrecognized consensus branch ID",
network_upgrade: NetworkUpgrade::Nu7,
rk: None,
},
LibrustzcashConversionFailure {
name: "incorrect curve point encoding",
network_upgrade: NetworkUpgrade::Nu5,
rk: Some([0xff; 32]),
},
LibrustzcashConversionFailure {
name: "rk=identity",
network_upgrade: NetworkUpgrade::Nu5,
rk: Some([0; 32]),
},
];
for case in cases {
let network = librustzcash_conversion_test_network(case.network_upgrade);
let block = block_with_librustzcash_conversion_failure(case, &network);
let state_service = zakura_state::init_test(&network).await;
let transaction = transaction::Verifier::new_for_tests(&network, state_service.clone());
let transaction = Buffer::new(BoxService::new(transaction), 1);
let block_verifier =
SemanticBlockVerifier::new(&network, state_service.clone(), transaction);
let result = block_verifier
.oneshot(Request::CheckProposal(Arc::new(block)))
.await;
assert!(
matches!(
result,
Err(VerifyBlockError::Transaction(
TransactionError::UnsupportedByNetworkUpgrade(5, nu),
)) if nu == case.network_upgrade
),
"{}: expected librustzcash conversion failure, got {result:?}",
case.name,
);
}
}
fn librustzcash_conversion_test_network(network_upgrade: NetworkUpgrade) -> Network {
let genesis_block =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..])
.expect("genesis block should deserialize");
let target_difficulty_limit = genesis_block
.header
.difficulty_threshold
.to_expanded()
.expect("genesis difficulty threshold should be valid");
let activation_heights = match network_upgrade {
NetworkUpgrade::Nu5 => ConfiguredActivationHeights {
nu5: Some(1),
..Default::default()
},
NetworkUpgrade::Nu7 => ConfiguredActivationHeights {
nu7: Some(1),
..Default::default()
},
_ => panic!("test cases only use NU5 and NU7"),
};
Parameters::build()
.with_activation_heights(activation_heights)
.expect("failed to set test activation heights")
.clear_funding_streams()
.with_slow_start_interval(Height::MIN)
.with_disable_pow(true)
.disable_temporary_orchard_disabling_soft_fork()
.with_target_difficulty_limit(target_difficulty_limit)
.expect("failed to set target difficulty limit")
.to_network()
.expect("failed to build configured network")
}
fn block_with_librustzcash_conversion_failure(
case: LibrustzcashConversionFailure,
network: &Network,
) -> Block {
let height = Height(1);
let mut block =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..])
.expect("genesis block should deserialize");
block.transactions = vec![
Arc::new(v5_coinbase_transaction(
case.network_upgrade,
height,
network,
)),
Arc::new(failing_librustzcash_v5_transaction(case, height)),
];
Arc::make_mut(&mut block.header).merkle_root = block.transactions.iter().collect();
block
}
fn v5_coinbase_transaction(
network_upgrade: NetworkUpgrade,
height: Height,
network: &Network,
) -> Transaction {
let mut outputs = vec![transparent::Output {
value: block_subsidy(height, network).expect("valid test block subsidy"),
lock_script: transparent::Script::new(&[0]),
}];
outputs.extend(
network
.lockbox_disbursements(height)
.into_iter()
.map(|(address, value)| transparent::Output {
value,
lock_script: address.script(),
}),
);
Transaction::V5 {
network_upgrade,
lock_time: LockTime::unlocked(),
expiry_height: height,
inputs: vec![transparent::Input::Coinbase {
height,
data: vec![],
sequence: u32::MAX,
}],
outputs,
sapling_shielded_data: None,
orchard_shielded_data: None,
}
}
fn failing_librustzcash_v5_transaction(
case: LibrustzcashConversionFailure,
height: Height,
) -> Transaction {
let mut shielded_data = orchard_fixture();
shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS;
shielded_data.value_balance = Amount::zero();
shielded_data.proof = Halo2Proof(vec![
0;
::orchard::Proof::expected_proof_size(
shielded_data.actions.len()
)
]);
if let Some(rk) = case.rk {
shielded_data
.actions
.iter_mut()
.next()
.expect("Orchard fixture has at least one action")
.action
.rk = rk.into();
}
Transaction::V5 {
network_upgrade: case.network_upgrade,
lock_time: LockTime::unlocked(),
expiry_height: height,
inputs: vec![],
outputs: vec![],
sapling_shielded_data: None,
orchard_shielded_data: Some(shielded_data),
}
}
fn orchard_fixture() -> orchard::ShieldedData {
v5_transactions(Network::new_default_testnet().block_iter())
.find_map(|transaction| transaction.orchard_shielded_data().cloned())
.expect("test vectors include a transaction with Orchard shielded data")
}
#[test]
fn miner_fees_validation_includes_ironwood_balance() {
let _init_guard = zakura_test::init();
let network = Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
nu6_3: Some(1),
..Default::default()
})
.expect("failed to set NU6.3 activation height")
.clear_funding_streams()
.to_network()
.expect("failed to build configured network");
let height = Height(1);
let output_value = Amount::try_from(10).expect("valid test amount");
let coinbase_tx = Transaction::V6 {
network_upgrade: NetworkUpgrade::Nu6_3,
lock_time: LockTime::Height(Height(0)),
expiry_height: height,
inputs: vec![transparent::Input::Coinbase {
height,
data: vec![],
sequence: u32::MAX,
}],
outputs: vec![transparent::Output {
value: output_value,
lock_script: transparent::Script::new(&[0]),
}],
sapling_shielded_data: None,
orchard_shielded_data: None,
ironwood_shielded_data: Some(ironwood_shielded_data(1)),
};
assert_eq!(
check::miner_fees_are_valid(
&coinbase_tx,
height,
Amount::zero(),
output_value,
DeferredPoolBalanceChange::zero(),
&network,
),
Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees,
)))
);
assert_eq!(
check::miner_fees_are_valid(
&coinbase_tx,
height,
Amount::zero(),
Amount::try_from(9).expect("valid test amount"),
DeferredPoolBalanceChange::zero(),
&network,
),
Ok(())
);
}
fn ironwood_shielded_data(value_balance: i64) -> ironwood::ShieldedData {
let mut shielded_data = orchard_fixture();
shielded_data.value_balance =
Amount::<NegativeAllowed>::try_from(value_balance).expect("valid test amount");
shielded_data
}
#[test]
fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let block_iter = zakura_test::vectors::BLOCKS.iter();
let now = Utc::now();
for block in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
check::time_is_valid_at(
&block.header,
now,
&block
.coinbase_height()
.expect("block test vector height should be valid"),
&block.hash(),
)
.expect("the header time from a historical block should be valid, based on the test machine's local clock. Hint: check the test machine's time, date, and timezone.");
}
Ok(())
}
#[test]
fn merkle_root_is_valid() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
merkle_root_is_valid_for_network(network)?;
}
for network in Network::iter() {
merkle_root_fake_v5_for_network(network)?;
}
Ok(())
}
fn merkle_root_is_valid_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (_height, block) in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
let transaction_hashes = block
.transactions
.iter()
.map(|tx| tx.hash())
.collect::<Vec<_>>();
check::merkle_root_validity(&network, &block, &transaction_hashes)
.expect("merkle root should be valid for this block");
}
Ok(())
}
fn merkle_root_fake_v5_for_network(network: Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (height, block) in block_iter {
let mut block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
if *height
< NetworkUpgrade::Overwinter
.activation_height(&network)
.expect("a valid overwinter activation height")
.0
{
continue;
}
let transactions: Vec<Arc<Transaction>> = block
.transactions
.iter()
.map(AsRef::as_ref)
.map(|t| transaction_to_fake_v5(t, &network, Height(*height)))
.map(Into::into)
.collect();
block.transactions = transactions;
let transaction_hashes = block
.transactions
.iter()
.map(|tx| tx.hash())
.collect::<Vec<_>>();
Arc::make_mut(&mut block.header).merkle_root = transaction_hashes.iter().cloned().collect();
check::merkle_root_validity(&network, &block, &transaction_hashes)
.expect("merkle root should be valid for this block");
}
Ok(())
}
#[test]
fn merkle_root_validity_rejects_duplicate_transaction_hash() -> Result<(), Report> {
let _init_guard = zakura_test::init();
let network = Network::Mainnet;
let mut block =
Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_1180900_BYTES[..])
.expect("block should deserialize");
let duplicate = block
.transactions
.first()
.expect("block has coinbase")
.clone();
block.transactions.push(duplicate);
let transaction_hashes: Vec<_> = block.transactions.iter().map(|tx| tx.hash()).collect();
Arc::make_mut(&mut block.header).merkle_root = transaction_hashes.iter().cloned().collect();
let result = check::merkle_root_validity(&network, &block, &transaction_hashes);
assert_eq!(result, Err(BlockError::DuplicateTransaction));
Ok(())
}
#[test]
fn legacy_sigops_count_for_large_generated_blocks() {
let _init_guard = zakura_test::init();
let block = large_single_transaction_block_many_inputs();
let mut legacy_sigop_count = 0;
for tx in block.transactions {
let tx_sigop_count = tx.sigops().expect("unexpected invalid sigop count");
assert_eq!(tx_sigop_count, 0);
legacy_sigop_count += tx_sigop_count;
}
assert_eq!(legacy_sigop_count, 0);
let block = large_multi_transaction_block();
let mut sigops = 0;
for tx in block.transactions {
let tx_sigop_count = tx.sigops().expect("unexpected invalid sigop count");
assert_eq!(tx_sigop_count, 1);
sigops += tx_sigop_count;
}
assert!(sigops > MAX_BLOCK_SIGOPS);
}
#[test]
fn legacy_sigops_count_for_historic_blocks() {
let _init_guard = zakura_test::init();
for block in zakura_test::vectors::BLOCKS.iter() {
let mut sigops = 0;
let block: Block = block
.zcash_deserialize_into()
.expect("block test vector is valid");
for tx in block.transactions {
sigops += tx.sigops().expect("unexpected invalid sigop count");
}
assert!(sigops <= MAX_BLOCK_SIGOPS);
}
}
#[test]
fn transaction_expiration_height_validation() -> Result<(), Report> {
let _init_guard = zakura_test::init();
for network in Network::iter() {
transaction_expiration_height_for_network(&network)?;
}
Ok(())
}
fn transaction_expiration_height_for_network(network: &Network) -> Result<(), Report> {
let block_iter = network.block_iter();
for (&height, block) in block_iter {
let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize");
for (n, transaction) in block.transactions.iter().enumerate() {
if n == 0 {
let result = transaction::check::coinbase_expiry_height(
&Height(height),
transaction,
network,
);
assert!(result.is_ok());
} else {
let result =
transaction::check::non_coinbase_expiry_height(&Height(height), transaction);
assert!(result.is_ok());
}
}
}
Ok(())
}
#[test]
fn block_error_misbehavior_scores() {
use crate::error::BlockError;
assert_eq!(BlockError::NoTransactions.misbehavior_score(), 100);
assert_eq!(
BlockError::BadMerkleRoot {
actual: zakura_chain::block::merkle::Root([0; 32]),
expected: zakura_chain::block::merkle::Root([1; 32]),
}
.misbehavior_score(),
100
);
assert_eq!(
BlockError::WrongTransactionConsensusBranchId.misbehavior_score(),
100
);
assert_eq!(
BlockError::MissingHeight(zakura_chain::block::Hash([0; 32])).misbehavior_score(),
100
);
}
#[test]
fn verify_block_error_misbehavior_scores() {
let dup_err = zakura_state::CommitBlockError::Duplicate {
hash_or_height: None,
location: zakura_state::KnownBlock::BestChain,
};
assert_eq!(VerifyBlockError::Commit(dup_err).misbehavior_score(), 0);
}
#[test]
fn state_commit_duplicate_errors_are_duplicate_requests() {
let duplicate = zakura_state::CommitBlockError::Duplicate {
hash_or_height: None,
location: zakura_state::KnownBlock::BestChain,
};
let source: BoxError = Box::new(zakura_state::CommitSemanticallyVerifiedError::from(
duplicate,
));
let err = map_commit_error(source, zakura_chain::block::Hash([0; 32]));
assert!(
matches!(err, VerifyBlockError::Commit(_)),
"state commit errors must be unwrapped into VerifyBlockError::Commit, got: {err:?}"
);
assert!(err.is_duplicate_request());
assert_eq!(
err.duplicate_location(),
Some(&zakura_state::KnownBlock::BestChain)
);
assert_eq!(err.misbehavior_score(), 0);
}