#![allow(clippy::unwrap_in_result)]
mod prop;
mod vectors;
use color_eyre::Report;
use super::Network;
use crate::{
amount::{Amount, NonNegative},
block::Height,
parameters::{
subsidy::{
block_subsidy, halving, halving_divisor, height_for_halving, scheduled_block_subsidy,
ParameterSubsidy as _,
},
testnet::ConfiguredActivationHeights,
NetworkUpgrade,
},
};
#[test]
fn funding_stream_p2pkh_recipient_is_matched_exactly() -> Result<(), Report> {
use crate::{
block::Block,
parameters::{
subsidy::{
subsidy_is_valid, CoinbaseTransactionError, FundingStreamReceiver,
FundingStreamRecipient, FundingStreams, SubsidyError,
},
testnet::Parameters,
NetworkKind,
},
serialization::ZcashDeserializeInto,
transaction::{LockTime, Transaction},
transparent::{Address, Output},
};
use std::sync::Arc;
let height = Height(1_046_400);
let recipient = "t1MkHnkxVjNpNbCrSs3AJ8J7ZSp6NTYiUcG";
let configured_recipient = Address::from_pub_key_hash(
NetworkKind::Testnet,
recipient.parse::<Address>()?.hash_bytes(),
);
let streams = FundingStreams::new(
height..height.next()?,
[(
FundingStreamReceiver::MajorGrants,
FundingStreamRecipient::new(8, [configured_recipient.to_string()]),
)]
.into_iter()
.collect(),
);
let network = Parameters::build()
.with_funding_streams(vec![(&streams).into()])
.to_network()?;
let subsidy = block_subsidy(height, &network)?;
let payment = ((subsidy * 8)? / 100)?;
let mut block: Block =
zebra_test::vectors::BLOCK_MAINNET_1046400_BYTES.zcash_deserialize_into()?;
let inputs = block.transactions[0].inputs().to_vec();
for (address, expected) in [
(recipient, Ok(Default::default())),
(
"t3cFfPt1Bcvgez9ZbMBFWeZsskxTkPzGCow",
Err(CoinbaseTransactionError::Subsidy(
SubsidyError::FundingStreamNotFound,
)),
),
] {
let address: Address = address.parse()?;
block.transactions[0] = Arc::new(Transaction::test_v4(
inputs.clone(),
vec![Output::new(payment, address.script())],
LockTime::unlocked(),
Height::MIN,
));
assert_eq!(subsidy_is_valid(&block, &network, subsidy), expected);
}
Ok(())
}
#[test]
fn funding_stream_overlaps_are_rejected() {
use crate::parameters::{
network::error::ParametersBuilderError,
subsidy::FundingStreamReceiver,
testnet::{
ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, Parameters,
RegtestParameters,
},
};
let streams: Vec<_> = [(20, 22), (40, 41), (21, 23)]
.into_iter()
.map(|(start, end)| ConfiguredFundingStreams {
height_range: Some(Height(start)..Height(end)),
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Deferred,
numerator: 20,
addresses: None,
}]),
})
.collect();
let expected = ParametersBuilderError::OverlappingFundingStreamRanges {
first: Height(20)..Height(22),
second: Height(21)..Height(23),
};
assert_eq!(
Parameters::build()
.with_funding_streams(streams.clone())
.to_network()
.unwrap_err(),
expected,
);
assert_eq!(
Parameters::new_regtest(RegtestParameters {
funding_streams: Some(streams),
..Default::default()
})
.unwrap_err(),
expected,
);
}
#[test]
fn funding_stream_inherited_nu7_overlap_is_rejected() -> Result<(), Report> {
use crate::parameters::{
network::error::ParametersBuilderError,
subsidy::{constants::testnet, FundingStreamReceiver},
testnet::{ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, Parameters},
};
let extra = ConfiguredFundingStreams {
height_range: Some(Height(4_476_000)..Height(4_476_001)),
recipients: Some(vec![ConfiguredFundingStreamRecipient {
numerator: 1,
..ConfiguredFundingStreamRecipient::new_for(FundingStreamReceiver::Ecc)
}]),
};
let inherited = Parameters::build().with_funding_streams(vec![
Default::default(),
Default::default(),
Default::default(),
extra.clone(),
]);
assert!(matches!(
inherited.to_network(),
Err(ParametersBuilderError::OverlappingFundingStreamRanges { first, second })
if first.contains(&Height(4_476_000))
&& second == (Height(4_476_000)..Height(4_476_001))
));
let mut explicit: Vec<_> = testnet::FUNDING_STREAMS.iter().map(Into::into).collect();
explicit.push(extra);
let network = Parameters::build()
.with_funding_streams(explicit)
.to_network()?;
assert_eq!(
network
.funding_streams(Height(4_476_000))
.unwrap()
.recipient(FundingStreamReceiver::Ecc)
.unwrap()
.numerator(),
1,
);
Ok(())
}
#[test]
fn funding_stream_disjoint_and_empty_ranges_are_accepted() -> Result<(), Report> {
use crate::parameters::{
subsidy::FundingStreamReceiver,
testnet::{ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, Parameters},
};
let streams = [(22, 24, 2), (21, 21, 0), (20, 22, 1), (0, 0, 0)]
.into_iter()
.map(|(start, end, numerator)| ConfiguredFundingStreams {
height_range: Some(Height(start)..Height(end)),
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Deferred,
numerator,
addresses: None,
}]),
})
.collect();
let network = Parameters::build()
.with_funding_streams(streams)
.to_network()?;
for (height, numerator) in [(20, 1), (21, 1), (22, 2), (23, 2)] {
assert_eq!(
network
.funding_streams(Height(height))
.unwrap()
.recipient(FundingStreamReceiver::Deferred)
.unwrap()
.numerator(),
numerator,
);
}
assert!(network.funding_streams(Height(0)).is_none());
assert!(network.funding_streams(Height(24)).is_none());
Ok(())
}
#[test]
fn funding_stream_tex_recipient_is_rejected() -> Result<(), Report> {
use crate::parameters::{
network::error::ParametersBuilderError,
subsidy::FundingStreamReceiver,
testnet::{
ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, Parameters,
RegtestParameters,
},
};
let address = "textest1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqggnfr9";
let streams = vec![ConfiguredFundingStreams {
height_range: Some(Height(4_465_026)..Height(4_465_027)),
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::MajorGrants,
numerator: 8,
addresses: Some(vec![address.to_string()]),
}]),
}];
let expected = ParametersBuilderError::UnsupportedFundingStreamAddress {
receiver: FundingStreamReceiver::MajorGrants,
address: address.parse()?,
};
assert_eq!(
Parameters::build()
.with_funding_streams(streams.clone())
.to_network()
.unwrap_err(),
expected,
);
assert_eq!(
Parameters::new_regtest(RegtestParameters {
funding_streams: Some(streams),
..Default::default()
})
.unwrap_err(),
expected,
);
Ok(())
}
#[test]
fn halving_test() -> Result<(), Report> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
halving_for_network(&network)?;
}
Ok(())
}
fn halving_for_network(network: &Network) -> Result<(), Report> {
let blossom_height = NetworkUpgrade::Blossom.activation_height(network).unwrap();
let first_halving_height = network.height_for_first_halving();
assert_eq!(
1,
halving_divisor((network.slow_start_interval() + 1).unwrap(), network).unwrap()
);
assert_eq!(
1,
halving_divisor((blossom_height - 1).unwrap(), network).unwrap()
);
assert_eq!(1, halving_divisor(blossom_height, network).unwrap());
assert_eq!(
1,
halving_divisor((first_halving_height - 1).unwrap(), network).unwrap()
);
assert_eq!(2, halving_divisor(first_halving_height, network).unwrap());
assert_eq!(
2,
halving_divisor((first_halving_height + 1).unwrap(), network).unwrap()
);
for (index, divisor) in [
(2, Some(4)),
(3, Some(8)),
(10, Some(1024)),
(20, Some(1_048_576)),
(30, Some(1_073_741_824)),
(40, Some(1_099_511_627_776)),
(63, Some(9_223_372_036_854_775_808)),
(64, None),
(65, None),
] {
let height = height_for_halving(index, network).unwrap();
assert_eq!(
divisor,
halving_divisor(height, network),
"halving divisor at index {index}",
);
}
assert_eq!(
None,
halving_divisor(Height(Height::MAX_AS_U32 / 4), network),
);
assert_eq!(
None,
halving_divisor(Height(Height::MAX_AS_U32 / 2), network),
);
assert_eq!(None, halving_divisor(Height::MAX, network));
Ok(())
}
#[test]
fn block_subsidy_test() -> Result<(), Report> {
let _init_guard = zebra_test::init();
for network in Network::iter() {
block_subsidy_for_network(&network)?;
}
Ok(())
}
fn block_subsidy_for_network(network: &Network) -> Result<(), Report> {
let blossom_height = NetworkUpgrade::Blossom.activation_height(network).unwrap();
let first_halving_height = network.height_for_first_halving();
assert_eq!(
Amount::<NonNegative>::try_from(1_250_000_000)?,
block_subsidy((network.slow_start_interval() + 1).unwrap(), network,)?
);
assert_eq!(
Amount::<NonNegative>::try_from(1_250_000_000)?,
block_subsidy((blossom_height - 1).unwrap(), network)?
);
assert_eq!(
Amount::<NonNegative>::try_from(625_000_000)?,
block_subsidy(blossom_height, network)?
);
assert_eq!(
Amount::<NonNegative>::try_from(312_500_000)?,
block_subsidy(first_halving_height, network)?
);
for (index, pre_nu7_amount) in [
(2, 156_250_000_i64),
(7, 4_882_812),
(29, 1),
(30, 0),
(40, 0),
(50, 0),
(60, 0),
(63, 0),
(64, 0),
(65, 0),
] {
let height = height_for_halving(index, network).unwrap();
let expected = if NetworkUpgrade::current(network, height) >= NetworkUpgrade::Nu7 {
pre_nu7_amount / 3
} else {
pre_nu7_amount
};
assert_eq!(
Amount::<NonNegative>::try_from(expected)?,
scheduled_block_subsidy(height, network)?,
"scheduled subsidy at halving index {index}",
);
}
for height in [
Height(Height::MAX_AS_U32 / 4),
Height(Height::MAX_AS_U32 / 2),
Height::MAX,
] {
assert_eq!(
Amount::<NonNegative>::zero(),
scheduled_block_subsidy(height, network)?,
);
}
Ok(())
}
#[test]
fn check_height_for_num_halvings() {
for network in Network::iter() {
for h in 1..=halving(Height::MAX, &network) {
let Some(height_for_halving) = height_for_halving(h, &network) else {
panic!("could not find height for halving {h}");
};
let prev_height = height_for_halving
.previous()
.expect("there should be a previous height");
assert_eq!(
h,
halving(height_for_halving, &network),
"num_halvings should match the halving index"
);
assert_eq!(
h - 1,
halving(prev_height, &network),
"num_halvings for the prev height should be 1 less than the halving index"
);
}
}
}
fn nsm_mainnet_like_testnet(nu7: u32) -> Network {
nsm_mainnet_like_testnet_with_initial_balance(nu7, 35_080_000_000)
}
fn nsm_mainnet_like_testnet_with_initial_balance(
nu7: u32,
initial_nsm_value_balance: i64,
) -> Network {
use crate::parameters::testnet::{self, ConfiguredActivationHeights};
testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
nu7: Some(nu7),
..(&Network::Mainnet.activation_list()).into()
})
.unwrap()
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new())
.with_initial_nsm_value_balance(initial_nsm_value_balance.try_into().expect("valid amount"))
.with_nsm_reissuance_height(Height(nu7))
.to_network()
.unwrap()
}
fn nsm_pools_with_nsm_balance(nsm: i64) -> crate::value_balance::ValueBalance<NonNegative> {
let mut pools = crate::value_balance::ValueBalance::<NonNegative>::zero();
pools.set_nsm_amount(nsm.try_into().expect("valid amount"));
pools
}
#[test]
fn check_nsm_block_subsidy_fraction_follows_halving_interval() -> Result<(), Report> {
use crate::parameters::subsidy::{
block_subsidy_fraction_numerator, ParameterSubsidy, LN2_SCALED,
};
let _init_guard = zebra_test::init();
assert_eq!(Network::Mainnet.post_blossom_halving_interval(), 1_680_000);
assert_eq!(
block_subsidy_fraction_numerator(Height(4_000_000), &Network::Mainnet),
4126
);
let activation = Height(3_687_123);
let zip218_like = nsm_mainnet_like_testnet(activation.0);
assert_eq!(zip218_like.post_nu7_halving_interval(), 1_680_000 * 3);
assert_eq!(
block_subsidy_fraction_numerator(activation.previous()?, &zip218_like),
4126
);
assert_eq!(
block_subsidy_fraction_numerator(activation, &zip218_like),
1375
);
let regtest = Network::new_regtest(Default::default());
assert_eq!(regtest.post_blossom_halving_interval(), 288);
assert_eq!(
block_subsidy_fraction_numerator(Height(1), ®test),
LN2_SCALED / 288,
);
assert!(LN2_SCALED > u64::from(crate::block::Height::MAX_AS_U32));
Ok(())
}
#[test]
fn check_nsm_additional_block_subsidy_rounds_up() -> Result<(), Report> {
use crate::{
amount::MAX_MONEY,
parameters::subsidy::{additional_block_subsidy, BLOCK_SUBSIDY_FRACTION_DENOMINATOR},
};
let _init_guard = zebra_test::init();
let height = Height(3_687_123);
let network = nsm_mainnet_like_testnet(height.0);
for (nsm_value_balance, additional) in [
(0, 0),
(1, 1),
(7_272_727, 1),
(7_272_728, 2),
(10_000_000_000_i64, 1375),
(35_080_000_000_i64, 4824),
] {
assert_eq!(
additional_block_subsidy(height, &network, Amount::try_from(nsm_value_balance)?),
Amount::<NonNegative>::try_from(additional)?,
"NSM value balance: {nsm_value_balance}",
);
}
let interim = u128::try_from(MAX_MONEY)? * 1375;
assert!(interim < 1u128 << 63);
assert_eq!(
additional_block_subsidy(height, &network, Amount::try_from(MAX_MONEY)?),
Amount::<NonNegative>::try_from(
(interim as u64).div_ceil(BLOCK_SUBSIDY_FRACTION_DENOMINATOR)
)?,
);
for nsm_value_balance in [1, 2, 4126, 10_000_000_000_i64, MAX_MONEY] {
let nsm_value_balance = Amount::<NonNegative>::try_from(nsm_value_balance)?;
assert!(
additional_block_subsidy(Height(1), &network, nsm_value_balance) <= nsm_value_balance
);
}
Ok(())
}
#[test]
fn check_nsm_block_subsidy_with_parent_pools() -> Result<(), Report> {
use crate::{
parameters::subsidy::{
additional_block_subsidy, block_subsidy, block_subsidy_with_parent_pools,
nsm_reissuance_is_active, nsm_value_balance_before, nsm_value_balance_is_tracked,
scheduled_block_subsidy, SubsidyError,
},
value_balance::ValueBalance,
};
let _init_guard = zebra_test::init();
let activation = Height(3_687_123);
let network = nsm_mainnet_like_testnet(activation.0);
let initial = network.initial_nsm_value_balance();
assert!(!nsm_value_balance_is_tracked(activation, &Network::Mainnet));
assert!(!nsm_reissuance_is_active(activation, &Network::Mainnet));
assert_eq!(Network::Mainnet.nsm_reissuance_height(), None);
assert_eq!(network.nsm_reissuance_height(), Some(activation));
assert!(!nsm_value_balance_is_tracked(
(activation - 1).unwrap(),
&network
));
assert!(!nsm_reissuance_is_active(
(activation - 1).unwrap(),
&network
));
assert!(nsm_value_balance_is_tracked(activation, &network));
assert!(nsm_reissuance_is_active(activation, &network));
let before_activation = (activation - 1).unwrap();
assert_eq!(
block_subsidy_with_parent_pools(
before_activation,
&network,
nsm_pools_with_nsm_balance(12_345)
)?,
scheduled_block_subsidy(before_activation, &network)?,
);
assert_eq!(
nsm_value_balance_before(activation, &network, Amount::zero()),
initial,
);
assert_eq!(
block_subsidy_with_parent_pools(activation, &network, ValueBalance::zero())?,
(scheduled_block_subsidy(activation, &network)?
+ additional_block_subsidy(activation, &network, initial))?,
);
let after_activation = (activation + 1).unwrap();
let nsm_value_balance = Amount::<NonNegative>::try_from(10_000_000_000_i64)?;
let mut parent_pools = ValueBalance::<NonNegative>::zero();
parent_pools.set_nsm_amount(nsm_value_balance);
assert_eq!(
nsm_value_balance_before(after_activation, &network, parent_pools.nsm_amount()),
nsm_value_balance,
);
assert_eq!(
block_subsidy_with_parent_pools(after_activation, &network, parent_pools)?,
(scheduled_block_subsidy(after_activation, &network)?
+ additional_block_subsidy(after_activation, &network, nsm_value_balance))?,
);
assert_eq!(
block_subsidy_with_parent_pools(after_activation, &network, ValueBalance::zero())?,
scheduled_block_subsidy(after_activation, &network)?,
);
assert_eq!(
block_subsidy(activation, &network),
Err(SubsidyError::ParentChainValuePoolsRequired(activation)),
);
Ok(())
}
#[test]
fn check_nsm_nsm_value_balance_change() -> Result<(), Report> {
use std::ops::Neg;
use crate::{
amount::NegativeAllowed,
parameters::subsidy::{
additional_block_subsidy, nsm_fee_contribution, nsm_value_balance_change,
},
value_balance::ValueBalance,
};
let _init_guard = zebra_test::init();
let activation = Height(3_687_123);
let network = nsm_mainnet_like_testnet(activation.0);
let initial = network.initial_nsm_value_balance();
assert_eq!(
nsm_value_balance_change(
(activation - 1).unwrap(),
&network,
ValueBalance::zero(),
Amount::zero()
)?,
Amount::<NegativeAllowed>::zero(),
);
let seeded =
nsm_value_balance_change(activation, &network, ValueBalance::zero(), Amount::zero())?;
assert_eq!(
seeded,
(initial.constrain::<NegativeAllowed>()?
- additional_block_subsidy(activation, &network, initial)
.constrain::<NegativeAllowed>()?)?,
);
assert!(seeded > Amount::<NegativeAllowed>::zero());
let nsm_value_balance = Amount::<NonNegative>::try_from(10_000_000_000_i64)?;
let change = nsm_value_balance_change(
(activation + 1).unwrap(),
&network,
nsm_pools_with_nsm_balance(nsm_value_balance.into()),
Amount::zero(),
)?;
assert_eq!(
change,
additional_block_subsidy((activation + 1).unwrap(), &network, nsm_value_balance)
.constrain::<NegativeAllowed>()?
.neg(),
);
let fees = Amount::<NonNegative>::try_from(1_001)?;
assert_eq!(
nsm_value_balance_change(
(activation + 1).unwrap(),
&network,
nsm_pools_with_nsm_balance(nsm_value_balance.into()),
fees,
)?,
(change
+ nsm_fee_contribution((activation + 1).unwrap(), &network, fees)
.constrain::<NegativeAllowed>()?)?,
);
assert!(
(nsm_value_balance.constrain::<NegativeAllowed>()? + change)?
>= Amount::<NegativeAllowed>::zero()
);
Ok(())
}
#[test]
fn check_nsm_reissuance_starts_at_deployment_height() -> Result<(), Report> {
use std::ops::Neg;
use crate::{
amount::NegativeAllowed,
parameters::{
subsidy::{
additional_block_subsidy, block_subsidy, block_subsidy_with_parent_pools,
nsm_reissuance_is_active, nsm_value_balance_change, nsm_value_balance_is_tracked,
scheduled_block_subsidy, SubsidyError,
},
testnet::{self, ConfiguredActivationHeights},
},
value_balance::ValueBalance,
};
let _init_guard = zebra_test::init();
let nu7 = Height(3_687_123);
let deployment = Height(3_687_130);
let initial = Amount::<NonNegative>::try_from(35_080_000_000_i64)?;
let network = testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
nu7: Some(nu7.0),
..(&Network::Mainnet.activation_list()).into()
})?
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new())
.with_initial_nsm_value_balance(initial)
.with_nsm_reissuance_height(deployment)
.to_network()?;
assert_eq!(network.nsm_reissuance_height(), Some(deployment));
assert!(nsm_value_balance_is_tracked(nu7, &network));
assert!(!nsm_reissuance_is_active(nu7, &network));
assert!(additional_block_subsidy(nu7, &network, initial).is_zero());
assert_eq!(
nsm_value_balance_change(nu7, &network, ValueBalance::zero(), Amount::zero())?,
initial.constrain::<NegativeAllowed>()?,
);
assert_eq!(
block_subsidy_with_parent_pools(nu7, &network, ValueBalance::zero())?,
scheduled_block_subsidy(nu7, &network)?,
);
assert_eq!(
block_subsidy(nu7, &network)?,
scheduled_block_subsidy(nu7, &network)?
);
let before_deployment = (deployment - 1).unwrap();
assert_eq!(
nsm_value_balance_change(
before_deployment,
&network,
nsm_pools_with_nsm_balance(initial.into()),
Amount::zero()
)?,
Amount::<NegativeAllowed>::zero(),
);
assert!(nsm_reissuance_is_active(deployment, &network));
let reissued = additional_block_subsidy(deployment, &network, initial);
assert!(!reissued.is_zero());
assert_eq!(
block_subsidy_with_parent_pools(
deployment,
&network,
nsm_pools_with_nsm_balance(initial.into())
)?,
(scheduled_block_subsidy(deployment, &network)? + reissued)?,
);
assert_eq!(
nsm_value_balance_change(
deployment,
&network,
nsm_pools_with_nsm_balance(initial.into()),
Amount::zero()
)?,
reissued.constrain::<NegativeAllowed>()?.neg(),
);
assert_eq!(
block_subsidy(deployment, &network),
Err(SubsidyError::ParentChainValuePoolsRequired(deployment)),
);
Ok(())
}
#[test]
fn check_nsm_reissuance_height_must_follow_nu7() -> Result<(), Report> {
use crate::parameters::{
network::error::ParametersBuilderError,
testnet::{self, ConfiguredActivationHeights, RegtestParameters},
};
let _init_guard = zebra_test::init();
let build = |nu7: Option<u32>, deployment: u32| {
testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
nu7,
..(&Network::Mainnet.activation_list()).into()
})
.unwrap()
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new())
.with_nsm_reissuance_height(Height(deployment))
.to_network()
};
assert!(matches!(
build(Some(3_687_123), 3_687_122),
Err(ParametersBuilderError::NsmReissuanceHeightBeforeNu7)
));
assert!(matches!(
build(None, 3_687_123),
Err(ParametersBuilderError::NsmReissuanceHeightBeforeNu7)
));
assert!(build(Some(3_687_123), 3_687_123).is_ok());
assert!(matches!(
testnet::Parameters::new_regtest(RegtestParameters {
activation_heights: ConfiguredActivationHeights {
nu7: Some(20),
..Default::default()
},
nsm_reissuance_height: Some(Height(19)),
..Default::default()
}),
Err(ParametersBuilderError::NsmReissuanceHeightBeforeNu7)
));
Ok(())
}
#[test]
fn check_nsm_pool_halves_over_a_halving_period() -> Result<(), Report> {
use crate::parameters::subsidy::{additional_block_subsidy, ParameterSubsidy};
let _init_guard = zebra_test::init();
let short = crate::parameters::testnet::Parameters::build()
.with_slow_start_interval(Height::MIN)
.with_activation_heights(crate::parameters::testnet::ConfiguredActivationHeights {
canopy: Some(1),
nu5: Some(1),
nu6: Some(1),
nu6_3: Some(1),
nu7: Some(1),
..Default::default()
})?
.with_halving_interval(5_000)?
.clear_funding_streams()
.with_lockbox_disbursements(Vec::new())
.with_nsm_reissuance_height(Height(1))
.to_network()?;
let short_interval = short.post_nu7_halving_interval();
let start = Amount::<NonNegative>::try_from(35_080_000_000_i64)?;
let mut balance = start;
for _ in 0..short_interval {
balance = (balance - additional_block_subsidy(Height(1), &short, balance))?;
}
let half = (start / 2)?;
let tolerance = (start / 100)?;
assert!(
balance <= (half + tolerance)? && (balance + tolerance)? >= half,
"after {short_interval} blocks the balance is {balance:?}, expected about {half:?}",
);
Ok(())
}
#[test]
fn check_zip235_nsm_fee_contribution() -> Result<(), Report> {
use crate::{
amount::MAX_MONEY,
parameters::{
subsidy::nsm_fee_contribution,
testnet::{self, ConfiguredActivationHeights},
},
};
let _init_guard = zebra_test::init();
let nu7 = Height(3_687_123);
let network = testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
nu7: Some(nu7.0),
..(&Network::Mainnet.activation_list()).into()
})?
.to_network()?;
for (fees, contribution) in [
(0, 0_i64),
(1, 0),
(2, 1),
(3, 1),
(4, 2),
(5, 3),
(9, 5),
(10, 6),
(1_000, 600),
(1_001, 600),
(20_002, 12_001),
(MAX_MONEY - 1, 1_259_999_999_999_999),
(MAX_MONEY, 1_260_000_000_000_000),
] {
let fees = Amount::<NonNegative>::try_from(fees)?;
assert_eq!(
nsm_fee_contribution(nu7, &network, fees),
Amount::<NonNegative>::try_from(contribution)?,
);
assert!(nsm_fee_contribution((nu7 - 1).unwrap(), &network, fees).is_zero());
}
Ok(())
}
#[test]
fn check_zip235_nsm_value_balance_change_overflow() -> Result<(), Report> {
use crate::{
amount::{NegativeAllowed, MAX_MONEY},
parameters::subsidy::{
additional_block_subsidy, nsm_fee_contribution, nsm_value_balance_change,
},
value_balance::ValueBalance,
};
let _init_guard = zebra_test::init();
let activation = Height(3_687_123);
let network = nsm_mainnet_like_testnet_with_initial_balance(activation.0, MAX_MONEY);
let seed = Amount::<NonNegative>::try_from(MAX_MONEY)?;
let reissued = additional_block_subsidy(activation, &network, seed);
let fees = Amount::<NonNegative>::try_from(2)?;
assert_eq!(
nsm_fee_contribution(activation, &network, fees),
Amount::<NonNegative>::try_from(1)?
);
assert_eq!(
nsm_value_balance_change(activation, &network, ValueBalance::zero(), fees)?,
((seed.constrain::<NegativeAllowed>()? - reissued.constrain::<NegativeAllowed>()?)?
+ Amount::<NegativeAllowed>::try_from(1)?)?,
);
let fees = (reissued + reissued)?;
assert!(nsm_fee_contribution(activation, &network, fees) > reissued);
assert!(nsm_value_balance_change(activation, &network, ValueBalance::zero(), fees).is_err());
Ok(())
}
fn nu7_network(nu7_height: u32) -> Network {
Network::new_regtest(
crate::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_target_spacing_and_averaging_window() {
let _init_guard = zebra_test::init();
let network = nu7_network(1_000);
for (upgrade, height, spacing, window, timespan) in [
(NetworkUpgrade::Nu6_3, Height(999), 75, 17, 1275),
(NetworkUpgrade::Nu7, Height(1_000), 25, 102, 2550),
] {
let spacing = chrono::Duration::seconds(spacing);
assert_eq!(upgrade.target_spacing(), spacing);
assert_eq!(upgrade.averaging_window(), window);
assert_eq!(
upgrade.averaging_window_timespan(),
chrono::Duration::seconds(timespan)
);
assert_eq!(
NetworkUpgrade::target_spacing_for_height(&network, height),
spacing
);
assert_eq!(
NetworkUpgrade::averaging_window_for_height(&network, height),
window
);
}
}
#[test]
fn zip_218_block_subsidy() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let network = nu7_network(1_000);
let pre_nu7 = Height(999);
let nu7 = Height(1_000);
let pre_nu7_subsidy = scheduled_block_subsidy(pre_nu7, &network)?;
let nu7_subsidy = scheduled_block_subsidy(nu7, &network)?;
assert_eq!(halving(pre_nu7, &network), halving(nu7, &network));
assert_eq!(nu7_subsidy, (pre_nu7_subsidy / 3)?);
Ok(())
}
#[test]
fn zip_218_halving_interval() {
let _init_guard = zebra_test::init();
let network = nu7_network(1_000);
assert_eq!(
network.post_nu7_halving_interval(),
network.post_blossom_halving_interval() * 3,
);
assert_eq!(
halving(Height(999), &network),
halving(Height(1_000), &network),
);
for h in 1..20 {
let height = height_for_halving(h, &network).expect("halving height is representable");
let previous = height.previous().expect("there is a previous height");
assert_eq!(
halving(height, &network),
h,
"the halving index at height_for_halving({h}) should be {h}",
);
assert_eq!(
halving(previous, &network),
h - 1,
"the halving index just below height_for_halving({h}) should be {}",
h - 1,
);
}
}
#[test]
fn funding_stream_address_period_floors_negative_heights() {
use crate::parameters::{
subsidy::funding_stream_address_period,
testnet::{ConfiguredActivationHeights, Parameters},
};
let _init_guard = zebra_test::init();
let network = Parameters::build()
.with_slow_start_interval(Height(0))
.with_activation_heights(ConfiguredActivationHeights {
canopy: Some(30),
nu5: Some(35),
nu6: Some(40),
nu6_1: Some(45),
nu6_2: Some(47),
nu6_3: Some(48),
nu7: Some(50),
..Default::default()
})
.expect("activation heights are valid")
.with_funding_streams(Vec::new())
.to_network()
.expect("failed to build configured network");
let interval = network.funding_stream_address_change_interval() * 3;
let first_halving = network.height_for_first_halving();
let post_blossom = network.post_blossom_halving_interval();
let period_zero_start = (Height::MIN
+ (3 * (i64::from(first_halving.0) - post_blossom) - 2 * 50))
.expect("the NU7-adjusted zero point is a valid height");
for (offset, expected) in [
(0, 0),
(interval - 1, 0),
(interval, 1),
(-1, -1),
(-interval, -1),
(-interval - 1, -2),
] {
let height = (period_zero_start + offset).expect("valid height");
assert_eq!(
funding_stream_address_period(height, &network),
expected,
"incorrect funding stream period at {height:?}",
);
}
}
#[test]
fn first_halving_heights_are_unchanged() {
let _init_guard = zebra_test::init();
for (network, expected) in [
(Network::Mainnet, Height(1_046_400)),
(Network::new_default_testnet(), Height(1_116_000)),
(Network::new_regtest(Default::default()), Height(287)),
] {
assert_eq!(
network.height_for_first_halving(),
expected,
"the first halving height must not move on {network}",
);
}
assert_eq!(
Network::Mainnet.height_for_first_halving(),
NetworkUpgrade::Canopy
.activation_height(&Network::Mainnet)
.expect("Canopy is activated on Mainnet"),
);
}
#[test]
fn first_halving_follows_the_zip_218_schedule() {
let _init_guard = zebra_test::init();
let network = Network::new_regtest(
ConfiguredActivationHeights {
nu7: Some(1),
..Default::default()
}
.into(),
);
let first_halving = network.height_for_first_halving();
let unstretched = Network::new_regtest(Default::default()).height_for_first_halving();
assert!(
first_halving > unstretched,
"ZIP 218 should push the first halving {first_halving:?} past its unstretched height \
{unstretched:?}",
);
assert_eq!(halving(first_halving, &network), 1);
assert_eq!(
halving(
first_halving
.previous()
.expect("the first halving is above genesis"),
&network
),
0,
);
}