From b1cf2af62f20569d0a6f4c81a781e0113206988d Mon Sep 17 00:00:00 2001 From: evan-forbes Date: Fri, 22 May 2026 14:43:51 -0500 Subject: [PATCH 1/2] feat(consensus): gate NSM LTS support --- Cargo.lock | 3 + Cargo.toml | 2 +- zebra-chain/src/block.rs | 23 +- zebra-chain/src/lib.rs | 8 + zebra-chain/src/parameters/network.rs | 8 + zebra-chain/src/parameters/network/error.rs | 4 + zebra-chain/src/parameters/network/subsidy.rs | 86 +++ zebra-chain/src/parameters/network/testnet.rs | 51 ++ zebra-chain/src/parameters/network/tests.rs | 2 + .../src/parameters/network/tests/lts.rs | 296 ++++++++ zebra-chain/src/transaction/builder.rs | 12 +- zebra-chain/src/value_balance.rs | 108 ++- zebra-chain/src/value_balance/arbitrary.rs | 46 +- zebra-chain/src/value_balance/tests/prop.rs | 74 +- zebra-consensus/Cargo.toml | 3 + zebra-consensus/src/block/check.rs | 59 +- zebra-consensus/src/block/tests.rs | 215 +----- zebra-consensus/src/checkpoint.rs | 20 + zebra-consensus/src/checkpoint/tests.rs | 94 +++ zebra-consensus/src/transaction.rs | 63 +- zebra-consensus/src/transaction/check.rs | 4 - zebra-consensus/src/transaction/tests.rs | 257 +++++++ zebra-network/src/config.rs | 23 +- zebra-network/src/config/tests/vectors.rs | 19 + zebra-rpc/src/methods.rs | 22 +- zebra-rpc/src/methods/tests/snapshot.rs | 16 +- ...rbose_hash_verbosity_1_nsm@mainnet_10.snap | 81 +++ ...rbose_hash_verbosity_1_nsm@testnet_10.snap | 81 +++ ...rbose_hash_verbosity_2_nsm@mainnet_10.snap | 136 ++++ ...rbose_hash_verbosity_2_nsm@testnet_10.snap | 136 ++++ ...hash_verbosity_default_nsm@mainnet_10.snap | 81 +++ ...hash_verbosity_default_nsm@testnet_10.snap | 81 +++ ...ose_height_verbosity_1_nsm@mainnet_10.snap | 81 +++ ...ose_height_verbosity_1_nsm@testnet_10.snap | 81 +++ ...ose_height_verbosity_2_nsm@mainnet_10.snap | 136 ++++ ...ose_height_verbosity_2_nsm@testnet_10.snap | 136 ++++ ...ight_verbosity_default_nsm@mainnet_10.snap | 81 +++ ...ight_verbosity_default_nsm@testnet_10.snap | 81 +++ ...o_future_nu6_height_nsm@nu6testnet_10.snap | 101 +++ .../get_blockchain_info_nsm@mainnet_10.snap | 106 +++ .../get_blockchain_info_nsm@testnet_10.snap | 106 +++ .../src/methods/types/get_block_template.rs | 137 +++- .../methods/types/get_block_template/tests.rs | 168 ++++- .../types/get_block_template/zip317.rs | 46 +- .../types/get_block_template/zip317/tests.rs | 14 +- .../src/methods/types/get_blockchain_info.rs | 15 +- zebra-state/src/constants.rs | 3 + zebra-state/src/error.rs | 19 + zebra-state/src/request.rs | 5 +- zebra-state/src/service/check.rs | 2 + zebra-state/src/service/check/difficulty.rs | 2 +- zebra-state/src/service/check/lts.rs | 281 ++++++++ zebra-state/src/service/check/tests.rs | 2 + zebra-state/src/service/check/tests/lts.rs | 654 ++++++++++++++++++ .../finalized_state/disk_format/chain.rs | 20 +- .../finalized_state/disk_format/tests/prop.rs | 42 ++ .../disk_format/tests/snapshot.rs | 11 +- .../block_info_raw_data_nsm@mainnet_0.snap | 10 + .../block_info_raw_data_nsm@mainnet_1.snap | 14 + .../block_info_raw_data_nsm@mainnet_2.snap | 18 + .../block_info_raw_data_nsm@testnet_0.snap | 10 + .../block_info_raw_data_nsm@testnet_1.snap | 14 + .../block_info_raw_data_nsm@testnet_2.snap | 18 + ...ain_value_pool_raw_data_nsm@mainnet_0.snap | 10 + ...ain_value_pool_raw_data_nsm@mainnet_1.snap | 10 + ...ain_value_pool_raw_data_nsm@mainnet_2.snap | 10 + ...ain_value_pool_raw_data_nsm@testnet_0.snap | 10 + ...ain_value_pool_raw_data_nsm@testnet_1.snap | 10 + ...ain_value_pool_raw_data_nsm@testnet_2.snap | 10 + .../finalized_state/disk_format/upgrade.rs | 6 +- .../block_info_and_address_received.rs | 3 + .../src/service/finalized_state/tests/prop.rs | 5 + .../service/finalized_state/zebra_db/chain.rs | 27 +- .../src/service/non_finalized_state.rs | 22 +- .../src/service/non_finalized_state/backup.rs | 54 +- zebra-state/src/service/read/difficulty.rs | 9 +- zebrad/tests/acceptance.rs | 4 + 77 files changed, 4306 insertions(+), 412 deletions(-) create mode 100644 zebra-chain/src/parameters/network/tests/lts.rs create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default_nsm@testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height_nsm@nu6testnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@mainnet_10.snap create mode 100644 zebra-rpc/src/methods/tests/snapshots/get_blockchain_info_nsm@testnet_10.snap create mode 100644 zebra-state/src/service/check/lts.rs create mode 100644 zebra-state/src/service/check/tests/lts.rs create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_0.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_1.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@mainnet_2.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_0.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_1.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data_nsm@testnet_2.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_0.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_1.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@mainnet_2.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_0.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_1.snap create mode 100644 zebra-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data_nsm@testnet_2.snap 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..38390bcfc38 100644 --- a/zebra-state/src/service/finalized_state/disk_format/upgrade.rs +++ b/zebra-state/src/service/finalized_state/disk_format/upgrade.rs @@ -102,7 +102,11 @@ fn format_upgrades( Version::new(26, 0, 0), )), Box::new(block_info_and_address_received::Upgrade), - ] as [Box; 5]) + Box::new(no_migration::NoMigration::new( + "add LTS value pool compatibility", + Version::new(27, 1, 0), + )), + ] as [Box; 6]) .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"); From 33cc897cf587a308e1ec567a142c582fb1a07629 Mon Sep 17 00:00:00 2001 From: evan-forbes Date: Fri, 22 May 2026 18:11:19 -0500 Subject: [PATCH 2/2] fix(state): gate 27.1.0 LTS migration entry behind nsm cfg The `format_upgrades` list unconditionally included a 27.1.0 NoMigration entry for the LTS value-pool format. The migration apply loop in `apply_format_upgrade` iterates over every upgrade whose version is greater than the on-disk version, with no upper bound at the running version, and calls `mark_as_upgraded_to` for each. In a default build (nsm off) the running database version is 27.0.0. When such a build upgrades any older database it would reach the 27.1.0 entry and `mark_as_upgraded_to` would trip its `format_upgrade_version <= running_version` assertion, panicking the format-change thread. The reference branch never hit this because it is an always-nsm build whose running version is 27.1.0. Gate the 27.1.0 entry behind `cfg(zcash_unstable = "nsm")` so default builds never see an upgrade version newer than their own running version. This keeps the entry only in nsm builds, where the running version is also 27.1.0 and the assertion holds. Switched the array to a Vec so the entry can be cfg-gated in place. --- .../service/finalized_state/disk_format/upgrade.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 38390bcfc38..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,11 +108,14 @@ fn format_upgrades( Version::new(26, 0, 0), )), Box::new(block_info_and_address_received::Upgrade), + #[cfg(zcash_unstable = "nsm")] Box::new(no_migration::NoMigration::new( "add LTS value pool compatibility", Version::new(27, 1, 0), )), - ] as [Box; 6]) + ]; + + upgrades .into_iter() .filter(move |upgrade| upgrade.version() > min_version()) }