Skip to content

Commit

Permalink
change: Refactor error handling for block subsidies (#8735)
Browse files Browse the repository at this point in the history
* Addresses clippy lints

* checks network magic and returns early from `is_regtest()`

* Moves  `subsidy.rs` to `zebra-chain`, refactors funding streams into structs, splits them into pre/post NU6 funding streams, and adds them as a field on `testnet::Parameters`

* Replaces Vec with HashMap, adds `ConfiguredFundingStreams` type and conversion logic with constraints.

Minor refactors

* Empties recipients list

* Adds a comment on num_addresses calculation being invalid for configured Testnets, but that being okay since configured testnet parameters are checked when they're being built

* Documentation fixes, minor cleanup, renames a test, adds TODOs, and fixes test logic

* Removes unnecessary `ParameterSubsidy` impl for &Network, adds docs and TODOs

* Adds a "deferred" FundingStreamReceiver, adds a post-NU6 funding streams, updates the `miner_fees_are_valid()` and `subsidy_is_valid()` functions to check that the deferred pool contribution is valid and that there are no unclaimed block subsidies after NU6 activation, and adds some TODOs

* adds `lockbox_input_value()` fn

* Adds TODOs for linking to relevant ZIPs and updating height ranges

* Adds `nu6_lockbox_funding_stream` acceptance test

* updates funding stream values test to check post-NU6 funding streams too, adds Mainnet/Testnet NU6 activation heights, fixes lints/compilation issue

* Reverts Mainnet/Testnet NU6 activation height definitions, updates `test_funding_stream_values()` to use a configured testnet with the post-NU6 Mainnet funding streams height range

* reverts unnecessary refactor

* appease clippy

* Adds a test for `lockbox_input_value()`

* Applies suggestions from code review

* Fixes potential panic

* Fixes bad merge

* Update zebra-chain/src/parameters/network_upgrade.rs

* Updates acceptance test to check that invalid blocks are rejected

* Checks that the original valid block template at height 2 is accepted as a block submission

* Reverts changes for coinbase should balance exactly ZIP

* Add `Deferred` to `ValueBalance`

* Update snapshots

* Unrelated: Revise docs

* Add TODOs

* Stop recalculating the block subsidy

* Track deferred balances

* Support heights below slow start shift in halvings

* Fix `CheckpointVerifiedBlock` conversion in tests

* Allow deserialization of legacy `ValueBalance`s

* Simplify docs

* Fix warnings raised by Clippy

* Fix warnings raised by `cargo fmt`

* Update zebra-chain/src/block.rs

Co-authored-by: Arya <[email protected]>

* Refactor docs around chain value pool changes

* updates test name

* Updates deferred pool funding stream name to "Lockbox", moves post-NU6 height ranges to constants, updates TODO

* Updates `get_block_subsidy()` RPC method to exclude lockbox funding stream from `fundingstreams` field

* Adds a TODO for updating `FundingStreamReceiver::name()` method docs

* Updates `FundingStreamRecipient::new()` to accept an iterator of items instead of an option of an iterator, updates a comment quoting the coinbase transaction balance consensus rule to note that the current code is inconsistent with the protocol spec, adds a TODO for updating the quote there once the protocol spec has been updated.

* Update zebra-consensus/src/checkpoint.rs

Co-authored-by: Arya <[email protected]>

* Update docs for value balances

* Cleanup: Simplify getting info for FS receivers

* Avoid a panic when deserializing value balances

* Uses FPF Testnet address for post-NU6 testnet funding streams

* Updates the NU6 consensus branch id

* Update zebra-consensus/src/checkpoint.rs

* Bump the major database format version

* Add a database upgrade mark

* Fix tests after merge

* Improve docs

* Consolidate error handling for block subsidies

---------

Co-authored-by: Arya <[email protected]>
Co-authored-by: Pili Guerra <[email protected]>
  • Loading branch information
3 people authored Aug 7, 2024
1 parent 82ded59 commit d70e602
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 53 deletions.
2 changes: 1 addition & 1 deletion zebra-chain/src/amount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ where

#[allow(missing_docs)]
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
/// Errors that can be returned when validating `Amount`s
/// Errors that can be returned when validating [`Amount`]s.
pub enum Error {
/// input {value} is outside of valid range for zatoshi Amount, valid_range={range:?}
Constraint {
Expand Down
2 changes: 1 addition & 1 deletion zebra-chain/src/value_balance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ impl ValueBalance<NonNegative> {
pub fn from_bytes(bytes: &[u8]) -> Result<ValueBalance<NonNegative>, ValueBalanceError> {
let bytes_length = bytes.len();

// Return an error early if bytes don't have the right lenght instead of panicking later.
// Return an error early if bytes don't have the right length instead of panicking later.
match bytes_length {
32 | 40 => {}
_ => return Err(Unparsable),
Expand Down
3 changes: 2 additions & 1 deletion zebra-consensus/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub struct SemanticBlockVerifier<S, V> {
transaction_verifier: V,
}

/// Block verification errors.
// TODO: dedupe with crate::error::BlockError
#[non_exhaustive]
#[allow(missing_docs)]
Expand Down Expand Up @@ -86,7 +87,7 @@ pub enum VerifyBlockError {
Transaction(#[from] TransactionError),

#[error("invalid block subsidy")]
Subsidy(#[from] zebra_chain::amount::Error),
Subsidy(#[from] SubsidyError),
}

impl VerifyBlockError {
Expand Down
97 changes: 50 additions & 47 deletions zebra-consensus/src/block/subsidy/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use zebra_chain::{
transaction::Transaction,
};

use crate::funding_stream_values;
use crate::{block::SubsidyError, funding_stream_values};

/// The divisor used for halvings.
///
Expand All @@ -25,9 +25,7 @@ pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
.activation_height(network)
.expect("blossom activation height should be available");

if height < network.slow_start_shift() {
None
} else if height < blossom_height {
if height < blossom_height {
let pre_blossom_height = height - network.slow_start_shift();
let halving_shift = pre_blossom_height / PRE_BLOSSOM_HALVING_INTERVAL;

Expand Down Expand Up @@ -62,7 +60,10 @@ pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
/// `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(height: Height, network: &Network) -> Result<Amount<NonNegative>, Error> {
pub fn block_subsidy(
height: Height,
network: &Network,
) -> Result<Amount<NonNegative>, SubsidyError> {
let blossom_height = Blossom
.activation_height(network)
.expect("blossom activation height should be available");
Expand All @@ -75,23 +76,20 @@ pub fn block_subsidy(height: Height, network: &Network) -> Result<Amount<NonNega
return Ok(Amount::zero());
};

// TODO: Add this as a field on `testnet::Parameters` instead of checking `disable_pow()`, this is 0 for Regtest in zcashd,
// see <https://github.com/zcash/zcash/blob/master/src/chainparams.cpp#L640>
if height < network.slow_start_interval() && !network.disable_pow() {
unreachable!(
"unsupported block height {height:?}: callers should handle blocks below {:?}",
network.slow_start_interval()
)
// Zebra doesn't need to calculate block subsidies for blocks with heights in the slow start
// interval because it handles those blocks through checkpointing.
if height < network.slow_start_interval() {
Err(SubsidyError::UnsupportedHeight)
} else if height < blossom_height {
// this calculation is exact, because the halving divisor is 1 here
Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)
Ok(Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)?)
} else {
let scaled_max_block_subsidy =
MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
// in future halvings, this calculation might not be exact
// Amount division is implemented using integer division,
// which truncates (rounds down) the result, as specified
Amount::try_from(scaled_max_block_subsidy / halving_div)
Ok(Amount::try_from(scaled_max_block_subsidy / halving_div)?)
}
}

Expand Down Expand Up @@ -307,127 +305,132 @@ mod test {
// After slow-start mining and before Blossom the block subsidy is 12.5 ZEC
// 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)
Amount::<NonNegative>::try_from(1_250_000_000)?,
block_subsidy((network.slow_start_interval() + 1).unwrap(), network)?
);
assert_eq!(
Amount::try_from(1_250_000_000),
block_subsidy((blossom_height - 1).unwrap(), network)
Amount::<NonNegative>::try_from(1_250_000_000)?,
block_subsidy((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)
Amount::<NonNegative>::try_from(625_000_000)?,
block_subsidy(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)
Amount::<NonNegative>::try_from(312_500_000)?,
block_subsidy(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),
Amount::<NonNegative>::try_from(156_250_000)?,
block_subsidy(
(first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(),
network
)
)?
);

// After the 7th halving, the block subsidy is reduced to 0.04882812 ZEC
// Check that the block subsidy rounds down correctly, and there are no errors
assert_eq!(
Amount::try_from(4_882_812),
Amount::<NonNegative>::try_from(4_882_812)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 6)).unwrap(),
network
)
)?
);

// After the 29th halving, the block subsidy is 1 zatoshi
// Check that the block subsidy is calculated correctly at the limit
assert_eq!(
Amount::try_from(1),
Amount::<NonNegative>::try_from(1)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 28)).unwrap(),
network
)
)?
);

// After the 30th halving, there is no block subsidy
// Check that there are no errors
assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 29)).unwrap(),
network
)
)?
);

assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 39)).unwrap(),
network
)
)?
);

assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 49)).unwrap(),
network
)
)?
);

assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 59)).unwrap(),
network
)
)?
);

// The largest possible integer divisor
assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 62)).unwrap(),
network
)
)?
);

// Other large divisors which should also result in zero
assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 63)).unwrap(),
network
)
)?
);

assert_eq!(
Amount::try_from(0),
Amount::<NonNegative>::try_from(0)?,
block_subsidy(
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 64)).unwrap(),
network
)
)?
);

assert_eq!(
Amount::try_from(0),
block_subsidy(Height(Height::MAX_AS_U32 / 4), network)
Amount::<NonNegative>::try_from(0)?,
block_subsidy(Height(Height::MAX_AS_U32 / 4), network)?
);

assert_eq!(
Amount::<NonNegative>::try_from(0)?,
block_subsidy(Height(Height::MAX_AS_U32 / 2), network)?
);

assert_eq!(
Amount::try_from(0),
block_subsidy(Height(Height::MAX_AS_U32 / 2), network)
Amount::<NonNegative>::try_from(0)?,
block_subsidy(Height::MAX, network)?
);
assert_eq!(Amount::try_from(0), block_subsidy(Height::MAX, network));

Ok(())
}
Expand Down
8 changes: 6 additions & 2 deletions zebra-consensus/src/checkpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use crate::{
Progress::{self, *},
TargetHeight::{self, *},
},
error::BlockError,
error::{BlockError, SubsidyError},
funding_stream_values, BoxError, ParameterCheckpoint as _,
};

Expand Down Expand Up @@ -608,6 +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)?)?
Expand Down Expand Up @@ -991,7 +993,9 @@ pub enum VerifyCheckpointError {
#[error(transparent)]
VerifyBlock(VerifyBlockError),
#[error("invalid block subsidy")]
SubsidyError(#[from] amount::Error),
SubsidyError(#[from] SubsidyError),
#[error("invalid amount")]
AmountError(#[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:?}")]
Expand Down
16 changes: 15 additions & 1 deletion zebra-consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use proptest_derive::Arbitrary;
/// Workaround for format string identifier rules.
const MAX_EXPIRY_HEIGHT: block::Height = block::Height::MAX_EXPIRY_HEIGHT;

#[derive(Error, Copy, Clone, Debug, PartialEq, Eq)]
/// Block subsidy errors.
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[allow(missing_docs)]
pub enum SubsidyError {
#[error("no coinbase transaction in block")]
Expand All @@ -36,8 +37,21 @@ pub enum SubsidyError {

#[error("a sum of amounts overflowed")]
SumOverflow,

#[error("unsupported height")]
UnsupportedHeight,

#[error("invalid amount")]
InvalidAmount(amount::Error),
}

impl From<amount::Error> 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)]
Expand Down

0 comments on commit d70e602

Please sign in to comment.