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:
parent
f7c1907fb6
commit
62bfa15e96
|
@ -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
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue