Block Subsidy and Founders Reward Amounts (#1051)

* add general and founders reward subsidy modules
* validate founders reward
* Use funding streams after Canopy on testnet
ZIP-1014 only applies to mainnet, where Canopy is at the first halving.
On testnet, Canopy is before the first halving, and the dev fund rules
apply from Canopy. (See ZIP-214.)
Co-authored-by: teor <teor@riseup.net>
Co-authored-by: Jane Lusby <jlusby42@gmail.com>

* pass all test vectors through current subsidy validation
* Add testnet and halving subsidy tests
* add subsidy validation error tests

* rename block validation methods
* add network to block verifier

* add amount operators
* Implement Ord, Eq, and Hash for Amount
* Implement Add<Height> for Height
And make the existing Height operators do range checks.
* Apply operator suggestions
Co-authored-by: Jane Lusby <jlusby42@gmail.com>
This commit is contained in:
Alfredo Garcia 2020-10-12 17:54:48 -03:00 committed by GitHub
parent 766baea9d8
commit c93f0b3a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 878 additions and 21 deletions

View File

@ -6,7 +6,9 @@
//! [`Result`](std::result::Result)s.
use std::{
cmp::Ordering,
convert::{TryFrom, TryInto},
hash::{Hash, Hasher},
marker::PhantomData,
ops::RangeInclusive,
};
@ -17,7 +19,7 @@ use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt};
type Result<T, E = Error> = std::result::Result<T, E>;
/// A runtime validated type for representing amounts of zatoshis
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Hash)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(try_from = "i64")]
#[serde(bound = "C: Constraint")]
pub struct Amount<C = NegativeAllowed>(i64, PhantomData<C>);
@ -191,6 +193,75 @@ where
}
}
impl<C> Hash for Amount<C> {
/// Amounts with the same value are equal, even if they have different constraints
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<C1, C2> PartialEq<Amount<C2>> for Amount<C1> {
fn eq(&self, other: &Amount<C2>) -> bool {
self.0.eq(&other.0)
}
}
impl Eq for Amount<NegativeAllowed> {}
impl Eq for Amount<NonNegative> {}
impl<C1, C2> PartialOrd<Amount<C2>> for Amount<C1> {
fn partial_cmp(&self, other: &Amount<C2>) -> Option<Ordering> {
Some(self.0.cmp(&other.0))
}
}
impl Ord for Amount<NegativeAllowed> {
fn cmp(&self, other: &Amount<NegativeAllowed>) -> Ordering {
self.0.cmp(&other.0)
}
}
impl Ord for Amount<NonNegative> {
fn cmp(&self, other: &Amount<NonNegative>) -> Ordering {
self.0.cmp(&other.0)
}
}
impl std::ops::Mul<u64> for Amount<NonNegative> {
type Output = Result<Amount<NonNegative>>;
fn mul(self, rhs: u64) -> Self::Output {
let value = (self.0 as u64)
.checked_mul(rhs)
.ok_or(Error::MultiplicationOverflow {
amount: self.0,
multiplier: rhs,
})?;
value.try_into()
}
}
impl std::ops::Mul<Amount<NonNegative>> for u64 {
type Output = Result<Amount<NonNegative>>;
fn mul(self, rhs: Amount<NonNegative>) -> Self::Output {
rhs.mul(self)
}
}
impl std::ops::Div<u64> for Amount<NonNegative> {
type Output = Result<Amount<NonNegative>>;
fn div(self, rhs: u64) -> Self::Output {
let quotient = (self.0 as u64)
.checked_div(rhs)
.ok_or(Error::DivideByZero { amount: self.0 })?;
Ok(quotient
.try_into()
.expect("division by a positive integer always stays within the constraint"))
}
}
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)]
#[allow(missing_docs)]
/// Errors that can be returned when validating `Amount`s
@ -205,6 +276,10 @@ pub enum Error {
value: u64,
source: std::num::TryFromIntError,
},
/// i64 overflow when multiplying i64 non-negative amount {amount} by u64 {multiplier}
MultiplicationOverflow { amount: i64, multiplier: u64 },
/// cannot divide amount {amount} by zero
DivideByZero { amount: i64 },
}
/// Marker type for `Amount` that allows negative values.
@ -243,8 +318,11 @@ impl Constraint for NonNegative {
}
}
/// Number of zatoshis in 1 ZEC
pub const COIN: i64 = 100_000_000;
/// The maximum zatoshi amount.
pub const MAX_MONEY: i64 = 21_000_000 * 100_000_000;
pub const MAX_MONEY: i64 = 21_000_000 * COIN;
/// A trait for defining constraints on `Amount`
pub trait Constraint {
@ -315,6 +393,11 @@ where
#[cfg(test)]
mod test {
use super::*;
use std::{
collections::hash_map::RandomState, collections::HashSet, fmt::Debug, iter::FromIterator,
};
use color_eyre::eyre::Result;
#[test]
@ -486,4 +569,78 @@ mod test {
Ok(())
}
#[test]
fn hash() -> Result<()> {
let one = Amount::<NonNegative>::try_from(1)?;
let another_one = Amount::<NonNegative>::try_from(1)?;
let zero = Amount::<NonNegative>::try_from(0)?;
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
HashSet::from_iter([one].iter().cloned());
assert_eq!(hash_set.len(), 1);
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
HashSet::from_iter([one, one].iter().cloned());
assert_eq!(hash_set.len(), 1, "Amount hashes are consistent");
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
HashSet::from_iter([one, another_one].iter().cloned());
assert_eq!(hash_set.len(), 1, "Amount hashes are by value");
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
HashSet::from_iter([one, zero].iter().cloned());
assert_eq!(
hash_set.len(),
2,
"Amount hashes are different for different values"
);
Ok(())
}
#[test]
fn ordering_constraints() -> Result<()> {
ordering::<NonNegative, NonNegative>()?;
ordering::<NonNegative, NegativeAllowed>()?;
ordering::<NegativeAllowed, NonNegative>()?;
ordering::<NegativeAllowed, NegativeAllowed>()?;
Ok(())
}
fn ordering<C1, C2>() -> Result<()>
where
C1: Constraint + Debug,
C2: Constraint + Debug,
{
let zero = Amount::<C1>::try_from(0)?;
let one = Amount::<C2>::try_from(1)?;
let another_one = Amount::<C1>::try_from(1)?;
assert_eq!(one, one);
assert_eq!(one, another_one, "Amount equality is by value");
assert_ne!(one, zero);
assert_ne!(zero, one);
assert!(one > zero);
assert!(zero < one);
assert!(zero <= one);
let negative_one = Amount::<NegativeAllowed>::try_from(-1)?;
let negative_two = Amount::<NegativeAllowed>::try_from(-2)?;
assert_ne!(negative_one, zero);
assert_ne!(negative_one, one);
assert!(negative_one < zero);
assert!(negative_one <= one);
assert!(zero > negative_one);
assert!(zero >= negative_one);
assert!(negative_two < negative_one);
assert!(negative_one > negative_two);
Ok(())
}
}

View File

@ -1,6 +1,9 @@
use crate::serialization::SerializationError;
use std::ops::{Add, Sub};
use std::{
convert::TryFrom,
ops::{Add, Sub},
};
/// The height of a block is the length of the chain back to the genesis block.
///
@ -47,19 +50,51 @@ impl Height {
pub const MAX_AS_U32: u32 = Self::MAX.0;
}
impl Add<Height> for Height {
type Output = Option<Height>;
fn add(self, rhs: Height) -> Option<Height> {
// We know that both values are positive integers. Therefore, the result is
// positive, and we can skip the conversions. The checked_add is required,
// because the result may overflow.
let height = self.0.checked_add(rhs.0)?;
let height = Height(height);
if height <= Height::MAX && height >= Height::MIN {
Some(height)
} else {
None
}
}
}
impl Sub<Height> for Height {
type Output = i32;
/// Panics if the inputs or result are outside the valid i32 range.
fn sub(self, rhs: Height) -> i32 {
(self.0 as i32) - (rhs.0 as i32)
// We construct heights from integers without any checks,
// so the inputs or result could be out of range.
let lhs = i32::try_from(self.0)
.expect("out of range input `self`: inputs should be valid Heights");
let rhs =
i32::try_from(rhs.0).expect("out of range input `rhs`: inputs should be valid Heights");
lhs.checked_sub(rhs)
.expect("out of range result: valid input heights should yield a valid result")
}
}
// We don't implement Add<u32> or Sub<u32>, because they cause type inference issues for integer constants.
impl Add<i32> for Height {
type Output = Option<Height>;
fn add(self, rhs: i32) -> Option<Height> {
let result = ((self.0 as i32) + rhs) as u32;
// Because we construct heights from integers without any checks,
// the input values could be outside the valid range for i32.
let lhs = i32::try_from(self.0).ok()?;
let result = lhs.checked_add(rhs)?;
let result = u32::try_from(result).ok()?;
match result {
h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)),
_ => None,
@ -71,7 +106,10 @@ impl Sub<i32> for Height {
type Output = Option<Height>;
fn sub(self, rhs: i32) -> Option<Height> {
let result = ((self.0 as i32) - rhs) as u32;
// These checks are required, see above for details.
let lhs = i32::try_from(self.0).ok()?;
let result = lhs.checked_sub(rhs)?;
let result = u32::try_from(result).ok()?;
match result {
h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)),
_ => None,
@ -94,14 +132,52 @@ impl Arbitrary for Height {
#[test]
fn operator_tests() {
assert_eq!(Some(Height(2)), Height(1) + Height(1));
assert_eq!(None, Height::MAX + Height(1));
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) + Height(0));
assert_eq!(None, Height(i32::MAX as u32) + Height(0));
assert_eq!(None, Height(u32::MAX) + Height(0));
assert_eq!(Some(Height(2)), Height(1) + 1);
assert_eq!(None, Height::MAX + 1);
// Adding negative numbers
assert_eq!(Some(Height(1)), Height(2) + -1);
assert_eq!(Some(Height(0)), Height(1) + -1);
assert_eq!(None, Height(0) + -1);
assert_eq!(Some(Height(Height::MAX_AS_U32 - 1)), Height::MAX + -1);
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
// `+ 0` would also cause an error here, but it triggers a spurious clippy lint
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) + 1);
assert_eq!(None, Height(i32::MAX as u32) + 1);
assert_eq!(None, Height(u32::MAX) + 1);
// Adding negative numbers
assert_eq!(None, Height(i32::MAX as u32) + -1);
assert_eq!(None, Height(u32::MAX) + -1);
assert_eq!(Some(Height(1)), Height(2) - 1);
assert_eq!(Some(Height(0)), Height(1) - 1);
assert_eq!(None, Height(0) - 1);
assert_eq!(Some(Height(Height::MAX_AS_U32 - 1)), Height::MAX - 1);
// Subtracting negative numbers
assert_eq!(Some(Height(2)), Height(1) - -1);
assert_eq!(Some(Height::MAX), Height(Height::MAX_AS_U32 - 1) - -1);
assert_eq!(None, Height::MAX - -1);
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
assert_eq!(None, Height(i32::MAX as u32) - 1);
assert_eq!(None, Height(u32::MAX) - 1);
// Subtracting negative numbers
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) - -1);
assert_eq!(None, Height(i32::MAX as u32) - -1);
assert_eq!(None, Height(u32::MAX) - -1);
// Sub<Height> panics on out of range errors
assert_eq!(1, Height(2) - Height(1));
assert_eq!(0, Height(1) - Height(1));
assert_eq!(-1, Height(0) - Height(1));
assert_eq!(-5, Height(2) - Height(7));
assert_eq!(Height::MAX_AS_U32 as i32, Height::MAX - Height(0));
assert_eq!(1, Height::MAX - Height(Height::MAX_AS_U32 - 1));
assert_eq!(-1, Height(Height::MAX_AS_U32 - 1) - Height::MAX);
assert_eq!(-(Height::MAX_AS_U32 as i32), Height(0) - Height::MAX);
}

View File

@ -22,6 +22,7 @@ use tower::{Service, ServiceExt};
use zebra_chain::{
block::{self, Block},
parameters::Network,
work::equihash,
};
use zebra_state as zs;
@ -30,6 +31,7 @@ use crate::error::*;
use crate::BoxError;
mod check;
mod subsidy;
#[cfg(test)]
mod tests;
@ -40,6 +42,9 @@ where
S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
S::Future: Send + 'static,
{
/// The network to be verified.
network: Network,
/// The underlying state service, possibly wrapped in other services.
state_service: S,
}
@ -70,8 +75,11 @@ where
S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
S::Future: Send + 'static,
{
pub fn new(state_service: S) -> Self {
Self { state_service }
pub fn new(network: Network, state_service: S) -> Self {
Self {
network,
state_service,
}
}
}
@ -94,6 +102,7 @@ where
fn call(&mut self, block: Arc<Block>) -> Self::Future {
let mut state_service = self.state_service.clone();
let network = self.network;
// TODO(jlusby): Error = Report, handle errors from state_service.
async move {
@ -138,15 +147,16 @@ where
difficulty_threshold,
))?;
}
check::is_equihash_solution_valid(&block.header)?;
check::equihash_solution_is_valid(&block.header)?;
// Since errors cause an early exit, try to do the
// quick checks first.
// Field validity and structure checks
let now = Utc::now();
check::is_time_valid_at(&block.header, now).map_err(VerifyBlockError::Time)?;
check::is_coinbase_first(&block)?;
check::time_is_valid_at(&block.header, now).map_err(VerifyBlockError::Time)?;
check::coinbase_is_first(&block)?;
check::subsidy_is_correct(network, &block)?;
// TODO: context-free header verification: merkle root

View File

@ -4,11 +4,16 @@ use chrono::{DateTime, Utc};
use zebra_chain::{
block::{Block, Header},
parameters::NetworkUpgrade,
work::equihash,
};
use crate::error::*;
use crate::BoxError;
use crate::{error::*, parameters::SLOW_START_INTERVAL};
use zebra_chain::parameters::Network;
use super::subsidy;
/// Check that there is exactly one coinbase transaction in `Block`, and that
/// the coinbase transaction is the first transaction in the block.
@ -18,7 +23,7 @@ use crate::BoxError;
/// fees paid by transactions included in this block." [§3.10][3.10]
///
/// [3.10]: https://zips.z.cash/protocol/protocol.pdf#coinbasetransactions
pub fn is_coinbase_first(block: &Block) -> Result<(), BlockError> {
pub fn coinbase_is_first(block: &Block) -> Result<(), BlockError> {
let first = block
.transactions
.get(0)
@ -35,7 +40,7 @@ pub fn is_coinbase_first(block: &Block) -> Result<(), BlockError> {
}
/// Returns true if the header is valid based on its `EquihashSolution`
pub fn is_equihash_solution_valid(header: &Header) -> Result<(), equihash::Error> {
pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error> {
header.solution.check(&header)
}
@ -53,6 +58,49 @@ pub fn is_equihash_solution_valid(header: &Header) -> Result<(), equihash::Error
/// accepted." [§7.5][7.5]
///
/// [7.5]: https://zips.z.cash/protocol/protocol.pdf#blockheader
pub fn is_time_valid_at(header: &Header, now: DateTime<Utc>) -> Result<(), BoxError> {
pub fn time_is_valid_at(header: &Header, now: DateTime<Utc>) -> Result<(), BoxError> {
header.is_time_valid_at(now)
}
/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts
pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockError> {
let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?;
let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?;
let halving_div = subsidy::general::halving_divisor(height, network);
let canopy_activation_height = NetworkUpgrade::Canopy
.activation_height(network)
.expect("Canopy activation height is known");
// TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees
// Check founders reward and funding streams
if height < SLOW_START_INTERVAL {
unreachable!(
"unsupported block height: callers should handle blocks below {:?}",
SLOW_START_INTERVAL
)
} else if halving_div.count_ones() != 1 {
unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two")
} else if height < canopy_activation_height {
// Founders rewards are paid up to Canopy activation, on both mainnet and testnet
let founders_reward = subsidy::founders_reward::founders_reward(height, network)
.expect("invalid Amount: founders reward should be valid");
let matching_values = subsidy::general::find_output_with_amount(coinbase, founders_reward);
// TODO: the exact founders reward value must be sent as a single output to the correct address
if !matching_values.is_empty() {
Ok(())
} else {
Err(SubsidyError::FoundersRewardNotFound)?
}
} else if halving_div < 4 {
// Funding streams are paid from Canopy activation to the second halving
// Note: Canopy activation is at the first halving on mainnet, but not on testnet
// ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet
unimplemented!("funding stream block subsidy validation is not implemented")
} else {
// Future halving, with no founders reward or funding streams
Ok(())
}
}

View File

@ -0,0 +1,8 @@
//! Validate coinbase transaction rewards as described in [§7.7][7.7]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
/// Founders Reward functions apply for blocks before Canopy.
pub mod founders_reward;
/// General subsidy functions apply for blocks after slow-start mining.
pub mod general;

View File

@ -0,0 +1,61 @@
//! Founders Reward calculations. - [§7.7][7.7]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use std::convert::TryFrom;
use zebra_chain::{
amount::{Amount, Error, NonNegative},
block::Height,
parameters::Network,
};
use crate::block::subsidy::general::{block_subsidy, halving_divisor};
use crate::parameters::subsidy::FOUNDERS_FRACTION_DIVISOR;
/// `FoundersReward(height)` as described in [protocol specification §7.7][7.7]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn founders_reward(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
if halving_divisor(height, network) == 1 {
// this calculation is exact, because the block subsidy is divisible by
// the FOUNDERS_FRACTION_DIVISOR until long after the first halving
block_subsidy(height, network)? / FOUNDERS_FRACTION_DIVISOR
} else {
Amount::try_from(0)
}
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::Report;
use zebra_chain::parameters::NetworkUpgrade::*;
#[test]
fn test_founders_reward() -> Result<(), Report> {
let network = Network::Mainnet;
let blossom_height = Blossom.activation_height(network).unwrap();
let canopy_height = Canopy.activation_height(network).unwrap();
// Founders reward is 20% of the block subsidy
// https://z.cash/support/faq/#founders-reward-recipients
// Before Blossom this is 20*12.5/100 = 2.5 ZEC
assert_eq!(
Amount::try_from(250_000_000),
founders_reward((blossom_height - 1).unwrap(), network)
);
// Founders reward is still 20% of the block subsidy but the block reward is half what it was
// After Blossom this is 20*6.25/100 = 1.25 ZEC
// https://z.cash/upgrade/blossom/
assert_eq!(
Amount::try_from(125_000_000),
founders_reward(blossom_height, network)
);
// After first halving(coinciding with Canopy) founders reward will expire
// https://z.cash/support/faq/#does-the-founders-reward-expire
assert_eq!(Amount::try_from(0), founders_reward(canopy_height, network));
Ok(())
}
}

View File

@ -0,0 +1,326 @@
//! Block and Miner subsidies, halvings and target spacing modifiers. - [§7.7][7.7]
//!
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
use std::convert::TryFrom;
use zebra_chain::{
amount::{Amount, Error, NonNegative},
block::Height,
parameters::{Network, NetworkUpgrade::*},
transaction::Transaction,
transparent,
};
use crate::parameters::subsidy::*;
/// The divisor used for halvings.
///
/// `1 << Halving(height)`, as described in [protocol specification §7.7][7.7]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn halving_divisor(height: Height, network: Network) -> u64 {
let blossom_height = Blossom
.activation_height(network)
.expect("blossom activation height should be available");
if height < SLOW_START_SHIFT {
unreachable!(
"unsupported block height: callers should handle blocks below {:?}",
SLOW_START_SHIFT
)
} else if height < blossom_height {
let scaled_pre_blossom_height = (height - SLOW_START_SHIFT) as u64;
let halving_shift = scaled_pre_blossom_height / (PRE_BLOSSOM_HALVING_INTERVAL.0 as u64);
1 << halving_shift
} else {
let scaled_pre_blossom_height =
(blossom_height - SLOW_START_SHIFT) as u64 * BLOSSOM_POW_TARGET_SPACING_RATIO;
let post_blossom_height = (height - blossom_height) as u64;
let halving_shift = (scaled_pre_blossom_height + post_blossom_height)
/ (POST_BLOSSOM_HALVING_INTERVAL.0 as u64);
1 << halving_shift
}
}
/// `BlockSubsidy(height)` as described in [protocol specification §7.7][7.7]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
pub fn block_subsidy(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
let blossom_height = Blossom
.activation_height(network)
.expect("blossom activation height should be available");
let halving_div = halving_divisor(height, network);
if height < SLOW_START_INTERVAL {
unreachable!(
"unsupported block height: callers should handle blocks below {:?}",
SLOW_START_INTERVAL
)
} else if height < blossom_height {
// this calculation is exact, because the halving divisor is 1 here
Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)
} else {
let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO;
// in future halvings, this calculation might not be exact
// Amount division is implemented using integer division,
// which truncates (rounds down) the result, as specified
Amount::try_from(scaled_max_block_subsidy / halving_div)
}
}
/// `MinerSubsidy(height)` as described in [protocol specification §7.7][7.7]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
///
/// `non_miner_reward` is the founders reward or funding stream value.
/// If all the rewards for a block go to the miner, use `None`.
#[allow(dead_code)]
pub fn miner_subsidy(
height: Height,
network: Network,
non_miner_reward: Option<Amount<NonNegative>>,
) -> Result<Amount<NonNegative>, Error> {
if let Some(non_miner_reward) = non_miner_reward {
block_subsidy(height, network)? - non_miner_reward
} else {
block_subsidy(height, network)
}
}
/// Returns a list of outputs in `Transaction`, which have a value equal to `Amount`.
pub fn find_output_with_amount(
transaction: &Transaction,
amount: Amount<NonNegative>,
) -> Vec<transparent::Output> {
// TODO: shielded coinbase - Heartwood
transaction
.outputs()
.iter()
.filter(|o| o.value == amount)
.cloned()
.collect()
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::Report;
#[test]
fn halving_test() -> Result<(), Report> {
halving_for_network(Network::Mainnet)?;
halving_for_network(Network::Testnet)?;
Ok(())
}
fn halving_for_network(network: Network) -> Result<(), Report> {
let blossom_height = Blossom.activation_height(network).unwrap();
let first_halving_height = match network {
Network::Mainnet => Canopy.activation_height(network).unwrap(),
// Based on "7.7 Calculation of Block Subsidy and Founders Reward"
Network::Testnet => Height(1_116_000),
};
assert_eq!(1, halving_divisor((blossom_height - 1).unwrap(), network));
assert_eq!(1, halving_divisor(blossom_height, network));
assert_eq!(
1,
halving_divisor((first_halving_height - 1).unwrap(), network)
);
assert_eq!(2, halving_divisor(first_halving_height, network));
assert_eq!(
2,
halving_divisor((first_halving_height + 1).unwrap(), network)
);
assert_eq!(
4,
halving_divisor(
(first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(),
network
)
);
assert_eq!(
8,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 2)).unwrap(),
network
)
);
assert_eq!(
1024,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 9)).unwrap(),
network
)
);
assert_eq!(
1024 * 1024,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 19)).unwrap(),
network
)
);
assert_eq!(
1024 * 1024 * 1024,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 29)).unwrap(),
network
)
);
assert_eq!(
1024 * 1024 * 1024 * 1024,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 39)).unwrap(),
network
)
);
// The largest possible divisor
assert_eq!(
1 << 63,
halving_divisor(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 62)).unwrap(),
network
)
);
Ok(())
}
#[test]
fn block_subsidy_test() -> Result<(), Report> {
block_subsidy_for_network(Network::Mainnet)?;
block_subsidy_for_network(Network::Testnet)?;
Ok(())
}
fn block_subsidy_for_network(network: Network) -> Result<(), Report> {
let blossom_height = Blossom.activation_height(network).unwrap();
let first_halving_height = match network {
Network::Mainnet => Canopy.activation_height(network).unwrap(),
// Based on "7.7 Calculation of Block Subsidy and Founders Reward"
Network::Testnet => Height(1_116_000),
};
// After slow-start mining and before Blossom the block subsidy is 12.5 ZEC
// https://z.cash/support/faq/#what-is-slow-start-mining
assert_eq!(
Amount::try_from(1_250_000_000),
block_subsidy((blossom_height - 1).unwrap(), network)
);
// After Blossom the block subsidy is reduced to 6.25 ZEC without halving
// https://z.cash/upgrade/blossom/
assert_eq!(
Amount::try_from(625_000_000),
block_subsidy(blossom_height, network)
);
// After the 1st halving, the block subsidy is reduced to 3.125 ZEC
// https://z.cash/upgrade/canopy/
assert_eq!(
Amount::try_from(312_500_000),
block_subsidy(first_halving_height, network)
);
// After the 2nd halving, the block subsidy is reduced to 1.5625 ZEC
// See "7.7 Calculation of Block Subsidy and Founders Reward"
assert_eq!(
Amount::try_from(156_250_000),
block_subsidy(
(first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(),
network
)
);
// After the 7th halving, the block subsidy is reduced to 0.04882812 ZEC
// Check that the block subsidy rounds down correctly, and there are no errors
assert_eq!(
Amount::try_from(4_882_812),
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 6)).unwrap(),
network
)
);
// After the 29th halving, the block subsidy is 1 zatoshi
// Check that the block subsidy is calculated correctly at the limit
assert_eq!(
Amount::try_from(1),
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 28)).unwrap(),
network
)
);
// After the 30th halving, there is no block subsidy
// Check that there are no errors
assert_eq!(
Amount::try_from(0),
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 29)).unwrap(),
network
)
);
// The largest possible divisor
assert_eq!(
Amount::try_from(0),
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 62)).unwrap(),
network
)
);
Ok(())
}
#[test]
fn miner_subsidy_test() -> Result<(), Report> {
miner_subsidy_for_network(Network::Mainnet)?;
miner_subsidy_for_network(Network::Testnet)?;
Ok(())
}
fn miner_subsidy_for_network(network: Network) -> Result<(), Report> {
use crate::block::subsidy::founders_reward::founders_reward;
let blossom_height = Blossom.activation_height(network).unwrap();
// Miner reward before Blossom is 80% of the total block reward
// 80*12.5/100 = 10 ZEC
let founders_amount = founders_reward((blossom_height - 1).unwrap(), network)?;
assert_eq!(
Amount::try_from(1_000_000_000),
miner_subsidy(
(blossom_height - 1).unwrap(),
network,
Some(founders_amount)
)
);
// After blossom the total block reward is "halved", miner still get 80%
// 80*6.25/100 = 5 ZEC
let founders_amount = founders_reward(blossom_height, network)?;
assert_eq!(
Amount::try_from(500_000_000),
miner_subsidy(blossom_height, network, Some(founders_amount))
);
// TODO: After first halving, miner will get 2.5 ZEC per mined block
// but we need funding streams code to get this number
// TODO: After second halving, there will be no funding streams, and
// miners will get all the block reward
// TODO: also add some very large halvings
Ok(())
}
}

View File

@ -1,5 +1,7 @@
//! Tests for block verification
use crate::parameters::SLOW_START_INTERVAL;
use super::*;
use std::sync::Arc;
@ -11,7 +13,7 @@ use tower::buffer::Buffer;
use zebra_chain::block::{self, Block};
use zebra_chain::{
parameters::Network,
parameters::{Network, NetworkUpgrade},
serialization::{ZcashDeserialize, ZcashDeserializeInto},
};
use zebra_test::transcript::{TransError, Transcript};
@ -113,7 +115,7 @@ async fn check_transcripts() -> Result<(), Report> {
let network = Network::Mainnet;
let state_service = zebra_state::init(zebra_state::Config::ephemeral(), network);
let block_verifier = Buffer::new(BlockVerifier::new(state_service.clone()), 1);
let block_verifier = Buffer::new(BlockVerifier::new(network, state_service.clone()), 1);
for transcript_data in &[
&VALID_BLOCK_TRANSCRIPT,
@ -140,6 +142,106 @@ fn time_check_past_block() {
// a long time in the past. So it's unlikely that the test machine
// will have a clock that's far enough in the past for the test to
// fail.
check::is_time_valid_at(&block.header, now)
check::time_is_valid_at(&block.header, now)
.expect("the header time from a mainnet block should be valid");
}
#[test]
fn subsidy_is_correct_test() -> Result<(), Report> {
subsidy_is_correct_for_network(Network::Mainnet)?;
subsidy_is_correct_for_network(Network::Testnet)?;
Ok(())
}
fn subsidy_is_correct_for_network(network: Network) -> Result<(), Report> {
let block_iter = match network {
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
};
for (&height, block) in block_iter {
let block = block
.zcash_deserialize_into::<Block>()
.expect("block is structurally valid");
// TODO: first halving, second halving, third halving, and very large halvings
if block::Height(height) > SLOW_START_INTERVAL
&& block::Height(height) < NetworkUpgrade::Canopy.activation_height(network).unwrap()
{
check::subsidy_is_correct(network, &block)
.expect("subsidies should pass for this block");
}
}
Ok(())
}
#[test]
fn nocoinbase_validation_failure() -> Result<(), Report> {
use crate::error::*;
let network = Network::Mainnet;
// Get a header form a block in the mainnet that is inside the founders reward period.
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");
// Remove coinbase transaction
block.transactions.remove(0);
// Validate the block
let result = check::subsidy_is_correct(network, &block).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase));
assert_eq!(expected, result);
Ok(())
}
#[test]
fn founders_reward_validation_failure() -> Result<(), Report> {
use crate::error::*;
use zebra_chain::transaction::Transaction;
let network = Network::Mainnet;
// Get a header from a block in the mainnet that is inside the founders reward period.
let header =
block::Header::zcash_deserialize(&zebra_test::vectors::HEADER_MAINNET_415000_BYTES[..])
.unwrap();
// From the same block get the coinbase transaction
let block =
Arc::<Block>::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_415000_BYTES[..])
.expect("block should deserialize");
// Build the new transaction with modified coinbase outputs
let tx = block
.transactions
.get(0)
.map(|transaction| Transaction::V3 {
inputs: transaction.inputs().to_vec(),
outputs: vec![transaction.outputs()[0].clone()],
lock_time: transaction.lock_time(),
expiry_height: transaction.expiry_height().unwrap(),
joinsplit_data: None,
})
.unwrap();
// Build new block
let mut transactions: Vec<Arc<zebra_chain::transaction::Transaction>> = Vec::new();
transactions.push(Arc::new(tx));
let block = Block {
header,
transactions,
};
// Validate it
let result = check::subsidy_is_correct(network, &block).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::FoundersRewardNotFound,
));
assert_eq!(expected, result);
Ok(())
}

View File

@ -169,7 +169,7 @@ where
};
tracing::info!(?tip, ?max_checkpoint_height, "initializing chain verifier");
let block = BlockVerifier::new(state_service.clone());
let block = BlockVerifier::new(network, state_service.clone());
let checkpoint = CheckpointVerifier::from_checkpoint_list(list, tip, state_service);
Buffer::new(

View File

@ -2,16 +2,34 @@
use thiserror::Error;
#[derive(Error, Debug)]
#[derive(Error, Debug, PartialEq)]
pub enum SubsidyError {
#[error("no coinbase transaction in block")]
NoCoinbase,
#[error("founders reward output not found")]
FoundersRewardNotFound,
}
#[derive(Error, Debug, PartialEq)]
pub enum TransactionError {
#[error("first transaction must be coinbase")]
CoinbasePosition,
#[error("coinbase input found in non-coinbase transaction")]
CoinbaseInputFound,
#[error("coinbase transaction failed subsidy validation")]
Subsidy(#[from] SubsidyError),
}
#[derive(Error, Debug)]
impl From<SubsidyError> for BlockError {
fn from(err: SubsidyError) -> BlockError {
BlockError::Transaction(TransactionError::Subsidy(err))
}
}
#[derive(Error, Debug, PartialEq)]
pub enum BlockError {
#[error("block contains invalid transactions")]
Transaction(#[from] TransactionError),

View File

@ -14,9 +14,11 @@
pub mod genesis;
pub mod minimum_difficulty;
pub mod subsidy;
pub use genesis::*;
pub use minimum_difficulty::*;
pub use subsidy::*;
#[cfg(test)]
mod tests;

View File

@ -0,0 +1,49 @@
//! Constants for Block Subsidy, Funding Streams, and Founders Reward
use std::time::Duration;
use zebra_chain::{amount::COIN, block::Height};
/// An initial period from Genesis to this Height where the block subsidy is gradually incremented. [What is slow-start mining][slow-mining]
///
/// [slow-mining]: https://z.cash/support/faq/#what-is-slow-start-mining
pub const SLOW_START_INTERVAL: Height = Height(20_000);
/// `SlowStartShift()` as described in [protocol specification §7.7][7.7]
///
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
///
/// This calculation is exact, because `SLOW_START_INTERVAL` is divisible by 2.
pub const SLOW_START_SHIFT: Height = Height(SLOW_START_INTERVAL.0 / 2);
/// The largest block subsidy, used before the first halving.
///
/// We use `25 / 2` instead of `12.5`, so that we can calculate the correct value without using floating-point.
/// This calculation is exact, because COIN is divisible by 2, and the division is done last.
pub const MAX_BLOCK_SUBSIDY: u64 = ((25 * COIN) / 2) as u64;
/// The blocktime before Blossom, used to calculate ratio.
pub const PRE_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(150);
/// The blocktime after Blossom, used to calculate ratio.
pub const POST_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(75);
/// Used as a multiplier to get the new halving interval after Blossom.
pub const BLOSSOM_POW_TARGET_SPACING_RATIO: u64 =
PRE_BLOSSOM_POW_TARGET_SPACING.as_secs() / POST_BLOSSOM_POW_TARGET_SPACING.as_secs();
/// Halving is at about every 4 years, before Blossom block time is 150 seconds.
///
/// `(60 * 60 * 24 * 365 * 4) / 150 = 840960`
pub const PRE_BLOSSOM_HALVING_INTERVAL: Height = Height(840_000);
/// After Blossom the block time is reduced to 75 seconds but halving period should remain around 4 years.
pub const POST_BLOSSOM_HALVING_INTERVAL: Height =
Height((PRE_BLOSSOM_HALVING_INTERVAL.0 as u64 * BLOSSOM_POW_TARGET_SPACING_RATIO) as u32);
/// The divisor used to calculate the FoundersFraction.
///
/// Derivation: FOUNDERS_FRACTION_DIVISOR = 1/FoundersFraction
///
/// Usage: founders_reward = block_subsidy / FOUNDERS_FRACTION_DIVISOR
pub const FOUNDERS_FRACTION_DIVISOR: u64 = 5;