pub(crate) mod constants;
mod check;
#[cfg(test)]
mod tests;
use std::{collections::HashMap, sync::LazyLock};
use crate::{
amount::{self, Amount, NegativeAllowed, NonNegative, MAX_MONEY},
block::{Height, HeightDiff},
parameters::{Network, NetworkUpgrade},
transparent,
value_balance::ValueBalance,
};
pub use check::{miner_fees_are_valid, subsidy_is_valid, CoinbaseTransactionError};
use constants::{
BLOSSOM_POW_TARGET_SPACING_RATIO, FUNDING_STREAM_RECEIVER_DENOMINATOR,
FUNDING_STREAM_SPECIFICATION, LOCKBOX_SPECIFICATION, MAX_BLOCK_SUBSIDY,
NU7_POW_TARGET_SPACING_RATIO, POST_BLOSSOM_HALVING_INTERVAL, POST_NU7_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(super) fn set_end_height(&mut self, end: Height) {
self.height_range.end = end;
}
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!(
target_len == 0 || !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 post_nu7_halving_interval(&self) -> HeightDiff;
fn nu7_activation_height(&self) -> Option<Height>;
fn funding_stream_address_change_interval(&self) -> HeightDiff;
}
impl ParameterSubsidy for Network {
fn height_for_first_halving(&self) -> Height {
height_for_halving(1, self).expect("the first halving height is representable")
}
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 post_nu7_halving_interval(&self) -> HeightDiff {
match self {
Network::Mainnet => POST_NU7_HALVING_INTERVAL,
Network::Testnet(params) => {
params.post_blossom_halving_interval()
* HeightDiff::from(NU7_POW_TARGET_SPACING_RATIO)
}
}
}
fn nu7_activation_height(&self) -> Option<Height> {
NetworkUpgrade::Nu7.activation_height(self)
}
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,
) -> HeightDiff {
if let Some(nu7) = network.nu7_activation_height().filter(|&nu7| height >= nu7) {
return nu7_funding_stream_address_period(height, nu7, network);
}
let height_after_first_halving = height - network.height_for_first_halving();
(height_after_first_halving + network.post_blossom_halving_interval())
.div_euclid(network.funding_stream_address_change_interval())
}
fn nu7_funding_stream_address_period<N: ParameterSubsidy>(
height: Height,
nu7: Height,
network: &N,
) -> HeightDiff {
let ratio = HeightDiff::from(NU7_POW_TARGET_SPACING_RATIO);
let pre_nu7_blocks =
nu7 - network.height_for_first_halving() + network.post_blossom_halving_interval();
let post_nu7_blocks = height - nu7;
(ratio * pre_nu7_blocks + post_nu7_blocks)
.div_euclid(ratio * network.funding_stream_address_change_interval())
}
pub(super) fn nu7_funding_stream_end_height(original_end: Height, nu7: Option<Height>) -> Height {
match nu7.filter(|&activation| activation < original_end) {
Some(activation) => (activation
+ (original_end - activation) * HeightDiff::from(NU7_POW_TARGET_SPACING_RATIO))
.expect("canonical funding stream end heights scaled by three fit in Height"),
None => original_end,
}
}
fn funding_stream_address_index(
height: Height,
network: &Network,
receiver: FundingStreamReceiver,
) -> Option<usize> {
if receiver == FundingStreamReceiver::Deferred {
return None;
}
let funding_streams = network.funding_streams(height)?;
let num_addresses = funding_streams.recipient(receiver)?.addresses().len();
let index = usize::try_from(
1 + funding_stream_address_period(height, network)
- funding_stream_address_period(funding_streams.height_range().start, network),
)
.ok()?;
assert!(index > 0 && index <= num_addresses);
Some(index - 1)
}
pub fn funding_stream_address(
height: Height,
network: &Network,
receiver: FundingStreamReceiver,
) -> Option<&transparent::Address> {
let index = funding_stream_address_index(height, network, receiver)?;
let funding_streams = network.funding_streams(height)?;
funding_streams.recipient(receiver)?.addresses().get(index)
}
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)?
.checked_add(slow_start_shift)?;
let height = if unscaled_height > blossom_height {
(unscaled_height - blossom_height)
.checked_mul(i64::from(BLOSSOM_POW_TARGET_SPACING_RATIO))?
.checked_add(blossom_height)?
} else {
unscaled_height
};
let height = match NetworkUpgrade::Nu7.activation_height(network) {
Some(nu7_height) if height > i64::from(nu7_height.0) => {
let nu7_height = i64::from(nu7_height.0);
(height - nu7_height)
.checked_mul(i64::from(NU7_POW_TARGET_SPACING_RATIO))?
.checked_add(nu7_height)?
}
_ => 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("{0}")]
Other(String),
#[error("invalid amount")]
InvalidAmount(#[from] amount::Error),
#[error(
"the block subsidy at height {0:?} depends on the chain value pools after its parent block"
)]
ParentChainValuePoolsRequired(Height),
}
pub const MAINNET_INITIAL_NSM_VALUE_BALANCE: u64 = 36_858_445_520;
pub const TESTNET_INITIAL_NSM_VALUE_BALANCE: u64 = 55_768_414_957;
impl Network {
pub fn initial_nsm_value_balance(&self) -> Amount<NonNegative> {
match self {
Network::Mainnet => Amount::try_from(MAINNET_INITIAL_NSM_VALUE_BALANCE)
.expect("hard-coded initial NSM value balance should be valid"),
Network::Testnet(params) => params.initial_nsm_value_balance(),
}
}
pub fn nsm_reissuance_height(&self) -> Option<Height> {
match self {
Network::Mainnet => {
static HEIGHT: LazyLock<Option<Height>> = LazyLock::new(|| {
nsm_reissuance_crossing_height(&Network::Mainnet)
.expect("the hard-coded Mainnet subsidy schedule is valid")
});
*HEIGHT
}
Network::Testnet(params) => params.nsm_reissuance_height(),
}
}
}
pub(super) fn nsm_reissuance_crossing_height(
network: &Network,
) -> Result<Option<Height>, SubsidyError> {
let Some(activation) = NetworkUpgrade::Nu7.activation_height(network) else {
return Ok(None);
};
let Some(third_halving) = height_for_halving(3, network) else {
return Ok(None);
};
let mut low = activation.0.max(third_halving.0 + 1);
let mut high =
height_for_halving(4, network).map_or(Height::MAX.0, |height| height.0.saturating_sub(1));
if low > high {
return Ok(None);
}
let crossed = |height| -> Result<bool, SubsidyError> {
let issued = cumulative_scheduled_issuance_zatoshis(Height(height - 1), network)?;
let reserve = u64::try_from(MAX_MONEY)
.expect("MAX_MONEY is positive")
.saturating_sub(issued);
let reference =
(u128::from(reserve) * 1_375).div_ceil(u128::from(BLOCK_SUBSIDY_FRACTION_DENOMINATOR));
Ok(reference < u128::from(u64::from(scheduled_block_subsidy(Height(height), network)?)))
};
if !crossed(high)? {
return Ok(None);
}
while low < high {
let middle = low + (high - low) / 2;
if crossed(middle)? {
high = middle;
} else {
low = middle + 1;
}
}
Ok(Some(Height(low)))
}
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 nu7_height = NetworkUpgrade::Nu7.activation_height(network);
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 if nu7_height.is_none_or(|nu7_height| height < nu7_height) {
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()
} else {
let nu7_height = nu7_height.expect("checked by the branch condition above");
let scaled_pre_blossom_height = (blossom_height - slow_start_shift)
* HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
* HeightDiff::from(NU7_POW_TARGET_SPACING_RATIO);
let scaled_post_blossom_height =
(nu7_height - blossom_height) * HeightDiff::from(NU7_POW_TARGET_SPACING_RATIO);
let post_nu7_height = height - nu7_height;
(scaled_pre_blossom_height + scaled_post_blossom_height + post_nu7_height)
/ network.post_nu7_halving_interval()
};
halving_index
.try_into()
.expect("already checked for negatives")
}
pub const LN2_SCALED: u64 = 6_931_680_000;
pub const BLOCK_SUBSIDY_FRACTION_DENOMINATOR: u64 = 10_000_000_000;
pub fn halving_interval(height: Height, network: &Network) -> HeightDiff {
if NetworkUpgrade::current(network, height) >= NetworkUpgrade::Nu7 {
network.post_nu7_halving_interval()
} else {
network.post_blossom_halving_interval()
}
}
pub fn block_subsidy_fraction_numerator(height: Height, network: &Network) -> u64 {
let halving_interval =
u64::try_from(halving_interval(height, network)).expect("halving intervals are positive");
LN2_SCALED / halving_interval
}
pub fn nsm_fee_contribution(
height: Height,
network: &Network,
block_miner_fees: Amount<NonNegative>,
) -> Amount<NonNegative> {
if !nsm_value_balance_is_tracked(height, network) {
return Amount::zero();
}
let fees = u64::from(block_miner_fees);
(fees / 10 * 6 + fees % 10 * 6 / 10)
.try_into()
.expect("the contribution is at most the fees, which are a valid amount")
}
pub fn nsm_value_balance_is_tracked(height: Height, network: &Network) -> bool {
NetworkUpgrade::Nu7
.activation_height(network)
.is_some_and(|activation_height| height >= activation_height)
}
pub fn nsm_reissuance_is_active(height: Height, network: &Network) -> bool {
network
.nsm_reissuance_height()
.is_some_and(|deployment_height| height >= deployment_height)
}
fn nsm_seed(height: Height, network: &Network) -> Amount<NonNegative> {
if Some(height) == NetworkUpgrade::Nu7.activation_height(network) {
network.initial_nsm_value_balance()
} else {
Amount::zero()
}
}
pub fn nsm_value_balance_before(
height: Height,
network: &Network,
parent_nsm_value_balance: Amount<NonNegative>,
) -> Amount<NonNegative> {
(parent_nsm_value_balance + nsm_seed(height, network))
.expect("the balance is zero before NU7 activation, and the seed is zero after it")
}
pub fn nsm_value_balance_change(
height: Height,
network: &Network,
parent_chain_value_pools: ValueBalance<NonNegative>,
block_miner_fees: Amount<NonNegative>,
) -> Result<Amount<NegativeAllowed>, amount::Error> {
if !nsm_value_balance_is_tracked(height, network) {
return Ok(Amount::zero());
}
let seed = nsm_seed(height, network).constrain::<NegativeAllowed>()?;
let contributed =
nsm_fee_contribution(height, network, block_miner_fees).constrain::<NegativeAllowed>()?;
let reissued = additional_block_subsidy(
height,
network,
nsm_value_balance_before(height, network, parent_chain_value_pools.nsm_amount()),
)
.constrain::<NegativeAllowed>()?;
(seed - reissued)? + contributed
}
pub fn additional_block_subsidy(
height: Height,
network: &Network,
nsm_value_balance: Amount<NonNegative>,
) -> Amount<NonNegative> {
if !nsm_reissuance_is_active(height, network) {
return Amount::zero();
}
let additional_block_subsidy = (u128::from(u64::from(nsm_value_balance))
* u128::from(block_subsidy_fraction_numerator(height, network)))
.div_ceil(u128::from(BLOCK_SUBSIDY_FRACTION_DENOMINATOR));
u64::try_from(additional_block_subsidy)
.ok()
.and_then(|additional_block_subsidy| Amount::try_from(additional_block_subsidy).ok())
.expect("BLOCK_SUBSIDY_FRACTION is less than 1, so the result is at most the balance")
}
pub fn block_subsidy_with_parent_pools(
height: Height,
net: &Network,
parent_chain_value_pools: ValueBalance<NonNegative>,
) -> Result<Amount<NonNegative>, SubsidyError> {
block_subsidy_with_parent_nsm_value_balance(height, net, parent_chain_value_pools.nsm_amount())
}
pub fn block_subsidy_with_parent_nsm_value_balance(
height: Height,
net: &Network,
parent_nsm_value_balance: Amount<NonNegative>,
) -> Result<Amount<NonNegative>, SubsidyError> {
let scheduled_block_subsidy = scheduled_block_subsidy(height, net)?;
if !nsm_reissuance_is_active(height, net) {
return Ok(scheduled_block_subsidy);
}
let nsm_value_balance = nsm_value_balance_before(height, net, parent_nsm_value_balance);
Ok((scheduled_block_subsidy + additional_block_subsidy(height, net, nsm_value_balance))?)
}
pub fn block_subsidy(height: Height, net: &Network) -> Result<Amount<NonNegative>, SubsidyError> {
if nsm_reissuance_is_active(height, net) {
return Err(SubsidyError::ParentChainValuePoolsRequired(height));
}
scheduled_block_subsidy(height, net)
}
pub fn scheduled_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 nu = NetworkUpgrade::current(net, height);
let base_subsidy = if nu < NetworkUpgrade::Blossom {
MAX_BLOCK_SUBSIDY
} else if nu < NetworkUpgrade::Nu7 {
MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
} else {
MAX_BLOCK_SUBSIDY
/ u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
/ u64::from(NU7_POW_TARGET_SPACING_RATIO)
};
base_subsidy / halving_div
};
Ok(Amount::try_from(amount)?)
}
pub fn cumulative_scheduled_issuance(
height: Height,
network: &Network,
) -> Result<Amount<NonNegative>, SubsidyError> {
Ok(Amount::try_from(cumulative_scheduled_issuance_zatoshis(
height, network,
)?)?)
}
pub(super) fn cumulative_scheduled_issuance_zatoshis(
height: Height,
network: &Network,
) -> Result<u64, SubsidyError> {
let end = u64::from(height) + 1;
let mut slow_end = u64::from(network.slow_start_interval()).min(end);
if slow_end > 0
&& halving_divisor(
Height(u32::try_from(slow_end - 1).map_err(|_| SubsidyError::Overflow)?),
network,
)
.is_none()
{
let mut low = 0;
while low < slow_end {
let mid = low + (slow_end - low) / 2;
if halving_divisor(
Height(u32::try_from(mid).map_err(|_| SubsidyError::Overflow)?),
network,
)
.is_some()
{
low = mid + 1;
} else {
slow_end = mid;
}
}
}
let mut total = 0u64;
if slow_end > 0 {
let last = slow_end - 1;
let shift = u64::from(network.slow_start_shift());
total = (MAX_BLOCK_SUBSIDY / u64::from(network.slow_start_interval()))
* (last * (last + 1) / 2 + slow_end.saturating_sub(shift));
}
let mut start = slow_end;
while start < end {
let height = Height(u32::try_from(start).map_err(|_| SubsidyError::Overflow)?);
let subsidy = u64::from(scheduled_block_subsidy(height, network)?);
if subsidy == 0 {
break;
}
let era_end = [NetworkUpgrade::Blossom, NetworkUpgrade::Nu7]
.into_iter()
.filter_map(|upgrade| upgrade.activation_height(network))
.map(u64::from)
.filter(|&boundary| boundary > start)
.min()
.unwrap_or(end)
.min(end);
let index = halving(height, network);
let (mut low, mut high) = (start + 1, era_end);
while low < high {
let mid = low + (high - low) / 2;
if halving(
Height(u32::try_from(mid).map_err(|_| SubsidyError::Overflow)?),
network,
) == index
{
low = mid + 1;
} else {
high = mid;
}
}
total = total
.checked_add(subsidy * (low - start))
.ok_or(SubsidyError::Overflow)?;
start = low;
}
Ok(total)
}
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 {
scheduled_block_subsidy(height, net)
.map(|subsidy| subsidy.div_exact(5))
.expect("block subsidy must be valid for founders rewards")
} else {
Amount::zero()
}
}