pub(crate) mod constants;
use std::collections::HashMap;
use crate::{
amount::{self, Amount, NonNegative},
block::{Height, HeightDiff},
parameters::{Network, NetworkUpgrade},
transparent,
};
use constants::{
regtest, testnet, BLOSSOM_POW_TARGET_SPACING_RATIO, FUNDING_STREAM_RECEIVER_DENOMINATOR,
FUNDING_STREAM_SPECIFICATION, LOCKBOX_SPECIFICATION, MAX_BLOCK_SUBSIDY,
POST_BLOSSOM_HALVING_INTERVAL, PRE_BLOSSOM_HALVING_INTERVAL,
};
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FundingStreamReceiver {
#[serde(rename = "ECC")]
Ecc,
ZcashFoundation,
MajorGrants,
Deferred,
}
impl FundingStreamReceiver {
pub fn info(&self, is_post_nu6: bool) -> (&'static str, &'static str) {
if is_post_nu6 {
(
match self {
FundingStreamReceiver::Ecc => "Electric Coin Company",
FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
FundingStreamReceiver::MajorGrants => "Zcash Community Grants NU6",
FundingStreamReceiver::Deferred => "Lockbox NU6",
},
LOCKBOX_SPECIFICATION,
)
} else {
(
match self {
FundingStreamReceiver::Ecc => "Electric Coin Company",
FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
FundingStreamReceiver::MajorGrants => "Major Grants",
FundingStreamReceiver::Deferred => "Lockbox NU6",
},
FUNDING_STREAM_SPECIFICATION,
)
}
}
pub fn is_deferred(&self) -> bool {
matches!(self, Self::Deferred)
}
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct FundingStreams {
height_range: std::ops::Range<Height>,
recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
}
impl FundingStreams {
pub fn new(
height_range: std::ops::Range<Height>,
recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
) -> Self {
Self {
height_range,
recipients,
}
}
pub fn empty() -> Self {
Self::new(Height::MAX..Height::MAX, HashMap::new())
}
pub fn height_range(&self) -> &std::ops::Range<Height> {
&self.height_range
}
pub fn recipients(&self) -> &HashMap<FundingStreamReceiver, FundingStreamRecipient> {
&self.recipients
}
pub fn recipient(&self, receiver: FundingStreamReceiver) -> Option<&FundingStreamRecipient> {
self.recipients.get(&receiver)
}
pub fn extend_recipient_addresses(&mut self, target_len: usize) {
for (receiver, recipient) in &mut self.recipients {
if receiver.is_deferred() {
continue;
}
recipient.extend_addresses(target_len);
}
}
}
#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct FundingStreamRecipient {
numerator: u64,
addresses: Vec<transparent::Address>,
}
impl FundingStreamRecipient {
pub fn new<I, T>(numerator: u64, addresses: I) -> Self
where
T: ToString,
I: IntoIterator<Item = T>,
{
Self {
numerator,
addresses: addresses
.into_iter()
.map(|addr| {
let addr = addr.to_string();
addr.parse()
.expect("funding stream address must deserialize")
})
.collect(),
}
}
pub fn numerator(&self) -> u64 {
self.numerator
}
pub fn addresses(&self) -> &[transparent::Address] {
&self.addresses
}
pub fn extend_addresses(&mut self, target_len: usize) {
assert!(
!self.addresses.is_empty(),
"cannot extend addresses for empty recipient"
);
self.addresses = self
.addresses
.iter()
.cycle()
.take(target_len)
.cloned()
.collect();
}
}
pub trait ParameterSubsidy {
fn height_for_first_halving(&self) -> Height;
fn post_blossom_halving_interval(&self) -> HeightDiff;
fn pre_blossom_halving_interval(&self) -> HeightDiff;
fn funding_stream_address_change_interval(&self) -> HeightDiff;
}
impl ParameterSubsidy for Network {
fn height_for_first_halving(&self) -> Height {
match self {
Network::Mainnet => NetworkUpgrade::Canopy
.activation_height(self)
.expect("canopy activation height should be available"),
Network::Testnet(params) => {
if params.is_regtest() {
regtest::FIRST_HALVING
} else if params.is_default_testnet() {
testnet::FIRST_HALVING
} else {
height_for_halving(1, self).expect("first halving height should be available")
}
}
}
}
fn post_blossom_halving_interval(&self) -> HeightDiff {
match self {
Network::Mainnet => POST_BLOSSOM_HALVING_INTERVAL,
Network::Testnet(params) => params.post_blossom_halving_interval(),
}
}
fn pre_blossom_halving_interval(&self) -> HeightDiff {
match self {
Network::Mainnet => PRE_BLOSSOM_HALVING_INTERVAL,
Network::Testnet(params) => params.pre_blossom_halving_interval(),
}
}
fn funding_stream_address_change_interval(&self) -> HeightDiff {
self.post_blossom_halving_interval() / 48
}
}
pub fn funding_stream_address_period<N: ParameterSubsidy>(height: Height, network: &N) -> u32 {
let height_after_first_halving = height - network.height_for_first_halving();
let address_period = (height_after_first_halving + network.post_blossom_halving_interval())
/ network.funding_stream_address_change_interval();
address_period
.try_into()
.expect("all values are positive and smaller than the input height")
}
pub fn height_for_halving(halving: u32, network: &Network) -> Option<Height> {
if halving == 0 {
return Some(Height(0));
}
let slow_start_shift = i64::from(network.slow_start_shift().0);
let blossom_height = i64::from(NetworkUpgrade::Blossom.activation_height(network)?.0);
let pre_blossom_halving_interval = network.pre_blossom_halving_interval();
let halving_index = i64::from(halving);
let unscaled_height = halving_index.checked_mul(pre_blossom_halving_interval)?;
let pre_blossom_height = unscaled_height
.min(blossom_height)
.checked_add(slow_start_shift)?;
let post_blossom_height = 0
.max(unscaled_height - blossom_height)
.checked_mul(i64::from(BLOSSOM_POW_TARGET_SPACING_RATIO))?
.checked_add(slow_start_shift)?;
let height = pre_blossom_height.checked_add(post_blossom_height)?;
let height = u32::try_from(height).ok()?;
height.try_into().ok()
}
pub fn funding_stream_values(
height: Height,
network: &Network,
expected_block_subsidy: Amount<NonNegative>,
) -> Result<HashMap<FundingStreamReceiver, Amount<NonNegative>>, amount::Error> {
let mut results = HashMap::new();
if expected_block_subsidy.is_zero() {
return Ok(results);
}
if NetworkUpgrade::current(network, height) >= NetworkUpgrade::Canopy {
let funding_streams = network.funding_streams(height);
if let Some(funding_streams) = funding_streams {
for (&receiver, recipient) in funding_streams.recipients() {
let amount_value = ((expected_block_subsidy * recipient.numerator())?
/ FUNDING_STREAM_RECEIVER_DENOMINATOR)?;
results.insert(receiver, amount_value);
}
}
}
Ok(results)
}
#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum SubsidyError {
#[error("no coinbase transaction in block")]
NoCoinbase,
#[error("funding stream expected output not found")]
FundingStreamNotFound,
#[error("founders reward output not found")]
FoundersRewardNotFound,
#[error("one-time lockbox disbursement output not found")]
OneTimeLockboxDisbursementNotFound,
#[error("miner fees are invalid")]
InvalidMinerFees,
#[error("addition of amounts overflowed")]
Overflow,
#[error("subtraction of amounts underflowed")]
Underflow,
#[error("unsupported height")]
UnsupportedHeight,
#[error("invalid amount")]
InvalidAmount(#[from] amount::Error),
#[cfg(zcash_unstable = "zip235")]
#[error("invalid zip233 amount")]
InvalidZip233Amount,
}
pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
1u64.checked_shl(halving(height, network))
}
pub fn halving(height: Height, network: &Network) -> u32 {
let slow_start_shift = network.slow_start_shift();
let blossom_height = NetworkUpgrade::Blossom
.activation_height(network)
.expect("blossom activation height should be available");
let halving_index = if height < slow_start_shift {
0
} else if height < blossom_height {
let pre_blossom_height = height - slow_start_shift;
pre_blossom_height / network.pre_blossom_halving_interval()
} else {
let pre_blossom_height = blossom_height - slow_start_shift;
let scaled_pre_blossom_height =
pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
let post_blossom_height = height - blossom_height;
(scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
};
halving_index
.try_into()
.expect("already checked for negatives")
}
pub fn block_subsidy(height: Height, net: &Network) -> Result<Amount<NonNegative>, SubsidyError> {
let Some(halving_div) = halving_divisor(height, net) else {
return Ok(Amount::zero());
};
let slow_start_interval = net.slow_start_interval();
let amount = if height < slow_start_interval {
let slow_start_rate = MAX_BLOCK_SUBSIDY / u64::from(slow_start_interval);
if height < net.slow_start_shift() {
slow_start_rate * u64::from(height)
} else {
slow_start_rate * (u64::from(height) + 1)
}
} else {
let base_subsidy = if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
MAX_BLOCK_SUBSIDY
} else {
MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
};
base_subsidy / halving_div
};
Ok(Amount::try_from(amount)?)
}
pub fn miner_subsidy(
height: Height,
network: &Network,
expected_block_subsidy: Amount<NonNegative>,
) -> Result<Amount<NonNegative>, amount::Error> {
let founders_reward = founders_reward(network, height);
let funding_streams_sum = funding_stream_values(height, network, expected_block_subsidy)?
.values()
.sum::<Result<Amount<NonNegative>, _>>()?;
expected_block_subsidy - founders_reward - funding_streams_sum
}
pub fn founders_reward_address(net: &Network, height: Height) -> Option<transparent::Address> {
let founders_address_list = net.founder_address_list();
let num_founder_addresses = u32::try_from(founders_address_list.len()).ok()?;
let slow_start_shift = u32::from(net.slow_start_shift());
let pre_blossom_halving_interval = u32::try_from(net.pre_blossom_halving_interval()).ok()?;
let founder_address_change_interval = slow_start_shift
.checked_add(pre_blossom_halving_interval)?
.div_ceil(num_founder_addresses);
let founder_address_adjusted_height =
if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
u32::from(height)
} else {
NetworkUpgrade::Blossom
.activation_height(net)
.and_then(|h| {
let blossom_activation_height = u32::from(h);
let height = u32::from(height);
blossom_activation_height.checked_add(
height.checked_sub(blossom_activation_height)?
/ BLOSSOM_POW_TARGET_SPACING_RATIO,
)
})?
};
let founder_address_index =
usize::try_from(founder_address_adjusted_height / founder_address_change_interval).ok()?;
founders_address_list
.get(founder_address_index)
.and_then(|a| a.parse().ok())
}
pub fn founders_reward(net: &Network, height: Height) -> Amount<NonNegative> {
if halving(height, net) < 1 && NetworkUpgrade::current(net, height) < NetworkUpgrade::Canopy {
block_subsidy(height, net)
.map(|subsidy| subsidy.div_exact(5))
.expect("block subsidy must be valid for founders rewards")
} else {
Amount::zero()
}
}