From 62bfa15e96740796ecb8d570c37aabaf32017183 Mon Sep 17 00:00:00 2001 From: Alfredo Garcia Date: Mon, 8 Nov 2021 19:33:12 -0300 Subject: [PATCH] Validate funding stream amounts in coinbase transaction (#3017) * validate funding stream amounts in the coinbase * clippy * use `i64::from()` and remove `number()` method from `Amount` * move tests to their own file * refactor the funding stream check * use `Amount`s in funding streams calculation * remove unused import * add import to tests * expand test vectors * add notes to `funding_stream_values()` --- zebra-consensus/src/block/check.rs | 28 ++++++- zebra-consensus/src/block/subsidy.rs | 2 + .../src/block/subsidy/funding_streams.rs | 52 +++++++++++++ .../block/subsidy/funding_streams/tests.rs | 56 +++++++++++++ zebra-consensus/src/block/subsidy/general.rs | 12 ++- zebra-consensus/src/block/tests.rs | 78 ++++++++++++++++++- zebra-consensus/src/error.rs | 3 + zebra-consensus/src/parameters/subsidy.rs | 43 +++++++++- 8 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 zebra-consensus/src/block/subsidy/funding_streams.rs create mode 100644 zebra-consensus/src/block/subsidy/funding_streams/tests.rs diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 9f8f1bd80..9c7b4a959 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -1,8 +1,10 @@ //! Consensus check functions use chrono::{DateTime, Utc}; +use std::collections::HashSet; use zebra_chain::{ + amount::{Amount, NonNegative}, block::{Block, Hash, Header, Height}, parameters::{Network, NetworkUpgrade}, transaction, @@ -131,9 +133,28 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro // 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 - tracing::trace!("funding stream block subsidy validation is not implemented"); - // Return ok for now - Ok(()) + + let funding_streams = subsidy::funding_streams::funding_stream_values(height, network) + .expect("We always expect a funding stream hashmap response even if empty"); + + let funding_stream_amounts: HashSet> = funding_streams + .iter() + .map(|(_receiver, amount)| *amount) + .collect(); + let output_amounts = subsidy::general::output_amounts(coinbase); + + // Consensus rule:[Canopy onward] The coinbase transaction at block height `height` + // MUST contain at least one output per funding stream `fs` active at `height`, + // that pays `fs.Value(height)` zatoshi in the prescribed way to the stream's + // recipient address represented by `fs.AddressList[fs.AddressIndex(height)] + + // TODO: We are only checking each fundign stream reward is present in the + // coinbase transaction outputs but not the recipient addresses. + if funding_stream_amounts.is_subset(&output_amounts) { + Ok(()) + } else { + Err(SubsidyError::FundingStreamNotFound)? + } } else { // Future halving, with no founders reward or funding streams Ok(()) @@ -221,7 +242,6 @@ pub fn merkle_root_validity( // // To prevent malleability (CVE-2012-2459), we also need to check // whether the transaction hashes are unique. - use std::collections::HashSet; if transaction_hashes.len() != transaction_hashes.iter().collect::>().len() { return Err(BlockError::DuplicateTransaction); } diff --git a/zebra-consensus/src/block/subsidy.rs b/zebra-consensus/src/block/subsidy.rs index fad87f332..dcd883909 100644 --- a/zebra-consensus/src/block/subsidy.rs +++ b/zebra-consensus/src/block/subsidy.rs @@ -4,5 +4,7 @@ /// Founders' Reward functions apply for blocks before Canopy. pub mod founders_reward; +/// Funding Streams functions apply for blocks at and after Canopy. +pub mod funding_streams; /// General subsidy functions apply for blocks after slow-start mining. pub mod general; diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-consensus/src/block/subsidy/funding_streams.rs new file mode 100644 index 000000000..364124917 --- /dev/null +++ b/zebra-consensus/src/block/subsidy/funding_streams.rs @@ -0,0 +1,52 @@ +//! Funding Streams calculations. - [§7.7][7.7] +//! +//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies + +use zebra_chain::{ + amount::{Amount, Error, NonNegative}, + block::Height, + parameters::{Network, NetworkUpgrade::*}, +}; + +use crate::{ + block::subsidy::general::block_subsidy, + parameters::subsidy::{ + FundingStreamReceiver, FUNDING_STREAM_HEIGHT_RANGES, FUNDING_STREAM_RECEIVER_DENOMINATOR, + FUNDING_STREAM_RECEIVER_NUMERATORS, + }, +}; + +#[cfg(test)] +mod tests; + +/// Returns the `fs.Value(height)` for each stream receiver +/// as described in [protocol specification §7.7][7.7] +/// +/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies +use std::collections::HashMap; +pub fn funding_stream_values( + height: Height, + network: Network, +) -> Result>, Error> { + let canopy_height = Canopy.activation_height(network).unwrap(); + let mut results = HashMap::new(); + + if height >= canopy_height { + let range = FUNDING_STREAM_HEIGHT_RANGES.get(&network).unwrap(); + if range.contains(&height) { + let block_subsidy = block_subsidy(height, network)?; + for (&receiver, &numerator) in FUNDING_STREAM_RECEIVER_NUMERATORS.iter() { + // - Spec equation: `fs.value = floor(block_subsidy(height)*(fs.numerator/fs.denominator))`: + // https://zips.z.cash/protocol/protocol.pdf#subsidies + // - In Rust, "integer division rounds towards zero": + // https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators + // This is the same as `floor()`, because these numbers are all positive. + let amount_value = + ((block_subsidy * numerator)? / FUNDING_STREAM_RECEIVER_DENOMINATOR)?; + + results.insert(receiver, amount_value); + } + } + } + Ok(results) +} diff --git a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs new file mode 100644 index 000000000..9826367f5 --- /dev/null +++ b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs @@ -0,0 +1,56 @@ +use super::*; +use color_eyre::Report; +use std::convert::TryFrom; + +#[test] +// Check funding streams are correct in the entire period. +fn test_funding_stream_values() -> Result<(), Report> { + zebra_test::init(); + let network = Network::Mainnet; + + // funding streams not active + let canopy_height_minus1 = Canopy.activation_height(network).unwrap() - 1; + assert!(funding_stream_values(canopy_height_minus1.unwrap(), network)?.is_empty()); + + // funding stream is active + let canopy_height = Canopy.activation_height(network); + let canopy_height_plus1 = Canopy.activation_height(network).unwrap() + 1; + let canopy_height_plus2 = Canopy.activation_height(network).unwrap() + 2; + + let mut hash_map = HashMap::new(); + hash_map.insert(FundingStreamReceiver::Ecc, Amount::try_from(21_875_000)?); + hash_map.insert( + FundingStreamReceiver::ZcashFoundation, + Amount::try_from(15_625_000)?, + ); + hash_map.insert( + FundingStreamReceiver::MajorGrants, + Amount::try_from(25_000_000)?, + ); + + assert_eq!( + funding_stream_values(canopy_height.unwrap(), network).unwrap(), + hash_map + ); + assert_eq!( + funding_stream_values(canopy_height_plus1.unwrap(), network).unwrap(), + hash_map + ); + assert_eq!( + funding_stream_values(canopy_height_plus2.unwrap(), network).unwrap(), + hash_map + ); + + // funding stream period is ending + let range = FUNDING_STREAM_HEIGHT_RANGES.get(&network).unwrap(); + let end = range.end; + let last = end - 1; + + assert_eq!( + funding_stream_values(last.unwrap(), network).unwrap(), + hash_map + ); + assert!(funding_stream_values(end, network)?.is_empty()); + + Ok(()) +} diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 5587c8b12..22068d5b7 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -2,7 +2,7 @@ //! //! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies -use std::convert::TryFrom; +use std::{collections::HashSet, convert::TryFrom}; use zebra_chain::{ amount::{Amount, Error, NonNegative}, @@ -102,6 +102,16 @@ pub fn find_output_with_amount( .collect() } +/// Returns all output amounts in `Transaction`. +pub fn output_amounts(transaction: &Transaction) -> HashSet> { + transaction + .outputs() + .iter() + .map(|o| &o.value) + .cloned() + .collect() +} + #[cfg(test)] mod test { use super::*; diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 2ef3c2d6d..d5d6296c6 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -1,6 +1,9 @@ //! Tests for block verification -use crate::{parameters::SLOW_START_INTERVAL, script}; +use crate::{ + parameters::{SLOW_START_INTERVAL, SLOW_START_SHIFT}, + script, +}; use super::*; @@ -413,6 +416,79 @@ fn founders_reward_validation_failure() -> Result<(), Report> { Ok(()) } +#[test] +fn funding_stream_validation() -> Result<(), Report> { + zebra_test::init(); + + funding_stream_validation_for_network(Network::Mainnet)?; + funding_stream_validation_for_network(Network::Testnet)?; + + Ok(()) +} + +fn funding_stream_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"); + + // Validate + let result = check::subsidy_is_valid(&block, network); + assert!(result.is_ok()); + } + } + + Ok(()) +} + +#[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. + let block = + Arc::::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_1046400_BYTES[..]) + .expect("block should deserialize"); + + // Build the new transaction with modified coinbase outputs + 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(), + expiry_height: Height(0), + joinsplit_data: None, + sapling_shielded_data: None, + }) + .unwrap(); + + // Build new block + let transactions: Vec> = vec![Arc::new(tx)]; + let block = Block { + header: block.header, + transactions, + }; + + // Validate it + let result = check::subsidy_is_valid(&block, network).unwrap_err(); + let expected = BlockError::Transaction(TransactionError::Subsidy( + SubsidyError::FundingStreamNotFound, + )); + assert_eq!(expected, result); + + Ok(()) +} + #[test] fn time_is_valid_for_historical_blocks() -> Result<(), Report> { zebra_test::init(); diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 3759a648e..0622f06b5 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -21,6 +21,9 @@ pub enum SubsidyError { #[error("founders reward output not found")] FoundersRewardNotFound, + + #[error("funding stream output not found")] + FundingStreamNotFound, } #[derive(Error, Clone, Debug, PartialEq, Eq)] diff --git a/zebra-consensus/src/parameters/subsidy.rs b/zebra-consensus/src/parameters/subsidy.rs index 1ea5d0cec..2afadccd1 100644 --- a/zebra-consensus/src/parameters/subsidy.rs +++ b/zebra-consensus/src/parameters/subsidy.rs @@ -1,6 +1,9 @@ //! Constants for Block Subsidy, Funding Streams, and Founders' Reward -use zebra_chain::{amount::COIN, block::Height}; +use lazy_static::lazy_static; +use std::collections::HashMap; + +use zebra_chain::{amount::COIN, block::Height, parameters::Network}; /// An initial period from Genesis to this Height where the block subsidy is gradually incremented. [What is slow-start mining][slow-mining] /// @@ -41,3 +44,41 @@ pub const POST_BLOSSOM_HALVING_INTERVAL: Height = /// /// Usage: founders_reward = block_subsidy / FOUNDERS_FRACTION_DIVISOR pub const FOUNDERS_FRACTION_DIVISOR: u64 = 5; + +/// The funding stream receiver categories +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum FundingStreamReceiver { + Ecc, + ZcashFoundation, + MajorGrants, +} + +/// Denominator as described in [protocol specification §7.9.1][7.9.1]. +/// +/// [7.9.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams +pub const FUNDING_STREAM_RECEIVER_DENOMINATOR: u64 = 100; + +lazy_static! { + /// The numerator for each funding stream receiving category + /// as described in [protocol specification §7.9.1][7.9.1]. + /// + /// [7.9.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams + pub static ref FUNDING_STREAM_RECEIVER_NUMERATORS: HashMap = { + let mut hash_map = HashMap::new(); + hash_map.insert(FundingStreamReceiver::Ecc, 7); + hash_map.insert(FundingStreamReceiver::ZcashFoundation, 5); + hash_map.insert(FundingStreamReceiver::MajorGrants, 8); + hash_map + }; + + /// Start and end Heights for funding streams + /// as described in [protocol specification §7.9.1][7.9.1]. + /// + /// [7.9.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams + pub static ref FUNDING_STREAM_HEIGHT_RANGES: HashMap> = { + let mut hash_map = HashMap::new(); + hash_map.insert(Network::Mainnet, Height(1_046_400)..Height(2_726_400)); + hash_map.insert(Network::Testnet, Height(1_028_500)..Height(2_796_000)); + hash_map + }; +}