diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index c165b7e0..2f677c47 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -266,6 +266,21 @@ impl Transaction { .contains(orchard::Flags::ENABLE_OUTPUTS)) } + /// Does this transaction has at least one flag when we have at least one orchard action? + /// + /// [NU5 onward] If effectiveVersion >= 5 and nActionsOrchard > 0, then at least one + /// of enableSpendsOrchard and enableOutputsOrchard MUST be 1. + /// + /// https://zips.z.cash/protocol/protocol.pdf#txnconsensus + pub fn has_enough_orchard_flags(&self) -> bool { + if self.version() < 5 || self.orchard_actions().count() == 0 { + return true; + } + self.orchard_flags() + .unwrap_or_else(orchard::Flags::empty) + .intersects(orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS) + } + /// Returns the [`CoinbaseSpendRestriction`] for this transaction, /// assuming it is mined at `spend_height`. pub fn coinbase_spend_restriction( diff --git a/zebra-consensus/src/error.rs b/zebra-consensus/src/error.rs index 6432902b..3759a648 100644 --- a/zebra-consensus/src/error.rs +++ b/zebra-consensus/src/error.rs @@ -116,6 +116,9 @@ pub enum TransactionError { #[error("orchard double-spend: duplicate nullifier: {_0:?}")] DuplicateOrchardNullifier(orchard::Nullifier), + + #[error("must have at least one active orchard flag")] + NotEnoughFlags, } impl From for TransactionError { diff --git a/zebra-consensus/src/transaction.rs b/zebra-consensus/src/transaction.rs index 201b8081..37666934 100644 --- a/zebra-consensus/src/transaction.rs +++ b/zebra-consensus/src/transaction.rs @@ -265,6 +265,7 @@ where // Do basic checks first check::has_inputs_and_outputs(&tx)?; + check::has_enough_orchard_flags(&tx)?; if req.is_mempool() && tx.has_any_coinbase_inputs() { return Err(TransactionError::CoinbaseInMempool); diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 3345ea03..ed5a6719 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -40,6 +40,20 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> } } +/// Checks that the transaction has enough orchard flags. +/// +/// For `Transaction::V5` only: +/// * If `orchard_actions_count` > 0 then at least one of +/// `ENABLE_SPENDS|ENABLE_OUTPUTS` must be active. +/// +/// https://zips.z.cash/protocol/protocol.pdf#txnencodingandconsensus +pub fn has_enough_orchard_flags(tx: &Transaction) -> Result<(), TransactionError> { + if !tx.has_enough_orchard_flags() { + return Err(TransactionError::NotEnoughFlags); + } + Ok(()) +} + /// Check that a coinbase transaction has no PrevOut inputs, JoinSplits, or spends. /// /// A coinbase transaction MUST NOT have any transparent inputs, JoinSplit descriptions, diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index dfe7f859..2249540c 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -114,6 +114,49 @@ fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() { assert!(check::has_inputs_and_outputs(&transaction).is_ok()); } +#[test] +fn fake_v5_transaction_with_orchard_actions_has_flags() { + // Find a transaction with no inputs or outputs to use as base + let mut transaction = fake_v5_transactions_for_network( + Network::Mainnet, + zebra_test::vectors::MAINNET_BLOCKS.iter(), + ) + .rev() + .find(|transaction| { + transaction.inputs().is_empty() + && transaction.outputs().is_empty() + && transaction.sapling_spends_per_anchor().next().is_none() + && transaction.sapling_outputs().next().is_none() + && transaction.joinsplit_count() == 0 + }) + .expect("At least one fake V5 transaction with no inputs and no outputs"); + + // Insert fake Orchard shielded data to the transaction, which has at least one action (this is + // guaranteed structurally by `orchard::ShieldedData`) + insert_fake_orchard_shielded_data(&mut transaction); + + // The check will fail if the transaction has no flags + assert_eq!( + check::has_enough_orchard_flags(&transaction), + Err(TransactionError::NotEnoughFlags) + ); + + // If we add ENABLE_SPENDS flag it will pass. + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_SPENDS; + assert!(check::has_enough_orchard_flags(&transaction).is_ok()); + + // If we add ENABLE_OUTPUTS flag instead, it will pass. + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_OUTPUTS; + assert!(check::has_enough_orchard_flags(&transaction).is_ok()); + + // If we add BOTH ENABLE_SPENDS and ENABLE_OUTPUTS flags it will pass. + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS; + assert!(check::has_enough_orchard_flags(&transaction).is_ok()); +} + #[test] fn v5_transaction_with_no_inputs_fails_validation() { let transaction = fake_v5_transactions_for_network(