diff --git a/Cargo.lock b/Cargo.lock index c9e9dcbcb5e..b42e8d04d14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,6 +1343,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "equihash" +version = "0.2.0" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" +dependencies = [ + "blake2b_simd", + "byteorder", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1372,8 +1381,7 @@ dependencies = [ [[package]] name = "f4jumble" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a83e8d7fd0c526af4aad893b7c9fe41e2699ed8a776a6c74aecdeafe05afc75" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "blake2b_simd", ] @@ -5549,14 +5557,14 @@ dependencies = [ [[package]] name = "which" -version = "4.4.2" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" dependencies = [ "either", "home", - "once_cell", "rustix", + "winsafe", ] [[package]] @@ -5806,6 +5814,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wyz" version = "0.5.1" @@ -5836,8 +5850,7 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" [[package]] name = "zcash_address" version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff95eac82f71286a79c750e674550d64fb2b7aadaef7b89286b2917f645457d" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "bech32", "bs58", @@ -5849,8 +5862,7 @@ dependencies = [ [[package]] name = "zcash_client_backend" version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbeeede366fdb642710d3c59fc2090489affd075f66db53ed11bb7138d2d0258" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "base64 0.22.1", "bech32", @@ -5889,8 +5901,7 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052d8230202f0a018cd9b5d1b56b94cd25e18eccc2d8665073bcea8261ab87fc" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "byteorder", "nonempty", @@ -5899,8 +5910,7 @@ dependencies = [ [[package]] name = "zcash_history" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fde17bf53792f9c756b313730da14880257d7661b5bfc69d0571c3a7c11a76d" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "blake2b_simd", "byteorder", @@ -5910,8 +5920,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8162c94957f1e379b8e2fb30f97b95cfa93ac9c6bc02895946ca6392d1abb81" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "bech32", "blake2b_simd", @@ -5949,8 +5958,7 @@ dependencies = [ [[package]] name = "zcash_primitives" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab47d526d7fd6f88b3a2854ad81b54757a80c2aeadd1d8b06f690556af9743c" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "aes", "bip32", @@ -5958,7 +5966,7 @@ dependencies = [ "bs58", "byteorder", "document-features", - "equihash", + "equihash 0.2.0 (git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra)", "ff", "fpe", "group", @@ -5988,8 +5996,7 @@ dependencies = [ [[package]] name = "zcash_proofs" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daba607872e60d91a09248d8e1ea3d6801c819fb80d67016d9de02d81323c10d" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "bellman", "blake2b_simd", @@ -6011,8 +6018,7 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bc22b9155b2c7eb20105cd06de170d188c1bc86489b92aa3fda7b8da8d96acf" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "document-features", "memuse", @@ -6054,7 +6060,7 @@ dependencies = [ "criterion", "dirs", "ed25519-zebra", - "equihash", + "equihash 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "futures", "group", "halo2_proofs", @@ -6553,8 +6559,7 @@ dependencies = [ [[package]] name = "zip321" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3e613defb0940acef1f54774b51c7f48f2fa705613dd800870dc69f35cd2ea" +source = "git+https://github.com/ShieldedLabs/librustzcash/?branch=nsm-zebra#5fcd3034e47d425df4846192abd4d84fb3752207" dependencies = [ "base64 0.22.1", "nom", diff --git a/Cargo.toml b/Cargo.toml index c50c93ad414..bcfa96862e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,17 +22,18 @@ resolver = "2" # `cargo release` settings [workspace.dependencies] -incrementalmerkletree = { version = "0.7.0", features = ["legacy-api"] } +incrementalmerkletree = "0.7.0" orchard = "0.10.0" sapling-crypto = "0.3.0" -zcash_address = "0.6.0" -zcash_client_backend = "0.14.0" -zcash_encoding = "0.2.1" -zcash_history = "0.4.0" -zcash_keys = "0.4.0" -zcash_primitives = "0.19.0" -zcash_proofs = "0.19.0" -zcash_protocol = "0.4.0" +# TODO: Revert to a release once librustzcash is released (#8749). +zcash_address = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_client_backend = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_encoding = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_history = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_keys = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_primitives = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_proofs = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } +zcash_protocol = { git = "https://github.com/ShieldedLabs/librustzcash/", branch = "nsm-zebra" } [workspace.metadata.release] diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index b43e77f149a..b35dafb5b5b 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -17,6 +17,8 @@ categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding"] [features] default = [] +nsm = [] + # Production features that activate extra functionality # Consensus-critical conversion from JSON to Zcash types @@ -178,3 +180,6 @@ required-features = ["bench"] [[bench]] name = "redpallas" harness = false + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nsm"))'] } diff --git a/zebra-chain/src/amount.rs b/zebra-chain/src/amount.rs index f4a81c14893..8f965246e94 100644 --- a/zebra-chain/src/amount.rs +++ b/zebra-chain/src/amount.rs @@ -557,7 +557,7 @@ impl Constraint for NonNegative { /// -MAX_MONEY..=0, /// ); /// ``` -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)] pub struct NegativeOrZero; impl Constraint for NegativeOrZero { diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 5bbc774d35c..e247ac9fa6e 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -19,14 +19,15 @@ use crate::{ }; mod commitment; -mod error; mod hash; mod header; mod height; mod serialize; +pub mod error; pub mod genesis; pub mod merkle; +pub mod subsidy; #[cfg(any(test, feature = "proptest-impl"))] pub mod arbitrary; diff --git a/zebra-chain/src/block/commitment.rs b/zebra-chain/src/block/commitment.rs index 2cb09e75b22..4b0e50cf620 100644 --- a/zebra-chain/src/block/commitment.rs +++ b/zebra-chain/src/block/commitment.rs @@ -139,6 +139,10 @@ impl Commitment { (Nu5 | Nu6, _) => Ok(ChainHistoryBlockTxAuthCommitment( ChainHistoryBlockTxAuthCommitmentHash(bytes), )), + #[cfg(zcash_unstable = "nsm")] + (ZFuture, _) => Ok(ChainHistoryBlockTxAuthCommitment( + ChainHistoryBlockTxAuthCommitmentHash(bytes), + )), } } diff --git a/zebra-chain/src/block/error.rs b/zebra-chain/src/block/error.rs index 1981cfe6050..622cfcf5148 100644 --- a/zebra-chain/src/block/error.rs +++ b/zebra-chain/src/block/error.rs @@ -2,9 +2,22 @@ use thiserror::Error; +use crate::error::SubsidyError; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + +#[derive(Clone, Error, Debug, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[allow(missing_docs)] -#[derive(Error, Debug, PartialEq, Eq)] pub enum BlockError { + #[error("block has no transactions")] + NoTransactions, + #[error("transaction has wrong consensus branch id for block network upgrade")] WrongTransactionConsensusBranchId, + + #[error("block failed subsidy validation")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Subsidy(#[from] SubsidyError), } diff --git a/zebra-consensus/src/block/subsidy.rs b/zebra-chain/src/block/subsidy.rs similarity index 100% rename from zebra-consensus/src/block/subsidy.rs rename to zebra-chain/src/block/subsidy.rs diff --git a/zebra-consensus/src/block/subsidy/funding_streams.rs b/zebra-chain/src/block/subsidy/funding_streams.rs similarity index 99% rename from zebra-consensus/src/block/subsidy/funding_streams.rs rename to zebra-chain/src/block/subsidy/funding_streams.rs index f1551a224e2..e0e4e54c1cc 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams.rs +++ b/zebra-chain/src/block/subsidy/funding_streams.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; -use zebra_chain::{ +use crate::{ amount::{Amount, Error, NonNegative}, block::Height, parameters::{subsidy::*, Network, NetworkUpgrade::*}, diff --git a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs b/zebra-chain/src/block/subsidy/funding_streams/tests.rs similarity index 95% rename from zebra-consensus/src/block/subsidy/funding_streams/tests.rs rename to zebra-chain/src/block/subsidy/funding_streams/tests.rs index 91faa923c93..c7af7265635 100644 --- a/zebra-consensus/src/block/subsidy/funding_streams/tests.rs +++ b/zebra-chain/src/block/subsidy/funding_streams/tests.rs @@ -1,9 +1,11 @@ //! Tests for funding streams. use color_eyre::Report; -use zebra_chain::parameters::{subsidy::FundingStreamReceiver, NetworkKind}; -use crate::block::subsidy::general::block_subsidy; +use crate::{ + block::subsidy::general::block_subsidy_pre_nsm, + parameters::{subsidy::FundingStreamReceiver, NetworkKind}, +}; use super::*; @@ -70,7 +72,8 @@ fn test_funding_stream_values() -> Result<(), Report> { nu6_fund_height_range.end, nu6_fund_height_range.end.next().unwrap(), ] { - let fsv = funding_stream_values(height, network, block_subsidy(height, network)?).unwrap(); + let fsv = funding_stream_values(height, network, block_subsidy_pre_nsm(height, network)?) + .unwrap(); if height < canopy_activation_height { assert!(fsv.is_empty()); diff --git a/zebra-consensus/src/block/subsidy/general.rs b/zebra-chain/src/block/subsidy/general.rs similarity index 86% rename from zebra-consensus/src/block/subsidy/general.rs rename to zebra-chain/src/block/subsidy/general.rs index 56de345dd7a..9ba492ed3f8 100644 --- a/zebra-consensus/src/block/subsidy/general.rs +++ b/zebra-chain/src/block/subsidy/general.rs @@ -6,15 +6,14 @@ use std::collections::HashSet; -use zebra_chain::{ +use crate::{ amount::{Amount, Error, NonNegative}, - block::{Height, HeightDiff}, + block::{subsidy::funding_streams::funding_stream_values, Height, HeightDiff}, + error::SubsidyError, parameters::{subsidy::*, Network, NetworkUpgrade::*}, transaction::Transaction, }; -use crate::{block::SubsidyError, funding_stream_values}; - /// The divisor used for halvings. /// /// `1 << Halving(height)`, as described in [protocol specification §7.8][7.8] @@ -58,10 +57,36 @@ pub fn num_halvings(height: Height, network: &Network) -> u32 { .expect("already checked for negatives") } +#[cfg(zcash_unstable = "nsm")] +pub fn block_subsidy( + height: Height, + network: &Network, + money_reserve: Amount, +) -> Result, SubsidyError> { + let nsm_activation_height = ZFuture + .activation_height(network) + .expect("ZFuture activation height should be available"); + + if height < nsm_activation_height { + block_subsidy_pre_nsm(height, network) + } else { + let money_reserve: i64 = money_reserve.into(); + let money_reserve: i128 = money_reserve.into(); + const BLOCK_SUBSIDY_DENOMINATOR: i128 = 10_000_000_000; + const BLOCK_SUBSIDY_NUMERATOR: i128 = 4_126; + + // calculate the block subsidy (in zatoshi) using the money reserve, note the rounding up + let subsidy = (money_reserve * BLOCK_SUBSIDY_NUMERATOR + (BLOCK_SUBSIDY_DENOMINATOR - 1)) + / BLOCK_SUBSIDY_DENOMINATOR; + + Ok(subsidy.try_into().expect("subsidy should fit in Amount")) + } +} + /// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8] /// /// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies -pub fn block_subsidy( +pub fn block_subsidy_pre_nsm( height: Height, network: &Network, ) -> Result, SubsidyError> { @@ -270,32 +295,32 @@ mod test { // https://z.cash/support/faq/#what-is-slow-start-mining assert_eq!( Amount::::try_from(1_250_000_000)?, - block_subsidy((network.slow_start_interval() + 1).unwrap(), network)? + block_subsidy_pre_nsm((network.slow_start_interval() + 1).unwrap(), network)? ); assert_eq!( Amount::::try_from(1_250_000_000)?, - block_subsidy((blossom_height - 1).unwrap(), network)? + block_subsidy_pre_nsm((blossom_height - 1).unwrap(), network)? ); // After Blossom the block subsidy is reduced to 6.25 ZEC without halving // https://z.cash/upgrade/blossom/ assert_eq!( Amount::::try_from(625_000_000)?, - block_subsidy(blossom_height, network)? + block_subsidy_pre_nsm(blossom_height, network)? ); // After the 1st halving, the block subsidy is reduced to 3.125 ZEC // https://z.cash/upgrade/canopy/ assert_eq!( Amount::::try_from(312_500_000)?, - block_subsidy(first_halving_height, network)? + block_subsidy_pre_nsm(first_halving_height, network)? ); // After the 2nd halving, the block subsidy is reduced to 1.5625 ZEC // See "7.8 Calculation of Block Subsidy and Founders' Reward" assert_eq!( Amount::::try_from(156_250_000)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(), network )? @@ -305,7 +330,7 @@ mod test { // Check that the block subsidy rounds down correctly, and there are no errors assert_eq!( Amount::::try_from(4_882_812)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 6)).unwrap(), network )? @@ -315,7 +340,7 @@ mod test { // Check that the block subsidy is calculated correctly at the limit assert_eq!( Amount::::try_from(1)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 28)).unwrap(), network )? @@ -325,7 +350,7 @@ mod test { // Check that there are no errors assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 29)).unwrap(), network )? @@ -333,7 +358,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 39)).unwrap(), network )? @@ -341,7 +366,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 49)).unwrap(), network )? @@ -349,7 +374,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 59)).unwrap(), network )? @@ -358,7 +383,7 @@ mod test { // The largest possible integer divisor assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 62)).unwrap(), network )? @@ -367,7 +392,7 @@ mod test { // Other large divisors which should also result in zero assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 63)).unwrap(), network )? @@ -375,7 +400,7 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy( + block_subsidy_pre_nsm( (first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL * 64)).unwrap(), network )? @@ -383,17 +408,17 @@ mod test { assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height(Height::MAX_AS_U32 / 4), network)? + block_subsidy_pre_nsm(Height(Height::MAX_AS_U32 / 4), network)? ); assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height(Height::MAX_AS_U32 / 2), network)? + block_subsidy_pre_nsm(Height(Height::MAX_AS_U32 / 2), network)? ); assert_eq!( Amount::::try_from(0)?, - block_subsidy(Height::MAX, network)? + block_subsidy_pre_nsm(Height::MAX, network)? ); Ok(()) @@ -403,9 +428,7 @@ mod test { fn check_height_for_num_halvings() { for network in Network::iter() { for halving in 1..1000 { - let Some(height_for_halving) = - zebra_chain::parameters::subsidy::height_for_halving(halving, &network) - else { + let Some(height_for_halving) = height_for_halving(halving, &network) else { panic!("could not find height for halving {halving}"); }; diff --git a/zebra-chain/src/error.rs b/zebra-chain/src/error.rs index a3182a21feb..f346b85b43a 100644 --- a/zebra-chain/src/error.rs +++ b/zebra-chain/src/error.rs @@ -1,6 +1,11 @@ //! Errors that can occur inside any `zebra-chain` submodule. use thiserror::Error; +use crate::{amount, block::error::BlockError}; + +#[cfg(any(test, feature = "proptest-impl"))] +use proptest_derive::Arbitrary; + /// Errors related to random bytes generation. #[derive(Error, Copy, Clone, Debug, PartialEq, Eq)] pub enum RandError { @@ -51,3 +56,84 @@ pub enum AddressError { #[error("Randomness did not hash into the Jubjub group for producing a new diversifier")] DiversifierGenerationFailure, } + +#[derive(Error, Clone, Debug, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum SubsidyError { + #[error("no coinbase transaction in block")] + NoCoinbase, + + #[error("funding stream expected output not found")] + FundingStreamNotFound, + + #[error("miner fees are invalid")] + InvalidMinerFees, + + #[error("a sum of amounts overflowed")] + SumOverflow, + + #[error("unsupported height")] + UnsupportedHeight, + + #[error("invalid amount")] + InvalidAmount(amount::Error), + + #[error("invalid burn amount")] + InvalidBurnAmount, + + #[error("unexpected error occurred: {0}")] + Other(String), +} + +impl From for SubsidyError { + fn from(amount: amount::Error) -> Self { + Self::InvalidAmount(amount) + } +} + +#[derive(Error, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] +#[allow(missing_docs)] +pub enum CoinbaseTransactionError { + #[error("first transaction must be coinbase")] + Position, + + #[error("coinbase input found in non-coinbase transaction")] + AfterFirst, + + #[error("coinbase transaction MUST NOT have any JoinSplit descriptions")] + HasJoinSplit, + + #[error("coinbase transaction MUST NOT have any Spend descriptions")] + HasSpend, + + #[error("coinbase transaction MUST NOT have any Output descriptions pre-Heartwood")] + HasOutputPreHeartwood, + + #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] + HasEnableSpendsOrchard, + + #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")] + OutputsNotDecryptable, + + #[error("coinbase inputs MUST NOT exist in mempool")] + InMempool, + + #[error( + "coinbase expiry {expiry_height:?} must be the same as the block {block_height:?} \ + after NU5 activation, failing transaction: {transaction_hash:?}" + )] + ExpiryBlockHeight { + expiry_height: Option, + block_height: crate::block::Height, + transaction_hash: crate::transaction::Hash, + }, + + #[error("coinbase transaction failed subsidy validation")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Subsidy(#[from] SubsidyError), + + #[error("TODO")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Block(#[from] BlockError), +} diff --git a/zebra-chain/src/history_tree.rs b/zebra-chain/src/history_tree.rs index 91fa3a17628..32ac12c039e 100644 --- a/zebra-chain/src/history_tree.rs +++ b/zebra-chain/src/history_tree.rs @@ -112,6 +112,17 @@ impl NonEmptyHistoryTree { )?; InnerHistoryTree::OrchardOnward(tree) } + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => { + let tree = Tree::::new_from_cache( + network, + network_upgrade, + size, + &peaks, + &Default::default(), + )?; + InnerHistoryTree::OrchardOnward(tree) + } }; Ok(Self { network: network.clone(), @@ -165,6 +176,16 @@ impl NonEmptyHistoryTree { )?; (InnerHistoryTree::OrchardOnward(tree), entry) } + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => { + let (tree, entry) = Tree::::new_from_block( + network, + block, + sapling_root, + orchard_root, + )?; + (InnerHistoryTree::OrchardOnward(tree), entry) + } }; let mut peaks = BTreeMap::new(); peaks.insert(0u32, entry); diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index 78f7a69a302..cf42515420f 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -206,6 +206,9 @@ pub struct ConfiguredActivationHeights { /// Activation height for `NU6` network upgrade. #[serde(rename = "NU6")] pub nu6: Option, + #[cfg(zcash_unstable = "nsm")] + #[serde(rename = "ZFuture")] + pub zfuture: Option, } /// Builder for the [`Parameters`] struct. @@ -336,6 +339,8 @@ impl ParametersBuilder { canopy, nu5, nu6, + #[cfg(zcash_unstable = "nsm")] + zfuture, }: ConfiguredActivationHeights, ) -> Self { use NetworkUpgrade::*; @@ -348,7 +353,7 @@ impl ParametersBuilder { // // These must be in order so that later network upgrades overwrite prior ones // if multiple network upgrades are configured with the same activation height. - let activation_heights: BTreeMap<_, _> = before_overwinter + let activation_heights = before_overwinter .into_iter() .map(|h| (h, BeforeOverwinter)) .chain(overwinter.into_iter().map(|h| (h, Overwinter))) @@ -357,7 +362,13 @@ impl ParametersBuilder { .chain(heartwood.into_iter().map(|h| (h, Heartwood))) .chain(canopy.into_iter().map(|h| (h, Canopy))) .chain(nu5.into_iter().map(|h| (h, Nu5))) - .chain(nu6.into_iter().map(|h| (h, Nu6))) + .chain(nu6.into_iter().map(|h| (h, Nu6))); + + #[cfg(zcash_unstable = "nsm")] + let activation_heights = + activation_heights.chain(zfuture.into_iter().map(|h| (h, ZFuture))); + + let activation_heights: BTreeMap<_, _> = activation_heights .map(|(h, nu)| (h.try_into().expect("activation height must be valid"), nu)) .collect(); @@ -604,6 +615,8 @@ impl Parameters { canopy: Some(1), nu5: nu5_activation_height, nu6: nu6_activation_height, + #[cfg(zcash_unstable = "nsm")] + zfuture: nu5_activation_height.map(|height| height + 1), ..Default::default() }) .with_halving_interval(PRE_BLOSSOM_REGTEST_HALVING_INTERVAL); diff --git a/zebra-chain/src/parameters/network/tests/vectors.rs b/zebra-chain/src/parameters/network/tests/vectors.rs index 4282c86844f..0874cf6900f 100644 --- a/zebra-chain/src/parameters/network/tests/vectors.rs +++ b/zebra-chain/src/parameters/network/tests/vectors.rs @@ -1,7 +1,7 @@ //! Fixed test vectors for the network consensus parameters. +use zcash_primitives::consensus::NetworkConstants as _; use zcash_primitives::consensus::{self as zp_consensus, Parameters}; -use zcash_protocol::consensus::NetworkConstants as _; use crate::{ block::Height, @@ -31,6 +31,8 @@ fn check_parameters_impl() { zp_consensus::NetworkUpgrade::Heartwood, zp_consensus::NetworkUpgrade::Canopy, zp_consensus::NetworkUpgrade::Nu5, + #[cfg(zcash_unstable = "nsm")] + zp_consensus::NetworkUpgrade::ZFuture, ]; for (network, zp_network) in [ @@ -109,7 +111,10 @@ fn activates_network_upgrades_correctly() { let expected_activation_height = 1; let network = testnet::Parameters::build() .with_activation_heights(ConfiguredActivationHeights { + #[cfg(not(zcash_unstable = "nsm"))] nu6: Some(expected_activation_height), + #[cfg(zcash_unstable = "nsm")] + zfuture: Some(expected_activation_height), ..Default::default() }) .to_network(); @@ -141,6 +146,8 @@ fn activates_network_upgrades_correctly() { (Height(1), NetworkUpgrade::Canopy), // TODO: Remove this once the testnet parameters are being serialized (#8920). (Height(100), NetworkUpgrade::Nu5), + #[cfg(zcash_unstable = "nsm")] + (Height(101), NetworkUpgrade::ZFuture), ]; for (network, expected_activation_heights) in [ diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 57165d0c760..15c6b8b4ca6 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -15,6 +15,7 @@ use hex::{FromHex, ToHex}; use proptest_derive::Arbitrary; /// A list of network upgrades in the order that they must be activated. +#[cfg(not(zcash_unstable = "nsm"))] pub const NETWORK_UPGRADES_IN_ORDER: [NetworkUpgrade; 9] = [ Genesis, BeforeOverwinter, @@ -27,6 +28,21 @@ pub const NETWORK_UPGRADES_IN_ORDER: [NetworkUpgrade; 9] = [ Nu6, ]; +#[cfg(zcash_unstable = "nsm")] +pub const NETWORK_UPGRADES_IN_ORDER: [NetworkUpgrade; 10] = [ + Genesis, + BeforeOverwinter, + Overwinter, + Sapling, + Blossom, + Heartwood, + Canopy, + Nu5, + Nu6, + #[cfg(zcash_unstable = "nsm")] + ZFuture, +]; + /// A Zcash network upgrade. /// /// Network upgrades change the Zcash network protocol or consensus rules. Note that they have no @@ -61,6 +77,10 @@ pub enum NetworkUpgrade { /// The Zcash protocol after the NU6 upgrade. #[serde(rename = "NU6")] Nu6, + #[cfg(zcash_unstable = "nsm")] + #[serde(rename = "ZFuture")] + #[allow(non_snake_case)] + ZFuture, } impl fmt::Display for NetworkUpgrade { @@ -89,7 +109,10 @@ 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: Update NU6 activation height once it's been specified. (block::Height(2_726_400), Nu6), + #[cfg(zcash_unstable = "nsm")] + (block::Height(3_000_000), ZFuture), ]; /// Fake mainnet network upgrade activation heights, used in tests. @@ -104,6 +127,8 @@ const FAKE_MAINNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] = &[ (block::Height(30), Canopy), (block::Height(35), Nu5), (block::Height(40), Nu6), + #[cfg(zcash_unstable = "nsm")] + (block::Height(45), ZFuture), ]; /// Testnet network upgrade activation heights. @@ -126,6 +151,8 @@ pub(super) const TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] (block::Height(1_028_500), Canopy), (block::Height(1_842_420), Nu5), (block::Height(2_976_000), Nu6), + #[cfg(zcash_unstable = "nsm")] + (block::Height(3_000_000), ZFuture), ]; /// Fake testnet network upgrade activation heights, used in tests. @@ -140,6 +167,8 @@ const FAKE_TESTNET_ACTIVATION_HEIGHTS: &[(block::Height, NetworkUpgrade)] = &[ (block::Height(30), Canopy), (block::Height(35), Nu5), (block::Height(40), Nu6), + #[cfg(zcash_unstable = "nsm")] + (block::Height(45), ZFuture), ]; /// The Consensus Branch Id, used to bind transactions and blocks to a @@ -216,6 +245,8 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] = (Canopy, ConsensusBranchId(0xe9ff75a6)), (Nu5, ConsensusBranchId(0xc2d6d0b4)), (Nu6, ConsensusBranchId(0xc8e71055)), + #[cfg(zcash_unstable = "nsm")] + (ZFuture, ConsensusBranchId(0xffff_ffff)), ]; /// The target block spacing before Blossom. @@ -332,7 +363,12 @@ impl NetworkUpgrade { Heartwood => Some(Canopy), Canopy => Some(Nu5), Nu5 => Some(Nu6), + #[cfg(not(zcash_unstable = "nsm"))] Nu6 => None, + #[cfg(zcash_unstable = "nsm")] + Nu6 => Some(ZFuture), + #[cfg(zcash_unstable = "nsm")] + ZFuture => None, } } @@ -410,6 +446,8 @@ impl NetworkUpgrade { let spacing_seconds = match self { Genesis | BeforeOverwinter | Overwinter | Sapling => PRE_BLOSSOM_POW_TARGET_SPACING, Blossom | Heartwood | Canopy | Nu5 | Nu6 => POST_BLOSSOM_POW_TARGET_SPACING.into(), + #[cfg(zcash_unstable = "nsm")] + ZFuture => POST_BLOSSOM_POW_TARGET_SPACING.into(), }; Duration::seconds(spacing_seconds) @@ -531,6 +569,8 @@ impl From for NetworkUpgrade { zcash_protocol::consensus::NetworkUpgrade::Canopy => Self::Canopy, zcash_protocol::consensus::NetworkUpgrade::Nu5 => Self::Nu5, zcash_protocol::consensus::NetworkUpgrade::Nu6 => Self::Nu6, + #[cfg(zcash_unstable = "nsm")] + zcash_protocol::consensus::NetworkUpgrade::ZFuture => Self::ZFuture, } } } diff --git a/zebra-chain/src/parameters/transaction.rs b/zebra-chain/src/parameters/transaction.rs index bab59e794db..121d38933bd 100644 --- a/zebra-chain/src/parameters/transaction.rs +++ b/zebra-chain/src/parameters/transaction.rs @@ -11,3 +11,8 @@ pub const SAPLING_VERSION_GROUP_ID: u32 = 0x892F_2085; /// Orchard transactions must use transaction version 5 and this version /// group ID. Sapling transactions can use v4 or v5 transactions. pub const TX_V5_VERSION_GROUP_ID: u32 = 0x26A7_270A; + +/// The version group ID for version ZFUTURE transactions. +#[cfg(zcash_unstable = "nsm")] +pub const TX_ZFUTURE_VERSION_GROUP_ID: u32 = + zcash_primitives::transaction::ZFUTURE_VERSION_GROUP_ID; diff --git a/zebra-chain/src/primitives/zcash_history.rs b/zebra-chain/src/primitives/zcash_history.rs index e8ca97d63f8..c8c9bac8bc3 100644 --- a/zebra-chain/src/primitives/zcash_history.rs +++ b/zebra-chain/src/primitives/zcash_history.rs @@ -290,6 +290,21 @@ impl Version for zcash_history::V1 { end_height: height.0 as u64, sapling_tx: sapling_tx_count, }, + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => zcash_history::NodeData { + consensus_branch_id: branch_id.into(), + subtree_commitment: block_hash, + start_time: time, + end_time: time, + start_target: target, + end_target: target, + start_sapling_root: sapling_root, + end_sapling_root: sapling_root, + subtree_total_work: work, + start_height: height.0 as u64, + end_height: height.0 as u64, + sapling_tx: sapling_tx_count, + }, } } } diff --git a/zebra-chain/src/primitives/zcash_primitives.rs b/zebra-chain/src/primitives/zcash_primitives.rs index 7ab2f32d751..7a8ed9afcb6 100644 --- a/zebra-chain/src/primitives/zcash_primitives.rs +++ b/zebra-chain/src/primitives/zcash_primitives.rs @@ -165,6 +165,10 @@ impl TryFrom<&Transaction> for zp_tx::Transaction { Transaction::V5 { network_upgrade, .. } => network_upgrade, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + network_upgrade, .. + } => network_upgrade, Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 3df3edc8d53..27822189a28 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -37,6 +37,8 @@ pub use sighash::{HashType, SigHash, SigHasher}; pub use unmined::{ zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD, }; +#[cfg(zcash_unstable = "nsm")] +use zcash_primitives::transaction::ZFUTURE_TX_VERSION; use crate::{ amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative}, @@ -142,6 +144,30 @@ pub enum Transaction { /// The orchard data for this transaction, if any. orchard_shielded_data: Option, }, + /// A future version of transaction + #[cfg(zcash_unstable = "nsm")] + #[allow(non_snake_case)] + ZFuture { + /// The Network Upgrade for this transaction. + /// + /// Derived from the ConsensusBranchId field. + network_upgrade: NetworkUpgrade, + /// The earliest time or block height that this transaction can be added to the + /// chain. + lock_time: LockTime, + /// The latest block height that this transaction can be added to the chain. + expiry_height: block::Height, + /// The transparent inputs to the transaction. + inputs: Vec, + /// The transparent outputs from the transaction. + outputs: Vec, + /// The sapling shielded data for this transaction, if any. + sapling_shielded_data: Option>, + /// The orchard data for this transaction, if any. + orchard_shielded_data: Option, + /// The burn amount for this transaction, if any. + burn_amount: Amount, + }, } impl fmt::Display for Transaction { @@ -168,6 +194,8 @@ impl fmt::Display for Transaction { fmter.field("sapling_spends", &self.sapling_spends_per_anchor().count()); fmter.field("sapling_outputs", &self.sapling_outputs().count()); fmter.field("orchard_actions", &self.orchard_actions().count()); + #[cfg(zcash_unstable = "nsm")] + fmter.field("burn_amount", &self.burn_amount()); fmter.field("unmined_id", &self.unmined_id()); @@ -248,6 +276,8 @@ impl Transaction { | Transaction::V3 { .. } | Transaction::V4 { .. } => None, Transaction::V5 { .. } => Some(AuthDigest::from(self)), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Some(AuthDigest::from(self)), } } @@ -276,6 +306,11 @@ impl Transaction { !self.outputs().is_empty() || self.has_shielded_outputs() } + #[cfg(zcash_unstable = "nsm")] + pub fn has_burn_amount(&self) -> bool { + self.burn_amount() > Amount::::zero() + } + /// Does this transaction have shielded outputs? /// /// See [`Self::has_transparent_or_shielded_outputs`] for details. @@ -321,6 +356,8 @@ impl Transaction { match self { Transaction::V1 { .. } | Transaction::V2 { .. } => false, Transaction::V3 { .. } | Transaction::V4 { .. } | Transaction::V5 { .. } => true, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => true, } } @@ -332,6 +369,8 @@ impl Transaction { Transaction::V3 { .. } => 3, Transaction::V4 { .. } => 4, Transaction::V5 { .. } => 5, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => ZFUTURE_TX_VERSION, } } @@ -343,6 +382,8 @@ impl Transaction { | Transaction::V3 { lock_time, .. } | Transaction::V4 { lock_time, .. } | Transaction::V5 { lock_time, .. } => *lock_time, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { lock_time, .. } => *lock_time, }; // `zcashd` checks that the block height is greater than the lock height. @@ -390,6 +431,8 @@ impl Transaction { | Transaction::V3 { lock_time, .. } | Transaction::V4 { lock_time, .. } | Transaction::V5 { lock_time, .. } => *lock_time, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { lock_time, .. } => *lock_time, }; let mut lock_time_bytes = Vec::new(); lock_time @@ -426,6 +469,14 @@ impl Transaction { block::Height(0) => None, block::Height(expiry_height) => Some(block::Height(*expiry_height)), }, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { expiry_height, .. } => match expiry_height { + // Consensus rule: + // > No limit: To set no limit on transactions (so that they do not expire), nExpiryHeight should be set to 0. + // https://zips.z.cash/zip-0203#specification + block::Height(0) => None, + block::Height(expiry_height) => Some(block::Height(*expiry_height)), + }, } } @@ -452,6 +503,11 @@ impl Transaction { ref mut expiry_height, .. } => expiry_height, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + ref mut expiry_height, + .. + } => expiry_height, } } @@ -468,6 +524,10 @@ impl Transaction { Transaction::V5 { network_upgrade, .. } => Some(*network_upgrade), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + network_upgrade, .. + } => Some(*network_upgrade), } } @@ -481,6 +541,8 @@ impl Transaction { Transaction::V3 { ref inputs, .. } => inputs, Transaction::V4 { ref inputs, .. } => inputs, Transaction::V5 { ref inputs, .. } => inputs, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { ref inputs, .. } => inputs, } } @@ -493,6 +555,8 @@ impl Transaction { Transaction::V3 { ref mut inputs, .. } => inputs, Transaction::V4 { ref mut inputs, .. } => inputs, Transaction::V5 { ref mut inputs, .. } => inputs, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { ref mut inputs, .. } => inputs, } } @@ -511,6 +575,8 @@ impl Transaction { Transaction::V3 { ref outputs, .. } => outputs, Transaction::V4 { ref outputs, .. } => outputs, Transaction::V5 { ref outputs, .. } => outputs, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { ref outputs, .. } => outputs, } } @@ -533,6 +599,10 @@ impl Transaction { Transaction::V5 { ref mut outputs, .. } => outputs, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + ref mut outputs, .. + } => outputs, } } @@ -581,6 +651,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -616,6 +688,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => 0, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => 0, } } @@ -655,6 +729,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -691,6 +767,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => None, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => None, } } @@ -699,6 +777,8 @@ impl Transaction { match self { // No JoinSplits Transaction::V1 { .. } | Transaction::V5 { .. } => false, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => false, // JoinSplits-on-BCTV14 Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => { @@ -746,6 +826,8 @@ impl Transaction { } | Transaction::V1 { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -766,6 +848,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.anchors()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.anchors()), // No Spends Transaction::V1 { .. } @@ -779,6 +866,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -804,6 +896,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.spends_per_anchor()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.spends_per_anchor()), // No Spends Transaction::V1 { .. } @@ -817,6 +914,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -832,6 +934,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.outputs()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.outputs()), // No Outputs Transaction::V1 { .. } @@ -845,6 +952,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -862,6 +974,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.nullifiers()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.nullifiers()), // No Spends Transaction::V1 { .. } @@ -875,6 +992,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -892,6 +1014,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.note_commitments()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.note_commitments()), // No Spends Transaction::V1 { .. } @@ -905,6 +1032,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -920,6 +1052,11 @@ impl Transaction { sapling_shielded_data, .. } => sapling_shielded_data.is_some(), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data, + .. + } => sapling_shielded_data.is_some(), } } @@ -934,6 +1071,11 @@ impl Transaction { orchard_shielded_data, .. } => orchard_shielded_data.as_ref(), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref(), // No Orchard shielded data Transaction::V1 { .. } @@ -952,6 +1094,11 @@ impl Transaction { orchard_shielded_data: Some(orchard_shielded_data), .. } => Some(orchard_shielded_data), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(orchard_shielded_data), Transaction::V1 { .. } | Transaction::V2 { .. } @@ -961,6 +1108,11 @@ impl Transaction { orchard_shielded_data: None, .. } => None, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + orchard_shielded_data: None, + .. + } => None, } } @@ -1087,6 +1239,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -1136,6 +1290,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -1183,6 +1339,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -1232,6 +1390,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(std::iter::empty()), } } @@ -1273,6 +1433,8 @@ impl Transaction { .. } | Transaction::V5 { .. } => Box::new(iter::empty()), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => Box::new(iter::empty()), }; joinsplit_value_balances.map(ValueBalance::from_sprout_amount) @@ -1314,6 +1476,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => sapling_shielded_data.value_balance, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => sapling_shielded_data.value_balance, Transaction::V1 { .. } | Transaction::V2 { .. } @@ -1326,6 +1493,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Amount::zero(), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => Amount::zero(), }; ValueBalance::from_sapling_amount(sapling_value_balance) @@ -1346,6 +1518,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Some(&mut sapling_shielded_data.value_balance), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Some(&mut sapling_shielded_data.value_balance), Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } @@ -1357,6 +1534,11 @@ impl Transaction { sapling_shielded_data: None, .. } => None, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data: None, + .. + } => None, } } @@ -1391,6 +1573,10 @@ impl Transaction { } /// Returns the value balances for this transaction using the provided transparent outputs. + /// Get the value balances for this transaction, + /// using the transparent outputs spent in this transaction. + /// + /// See `value_balance` for details. pub(crate) fn value_balance_from_outputs( &self, outputs: &HashMap, @@ -1427,4 +1613,17 @@ impl Transaction { ) -> Result, ValueBalanceError> { self.value_balance_from_outputs(&outputs_from_utxos(utxos.clone())) } + + /// Access the transparent inputs of this transaction, regardless of version. + #[cfg(zcash_unstable = "nsm")] + pub fn burn_amount(&self) -> Amount { + match self { + Transaction::V1 { .. } + | Transaction::V2 { .. } + | Transaction::V3 { .. } + | Transaction::V4 { .. } + | Transaction::V5 { .. } => Amount::zero(), + Transaction::ZFuture { burn_amount, .. } => *burn_amount, + } + } } diff --git a/zebra-chain/src/transaction/arbitrary.rs b/zebra-chain/src/transaction/arbitrary.rs index cf4aa7a9552..aa6d4ddb2a8 100644 --- a/zebra-chain/src/transaction/arbitrary.rs +++ b/zebra-chain/src/transaction/arbitrary.rs @@ -783,6 +783,12 @@ impl Arbitrary for Transaction { Self::v5_strategy(ledger_state) ] .boxed(), + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => prop_oneof![ + Self::v4_strategy(ledger_state.clone()), + Self::v5_strategy(ledger_state) + ] + .boxed(), } } @@ -918,6 +924,23 @@ pub fn transaction_to_fake_v5( orchard_shielded_data: None, }, v5 @ V5 { .. } => v5.clone(), + #[cfg(zcash_unstable = "nsm")] + ZFuture { + inputs, + outputs, + lock_time, + sapling_shielded_data, + orchard_shielded_data, + .. + } => V5 { + network_upgrade: block_nu, + inputs: inputs.to_vec(), + outputs: outputs.to_vec(), + lock_time: *lock_time, + expiry_height: height, + sapling_shielded_data: sapling_shielded_data.clone(), + orchard_shielded_data: orchard_shielded_data.clone(), + }, } } diff --git a/zebra-chain/src/transaction/builder.rs b/zebra-chain/src/transaction/builder.rs index 37397353aab..f4397d51465 100644 --- a/zebra-chain/src/transaction/builder.rs +++ b/zebra-chain/src/transaction/builder.rs @@ -9,6 +9,120 @@ use crate::{ }; impl Transaction { + /// Returns a new version zfuture coinbase transaction for `network` and `height`, + /// which contains the specified `outputs`. + #[cfg(zcash_unstable = "nsm")] + pub fn new_zfuture_coinbase( + network: &Network, + height: Height, + outputs: impl IntoIterator, transparent::Script)>, + extra_coinbase_data: Vec, + like_zcashd: bool, + miner_fee: Amount, + burn_amount: Option>, + ) -> Transaction { + let mut extra_data = None; + let mut sequence = None; + + // `zcashd` includes an extra byte after the coinbase height in the coinbase data, + // and a sequence number of u32::MAX. + if like_zcashd { + extra_data = Some(vec![0x00]); + sequence = Some(u32::MAX); + } + + // Override like_zcashd if extra_coinbase_data was supplied + if !extra_coinbase_data.is_empty() { + extra_data = Some(extra_coinbase_data); + } + + // # Consensus + // + // These consensus rules apply to v5 coinbase transactions after NU5 activation: + // + // > If effectiveVersion ≥ 5 then this condition MUST hold: + // > tx_in_count > 0 or nSpendsSapling > 0 or + // > (nActionsOrchard > 0 and enableSpendsOrchard = 1). + // + // > A coinbase transaction for a block at block height greater than 0 MUST have + // > a script that, as its first item, encodes the block height height as follows. ... + // > let heightBytes be the signed little-endian representation of height, + // > using the minimum nonzero number of bytes such that the most significant byte + // > is < 0x80. The length of heightBytes MUST be in the range {1 .. 5}. + // > Then the encoding is the length of heightBytes encoded as one byte, + // > followed by heightBytes itself. This matches the encoding used by Bitcoin + // > in the implementation of [BIP-34] + // > (but the description here is to be considered normative). + // + // > A coinbase transaction script MUST have length in {2 .. 100} bytes. + // + // Zebra adds extra coinbase data if configured to do so. + // + // Since we're not using a lock time, any sequence number is valid here. + // See `Transaction::lock_time()` for the relevant consensus rules. + // + // + let inputs = vec![transparent::Input::new_coinbase( + height, extra_data, sequence, + )]; + + // > The block subsidy is composed of a miner subsidy and a series of funding streams. + // + // + // + // > The total value in zatoshi of transparent outputs from a coinbase transaction, + // > minus vbalanceSapling, minus vbalanceOrchard, MUST NOT be greater than + // > the value in zatoshi of block subsidy plus the transaction fees + // > paid by transactions in this block. + // + // > If effectiveVersion ≥ 5 then this condition MUST hold: + // > tx_out_count > 0 or nOutputsSapling > 0 or + // > (nActionsOrchard > 0 and enableOutputsOrchard = 1). + // + // + let outputs: Vec<_> = outputs + .into_iter() + .map(|(amount, lock_script)| transparent::Output::new_coinbase(amount, lock_script)) + .collect(); + assert!( + !outputs.is_empty(), + "invalid coinbase transaction: must have at least one output" + ); + + Transaction::ZFuture { + // > The transaction version number MUST be 4 or 5. ... + // > If the transaction version number is 5 then the version group ID + // > MUST be 0x26A7270A. + // > If effectiveVersion ≥ 5, the nConsensusBranchId field MUST match the consensus + // > branch ID used for SIGHASH transaction hashes, as specified in [ZIP-244]. + network_upgrade: NetworkUpgrade::current(network, height), + + // There is no documented consensus rule for the lock time field in coinbase + // transactions, so we just leave it unlocked. (We could also set it to `height`.) + lock_time: LockTime::unlocked(), + + // > The nExpiryHeight field of a coinbase transaction MUST be equal to its + // > block height. + expiry_height: height, + + inputs, + outputs, + + // Zebra does not support shielded coinbase yet. + // + // > In a version 5 coinbase transaction, the enableSpendsOrchard flag MUST be 0. + // > In a version 5 transaction, the reserved bits 2 .. 7 of the flagsOrchard field + // > MUST be zero. + // + // See the Zcash spec for additional shielded coinbase consensus rules. + sapling_shielded_data: None, + orchard_shielded_data: None, + + // > The NSM burn_amount field [ZIP-233] must be set at minimum to 60% of miner fees [ZIP-235]. + burn_amount: burn_amount.unwrap_or_else(|| ((miner_fee * 6).unwrap() / 10).unwrap()), + } + } + /// Returns a new version 5 coinbase transaction for `network` and `height`, /// which contains the specified `outputs`. pub fn new_v5_coinbase( diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index 0e583efc5bf..29e7fac5faa 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -8,6 +8,9 @@ use halo2::pasta::group::ff::PrimeField; use hex::FromHex; use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; +#[cfg(zcash_unstable = "nsm")] +use crate::parameters::TX_ZFUTURE_VERSION_GROUP_ID; + use crate::{ amount, block::MAX_BLOCK_BYTES, @@ -672,6 +675,54 @@ impl ZcashSerialize for Transaction { // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. orchard_shielded_data.zcash_serialize(&mut writer)?; } + + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + burn_amount, + } => { + // Denoted as `nVersionGroupId` in the spec. + writer.write_u32::(TX_ZFUTURE_VERSION_GROUP_ID)?; + + // Denoted as `nConsensusBranchId` in the spec. + writer.write_u32::(u32::from( + network_upgrade + .branch_id() + .expect("valid transactions must have a network upgrade with a branch id"), + ))?; + + // Denoted as `lock_time` in the spec. + lock_time.zcash_serialize(&mut writer)?; + + // Denoted as `nExpiryHeight` in the spec. + writer.write_u32::(expiry_height.0)?; + + // Denoted as `tx_in_count` and `tx_in` in the spec. + inputs.zcash_serialize(&mut writer)?; + + // Denoted as `tx_out_count` and `tx_out` in the spec. + outputs.zcash_serialize(&mut writer)?; + + // A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`, + // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, + // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and + // `bindingSigSapling`. + sapling_shielded_data.zcash_serialize(&mut writer)?; + + // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, + // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, + // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. + orchard_shielded_data.zcash_serialize(&mut writer)?; + + // Denoted as `burn_amount` in the spec. + burn_amount.zcash_serialize(&mut writer)?; + } } Ok(()) } @@ -928,6 +979,62 @@ impl ZcashDeserialize for Transaction { orchard_shielded_data, }) } + #[cfg(zcash_unstable = "nsm")] + (ZFUTURE_TX_VERSION, true) => { + // Denoted as `nVersionGroupId` in the spec. + let id = limited_reader.read_u32::()?; + if id != TX_ZFUTURE_VERSION_GROUP_ID { + return Err(SerializationError::Parse( + "expected TX_ZFUTURE_VERSION_GROUP_ID", + )); + } + // Denoted as `nConsensusBranchId` in the spec. + // Convert it to a NetworkUpgrade + let network_upgrade = + NetworkUpgrade::from_branch_id(limited_reader.read_u32::()?) + .ok_or_else(|| { + SerializationError::Parse( + "expected a valid network upgrade from the consensus branch id", + ) + })?; + + // Denoted as `lock_time` in the spec. + let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?; + + // Denoted as `nExpiryHeight` in the spec. + let expiry_height = block::Height(limited_reader.read_u32::()?); + + // Denoted as `tx_in_count` and `tx_in` in the spec. + let inputs = Vec::zcash_deserialize(&mut limited_reader)?; + + // Denoted as `tx_out_count` and `tx_out` in the spec. + let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + + // A bundle of fields denoted in the spec as `nSpendsSapling`, `vSpendsSapling`, + // `nOutputsSapling`,`vOutputsSapling`, `valueBalanceSapling`, `anchorSapling`, + // `vSpendProofsSapling`, `vSpendAuthSigsSapling`, `vOutputProofsSapling` and + // `bindingSigSapling`. + let sapling_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + + // A bundle of fields denoted in the spec as `nActionsOrchard`, `vActionsOrchard`, + // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, + // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. + let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + + // Denoted as `burn_amount` in the spec. + let burn_amount = (&mut limited_reader).zcash_deserialize_into()?; + + Ok(Transaction::ZFuture { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + burn_amount, + }) + } (_, _) => Err(SerializationError::Parse("bad tx header")), } } diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 66d5009ed05..7cc4987d3b4 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -1001,6 +1001,27 @@ fn binding_signatures_for_network(network: Network) { ) .expect("a valid redjubjub::VerificationKey"); + bvk.verify( + shielded_sighash.as_ref(), + &sapling_shielded_data.binding_sig, + ) + .expect("must pass verification"); + } + } + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data, + .. + } => { + if let Some(sapling_shielded_data) = sapling_shielded_data { + let shielded_sighash = + tx.sighash(upgrade.branch_id().unwrap(), HashType::ALL, &[], None); + + let bvk = redjubjub::VerificationKey::try_from( + sapling_shielded_data.binding_verification_key(), + ) + .expect("a valid redjubjub::VerificationKey"); + bvk.verify( shielded_sighash.as_ref(), &sapling_shielded_data.binding_sig, diff --git a/zebra-chain/src/transaction/txid.rs b/zebra-chain/src/transaction/txid.rs index f67f6dee58d..051b54e6dc7 100644 --- a/zebra-chain/src/transaction/txid.rs +++ b/zebra-chain/src/transaction/txid.rs @@ -29,6 +29,8 @@ impl<'a> TxIdBuilder<'a> { | Transaction::V3 { .. } | Transaction::V4 { .. } => self.txid_v1_to_v4(), Transaction::V5 { .. } => self.txid_v5(), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => self.txid_zfuture(), } } @@ -52,4 +54,13 @@ impl<'a> TxIdBuilder<'a> { let alt_tx: zcash_primitives::transaction::Transaction = self.trans.try_into()?; Ok(Hash(*alt_tx.txid().as_ref())) } + + #[cfg(zcash_unstable = "nsm")] + /// Compute the Transaction ID for a ZFuture transaction in the given network upgrade. + fn txid_zfuture(self) -> Result { + // The v5 txid (from ZIP-244) is computed using librustzcash. Convert the zebra + // transaction to a librustzcash transaction. + let alt_tx: zcash_primitives::transaction::Transaction = self.trans.try_into()?; + Ok(Hash(*alt_tx.txid().as_ref())) + } } diff --git a/zebra-chain/src/transaction/unmined.rs b/zebra-chain/src/transaction/unmined.rs index da716573e8b..db676dd8f7f 100644 --- a/zebra-chain/src/transaction/unmined.rs +++ b/zebra-chain/src/transaction/unmined.rs @@ -124,7 +124,7 @@ impl fmt::Display for UnminedTxId { .debug_tuple("transaction::Hash") .field(&"private") .finish(), - Witnessed(_id) => f.debug_tuple("WtxId").field(&"private").finish(), + Witnessed(id) => f.debug_tuple("WtxId").field(id).finish(), } } } @@ -141,6 +141,8 @@ impl From<&Transaction> for UnminedTxId { match transaction { V1 { .. } | V2 { .. } | V3 { .. } | V4 { .. } => Legacy(transaction.into()), V5 { .. } => Witnessed(transaction.into()), + #[cfg(zcash_unstable = "nsm")] + ZFuture { .. } => Witnessed(transaction.into()), } } } diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index b2b33e878a7..ece16daf9c9 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -8,7 +8,9 @@ use core::fmt; use std::{borrow::Borrow, collections::HashMap}; #[cfg(any(test, feature = "proptest-impl"))] -use crate::{amount::MAX_MONEY, transaction::Transaction, transparent}; +use crate::{transaction::Transaction, transparent}; + +use crate::amount::MAX_MONEY; #[cfg(any(test, feature = "proptest-impl"))] mod arbitrary; @@ -395,6 +397,15 @@ impl ValueBalance { deferred, }) } + + #[cfg(zcash_unstable = "nsm")] + pub fn money_reserve(&self) -> Amount { + let max_money: Amount = MAX_MONEY + .try_into() + .expect("MAX_MONEY should be a valid amount"); + (max_money - self.transparent - self.sprout - self.sapling - self.orchard - self.deferred) + .expect("Expected non-negative value") + } } #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index cf8424d1606..3da49337f1c 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -17,6 +17,8 @@ categories = ["asynchronous", "cryptography::cryptocurrencies"] [features] default = [] +nsm = [] + # Production features that activate extra dependencies, or extra features in dependencies progress-bar = [ @@ -97,3 +99,7 @@ tracing-subscriber = "0.3.18" zebra-state = { path = "../zebra-state", version = "1.0.0-beta.42", features = ["proptest-impl"] } zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.42", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/", version = "1.0.0-beta.42" } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nsm"))'] } + diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index 611aea2ceba..7e9535f4b71 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -22,11 +22,7 @@ use tower::{Service, ServiceExt}; use tracing::Instrument; use zebra_chain::{ - amount::Amount, - block, - parameters::{subsidy::FundingStreamReceiver, Network}, - transparent, - work::equihash, + amount::Amount, block, error::SubsidyError, parameters::Network, transparent, work::equihash, }; use zebra_state as zs; @@ -34,7 +30,6 @@ use crate::{error::*, transaction as tx, BoxError}; pub mod check; pub mod request; -pub mod subsidy; pub use request::Request; @@ -214,11 +209,8 @@ where let now = Utc::now(); check::time_is_valid_at(&block.header, now, &height, &hash) .map_err(VerifyBlockError::Time)?; - let coinbase_tx = check::coinbase_is_first(&block)?; - - let expected_block_subsidy = subsidy::general::block_subsidy(height, &network)?; - - check::subsidy_is_valid(&block, &network, expected_block_subsidy)?; + let coinbase_tx = zs::check::coinbase_is_first(&block) + .map_err(|err| VerifyBlockError::Transaction(TransactionError::Coinbase(err)))?; // Now do the slower checks @@ -284,16 +276,6 @@ where })?; } - // See [ZIP-1015](https://zips.z.cash/zip-1015). - let expected_deferred_amount = subsidy::funding_streams::funding_stream_values( - height, - &network, - expected_block_subsidy, - ) - .expect("we always expect a funding stream hashmap response even if empty") - .remove(&FundingStreamReceiver::Deferred) - .unwrap_or_default(); - let block_miner_fees = block_miner_fees.map_err(|amount_error| BlockError::SummingMinerFees { height, @@ -301,15 +283,6 @@ where source: amount_error, })?; - check::miner_fees_are_valid( - &coinbase_tx, - height, - block_miner_fees, - expected_block_subsidy, - expected_deferred_amount, - &network, - )?; - // Finally, submit the block for contextual verification. let new_outputs = Arc::into_inner(known_utxos) .expect("all verification tasks using known_utxos are complete"); @@ -320,7 +293,8 @@ where height, new_outputs, transaction_hashes, - deferred_balance: Some(expected_deferred_amount), + deferred_balance: None, + block_miner_fees: Some(block_miner_fees), }; // Return early for proposal requests when getblocktemplate-rpcs feature is enabled diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 24ef2ba2ed1..9f422dfc0c6 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -1,14 +1,13 @@ //! Consensus check functions -use std::{collections::HashSet, sync::Arc}; +use std::collections::HashSet; use chrono::{DateTime, Utc}; use zebra_chain::{ - amount::{Amount, Error as AmountError, NonNegative}, block::{Block, Hash, Header, Height}, - parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade}, - transaction::{self, Transaction}, + parameters::Network, + transaction, work::{ difficulty::{ExpandedDifficulty, ParameterDifficulty as _}, equihash, @@ -17,50 +16,6 @@ use zebra_chain::{ use crate::error::*; -use super::subsidy; - -/// Checks if there is exactly one coinbase transaction in `Block`, -/// and if that coinbase transaction is the first transaction in the block. -/// Returns the coinbase transaction is successful. -/// -/// > A transaction that has a single transparent input with a null prevout field, -/// > is called a coinbase transaction. Every block has a single coinbase -/// > transaction as the first transaction in the block. -/// -/// -pub fn coinbase_is_first(block: &Block) -> Result, BlockError> { - // # Consensus - // - // > A block MUST have at least one transaction - // - // - let first = block - .transactions - .first() - .ok_or(BlockError::NoTransactions)?; - // > The first transaction in a block MUST be a coinbase transaction, - // > and subsequent transactions MUST NOT be coinbase transactions. - // - // - // - // > A transaction that has a single transparent input with a null prevout - // > field, is called a coinbase transaction. - // - // - let mut rest = block.transactions.iter().skip(1); - if !first.is_coinbase() { - Err(TransactionError::CoinbasePosition)?; - } - // > A transparent input in a non-coinbase transaction MUST NOT have a null prevout - // - // - if !rest.all(|tx| tx.is_valid_non_coinbase()) { - Err(TransactionError::CoinbaseAfterFirst)?; - } - - Ok(first.clone()) -} - /// Returns `Ok(ExpandedDifficulty)` if the`difficulty_threshold` of `header` is at least as difficult as /// the target difficulty limit for `network` (PoWLimit) /// @@ -141,148 +96,6 @@ pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error header.solution.check(header) } -/// Returns `Ok(())` if the block subsidy in `block` is valid for `network` -/// -/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts -pub fn subsidy_is_valid( - block: &Block, - network: &Network, - expected_block_subsidy: Amount, -) -> Result<(), BlockError> { - let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; - let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?; - - // Validate funding streams - let Some(halving_div) = subsidy::general::halving_divisor(height, network) else { - // Far future halving, with no founders reward or funding streams - return Ok(()); - }; - - let canopy_activation_height = NetworkUpgrade::Canopy - .activation_height(network) - .expect("Canopy activation height is known"); - - let slow_start_interval = network.slow_start_interval(); - - if height < slow_start_interval { - unreachable!( - "unsupported block height: callers should handle blocks below {:?}", - slow_start_interval - ) - } else if halving_div.count_ones() != 1 { - unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") - } else if height < canopy_activation_height { - // Founders rewards are paid up to Canopy activation, on both mainnet and testnet. - // But we checkpoint in Canopy so founders reward does not apply for Zebra. - unreachable!("we cannot verify consensus rules before Canopy activation"); - } else if halving_div < 8 { - // Funding streams are paid from Canopy activation to the second halving - // Note: Canopy activation is at the first halving on mainnet, but not on testnet - // ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet - // funding stream amount values - let funding_streams = subsidy::funding_streams::funding_stream_values( - height, - network, - expected_block_subsidy, - ) - // we always expect a funding stream hashmap response even if empty - .map_err(|err| BlockError::Other(err.to_string()))?; - - // # Consensus - // - // > [Canopy onward] The coinbase transaction at block height `height` - // > MUST contain at least one output per funding stream `fs` active at `height`, - // > that pays `fs.Value(height)` zatoshi in the prescribed way to the stream's - // > recipient address represented by `fs.AddressList[fs.AddressIndex(height)] - // - // https://zips.z.cash/protocol/protocol.pdf#fundingstreams - for (receiver, expected_amount) in funding_streams { - if receiver == FundingStreamReceiver::Deferred { - // The deferred pool contribution is checked in `miner_fees_are_valid()` - // See [ZIP-1015](https://zips.z.cash/zip-1015) for more details. - continue; - } - - let address = - subsidy::funding_streams::funding_stream_address(height, network, receiver) - // funding stream receivers other than the deferred pool must have an address - .ok_or_else(|| { - BlockError::Other(format!( - "missing funding stream address at height {height:?}" - )) - })?; - - let has_expected_output = - subsidy::funding_streams::filter_outputs_by_address(coinbase, address) - .iter() - .map(zebra_chain::transparent::Output::value) - .any(|value| value == expected_amount); - - if !has_expected_output { - Err(SubsidyError::FundingStreamNotFound)?; - } - } - Ok(()) - } else { - // Future halving, with no founders reward or funding streams - Ok(()) - } -} - -/// Returns `Ok(())` if the miner fees consensus rule is valid. -/// -/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus -pub fn miner_fees_are_valid( - coinbase_tx: &Transaction, - height: Height, - block_miner_fees: Amount, - expected_block_subsidy: Amount, - expected_deferred_amount: Amount, - network: &Network, -) -> Result<(), BlockError> { - let transparent_value_balance = subsidy::general::output_amounts(coinbase_tx) - .iter() - .sum::, AmountError>>() - .map_err(|_| SubsidyError::SumOverflow)? - .constrain() - .expect("positive value always fit in `NegativeAllowed`"); - let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount(); - let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount(); - - // # Consensus - // - // > - define the total output value of its coinbase transaction to be the total value in zatoshi of its transparent - // > outputs, minus vbalanceSapling, minus vbalanceOrchard, plus totalDeferredOutput(height); - // > – define the total input value of its coinbase transaction to be the value in zatoshi of the block subsidy, - // > plus the transaction fees paid by transactions in the block. - // - // https://zips.z.cash/protocol/protocol.pdf#txnconsensus - // - // The expected lockbox funding stream output of the coinbase transaction is also subtracted - // from the block subsidy value plus the transaction fees paid by transactions in this block. - let total_output_value = (transparent_value_balance - sapling_value_balance - orchard_value_balance - + expected_deferred_amount.constrain().expect("valid Amount with NonNegative constraint should be valid with NegativeAllowed constraint")) - .map_err(|_| SubsidyError::SumOverflow)?; - let total_input_value = - (expected_block_subsidy + block_miner_fees).map_err(|_| SubsidyError::SumOverflow)?; - - // # Consensus - // - // > [Pre-NU6] The total output of a coinbase transaction MUST NOT be greater than its total - // input. - // - // > [NU6 onward] The total output of a coinbase transaction MUST be equal to its total input. - if if NetworkUpgrade::current(network, height) < NetworkUpgrade::Nu6 { - total_output_value > total_input_value - } else { - total_output_value != total_input_value - } { - Err(SubsidyError::InvalidMinerFees)? - }; - - Ok(()) -} - /// Returns `Ok(())` if `header.time` is less than or equal to /// 2 hours in the future, according to the node's local clock (`now`). /// diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index e6eb6f2c4b9..8e39ba84c76 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -7,20 +7,26 @@ use tower::{buffer::Buffer, util::BoxService}; use zebra_chain::{ amount::MAX_MONEY, block::{ + error, subsidy, + subsidy::general::block_subsidy_pre_nsm, tests::generate::{ large_multi_transaction_block, large_single_transaction_block_many_inputs, }, Block, Height, }, - parameters::NetworkUpgrade, + error::CoinbaseTransactionError, + parameters::{subsidy::FundingStreamReceiver, NetworkUpgrade}, serialization::{ZcashDeserialize, ZcashDeserializeInto}, transaction::{arbitrary::transaction_to_fake_v5, LockTime, Transaction}, work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY}, }; use zebra_script::CachedFfiTransaction; +use zebra_state::check::{ + coinbase_is_first, miner_fees_are_valid, subsidy_is_valid, transaction_miner_fees_are_valid, +}; use zebra_test::transcript::{ExpectedTranscriptError, Transcript}; -use crate::{block_subsidy, transaction}; +use crate::{block::check, difficulty_is_valid, transaction}; use super::*; @@ -167,8 +173,7 @@ fn coinbase_is_first_for_historical_blocks() -> Result<(), Report> { .zcash_deserialize_into::() .expect("block is structurally valid"); - check::coinbase_is_first(&block) - .expect("the coinbase in a historical block should be valid"); + coinbase_is_first(&block).expect("the coinbase in a historical block should be valid"); } Ok(()) @@ -192,7 +197,7 @@ fn difficulty_is_valid_for_network(network: Network) -> Result<(), Report> { .zcash_deserialize_into::() .expect("block is structurally valid"); - check::difficulty_is_valid(&block.header, &network, &Height(height), &block.hash()) + difficulty_is_valid(&block.header, &network, &Height(height), &block.hash()) .expect("the difficulty from a historical block should be valid"); } @@ -215,8 +220,7 @@ fn difficulty_validation_failure() -> Result<(), Report> { Arc::make_mut(&mut block.header).difficulty_threshold = INVALID_COMPACT_DIFFICULTY; // Validate the block - let result = - check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); + let result = difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); let expected = BlockError::InvalidDifficulty(height, hash); assert_eq!(expected, result); @@ -230,8 +234,7 @@ fn difficulty_validation_failure() -> Result<(), Report> { let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap(); // Validate the block as if it is a mainnet block - let result = - check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); + let result = difficulty_is_valid(&block.header, &Network::Mainnet, &height, &hash).unwrap_err(); let expected = BlockError::TargetDifficultyLimit( height, hash, @@ -251,8 +254,8 @@ fn difficulty_validation_failure() -> Result<(), Report> { let difficulty_threshold = block.header.difficulty_threshold.to_expanded().unwrap(); // Validate the block - let result = check::difficulty_is_valid(&block.header, &Network::Mainnet, &height, &bad_hash) - .unwrap_err(); + let result = + difficulty_is_valid(&block.header, &Network::Mainnet, &height, &bad_hash).unwrap_err(); let expected = BlockError::DifficultyFilter(height, bad_hash, difficulty_threshold, Network::Mainnet); assert_eq!(expected, result); @@ -303,10 +306,10 @@ fn subsidy_is_valid_for_network(network: Network) -> Result<(), Report> { // TODO: first halving, second halving, third halving, and very large halvings if height >= canopy_activation_height { - let expected_block_subsidy = - subsidy::general::block_subsidy(height, &network).expect("valid block subsidy"); + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm(height, &network) + .expect("valid block subsidy"); - check::subsidy_is_valid(&block, &network, expected_block_subsidy) + subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidies should pass for this block"); } } @@ -326,7 +329,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { .expect("block should deserialize"); let mut block = Arc::try_unwrap(block).expect("block should unwrap"); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -338,12 +341,12 @@ fn coinbase_validation_failure() -> Result<(), Report> { block.transactions.remove(0); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::NoTransactions; + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::Block(error::BlockError::NoTransactions); assert_eq!(expected, result); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); + let expected = SubsidyError::NoCoinbase; assert_eq!(expected, result); // Get another funding stream block, and delete the coinbase transaction @@ -352,7 +355,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { .expect("block should deserialize"); let mut block = Arc::try_unwrap(block).expect("block should unwrap"); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -364,12 +367,12 @@ fn coinbase_validation_failure() -> Result<(), Report> { block.transactions.remove(0); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::CoinbasePosition); + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::Position; assert_eq!(expected, result); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase)); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy).unwrap_err(); + let expected = SubsidyError::NoCoinbase; assert_eq!(expected, result); // Get another funding stream, and duplicate the coinbase transaction @@ -388,11 +391,11 @@ fn coinbase_validation_failure() -> Result<(), Report> { ); // Validate the block using coinbase_is_first - let result = check::coinbase_is_first(&block).unwrap_err(); - let expected = BlockError::Transaction(TransactionError::CoinbaseAfterFirst); + let result = coinbase_is_first(&block).unwrap_err(); + let expected = CoinbaseTransactionError::AfterFirst; assert_eq!(expected, result); - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -400,7 +403,7 @@ fn coinbase_validation_failure() -> Result<(), Report> { ) .expect("valid block subsidy"); - check::subsidy_is_valid(&block, &network, expected_block_subsidy) + subsidy_is_valid(&block, &network, expected_block_subsidy) .expect("subsidy does not check for extra coinbase transactions"); Ok(()) @@ -428,11 +431,11 @@ fn funding_stream_validation_for_network(network: Network) -> Result<(), Report> if height >= canopy_activation_height { let block = Block::zcash_deserialize(&block[..]).expect("block should deserialize"); - let expected_block_subsidy = - subsidy::general::block_subsidy(height, &network).expect("valid block subsidy"); + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm(height, &network) + .expect("valid block subsidy"); // Validate - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy); assert!(result.is_ok()); } } @@ -476,7 +479,7 @@ fn funding_stream_validation_failure() -> Result<(), Report> { }; // Validate it - let expected_block_subsidy = subsidy::general::block_subsidy( + let expected_block_subsidy = subsidy::general::block_subsidy_pre_nsm( block .coinbase_height() .expect("block should have coinbase height"), @@ -484,10 +487,8 @@ fn funding_stream_validation_failure() -> Result<(), Report> { ) .expect("valid block subsidy"); - let result = check::subsidy_is_valid(&block, &network, expected_block_subsidy); - let expected = Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::FundingStreamNotFound, - ))); + let result = subsidy_is_valid(&block, &network, expected_block_subsidy); + let expected = Err(SubsidyError::FundingStreamNotFound); assert_eq!(expected, result); Ok(()) @@ -509,11 +510,11 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> { for (&height, block) in block_iter { let height = Height(height); if height > network.slow_start_shift() { - let coinbase_tx = check::coinbase_is_first( + let coinbase_tx = coinbase_is_first( &Block::zcash_deserialize(&block[..]).expect("block should deserialize"), )?; - let expected_block_subsidy = block_subsidy(height, &network)?; + let expected_block_subsidy = block_subsidy_pre_nsm(height, &network)?; // See [ZIP-1015](https://zips.z.cash/zip-1015). let expected_deferred_amount = subsidy::funding_streams::funding_stream_values( @@ -525,7 +526,7 @@ fn miner_fees_validation_for_network(network: Network) -> Result<(), Report> { .remove(&FundingStreamReceiver::Deferred) .unwrap_or_default(); - assert!(check::miner_fees_are_valid( + assert!(transaction_miner_fees_are_valid( &coinbase_tx, height, // Set the miner fees to a high-enough amount. @@ -548,7 +549,7 @@ fn miner_fees_validation_failure() -> Result<(), Report> { let block = Block::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_347499_BYTES[..]) .expect("block should deserialize"); let height = block.coinbase_height().expect("valid coinbase height"); - let expected_block_subsidy = block_subsidy(height, &network)?; + let expected_block_subsidy = block_subsidy_pre_nsm(height, &network)?; // See [ZIP-1015](https://zips.z.cash/zip-1015). let expected_deferred_amount: Amount = subsidy::funding_streams::funding_stream_values(height, &network, expected_block_subsidy) @@ -557,8 +558,8 @@ fn miner_fees_validation_failure() -> Result<(), Report> { .unwrap_or_default(); assert_eq!( - check::miner_fees_are_valid( - check::coinbase_is_first(&block)?.as_ref(), + transaction_miner_fees_are_valid( + coinbase_is_first(&block)?.as_ref(), height, // Set the miner fee to an invalid amount. Amount::zero(), @@ -566,9 +567,91 @@ fn miner_fees_validation_failure() -> Result<(), Report> { expected_deferred_amount, &network ), - Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::InvalidMinerFees, - ))) + Err(SubsidyError::InvalidMinerFees) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_fails_when_burn_amount_is_zero() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = Amount::zero(); + let expected_block_subsidy = 100_000_000.try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Err(SubsidyError::InvalidBurnAmount) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_succeeds_when_burn_amount_is_correct() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = 600.try_into().unwrap(); + let expected_block_subsidy = (100_000_600).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Ok(()) + ); + + Ok(()) +} + +#[cfg(zcash_unstable = "nsm")] +#[test] +fn miner_fees_validation_fails_when_burn_amount_is_incorrect() -> Result<(), Report> { + let transparent_value_balance = 100_001_000.try_into().unwrap(); + let sapling_value_balance = Amount::zero(); + let orchard_value_balance = Amount::zero(); + let burn_amount = 500.try_into().unwrap(); + let expected_block_subsidy = (100_000_500).try_into().unwrap(); + let block_miner_fees = 1000.try_into().unwrap(); + let expected_deferred_amount = Amount::zero(); + + assert_eq!( + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + NetworkUpgrade::ZFuture + ), + Err(SubsidyError::InvalidBurnAmount) ); Ok(()) diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index 039ea6e33e3..0deb121eab0 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -30,20 +30,20 @@ use tracing::instrument; use zebra_chain::{ amount, block::{self, Block}, - parameters::{subsidy::FundingStreamReceiver, Network, GENESIS_PREVIOUS_BLOCK_HASH}, + error::SubsidyError, + parameters::{Network, GENESIS_PREVIOUS_BLOCK_HASH}, work::equihash, }; use zebra_state::{self as zs, CheckpointVerifiedBlock}; use crate::{ block::VerifyBlockError, - block_subsidy, checkpoint::types::{ Progress::{self, *}, TargetHeight::{self, *}, }, - error::{BlockError, SubsidyError}, - funding_stream_values, BoxError, ParameterCheckpoint as _, + error::BlockError, + BoxError, ParameterCheckpoint as _, }; pub(crate) mod list; @@ -608,18 +608,8 @@ where crate::block::check::equihash_solution_is_valid(&block.header)?; } - // We can't get the block subsidy for blocks with heights in the slow start interval, so we - // omit the calculation of the expected deferred amount. - let expected_deferred_amount = if height > self.network.slow_start_interval() { - // See [ZIP-1015](https://zips.z.cash/zip-1015). - funding_stream_values(height, &self.network, block_subsidy(height, &self.network)?)? - .remove(&FundingStreamReceiver::Deferred) - } else { - None - }; - // don't do precalculation until the block passes basic difficulty checks - let block = CheckpointVerifiedBlock::new(block, Some(hash), expected_deferred_amount); + let block = CheckpointVerifiedBlock::new(block, Some(hash), None); crate::block::check::merkle_root_validity( &self.network, diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 8fe14c62d52..f2ed70df3fc 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -9,7 +9,9 @@ use chrono::{DateTime, Utc}; use thiserror::Error; use zebra_chain::{ - amount, block, orchard, sapling, sprout, + amount, block, + error::{CoinbaseTransactionError, SubsidyError}, + orchard, sapling, sprout, transparent::{self, MIN_TRANSPARENT_COINBASE_MATURITY}, }; use zebra_state::ValidateContextError; @@ -22,64 +24,11 @@ use proptest_derive::Arbitrary; /// Workaround for format string identifier rules. const MAX_EXPIRY_HEIGHT: block::Height = block::Height::MAX_EXPIRY_HEIGHT; -/// Block subsidy errors. -#[derive(Error, Clone, Debug, PartialEq, Eq)] -#[allow(missing_docs)] -pub enum SubsidyError { - #[error("no coinbase transaction in block")] - NoCoinbase, - - #[error("funding stream expected output not found")] - FundingStreamNotFound, - - #[error("miner fees are invalid")] - InvalidMinerFees, - - #[error("a sum of amounts overflowed")] - SumOverflow, - - #[error("unsupported height")] - UnsupportedHeight, - - #[error("invalid amount")] - InvalidAmount(amount::Error), -} - -impl From for SubsidyError { - fn from(amount: amount::Error) -> Self { - Self::InvalidAmount(amount) - } -} - /// Errors for semantic transaction validation. #[derive(Error, Clone, Debug, PartialEq, Eq)] #[cfg_attr(any(test, feature = "proptest-impl"), derive(Arbitrary))] #[allow(missing_docs)] pub enum TransactionError { - #[error("first transaction must be coinbase")] - CoinbasePosition, - - #[error("coinbase input found in non-coinbase transaction")] - CoinbaseAfterFirst, - - #[error("coinbase transaction MUST NOT have any JoinSplit descriptions")] - CoinbaseHasJoinSplit, - - #[error("coinbase transaction MUST NOT have any Spend descriptions")] - CoinbaseHasSpend, - - #[error("coinbase transaction MUST NOT have any Output descriptions pre-Heartwood")] - CoinbaseHasOutputPreHeartwood, - - #[error("coinbase transaction MUST NOT have the EnableSpendsOrchard flag set")] - CoinbaseHasEnableSpendsOrchard, - - #[error("coinbase transaction Sapling or Orchard outputs MUST be decryptable with an all-zero outgoing viewing key")] - CoinbaseOutputsNotDecryptable, - - #[error("coinbase inputs MUST NOT exist in mempool")] - CoinbaseInMempool, - #[error("non-coinbase transactions MUST NOT have coinbase inputs")] NonCoinbaseHasCoinbaseInput, @@ -90,15 +39,9 @@ pub enum TransactionError { #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] LockedUntilAfterBlockTime(DateTime), - #[error( - "coinbase expiry {expiry_height:?} must be the same as the block {block_height:?} \ - after NU5 activation, failing transaction: {transaction_hash:?}" - )] - CoinbaseExpiryBlockHeight { - expiry_height: Option, - block_height: zebra_chain::block::Height, - transaction_hash: zebra_chain::transaction::Hash, - }, + #[error("coinbase transaction error")] + #[cfg_attr(any(test, feature = "proptest-impl"), proptest(skip))] + Coinbase(#[from] CoinbaseTransactionError), #[error( "expiry {expiry_height:?} must be less than the maximum {MAX_EXPIRY_HEIGHT:?} \ diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 95381fd9e07..753a41117b0 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -47,13 +47,8 @@ pub mod transaction; #[cfg(any(test, feature = "proptest-impl"))] pub use block::check::difficulty_is_valid; -pub use block::{ - subsidy::{ - funding_streams::{funding_stream_address, funding_stream_values, new_coinbase_script}, - general::{block_subsidy, miner_subsidy}, - }, - Request, VerifyBlockError, MAX_BLOCK_SIGOPS, -}; +pub use block::{Request, VerifyBlockError, MAX_BLOCK_SIGOPS}; + pub use checkpoint::{ list::ParameterCheckpoint, CheckpointList, VerifyCheckpointError, MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECKPOINT_HEIGHT_GAP, diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 1c303003615..08bcb91431f 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -18,7 +18,9 @@ use tracing::Instrument; use zebra_chain::{ amount::{Amount, NonNegative}, - block, orchard, + block, + error::CoinbaseTransactionError, + orchard, parameters::{Network, NetworkUpgrade}, primitives::Groth16Proof, sapling, @@ -309,7 +311,7 @@ where // Validate the coinbase input consensus rules if req.is_mempool() && tx.is_coinbase() { - return Err(TransactionError::CoinbaseInMempool); + return Err(TransactionError::Coinbase(CoinbaseTransactionError::InMempool)); } if tx.is_coinbase() { @@ -413,6 +415,18 @@ where sapling_shielded_data, orchard_shielded_data, )?, + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data, + orchard_shielded_data, + .. } => Self::verify_zfuture_transaction( + &req, + &network, + script_verifier, + cached_ffi_transaction.clone(), + sapling_shielded_data, + orchard_shielded_data, + )?, }; if let Some(unmined_tx) = req.mempool_transaction() { @@ -444,17 +458,21 @@ where // Get the `value_balance` to calculate the transaction fee. let value_balance = tx.value_balance(&spent_utxos); + let burn_amount = match *tx { + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture{ .. } => tx.burn_amount(), + _ => Amount::zero() + }; + // Calculate the fee only for non-coinbase transactions. let mut miner_fee = None; if !tx.is_coinbase() { // TODO: deduplicate this code with remaining_transaction_value()? - miner_fee = match value_balance { - Ok(vb) => match vb.remaining_transaction_value() { - Ok(tx_rtv) => Some(tx_rtv), - Err(_) => return Err(TransactionError::IncorrectFee), - }, - Err(_) => return Err(TransactionError::IncorrectFee), - }; + miner_fee = value_balance + .map(|vb| vb.remaining_transaction_value()) + .map(|tx_rtv| tx_rtv - burn_amount) + .or(Err(TransactionError::IncorrectFee))? + .ok(); } let legacy_sigop_count = cached_ffi_transaction.legacy_sigop_count()?; @@ -687,6 +705,9 @@ where transaction.version(), network_upgrade, )), + + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => Ok(()), } } @@ -767,6 +788,8 @@ where // Note: Here we verify the transaction version number of the above rule, the group // id is checked in zebra-chain crate, in the transaction serialize. NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 => Ok(()), + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => Ok(()), // Does not support V5 transactions NetworkUpgrade::Genesis @@ -782,6 +805,51 @@ where } } + /// Verify a ZFUTURE transaction. + #[cfg(zcash_unstable = "nsm")] + fn verify_zfuture_transaction( + request: &Request, + network: &Network, + script_verifier: script::Verifier, + cached_ffi_transaction: Arc, + sapling_shielded_data: &Option>, + orchard_shielded_data: &Option, + ) -> Result { + let transaction = request.transaction(); + let upgrade = request.upgrade(network); + + if upgrade != NetworkUpgrade::ZFuture { + return Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + upgrade, + )); + } + + let shielded_sighash = transaction.sighash( + upgrade + .branch_id() + .expect("Overwinter-onwards must have branch ID, and we checkpoint on Canopy"), + HashType::ALL, + cached_ffi_transaction.all_previous_outputs(), + None, + ); + + Ok(Self::verify_transparent_inputs_and_outputs( + request, + network, + script_verifier, + cached_ffi_transaction, + )? + .and(Self::verify_sapling_shielded_data( + sapling_shielded_data, + &shielded_sighash, + )?) + .and(Self::verify_orchard_shielded_data( + orchard_shielded_data, + &shielded_sighash, + )?)) + } + /// Verifies if a transaction's transparent inputs are valid using the provided /// `script_verifier` and `cached_ffi_transaction`. /// diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 66e3d0be595..ee9e764039f 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -14,6 +14,7 @@ use chrono::{DateTime, Utc}; use zebra_chain::{ amount::{Amount, NonNegative}, block::Height, + error::CoinbaseTransactionError, orchard::Flags, parameters::{Network, NetworkUpgrade}, primitives::zcash_note_encryption, @@ -122,9 +123,14 @@ pub fn lock_time_has_passed( /// /// This check counts both `Coinbase` and `PrevOut` transparent inputs. pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> { + #[cfg(zcash_unstable = "nsm")] + let has_other_outputs = tx.has_burn_amount(); + #[cfg(not(zcash_unstable = "nsm"))] + let has_other_outputs = false; + if !tx.has_transparent_or_shielded_inputs() { Err(TransactionError::NoInputs) - } else if !tx.has_transparent_or_shielded_outputs() { + } else if !tx.has_transparent_or_shielded_outputs() && !has_other_outputs { Err(TransactionError::NoOutputs) } else { Ok(()) @@ -167,14 +173,20 @@ pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError pub fn coinbase_tx_no_prevout_joinsplit_spend(tx: &Transaction) -> Result<(), TransactionError> { if tx.is_coinbase() { if tx.joinsplit_count() > 0 { - return Err(TransactionError::CoinbaseHasJoinSplit); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasJoinSplit, + )); } else if tx.sapling_spends_per_anchor().count() > 0 { - return Err(TransactionError::CoinbaseHasSpend); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasSpend, + )); } if let Some(orchard_shielded_data) = tx.orchard_shielded_data() { if orchard_shielded_data.flags.contains(Flags::ENABLE_SPENDS) { - return Err(TransactionError::CoinbaseHasEnableSpendsOrchard); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasEnableSpendsOrchard, + )); } } } @@ -340,7 +352,9 @@ pub fn coinbase_outputs_are_decryptable( } if !zcash_note_encryption::decrypts_successfully(transaction, network, height) { - return Err(TransactionError::CoinbaseOutputsNotDecryptable); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::OutputsNotDecryptable, + )); } Ok(()) @@ -367,11 +381,13 @@ pub fn coinbase_expiry_height( // if *block_height >= nu5_activation_height { if expiry_height != Some(*block_height) { - return Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height, - block_height: *block_height, - transaction_hash: coinbase.hash(), - }); + return Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height, + block_height: *block_height, + transaction_hash: coinbase.hash(), + }, + )); } else { return Ok(()); } diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 0a4c21bb039..1dd1150d973 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -12,6 +12,7 @@ use tower::{service_fn, ServiceExt}; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, + error::CoinbaseTransactionError, orchard::AuthorizedAction, parameters::{Network, NetworkUpgrade}, primitives::{ed25519, x25519, Groth16Proof}, @@ -844,7 +845,9 @@ fn v5_coinbase_transaction_with_enable_spends_flag_fails_validation() { assert_eq!( check::coinbase_tx_no_prevout_joinsplit_spend(&transaction), - Err(TransactionError::CoinbaseHasEnableSpendsOrchard) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::HasEnableSpendsOrchard + )) ); } @@ -1705,11 +1708,13 @@ async fn v5_coinbase_transaction_expiry_height() { assert_eq!( result, - Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height: Some(new_expiry_height), - block_height, - transaction_hash: new_transaction.hash(), - }) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height: Some(new_expiry_height), + block_height, + transaction_hash: new_transaction.hash(), + } + )) ); // Decrement the expiry height so that it becomes invalid. @@ -1730,11 +1735,13 @@ async fn v5_coinbase_transaction_expiry_height() { assert_eq!( result, - Err(TransactionError::CoinbaseExpiryBlockHeight { - expiry_height: Some(new_expiry_height), - block_height, - transaction_hash: new_transaction.hash(), - }) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::ExpiryBlockHeight { + expiry_height: Some(new_expiry_height), + block_height, + transaction_hash: new_transaction.hash(), + } + )) ); // Test with matching heights again, but using a very high value @@ -2925,7 +2932,9 @@ fn shielded_outputs_are_not_decryptable_for_fake_v5_blocks() { &network, NetworkUpgrade::Nu5.activation_height(&network).unwrap(), ), - Err(TransactionError::CoinbaseOutputsNotDecryptable) + Err(TransactionError::Coinbase( + CoinbaseTransactionError::OutputsNotDecryptable + )) ); } } diff --git a/zebra-consensus/src/transaction/tests/prop.rs b/zebra-consensus/src/transaction/tests/prop.rs index f45b4731de0..876c9e2881c 100644 --- a/zebra-consensus/src/transaction/tests/prop.rs +++ b/zebra-consensus/src/transaction/tests/prop.rs @@ -29,7 +29,7 @@ proptest! { (network, block_height) in sapling_onwards_strategy(), block_time in datetime_full(), relative_source_fund_heights in vec(0.0..1.0, 1..=MAX_TRANSPARENT_INPUTS), - transaction_version in 4_u8..=5, + transaction_version in 4_u8..=6, ) { let _init_guard = zebra_test::init(); @@ -300,6 +300,9 @@ fn mock_transparent_transaction( // Create the mock transaction let expiry_height = block_height; + #[cfg(zcash_unstable = "nsm")] + let burn_amount = Amount::zero(); + let transaction = match transaction_version { 4 => Transaction::V4 { inputs, @@ -309,7 +312,17 @@ fn mock_transparent_transaction( joinsplit_data: None, sapling_shielded_data: None, }, - 5 => Transaction::V5 { + 5 | 6 => Transaction::V5 { + inputs, + outputs, + lock_time, + expiry_height, + sapling_shielded_data: None, + orchard_shielded_data: None, + network_upgrade, + }, + #[cfg(zcash_unstable = "nsm")] + 255 => Transaction::ZFuture { inputs, outputs, lock_time, @@ -317,6 +330,7 @@ fn mock_transparent_transaction( sapling_shielded_data: None, orchard_shielded_data: None, network_upgrade, + burn_amount, }, invalid_version => unreachable!("invalid transaction version: {}", invalid_version), }; @@ -345,6 +359,8 @@ fn sanitize_transaction_version( Overwinter => 3, Sapling | Blossom | Heartwood | Canopy => 4, Nu5 | Nu6 => 5, + #[cfg(zcash_unstable = "nsm")] + ZFuture => 0x00FF, } }; diff --git a/zebra-network/Cargo.toml b/zebra-network/Cargo.toml index e4967cc66f2..88bdf504725 100644 --- a/zebra-network/Cargo.toml +++ b/zebra-network/Cargo.toml @@ -27,6 +27,8 @@ categories = ["asynchronous", "cryptography::cryptocurrencies", "encoding", "net [features] default = [] +nsm = [] + # Production features that activate extra dependencies, or extra features in dependencies progress-bar = [ @@ -95,3 +97,6 @@ toml = "0.8.19" zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/" } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nsm"))'] } diff --git a/zebra-network/src/constants.rs b/zebra-network/src/constants.rs index a116fd63018..d540c32df20 100644 --- a/zebra-network/src/constants.rs +++ b/zebra-network/src/constants.rs @@ -337,10 +337,10 @@ pub const TIMESTAMP_TRUNCATION_SECONDS: u32 = 30 * 60; /// /// The current protocol version typically changes before Mainnet and Testnet /// network upgrades. -/// -/// This version of Zebra draws the current network protocol version from -/// [ZIP-253](https://zips.z.cash/zip-0253). +#[cfg(not(zcash_unstable = "nsm"))] pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_120); +#[cfg(zcash_unstable = "nsm")] +pub const CURRENT_NETWORK_PROTOCOL_VERSION: Version = Version(170_140); /// The default RTT estimate for peer responses. /// diff --git a/zebra-network/src/protocol/external/types.rs b/zebra-network/src/protocol/external/types.rs index c6241ba4d78..22e1b98a5a6 100644 --- a/zebra-network/src/protocol/external/types.rs +++ b/zebra-network/src/protocol/external/types.rs @@ -106,6 +106,10 @@ impl Version { (Mainnet, Nu5) => 170_100, (Testnet(params), Nu6) if params.is_default_testnet() => 170_110, (Mainnet, Nu6) => 170_120, + #[cfg(zcash_unstable = "nsm")] + (Testnet(params), ZFuture) if params.is_default_testnet() => 170_130, + #[cfg(zcash_unstable = "nsm")] + (Mainnet, ZFuture) => 170_140, // It should be fine to reject peers with earlier network protocol versions on custom testnets for now. (Testnet(_), _) => CURRENT_NETWORK_PROTOCOL_VERSION.0, @@ -205,6 +209,10 @@ mod test { let _init_guard = zebra_test::init(); let highest_network_upgrade = NetworkUpgrade::current(network, block::Height::MAX); + #[cfg(zcash_unstable = "nsm")] + assert!(highest_network_upgrade == ZFuture, + "expected coverage of all network upgrades: add the new network upgrade to the list in this test"); + #[cfg(not(zcash_unstable = "nsm"))] assert!(highest_network_upgrade == Nu6 || highest_network_upgrade == Nu5, "expected coverage of all network upgrades: add the new network upgrade to the list in this test"); @@ -217,6 +225,8 @@ mod test { Canopy, Nu5, Nu6, + #[cfg(zcash_unstable = "nsm")] + ZFuture, ] { let height = network_upgrade.activation_height(network); if let Some(height) = height { diff --git a/zebra-rpc/Cargo.toml b/zebra-rpc/Cargo.toml index 85be248bc76..7b07cbac9c7 100644 --- a/zebra-rpc/Cargo.toml +++ b/zebra-rpc/Cargo.toml @@ -21,6 +21,8 @@ categories = [ [features] +nsm = [] + indexer-rpcs = [ "tonic-build", "tonic", @@ -140,3 +142,7 @@ zebra-state = { path = "../zebra-state", version = "1.0.0-beta.42", features = [ ] } zebra-test = { path = "../zebra-test", version = "1.0.0-beta.42" } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nsm"))'] } + diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 2d50552cfec..88a51ca5981 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -11,7 +11,11 @@ use zcash_address::{unified::Encoding, TryFromAddress}; use zebra_chain::{ amount::{self, Amount, NonNegative}, - block::{self, Block, Height, TryIntoHeight}, + block::{ + self, + subsidy::{funding_streams, general}, + Block, Height, TryIntoHeight, + }, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip, parameters::{ @@ -25,12 +29,13 @@ use zebra_chain::{ }, work::difficulty::{ParameterDifficulty as _, U256}, }; -use zebra_consensus::{ - block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, RouterError, -}; + +use zebra_consensus::RouterError; + +use zebra_state::{ReadRequest, ReadResponse}; + use zebra_network::AddressBookPeers; use zebra_node_services::mempool; -use zebra_state::{ReadRequest, ReadResponse}; use crate::methods::{ best_chain_tip_height, @@ -286,13 +291,13 @@ pub trait GetBlockTemplateRpc { address: String, ) -> BoxFuture>; - #[rpc(name = "generate")] /// Mine blocks immediately. Returns the block hashes of the generated blocks. /// /// # Parameters /// /// - `num_blocks`: (numeric, required, example=1) Number of blocks to be generated. /// + /// - `burn_amount`: (numeric, optional) The amount of money to be burned in a transaction [ZIP-233] /// # Notes /// /// Only works if the network of the running zebrad process is `Regtest`. @@ -300,7 +305,14 @@ pub trait GetBlockTemplateRpc { /// zcashd reference: [`generate`](https://zcash.github.io/rpc/generate.html) /// method: post /// tags: generating - fn generate(&self, num_blocks: u32) -> BoxFuture>>; + #[rpc(name = "generate")] + fn generate( + &self, + num_blocks: u32, + // the burn_amount field should be currently hidden behind zcash_unstable flag, but it doesn't compile due to the rpc macro + //#[cfg(zcash_unstable = "nsm")] + burn_amount: Option>, + ) -> BoxFuture>>; } /// RPC method implementations. @@ -613,7 +625,7 @@ where let mempool = self.mempool.clone(); let mut latest_chain_tip = self.latest_chain_tip.clone(); let sync_status = self.sync_status.clone(); - let state = self.state.clone(); + let mut state = self.state.clone(); if let Some(HexData(block_proposal_bytes)) = parameters .as_ref() @@ -882,6 +894,45 @@ where "selecting transactions for the template from the mempool" ); + #[cfg(zcash_unstable = "nsm")] + let burn_amount = if let Some(params) = parameters { + params.burn_amount + } else { + None + }; + + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = { + let money_reserve = match state + .ready() + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? + .call(ReadRequest::TipPoolValues) + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? { + ReadResponse::TipPoolValues { + tip_hash: _, + tip_height: _, + value_balance, + } => value_balance.money_reserve(), + _ => unreachable!("wrong response to ReadRequest::TipPoolValues"), + }; + general::block_subsidy(next_block_height, &network, money_reserve) + .map_server_error()? + }; + + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(next_block_height, &network).map_server_error()?; + // Randomly select some mempool transactions. let mempool_txs = zip317::select_mempool_transactions( &network, @@ -890,6 +941,9 @@ where mempool_txs, debug_like_zcashd, extra_coinbase_data.clone(), + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ) .await; @@ -902,7 +956,6 @@ where ); // - After this point, the template only depends on the previously fetched data. - let response = GetBlockTemplate::new( &network, &miner_address, @@ -912,6 +965,9 @@ where submit_old, debug_like_zcashd, extra_coinbase_data, + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ); Ok(response.into()) @@ -1202,6 +1258,8 @@ where fn get_block_subsidy(&self, height: Option) -> BoxFuture> { let latest_chain_tip = self.latest_chain_tip.clone(); let network = self.network.clone(); + #[cfg(zcash_unstable = "nsm")] + let mut state_service = self.state.clone(); async move { let height = if let Some(height) = height { @@ -1223,12 +1281,42 @@ where // Always zero for post-halving blocks let founders = Amount::zero(); - let total_block_subsidy = block_subsidy(height, &network).map_server_error()?; + #[cfg(zcash_unstable = "nsm")] + let total_block_subsidy = { + let money_reserve = match state_service + .ready() + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? + .call(ReadRequest::TipPoolValues) + .await + .map_err(|_| Error { + code: ErrorCode::InternalError, + message: "".into(), + data: None, + })? { + ReadResponse::TipPoolValues { + tip_hash: _, + tip_height: _, + value_balance, + } => value_balance.money_reserve(), + _ => unreachable!("wrong response to ReadRequest::TipPoolValues"), + }; + general::block_subsidy(height, &network, money_reserve).map_server_error()? + }; + + #[cfg(not(zcash_unstable = "nsm"))] + let total_block_subsidy = + general::block_subsidy_pre_nsm(height, &network).map_server_error()?; + let miner_subsidy = - miner_subsidy(height, &network, total_block_subsidy).map_server_error()?; + general::miner_subsidy(height, &network, total_block_subsidy).map_server_error()?; let (lockbox_streams, mut funding_streams): (Vec<_>, Vec<_>) = - funding_stream_values(height, &network, total_block_subsidy) + funding_streams::funding_stream_values(height, &network, total_block_subsidy) .map_server_error()? .into_iter() // Separate the funding streams into deferred and non-deferred streams @@ -1255,7 +1343,8 @@ where streams .into_iter() .map(|(receiver, value)| { - let address = funding_stream_address(height, &network, receiver); + let address = + funding_streams::funding_stream_address(height, &network, receiver); FundingStream::new(is_nu6, receiver, value, address) }) .collect() @@ -1406,7 +1495,12 @@ where .boxed() } - fn generate(&self, num_blocks: u32) -> BoxFuture>> { + fn generate( + &self, + num_blocks: u32, + //#[cfg(zcash_unstable = "nsm")] + burn_amount: Option>, + ) -> BoxFuture>> { let rpc: GetBlockTemplateRpcImpl< Mempool, State, @@ -1427,8 +1521,18 @@ where } let mut block_hashes = Vec::new(); + #[cfg(not(zcash_unstable = "nsm"))] + let params = None; + #[cfg(zcash_unstable = "nsm")] + let params = Some(get_block_template::JsonParameters { + burn_amount, + ..Default::default() + }); for _ in 0..num_blocks { - let block_template = rpc.get_block_template(None).await.map_server_error()?; + let block_template = rpc + .get_block_template(params.clone()) + .await + .map_server_error()?; let get_block_template::Response::TemplateMode(block_template) = block_template else { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs index 8e9578180be..50272c27a36 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 @@ -10,6 +10,7 @@ use zebra_chain::{ block::{ self, merkle::{self, AuthDataRoot}, + subsidy::{funding_streams, general}, Block, ChainHistoryBlockTxAuthCommitmentHash, ChainHistoryMmrRootHash, Height, }, chain_sync_status::ChainSyncStatus, @@ -19,12 +20,10 @@ use zebra_chain::{ transaction::{Transaction, UnminedTx, VerifiedUnminedTx}, transparent, }; -use zebra_consensus::{ - block_subsidy, funding_stream_address, funding_stream_values, miner_subsidy, -}; -use zebra_node_services::mempool; use zebra_state::GetBlockTemplateChainInfo; +use zebra_node_services::mempool; + use crate::methods::{ errors::OkOrServerError, get_block_template_rpcs::{ @@ -289,6 +288,7 @@ where /// /// If `like_zcashd` is true, try to match the coinbase transactions generated by `zcashd` /// in the `getblocktemplate` RPC. +#[allow(clippy::too_many_arguments)] pub fn generate_coinbase_and_roots( network: &Network, block_template_height: Height, @@ -297,6 +297,8 @@ pub fn generate_coinbase_and_roots( history_tree: Arc, like_zcashd: bool, extra_coinbase_data: Vec, + expected_block_subsidy: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Option>, ) -> (TransactionTemplate, DefaultRoots) { // Generate the coinbase transaction let miner_fee = calculate_miner_fee(mempool_txs); @@ -307,6 +309,9 @@ pub fn generate_coinbase_and_roots( miner_fee, like_zcashd, extra_coinbase_data, + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ); // Calculate block default roots @@ -333,6 +338,7 @@ pub fn generate_coinbase_and_roots( /// /// If `like_zcashd` is true, try to match the coinbase transactions generated by `zcashd` /// in the `getblocktemplate` RPC. +#[allow(clippy::too_many_arguments)] pub fn generate_coinbase_transaction( network: &Network, height: Height, @@ -340,13 +346,67 @@ pub fn generate_coinbase_transaction( miner_fee: Amount, like_zcashd: bool, extra_coinbase_data: Vec, + expected_block_subsidy: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Option>, ) -> UnminedTx { - let outputs = standard_coinbase_outputs(network, height, miner_address, miner_fee, like_zcashd); + let outputs = standard_coinbase_outputs( + network, + height, + miner_address, + miner_fee, + like_zcashd, + expected_block_subsidy, + ); if like_zcashd { + #[cfg(zcash_unstable = "nsm")] + { + let network_upgrade = NetworkUpgrade::current(network, height); + if network_upgrade < NetworkUpgrade::ZFuture { + Transaction::new_v4_coinbase( + network, + height, + outputs, + like_zcashd, + extra_coinbase_data, + ) + .into() + } else { + Transaction::new_zfuture_coinbase( + network, + height, + outputs, + extra_coinbase_data, + like_zcashd, + miner_fee, + burn_amount, + ) + .into() + } + } + #[cfg(not(zcash_unstable = "nsm"))] Transaction::new_v4_coinbase(network, height, outputs, like_zcashd, extra_coinbase_data) .into() } else { + #[cfg(zcash_unstable = "nsm")] + { + let network_upgrade = NetworkUpgrade::current(network, height); + if network_upgrade < NetworkUpgrade::ZFuture { + Transaction::new_v5_coinbase(network, height, outputs, extra_coinbase_data).into() + } else { + Transaction::new_zfuture_coinbase( + network, + height, + outputs, + extra_coinbase_data, + like_zcashd, + miner_fee, + burn_amount, + ) + .into() + } + } + #[cfg(not(zcash_unstable = "nsm"))] Transaction::new_v5_coinbase(network, height, outputs, extra_coinbase_data).into() } } @@ -375,10 +435,11 @@ pub fn standard_coinbase_outputs( miner_address: &transparent::Address, miner_fee: Amount, like_zcashd: bool, + expected_block_subsidy: Amount, ) -> Vec<(Amount, transparent::Script)> { - let expected_block_subsidy = block_subsidy(height, network).expect("valid block subsidy"); - let funding_streams = funding_stream_values(height, network, expected_block_subsidy) - .expect("funding stream value calculations are valid for reasonable chain heights"); + let funding_streams = + funding_streams::funding_stream_values(height, network, expected_block_subsidy) + .expect("funding stream value calculations are valid for reasonable chain heights"); // Optional TODO: move this into a zebra_consensus function? let funding_streams: HashMap< @@ -389,12 +450,15 @@ pub fn standard_coinbase_outputs( .filter_map(|(receiver, amount)| { Some(( receiver, - (amount, funding_stream_address(height, network, receiver)?), + ( + amount, + funding_streams::funding_stream_address(height, network, receiver)?, + ), )) }) .collect(); - let miner_reward = miner_subsidy(height, network, expected_block_subsidy) + let miner_reward = general::miner_subsidy(height, network, expected_block_subsidy) .expect("reward calculations are valid for reasonable chain heights") + miner_fee; let miner_reward = diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs index d7c31e11a81..c2347ac0726 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template.rs @@ -4,7 +4,7 @@ use std::fmt; use zebra_chain::{ - amount, + amount::{self, Amount, NonNegative}, block::{ChainHistoryBlockTxAuthCommitmentHash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION}, parameters::Network, serialization::DateTime32, @@ -231,6 +231,8 @@ impl GetBlockTemplate { submit_old: Option, like_zcashd: bool, extra_coinbase_data: Vec, + expected_block_subsidy: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Option>, ) -> Self { // Calculate the next block height. let next_block_height = @@ -271,6 +273,9 @@ impl GetBlockTemplate { chain_tip_and_local_time.history_tree.clone(), like_zcashd, extra_coinbase_data, + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ); // Convert difficulty diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs index 73e1ed820ba..0230a50ce1d 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/types/get_block_template/parameters.rs @@ -1,5 +1,7 @@ //! Parameter types for the `getblocktemplate` RPC. +use zebra_chain::amount::{Amount, NonNegative}; + use crate::methods::{get_block_template_rpcs::types::long_poll::LongPollId, hex_data::HexData}; /// Defines whether the RPC method should generate a block template or attempt to validate a block proposal. @@ -89,6 +91,10 @@ pub struct JsonParameters { /// currently unused. #[serde(rename = "workid")] pub _work_id: Option, + + /// The amount of money to be burned in a transaction [ZIP-233] + #[cfg(zcash_unstable = "nsm")] + pub burn_amount: Option>, } impl JsonParameters { 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 fc0805b533d..e5039fb1e0f 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 @@ -220,6 +220,8 @@ pub fn proposal_block_from_template( NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 => { block_commitments_hash.bytes_in_serialized_order().into() } + #[cfg(zcash_unstable = "nsm")] + NetworkUpgrade::ZFuture => block_commitments_hash.bytes_in_serialized_order().into(), }; Ok(Block { diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/zip317.rs b/zebra-rpc/src/methods/get_block_template_rpcs/zip317.rs index 3f0979dc266..e3802a3326c 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/zip317.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/zip317.rs @@ -12,7 +12,7 @@ use rand::{ }; use zebra_chain::{ - amount::NegativeOrZero, + amount::{Amount, NegativeOrZero, NonNegative}, block::{Height, MAX_BLOCK_BYTES}, parameters::Network, transaction::{zip317::BLOCK_UNPAID_ACTION_LIMIT, VerifiedUnminedTx}, @@ -36,6 +36,7 @@ use crate::methods::get_block_template_rpcs::{ /// Returns selected transactions from `mempool_txs`. /// /// [ZIP-317]: https://zips.z.cash/zip-0317#block-production +#[allow(clippy::too_many_arguments)] pub async fn select_mempool_transactions( network: &Network, next_block_height: Height, @@ -43,6 +44,8 @@ pub async fn select_mempool_transactions( mempool_txs: Vec, like_zcashd: bool, extra_coinbase_data: Vec, + expected_block_subsidy: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Option>, ) -> Vec { // Use a fake coinbase transaction to break the dependency between transaction // selection, the miner fee, and the fee payment in the coinbase transaction. @@ -52,6 +55,9 @@ pub async fn select_mempool_transactions( miner_address, like_zcashd, extra_coinbase_data, + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ); // Setup the transaction lists. @@ -120,6 +126,8 @@ pub fn fake_coinbase_transaction( miner_address: &transparent::Address, like_zcashd: bool, extra_coinbase_data: Vec, + expected_block_subsidy: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Option>, ) -> TransactionTemplate { // Block heights are encoded as variable-length (script) and `u32` (lock time, expiry height). // They can also change the `u32` consensus branch id. @@ -139,6 +147,9 @@ pub fn fake_coinbase_transaction( miner_fee, like_zcashd, extra_coinbase_data, + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + burn_amount, ); TransactionTemplate::from_coinbase(&coinbase_tx, miner_fee) diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index f4d7804088e..5f8328fa7b9 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -46,6 +46,8 @@ async fn test_rpc_response_data() { .with_activation_heights(ConfiguredActivationHeights { blossom: Some(584_000), nu6: Some(POST_NU6_FUNDING_STREAMS_TESTNET.height_range().start.0), + #[cfg(zcash_unstable = "nsm")] + zfuture: Some(3_000_000), ..Default::default() }) .to_network(); diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index 5b5a21e23d0..d8ca55e901e 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -1246,6 +1246,7 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { parameters::NetworkKind, serialization::DateTime32, transaction::{zip317, VerifiedUnminedTx}, + value_balance::ValueBalance, work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256}, }; use zebra_consensus::MAX_BLOCK_SIGOPS; @@ -1337,6 +1338,16 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) { max_time: fake_max_time, history_tree: fake_history_tree(&Mainnet), })); + + #[cfg(zcash_unstable = "nsm")] + read_state + .expect_request_that(|req| matches!(req, ReadRequest::TipPoolValues)) + .await + .respond(ReadResponse::TipPoolValues { + tip_height: fake_tip_height, + tip_hash: fake_tip_hash, + value_balance: ValueBalance::zero(), + }); } }; diff --git a/zebra-state/Cargo.toml b/zebra-state/Cargo.toml index 55f4f2e1556..1d5c357fa8f 100644 --- a/zebra-state/Cargo.toml +++ b/zebra-state/Cargo.toml @@ -16,6 +16,12 @@ categories = ["asynchronous", "caching", "cryptography::cryptocurrencies"] [features] +default = [] + +nsm = [ + "zebra-chain/nsm", +] + # Production features that activate extra dependencies, or extra features in dependencies progress-bar = [ @@ -110,3 +116,6 @@ tokio = { version = "1.41.0", features = ["full", "tracing", "test-util"] } zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.42", features = ["proptest-impl"] } zebra-test = { path = "../zebra-test/", version = "1.0.0-beta.42" } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("nsm"))'] } diff --git a/zebra-state/src/arbitrary.rs b/zebra-state/src/arbitrary.rs index 5c0b837566a..7909b40ce83 100644 --- a/zebra-state/src/arbitrary.rs +++ b/zebra-state/src/arbitrary.rs @@ -38,6 +38,7 @@ impl Prepare for Arc { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } } @@ -112,6 +113,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: _, + block_miner_fees: _, } = block.into(); Self { diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index cf495311efb..e3fc2846675 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -8,6 +8,7 @@ use thiserror::Error; use zebra_chain::{ amount::{self, NegativeAllowed, NonNegative}, block, + error::{CoinbaseTransactionError, SubsidyError}, history_tree::HistoryTreeError, orchard, sapling, sprout, transaction, transparent, value_balance::{ValueBalance, ValueBalanceError}, @@ -264,6 +265,24 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("could not validate block subsidy")] + SubsidyError(Box), + + #[error("could not validate coinbase transaction")] + CoinbaseTransactionError(Box), +} + +impl From for ValidateContextError { + fn from(err: SubsidyError) -> Self { + ValidateContextError::SubsidyError(Box::new(err)) + } +} + +impl From for ValidateContextError { + fn from(err: CoinbaseTransactionError) -> Self { + ValidateContextError::CoinbaseTransactionError(Box::new(err)) + } } /// Trait for creating the corresponding duplicate nullifier error from a nullifier. diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 56be011d48e..bcb92ec6723 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -163,6 +163,8 @@ pub struct SemanticallyVerifiedBlock { pub transaction_hashes: Arc<[transaction::Hash]>, /// This block's contribution to the deferred pool. pub deferred_balance: Option>, + /// This block's miner fees + pub block_miner_fees: Option>, } /// A block ready to be committed directly to the finalized state with @@ -392,6 +394,7 @@ impl ContextuallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance, + block_miner_fees: _, } = semantically_verified; // This is redundant for the non-finalized state, @@ -453,6 +456,7 @@ impl SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } @@ -485,6 +489,7 @@ impl From> for SemanticallyVerifiedBlock { new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, } } } @@ -504,6 +509,7 @@ impl From for SemanticallyVerifiedBlock { .constrain::() .expect("deferred balance in a block must me non-negative"), ), + block_miner_fees: None, } } } @@ -517,6 +523,7 @@ impl From for SemanticallyVerifiedBlock { new_outputs: finalized.new_outputs, transaction_hashes: finalized.transaction_hashes, deferred_balance: finalized.deferred_balance, + block_miner_fees: None, } } } @@ -625,6 +632,9 @@ pub enum Request { /// with the current best chain tip. Tip, + #[cfg(zcash_unstable = "nsm")] + TipPoolValues, + /// Computes a block locator object based on the current best chain. /// /// Returns [`Response::BlockLocator`] with hashes starting @@ -778,6 +788,8 @@ impl Request { Request::AwaitUtxo(_) => "await_utxo", Request::Depth(_) => "depth", Request::Tip => "tip", + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => "tip_pool_values", Request::BlockLocator => "block_locator", Request::Transaction(_) => "transaction", Request::UnspentBestChainUtxo { .. } => "unspent_best_chain_utxo", @@ -1128,6 +1140,8 @@ impl TryFrom for ReadRequest { fn try_from(request: Request) -> Result { match request { Request::Tip => Ok(ReadRequest::Tip), + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => Ok(ReadRequest::TipPoolValues), Request::Depth(hash) => Ok(ReadRequest::Depth(hash)), Request::BestChainNextMedianTimePast => Ok(ReadRequest::BestChainNextMedianTimePast), Request::BestChainBlockHash(hash) => Ok(ReadRequest::BestChainBlockHash(hash)), diff --git a/zebra-state/src/response.rs b/zebra-state/src/response.rs index 77c252b0c75..4e6f3c0cde6 100644 --- a/zebra-state/src/response.rs +++ b/zebra-state/src/response.rs @@ -39,6 +39,16 @@ pub enum Response { // `LatestChainTip::best_tip_height_and_hash()` Tip(Option<(block::Height, block::Hash)>), + /// Response to [`Request::TipPoolValues`] with the current best chain tip values. + TipPoolValues { + /// The current best chain tip height. + tip_height: block::Height, + /// The current best chain tip hash. + tip_hash: block::Hash, + /// The value pool balance at the current best chain tip. + value_balance: ValueBalance, + }, + /// Response to [`Request::BlockLocator`] with a block locator object. BlockLocator(Vec), @@ -303,8 +313,7 @@ impl TryFrom for Response { ReadResponse::ValidBestChainTipNullifiersAndAnchors => Ok(Response::ValidBestChainTipNullifiersAndAnchors), - ReadResponse::TipPoolValues { .. } - | ReadResponse::TransactionIdsForBlock(_) + ReadResponse::TransactionIdsForBlock(_) | ReadResponse::SaplingTree(_) | ReadResponse::OrchardTree(_) | ReadResponse::SaplingSubtrees(_) @@ -315,6 +324,14 @@ impl TryFrom for Response { Err("there is no corresponding Response for this ReadResponse") } + #[cfg(zcash_unstable = "nsm")] + ReadResponse::TipPoolValues { tip_height, tip_hash, value_balance } => Ok(Response::TipPoolValues { tip_height, tip_hash, value_balance }), + + #[cfg(not(zcash_unstable = "nsm"))] + ReadResponse::TipPoolValues { .. } => { + Err("there is no corresponding Response for this ReadResponse") + } + #[cfg(feature = "getblocktemplate-rpcs")] ReadResponse::ValidBlockProposal => Ok(Response::ValidBlockProposal), diff --git a/zebra-state/src/service.rs b/zebra-state/src/service.rs index adc61f887ae..26040dd68e5 100644 --- a/zebra-state/src/service.rs +++ b/zebra-state/src/service.rs @@ -1114,6 +1114,21 @@ impl Service for StateService { .boxed() } + #[cfg(zcash_unstable = "nsm")] + Request::TipPoolValues => { + // Redirect the request to the concurrent ReadStateService + let read_service = self.read_service.clone(); + async move { + let req = req + .try_into() + .expect("ReadRequest conversion should not fail"); + let rsp = read_service.oneshot(req).await?; + let rsp = rsp.try_into().expect("Response conversion should not fail"); + Ok(rsp) + } + .boxed() + } + #[cfg(feature = "getblocktemplate-rpcs")] Request::CheckBlockProposalValidity(_) => { // Redirect the request to the concurrent ReadStateService @@ -1861,7 +1876,7 @@ impl Service for ReadStateService { } #[cfg(feature = "getblocktemplate-rpcs")] - ReadRequest::CheckBlockProposalValidity(semantically_verified) => { + ReadRequest::CheckBlockProposalValidity(mut semantically_verified) => { let state = self.clone(); // # Performance @@ -1892,7 +1907,7 @@ impl Service for ReadStateService { write::validate_and_commit_non_finalized( &state.db, &mut latest_non_finalized_state, - semantically_verified, + &mut semantically_verified, )?; // The work is done in the future. diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 04ea61d6982..03afa1d7857 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -116,6 +116,7 @@ impl From for ChainTipBlock { new_outputs: _, transaction_hashes, deferred_balance: _, + block_miner_fees: _, } = prepared; Self { diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index ced63bfea16..3c010ff5a5f 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -5,9 +5,16 @@ use std::{borrow::Borrow, sync::Arc}; use chrono::Duration; use zebra_chain::{ - block::{self, Block, ChainHistoryBlockTxAuthCommitmentHash, CommitmentError}, + amount::{Amount, Error as AmountError, NonNegative, MAX_MONEY}, + block::{ + self, error::BlockError, subsidy::funding_streams, subsidy::general, Block, + ChainHistoryBlockTxAuthCommitmentHash, CommitmentError, Height, + }, + error::{CoinbaseTransactionError, SubsidyError}, history_tree::HistoryTree, - parameters::{Network, NetworkUpgrade}, + parameters::{subsidy::FundingStreamReceiver, Network, NetworkUpgrade}, + transaction, + value_balance::ValueBalance, work::difficulty::CompactDifficulty, }; @@ -48,10 +55,11 @@ pub(crate) use difficulty::AdjustedDifficulty; /// with its parent block. #[tracing::instrument(skip(semantically_verified, finalized_tip_height, relevant_chain))] pub(crate) fn block_is_valid_for_recent_chain( - semantically_verified: &SemanticallyVerifiedBlock, + semantically_verified: &mut SemanticallyVerifiedBlock, network: &Network, finalized_tip_height: Option, relevant_chain: C, + pool_value_balance: Option>, ) -> Result<(), ValidateContextError> where C: IntoIterator, @@ -83,6 +91,55 @@ where .expect("valid blocks have a coinbase height"); check::height_one_more_than_parent_height(parent_height, semantically_verified.height)?; + if semantically_verified.height > network.slow_start_interval() { + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(semantically_verified.height, network)?; + + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = { + let money_reserve = if semantically_verified.height > 1.try_into().unwrap() { + pool_value_balance + .expect("a chain must contain valid pool value balance") + .money_reserve() + } else { + MAX_MONEY.try_into().unwrap() + }; + general::block_subsidy(semantically_verified.height, network, money_reserve)? + }; + + subsidy_is_valid( + &semantically_verified.block, + network, + expected_block_subsidy, + )?; + + // TODO: Add link to lockbox stream ZIP + let expected_deferred_amount = funding_streams::funding_stream_values( + semantically_verified.height, + network, + expected_block_subsidy, + ) + .expect("we always expect a funding stream hashmap response even if empty") + .remove(&FundingStreamReceiver::Deferred) + .unwrap_or_default(); + + semantically_verified.deferred_balance = Some(expected_deferred_amount); + + let coinbase_tx = coinbase_is_first(&semantically_verified.block)?; + + check::transaction_miner_fees_are_valid( + &coinbase_tx, + semantically_verified.height, + semantically_verified + .block_miner_fees + .expect("block must have miner fees calculated"), + expected_block_subsidy, + expected_deferred_amount, + network, + )?; + } + // skip this check during tests if we don't have enough blocks in the chain // process_queued also checks the chain length, so we can skip this assertion during testing // (tests that want to check this code should use the correct number of blocks) @@ -388,7 +445,7 @@ where pub(crate) fn initial_contextual_validity( finalized_state: &ZebraDb, non_finalized_state: &NonFinalizedState, - semantically_verified: &SemanticallyVerifiedBlock, + semantically_verified: &mut SemanticallyVerifiedBlock, ) -> Result<(), ValidateContextError> { let relevant_chain = any_ancestor_blocks( non_finalized_state, @@ -396,15 +453,261 @@ pub(crate) fn initial_contextual_validity( semantically_verified.block.header.previous_block_hash, ); + let pool_value_balance = non_finalized_state + .best_chain() + .map(|chain| chain.chain_value_pools) + .or_else(|| { + finalized_state + .finalized_tip_height() + .filter(|x| (*x + 1).unwrap() == semantically_verified.height) + .map(|_| finalized_state.finalized_value_pool()) + }); + // Security: check proof of work before any other checks check::block_is_valid_for_recent_chain( semantically_verified, &non_finalized_state.network, finalized_state.finalized_tip_height(), relevant_chain, + pool_value_balance, )?; check::nullifier::no_duplicates_in_finalized_chain(semantically_verified, finalized_state)?; Ok(()) } + +/// Checks if there is exactly one coinbase transaction in `Block`, +/// and if that coinbase transaction is the first transaction in the block. +/// Returns the coinbase transaction is successful. +/// +/// > A transaction that has a single transparent input with a null prevout field, +/// > is called a coinbase transaction. Every block has a single coinbase +/// > transaction as the first transaction in the block. +/// +/// +pub fn coinbase_is_first( + block: &Block, +) -> Result, CoinbaseTransactionError> { + // # Consensus + // + // > A block MUST have at least one transaction + // + // + let first = block + .transactions + .first() + .ok_or(BlockError::NoTransactions)?; + // > The first transaction in a block MUST be a coinbase transaction, + // > and subsequent transactions MUST NOT be coinbase transactions. + // + // + // + // > A transaction that has a single transparent input with a null prevout + // > field, is called a coinbase transaction. + // + // + let mut rest = block.transactions.iter().skip(1); + if !first.is_coinbase() { + Err(CoinbaseTransactionError::Position)?; + } + // > A transparent input in a non-coinbase transaction MUST NOT have a null prevout + // + // + if !rest.all(|tx| tx.is_valid_non_coinbase()) { + Err(CoinbaseTransactionError::AfterFirst)?; + } + + Ok(first.clone()) +} + +/// Returns `Ok(())` if the block subsidy in `block` is valid for `network` +/// +/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts +pub fn subsidy_is_valid( + block: &Block, + network: &Network, + expected_block_subsidy: Amount, +) -> Result<(), SubsidyError> { + let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?; + let coinbase = block.transactions.first().ok_or(SubsidyError::NoCoinbase)?; + + // Validate funding streams + let Some(halving_div) = general::halving_divisor(height, network) else { + // Far future halving, with no founders reward or funding streams + return Ok(()); + }; + + let canopy_activation_height = NetworkUpgrade::Canopy + .activation_height(network) + .expect("Canopy activation height is known"); + + let slow_start_interval = network.slow_start_interval(); + + if height < slow_start_interval { + unreachable!( + "unsupported block height: callers should handle blocks below {:?}", + slow_start_interval + ) + } else if halving_div.count_ones() != 1 { + unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two") + } else if height < canopy_activation_height { + // Founders rewards are paid up to Canopy activation, on both mainnet and testnet. + // But we checkpoint in Canopy so founders reward does not apply for Zebra. + unreachable!("we cannot verify consensus rules before Canopy activation"); + } else if halving_div < 8 { + // Funding streams are paid from Canopy activation to the second halving + // Note: Canopy activation is at the first halving on mainnet, but not on testnet + // ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet + // funding stream amount values + let funding_streams = + funding_streams::funding_stream_values(height, network, expected_block_subsidy) + // we always expect a funding stream hashmap response even if empty + .map_err(|err| SubsidyError::Other(err.to_string()))?; + + // # Consensus + // + // > [Canopy onward] The coinbase transaction at block height `height` + // > MUST contain at least one output per funding stream `fs` active at `height`, + // > that pays `fs.Value(height)` zatoshi in the prescribed way to the stream's + // > recipient address represented by `fs.AddressList[fs.AddressIndex(height)] + // + // https://zips.z.cash/protocol/protocol.pdf#fundingstreams + for (receiver, expected_amount) in funding_streams { + if receiver == FundingStreamReceiver::Deferred { + // The deferred pool contribution is checked in `miner_fees_are_valid()` + // See [ZIP-1015](https://zips.z.cash/zip-1015) for more details. + continue; + } + + let address = funding_streams::funding_stream_address(height, network, receiver) + // funding stream receivers other than the deferred pool must have an address + .ok_or_else(|| { + SubsidyError::Other(format!( + "missing funding stream address at height {height:?}" + )) + })?; + + let has_expected_output = funding_streams::filter_outputs_by_address(coinbase, address) + .iter() + .map(zebra_chain::transparent::Output::value) + .any(|value| value == expected_amount); + + if !has_expected_output { + Err(SubsidyError::FundingStreamNotFound)?; + } + } + Ok(()) + } else { + // Future halving, with no founders reward or funding streams + Ok(()) + } +} + +/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// +/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus +pub fn transaction_miner_fees_are_valid( + coinbase_tx: &transaction::Transaction, + height: Height, + block_miner_fees: Amount, + expected_block_subsidy: Amount, + expected_deferred_amount: Amount, + network: &Network, +) -> Result<(), SubsidyError> { + let network_upgrade = NetworkUpgrade::current(network, height); + let transparent_value_balance = general::output_amounts(coinbase_tx) + .iter() + .sum::, AmountError>>() + .map_err(|_| SubsidyError::SumOverflow)? + .constrain() + .expect("positive value always fit in `NegativeAllowed`"); + let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount(); + let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount(); + + // Coinbase transaction can still have a ZSF deposit + #[cfg(zcash_unstable = "nsm")] + let burn_amount = coinbase_tx + .burn_amount() + .constrain() + .expect("positive value always fit in `NegativeAllowed`"); + + miner_fees_are_valid( + transparent_value_balance, + sapling_value_balance, + orchard_value_balance, + #[cfg(zcash_unstable = "nsm")] + burn_amount, + expected_block_subsidy, + block_miner_fees, + expected_deferred_amount, + network_upgrade, + ) +} + +/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// +/// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus +#[allow(clippy::too_many_arguments)] +pub fn miner_fees_are_valid( + transparent_value_balance: Amount, + sapling_value_balance: Amount, + orchard_value_balance: Amount, + #[cfg(zcash_unstable = "nsm")] burn_amount: Amount, + expected_block_subsidy: Amount, + block_miner_fees: Amount, + expected_deferred_amount: Amount, + network_upgrade: NetworkUpgrade, +) -> Result<(), SubsidyError> { + // TODO: Update the quote below once its been updated for NU6. + // + // # Consensus + // + // > The total value in zatoshi of transparent outputs from a coinbase transaction, + // > minus vbalanceSapling, minus vbalanceOrchard, MUST NOT be greater than the value + // > in zatoshi of block subsidy plus the transaction fees paid by transactions in this block. + // + // https://zips.z.cash/protocol/protocol.pdf#txnconsensus + // + // The expected lockbox funding stream output of the coinbase transaction is also subtracted + // from the block subsidy value plus the transaction fees paid by transactions in this block. + #[cfg(zcash_unstable = "nsm")] + let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance + + burn_amount) + .map_err(|_| SubsidyError::SumOverflow)?; + #[cfg(not(zcash_unstable = "nsm"))] + let left = (transparent_value_balance - sapling_value_balance - orchard_value_balance) + .map_err(|_| SubsidyError::SumOverflow)?; + let right = (expected_block_subsidy + block_miner_fees - expected_deferred_amount) + .map_err(|_| SubsidyError::SumOverflow)?; + + // TODO: Updadte the quotes below if the final phrasing changes in the spec for NU6. + // + // # Consensus + // + // > [Pre-NU6] The total output of a coinbase transaction MUST NOT be greater than its total + // input. + // + // > [NU6 onward] The total output of a coinbase transaction MUST be equal to its total input. + let block_before_nu6 = network_upgrade < NetworkUpgrade::Nu6; + let miner_fees_valid = if block_before_nu6 { + left <= right + } else { + left == right + }; + + if !miner_fees_valid { + Err(SubsidyError::InvalidMinerFees)? + }; + + // Verify that the NSM burn amount is at least the minimum required amount (ZIP-235). + #[cfg(zcash_unstable = "nsm")] + if network_upgrade == NetworkUpgrade::ZFuture { + let minimum_burn_amount = ((block_miner_fees * 6).unwrap() / 10).unwrap(); + if burn_amount < minimum_burn_amount { + Err(SubsidyError::InvalidBurnAmount)? + } + } + + Ok(()) +} diff --git a/zebra-state/src/service/check/tests/anchors.rs b/zebra-state/src/service/check/tests/anchors.rs index 09d33b29190..bf29d8edb02 100644 --- a/zebra-state/src/service/check/tests/anchors.rs +++ b/zebra-state/src/service/check/tests/anchors.rs @@ -53,7 +53,7 @@ fn check_sprout_anchors() { .expect("block should deserialize"); // Add initial transactions to [`block_1`]. - let block_1 = prepare_sprout_block(block_1, block_395); + let mut block_1 = prepare_sprout_block(block_1, block_395); // Create a block at height == 2 that references the Sprout note commitment tree state // from [`block_1`]. @@ -68,7 +68,7 @@ fn check_sprout_anchors() { .expect("block should deserialize"); // Add the transactions with the first anchors to [`block_2`]. - let block_2 = prepare_sprout_block(block_2, block_396); + let mut block_2 = prepare_sprout_block(block_2, block_396); let unmined_txs: Vec<_> = block_2 .block @@ -98,7 +98,7 @@ fn check_sprout_anchors() { assert!(validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block_1 + &mut block_1 ) .is_ok()); @@ -114,7 +114,11 @@ fn check_sprout_anchors() { // Validate and commit [`block_2`]. This will also check the anchors. assert_eq!( - validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block_2), + validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + &mut block_2 + ), Ok(()) ); } @@ -249,7 +253,7 @@ fn check_sapling_anchors() { })) }); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); // Create a block at height == 2 that references the Sapling note commitment tree state // from earlier block @@ -295,7 +299,7 @@ fn check_sapling_anchors() { })) }); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let unmined_txs: Vec<_> = block2 .block @@ -320,7 +324,7 @@ fn check_sapling_anchors() { assert!(validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ) .is_ok()); @@ -335,7 +339,11 @@ fn check_sapling_anchors() { assert!(check_unmined_tx_anchors_result.is_ok()); assert_eq!( - validate_and_commit_non_finalized(&finalized_state.db, &mut non_finalized_state, block2), + validate_and_commit_non_finalized( + &finalized_state.db, + &mut non_finalized_state, + &mut block2 + ), Ok(()) ); } diff --git a/zebra-state/src/service/check/tests/nullifier.rs b/zebra-state/src/service/check/tests/nullifier.rs index 0392f1c8e79..ae6c4f4b7fd 100644 --- a/zebra-state/src/service/check/tests/nullifier.rs +++ b/zebra-state/src/service/check/tests/nullifier.rs @@ -104,7 +104,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); // the block was committed @@ -154,11 +154,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // if the random proptest data produces other errors, @@ -215,11 +215,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -276,11 +276,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -368,7 +368,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -383,11 +383,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( @@ -463,7 +463,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -508,11 +508,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -564,11 +564,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -647,7 +647,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -661,11 +661,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( @@ -743,7 +743,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -789,11 +789,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -849,11 +849,11 @@ proptest! { let previous_mem = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); prop_assert_eq!( @@ -936,7 +936,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); prop_assert_eq!(commit_result, Ok(())); @@ -949,11 +949,11 @@ proptest! { previous_mem = non_finalized_state.clone(); } - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); prop_assert_eq!( diff --git a/zebra-state/src/service/check/tests/utxo.rs b/zebra-state/src/service/check/tests/utxo.rs index acdc2d399a7..cde6317b20e 100644 --- a/zebra-state/src/service/check/tests/utxo.rs +++ b/zebra-state/src/service/check/tests/utxo.rs @@ -203,7 +203,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone() + &mut block1.clone() ); // the block was committed @@ -289,7 +289,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2.clone() + &mut block2.clone() ); // the block was committed @@ -364,11 +364,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -428,11 +428,11 @@ proptest! { block2.transactions.push(spend_transaction.into()); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); // the block was rejected @@ -513,11 +513,11 @@ proptest! { .transactions .extend([spend_transaction1.into(), spend_transaction2.into()]); - let block2 = Arc::new(block2).prepare(); + let mut block2 = Arc::new(block2).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2 + &mut block2 ); // the block was rejected @@ -630,7 +630,7 @@ proptest! { let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block2.clone() + &mut block2.clone() ); // the block was committed @@ -663,11 +663,11 @@ proptest! { previous_non_finalized_state = non_finalized_state.clone(); } - let block3 = Arc::new(block3).prepare(); + let mut block3 = Arc::new(block3).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block3 + &mut block3 ); // the block was rejected @@ -739,11 +739,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -806,11 +806,11 @@ proptest! { let (finalized_state, mut non_finalized_state, genesis) = new_state_with_mainnet_genesis(); let previous_non_finalized_state = non_finalized_state.clone(); - let block1 = Arc::new(block1).prepare(); + let mut block1 = Arc::new(block1).prepare(); let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1 + &mut block1 ); // the block was rejected @@ -908,7 +908,7 @@ fn new_state_with_mainnet_transparent_data( let commit_result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - block1.clone(), + &mut block1.clone(), ); // the block was committed diff --git a/zebra-state/src/service/finalized_state/disk_db.rs b/zebra-state/src/service/finalized_state/disk_db.rs index 69be5a4585f..30a3f17b1d8 100644 --- a/zebra-state/src/service/finalized_state/disk_db.rs +++ b/zebra-state/src/service/finalized_state/disk_db.rs @@ -990,7 +990,7 @@ impl DiskDb { // // We don't attempt to guard against malicious symlinks created by attackers // (TOCTOU attacks). Zebra should not be run with elevated privileges. - if !old_path.starts_with(&cache_path) { + if !old_path.starts_with(cache_path) { info!("skipped reusing previous state cache: state is outside cache directory"); return; } diff --git a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs index 194f2202a87..363d655f153 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/block/tests/vectors.rs @@ -137,6 +137,7 @@ fn test_block_db_round_trip_with( new_outputs, transaction_hashes, deferred_balance: None, + block_miner_fees: None, }) }; diff --git a/zebra-state/src/service/non_finalized_state/chain.rs b/zebra-state/src/service/non_finalized_state/chain.rs index 12ee0528776..e46e704459e 100644 --- a/zebra-state/src/service/non_finalized_state/chain.rs +++ b/zebra-state/src/service/non_finalized_state/chain.rs @@ -1524,6 +1524,21 @@ impl Chain { V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", ), + #[cfg(zcash_unstable = "nsm")] + ZFuture { + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + .. + } => ( + inputs, + outputs, + &None, + &None, + sapling_shielded_data, + orchard_shielded_data, + ) }; // add key `transaction.hash` and value `(height, tx_index)` to `tx_loc_by_hash` @@ -1685,6 +1700,21 @@ impl UpdateWith for Chain { V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", ), + #[cfg(zcash_unstable = "nsm")] + ZFuture { + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + .. + } => ( + inputs, + outputs, + &None, + &None, + sapling_shielded_data, + orchard_shielded_data, + ), }; // remove the utxos this produced diff --git a/zebra-state/src/service/write.rs b/zebra-state/src/service/write.rs index acbc5c14ce1..9223fd046d8 100644 --- a/zebra-state/src/service/write.rs +++ b/zebra-state/src/service/write.rs @@ -7,7 +7,13 @@ use tokio::sync::{ }; use zebra_chain::{ - block::{self, Height}, + amount::MAX_MONEY, + block::{ + self, + subsidy::{funding_streams::funding_stream_values, general}, + Height, + }, + parameters::subsidy::FundingStreamReceiver, transparent::EXTRA_ZEBRA_COINBASE_DATA, }; @@ -49,15 +55,15 @@ const PARENT_ERROR_MAP_LIMIT: usize = MAX_BLOCK_REORG_HEIGHT as usize * 2; pub(crate) fn validate_and_commit_non_finalized( finalized_state: &ZebraDb, non_finalized_state: &mut NonFinalizedState, - prepared: SemanticallyVerifiedBlock, + prepared: &mut SemanticallyVerifiedBlock, ) -> Result<(), CommitSemanticallyVerifiedError> { - check::initial_contextual_validity(finalized_state, non_finalized_state, &prepared)?; + check::initial_contextual_validity(finalized_state, non_finalized_state, prepared)?; let parent_hash = prepared.block.header.previous_block_hash; if finalized_state.finalized_tip_hash() == parent_hash { - non_finalized_state.commit_new_chain(prepared, finalized_state)?; + non_finalized_state.commit_new_chain(prepared.clone(), finalized_state)?; } else { - non_finalized_state.commit_block(prepared, finalized_state)?; + non_finalized_state.commit_block(prepared.clone(), finalized_state)?; } Ok(()) @@ -144,7 +150,7 @@ pub fn write_blocks_from_channels( // Write all the finalized blocks sent by the state, // until the state closes the finalized block channel's sender. - while let Some(ordered_block) = finalized_block_write_receiver.blocking_recv() { + while let Some(mut ordered_block) = finalized_block_write_receiver.blocking_recv() { // TODO: split these checks into separate functions if invalid_block_reset_sender.is_closed() { @@ -164,7 +170,8 @@ pub fn write_blocks_from_channels( .map(|height| (height + 1).expect("committed heights are valid")) .unwrap_or(Height(0)); - if ordered_block.0.height != next_valid_height { + let ordered_block_height = ordered_block.0.height; + if ordered_block_height != next_valid_height { debug!( ?next_valid_height, invalid_height = ?ordered_block.0.height, @@ -178,6 +185,35 @@ pub fn write_blocks_from_channels( continue; } + let network = finalized_state.network(); + // We can't get the block subsidy for blocks with heights in the slow start interval, so we + // omit the calculation of the expected deferred amount. + let expected_deferred_amount = if ordered_block_height > network.slow_start_interval() { + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = + general::block_subsidy_pre_nsm(ordered_block_height, &network) + .expect("valid block subsidy"); + + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = { + let money_reserve = if ordered_block_height > 1.try_into().unwrap() { + finalized_state.db.finalized_value_pool().money_reserve() + } else { + MAX_MONEY.try_into().unwrap() + }; + general::block_subsidy(ordered_block_height, &network, money_reserve) + .expect("valid block subsidy") + }; + + // TODO: Add link to lockbox stream ZIP + funding_stream_values(ordered_block_height, &network, expected_block_subsidy) + .expect("we always expect a funding stream hashmap response even if empty") + .remove(&FundingStreamReceiver::Deferred) + } else { + None + }; + ordered_block.0.deferred_balance = expected_deferred_amount; + // Try committing the block match finalized_state .commit_finalized(ordered_block, prev_finalized_note_commitment_trees.take()) @@ -224,7 +260,8 @@ pub fn write_blocks_from_channels( // Save any errors to propagate down to queued child blocks let mut parent_error_map: IndexMap = IndexMap::new(); - while let Some((queued_child, rsp_tx)) = non_finalized_block_write_receiver.blocking_recv() { + while let Some((mut queued_child, rsp_tx)) = non_finalized_block_write_receiver.blocking_recv() + { let child_hash = queued_child.hash; let parent_hash = queued_child.block.header.previous_block_hash; let parent_error = parent_error_map.get(&parent_hash); @@ -248,7 +285,7 @@ pub fn write_blocks_from_channels( result = validate_and_commit_non_finalized( &finalized_state.db, &mut non_finalized_state, - queued_child, + &mut queued_child, ) .map_err(CloneError::from); } diff --git a/zebra-state/src/tests.rs b/zebra-state/src/tests.rs index 488ab4227bd..bd7a6b661cf 100644 --- a/zebra-state/src/tests.rs +++ b/zebra-state/src/tests.rs @@ -34,6 +34,8 @@ impl FakeChainHelper for Arc { Transaction::V3 { inputs, .. } => &mut inputs[0], Transaction::V4 { inputs, .. } => &mut inputs[0], Transaction::V5 { inputs, .. } => &mut inputs[0], + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { inputs, .. } => &mut inputs[0], }; match input { diff --git a/zebrad/Cargo.toml b/zebrad/Cargo.toml index 768f68dfcb3..393d668d28d 100644 --- a/zebrad/Cargo.toml +++ b/zebrad/Cargo.toml @@ -156,6 +156,8 @@ test_sync_to_mandatory_checkpoint_testnet = [] test_sync_past_mandatory_checkpoint_mainnet = [] test_sync_past_mandatory_checkpoint_testnet = [] +nsm = ["zebra-chain/nsm", "zebra-network/nsm", "zebra-consensus/nsm", "zebra-rpc/nsm"] + [dependencies] zebra-chain = { path = "../zebra-chain", version = "1.0.0-beta.42" } zebra-consensus = { path = "../zebra-consensus", version = "1.0.0-beta.42" } @@ -299,4 +301,4 @@ zebra-grpc = { path = "../zebra-grpc", version = "0.1.0-alpha.9" } zebra-utils = { path = "../zebra-utils", version = "1.0.0-beta.42" } [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)', 'cfg(zcash_unstable, values("nsm"))'] } diff --git a/zebrad/src/components/mempool/storage/tests/prop.rs b/zebrad/src/components/mempool/storage/tests/prop.rs index eca65935acb..8b56baee93e 100644 --- a/zebrad/src/components/mempool/storage/tests/prop.rs +++ b/zebrad/src/components/mempool/storage/tests/prop.rs @@ -568,6 +568,9 @@ impl SpendConflictTestInput { // No JoinSplits Transaction::V1 { .. } | Transaction::V5 { .. } => {} + + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { .. } => {} } } } @@ -638,6 +641,14 @@ impl SpendConflictTestInput { Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts) } + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + sapling_shielded_data, + .. + } => { + Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts) + } + // No Spends Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {} } @@ -709,6 +720,12 @@ impl SpendConflictTestInput { .. } => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts), + #[cfg(zcash_unstable = "nsm")] + Transaction::ZFuture { + orchard_shielded_data, + .. + } => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts), + // No Spends Transaction::V1 { .. } | Transaction::V2 { .. } diff --git a/zebrad/src/components/miner.rs b/zebrad/src/components/miner.rs index cb32cc91981..d9240abe71f 100644 --- a/zebrad/src/components/miner.rs +++ b/zebrad/src/components/miner.rs @@ -258,6 +258,8 @@ where capabilities: vec![LongPoll, CoinbaseTxn], long_poll_id: None, _work_id: None, + #[cfg(zcash_unstable = "nsm")] + burn_amount: None, }; // Shut down the task when all the template receivers are dropped, or Zebra shuts down. diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index cd3572ce3f2..18704a8b369 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -2979,7 +2979,7 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { primitives::byte_array::increment_big_endian, }; use zebra_rpc::methods::GetBlockHash; - use zebra_state::{ReadResponse, Response}; + use zebra_state::{ReadResponse, Response, SemanticallyVerifiedBlock}; let _init_guard = zebra_test::init(); let mut config = os_assigned_rpc_port_config(false, &Network::new_regtest(None, None))?; @@ -3108,10 +3108,12 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { .bytes_in_serialized_order() .into(); + let mut semantically_verified: SemanticallyVerifiedBlock = Arc::new(block.clone()).into(); + semantically_verified.block_miner_fees = Some(0.try_into().unwrap()); let Response::Committed(block_hash) = state2 .clone() .oneshot(zebra_state::Request::CommitSemanticallyVerifiedBlock( - Arc::new(block.clone()).into(), + semantically_verified, )) .await .map_err(|err| eyre!(err))? @@ -3245,6 +3247,8 @@ async fn trusted_chain_sync_handles_forks_correctly() -> Result<()> { #[cfg(feature = "getblocktemplate-rpcs")] async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { use zebra_chain::{ + amount::MAX_MONEY, + block::subsidy::general, chain_sync_status::MockSyncStatus, parameters::{ subsidy::{FundingStreamReceiver, FUNDING_STREAM_MG_ADDRESSES_TESTNET}, @@ -3284,6 +3288,8 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { .with_slow_start_interval(Height::MIN) .with_activation_heights(ConfiguredActivationHeights { nu6: Some(1), + #[cfg(zcash_unstable = "nsm")] + zfuture: Some(2), ..Default::default() }); @@ -3440,14 +3446,27 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { }) .to_network(); + let block_height = Height(block_template.height); + #[cfg(zcash_unstable = "nsm")] + let expected_block_subsidy = general::block_subsidy( + block_height, + &network, + MAX_MONEY.try_into().expect("MAX_MONEY is a valid amount"), + )?; + #[cfg(not(zcash_unstable = "nsm"))] + let expected_block_subsidy = general::block_subsidy_pre_nsm(block_height, &network)?; + let (coinbase_txn, default_roots) = generate_coinbase_and_roots( &network, - Height(block_template.height), + block_height, &miner_address, &[], history_tree.clone(), true, vec![], + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + None, ); let block_template = GetBlockTemplate { @@ -3491,6 +3510,9 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { history_tree.clone(), true, vec![], + expected_block_subsidy, + #[cfg(zcash_unstable = "nsm")] + None, ); let block_template = GetBlockTemplate {