diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 8fb4388e23c..5bbc774d35c 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -5,7 +5,7 @@ use std::{collections::HashMap, fmt, ops::Neg, sync::Arc}; use halo2::pasta::pallas; use crate::{ - amount::NegativeAllowed, + amount::{Amount, NegativeAllowed, NonNegative}, block::merkle::AuthDataRoot, fmt::DisplayToDebug, orchard, @@ -205,34 +205,39 @@ impl Block { .expect("number of transactions must fit u64") } - /// Get the overall chain value pool change in this block, - /// the negative sum of the transaction value balances in this block. + /// Returns the overall chain value pool change in this block---the negative sum of the + /// transaction value balances in this block. /// - /// These are the changes in the transparent, sprout, sapling, and orchard - /// chain value pools, as a result of this block. + /// These are the changes in the transparent, Sprout, Sapling, Orchard, and + /// Deferred chain value pools, as a result of this block. /// - /// Positive values are added to the corresponding chain value pool. - /// Negative values are removed from the corresponding pool. + /// Positive values are added to the corresponding chain value pool and negative values are + /// removed from the corresponding pool. /// /// /// - /// `utxos` must contain the [`transparent::Utxo`]s of every input in this block, - /// including UTXOs created by earlier transactions in this block. - /// (It can also contain unrelated UTXOs, which are ignored.) + /// The given `utxos` must contain the [`transparent::Utxo`]s of every input in this block, + /// including UTXOs created by earlier transactions in this block. It can also contain unrelated + /// UTXOs, which are ignored. /// - /// Note: the chain value pool has the opposite sign to the transaction - /// value pool. + /// Note that the chain value pool has the opposite sign to the transaction value pool. pub fn chain_value_pool_change( &self, utxos: &HashMap, + deferred_balance: Option>, ) -> Result, ValueBalanceError> { - let transaction_value_balance_total = self + Ok(*self .transactions .iter() .flat_map(|t| t.value_balance(utxos)) - .sum::, _>>()?; - - Ok(transaction_value_balance_total.neg()) + .sum::, _>>()? + .neg() + .set_deferred_amount( + deferred_balance + .unwrap_or(Amount::zero()) + .constrain::() + .map_err(ValueBalanceError::Deferred)?, + )) } /// Compute the root of the authorizing data Merkle tree, diff --git a/zebra-chain/src/parameters/network/subsidy.rs b/zebra-chain/src/parameters/network/subsidy.rs index 739fda8ec3e..1112ac1cf1e 100644 --- a/zebra-chain/src/parameters/network/subsidy.rs +++ b/zebra-chain/src/parameters/network/subsidy.rs @@ -68,20 +68,24 @@ pub enum FundingStreamReceiver { } impl FundingStreamReceiver { - /// The name for each funding stream receiver, as described in [ZIP-1014] and [`zcashd`]. + /// Returns a human-readable name and a specification URL for the receiver, as described in + /// [ZIP-1014] and [`zcashd`]. /// /// [ZIP-1014]: https://zips.z.cash/zip-1014#abstract /// [`zcashd`]: https://github.com/zcash/zcash/blob/3f09cfa00a3c90336580a127e0096d99e25a38d6/src/consensus/funding.cpp#L13-L32 // TODO: Update method documentation with a reference to https://zips.z.cash/draft-nuttycom-funding-allocation once its // status is updated to 'Proposed'. - pub fn name(self) -> &'static str { - match self { - FundingStreamReceiver::Ecc => "Electric Coin Company", - FundingStreamReceiver::ZcashFoundation => "Zcash Foundation", - FundingStreamReceiver::MajorGrants => "Major Grants", - // TODO: Find out what this should be called and update the funding stream name. - FundingStreamReceiver::Deferred => "Lockbox", - } + pub fn info(&self) -> (&'static str, &'static str) { + ( + match self { + FundingStreamReceiver::Ecc => "Electric Coin Company", + FundingStreamReceiver::ZcashFoundation => "Zcash Foundation", + FundingStreamReceiver::MajorGrants => "Major Grants", + // TODO: Find out what this should be called and update the funding stream name + FundingStreamReceiver::Deferred => "Lockbox", + }, + FUNDING_STREAM_SPECIFICATION, + ) } } @@ -90,6 +94,7 @@ impl FundingStreamReceiver { /// [7.10.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams pub const FUNDING_STREAM_RECEIVER_DENOMINATOR: u64 = 100; +// TODO: Update the link for post-NU6 funding streams. /// The specification for all current funding stream receivers, a URL that links to [ZIP-214]. /// /// [ZIP-214]: https://zips.z.cash/zip-0214 diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 2593060af47..3df3edc8d53 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -1390,10 +1390,7 @@ impl Transaction { .map(|shielded_data| &mut shielded_data.value_balance) } - /// Get the value balances for this transaction, - /// using the transparent outputs spent in this transaction. - /// - /// See `value_balance` for details. + /// Returns the value balances for this transaction using the provided transparent outputs. pub(crate) fn value_balance_from_outputs( &self, outputs: &HashMap, @@ -1404,25 +1401,26 @@ impl Transaction { + self.orchard_value_balance() } - /// Get the value balances for this transaction. - /// These are the changes in the transaction value pool, - /// split up into transparent, sprout, sapling, and orchard values. + /// Returns the value balances for this transaction. /// - /// Calculated as the sum of the inputs and outputs from each pool, - /// or the sum of the value balances from each pool. + /// These are the changes in the transaction value pool, split up into transparent, Sprout, + /// Sapling, and Orchard values. /// - /// Positive values are added to this transaction's value pool, - /// and removed from the corresponding chain value pool. - /// Negative values are removed from this transaction, - /// and added to the corresponding pool. + /// Calculated as the sum of the inputs and outputs from each pool, or the sum of the value + /// balances from each pool. + /// + /// Positive values are added to this transaction's value pool, and removed from the + /// corresponding chain value pool. Negative values are removed from this transaction, and added + /// to the corresponding pool. /// /// /// - /// `utxos` must contain the utxos of every input in the transaction, - /// including UTXOs created by earlier transactions in this block. + /// `utxos` must contain the utxos of every input in the transaction, including UTXOs created by + /// earlier transactions in this block. + /// + /// ## Note /// - /// Note: the chain value pool has the opposite sign to the transaction - /// value pool. + /// The chain value pool has the opposite sign to the transaction value pool. pub fn value_balance( &self, utxos: &HashMap, diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index 63438cd062a..7e93d349b3b 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -1,16 +1,14 @@ -//! A type that can hold the four types of Zcash value pools. +//! Balances in chain value pools and transaction value pools. -use crate::{ - amount::{self, Amount, Constraint, NegativeAllowed, NonNegative}, - block::Block, - transparent, -}; +use crate::amount::{self, Amount, Constraint, NegativeAllowed, NonNegative}; use core::fmt; + +#[cfg(any(test, feature = "proptest-impl"))] use std::{borrow::Borrow, collections::HashMap}; #[cfg(any(test, feature = "proptest-impl"))] -use crate::{amount::MAX_MONEY, transaction::Transaction}; +use crate::{amount::MAX_MONEY, transaction::Transaction, transparent}; #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -20,13 +18,14 @@ mod tests; use ValueBalanceError::*; -/// An amount spread between different Zcash pools. +/// A balance in each chain value pool or transaction value pool. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct ValueBalance { transparent: Amount, sprout: Amount, sapling: Amount, orchard: Amount, + deferred: Amount, } impl ValueBalance @@ -116,6 +115,17 @@ where self } + /// Returns the deferred amount. + pub fn deferred_amount(&self) -> Amount { + self.deferred + } + + /// Sets the deferred amount without affecting other amounts. + pub fn set_deferred_amount(&mut self, deferred_amount: Amount) -> &Self { + self.deferred = deferred_amount; + self + } + /// Creates a [`ValueBalance`] where all the pools are zero. pub fn zero() -> Self { let zero = Amount::zero(); @@ -124,6 +134,7 @@ where sprout: zero, sapling: zero, orchard: zero, + deferred: zero, } } @@ -138,6 +149,7 @@ where sprout: self.sprout.constrain().map_err(Sprout)?, sapling: self.sapling.constrain().map_err(Sapling)?, orchard: self.orchard.constrain().map_err(Orchard)?, + deferred: self.deferred.constrain().map_err(Deferred)?, }) } } @@ -166,60 +178,6 @@ impl ValueBalance { } impl ValueBalance { - /// Returns the sum of this value balance, and the chain value pool changes in `block`. - /// - /// `utxos` must contain the [`transparent::Utxo`]s of every input in this block, - /// including UTXOs created by earlier transactions in this block. - /// - /// Note: the chain value pool has the opposite sign to the transaction - /// value pool. - /// - /// See [`Block::chain_value_pool_change`] for details. - /// - /// # Consensus - /// - /// > If the Sprout chain value pool balance would become negative in the block chain - /// > created as a result of accepting a block, then all nodes MUST reject the block as invalid. - /// - /// - /// - /// > If the Sapling chain value pool balance would become negative in the block chain - /// > created as a result of accepting a block, then all nodes MUST reject the block as invalid. - /// - /// - /// - /// > If the Orchard chain value pool balance would become negative in the block chain - /// > created as a result of accepting a block , then all nodes MUST reject the block as invalid. - /// - /// - /// - /// > If any of the "Sprout chain value pool balance", "Sapling chain value pool balance", or - /// > "Orchard chain value pool balance" would become negative in the block chain created - /// > as a result of accepting a block, then all nodes MUST reject the block as invalid. - /// - /// - /// - /// Zebra also checks that the transparent value pool is non-negative. - /// In Zebra, we define this pool as the sum of all unspent transaction outputs. - /// (Despite their encoding as an `int64`, transparent output values must be non-negative.) - /// - /// This is a consensus rule derived from Bitcoin: - /// - /// > because a UTXO can only be spent once, - /// > the full value of the included UTXOs must be spent or given to a miner as a transaction fee. - /// - /// - pub fn add_block( - self, - block: impl Borrow, - utxos: &HashMap, - ) -> Result, ValueBalanceError> { - let chain_value_pool_change = block.borrow().chain_value_pool_change(utxos)?; - - // This will error if the chain value pool balance gets negative with the change. - self.add_chain_value_pool_change(chain_value_pool_change) - } - /// Returns the sum of this value balance, and the chain value pool changes in `transaction`. /// /// `outputs` must contain the [`transparent::Output`]s of every input in this transaction, @@ -228,9 +186,6 @@ impl ValueBalance { /// Note: the chain value pool has the opposite sign to the transaction /// value pool. /// - /// See [`Block::chain_value_pool_change`] and [`Transaction::value_balance`] - /// for details. - /// /// # Consensus /// /// > If any of the "Sprout chain value pool balance", "Sapling chain value pool balance", or @@ -269,9 +224,6 @@ impl ValueBalance { /// /// Note: the chain value pool has the opposite sign to the transaction /// value pool. Inputs remove value from the chain value pool. - /// - /// See [`Block::chain_value_pool_change`] and [`Transaction::value_balance`] - /// for details. #[cfg(any(test, feature = "proptest-impl"))] pub fn add_transparent_input( self, @@ -289,12 +241,46 @@ impl ValueBalance { self.add_chain_value_pool_change(transparent_value_pool_change) } - /// Returns the sum of this value balance, and the `chain_value_pool_change`. + /// Returns the sum of this value balance, and the given `chain_value_pool_change`. /// - /// Note: the chain value pool has the opposite sign to the transaction - /// value pool. + /// Note that the chain value pool has the opposite sign to the transaction value pool. + /// + /// # Consensus + /// + /// > If the Sprout chain value pool balance would become negative in the block chain + /// > created as a result of accepting a block, then all nodes MUST reject the block as invalid. + /// + /// + /// + /// > If the Sapling chain value pool balance would become negative in the block chain + /// > created as a result of accepting a block, then all nodes MUST reject the block as invalid. /// - /// See `add_block` for details. + /// + /// + /// > If the Orchard chain value pool balance would become negative in the block chain + /// > created as a result of accepting a block , then all nodes MUST reject the block as invalid. + /// + /// + /// + /// > If any of the "Sprout chain value pool balance", "Sapling chain value pool balance", or + /// > "Orchard chain value pool balance" would become negative in the block chain created + /// > as a result of accepting a block, then all nodes MUST reject the block as invalid. + /// + /// + /// + /// Zebra also checks that the transparent value pool is non-negative. + /// In Zebra, we define this pool as the sum of all unspent transaction outputs. + /// (Despite their encoding as an `int64`, transparent output values must be non-negative.) + /// + /// This is a consensus rule derived from Bitcoin: + /// + /// > because a UTXO can only be spent once, + /// > the full value of the included UTXOs must be spent or given to a miner as a transaction fee. + /// + /// + /// + /// We implement the consensus rules above by constraining the returned value balance to + /// [`ValueBalance`]. #[allow(clippy::unwrap_in_result)] pub fn add_chain_value_pool_change( self, @@ -333,15 +319,20 @@ impl ValueBalance { } /// To byte array - pub fn to_bytes(self) -> [u8; 32] { - let transparent = self.transparent.to_bytes(); - let sprout = self.sprout.to_bytes(); - let sapling = self.sapling.to_bytes(); - let orchard = self.orchard.to_bytes(); - match [transparent, sprout, sapling, orchard].concat().try_into() { + pub fn to_bytes(self) -> [u8; 40] { + match [ + self.transparent.to_bytes(), + self.sprout.to_bytes(), + self.sapling.to_bytes(), + self.orchard.to_bytes(), + self.deferred.to_bytes(), + ] + .concat() + .try_into() + { Ok(bytes) => bytes, _ => unreachable!( - "Four [u8; 8] should always concat with no error into a single [u8; 32]" + "five [u8; 8] should always concat with no error into a single [u8; 40]" ), } } @@ -349,39 +340,59 @@ impl ValueBalance { /// From byte array #[allow(clippy::unwrap_in_result)] pub fn from_bytes(bytes: &[u8]) -> Result, ValueBalanceError> { + let bytes_length = bytes.len(); + + // Return an error early if bytes don't have the right lenght instead of panicking later. + match bytes_length { + 32 | 40 => {} + _ => return Err(Unparsable), + }; + let transparent = Amount::from_bytes( bytes[0..8] .try_into() - .expect("Extracting the first quarter of a [u8; 32] should always succeed"), + .expect("transparent amount should be parsable"), ) .map_err(Transparent)?; let sprout = Amount::from_bytes( bytes[8..16] .try_into() - .expect("Extracting the second quarter of a [u8; 32] should always succeed"), + .expect("sprout amount should be parsable"), ) .map_err(Sprout)?; let sapling = Amount::from_bytes( bytes[16..24] .try_into() - .expect("Extracting the third quarter of a [u8; 32] should always succeed"), + .expect("sapling amount should be parsable"), ) .map_err(Sapling)?; let orchard = Amount::from_bytes( bytes[24..32] .try_into() - .expect("Extracting the last quarter of a [u8; 32] should always succeed"), + .expect("orchard amount should be parsable"), ) .map_err(Orchard)?; + let deferred = match bytes_length { + 32 => Amount::zero(), + 40 => Amount::from_bytes( + bytes[32..40] + .try_into() + .expect("deferred amount should be parsable"), + ) + .map_err(Deferred)?, + _ => return Err(Unparsable), + }; + Ok(ValueBalance { transparent, sprout, sapling, orchard, + deferred, }) } } @@ -400,6 +411,12 @@ pub enum ValueBalanceError { /// orchard amount error {0} Orchard(amount::Error), + + /// deferred amount error {0} + Deferred(amount::Error), + + /// ValueBalance is unparsable + Unparsable, } impl fmt::Display for ValueBalanceError { @@ -409,6 +426,8 @@ impl fmt::Display for ValueBalanceError { Sprout(e) => format!("sprout amount err: {e}"), Sapling(e) => format!("sapling amount err: {e}"), Orchard(e) => format!("orchard amount err: {e}"), + Deferred(e) => format!("deferred amount err: {e}"), + Unparsable => "value balance is unparsable".to_string(), }) } } @@ -424,6 +443,7 @@ where sprout: (self.sprout + rhs.sprout).map_err(Sprout)?, sapling: (self.sapling + rhs.sapling).map_err(Sapling)?, orchard: (self.orchard + rhs.orchard).map_err(Orchard)?, + deferred: (self.deferred + rhs.deferred).map_err(Deferred)?, }) } } @@ -472,6 +492,7 @@ where sprout: (self.sprout - rhs.sprout).map_err(Sprout)?, sapling: (self.sapling - rhs.sapling).map_err(Sapling)?, orchard: (self.orchard - rhs.orchard).map_err(Orchard)?, + deferred: (self.deferred - rhs.deferred).map_err(Deferred)?, }) } } @@ -540,6 +561,7 @@ where sprout: self.sprout.neg(), sapling: self.sapling.neg(), orchard: self.orchard.neg(), + deferred: self.deferred.neg(), } } } diff --git a/zebra-chain/src/value_balance/arbitrary.rs b/zebra-chain/src/value_balance/arbitrary.rs index fa6a7c282a3..353a9f08e32 100644 --- a/zebra-chain/src/value_balance/arbitrary.rs +++ b/zebra-chain/src/value_balance/arbitrary.rs @@ -10,12 +10,14 @@ impl Arbitrary for ValueBalance { any::>(), any::>(), any::>(), + any::>(), ) - .prop_map(|(transparent, sprout, sapling, orchard)| Self { + .prop_map(|(transparent, sprout, sapling, orchard, deferred)| Self { transparent, sprout, sapling, orchard, + deferred, }) .boxed() } @@ -32,12 +34,14 @@ impl Arbitrary for ValueBalance { any::>(), any::>(), any::>(), + any::>(), ) - .prop_map(|(transparent, sprout, sapling, orchard)| Self { + .prop_map(|(transparent, sprout, sapling, orchard, deferred)| Self { transparent, sprout, sapling, orchard, + deferred, }) .boxed() } diff --git a/zebra-chain/src/value_balance/tests/prop.rs b/zebra-chain/src/value_balance/tests/prop.rs index eb05bcb766a..434c54f86fb 100644 --- a/zebra-chain/src/value_balance/tests/prop.rs +++ b/zebra-chain/src/value_balance/tests/prop.rs @@ -16,15 +16,17 @@ proptest! { let sprout = value_balance1.sprout + value_balance2.sprout; let sapling = value_balance1.sapling + value_balance2.sapling; let orchard = value_balance1.orchard + value_balance2.orchard; + let deferred = value_balance1.deferred + value_balance2.deferred; - match (transparent, sprout, sapling, orchard) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( value_balance1 + value_balance2, Ok(ValueBalance { transparent, sprout, sapling, orchard, + deferred }) ), _ => prop_assert!( @@ -33,7 +35,8 @@ proptest! { Err(ValueBalanceError::Transparent(_) | ValueBalanceError::Sprout(_) | ValueBalanceError::Sapling(_) - | ValueBalanceError::Orchard(_)) + | ValueBalanceError::Orchard(_) + | ValueBalanceError::Deferred(_)) ) ), } @@ -49,26 +52,27 @@ proptest! { let sprout = value_balance1.sprout - value_balance2.sprout; let sapling = value_balance1.sapling - value_balance2.sapling; let orchard = value_balance1.orchard - value_balance2.orchard; + let deferred = value_balance1.deferred - value_balance2.deferred; - match (transparent, sprout, sapling, orchard) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( value_balance1 - value_balance2, Ok(ValueBalance { transparent, sprout, sapling, orchard, + deferred }) ), - _ => prop_assert!( - matches!( + _ => prop_assert!(matches!( value_balance1 - value_balance2, Err(ValueBalanceError::Transparent(_) | ValueBalanceError::Sprout(_) | ValueBalanceError::Sapling(_) - | ValueBalanceError::Orchard(_)) - ) - ), + | ValueBalanceError::Orchard(_) + | ValueBalanceError::Deferred(_)) + )), } } @@ -85,23 +89,27 @@ proptest! { let sprout = value_balance1.sprout + value_balance2.sprout; let sapling = value_balance1.sapling + value_balance2.sapling; let orchard = value_balance1.orchard + value_balance2.orchard; + let deferred = value_balance1.deferred + value_balance2.deferred; - match (transparent, sprout, sapling, orchard) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( collection.iter().sum::, ValueBalanceError>>(), Ok(ValueBalance { transparent, sprout, sapling, orchard, + deferred }) ), - _ => prop_assert!(matches!(collection.iter().sum(), - Err(ValueBalanceError::Transparent(_) - | ValueBalanceError::Sprout(_) - | ValueBalanceError::Sapling(_) - | ValueBalanceError::Orchard(_)) - )) + _ => prop_assert!(matches!( + collection.iter().sum(), + Err(ValueBalanceError::Transparent(_) + | ValueBalanceError::Sprout(_) + | ValueBalanceError::Sapling(_) + | ValueBalanceError::Orchard(_) + | ValueBalanceError::Deferred(_)) + )) } } @@ -115,11 +123,27 @@ proptest! { } #[test] - fn value_balance_deserialization(bytes in any::<[u8; 32]>()) { + fn value_balance_deserialization(bytes in any::<[u8; 40]>()) { let _init_guard = zebra_test::init(); if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { prop_assert_eq!(bytes, deserialized.to_bytes()); } } + + /// The legacy version of [`ValueBalance`] had 32 bytes compared to the current 40 bytes, + /// but it's possible to correctly instantiate the current version of [`ValueBalance`] from + /// the legacy format, so we test if Zebra can still deserialiaze the legacy format. + #[test] + fn legacy_value_balance_deserialization(bytes in any::<[u8; 32]>()) { + let _init_guard = zebra_test::init(); + + if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { + let deserialized = deserialized.to_bytes(); + let mut extended_bytes = [0u8; 40]; + extended_bytes[..32].copy_from_slice(&bytes); + prop_assert_eq!(extended_bytes, deserialized); + } + } + } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index b888e882282..a428a67f91e 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -21,7 +21,13 @@ use thiserror::Error; use tower::{Service, ServiceExt}; use tracing::Instrument; -use zebra_chain::{amount::Amount, block, parameters::Network, transparent, work::equihash}; +use zebra_chain::{ + amount::Amount, + block, + parameters::{subsidy::FundingStreamReceiver, Network}, + transparent, + work::equihash, +}; use zebra_state as zs; use crate::{error::*, transaction as tx, BoxError}; @@ -78,6 +84,9 @@ pub enum VerifyBlockError { #[error("invalid transaction")] Transaction(#[from] TransactionError), + + #[error("invalid block subsidy")] + Subsidy(#[from] zebra_chain::amount::Error), } impl VerifyBlockError { @@ -205,7 +214,10 @@ where check::time_is_valid_at(&block.header, now, &height, &hash) .map_err(VerifyBlockError::Time)?; let coinbase_tx = check::coinbase_is_first(&block)?; - check::subsidy_is_valid(&block, &network)?; + + let expected_block_subsidy = subsidy::general::block_subsidy(height, &network)?; + + check::subsidy_is_valid(&block, &network, expected_block_subsidy)?; // Now do the slower checks @@ -271,13 +283,29 @@ 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, hash, source: amount_error, })?; - check::miner_fees_are_valid(&block, &network, block_miner_fees)?; + + check::miner_fees_are_valid( + &block, + block_miner_fees, + expected_block_subsidy, + expected_deferred_amount, + )?; // Finally, submit the block for contextual verification. let new_outputs = Arc::into_inner(known_utxos) @@ -289,6 +317,7 @@ where height, new_outputs, transaction_hashes, + deferred_balance: Some(expected_deferred_amount), }; // 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 2e27bc42aa8..eca45e8bc0c 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -144,7 +144,11 @@ pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error /// 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) -> Result<(), BlockError> { +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)?; @@ -182,8 +186,12 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr // 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) - .expect("We always expect a funding stream hashmap response even if empty"); + 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 // @@ -227,10 +235,10 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr /// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus pub fn miner_fees_are_valid( block: &Block, - network: &Network, block_miner_fees: Amount, + expected_block_subsidy: Amount, + expected_deferred_amount: Amount, ) -> Result<(), BlockError> { - let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?; let transparent_value_balance: Amount = subsidy::general::output_amounts(coinbase) @@ -242,15 +250,6 @@ pub fn miner_fees_are_valid( let sapling_value_balance = coinbase.sapling_value_balance().sapling_amount(); let orchard_value_balance = coinbase.orchard_value_balance().orchard_amount(); - let block_subsidy = subsidy::general::block_subsidy(height, network) - .expect("a valid block subsidy for this height and network"); - - // TODO: Add link to lockbox stream ZIP - let expected_deferred_amount = subsidy::funding_streams::funding_stream_values(height, network) - .expect("we always expect a funding stream hashmap response even if empty") - .remove(&FundingStreamReceiver::Deferred) - .unwrap_or_default(); - // # Consensus // // > The total value in zatoshi of transparent outputs from a coinbase transaction, @@ -266,7 +265,7 @@ pub fn miner_fees_are_valid( // https://zips.z.cash/draft-nuttycom-funding-allocation and https://zips.z.cash/draft-hopwood-coinbase-balance. let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance) .map_err(|_| SubsidyError::SumOverflow)?; - let right = (block_subsidy + block_miner_fees - expected_deferred_amount) + let right = (expected_block_subsidy + block_miner_fees - expected_deferred_amount) .map_err(|_| SubsidyError::SumOverflow)?; if left > right { diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-consensus/src/block/subsidy/funding_streams.rs index bdf72809f72..ce0bbf792ad 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams.rs @@ -12,8 +12,6 @@ use zebra_chain::{ transparent::{self, Script}, }; -use crate::block::subsidy::general::block_subsidy; - #[cfg(test)] mod tests; @@ -24,6 +22,7 @@ mod tests; pub fn funding_stream_values( height: Height, network: &Network, + expected_block_subsidy: Amount, ) -> Result>, Error> { let canopy_height = Canopy.activation_height(network).unwrap(); let mut results = HashMap::new(); @@ -31,14 +30,13 @@ pub fn funding_stream_values( if height >= canopy_height { let funding_streams = network.funding_streams(height); if funding_streams.height_range().contains(&height) { - let block_subsidy = block_subsidy(height, network)?; for (&receiver, recipient) in funding_streams.recipients() { // - Spec equation: `fs.value = floor(block_subsidy(height)*(fs.numerator/fs.denominator))`: // https://zips.z.cash/protocol/protocol.pdf#subsidies // - In Rust, "integer division rounds towards zero": // https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators // This is the same as `floor()`, because these numbers are all positive. - let amount_value = ((block_subsidy * recipient.numerator())? + let amount_value = ((expected_block_subsidy * recipient.numerator())? / FUNDING_STREAM_RECEIVER_DENOMINATOR)?; results.insert(receiver, amount_value); @@ -93,13 +91,6 @@ pub fn funding_stream_address( funding_streams.recipient(receiver)?.addresses().get(index) } -/// Return a human-readable name and a specification URL for the funding stream `receiver`. -pub fn funding_stream_recipient_info( - receiver: FundingStreamReceiver, -) -> (&'static str, &'static str) { - (receiver.name(), FUNDING_STREAM_SPECIFICATION) -} - /// Given a funding stream P2SH address, create a script and check if it is the same /// as the given lock_script as described in [protocol specification §7.10][7.10] /// diff --git a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs index 96d881f70f2..626b983fac6 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs @@ -10,6 +10,8 @@ use zebra_chain::parameters::{ NetworkKind, }; +use crate::block::subsidy::general::block_subsidy; + use super::*; /// Check mainnet funding stream values are correct for the entire period. @@ -19,13 +21,19 @@ fn test_funding_stream_values() -> Result<(), Report> { let network = &Network::Mainnet; // funding streams not active - let canopy_height_minus1 = Canopy.activation_height(network).unwrap() - 1; - assert!(funding_stream_values(canopy_height_minus1.unwrap(), network)?.is_empty()); + let canopy_height_minus1 = (Canopy.activation_height(network).unwrap() - 1).unwrap(); + + assert!(funding_stream_values( + canopy_height_minus1, + network, + block_subsidy(canopy_height_minus1, network)? + )? + .is_empty()); // funding stream is active - let canopy_height = Canopy.activation_height(network); - let canopy_height_plus1 = Canopy.activation_height(network).unwrap() + 1; - let canopy_height_plus2 = Canopy.activation_height(network).unwrap() + 2; + let canopy_height = Canopy.activation_height(network).unwrap(); + let canopy_height_plus1 = (Canopy.activation_height(network).unwrap() + 1).unwrap(); + let canopy_height_plus2 = (Canopy.activation_height(network).unwrap() + 2).unwrap(); let mut hash_map = HashMap::new(); hash_map.insert(FundingStreamReceiver::Ecc, Amount::try_from(21_875_000)?); @@ -39,28 +47,46 @@ fn test_funding_stream_values() -> Result<(), Report> { ); assert_eq!( - funding_stream_values(canopy_height.unwrap(), network).unwrap(), + funding_stream_values( + canopy_height, + network, + block_subsidy(canopy_height, network)? + ) + .unwrap(), hash_map ); + assert_eq!( - funding_stream_values(canopy_height_plus1.unwrap(), network).unwrap(), + funding_stream_values( + canopy_height_plus1, + network, + block_subsidy(canopy_height_plus1, network)? + ) + .unwrap(), hash_map ); + assert_eq!( - funding_stream_values(canopy_height_plus2.unwrap(), network).unwrap(), + funding_stream_values( + canopy_height_plus2, + network, + block_subsidy(canopy_height_plus2, network)? + ) + .unwrap(), hash_map ); // funding stream period is ending let range = network.pre_nu6_funding_streams().height_range(); let end = range.end; - let last = end - 1; + let last = (end - 1).unwrap(); assert_eq!( - funding_stream_values(last.unwrap(), network).unwrap(), + funding_stream_values(last, network, block_subsidy(last, network)?).unwrap(), hash_map ); - assert!(funding_stream_values(end, network)?.is_empty()); + + assert!(funding_stream_values(end, network, block_subsidy(end, network)?)?.is_empty()); // TODO: Replace this with Mainnet once there's an NU6 activation height defined for Mainnet let network = testnet::Parameters::build() @@ -110,7 +136,64 @@ fn test_funding_stream_values() -> Result<(), Report> { Height(nu6_height.0 + 1), Height(nu6_height.0 + 1), ] { - assert_eq!(funding_stream_values(height, &network).unwrap(), hash_map); + assert_eq!( + funding_stream_values(height, &network, block_subsidy(height, &network)?).unwrap(), + hash_map + ); + } + + // TODO: Replace this with Mainnet once there's an NU6 activation height defined for Mainnet + let network = testnet::Parameters::build() + .with_activation_heights(ConfiguredActivationHeights { + blossom: Some(Blossom.activation_height(&network).unwrap().0), + nu6: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().start.0), + ..Default::default() + }) + .with_post_nu6_funding_streams(ConfiguredFundingStreams { + // Start checking funding streams from block height 1 + height_range: Some(POST_NU6_FUNDING_STREAMS_MAINNET.height_range().clone()), + // Use default post-NU6 recipients + recipients: Some( + POST_NU6_FUNDING_STREAMS_TESTNET + .recipients() + .iter() + .map(|(&receiver, recipient)| ConfiguredFundingStreamRecipient { + receiver, + numerator: recipient.numerator(), + addresses: Some( + recipient + .addresses() + .iter() + .map(|addr| addr.to_string()) + .collect(), + ), + }) + .collect(), + ), + }) + .to_network(); + + let mut hash_map = HashMap::new(); + hash_map.insert( + FundingStreamReceiver::Deferred, + Amount::try_from(18_750_000)?, + ); + hash_map.insert( + FundingStreamReceiver::MajorGrants, + Amount::try_from(12_500_000)?, + ); + + let nu6_height = Nu6.activation_height(&network).unwrap(); + + for height in [ + nu6_height, + Height(nu6_height.0 + 1), + Height(nu6_height.0 + 1), + ] { + assert_eq!( + funding_stream_values(height, &network, block_subsidy(height, &network)?).unwrap(), + hash_map + ); } Ok(()) diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 0485ab0735c..83257420bf8 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -26,10 +26,7 @@ pub fn halving_divisor(height: Height, network: &Network) -> Option { .expect("blossom activation height should be available"); if height < network.slow_start_shift() { - panic!( - "unsupported block height {height:?}: checkpoints should handle blocks below {:?}", - network.slow_start_shift() - ) + None } else if height < blossom_height { let pre_blossom_height = height - network.slow_start_shift(); let halving_shift = pre_blossom_height / PRE_BLOSSOM_HALVING_INTERVAL; @@ -101,11 +98,17 @@ pub fn block_subsidy(height: Height, network: &Network) -> Result Result, Error> { +pub fn miner_subsidy( + height: Height, + network: &Network, + expected_block_subsidy: Amount, +) -> Result, Error> { let total_funding_stream_amount: Result, _> = - funding_stream_values(height, network)?.values().sum(); + funding_stream_values(height, network, expected_block_subsidy)? + .values() + .sum(); - block_subsidy(height, network)? - total_funding_stream_amount? + expected_block_subsidy - total_funding_stream_amount? } /// Returns all output amounts in `Transaction`. @@ -129,10 +132,13 @@ fn lockbox_input_value(network: &Network, height: Height) -> Amount return Amount::zero(); }; - let &deferred_amount_per_block = funding_stream_values(nu6_activation_height, network) - .expect("we always expect a funding stream hashmap response even if empty") - .get(&FundingStreamReceiver::Deferred) - .expect("we expect a lockbox funding stream after NU5"); + let expected_block_subsidy = block_subsidy(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) + .expect("we always expect a funding stream hashmap response even if empty") + .get(&FundingStreamReceiver::Deferred) + .expect("we expect a lockbox funding stream after NU5"); let post_nu6_funding_stream_height_range = network.post_nu6_funding_streams().height_range(); diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index f5b93faa6a8..8f202a716e2 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -20,7 +20,7 @@ use zebra_chain::{ use zebra_script::CachedFfiTransaction; use zebra_test::transcript::{ExpectedTranscriptError, Transcript}; -use crate::transaction; +use crate::{block_subsidy, transaction}; use super::*; @@ -292,6 +292,7 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> { let block_iter = network.block_iter(); for (&height, block) in block_iter { + let height = block::Height(height); let block = block .zcash_deserialize_into::() .expect("block is structurally valid"); @@ -301,8 +302,11 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> { .expect("Canopy activation height is known"); // TODO: first halving, second halving, third halving, and very large halvings - if block::Height(height) >= canopy_activation_height { - check::subsidy_is_valid(&block, &network) + if height >= canopy_activation_height { + let expected_block_subsidy = + subsidy::general::block_subsidy(height, &network).expect("valid block subsidy"); + + check::subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidies should pass for this block"); } } @@ -322,6 +326,14 @@ 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( + block + .coinbase_height() + .expect("block should have coinbase height"), + &network, + ) + .expect("valid block subsidy"); + // Remove coinbase transaction block.transactions.remove(0); @@ -330,8 +342,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { let expected = BlockError::NoTransactions; assert_eq!(expected, result); - // Validate the block using subsidy_is_valid - let result = check::subsidy_is_valid(&block, &network).unwrap_err(); + let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); assert_eq!(expected, result); @@ -341,6 +352,14 @@ 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( + block + .coinbase_height() + .expect("block should have coinbase height"), + &network, + ) + .expect("valid block subsidy"); + // Remove coinbase transaction block.transactions.remove(0); @@ -349,8 +368,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { let expected = BlockError::Transaction(TransactionError::CoinbasePosition); assert_eq!(expected, result); - // Validate the block using subsidy_is_valid - let result = check::subsidy_is_valid(&block, &network).unwrap_err(); + let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); assert_eq!(expected, result); @@ -374,8 +392,15 @@ fn coinbase_validation_failure() -> Result<(), Report> { let expected = BlockError::Transaction(TransactionError::CoinbaseAfterFirst); assert_eq!(expected, result); - // Validate the block using subsidy_is_valid, which does not detect this error - check::subsidy_is_valid(&block, &network) + let expected_block_subsidy = subsidy::general::block_subsidy( + block + .coinbase_height() + .expect("block should have coinbase height"), + &network, + ) + .expect("valid block subsidy"); + + check::subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidy does not check for extra coinbase transactions"); Ok(()) @@ -399,11 +424,15 @@ fn funding_stream_validation_for_network(network: Network) -> Result<(), Report> .expect("Canopy activation height is known"); for (&height, block) in block_iter { - if Height(height) >= canopy_activation_height { + let height = Height(height); + + 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"); // Validate - let result = check::subsidy_is_valid(&block, &network); + let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); assert!(result.is_ok()); } } @@ -447,7 +476,15 @@ fn funding_stream_validation_failure() -> Result<(), Report> { }; // Validate it - let result = check::subsidy_is_valid(&block, &network); + let expected_block_subsidy = subsidy::general::block_subsidy( + block + .coinbase_height() + .expect("block should have coinbase height"), + &network, + ) + .expect("valid block subsidy"); + + let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); let expected = Err(BlockError::Transaction(TransactionError::Subsidy( SubsidyError::FundingStreamNotFound, ))); @@ -470,14 +507,30 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> { let block_iter = network.block_iter(); for (&height, block) in block_iter { - if Height(height) > network.slow_start_shift() { + let height = Height(height); + if height > network.slow_start_shift() { let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize"); + let expected_block_subsidy = block_subsidy(height, &network)?; + // TODO: Add link to lockbox stream ZIP + let expected_deferred_amount = subsidy::funding_streams::funding_stream_values( + height, + &network, + expected_block_subsidy, + ) + .expect("we always expect a funding stream hashmap response even if empty") + .remove(&FundingStreamReceiver::Deferred) + .unwrap_or_default(); // fake the miner fee to a big amount let miner_fees = Amount::try_from(MAX_MONEY / 2).unwrap(); // Validate - let result = check::miner_fees_are_valid(&block, &network, miner_fees); + let result = check::miner_fees_are_valid( + &block, + miner_fees, + expected_block_subsidy, + expected_deferred_amount, + ); assert!(result.is_ok()); } } @@ -489,16 +542,28 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> { fn miner_fees_validation_failure() -> Result<(), Report> { let _init_guard = zebra_test::init(); let network = Network::Mainnet; - let block = Arc::::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..]) .expect("block should deserialize"); + let height = block.coinbase_height().expect("valid coinbase height"); + let expected_block_subsidy = block_subsidy(height, &network)?; + // TODO: Add link to lockbox stream ZIP + 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(); // fake the miner fee to a small amount let miner_fees = Amount::zero(); // Validate - let result = check::miner_fees_are_valid(&block, &network, miner_fees); + let result = check::miner_fees_are_valid( + &block, + miner_fees, + expected_block_subsidy, + expected_deferred_amount, + ); let expected = Err(BlockError::Transaction(TransactionError::Subsidy( SubsidyError::InvalidMinerFees, diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index 53432bb1ca4..1138d2dbe2b 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -28,21 +28,22 @@ use tower::{Service, ServiceExt}; use tracing::instrument; use zebra_chain::{ + amount, block::{self, Block}, - parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, + parameters::{subsidy::FundingStreamReceiver, Network, GENESIS_PREVIOUS_BLOCK_HASH}, work::equihash, }; use zebra_state::{self as zs, CheckpointVerifiedBlock}; use crate::{ block::VerifyBlockError, + block_subsidy, checkpoint::types::{ - Progress, - Progress::*, + Progress::{self, *}, TargetHeight::{self, *}, }, error::BlockError, - BoxError, ParameterCheckpoint as _, + funding_stream_values, BoxError, ParameterCheckpoint as _, }; pub(crate) mod list; @@ -607,8 +608,16 @@ where crate::block::check::equihash_solution_is_valid(&block.header)?; } + 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::with_hash(block, hash); + let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount); crate::block::check::merkle_root_validity( &self.network, @@ -981,6 +990,8 @@ pub enum VerifyCheckpointError { CheckpointList(BoxError), #[error(transparent)] VerifyBlock(VerifyBlockError), + #[error("invalid block subsidy")] + SubsidyError(#[from] amount::Error), #[error("too many queued blocks at this height")] QueuedLimit, #[error("the block hash does not match the chained checkpoint hash, expected {expected:?} found {found:?}")] diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 66323a3b550..95381fd9e07 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -49,11 +49,8 @@ pub use block::check::difficulty_is_valid; pub use block::{ subsidy::{ - funding_streams::{ - funding_stream_address, funding_stream_recipient_info, funding_stream_values, - new_coinbase_script, - }, - general::miner_subsidy, + funding_streams::{funding_stream_address, funding_stream_values, new_coinbase_script}, + general::{block_subsidy, miner_subsidy}, }, Request, VerifyBlockError, MAX_BLOCK_SIGOPS, }; diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index f191c3a287c..fd4dffc4208 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -22,7 +22,9 @@ use zebra_chain::{ }, work::difficulty::{ParameterDifficulty as _, U256}, }; -use zebra_consensus::{funding_stream_address, funding_stream_values, miner_subsidy, RouterError}; +use zebra_consensus::{ + block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, RouterError, +}; use zebra_network::AddressBookPeers; use zebra_node_services::mempool; use zebra_state::{ReadRequest, ReadResponse}; @@ -1176,20 +1178,28 @@ where }); } - let miner = miner_subsidy(height, &network).map_err(|error| Error { - code: ErrorCode::ServerError(0), - message: error.to_string(), - data: None, - })?; - // Always zero for post-halving blocks - let founders = Amount::zero(); + let expected_block_subsidy = + block_subsidy(height, &network).map_err(|error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + })?; - let funding_streams = - funding_stream_values(height, &network).map_err(|error| Error { + let miner_subsidy = + miner_subsidy(height, &network, expected_block_subsidy).map_err(|error| Error { code: ErrorCode::ServerError(0), message: error.to_string(), data: None, })?; + // Always zero for post-halving blocks + let founders = Amount::zero(); + + let funding_streams = funding_stream_values(height, &network, expected_block_subsidy) + .map_err(|error| Error { + code: ErrorCode::ServerError(0), + message: error.to_string(), + data: None, + })?; let mut funding_streams: Vec<_> = funding_streams .iter() .filter_map(|(receiver, value)| { @@ -1208,7 +1218,7 @@ where let (_receivers, funding_streams): (Vec<_>, _) = funding_streams.into_iter().unzip(); Ok(BlockSubsidy { - miner: miner.into(), + miner: miner_subsidy.into(), founders: founders.into(), funding_streams, }) 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 00fb4d8a9d2..c25c7b03ce6 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 @@ -19,7 +19,9 @@ use zebra_chain::{ transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, transparent, }; -use zebra_consensus::{funding_stream_address, funding_stream_values, miner_subsidy}; +use zebra_consensus::{ + block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, +}; use zebra_node_services::mempool; use zebra_state::GetBlockTemplateChainInfo; @@ -375,7 +377,8 @@ pub fn standard_coinbase_outputs( miner_fee: Amount, like_zcashd: bool, ) -> Vec<(Amount, transparent::Script)> { - let funding_streams = funding_stream_values(height, network) + 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"); // Optional TODO: move this into a zebra_consensus function? @@ -392,7 +395,7 @@ pub fn standard_coinbase_outputs( }) .collect(); - let miner_reward = miner_subsidy(height, network) + let miner_reward = 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/get_block_template_rpcs/types/subsidy.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs index f5ac478bf6b..51ff300530b 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/subsidy.rs @@ -5,7 +5,6 @@ use zebra_chain::{ parameters::subsidy::FundingStreamReceiver, transparent, }; -use zebra_consensus::funding_stream_recipient_info; use crate::methods::get_block_template_rpcs::types::zec::Zec; @@ -69,10 +68,10 @@ impl FundingStream { value: Amount, address: &transparent::Address, ) -> FundingStream { - let (recipient, specification) = funding_stream_recipient_info(receiver); + let (name, specification) = receiver.info(); FundingStream { - recipient: recipient.to_string(), + recipient: name.to_string(), specification: specification.to_string(), value: value.into(), value_zat: value, diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index c2176296ca7..5c0b837566a 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use zebra_chain::{ - amount::{Amount, NegativeAllowed}, + amount::Amount, block::{self, Block}, transaction::Transaction, transparent, @@ -11,7 +11,7 @@ use zebra_chain::{ }; use crate::{ - request::ContextuallyVerifiedBlock, service::chain_tip::ChainTipBlock, CheckpointVerifiedBlock, + request::ContextuallyVerifiedBlock, service::chain_tip::ChainTipBlock, SemanticallyVerifiedBlock, }; @@ -37,6 +37,7 @@ impl Prepare for Arc { height, new_outputs, transaction_hashes, + deferred_balance: None, } } } @@ -60,18 +61,6 @@ impl SemanticallyVerifiedBlock { ContextuallyVerifiedBlock::test_with_zero_spent_utxos(self) } - /// Returns a [`ContextuallyVerifiedBlock`] created from this block, - /// using a fake chain value pool change. - /// - /// Only for use in tests. - #[cfg(test)] - pub fn test_with_chain_pool_change( - &self, - fake_chain_value_pool_change: ValueBalance, - ) -> ContextuallyVerifiedBlock { - ContextuallyVerifiedBlock::test_with_chain_pool_change(self, fake_chain_value_pool_change) - } - /// Returns a [`ContextuallyVerifiedBlock`] created from this block, /// with no chain value pool change. /// @@ -112,19 +101,17 @@ impl ContextuallyVerifiedBlock { } /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], - /// using a fake chain value pool change. + /// with no chain value pool change. /// /// Only for use in tests. - pub fn test_with_chain_pool_change( - block: impl Into, - fake_chain_value_pool_change: ValueBalance, - ) -> Self { + pub fn test_with_zero_chain_pool_change(block: impl Into) -> Self { let SemanticallyVerifiedBlock { block, hash, height, new_outputs, transaction_hashes, + deferred_balance: _, } = block.into(); Self { @@ -137,40 +124,7 @@ impl ContextuallyVerifiedBlock { // TODO: fix the tests, and stop adding unrelated inputs and outputs. spent_outputs: new_outputs, transaction_hashes, - chain_value_pool_change: fake_chain_value_pool_change, + chain_value_pool_change: ValueBalance::zero(), } } - - /// Create a [`ContextuallyVerifiedBlock`] from a [`Block`] or [`SemanticallyVerifiedBlock`], - /// with no chain value pool change. - /// - /// Only for use in tests. - pub fn test_with_zero_chain_pool_change(block: impl Into) -> Self { - Self::test_with_chain_pool_change(block, ValueBalance::zero()) - } -} - -impl CheckpointVerifiedBlock { - /// Create a block that's ready to be committed to the finalized state, - /// using a precalculated [`block::Hash`] and [`block::Height`]. - /// - /// This is a test-only method, prefer [`CheckpointVerifiedBlock::with_hash`]. - #[cfg(any(test, feature = "proptest-impl"))] - pub fn with_hash_and_height( - block: Arc, - hash: block::Hash, - height: block::Height, - ) -> Self { - let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); - let new_outputs = - transparent::new_ordered_outputs_with_height(&block, height, &transaction_hashes); - - Self(SemanticallyVerifiedBlock { - block, - hash, - height, - new_outputs, - transaction_hashes, - }) - } } diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs index 1cbe05e8342..3e33b73c254 100644 --- a/zebra-state/src/constants.rs +++ b/zebra-state/src/constants.rs @@ -46,7 +46,7 @@ pub const STATE_DATABASE_KIND: &str = "state"; /// /// Instead of using this constant directly, use [`constants::state_database_format_version_in_code()`] /// or [`config::database_format_version_on_disk()`] to get the full semantic format version. -const DATABASE_FORMAT_VERSION: u64 = 25; +const DATABASE_FORMAT_VERSION: u64 = 26; /// The database format minor version, incremented each time the on-disk database format has a /// significant data format change. @@ -55,7 +55,7 @@ const DATABASE_FORMAT_VERSION: u64 = 25; /// - adding new column families, /// - changing the format of a column family in a compatible way, or /// - breaking changes with compatibility code in all supported Zebra versions. -const DATABASE_FORMAT_MINOR_VERSION: u64 = 3; +const DATABASE_FORMAT_MINOR_VERSION: u64 = 0; /// The database format patch version, incremented each time the on-disk database format has a /// significant format compatibility fix. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 6f785a9d250..338c8530104 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -7,7 +7,7 @@ use std::{ }; use zebra_chain::{ - amount::NegativeAllowed, + amount::{Amount, NegativeAllowed, NonNegative}, block::{self, Block}, history_tree::HistoryTree, orchard, @@ -161,6 +161,8 @@ pub struct SemanticallyVerifiedBlock { /// A precomputed list of the hashes of the transactions in this block, /// in the same order as `block.transactions`. pub transaction_hashes: Arc<[transaction::Hash]>, + /// This block's contribution to the deferred pool. + pub deferred_balance: Option>, } /// A block ready to be committed directly to the finalized state with @@ -289,6 +291,8 @@ pub struct FinalizedBlock { pub(super) transaction_hashes: Arc<[transaction::Hash]>, /// The tresstate associated with the block. pub(super) treestate: Treestate, + /// This block's contribution to the deferred pool. + pub(super) deferred_balance: Option>, } impl FinalizedBlock { @@ -314,6 +318,7 @@ impl FinalizedBlock { new_outputs: block.new_outputs, transaction_hashes: block.transaction_hashes, treestate, + deferred_balance: block.deferred_balance, } } } @@ -386,6 +391,7 @@ impl ContextuallyVerifiedBlock { height, new_outputs, transaction_hashes, + deferred_balance, } = semantically_verified; // This is redundant for the non-finalized state, @@ -401,75 +407,116 @@ impl ContextuallyVerifiedBlock { new_outputs, spent_outputs: spent_outputs.clone(), transaction_hashes, - chain_value_pool_change: block - .chain_value_pool_change(&utxos_from_ordered_utxos(spent_outputs))?, + chain_value_pool_change: block.chain_value_pool_change( + &utxos_from_ordered_utxos(spent_outputs), + deferred_balance, + )?, }) } } +impl CheckpointVerifiedBlock { + /// Creates a [`CheckpointVerifiedBlock`] from [`Block`] with optional deferred balance and + /// optional pre-computed hash. + pub fn new( + block: Arc, + hash: Option, + deferred_balance: Option>, + ) -> Self { + let mut block = Self::with_hash(block.clone(), hash.unwrap_or(block.hash())); + block.deferred_balance = deferred_balance; + block + } + /// Creates a block that's ready to be committed to the finalized state, + /// using a precalculated [`block::Hash`]. + /// + /// Note: a [`CheckpointVerifiedBlock`] isn't actually finalized + /// until [`Request::CommitCheckpointVerifiedBlock`] returns success. + pub fn with_hash(block: Arc, hash: block::Hash) -> Self { + Self(SemanticallyVerifiedBlock::with_hash(block, hash)) + } +} + impl SemanticallyVerifiedBlock { - fn with_hash(block: Arc, hash: block::Hash) -> Self { + /// Creates [`SemanticallyVerifiedBlock`] from [`Block`] and [`block::Hash`]. + pub fn with_hash(block: Arc, hash: block::Hash) -> Self { let height = block .coinbase_height() - .expect("coinbase height was already checked"); + .expect("semantically verified block should have a coinbase height"); let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - SemanticallyVerifiedBlock { + Self { block, hash, height, new_outputs, transaction_hashes, + deferred_balance: None, } } -} -impl CheckpointVerifiedBlock { - /// Create a block that's ready to be committed to the finalized state, - /// using a precalculated [`block::Hash`]. - /// - /// Note: a [`CheckpointVerifiedBlock`] isn't actually finalized - /// until [`Request::CommitCheckpointVerifiedBlock`] returns success. - pub fn with_hash(block: Arc, hash: block::Hash) -> Self { - Self(SemanticallyVerifiedBlock::with_hash(block, hash)) + /// Sets the deferred balance in the block. + pub fn with_deferred_balance(mut self, deferred_balance: Option>) -> Self { + self.deferred_balance = deferred_balance; + self } } impl From> for CheckpointVerifiedBlock { fn from(block: Arc) -> Self { - let hash = block.hash(); - - CheckpointVerifiedBlock::with_hash(block, hash) + CheckpointVerifiedBlock(SemanticallyVerifiedBlock::from(block)) } } impl From> for SemanticallyVerifiedBlock { fn from(block: Arc) -> Self { let hash = block.hash(); + let height = block + .coinbase_height() + .expect("semantically verified block should have a coinbase height"); + let transaction_hashes: Arc<[_]> = block.transactions.iter().map(|tx| tx.hash()).collect(); + let new_outputs = transparent::new_ordered_outputs(&block, &transaction_hashes); - SemanticallyVerifiedBlock::with_hash(block, hash) - } -} - -impl From for SemanticallyVerifiedBlock { - fn from(contextually_valid: ContextuallyVerifiedBlock) -> Self { - let ContextuallyVerifiedBlock { + Self { block, hash, height, new_outputs, - spent_outputs: _, transaction_hashes, - chain_value_pool_change: _, - } = contextually_valid; + deferred_balance: None, + } + } +} +impl From for SemanticallyVerifiedBlock { + fn from(valid: ContextuallyVerifiedBlock) -> Self { Self { - block, - hash, - height, - new_outputs, - transaction_hashes, + block: valid.block, + hash: valid.hash, + height: valid.height, + new_outputs: valid.new_outputs, + transaction_hashes: valid.transaction_hashes, + deferred_balance: Some( + valid + .chain_value_pool_change + .deferred_amount() + .constrain::() + .expect("deferred balance in a block must me non-negative"), + ), + } + } +} + +impl From for SemanticallyVerifiedBlock { + fn from(finalized: FinalizedBlock) -> Self { + Self { + block: finalized.block, + hash: finalized.hash, + height: finalized.height, + new_outputs: finalized.new_outputs, + transaction_hashes: finalized.transaction_hashes, + deferred_balance: finalized.deferred_balance, } } } @@ -937,7 +984,7 @@ pub enum ReadRequest { /// Looks up the balance of a set of transparent addresses. /// - /// Returns an [`Amount`](zebra_chain::amount::Amount) with the total + /// Returns an [`Amount`] with the total /// balance of the set of addresses. AddressBalance(HashSet), diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 3fb016aa356..04ea61d6982 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -115,6 +115,7 @@ impl From for ChainTipBlock { height, new_outputs: _, transaction_hashes, + deferred_balance: _, } = prepared; Self { diff --git a/zebra-state/src/service/finalized_state/disk_format/chain.rs b/zebra-state/src/service/finalized_state/disk_format/chain.rs index c7909bbb11b..b5a2db8de35 100644 --- a/zebra-state/src/service/finalized_state/disk_format/chain.rs +++ b/zebra-state/src/service/finalized_state/disk_format/chain.rs @@ -21,7 +21,7 @@ use zebra_chain::{ use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; impl IntoDisk for ValueBalance { - type Bytes = [u8; 32]; + type Bytes = [u8; 40]; fn as_bytes(&self) -> Self::Bytes { self.to_bytes() diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap index 4ee189bcac8..1dcca432eb9 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap @@ -1,12 +1,10 @@ --- source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs -assertion_line: 125 expression: cf_data - --- [ KV( k: "", - v: "24f4000000000000000000000000000000000000000000000000000000000000", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap index 4f67e28019a..e98c0e2cb65 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap @@ -1,12 +1,10 @@ --- source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs -assertion_line: 125 expression: cf_data - --- [ KV( k: "", - v: "6cdc020000000000000000000000000000000000000000000000000000000000", + v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap index 4ee189bcac8..1dcca432eb9 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap @@ -1,12 +1,10 @@ --- source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs -assertion_line: 125 expression: cf_data - --- [ KV( k: "", - v: "24f4000000000000000000000000000000000000000000000000000000000000", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap index 4f67e28019a..e98c0e2cb65 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap @@ -1,12 +1,10 @@ --- source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs -assertion_line: 125 expression: cf_data - --- [ KV( k: "", - v: "6cdc020000000000000000000000000000000000000000000000000000000000", + v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs index 7dc157b2f05..f8ce127843f 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs @@ -541,6 +541,14 @@ impl DbFormatChange { timer.finish(module_path!(), line!(), "tree keys and caches upgrade"); } + let version_for_upgrading_value_balance_format = + Version::parse("26.0.0").expect("hard-coded version string should be valid"); + + // Check if we need to do the upgrade. + if older_disk_version < &version_for_upgrading_value_balance_format { + Self::mark_as_upgraded_to(db, &version_for_upgrading_value_balance_format) + } + // # New Upgrades Usually Go Here // // New code goes above this comment! 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 1b4f7db9a73..194f2202a87 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 @@ -22,6 +22,7 @@ use zebra_chain::{ }, parameters::Network::{self, *}, serialization::{ZcashDeserializeInto, ZcashSerialize}, + transparent::new_ordered_outputs_with_height, }; use zebra_test::vectors::{MAINNET_BLOCKS, TESTNET_BLOCKS}; @@ -29,7 +30,7 @@ use crate::{ constants::{state_database_format_version_in_code, STATE_DATABASE_KIND}, request::{FinalizedBlock, Treestate}, service::finalized_state::{disk_db::DiskWriteBatch, ZebraDb, STATE_COLUMN_FAMILIES_IN_CODE}, - CheckpointVerifiedBlock, Config, + CheckpointVerifiedBlock, Config, SemanticallyVerifiedBlock, }; /// Storage round-trip test for block and transaction data in the finalized state database. @@ -117,14 +118,26 @@ fn test_block_db_round_trip_with( // Now, use the database let original_block = Arc::new(original_block); let checkpoint_verified = if original_block.coinbase_height().is_some() { - original_block.clone().into() + CheckpointVerifiedBlock::from(original_block.clone()) } else { // Fake a zero height - CheckpointVerifiedBlock::with_hash_and_height( - original_block.clone(), - original_block.hash(), - Height(0), - ) + let hash = original_block.hash(); + let transaction_hashes: Arc<[_]> = original_block + .transactions + .iter() + .map(|tx| tx.hash()) + .collect(); + let new_outputs = + new_ordered_outputs_with_height(&original_block, Height(0), &transaction_hashes); + + CheckpointVerifiedBlock(SemanticallyVerifiedBlock { + block: original_block.clone(), + hash, + height: Height(0), + new_outputs, + transaction_hashes, + deferred_balance: None, + }) }; let dummy_treestate = Treestate::default(); diff --git a/zebra-state/src/service/finalized_state/zebra_db/chain.rs b/zebra-state/src/service/finalized_state/zebra_db/chain.rs index 93c65322d39..5653af1c3f7 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/chain.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/chain.rs @@ -12,7 +12,6 @@ //! each time the database format (column, serialization, etc) changes. use std::{ - borrow::Borrow, collections::{BTreeMap, HashMap}, sync::Arc, }; @@ -203,16 +202,24 @@ impl DiskWriteBatch { // Value pool methods - /// Prepare a database batch containing the chain value pool update from `finalized.block`, - /// and return it (without actually writing anything). + /// Prepares a database batch containing the chain value pool update from `finalized.block`, and + /// returns it without actually writing anything. /// - /// If this method returns an error, it will be propagated, - /// and the batch should not be written to the database. + /// The batch is modified by this method and written by the caller. The caller should not write + /// the batch if this method returns an error. + /// + /// The parameter `utxos_spent_by_block` must contain the [`transparent::Utxo`]s of every input + /// in this block, including UTXOs created by earlier transactions in this block. + /// + /// Note that the chain value pool has the opposite sign to the transaction value pool. See the + /// [`chain_value_pool_change`] and [`add_chain_value_pool_change`] methods for more details. /// /// # Errors /// /// - Propagates any errors from updating value pools - #[allow(clippy::unwrap_in_result)] + /// + /// [`chain_value_pool_change`]: zebra_chain::block::Block::chain_value_pool_change + /// [`add_chain_value_pool_change`]: ValueBalance::add_chain_value_pool_change pub fn prepare_chain_value_pools_batch( &mut self, db: &ZebraDb, @@ -220,14 +227,18 @@ impl DiskWriteBatch { utxos_spent_by_block: HashMap, value_pool: ValueBalance, ) -> Result<(), BoxError> { - let chain_value_pools_cf = db.chain_value_pools_cf().with_batch_for_writing(self); - - let FinalizedBlock { block, .. } = finalized; - - let new_pool = value_pool.add_block(block.borrow(), &utxos_spent_by_block)?; - - // The batch is modified by this method and written by the caller. - let _ = chain_value_pools_cf.zs_insert(&(), &new_pool); + let _ = db + .chain_value_pools_cf() + .with_batch_for_writing(self) + .zs_insert( + &(), + &value_pool.add_chain_value_pool_change( + finalized.block.chain_value_pool_change( + &utxos_spent_by_block, + finalized.deferred_balance, + )?, + )?, + ); Ok(()) } diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 45bbb02684a..e25b1fd171b 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -53,17 +53,18 @@ pub struct Chain { // /// The last height this chain forked at. Diagnostics only. /// - /// This field is only used for metrics, it is not consensus-critical, and it is not checked - /// for equality. + /// This field is only used for metrics. It is not consensus-critical, and it is not checked for + /// equality. /// - /// We keep the same last fork height in both sides of a clone, because every new block clones - /// a chain, even if it's just growing that chain. + /// We keep the same last fork height in both sides of a clone, because every new block clones a + /// chain, even if it's just growing that chain. + /// + /// # Note + /// + /// Most diagnostics are implemented on the `NonFinalizedState`, rather than each chain. Some + /// diagnostics only use the best chain, and others need to modify the Chain state, but that's + /// difficult with `Arc`s. pub(super) last_fork_height: Option, - // # Note - // - // Most diagnostics are implemented on the NonFinalizedState, rather than each chain. - // Some diagnostics only use the best chain, and others need to modify the Chain state, - // but that's difficult with `Arc`s. } /// The internal state of [`Chain`]. @@ -199,12 +200,11 @@ pub struct ChainInner { // Chain Pools // - /// The chain value pool balances of the tip of this [`Chain`], - /// including the block value pool changes from all finalized blocks, - /// and the non-finalized blocks in this chain. + /// The chain value pool balances of the tip of this [`Chain`], including the block value pool + /// changes from all finalized blocks, and the non-finalized blocks in this chain. /// - /// When a new chain is created from the finalized tip, - /// it is initialized with the finalized tip chain value pool balances. + /// When a new chain is created from the finalized tip, it is initialized with the finalized tip + /// chain value pool balances. pub(crate) chain_value_pools: ValueBalance, } diff --git a/zebra-state/src/service/tests.rs b/zebra-state/src/service/tests.rs index ca8d195af41..9b2e6f32ff8 100644 --- a/zebra-state/src/service/tests.rs +++ b/zebra-state/src/service/tests.rs @@ -420,7 +420,7 @@ proptest! { // which is not included in the UTXO set if block.height > block::Height(0) { let utxos = &block.new_outputs.iter().map(|(k, ordered_utxo)| (*k, ordered_utxo.utxo.clone())).collect(); - let block_value_pool = &block.block.chain_value_pool_change(utxos)?; + let block_value_pool = &block.block.chain_value_pool_change(utxos, None)?; expected_finalized_value_pool += *block_value_pool; } @@ -447,7 +447,7 @@ proptest! { let mut expected_non_finalized_value_pool = Ok(expected_finalized_value_pool?); for block in non_finalized_blocks { let utxos = block.new_outputs.clone(); - let block_value_pool = &block.block.chain_value_pool_change(&transparent::utxos_from_ordered_utxos(utxos))?; + let block_value_pool = &block.block.chain_value_pool_change(&transparent::utxos_from_ordered_utxos(utxos), None)?; expected_non_finalized_value_pool += *block_value_pool; let result_receiver = state_service.queue_and_commit_to_non_finalized_state(block.clone()); @@ -586,10 +586,8 @@ fn continuous_empty_blocks_from_test_vectors() -> impl Strategy< }) .prop_map(|(network, mut blocks, finalized_blocks_count)| { let non_finalized_blocks = blocks.split_off(finalized_blocks_count); - let finalized_blocks: Vec<_> = blocks - .into_iter() - .map(|prepared_block| CheckpointVerifiedBlock::from(prepared_block.block)) - .collect(); + let finalized_blocks: Vec<_> = + blocks.into_iter().map(CheckpointVerifiedBlock).collect(); ( network,