diff --git a/Cargo.lock b/Cargo.lock index cbe22c32198..c26b69fd3b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7720,6 +7720,7 @@ dependencies = [ "proptest", "proptest-derive", "rand 0.8.6", + "rand_core 0.6.4", "rayon", "sapling-crypto", "serde", @@ -7733,8 +7734,10 @@ dependencies = [ "tracing-error", "tracing-futures", "tracing-subscriber", + "zcash_primitives", "zcash_proofs", "zcash_protocol", + "zcash_transparent", "zebra-chain", "zebra-node-services", "zebra-script", diff --git a/Cargo.toml b/Cargo.toml index 41c1f6bf19d..cdc0acc074e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -325,7 +325,7 @@ debug = false # The linter should ignore these expected config flags/values unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(tokio_unstable)', # Used by tokio-console - 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7", "zip235"))' # Used in Zebra and librustzcash + 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7", "zip235", "nsm"))' # Used in Zebra and librustzcash ] } # High-risk code diff --git a/zebra-chain/src/block.rs b/zebra-chain/src/block.rs index 2fa724939d1..c1caedb2142 100644 --- a/zebra-chain/src/block.rs +++ b/zebra-chain/src/block.rs @@ -225,22 +225,31 @@ impl Block { /// UTXOs, which are ignored. /// /// Note that the chain value pool has the opposite sign to the transaction value pool. + /// + /// The Long-Term Support (NSM / ZIP-234) pool delta is **not** set here. + /// The `lts` leg of the returned [`ValueBalance`] is left at zero; + /// callers that track the LTS pool must derive the signed implicit + /// coinbase claim and call [`set_lts_amount`] on the result. + /// + /// [`set_lts_amount`]: crate::value_balance::ValueBalance::set_lts_amount pub fn chain_value_pool_change( &self, utxos: &HashMap, deferred_pool_balance_change: Option, ) -> Result, ValueBalanceError> { - Ok(*self + let mut value_balance = self .transactions .iter() .flat_map(|t| t.value_balance(utxos)) .sum::, _>>()? - .neg() - .set_deferred_amount( - deferred_pool_balance_change - .map(DeferredPoolBalanceChange::value) - .unwrap_or_default(), - )) + .neg(); + value_balance.set_deferred_amount( + deferred_pool_balance_change + .map(DeferredPoolBalanceChange::value) + .unwrap_or_default(), + ); + + Ok(value_balance) } /// Compute the root of the authorizing data Merkle tree, diff --git a/zebra-chain/src/lib.rs b/zebra-chain/src/lib.rs index 18296210f7e..a1b65f2ad5d 100644 --- a/zebra-chain/src/lib.rs +++ b/zebra-chain/src/lib.rs @@ -9,6 +9,14 @@ // Required by bitvec! macro #![recursion_limit = "256"] +#[cfg(all( + zcash_unstable = "nsm", + not(all(zcash_unstable = "nu7", zcash_unstable = "zip235")) +))] +compile_error!( + "zcash_unstable=\"nsm\" requires zcash_unstable=\"nu7\" and zcash_unstable=\"zip235\"" +); + #[macro_use] extern crate bitflags; diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index dbef6504ef3..6f5edfc148c 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -294,6 +294,14 @@ impl Network { .expect("Sapling activation height needs to be set") } + /// Returns the height where V4 transactions stop being accepted. + pub fn v4_deprecation_height(&self) -> Option { + match self { + Self::Mainnet => None, + Self::Testnet(params) => params.v4_deprecation_height(), + } + } + /// Returns the expected total value of the sum of all NU6.1 one-time lockbox disbursement output values for this network at /// the provided height. pub fn lockbox_disbursement_total_amount(&self, height: Height) -> Amount { diff --git a/zebra-chain/src/parameters/network/error.rs b/zebra-chain/src/parameters/network/error.rs index 1a05d66826c..f27bc719ed7 100644 --- a/zebra-chain/src/parameters/network/error.rs +++ b/zebra-chain/src/parameters/network/error.rs @@ -55,6 +55,10 @@ pub enum ParametersBuilderError { #[non_exhaustive] Nu7RequiresUnstableCfg, + #[error("V4 deprecation height must be after NU7 activation height")] + #[non_exhaustive] + InvalidV4DeprecationHeight, + #[error("difficulty limits are valid expanded values")] #[non_exhaustive] InvaildDifficultyLimits, diff --git a/zebra-chain/src/parameters/network/subsidy.rs b/zebra-chain/src/parameters/network/subsidy.rs index 1fc468cae7c..63b606e07b8 100644 --- a/zebra-chain/src/parameters/network/subsidy.rs +++ b/zebra-chain/src/parameters/network/subsidy.rs @@ -540,6 +540,92 @@ pub fn founders_reward_address(net: &Network, height: Height) -> Option Option { + let nu7 = NetworkUpgrade::Nu7.activation_height(network)?; + let target_halving = halving(nu7, network).checked_add(2)?; + height_for_halving(target_halving, network) +} + +/// Numerator of the per-block LTS payout fraction (ZIP-234 smooth issuance). +#[cfg(zcash_unstable = "nsm")] +const LTS_PAYOUT_FRACTION_NUMERATOR: u64 = 4_126; + +/// Denominator of the per-block LTS payout fraction (ZIP-234 smooth issuance). +#[cfg(zcash_unstable = "nsm")] +const LTS_PAYOUT_FRACTION_DENOMINATOR: u64 = 10_000_000_000; + +/// Per-block LTS payout for `height`, given the LTS pool snapshot at the +/// parent block. +/// +/// Returns `Amount::zero()` before [`lts_disbursement_start`], when NU7 is +/// unconfigured, or when `parent_pool` is empty. +/// +/// Inside the disbursement window the payout is recomputed every block as a +/// ZIP-234 smooth-issuance ceiling fraction of the parent LTS pool: +/// +/// ```text +/// payout = ceil(parent_pool * LTS_PAYOUT_FRACTION_NUMERATOR +/// / LTS_PAYOUT_FRACTION_DENOMINATOR) +/// payout = min(payout, parent_pool) +/// ``` +/// +/// The current block's own LTS contributions don't affect the current block's +/// payout — they enter the pool in this block and only affect the next +/// block. This parent-pool rule avoids a circular dependency between the +/// coinbase output, implicit ZIP-235 deposits, transaction fees, and the +/// state transition for the same block. Halving boundaries have no special +/// effect on the LTS payout rate after disbursement begins. +/// +/// Ceiling division mirrors ZIP-234 and drains a one-zatoshi pool in one +/// block, so no separate dust rule is needed. +#[cfg(zcash_unstable = "nsm")] +pub fn lts_payout( + height: Height, + network: &Network, + parent_pool: Amount, +) -> Amount { + let Some(start) = lts_disbursement_start(network) else { + return Amount::zero(); + }; + + if height < start { + return Amount::zero(); + } + + let parent_pool_u = u64::from(parent_pool); + if parent_pool_u == 0 { + return Amount::zero(); + } + + // u128 keeps the consensus math obvious: parent_pool fits in u64, and the + // numerator multiplication can't overflow u128 for any plausible pool. + let numerator = u128::from(parent_pool_u) * u128::from(LTS_PAYOUT_FRACTION_NUMERATOR); + let payout = numerator.div_ceil(u128::from(LTS_PAYOUT_FRACTION_DENOMINATOR)); + let capped = payout.min(u128::from(parent_pool_u)); + + // capped ≤ parent_pool ≤ u64::MAX, so the conversion through u64 and into + // Amount can't fail. + Amount::try_from(u64::try_from(capped).expect("capped ≤ parent_pool ≤ u64::MAX")) + .expect("capped LTS payout fits in Amount because it is at most parent_pool") +} + /// `FoundersReward(height)` as described in [§7.8]. /// /// [§7.8]: diff --git a/zebra-chain/src/parameters/network/testnet.rs b/zebra-chain/src/parameters/network/testnet.rs index e625cba0133..f799013e064 100644 --- a/zebra-chain/src/parameters/network/testnet.rs +++ b/zebra-chain/src/parameters/network/testnet.rs @@ -455,6 +455,8 @@ pub struct ParametersBuilder { genesis_hash: block::Hash, /// The network upgrade activation heights for this network, see [`Parameters::activation_heights`] for more details. activation_heights: BTreeMap, + /// The height at which V4 transactions are no longer accepted. + v4_deprecation_height: Option, /// Slow start interval for this network slow_start_interval: Height, /// Funding streams for this network @@ -492,6 +494,7 @@ impl Default for ParametersBuilder { // // `Genesis` network upgrade activation height must always be 0 activation_heights: TESTNET_ACTIVATION_HEIGHTS.iter().cloned().collect(), + v4_deprecation_height: None, genesis_hash: TESTNET_GENESIS_HASH .parse() .expect("hard-coded hash parses"), @@ -678,6 +681,17 @@ impl ParametersBuilder { Ok(self) } + /// Sets the height where V4 transactions stop being accepted. + pub fn with_v4_deprecation_height(mut self, height: Height) -> Self { + self.v4_deprecation_height = Some(height); + self + } + + fn with_optional_v4_deprecation_height(mut self, height: Option) -> Self { + self.v4_deprecation_height = height; + self + } + /// Sets the slow start interval to be used in the [`Parameters`] being built. pub fn with_slow_start_interval(mut self, slow_start_interval: Height) -> Self { self.slow_start_interval = slow_start_interval; @@ -832,6 +846,7 @@ impl ParametersBuilder { network_magic, genesis_hash, activation_heights, + v4_deprecation_height, slow_start_interval, funding_streams, should_lock_funding_stream_address_period: _, @@ -849,6 +864,7 @@ impl ParametersBuilder { network_magic, genesis_hash, activation_heights, + v4_deprecation_height, slow_start_interval, slow_start_shift: Height(slow_start_interval.0 / 2), funding_streams, @@ -868,10 +884,29 @@ impl ParametersBuilder { Network::new_configured_testnet(self.clone().finish()) } + fn validate_v4_deprecation_height(&self) -> Result<(), ParametersBuilderError> { + let Some(v4_deprecation_height) = self.v4_deprecation_height else { + return Ok(()); + }; + + let network = self.to_network_unchecked(); + let Some(nu7_activation_height) = NetworkUpgrade::Nu7.activation_height(&network) else { + return Err(ParametersBuilderError::InvalidV4DeprecationHeight); + }; + + if v4_deprecation_height <= nu7_activation_height { + return Err(ParametersBuilderError::InvalidV4DeprecationHeight); + } + + Ok(()) + } + /// Checks funding streams and converts the builder to a configured [`Network::Testnet`] pub fn to_network(self) -> Result { let network = self.to_network_unchecked(); + self.validate_v4_deprecation_height()?; + // Final check that the configured funding streams will be valid for these Testnet parameters. for fs in &self.funding_streams { // Check that the funding streams are valid for the configured Testnet parameters. @@ -896,6 +931,7 @@ impl ParametersBuilder { network_magic, genesis_hash, activation_heights, + v4_deprecation_height, slow_start_interval, funding_streams, should_lock_funding_stream_address_period: _, @@ -910,6 +946,7 @@ impl ParametersBuilder { } = Self::default(); self.activation_heights == activation_heights + && self.v4_deprecation_height == v4_deprecation_height && self.network_magic == network_magic && self.genesis_hash == genesis_hash && self.slow_start_interval == slow_start_interval @@ -929,6 +966,8 @@ impl ParametersBuilder { pub struct RegtestParameters { /// The configured network upgrade activation heights to use on Regtest pub activation_heights: ConfiguredActivationHeights, + /// The height at which V4 transactions are no longer accepted on Regtest. + pub v4_deprecation_height: Option, /// Configured funding streams pub funding_streams: Option>, /// Expected one-time lockbox disbursement outputs in NU6.1 activation block coinbase for Regtest @@ -959,6 +998,8 @@ pub struct Parameters { genesis_hash: block::Hash, /// The network upgrade activation heights for this network. activation_heights: BTreeMap, + /// The height at which V4 transactions are no longer accepted. + v4_deprecation_height: Option, /// Slow start interval for this network slow_start_interval: Height, /// Slow start shift for this network, always half the slow start interval @@ -1020,6 +1061,7 @@ impl Parameters { pub fn new_regtest( RegtestParameters { activation_heights, + v4_deprecation_height, funding_streams, lockbox_disbursements, checkpoints, @@ -1036,11 +1078,14 @@ impl Parameters { // Removes default Testnet activation heights if not configured, // most network upgrades are disabled by default for Regtest in zcashd .with_activation_heights(activation_heights.for_regtest())? + .with_optional_v4_deprecation_height(v4_deprecation_height) .with_halving_interval(PRE_BLOSSOM_REGTEST_HALVING_INTERVAL)? .with_funding_streams(funding_streams.unwrap_or_default()) .with_lockbox_disbursements(lockbox_disbursements.unwrap_or_default()) .with_checkpoints(checkpoints.unwrap_or_default())?; + parameters.validate_v4_deprecation_height()?; + if Some(true) == extend_funding_stream_addresses_as_required { parameters = parameters.extend_funding_streams(); } @@ -1070,6 +1115,7 @@ impl Parameters { genesis_hash, // Activation heights are configurable on Regtest activation_heights: _, + v4_deprecation_height: _, slow_start_interval, slow_start_shift, funding_streams: _, @@ -1115,6 +1161,11 @@ impl Parameters { &self.activation_heights } + /// Returns the height where V4 transactions stop being accepted. + pub fn v4_deprecation_height(&self) -> Option { + self.v4_deprecation_height + } + /// Returns slow start interval for this network pub fn slow_start_interval(&self) -> Height { self.slow_start_interval diff --git a/zebra-chain/src/parameters/network/tests.rs b/zebra-chain/src/parameters/network/tests.rs index b78b597b186..01ff7e9f5fc 100644 --- a/zebra-chain/src/parameters/network/tests.rs +++ b/zebra-chain/src/parameters/network/tests.rs @@ -1,5 +1,7 @@ #![allow(clippy::unwrap_in_result)] +#[cfg(zcash_unstable = "nsm")] +mod lts; mod prop; mod vectors; diff --git a/zebra-chain/src/parameters/network/tests/lts.rs b/zebra-chain/src/parameters/network/tests/lts.rs new file mode 100644 index 00000000000..0a5f80bd57f --- /dev/null +++ b/zebra-chain/src/parameters/network/tests/lts.rs @@ -0,0 +1,296 @@ +//! Unit tests for the LTS / NSM payout helpers +//! ([`crate::parameters::subsidy::lts_disbursement_start`], +//! [`crate::parameters::subsidy::lts_payout`]). +//! +//! These tests exercise the pure payout function in isolation. The payout +//! is a continuous ZIP-234 ceiling fraction of the parent LTS pool — the +//! per-block dynamics across multiple blocks (decay, inflow propagation) +//! are also covered here at the function level, and the per-fork resolution +//! path is covered by the contextual block-validation tests in `zebra-state`. + +use crate::{ + amount::{Amount, NonNegative}, + block::Height, + parameters::{ + subsidy::{lts_disbursement_start, lts_payout}, + testnet::ConfiguredActivationHeights, + Network, NetworkUpgrade, + }, +}; + +/// Builds a regtest with NU7 active at height 1 — the smallest config that +/// makes [`lts_disbursement_start`] return `Some(_)`. +fn regtest_nu7_at_1() -> Network { + Network::new_regtest( + ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + } + .into(), + ) +} + +/// Closed-form expected payout matching the ZIP-234 ceiling rule used by the +/// implementation. Test-side reference for the consensus formula. +fn expected_payout_for(parent_pool: u64) -> u64 { + if parent_pool == 0 { + return 0; + } + let numerator = u128::from(parent_pool) * 4_126u128; + let payout = numerator.div_ceil(10_000_000_000u128); + u64::try_from(payout.min(u128::from(parent_pool))).unwrap() +} + +/// `lts_disbursement_start = height_for_halving(halving(NU7) + 2, network)`, +/// and is `None` on networks where NU7 isn't configured. +#[test] +fn lts_disbursement_start_requires_nu7() { + // Mainnet does not have NU7 configured → no disbursement_start. + assert_eq!(None, lts_disbursement_start(&Network::Mainnet)); + + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).expect("regtest has NU7 configured"); + let nu7 = NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("regtest has NU7 configured"); + assert!( + start > nu7, + "disbursement_start ({start:?}) should be after NU7 ({nu7:?})" + ); +} + +/// Before the disbursement window, the payout is zero regardless of pool size. +#[test] +fn lts_payout_zero_before_disbursement_start() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + let one_zec = Amount::::try_from(100_000_000).unwrap(); + + // One block before disbursement: payout = 0 even with a huge pool. + let pre_height = start.previous().unwrap(); + assert_eq!( + Amount::::zero(), + lts_payout(pre_height, &network, one_zec) + ); + + // At genesis: payout = 0. + assert_eq!( + Amount::::zero(), + lts_payout(Height(0), &network, one_zec) + ); +} + +/// On a network without NU7 configured (so no disbursement start), the payout +/// is zero at every height regardless of parent pool. +#[test] +fn lts_payout_zero_when_nu7_unconfigured() { + let one_zec = Amount::::try_from(100_000_000).unwrap(); + let mainnet_height = Height(2_000_000); + assert_eq!( + Amount::::zero(), + lts_payout(mainnet_height, &Network::Mainnet, one_zec) + ); +} + +/// At and within the disbursement window, an empty parent pool yields a zero +/// payout. +#[test] +fn lts_payout_zero_when_parent_pool_is_empty() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + assert_eq!( + Amount::::zero(), + lts_payout(start, &network, Amount::::zero()) + ); + + // Same well past disbursement_start. + assert_eq!( + Amount::::zero(), + lts_payout( + (start + 100).unwrap(), + &network, + Amount::::zero() + ) + ); +} + +/// Inside the disbursement window the payout equals +/// `ceil(parent_pool * 4126 / 10_000_000_000)`. Exercises a few pool sizes +/// against the closed-form helper to pin down the ZIP-234 fraction and the +/// ceiling rounding. +#[test] +fn lts_payout_matches_zip234_ceiling_fraction() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + // 10_000_000_000 zatoshi → exactly 4126 (no remainder). + let exact = 10_000_000_000u64; + assert_eq!( + Amount::::try_from(expected_payout_for(exact)).unwrap(), + lts_payout( + start, + &network, + Amount::::try_from(exact).unwrap() + ) + ); + assert_eq!(4_126, expected_payout_for(exact)); + + // 1 ZEC = 100_000_000 zatoshi → ceil(100_000_000 * 4126 / 10^10) = 42. + let one_zec = 100_000_000u64; + assert_eq!(42, expected_payout_for(one_zec)); + assert_eq!( + Amount::::try_from(42u64).unwrap(), + lts_payout( + start, + &network, + Amount::::try_from(one_zec).unwrap() + ) + ); + + // A pool whose multiplication is not divisible by 10^10 must round up. + // parent_pool = 1234567 → numerator = 1234567 * 4126 = 5_093_823_442; + // ceil(5_093_823_442 / 10^10) = 1. + let small_residual = 1_234_567u64; + assert_eq!(1, expected_payout_for(small_residual)); + assert_eq!( + Amount::::try_from(1u64).unwrap(), + lts_payout( + start, + &network, + Amount::::try_from(small_residual).unwrap() + ) + ); +} + +/// A one-zatoshi pool drains in a single block under the ceiling rule — +/// no separate dust handling is needed. +#[test] +fn lts_payout_one_zatoshi_pool_drains_in_one_block() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + let one = Amount::::try_from(1u64).unwrap(); + + assert_eq!(one, lts_payout(start, &network, one)); +} + +/// The payout is always capped by the parent pool, so the chain never +/// underflows. Covers small pools where the ZIP-234 ceiling could otherwise +/// exceed the available amount. +#[test] +fn lts_payout_never_exceeds_parent_pool() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + for pool in [1u64, 7, 1_000, 1_234_567, 100_000_000, 10_000_000_000] { + let parent_pool = Amount::::try_from(pool).unwrap(); + let payout = lts_payout(start, &network, parent_pool); + assert!( + u64::from(payout) <= pool, + "payout {} must not exceed parent pool {}", + u64::from(payout), + pool + ); + assert_eq!( + Amount::::try_from(expected_payout_for(pool)).unwrap(), + payout + ); + } +} + +/// Across two consecutive blocks with no inflow, the parent pool shrinks by +/// the previous payout and the next payout is recomputed from the smaller +/// pool — never larger than the prior payout. +#[test] +fn lts_payout_decays_across_two_blocks_without_inflow() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + let parent_pool_n = 1_000_000_000_000u64; // 10_000 ZEC + let payout_n = u64::from(lts_payout( + start, + &network, + Amount::::try_from(parent_pool_n).unwrap(), + )); + assert_eq!(expected_payout_for(parent_pool_n), payout_n); + + // Next block's parent pool is the prior parent pool minus the prior payout + // (no inflow this block). + let parent_pool_n_plus_1 = parent_pool_n - payout_n; + let payout_n_plus_1 = u64::from(lts_payout( + start.next().unwrap(), + &network, + Amount::::try_from(parent_pool_n_plus_1).unwrap(), + )); + assert_eq!(expected_payout_for(parent_pool_n_plus_1), payout_n_plus_1); + + assert!( + payout_n_plus_1 <= payout_n, + "no-inflow decay must be monotone: payout_n_plus_1 ({payout_n_plus_1}) > payout_n ({payout_n})" + ); + assert!( + payout_n_plus_1 > 0, + "pool is far from zero — decay shouldn't bottom out" + ); +} + +/// A large LTS contribution at block N enters the pool in block N and only +/// affects block N+1's payout — block N's payout still uses the parent pool. +/// Exercises the parent-pool rule that breaks the within-block circularity. +#[test] +fn lts_payout_inflow_at_block_n_affects_block_n_plus_1_only() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + let parent_pool = 1_000_000_000_000u64; // 10_000 ZEC at block N's parent + let contribution = 500_000_000_000u64; // hefty inflow during block N + + // Block N's payout uses the parent pool only — the inflow doesn't appear. + let payout_n = u64::from(lts_payout( + start, + &network, + Amount::::try_from(parent_pool).unwrap(), + )); + assert_eq!(expected_payout_for(parent_pool), payout_n); + + // After block N applies, the new pool = parent_pool + contribution - payout_n. + let new_pool = parent_pool + contribution - payout_n; + + // Block N+1's payout reflects the new pool. + let payout_n_plus_1 = u64::from(lts_payout( + start.next().unwrap(), + &network, + Amount::::try_from(new_pool).unwrap(), + )); + assert_eq!(expected_payout_for(new_pool), payout_n_plus_1); + + // The inflow makes the next payout strictly larger than the current one — + // the contribution propagates forward, not into the same block. + assert!( + payout_n_plus_1 > payout_n, + "inflow should grow the next block's payout: {payout_n_plus_1} ≤ {payout_n}" + ); +} + +/// Halving boundaries have no special LTS-payout effect under the continuous +/// rule: with the same parent pool on both sides of the boundary, the payout +/// is identical. The block subsidy still halves at the boundary — that +/// schedule lives in `block_subsidy`, not in the LTS path. +#[test] +fn lts_payout_no_special_effect_at_halving_boundary() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + // Pick any two heights well inside the disbursement window. The math is + // height-independent inside the window — the rule depends only on the + // parent pool, not on whether `height` straddles a halving boundary. + let height_a = (start + 5).unwrap(); + let height_b = (start + 5_000).unwrap(); + let pool = Amount::::try_from(1_000_000_000_000u64).unwrap(); + + assert_eq!( + lts_payout(height_a, &network, pool), + lts_payout(height_b, &network, pool), + "same parent pool ⇒ same payout, regardless of height inside the disbursement window" + ); +} diff --git a/zebra-chain/src/transaction/builder.rs b/zebra-chain/src/transaction/builder.rs index 8aa4135b83e..cec27a0c82e 100644 --- a/zebra-chain/src/transaction/builder.rs +++ b/zebra-chain/src/transaction/builder.rs @@ -8,6 +8,15 @@ use crate::{ transparent, }; +/// Returns the ZIP-235 minimum ZIP-233 amount for `miner_fee`. +#[cfg(zcash_unstable = "zip235")] +pub fn zip235_minimum_zip233_amount(miner_fee: Amount) -> Amount { + let minimum = u64::from(miner_fee) * 6 / 10; + + Amount::try_from(minimum) + .expect("60% of a valid non-negative amount is also a valid non-negative amount") +} + impl Transaction { /// Returns a new version 6 coinbase transaction for `network` and `height`, /// which contains the specified `outputs`. @@ -89,8 +98,7 @@ impl Transaction { // > The NSM zip233_amount field [ZIP-233] must be set at minimum to 60% of miner fees [ZIP-235]. #[cfg(zcash_unstable = "zip235")] - zip233_amount: zip233_amount - .unwrap_or_else(|| ((miner_fee * 6).unwrap() / 10).unwrap()), + zip233_amount: zip233_amount.unwrap_or_else(|| zip235_minimum_zip233_amount(miner_fee)), #[cfg(not(zcash_unstable = "zip235"))] zip233_amount: zip233_amount.unwrap_or(Amount::zero()), diff --git a/zebra-chain/src/value_balance.rs b/zebra-chain/src/value_balance.rs index b2b33e878a7..6c4fc4c4f48 100644 --- a/zebra-chain/src/value_balance.rs +++ b/zebra-chain/src/value_balance.rs @@ -26,6 +26,10 @@ pub struct ValueBalance { sapling: Amount, orchard: Amount, deferred: Amount, + /// Long-Term Support (NSM/ZIP-234) chain value pool balance. + /// Always zero pre-NU7. Without the `cfg(zcash_unstable = "nsm")` build + /// cfg, no inflow or outflow is ever recorded, so this stays at zero. + lts: Amount, } impl ValueBalance @@ -126,6 +130,17 @@ where self } + /// Returns the LTS pool amount. + pub fn lts_amount(&self) -> Amount { + self.lts + } + + /// Sets the LTS pool amount without affecting other amounts. + pub fn set_lts_amount(&mut self, lts_amount: Amount) -> &Self { + self.lts = lts_amount; + self + } + /// Creates a [`ValueBalance`] where all the pools are zero. pub fn zero() -> Self { let zero = Amount::zero(); @@ -135,6 +150,7 @@ where sapling: zero, orchard: zero, deferred: zero, + lts: zero, } } @@ -150,6 +166,7 @@ where sapling: self.sapling.constrain().map_err(Sapling)?, orchard: self.orchard.constrain().map_err(Orchard)?, deferred: self.deferred.constrain().map_err(Deferred)?, + lts: self.lts.constrain().map_err(Lts)?, }) } } @@ -319,6 +336,7 @@ impl ValueBalance { } /// To byte array + #[cfg(not(zcash_unstable = "nsm"))] pub fn to_bytes(self) -> [u8; 40] { match [ self.transparent.to_bytes(), @@ -337,6 +355,27 @@ impl ValueBalance { } } + /// To byte array + #[cfg(zcash_unstable = "nsm")] + pub fn to_bytes(self) -> [u8; 48] { + match [ + self.transparent.to_bytes(), + self.sprout.to_bytes(), + self.sapling.to_bytes(), + self.orchard.to_bytes(), + self.deferred.to_bytes(), + self.lts.to_bytes(), + ] + .concat() + .try_into() + { + Ok(bytes) => bytes, + _ => unreachable!( + "six [u8; 8] should always concat with no error into a single [u8; 48]" + ), + } + } + /// From byte array #[allow(clippy::unwrap_in_result)] pub fn from_bytes(bytes: &[u8]) -> Result, ValueBalanceError> { @@ -344,47 +383,36 @@ impl ValueBalance { // Return an error early if bytes don't have the right length instead of panicking later. match bytes_length { - 32 | 40 => {} + 32 | 40 | 48 => {} _ => return Err(Unparsable), }; - let transparent = Amount::from_bytes( - bytes[0..8] - .try_into() - .expect("transparent amount should be parsable"), - ) - .map_err(Transparent)?; - - let sprout = Amount::from_bytes( - bytes[8..16] - .try_into() - .expect("sprout amount should be parsable"), - ) - .map_err(Sprout)?; - - let sapling = Amount::from_bytes( - bytes[16..24] - .try_into() - .expect("sapling amount should be parsable"), - ) - .map_err(Sapling)?; - - let orchard = Amount::from_bytes( - bytes[24..32] - .try_into() - .expect("orchard amount should be parsable"), - ) - .map_err(Orchard)?; - - let deferred = match bytes_length { - 32 => Amount::zero(), - 40 => Amount::from_bytes( - bytes[32..40] + // Each pool is a little-endian i64 at a fixed 8-byte offset. Pools + // past the end of the input default to zero, so legacy 32- and + // 40-byte records still parse. + let amount = |range: std::ops::Range, + map_err: fn(amount::Error) -> ValueBalanceError| { + Amount::from_bytes( + bytes[range] .try_into() - .expect("deferred amount should be parsable"), + .expect("an 8-byte slice always parses into an Amount"), ) - .map_err(Deferred)?, - _ => return Err(Unparsable), + .map_err(map_err) + }; + + let transparent = amount(0..8, Transparent)?; + let sprout = amount(8..16, Sprout)?; + let sapling = amount(16..24, Sapling)?; + let orchard = amount(24..32, Orchard)?; + let deferred = if bytes_length >= 40 { + amount(32..40, Deferred)? + } else { + Amount::zero() + }; + let lts = if bytes_length >= 48 { + amount(40..48, Lts)? + } else { + Amount::zero() }; Ok(ValueBalance { @@ -393,6 +421,7 @@ impl ValueBalance { sapling, orchard, deferred, + lts, }) } } @@ -415,6 +444,9 @@ pub enum ValueBalanceError { /// deferred amount error {0} Deferred(amount::Error), + /// LTS amount error {0} + Lts(amount::Error), + /// ValueBalance is unparsable Unparsable, } @@ -427,6 +459,7 @@ impl fmt::Display for ValueBalanceError { Sapling(e) => format!("sapling amount err: {e}"), Orchard(e) => format!("orchard amount err: {e}"), Deferred(e) => format!("deferred amount err: {e}"), + Lts(e) => format!("lts amount err: {e}"), Unparsable => "value balance is unparsable".to_string(), }) } @@ -444,6 +477,7 @@ where sapling: (self.sapling + rhs.sapling).map_err(Sapling)?, orchard: (self.orchard + rhs.orchard).map_err(Orchard)?, deferred: (self.deferred + rhs.deferred).map_err(Deferred)?, + lts: (self.lts + rhs.lts).map_err(Lts)?, }) } } @@ -493,6 +527,7 @@ where sapling: (self.sapling - rhs.sapling).map_err(Sapling)?, orchard: (self.orchard - rhs.orchard).map_err(Orchard)?, deferred: (self.deferred - rhs.deferred).map_err(Deferred)?, + lts: (self.lts - rhs.lts).map_err(Lts)?, }) } } @@ -562,6 +597,7 @@ where sapling: self.sapling.neg(), orchard: self.orchard.neg(), deferred: self.deferred.neg(), + lts: self.lts.neg(), } } } diff --git a/zebra-chain/src/value_balance/arbitrary.rs b/zebra-chain/src/value_balance/arbitrary.rs index 353a9f08e32..a5207612839 100644 --- a/zebra-chain/src/value_balance/arbitrary.rs +++ b/zebra-chain/src/value_balance/arbitrary.rs @@ -1,6 +1,16 @@ use crate::{amount::*, value_balance::*}; use proptest::prelude::*; +#[cfg(zcash_unstable = "nsm")] +fn active_lts(lts: Amount) -> Amount { + lts +} + +#[cfg(not(zcash_unstable = "nsm"))] +fn active_lts(_lts: Amount) -> Amount { + Amount::zero() +} + impl Arbitrary for ValueBalance { type Parameters = (); @@ -11,14 +21,18 @@ impl Arbitrary for ValueBalance { any::>(), any::>(), any::>(), + any::>(), ) - .prop_map(|(transparent, sprout, sapling, orchard, deferred)| Self { - transparent, - sprout, - sapling, - orchard, - deferred, - }) + .prop_map( + |(transparent, sprout, sapling, orchard, deferred, lts)| Self { + transparent, + sprout, + sapling, + orchard, + deferred, + lts: active_lts(lts), + }, + ) .boxed() } @@ -35,14 +49,18 @@ impl Arbitrary for ValueBalance { any::>(), any::>(), any::>(), + any::>(), ) - .prop_map(|(transparent, sprout, sapling, orchard, deferred)| Self { - transparent, - sprout, - sapling, - orchard, - deferred, - }) + .prop_map( + |(transparent, sprout, sapling, orchard, deferred, lts)| Self { + transparent, + sprout, + sapling, + orchard, + deferred, + lts: active_lts(lts), + }, + ) .boxed() } diff --git a/zebra-chain/src/value_balance/tests/prop.rs b/zebra-chain/src/value_balance/tests/prop.rs index 434c54f86fb..431b44c206c 100644 --- a/zebra-chain/src/value_balance/tests/prop.rs +++ b/zebra-chain/src/value_balance/tests/prop.rs @@ -17,16 +17,18 @@ proptest! { let sapling = value_balance1.sapling + value_balance2.sapling; let orchard = value_balance1.orchard + value_balance2.orchard; let deferred = value_balance1.deferred + value_balance2.deferred; + let lts = value_balance1.lts + value_balance2.lts; - match (transparent, sprout, sapling, orchard, deferred) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, lts) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(lts)) => prop_assert_eq!( value_balance1 + value_balance2, Ok(ValueBalance { transparent, sprout, sapling, orchard, - deferred + deferred, + lts, }) ), _ => prop_assert!( @@ -36,7 +38,8 @@ proptest! { | ValueBalanceError::Sprout(_) | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) - | ValueBalanceError::Deferred(_)) + | ValueBalanceError::Deferred(_) + | ValueBalanceError::Lts(_)) ) ), } @@ -53,16 +56,18 @@ proptest! { let sapling = value_balance1.sapling - value_balance2.sapling; let orchard = value_balance1.orchard - value_balance2.orchard; let deferred = value_balance1.deferred - value_balance2.deferred; + let lts = value_balance1.lts - value_balance2.lts; - match (transparent, sprout, sapling, orchard, deferred) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, lts) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(lts)) => prop_assert_eq!( value_balance1 - value_balance2, Ok(ValueBalance { transparent, sprout, sapling, orchard, - deferred + deferred, + lts, }) ), _ => prop_assert!(matches!( @@ -71,7 +76,8 @@ proptest! { | ValueBalanceError::Sprout(_) | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) - | ValueBalanceError::Deferred(_)) + | ValueBalanceError::Deferred(_) + | ValueBalanceError::Lts(_)) )), } } @@ -90,16 +96,18 @@ proptest! { let sapling = value_balance1.sapling + value_balance2.sapling; let orchard = value_balance1.orchard + value_balance2.orchard; let deferred = value_balance1.deferred + value_balance2.deferred; + let lts = value_balance1.lts + value_balance2.lts; - match (transparent, sprout, sapling, orchard, deferred) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, lts) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(lts)) => prop_assert_eq!( collection.iter().sum::, ValueBalanceError>>(), Ok(ValueBalance { transparent, sprout, sapling, orchard, - deferred + deferred, + lts, }) ), _ => prop_assert!(matches!( @@ -108,7 +116,8 @@ proptest! { | ValueBalanceError::Sprout(_) | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) - | ValueBalanceError::Deferred(_)) + | ValueBalanceError::Deferred(_) + | ValueBalanceError::Lts(_)) )) } } @@ -122,6 +131,7 @@ proptest! { prop_assert_eq!(value_balance, serialized_value_balance); } + #[cfg(not(zcash_unstable = "nsm"))] #[test] fn value_balance_deserialization(bytes in any::<[u8; 40]>()) { let _init_guard = zebra_test::init(); @@ -131,19 +141,51 @@ proptest! { } } - /// The legacy version of [`ValueBalance`] had 32 bytes compared to the current 40 bytes, - /// but it's possible to correctly instantiate the current version of [`ValueBalance`] from - /// the legacy format, so we test if Zebra can still deserialiaze the legacy format. + #[cfg(zcash_unstable = "nsm")] #[test] - fn legacy_value_balance_deserialization(bytes in any::<[u8; 32]>()) { + fn value_balance_deserialization_nsm(bytes in any::<[u8; 48]>()) { + let _init_guard = zebra_test::init(); + + if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { + prop_assert_eq!(bytes, deserialized.to_bytes()); + } + } + + /// The legacy 32-byte format predates the deferred (ZIP-1015) and LTS (NSM) + /// pools. Both default to zero on parse; round-tripping back through + /// [`ValueBalance::to_bytes`] produces the active layout with the legacy + /// bytes intact in the first 32. + #[test] + fn legacy_value_balance_deserialization_32(bytes in any::<[u8; 32]>()) { let _init_guard = zebra_test::init(); if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { let deserialized = deserialized.to_bytes(); + #[cfg(not(zcash_unstable = "nsm"))] let mut extended_bytes = [0u8; 40]; + #[cfg(zcash_unstable = "nsm")] + let mut extended_bytes = [0u8; 48]; extended_bytes[..32].copy_from_slice(&bytes); prop_assert_eq!(extended_bytes, deserialized); } } + /// The 40-byte format predates the LTS (NSM) pool. The lts field defaults + /// to zero on parse; round-tripping back produces the active layout with + /// the legacy bytes intact in the first 40. + #[test] + fn legacy_value_balance_deserialization_40(bytes in any::<[u8; 40]>()) { + let _init_guard = zebra_test::init(); + + if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { + let deserialized = deserialized.to_bytes(); + #[cfg(not(zcash_unstable = "nsm"))] + let mut extended_bytes = [0u8; 40]; + #[cfg(zcash_unstable = "nsm")] + let mut extended_bytes = [0u8; 48]; + extended_bytes[..40].copy_from_slice(&bytes); + prop_assert_eq!(extended_bytes, deserialized); + } + } + } diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index eda6361f7d3..9ecf5019bd4 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -98,6 +98,9 @@ zebra-chain = { path = "../zebra-chain", version = "7.0.0", features = ["proptes zebra-test = { path = "../zebra-test/", version = "3.0.0" } criterion = { workspace = true, features = ["html_reports"] } +rand_core = { workspace = true } +zcash_primitives = { workspace = true, features = ["transparent-inputs", "zip-233"] } +zcash_transparent = { workspace = true, features = ["transparent-inputs"] } [[bench]] name = "groth16" diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 86892c62cb9..dd56c5d8f35 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -303,7 +303,20 @@ pub fn subsidy_is_valid( } } -/// Returns `Ok(())` if the miner fees consensus rule is valid. +/// Validates the coinbase miner-fees consensus rule. +/// +/// The semantic check performed here is: +/// +/// - Pre-NU6: `total_output ≤ total_input`. +/// - NU6: `total_output = total_input`. +/// - NU7 onward (with `nsm`): the LTS (Long-Term Support / NSM / +/// ZIP-234/235) pool is represented by the signed coinbase under-claim or +/// over-claim, so the semantic verifier only checks arithmetic here. The +/// contextual verifier independently re-derives the signed implied claim +/// from the block bytes and validates it against the parent pool. +/// +/// On builds where NSM isn't enabled (no `cfg(zcash_unstable = "nsm")`, or +/// pre-NU7), the strict equality / inequality is enforced directly here. /// /// [7.1.2]: https://zips.z.cash/protocol/protocol.pdf#txnconsensus pub fn miner_fees_are_valid( @@ -325,26 +338,6 @@ pub fn miner_fees_are_valid( 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 NSM deposit - #[cfg(zcash_unstable = "zip235")] - let zip233_amount: Amount = coinbase_tx - .zip233_amount() - .constrain() - .map_err(|_| SubsidyError::InvalidZip233Amount)?; - - #[cfg(not(zcash_unstable = "zip235"))] - let zip233_amount = Amount::zero(); - - #[cfg(zcash_unstable = "zip235")] - if let Some(nsm_activation_height) = NetworkUpgrade::Nu7.activation_height(network) { - if height >= nsm_activation_height { - let minimum_zip233_amount = ((block_miner_fees * 6).unwrap() / 10).unwrap(); - if zip233_amount < minimum_zip233_amount { - Err(SubsidyError::InvalidZip233Amount)? - } - } - } - // # Consensus // // > - define the total output value of its coinbase transaction to be the total value in zatoshi of its transparent @@ -358,13 +351,31 @@ pub fn miner_fees_are_valid( // 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_pool_balance_change.value() - + zip233_amount) - .map_err(|_| SubsidyError::Overflow)?; + + expected_deferred_pool_balance_change.value()) + .map_err(|_| SubsidyError::Overflow)?; let total_input_value = (expected_block_subsidy + block_miner_fees).map_err(|_| SubsidyError::Overflow)?; + // With NSM enabled at NU7+, the miner may under-claim to deposit into the + // LTS pool or over-claim by the scheduled LTS payout net of the required + // deposit. The contextual verifier checks the signed implied claim + // against chain history, so the semantic check only ensures the value + // equation above is arithmetically well-formed. + // + // Without NSM (compile-time-off, or pre-NU7), we keep the historical + // strict checks: pre-NU6 `output ≤ input`, NU6 `output = input`. + #[cfg(zcash_unstable = "nsm")] + let nsm_active = NetworkUpgrade::Nu7 + .activation_height(network) + .is_some_and(|h| height >= h); + #[cfg(not(zcash_unstable = "nsm"))] + let nsm_active = false; + + if nsm_active { + return Ok(()); + } + // # Consensus // // > [Pre-NU6] The total output of a coinbase transaction MUST NOT be greater than its total diff --git a/zebra-consensus/src/block/tests.rs b/zebra-consensus/src/block/tests.rs index 0f54f4f7436..b0473918734 100644 --- a/zebra-consensus/src/block/tests.rs +++ b/zebra-consensus/src/block/tests.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use color_eyre::eyre::{eyre, Report}; use once_cell::sync::Lazy; +#[cfg(zcash_unstable = "nu7")] use proptest::{ arbitrary::any, strategy::{Strategy, ValueTree}, @@ -15,22 +16,27 @@ use tower::{buffer::Buffer, util::BoxService}; use zebra_chain::{ amount::{DeferredPoolBalanceChange, MAX_MONEY}, - at_least_one, block::{ tests::generate::{ large_multi_transaction_block, large_single_transaction_block_many_inputs, }, Block, Height, }, + parameters::{subsidy::block_subsidy, Network, NetworkUpgrade}, + serialization::{ZcashDeserialize, ZcashDeserializeInto}, + transaction::{arbitrary::transaction_to_fake_v5, LockTime, Transaction}, + work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY}, +}; +#[cfg(zcash_unstable = "nu7")] +use zebra_chain::{ + at_least_one, parameters::{ - subsidy::block_subsidy, testnet, Network, NetworkUpgrade, GLOBAL_SHIELDED_BUDGET, - ORCHARD_BLOCK_ACTION_LIMIT, SAPLING_BLOCK_IO_LIMIT, SPROUT_BLOCK_JOINSPLIT_LIMIT, + testnet, GLOBAL_SHIELDED_BUDGET, ORCHARD_BLOCK_ACTION_LIMIT, SAPLING_BLOCK_IO_LIMIT, + SPROUT_BLOCK_JOINSPLIT_LIMIT, }, primitives::Groth16Proof, sapling, - serialization::{ZcashDeserialize, ZcashDeserializeInto}, - transaction::{arbitrary::transaction_to_fake_v5, JoinSplitData, LockTime, Transaction}, - work::difficulty::{ParameterDifficulty as _, INVALID_COMPACT_DIFFICULTY}, + transaction::JoinSplitData, }; use zebra_script::Sigops; use zebra_test::transcript::{ExpectedTranscriptError, Transcript}; @@ -589,198 +595,13 @@ fn miner_fees_validation_failure() -> Result<(), Report> { Ok(()) } -#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] -#[test] -fn miner_fees_validation_fails_when_zip233_amount_is_zero() -> Result<(), Report> { - use zebra_chain::parameters::testnet::{ - self, ConfiguredActivationHeights, ConfiguredFundingStreams, - }; - - let transparent_value_balance = 100_001_000.try_into().unwrap(); - let zip233_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 = DeferredPoolBalanceChange::new(Amount::zero()); - - let regtest = testnet::Parameters::build() - .with_slow_start_interval(Height::MIN) - .with_activation_heights(ConfiguredActivationHeights { - nu7: Some(1), - ..Default::default() - }) - .unwrap() - .with_funding_streams(vec![ConfiguredFundingStreams { - height_range: Some(Height(1)..Height(10)), - recipients: None, - }]) - .to_network() - .unwrap(); - - let network_upgrade = NetworkUpgrade::Nu7; - let height = network_upgrade - .activation_height(®test) - .expect("failed to get the activation height for Nu7"); - - let coinbase_tx = Transaction::V6 { - network_upgrade, - lock_time: LockTime::unlocked(), - expiry_height: height, - zip233_amount, - inputs: vec![], - outputs: vec![transparent::Output::new( - transparent_value_balance, - zebra_chain::transparent::Script::new(&[]), - )], - sapling_shielded_data: None, - orchard_shielded_data: None, - }; - assert_eq!( - check::miner_fees_are_valid( - &coinbase_tx, - height, - block_miner_fees, - expected_block_subsidy, - expected_deferred_amount, - ®test, - ), - Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::InvalidZip233Amount - ))) - ); - - Ok(()) -} - -#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] -#[test] -fn miner_fees_validation_succeeds_when_zip233_amount_is_correct() -> Result<(), Report> { - use zebra_chain::parameters::testnet::{ - self, ConfiguredActivationHeights, ConfiguredFundingStreams, - }; - - let transparent_value_balance = 100_001_000.try_into().unwrap(); - let zip233_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 = DeferredPoolBalanceChange::new(Amount::zero()); - - let regtest = testnet::Parameters::build() - .with_slow_start_interval(Height::MIN) - .with_activation_heights(ConfiguredActivationHeights { - nu7: Some(1), - ..Default::default() - }) - .unwrap() - .with_funding_streams(vec![ConfiguredFundingStreams { - height_range: Some(Height(1)..Height(10)), - recipients: None, - }]) - .to_network() - .unwrap(); - - let network_upgrade = NetworkUpgrade::Nu7; - let height = network_upgrade - .activation_height(®test) - .expect("failed to get the activation height for Nu7"); - - let coinbase_tx = Transaction::V6 { - network_upgrade, - lock_time: LockTime::unlocked(), - expiry_height: height, - zip233_amount, - inputs: vec![], - outputs: vec![transparent::Output::new( - transparent_value_balance, - zebra_chain::transparent::Script::new(&[]), - )], - sapling_shielded_data: None, - orchard_shielded_data: None, - }; - - assert_eq!( - check::miner_fees_are_valid( - &coinbase_tx, - height, - block_miner_fees, - expected_block_subsidy, - expected_deferred_amount, - ®test, - ), - Ok(()) - ); - - Ok(()) -} - -#[cfg(all(feature = "tx_v6", zcash_unstable = "zip235"))] -#[test] -fn miner_fees_validation_fails_when_zip233_amount_is_incorrect() -> Result<(), Report> { - use zebra_chain::parameters::testnet::{ - self, ConfiguredActivationHeights, ConfiguredFundingStreams, - }; - - let transparent_value_balance = 100_001_000.try_into().unwrap(); - let zip233_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 = DeferredPoolBalanceChange::new(Amount::zero()); - - let regtest = testnet::Parameters::build() - .with_slow_start_interval(Height::MIN) - .with_activation_heights(ConfiguredActivationHeights { - nu7: Some(1), - ..Default::default() - }) - .unwrap() - .with_funding_streams(vec![ConfiguredFundingStreams { - height_range: Some(Height(1)..Height(10)), - recipients: None, - }]) - .to_network() - .unwrap(); - - let network_upgrade = NetworkUpgrade::Nu7; - let height = network_upgrade - .activation_height(®test) - .expect("failed to get the activation height for Nu7"); - - let coinbase_tx = Transaction::V6 { - network_upgrade, - lock_time: LockTime::unlocked(), - expiry_height: height, - zip233_amount, - inputs: vec![], - outputs: vec![transparent::Output::new( - transparent_value_balance, - zebra_chain::transparent::Script::new(&[]), - )], - sapling_shielded_data: None, - orchard_shielded_data: None, - }; - - assert_eq!( - check::miner_fees_are_valid( - &coinbase_tx, - height, - block_miner_fees, - expected_block_subsidy, - expected_deferred_amount, - ®test, - ), - Err(BlockError::Transaction(TransactionError::Subsidy( - SubsidyError::InvalidZip233Amount - ))) - ); - - Ok(()) -} - /// Smoke test for the per-block shielded action limits introduced by the draft /// "Shorter Block Target Spacing" ZIP. The limits only apply at and after NU7 /// activation; pre-NU7 the check is a no-op, so historical block test vectors /// must all pass, and a (very small) historical block treated as if it were at /// NU7 activation must still pass because its shielded counts are all zero or /// well below the limits. +#[cfg(zcash_unstable = "nu7")] #[test] fn shielded_action_limits_smoke() -> Result<(), Report> { use zebra_chain::parameters::testnet::{self, ConfiguredActivationHeights}; @@ -825,6 +646,7 @@ fn shielded_action_limits_smoke() -> Result<(), Report> { Ok(()) } +#[cfg(zcash_unstable = "nu7")] #[test] fn shielded_action_limits_reject_orchard_over_limit() { let count = limit_plus_one(ORCHARD_BLOCK_ACTION_LIMIT); @@ -844,6 +666,7 @@ fn shielded_action_limits_reject_orchard_over_limit() { ); } +#[cfg(zcash_unstable = "nu7")] #[test] fn shielded_action_limits_reject_sapling_over_limit() { let count = limit_plus_one(SAPLING_BLOCK_IO_LIMIT); @@ -863,6 +686,7 @@ fn shielded_action_limits_reject_sapling_over_limit() { ); } +#[cfg(zcash_unstable = "nu7")] #[test] fn shielded_action_limits_reject_sprout_over_limit() { let count = limit_plus_one(SPROUT_BLOCK_JOINSPLIT_LIMIT); @@ -882,6 +706,7 @@ fn shielded_action_limits_reject_sprout_over_limit() { ); } +#[cfg(zcash_unstable = "nu7")] #[test] fn shielded_action_limits_reject_global_budget_over_limit() { let err = check::shielded_action_limits_are_valid( @@ -906,6 +731,7 @@ fn shielded_action_limits_reject_global_budget_over_limit() { ); } +#[cfg(zcash_unstable = "nu7")] fn nu7_active_testnet() -> Network { testnet::Parameters::build() .with_slow_start_interval(Height(0)) @@ -919,6 +745,7 @@ fn nu7_active_testnet() -> Network { .expect("configured testnet is valid") } +#[cfg(zcash_unstable = "nu7")] fn limit_plus_one(limit: u32) -> usize { limit .checked_add(1) @@ -927,6 +754,7 @@ fn limit_plus_one(limit: u32) -> usize { .expect("test limit fits in usize") } +#[cfg(zcash_unstable = "nu7")] fn fake_v5_with_orchard_actions(count: usize) -> Arc { let mut tx = empty_v5_transaction(); let shielded_data = @@ -941,6 +769,7 @@ fn fake_v5_with_orchard_actions(count: usize) -> Arc { Arc::new(tx) } +#[cfg(zcash_unstable = "nu7")] fn fake_v5_with_sapling_outputs(count: usize) -> Arc { let mut runner = TestRunner::default(); let mut shielded_data = any::>() @@ -967,6 +796,7 @@ fn fake_v5_with_sapling_outputs(count: usize) -> Arc { Arc::new(tx) } +#[cfg(zcash_unstable = "nu7")] fn fake_v4_with_sprout_joinsplits(count: usize) -> Arc { let mut runner = TestRunner::default(); let mut joinsplit_data = any::>() @@ -988,6 +818,7 @@ fn fake_v4_with_sprout_joinsplits(count: usize) -> Arc { }) } +#[cfg(zcash_unstable = "nu7")] fn empty_v5_transaction() -> Transaction { Transaction::V5 { network_upgrade: NetworkUpgrade::Nu5, diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs index 6c3387fa57e..7f0726831e8 100644 --- a/zebra-consensus/src/checkpoint.rs +++ b/zebra-consensus/src/checkpoint.rs @@ -262,6 +262,26 @@ where ) -> Self { // All the initialisers should call this function, so we only have to // change fields or default values in one place. + + // The checkpoint verifier finalises blocks without running the + // contextual LTS payout/deposit check, so the checkpoint range must + // end strictly below NU7 activation. In the no-v6 NSM model, implicit + // ZIP-235 deposits start at NU7 even though LTS payouts begin later. + // Lifting this requires teaching the checkpoint path to validate + // per-block LTS value movement (see `flup_dispersment.md`). + #[cfg(zcash_unstable = "nsm")] + if let Some(nu7_activation_height) = + zebra_chain::parameters::NetworkUpgrade::Nu7.activation_height(network) + { + let max_height = checkpoint_list.max_height(); + assert!( + max_height < nu7_activation_height, + "final checkpoint at {max_height:?} must be below \ + NU7 activation height {nu7_activation_height:?} on {network:?}: \ + the checkpoint verifier does not validate LTS deposits or payouts" + ); + } + let (initial_tip_hash, verifier_progress) = progress_from_tip(&checkpoint_list, initial_tip); diff --git a/zebra-consensus/src/checkpoint/tests.rs b/zebra-consensus/src/checkpoint/tests.rs index 098e7a61426..d8cda04cebf 100644 --- a/zebra-consensus/src/checkpoint/tests.rs +++ b/zebra-consensus/src/checkpoint/tests.rs @@ -814,3 +814,97 @@ async fn hard_coded_mainnet() -> Result<(), Report> { Ok(()) } + +/// Constructing a `CheckpointVerifier` whose final checkpoint is at or above +/// NU7 activation must panic, because the checkpoint verifier finalises blocks +/// without running the contextual LTS payout/deposit check. +#[cfg(zcash_unstable = "nsm")] +#[tokio::test(flavor = "multi_thread")] +#[should_panic(expected = "NU7 activation height")] +async fn checkpoint_at_nu7_activation_panics() { + use zebra_chain::parameters::testnet::{self, ConfiguredActivationHeights}; + + let _init_guard = zebra_test::init(); + + // Regtest-style network with NU7 active. The actual value doesn't matter, + // we read it back at runtime. + let network = testnet::Parameters::build() + .with_slow_start_interval(block::Height::MIN) + .with_halving_interval(144) + .expect("halving interval is valid") + .with_activation_heights(ConfiguredActivationHeights { + before_overwinter: Some(1), + overwinter: Some(2), + sapling: Some(3), + blossom: Some(4), + heartwood: Some(5), + canopy: Some(6), + nu5: Some(7), + nu6: Some(8), + nu6_1: Some(9), + nu7: Some(10), + }) + .expect("activation heights are monotonic") + .extend_funding_streams() + .to_network() + .expect("network builds"); + + let nu7_activation_height = zebra_chain::parameters::NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("NU7 activation height is configured"); + + // Smallest violating list: genesis + a checkpoint exactly at NU7 activation. + let checkpoints = vec![ + (block::Height(0), block::Hash([0xaa; 32])), + (nu7_activation_height, block::Hash([0xbb; 32])), + ]; + + let state_service = zebra_state::init_test(&network).await; + let _ = CheckpointVerifier::from_list(checkpoints, &network, None, state_service); +} + +/// A `CheckpointVerifier` whose final checkpoint sits one block below NU7 +/// activation must construct successfully. That's the largest valid range. +#[cfg(zcash_unstable = "nsm")] +#[tokio::test(flavor = "multi_thread")] +async fn checkpoint_just_below_nu7_activation_succeeds() { + use zebra_chain::parameters::testnet::{self, ConfiguredActivationHeights}; + + let _init_guard = zebra_test::init(); + + let network = testnet::Parameters::build() + .with_slow_start_interval(block::Height::MIN) + .with_halving_interval(144) + .expect("halving interval is valid") + .with_activation_heights(ConfiguredActivationHeights { + before_overwinter: Some(1), + overwinter: Some(2), + sapling: Some(3), + blossom: Some(4), + heartwood: Some(5), + canopy: Some(6), + nu5: Some(7), + nu6: Some(8), + nu6_1: Some(9), + nu7: Some(10), + }) + .expect("activation heights are monotonic") + .extend_funding_streams() + .to_network() + .expect("network builds"); + + let nu7_activation_height = zebra_chain::parameters::NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("NU7 activation height is configured"); + let last_valid = block::Height(nu7_activation_height.0 - 1); + + let checkpoints = vec![ + (block::Height(0), block::Hash([0xcc; 32])), + (last_valid, block::Hash([0xdd; 32])), + ]; + + let state_service = zebra_state::init_test(&network).await; + // Must not panic. + let _ = CheckpointVerifier::from_list(checkpoints, &network, None, state_service) + .expect("checkpoint list one below NU7 activation is accepted"); +} diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index cbfc548236f..c1f1eb6506b 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -484,8 +484,11 @@ where } let nu = req.upgrade(&network); - let cached_ffi_transaction = - Arc::new(CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), nu).map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?); + let ffi_nu = Self::ffi_network_upgrade(tx.as_ref(), &network, req.height(), nu)?; + let cached_ffi_transaction = Arc::new( + CachedFfiTransaction::new(tx.clone(), Arc::new(spent_outputs), ffi_nu) + .map_err(|_| TransactionError::UnsupportedByNetworkUpgrade(tx.version(), nu))?, + ); tracing::trace!(?tx_id, "got state UTXOs"); @@ -499,7 +502,6 @@ where .. } => Self::verify_v4_transaction( &req, - &network, script_verifier, cached_ffi_transaction.clone(), joinsplit_data, @@ -550,22 +552,13 @@ where // Get the `value_balance` to calculate the transaction fee. let value_balance = tx.value_balance(&spent_utxos); - let zip233_amount = match *tx { - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - Transaction::V6{ .. } => tx.zip233_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) => match tx_rtv - zip233_amount { - Ok(fee) => Some(fee), - Err(_) => return Err(TransactionError::IncorrectFee), - } + Ok(tx_rtv) => Some(tx_rtv), Err(_) => return Err(TransactionError::IncorrectFee), }, Err(_) => return Err(TransactionError::IncorrectFee), @@ -826,16 +819,10 @@ where #[allow(clippy::unwrap_in_result)] fn verify_v4_transaction( request: &Request, - network: &Network, script_verifier: script::Verifier, cached_ffi_transaction: Arc, joinsplit_data: &Option>, ) -> Result { - let tx = request.transaction(); - let nu = request.upgrade(network); - - Self::verify_v4_transaction_network_upgrade(&tx, nu)?; - let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle(); let sighash = cached_ffi_transaction @@ -854,8 +841,17 @@ where /// Verifies if a V4 `transaction` is supported by `network_upgrade`. fn verify_v4_transaction_network_upgrade( transaction: &Transaction, + network: &Network, + height: block::Height, network_upgrade: NetworkUpgrade, ) -> Result<(), TransactionError> { + // From NU7 onward, V4 transactions remain valid only until the + // optional V4 deprecation height (testnet-only); after it they are + // rejected. Networks with no configured height never deprecate V4. + let v4_allowed_after_nu7 = network + .v4_deprecation_height() + .is_none_or(|deprecation_height| height < deprecation_height); + match network_upgrade { // Supports V4 transactions // @@ -881,9 +877,17 @@ where | NetworkUpgrade::Nu6_1 => Ok(()), #[cfg(zcash_unstable = "zfuture")] - NetworkUpgrade::ZFuture => Ok(()), + NetworkUpgrade::ZFuture if v4_allowed_after_nu7 => Ok(()), + + NetworkUpgrade::Nu7 if v4_allowed_after_nu7 => Ok(()), // Does not support V4 transactions + #[cfg(zcash_unstable = "zfuture")] + NetworkUpgrade::ZFuture => Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )), + NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter | NetworkUpgrade::Overwinter @@ -894,6 +898,25 @@ where } } + /// Returns the network upgrade used for FFI sighash construction. + fn ffi_network_upgrade( + transaction: &Transaction, + network: &Network, + height: block::Height, + network_upgrade: NetworkUpgrade, + ) -> Result { + if matches!(transaction, Transaction::V4 { .. }) { + Self::verify_v4_transaction_network_upgrade( + transaction, + network, + height, + network_upgrade, + )?; + } + + Ok(network_upgrade) + } + /// Verify a V5 transaction. /// /// Returns a set of asynchronous checks that must all succeed for the transaction to be diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index ecdc66197bc..289efe5cc8d 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -122,10 +122,6 @@ 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(all(zcash_unstable = "nu7", feature = "tx_v6"))] - let has_other_circulation_effects = tx.has_zip233_amount(); - - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] let has_other_circulation_effects = false; if !tx.has_transparent_or_shielded_inputs() { diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 174879bf24f..f0d7cc720f6 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -17,6 +17,8 @@ use halo2::pasta::{group::ff::PrimeField, pallas}; use tokio::time::timeout; use tower::{buffer::Buffer, service_fn, ServiceExt}; +#[cfg(zcash_unstable = "nu7")] +use zebra_chain::parameters::testnet::RegtestParameters; use zebra_chain::{ amount::{Amount, NonNegative}, block::{self, Block, Height}, @@ -59,6 +61,32 @@ fn test_timeout() -> std::time::Duration { } } +#[cfg(zcash_unstable = "nu7")] +fn ffi_network_upgrade_for_test( + _verifier: &Verifier, + transaction: &Transaction, + network: &Network, + height: block::Height, + network_upgrade: NetworkUpgrade, +) -> Result +where + ZS: tower::Service< + zebra_state::Request, + Response = zebra_state::Response, + Error = crate::BoxError, + > + Send + + Clone + + 'static, + ZS::Future: Send + 'static, + Mempool: tower::Service + + Send + + Clone + + 'static, + Mempool::Future: Send + 'static, +{ + Verifier::::ffi_network_upgrade(transaction, network, height, network_upgrade) +} + #[test] fn v5_transactions_basic_check() -> Result<(), Report> { let _init_guard = zebra_test::init(); @@ -1297,6 +1325,119 @@ async fn v5_transaction_is_accepted_after_nu5_activation() { } } +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +#[tokio::test] +async fn v6_transaction_with_sapling_shielded_data_is_accepted_after_nu7_activation() { + use std::convert::Infallible; + + use zcash_primitives::transaction::{ + builder::{BuildConfig, Builder}, + fees::zip317, + }; + use zcash_protocol::{memo::MemoBytes, value::Zatoshis}; + use zcash_transparent::builder::TransparentSigningSet; + + let _init_guard = zebra_test::init(); + + let network = Network::new_regtest( + ConfiguredActivationHeights { + nu5: Some(1), + nu6: Some(2), + nu6_1: Some(3), + nu7: Some(4), + ..Default::default() + } + .into(), + ); + let height = NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("NU7 activation height is configured"); + + let extsk = sapling_crypto::zip32::ExtendedSpendingKey::master(&[]); + let dfvk = extsk.to_diversifiable_full_viewing_key(); + let to = dfvk.default_address().1; + let note_value = Zatoshis::const_from_u64(50_000); + let note = to.create_note( + sapling_crypto::value::NoteValue::from_raw(note_value.into()), + sapling_crypto::Rseed::BeforeZip212(jubjub::Fr::from(1)), + ); + + let mut tree = sapling_crypto::CommitmentTree::empty(); + tree.append(sapling_crypto::Node::from_cmu(¬e.cmu())) + .expect("note commitment should append to empty Sapling tree"); + let witness = sapling_crypto::IncrementalWitness::from_tree(tree) + .expect("Sapling tree with one note should produce a witness"); + + let build_config = BuildConfig::Standard { + sapling_anchor: Some(witness.root().into()), + orchard_anchor: None, + }; + let mut builder = Builder::new(network.clone(), height.into(), build_config); + builder + .add_sapling_spend::(dfvk.fvk().clone(), note, witness.path().unwrap()) + .expect("Sapling spend should be added to builder"); + + let fee = builder + .get_fee(&zip317::FeeRule::standard()) + .expect("ZIP-317 fee should be computable"); + let output_value = (note_value - fee).expect("note should cover fee and Sapling output"); + builder + .add_sapling_output::( + Some(dfvk.fvk().ovk), + to, + output_value, + MemoBytes::empty(), + ) + .expect("Sapling output should be added to builder"); + + let prover = zcash_proofs::prover::LocalTxProver::bundled(); + let built_transaction = builder + .build( + &TransparentSigningSet::new(), + &[extsk], + &[], + rand_core::OsRng, + &prover, + &prover, + &zip317::FeeRule::standard(), + ) + .expect("valid v6 Sapling transaction should build") + .transaction() + .clone(); + + let mut transaction_bytes = Vec::new(); + built_transaction + .write(&mut transaction_bytes) + .expect("valid v6 Sapling transaction should serialize"); + let transaction = transaction_bytes + .zcash_deserialize_into::() + .expect("valid v6 Sapling transaction should deserialize into Zebra"); + + assert!(transaction.version() > 4); + assert!(transaction.has_sapling_shielded_data()); + assert_eq!( + NetworkUpgrade::current(&network, height), + NetworkUpgrade::Nu7 + ); + + let expected = transaction.unmined_id(); + let result = Verifier::new_for_tests( + &network, + service_fn(|_| async { unreachable!("Service should not be called") }), + ) + .oneshot(Request::Block { + transaction_hash: transaction.hash(), + transaction: Arc::new(transaction), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height, + time: DateTime::::MAX_UTC, + }) + .await; + + assert_eq!(result.expect("success").tx_id(), expected); +} + /// Test if V4 transaction with transparent funds is accepted. #[tokio::test] async fn v4_transaction_with_transparent_transfer_is_accepted() { @@ -2479,6 +2620,122 @@ fn v4_with_signed_sprout_transfer_is_accepted() { }) } +/// Test if a signed V4 transaction with a [`sprout::JoinSplit`] is rejected at +/// its configured deprecation height. +#[cfg(zcash_unstable = "nu7")] +#[tokio::test] +async fn v4_with_signed_sprout_transfer_is_rejected_at_deprecation_height() { + let _init_guard = zebra_test::init(); + + let (_, transaction) = test_transactions(&Network::Mainnet) + .rev() + .filter(|(_, transaction)| !transaction.is_coinbase() && transaction.inputs().is_empty()) + .find(|(_, transaction)| transaction.sprout_groth16_joinsplits().next().is_some()) + .expect("No transaction found with Groth16 JoinSplits"); + + assert_eq!(transaction.version(), 4); + assert!(transaction.has_sprout_joinsplit_data()); + + let network = Network::new_regtest(RegtestParameters { + activation_heights: ConfiguredActivationHeights { + nu5: Some(1), + nu6: Some(2), + nu6_1: Some(3), + nu7: Some(4), + ..Default::default() + }, + v4_deprecation_height: Some(Height(5)), + ..Default::default() + }); + let nu7_height = NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("NU7 activation height is configured"); + + assert_eq!( + NetworkUpgrade::current(&network, nu7_height), + NetworkUpgrade::Nu7 + ); + let verifier = Verifier::new_for_tests( + &network, + service_fn(|_: zebra_state::Request| async { + unreachable!("State service should not be called") + }), + ); + assert_eq!( + ffi_network_upgrade_for_test( + &verifier, + &transaction, + &network, + nu7_height, + NetworkUpgrade::Nu7, + ) + .expect("v4 transaction is supported before its deprecation height"), + NetworkUpgrade::Nu7, + ); + + let result = Verifier::new_for_tests( + &network, + service_fn(|_| async { unreachable!("State service should not be called") }), + ) + .oneshot(Request::Block { + transaction_hash: transaction.hash(), + transaction: transaction.clone(), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height: nu7_height, + time: DateTime::::MAX_UTC, + }) + .await; + + #[cfg(zcash_unstable = "nu7")] + assert!( + !matches!( + result, + Err(TransactionError::UnsupportedByNetworkUpgrade( + 4, + NetworkUpgrade::Nu7 + )) + ), + "v4 transactions are not rejected by the version gate before the configured deprecation height" + ); + + #[cfg(not(zcash_unstable = "nu7"))] + assert_eq!( + result, + Err(TransactionError::UnsupportedByNetworkUpgrade( + 4, + NetworkUpgrade::Nu7 + )), + "without the NU7 librustzcash cfg, the active NU7 branch id is unavailable to the FFI verifier" + ); + + let deprecation_height = network + .v4_deprecation_height() + .expect("v4 deprecation height is configured"); + + let result = Verifier::new_for_tests( + &network, + service_fn(|_| async { unreachable!("State service should not be called") }), + ) + .oneshot(Request::Block { + transaction_hash: transaction.hash(), + transaction, + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height: deprecation_height, + time: DateTime::::MAX_UTC, + }) + .await; + + assert_eq!( + result, + Err(TransactionError::UnsupportedByNetworkUpgrade( + 4, + NetworkUpgrade::Nu7 + )) + ); +} + /// Test if an V4 transaction with a modified [`sprout::JoinSplit`] is rejected. /// /// This test verifies if the transaction verifier correctly rejects the transaction because of the diff --git a/zebra-network/src/config.rs b/zebra-network/src/config.rs index b64aa6fb230..62664fa316a 100644 --- a/zebra-network/src/config.rs +++ b/zebra-network/src/config.rs @@ -14,6 +14,7 @@ use tokio::fs; use tracing::Span; use zebra_chain::{ + block::Height, common::atomic_write, parameters::{ testnet::{ @@ -597,6 +598,7 @@ struct DTestnetParameters { disable_pow: Option, genesis_hash: Option, activation_heights: Option, + v4_deprecation_height: Option, pre_nu6_funding_streams: Option, post_nu6_funding_streams: Option, funding_streams: Option>, @@ -677,6 +679,7 @@ impl From> for DTestnetParameters { disable_pow: Some(params.disable_pow()), genesis_hash: Some(params.genesis_hash().to_string()), activation_heights: Some(params.activation_heights().into()), + v4_deprecation_height: params.v4_deprecation_height().map(|height| height.0), pre_nu6_funding_streams: None, post_nu6_funding_streams: None, funding_streams: Some(params.funding_streams().iter().map(Into::into).collect()), @@ -776,7 +779,7 @@ impl<'de> Deserialize<'de> for Config { build_configured_testnet::(*params, &initial_testnet_peers)? } (DNetwork::ConfiguredRegtest { params, .. }, _) => { - Network::new_regtest(build_regtest_params(*params)) + build_configured_regtest::(*params)? } (DNetwork::DefaultForKind(NetworkKind::Mainnet), _) => Network::Mainnet, (DNetwork::DefaultForKind(NetworkKind::Testnet), Some(params)) => { @@ -786,7 +789,7 @@ impl<'de> Deserialize<'de> for Config { Network::new_default_testnet() } (DNetwork::DefaultForKind(NetworkKind::Regtest), Some(params)) => { - Network::new_regtest(build_regtest_params(params)) + build_configured_regtest::(params)? } (DNetwork::DefaultForKind(NetworkKind::Regtest), None) => { Network::new_regtest(Default::default()) @@ -880,6 +883,7 @@ where disable_pow, genesis_hash, activation_heights, + v4_deprecation_height, pre_nu6_funding_streams, post_nu6_funding_streams, funding_streams, @@ -935,6 +939,10 @@ where .map_err(de::Error::custom)? } + if let Some(v4_deprecation_height) = v4_deprecation_height { + params_builder = params_builder.with_v4_deprecation_height(Height(v4_deprecation_height)); + } + if let Some(halving_interval) = pre_blossom_halving_interval { params_builder = params_builder .with_halving_interval(halving_interval.into()) @@ -989,6 +997,7 @@ where fn build_regtest_params(params: DTestnetParameters) -> RegtestParameters { let DTestnetParameters { activation_heights, + v4_deprecation_height, pre_nu6_funding_streams, post_nu6_funding_streams, funding_streams, @@ -1010,9 +1019,19 @@ fn build_regtest_params(params: DTestnetParameters) -> RegtestParameters { RegtestParameters { activation_heights: activation_heights.unwrap_or_default(), + v4_deprecation_height: v4_deprecation_height.map(Height), funding_streams: Some(funding_streams_vec), lockbox_disbursements, checkpoints: Some(checkpoints), extend_funding_stream_addresses_as_required, } } + +fn build_configured_regtest<'de, D>(params: DTestnetParameters) -> Result +where + D: Deserializer<'de>, +{ + testnet::Parameters::new_regtest(build_regtest_params(params)) + .map(Network::new_configured_testnet) + .map_err(de::Error::custom) +} diff --git a/zebra-network/src/config/tests/vectors.rs b/zebra-network/src/config/tests/vectors.rs index 8207857c218..969b5fc1bd5 100644 --- a/zebra-network/src/config/tests/vectors.rs +++ b/zebra-network/src/config/tests/vectors.rs @@ -76,6 +76,25 @@ fn testnet_params_serialization_roundtrip() { assert_eq!(config, deserialized); } +#[test] +fn invalid_regtest_v4_deprecation_height_returns_config_error() { + let _init_guard = zebra_test::init(); + + let err = toml::from_str::( + r#" + network = "Regtest" + + [testnet_parameters] + v4_deprecation_height = 5 + "#, + ) + .expect_err("invalid regtest parameters should return a config error"); + + assert!(err + .to_string() + .contains("V4 deprecation height must be after NU7 activation height")); +} + #[test] fn default_config_uses_ipv6() { let _init_guard = zebra_test::init(); diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 18633defe98..3d2b00a6bc6 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -2520,8 +2520,6 @@ where mempool_txs, mempool_tx_deps, extra_coinbase_data.clone(), - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - None, ); tracing::debug!( @@ -2534,6 +2532,18 @@ where // - After this point, the template only depends on the previously fetched data. + // Compute the LTS (NSM / ZIP-234) payout the coinbase is allowed to + // claim at `next_block_height`, so the generated coinbase passes the + // contextual verifier when the resulting block is submitted. Zero + // outside the disbursement window. + #[cfg(zcash_unstable = "nsm")] + let lts_payout = types::get_block_template::expected_lts_payout( + &network, + next_block_height, + read_state.clone(), + ) + .await?; + let response = BlockTemplateResponse::new_internal( &network, &miner_address, @@ -2542,8 +2552,8 @@ where mempool_txs, submit_old, extra_coinbase_data, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - None, + #[cfg(zcash_unstable = "nsm")] + lts_payout, ); Ok(response.into()) @@ -3366,7 +3376,11 @@ impl GetInfoResponse { } /// Type alias for the array of `GetBlockchainInfoBalance` objects +#[cfg(not(zcash_unstable = "nsm"))] pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 5]; +/// Type alias for the array of `GetBlockchainInfoBalance` objects +#[cfg(zcash_unstable = "nsm")] +pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 6]; /// Response to a `getblockchaininfo` RPC request. /// diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index 44dafc3fae2..a35c441380b 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -717,7 +717,12 @@ fn snapshot_rpc_getblockchaininfo( settings: &insta::Settings, ) { settings.bind(|| { - insta::assert_json_snapshot!(format!("get_blockchain_info{variant_suffix}"), info, { + #[cfg(zcash_unstable = "nsm")] + let nsm_suffix = "_nsm"; + #[cfg(not(zcash_unstable = "nsm"))] + let nsm_suffix = ""; + + insta::assert_json_snapshot!(format!("get_blockchain_info{variant_suffix}{nsm_suffix}"), info, { ".estimatedheight" => dynamic_redaction(|value, _path| { // assert that the value looks like a valid height here assert!(u32::try_from(value.as_u64().unwrap()).unwrap() < Height::MAX_AS_U32); @@ -772,7 +777,14 @@ fn snapshot_rpc_getblock_verbose( block: GetBlockResponse, settings: &insta::Settings, ) { - settings.bind(|| insta::assert_json_snapshot!(format!("get_block_verbose_{variant}"), block)); + settings.bind(|| { + #[cfg(zcash_unstable = "nsm")] + let nsm_suffix = "_nsm"; + #[cfg(not(zcash_unstable = "nsm"))] + let nsm_suffix = ""; + + insta::assert_json_snapshot!(format!("get_block_verbose_{variant}{nsm_suffix}"), block) + }); } /// Check valid `getblockheader` response using `cargo insta`. diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@mainnet_10.snap new file mode 100644 index 00000000000..dd749d95a2f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@mainnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@testnet_10.snap new file mode 100644 index 00000000000..63b6e53ab95 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@testnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@mainnet_10.snap new file mode 100644 index 00000000000..e74079fb806 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@mainnet_10.snap @@ -0,0 +1,136 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + { + "in_active_chain": true, + "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000", + "height": 1, + "confirmations": 10, + "vin": [ + { + "coinbase": "5100", + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0005, + "valueZat": 50000, + "n": 0, + "scriptPubKey": { + "asm": "027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875 OP_CHECKSIG", + "hex": "21027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac", + "type": "pubkey" + } + }, + { + "value": 0.000125, + "valueZat": 12500, + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 7d46a730d31f97b1930d3368a967c309bd4d136a OP_EQUAL", + "hex": "a9147d46a730d31f97b1930d3368a967c309bd4d136a87", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd" + ] + } + } + ], + "vShieldedSpend": [], + "vShieldedOutput": [], + "vjoinsplit": [], + "orchard": { + "actions": [], + "valueBalance": 0.0, + "valueBalanceZat": 0 + }, + "valueBalance": 0.0, + "valueBalanceZat": 0, + "size": 129, + "time": 1477671596, + "txid": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "overwintered": false, + "version": 1, + "locktime": 0, + "blockhash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "blocktime": 1477671596 + } + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@testnet_10.snap new file mode 100644 index 00000000000..6b8e04f836b --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@testnet_10.snap @@ -0,0 +1,136 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + { + "in_active_chain": true, + "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000", + "height": 1, + "confirmations": 10, + "vin": [ + { + "coinbase": "510101", + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0005, + "valueZat": 50000, + "n": 0, + "scriptPubKey": { + "asm": "025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99 OP_CHECKSIG", + "hex": "21025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99ac", + "type": "pubkey" + } + }, + { + "value": 0.000125, + "valueZat": 12500, + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 ef775f1f997f122a062fff1a2d7443abd1f9c642 OP_EQUAL", + "hex": "a914ef775f1f997f122a062fff1a2d7443abd1f9c64287", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi" + ] + } + } + ], + "vShieldedSpend": [], + "vShieldedOutput": [], + "vjoinsplit": [], + "orchard": { + "actions": [], + "valueBalance": 0.0, + "valueBalanceZat": 0 + }, + "valueBalance": 0.0, + "valueBalanceZat": 0, + "size": 130, + "time": 1477674473, + "txid": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "overwintered": false, + "version": 1, + "locktime": 0, + "blockhash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "blocktime": 1477674473 + } + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@mainnet_10.snap new file mode 100644 index 00000000000..dd749d95a2f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@mainnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@testnet_10.snap new file mode 100644 index 00000000000..63b6e53ab95 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@testnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@mainnet_10.snap new file mode 100644 index 00000000000..dd749d95a2f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@mainnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@testnet_10.snap new file mode 100644 index 00000000000..63b6e53ab95 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@testnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@mainnet_10.snap new file mode 100644 index 00000000000..e74079fb806 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@mainnet_10.snap @@ -0,0 +1,136 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + { + "in_active_chain": true, + "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0250c30000000000002321027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875acd43000000000000017a9147d46a730d31f97b1930d3368a967c309bd4d136a8700000000", + "height": 1, + "confirmations": 10, + "vin": [ + { + "coinbase": "5100", + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0005, + "valueZat": 50000, + "n": 0, + "scriptPubKey": { + "asm": "027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875 OP_CHECKSIG", + "hex": "21027a46eb513588b01b37ea24303f4b628afd12cc20df789fede0921e43cad3e875ac", + "type": "pubkey" + } + }, + { + "value": 0.000125, + "valueZat": 12500, + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 7d46a730d31f97b1930d3368a967c309bd4d136a OP_EQUAL", + "hex": "a9147d46a730d31f97b1930d3368a967c309bd4d136a87", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd" + ] + } + } + ], + "vShieldedSpend": [], + "vShieldedOutput": [], + "vjoinsplit": [], + "orchard": { + "actions": [], + "valueBalance": 0.0, + "valueBalanceZat": 0 + }, + "valueBalance": 0.0, + "valueBalanceZat": 0, + "size": 129, + "time": 1477671596, + "txid": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "overwintered": false, + "version": 1, + "locktime": 0, + "blockhash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "blocktime": 1477671596 + } + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@testnet_10.snap new file mode 100644 index 00000000000..6b8e04f836b --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@testnet_10.snap @@ -0,0 +1,136 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + { + "in_active_chain": true, + "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0250c30000000000002321025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99acd43000000000000017a914ef775f1f997f122a062fff1a2d7443abd1f9c6428700000000", + "height": 1, + "confirmations": 10, + "vin": [ + { + "coinbase": "510101", + "sequence": 4294967295 + } + ], + "vout": [ + { + "value": 0.0005, + "valueZat": 50000, + "n": 0, + "scriptPubKey": { + "asm": "025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99 OP_CHECKSIG", + "hex": "21025229e1240a21004cf8338db05679fa34753706e84f6aebba086ba04317fd8f99ac", + "type": "pubkey" + } + }, + { + "value": 0.000125, + "valueZat": 12500, + "n": 1, + "scriptPubKey": { + "asm": "OP_HASH160 ef775f1f997f122a062fff1a2d7443abd1f9c642 OP_EQUAL", + "hex": "a914ef775f1f997f122a062fff1a2d7443abd1f9c64287", + "reqSigs": 1, + "type": "scripthash", + "addresses": [ + "t2UNzUUx8mWBCRYPRezvA363EYXyEpHokyi" + ] + } + } + ], + "vShieldedSpend": [], + "vShieldedOutput": [], + "vjoinsplit": [], + "orchard": { + "actions": [], + "valueBalance": 0.0, + "valueBalanceZat": 0 + }, + "valueBalance": 0.0, + "valueBalanceZat": 0, + "size": 130, + "time": 1477674473, + "txid": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "overwintered": false, + "version": 1, + "locktime": 0, + "blockhash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "blocktime": 1477674473 + } + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@mainnet_10.snap new file mode 100644 index 00000000000..dd749d95a2f --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@mainnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "0007bc227e1c57a4a70e237cad00e7b7ce565155ab49166bc57397a26d339283", + "confirmations": 10, + "size": 1617, + "height": 1, + "version": 4, + "merkleroot": "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "851bf6fbf7a976327817c738c489d7fa657752445430922d94c983c0b9ed4609" + ], + "time": 1477671596, + "nonce": "9057977ea6d4ae867decc96359fcf2db8cdebcbfb3bd549de4f21f16cfe83475", + "solution": "002b2ee0d2f5d0c1ebf5a265b6f5b428f2fdc9aaea07078a6c5cab4f1bbfcd56489863deae6ea3fd8d3d0762e8e5295ff2670c9e90d8e8c68a54a40927e82a65e1d44ced20d835818e172d7b7f5ffe0245d0c3860a3f11af5658d68b6a7253b4684ffef5242fefa77a0bfc3437e8d94df9dc57510f5a128e676dd9ddf23f0ef75b460090f507499585541ab53a470c547ea02723d3a979930941157792c4362e42d3b9faca342a5c05a56909b046b5e92e2870fca7c932ae2c2fdd97d75b6e0ecb501701c1250246093c73efc5ec2838aeb80b59577741aa5ccdf4a631b79f70fc419e28714fa22108d991c29052b2f5f72294c355b57504369313470ecdd8e0ae97fc48e243a38c2ee7315bb05b7de9602047e97449c81e46746513221738dc729d7077a1771cea858865d85261e71e82003ccfbba2416358f023251206d6ef4c5596bc35b2b5bce3e9351798aa2c9904723034e5815c7512d260cc957df5db6adf9ed7272483312d1e68c60955a944e713355089876a704aef06359238f6de5a618f7bd0b4552ba72d05a6165e582f62d55ff2e1b76991971689ba3bee16a520fd85380a6e5a31de4dd4654d561101ce0ca390862d5774921eae2c284008692e9e08562144e8aa1f399a9d3fab0c4559c1f12bc945e626f7a89668613e8829767f4116ee9a4f832cf7c3ade3a7aba8cb04de39edd94d0d05093ed642adf9fbd9d373a80832ffd1c62034e4341546b3515f0e42e6d8570393c6754be5cdb7753b4709527d3f164aebf3d315934f7b3736a1b31052f6cc5699758950331163b3df05b9772e9bf99c8c77f8960e10a15edb06200106f45742d740c422c86b7e4f5a52d3732aa79ee54cfc92f76e03c268ae226477c19924e733caf95b8f350233a5312f4ed349d3ad76f032358f83a6d0d6f83b2a456742aad7f3e615fa72286300f0ea1c9793831ef3a5a4ae08640a6e32f53d1cba0be284b25e923d0d110ba227e54725632efcbbe17c05a9cde976504f6aece0c461b562cfae1b85d5f6782ee27b3e332ac0775f681682ce524b32889f1dc4231226f1aada0703beaf8d41732c9647a0a940a86f8a1be7f239c44fcaa7ed7a055506bdbe1df848f9e047226bee1b6d788a03f6e352eead99b419cfc41741942dbeb7a5c55788d5a3e636d8aab7b36b4db71d16700373bbc1cdeba8f9b1db10bf39a621bc737ea4f4e333698d6e09b51ac7a97fb6fd117ccad1d6b6b3a7451699d5bfe448650396d7b58867b3b0872be13ad0b43da267df0ad77025155f04e20c56d6a9befb3e9c7d23b82cbf3a534295ebda540682cc81be9273781b92519c858f9c25294fbacf75c3b3c15bda6d36de1c83336f93e96910dbdcb190d6ef123c98565ff6df1e903f57d4e4df167ba6b829d6d9713eb2126b0cf869940204137babcc6a1b7cb2f0b94318a7460e5d1a605c249bd2e72123ebad332332c18adcb285ed8874dbde084ebcd4f744465350d57110f037fffed1569d642c258749e65b0d13e117eaa37014a769b5ab479b7c77178880e77099f999abe712e543dbbf626ca9bcfddc42ff2f109d21c8bd464894e55ae504fdf81e1a7694180225da7dac8879abd1036cf26bb50532b8cf138b337a1a1bd1a43f8dd70b7399e2690c8e7a5a1fe099026b8f2a6f65fc0dbedda15ba65e0abd66c7176fb426980549892b4817de78e345a7aeab05744c3def4a2f283b4255b02c91c1af7354a368c67a11703c642a385c7453131ce3a78b24c5e22ab7e136a38498ce82082181884418cb4d6c2920f258a3ad20cfbe7104af1c6c6cb5e58bf29a9901721ad19c0a260cd09a3a772443a45aea4a5c439a95834ef5dc2e26343278947b7b796f796ae9bcadb29e2899a1d7313e6f7bfb6f8b", + "bits": "1f07ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "00040fe8ec8471911baa1db1266ea15dd06b4a8a5c453883c000b031973dce08", + "nextblockhash": "0002a26c902619fc964443264feb16f1e3e2d71322fc53dcb81cc5d797e273ed" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@testnet_10.snap new file mode 100644 index 00000000000..63b6e53ab95 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@testnet_10.snap @@ -0,0 +1,81 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: block +--- +{ + "hash": "025579869bcf52a989337342f5f57a84f3a28b968f7d6a8307902b065a668d23", + "confirmations": 10, + "size": 1618, + "height": 1, + "version": 4, + "merkleroot": "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75", + "blockcommitments": "0000000000000000000000000000000000000000000000000000000000000000", + "finalsaplingroot": "0000000000000000000000000000000000000000000000000000000000000000", + "nTx": 1, + "tx": [ + "f37e9f691fffb635de0999491d906ee85ba40cd36dae9f6e5911a8277d7c5f75" + ], + "time": 1477674473, + "nonce": "0000e5739438a096ca89cde16bcf6001e0c5a7ce6f7c591d26314c26c2560000", + "solution": "0053f4438864bc5d6dfc009d4bba545ac5e5feaaf46f9455b975b02115f842a966e26517ce678f1c074d09cc8d0049a190859eb505af5f3e760312fbbe54da115db2bc03c96408f39b679891790b539d2d9d17a801dc6af9af14ca3f6ba060edce2a1dd45aa45f11fe37dbaf1eb2647ae7c393f6680c3d5d7e53687e34530f48edf58924a04d3e0231c150b1c8218998f674bc171edd222bcb4ac4ba4ea52d7baa86399f371d5284043e1e166f9069dd0f2904ff94c7922a70fa7c660e0553cc40a20d9ee08eb3f47278485801ddae9c270411360773f0b74e03db2d92c50952c9bd4924bbca2a260e1235e99df51fe71e75744232f2d641ef94f394110a5ad05f51a057e4cb515b92c16cb1404a8cdcc43d4a4bb2caa54ca35dccf41aa7d832da65123b7029223c46ed2a13387d598d445435d3cb32fdad9e27672903864c90d86353b162033078327b5b7aaffc89b40096ae004f2d5c6bd2c99188574348518db66e9b6020f93f12ee1c06f7b00fe346fefceaffb1da9e3cdf08285057f549733eb10825737fcd1431bfdfb155f323f24e95a869212baacf445b30f2670206645779110e6547d5da90a5f2fe5151da911d5ecd5a833023661d1356b6c395d85968947678d53efd4db7b06f23b21125e74492644277ea0c1131b80d6a4e3e8093b82332556fbb3255a55ac3f0b7e4844c0e12bf577c37fd02323ae5ef4781772ed501d63b568032a3d31576c5104a48c01ac54f715286932351a8adc8cf2467a84a0572e99f366ee00f82c3735545fd4bb941d591ce70070425a81304272db89887949bc7dd8236bb7e82190f9815da938cd6e8fec7660e91354326a7a9bfe38120e97997fca3c289d54513ed00286c2b825fbe84f91a39528f335674b5e957425a6edfdd00f2feb2c2df575616197998c1e964e069875d4d934f419a9b02b100848d023b76d47bd4e284c3895ef9227a40d8ea8826e86c7155d6aa95b8f9175812523a32cd611efc700688e03f7c245c5bff01718281b5d75cefe8318b2c08962236b14a0bf79534c203df735fd9cced97cbae07c2b4ee9cda8c9993f3f6277ff3fec261fb94d3961c4befe4b0893dcf67b312c7d8d6ff7adc8539cb2b1d3534fccf109efddd07a9f1e77b94ab1e505b164221dca1c34621b1e9d234c31a032a401267d95f65b800d579a2482638dfeade804149c81e95d7ef5510ac0b6212231506b1c635a2e1d2f0c9712989f9f246762fadb4c55c20f707dcc0e510a33e9465fc5d5bdbfa524dab0d7a1c6a1baaa36869cf542aa2257c5c44ef07547a570343442c6091e13bc04d559dc0e6db5b001861914bf956816edce2a86b274bd97f27e2dbb08608c16a3e5d8595952faa91fb162d7fa6a7a47e849a1ad8fab3ba620ee3295a04fe13e5fb655ac92ae60d01020b8999526af8d56b28733e69c9ffb285de27c61edc0bf62261ac0787eff347d0fcd62257301ede9603106ea41650a3e3119bd5c4e86a7f6a3f00934f3a545f7f21d41699f3e35d38cf925a8bdaf2bf7eedea11c31c3d8bf6c527c77c6378281cdf02211a58fa5e46d28d7e7c5fb79d69b31703fd752395da115845952cf99aaeb2155c2ab951a69f67d938f223185567e52cfa3e57b62c790bf78674c4b02c12b7d3225fe8f705b408ba11c24245b3924482e2f3480994461b550641a88cd941d371139f3498afacdcba1249631402b20695760eaada5376e68df0e45139c410700effc9420dc3726515e7fcb3f349320f30511451964bd9b6530682efec65910ceb548aa2ab05ac3309e803161697213631ae8e13cc7d223ac28446c1bf94a19a8782ac16ff57df7ee4f10fb6e488c02c68d6b6dee6987f6d2c39227da366c59f54ff67e312ca530e7c467c3dc8", + "bits": "2007ffff", + "difficulty": 1.0, + "chainSupply": { + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.000625, + "chainValueZat": 62500, + "monitored": true, + "valueDelta": 0.000625, + "valueDeltaZat": 62500 + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 + } + ], + "trees": {}, + "previousblockhash": "05a60a92d99d85997cce3b87616c089f6124d7342af37106edc76126334a2c38", + "nextblockhash": "00f1a49e54553ac3ef735f2eb1d8247c9a87c22a47dbd7823ae70adcd6c21a18" +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height_nsm@nu6testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height_nsm@nu6testnet_10.snap new file mode 100644 index 00000000000..1c72cbf8b1b --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height_nsm@nu6testnet_10.snap @@ -0,0 +1,101 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: info +--- +{ + "chain": "test", + "blocks": 10, + "headers": 10, + "difficulty": 1.0, + "verificationprogress": "[f64]", + "chainwork": 0, + "pruned": false, + "size_on_disk": 0, + "commitments": 0, + "bestblockhash": "079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112", + "estimatedheight": "[Height]", + "chainSupply": { + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + } + ], + "upgrades": { + "5ba81b19": { + "name": "Overwinter", + "activationheight": 584000, + "status": "pending" + }, + "76b809bb": { + "name": "Sapling", + "activationheight": 584000, + "status": "pending" + }, + "2bb40e60": { + "name": "Blossom", + "activationheight": 584000, + "status": "pending" + }, + "f5b9230b": { + "name": "Heartwood", + "activationheight": 2976000, + "status": "pending" + }, + "e9ff75a6": { + "name": "Canopy", + "activationheight": 2976000, + "status": "pending" + }, + "c2d6d0b4": { + "name": "NU5", + "activationheight": 2976000, + "status": "pending" + }, + "c8e71055": { + "name": "NU6", + "activationheight": 2976000, + "status": "pending" + } + }, + "consensus": { + "chaintip": "00000000", + "nextblock": "00000000" + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@mainnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@mainnet_10.snap new file mode 100644 index 00000000000..3e785de4994 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@mainnet_10.snap @@ -0,0 +1,106 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: info +--- +{ + "chain": "main", + "blocks": 10, + "headers": 10, + "difficulty": 1.0, + "verificationprogress": "[f64]", + "chainwork": 0, + "pruned": false, + "size_on_disk": 0, + "commitments": 0, + "bestblockhash": "00074c46a4aa8172df8ae2ad1848a2e084e1b6989b7d9e6132adc938bf835b36", + "estimatedheight": "[Height]", + "chainSupply": { + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + } + ], + "upgrades": { + "5ba81b19": { + "name": "Overwinter", + "activationheight": 347500, + "status": "pending" + }, + "76b809bb": { + "name": "Sapling", + "activationheight": 419200, + "status": "pending" + }, + "2bb40e60": { + "name": "Blossom", + "activationheight": 653600, + "status": "pending" + }, + "f5b9230b": { + "name": "Heartwood", + "activationheight": 903000, + "status": "pending" + }, + "e9ff75a6": { + "name": "Canopy", + "activationheight": 1046400, + "status": "pending" + }, + "c2d6d0b4": { + "name": "NU5", + "activationheight": 1687104, + "status": "pending" + }, + "c8e71055": { + "name": "NU6", + "activationheight": 2726400, + "status": "pending" + }, + "4dec4df0": { + "name": "NU6.1", + "activationheight": 3146400, + "status": "pending" + } + }, + "consensus": { + "chaintip": "00000000", + "nextblock": "00000000" + } +} diff --git a/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@testnet_10.snap b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@testnet_10.snap new file mode 100644 index 00000000000..7739c2bba65 --- /dev/null +++ b/zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@testnet_10.snap @@ -0,0 +1,106 @@ +--- +source: zebra-rpc/src/methods/tests/snapshot.rs +expression: info +--- +{ + "chain": "test", + "blocks": 10, + "headers": 10, + "difficulty": 1.0, + "verificationprogress": "[f64]", + "chainwork": 0, + "pruned": false, + "size_on_disk": 0, + "commitments": 0, + "bestblockhash": "079f4c752729be63e6341ee9bce42fbbe37236aba22e3deb82405f3c2805c112", + "estimatedheight": "[Height]", + "chainSupply": { + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + "valuePools": [ + { + "id": "transparent", + "chainValue": 0.034375, + "chainValueZat": 3437500, + "monitored": true + }, + { + "id": "sprout", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "sapling", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "orchard", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lockbox", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + }, + { + "id": "lts", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false + } + ], + "upgrades": { + "5ba81b19": { + "name": "Overwinter", + "activationheight": 207500, + "status": "pending" + }, + "76b809bb": { + "name": "Sapling", + "activationheight": 280000, + "status": "pending" + }, + "2bb40e60": { + "name": "Blossom", + "activationheight": 584000, + "status": "pending" + }, + "f5b9230b": { + "name": "Heartwood", + "activationheight": 903800, + "status": "pending" + }, + "e9ff75a6": { + "name": "Canopy", + "activationheight": 1028500, + "status": "pending" + }, + "c2d6d0b4": { + "name": "NU5", + "activationheight": 1842420, + "status": "pending" + }, + "c8e71055": { + "name": "NU6", + "activationheight": 2976000, + "status": "pending" + }, + "4dec4df0": { + "name": "NU6.1", + "activationheight": 3536500, + "status": "pending" + } + }, + "consensus": { + "chaintip": "00000000", + "nextblock": "00000000" + } +} diff --git a/zebra-rpc/src/methods/types/get_block_template.rs b/zebra-rpc/src/methods/types/get_block_template.rs index 5916a438e07..2730a74d295 100644 --- a/zebra-rpc/src/methods/types/get_block_template.rs +++ b/zebra-rpc/src/methods/types/get_block_template.rs @@ -280,9 +280,7 @@ impl BlockTemplateResponse { #[cfg(test)] mempool_txs: Vec<(InBlockTxDependenciesDepth, VerifiedUnminedTx)>, submit_old: Option, extra_coinbase_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, + #[cfg(zcash_unstable = "nsm")] lts_payout: Amount, ) -> Self { // Calculate the next block height. let next_block_height = @@ -334,8 +332,8 @@ impl BlockTemplateResponse { &mempool_txs, chain_tip_and_local_time.chain_history_root, extra_coinbase_data, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - zip233_amount, + #[cfg(zcash_unstable = "nsm")] + lts_payout, ) .expect("coinbase should be valid under the given parameters"); @@ -781,6 +779,90 @@ where Ok(chain_info) } +/// Computes the LTS (Long-Term Support / NSM / ZIP-234) payout that the +/// coinbase at `next_height` is allowed to claim, by looking up the parent +/// block's LTS pool snapshot via the read-state service and applying the +/// same ZIP-234 smooth-issuance ceiling formula as the contextual verifier. +/// +/// Returns `Amount::zero()` outside the disbursement window (including when +/// NU7 is unconfigured). +#[cfg(zcash_unstable = "nsm")] +pub async fn expected_lts_payout( + network: &Network, + next_height: Height, + state: State, +) -> RpcResult> +where + State: Service< + zebra_state::ReadRequest, + Response = zebra_state::ReadResponse, + Error = zebra_state::BoxError, + > + Clone + + Send + + Sync + + 'static, +{ + use zebra_chain::parameters::subsidy::{lts_disbursement_start, lts_payout}; + + let Some(start) = lts_disbursement_start(network) else { + return Ok(Amount::zero()); + }; + + if next_height < start { + return Ok(Amount::zero()); + } + + // parent_height = next_height - 1 is always available because + // next_height ≥ disbursement_start ≥ NU7 activation > 0. + let parent_height = next_height + .previous() + .expect("next_height ≥ disbursement_start > 0, parent height exists"); + + let parent_pool = read_lts_pool_at(state, parent_height).await?; + + Ok(lts_payout(next_height, network, parent_pool)) +} + +/// Read the LTS pool balance recorded *after* the block at `height` from the +/// best chain via [`zebra_state::ReadRequest::BlockInfo`]. +#[cfg(zcash_unstable = "nsm")] +async fn read_lts_pool_at(state: State, height: Height) -> RpcResult> +where + State: Service< + zebra_state::ReadRequest, + Response = zebra_state::ReadResponse, + Error = zebra_state::BoxError, + > + Clone + + Send + + Sync + + 'static, +{ + use zebra_state::{HashOrHeight, ReadRequest, ReadResponse}; + + let response = state + .oneshot(ReadRequest::BlockInfo(HashOrHeight::Height(height))) + .await + .map_err(|error| ErrorObject::owned(0, error.to_string(), None::<()>))?; + + let block_info = match response { + ReadResponse::BlockInfo(info) => info, + _ => unreachable!("incorrect response to ReadRequest::BlockInfo"), + }; + + let block_info = block_info.ok_or_else(|| { + ErrorObject::owned( + 0, + format!( + "no block info available at ancestor height {height:?} \ + needed for LTS payout computation" + ), + None::<()>, + ) + })?; + + Ok(block_info.value_pools().lts_amount()) +} + /// Returns the transactions that are currently in `mempool`, or None if the /// `last_seen_tip_hash` from the mempool response doesn't match the tip hash from the state. /// @@ -829,31 +911,24 @@ pub fn generate_coinbase_and_roots( mempool_txs: &[VerifiedUnminedTx], chain_history_root: Option, miner_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, + #[cfg(zcash_unstable = "nsm")] lts_payout: Amount, ) -> Result<(TransactionTemplate, DefaultRoots), &'static str> { let miner_fee = calculate_miner_fee(mempool_txs); - let outputs = standard_coinbase_outputs(network, height, miner_address, miner_fee); let current_nu = NetworkUpgrade::current(network, height); + let outputs = standard_coinbase_outputs( + network, + height, + miner_address, + miner_fee, + #[cfg(zcash_unstable = "nsm")] + lts_payout, + ); let tx = match current_nu { NetworkUpgrade::Canopy => Transaction::new_v4_coinbase(height, outputs, miner_data), - NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 => { + NetworkUpgrade::Nu5 | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 | NetworkUpgrade::Nu7 => { Transaction::new_v5_coinbase(network, height, outputs, miner_data) } - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] - NetworkUpgrade::Nu7 => Transaction::new_v5_coinbase(network, height, outputs, miner_data), - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - NetworkUpgrade::Nu7 => Transaction::new_v6_coinbase( - network, - height, - outputs, - miner_data, - zip233_amount, - #[cfg(zcash_unstable = "zip235")] - miner_fee, - ), _ => Err("Zebra does not support generating pre-Canopy coinbase transactions")?, } .into(); @@ -894,6 +969,7 @@ pub fn standard_coinbase_outputs( height: Height, miner_address: &Address, miner_fee: Amount, + #[cfg(zcash_unstable = "nsm")] lts_payout: 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) @@ -904,6 +980,23 @@ pub fn standard_coinbase_outputs( + miner_fee; let miner_reward = miner_reward.expect("reward calculations are valid for reasonable chain heights"); + // The LTS payout is funded from the on-chain LTS pool (accumulated burn), + // and routed into the miner's primary transparent output. The contextual + // verifier checks that the claimed amount matches the per-block rate + // derived from the chain's pool history. + #[cfg(zcash_unstable = "nsm")] + let miner_reward = + (miner_reward + lts_payout).expect("miner reward + lts payout is bounded by MAX_MONEY"); + #[cfg(all(zcash_unstable = "nsm", zcash_unstable = "zip235"))] + let miner_reward = if NetworkUpgrade::current(network, height) >= NetworkUpgrade::Nu7 { + let minimum_zip233_amount = + zebra_chain::transaction::builder::zip235_minimum_zip233_amount(miner_fee); + + (miner_reward - minimum_zip233_amount) + .expect("coinbase ZIP-233 minimum is funded by the miner fee") + } else { + miner_reward + }; // Optional TODO: move this into a zebra_consensus function? let funding_streams: HashMap< diff --git a/zebra-rpc/src/methods/types/get_block_template/tests.rs b/zebra-rpc/src/methods/types/get_block_template/tests.rs index 09d99e21f5c..5eec151004b 100644 --- a/zebra-rpc/src/methods/types/get_block_template/tests.rs +++ b/zebra-rpc/src/methods/types/get_block_template/tests.rs @@ -16,12 +16,21 @@ use zebra_chain::{ use super::{check_block_template_supported, standard_coinbase_outputs}; +#[cfg(all(zcash_unstable = "nsm", zcash_unstable = "zip235"))] +use super::generate_coinbase_and_roots; +#[cfg(all(zcash_unstable = "nsm", zcash_unstable = "zip235"))] +use zebra_chain::{ + amount::{NegativeAllowed, NonNegative}, + block, + parameters::subsidy::block_subsidy, +}; + #[test] fn block_template_before_canopy_returns_error() -> Result<(), Box> { let network = Network::new_regtest( ConfiguredActivationHeights { overwinter: Some(5), - nu7: Some(5), + canopy: Some(5), ..Default::default() } .into(), @@ -59,6 +68,8 @@ fn minimal_coinbase() -> Result<(), Box> { Height(1), &Address::from(TransparentAddress::PublicKeyHash([0x42; 20])), Amount::zero(), + #[cfg(zcash_unstable = "nsm")] + Amount::zero(), ); // It should be possible to generate a coinbase tx from these params. @@ -69,3 +80,158 @@ fn minimal_coinbase() -> Result<(), Box> { Ok(()) } + +/// `standard_coinbase_outputs` routes the LTS payout into the miner's primary +/// transparent output. Increasing `lts_payout` by `delta` must increase the +/// first output's amount by exactly `delta`, with all other outputs (funding +/// streams, lockbox disbursements, miner reward at zero LTS) unchanged. +/// +/// This pins down the contract the contextual verifier relies on: the claim +/// the miner makes via `coinbase_outputs` matches the per-block rate computed +/// from the pool snapshot. +#[cfg(zcash_unstable = "nsm")] +#[test] +fn standard_coinbase_outputs_route_lts_payout_into_miner_reward( +) -> Result<(), Box> { + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu6: Some(1), + ..Default::default() + })? + .with_funding_streams(vec![ConfiguredFundingStreams { + height_range: Some(Height(1)..Height(10)), + recipients: None, + }]) + .to_network()?; + + let miner_address = Address::from(TransparentAddress::PublicKeyHash([0x42; 20])); + let miner_fee = Amount::zero(); + let lts_payout = Amount::try_from(123_456_u64)?; + + // Baseline: no LTS payout. + let baseline = standard_coinbase_outputs( + ®test, + Height(1), + &miner_address, + miner_fee, + Amount::zero(), + ); + // With a non-zero LTS payout. + let with_lts = + standard_coinbase_outputs(®test, Height(1), &miner_address, miner_fee, lts_payout); + + assert_eq!( + baseline.len(), + with_lts.len(), + "LTS payout must not change the number of outputs" + ); + + // The miner reward is always the first output (see standard_coinbase_outputs). + let (baseline_miner_amount, baseline_miner_script) = &baseline[0]; + let (lts_miner_amount, lts_miner_script) = &with_lts[0]; + assert_eq!( + baseline_miner_script, lts_miner_script, + "miner reward script must not depend on lts_payout" + ); + let delta = (*lts_miner_amount - *baseline_miner_amount)?; + assert_eq!( + lts_payout, delta, + "miner reward must grow by exactly the lts_payout" + ); + + // All other outputs (funding streams, lockboxes) must be byte-identical + // between the two calls — only the miner reward changes. + assert_eq!(&baseline[1..], &with_lts[1..]); + + Ok(()) +} + +/// Fee-bearing generated coinbases must round-trip through the same +/// value-balance equation used by the semantic and contextual verifiers. +#[cfg(all(zcash_unstable = "nsm", zcash_unstable = "zip235"))] +#[test] +fn generated_coinbase_implies_lts_payout_net_zip235_deposit( +) -> Result<(), Box> { + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + })? + .to_network()?; + + let height = Height(2); + let miner_address = Address::from(TransparentAddress::PublicKeyHash([0x42; 20])); + let miner_fee = Amount::try_from(10_000_u64)?; + let lts_payout = Amount::try_from(123_u64)?; + let minimum_deposit = + zebra_chain::transaction::builder::zip235_minimum_zip233_amount(miner_fee); + let outputs = + standard_coinbase_outputs(®test, height, &miner_address, miner_fee, lts_payout); + let coinbase_tx = Transaction::new_v5_coinbase(®test, height, outputs, Vec::new()); + let coinbase_tx = coinbase_tx + .zcash_serialize_to_vec()? + .zcash_deserialize_into::()?; + + let transparent_value: Amount = coinbase_tx + .outputs() + .iter() + .map(|output| output.value()) + .sum::, _>>()? + .constrain()?; + let total_output_value = transparent_value; + + let expected_block_subsidy: Amount = + block_subsidy(height, ®test)?.constrain()?; + let miner_fee: Amount = miner_fee.constrain()?; + let total_input_value = (expected_block_subsidy + miner_fee)?; + + let implied_lts_claim = (total_output_value - total_input_value)?; + let lts_payout: Amount = lts_payout.constrain()?; + let minimum_deposit_signed: Amount = minimum_deposit.constrain()?; + let implied_deposit = (lts_payout - implied_lts_claim)?; + + assert_eq!( + minimum_deposit, implied_deposit, + "generated coinbase must under-claim by the ZIP-235 minimum" + ); + assert_eq!( + (lts_payout - minimum_deposit_signed)?, + implied_lts_claim, + "v5 coinbase output must imply the LTS payout net of the ZIP-235 deposit" + ); + + Ok(()) +} + +/// The full template coinbase path must stay on v5 at NU7, so computing the +/// coinbase template ID must not enter the removed V6 txid/auth-digest path. +#[cfg(all(zcash_unstable = "nsm", zcash_unstable = "zip235"))] +#[test] +#[allow(clippy::unwrap_in_result)] +fn nu7_generate_coinbase_and_roots_uses_v5_template_path() -> Result<(), Box> +{ + let regtest = testnet::Parameters::build() + .with_slow_start_interval(Height::MIN) + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + })? + .to_network()?; + + let (coinbase_txn, _default_roots) = generate_coinbase_and_roots( + ®test, + Height(2), + &Address::from(TransparentAddress::PublicKeyHash([0x42; 20])), + &[], + Some(block::CHAIN_HISTORY_ACTIVATION_RESERVED.into()), + Vec::new(), + Amount::zero(), + ) + .expect("NU7 v5 coinbase template should be generated without V6 txid support"); + + assert!(!coinbase_txn.data.as_ref().is_empty()); + + Ok(()) +} diff --git a/zebra-rpc/src/methods/types/get_block_template/zip317.rs b/zebra-rpc/src/methods/types/get_block_template/zip317.rs index 9551e9babb7..8833f7cae11 100644 --- a/zebra-rpc/src/methods/types/get_block_template/zip317.rs +++ b/zebra-rpc/src/methods/types/get_block_template/zip317.rs @@ -29,12 +29,9 @@ use zebra_node_services::mempool::TransactionDependencies; use crate::methods::types::transaction::TransactionTemplate; -#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +#[cfg(zcash_unstable = "nsm")] use crate::methods::Amount; -#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] -use zebra_chain::amount::NonNegative; - #[cfg(test)] mod tests; @@ -69,9 +66,6 @@ pub fn select_mempool_transactions( mempool_txs: Vec, mempool_tx_deps: TransactionDependencies, extra_coinbase_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, ) -> Vec { // Use a fake coinbase transaction to break the dependency between transaction // selection, the miner fee, and the fee payment in the coinbase transaction. @@ -80,8 +74,6 @@ pub fn select_mempool_transactions( next_block_height, miner_address, extra_coinbase_data, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - zip233_amount, ); let tx_dependencies = mempool_tx_deps.dependencies(); @@ -239,9 +231,6 @@ pub fn fake_coinbase_transaction( height: Height, miner_address: &Address, extra_coinbase_data: Vec, - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] zip233_amount: Option< - Amount, - >, ) -> TransactionTemplate { // Block heights are encoded as variable-length (script) and `u32` (lock time, expiry height). // They can also change the `u32` consensus branch id. @@ -253,35 +242,26 @@ pub fn fake_coinbase_transaction( // so one zat has the same size as the real amount: // https://developer.bitcoin.org/reference/transactions.html#txout-a-transaction-output let miner_fee = 1.try_into().expect("amount is valid and non-negative"); - let outputs = standard_coinbase_outputs(net, height, miner_address, miner_fee); - let network_upgrade = NetworkUpgrade::current(net, height); + // The LTS payout and ZIP-233 transparent-output adjustment only affect the + // i64 amount in the miner's transparent output, not its serialized size or + // sigop count, so the fake coinbase used for transaction selection + // budgeting can ignore them. + let outputs = standard_coinbase_outputs( + net, + height, + miner_address, + miner_fee, + #[cfg(zcash_unstable = "nsm")] + Amount::zero(), + ); - #[cfg(not(all(zcash_unstable = "nu7", feature = "tx_v6")))] let coinbase = if network_upgrade.branch_id().is_none() { Transaction::new_v4_coinbase(height, outputs, extra_coinbase_data).into() } else { Transaction::new_v5_coinbase(net, height, outputs, extra_coinbase_data).into() }; - #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] - let coinbase = if network_upgrade.branch_id().is_none() { - Transaction::new_v4_coinbase(height, outputs, extra_coinbase_data).into() - } else if network_upgrade < NetworkUpgrade::Nu7 { - Transaction::new_v5_coinbase(net, height, outputs, extra_coinbase_data).into() - } else { - Transaction::new_v6_coinbase( - net, - height, - outputs, - extra_coinbase_data, - zip233_amount, - #[cfg(zcash_unstable = "zip235")] - miner_fee, - ) - .into() - }; - TransactionTemplate::from_coinbase(&coinbase, miner_fee) } diff --git a/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs b/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs index ae30359b04e..b9f80aa06fa 100644 --- a/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs +++ b/zebra-rpc/src/methods/types/get_block_template/zip317/tests.rs @@ -32,21 +32,13 @@ fn fake_coinbase_before_branch_id_does_not_panic() -> Result<(), Box [Self; 5] { + pub fn zero_pools() -> crate::methods::BlockchainValuePoolBalances { Self::value_pools(Default::default(), None) } @@ -87,11 +87,17 @@ impl GetBlockchainInfoBalance { Self::new_internal("lockbox", amount, delta) } + /// Creates a [`GetBlockchainInfoBalance`] for the LTS (NSM) pool. + #[cfg(zcash_unstable = "nsm")] + pub fn lts(amount: Amount, delta: Option>) -> Self { + Self::new_internal("lts", amount, delta) + } + /// Converts a [`ValueBalance`] to a list of [`GetBlockchainInfoBalance`]s. pub fn value_pools( value_balance: ValueBalance, delta_balance: Option>, - ) -> [Self; 5] { + ) -> crate::methods::BlockchainValuePoolBalances { [ Self::transparent( value_balance.transparent_amount(), @@ -113,6 +119,11 @@ impl GetBlockchainInfoBalance { value_balance.deferred_amount(), delta_balance.map(|b| b.deferred_amount()), ), + #[cfg(zcash_unstable = "nsm")] + Self::lts( + value_balance.lts_amount(), + delta_balance.map(|b| b.lts_amount()), + ), ] } diff --git a/zebra-state/src/constants.rs b/zebra-state/src/constants.rs index 9e46d79e185..126cfa63908 100644 --- a/zebra-state/src/constants.rs +++ b/zebra-state/src/constants.rs @@ -55,7 +55,10 @@ const DATABASE_FORMAT_VERSION: u64 = 27; /// - adding new column families, /// - changing the format of a column family in a compatible way, or /// - breaking changes with compatibility code in all supported Zebra versions. +#[cfg(not(zcash_unstable = "nsm"))] const DATABASE_FORMAT_MINOR_VERSION: u64 = 0; +#[cfg(zcash_unstable = "nsm")] +const DATABASE_FORMAT_MINOR_VERSION: u64 = 1; /// The database format patch version, incremented each time the on-disk database format has a /// significant format compatibility fix. diff --git a/zebra-state/src/error.rs b/zebra-state/src/error.rs index 5b4504f57f3..f48773548c5 100644 --- a/zebra-state/src/error.rs +++ b/zebra-state/src/error.rs @@ -6,6 +6,8 @@ use chrono::{DateTime, Utc}; use derive_new::new; use thiserror::Error; +#[cfg(zcash_unstable = "nsm")] +use zebra_chain::amount::Amount; use zebra_chain::{ amount::{self, NegativeAllowed, NonNegative}, block, @@ -339,6 +341,23 @@ pub enum ValidateContextError { height: Option, }, + #[cfg(zcash_unstable = "nsm")] + #[error("missing BlockInfo for LTS (NSM) ancestor height {height:?}")] + #[non_exhaustive] + MissingLtsBlockInfo { height: block::Height }, + + #[cfg(zcash_unstable = "nsm")] + #[error( + "invalid LTS (NSM) deposit at {height:?}: \ + expected at least {expected_minimum:?}, actual {actual:?}" + )] + #[non_exhaustive] + InvalidLtsDeposit { + height: block::Height, + expected_minimum: Amount, + actual: Amount, + }, + #[error("error updating a note commitment tree: {0}")] NoteCommitmentTreeError(#[from] zebra_chain::parallel::tree::NoteCommitmentTreeError), diff --git a/zebra-state/src/request.rs b/zebra-state/src/request.rs index 82620468285..f47b8b2fc26 100644 --- a/zebra-state/src/request.rs +++ b/zebra-state/src/request.rs @@ -360,7 +360,10 @@ impl Treestate { /// /// Zebra's state service passes this `enum` over to the finalized state /// when committing a block. -#[allow(missing_docs)] +// This value is constructed and consumed once per block commit (never stored +// in a collection), so the variant size difference doesn't justify boxing the +// large field and paying a heap allocation on the commit path. +#[allow(missing_docs, clippy::large_enum_variant)] pub enum FinalizableBlock { Checkpoint { checkpoint_verified: CheckpointVerifiedBlock, diff --git a/zebra-state/src/service/check.rs b/zebra-state/src/service/check.rs index bc590689c01..25d8bed38f7 100644 --- a/zebra-state/src/service/check.rs +++ b/zebra-state/src/service/check.rs @@ -28,6 +28,8 @@ use crate::service::non_finalized_state::Chain; pub(crate) mod anchors; pub(crate) mod difficulty; +#[cfg(zcash_unstable = "nsm")] +pub(crate) mod lts; pub(crate) mod nullifier; pub(crate) mod utxo; diff --git a/zebra-state/src/service/check/difficulty.rs b/zebra-state/src/service/check/difficulty.rs index 19ec7f02b88..08f6dac6db5 100644 --- a/zebra-state/src/service/check/difficulty.rs +++ b/zebra-state/src/service/check/difficulty.rs @@ -382,7 +382,7 @@ pub fn pow_adjustment_block_span_for_height(network: &Network, height: block::He NetworkUpgrade::averaging_window_for_height(network, height) + POW_MEDIAN_BLOCK_SPAN } -#[cfg(test)] +#[cfg(all(test, zcash_unstable = "nu7"))] mod tests { use super::*; diff --git a/zebra-state/src/service/check/lts.rs b/zebra-state/src/service/check/lts.rs new file mode 100644 index 00000000000..9158f7f1fad --- /dev/null +++ b/zebra-state/src/service/check/lts.rs @@ -0,0 +1,281 @@ +//! Contextual check for ZIP-234/235 (NSM) Long-Term Support pool payouts. +//! +//! The semantic verifier (`miner_fees_are_valid`) only checks that the +//! coinbase value equation is arithmetically well-formed. This module +//! re-derives the signed implied claim from the block bytes and checks it +//! against the expected per-block payout computed from the parent block's LTS +//! pool snapshot (ZIP-234 smooth-issuance ceiling rule) and the ZIP-235 +//! minimum deposit. + +use std::collections::HashMap; + +use zebra_chain::{ + amount::{Amount, NegativeAllowed, NonNegative}, + block::{Block, Height}, + parameters::{ + subsidy::{block_subsidy, funding_stream_values, lts_disbursement_start, lts_payout}, + Network, NetworkUpgrade, + }, + transparent, + value_balance::ValueBalanceError, +}; + +use crate::{ + service::{finalized_state::ZebraDb, non_finalized_state::Chain}, + HashOrHeight, ValidateContextError, +}; + +/// Validate the LTS claim implied by the coinbase of `block` matches the +/// expected per-block payout for `height` given the chain's pool history. +/// +/// `parent_chain` is the candidate non-finalized chain the new block is being +/// appended to (it may be empty if the block extends the finalized tip +/// directly). The parent-block LTS pool snapshot is resolved by looking at +/// `parent_chain` first, then falling back to `finalized_state` — so each +/// fork's claim is validated against its own pool history. +/// +/// `spent_utxos` must contain the UTXOs spent by every transparent input in +/// `block` (including outputs created by earlier transactions in the same +/// block); those are used to compute the per-block miner fees needed to +/// re-derive the implied claim. +/// +/// On success returns the LTS pool delta this block contributes (the signed +/// coinbase under-/over-claim, i.e. `-implied_claim`), which the caller sets +/// on the block's `chain_value_pool_change`. This avoids re-deriving the +/// value balance in [`block_lts_pool_delta`] on the non-finalized path. +/// Returns `InvalidLtsDeposit` if the miner over-claims relative to the +/// scheduled payout and required deposit. +#[allow(clippy::unwrap_in_result)] +pub(crate) fn check_claimed_lts_payout( + network: &Network, + parent_chain: &Chain, + finalized_state: &ZebraDb, + height: Height, + block: &Block, + spent_utxos: &HashMap, +) -> Result, ValidateContextError> { + // The LTS contextual check only applies once NSM activates at NU7. Before + // NU7, the semantic verifier already enforces strict transparent + // conservation (NU6 onward) or the historical pre-NU6 inequality, so the + // implied-claim derivation against pre-NSM blocks would fight that math. + let Some(nsm_activation_height) = NetworkUpgrade::Nu7.activation_height(network) else { + return Ok(Amount::zero()); + }; + if height < nsm_activation_height { + return Ok(Amount::zero()); + } + + let expected = expected_lts_payout(network, parent_chain, finalized_state, height)?; + let LtsValueBalance { + implied_claim, + block_miner_fees, + } = derive_lts_value_balance(block, network, height, spent_utxos).map_err( + |value_balance_error| ValidateContextError::CalculateBlockChainValueChange { + value_balance_error, + height, + block_hash: block.hash(), + transaction_count: block.transactions.len(), + spent_utxo_count: spent_utxos.len(), + }, + )?; + + let expected_claim: Amount = + expected.constrain().map_err(|value_balance_error| { + ValidateContextError::CalculateBlockChainValueChange { + value_balance_error: ValueBalanceError::Lts(value_balance_error), + height, + block_hash: block.hash(), + transaction_count: block.transactions.len(), + spent_utxo_count: spent_utxos.len(), + } + })?; + let implied_deposit = (expected_claim - implied_claim).map_err(|value_balance_error| { + ValidateContextError::CalculateBlockChainValueChange { + value_balance_error: ValueBalanceError::Lts(value_balance_error), + height, + block_hash: block.hash(), + transaction_count: block.transactions.len(), + spent_utxo_count: spent_utxos.len(), + } + })?; + + let minimum_deposit = + zebra_chain::transaction::builder::zip235_minimum_zip233_amount(block_miner_fees); + + if implied_deposit < minimum_deposit { + return Err(ValidateContextError::InvalidLtsDeposit { + height, + expected_minimum: minimum_deposit, + actual: implied_deposit, + }); + } + + Ok(-implied_claim) +} + +/// Returns the LTS pool delta this block contributes to the chain pool. +/// +/// In the no-v6 NSM model, the LTS pool delta is the negative signed implied +/// coinbase claim. A coinbase under-claim increases the pool; an over-claim +/// decreases it. +/// +/// Used by the finalized commit path to set the `lts_amount` leg of the +/// per-block `chain_value_pool_change`. The non-finalized path reuses the +/// delta returned by [`check_claimed_lts_payout`] instead of calling this. +pub(crate) fn block_lts_pool_delta( + block: &Block, + network: &Network, + height: Height, + spent_utxos: &HashMap, +) -> Result, ValueBalanceError> { + let Some(nsm_activation_height) = NetworkUpgrade::Nu7.activation_height(network) else { + return Ok(Amount::zero()); + }; + if height < nsm_activation_height { + return Ok(Amount::zero()); + } + + let implied_claim = + derive_lts_value_balance(block, network, height, spent_utxos)?.implied_claim; + + Ok(-implied_claim) +} + +/// Computes the expected LTS payout for `height` from chain history. +/// +/// `parent_chain` is the candidate non-finalized chain the new block is being +/// appended to (it may be empty if the block extends the finalized tip +/// directly). The parent-block LTS pool snapshot is resolved by looking at +/// `parent_chain` first, then falling back to `finalized_state` — so each +/// fork's expected payout reflects its own pool history. +#[allow(clippy::unwrap_in_result)] +pub(crate) fn expected_lts_payout( + network: &Network, + parent_chain: &Chain, + finalized_state: &ZebraDb, + height: Height, +) -> Result, ValidateContextError> { + let Some(start) = lts_disbursement_start(network) else { + return Ok(Amount::zero()); + }; + if height < start { + return Ok(Amount::zero()); + } + + // The parent pool snapshot is the LTS pool *after* the block at + // `height - 1`. height ≥ disbursement_start ≥ NU7 activation > 0, so the + // parent height is always available. + let parent_height = height + .previous() + .expect("height ≥ disbursement_start > 0, parent height exists"); + let parent_pool = resolve_lts_pool_at(parent_chain, finalized_state, parent_height)?; + + Ok(lts_payout(height, network, parent_pool)) +} + +struct LtsValueBalance { + implied_claim: Amount, + block_miner_fees: Amount, +} + +/// Re-derive the LTS value-balance terms from the coinbase value-balance equation. +/// Mirrors the formula in `miner_fees_are_valid` in `zebra-consensus`, +/// without that crate's tower-service plumbing. +/// +/// `implied_claim = total_coinbase_output − (expected_subsidy + block_miner_fees)` +/// +/// where `total_coinbase_output` is the coinbase's transparent + shielded + +/// deferred contribution to the chain value pool, and `block_miner_fees` is +/// the sum of per-tx miner fees over non-coinbase transactions. +#[allow(clippy::unwrap_in_result)] +fn derive_lts_value_balance( + block: &Block, + network: &Network, + height: Height, + spent_utxos: &HashMap, +) -> Result { + use zebra_chain::amount::Error as AmountError; + use zebra_chain::parameters::subsidy::FundingStreamReceiver; + + let coinbase_tx = block + .transactions + .first() + .expect("verified block has a coinbase transaction"); + + // Coinbase total output (transparent − sapling − orchard + deferred). + let transparent_value_balance: Amount = coinbase_tx + .outputs() + .iter() + .map(|output| output.value()) + .sum::, AmountError>>() + .map_err(ValueBalanceError::Transparent)? + .constrain() + .map_err(ValueBalanceError::Transparent)?; + let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount(); + let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount(); + + // Expected block subsidy and deferred-pool contribution for this height. + // Both are pure functions of `(height, network)`; they only fail on + // misconfigured networks (NU activation heights missing or out of range) + // — none of which can apply here, since we've already passed the + // semantic verifier. Mirror the `subsidy_is_valid` derivation. + let expected_block_subsidy = block_subsidy(height, network) + .expect("contextual LTS check: block_subsidy is valid for verified-height block"); + let mut funding_streams = funding_stream_values(height, network, expected_block_subsidy) + .expect("contextual LTS check: funding stream values are valid for verified-height block"); + let deferred_pool_balance_change_nn = funding_streams + .remove(&FundingStreamReceiver::Deferred) + .unwrap_or_default(); + let deferred_pool_balance_change: Amount = deferred_pool_balance_change_nn + .constrain() + .map_err(ValueBalanceError::Transparent)?; + + let total_output_value = + (transparent_value_balance - sapling_value_balance - orchard_value_balance + + deferred_pool_balance_change) + .map_err(ValueBalanceError::Transparent)?; + + // Block miner fees match the formula in `zebra-consensus`'s transaction + // verifier (`Response::miner_fee()`). Coinbase contributes nothing to fees. + let mut block_miner_fees: Amount = Amount::zero(); + for tx in block.transactions.iter().skip(1) { + let vb = tx.value_balance(spent_utxos)?; + let rtv = vb + .remaining_transaction_value() + .map_err(ValueBalanceError::Transparent)?; + block_miner_fees = (block_miner_fees + rtv).map_err(ValueBalanceError::Transparent)?; + } + + let total_input_value: Amount = (expected_block_subsidy + block_miner_fees) + .map_err(ValueBalanceError::Transparent)? + .constrain() + .map_err(ValueBalanceError::Transparent)?; + + let implied_claim = + (total_output_value - total_input_value).map_err(ValueBalanceError::Transparent)?; + + Ok(LtsValueBalance { + implied_claim, + block_miner_fees, + }) +} + +/// Resolve the LTS pool balance *after* the block at `height` by consulting +/// the non-finalized chain first, then the finalized state. +/// +/// Returns an error if neither the chain nor the finalized state has block info +/// at `height`. This is a contextual invariant: every height ≤ tip has a +/// `BlockInfo` record (after the v27 disk-format upgrade), and we only call +/// this with heights that are ancestors of the candidate block. +fn resolve_lts_pool_at( + parent_chain: &Chain, + finalized_state: &ZebraDb, + height: Height, +) -> Result, ValidateContextError> { + let info = parent_chain + .block_info(HashOrHeight::Height(height)) + .or_else(|| finalized_state.block_info(HashOrHeight::Height(height))) + .ok_or(ValidateContextError::MissingLtsBlockInfo { height })?; + + Ok(info.value_pools().lts_amount()) +} diff --git a/zebra-state/src/service/check/tests.rs b/zebra-state/src/service/check/tests.rs index 5c99d1218bf..d7371a8b26c 100644 --- a/zebra-state/src/service/check/tests.rs +++ b/zebra-state/src/service/check/tests.rs @@ -3,6 +3,8 @@ #![allow(clippy::unwrap_in_result)] mod anchors; +#[cfg(zcash_unstable = "nsm")] +mod lts; mod nullifier; mod utxo; mod vectors; diff --git a/zebra-state/src/service/check/tests/lts.rs b/zebra-state/src/service/check/tests/lts.rs new file mode 100644 index 00000000000..ee9af3136ab --- /dev/null +++ b/zebra-state/src/service/check/tests/lts.rs @@ -0,0 +1,654 @@ +//! Tests for the chain-history-based portion of the contextual LTS / NSM +//! payout check ([`super::super::lts::expected_lts_payout`]). +//! +//! These tests exercise the resolver against a hand-built [`Chain`] whose +//! `block_info_by_height` map carries planted LTS pool snapshots. The +//! [`ZebraDb`] passed to the check is empty — the test layout keeps every +//! ancestor we look up inside `parent_chain`, so the finalized fallback is +//! never hit. End-to-end tests of `validate_and_commit` would also exercise +//! the fallback path; that's covered by the full-chain integration tests +//! and is out of scope here. +//! +//! The block-bytes side of the contextual check (the implied-claim +//! derivation and ZIP-235 deposit floor in +//! [`super::super::lts::check_claimed_lts_payout`]) is exercised both by the +//! deposit/over-claim tests below and by full-block validation tests. +//! +//! Under the continuous-payout rule, the only state lookup is the parent +//! block's LTS pool snapshot — there is no era-start lookup. + +use std::{collections::HashMap, sync::Arc}; + +use chrono::DateTime; + +use zebra_chain::{ + amount::{Amount, NegativeAllowed, NonNegative}, + block::{merkle, Block, Hash, Header, Height}, + block_info::BlockInfo, + fmt::HexDebug, + parameters::{ + subsidy::{ + block_subsidy, funding_stream_values, lts_disbursement_start, lts_payout, + FundingStreamReceiver, + }, + testnet::ConfiguredActivationHeights, + Network, NetworkUpgrade, + }, + transaction::{self, LockTime, Transaction}, + transparent, + value_balance::ValueBalance, + work::{difficulty::ParameterDifficulty, equihash::Solution}, +}; + +use crate::{ + service::{ + check::lts::{check_claimed_lts_payout, expected_lts_payout}, + finalized_state::FinalizedState, + non_finalized_state::Chain, + }, + Config, ValidateContextError, +}; + +/// Builds a regtest with NU7 active at height 1 — the same network used by +/// the unit tests on `lts_payout`. +fn regtest_nu7_at_1() -> Network { + Network::new_regtest( + ConfiguredActivationHeights { + nu7: Some(1), + ..Default::default() + } + .into(), + ) +} + +/// Constructs an empty [`Chain`] suitable for injecting `BlockInfo` records +/// directly. The note-commitment trees and history tree are defaulted; we +/// don't exercise any path that reads them. +fn empty_chain(network: &Network) -> Chain { + Chain::new( + network, + Height(0), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ValueBalance::zero(), + ) +} + +/// Plants a [`BlockInfo`] record at `height` carrying `lts_pool` as the LTS +/// balance. Other pool fields are left at zero — the LTS check only reads +/// `value_pools().lts_amount()`. +fn plant_lts_pool(chain: &mut Chain, height: Height, lts_pool: u64) { + let mut value_pools = ValueBalance::::zero(); + value_pools.set_lts_amount(Amount::::try_from(lts_pool).unwrap()); + chain + .block_info_by_height + .insert(height, BlockInfo::new(value_pools, 0)); +} + +fn ephemeral_finalized_state(network: &Network) -> FinalizedState { + FinalizedState::new( + &Config::ephemeral(), + network, + #[cfg(feature = "elasticsearch")] + false, + ) +} + +fn block_with_coinbase_output( + network: &Network, + height: Height, + output_value: Amount, +) -> Block { + let coinbase = Arc::new(Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::unlocked(), + expiry_height: height, + inputs: vec![transparent::Input::new_coinbase(height, Vec::new(), None)], + outputs: vec![transparent::Output::new( + output_value, + transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + }); + let transactions = vec![coinbase]; + let merkle_root: merkle::Root = transactions.iter().cloned().collect(); + + Block { + header: Arc::new(Header { + version: 4, + previous_block_hash: Hash([0; 32]), + merkle_root, + commitment_bytes: HexDebug([0; 32]), + time: DateTime::from_timestamp(1_700_000_000, 0) + .expect("hard-coded timestamp is valid"), + difficulty_threshold: network.target_difficulty_limit().to_compact(), + nonce: HexDebug([0; 32]), + solution: Solution::for_proposal(), + }), + transactions, + } +} + +/// The ZIP-234 deferred (lockbox) funding-stream contribution to the chain +/// value pool at `height`. The contextual derivation adds this to the coinbase +/// total output, so the deposit-targeting helper must account for it. +fn deferred_pool_change(network: &Network, height: Height) -> Amount { + let subsidy = block_subsidy(height, network).unwrap(); + funding_stream_values(height, network, subsidy) + .unwrap() + .remove(&FundingStreamReceiver::Deferred) + .unwrap_or_else(Amount::zero) +} + +/// Builds a block whose coinbase has a single transparent output of +/// `coinbase_output_value`, plus — when `fee > 0` — one transparent +/// fee-paying transaction whose `remaining_transaction_value` equals `fee`. +/// Returns the block and the `spent_utxos` map covering the fee tx's input. +fn block_with_coinbase_and_fee( + network: &Network, + height: Height, + coinbase_output_value: Amount, + fee: Amount, +) -> (Block, HashMap) { + let coinbase = Arc::new(Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::unlocked(), + expiry_height: height, + inputs: vec![transparent::Input::new_coinbase(height, Vec::new(), None)], + outputs: vec![transparent::Output::new( + coinbase_output_value, + transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + }); + + let mut transactions = vec![coinbase]; + let mut spent_utxos = HashMap::new(); + + if fee > Amount::::zero() { + // input − output == fee, so the tx's remaining value (the miner fee) is `fee`. + let output_value = Amount::::try_from(1_000).unwrap(); + let input_value = (output_value + fee).unwrap(); + let outpoint = transparent::OutPoint::from_usize(transaction::Hash([7; 32]), 0); + spent_utxos.insert( + outpoint, + transparent::Utxo::new( + transparent::Output::new(input_value, transparent::Script::new(&[])), + Height(1), + false, + ), + ); + transactions.push(Arc::new(Transaction::V5 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::unlocked(), + expiry_height: height, + inputs: vec![transparent::Input::PrevOut { + outpoint, + unlock_script: transparent::Script::new(&[]), + sequence: 0, + }], + outputs: vec![transparent::Output::new( + output_value, + transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + })); + } + + let merkle_root: merkle::Root = transactions.iter().cloned().collect(); + let block = Block { + header: Arc::new(Header { + version: 4, + previous_block_hash: Hash([0; 32]), + merkle_root, + commitment_bytes: HexDebug([0; 32]), + time: DateTime::from_timestamp(1_700_000_000, 0) + .expect("hard-coded timestamp is valid"), + difficulty_threshold: network.target_difficulty_limit().to_compact(), + nonce: HexDebug([0; 32]), + solution: Solution::for_proposal(), + }), + transactions, + }; + + (block, spent_utxos) +} + +/// Coinbase output value that makes the contextual implied claim equal +/// `target_claim` for a block at `height` carrying `fee` in miner fees. +/// +/// The contextual derivation is +/// `implied_claim = output + deferred − subsidy − fee`, so +/// `output = target_claim + subsidy + fee − deferred`. +fn coinbase_output_for_claim( + network: &Network, + height: Height, + fee: Amount, + target_claim: i64, +) -> Amount { + let subsidy = i64::from(block_subsidy(height, network).unwrap()); + let deferred = i64::from(deferred_pool_change(network, height)); + let fee = i64::from(fee); + + Amount::::try_from(target_claim + subsidy + fee - deferred) + .expect("test coinbase output is a valid non-negative amount") +} + +/// Before `lts_disbursement_start`, the expected payout is zero — there is +/// no LTS pool snapshot to consult yet. +#[test] +fn expected_payout_zero_before_disbursement_start() { + let network = regtest_nu7_at_1(); + let chain = empty_chain(&network); + let finalized = ephemeral_finalized_state(&network); + let pre_height = lts_disbursement_start(&network) + .unwrap() + .previous() + .unwrap(); + + assert_eq!( + Amount::::zero(), + expected_lts_payout(&network, &chain, &finalized.db, pre_height) + .expect("pre-disbursement payout does not need BlockInfo"), + ); +} + +/// After NU7 activates but before `lts_disbursement_start`, the expected LTS +/// payout is still zero, so a positive implied claim must be rejected. +#[test] +fn claimed_payout_rejects_positive_claim_before_disbursement_start() { + let network = regtest_nu7_at_1(); + let chain = empty_chain(&network); + let finalized = ephemeral_finalized_state(&network); + let pre_height = lts_disbursement_start(&network) + .unwrap() + .previous() + .unwrap(); + + assert!( + pre_height >= NetworkUpgrade::Nu7.activation_height(&network).unwrap(), + "test height must be inside the pre-disbursement NU7 window" + ); + + let subsidy = block_subsidy(pre_height, &network).unwrap(); + let excess = Amount::::try_from(1u64).unwrap(); + let output_value = (subsidy + excess).unwrap(); + let block = block_with_coinbase_output(&network, pre_height, output_value); + + let err = check_claimed_lts_payout( + &network, + &chain, + &finalized.db, + pre_height, + &block, + &HashMap::new(), + ) + .expect_err("positive pre-disbursement LTS claim must be rejected"); + + let ValidateContextError::InvalidLtsDeposit { + height, + expected_minimum, + actual, + } = err + else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!(pre_height, height); + assert_eq!(Amount::::zero(), expected_minimum); + assert!(actual < Amount::::zero()); +} + +/// Before the disbursement window, a coinbase that under-claims (deposits into +/// the LTS pool) by at least the ZIP-235 minimum is accepted, and the returned +/// pool delta equals the deposited amount. +#[test] +fn claimed_payout_accepts_under_claim_before_disbursement_start() { + let network = regtest_nu7_at_1(); + let chain = empty_chain(&network); + let finalized = ephemeral_finalized_state(&network); + let pre_height = lts_disbursement_start(&network) + .unwrap() + .previous() + .unwrap(); + + // No fees → the minimum deposit is zero; deposit 500 zatoshi (claim = −500). + let deposit = 500i64; + let output = coinbase_output_for_claim(&network, pre_height, Amount::zero(), -deposit); + let (block, spent_utxos) = + block_with_coinbase_and_fee(&network, pre_height, output, Amount::zero()); + + let delta = check_claimed_lts_payout( + &network, + &chain, + &finalized.db, + pre_height, + &block, + &spent_utxos, + ) + .expect("under-claim that meets the zero minimum deposit is valid"); + + // pool delta = −implied_claim = +deposit + assert_eq!(Amount::::try_from(deposit).unwrap(), delta); +} + +/// Inside the disbursement window, claiming more than the scheduled payout +/// (net of the minimum deposit) is rejected as an over-claim. +#[test] +fn claimed_payout_rejects_over_claim_in_disbursement_window() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + let parent_height = start.previous().unwrap(); + + let parent_pool = 10_000_000_000u64; // expected payout = 4126 + let mut chain = empty_chain(&network); + plant_lts_pool(&mut chain, parent_height, parent_pool); + let finalized = ephemeral_finalized_state(&network); + + let expected = i64::from(lts_payout( + start, + &network, + Amount::::try_from(parent_pool).unwrap(), + )); + assert!(expected > 0, "test needs a positive scheduled payout"); + + // Claim one zatoshi more than the scheduled payout (zero fees → zero minimum). + let output = coinbase_output_for_claim(&network, start, Amount::zero(), expected + 1); + let (block, spent_utxos) = block_with_coinbase_and_fee(&network, start, output, Amount::zero()); + + let err = + check_claimed_lts_payout(&network, &chain, &finalized.db, start, &block, &spent_utxos) + .expect_err("over-claiming the scheduled payout must be rejected"); + + let ValidateContextError::InvalidLtsDeposit { + height, + expected_minimum, + actual, + } = err + else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!(start, height); + assert_eq!(Amount::::zero(), expected_minimum); + assert_eq!(Amount::::try_from(-1).unwrap(), actual); +} + +/// With non-zero miner fees, the contextual check enforces the ZIP-235 60% +/// floor: depositing exactly the minimum is accepted; one zatoshi short is +/// rejected. +#[test] +fn claimed_payout_enforces_zip235_minimum_with_fees() { + let network = regtest_nu7_at_1(); + let chain = empty_chain(&network); + let finalized = ephemeral_finalized_state(&network); + let pre_height = lts_disbursement_start(&network) + .unwrap() + .previous() + .unwrap(); + + let fee = Amount::::try_from(1_000).unwrap(); + let minimum = 600i64; // 60% of 1000 + + // Deposit exactly the minimum: claim = −minimum, so deposit == floor → accepted. + let output = coinbase_output_for_claim(&network, pre_height, fee, -minimum); + let (block, spent_utxos) = block_with_coinbase_and_fee(&network, pre_height, output, fee); + + let delta = check_claimed_lts_payout( + &network, + &chain, + &finalized.db, + pre_height, + &block, + &spent_utxos, + ) + .expect("depositing exactly the ZIP-235 minimum is valid"); + assert_eq!(Amount::::try_from(minimum).unwrap(), delta); + + // Deposit one zatoshi short of the minimum → rejected. + let output = coinbase_output_for_claim(&network, pre_height, fee, -(minimum - 1)); + let (block, spent_utxos) = block_with_coinbase_and_fee(&network, pre_height, output, fee); + + let err = check_claimed_lts_payout( + &network, + &chain, + &finalized.db, + pre_height, + &block, + &spent_utxos, + ) + .expect_err("depositing below the ZIP-235 minimum must be rejected"); + + let ValidateContextError::InvalidLtsDeposit { + expected_minimum, + actual, + .. + } = err + else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!( + Amount::::try_from(minimum).unwrap(), + expected_minimum + ); + assert_eq!( + Amount::::try_from(minimum - 1).unwrap(), + actual + ); +} + +/// At `lts_disbursement_start`, the expected payout is +/// `ceil(parent_pool * 4126 / 10_000_000_000)` derived from the parent pool +/// snapshot in the chain. +#[test] +fn expected_payout_matches_lts_payout_at_disbursement_start() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + // Pick a parent pool large enough that the expected payout is positive. + let parent_pool_u = 10_000_000_000u64; // expected payout = 4126 exactly + let parent_height = start.previous().unwrap(); + let mut chain = empty_chain(&network); + plant_lts_pool(&mut chain, parent_height, parent_pool_u); + let finalized = ephemeral_finalized_state(&network); + + let expected_amount = lts_payout( + start, + &network, + Amount::::try_from(parent_pool_u).unwrap(), + ); + assert_eq!( + Amount::::try_from(4_126u64).unwrap(), + expected_amount, + "sanity-check the closed-form payout" + ); + + assert_eq!( + expected_amount, + expected_lts_payout(&network, &chain, &finalized.db, start) + .expect("planted parent BlockInfo is available"), + ); +} + +#[test] +fn expected_payout_errors_when_parent_block_info_is_missing() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + let chain = empty_chain(&network); + let finalized = ephemeral_finalized_state(&network); + let parent_height = start.previous().unwrap(); + + let err = expected_lts_payout(&network, &chain, &finalized.db, start) + .expect_err("missing parent BlockInfo should be a validation error"); + + let ValidateContextError::MissingLtsBlockInfo { height } = err else { + panic!("unexpected error: {err:?}"); + }; + + assert_eq!(parent_height, height); +} + +/// On the tail block where the parent pool is tiny, the ceiling rule pays out +/// at most the parent pool. +#[test] +fn expected_payout_tail_block_capped_to_parent_pool() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + let test_height = (start + 5).unwrap(); + let parent_height = test_height.previous().unwrap(); + + // Parent pool of 7 zatoshi → ceiling rule says payout = 1, capped to 7 + // by the cap-to-parent-pool rule. + let parent_pool_u = 7u64; + let mut chain = empty_chain(&network); + plant_lts_pool(&mut chain, parent_height, parent_pool_u); + let finalized = ephemeral_finalized_state(&network); + + let expected_amount = lts_payout( + test_height, + &network, + Amount::::try_from(parent_pool_u).unwrap(), + ); + assert!( + u64::from(expected_amount) <= parent_pool_u, + "expected payout must be capped by parent pool" + ); + + assert_eq!( + expected_amount, + expected_lts_payout(&network, &chain, &finalized.db, test_height) + .expect("planted parent BlockInfo is available"), + ); +} + +/// On a non-finalized fork whose parent is *not* the current best-chain +/// tip, the expected payout is computed from *that* fork's own pool +/// history — not from a sibling fork or the finalized tip. This is the +/// re-org safety property: the LTS check is per-`Chain`, not per-best-chain. +#[test] +fn expected_payout_uses_parent_chains_own_pool_history() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + let parent_height = start.previous().unwrap(); + + let finalized = ephemeral_finalized_state(&network); + + // Fork A: parent pool = 10_000_000_000 → expected payout = 4126. + let pool_a = 10_000_000_000u64; + let mut chain_a = empty_chain(&network); + plant_lts_pool(&mut chain_a, parent_height, pool_a); + + // Fork B: parent pool = 5_000_000_000 → expected payout = 2063. + let pool_b = 5_000_000_000u64; + let mut chain_b = empty_chain(&network); + plant_lts_pool(&mut chain_b, parent_height, pool_b); + + let expected_a = lts_payout( + start, + &network, + Amount::::try_from(pool_a).unwrap(), + ); + let expected_b = lts_payout( + start, + &network, + Amount::::try_from(pool_b).unwrap(), + ); + assert_ne!(expected_a, expected_b, "test design: forks must differ"); + + assert_eq!( + expected_a, + expected_lts_payout(&network, &chain_a, &finalized.db, start) + .expect("planted fork A parent BlockInfo is available"), + ); + assert_eq!( + expected_b, + expected_lts_payout(&network, &chain_b, &finalized.db, start) + .expect("planted fork B parent BlockInfo is available"), + ); +} + +/// Halving boundaries have no special effect on the LTS payout: with the same +/// parent-pool snapshot on both sides of a halving boundary, the contextual +/// check produces the same expected payout. +#[test] +fn expected_payout_unchanged_across_halving_boundary() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + let height_a = (start + 1).unwrap(); + let height_b = (start + 5_000).unwrap(); + let parent_a = height_a.previous().unwrap(); + let parent_b = height_b.previous().unwrap(); + let pool = 1_000_000_000_000u64; + + let mut chain = empty_chain(&network); + plant_lts_pool(&mut chain, parent_a, pool); + plant_lts_pool(&mut chain, parent_b, pool); + let finalized = ephemeral_finalized_state(&network); + + let expected_a = expected_lts_payout(&network, &chain, &finalized.db, height_a) + .expect("planted parent BlockInfo for height A is available"); + let expected_b = expected_lts_payout(&network, &chain, &finalized.db, height_b) + .expect("planted parent BlockInfo for height B is available"); + assert_eq!( + expected_a, expected_b, + "expected payout is height-independent inside the disbursement window" + ); +} + +/// Block N's expected payout uses the parent pool. Block N+1's parent pool +/// is `parent_pool + contribution - payout_N`, so block N+1's expected +/// payout reflects the inflow from block N. +#[test] +fn expected_payout_reflects_block_n_inflow_at_block_n_plus_1() { + let network = regtest_nu7_at_1(); + let start = lts_disbursement_start(&network).unwrap(); + + let height_n = (start + 1).unwrap(); + let height_n_plus_1 = (start + 2).unwrap(); + let parent_n = height_n.previous().unwrap(); + let parent_n_plus_1 = height_n_plus_1.previous().unwrap(); + + let parent_pool_n = 1_000_000_000_000u64; + let contribution = 500_000_000_000u64; + + let payout_n = lts_payout( + height_n, + &network, + Amount::::try_from(parent_pool_n).unwrap(), + ); + + let parent_pool_n_plus_1 = parent_pool_n + contribution - u64::from(payout_n); + let payout_n_plus_1 = lts_payout( + height_n_plus_1, + &network, + Amount::::try_from(parent_pool_n_plus_1).unwrap(), + ); + assert!( + payout_n_plus_1 > payout_n, + "block N's contribution should grow block N+1's payout" + ); + + let mut chain = empty_chain(&network); + plant_lts_pool(&mut chain, parent_n, parent_pool_n); + plant_lts_pool(&mut chain, parent_n_plus_1, parent_pool_n_plus_1); + let finalized = ephemeral_finalized_state(&network); + + assert_eq!( + payout_n, + expected_lts_payout(&network, &chain, &finalized.db, height_n) + .expect("planted parent BlockInfo for block N is available"), + ); + assert_eq!( + payout_n_plus_1, + expected_lts_payout(&network, &chain, &finalized.db, height_n_plus_1) + .expect("planted parent BlockInfo for block N+1 is available"), + ); +} diff --git a/zebra-state/src/service/finalized_state/disk_format/chain.rs b/zebra-state/src/service/finalized_state/disk_format/chain.rs index 272aabfc7dc..45a17793dd2 100644 --- a/zebra-state/src/service/finalized_state/disk_format/chain.rs +++ b/zebra-state/src/service/finalized_state/disk_format/chain.rs @@ -22,7 +22,10 @@ use zebra_chain::{ use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; impl IntoDisk for ValueBalance { + #[cfg(not(zcash_unstable = "nsm"))] type Bytes = [u8; 40]; + #[cfg(zcash_unstable = "nsm")] + type Bytes = [u8; 48]; fn as_bytes(&self) -> Self::Bytes { self.to_bytes() @@ -110,10 +113,21 @@ impl IntoDisk for BlockInfo { impl FromDisk for BlockInfo { fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - // We want to be forward-compatible, so this must work even if the - // size of the buffer is larger than expected. + // The on-disk record has grown over time: + // 44 bytes — pre-NSM: 40 ValueBalance + 4 size + // 52 bytes — post-NSM: 48 ValueBalance + 4 size + // We accept anything ≥ 52 bytes (forward-compat) for the new layout, + // and 44..52 for the legacy layout. Older layouts default the LTS pool + // to zero on parse (handled inside ValueBalance::from_bytes). match bytes.as_ref().len() { - 44.. => { + 52.. => { + let value_pools = ValueBalance::::from_bytes(&bytes.as_ref()[0..48]) + .expect("must work for 48 bytes"); + let size = + u32::from_le_bytes(bytes.as_ref()[48..52].try_into().expect("must be 4 bytes")); + BlockInfo::new(value_pools, size) + } + 44..52 => { let value_pools = ValueBalance::::from_bytes(&bytes.as_ref()[0..40]) .expect("must work for 40 bytes"); let size = diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs b/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs index 44108cd4f53..5f79ef381c5 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/disk_format/tests/prop.rs @@ -488,3 +488,45 @@ fn roundtrip_value_balance() { proptest!(|(val in any::>())| assert_value_properties(val)); } + +/// Round-trip [`BlockInfo`] through its on-disk format, exercising both the +/// post-NSM 52-byte layout (48-byte ValueBalance + 4-byte size) and the +/// legacy 44-byte layout (40-byte ValueBalance + 4-byte size). The legacy +/// layout is parsed with `lts = 0`, so a hand-built 44-byte record +/// round-trips through `FromDisk + IntoDisk` to the new 52-byte layout — +/// not back to 44 bytes. +#[cfg(zcash_unstable = "nsm")] +#[test] +fn roundtrip_block_info_layouts() { + use zebra_chain::block_info::BlockInfo; + + use crate::service::finalized_state::disk_format::FromDisk; + + let _init_guard = zebra_test::init(); + + // Post-NSM (52-byte) layout: full round-trip via IntoDisk + FromDisk. + let mut value_pools = ValueBalance::::zero(); + value_pools.set_lts_amount(Amount::::try_from(1234).unwrap()); + let info = BlockInfo::new(value_pools, 4096); + let bytes = info.as_bytes(); + assert_eq!(52, bytes.len(), "post-NSM record is 48 + 4 bytes"); + let parsed = BlockInfo::from_bytes(&bytes); + assert_eq!(info, parsed); + + // Legacy 44-byte layout: 40-byte ValueBalance (lts implicitly zero) + + // 4-byte size. Hand-build the bytes and confirm the parse plumbs a + // zero LTS pool. + let mut legacy = [0u8; 44]; + // Stash a recognisable size so we can assert it survives the parse. + legacy[40..44].copy_from_slice(&7777u32.to_le_bytes()); + let parsed_legacy = BlockInfo::from_bytes(&legacy[..]); + assert_eq!(7777, parsed_legacy.size()); + assert_eq!( + Amount::::zero(), + parsed_legacy.value_pools().lts_amount(), + "legacy 44-byte records default the LTS pool to zero" + ); + + // A new write of the legacy-parsed record uses the 52-byte layout. + assert_eq!(52, parsed_legacy.as_bytes().len()); +} diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs b/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs index 76a1f0cba99..48afc162a28 100644 --- a/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs @@ -140,10 +140,19 @@ fn snapshot_raw_rocksdb_column_family_data(db: &DiskDb, original_cf_names: &[Str // distinguish column family names from empty column families empty_column_families.push(format!("{cf_name}: no entries")); } else { + let snapshot_name = format!("{cf_name}_raw_data"); + #[cfg(zcash_unstable = "nsm")] + let snapshot_name = if matches!(cf_name.as_str(), "block_info" | "tip_chain_value_pool") + { + format!("{snapshot_name}_nsm") + } else { + snapshot_name + }; + // The note commitment tree snapshots will change if the trees do not have cached roots. // But we expect them to always have cached roots, // because those roots are used to populate the anchor column families. - insta::assert_ron_snapshot!(format!("{cf_name}_raw_data"), cf_data); + insta::assert_ron_snapshot!(snapshot_name, cf_data); } } diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_0.snap new file mode 100644 index 00000000000..8841b8565e3 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_0.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_1.snap new file mode 100644 index 00000000000..d67bac0b1e7 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_1.snap @@ -0,0 +1,14 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), + KV( + k: "000001", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_2.snap new file mode 100644 index 00000000000..7adf04d44a2 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_2.snap @@ -0,0 +1,18 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), + KV( + k: "000001", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + ), + KV( + k: "000002", + v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_0.snap new file mode 100644 index 00000000000..8841b8565e3 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_0.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_1.snap new file mode 100644 index 00000000000..853b7bec640 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_1.snap @@ -0,0 +1,14 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), + KV( + k: "000001", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_2.snap new file mode 100644 index 00000000000..29cda9499f7 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_2.snap @@ -0,0 +1,18 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + ), + KV( + k: "000001", + v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + ), + KV( + k: "000002", + v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_0.snap new file mode 100644 index 00000000000..265231f491e --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_0.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_1.snap new file mode 100644 index 00000000000..d0457c51a56 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_1.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_2.snap new file mode 100644 index 00000000000..aca4654f3fc --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_2.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_0.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_0.snap new file mode 100644 index 00000000000..265231f491e --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_0.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_1.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_1.snap new file mode 100644 index 00000000000..d0457c51a56 --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_1.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_2.snap b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_2.snap new file mode 100644 index 00000000000..aca4654f3fc --- /dev/null +++ b/zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_2.snap @@ -0,0 +1,10 @@ +--- +source: zebra-state/src/service/finalized_state/disk_format/tests/snapshot.rs +expression: cf_data +--- +[ + KV( + k: "", + v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ), +] diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs index 2134a12df81..e5229baea1f 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs @@ -93,7 +93,13 @@ fn format_upgrades( let min_version = move || min_version.clone().unwrap_or(Version::new(0, 0, 0)); // Note: Disk format upgrades must be run in order of database version. - ([ + // + // The 27.1.0 LTS value-pool entry only exists in `nsm` builds, where the + // running database version is also 27.1.0. In default builds the running + // version stays at 27.0.0, so this entry must be omitted: otherwise an + // upgrade from an older database would reach it and `mark_as_upgraded_to` + // would assert (it forbids marking a version newer than the running one). + let upgrades: Vec> = vec![ Box::new(prune_trees::PruneTrees), Box::new(add_subtrees::AddSubtrees), Box::new(tree_keys_and_caches_upgrade::FixTreeKeyTypeAndCacheGenesisRoots), @@ -102,7 +108,14 @@ fn format_upgrades( Version::new(26, 0, 0), )), Box::new(block_info_and_address_received::Upgrade), - ] as [Box; 5]) + #[cfg(zcash_unstable = "nsm")] + Box::new(no_migration::NoMigration::new( + "add LTS value pool compatibility", + Version::new(27, 1, 0), + )), + ]; + + upgrades .into_iter() .filter(move |upgrade| upgrade.version() > min_version()) } diff --git a/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs b/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs index ad059d7d844..3a01e95f869 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade/block_info_and_address_received.rs @@ -198,6 +198,9 @@ impl DiskFormatUpgrade for Upgrade { }; // Add this block's value pool changes to the total value pool. + // The LTS leg is left at zero — this database upgrade replays + // historical pre-NSM blocks, where neither implicit deposits nor + // any LTS payout is ever non-zero. value_pool = value_pool .add_chain_value_pool_change( block diff --git a/zebra-state/src/service/finalized_state/tests/prop.rs b/zebra-state/src/service/finalized_state/tests/prop.rs index 322185a6446..189ecf57829 100644 --- a/zebra-state/src/service/finalized_state/tests/prop.rs +++ b/zebra-state/src/service/finalized_state/tests/prop.rs @@ -73,7 +73,12 @@ fn all_upgrades_and_wrong_commitments_with_fake_activation_heights() -> Result<( nu5: Some(35), nu6: Some(40), nu6_1: Some(45), + // NSM adds contextual LTS payout validation. The arbitrary fake + // blocks in this commitment-focused test do not fund that pool. + #[cfg(all(zcash_unstable = "nu7", not(zcash_unstable = "nsm")))] nu7: Some(50), + #[cfg(any(not(zcash_unstable = "nu7"), zcash_unstable = "nsm"))] + nu7: None, }) .expect("failed to set activation heights") .extend_funding_streams() diff --git a/zebra-state/src/service/finalized_state/zebra_db/chain.rs b/zebra-state/src/service/finalized_state/zebra_db/chain.rs index 585f4b9edac..7d6198d7da3 100644 --- a/zebra-state/src/service/finalized_state/zebra_db/chain.rs +++ b/zebra-state/src/service/finalized_state/zebra_db/chain.rs @@ -253,7 +253,8 @@ impl DiskWriteBatch { utxos_spent_by_block: HashMap, value_pool: ValueBalance, ) -> Result<(), ValidateContextError> { - let block_value_pool_change = finalized + #[cfg_attr(not(zcash_unstable = "nsm"), allow(unused_mut))] + let mut block_value_pool_change = finalized .block .chain_value_pool_change( &utxos_spent_by_block, @@ -269,6 +270,30 @@ impl DiskWriteBatch { } })?; + // Set the LTS pool delta (the signed coinbase under-/over-claim). The + // block has been contextually validated by the time it reaches the + // finalized commit path, so we re-derive the leg from the block bytes + // here rather than threading it through. + #[cfg(zcash_unstable = "nsm")] + { + let lts_delta = crate::service::check::lts::block_lts_pool_delta( + &finalized.block, + &db.network(), + finalized.height, + &utxos_spent_by_block, + ) + .map_err(|value_balance_error| { + ValidateContextError::CalculateBlockChainValueChange { + value_balance_error, + height: finalized.height, + block_hash: finalized.hash, + transaction_count: finalized.transaction_hashes.len(), + spent_utxo_count: utxos_spent_by_block.len(), + } + })?; + block_value_pool_change.set_lts_amount(lts_delta); + } + let new_value_pool = value_pool .add_chain_value_pool_change(block_value_pool_change) .map_err(|value_balance_error| ValidateContextError::AddValuePool { diff --git a/zebra-state/src/service/non_finalized_state.rs b/zebra-state/src/service/non_finalized_state.rs index 485acecbcfe..0dc7693ee29 100644 --- a/zebra-state/src/service/non_finalized_state.rs +++ b/zebra-state/src/service/non_finalized_state.rs @@ -588,8 +588,24 @@ impl NonFinalizedState { &prepared, ); + // ZIP-234/235 Long-Term Support pool check. Re-derives the miner's + // signed implied claim from the block bytes (mirroring the semantic + // verifier's value-balance equation), validates it against the chain's + // LTS pool history and the ZIP-235 deposit floor, and returns the pool + // delta (the signed under-/over-claim) to apply below. + #[cfg(zcash_unstable = "nsm")] + let lts_delta = check::lts::check_claimed_lts_payout( + &self.network, + &new_chain, + finalized_state, + prepared.height, + &prepared.block, + &transparent::utxos_from_ordered_utxos(spent_utxos.clone()), + )?; + // Quick check that doesn't read from disk - let contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( + #[cfg_attr(not(zcash_unstable = "nsm"), allow(unused_mut))] + let mut contextual = ContextuallyVerifiedBlock::with_block_and_spent_utxos( prepared.clone(), spent_utxos.clone(), ) @@ -603,6 +619,10 @@ impl NonFinalizedState { } })?; + // Apply the LTS pool delta confirmed by `check_claimed_lts_payout`. + #[cfg(zcash_unstable = "nsm")] + contextual.chain_value_pool_change.set_lts_amount(lts_delta); + Self::validate_and_update_parallel(new_chain, contextual, sprout_final_treestates) } diff --git a/zebra-state/src/service/non_finalized_state/backup.rs b/zebra-state/src/service/non_finalized_state/backup.rs index cdb284e7862..d73027184b2 100644 --- a/zebra-state/src/service/non_finalized_state/backup.rs +++ b/zebra-state/src/service/non_finalized_state/backup.rs @@ -162,6 +162,8 @@ impl From<&ContextuallyVerifiedBlock> for NonFinalizedBlockBackup { impl NonFinalizedBlockBackup { /// Encodes a [`NonFinalizedBlockBackup`] as a vector of bytes. + /// + /// Layout: `[deferred_pool_balance_change(8) | block_bytes...]`. fn as_bytes(&self) -> Vec { let block_bytes = self .block @@ -177,9 +179,9 @@ impl NonFinalizedBlockBackup { /// Constructs a new [`NonFinalizedBlockBackup`] from a vector of bytes. #[allow(clippy::unwrap_in_result)] fn from_bytes(bytes: Vec) -> Result { - let (deferred_pool_balance_change_bytes, block_bytes) = bytes - .split_at_checked(size_of::()) - .ok_or(io::Error::new( + let prefix_len = size_of::(); + let (deferred_pool_balance_change_bytes, block_bytes) = + bytes.split_at_checked(prefix_len).ok_or(io::Error::new( ErrorKind::InvalidInput, "input is too short", ))?; @@ -345,3 +347,49 @@ fn process_backup_dir_entry(entry: DirEntry) -> Option<(block::Hash, PathBuf)> { Some((block_hash, entry.path())) } + +#[cfg(test)] +mod tests { + use super::*; + + use zebra_test::vectors::MAINNET_BLOCKS; + + fn sample_block() -> Arc { + let bytes = MAINNET_BLOCKS + .iter() + .next() + .expect("at least one mainnet test block") + .1; + Arc::new(bytes.zcash_deserialize_into().expect("valid block")) + } + + fn assert_round_trip(deferred: i64) { + let original = NonFinalizedBlockBackup { + block: sample_block(), + deferred_pool_balance_change: Amount::try_from(deferred) + .expect("test deferred amount in range"), + }; + let bytes = original.as_bytes(); + let decoded = + NonFinalizedBlockBackup::from_bytes(bytes).expect("round-trip parse succeeds"); + assert_eq!(original, decoded); + } + + #[test] + fn backup_round_trips_with_zero_deferred() { + assert_round_trip(0); + } + + #[test] + fn backup_round_trips_with_non_zero_deferred() { + assert_round_trip(12_345); + } + + #[test] + fn backup_from_bytes_rejects_short_prefix() { + let too_short = vec![0u8; size_of::() - 1]; + let err = NonFinalizedBlockBackup::from_bytes(too_short) + .expect_err("input shorter than the prefix must fail"); + assert_eq!(err.kind(), ErrorKind::InvalidInput); + } +} diff --git a/zebra-state/src/service/read/difficulty.rs b/zebra-state/src/service/read/difficulty.rs index eb308508f21..b32277849db 100644 --- a/zebra-state/src/service/read/difficulty.rs +++ b/zebra-state/src/service/read/difficulty.rs @@ -368,21 +368,20 @@ fn adjust_difficulty_and_time_for_testnet( } } -#[cfg(test)] +#[cfg(all(test, zcash_unstable = "nu7"))] mod tests { use super::*; use zebra_chain::{ - block, - parameters::{testnet, ParameterDifficulty}, - serialization::Duration32, + block, parameters::testnet, serialization::Duration32, + work::difficulty::ParameterDifficulty, }; #[test] fn get_block_template_testnet_min_difficulty_uses_candidate_height_spacing() { let previous_block_height = Height(2_999_999); let candidate_block_height = (previous_block_height + 1).unwrap(); - let previous_block_time = DateTime32::from_seconds(1_000); + let previous_block_time = DateTime32::from(1_000u32); let network = testnet::Parameters::build() .with_activation_heights(testnet::ConfiguredActivationHeights { diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index 2fea468cc31..eb2da973c76 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -3606,6 +3606,8 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { vec![], #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, + #[cfg(zcash_unstable = "nsm")] + zebra_chain::amount::Amount::zero(), ) .expect("coinbase transaction should be valid under the given parameters"); @@ -3667,6 +3669,8 @@ async fn nu6_funding_streams_and_coinbase_balance() -> Result<()> { vec![], #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] None, + #[cfg(zcash_unstable = "nsm")] + zebra_chain::amount::Amount::zero(), ) .expect("coinbase transaction should be valid under the given parameters");