change(consensus): Add lockbox funding stream (#8694)

* 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

* 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.

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

* Updates the NU6 consensus branch id

---------

Co-authored-by: Pili Guerra <mpguerra@users.noreply.github.com>
This commit is contained in:
Arya 2024-08-01 19:22:36 -04:00 committed by GitHub
parent 45261a26eb
commit e56ee4c792
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 587 additions and 71 deletions

View File

@ -176,3 +176,7 @@ required-features = ["bench"]
[[bench]]
name = "redpallas"
harness = false
[lints.rust]
# TODO: Remove this once it's no longer needed for NU6.
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nu6"))'] }

View File

@ -62,6 +62,9 @@ pub enum FundingStreamReceiver {
/// The Major Grants (Zcash Community Grants) funding stream.
MajorGrants,
/// The deferred pool contribution.
// TODO: Add link to lockbox stream ZIP
Deferred,
}
impl FundingStreamReceiver {
@ -69,11 +72,15 @@ impl FundingStreamReceiver {
///
/// [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",
}
}
}
@ -181,7 +188,7 @@ lazy_static! {
recipients: [
(
FundingStreamReceiver::Ecc,
FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_MAINNET.iter()),
FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_MAINNET),
),
(
FundingStreamReceiver::ZcashFoundation,
@ -199,8 +206,21 @@ lazy_static! {
/// The post-NU6 funding streams for Mainnet
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
pub static ref POST_NU6_FUNDING_STREAMS_MAINNET: FundingStreams = FundingStreams {
height_range: Height(2_726_400)..Height(3_146_400),
recipients: HashMap::new()
// TODO: Adjust this height range and recipient list once a proposal is selected
height_range: POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET,
recipients: [
(
FundingStreamReceiver::Deferred,
FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
),
(
FundingStreamReceiver::MajorGrants,
// TODO: Update these addresses
FundingStreamRecipient::new(8, FUNDING_STREAM_MG_ADDRESSES_MAINNET),
),
]
.into_iter()
.collect()
};
/// The pre-NU6 funding streams for Testnet as described in [protocol specification §7.10.1][7.10.1]
@ -229,11 +249,45 @@ lazy_static! {
// TODO: Add a reference to lockbox stream ZIP, this is currently based on the number of blocks between the
// start and end heights for Mainnet in https://zips.z.cash/draft-nuttycom-funding-allocation
pub static ref POST_NU6_FUNDING_STREAMS_TESTNET: FundingStreams = FundingStreams {
height_range: Height(2_942_000)..Height(3_362_000),
recipients: HashMap::new()
// TODO: Adjust this height range and recipient list once a proposal is selected
height_range: POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET,
recipients: [
(
FundingStreamReceiver::Deferred,
FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
),
(
FundingStreamReceiver::MajorGrants,
// TODO: Update these addresses
FundingStreamRecipient::new(8, POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET),
),
]
.into_iter()
.collect()
};
}
/// The start height of post-NU6 funding streams on Mainnet
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
const POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET: u32 = 2_726_400;
/// The start height of post-NU6 funding streams on Testnet
// TODO: Add a reference to lockbox stream ZIP, this is currently based on https://zips.z.cash/draft-nuttycom-funding-allocation
const POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET: u32 = 2_942_000;
/// The number of blocks contained in the post-NU6 funding streams height ranges on Mainnet or Testnet.
const POST_NU6_FUNDING_STREAM_NUM_BLOCKS: u32 = 420_000;
/// The post-NU6 funding stream height range on Mainnet
const POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET: std::ops::Range<Height> =
Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET)
..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
/// The post-NU6 funding stream height range on Testnet
const POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET: std::ops::Range<Height> =
Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET)
..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
/// Address change interval function here as a constant
/// as described in [protocol specification §7.10.1][7.10.1].
///
@ -402,6 +456,18 @@ pub const FUNDING_STREAM_ZF_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRES
pub const FUNDING_STREAM_MG_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
["t2Gvxv2uNM7hbbACjNox4H6DjByoKZ2Fa3P"; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
/// Number of addresses for each post-NU6 funding stream in the Testnet.
/// In the spec ([protocol specification §7.10][7.10]) this is defined as: `fs.addressindex(fs.endheight - 1)`
/// however we know this value beforehand so we prefer to make it a constant instead.
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
pub const POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET: usize = 13;
/// List of addresses for the Major Grants post-NU6 funding stream in the Testnet administered by the Financial Privacy Fund (FPF).
pub const POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET: [&str;
POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
["t2HifwjUj9uyxr9bknR8LFuQbc98c3vkXtu"; POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
/// Returns the address change period
/// as described in [protocol specification §7.10][7.10]
///

View File

@ -68,7 +68,7 @@ pub struct ConfiguredFundingStreamRecipient {
/// The numerator for each funding stream receiver category, see [`FundingStreamRecipient::numerator`] for more details.
pub numerator: u64,
/// Addresses for the funding stream recipient, see [`FundingStreamRecipient::addresses`] for more details.
pub addresses: Vec<String>,
pub addresses: Option<Vec<String>>,
}
impl ConfiguredFundingStreamRecipient {
@ -76,7 +76,7 @@ impl ConfiguredFundingStreamRecipient {
pub fn into_recipient(self) -> (FundingStreamReceiver, FundingStreamRecipient) {
(
self.receiver,
FundingStreamRecipient::new(self.numerator, self.addresses),
FundingStreamRecipient::new(self.numerator, self.addresses.unwrap_or_default()),
)
}
}
@ -133,8 +133,12 @@ impl ConfiguredFundingStreams {
))
.expect("no overflow should happen in this sub") as usize;
for recipient in funding_streams.recipients().values() {
// TODO: Make an exception for the `Deferred` receiver.
for (&receiver, recipient) in funding_streams.recipients() {
if receiver == FundingStreamReceiver::Deferred {
// The `Deferred` receiver doesn't need any addresses.
continue;
}
assert!(
recipient.addresses().len() >= expected_min_num_addresses,
"recipients must have a sufficient number of addresses for height range, \

View File

@ -320,9 +320,11 @@ fn check_configured_funding_stream_constraints() {
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Ecc,
numerator: 20,
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
addresses: Some(
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
),
}]),
..Default::default()
},
@ -330,9 +332,11 @@ fn check_configured_funding_stream_constraints() {
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Ecc,
numerator: 100,
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
addresses: Some(
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
),
}]),
..Default::default()
},
@ -398,7 +402,7 @@ fn check_configured_funding_stream_constraints() {
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Ecc,
numerator: 10,
addresses: vec![],
addresses: Some(vec![]),
}]),
..Default::default()
});
@ -410,9 +414,11 @@ fn check_configured_funding_stream_constraints() {
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Ecc,
numerator: 101,
addresses: FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
addresses: Some(
FUNDING_STREAM_ECC_ADDRESSES_TESTNET
.map(Into::into)
.to_vec(),
),
}]),
..Default::default()
});
@ -424,9 +430,11 @@ fn check_configured_funding_stream_constraints() {
recipients: Some(vec![ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Ecc,
numerator: 10,
addresses: FUNDING_STREAM_ECC_ADDRESSES_MAINNET
.map(Into::into)
.to_vec(),
addresses: Some(
FUNDING_STREAM_ECC_ADDRESSES_MAINNET
.map(Into::into)
.to_vec(),
),
}]),
..Default::default()
});

View File

@ -88,7 +88,8 @@ pub(super) const MAINNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)]
(block::Height(903_000), Heartwood),
(block::Height(1_046_400), Canopy),
(block::Height(1_687_104), Nu5),
// TODO: Add NU6.
// TODO: Add NU6
// (block::Height(2_726_400), Nu6),
];
/// Fake mainnet network upgrade activation heights, used in tests.
@ -124,7 +125,8 @@ pub(super) const TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)]
(block::Height(903_800), Heartwood),
(block::Height(1_028_500), Canopy),
(block::Height(1_842_420), Nu5),
// TODO: Add NU6.
// TODO: Add NU6
// (block::Height(2_942_000), Nu6),
];
/// Fake testnet network upgrade activation heights, used in tests.
@ -214,8 +216,7 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] =
(Heartwood, ConsensusBranchId(0xf5b9230b)),
(Canopy, ConsensusBranchId(0xe9ff75a6)),
(Nu5, ConsensusBranchId(0xc2d6d0b4)),
// TODO: Use the real consensus branch ID once it's specified.
(Nu6, ConsensusBranchId(0xdeadc0de)),
(Nu6, ConsensusBranchId(0xc8e71055)),
];
/// The target block spacing before Blossom.
@ -530,6 +531,8 @@ impl From<zcash_protocol::consensus::NetworkUpgrade> for NetworkUpgrade {
zcash_protocol::consensus::NetworkUpgrade::Heartwood => Self::Heartwood,
zcash_protocol::consensus::NetworkUpgrade::Canopy => Self::Canopy,
zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5,
#[cfg(zcash_unstable = "nu6")]
zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6,
}
}
}

View File

@ -7,7 +7,7 @@ use chrono::{DateTime, Utc};
use zebra_chain::{
amount::{Amount, Error as AmountError, NonNegative},
block::{Block, Hash, Header, Height},
parameters::{Network, NetworkUpgrade},
parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade},
transaction,
work::{
difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
@ -177,7 +177,7 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr
// 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 < 4 {
} 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
@ -194,8 +194,16 @@ pub fn subsidy_is_valid(block: &Block, network: &Network) -> Result<(), BlockErr
//
// https://zips.z.cash/protocol/protocol.pdf#fundingstreams
for (receiver, expected_amount) in funding_streams {
let address =
subsidy::funding_streams::funding_stream_address(height, network, receiver);
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)
@ -237,6 +245,12 @@ pub fn miner_fees_are_valid(
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,
@ -244,9 +258,16 @@ pub fn miner_fees_are_valid(
// > 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.
//
// TODO: Update the quote from the protocol specification once its been updated to reflect the changes in
// https://zips.z.cash/draft-nuttycom-funding-allocation and https://zips.z.cash/draft-hopwood-coinbase-balance.
let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance)
.map_err(|_| SubsidyError::SumOverflow)?;
let right = (block_subsidy + block_miner_fees).map_err(|_| SubsidyError::SumOverflow)?;
let right = (block_subsidy + block_miner_fees - expected_deferred_amount)
.map_err(|_| SubsidyError::SumOverflow)?;
if left > right {
Err(SubsidyError::InvalidMinerFees)?;

View File

@ -52,8 +52,17 @@ pub fn funding_stream_values(
/// as described in [protocol specification §7.10][7.10]
///
/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
fn funding_stream_address_index(height: Height, network: &Network) -> usize {
fn funding_stream_address_index(
height: Height,
network: &Network,
receiver: FundingStreamReceiver,
) -> Option<usize> {
if receiver == FundingStreamReceiver::Deferred {
return None;
}
let funding_streams = network.funding_streams(height);
let num_addresses = funding_streams.recipient(receiver)?.addresses().len();
let index = 1u32
.checked_add(funding_stream_address_period(height, network))
@ -64,22 +73,10 @@ fn funding_stream_address_index(height: Height, network: &Network) -> usize {
))
.expect("no overflow should happen in this sub") as usize;
// Funding stream recipients may not have the same number of addresses on configured Testnets,
// the number of addresses for each recipient should be validated for a configured height range
// when configured Testnet parameters are built.
let num_addresses = funding_streams
.recipients()
.values()
.next()
// TODO: Return an Option from this function and replace `.unwrap()` with `?`
.unwrap()
.addresses()
.len();
assert!(index > 0 && index <= num_addresses);
// spec formula will output an index starting at 1 but
// Zebra indices for addresses start at zero, return converted.
index - 1
Some(index - 1)
}
/// Return the address corresponding to given height, network and funding stream receiver.
@ -90,17 +87,10 @@ pub fn funding_stream_address(
height: Height,
network: &Network,
receiver: FundingStreamReceiver,
) -> &transparent::Address {
let index = funding_stream_address_index(height, network);
) -> Option<&transparent::Address> {
let index = funding_stream_address_index(height, network, receiver)?;
let funding_streams = network.funding_streams(height);
funding_streams
.recipient(receiver)
// TODO: Change return type to option and return None here instead of panicking
.unwrap()
.addresses()
.get(index)
// TODO: Change return type to option and return None here instead of panicking
.unwrap()
funding_streams.recipient(receiver)?.addresses().get(index)
}
/// Return a human-readable name and a specification URL for the funding stream `receiver`.

View File

@ -1,7 +1,14 @@
//! Tests for funding streams.
use color_eyre::Report;
use zebra_chain::parameters::{subsidy::FundingStreamReceiver, NetworkKind};
use zebra_chain::parameters::{
subsidy::FundingStreamReceiver,
testnet::{
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
ConfiguredFundingStreams,
},
NetworkKind,
};
use super::*;
@ -45,7 +52,6 @@ fn test_funding_stream_values() -> Result<(), Report> {
);
// funding stream period is ending
// TODO: Check post-NU6 funding streams here as well.
let range = network.pre_nu6_funding_streams().height_range();
let end = range.end;
let last = end - 1;
@ -56,6 +62,57 @@ fn test_funding_stream_values() -> Result<(), Report> {
);
assert!(funding_stream_values(end, network)?.is_empty());
// 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).unwrap(), hash_map);
}
Ok(())
}

View File

@ -118,10 +118,44 @@ pub fn output_amounts(transaction: &Transaction) -> HashSet<Amount<NonNegative>>
.collect()
}
/// Lockbox funding stream total input value for a block height.
///
/// Assumes a constant funding stream amount per block.
// TODO: Cache the lockbox value balance in zebra-state (will be required for tracking lockbox
// value balance after the Zcash Sustainability Fund ZIPs or after a ZIP for spending from the deferred pool)
#[allow(dead_code)]
fn lockbox_input_value(network: &Network, height: Height) -> Amount<NonNegative> {
let Some(nu6_activation_height) = Nu6.activation_height(network) else {
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 post_nu6_funding_stream_height_range = network.post_nu6_funding_streams().height_range();
// `min(height, last_height_with_deferred_pool_contribution) - (nu6_activation_height - 1)`,
// We decrement NU6 activation height since it's an inclusive lower bound.
// Funding stream height range end bound is not incremented since it's an exclusive end bound
let num_blocks_with_lockbox_output = (height.0 + 1)
.min(post_nu6_funding_stream_height_range.end.0)
.checked_sub(post_nu6_funding_stream_height_range.start.0)
.unwrap_or_default();
(deferred_amount_per_block * num_blocks_with_lockbox_output.into())
.expect("lockbox input value should fit in Amount")
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::Report;
use zebra_chain::parameters::testnet::{
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
ConfiguredFundingStreams,
};
#[test]
fn halving_test() -> Result<(), Report> {
@ -391,4 +425,71 @@ mod test {
Ok(())
}
#[test]
fn check_lockbox_input_value() -> Result<(), Report> {
let _init_guard = zebra_test::init();
let network = testnet::Parameters::build()
.with_activation_heights(ConfiguredActivationHeights {
blossom: Some(Blossom.activation_height(&Network::Mainnet).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 nu6_height = Nu6.activation_height(&network).unwrap();
let post_nu6_funding_streams = network.post_nu6_funding_streams();
let height_range = post_nu6_funding_streams.height_range();
let last_funding_stream_height = post_nu6_funding_streams
.height_range()
.end
.previous()
.expect("the previous height should be valid");
assert_eq!(
Amount::<NonNegative>::zero(),
lockbox_input_value(&network, Height::MIN)
);
let expected_lockbox_value: Amount<NonNegative> = Amount::try_from(18_750_000)?;
assert_eq!(
expected_lockbox_value,
lockbox_input_value(&network, nu6_height)
);
let num_blocks_total = height_range.end.0 - height_range.start.0;
let expected_input_per_block: Amount<NonNegative> = Amount::try_from(18_750_000)?;
let expected_lockbox_value = (expected_input_per_block * num_blocks_total.into())?;
assert_eq!(
expected_lockbox_value,
lockbox_input_value(&network, last_funding_stream_height)
);
Ok(())
}
}

View File

@ -636,12 +636,7 @@ where
//
// Optional TODO:
// - add `async changed()` method to ChainSyncStatus (like `ChainTip`)
// TODO:
// - Add a `disable_peers` field to `Network` to check instead of `disable_pow()` (#8361)
// - Check the field in `sync_status` so it applies to the mempool as well.
if !network.disable_pow() {
check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?;
}
check_synced_to_tip(&network, latest_chain_tip.clone(), sync_status.clone())?;
// TODO: return an error if we have no peers, like `zcashd` does,
// and add a developer config that mines regardless of how many peers we have.
// https://github.com/zcash/zcash/blob/6fdd9f1b81d3b228326c9826fa10696fc516444b/src/miner.cpp#L865-L880
@ -1197,9 +1192,9 @@ where
})?;
let mut funding_streams: Vec<_> = funding_streams
.iter()
.map(|(receiver, value)| {
let address = funding_stream_address(height, &network, *receiver);
(*receiver, FundingStream::new(*receiver, *value, address))
.filter_map(|(receiver, value)| {
let address = funding_stream_address(height, &network, *receiver)?;
Some((*receiver, FundingStream::new(*receiver, *value, address)))
})
.collect();

View File

@ -154,6 +154,7 @@ where
// - State and syncer checks
/// Returns an error if Zebra is not synced to the consensus chain tip.
/// Returns early with `Ok(())` if Proof-of-Work is disabled on the provided `network`.
/// This error might be incorrect if the local clock is skewed.
pub fn check_synced_to_tip<Tip, SyncStatus>(
network: &Network,
@ -164,6 +165,13 @@ where
Tip: ChainTip + Clone + Send + Sync + 'static,
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
{
// TODO:
// - Add a `disable_peers` field to `Network` to check instead of `disable_pow()` (#8361)
// - Check the field in `sync_status` so it applies to the mempool as well.
if network.disable_pow() {
return Ok(());
}
// The tip estimate may not be the same as the one coming from the state
// but this is ok for an estimate
let (estimated_distance_to_chain_tip, local_tip_height) = latest_chain_tip
@ -376,11 +384,11 @@ pub fn standard_coinbase_outputs(
(Amount<NonNegative>, &transparent::Address),
> = funding_streams
.into_iter()
.map(|(receiver, amount)| {
(
.filter_map(|(receiver, amount)| {
Some((
receiver,
(amount, funding_stream_address(height, network, receiver)),
)
(amount, funding_stream_address(height, network, receiver)?),
))
})
.collect();

View File

@ -59,6 +59,11 @@ impl ProposalResponse {
ProposalResponse::Rejected(final_error.to_string())
}
/// Returns true if self is [`ProposalResponse::Valid`]
pub fn is_valid(&self) -> bool {
matches!(self, Self::Valid)
}
}
impl From<ProposalResponse> for Response {

View File

@ -299,4 +299,5 @@ zebra-grpc = { path = "../zebra-grpc", version = "0.1.0-alpha.5" }
zebra-utils = { path = "../zebra-utils", version = "1.0.0-beta.38" }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
# TODO: Remove 'cfg(zcash_unstable, values("nu6"))' once it's no longer needed for NU6.
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)', 'cfg(zcash_unstable, values("nu6"))'] }

View File

@ -3228,3 +3228,256 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> {
Ok(())
}
/// Test successful block template submission as a block proposal or submission on a custom Testnet.
///
/// This test can be run locally with:
/// `RUSTFLAGS='--cfg zcash_unstable="nu6"' cargo test --package zebrad --test acceptance --features getblocktemplate-rpcs -- nu6_funding_streams --exact --show-output`
#[tokio::test(flavor = "multi_thread")]
#[cfg(all(feature = "getblocktemplate-rpcs", zcash_unstable = "nu6"))]
async fn nu6_funding_streams() -> Result<()> {
use zebra_chain::{
chain_sync_status::MockSyncStatus,
parameters::{
subsidy::{FundingStreamReceiver, FUNDING_STREAM_MG_ADDRESSES_TESTNET},
testnet::{
self, ConfiguredActivationHeights, ConfiguredFundingStreamRecipient,
ConfiguredFundingStreams,
},
NetworkUpgrade,
},
serialization::ZcashSerialize,
work::difficulty::U256,
};
use zebra_network::address_book_peers::MockAddressBookPeers;
use zebra_node_services::mempool;
use zebra_rpc::methods::{
get_block_template_rpcs::{
get_block_template::{
fetch_state_tip_and_local_time, generate_coinbase_and_roots,
proposal_block_from_template, GetBlockTemplate, GetBlockTemplateRequestMode,
},
types::get_block_template,
types::submit_block,
},
hex_data::HexData,
GetBlockTemplateRpc, GetBlockTemplateRpcImpl,
};
use zebra_test::mock_service::MockService;
let _init_guard = zebra_test::init();
tracing::info!("running nu6_funding_streams_and_coinbase_balance test");
let base_network_params = testnet::Parameters::build()
// Regtest genesis hash
.with_genesis_hash("029f11d80ef9765602235e1bc9727e3eb6ba20839319f761fee920d63401e327")
.with_target_difficulty_limit(U256::from_big_endian(&[0x0f; 32]))
.with_disable_pow(true)
.with_slow_start_interval(Height::MIN)
.with_activation_heights(ConfiguredActivationHeights {
nu6: Some(1),
..Default::default()
});
let network = base_network_params
.clone()
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
// Start checking funding streams from block height 1
height_range: Some(Height(1)..Height(100)),
// Use default post-NU6 recipients
recipients: None,
})
.to_network();
tracing::info!("built configured Testnet, starting state service and block verifier");
let default_test_config = default_test_config(&network)?;
let mining_config = default_test_config.mining;
let miner_address = mining_config
.miner_address
.clone()
.expect("hard-coded config should have a miner address");
let (state, read_state, latest_chain_tip, _chain_tip_change) =
zebra_state::init_test_services(&network);
let (
block_verifier_router,
_transaction_verifier,
_parameter_download_task_handle,
_max_checkpoint_height,
) = zebra_consensus::router::init(zebra_consensus::Config::default(), &network, state.clone())
.await;
tracing::info!("started state service and block verifier, committing Regtest genesis block");
let genesis_hash = block_verifier_router
.clone()
.oneshot(zebra_consensus::Request::Commit(regtest_genesis_block()))
.await
.expect("should validate Regtest genesis block");
let mut mempool = MockService::build()
.with_max_request_delay(Duration::from_secs(5))
.for_unit_tests();
let mut mock_sync_status = MockSyncStatus::default();
mock_sync_status.set_is_close_to_tip(true);
let get_block_template_rpc_impl = GetBlockTemplateRpcImpl::new(
&network,
mining_config,
mempool.clone(),
read_state.clone(),
latest_chain_tip,
block_verifier_router,
mock_sync_status,
MockAddressBookPeers::default(),
);
let make_mock_mempool_request_handler = || async move {
mempool
.expect_request(mempool::Request::FullTransactions)
.await
.respond(mempool::Response::FullTransactions {
transactions: vec![],
// tip hash needs to match chain info for long poll requests
last_seen_tip_hash: genesis_hash,
});
};
let block_template_fut = get_block_template_rpc_impl.get_block_template(None);
let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()();
let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler);
let get_block_template::Response::TemplateMode(block_template) =
block_template.expect("unexpected error in getblocktemplate RPC call")
else {
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
};
let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?;
let hex_proposal_block = HexData(proposal_block.zcash_serialize_to_vec()?);
// Check that the block template is a valid block proposal
let get_block_template::Response::ProposalMode(block_proposal_result) =
get_block_template_rpc_impl
.get_block_template(Some(get_block_template::JsonParameters {
mode: GetBlockTemplateRequestMode::Proposal,
data: Some(hex_proposal_block),
..Default::default()
}))
.await?
else {
panic!(
"this getblocktemplate call should return the `ProposalMode` variant of the response"
)
};
assert!(
block_proposal_result.is_valid(),
"block proposal should succeed"
);
// Submit the same block
let submit_block_response = get_block_template_rpc_impl
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
.await?;
assert_eq!(
submit_block_response,
submit_block::Response::Accepted,
"valid block should be accepted"
);
// Use an invalid coinbase transaction (with an output value greater than the `block_subsidy + miner_fees - expected_lockbox_funding_stream`)
let make_configured_recipients_with_lockbox_numerator = |numerator| {
Some(vec![
ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::Deferred,
numerator,
addresses: None,
},
ConfiguredFundingStreamRecipient {
receiver: FundingStreamReceiver::MajorGrants,
numerator: 8,
addresses: Some(
FUNDING_STREAM_MG_ADDRESSES_TESTNET
.map(ToString::to_string)
.to_vec(),
),
},
])
};
// Gets the next block template
let block_template_fut = get_block_template_rpc_impl.get_block_template(None);
let mock_mempool_request_handler = make_mock_mempool_request_handler.clone()();
let (block_template, _) = tokio::join!(block_template_fut, mock_mempool_request_handler);
let get_block_template::Response::TemplateMode(block_template) =
block_template.expect("unexpected error in getblocktemplate RPC call")
else {
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
};
let valid_original_block_template = block_template.clone();
let zebra_state::GetBlockTemplateChainInfo { history_tree, .. } =
fetch_state_tip_and_local_time(read_state.clone()).await?;
let network = base_network_params
.clone()
.with_post_nu6_funding_streams(ConfiguredFundingStreams {
height_range: Some(Height(1)..Height(100)),
recipients: make_configured_recipients_with_lockbox_numerator(0),
})
.to_network();
let (coinbase_txn, default_roots) = generate_coinbase_and_roots(
&network,
Height(block_template.height),
&miner_address,
&[],
history_tree.clone(),
true,
vec![],
);
let block_template = GetBlockTemplate {
coinbase_txn,
block_commitments_hash: default_roots.block_commitments_hash,
light_client_root_hash: default_roots.block_commitments_hash,
final_sapling_root_hash: default_roots.block_commitments_hash,
default_roots,
..(*block_template)
};
let proposal_block = proposal_block_from_template(&block_template, None, NetworkUpgrade::Nu6)?;
// Submit the invalid block with an excessive coinbase output value
let submit_block_response = get_block_template_rpc_impl
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
.await?;
tracing::info!(?submit_block_response, "submitted invalid block");
assert_eq!(
submit_block_response,
submit_block::Response::ErrorResponse(submit_block::ErrorResponse::Rejected),
"invalid block with excessive coinbase output value should be rejected"
);
// Check that the original block template can be submitted successfully
let proposal_block =
proposal_block_from_template(&valid_original_block_template, None, NetworkUpgrade::Nu6)?;
let submit_block_response = get_block_template_rpc_impl
.submit_block(HexData(proposal_block.zcash_serialize_to_vec()?), None)
.await?;
assert_eq!(
submit_block_response,
submit_block::Response::Accepted,
"valid block should be accepted"
);
Ok(())
}