From bbd7826b016804e9ffc6a0ccf33bb88fb4617a26 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 10 Sep 2026 15:36:04 +0200 Subject: [PATCH 1/4] feat(consensus): disallow version 4 transactions at NU7 --- crates/zakura-consensus/src/transaction.rs | 32 +++++- .../zakura-consensus/src/transaction/tests.rs | 97 ++++++++++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/crates/zakura-consensus/src/transaction.rs b/crates/zakura-consensus/src/transaction.rs index 0866ec082..4448226ba 100644 --- a/crates/zakura-consensus/src/transaction.rs +++ b/crates/zakura-consensus/src/transaction.rs @@ -985,7 +985,7 @@ where let tx = request.transaction(); let nu = request.upgrade(network); - Self::verify_v4_transaction_network_upgrade(&tx, nu)?; + Self::verify_v4_transaction_network_upgrade(&tx, network, request.height(), nu)?; let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle(); @@ -1002,11 +1002,33 @@ where .and(Self::verify_sapling_bundle(sapling_bundle, &sighash, tx_id))) } - /// Verifies if a V4 `transaction` is supported by `network_upgrade`. + /// Verifies if a V4 `transaction` is supported by `network_upgrade` at + /// `height` on `network`. fn verify_v4_transaction_network_upgrade( transaction: &Transaction, + network: &Network, + height: block::Height, network_upgrade: NetworkUpgrade, ) -> Result<(), TransactionError> { + // # Consensus + // + // > [NU7 onward] The transaction version number MUST be 5 or 6. + // + // https://zips.z.cash/zip-2003 + // + // Use NU7's exact activation height: a later configured upgrade must + // not substitute its height when this network omits NU7. + if cfg!(feature = "nu7-experimental") + && NetworkUpgrade::Nu7 + .activation_height(network) + .is_some_and(|nu7_height| height >= nu7_height) + { + return Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )); + } + match network_upgrade { // Supports V4 transactions // @@ -1031,7 +1053,8 @@ where | NetworkUpgrade::Nu6 | NetworkUpgrade::Nu6_1 | NetworkUpgrade::Nu6_2 - | NetworkUpgrade::Nu6_3 => Ok(()), + | NetworkUpgrade::Nu6_3 + | NetworkUpgrade::Nu7 => Ok(()), #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => Ok(()), @@ -1039,8 +1062,7 @@ where // Does not support V4 transactions NetworkUpgrade::Genesis | NetworkUpgrade::BeforeOverwinter - | NetworkUpgrade::Overwinter - | NetworkUpgrade::Nu7 => Err(TransactionError::UnsupportedByNetworkUpgrade( + | NetworkUpgrade::Overwinter => Err(TransactionError::UnsupportedByNetworkUpgrade( transaction.version(), network_upgrade, )), diff --git a/crates/zakura-consensus/src/transaction/tests.rs b/crates/zakura-consensus/src/transaction/tests.rs index d73e21e36..66bde6674 100644 --- a/crates/zakura-consensus/src/transaction/tests.rs +++ b/crates/zakura-consensus/src/transaction/tests.rs @@ -46,7 +46,7 @@ use zakura_chain::{ironwood, orchard}; use zakura_node_services::mempool; use zakura_state::ValidateContextError; -use zakura_test::mock_service::MockService; +use zakura_test::mock_service::{MockService, PanicAssertion}; use crate::{error::TransactionError, primitives, transaction::POLL_MEMPOOL_DELAY, BoxError}; @@ -4452,6 +4452,101 @@ async fn v5_with_duplicate_orchard_action() { } } +/// Checks that ZIP 2003 accepts V4 transactions below NU7 and rejects them +/// from NU7 when the experimental rules are enabled. +#[test] +fn v4_deprecation_boundary() { + let _init_guard = zakura_test::init(); + + let nu7 = Height(2_000_000); + let transaction = test_transactions(&Network::Mainnet) + .map(|(_, transaction)| transaction) + .find(|transaction| matches!(**transaction, Transaction::V4 { .. })) + .expect("the test vectors contain a V4 transaction"); + let network = configured_network_with_nu7(Some(nu7)); + + assert!( + verify_v4_at( + &network, + &transaction, + nu7.previous().expect("NU7 is above the minimum height"), + ) + .is_ok(), + "a V4 transaction must be valid below the NU7 activation height", + ); + + let expected = if cfg!(feature = "nu7-experimental") { + Err(TransactionError::UnsupportedByNetworkUpgrade( + 4, + NetworkUpgrade::Nu7, + )) + } else { + Ok(()) + }; + assert_eq!( + verify_v4_at(&network, &transaction, nu7), + expected, + "V4 deprecation must match the experimental build at NU7", + ); + assert_eq!( + verify_v4_at(&network, &transaction, Height(nu7.0 + 1)), + expected, + "V4 deprecation must match the experimental build after NU7", + ); + + let no_nu7 = configured_network_with_nu7(None); + assert!( + verify_v4_at(&no_nu7, &transaction, Height::MAX).is_ok(), + "a network without an exact NU7 activation must keep accepting V4", + ); +} + +/// Returns a configured network whose latest upgrade is NU6.3 unless `nu7` +/// supplies an exact NU7 activation height. +fn configured_network_with_nu7(nu7: Option) -> Network { + Parameters::build() + .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), + nu6_2: Some(10), + nu6_3: Some(11), + nu7: nu7.map(|height| height.0), + ..Default::default() + }) + .expect("activation heights are ordered") + .clear_funding_streams() + .to_network() + .expect("the configured network parameters are valid") +} + +/// A [`Verifier`] with concrete service types for calling its associated +/// network-upgrade check in tests. +type TestVerifier = Verifier< + MockService, + MockService, +>; + +/// Runs the V4 network-upgrade check at `height` on `network`. +fn verify_v4_at( + network: &Network, + transaction: &Transaction, + height: Height, +) -> Result<(), TransactionError> { + TestVerifier::verify_v4_transaction_network_upgrade( + transaction, + network, + height, + NetworkUpgrade::current(network, height), + ) +} + /// Checks the activation boundary of the temporary Orchard-disabling soft fork: /// it is inactive below the configured height and active at and above it, can be /// disabled entirely, and Mainnet uses its fixed activation height. From 599db0c3af61088ee9c9ce70a2a1a051047f3cd5 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 10 Sep 2026 15:36:57 +0200 Subject: [PATCH 2/4] docs(changelog): add the ZIP 2003 fragment --- docs/changelog/unreleased/959.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/unreleased/959.md diff --git a/docs/changelog/unreleased/959.md b/docs/changelog/unreleased/959.md new file mode 100644 index 000000000..42ecfcc88 --- /dev/null +++ b/docs/changelog/unreleased/959.md @@ -0,0 +1,5 @@ +## Changed + +- The experimental NU7 rules now reject version 4 transactions from the exact + NU7 activation height, implementing ZIP 2003 without changing default-build + behavior ([#959](https://github.com/zakura-core/zakura/pull/959)). From 40e5b41956a7cc1f1847483682c0ac8dfd8c6306 Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 10 Sep 2026 15:40:04 +0200 Subject: [PATCH 3/4] test(consensus): keep the NU7 fixture cfg-complete --- crates/zakura-consensus/src/transaction/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zakura-consensus/src/transaction/tests.rs b/crates/zakura-consensus/src/transaction/tests.rs index 66bde6674..75015ce2e 100644 --- a/crates/zakura-consensus/src/transaction/tests.rs +++ b/crates/zakura-consensus/src/transaction/tests.rs @@ -4518,7 +4518,8 @@ fn configured_network_with_nu7(nu7: Option) -> Network { nu6_2: Some(10), nu6_3: Some(11), nu7: nu7.map(|height| height.0), - ..Default::default() + #[cfg(zcash_unstable = "zfuture")] + zfuture: None, }) .expect("activation heights are ordered") .clear_funding_streams() From f4cc8a1f55c3adc38b4a34d5fc473fb09f2452ef Mon Sep 17 00:00:00 2001 From: Dev Ojha Date: Thu, 10 Sep 2026 15:41:10 +0200 Subject: [PATCH 4/4] test(consensus): derive ZIP 2003 boundary values --- crates/zakura-consensus/src/transaction/tests.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/zakura-consensus/src/transaction/tests.rs b/crates/zakura-consensus/src/transaction/tests.rs index 75015ce2e..86c5c8b51 100644 --- a/crates/zakura-consensus/src/transaction/tests.rs +++ b/crates/zakura-consensus/src/transaction/tests.rs @@ -4477,7 +4477,7 @@ fn v4_deprecation_boundary() { let expected = if cfg!(feature = "nu7-experimental") { Err(TransactionError::UnsupportedByNetworkUpgrade( - 4, + transaction.version(), NetworkUpgrade::Nu7, )) } else { @@ -4489,7 +4489,11 @@ fn v4_deprecation_boundary() { "V4 deprecation must match the experimental build at NU7", ); assert_eq!( - verify_v4_at(&network, &transaction, Height(nu7.0 + 1)), + verify_v4_at( + &network, + &transaction, + nu7.next().expect("NU7 is below the maximum height"), + ), expected, "V4 deprecation must match the experimental build after NU7", );