diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 7b113d8a37d..04d7f7ec652 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -1157,7 +1157,7 @@ impl Transaction { /// and added to sapling pool. /// /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions - fn sapling_value_balance(&self) -> ValueBalance { + pub fn sapling_value_balance(&self) -> ValueBalance { let sapling_value_balance = match self { Transaction::V4 { sapling_shielded_data: Some(sapling_shielded_data), @@ -1224,7 +1224,7 @@ impl Transaction { /// and added to orchard pool. /// /// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions - fn orchard_value_balance(&self) -> ValueBalance { + pub fn orchard_value_balance(&self) -> ValueBalance { let orchard_value_balance = self .orchard_shielded_data() .map(|shielded_data| shielded_data.value_balance) diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index b912792ba8b..ebeb3e44d49 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -259,13 +259,13 @@ where })?; } - // TODO: check miner subsidy and miner fees (#1162) - let _block_miner_fees = + let block_miner_fees = block_miner_fees.map_err(|amount_error| BlockError::SummingMinerFees { height, hash, source: amount_error, })?; + check::miner_fees_are_valid(&block, network, block_miner_fees)?; // Finally, submit the block for contextual verification. let new_outputs = Arc::try_unwrap(known_utxos) diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index abec8df930b..94ff3ceeacf 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc}; use std::collections::HashSet; use zebra_chain::{ + amount::{Amount, Error as AmountError, NonNegative}, block::{Block, Hash, Header, Height}, parameters::{Network, NetworkUpgrade}, transaction, @@ -94,21 +95,19 @@ pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error header.solution.check(header) } -/// Returns `Ok(())` if the block subsidy and miner fees in `block` are valid for `network` +/// Returns `Ok(())` if the block subsidy in `block` is valid for `network` /// /// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockError> { let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?; + // Validate founders reward and funding streams 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 {:?}", @@ -161,6 +160,45 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro } } +/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// +/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus +pub fn miner_fees_are_valid( + block: &Block, + network: Network, + block_miner_fees: Amount, +) -> Result<(), BlockError> { + let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; + let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?; + + let transparent_value_balance: Amount = subsidy::general::output_amounts(coinbase) + .iter() + .sum::, AmountError>>() + .map_err(|_| SubsidyError::SumOverflow)? + .constrain() + .expect("positive value always fit in `NegativeAllowed`"); + let sapling_value_balance = coinbase.sapling_value_balance().sapling_amount(); + let orchard_value_balance = coinbase.orchard_value_balance().orchard_amount(); + + let block_subsidy = subsidy::general::block_subsidy(height, network) + .expect("a valid block subsidy for this height and network"); + + // Consensus rule: The total value in zatoshi of transparent outputs from a + // coinbase transaction, minus vbalanceSapling, minus vbalanceOrchard, MUST NOT + // be greater than the value in zatoshi of block subsidy plus the transaction fees + // paid by transactions in this block. + // https://zips.z.cash/protocol/protocol.pdf#txnconsensus + let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance) + .map_err(|_| SubsidyError::SumOverflow)?; + let right = (block_subsidy + block_miner_fees).map_err(|_| SubsidyError::SumOverflow)?; + + if left > right { + return Err(SubsidyError::InvalidMinerFees)?; + } + + Ok(()) +} + /// Returns `Ok(())` if `header.time` is less than or equal to /// 2 hours in the future, according to the node's local clock (`now`). /// diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-consensus/src/block/subsidy/funding_streams.rs index 1e422ea9fb2..028539cd49b 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams.rs @@ -53,7 +53,7 @@ pub fn funding_stream_values( /// as described in [protocol specification ยง7.10][7.10] /// /// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams -fn height_for_first_halving(network: Network) -> Height { +pub fn height_for_first_halving(network: Network) -> Height { // First halving on Mainnet is at Canopy // while in Testnet is at block constant height of `1_116_000` // https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 45aaf647092..02856ccb430 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -118,6 +118,11 @@ mod test { use super::*; use color_eyre::Report; + use crate::block::subsidy::{ + founders_reward::founders_reward, + funding_streams::{funding_stream_values, height_for_first_halving}, + }; + #[test] fn halving_test() -> Result<(), Report> { zebra_test::init(); @@ -307,8 +312,8 @@ mod test { } 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 first_halving_height = height_for_first_halving(network); // Miner reward before Blossom is 80% of the total block reward // 80*12.5/100 = 10 ZEC @@ -330,8 +335,17 @@ mod test { 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 + // After first halving, miner will get 2.5 ZEC per mined block (not counting fees) + let funding_stream_values = funding_stream_values(first_halving_height, network)? + .iter() + .map(|row| row.1) + .sum::, Error>>() + .unwrap(); + + assert_eq!( + Amount::try_from(250_000_000), + miner_subsidy(first_halving_height, network, Some(funding_stream_values)) + ); // TODO: After second halving, there will be no funding streams, and // miners will get all the block reward diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 13843ad3b1e..e7a6d7259f6 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -1,6 +1,6 @@ //! Tests for block verification -use std::sync::Arc; +use std::{convert::TryFrom, sync::Arc}; use chrono::Utc; use color_eyre::eyre::{eyre, Report}; @@ -8,6 +8,7 @@ use once_cell::sync::Lazy; use tower::{buffer::Buffer, util::BoxService}; use zebra_chain::{ + amount::{Amount, MAX_MONEY}, block::{ self, tests::generate::{large_multi_transaction_block, large_single_transaction_block}, @@ -196,7 +197,6 @@ fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> { #[test] fn difficulty_validation_failure() -> Result<(), Report> { zebra_test::init(); - use crate::error::*; // Get a block in the mainnet, and mangle its difficulty field let block = @@ -306,8 +306,6 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> { #[test] fn coinbase_validation_failure() -> Result<(), Report> { zebra_test::init(); - use crate::error::*; - let network = Network::Mainnet; // Get a block in the mainnet that is inside the founders reward period, @@ -379,9 +377,6 @@ fn coinbase_validation_failure() -> Result<(), Report> { #[test] fn founders_reward_validation_failure() -> Result<(), Report> { zebra_test::init(); - use crate::error::*; - use zebra_chain::transaction::Transaction; - let network = Network::Mainnet; // Get a block in the mainnet that is inside the founders reward period. @@ -393,12 +388,16 @@ fn founders_reward_validation_failure() -> Result<(), Report> { 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().unwrap_or_else(LockTime::unlocked), - expiry_height: Height(0), - joinsplit_data: None, + .map(|transaction| { + let mut output = transaction.outputs()[0].clone(); + output.value = Amount::try_from(i32::MAX).unwrap(); + Transaction::V3 { + inputs: transaction.inputs().to_vec(), + outputs: vec![output], + lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked), + expiry_height: Height(0), + joinsplit_data: None, + } }) .unwrap(); @@ -410,10 +409,11 @@ fn founders_reward_validation_failure() -> Result<(), Report> { }; // Validate it - let result = check::subsidy_is_valid(&block, network).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy( + let result = check::subsidy_is_valid(&block, network); + let expected = Err(BlockError::Transaction(TransactionError::Subsidy( SubsidyError::FoundersRewardNotFound, - )); + ))); + assert_eq!(expected, result); Ok(()) @@ -451,9 +451,6 @@ fn funding_stream_validation_for_network(network: Network) -> Result<(), Report> #[test] fn funding_stream_validation_failure() -> Result<(), Report> { zebra_test::init(); - use crate::error::*; - use zebra_chain::transaction::Transaction; - let network = Network::Mainnet; // Get a block in the mainnet that is inside the funding stream period. @@ -465,13 +462,17 @@ fn funding_stream_validation_failure() -> Result<(), Report> { let tx = block .transactions .get(0) - .map(|transaction| Transaction::V4 { - inputs: transaction.inputs().to_vec(), - outputs: vec![transaction.outputs()[0].clone()], - lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked), - expiry_height: Height(0), - joinsplit_data: None, - sapling_shielded_data: None, + .map(|transaction| { + let mut output = transaction.outputs()[0].clone(); + output.value = Amount::try_from(i32::MAX).unwrap(); + Transaction::V4 { + inputs: transaction.inputs().to_vec(), + outputs: vec![output], + lock_time: transaction.lock_time().unwrap_or_else(LockTime::unlocked), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + } }) .unwrap(); @@ -483,10 +484,65 @@ fn funding_stream_validation_failure() -> Result<(), Report> { }; // Validate it - let result = check::subsidy_is_valid(&block, network).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy( + let result = check::subsidy_is_valid(&block, network); + let expected = Err(BlockError::Transaction(TransactionError::Subsidy( SubsidyError::FundingStreamNotFound, - )); + ))); + assert_eq!(expected, result); + + Ok(()) +} + +#[test] +fn miner_fees_validation_success() -> Result<(), Report> { + zebra_test::init(); + + miner_fees_validation_for_network(Network::Mainnet)?; + miner_fees_validation_for_network(Network::Testnet)?; + + Ok(()) +} + +fn miner_fees_validation_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 { + if Height(height) > SLOW_START_SHIFT { + let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize"); + + // fake the miner fee to a big amount + let miner_fees = Amount::try_from(MAX_MONEY / 2).unwrap(); + + // Validate + let result = check::miner_fees_are_valid(&block, network, miner_fees); + assert!(result.is_ok()); + } + } + + Ok(()) +} + +#[test] +fn miner_fees_validation_failure() -> Result<(), Report> { + zebra_test::init(); + let network = Network::Mainnet; + + let block = + Arc::::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..]) + .expect("block should deserialize"); + + // fake the miner fee to a small amount + let miner_fees = Amount::zero(); + + // Validate + let result = check::miner_fees_are_valid(&block, network, miner_fees); + + let expected = Err(BlockError::Transaction(TransactionError::Subsidy( + SubsidyError::InvalidMinerFees, + ))); assert_eq!(expected, result); Ok(()) diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index ec14f64758c..8690343a6c3 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -25,6 +25,12 @@ pub enum SubsidyError { #[error("funding stream expected output not found")] FundingStreamNotFound, + + #[error("miner fees are invalid")] + InvalidMinerFees, + + #[error("a sum of amounts overflowed")] + SumOverflow, } #[derive(Error, Clone, Debug, PartialEq, Eq)]