From c93f0b3a2efd84f41c8fba6902908ff030f2ff3e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 12 Oct 2020 17:54:48 -0300 Subject: [PATCH] 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 Co-authored-by: Jane Lusby * 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 for Height And make the existing Height operators do range checks. * Apply operator suggestions Co-authored-by: Jane Lusby --- zebra-chain/src/amount.rs | 161 ++++++++- zebra-chain/src/block/height.rs | 84 ++++- zebra-consensus/src/block.rs | 20 +- zebra-consensus/src/block/check.rs | 56 ++- zebra-consensus/src/block/subsidy.rs | 8 + .../src/block/subsidy/founders_reward.rs | 61 ++++ zebra-consensus/src/block/subsidy/general.rs | 326 ++++++++++++++++++ zebra-consensus/src/block/tests.rs | 108 +++++- zebra-consensus/src/chain.rs | 2 +- zebra-consensus/src/error.rs | 22 +- zebra-consensus/src/parameters.rs | 2 + zebra-consensus/src/parameters/subsidy.rs | 49 +++ 12 files changed, 878 insertions(+), 21 deletions(-) create mode 100644 zebra-consensus/src/block/subsidy.rs create mode 100644 zebra-consensus/src/block/subsidy/founders_reward.rs create mode 100644 zebra-consensus/src/block/subsidy/general.rs create mode 100644 zebra-consensus/src/parameters/subsidy.rs diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index d65df02f1..0cf8ddd84 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -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 = std::result::Result; /// 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(i64, PhantomData); @@ -191,6 +193,75 @@ where } } +impl Hash for Amount { + /// Amounts with the same value are equal, even if they have different constraints + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl PartialEq> for Amount { + fn eq(&self, other: &Amount) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for Amount {} +impl Eq for Amount {} + +impl PartialOrd> for Amount { + fn partial_cmp(&self, other: &Amount) -> Option { + Some(self.0.cmp(&other.0)) + } +} + +impl Ord for Amount { + fn cmp(&self, other: &Amount) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl Ord for Amount { + fn cmp(&self, other: &Amount) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl std::ops::Mul for Amount { + type Output = Result>; + + 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> for u64 { + type Output = Result>; + + fn mul(self, rhs: Amount) -> Self::Output { + rhs.mul(self) + } +} + +impl std::ops::Div for Amount { + type Output = Result>; + + 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::::try_from(1)?; + let another_one = Amount::::try_from(1)?; + let zero = Amount::::try_from(0)?; + + let hash_set: HashSet, RandomState> = + HashSet::from_iter([one].iter().cloned()); + assert_eq!(hash_set.len(), 1); + + let hash_set: HashSet, RandomState> = + HashSet::from_iter([one, one].iter().cloned()); + assert_eq!(hash_set.len(), 1, "Amount hashes are consistent"); + + let hash_set: HashSet, RandomState> = + HashSet::from_iter([one, another_one].iter().cloned()); + assert_eq!(hash_set.len(), 1, "Amount hashes are by value"); + + let hash_set: HashSet, 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::()?; + ordering::()?; + ordering::()?; + ordering::()?; + + Ok(()) + } + + fn ordering() -> Result<()> + where + C1: Constraint + Debug, + C2: Constraint + Debug, + { + let zero = Amount::::try_from(0)?; + let one = Amount::::try_from(1)?; + let another_one = Amount::::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::::try_from(-1)?; + let negative_two = Amount::::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(()) + } } diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index d10afce16..a9465db5f 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -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 for Height { + type Output = Option; + + fn add(self, rhs: Height) -> Option { + // 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 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 or Sub, because they cause type inference issues for integer constants. + impl Add for Height { type Output = Option; fn add(self, rhs: i32) -> Option { - 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 for Height { type Output = Option; fn sub(self, rhs: i32) -> Option { - 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 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); } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 6d1c8c075..b170d0a31 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -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 + 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 + 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) -> 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 diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 8a446a419..d993e6ce2 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -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) -> Result<(), BoxError> { +pub fn time_is_valid_at(header: &Header, now: DateTime) -> 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(()) + } +} diff --git a/zebra-consensus/src/block/subsidy.rs b/zebra-consensus/src/block/subsidy.rs new file mode 100644 index 000000000..ec702088e --- /dev/null +++ b/zebra-consensus/src/block/subsidy.rs @@ -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; diff --git a/zebra-consensus/src/block/subsidy/founders_reward.rs b/zebra-consensus/src/block/subsidy/founders_reward.rs new file mode 100644 index 000000000..953968487 --- /dev/null +++ b/zebra-consensus/src/block/subsidy/founders_reward.rs @@ -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, 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(()) + } +} diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs new file mode 100644 index 000000000..f8970db38 --- /dev/null +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -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, 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>, +) -> Result, 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, +) -> Vec { + // 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(()) + } +} diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 64ff82bb0..184d76e3f 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -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::() + .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::::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::::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> = 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(()) +} diff --git a/zebra-consensus/src/chain.rs b/zebra-consensus/src/chain.rs index ecb37fce6..a529b7be6 100644 --- a/zebra-consensus/src/chain.rs +++ b/zebra-consensus/src/chain.rs @@ -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( diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 7c1e60fa5..6e9b04d90 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -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 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), diff --git a/zebra-consensus/src/parameters.rs b/zebra-consensus/src/parameters.rs index 710f8f0ed..7fb59db47 100644 --- a/zebra-consensus/src/parameters.rs +++ b/zebra-consensus/src/parameters.rs @@ -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; diff --git a/zebra-consensus/src/parameters/subsidy.rs b/zebra-consensus/src/parameters/subsidy.rs new file mode 100644 index 000000000..03894b311 --- /dev/null +++ b/zebra-consensus/src/parameters/subsidy.rs @@ -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;