use std::fmt::Debug; use proptest::prelude::*; use proptest_derive::Arbitrary; use zebra_chain::{ at_least_one, orchard, primitives::Groth16Proof, sapling, transaction::{self, Transaction, UnminedTx}, transparent, LedgerState, }; use crate::components::mempool::storage::SameEffectsRejectionError; use super::super::{MempoolError, Storage}; proptest! { /// Test if a transaction that has a spend conflict with a transaction already in the mempool /// is rejected. /// /// A spend conflict in this case is when two transactions spend the same UTXO or reveal the /// same nullifier. #[test] fn conflicting_transactions_are_rejected(input in any::()) { let mut storage = Storage::default(); let (first_transaction, second_transaction) = input.conflicting_transactions(); let input_permutations = vec![ (first_transaction.clone(), second_transaction.clone()), (second_transaction, first_transaction), ]; for (transaction_to_accept, transaction_to_reject) in input_permutations { let id_to_accept = transaction_to_accept.id; let id_to_reject = transaction_to_reject.id; assert_eq!( storage.insert(transaction_to_accept), Ok(id_to_accept) ); assert_eq!( storage.insert(transaction_to_reject), Err(MempoolError::StorageEffects(SameEffectsRejectionError::SpendConflict)) ); assert!(storage.contains_rejected_exact(&id_to_reject)); storage.clear(); } } } /// Test input consisting of two transactions and a conflict to be applied to them. /// /// When the conflict is applied, both transactions will have a shared spend (either a UTXO used as /// an input, or a nullifier revealed by both transactions). #[derive(Arbitrary, Debug)] enum SpendConflictTestInput { /// Test V4 transactions to include Sprout nullifier conflicts. V4 { #[proptest(strategy = "Transaction::v4_strategy(LedgerState::default())")] first: Transaction, #[proptest(strategy = "Transaction::v4_strategy(LedgerState::default())")] second: Transaction, conflict: SpendConflictForTransactionV4, }, /// Test V5 transactions to include Orchard nullifier conflicts. V5 { #[proptest(strategy = "Transaction::v5_strategy(LedgerState::default())")] first: Transaction, #[proptest(strategy = "Transaction::v5_strategy(LedgerState::default())")] second: Transaction, conflict: SpendConflictForTransactionV5, }, } impl SpendConflictTestInput { /// Return two transactions that have a spend conflict. pub fn conflicting_transactions(self) -> (UnminedTx, UnminedTx) { let (first, second) = match self { SpendConflictTestInput::V4 { mut first, mut second, conflict, } => { conflict.clone().apply_to(&mut first); conflict.apply_to(&mut second); (first, second) } SpendConflictTestInput::V5 { mut first, mut second, conflict, } => { conflict.clone().apply_to(&mut first); conflict.apply_to(&mut second); (first, second) } }; (first.into(), second.into()) } } /// A spend conflict valid for V4 transactions. #[derive(Arbitrary, Clone, Debug)] enum SpendConflictForTransactionV4 { Transparent(Box), Sprout(Box), Sapling(Box>), } /// A spend conflict valid for V5 transactions. #[derive(Arbitrary, Clone, Debug)] enum SpendConflictForTransactionV5 { Transparent(Box), Sapling(Box>), Orchard(Box), } /// A conflict caused by spending the same UTXO. #[derive(Arbitrary, Clone, Debug)] struct TransparentSpendConflict { new_input: transparent::Input, } /// A conflict caused by revealing the same Sprout nullifier. #[derive(Arbitrary, Clone, Debug)] struct SproutSpendConflict { new_joinsplit_data: transaction::JoinSplitData, } /// A conflict caused by revealing the same Sapling nullifier. #[derive(Clone, Debug)] struct SaplingSpendConflict { new_spend: sapling::Spend, new_shared_anchor: A::Shared, fallback_shielded_data: sapling::ShieldedData, } /// A conflict caused by revealing the same Orchard nullifier. #[derive(Arbitrary, Clone, Debug)] struct OrchardSpendConflict { new_shielded_data: orchard::ShieldedData, } impl SpendConflictForTransactionV4 { /// Apply a spend conflict to a V4 transaction. /// /// Changes the `transaction_v4` to include the spend that will result in a conflict. pub fn apply_to(self, transaction_v4: &mut Transaction) { let (inputs, joinsplit_data, sapling_shielded_data) = match transaction_v4 { Transaction::V4 { inputs, joinsplit_data, sapling_shielded_data, .. } => (inputs, joinsplit_data, sapling_shielded_data), _ => unreachable!("incorrect transaction version generated for test"), }; use SpendConflictForTransactionV4::*; match self { Transparent(transparent_conflict) => transparent_conflict.apply_to(inputs), Sprout(sprout_conflict) => sprout_conflict.apply_to(joinsplit_data), Sapling(sapling_conflict) => sapling_conflict.apply_to(sapling_shielded_data), } } } impl SpendConflictForTransactionV5 { /// Apply a spend conflict to a V5 transaction. /// /// Changes the `transaction_v5` to include the spend that will result in a conflict. pub fn apply_to(self, transaction_v5: &mut Transaction) { let (inputs, sapling_shielded_data, orchard_shielded_data) = match transaction_v5 { Transaction::V5 { inputs, sapling_shielded_data, orchard_shielded_data, .. } => (inputs, sapling_shielded_data, orchard_shielded_data), _ => unreachable!("incorrect transaction version generated for test"), }; use SpendConflictForTransactionV5::*; match self { Transparent(transparent_conflict) => transparent_conflict.apply_to(inputs), Sapling(sapling_conflict) => sapling_conflict.apply_to(sapling_shielded_data), Orchard(orchard_conflict) => orchard_conflict.apply_to(orchard_shielded_data), } } } impl TransparentSpendConflict { /// Apply a transparent spend conflict. /// /// Adds a new input to a transaction's list of transparent `inputs`. The transaction will then /// conflict with any other transaction that also has that same new input. pub fn apply_to(self, inputs: &mut Vec) { inputs.push(self.new_input); } } impl SproutSpendConflict { /// Apply a Sprout spend conflict. /// /// Ensures that a transaction's `joinsplit_data` has a nullifier used to represent a conflict. /// If the transaction already has Sprout joinsplits, the first nullifier is replaced with the /// new nullifier. Otherwise, a joinsplit is inserted with that new nullifier in the /// transaction. /// /// The transaction will then conflict with any other transaction with the same new nullifier. pub fn apply_to(self, joinsplit_data: &mut Option>) { if let Some(existing_joinsplit_data) = joinsplit_data.as_mut() { existing_joinsplit_data.first.nullifiers[0] = self.new_joinsplit_data.first.nullifiers[0]; } else { *joinsplit_data = Some(self.new_joinsplit_data); } } } /// Generate arbitrary [`SaplingSpendConflict`]s. /// /// This had to be implemented manually because of the constraints required as a consequence of the /// generic type parameter. impl Arbitrary for SaplingSpendConflict where A: sapling::AnchorVariant + Clone + Debug + 'static, A::Shared: Arbitrary, sapling::Spend: Arbitrary, sapling::TransferData: Arbitrary, { type Parameters = (); fn arbitrary_with(_: Self::Parameters) -> Self::Strategy { any::<(sapling::Spend, A::Shared, sapling::ShieldedData)>() .prop_map(|(new_spend, new_shared_anchor, fallback_shielded_data)| { SaplingSpendConflict { new_spend, new_shared_anchor, fallback_shielded_data, } }) .boxed() } type Strategy = BoxedStrategy; } impl SaplingSpendConflict { /// Apply a Sapling spend conflict. /// /// Ensures that a transaction's `sapling_shielded_data` has a nullifier used to represent a /// conflict. If the transaction already has Sapling shielded data, a new spend is added with /// the new nullifier. Otherwise, a fallback instance of Sapling shielded data is inserted in /// the transaction, and then the spend is added. /// /// The transaction will then conflict with any other transaction with the same new nullifier. pub fn apply_to(self, sapling_shielded_data: &mut Option>) { use sapling::TransferData::*; let shielded_data = sapling_shielded_data.get_or_insert(self.fallback_shielded_data); match &mut shielded_data.transfers { SpendsAndMaybeOutputs { ref mut spends, .. } => spends.push(self.new_spend), JustOutputs { ref mut outputs } => { let new_outputs = outputs.clone(); shielded_data.transfers = SpendsAndMaybeOutputs { shared_anchor: self.new_shared_anchor, spends: at_least_one![self.new_spend], maybe_outputs: new_outputs.into_vec(), }; } } } } impl OrchardSpendConflict { /// Apply a Orchard spend conflict. /// /// Ensures that a transaction's `orchard_shielded_data` has a nullifier used to represent a /// conflict. If the transaction already has Orchard shielded data, a new action is added with /// the new nullifier. Otherwise, a fallback instance of Orchard shielded data that contains /// the new action is inserted in the transaction. /// /// The transaction will then conflict with any other transaction with the same new nullifier. pub fn apply_to(self, orchard_shielded_data: &mut Option) { if let Some(shielded_data) = orchard_shielded_data.as_mut() { shielded_data.actions.first_mut().action.nullifier = self.new_shielded_data.actions.first().action.nullifier; } else { *orchard_shielded_data = Some(self.new_shielded_data); } } }