diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index d4b08663..b86b990e 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -483,4 +483,11 @@ impl Transaction { .map(orchard::ShieldedData::nullifiers) .flatten() } + + /// Access the [`orchard::Flags`] in this transaction, if there is any, + /// regardless of version. + pub fn orchard_flags(&self) -> Option { + self.orchard_shielded_data() + .map(|orchard_shielded_data| orchard_shielded_data.flags) + } } diff --git a/zebra-consensus/src/transaction/check.rs b/zebra-consensus/src/transaction/check.rs index 4ee98a07..dcf8a575 100644 --- a/zebra-consensus/src/transaction/check.rs +++ b/zebra-consensus/src/transaction/check.rs @@ -13,12 +13,14 @@ use crate::error::TransactionError; /// Checks that the transaction has inputs and outputs. /// /// For `Transaction::V4`: -/// * at least one of `tx_in_count`, `nSpendsSapling`, and `nJoinSplit` MUST be non-zero. -/// * at least one of `tx_out_count`, `nOutputsSapling`, and `nJoinSplit` MUST be non-zero. +/// * At least one of `tx_in_count`, `nSpendsSapling`, and `nJoinSplit` MUST be non-zero. +/// * At least one of `tx_out_count`, `nOutputsSapling`, and `nJoinSplit` MUST be non-zero. /// /// For `Transaction::V5`: -/// * at least one of `tx_in_count`, `nSpendsSapling`, and `nActionsOrchard` MUST be non-zero. -/// * at least one of `tx_out_count`, `nOutputsSapling`, and `nActionsOrchard` MUST be non-zero. +/// * This condition must hold: `tx_in_count` > 0 or `nSpendsSapling` > 0 or +/// (`nActionsOrchard` > 0 and `enableSpendsOrchard` = 1) +/// * This condition must hold: `tx_out_count` > 0 or `nOutputsSapling` > 0 or +/// (`nActionsOrchard` > 0 and `enableOutputsOrchard` = 1) /// /// This check counts both `Coinbase` and `PrevOut` transparent inputs. /// @@ -30,10 +32,22 @@ pub fn has_inputs_and_outputs(tx: &Transaction) -> Result<(), TransactionError> let n_spends_sapling = tx.sapling_spends_per_anchor().count(); let n_outputs_sapling = tx.sapling_outputs().count(); let n_actions_orchard = tx.orchard_actions().count(); + let flags_orchard = tx.orchard_flags().unwrap_or_else(Flags::empty); - if tx_in_count + n_spends_sapling + n_joinsplit + n_actions_orchard == 0 { + // TODO: Improve the code to express the spec rules better #2410. + if tx_in_count + + n_spends_sapling + + n_joinsplit + + (n_actions_orchard > 0 && flags_orchard.contains(Flags::ENABLE_SPENDS)) as usize + == 0 + { Err(TransactionError::NoInputs) - } else if tx_out_count + n_outputs_sapling + n_joinsplit + n_actions_orchard == 0 { + } else if tx_out_count + + n_outputs_sapling + + n_joinsplit + + (n_actions_orchard > 0 && flags_orchard.contains(Flags::ENABLE_OUTPUTS)) as usize + == 0 + { Err(TransactionError::NoOutputs) } else { Ok(()) diff --git a/zebra-consensus/src/transaction/tests.rs b/zebra-consensus/src/transaction/tests.rs index 9f2922a9..13787119 100644 --- a/zebra-consensus/src/transaction/tests.rs +++ b/zebra-consensus/src/transaction/tests.rs @@ -86,8 +86,35 @@ fn fake_v5_transaction_with_orchard_actions_has_inputs_and_outputs() { // guaranteed structurally by `orchard::ShieldedData`) insert_fake_orchard_shielded_data(&mut transaction); - // If a transaction has at least one Orchard shielded action, it should be considered to have - // inputs and/or outputs + // The check will fail if the transaction has no flags + assert_eq!( + check::has_inputs_and_outputs(&transaction), + Err(TransactionError::NoInputs) + ); + + // If we add ENABLE_SPENDS flag it will pass the inputs check but fails with the outputs + // TODO: Avoid new calls to `insert_fake_orchard_shielded_data` for each check #2409. + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_SPENDS; + + assert_eq!( + check::has_inputs_and_outputs(&transaction), + Err(TransactionError::NoOutputs) + ); + + // If we add ENABLE_OUTPUTS flag it will pass the outputs check but fails with the inputs + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_OUTPUTS; + + assert_eq!( + check::has_inputs_and_outputs(&transaction), + Err(TransactionError::NoInputs) + ); + + // Finally make it valid by adding both required flags + let shielded_data = insert_fake_orchard_shielded_data(&mut transaction); + shielded_data.flags = orchard::Flags::ENABLE_SPENDS | orchard::Flags::ENABLE_OUTPUTS; + assert!(check::has_inputs_and_outputs(&transaction).is_ok()); }