Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate miner transaction fees #3067

Merged
merged 15 commits into from
Nov 24, 2021
4 changes: 2 additions & 2 deletions zebra-chain/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,7 @@ impl Transaction {
/// and added to sapling pool.
///
/// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions
fn sapling_value_balance(&self) -> ValueBalance<NegativeAllowed> {
pub fn sapling_value_balance(&self) -> ValueBalance<NegativeAllowed> {
let sapling_value_balance = match self {
Transaction::V4 {
sapling_shielded_data: Some(sapling_shielded_data),
Expand Down Expand Up @@ -1167,7 +1167,7 @@ impl Transaction {
/// and added to orchard pool.
///
/// https://zebra.zfnd.org/dev/rfcs/0012-value-pools.html#definitions
fn orchard_value_balance(&self) -> ValueBalance<NegativeAllowed> {
pub fn orchard_value_balance(&self) -> ValueBalance<NegativeAllowed> {
let orchard_value_balance = self
.orchard_shielded_data()
.map(|shielded_data| shielded_data.value_balance)
Expand Down
34 changes: 31 additions & 3 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use chrono::{DateTime, Utc};
use std::collections::HashSet;

use zebra_chain::{
amount::{Amount, Error as AmountError, NonNegative},
block::{Block, Hash, Header, Height},
parameters::{Network, NetworkUpgrade},
transaction,
Expand Down Expand Up @@ -101,14 +102,41 @@ pub fn subsidy_is_valid(block: &Block, network: Network) -> Result<(), BlockErro
let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?;
let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?;

// Consensus rule: The total value in zatoshi of transparent outputs from a
// coinbase transaction, minus vbalanceSapling, minus vbalanceOrchard, MUST NOT
// be greater than the value in zatoshi of block subsidy plus the transaction fees
// paid by transactions in this block.
// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus
let transparent_value_balance: Amount = subsidy::general::output_amounts(coinbase)
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
.iter()
.sum::<Result<Amount<NonNegative>, AmountError>>()
.expect("the sum of all outputs wil lalways be positive")
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
.constrain()
.expect("positive value always fit in `NegativeAllowed`");
let sapling_value_balance = coinbase.sapling_value_balance().sapling_amount();
let orchard_value_balance = coinbase.orchard_value_balance().orchard_amount();

let block_subsidy: Amount = subsidy::general::block_subsidy(height, network)
.expect("a valid block subsidy for this height and network")
.constrain()
.expect("positive value always fit in `NegativeAllowed`");

// Consensus rule implementation: block fee must be at least zero.
teor2345 marked this conversation as resolved.
Show resolved Hide resolved
let fees: Result<Amount<NonNegative>, AmountError> =
(transparent_value_balance - sapling_value_balance - orchard_value_balance - block_subsidy)
.expect("should not overflow")
.constrain();

if fees.is_err() {
Err(SubsidyError::NegativeFees)?
}
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved

// Validate founders reward and funding streams
let halving_div = subsidy::general::halving_divisor(height, network);
let canopy_activation_height = NetworkUpgrade::Canopy
.activation_height(network)
.expect("Canopy activation height is known");

// TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees

// Check founders reward and funding streams
if height < SLOW_START_INTERVAL {
unreachable!(
"unsupported block height: callers should handle blocks below {:?}",
Expand Down
2 changes: 1 addition & 1 deletion zebra-consensus/src/block/subsidy/funding_streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub fn funding_stream_values(
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
fn height_for_first_halving(network: Network) -> Height {
pub fn height_for_first_halving(network: Network) -> Height {
// First halving on Mainnet is at Canopy
// while in Testnet is at block constant height of `1_116_000`
// https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
Expand Down
20 changes: 17 additions & 3 deletions zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ mod test {
use super::*;
use color_eyre::Report;

use crate::block::subsidy::{
founders_reward::founders_reward,
funding_streams::{funding_stream_values, height_for_first_halving},
};

#[test]
fn halving_test() -> Result<(), Report> {
zebra_test::init();
Expand Down Expand Up @@ -307,8 +312,8 @@ mod test {
}

fn miner_subsidy_for_network(network: Network) -> Result<(), Report> {
use crate::block::subsidy::founders_reward::founders_reward;
let blossom_height = Blossom.activation_height(network).unwrap();
let first_halving_height = height_for_first_halving(network);

// Miner reward before Blossom is 80% of the total block reward
// 80*12.5/100 = 10 ZEC
Expand All @@ -330,8 +335,17 @@ mod test {
miner_subsidy(blossom_height, network, Some(founders_amount))
);

// TODO: After first halving, miner will get 2.5 ZEC per mined block
// but we need funding streams code to get this number
// After first halving, miner will get 2.5 ZEC per mined block (not counting fees)
let funding_stream_values = funding_stream_values(first_halving_height, network)?
.iter()
.map(|row| row.1)
.sum::<Result<Amount<NonNegative>, Error>>()
.unwrap();

assert_eq!(
Amount::try_from(250_000_000),
miner_subsidy(first_halving_height, network, Some(funding_stream_values))
);

// TODO: After second halving, there will be no funding streams, and
// miners will get all the block reward
Expand Down
79 changes: 59 additions & 20 deletions zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
//! Tests for block verification

use std::sync::Arc;
use super::*;

oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved
use std::{convert::TryFrom, sync::Arc};

use chrono::Utc;
use color_eyre::eyre::{eyre, Report};
use once_cell::sync::Lazy;
use tower::{buffer::Buffer, util::BoxService};

use zebra_chain::{
amount::Amount,
block::{
self,
tests::generate::{large_multi_transaction_block, large_single_transaction_block},
Expand All @@ -26,8 +29,6 @@ use crate::{
script, transaction,
};

use super::*;

static VALID_BLOCK_TRANSCRIPT: Lazy<
Vec<(Arc<Block>, Result<block::Hash, ExpectedTranscriptError>)>,
> = Lazy::new(|| {
Expand Down Expand Up @@ -196,7 +197,6 @@ fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> {
#[test]
fn difficulty_validation_failure() -> Result<(), Report> {
zebra_test::init();
use crate::error::*;

// Get a block in the mainnet, and mangle its difficulty field
let block =
Expand Down Expand Up @@ -306,8 +306,6 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> {
#[test]
fn coinbase_validation_failure() -> Result<(), Report> {
zebra_test::init();
use crate::error::*;

let network = Network::Mainnet;

// Get a block in the mainnet that is inside the founders reward period,
Expand Down Expand Up @@ -379,9 +377,6 @@ fn coinbase_validation_failure() -> Result<(), Report> {
#[test]
fn founders_reward_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 founders reward period.
Expand All @@ -393,12 +388,16 @@ fn founders_reward_validation_failure() -> Result<(), Report> {
let tx = block
.transactions
.get(0)
.map(|transaction| Transaction::V3 {
inputs: transaction.inputs().to_vec(),
outputs: vec![transaction.outputs()[0].clone()],
lock_time: transaction.lock_time(),
expiry_height: Height(0),
joinsplit_data: None,
.map(|transaction| {
let mut output = transaction.outputs()[0].clone();
output.value = Amount::try_from(i32::MAX).unwrap();
Transaction::V3 {
inputs: transaction.inputs().to_vec(),
outputs: vec![output],
lock_time: transaction.lock_time(),
expiry_height: Height(0),
joinsplit_data: None,
}
})
.unwrap();

Expand Down Expand Up @@ -451,9 +450,51 @@ fn funding_stream_validation_for_network(network: Network) -> Result<(), Report>
#[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| {
let mut output = transaction.outputs()[0].clone();
output.value = Amount::try_from(i32::MAX).unwrap();
Transaction::V4 {
inputs: transaction.inputs().to_vec(),
outputs: vec![output],
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);
oxarbitrage marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}

#[test]
fn negative_fee_validation_failure() -> Result<(), Report> {
zebra_test::init();
let network = Network::Mainnet;

// Get a block in the mainnet that is inside the funding stream period.
Expand Down Expand Up @@ -484,9 +525,7 @@ fn funding_stream_validation_failure() -> Result<(), Report> {

// Validate it
let result = check::subsidy_is_valid(&block, network).unwrap_err();
let expected = BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::FundingStreamNotFound,
));
let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NegativeFees));
assert_eq!(expected, result);

Ok(())
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 @@ -24,6 +24,9 @@ pub enum SubsidyError {

#[error("funding stream expected output not found")]
FundingStreamNotFound,

#[error("transaction fees must be positive")]
NegativeFees,
}

#[derive(Error, Clone, Debug, PartialEq, Eq)]
Expand Down