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

change(consensus): Require that coinbase transactions balance exactly after NU6 activation #8727

Merged
merged 41 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bf1278a
Addresses clippy lints
arya2 Jul 25, 2024
aa88979
checks network magic and returns early from `is_regtest()`
arya2 Jul 22, 2024
d9a53e6
Moves `subsidy.rs` to `zebra-chain`, refactors funding streams into …
arya2 Jul 23, 2024
124e249
Replaces Vec with HashMap, adds `ConfiguredFundingStreams` type and c…
arya2 Jul 24, 2024
cbf7ef3
Empties recipients list
arya2 Jul 24, 2024
54af589
Adds a comment on num_addresses calculation being invalid for configu…
arya2 Jul 24, 2024
db434c3
Documentation fixes, minor cleanup, renames a test, adds TODOs, and f…
arya2 Jul 24, 2024
7d7f606
Removes unnecessary `ParameterSubsidy` impl for &Network, adds docs a…
arya2 Jul 24, 2024
77a6f2c
Adds a "deferred" FundingStreamReceiver, adds a post-NU6 funding stre…
arya2 Jul 24, 2024
dbc7ca9
adds `lockbox_input_value()` fn
arya2 Jul 24, 2024
0f962c5
Adds TODOs for linking to relevant ZIPs and updating height ranges
arya2 Jul 24, 2024
9bca31f
Adds `nu6_lockbox_funding_stream` acceptance test
arya2 Jul 24, 2024
0d22b68
updates funding stream values test to check post-NU6 funding streams …
arya2 Jul 24, 2024
039c9a1
Reverts Mainnet/Testnet NU6 activation height definitions, updates `t…
arya2 Jul 24, 2024
2f14517
reverts unnecessary refactor
arya2 Jul 24, 2024
33a5545
appease clippy
arya2 Jul 24, 2024
1183d49
Adds a test for `lockbox_input_value()`
arya2 Jul 24, 2024
b523bf5
Applies suggestions from code review
arya2 Jul 25, 2024
7df2e28
Fixes potential panic
arya2 Jul 26, 2024
95378fd
Merge branch 'main' into deferred-pool
arya2 Jul 29, 2024
7173af7
Fixes bad merge
arya2 Jul 29, 2024
c6244e6
Update zebra-chain/src/parameters/network_upgrade.rs
arya2 Jul 29, 2024
166bc23
Updates acceptance test to check that invalid blocks are rejected
arya2 Jul 29, 2024
c113601
Checks that the original valid block template at height 2 is accepted…
arya2 Jul 29, 2024
4627c08
Reverts changes for coinbase should balance exactly ZIP
arya2 Jul 29, 2024
6aa2858
Merge branch 'main' into deferred-pool
mpguerra Jul 30, 2024
b82f2be
Merge branch 'main' into deferred-pool
arya2 Jul 31, 2024
126fdab
updates test name
arya2 Aug 1, 2024
bf49432
Updates deferred pool funding stream name to "Lockbox", moves post-NU…
arya2 Aug 1, 2024
14fbfb4
Updates `get_block_subsidy()` RPC method to exclude lockbox funding s…
arya2 Aug 1, 2024
7987c76
Adds a TODO for updating `FundingStreamReceiver::name()` method docs
arya2 Aug 1, 2024
e5eaf38
Updates `FundingStreamRecipient::new()` to accept an iterator of item…
arya2 Aug 1, 2024
8949301
Uses FPF Testnet address for post-NU6 testnet funding streams
arya2 Aug 1, 2024
cdb1684
Updates the NU6 consensus branch id
arya2 Aug 1, 2024
6057fdf
checks that coinbase transactions balance exactly
arya2 Jul 29, 2024
f308d49
updates test name
arya2 Aug 1, 2024
782e9ba
Merge branch 'main' into coinbase-should-balance-exactly
arya2 Aug 5, 2024
12dc770
Merge branch 'main' into coinbase-should-balance-exactly
mpguerra Aug 6, 2024
31678a3
Merge branch 'main' into coinbase-should-balance-exactly
upbqdn Aug 7, 2024
ad2a425
Add a TODO
upbqdn Aug 8, 2024
78dc6b6
Refactor `miner_fees_are_valid`
upbqdn Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion zebra-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,12 @@ where
})?;

check::miner_fees_are_valid(
&block,
&coinbase_tx,
height,
block_miner_fees,
expected_block_subsidy,
expected_deferred_amount,
&network,
)?;

// Finally, submit the block for contextual verification.
Expand Down
37 changes: 24 additions & 13 deletions zebra-consensus/src/block/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use zebra_chain::{
amount::{Amount, Error as AmountError, NonNegative},
block::{Block, Hash, Header, Height},
parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade},
transaction,
transaction::{self, Transaction},
work::{
difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
equihash,
Expand Down Expand Up @@ -234,22 +234,24 @@ pub fn subsidy_is_valid(
///
/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus
pub fn miner_fees_are_valid(
block: &Block,
coinbase_tx: &Transaction,
height: Height,
block_miner_fees: Amount<NonNegative>,
expected_block_subsidy: Amount<NonNegative>,
expected_deferred_amount: Amount<NonNegative>,
network: &Network,
) -> Result<(), BlockError> {
let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?;

let transparent_value_balance: Amount = subsidy::general::output_amounts(coinbase)
let transparent_value_balance = subsidy::general::output_amounts(coinbase_tx)
.iter()
.sum::<Result<Amount<NonNegative>, AmountError>>()
.map_err(|_| SubsidyError::SumOverflow)?
.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 sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount();
let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount();

// TODO: Update the quote below once its been updated for NU6.
//
// # Consensus
//
// > The total value in zatoshi of transparent outputs from a coinbase transaction,
Expand All @@ -260,17 +262,26 @@ pub fn miner_fees_are_valid(
//
// The expected lockbox funding stream output of the coinbase transaction is also subtracted
// from the block subsidy value plus the transaction fees paid by transactions in this block.
//
// TODO: Update the quote from the protocol specification once its been updated to reflect the changes in
// https://zips.z.cash/draft-nuttycom-funding-allocation and https://zips.z.cash/draft-hopwood-coinbase-balance.
let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance)
.map_err(|_| SubsidyError::SumOverflow)?;
let right = (expected_block_subsidy + block_miner_fees - expected_deferred_amount)
.map_err(|_| SubsidyError::SumOverflow)?;

if left > right {
Err(SubsidyError::InvalidMinerFees)?;
}
// TODO: Updadte the quotes below if the final phrasing changes in the spec for NU6.
//
// # Consensus
//
// > [Pre-NU6] The total output of a coinbase transaction MUST NOT be greater than its total
// input.
//
// > [NU6 onward] The total output of a coinbase transaction MUST be equal to its total input.
if if NetworkUpgrade::current(network, height) < NetworkUpgrade::Nu6 {
left > right
} else {
left != right
} {
Err(SubsidyError::InvalidMinerFees)?
};

Ok(())
}
Expand Down
2 changes: 2 additions & 0 deletions zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//!
//! [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies

// TODO: Move the contents of this mod to the parent mod and remove this mod.

use std::collections::HashSet;

use zebra_chain::{
Expand Down
55 changes: 28 additions & 27 deletions zebra-consensus/src/block/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,12 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> {
for (&height, block) in block_iter {
let height = Height(height);
if height > network.slow_start_shift() {
let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize");
let coinbase_tx = check::coinbase_is_first(
&Block::zcash_deserialize(&block[..]).expect("block should deserialize"),
)?;

let expected_block_subsidy = block_subsidy(height, &network)?;

// TODO: Add link to lockbox stream ZIP
let expected_deferred_amount = subsidy::funding_streams::funding_stream_values(
height,
Expand All @@ -521,17 +525,16 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> {
.remove(&FundingStreamReceiver::Deferred)
.unwrap_or_default();

// fake the miner fee to a big amount
let miner_fees = Amount::try_from(MAX_MONEY / 2).unwrap();

// Validate
let result = check::miner_fees_are_valid(
&block,
miner_fees,
assert!(check::miner_fees_are_valid(
&coinbase_tx,
height,
// Set the miner fees to a high-enough amount.
Amount::try_from(MAX_MONEY / 2).unwrap(),
expected_block_subsidy,
expected_deferred_amount,
);
assert!(result.is_ok());
&network,
)
.is_ok(),);
}
}

Expand All @@ -542,9 +545,8 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> {
fn miner_fees_validation_failure() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let network = Network::Mainnet;
let block =
Arc::<Block>::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..])
.expect("block should deserialize");
let block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..])
.expect("block should deserialize");
let height = block.coinbase_height().expect("valid coinbase height");
let expected_block_subsidy = block_subsidy(height, &network)?;
// TODO: Add link to lockbox stream ZIP
Expand All @@ -554,22 +556,21 @@ fn miner_fees_validation_failure() -> Result<(), Report> {
.remove(&FundingStreamReceiver::Deferred)
.unwrap_or_default();

// fake the miner fee to a small amount
let miner_fees = Amount::zero();

// Validate
let result = check::miner_fees_are_valid(
&block,
miner_fees,
expected_block_subsidy,
expected_deferred_amount,
assert_eq!(
check::miner_fees_are_valid(
check::coinbase_is_first(&block)?.as_ref(),
height,
// Set the miner fee to an invalid amount.
Amount::zero(),
expected_block_subsidy,
expected_deferred_amount,
&network
),
Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees,
)))
);

let expected = Err(BlockError::Transaction(TransactionError::Subsidy(
SubsidyError::InvalidMinerFees,
)));
assert_eq!(expected, result);

Ok(())
}

Expand Down
47 changes: 45 additions & 2 deletions zebrad/tests/acceptance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3236,10 +3236,10 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> {
/// Test successful block template submission as a block proposal or submission on a custom Testnet.
///
/// This test can be run locally with:
/// `RUSTFLAGS='--cfg zcash_unstable="nu6"' cargo test --package zebrad --test acceptance --features getblocktemplate-rpcs -- nu6_funding_streams --exact --show-output`
/// `RUSTFLAGS='--cfg zcash_unstable="nu6"' cargo test --package zebrad --test acceptance --features getblocktemplate-rpcs -- nu6_funding_streams_and_coinbase_balance --exact --show-output`
#[tokio::test(flavor = "multi_thread")]
#[cfg(all(feature = "getblocktemplate-rpcs", zcash_unstable = "nu6"))]
async fn nu6_funding_streams() -> Result<()> {
async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> {
use zebra_chain::{
chain_sync_status::MockSyncStatus,
parameters::{
Expand Down Expand Up @@ -3470,6 +3470,49 @@ async fn nu6_funding_streams() -> Result<()> {
"invalid block with excessive coinbase output value should be rejected"
);

// Use an invalid coinbase transaction (with an output value less than than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`)
let network = base_network_params
.clone()
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
height_range: Some(Height(1)..Height(100)),
recipients: make_configured_recipients_with_lockbox_numerator(20),
})
.to_network();

let (coinbase_txn, default_roots) = generate_coinbase_and_roots(
&network,
Height(block_template.height),
&miner_address,
&[],
history_tree.clone(),
true,
vec![],
);

let block_template = GetBlockTemplate {
coinbase_txn,
block_commitments_hash: default_roots.block_commitments_hash,
light_client_root_hash: default_roots.block_commitments_hash,
final_sapling_root_hash: default_roots.block_commitments_hash,
default_roots,
..block_template
};

let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?;

// Submit the invalid block with an excessive coinbase input value
let submit_block_response = get_block_template_rpc_impl
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
.await?;

tracing::info!(?submit_block_response, "submitted invalid block");

assert_eq!(
submit_block_response,
submit_block::Response::ErrorResponse(submit_block::ErrorResponse::Rejected),
"invalid block with insufficient coinbase output value should be rejected"
);

// Check that the original block template can be submitted successfully
let proposal_block =
proposal_block_from_template(&valid_original_block_template, None, NetworkUpgrade::Nu6)?;
Expand Down
Loading