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
|
//! Consensus check functions
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
|
amount::{Amount, NonNegative},
|
||||||
block::{Block, Hash, Header, Height},
|
block::{Block, Hash, Header, Height},
|
||||||
parameters::{Network, NetworkUpgrade},
|
parameters::{Network, NetworkUpgrade},
|
||||||
transaction,
|
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
|
// 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
|
// 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
|
// 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(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(SubsidyError::FundingStreamNotFound)?
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Future halving, with no founders reward or funding streams
|
// Future halving, with no founders reward or funding streams
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -221,7 +242,6 @@ pub fn merkle_root_validity(
|
||||||
//
|
//
|
||||||
// To prevent malleability (CVE-2012-2459), we also need to check
|
// To prevent malleability (CVE-2012-2459), we also need to check
|
||||||
// whether the transaction hashes are unique.
|
// whether the transaction hashes are unique.
|
||||||
use std::collections::HashSet;
|
|
||||||
if transaction_hashes.len() != transaction_hashes.iter().collect::<HashSet<_>>().len() {
|
if transaction_hashes.len() != transaction_hashes.iter().collect::<HashSet<_>>().len() {
|
||||||
return Err(BlockError::DuplicateTransaction);
|
return Err(BlockError::DuplicateTransaction);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,7 @@
|
||||||
|
|
||||||
/// Founders' Reward functions apply for blocks before Canopy.
|
/// Founders' Reward functions apply for blocks before Canopy.
|
||||||
pub mod founders_reward;
|
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.
|
/// General subsidy functions apply for blocks after slow-start mining.
|
||||||
pub mod general;
|
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
|
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
use std::{collections::HashSet, convert::TryFrom};
|
||||||
|
|
||||||
use zebra_chain::{
|
use zebra_chain::{
|
||||||
amount::{Amount, Error, NonNegative},
|
amount::{Amount, Error, NonNegative},
|
||||||
|
@ -102,6 +102,16 @@ pub fn find_output_with_amount(
|
||||||
.collect()
|
.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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//! Tests for block verification
|
//! Tests for block verification
|
||||||
|
|
||||||
use crate::{parameters::SLOW_START_INTERVAL, script};
|
use crate::{
|
||||||
|
parameters::{SLOW_START_INTERVAL, SLOW_START_SHIFT},
|
||||||
|
script,
|
||||||
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -413,6 +416,79 @@ fn founders_reward_validation_failure() -> Result<(), Report> {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
|
fn time_is_valid_for_historical_blocks() -> Result<(), Report> {
|
||||||
zebra_test::init();
|
zebra_test::init();
|
||||||
|
|
|
@ -21,6 +21,9 @@ pub enum SubsidyError {
|
||||||
|
|
||||||
#[error("founders reward output not found")]
|
#[error("founders reward output not found")]
|
||||||
FoundersRewardNotFound,
|
FoundersRewardNotFound,
|
||||||
|
|
||||||
|
#[error("funding stream output not found")]
|
||||||
|
FundingStreamNotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Clone, Debug, PartialEq, Eq)]
|
#[derive(Error, Clone, Debug, PartialEq, Eq)]
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//! Constants for Block Subsidy, Funding Streams, and Founders' Reward
|
//! 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]
|
/// 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
|
/// Usage: founders_reward = block_subsidy / FOUNDERS_FRACTION_DIVISOR
|
||||||
pub const FOUNDERS_FRACTION_DIVISOR: u64 = 5;
|
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