Skip to content

Commit

Permalink
Validate funding stream amounts in coinbase transaction (#3017)
Browse files Browse the repository at this point in the history
* 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()`
  • Loading branch information
oxarbitrage authored Nov 8, 2021
1 parent f7c1907 commit 62bfa15
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 7 deletions.
28 changes: 24 additions & 4 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<Amount<NonNegative>> = 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(())
Expand Down Expand Up @@ -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::<HashSet<_>>().len() {
return Err(BlockError::DuplicateTransaction);
}
Expand Down
2 changes: 2 additions & 0 deletions zebra-consensus/src/block/subsidy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
52 changes: 52 additions & 0 deletions zebra-consensus/src/block/subsidy/funding_streams.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap<FundingStreamReceiver, Amount<NonNegative>>, 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)
}
56 changes: 56 additions & 0 deletions zebra-consensus/src/block/subsidy/funding_streams/tests.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
12 changes: 11 additions & 1 deletion zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -102,6 +102,16 @@ pub fn find_output_with_amount(
.collect()
}

/// Returns all output amounts in `Transaction`.
pub fn output_amounts(transaction: &Transaction) -> HashSet<Amount<NonNegative>> {
transaction
.outputs()
.iter()
.map(|o| &o.value)
.cloned()
.collect()
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
78 changes: 77 additions & 1 deletion zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
@@ -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::*;

Expand Down Expand Up @@ -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::<Block>::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<Arc<zebra_chain::transaction::Transaction>> = 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();
Expand Down
3 changes: 3 additions & 0 deletions zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
43 changes: 42 additions & 1 deletion zebra-consensus/src/parameters/subsidy.rs
Original file line number Diff line number Diff line change
@@ -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]
///
Expand Down Expand Up @@ -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<FundingStreamReceiver, u64> = {
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<Network, std::ops::Range<Height>> = {
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
};
}

0 comments on commit 62bfa15

Please sign in to comment.