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()`
This commit is contained in:
Alfredo Garcia 2021-11-08 19:33:12 -03:00 committed by GitHub
parent f7c1907fb6
commit 62bfa15e96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 267 additions and 7 deletions

View File

@ -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<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(())
@ -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);
}

View File

@ -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;

View File

@ -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)
}

View File

@ -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(())
}

View File

@ -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<Amount<NonNegative>> {
transaction
.outputs()
.iter()
.map(|o| &o.value)
.cloned()
.collect()
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -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::<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();

View File

@ -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)]

View File

@ -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<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
};
}