diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 801754db14c..ae3c9fec21e 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -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"))'] } diff --git a/zebra-chain/src/parameters/network/subsidy.rs b/zebra-chain/src/parameters/network/subsidy.rs index 7528058d5bd..739fda8ec3e 100644 --- a/zebra-chain/src/parameters/network/subsidy.rs +++ b/zebra-chain/src/parameters/network/subsidy.rs @@ -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(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(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] /// diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 97ffb20a9d4..80cb8419c77 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -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, + pub addresses: Option>, } 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, \ diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index c6020590ab4..c839a26c116 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -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() }); diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 2000c968f00..121a5bdcf13 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -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 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, } } } diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 0463a9c41a9..2e27bc42aa8 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -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)?; diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-consensus/src/block/subsidy/funding_streams.rs index 886ca77ff24..bdf72809f72 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams.rs @@ -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 { + 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`. diff --git a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs index da2dc225179..96d881f70f2 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs +++ b/zebra-consensus/src/block/subsidy/funding_streams/tests.rs @@ -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(()) } diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-consensus/src/block/subsidy/general.rs index 193f32cb2b3..0485ab0735c 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-consensus/src/block/subsidy/general.rs @@ -118,10 +118,44 @@ pub fn output_amounts(transaction: &Transaction) -> HashSet> .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 { + 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::::zero(), + lockbox_input_value(&network, Height::MIN) + ); + + let expected_lockbox_value: Amount = 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 = 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(()) + } } diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 7c75a02779c..f191c3a287c 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -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(); 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 899566ee9a8..00fb4d8a9d2 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 @@ -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( 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, &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(); diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs index ccb33a35f26..fc0805b533d 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/proposal.rs @@ -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 for Response { diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 20cdc72707e..079f8efc734 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -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"))'] } diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index 4cc2267049d..b262d3c9d68 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -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(()) +}