From feed1503a04f29274bfd06fb583f1dc6d8d630b1 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 15:24:16 -0300 Subject: [PATCH 01/21] add amount operators --- zebra-chain/src/amount.rs | 95 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index d65df02f1c8..3fa8d53d1b1 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -191,6 +191,61 @@ where } } +impl PartialOrd for Amount +where + Amount: Eq, + C: Constraint, +{ + fn partial_cmp(&self, other: &Amount) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Amount +where + Amount: Eq, + C: Constraint, +{ + fn cmp(&self, other: &Amount) -> std::cmp::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 })?; + // since this is a division by a positive integer, + // the quotient must be within the constrained range + Ok(quotient.try_into().unwrap()) + } +} + #[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)] #[allow(missing_docs)] /// Errors that can be returned when validating `Amount`s @@ -205,6 +260,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 }, + /// division by zero is an invalid operation, amount {amount} + DivideByZero { amount: i64 }, } /// Marker type for `Amount` that allows negative values. @@ -243,8 +302,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 { @@ -486,4 +548,35 @@ mod test { Ok(()) } + + #[test] + fn ordering() -> Result<()> { + let one = Amount::::try_from(1)?; + let zero = Amount::::try_from(0)?; + + assert!(one > zero); + assert!(one == one); + assert!(one != zero); + assert!(one >= one); + assert!(zero < one); + assert!(zero == zero); + assert!(zero != one); + assert!(zero <= one); + + let one = Amount::::try_from(1)?; + let zero = Amount::::try_from(0)?; + let negative_one = Amount::::try_from(-1)?; + let negative_two = Amount::::try_from(-2)?; + + assert!(negative_one < zero); + assert!(negative_one == negative_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(()) + } } From c150f49705409bb1fe69d3a5409f59203314be2e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 15:45:50 -0300 Subject: [PATCH 02/21] rename block validation methods --- zebra-consensus/src/block.rs | 6 +++--- zebra-consensus/src/block/check.rs | 6 +++--- zebra-consensus/src/block/tests.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 6d1c8c07525..4b55a493d70 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -138,15 +138,15 @@ 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)?; // TODO: context-free header verification: merkle root diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 8a446a419e3..cc793b267ce 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -18,7 +18,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 +35,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 +53,6 @@ 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) } diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 64ff82bb01a..21f887318da 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -140,6 +140,6 @@ 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"); } From c51e9c8ccb049be3ce59e4efbc249ff6ad182df4 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 15:57:28 -0300 Subject: [PATCH 03/21] add network to block verifier --- zebra-consensus/src/block.rs | 12 ++++++++++-- zebra-consensus/src/block/tests.rs | 2 +- zebra-consensus/src/chain.rs | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 4b55a493d70..68271ee4ed2 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; @@ -40,6 +41,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 +74,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 +101,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 { diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 21f887318da..8b94378076c 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -113,7 +113,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, diff --git a/zebra-consensus/src/chain.rs b/zebra-consensus/src/chain.rs index 1a5c8474ddd..49d2a05dd25 100644 --- a/zebra-consensus/src/chain.rs +++ b/zebra-consensus/src/chain.rs @@ -161,7 +161,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( From 6261820cbc251e10e7e22934385a39d1f31f714e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 16:23:47 -0300 Subject: [PATCH 04/21] add general and founders reward subsidy modules --- zebra-consensus/src/block.rs | 1 + zebra-consensus/src/block/subsidy.rs | 8 + .../src/block/subsidy/founders_reward.rs | 61 ++++++ zebra-consensus/src/block/subsidy/general.rs | 173 ++++++++++++++++++ zebra-consensus/src/parameters.rs | 2 + zebra-consensus/src/parameters/subsidy.rs | 42 +++++ 6 files changed, 287 insertions(+) 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-consensus/src/block.rs b/zebra-consensus/src/block.rs index 68271ee4ed2..1ff089af0f1 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -31,6 +31,7 @@ use crate::error::*; use crate::BoxError; mod check; +mod subsidy; #[cfg(test)] mod tests; diff --git a/zebra-consensus/src/block/subsidy.rs b/zebra-consensus/src/block/subsidy.rs new file mode 100644 index 00000000000..ec702088e38 --- /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 00000000000..9539684878d --- /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 00000000000..a62bfc1c776 --- /dev/null +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -0,0 +1,173 @@ +//! Block and Miner subsidies, halvings and target space 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::*}, +}; + +use super::founders_reward::founders_reward; +use crate::parameters::subsidy::*; + +/// `SlowStartShift()` as described in [protocol specification §7.7][7.7] +/// +/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies +fn slow_start_shift() -> Height { + Height(SLOW_START_INTERVAL.0 / 2) +} + +/// 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 { + if height < SLOW_START_INTERVAL { + panic!("can't verify before block {}", SLOW_START_INTERVAL.0) + } + let blossom_height = Blossom + .activation_height(network) + .expect("blossom activation height should be available"); + if height >= blossom_height { + 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 + } else { + 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 + } +} + +/// `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> { + if height < SLOW_START_INTERVAL { + panic!("can't verify before block {}", SLOW_START_INTERVAL.0) + } + let blossom_height = Blossom + .activation_height(network) + .expect("blossom activation height should be available"); + + let hd = halving_divisor(height, network); + if height >= blossom_height { + let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO; + // in future halvings, this calculation might not be exact + // in those cases, Amount division follows integer division, which truncates (rounds down) the result + Amount::try_from(scaled_max_block_subsidy / hd) + } else { + // this calculation is exact, because the halving divisor is 1 here + Amount::try_from(MAX_BLOCK_SUBSIDY / hd) + } +} + +/// `MinerSubsidy(height)` as described in [protocol specification §7.7][7.7] +/// +/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies +#[allow(dead_code)] +pub fn miner_subsidy(height: Height, network: Network) -> Result, Error> { + let canopy_height = Canopy + .activation_height(network) + .expect("canopy activation height should be available"); + if height >= canopy_height { + panic!("Can't validate Canopy yet"); + } + block_subsidy(height, network)? - founders_reward(height, network)? +} + +#[cfg(test)] +mod test { + use super::*; + use color_eyre::Report; + + #[test] + fn test_halving() -> Result<(), Report> { + let network = Network::Mainnet; + let blossom_height = Blossom.activation_height(network).unwrap(); + let canopy_height = Canopy.activation_height(network).unwrap(); + + assert_eq!(1, halving_divisor((blossom_height - 1).unwrap(), network)); + assert_eq!(1, halving_divisor(blossom_height, network)); + assert_eq!(1, halving_divisor((canopy_height - 1).unwrap(), network)); + assert_eq!(2, halving_divisor(canopy_height, network)); + assert_eq!(2, halving_divisor((canopy_height + 1).unwrap(), network)); + + Ok(()) + } + + #[test] + fn test_block_subsidy() -> Result<(), Report> { + let network = Network::Mainnet; + let blossom_height = Blossom.activation_height(network).unwrap(); + let canopy_height = Canopy.activation_height(network).unwrap(); + + // After slow-start mining and before Blossom the block reward 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 reward 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 1st halving(coinciding with Canopy) the block reward is reduced to 3.125 ZEC + // https://z.cash/upgrade/canopy/ + assert_eq!( + Amount::try_from(312_500_000), + block_subsidy(canopy_height, network) + ); + + Ok(()) + } + + #[test] + fn miner_subsidy_test() -> Result<(), Report> { + let network = Network::Mainnet; + let blossom_height = Blossom.activation_height(network).unwrap(); + let _canopy_height = Canopy.activation_height(network).unwrap(); + + // Miner reward before Blossom is 80% of the total block reward + // 80*12.5/100 = 10 ZEC + assert_eq!( + Amount::try_from(1_000_000_000), + miner_subsidy((blossom_height - 1).unwrap(), network) + ); + + // After blossom the total block reward is "halved", miner still get 80% + // 80*6.25/100 = 5 ZEC + assert_eq!( + Amount::try_from(500_000_000), + miner_subsidy(blossom_height, network) + ); + + // TODO: After halving(and Canopy) miner will get 2.5 ZEC per mined block + // but we need funding streams code to get this number from miner_subsidy + + Ok(()) + } + + #[test] + #[should_panic] + fn miner_subsidy_canopy_test() { + let network = Network::Mainnet; + let canopy_height = Canopy.activation_height(network).unwrap(); + + assert_eq!( + Amount::try_from(1_000_000_000), + miner_subsidy(canopy_height, network) + ) + } +} diff --git a/zebra-consensus/src/parameters.rs b/zebra-consensus/src/parameters.rs index 710f8f0ed80..7fb59db47ae 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 00000000000..98064d90cb8 --- /dev/null +++ b/zebra-consensus/src/parameters/subsidy.rs @@ -0,0 +1,42 @@ +//! 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); + +/// 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; From 3801cb6a107f3f4c43a8dcfb14ec9a81fb02855d Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 18:04:43 -0300 Subject: [PATCH 05/21] validate founders reward --- zebra-consensus/src/block.rs | 3 +- zebra-consensus/src/block/check.rs | 47 ++++++++++++++++++++ zebra-consensus/src/block/subsidy/general.rs | 16 +++++++ zebra-consensus/src/error.rs | 21 +++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 1ff089af0f1..b170d0a3143 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -102,7 +102,7 @@ where fn call(&mut self, block: Arc) -> Self::Future { let mut state_service = self.state_service.clone(); - let _network = self.network; + let network = self.network; // TODO(jlusby): Error = Report, handle errors from state_service. async move { @@ -156,6 +156,7 @@ where let now = Utc::now(); 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 cc793b267ce..e1d66a9cebb 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ + amount::{Amount, NonNegative}, block::{Block, Header}, work::equihash, }; @@ -10,6 +11,11 @@ use zebra_chain::{ use crate::error::*; use crate::BoxError; +use std::convert::TryInto; +use zebra_chain::parameters::{Network, NetworkUpgrade::*}; + +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. /// @@ -56,3 +62,44 @@ pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error 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() + .expect("always called on blocks with a coinbase height"); + + let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?; + let outputs = coinbase.outputs(); + + let canopy_height = Canopy + .activation_height(network) + .ok_or(SubsidyError::NoCanopy)?; + if height >= canopy_height { + panic!("Can't validate Canopy yet"); + } + + // validate founders reward + let mut valid_founders_reward = false; + if height < canopy_height { + let founders_reward = subsidy::founders_reward::founders_reward(height, network) + .expect("founders reward should be always a valid value"); + + let values = || { + outputs + .iter() + .map(|o| o.value.try_into().expect("value will be a valid amount")) + }; + + if values().any(|value: Amount| value == founders_reward) { + valid_founders_reward = true; + } + } + if !valid_founders_reward { + Err(SubsidyError::FoundersRewardNotFound)? + } else { + // TODO: the exact founders reward value must be sent as a single output to the correct address + // TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees + Ok(()) + } +} diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index a62bfc1c776..fa061d6ce1e 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -170,4 +170,20 @@ mod test { miner_subsidy(canopy_height, network) ) } + + #[test] + fn subsidy_is_correct_test() -> Result<(), Report> { + use crate::block::check; + use std::sync::Arc; + use zebra_chain::{block::Block, serialization::ZcashDeserialize}; + + let network = Network::Mainnet; + + for b in &[&zebra_test::vectors::BLOCK_MAINNET_434873_BYTES[..]] { + let block = Arc::::zcash_deserialize(*b)?; + check::subsidy_is_correct(network, &block) + .expect("subsidies should pass for this block"); + } + Ok(()) + } } diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 7c1e60fa59c..7016aad6f68 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -2,6 +2,18 @@ use thiserror::Error; +#[derive(Error, Debug)] +pub enum SubsidyError { + #[error("not a coinbase transaction")] + NoCoinbase, + + #[error("no canopy block configured")] + NoCanopy, + + #[error("founders reward output not found")] + FoundersRewardNotFound, +} + #[derive(Error, Debug)] pub enum TransactionError { #[error("first transaction must be coinbase")] @@ -9,6 +21,15 @@ pub enum TransactionError { #[error("coinbase input found in non-coinbase transaction")] CoinbaseInputFound, + + #[error("coinbase transaction contains invalid subsidy parameters")] + Subsidy(#[from] SubsidyError), +} + +impl From for BlockError { + fn from(err: SubsidyError) -> BlockError { + BlockError::Transaction(TransactionError::Subsidy(err)) + } } #[derive(Error, Debug)] From 9c39594e5511baa6205e7da360f73ac8ba58966e Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 18:56:49 -0300 Subject: [PATCH 06/21] pass all test vectors through current subsidy validation --- zebra-chain/src/amount.rs | 4 --- zebra-consensus/src/block/subsidy/general.rs | 32 ++++++++++++++------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 3fa8d53d1b1..1763cedfc76 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -555,11 +555,8 @@ mod test { let zero = Amount::::try_from(0)?; assert!(one > zero); - assert!(one == one); assert!(one != zero); - assert!(one >= one); assert!(zero < one); - assert!(zero == zero); assert!(zero != one); assert!(zero <= one); @@ -569,7 +566,6 @@ mod test { let negative_two = Amount::::try_from(-2)?; assert!(negative_one < zero); - assert!(negative_one == negative_one); assert!(negative_one != zero); assert!(negative_one <= one); assert!(zero > negative_one); diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index fa061d6ce1e..ca8dd736687 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -173,16 +173,30 @@ mod test { #[test] fn subsidy_is_correct_test() -> Result<(), Report> { - use crate::block::check; - use std::sync::Arc; - use zebra_chain::{block::Block, serialization::ZcashDeserialize}; - - let network = Network::Mainnet; + subsidy_is_correct(Network::Mainnet)?; + subsidy_is_correct(Network::Testnet)?; - for b in &[&zebra_test::vectors::BLOCK_MAINNET_434873_BYTES[..]] { - let block = Arc::::zcash_deserialize(*b)?; - check::subsidy_is_correct(network, &block) - .expect("subsidies should pass for this block"); + Ok(()) + } + fn subsidy_is_correct(network: Network) -> Result<(), Report> { + use crate::block::check; + use zebra_chain::{block::Block, serialization::ZcashDeserializeInto}; + + 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"); + + if Height(height) > SLOW_START_INTERVAL + && Height(height) < Canopy.activation_height(network).unwrap() + { + check::subsidy_is_correct(network, &block) + .expect("subsidies should pass for this block"); + } } Ok(()) } From abe0dbfcdcf14f9609ab3a6492fcb252cdb4f0f2 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Tue, 29 Sep 2020 19:19:29 -0300 Subject: [PATCH 07/21] fix minor errors --- zebra-consensus/src/block/check.rs | 7 +------ zebra-consensus/src/block/subsidy/general.rs | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index e1d66a9cebb..70e6dac5279 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -11,7 +11,6 @@ use zebra_chain::{ use crate::error::*; use crate::BoxError; -use std::convert::TryInto; use zebra_chain::parameters::{Network, NetworkUpgrade::*}; use super::subsidy; @@ -85,11 +84,7 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr let founders_reward = subsidy::founders_reward::founders_reward(height, network) .expect("founders reward should be always a valid value"); - let values = || { - outputs - .iter() - .map(|o| o.value.try_into().expect("value will be a valid amount")) - }; + let values = || outputs.iter().map(|o| o.value); if values().any(|value: Amount| value == founders_reward) { valid_founders_reward = true; diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index ca8dd736687..5cfefdb0331 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -1,4 +1,4 @@ -//! Block and Miner subsidies, halvings and target space modifiers. - [§7.7][7.7] +//! Block and Miner subsidies, halvings and target spacing modifiers. - [§7.7][7.7] //! //! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies From 7d1a0f1f664f2cd6e22e903785237d271473ed22 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 10:11:07 +1000 Subject: [PATCH 08/21] Tweak error messages --- zebra-consensus/src/block/check.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 70e6dac5279..047d1162b78 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -75,14 +75,14 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr .activation_height(network) .ok_or(SubsidyError::NoCanopy)?; if height >= canopy_height { - panic!("Can't validate Canopy yet"); + unimplemented!("Canopy block subsidy validation is not implemented"); } // validate founders reward let mut valid_founders_reward = false; if height < canopy_height { let founders_reward = subsidy::founders_reward::founders_reward(height, network) - .expect("founders reward should be always a valid value"); + .expect("founders reward should be a valid value"); let values = || outputs.iter().map(|o| o.value); From 40ec5fdb5565092f3fcc96ef54102d78ddfeed23 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 14:56:34 +1000 Subject: [PATCH 09/21] Implement Add for Height And make the existing Height operators do range checks. --- zebra-chain/src/block/height.rs | 83 +++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index d10afce16ee..d946dd2a72e 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,48 @@ 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 result = self.0.checked_add(rhs.0)?; + match result { + h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)), + _ => 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 +103,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 +129,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); } From c5a8d5a40c562e651a9d3e517b723fe9370d4fea Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 10:43:35 +1000 Subject: [PATCH 10/21] Implement Ord, Eq, and Hash for Amount And implement PartialEq and PartialOrd across different Amount constraints. This allows Amount to be compared with Amount. --- zebra-chain/src/amount.rs | 113 +++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 1763cedfc76..8eda97fc231 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,23 +193,56 @@ where } } -impl PartialOrd for Amount +impl Hash for Amount where - Amount: Eq, C: Constraint, { - fn partial_cmp(&self, other: &Amount) -> Option { - Some(self.cmp(other)) + /// 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 +where + C1: Constraint, + C2: Constraint, +{ + fn eq(&self, other: &Amount) -> bool { + self.partial_cmp(other) == Some(Ordering::Equal) } } +// We can't implement Eq between different amount constraints, +// because it leads to an unconstrained type parameter error +impl Eq for Amount +where + Amount: PartialEq, + C: Constraint, +{ +} + +impl PartialOrd> for Amount +where + Amount: PartialEq>, + C1: Constraint, + C2: Constraint, +{ + fn partial_cmp(&self, other: &Amount) -> Option { + Some(self.0.cmp(&other.0)) + } +} + +// We can't implement Ord between different amount constraints, +// because it leads to an unconstrained type parameter error impl Ord for Amount where Amount: Eq, + Amount: PartialOrd, C: Constraint, { - fn cmp(&self, other: &Amount) -> std::cmp::Ordering { - self.0.cmp(&other.0) + fn cmp(&self, other: &Amount) -> Ordering { + self.partial_cmp(&other).expect("Amount has a total order") } } @@ -377,6 +412,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] @@ -550,23 +590,70 @@ mod test { } #[test] - fn ordering() -> Result<()> { + 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!(one != zero); assert!(zero < one); - assert!(zero != one); assert!(zero <= one); - let one = Amount::::try_from(1)?; - let zero = Amount::::try_from(0)?; 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 != zero); assert!(negative_one <= one); assert!(zero > negative_one); assert!(zero >= negative_one); From 2aa9a5064dcb9985cdfe15f71d6ed9c66676bbdb Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 14:55:37 +1000 Subject: [PATCH 11/21] Remove the founders_reward dependency from subsidy::general --- zebra-consensus/src/block/subsidy/general.rs | 59 +++++++++++--------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 5cfefdb0331..a6d98aa3895 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -10,7 +10,6 @@ use zebra_chain::{ parameters::{Network, NetworkUpgrade::*}, }; -use super::founders_reward::founders_reward; use crate::parameters::subsidy::*; /// `SlowStartShift()` as described in [protocol specification §7.7][7.7] @@ -72,15 +71,20 @@ pub fn block_subsidy(height: Height, network: Network) -> Result Result, Error> { - let canopy_height = Canopy - .activation_height(network) - .expect("canopy activation height should be available"); - if height >= canopy_height { - panic!("Can't validate Canopy yet"); +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) } - block_subsidy(height, network)? - founders_reward(height, network)? } #[cfg(test)] @@ -135,40 +139,43 @@ mod test { #[test] fn miner_subsidy_test() -> Result<(), Report> { - let network = Network::Mainnet; + 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(); - let _canopy_height = Canopy.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) + 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) + miner_subsidy(blossom_height, network, Some(founders_amount)) ); - // TODO: After halving(and Canopy) miner will get 2.5 ZEC per mined block - // but we need funding streams code to get this number from miner_subsidy + // TODO: After first halving, miner will get 2.5 ZEC per mined block + // but we need funding streams code to get this number - Ok(()) - } - - #[test] - #[should_panic] - fn miner_subsidy_canopy_test() { - let network = Network::Mainnet; - let canopy_height = Canopy.activation_height(network).unwrap(); + // TODO: After second halving, there will be no funding streams, and + // miners will get all the block reward - assert_eq!( - Amount::try_from(1_000_000_000), - miner_subsidy(canopy_height, network) - ) + Ok(()) } #[test] From f4ca8e16e3fbbff0a43fc2b19292be19c982cdb0 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 14:57:13 +1000 Subject: [PATCH 12/21] Add testnet and halving subsidy tests --- zebra-consensus/src/block/subsidy/general.rs | 165 +++++++++++++++++-- 1 file changed, 149 insertions(+), 16 deletions(-) diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index a6d98aa3895..9b85cedfd84 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -93,45 +93,174 @@ mod test { use color_eyre::Report; #[test] - fn test_halving() -> Result<(), Report> { - let network = Network::Mainnet; + 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 canopy_height = Canopy.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((canopy_height - 1).unwrap(), network)); - assert_eq!(2, halving_divisor(canopy_height, network)); - assert_eq!(2, halving_divisor((canopy_height + 1).unwrap(), 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 test_block_subsidy() -> Result<(), Report> { - let network = Network::Mainnet; + 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 canopy_height = Canopy.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 reward is 12.5 ZEC + // 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 reward is reduced to 6.25 ZEC without halving + // 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 1st halving(coinciding with Canopy) the block reward is reduced to 3.125 ZEC + // 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(canopy_height, network) + 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(()) @@ -175,17 +304,20 @@ mod test { // 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(()) } #[test] fn subsidy_is_correct_test() -> Result<(), Report> { - subsidy_is_correct(Network::Mainnet)?; - subsidy_is_correct(Network::Testnet)?; + subsidy_is_correct_for_network(Network::Mainnet)?; + subsidy_is_correct_for_network(Network::Testnet)?; Ok(()) } - fn subsidy_is_correct(network: Network) -> Result<(), Report> { + + fn subsidy_is_correct_for_network(network: Network) -> Result<(), Report> { use crate::block::check; use zebra_chain::{block::Block, serialization::ZcashDeserializeInto}; @@ -198,6 +330,7 @@ mod test { .zcash_deserialize_into::() .expect("block is structurally valid"); + // TODO: first halving, second halving, and very large halvings if Height(height) > SLOW_START_INTERVAL && Height(height) < Canopy.activation_height(network).unwrap() { From 86a2ea5aff094e5b4f2947438182a42ed7888cc9 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 15:01:01 +1000 Subject: [PATCH 13/21] Make SLOW_START_SHIFT into a constant --- zebra-consensus/src/block/subsidy/general.rs | 11 ++--------- zebra-consensus/src/parameters/subsidy.rs | 7 +++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 9b85cedfd84..a5aff7a5811 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -12,13 +12,6 @@ use zebra_chain::{ use crate::parameters::subsidy::*; -/// `SlowStartShift()` as described in [protocol specification §7.7][7.7] -/// -/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies -fn slow_start_shift() -> Height { - Height(SLOW_START_INTERVAL.0 / 2) -} - /// The divisor used for halvings. /// /// `1 << Halving(height)`, as described in [protocol specification §7.7][7.7] @@ -33,13 +26,13 @@ pub fn halving_divisor(height: Height, network: Network) -> u64 { .expect("blossom activation height should be available"); if height >= blossom_height { let scaled_pre_blossom_height = - (blossom_height - slow_start_shift()) as u64 * BLOSSOM_POW_TARGET_SPACING_RATIO; + (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 } else { - let scaled_pre_blossom_height = (height - slow_start_shift()) as u64; + 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 } diff --git a/zebra-consensus/src/parameters/subsidy.rs b/zebra-consensus/src/parameters/subsidy.rs index 98064d90cb8..03894b311c5 100644 --- a/zebra-consensus/src/parameters/subsidy.rs +++ b/zebra-consensus/src/parameters/subsidy.rs @@ -9,6 +9,13 @@ use zebra_chain::{amount::COIN, block::Height}; /// [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. From 8ec0264a00c15c6ea80cf213a39e3a0fb5005dd8 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 15:19:07 +1000 Subject: [PATCH 14/21] Refactor subsidy functions to use match --- zebra-consensus/src/block/check.rs | 60 +++++++++++-------- zebra-consensus/src/block/subsidy/general.rs | 61 +++++++++++--------- zebra-consensus/src/error.rs | 3 - 3 files changed, 71 insertions(+), 53 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 047d1162b78..ea7015a75da 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -8,10 +8,10 @@ use zebra_chain::{ work::equihash, }; -use crate::error::*; use crate::BoxError; +use crate::{error::*, parameters::SLOW_START_INTERVAL}; -use zebra_chain::parameters::{Network, NetworkUpgrade::*}; +use zebra_chain::parameters::Network; use super::subsidy; @@ -67,34 +67,46 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr let height = block .coinbase_height() .expect("always called on blocks with a coinbase height"); + let halving_div = subsidy::general::halving_divisor(height, network); let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?; let outputs = coinbase.outputs(); - let canopy_height = Canopy - .activation_height(network) - .ok_or(SubsidyError::NoCanopy)?; - if height >= canopy_height { - unimplemented!("Canopy block subsidy validation is not implemented"); - } + // 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 + match (halving_div, height) { + (_, height) if (height < SLOW_START_INTERVAL) => unreachable!( + "unsupported block height: callers should handle blocks below {:?}", + SLOW_START_INTERVAL + ), + + (halving_div, _) if (halving_div.count_ones() != 1) => unreachable!( + "invalid halving divisor: the halving divisor must be a non-zero power of two" + ), - // validate founders reward - let mut valid_founders_reward = false; - if height < canopy_height { - let founders_reward = subsidy::founders_reward::founders_reward(height, network) - .expect("founders reward should be a valid value"); + (1, _) => { + // validate founders reward + let mut valid_founders_reward = false; + let founders_reward = subsidy::founders_reward::founders_reward(height, network) + .expect("invalid Amount: founders reward should be valid"); - let values = || outputs.iter().map(|o| o.value); + let values = || outputs.iter().map(|o| o.value); + if values().any(|value: Amount| value == founders_reward) { + valid_founders_reward = true; + } + if !valid_founders_reward { + Err(SubsidyError::FoundersRewardNotFound)?; + } - if values().any(|value: Amount| value == founders_reward) { - valid_founders_reward = true; + // TODO: the exact founders reward value must be sent as a single output to the correct address } - } - if !valid_founders_reward { - Err(SubsidyError::FoundersRewardNotFound)? - } else { - // TODO: the exact founders reward value must be sent as a single output to the correct address - // TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees - Ok(()) - } + + (2, _) => unimplemented!("funding stream block subsidy validation is not implemented"), + + // Valid halving, with no founders reward or funding streams + _ => {} + }; + + Ok(()) } diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index a5aff7a5811..cff8d092c72 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -18,23 +18,28 @@ use crate::parameters::subsidy::*; /// /// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies pub fn halving_divisor(height: Height, network: Network) -> u64 { - if height < SLOW_START_INTERVAL { - panic!("can't verify before block {}", SLOW_START_INTERVAL.0) - } let blossom_height = Blossom .activation_height(network) .expect("blossom activation height should be available"); - if height >= blossom_height { - 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 - } else { - 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 + + match height { + height if (height >= SLOW_START_SHIFT && 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 + } + height if (height >= blossom_height) => { + 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 + } + _ => unreachable!( + "unsupported block height: callers should handle blocks below {:?}", + SLOW_START_SHIFT + ), } } @@ -42,22 +47,26 @@ pub fn halving_divisor(height: Height, network: Network) -> u64 { /// /// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies pub fn block_subsidy(height: Height, network: Network) -> Result, Error> { - if height < SLOW_START_INTERVAL { - panic!("can't verify before block {}", SLOW_START_INTERVAL.0) - } let blossom_height = Blossom .activation_height(network) .expect("blossom activation height should be available"); + let halving_div = halving_divisor(height, network); - let hd = halving_divisor(height, network); - if height >= blossom_height { - let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO; - // in future halvings, this calculation might not be exact - // in those cases, Amount division follows integer division, which truncates (rounds down) the result - Amount::try_from(scaled_max_block_subsidy / hd) - } else { - // this calculation is exact, because the halving divisor is 1 here - Amount::try_from(MAX_BLOCK_SUBSIDY / hd) + match height { + height if (height >= SLOW_START_INTERVAL && height < blossom_height) => { + // this calculation is exact, because the halving divisor is 1 here + Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div) + } + height if (height >= blossom_height) => { + let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO; + // in future halvings, this calculation might not be exact + // in those cases, Amount division follows integer division, which truncates (rounds down) the result + Amount::try_from(scaled_max_block_subsidy / halving_div) + } + _ => unreachable!( + "unsupported block height: callers should handle blocks below {:?}", + SLOW_START_INTERVAL + ), } } diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 7016aad6f68..cf9c994babc 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -7,9 +7,6 @@ pub enum SubsidyError { #[error("not a coinbase transaction")] NoCoinbase, - #[error("no canopy block configured")] - NoCanopy, - #[error("founders reward output not found")] FoundersRewardNotFound, } From c0ffed5586782ef3c01efd25507c7e83e35570b8 Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 16:35:19 +1000 Subject: [PATCH 15/21] Refactor subsidy_is_correct to use match And extract a transaction value search function, which we can also use for funding streams. --- zebra-consensus/src/block/check.rs | 24 ++++++++------------ zebra-consensus/src/block/subsidy/general.rs | 16 +++++++++++++ 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index ea7015a75da..547fdb5aeff 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -3,7 +3,6 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ - amount::{Amount, NonNegative}, block::{Block, Header}, work::equihash, }; @@ -70,7 +69,6 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr let halving_div = subsidy::general::halving_divisor(height, network); let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?; - let outputs = coinbase.outputs(); // TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees @@ -87,26 +85,22 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr (1, _) => { // validate founders reward - let mut valid_founders_reward = false; let founders_reward = subsidy::founders_reward::founders_reward(height, network) .expect("invalid Amount: founders reward should be valid"); - - let values = || outputs.iter().map(|o| o.value); - if values().any(|value: Amount| value == founders_reward) { - valid_founders_reward = true; - } - if !valid_founders_reward { - Err(SubsidyError::FoundersRewardNotFound)?; - } + 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)? + } } (2, _) => unimplemented!("funding stream block subsidy validation is not implemented"), // Valid halving, with no founders reward or funding streams - _ => {} - }; - - Ok(()) + _ => Ok(()), + } } diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index cff8d092c72..5cccc377648 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -8,6 +8,8 @@ use zebra_chain::{ amount::{Amount, Error, NonNegative}, block::Height, parameters::{Network, NetworkUpgrade::*}, + transaction::Transaction, + transparent, }; use crate::parameters::subsidy::*; @@ -89,6 +91,20 @@ pub fn miner_subsidy( } } +/// 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::*; From 93e708d2c2312f26c1918696df2cea4435c1a53f Mon Sep 17 00:00:00 2001 From: teor Date: Wed, 30 Sep 2020 15:55:36 +1000 Subject: [PATCH 16/21] Move subsidy_is_correct_test to block::tests --- zebra-consensus/src/block/subsidy/general.rs | 32 ------------------- zebra-consensus/src/block/tests.rs | 33 +++++++++++++++++++- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 5cccc377648..4ada695a87d 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -326,36 +326,4 @@ mod test { Ok(()) } - - #[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> { - use crate::block::check; - use zebra_chain::{block::Block, serialization::ZcashDeserializeInto}; - - 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, and very large halvings - if Height(height) > SLOW_START_INTERVAL - && Height(height) < Canopy.activation_height(network).unwrap() - { - check::subsidy_is_correct(network, &block) - .expect("subsidies should pass for this block"); - } - } - Ok(()) - } } diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 8b94378076c..b4a1d0ef878 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}; @@ -143,3 +145,32 @@ fn time_check_past_block() { 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(()) +} From 8927ebc37ba529a2a3536b7b417740beeb130d2d Mon Sep 17 00:00:00 2001 From: teor Date: Thu, 1 Oct 2020 11:06:40 +1000 Subject: [PATCH 17/21] Apply operator suggestions Co-authored-by: Jane Lusby --- zebra-chain/src/amount.rs | 55 ++++++++++----------------------- zebra-chain/src/block/height.rs | 11 ++++--- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 8eda97fc231..49c851a8106 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -193,56 +193,37 @@ where } } -impl Hash for Amount -where - C: Constraint, -{ +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 -where - C1: Constraint, - C2: Constraint, -{ +impl PartialEq> for Amount { fn eq(&self, other: &Amount) -> bool { - self.partial_cmp(other) == Some(Ordering::Equal) + self.0.eq(&other.0) } } -// We can't implement Eq between different amount constraints, -// because it leads to an unconstrained type parameter error -impl Eq for Amount -where - Amount: PartialEq, - C: Constraint, -{ -} +impl Eq for Amount {} +impl Eq for Amount {} -impl PartialOrd> for Amount -where - Amount: PartialEq>, - C1: Constraint, - C2: Constraint, -{ +impl PartialOrd> for Amount { fn partial_cmp(&self, other: &Amount) -> Option { Some(self.0.cmp(&other.0)) } } -// We can't implement Ord between different amount constraints, -// because it leads to an unconstrained type parameter error -impl Ord for Amount -where - Amount: Eq, - Amount: PartialOrd, - C: Constraint, -{ - fn cmp(&self, other: &Amount) -> Ordering { - self.partial_cmp(&other).expect("Amount has a total order") +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) } } @@ -275,9 +256,7 @@ impl std::ops::Div for Amount { let quotient = (self.0 as u64) .checked_div(rhs) .ok_or(Error::DivideByZero { amount: self.0 })?; - // since this is a division by a positive integer, - // the quotient must be within the constrained range - Ok(quotient.try_into().unwrap()) + Ok(quotient.try_into().expect("division by a positive integer always stays within the constraint")) } } @@ -297,7 +276,7 @@ pub enum Error { }, /// i64 overflow when multiplying i64 non-negative amount {amount} by u64 {multiplier} MultiplicationOverflow { amount: i64, multiplier: u64 }, - /// division by zero is an invalid operation, amount {amount} + /// cannot divide amount {amount} by zero DivideByZero { amount: i64 }, } diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index d946dd2a72e..a9465db5f6b 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -57,10 +57,13 @@ impl Add for 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 result = self.0.checked_add(rhs.0)?; - match result { - h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)), - _ => None, + let height = self.0.checked_add(rhs.0)?; + let height = Height(height); + + if height <= Height::MAX && height >= Height::MIN { + Some(height) + } else { + None } } } From 919056f91bd6bfc7923aefede093f7c41e6f7ac2 Mon Sep 17 00:00:00 2001 From: teor Date: Thu, 1 Oct 2020 15:59:40 +1000 Subject: [PATCH 18/21] rustfmt --- zebra-chain/src/amount.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index 49c851a8106..0cf8ddd84e9 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -256,7 +256,9 @@ impl std::ops::Div for Amount { 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")) + Ok(quotient + .try_into() + .expect("division by a positive integer always stays within the constraint")) } } From 46e6526496871bd9a2a18d2135cc1953f9f59a6d Mon Sep 17 00:00:00 2001 From: teor Date: Thu, 1 Oct 2020 16:33:41 +1000 Subject: [PATCH 19/21] matches to if-elses --- zebra-consensus/src/block/check.rs | 45 +++++++--------- zebra-consensus/src/block/subsidy/general.rs | 55 +++++++++----------- 2 files changed, 46 insertions(+), 54 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 547fdb5aeff..0957d35ac31 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -73,34 +73,29 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr // 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 - match (halving_div, height) { - (_, height) if (height < SLOW_START_INTERVAL) => unreachable!( + if height < SLOW_START_INTERVAL { + unreachable!( "unsupported block height: callers should handle blocks below {:?}", SLOW_START_INTERVAL - ), - - (halving_div, _) if (halving_div.count_ones() != 1) => unreachable!( - "invalid halving divisor: the halving divisor must be a non-zero power of two" - ), - - (1, _) => { - // validate founders reward - 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.count_ones() != 1 { + unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") + } else if halving_div == 1 { + // validate founders reward + 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)? } - - (2, _) => unimplemented!("funding stream block subsidy validation is not implemented"), - + } else if halving_div == 2 { + unimplemented!("funding stream block subsidy validation is not implemented") + } else { // Valid halving, with no founders reward or funding streams - _ => Ok(()), + Ok(()) } } diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 4ada695a87d..f8970db38b7 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -24,24 +24,22 @@ pub fn halving_divisor(height: Height, network: Network) -> u64 { .activation_height(network) .expect("blossom activation height should be available"); - match height { - height if (height >= SLOW_START_SHIFT && 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 - } - height if (height >= blossom_height) => { - 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 - } - _ => unreachable!( + 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 } } @@ -54,21 +52,20 @@ pub fn block_subsidy(height: Height, network: Network) -> Result= SLOW_START_INTERVAL && height < blossom_height) => { - // this calculation is exact, because the halving divisor is 1 here - Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div) - } - height if (height >= blossom_height) => { - let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO; - // in future halvings, this calculation might not be exact - // in those cases, Amount division follows integer division, which truncates (rounds down) the result - Amount::try_from(scaled_max_block_subsidy / halving_div) - } - _ => unreachable!( + 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) } } From 2372341f8f456c88b1bb1ab7613fb5d4704b6924 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Thu, 1 Oct 2020 14:51:53 -0300 Subject: [PATCH 20/21] add subsidy validation error tests --- zebra-consensus/src/block/check.rs | 8 ++-- zebra-consensus/src/block/tests.rs | 71 ++++++++++++++++++++++++++++++ zebra-consensus/src/error.rs | 10 ++--- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 0957d35ac31..117e0f5f0db 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -63,13 +63,11 @@ pub fn time_is_valid_at(header: &Header, now: DateTime) -> Result<(), BoxEr /// [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() - .expect("always called on blocks with a coinbase height"); - let halving_div = subsidy::general::halving_divisor(height, network); - + 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); + // 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 diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index b4a1d0ef878..184d76e3f92 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -174,3 +174,74 @@ fn subsidy_is_correct_for_network(network: Network) -> Result<(), Report> { } 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/error.rs b/zebra-consensus/src/error.rs index cf9c994babc..6e9b04d90ba 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -2,16 +2,16 @@ use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum SubsidyError { - #[error("not a coinbase transaction")] + #[error("no coinbase transaction in block")] NoCoinbase, #[error("founders reward output not found")] FoundersRewardNotFound, } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum TransactionError { #[error("first transaction must be coinbase")] CoinbasePosition, @@ -19,7 +19,7 @@ pub enum TransactionError { #[error("coinbase input found in non-coinbase transaction")] CoinbaseInputFound, - #[error("coinbase transaction contains invalid subsidy parameters")] + #[error("coinbase transaction failed subsidy validation")] Subsidy(#[from] SubsidyError), } @@ -29,7 +29,7 @@ impl From for BlockError { } } -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum BlockError { #[error("block contains invalid transactions")] Transaction(#[from] TransactionError), From 6ef71ba0bbe0f1cfd9d60969a1c4ab876dfe4ccd Mon Sep 17 00:00:00 2001 From: teor Date: Fri, 9 Oct 2020 19:48:11 +1000 Subject: [PATCH 21/21] 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.) --- zebra-consensus/src/block/check.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 117e0f5f0db..d993e6ce253 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ block::{Block, Header}, + parameters::NetworkUpgrade, work::equihash, }; @@ -67,6 +68,9 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr 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 @@ -78,8 +82,8 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr ) } else if halving_div.count_ones() != 1 { unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") - } else if halving_div == 1 { - // validate founders reward + } 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); @@ -90,10 +94,13 @@ pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockEr } else { Err(SubsidyError::FoundersRewardNotFound)? } - } else if halving_div == 2 { + } 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 { - // Valid halving, with no founders reward or funding streams + // Future halving, with no founders reward or funding streams Ok(()) } }