Skip to content

Commit

Permalink
Test if conflicting transactions are rejected
Browse files Browse the repository at this point in the history
Generate two transactions (either V4 or V5) and insert a conflicting
spend, which can be either a transparent UTXO, or a nullifier for one of
the shielded pools. Check that any attempt to insert both transactions
causes one to be accepted and the other to be rejected.
  • Loading branch information
jvff committed Sep 24, 2021
1 parent 4f05b32 commit c87b067
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 0 deletions.
1 change: 1 addition & 0 deletions zebrad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ tokio = { version = "0.3.6", features = ["full", "test-util"] }
proptest = "0.10"
proptest-derive = "0.3"

zebra-chain = { path = "../zebra-chain", features = ["proptest-impl"] }
zebra-test = { path = "../zebra-test" }

[features]
Expand Down
1 change: 1 addition & 0 deletions zebrad/src/components/mempool/storage/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use zebra_chain::{
block::Block, parameters::Network, serialization::ZcashDeserializeInto, transaction::UnminedTx,
};

mod prop;
mod vectors;

pub fn unmined_transactions_in_blocks(
Expand Down
305 changes: 305 additions & 0 deletions zebrad/src/components/mempool/storage/tests/prop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
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 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::<SpendConflictTestInput>()) {
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::Rejected)
);

assert!(storage.contains_rejected(&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<TransparentSpendConflict>),
Sprout(Box<SproutSpendConflict>),
Sapling(Box<SaplingSpendConflict<sapling::PerSpendAnchor>>),
}

/// A spend conflict valid for V5 transactions.
#[derive(Arbitrary, Clone, Debug)]
enum SpendConflictForTransactionV5 {
Transparent(Box<TransparentSpendConflict>),
Sapling(Box<SaplingSpendConflict<sapling::SharedAnchor>>),
Orchard(Box<OrchardSpendConflict>),
}

/// 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<Groth16Proof>,
}

/// A conflict caused by revealing the same Sapling nullifier.
#[derive(Clone, Debug)]
struct SaplingSpendConflict<A: sapling::AnchorVariant + Clone> {
new_spend: sapling::Spend<A>,
new_shared_anchor: A::Shared,
fallback_shielded_data: sapling::ShieldedData<A>,
}

/// 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<transparent::Input>) {
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<transaction::JoinSplitData<Groth16Proof>>) {
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<A> Arbitrary for SaplingSpendConflict<A>
where
A: sapling::AnchorVariant + Clone + Debug + 'static,
A::Shared: Arbitrary,
sapling::Spend<A>: Arbitrary,
sapling::TransferData<A>: Arbitrary,
{
type Parameters = ();

fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
any::<(sapling::Spend<A>, A::Shared, sapling::ShieldedData<A>)>()
.prop_map(|(new_spend, new_shared_anchor, fallback_shielded_data)| {
SaplingSpendConflict {
new_spend,
new_shared_anchor,
fallback_shielded_data,
}
})
.boxed()
}

type Strategy = BoxedStrategy<Self>;
}

impl<A: sapling::AnchorVariant + Clone> SaplingSpendConflict<A> {
/// 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<sapling::ShieldedData<A>>) {
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<orchard::ShieldedData>) {
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);
}
}
}

0 comments on commit c87b067

Please sign in to comment.