use std::{
collections::{HashMap, HashSet},
io::{Cursor, Write},
sync::Arc,
};
use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc};
use crate::{
amount::{Amount, DeferredPoolBalanceChange, NonNegative, MAX_MONEY},
block::{
serialize::MAX_BLOCK_BYTES, Block, BlockTimeError, Commitment::*, Hash, Header, Height,
},
parameters::{Network, NetworkUpgrade::*},
sapling,
serialization::{
sha256d, SerializationError, ZcashDeserialize, ZcashDeserializeInto, ZcashSerialize,
},
transaction::{LockTime, Transaction},
transparent,
value_balance::ValueBalance,
};
use super::generate;
#[test]
fn blockheaderhash_debug() {
let _init_guard = zebra_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 = zebra_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 = zebra_test::init();
let max_money: Amount<NonNegative> = MAX_MONEY.try_into().expect("MAX_MONEY is a valid amount");
let coinbase = Transaction::test_v1(
vec![transparent::Input::Coinbase {
height: Height(1),
data: vec![0],
sequence: 0xFFFF_FFFF,
}],
vec![
transparent::Output::new(max_money, transparent::Script::new(&[])),
transparent::Output::new(max_money, transparent::Script::new(&[])),
],
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 = zebra_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,
DeferredPoolBalanceChange::zero(),
&Network::Mainnet,
ValueBalance::zero()
)
.is_err(),
"block-level aggregation should propagate transaction value-balance errors"
);
}
#[test]
fn genesis_transparent_outputs_are_not_issued_supply() {
use crate::parameters::testnet::{ConfiguredActivationHeights, Parameters};
let _init_guard = zebra_test::init();
for bytes in [
&*zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES,
&*zebra_test::vectors::BLOCK_TESTNET_GENESIS_BYTES,
] {
let genesis: Block = bytes.zcash_deserialize_into().unwrap();
assert!(genesis.transactions[0]
.outputs()
.iter()
.all(|output| output.value.is_zero()));
}
let network = Parameters::build()
.with_slow_start_interval(Height::MIN)
.with_activation_heights(ConfiguredActivationHeights {
nu7: Some(1),
..Default::default()
})
.unwrap()
.clear_funding_streams()
.with_lockbox_disbursements(vec![])
.to_network()
.unwrap();
let genesis: Block = zebra_test::vectors::BLOCK_TESTNET_GENESIS_BYTES
.zcash_deserialize_into()
.unwrap();
for value in [0, 1, MAX_MONEY] {
let mut genesis = genesis.clone();
let transaction = Arc::make_mut(&mut genesis.transactions[0]);
let mut outputs = transaction.outputs();
outputs[0].value = value.try_into().unwrap();
transaction.set_outputs(outputs);
for network in [&network, &Network::Mainnet] {
let change = genesis
.chain_value_pool_change(
&HashMap::new(),
DeferredPoolBalanceChange::zero(),
network,
ValueBalance::zero(),
)
.unwrap();
let pools = ValueBalance::zero()
.add_chain_value_pool_change(change)
.unwrap();
assert!(pools.transparent_amount().is_zero());
assert!(pools.nsm_amount().is_zero());
assert!(pools.total().unwrap().is_zero());
}
}
}
#[test]
fn chain_value_pool_change_accrues_the_nsm_reserve() {
use crate::{
parameters::testnet::ConfiguredActivationHeights, transaction::Hash as TransactionHash,
transparent::OutPoint,
};
let _init_guard = zebra_test::init();
let height = Height(1_000);
let spent_value: Amount<NonNegative> = 100_000.try_into().expect("valid amount");
let change: Amount<NonNegative> = 90_000.try_into().expect("valid amount");
let coinbase = Transaction::test_v1(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: 0xFFFF_FFFF,
}],
vec![transparent::Output::new(
change,
transparent::Script::new(&[]),
)],
LockTime::unlocked(),
);
let outpoint = OutPoint::from_usize(TransactionHash([0; 32]), 0);
let spend = Transaction::test_v1(
vec![transparent::Input::PrevOut {
outpoint,
unlock_script: transparent::Script::new(&[]),
sequence: 0,
}],
vec![transparent::Output::new(
change,
transparent::Script::new(&[]),
)],
LockTime::unlocked(),
);
let utxos = HashMap::from([(
outpoint,
transparent::Utxo::new(
transparent::Output::new(spent_value, transparent::Script::new(&[])),
Height(999),
false,
),
)]);
let header: Header = zebra_test::vectors::DUMMY_HEADER
.zcash_deserialize_into()
.expect("dummy header should deserialize");
let block = Block {
header: Arc::new(header),
transactions: vec![Arc::new(coinbase), Arc::new(spend)],
};
for (nu7, expected_reserve) in [(None, 0), (Some(height.0), 6_000), (Some(999), 6_000)] {
let network = Network::new_regtest(
ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(2),
nu6: Some(3),
nu6_1: Some(4),
nu6_2: Some(5),
nu6_3: Some(6),
nu7,
..Default::default()
}
.into(),
);
let (change, fees) = block
.chain_value_pool_change_and_fees(
&utxos,
DeferredPoolBalanceChange::zero(),
&network,
ValueBalance::zero(),
)
.expect("chain value pool change should be calculable");
assert_eq!(change.nsm_amount().zatoshis(), expected_reserve);
assert_eq!(fees.map(|fees| fees.zatoshis()), nu7.map(|_| 10_000));
}
}
#[test]
fn nsm_seed_reissuance_and_funding_follow_the_parent() -> Result<(), Box<dyn std::error::Error>> {
use crate::{
amount::NegativeAllowed,
parameters::{
subsidy::{
block_subsidy_with_parent_pools, funding_stream_address, funding_stream_values,
scheduled_block_subsidy, subsidy_is_valid, CoinbaseTransactionError,
FundingStreamReceiver, SubsidyError,
},
testnet::{
ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
ConfiguredFundingStreams, Parameters,
},
},
};
use std::ops::Neg;
let height = Height(1_000);
let reserve = Amount::<NonNegative>::try_from(10_000_000_000u64)?;
let network = Parameters::build()
.with_slow_start_interval(Height(0))
.with_activation_heights(ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(2),
nu6: Some(3),
nu6_1: Some(4),
nu6_2: Some(5),
nu6_3: Some(6),
nu7: Some(height.0),
..Default::default()
})?
.with_initial_nsm_value_balance(reserve)
.with_nsm_reissuance_height(height)
.with_funding_streams(vec![ConfiguredFundingStreams {
height_range: Some(height..Height(1_010)),
recipients: Some(vec![
ConfiguredFundingStreamRecipient::new_for(FundingStreamReceiver::MajorGrants),
ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Deferred,
numerator: 12,
addresses: None,
},
]),
}])
.to_network()?;
let before_seed = ValueBalance::zero();
let header: Header = zebra_test::vectors::DUMMY_HEADER.zcash_deserialize_into()?;
let make_block = |height, outputs| Block {
header: Arc::new(header),
transactions: vec![Arc::new(Transaction::test_v1(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
outputs,
LockTime::unlocked(),
))],
};
let before_activation = make_block(
Height(999),
vec![transparent::Output::new(
scheduled_block_subsidy(Height(999), &network)?,
transparent::Script::new(&[]),
)],
);
let pre_activation_change = before_activation.chain_value_pool_change(
&HashMap::new(),
DeferredPoolBalanceChange::zero(),
&network,
before_seed,
)?;
let parent = before_seed.add_chain_value_pool_change(pre_activation_change)?;
assert!(parent.nsm_amount().is_zero());
let total = block_subsidy_with_parent_pools(height, &network, parent)?;
assert_eq!(
(total - scheduled_block_subsidy(height, &network)?)?.zatoshis(),
1_375
);
let funding = funding_stream_values(height, &network, total)?;
let grant = funding[&FundingStreamReceiver::MajorGrants];
let deferred = funding[&FundingStreamReceiver::Deferred];
assert_eq!(grant.zatoshis(), total.zatoshis() * 8 / 100);
assert_eq!(deferred.zatoshis(), total.zatoshis() * 12 / 100);
let grant_script = funding_stream_address(height, &network, FundingStreamReceiver::MajorGrants)
.expect("configured funding address")
.script();
let miner = (total - grant - deferred)?;
let block = make_block(
height,
vec![
transparent::Output::new(miner, transparent::Script::new(&[])),
transparent::Output::new(grant, grant_script.clone()),
],
);
let deferred_change = DeferredPoolBalanceChange::new(deferred.constrain::<NegativeAllowed>()?);
let (change, fees) = block.chain_value_pool_change_and_fees(
&HashMap::new(),
deferred_change,
&network,
parent,
)?;
assert_eq!(fees, Some(Amount::zero()));
assert_eq!(change.nsm_amount().zatoshis(), 9_999_998_625);
let after = parent.add_chain_value_pool_change(change)?;
assert_eq!(after.nsm_amount().zatoshis(), 9_999_998_625);
assert_eq!(after.total()?, (parent.total()? + total)?);
let next = make_block(height.next()?, Vec::new());
let next_change = next.chain_value_pool_change(
&HashMap::new(),
DeferredPoolBalanceChange::zero(),
&network,
after,
)?;
assert_eq!(next_change.nsm_amount().zatoshis(), -1_375);
assert_eq!(ValueBalance::from_bytes(&after.to_bytes())?, after);
let scheduled_grant =
funding_stream_values(height, &network, scheduled_block_subsidy(height, &network)?)?
[&FundingStreamReceiver::MajorGrants];
let underfunded = make_block(
height,
vec![
transparent::Output::new(
(total - scheduled_grant - deferred)?,
transparent::Script::new(&[]),
),
transparent::Output::new(scheduled_grant, grant_script),
],
);
assert_eq!(
underfunded.chain_value_pool_change(&HashMap::new(), deferred_change, &network, parent)?,
change,
"ledger accounting does not validate funding payouts",
);
assert!(matches!(
subsidy_is_valid(&underfunded, &network, total),
Err(CoinbaseTransactionError::Subsidy(
SubsidyError::FundingStreamNotFound
)),
));
assert_eq!(subsidy_is_valid(&block, &network, total)?, deferred_change);
let mut other_parent = after;
other_parent.set_nsm_amount((reserve * 2)?);
let other_total = block_subsidy_with_parent_pools(height.next()?, &network, other_parent)?;
assert!(matches!(
subsidy_is_valid(&block, &network, other_total),
Err(CoinbaseTransactionError::Subsidy(
SubsidyError::FundingStreamNotFound
)),
));
assert_eq!(after.add_chain_value_pool_change(change.neg())?, parent);
assert_eq!(
parent.add_chain_value_pool_change(pre_activation_change.neg())?,
before_seed
);
Ok(())
}
#[test]
fn blockheader_serialization() {
let _init_guard = zebra_test::init();
const BLOCK_HEADER_LENGTH: usize = crate::work::equihash::Solution::INPUT_LENGTH
+ 32
+ 3
+ crate::work::equihash::SOLUTION_SIZE;
for block in zebra_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 blockheader_serialized_size() {
let _init_guard = zebra_test::init();
for block in zebra_test::vectors::BLOCKS.iter() {
let mut header = block[..Header::serialized_size(&Network::Mainnet)]
.zcash_deserialize_into::<Header>()
.expect("blockheader test vector should deserialize");
let serialized_header = header
.zcash_serialize_to_vec()
.expect("blockheader test vector should serialize");
assert_eq!(
serialized_header.len(),
Header::serialized_size(&Network::Mainnet),
"serialized header size should match Header::serialized_size on Mainnet"
);
header.solution = crate::work::equihash::Solution::from_bytes(
&[0; crate::work::equihash::REGTEST_SOLUTION_SIZE],
)
.expect("Regtest solution size should be valid");
let serialized_header = header
.zcash_serialize_to_vec()
.expect("Regtest blockheader should serialize");
assert_eq!(
serialized_header.len(),
Header::serialized_size(&Network::new_regtest(Default::default())),
"serialized header size should match Header::serialized_size on Regtest"
);
}
}
#[test]
fn round_trip_blocks() {
let _init_guard = zebra_test::init();
zebra_test::vectors::BLOCK_MAINNET_434873_BYTES
.zcash_deserialize_into::<Block>()
.expect("bad version block test vector should deserialize");
for block_bytes in zebra_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 = zebra_test::init();
zebra_test::vectors::BAD_BLOCK_MAINNET_202_BYTES
.zcash_deserialize_into::<Block>()
.expect_err("parsing fails");
}
#[test]
fn block_test_vectors_unique() {
let _init_guard = zebra_test::init();
let block_count = zebra_test::vectors::BLOCKS.len();
let block_hashes: HashSet<_> = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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 = zebra_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");
}
}
#[test]
fn transaction_fees_sum_the_non_coinbase_transactions() {
let _init_guard = zebra_test::init();
let height = Height(347_499);
let amount = |zatoshis: i64| Amount::<NonNegative>::try_from(zatoshis).expect("valid amount");
let output = |zatoshis| transparent::Output {
value: amount(zatoshis),
lock_script: transparent::Script::new(&[]),
};
let coinbase = Transaction::test_v4(
vec![transparent::Input::Coinbase {
height,
data: vec![0],
sequence: u32::MAX,
}],
vec![output(1_000_000)],
LockTime::unlocked(),
height,
);
let mut utxos = HashMap::new();
let mut transactions = vec![Arc::new(coinbase)];
for (index, fee) in [1, 1, 1_000].into_iter().enumerate() {
let outpoint = transparent::OutPoint {
hash: crate::transaction::Hash([index as u8 + 1; 32]),
index: 0,
};
utxos.insert(
outpoint,
transparent::Utxo::new(output(10_000), Height(1), false),
);
transactions.push(Arc::new(Transaction::test_v4(
vec![transparent::Input::PrevOut {
outpoint,
unlock_script: transparent::Script::new(&[]),
sequence: u32::MAX,
}],
vec![output(10_000 - fee)],
LockTime::unlocked(),
height,
)));
}
let mut block = zebra_test::vectors::BLOCK_MAINNET_347499_BYTES
.zcash_deserialize_into::<Block>()
.expect("block should deserialize");
let network = Network::new_regtest(
crate::parameters::testnet::ConfiguredActivationHeights {
nu7: Some(height.0),
..Default::default()
}
.into(),
);
for (count, fees, contribution) in [(1, 0, 0), (3, 2, 1), (4, 1_002, 601)] {
block.transactions = transactions[..count].to_vec();
assert_eq!(block.transaction_fees(&utxos), Ok(amount(fees)));
let (change, gross_fees) = block
.chain_value_pool_change_and_fees(
&utxos,
DeferredPoolBalanceChange::zero(),
&network,
ValueBalance::zero(),
)
.expect("valid transaction balances");
assert_eq!(gross_fees, Some(amount(fees)));
assert_eq!(change.nsm_amount().zatoshis(), contribution);
assert_eq!(change.transparent_amount().zatoshis(), 1_000_000 - fees);
}
Arc::make_mut(&mut block.transactions[3]).set_outputs(vec![output(10_001)]);
assert!(matches!(
block.chain_value_pool_change_and_fees(
&utxos,
DeferredPoolBalanceChange::zero(),
&network,
ValueBalance::zero(),
),
Err(crate::value_balance::ValueBalanceError::Total(_)),
));
let (change, fees) = block
.chain_value_pool_change_and_fees(
&utxos,
DeferredPoolBalanceChange::zero(),
&Network::Mainnet,
ValueBalance::zero(),
)
.expect("pre-NU7 accounting leaves fee validation to the verifier");
assert_eq!(fees, None);
assert_eq!(change.transparent_amount().zatoshis(), 999_999);
assert_eq!(change.nsm_amount().zatoshis(), 0);
}