diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 5bbc774d35c..e247ac9fa6e 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -19,14 +19,15 @@ use crate::{ }; mod commitment; -mod error; mod hash; mod header; mod height; mod serialize; +pub mod error; pub mod genesis; pub mod merkle; +pub mod subsidy; #[cfg(any(test, feature = "proptest-impl"))] pub mod arbitrary; diff --git a/zebra-chain/src/block/error.rs b/zebra-chain/src/block/error.rs index 1981cfe6050..622cfcf5148 100644 --- a/zebra-chain/src/block/error.rs +++ b/zebra-chain/src/block/error.rs @@ -2,9 +2,22 @@ use thiserror::Error; +use crate::error::SubsidyError; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + +#[derive(Clone, Error, Debug, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[allow(missing_docs)] -#[derive(Error, Debug, PartialEq, Eq)] pub enum BlockError { + #[error("block has no transactions")] + NoTransactions, + #[error("transaction has wrong consensus branch id for block network upgrade")] WrongTransactionConsensusBranchId, + + #[error("block failed subsidy validation")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Subsidy(#[from] SubsidyError), } diff --git a/zebra-consensus/src/block/subsidy.rs b/zebra-chain/src/block/subsidy.rs similarity index 100% rename from zebra-consensus/src/block/subsidy.rs rename to zebra-chain/src/block/subsidy.rs diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-chain/src/block/subsidy/funding_streams.rs similarity index 99% rename from zebra-consensus/src/block/subsidy/funding_streams.rs rename to zebra-chain/src/block/subsidy/funding_streams.rs index ce0bbf792ad..32a4b9ffc07 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-chain/src/block/subsidy/funding_streams.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; -use zebra_chain::{ +use crate::{ amount::{Amount, Error, NonNegative}, block::Height, parameters::{subsidy::*, Network, NetworkUpgrade::*}, diff --git a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs b/zebra-chain/src/block/subsidy/funding_streams/tests.rs similarity index 88% rename from zebra-consensus/src/block/subsidy/funding_streams/tests.rs rename to zebra-chain/src/block/subsidy/funding_streams/tests.rs index 626b983fac6..f650d4e3ef8 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs +++ b/zebra-chain/src/block/subsidy/funding_streams/tests.rs @@ -1,17 +1,19 @@ //! Tests for funding streams. use color_eyre::Report; -use zebra_chain::parameters::{ - subsidy::FundingStreamReceiver, - testnet::{ - self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient, - ConfiguredFundingStreams, + +use crate::{ + block::subsidy::general::block_subsidy_pre_nsm, + parameters::{ + subsidy::FundingStreamReceiver, + testnet::{ + self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient, + ConfiguredFundingStreams, + }, + NetworkKind, }, - NetworkKind, }; -use crate::block::subsidy::general::block_subsidy; - use super::*; /// Check mainnet funding stream values are correct for the entire period. @@ -26,7 +28,7 @@ fn test_funding_stream_values() -> Result<(), Report> { assert!(funding_stream_values( canopy_height_minus1, network, - block_subsidy(canopy_height_minus1, network)? + block_subsidy_pre_nsm(canopy_height_minus1, network)? )? .is_empty()); @@ -50,7 +52,7 @@ fn test_funding_stream_values() -> Result<(), Report> { funding_stream_values( canopy_height, network, - block_subsidy(canopy_height, network)? + block_subsidy_pre_nsm(canopy_height, network)? ) .unwrap(), hash_map @@ -60,7 +62,7 @@ fn test_funding_stream_values() -> Result<(), Report> { funding_stream_values( canopy_height_plus1, network, - block_subsidy(canopy_height_plus1, network)? + block_subsidy_pre_nsm(canopy_height_plus1, network)? ) .unwrap(), hash_map @@ -70,7 +72,7 @@ fn test_funding_stream_values() -> Result<(), Report> { funding_stream_values( canopy_height_plus2, network, - block_subsidy(canopy_height_plus2, network)? + block_subsidy_pre_nsm(canopy_height_plus2, network)? ) .unwrap(), hash_map @@ -82,11 +84,11 @@ fn test_funding_stream_values() -> Result<(), Report> { let last = (end - 1).unwrap(); assert_eq!( - funding_stream_values(last, network, block_subsidy(last, network)?).unwrap(), + funding_stream_values(last, network, block_subsidy_pre_nsm(last, network)?).unwrap(), hash_map ); - assert!(funding_stream_values(end, network, block_subsidy(end, network)?)?.is_empty()); + assert!(funding_stream_values(end, network, block_subsidy_pre_nsm(end, network)?)?.is_empty()); // TODO: Replace this with Mainnet once there's an NU6 activation height defined for Mainnet let network = testnet::Parameters::build() @@ -137,7 +139,8 @@ fn test_funding_stream_values() -> Result<(), Report> { Height(nu6_height.0 + 1), ] { assert_eq!( - funding_stream_values(height, &network, block_subsidy(height, &network)?).unwrap(), + funding_stream_values(height, &network, block_subsidy_pre_nsm(height, &network)?) + .unwrap(), hash_map ); } @@ -191,7 +194,8 @@ fn test_funding_stream_values() -> Result<(), Report> { Height(nu6_height.0 + 1), ] { assert_eq!( - funding_stream_values(height, &network, block_subsidy(height, &network)?).unwrap(), + funding_stream_values(height, &network, block_subsidy_pre_nsm(height, &network)?) + .unwrap(), hash_map ); } diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-chain/src/block/subsidy/general.rs similarity index 84% rename from zebra-consensus/src/block/subsidy/general.rs rename to zebra-chain/src/block/subsidy/general.rs index 343d4d7d8d0..ed1689d6d0d 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-chain/src/block/subsidy/general.rs @@ -6,15 +6,14 @@ use std::collections::HashSet; -use zebra_chain::{ +use crate::{ amount::{Amount, Error, NonNegative}, - block::{Height, HeightDiff}, + block::{subsidy::funding_streams::funding_stream_values, Height, HeightDiff}, + error::SubsidyError, parameters::{subsidy::*, Network, NetworkUpgrade::*}, transaction::Transaction, }; -use crate::{block::SubsidyError, funding_stream_values}; - /// The divisor used for halvings. /// /// `1 << Halving(height)`, as described in [protocol specification §7.8][7.8] @@ -23,45 +22,72 @@ use crate::{block::SubsidyError, funding_stream_values}; /// /// Returns `None` if the divisor would overflow a `u64`. pub fn halving_divisor(height: Height, network: &Network) -> Option { - // Some far-future shifts can be more than 63 bits - 1u64.checked_shl(num_halvings(height, network)) -} - -/// The halving index for a block height and network. -/// -/// `Halving(height)`, as described in [protocol specification §7.8][7.8] -/// -/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies -pub fn num_halvings(height: Height, network: &Network) -> u32 { - let slow_start_shift = network.slow_start_shift(); let blossom_height = Blossom .activation_height(network) .expect("blossom activation height should be available"); - let halving_index = if height < slow_start_shift { - 0 - } else if height < blossom_height { - let pre_blossom_height = height - slow_start_shift; - pre_blossom_height / network.pre_blossom_halving_interval() + if height < blossom_height { + let pre_blossom_height = height - network.slow_start_shift(); + let halving_shift = pre_blossom_height / PRE_BLOSSOM_HALVING_INTERVAL; + + let halving_div = 1u64 + .checked_shl( + halving_shift + .try_into() + .expect("already checked for negatives"), + ) + .expect("pre-blossom heights produce small shifts"); + + Some(halving_div) } else { - let pre_blossom_height = blossom_height - slow_start_shift; + let pre_blossom_height = blossom_height - network.slow_start_shift(); let scaled_pre_blossom_height = pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO); let post_blossom_height = height - blossom_height; - (scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval() - }; + let halving_shift = + (scaled_pre_blossom_height + post_blossom_height) / POST_BLOSSOM_HALVING_INTERVAL; + + // Some far-future shifts can be more than 63 bits + 1u64.checked_shl( + halving_shift + .try_into() + .expect("already checked for negatives"), + ) + } +} + +#[cfg(zcash_unstable = "nsm")] +pub fn block_subsidy( + height: Height, + network: &Network, + money_reserve: Amount, +) -> Result, SubsidyError> { + let nsm_activation_height = ZFuture + .activation_height(network) + .expect("ZFuture activation height should be available"); - halving_index - .try_into() - .expect("already checked for negatives") + if height < nsm_activation_height { + block_subsidy_pre_nsm(height, network) + } else { + let money_reserve: i64 = money_reserve.into(); + let money_reserve: i128 = money_reserve.into(); + const BLOCK_SUBSIDY_DENOMINATOR: i128 = 10_000_000_000; + const BLOCK_SUBSIDY_NUMERATOR: i128 = 4_126; + + // calculate the block subsidy (in zatoshi) using the money reserve, note the rounding up + let subsidy = (money_reserve * BLOCK_SUBSIDY_NUMERATOR + (BLOCK_SUBSIDY_DENOMINATOR - 1)) + / BLOCK_SUBSIDY_DENOMINATOR; + + Ok(subsidy.try_into().expect("subsidy should fit in Amount")) + } } /// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8] /// /// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies -pub fn block_subsidy( +pub fn block_subsidy_pre_nsm( height: Height, network: &Network, ) -> Result, SubsidyError> { @@ -131,7 +157,7 @@ fn lockbox_input_value(network: &Network, height: Height) -> Amount return Amount::zero(); }; - let expected_block_subsidy = block_subsidy(nu6_activation_height, network) + let expected_block_subsidy = block_subsidy_pre_nsm(nu6_activation_height, network) .expect("block at NU6 activation height must have valid expected subsidy"); let &deferred_amount_per_block = funding_stream_values(nu6_activation_height, network, expected_block_subsidy) @@ -156,11 +182,11 @@ fn lockbox_input_value(network: &Network, height: Height) -> Amount #[cfg(test)] mod test { use super::*; - use color_eyre::Report; - use zebra_chain::parameters::testnet::{ + use crate::parameters::testnet::{ self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient, ConfiguredFundingStreams, }; + use color_eyre::Report; #[test] fn halving_test() -> Result<(), Report> { @@ -307,32 +333,32 @@ mod test { // https://z.cash/support/faq/#what-is-slow-start-mining assert_eq!( Amount::::try_from(1_250_000_000)?, - block_subsidy((network.slow_start_interval() + 1).unwrap(), network)? + block_subsidy_pre_nsm((network.slow_start_interval() + 1).unwrap(), network)? ); assert_eq!( Amount::::try_from(1_250_000_000)?, - block_subsidy((blossom_height - 1).unwrap(), network)? + block_subsidy_pre_nsm((blossom_height - 1).unwrap(), network)? ); // After Blossom the block subsidy is reduced to 6.25 ZEC without halving // https://z.cash/upgrade/blossom/ assert_eq!( Amount::::try_from(625_000_000)?, - block_subsidy(blossom_height, network)? + block_subsidy_pre_nsm(blossom_height, network)? ); // After the 1st halving, the block subsidy is reduced to 3.125 ZEC // https://z.cash/upgrade/canopy/ assert_eq!( Amount::::try_from(312_500_000)?, - block_subsidy(first_halving_height, network)? + block_subsidy_pre_nsm(first_halving_height, network)? ); // After the 2nd halving, the block subsidy is reduced to 1.5625 ZEC // See "7.8 Calculation of Block Subsidy and Founders' Reward" assert_eq!( Amount::::try_from(156_250_000)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(), network )? @@ -342,7 +368,7 @@ mod test { // Check that the block subsidy rounds down correctly, and there are no errors assert_eq!( Amount::::try_from(4_882_812)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 6)).unwrap(), network )? @@ -352,7 +378,7 @@ mod test { // Check that the block subsidy is calculated correctly at the limit assert_eq!( Amount::::try_from(1)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 28)).unwrap(), network )? @@ -362,7 +388,7 @@ mod test { // Check that there are no errors assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 29)).unwrap(), network )? @@ -370,7 +396,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 39)).unwrap(), network )? @@ -378,7 +404,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 49)).unwrap(), network )? @@ -386,7 +412,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 59)).unwrap(), network )? @@ -395,7 +421,7 @@ mod test { // The largest possible integer divisor assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 62)).unwrap(), network )? @@ -404,7 +430,7 @@ mod test { // Other large divisors which should also result in zero assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 63)).unwrap(), network )? @@ -412,7 +438,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 64)).unwrap(), network )? @@ -420,17 +446,17 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height(Height::MAX_AS_U32 / 4), network)? + block_subsidy_pre_nsm(Height(Height::MAX_AS_U32 / 4), network)? ); assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height(Height::MAX_AS_U32 / 2), network)? + block_subsidy_pre_nsm(Height(Height::MAX_AS_U32 / 2), network)? ); assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height::MAX, network)? + block_subsidy_pre_nsm(Height::MAX, network)? ); Ok(()) @@ -502,33 +528,4 @@ mod test { Ok(()) } - - #[test] - fn check_height_for_num_halvings() { - for network in Network::iter() { - for halving in 1..1000 { - let Some(height_for_halving) = - zebra_chain::parameters::subsidy::height_for_halving(halving, &network) - else { - panic!("could not find height for halving {halving}"); - }; - - let prev_height = height_for_halving - .previous() - .expect("there should be a previous height"); - - assert_eq!( - halving, - num_halvings(height_for_halving, &network), - "num_halvings should match the halving index" - ); - - assert_eq!( - halving - 1, - num_halvings(prev_height, &network), - "num_halvings for the prev height should be 1 less than the halving index" - ); - } - } - } } diff --git a/zebra-chain/src/error.rs b/zebra-chain/src/error.rs index a3182a21feb..60b036ee215 100644 --- a/zebra-chain/src/error.rs +++ b/zebra-chain/src/error.rs @@ -1,6 +1,11 @@ //! Errors that can occur inside any `zebra-chain` submodule. use thiserror::Error; +use crate::{amount, block::error::BlockError}; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + /// Errors related to random bytes generation. #[derive(Error, Copy, Clone, Debug, PartialEq, Eq)] pub enum RandError { @@ -51,3 +56,81 @@ pub enum AddressError { #[error("Randomness did not hash into the Jubjub group for producing a new diversifier")] DiversifierGenerationFailure, } + +#[derive(Error, Clone, Debug, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum SubsidyError { + #[error("no coinbase transaction in block")] + NoCoinbase, + + #[error("funding stream expected output not found")] + FundingStreamNotFound, + + #[error("miner fees are invalid")] + InvalidMinerFees, + + #[error("a sum of amounts overflowed")] + SumOverflow, + + #[error("unsupported height")] + UnsupportedHeight, + + #[error("invalid amount")] + InvalidAmount(amount::Error), + + #[error("invalid burn amount")] + InvalidBurnAmount, +} + +impl From for SubsidyError { + fn from(amount: amount::Error) -> Self { + Self::InvalidAmount(amount) + } +} + +#[derive(Error, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +#[allow(missing_docs)] +pub enum CoinbaseTransactionError { + #[error("first transaction must be coinbase")] + Position, + + #[error("coinbase input found in non-coinbase transaction")] + AfterFirst, + + #[error("coinbase transaction MUST NOT have any JoinSplit descriptions")] + HasJoinSplit, + + #[error("coinbase transaction MUST NOT have any Spend descriptions")] + HasSpend, + + #[error("coinbase transaction MUST NOT have any Output descriptions pre-Heartwood")] + HasOutputPreHeartwood, + + #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] + HasEnableSpendsOrchard, + + #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")] + OutputsNotDecryptable, + + #[error("coinbase inputs MUST NOT exist in mempool")] + InMempool, + + #[error( + "coinbase expiry {expiry_height:?} must be the same as the block {block_height:?} \ + after NU5 activation, failing transaction: {transaction_hash:?}" + )] + ExpiryBlockHeight { + expiry_height: Option, + block_height: crate::block::Height, + transaction_hash: crate::transaction::Hash, + }, + + #[error("coinbase transaction failed subsidy validation")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Subsidy(#[from] SubsidyError), + + #[error("TODO")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Block(#[from] BlockError), +} diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 929e91f5841..651ac6caa50 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -616,7 +616,7 @@ impl Parameters { nu5: nu5_activation_height, nu6: nu6_activation_height, #[cfg(zcash_unstable = "nsm")] - zfuture: nu5_activation_height.map(|height| height + 1), + zfuture: nu5_activation_height.map(|height| height + 101), ..Default::default() }) .with_halving_interval(PRE_BLOSSOM_REGTEST_HALVING_INTERVAL); diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index 0874cf6900f..d54cf6b945a 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -147,7 +147,7 @@ fn activates_network_upgrades_correctly() { // TODO: Remove this once the testnet parameters are being serialized (#8920). (Height(100), NetworkUpgrade::Nu5), #[cfg(zcash_unstable = "nsm")] - (Height(101), NetworkUpgrade::ZFuture), + (Height(201), NetworkUpgrade::ZFuture), ]; for (network, expected_activation_heights) in [ diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index b75e6286201..aa6d4ddb2a8 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -925,7 +925,22 @@ pub fn transaction_to_fake_v5( }, v5 @ V5 { .. } => v5.clone(), #[cfg(zcash_unstable = "nsm")] - ZFuture { .. } => todo!(), + ZFuture { + inputs, + outputs, + lock_time, + sapling_shielded_data, + orchard_shielded_data, + .. + } => V5 { + network_upgrade: block_nu, + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: height, + sapling_shielded_data: sapling_shielded_data.clone(), + orchard_shielded_data: orchard_shielded_data.clone(), + }, } } diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index b2b33e878a7..ece16daf9c9 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -8,7 +8,9 @@ use core::fmt; use std::{borrow::Borrow, collections::HashMap}; #[cfg(any(test, feature = "proptest-impl"))] -use crate::{amount::MAX_MONEY, transaction::Transaction, transparent}; +use crate::{transaction::Transaction, transparent}; + +use crate::amount::MAX_MONEY; #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -395,6 +397,15 @@ impl ValueBalance { deferred, }) } + + #[cfg(zcash_unstable = "nsm")] + pub fn money_reserve(&self) -> Amount { + let max_money: Amount = MAX_MONEY + .try_into() + .expect("MAX_MONEY should be a valid amount"); + (max_money - self.transparent - self.sprout - self.sapling - self.orchard - self.deferred) + .expect("Expected non-negative value") + } } #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 2b0013a5a3d..7e9535f4b71 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -22,11 +22,7 @@ use tower::{Service, ServiceExt}; use tracing::Instrument; use zebra_chain::{ - amount::Amount, - block, - parameters::{subsidy::FundingStreamReceiver, Network}, - transparent, - work::equihash, + amount::Amount, block, error::SubsidyError, parameters::Network, transparent, work::equihash, }; use zebra_state as zs; @@ -34,7 +30,6 @@ use crate::{error::*, transaction as tx, BoxError}; pub mod check; pub mod request; -pub mod subsidy; pub use request::Request; @@ -214,11 +209,8 @@ where let now = Utc::now(); check::time_is_valid_at(&block.header, now, &height, &hash) .map_err(VerifyBlockError::Time)?; - let coinbase_tx = check::coinbase_is_first(&block)?; - - let expected_block_subsidy = subsidy::general::block_subsidy(height, &network)?; - - check::subsidy_is_valid(&block, &network, expected_block_subsidy)?; + let coinbase_tx = zs::check::coinbase_is_first(&block) + .map_err(|err| VerifyBlockError::Transaction(TransactionError::Coinbase(err)))?; // Now do the slower checks @@ -284,16 +276,6 @@ where })?; } - // TODO: Add link to lockbox stream ZIP - let expected_deferred_amount = subsidy::funding_streams::funding_stream_values( - height, - &network, - expected_block_subsidy, - ) - .expect("we always expect a funding stream hashmap response even if empty") - .remove(&FundingStreamReceiver::Deferred) - .unwrap_or_default(); - let block_miner_fees = block_miner_fees.map_err(|amount_error| BlockError::SummingMinerFees { height, @@ -301,15 +283,6 @@ where source: amount_error, })?; - check::miner_fees_are_valid( - &coinbase_tx, - height, - block_miner_fees, - expected_block_subsidy, - expected_deferred_amount, - &network, - )?; - // Finally, submit the block for contextual verification. let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); @@ -320,7 +293,8 @@ where height, new_outputs, transaction_hashes, - deferred_balance: Some(expected_deferred_amount), + deferred_balance: None, + block_miner_fees: Some(block_miner_fees), }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 189cfdc8493..9f422dfc0c6 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -1,14 +1,13 @@ //! Consensus check functions -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; use chrono::{DateTime, Utc}; use zebra_chain::{ - amount::{Amount, Error as AmountError, NonNegative}, block::{Block, Hash, Header, Height}, - parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade}, - transaction::{self, Transaction}, + parameters::Network, + transaction, work::{ difficulty::{ExpandedDifficulty, ParameterDifficulty as _}, equihash, @@ -17,50 +16,6 @@ use zebra_chain::{ use crate::error::*; -use super::subsidy; - -/// Checks if there is exactly one coinbase transaction in `Block`, -/// and if that coinbase transaction is the first transaction in the block. -/// Returns the coinbase transaction is successful. -/// -/// > A transaction that has a single transparent input with a null prevout field, -/// > is called a coinbase transaction. Every block has a single coinbase -/// > transaction as the first transaction in the block. -/// -/// -pub fn coinbase_is_first(block: &Block) -> Result, BlockError> { - // # Consensus - // - // > A block MUST have at least one transaction - // - // - let first = block - .transactions - .first() - .ok_or(BlockError::NoTransactions)?; - // > The first transaction in a block MUST be a coinbase transaction, - // > and subsequent transactions MUST NOT be coinbase transactions. - // - // - // - // > A transaction that has a single transparent input with a null prevout - // > field, is called a coinbase transaction. - // - // - let mut rest = block.transactions.iter().skip(1); - if !first.is_coinbase() { - Err(TransactionError::CoinbasePosition)?; - } - // > A transparent input in a non-coinbase transaction MUST NOT have a null prevout - // - // - if !rest.all(|tx| tx.is_valid_non_coinbase()) { - Err(TransactionError::CoinbaseAfterFirst)?; - } - - Ok(first.clone()) -} - /// Returns `Ok(ExpandedDifficulty)` if the`difficulty_threshold` of `header` is at least as difficult as /// the target difficulty limit for `network` (PoWLimit) /// @@ -141,151 +96,6 @@ pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error header.solution.check(header) } -/// Returns `Ok(())` if the block subsidy in `block` is valid for `network` -/// -/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts -pub fn subsidy_is_valid( - block: &Block, - network: &Network, - expected_block_subsidy: Amount, -) -> Result<(), BlockError> { - let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; - let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?; - - // Validate funding streams - let Some(halving_div) = subsidy::general::halving_divisor(height, network) else { - // Far future halving, with no founders reward or funding streams - return Ok(()); - }; - - let canopy_activation_height = NetworkUpgrade::Canopy - .activation_height(network) - .expect("Canopy activation height is known"); - - // TODO: Add this as a field on `testnet::Parameters` instead of checking `disable_pow()`, this is 0 for Regtest in zcashd, - // see - let slow_start_interval = if network.disable_pow() { - Height(0) - } else { - network.slow_start_interval() - }; - - if height < slow_start_interval { - unreachable!( - "unsupported block height: callers should handle blocks below {:?}", - slow_start_interval - ) - } else if halving_div.count_ones() != 1 { - unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") - } else if height < canopy_activation_height { - // Founders rewards are paid up to Canopy activation, on both mainnet and testnet. - // But we checkpoint in Canopy so founders reward does not apply for Zebra. - unreachable!("we cannot verify consensus rules before Canopy activation"); - } else if halving_div < 8 { - // 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 - // funding stream amount values - let funding_streams = subsidy::funding_streams::funding_stream_values( - height, - network, - expected_block_subsidy, - ) - .expect("We always expect a funding stream hashmap response even if empty"); - - // # Consensus - // - // > [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)] - // - // https://zips.z.cash/protocol/protocol.pdf#fundingstreams - for (receiver, expected_amount) in funding_streams { - if receiver == FundingStreamReceiver::Deferred { - // The deferred pool contribution is checked in `miner_fees_are_valid()` - // TODO: Add link to lockbox stream ZIP - continue; - } - - let address = subsidy::funding_streams::funding_stream_address( - height, network, receiver, - ) - .expect("funding stream receivers other than the deferred pool must have an address"); - - let has_expected_output = - subsidy::funding_streams::filter_outputs_by_address(coinbase, address) - .iter() - .map(zebra_chain::transparent::Output::value) - .any(|value| value == expected_amount); - - if !has_expected_output { - Err(SubsidyError::FundingStreamNotFound)?; - } - } - Ok(()) - } else { - // Future halving, with no founders reward or funding streams - Ok(()) - } -} - -/// Returns `Ok(())` if the miner fees consensus rule is valid. -/// -/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus -pub fn miner_fees_are_valid( - coinbase_tx: &Transaction, - height: Height, - block_miner_fees: Amount, - expected_block_subsidy: Amount, - expected_deferred_amount: Amount, - network: &Network, -) -> Result<(), BlockError> { - let transparent_value_balance = subsidy::general::output_amounts(coinbase_tx) - .iter() - .sum::, AmountError>>() - .map_err(|_| SubsidyError::SumOverflow)? - .constrain() - .expect("positive value always fit in `NegativeAllowed`"); - 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, - // > 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#txnconsensus - // - // 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. - 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)?; - - // 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(()) -} - /// Returns `Ok(())` if `header.time` is less than or equal to /// 2 hours in the future, according to the node's local clock (`now`). /// diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 030b3e15abd..53a27312f49 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -2,26 +2,31 @@ use color_eyre::eyre::{eyre, Report}; use once_cell::sync::Lazy; -use subsidy::general::block_subsidy; use tower::{buffer::Buffer, util::BoxService}; use zebra_chain::{ amount::MAX_MONEY, block::{ + error, subsidy, + subsidy::general::block_subsidy_pre_nsm, tests::generate::{ large_multi_transaction_block, large_single_transaction_block_many_inputs, }, Block, Height, }, - parameters::NetworkUpgrade, + error::CoinbaseTransactionError, + parameters::{subsidy::FundingStreamReceiver, NetworkUpgrade}, serialization::{ZcashDeserialize, ZcashDeserializeInto}, transaction::{arbitrary::transaction_to_fake_v5, LockTime, Transaction}, work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY}, }; use zebra_script::CachedFfiTransaction; +use zebra_state::check::{ + coinbase_is_first, miner_fees_are_valid, subsidy_is_valid, transaction_miner_fees_are_valid, +}; use zebra_test::transcript::{ExpectedTranscriptError, Transcript}; -use crate::transaction; +use crate::{block::check, difficulty_is_valid, transaction}; use super::*; @@ -168,8 +173,7 @@ fn coinbase_is_first_for_historical_blocks() -> Result<(), Report> { .zcash_deserialize_into::() .expect("block is structurally valid"); - check::coinbase_is_first(&block) - .expect("the coinbase in a historical block should be valid"); + coinbase_is_first(&block).expect("the coinbase in a historical block should be valid"); } Ok(()) @@ -193,7 +197,7 @@ fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> { .zcash_deserialize_into::() .expect("block is structurally valid"); - check::difficulty_is_valid(&block.header, &network, &Height(height), &block.hash()) + difficulty_is_valid(&block.header, &network, &Height(height), &block.hash()) .expect("the difficulty from a historical block should be valid"); } @@ -216,8 +220,7 @@ fn difficulty_validation_failure() -> Result<(), Report> { Arc::make_mut(&mut block.header).difficulty_threshold = INVALID_COMPACT_DIFFICULTY; // Validate the block - let result = - check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); + let result = difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); let expected = BlockError::InvalidDifficulty(height, hash); assert_eq!(expected, result); @@ -231,8 +234,7 @@ fn difficulty_validation_failure() -> Result<(), Report> { let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap(); // Validate the block as if it is a mainnet block - let result = - check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); + let result = difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); let expected = BlockError::TargetDifficultyLimit( height, hash, @@ -252,8 +254,8 @@ fn difficulty_validation_failure() -> Result<(), Report> { let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap(); // Validate the block - let result = check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &bad_hash) - .unwrap_err(); + let result = + difficulty_is_valid(&block.header, &Network::Mainnet, &height, &bad_hash).unwrap_err(); let expected = BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold, Network::Mainnet); assert_eq!(expected, result); @@ -304,10 +306,10 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> { // TODO: first halving, second halving, third halving, and very large halvings if height >= canopy_activation_height { - let expected_block_subsidy = - subsidy::general::block_subsidy(height, &network).expect("valid block subsidy"); + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm(height, &network) + .expect("valid block subsidy"); - check::subsidy_is_valid(&block, &network, expected_block_subsidy) + subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidies should pass for this block"); } } @@ -327,7 +329,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { .expect("block should deserialize"); let mut block = Arc::try_unwrap(block).expect("block should unwrap"); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -339,12 +341,12 @@ fn coinbase_validation_failure() -> Result<(), Report> { block.transactions.remove(0); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::NoTransactions; + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::Block(error::BlockError::NoTransactions); assert_eq!(expected, result); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); + let expected = SubsidyError::NoCoinbase; assert_eq!(expected, result); // Get another funding stream block, and delete the coinbase transaction @@ -353,7 +355,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { .expect("block should deserialize"); let mut block = Arc::try_unwrap(block).expect("block should unwrap"); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -365,12 +367,12 @@ fn coinbase_validation_failure() -> Result<(), Report> { block.transactions.remove(0); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::CoinbasePosition); + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::Position; assert_eq!(expected, result); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); + let expected = SubsidyError::NoCoinbase; assert_eq!(expected, result); // Get another funding stream, and duplicate the coinbase transaction @@ -389,11 +391,11 @@ fn coinbase_validation_failure() -> Result<(), Report> { ); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::CoinbaseAfterFirst); + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::AfterFirst; assert_eq!(expected, result); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -401,7 +403,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { ) .expect("valid block subsidy"); - check::subsidy_is_valid(&block, &network, expected_block_subsidy) + subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidy does not check for extra coinbase transactions"); Ok(()) @@ -429,11 +431,11 @@ fn funding_stream_validation_for_network(network: Network) -> Result<(), Report> if height >= canopy_activation_height { let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize"); - let expected_block_subsidy = - subsidy::general::block_subsidy(height, &network).expect("valid block subsidy"); + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm(height, &network) + .expect("valid block subsidy"); // Validate - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy); assert!(result.is_ok()); } } @@ -477,7 +479,7 @@ fn funding_stream_validation_failure() -> Result<(), Report> { }; // Validate it - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -485,10 +487,8 @@ fn funding_stream_validation_failure() -> Result<(), Report> { ) .expect("valid block subsidy"); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); - let expected = Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::FundingStreamNotFound, - ))); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy); + let expected = Err(SubsidyError::FundingStreamNotFound); assert_eq!(expected, result); Ok(()) @@ -510,11 +510,11 @@ 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 coinbase_tx = check::coinbase_is_first( + let coinbase_tx = coinbase_is_first( &Block::zcash_deserialize(&block[..]).expect("block should deserialize"), )?; - let expected_block_subsidy = block_subsidy(height, &network)?; + let expected_block_subsidy = block_subsidy_pre_nsm(height, &network)?; // TODO: Add link to lockbox stream ZIP let expected_deferred_amount = subsidy::funding_streams::funding_stream_values( @@ -526,7 +526,7 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> { .remove(&FundingStreamReceiver::Deferred) .unwrap_or_default(); - assert!(check::miner_fees_are_valid( + assert!(transaction_miner_fees_are_valid( &coinbase_tx, height, // Set the miner fees to a high-enough amount. @@ -549,7 +549,7 @@ fn miner_fees_validation_failure() -> Result<(), Report> { 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)?; + let expected_block_subsidy = block_subsidy_pre_nsm(height, &network)?; // TODO: Add link to lockbox stream ZIP let expected_deferred_amount = subsidy::funding_streams::funding_stream_values(height, &network, expected_block_subsidy) @@ -558,8 +558,8 @@ fn miner_fees_validation_failure() -> Result<(), Report> { .unwrap_or_default(); assert_eq!( - check::miner_fees_are_valid( - check::coinbase_is_first(&block)?.as_ref(), + transaction_miner_fees_are_valid( + coinbase_is_first(&block)?.as_ref(), height, // Set the miner fee to an invalid amount. Amount::zero(), @@ -567,9 +567,91 @@ fn miner_fees_validation_failure() -> Result<(), Report> { expected_deferred_amount, &network ), - Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::InvalidMinerFees, - ))) + Err(SubsidyError::InvalidMinerFees) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_fails_when_burn_amount_is_zero() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = Amount::zero(); + let expected_block_subsidy = 100_000_000.try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Err(SubsidyError::InvalidBurnAmount) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_succeeds_when_burn_amount_is_correct() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = 600.try_into().unwrap(); + let expected_block_subsidy = (100_000_600).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Ok(()) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_fails_when_burn_amount_is_incorrect() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = 500.try_into().unwrap(); + let expected_block_subsidy = (100_000_500).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Err(SubsidyError::InvalidBurnAmount) ); Ok(()) diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index dfdf915b5be..0deb121eab0 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -30,20 +30,20 @@ use tracing::instrument; use zebra_chain::{ amount, block::{self, Block}, - parameters::{subsidy::FundingStreamReceiver, Network, GENESIS_PREVIOUS_BLOCK_HASH}, + error::SubsidyError, + parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, work::equihash, }; use zebra_state::{self as zs, CheckpointVerifiedBlock}; use crate::{ block::VerifyBlockError, - block_subsidy, checkpoint::types::{ Progress::{self, *}, TargetHeight::{self, *}, }, - error::{BlockError, SubsidyError}, - funding_stream_values, BoxError, ParameterCheckpoint as _, + error::BlockError, + BoxError, ParameterCheckpoint as _, }; pub(crate) mod list; @@ -608,18 +608,8 @@ where crate::block::check::equihash_solution_is_valid(&block.header)?; } - // We can't get the block subsidy for blocks with heights in the slow start interval, so we - // omit the calculation of the expected deferred amount. - let expected_deferred_amount = if height > self.network.slow_start_interval() { - // TODO: Add link to lockbox stream ZIP - funding_stream_values(height, &self.network, block_subsidy(height, &self.network)?)? - .remove(&FundingStreamReceiver::Deferred) - } else { - None - }; - // don't do precalculation until the block passes basic difficulty checks - let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount); + let block = CheckpointVerifiedBlock::new(block, Some(hash), None); crate::block::check::merkle_root_validity( &self.network, diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 91dfbf0ce3a..e2701cd53ff 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -9,7 +9,9 @@ use chrono::{DateTime, Utc}; use thiserror::Error; use zebra_chain::{ - amount, block, orchard, sapling, sprout, + amount, block, + error::{CoinbaseTransactionError, SubsidyError}, + orchard, sapling, sprout, transparent::{self, MIN_TRANSPARENT_COINBASE_MATURITY}, }; use zebra_state::ValidateContextError; @@ -22,64 +24,11 @@ use proptest_derive::Arbitrary; /// Workaround for format string identifier rules. const MAX_EXPIRY_HEIGHT: block::Height = block::Height::MAX_EXPIRY_HEIGHT; -/// Block subsidy errors. -#[derive(Error, Clone, Debug, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum SubsidyError { - #[error("no coinbase transaction in block")] - NoCoinbase, - - #[error("funding stream expected output not found")] - FundingStreamNotFound, - - #[error("miner fees are invalid")] - InvalidMinerFees, - - #[error("a sum of amounts overflowed")] - SumOverflow, - - #[error("unsupported height")] - UnsupportedHeight, - - #[error("invalid amount")] - InvalidAmount(amount::Error), -} - -impl From for SubsidyError { - fn from(amount: amount::Error) -> Self { - Self::InvalidAmount(amount) - } -} - /// Errors for semantic transaction validation. #[derive(Error, Clone, Debug, PartialEq, Eq)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[allow(missing_docs)] pub enum TransactionError { - #[error("first transaction must be coinbase")] - CoinbasePosition, - - #[error("coinbase input found in non-coinbase transaction")] - CoinbaseAfterFirst, - - #[error("coinbase transaction MUST NOT have any JoinSplit descriptions")] - CoinbaseHasJoinSplit, - - #[error("coinbase transaction MUST NOT have any Spend descriptions")] - CoinbaseHasSpend, - - #[error("coinbase transaction MUST NOT have any Output descriptions pre-Heartwood")] - CoinbaseHasOutputPreHeartwood, - - #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] - CoinbaseHasEnableSpendsOrchard, - - #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")] - CoinbaseOutputsNotDecryptable, - - #[error("coinbase inputs MUST NOT exist in mempool")] - CoinbaseInMempool, - #[error("non-coinbase transactions MUST NOT have coinbase inputs")] NonCoinbaseHasCoinbaseInput, @@ -90,15 +39,9 @@ pub enum TransactionError { #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] LockedUntilAfterBlockTime(DateTime), - #[error( - "coinbase expiry {expiry_height:?} must be the same as the block {block_height:?} \ - after NU5 activation, failing transaction: {transaction_hash:?}" - )] - CoinbaseExpiryBlockHeight { - expiry_height: Option, - block_height: zebra_chain::block::Height, - transaction_hash: zebra_chain::transaction::Hash, - }, + #[error("coinbase transaction error")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Coinbase(#[from] CoinbaseTransactionError), #[error( "expiry {expiry_height:?} must be less than the maximum {MAX_EXPIRY_HEIGHT:?} \ diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 95381fd9e07..753a41117b0 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -47,13 +47,8 @@ pub mod transaction; #[cfg(any(test, feature = "proptest-impl"))] pub use block::check::difficulty_is_valid; -pub use block::{ - subsidy::{ - funding_streams::{funding_stream_address, funding_stream_values, new_coinbase_script}, - general::{block_subsidy, miner_subsidy}, - }, - Request, VerifyBlockError, MAX_BLOCK_SIGOPS, -}; +pub use block::{Request, VerifyBlockError, MAX_BLOCK_SIGOPS}; + pub use checkpoint::{ list::ParameterCheckpoint, CheckpointList, VerifyCheckpointError, MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP, diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index ed04ead0c4c..08bcb91431f 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -18,7 +18,9 @@ use tracing::Instrument; use zebra_chain::{ amount::{Amount, NonNegative}, - block, orchard, + block, + error::CoinbaseTransactionError, + orchard, parameters::{Network, NetworkUpgrade}, primitives::Groth16Proof, sapling, @@ -309,7 +311,7 @@ where // Validate the coinbase input consensus rules if req.is_mempool() && tx.is_coinbase() { - return Err(TransactionError::CoinbaseInMempool); + return Err(TransactionError::Coinbase(CoinbaseTransactionError::InMempool)); } if tx.is_coinbase() { diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 0891cc2b210..ee9e764039f 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -14,6 +14,7 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ amount::{Amount, NonNegative}, block::Height, + error::CoinbaseTransactionError, orchard::Flags, parameters::{Network, NetworkUpgrade}, primitives::zcash_note_encryption, @@ -172,14 +173,20 @@ pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> { if tx.is_coinbase() { if tx.joinsplit_count() > 0 { - return Err(TransactionError::CoinbaseHasJoinSplit); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasJoinSplit, + )); } else if tx.sapling_spends_per_anchor().count() > 0 { - return Err(TransactionError::CoinbaseHasSpend); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasSpend, + )); } if let Some(orchard_shielded_data) = tx.orchard_shielded_data() { if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) { - return Err(TransactionError::CoinbaseHasEnableSpendsOrchard); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasEnableSpendsOrchard, + )); } } } @@ -345,7 +352,9 @@ pub fn coinbase_outputs_are_decryptable( } if !zcash_note_encryption::decrypts_successfully(transaction, network, height) { - return Err(TransactionError::CoinbaseOutputsNotDecryptable); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::OutputsNotDecryptable, + )); } Ok(()) @@ -372,11 +381,13 @@ pub fn coinbase_expiry_height( // if *block_height >= nu5_activation_height { if expiry_height != Some(*block_height) { - return Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height, - block_height: *block_height, - transaction_hash: coinbase.hash(), - }); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height, + block_height: *block_height, + transaction_hash: coinbase.hash(), + }, + )); } else { return Ok(()); } diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 0a4c21bb039..1dd1150d973 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -12,6 +12,7 @@ use tower::{service_fn, ServiceExt}; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, + error::CoinbaseTransactionError, orchard::AuthorizedAction, parameters::{Network, NetworkUpgrade}, primitives::{ed25519, x25519, Groth16Proof}, @@ -844,7 +845,9 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { assert_eq!( check::coinbase_tx_no_prevout_joinsplit_spend(&transaction), - Err(TransactionError::CoinbaseHasEnableSpendsOrchard) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasEnableSpendsOrchard + )) ); } @@ -1705,11 +1708,13 @@ async fn v5_coinbase_transaction_expiry_height() { assert_eq!( result, - Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height: Some(new_expiry_height), - block_height, - transaction_hash: new_transaction.hash(), - }) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height: Some(new_expiry_height), + block_height, + transaction_hash: new_transaction.hash(), + } + )) ); // Decrement the expiry height so that it becomes invalid. @@ -1730,11 +1735,13 @@ async fn v5_coinbase_transaction_expiry_height() { assert_eq!( result, - Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height: Some(new_expiry_height), - block_height, - transaction_hash: new_transaction.hash(), - }) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height: Some(new_expiry_height), + block_height, + transaction_hash: new_transaction.hash(), + } + )) ); // Test with matching heights again, but using a very high value @@ -2925,7 +2932,9 @@ fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() { &network, NetworkUpgrade::Nu5.activation_height(&network).unwrap(), ), - Err(TransactionError::CoinbaseOutputsNotDecryptable) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::OutputsNotDecryptable + )) ); } } diff --git a/zebra-consensus/src/transaction/tests/prop.rs b/zebra-consensus/src/transaction/tests/prop.rs index 7ed4fb9cbbe..876c9e2881c 100644 --- a/zebra-consensus/src/transaction/tests/prop.rs +++ b/zebra-consensus/src/transaction/tests/prop.rs @@ -359,6 +359,7 @@ fn sanitize_transaction_version( Overwinter => 3, Sapling | Blossom | Heartwood | Canopy => 4, Nu5 | Nu6 => 5, + #[cfg(zcash_unstable = "nsm")] ZFuture => 0x00FF, } }; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 2d50552cfec..8fb111c6560 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -11,7 +11,11 @@ use zcash_address::{unified::Encoding, TryFromAddress}; use zebra_chain::{ amount::{self, Amount, NonNegative}, - block::{self, Block, Height, TryIntoHeight}, + block::{ + self, + subsidy::{funding_streams, general}, + Block, Height, TryIntoHeight, + }, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::{ @@ -25,12 +29,13 @@ use zebra_chain::{ }, work::difficulty::{ParameterDifficulty as _, U256}, }; -use zebra_consensus::{ - block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, RouterError, -}; + +use zebra_consensus::RouterError; + +use zebra_state::{ReadRequest, ReadResponse}; + use zebra_network::AddressBookPeers; use zebra_node_services::mempool; -use zebra_state::{ReadRequest, ReadResponse}; use crate::methods::{ best_chain_tip_height, @@ -1202,6 +1207,8 @@ where fn get_block_subsidy(&self, height: Option) -> BoxFuture> { let latest_chain_tip = self.latest_chain_tip.clone(); let network = self.network.clone(); + #[cfg(zcash_unstable = "nsm")] + let mut state_service = self.state.clone(); async move { let height = if let Some(height) = height { @@ -1223,12 +1230,42 @@ where // Always zero for post-halving blocks let founders = Amount::zero(); - let total_block_subsidy = block_subsidy(height, &network).map_server_error()?; + #[cfg(zcash_unstable = "nsm")] + let total_block_subsidy = { + let money_reserve = match state_service + .ready() + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? + .call(ReadRequest::TipPoolValues) + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? { + ReadResponse::TipPoolValues { + tip_hash: _, + tip_height: _, + value_balance, + } => value_balance.money_reserve(), + _ => unreachable!("wrong response to ReadRequest::TipPoolValues"), + }; + general::block_subsidy(height, &network, money_reserve).map_server_error()? + }; + + #[cfg(not(zcash_unstable = "nsm"))] + let total_block_subsidy = + general::block_subsidy_pre_nsm(height, &network).map_server_error()?; + let miner_subsidy = - miner_subsidy(height, &network, total_block_subsidy).map_server_error()?; + general::miner_subsidy(height, &network, total_block_subsidy).map_server_error()?; let (lockbox_streams, mut funding_streams): (Vec<_>, Vec<_>) = - funding_stream_values(height, &network, total_block_subsidy) + funding_streams::funding_stream_values(height, &network, total_block_subsidy) .map_server_error()? .into_iter() // Separate the funding streams into deferred and non-deferred streams @@ -1255,7 +1292,8 @@ where streams .into_iter() .map(|(receiver, value)| { - let address = funding_stream_address(height, &network, receiver); + let address = + funding_streams::funding_stream_address(height, &network, receiver); FundingStream::new(is_nu6, receiver, value, address) }) .collect() diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs index 8e9578180be..ad63123739e 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs @@ -6,10 +6,11 @@ use jsonrpc_core::{Error, ErrorCode, Result}; use tower::{Service, ServiceExt}; use zebra_chain::{ - amount::{self, Amount, NegativeOrZero, NonNegative}, + amount::{self, Amount, NegativeOrZero, NonNegative, MAX_MONEY}, block::{ self, merkle::{self, AuthDataRoot}, + subsidy::{funding_streams, general}, Block, ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash, Height, }, chain_sync_status::ChainSyncStatus, @@ -19,12 +20,10 @@ use zebra_chain::{ transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, transparent, }; -use zebra_consensus::{ - block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, -}; -use zebra_node_services::mempool; use zebra_state::GetBlockTemplateChainInfo; +use zebra_node_services::mempool; + use crate::methods::{ errors::OkOrServerError, get_block_template_rpcs::{ @@ -376,9 +375,19 @@ pub fn standard_coinbase_outputs( miner_fee: Amount, like_zcashd: bool, ) -> Vec<(Amount, transparent::Script)> { - let expected_block_subsidy = block_subsidy(height, network).expect("valid block subsidy"); - let funding_streams = funding_stream_values(height, network, expected_block_subsidy) - .expect("funding stream value calculations are valid for reasonable chain heights"); + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = general::block_subsidy( + height, + network, + MAX_MONEY.try_into().expect("MAX_MONEY is a valid amount"), + ) + .expect("valid block subsidy"); + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(height, network).expect("valid block subsidy"); + let funding_streams = + funding_streams::funding_stream_values(height, network, expected_block_subsidy) + .expect("funding stream value calculations are valid for reasonable chain heights"); // Optional TODO: move this into a zebra_consensus function? let funding_streams: HashMap< @@ -389,12 +398,15 @@ pub fn standard_coinbase_outputs( .filter_map(|(receiver, amount)| { Some(( receiver, - (amount, funding_stream_address(height, network, receiver)?), + ( + amount, + funding_streams::funding_stream_address(height, network, receiver)?, + ), )) }) .collect(); - let miner_reward = miner_subsidy(height, network, expected_block_subsidy) + let miner_reward = general::miner_subsidy(height, network, expected_block_subsidy) .expect("reward calculations are valid for reasonable chain heights") + miner_fee; let miner_reward = diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index f4d7804088e..5f8328fa7b9 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -46,6 +46,8 @@ async fn test_rpc_response_data() { .with_activation_heights(ConfiguredActivationHeights { blossom: Some(584_000), nu6: Some(POST_NU6_FUNDING_STREAMS_TESTNET.height_range().start.0), + #[cfg(zcash_unstable = "nsm")] + zfuture: Some(3_000_000), ..Default::default() }) .to_network(); diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap index e4d64069482..8932444dcb0 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap @@ -69,11 +69,6 @@ expression: info "name": "NU6", "activationheight": 2820000, "status": "pending" - }, - "ffffffff": { - "name": "ZFuture", - "activationheight": 3000000, - "status": "pending" } }, "consensus": { diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap index fbe49412e53..3bea6c01509 100644 --- a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap @@ -69,11 +69,6 @@ expression: info "name": "NU6", "activationheight": 2976000, "status": "pending" - }, - "ffffffff": { - "name": "ZFuture", - "activationheight": 3000000, - "status": "pending" } }, "consensus": { diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 5c0b837566a..7909b40ce83 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -38,6 +38,7 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } } @@ -112,6 +113,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, + block_miner_fees: _, } = block.into(); Self { diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index cf495311efb..e3fc2846675 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -8,6 +8,7 @@ use thiserror::Error; use zebra_chain::{ amount::{self, NegativeAllowed, NonNegative}, block, + error::{CoinbaseTransactionError, SubsidyError}, history_tree::HistoryTreeError, orchard, sapling, sprout, transaction, transparent, value_balance::{ValueBalance, ValueBalanceError}, @@ -264,6 +265,24 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("could not validate block subsidy")] + SubsidyError(Box), + + #[error("could not validate coinbase transaction")] + CoinbaseTransactionError(Box), +} + +impl From for ValidateContextError { + fn from(err: SubsidyError) -> Self { + ValidateContextError::SubsidyError(Box::new(err)) + } +} + +impl From for ValidateContextError { + fn from(err: CoinbaseTransactionError) -> Self { + ValidateContextError::CoinbaseTransactionError(Box::new(err)) + } } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..bcb92ec6723 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -163,6 +163,8 @@ pub struct SemanticallyVerifiedBlock { pub transaction_hashes: Arc<[transaction::Hash]>, /// This block's contribution to the deferred pool. pub deferred_balance: Option>, + /// This block's miner fees + pub block_miner_fees: Option>, } /// A block ready to be committed directly to the finalized state with @@ -392,6 +394,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, + block_miner_fees: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -453,6 +456,7 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } @@ -485,6 +489,7 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } } @@ -504,6 +509,7 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), + block_miner_fees: None, } } } @@ -517,6 +523,7 @@ impl From for SemanticallyVerifiedBlock { new_outputs: finalized.new_outputs, transaction_hashes: finalized.transaction_hashes, deferred_balance: finalized.deferred_balance, + block_miner_fees: None, } } } @@ -625,6 +632,9 @@ pub enum Request { /// with the current best chain tip. Tip, + #[cfg(zcash_unstable = "nsm")] + TipPoolValues, + /// Computes a block locator object based on the current best chain. /// /// Returns [`Response::BlockLocator`] with hashes starting @@ -778,6 +788,8 @@ impl Request { Request::AwaitUtxo(_) => "await_utxo", Request::Depth(_) => "depth", Request::Tip => "tip", + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => "tip_pool_values", Request::BlockLocator => "block_locator", Request::Transaction(_) => "transaction", Request::UnspentBestChainUtxo { .. } => "unspent_best_chain_utxo", @@ -1128,6 +1140,8 @@ impl TryFrom for ReadRequest { fn try_from(request: Request) -> Result { match request { Request::Tip => Ok(ReadRequest::Tip), + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => Ok(ReadRequest::TipPoolValues), Request::Depth(hash) => Ok(ReadRequest::Depth(hash)), Request::BestChainNextMedianTimePast => Ok(ReadRequest::BestChainNextMedianTimePast), Request::BestChainBlockHash(hash) => Ok(ReadRequest::BestChainBlockHash(hash)), diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 77c252b0c75..4e6f3c0cde6 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -39,6 +39,16 @@ pub enum Response { // `LatestChainTip::best_tip_height_and_hash()` Tip(Option<(block::Height, block::Hash)>), + /// Response to [`Request::TipPoolValues`] with the current best chain tip values. + TipPoolValues { + /// The current best chain tip height. + tip_height: block::Height, + /// The current best chain tip hash. + tip_hash: block::Hash, + /// The value pool balance at the current best chain tip. + value_balance: ValueBalance, + }, + /// Response to [`Request::BlockLocator`] with a block locator object. BlockLocator(Vec), @@ -303,8 +313,7 @@ impl TryFrom for Response { ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors), - ReadResponse::TipPoolValues { .. } - | ReadResponse::TransactionIdsForBlock(_) + ReadResponse::TransactionIdsForBlock(_) | ReadResponse::SaplingTree(_) | ReadResponse::OrchardTree(_) | ReadResponse::SaplingSubtrees(_) @@ -315,6 +324,14 @@ impl TryFrom for Response { Err("there is no corresponding Response for this ReadResponse") } + #[cfg(zcash_unstable = "nsm")] + ReadResponse::TipPoolValues { tip_height, tip_hash, value_balance } => Ok(Response::TipPoolValues { tip_height, tip_hash, value_balance }), + + #[cfg(not(zcash_unstable = "nsm"))] + ReadResponse::TipPoolValues { .. } => { + Err("there is no corresponding Response for this ReadResponse") + } + #[cfg(feature = "getblocktemplate-rpcs")] ReadResponse::ValidBlockProposal => Ok(Response::ValidBlockProposal), diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index adc61f887ae..26040dd68e5 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1114,6 +1114,21 @@ impl Service for StateService { .boxed() } + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => { + // Redirect the request to the concurrent ReadStateService + let read_service = self.read_service.clone(); + async move { + let req = req + .try_into() + .expect("ReadRequest conversion should not fail"); + let rsp = read_service.oneshot(req).await?; + let rsp = rsp.try_into().expect("Response conversion should not fail"); + Ok(rsp) + } + .boxed() + } + #[cfg(feature = "getblocktemplate-rpcs")] Request::CheckBlockProposalValidity(_) => { // Redirect the request to the concurrent ReadStateService @@ -1861,7 +1876,7 @@ impl Service for ReadStateService { } #[cfg(feature = "getblocktemplate-rpcs")] - ReadRequest::CheckBlockProposalValidity(semantically_verified) => { + ReadRequest::CheckBlockProposalValidity(mut semantically_verified) => { let state = self.clone(); // # Performance @@ -1892,7 +1907,7 @@ impl Service for ReadStateService { write::validate_and_commit_non_finalized( &state.db, &mut latest_non_finalized_state, - semantically_verified, + &mut semantically_verified, )?; // The work is done in the future. diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 04ea61d6982..03afa1d7857 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,6 +116,7 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, + block_miner_fees: _, } = prepared; Self { diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index ced63bfea16..6a690a62c35 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -5,9 +5,16 @@ use std::{borrow::Borrow, sync::Arc}; use chrono::Duration; use zebra_chain::{ - block::{self, Block, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError}, + amount::{Amount, Error as AmountError, NonNegative, MAX_MONEY}, + block::{ + self, error::BlockError, subsidy::funding_streams, subsidy::general, Block, + ChainHistoryBlockTxAuthCommitmentHash, CommitmentError, Height, + }, + error::{CoinbaseTransactionError, SubsidyError}, history_tree::HistoryTree, - parameters::{Network, NetworkUpgrade}, + parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade}, + transaction, + value_balance::ValueBalance, work::difficulty::CompactDifficulty, }; @@ -48,10 +55,11 @@ pub(crate) use difficulty::AdjustedDifficulty; /// with its parent block. #[tracing::instrument(skip(semantically_verified, finalized_tip_height, relevant_chain))] pub(crate) fn block_is_valid_for_recent_chain( - semantically_verified: &SemanticallyVerifiedBlock, + semantically_verified: &mut SemanticallyVerifiedBlock, network: &Network, finalized_tip_height: Option, relevant_chain: C, + pool_value_balance: Option>, ) -> Result<(), ValidateContextError> where C: IntoIterator, @@ -83,6 +91,55 @@ where .expect("valid blocks have a coinbase height"); check::height_one_more_than_parent_height(parent_height, semantically_verified.height)?; + if semantically_verified.height > network.slow_start_interval() { + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(semantically_verified.height, network)?; + + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = { + let money_reserve = if semantically_verified.height > 1.try_into().unwrap() { + pool_value_balance + .expect("a chain must contain valid pool value balance") + .money_reserve() + } else { + MAX_MONEY.try_into().unwrap() + }; + general::block_subsidy(semantically_verified.height, network, money_reserve)? + }; + + subsidy_is_valid( + &semantically_verified.block, + network, + expected_block_subsidy, + )?; + + // TODO: Add link to lockbox stream ZIP + let expected_deferred_amount = funding_streams::funding_stream_values( + semantically_verified.height, + network, + expected_block_subsidy, + ) + .expect("we always expect a funding stream hashmap response even if empty") + .remove(&FundingStreamReceiver::Deferred) + .unwrap_or_default(); + + semantically_verified.deferred_balance = Some(expected_deferred_amount); + + let coinbase_tx = coinbase_is_first(&semantically_verified.block)?; + + check::transaction_miner_fees_are_valid( + &coinbase_tx, + semantically_verified.height, + semantically_verified + .block_miner_fees + .expect("block must have miner fees calculated"), + expected_block_subsidy, + expected_deferred_amount, + network, + )?; + } + // skip this check during tests if we don't have enough blocks in the chain // process_queued also checks the chain length, so we can skip this assertion during testing // (tests that want to check this code should use the correct number of blocks) @@ -388,7 +445,7 @@ where pub(crate) fn initial_contextual_validity( finalized_state: &ZebraDb, non_finalized_state: &NonFinalizedState, - semantically_verified: &SemanticallyVerifiedBlock, + semantically_verified: &mut SemanticallyVerifiedBlock, ) -> Result<(), ValidateContextError> { let relevant_chain = any_ancestor_blocks( non_finalized_state, @@ -396,15 +453,263 @@ pub(crate) fn initial_contextual_validity( semantically_verified.block.header.previous_block_hash, ); + let pool_value_balance = non_finalized_state + .best_chain() + .map(|chain| chain.chain_value_pools) + .or_else(|| { + finalized_state + .finalized_tip_height() + .filter(|x| (*x + 1).unwrap() == semantically_verified.height) + .map(|_| finalized_state.finalized_value_pool()) + }); + // Security: check proof of work before any other checks check::block_is_valid_for_recent_chain( semantically_verified, &non_finalized_state.network, finalized_state.finalized_tip_height(), relevant_chain, + pool_value_balance, )?; check::nullifier::no_duplicates_in_finalized_chain(semantically_verified, finalized_state)?; Ok(()) } + +/// Checks if there is exactly one coinbase transaction in `Block`, +/// and if that coinbase transaction is the first transaction in the block. +/// Returns the coinbase transaction is successful. +/// +/// > A transaction that has a single transparent input with a null prevout field, +/// > is called a coinbase transaction. Every block has a single coinbase +/// > transaction as the first transaction in the block. +/// +/// +pub fn coinbase_is_first( + block: &Block, +) -> Result, CoinbaseTransactionError> { + // # Consensus + // + // > A block MUST have at least one transaction + // + // + let first = block + .transactions + .first() + .ok_or(BlockError::NoTransactions)?; + // > The first transaction in a block MUST be a coinbase transaction, + // > and subsequent transactions MUST NOT be coinbase transactions. + // + // + // + // > A transaction that has a single transparent input with a null prevout + // > field, is called a coinbase transaction. + // + // + let mut rest = block.transactions.iter().skip(1); + if !first.is_coinbase() { + Err(CoinbaseTransactionError::Position)?; + } + // > A transparent input in a non-coinbase transaction MUST NOT have a null prevout + // + // + if !rest.all(|tx| tx.is_valid_non_coinbase()) { + Err(CoinbaseTransactionError::AfterFirst)?; + } + + Ok(first.clone()) +} + +/// Returns `Ok(())` if the block subsidy in `block` is valid for `network` +/// +/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts +pub fn subsidy_is_valid( + block: &Block, + network: &Network, + expected_block_subsidy: Amount, +) -> Result<(), SubsidyError> { + let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; + let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?; + + // Validate funding streams + let Some(halving_div) = general::halving_divisor(height, network) else { + // Far future halving, with no founders reward or funding streams + return Ok(()); + }; + + let canopy_activation_height = NetworkUpgrade::Canopy + .activation_height(network) + .expect("Canopy activation height is known"); + + // TODO: Add this as a field on `testnet::Parameters` instead of checking `disable_pow()`, this is 0 for Regtest in zcashd, + // see + let slow_start_interval = if network.disable_pow() { + Height(0) + } else { + network.slow_start_interval() + }; + + if height < slow_start_interval { + unreachable!( + "unsupported block height: callers should handle blocks below {:?}", + slow_start_interval + ) + } else if halving_div.count_ones() != 1 { + unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") + } else if height < canopy_activation_height { + // Founders rewards are paid up to Canopy activation, on both mainnet and testnet. + // But we checkpoint in Canopy so founders reward does not apply for Zebra. + unreachable!("we cannot verify consensus rules before Canopy activation"); + } else if halving_div < 8 { + // 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 + // funding stream amount values + let funding_streams = + funding_streams::funding_stream_values(height, network, expected_block_subsidy) + .expect("We always expect a funding stream hashmap response even if empty"); + + // # Consensus + // + // > [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)] + // + // https://zips.z.cash/protocol/protocol.pdf#fundingstreams + for (receiver, expected_amount) in funding_streams { + if receiver == FundingStreamReceiver::Deferred { + // The deferred pool contribution is checked in `miner_fees_are_valid()` + // TODO: Add link to lockbox stream ZIP + continue; + } + + let address = funding_streams::funding_stream_address(height, network, receiver) + .expect( + "funding stream receivers other than the deferred pool must have an address", + ); + + let has_expected_output = funding_streams::filter_outputs_by_address(coinbase, address) + .iter() + .map(zebra_chain::transparent::Output::value) + .any(|value| value == expected_amount); + + if !has_expected_output { + Err(SubsidyError::FundingStreamNotFound)?; + } + } + Ok(()) + } else { + // Future halving, with no founders reward or funding streams + Ok(()) + } +} + +/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// +/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus +pub fn transaction_miner_fees_are_valid( + coinbase_tx: &transaction::Transaction, + height: Height, + block_miner_fees: Amount, + expected_block_subsidy: Amount, + expected_deferred_amount: Amount, + network: &Network, +) -> Result<(), SubsidyError> { + let network_upgrade = NetworkUpgrade::current(network, height); + let transparent_value_balance = general::output_amounts(coinbase_tx) + .iter() + .sum::, AmountError>>() + .map_err(|_| SubsidyError::SumOverflow)? + .constrain() + .expect("positive value always fit in `NegativeAllowed`"); + let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount(); + let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount(); + + // Coinbase transaction can still have a ZSF deposit + #[cfg(zcash_unstable = "nsm")] + let burn_amount = coinbase_tx + .burn_amount() + .constrain() + .expect("positive value always fit in `NegativeAllowed`"); + + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + #[cfg(zcash_unstable = "nsm")] + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + network_upgrade, + ) +} + +/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// +/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus +#[allow(clippy::too_many_arguments)] +pub fn miner_fees_are_valid( + transparent_value_balance: Amount, + sapling_value_balance: Amount, + orchard_value_balance: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Amount, + expected_block_subsidy: Amount, + block_miner_fees: Amount, + expected_deferred_amount: Amount, + network_upgrade: NetworkUpgrade, +) -> Result<(), SubsidyError> { + // TODO: Update the quote below once its been updated for NU6. + // + // # Consensus + // + // > 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#txnconsensus + // + // 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. + #[cfg(zcash_unstable = "nsm")] + let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance + + burn_amount) + .map_err(|_| SubsidyError::SumOverflow)?; + #[cfg(not(zcash_unstable = "nsm"))] + 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)?; + + // 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. + let block_before_nu6 = network_upgrade < NetworkUpgrade::Nu6; + let miner_fees_valid = if block_before_nu6 { + left <= right + } else { + left == right + }; + + if !miner_fees_valid { + Err(SubsidyError::InvalidMinerFees)? + }; + + // Verify that the NSM burn amount is at least the minimum required amount (ZIP-235). + #[cfg(zcash_unstable = "nsm")] + if network_upgrade == NetworkUpgrade::ZFuture { + let minimum_burn_amount = ((block_miner_fees * 6).unwrap() / 10).unwrap(); + if burn_amount < minimum_burn_amount { + Err(SubsidyError::InvalidBurnAmount)? + } + } + + Ok(()) +} diff --git a/zebra-state/src/service/check/tests/anchors.rs b/zebra-state/src/service/check/tests/anchors.rs index 09d33b29190..bf29d8edb02 100644 --- a/zebra-state/src/service/check/tests/anchors.rs +++ b/zebra-state/src/service/check/tests/anchors.rs @@ -53,7 +53,7 @@ fn check_sprout_anchors() { .expect("block should deserialize"); // Add initial transactions to [`block_1`]. - let block_1 = prepare_sprout_block(block_1, block_395); + let mut block_1 = prepare_sprout_block(block_1, block_395); // Create a block at height == 2 that references the Sprout note commitment tree state // from [`block_1`]. @@ -68,7 +68,7 @@ fn check_sprout_anchors() { .expect("block should deserialize"); // Add the transactions with the first anchors to [`block_2`]. - let block_2 = prepare_sprout_block(block_2, block_396); + let mut block_2 = prepare_sprout_block(block_2, block_396); let unmined_txs: Vec<_> = block_2 .block @@ -98,7 +98,7 @@ fn check_sprout_anchors() { assert!(validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block_1 + &mut block_1 ) .is_ok()); @@ -114,7 +114,11 @@ fn check_sprout_anchors() { // Validate and commit [`block_2`]. This will also check the anchors. assert_eq!( - validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block_2), + validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + &mut block_2 + ), Ok(()) ); } @@ -249,7 +253,7 @@ fn check_sapling_anchors() { })) }); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); // Create a block at height == 2 that references the Sapling note commitment tree state // from earlier block @@ -295,7 +299,7 @@ fn check_sapling_anchors() { })) }); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let unmined_txs: Vec<_> = block2 .block @@ -320,7 +324,7 @@ fn check_sapling_anchors() { assert!(validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ) .is_ok()); @@ -335,7 +339,11 @@ fn check_sapling_anchors() { assert!(check_unmined_tx_anchors_result.is_ok()); assert_eq!( - validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block2), + validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + &mut block2 + ), Ok(()) ); } diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 0392f1c8e79..ae6c4f4b7fd 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -104,7 +104,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); // the block was committed @@ -154,11 +154,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // if the random proptest data produces other errors, @@ -215,11 +215,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -276,11 +276,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -368,7 +368,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -383,11 +383,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( @@ -463,7 +463,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -508,11 +508,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -564,11 +564,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -647,7 +647,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -661,11 +661,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( @@ -743,7 +743,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -789,11 +789,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -849,11 +849,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -936,7 +936,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -949,11 +949,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( diff --git a/zebra-state/src/service/check/tests/utxo.rs b/zebra-state/src/service/check/tests/utxo.rs index acdc2d399a7..cde6317b20e 100644 --- a/zebra-state/src/service/check/tests/utxo.rs +++ b/zebra-state/src/service/check/tests/utxo.rs @@ -203,7 +203,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); // the block was committed @@ -289,7 +289,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2.clone() + &mut block2.clone() ); // the block was committed @@ -364,11 +364,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -428,11 +428,11 @@ proptest! { block2.transactions.push(spend_transaction.into()); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); // the block was rejected @@ -513,11 +513,11 @@ proptest! { .transactions .extend([spend_transaction1.into(), spend_transaction2.into()]); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); // the block was rejected @@ -630,7 +630,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2.clone() + &mut block2.clone() ); // the block was committed @@ -663,11 +663,11 @@ proptest! { previous_non_finalized_state = non_finalized_state.clone(); } - let block3 = Arc::new(block3).prepare(); + let mut block3 = Arc::new(block3).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block3 + &mut block3 ); // the block was rejected @@ -739,11 +739,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -806,11 +806,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -908,7 +908,7 @@ fn new_state_with_mainnet_transparent_data( let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone(), + &mut block1.clone(), ); // the block was committed diff --git a/zebra-state/src/service/finalized_state/disk_db.rs b/zebra-state/src/service/finalized_state/disk_db.rs index b5095d22192..6b8183e8253 100644 --- a/zebra-state/src/service/finalized_state/disk_db.rs +++ b/zebra-state/src/service/finalized_state/disk_db.rs @@ -990,7 +990,7 @@ impl DiskDb { // // We don't attempt to guard against malicious symlinks created by attackers // (TOCTOU attacks). Zebra should not be run with elevated privileges. - if !old_path.starts_with(&cache_path) { + if !old_path.starts_with(cache_path) { info!("skipped reusing previous state cache: state is outside cache directory"); return; } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 194f2202a87..363d655f153 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -137,6 +137,7 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, }) }; diff --git a/zebra-state/src/service/write.rs b/zebra-state/src/service/write.rs index acbc5c14ce1..9223fd046d8 100644 --- a/zebra-state/src/service/write.rs +++ b/zebra-state/src/service/write.rs @@ -7,7 +7,13 @@ use tokio::sync::{ }; use zebra_chain::{ - block::{self, Height}, + amount::MAX_MONEY, + block::{ + self, + subsidy::{funding_streams::funding_stream_values, general}, + Height, + }, + parameters::subsidy::FundingStreamReceiver, transparent::EXTRA_ZEBRA_COINBASE_DATA, }; @@ -49,15 +55,15 @@ const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2; pub(crate) fn validate_and_commit_non_finalized( finalized_state: &ZebraDb, non_finalized_state: &mut NonFinalizedState, - prepared: SemanticallyVerifiedBlock, + prepared: &mut SemanticallyVerifiedBlock, ) -> Result<(), CommitSemanticallyVerifiedError> { - check::initial_contextual_validity(finalized_state, non_finalized_state, &prepared)?; + check::initial_contextual_validity(finalized_state, non_finalized_state, prepared)?; let parent_hash = prepared.block.header.previous_block_hash; if finalized_state.finalized_tip_hash() == parent_hash { - non_finalized_state.commit_new_chain(prepared, finalized_state)?; + non_finalized_state.commit_new_chain(prepared.clone(), finalized_state)?; } else { - non_finalized_state.commit_block(prepared, finalized_state)?; + non_finalized_state.commit_block(prepared.clone(), finalized_state)?; } Ok(()) @@ -144,7 +150,7 @@ pub fn write_blocks_from_channels( // Write all the finalized blocks sent by the state, // until the state closes the finalized block channel's sender. - while let Some(ordered_block) = finalized_block_write_receiver.blocking_recv() { + while let Some(mut ordered_block) = finalized_block_write_receiver.blocking_recv() { // TODO: split these checks into separate functions if invalid_block_reset_sender.is_closed() { @@ -164,7 +170,8 @@ pub fn write_blocks_from_channels( .map(|height| (height + 1).expect("committed heights are valid")) .unwrap_or(Height(0)); - if ordered_block.0.height != next_valid_height { + let ordered_block_height = ordered_block.0.height; + if ordered_block_height != next_valid_height { debug!( ?next_valid_height, invalid_height = ?ordered_block.0.height, @@ -178,6 +185,35 @@ pub fn write_blocks_from_channels( continue; } + let network = finalized_state.network(); + // We can't get the block subsidy for blocks with heights in the slow start interval, so we + // omit the calculation of the expected deferred amount. + let expected_deferred_amount = if ordered_block_height > network.slow_start_interval() { + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(ordered_block_height, &network) + .expect("valid block subsidy"); + + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = { + let money_reserve = if ordered_block_height > 1.try_into().unwrap() { + finalized_state.db.finalized_value_pool().money_reserve() + } else { + MAX_MONEY.try_into().unwrap() + }; + general::block_subsidy(ordered_block_height, &network, money_reserve) + .expect("valid block subsidy") + }; + + // TODO: Add link to lockbox stream ZIP + funding_stream_values(ordered_block_height, &network, expected_block_subsidy) + .expect("we always expect a funding stream hashmap response even if empty") + .remove(&FundingStreamReceiver::Deferred) + } else { + None + }; + ordered_block.0.deferred_balance = expected_deferred_amount; + // Try committing the block match finalized_state .commit_finalized(ordered_block, prev_finalized_note_commitment_trees.take()) @@ -224,7 +260,8 @@ pub fn write_blocks_from_channels( // Save any errors to propagate down to queued child blocks let mut parent_error_map: IndexMap = IndexMap::new(); - while let Some((queued_child, rsp_tx)) = non_finalized_block_write_receiver.blocking_recv() { + while let Some((mut queued_child, rsp_tx)) = non_finalized_block_write_receiver.blocking_recv() + { let child_hash = queued_child.hash; let parent_hash = queued_child.block.header.previous_block_hash; let parent_error = parent_error_map.get(&parent_hash); @@ -248,7 +285,7 @@ pub fn write_blocks_from_channels( result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - queued_child, + &mut queued_child, ) .map_err(CloneError::from); } diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index cd3572ce3f2..73190609d12 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -2979,7 +2979,7 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { primitives::byte_array::increment_big_endian, }; use zebra_rpc::methods::GetBlockHash; - use zebra_state::{ReadResponse, Response}; + use zebra_state::{ReadResponse, Response, SemanticallyVerifiedBlock}; let _init_guard = zebra_test::init(); let mut config = os_assigned_rpc_port_config(false, &Network::new_regtest(None, None))?; @@ -3108,10 +3108,12 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { .bytes_in_serialized_order() .into(); + let mut semantically_verified: SemanticallyVerifiedBlock = Arc::new(block.clone()).into(); + semantically_verified.block_miner_fees = Some(0.try_into().unwrap()); let Response::Committed(block_hash) = state2 .clone() .oneshot(zebra_state::Request::CommitSemanticallyVerifiedBlock( - Arc::new(block.clone()).into(), + semantically_verified, )) .await .map_err(|err| eyre!(err))? @@ -3284,6 +3286,8 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { .with_slow_start_interval(Height::MIN) .with_activation_heights(ConfiguredActivationHeights { nu6: Some(1), + #[cfg(zcash_unstable = "nsm")] + zfuture: Some(10), ..Default::default() });