diff --git a/crates/zakura-chain/src/parameters/network.rs b/crates/zakura-chain/src/parameters/network.rs index 38bfb1ca4..12a251d94 100644 --- a/crates/zakura-chain/src/parameters/network.rs +++ b/crates/zakura-chain/src/parameters/network.rs @@ -342,6 +342,27 @@ impl Network { .collect() } + /// Returns the first height at which version 4 transactions are invalid. + /// + /// [ZIP 2003] deprecates version 4 transactions at NU7. The + /// `nu7-experimental` feature must be enabled. A network that does not + /// activate NU7 keeps accepting them. + /// + /// [ZIP 2003]: https://zips.z.cash/zip-2003 + pub fn v4_deprecation_height(&self) -> Option { + if cfg!(feature = "nu7-experimental") { + NetworkUpgrade::Nu7.activation_height(self) + } else { + None + } + } + + /// Returns whether version 4 transactions are invalid at `height` on this network. + pub fn is_v4_deprecated(&self, height: Height) -> bool { + self.v4_deprecation_height() + .is_some_and(|deprecation_height| height >= deprecation_height) + } + /// Returns the height at which the soft fork that temporarily disables Orchard /// actions in transactions activates, if it is configured for this network. pub fn temporary_orchard_disabling_soft_fork_height(&self) -> Option { diff --git a/crates/zakura-consensus/src/transaction.rs b/crates/zakura-consensus/src/transaction.rs index 460599930..25aa1aaf8 100644 --- a/crates/zakura-consensus/src/transaction.rs +++ b/crates/zakura-consensus/src/transaction.rs @@ -993,7 +993,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(); @@ -1010,11 +1010,28 @@ 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 + // + // ZIP 2003 deprecates V4 transactions at NU7. + if network.is_v4_deprecated(height) { + return Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )); + } + match network_upgrade { // Supports V4 transactions // @@ -1039,7 +1056,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(()), @@ -1047,8 +1065,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..7158680dc 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,93 @@ async fn v5_with_duplicate_orchard_action() { } } +/// Checks that ZIP 2003 accepts V4 transactions below NU7 and rejects them from NU7. +#[test] +fn v4_deprecation_boundary() { + let _init_guard = zakura_test::init(); + + let nu7 = Height(2_000_000); + let tx = test_transactions(&Network::Mainnet) + .map(|(_, tx)| tx) + .find(|tx| matches!(**tx, Transaction::V4 { .. })) + .expect("V4 tx"); + + let 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: Some(nu7.0), + }; + + let at_nu7 = Parameters::build() + .with_activation_heights(activation_heights) + .expect("activation heights are valid") + .clear_funding_streams() + .to_network() + .expect("failed to build configured network"); + + assert_eq!( + at_nu7.v4_deprecation_height(), + cfg!(feature = "nu7-experimental").then_some(nu7) + ); + assert!( + verify_v4_at(&at_nu7, &tx, nu7.previous().expect("height")).is_ok(), + "a V4 transaction must be valid below the deprecation height", + ); + if cfg!(feature = "nu7-experimental") { + assert_eq!( + verify_v4_at(&at_nu7, &tx, nu7), + Err(TransactionError::UnsupportedByNetworkUpgrade( + 4, + NetworkUpgrade::Nu7 + )), + "a V4 transaction must be invalid at the deprecation height", + ); + } else { + assert!( + verify_v4_at(&at_nu7, &tx, nu7).is_ok(), + "a default build must keep accepting V4 transactions", + ); + } + + // A network without NU7 keeps accepting V4 transactions. + let no_nu7 = Parameters::build() + .to_network() + .expect("failed to build configured network"); + + assert_eq!(no_nu7.v4_deprecation_height(), None); + assert!(!no_nu7.is_v4_deprecated(Height::MAX)); +} + +/// A [`Verifier`] with concrete service types, so a test can name its associated +/// functions without the compiler inferring the services from a call. +type TestVerifier = Verifier< + MockService, + MockService, +>; + +/// Runs the V4 network upgrade check for `tx` at `height` on `network`. +fn verify_v4_at( + network: &Network, + tx: &Transaction, + height: Height, +) -> Result<(), TransactionError> { + TestVerifier::verify_v4_transaction_network_upgrade( + tx, + 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. diff --git a/docs/changelog/unreleased/856.md b/docs/changelog/unreleased/856.md new file mode 100644 index 000000000..60c5440a1 --- /dev/null +++ b/docs/changelog/unreleased/856.md @@ -0,0 +1,7 @@ +## Changed + +- ZIP 2003 "Disallow version 4 transactions": The off-by-default + `nu7-experimental` feature makes Zakura reject version 4 transactions from + NU7 activation. This deprecates new Sprout transactions without exposing a + separate consensus configuration + ([#856](https://github.com/zakura-core/zakura/pull/856)).