#![allow(clippy::unwrap_in_result)]
use color_eyre::eyre::{eyre, Report};
use once_cell::sync::Lazy;
use tower::{buffer::Buffer, util::BoxService};
use zebra_chain::{
amount::{DeferredPoolBalanceChange, MAX_MONEY},
block::{
tests::generate::{
large_multi_transaction_block, large_single_transaction_block_many_inputs,
},
Block, Height,
},
parameters::NetworkUpgrade,
serialization::{ZcashDeserialize, ZcashDeserializeInto},
transaction::{arbitrary::transaction_to_fake_v5, LockTime, Transaction},
work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY},
};
use zebra_script::Sigops;
use zebra_test::transcript::{ExpectedTranscriptError, Transcript};
use crate::transaction;
use super::check::{miner_fees_are_valid, subsidy_is_valid};
use super::*;
mod nu7;
static VALID_BLOCK_TRANSCRIPT: Lazy<Vec<(Request, Result<block::Hash, ExpectedTranscriptError>)>> =
Lazy::new(|| {
let block: Arc<_> =
Block::zcash_deserialize(&zebra_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(&zebra_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(&zebra_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(&zebra_test::vectors::DUMMY_HEADER[..]).unwrap();
let block1 = Block {
header: header.into(),
transactions: Vec::new(),
};
let mut transactions = Vec::new();
let tx = zebra_test::vectors::DUMMY_TX1
.zcash_deserialize_into()
.unwrap();
transactions.push(tx);
let block2 = Block {
header: header.into(),
transactions,
};
let mut block3 =
Block::zcash_deserialize(&zebra_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 = zebra_test::init();
let network = Network::Mainnet;
let state_service = zebra_state::init_test(&network).await;
let transaction = transaction::BlockTxVerifier::new(&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 = zebra_test::init();
let block_iter = zebra_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 = zebra_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 = zebra_test::init();
let block =
Arc::<Block>::zcash_deserialize(&zebra_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(&zebra_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(&zebra_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 = zebra_test::init();
let block_iter = zebra_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)
.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 = zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
height,
&network,
Amount::zero(),
)
.expect("valid block subsidy");
subsidy_is_valid(&block, &network, expected_block_subsidy)
.expect("subsidies should pass for this block");
}
}
Ok(())
}
#[test]
fn coinbase_validation_failure() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block =
Arc::<Block>::zcash_deserialize(&zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
Amount::zero(),
)
.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 = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err();
let expected = BlockError::from(SubsidyError::NoCoinbase);
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
Amount::zero(),
)
.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 = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err();
let expected = BlockError::from(SubsidyError::NoCoinbase);
assert_eq!(expected, result);
let block =
Arc::<Block>::zcash_deserialize(&zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
Amount::zero(),
)
.expect("valid block subsidy");
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 = zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
height,
&network,
Amount::zero(),
)
.expect("valid block subsidy");
let result = subsidy_is_valid(&block, &network, expected_block_subsidy);
assert!(result.is_ok());
}
}
Ok(())
}
#[test]
fn funding_stream_validation_failure() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block =
Arc::<Block>::zcash_deserialize(&zebra_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::test_v4(
transaction.inputs().to_vec(),
vec![output],
transaction.lock_time().unwrap_or_else(LockTime::unlocked),
Height(0),
)
})
.unwrap();
let transactions: Vec<Arc<zebra_chain::transaction::Transaction>> = vec![Arc::new(tx)];
let block = Block {
header: block.header.clone(),
transactions,
};
let expected_block_subsidy =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
block
.coinbase_height()
.expect("block should have coinbase height"),
&network,
Amount::zero(),
)
.expect("valid block subsidy");
let result = subsidy_is_valid(&block, &network, expected_block_subsidy);
let expected = Err(BlockError::from(SubsidyError::FundingStreamNotFound));
assert_eq!(expected, result);
Ok(())
}
#[test]
fn miner_fees_validation_success() -> Result<(), Report> {
let _init_guard = zebra_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 =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
height,
&network,
Amount::zero(),
)?;
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!(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 = zebra_test::init();
let network = Network::Mainnet;
let block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..])
.expect("block should deserialize");
let height = block.coinbase_height().expect("valid coinbase height");
let expected_block_subsidy =
zebra_chain::parameters::subsidy::block_subsidy_with_parent_nsm_value_balance(
height,
&network,
Amount::zero(),
)?;
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!(
miner_fees_are_valid(
check::coinbase_is_first(&block)?.as_ref(),
height,
Amount::zero(),
expected_block_subsidy,
deferred_pool_balance_change,
&network
),
Err(BlockError::from(SubsidyError::InvalidMinerFees))
);
Ok(())
}
#[test]
fn zip235_coinbase_cant_claim_the_nsm_fee_contribution() -> Result<(), Report> {
use zebra_chain::{
amount::{Amount, NonNegative},
parameters::testnet::{self, ConfiguredActivationHeights},
transparent,
};
let _init_guard = zebra_test::init();
const NU7_HEIGHT: u32 = 10;
let network = testnet::Parameters::build()
.with_slow_start_interval(Height::MIN)
.with_activation_heights(ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(1),
nu6: Some(1),
nu6_3: Some(1),
nu7: Some(NU7_HEIGHT),
..Default::default()
})?
.clear_funding_streams()
.to_network()?;
let fees = Amount::<NonNegative>::try_from(1_001)?;
let miner_fees_are_valid = |height: Height, claimed_fees: i64| {
let block_subsidy = Amount::<NonNegative>::try_from(625_000_000).expect("valid amount");
let claimed_fees = Amount::<NonNegative>::try_from(claimed_fees).expect("valid amount");
let coinbase = Transaction::test_v4(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
vec![transparent::Output {
value: (block_subsidy + claimed_fees).expect("valid amount"),
lock_script: transparent::Script::new(&[]),
}],
LockTime::unlocked(),
height,
);
check::miner_fees_are_valid(
&coinbase,
height,
fees,
block_subsidy,
DeferredPoolBalanceChange::zero(),
&network,
)
};
let invalid_miner_fees = Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees,
)));
let before_nu7 = Height(NU7_HEIGHT - 1);
assert_eq!(miner_fees_are_valid(before_nu7, 1_001), Ok(()));
assert_eq!(miner_fees_are_valid(before_nu7, 401), invalid_miner_fees);
for height in [Height(NU7_HEIGHT), Height(NU7_HEIGHT + 1)] {
assert_eq!(miner_fees_are_valid(height, 401), Ok(()));
for claimed_fees in [400, 402, 1_001] {
assert_eq!(
miner_fees_are_valid(height, claimed_fees),
invalid_miner_fees
);
}
}
Ok(())
}
#[test]
fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let block_iter = zebra_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 = zebra_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 = zebra_test::init();
let network = Network::Mainnet;
let mut block = Block::zcash_deserialize(&zebra_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 = zebra_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 = zebra_test::init();
for block in zebra_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 = zebra_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: zebra_chain::block::merkle::Root([0; 32]),
expected: zebra_chain::block::merkle::Root([1; 32]),
}
.misbehavior_score(),
100
);
assert_eq!(
BlockError::WrongTransactionConsensusBranchId.misbehavior_score(),
100
);
assert_eq!(
BlockError::MissingHeight(zebra_chain::block::Hash([0; 32])).misbehavior_score(),
100
);
assert_eq!(BlockError::DuplicateTransaction.misbehavior_score(), 100);
}
#[test]
fn duplicate_transaction_scores_misbehavior_through_both_verifier_paths() {
use crate::{checkpoint::VerifyCheckpointError, router::RouterError};
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let mut block = Block::zcash_deserialize(&zebra_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 block_error = check::merkle_root_validity(&network, &block, &transaction_hashes)
.expect_err("duplicate transaction hashes must be rejected");
assert_eq!(block_error, BlockError::DuplicateTransaction);
let verify_block_error = VerifyBlockError::from(block_error.clone());
assert_eq!(verify_block_error.misbehavior_score(), 100);
assert_eq!(
RouterError::from(verify_block_error).misbehavior_score(),
100
);
let checkpoint_error = VerifyCheckpointError::from(block_error);
assert_eq!(checkpoint_error.misbehavior_score(), 100);
assert_eq!(RouterError::from(checkpoint_error).misbehavior_score(), 100);
}
#[test]
fn verify_block_error_misbehavior_scores() {
let dup_err = zebra_state::CommitBlockError::Duplicate {
hash_or_height: None,
location: zebra_state::KnownBlock::BestChain,
};
assert_eq!(VerifyBlockError::Commit(dup_err).misbehavior_score(), 0);
}
#[test]
fn state_commit_duplicate_errors_are_duplicate_requests() {
let duplicate = zs::CommitBlockError::Duplicate {
hash_or_height: None,
location: zs::KnownBlock::BestChain,
};
let source: BoxError = Box::new(zs::CommitSemanticallyVerifiedError::from(duplicate));
let err = map_commit_error(source, 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.misbehavior_score(), 0);
}
#[tokio::test]
async fn nsm_block_verifier_leaves_subsidy_checks_to_the_state() -> Result<(), Report> {
use zebra_chain::{
amount::{Amount, NonNegative},
parameters::testnet::{self, ConfiguredActivationHeights},
transaction::UnminedTxId,
transparent,
};
use zebra_test::mock_service::MockService;
let _init_guard = zebra_test::init();
const NU7_HEIGHT: u32 = 10;
let network = |deployment_height: Height| -> Result<Network, Report> {
let params = testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(1),
nu6: Some(1),
nu6_3: Some(1),
nu7: Some(NU7_HEIGHT),
..Default::default()
})?
.with_slow_start_interval(Height::MIN)
.with_nsm_reissuance_height(deployment_height)
.with_disable_pow(true)
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new());
Ok(params.to_network()?)
};
let block_at = |height: Height| -> Arc<Block> {
let mut block =
Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1_BYTES[..]).unwrap();
let coinbase = Transaction::test_v4(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
vec![transparent::Output {
value: Amount::<NonNegative>::try_from(1).unwrap(),
lock_script: transparent::Script::new(&[]),
}],
LockTime::unlocked(),
height,
);
block.transactions = vec![Arc::new(coinbase)];
let merkle_root = block.transactions.iter().map(|tx| tx.hash()).collect();
Arc::make_mut(&mut block.header).merkle_root = merkle_root;
Arc::new(block)
};
let deployed_at_nu7 = network(Height(NU7_HEIGHT))?;
let deployed_later = network(Height(NU7_HEIGHT + 2))?;
for (network, height, is_active) in [
(&deployed_at_nu7, Height(NU7_HEIGHT - 1), false),
(&deployed_at_nu7, Height(NU7_HEIGHT), true),
(&deployed_later, Height(NU7_HEIGHT), false),
(&deployed_later, Height(NU7_HEIGHT + 1), false),
(&deployed_later, Height(NU7_HEIGHT + 2), true),
] {
let block = block_at(height);
let hash = block.hash();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
let mut transaction_verifier: MockService<_, _, _, BoxError> =
MockService::build().for_unit_tests();
let verifier =
SemanticBlockVerifier::new(network, state.clone(), transaction_verifier.clone());
let verify = tokio::spawn(verifier.oneshot(Request::Commit(block.clone())));
state
.expect_request(zs::Request::KnownBlock(hash))
.await
.respond(zs::Response::KnownBlock(None));
transaction_verifier
.expect_request_that(|_| true)
.await
.respond(transaction::BlockResponse {
tx_id: UnminedTxId::from(block.transactions[0].as_ref()),
miner_fee: None,
sigops: 0,
});
if is_active {
state
.expect_request_that(|request| {
matches!(request, zs::Request::CommitSemanticallyVerifiedBlock(_))
})
.await
.respond(zs::Response::Committed(hash));
assert_eq!(
verify
.await?
.expect("the block verifier leaves the subsidy checks to the state"),
hash
);
} else {
let err = verify
.await?
.expect_err("the coinbase pays the wrong subsidy");
assert!(
matches!(
err,
VerifyBlockError::Block {
source: BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees
))
}
),
"unexpected error: {err:?}"
);
state.expect_no_requests().await;
}
}
Ok(())
}
#[test]
fn state_auth_commitment_errors_score_the_serving_peer() {
let commitment_error =
zebra_chain::block::CommitmentError::InvalidChainHistoryBlockTxAuthCommitment {
expected: [1; 32],
actual: [2; 32],
};
let commit_error = zs::CommitBlockError::ValidateContextError(Box::new(
zs::ValidateContextError::InvalidBlockCommitment(commitment_error),
));
let source: BoxError = Box::new(zs::CommitSemanticallyVerifiedError::from(commit_error));
let err = map_commit_error(source, 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(),
"a forged body is not a duplicate request, so it must not be treated as benign"
);
assert_eq!(
err.misbehavior_score(),
100,
"an authorizing-data commitment mismatch must score the peer that served the body"
);
let router_err = crate::router::RouterError::from(err);
assert_eq!(
router_err.misbehavior_score(),
100,
"the score must reach the syncer through RouterError"
);
}
fn nu7_network(nu7_height: u32) -> Network {
Network::new_regtest(
zebra_chain::parameters::testnet::ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(2),
nu6: Some(3),
nu6_1: Some(4),
nu6_2: Some(5),
nu6_3: Some(6),
nu7: Some(nu7_height),
..Default::default()
}
.into(),
)
}
#[test]
fn zip_218_shielded_action_limits() {
use zebra_chain::transaction::arbitrary::{fake_bundle_for_branch, fake_v6_transaction};
let _init_guard = zebra_test::init();
let network = nu7_network(1_000);
let mut block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1046400_BYTES[..])
.expect("block test vector is valid");
let orchard_tx =
zebra_chain::transaction::arbitrary::v5_transactions(Network::Mainnet.block_iter())
.find(|transaction| {
transaction.orchard_actions().count() == 2
&& transaction.joinsplit_count() == 0
&& transaction.sapling_spends_count() == 0
&& transaction.sapling_outputs().next().is_none()
})
.map(Arc::new)
.expect("a V5 transaction whose only shielded data is Orchard actions");
let ironwood_tx = Arc::new(fake_v6_transaction(
NetworkUpgrade::Nu7,
None,
Some(
fake_bundle_for_branch(
zcash_protocol::consensus::BranchId::Nu7,
orchard::ValuePool::Ironwood,
2,
1,
)
.expect("NU7 supports Ironwood"),
),
));
let sapling_tx = zebra_chain::transaction::arbitrary::transactions_from_blocks(
Network::Mainnet.block_iter(),
)
.map(|(_, transaction)| transaction)
.find(|transaction| {
transaction.orchard_actions().count() == 0
&& transaction.joinsplit_count() == 0
&& transaction.sapling_spends_count() + transaction.sapling_outputs().count() > 0
})
.expect("a transaction whose only shielded data is Sapling");
let orchard_actions = orchard_tx.orchard_actions().count();
let sapling_io = sapling_tx.sapling_spends_count() + sapling_tx.sapling_outputs().count();
let orchard_limit_count = ORCHARD_BLOCK_ACTION_LIMIT / orchard_actions;
let orchard_count = (ORCHARD_BLOCK_ACTION_LIMIT * 3 / 5).div_ceil(orchard_actions);
let sapling_count = (SAPLING_BLOCK_IO_LIMIT * 3 / 5).div_ceil(sapling_io);
assert!(orchard_count * orchard_actions <= ORCHARD_BLOCK_ACTION_LIMIT);
assert!(sapling_count * sapling_io <= SAPLING_BLOCK_IO_LIMIT);
assert!(orchard_count * orchard_actions + sapling_count * sapling_io > GLOBAL_SHIELDED_BUDGET);
for (orchard_count, ironwood_count, sapling_count, height, valid) in [
(orchard_limit_count + 1, 0, 0, Height(999), true),
(orchard_limit_count + 1, 0, 0, Height(1_000), false),
(orchard_limit_count, 0, 0, Height(1_000), true),
(0, 166, 0, Height(999), true),
(0, 166, 0, Height(1_000), false),
(0, 165, 0, Height(1_000), true),
(100, 100, 0, Height(999), true),
(100, 100, 0, Height(1_000), false),
(orchard_count, 0, sapling_count, Height(999), true),
(orchard_count, 0, sapling_count, Height(1_000), false),
(orchard_count, 0, 0, Height(1_000), true),
] {
block.transactions = std::iter::repeat_n(orchard_tx.clone(), orchard_count)
.chain(std::iter::repeat_n(ironwood_tx.clone(), ironwood_count))
.chain(std::iter::repeat_n(sapling_tx.clone(), sapling_count))
.collect();
let result =
check::shielded_action_limits_are_valid(&block, &network, height, block.hash());
if valid {
assert_eq!(
result,
Ok(()),
"counts {orchard_count}/{ironwood_count}/{sapling_count}, {height:?}"
);
} else {
assert!(
matches!(result, Err(BlockError::TooManyShieldedActions { .. })),
"counts {orchard_count}/{ironwood_count}/{sapling_count}, {height:?}: {result:?}",
);
}
}
}
#[tokio::test]
async fn shielded_limits_precede_coinbase_recovery() {
use zebra_chain::{transaction::arbitrary::fake_bundle_for_branch, transparent::Input};
let _init_guard = zebra_test::init();
let network = nu7_network(1_000);
let height = Height(1_001);
let bundle = fake_bundle_for_branch(
zcash_protocol::consensus::BranchId::Nu7,
orchard::ValuePool::Ironwood,
ORCHARD_BLOCK_ACTION_LIMIT + 2,
1,
)
.expect("NU7 supports Ironwood");
let transaction = Transaction::test_v6_with_bundles(
NetworkUpgrade::Nu7,
vec![Input::Coinbase {
height,
data: vec![],
sequence: u32::MAX,
}],
vec![],
LockTime::unlocked(),
height,
None,
Some(bundle),
);
assert!(transaction.has_shielded_outputs());
assert_eq!(
tx::check::coinbase_outputs_are_decryptable(&transaction, &network, height),
Err(TransactionError::CoinbaseOutputsNotDecryptable),
);
let mut block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1046400_BYTES[..])
.expect("block test vector is valid");
block.transactions = vec![Arc::new(transaction)];
Arc::make_mut(&mut block.header).merkle_root = block
.transactions
.iter()
.map(|transaction| transaction.hash())
.collect();
let state = tower::service_fn(|request| async move {
assert!(matches!(request, zs::Request::KnownBlock(_)));
Ok::<_, BoxError>(zs::Response::KnownBlock(None))
});
let transactions = tower::service_fn(|_| async {
unreachable!("an oversized block must not queue transaction verification")
});
let result = tokio::time::timeout(
std::time::Duration::from_secs(10),
SemanticBlockVerifier::new(&network, state, transactions)
.oneshot(Request::CheckProposal(Arc::new(block))),
)
.await
.expect("cheap shielded limit rejection must not stall");
assert!(
matches!(
result,
Err(VerifyBlockError::Block {
source: BlockError::TooManyShieldedActions { .. },
})
),
"{result:?}",
);
}
#[test]
fn shielded_action_counts_limits() {
let _init_guard = zebra_test::init();
for (orchard_actions, ironwood_actions, sapling_io, joinsplits, expected_limit) in [
(330, 0, 0, 0, None),
(331, 0, 0, 0, Some(330)),
(0, 330, 0, 0, None),
(0, 331, 0, 0, Some(330)),
(0, 0, 300, 0, None),
(0, 0, 301, 0, Some(300)),
(0, 0, 0, 0, None),
(0, 0, 0, 1, Some(0)),
(200, 200, 0, 0, Some(330)),
(200, 0, 200, 0, Some(330)),
] {
let counts = ShieldedActionCounts {
orchard_actions,
ironwood_actions,
sapling_io,
joinsplits,
};
assert_eq!(
counts.exceeded_limit().map(|(_, _, limit)| limit),
expected_limit,
"{counts:?}",
);
}
assert_eq!(
ShieldedActionCounts {
joinsplits: 1,
..Default::default()
}
.shielded_cost(),
2,
);
let saturated = ShieldedActionCounts {
orchard_actions: usize::MAX,
ironwood_actions: usize::MAX,
sapling_io: usize::MAX,
joinsplits: usize::MAX,
};
assert_eq!(
saturated.saturating_add(ShieldedActionCounts {
orchard_actions: 1,
ironwood_actions: 1,
sapling_io: 1,
joinsplits: 1,
}),
saturated,
"counts must not wrap below a limit",
);
assert_eq!(saturated.shielded_cost(), usize::MAX);
}