#![allow(clippy::unwrap_in_result)]
use std::{collections::HashSet, env, fmt::Debug, thread, time::Duration};
use proptest::{collection::vec, prelude::*};
use proptest_derive::Arbitrary;
use zebra_chain::{
amount::Amount,
at_least_one,
fmt::{DisplayToDebug, SummaryDebug},
orchard,
primitives::{Groth16Proof, ZkSnarkProof},
sapling,
serialization::AtLeastOne,
sprout,
transaction::{self, JoinSplitData, Transaction, UnminedTxId, VerifiedUnminedTx},
transparent, LedgerState,
};
use crate::components::mempool::tests::{
standard_verified_unmined_tx_strategy, standardize_transaction,
};
use crate::components::mempool::{
config::Config,
storage::{
eviction_list::EvictionList, MempoolError, RejectionError, SameEffectsTipRejectionError,
Storage, MAX_EVICTION_MEMORY_ENTRIES,
},
SameEffectsChainRejectionError,
};
use MultipleTransactionRemovalTestInput::*;
const DEFAULT_MEMPOOL_LIST_PROPTEST_CASES: u32 = 64;
const EVICTION_MEMORY_TIME: Duration = Duration::from_secs(60 * 60);
const MEMPOOL_TX_COUNT: usize = 4;
proptest! {
#![proptest_config(
proptest::test_runner::Config::with_cases(env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MEMPOOL_LIST_PROPTEST_CASES))
)]
#[test]
fn reject_lists_are_limited_insert_conflict(
input in any::<SpendConflictTestInput>(),
mut rejection_template in any::<UnminedTxId>()
) {
let mut storage = Storage::new(
&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::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.transaction.id;
prop_assert_eq!(storage.insert(transaction_to_accept, Vec::new(), None), Ok(id_to_accept));
let unique_ids = (0..MAX_EVICTION_MEMORY_ENTRIES as u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
});
for rejection in unique_ids {
storage.reject(rejection, SameEffectsTipRejectionError::SpendConflict.into());
}
prop_assert_eq!(storage.rejected_transaction_count(), MAX_EVICTION_MEMORY_ENTRIES);
prop_assert_eq!(
storage.insert(transaction_to_reject, Vec::new(), None),
Err(MempoolError::StorageEffectsTip(SameEffectsTipRejectionError::SpendConflict))
);
prop_assert_eq!(storage.rejected_transaction_count(), 0);
storage.clear();
}
}
#[test]
fn reject_lists_are_limited_insert_eviction(
transactions in vec(standard_verified_unmined_tx_strategy(), MEMPOOL_TX_COUNT + 1)
.prop_map(SummaryDebug),
mut rejection_template in any::<UnminedTxId>()
) {
let cost_limit = transactions.iter().take(MEMPOOL_TX_COUNT).map(|tx| tx.cost()).sum();
let mut storage: Storage = Storage::new(&Config {
tx_cost_limit: cost_limit,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::default()
});
let unique_ids = (0..MAX_EVICTION_MEMORY_ENTRIES as u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
});
for rejection in unique_ids {
storage.reject(rejection, SameEffectsChainRejectionError::RandomlyEvicted.into());
}
prop_assert_eq!(storage.rejected_transaction_count(), MAX_EVICTION_MEMORY_ENTRIES);
for (i, transaction) in transactions.iter().enumerate() {
let tx_id = transaction.transaction.id;
if i < transactions.len() - 1 {
prop_assert_eq!(
storage.insert(transaction.clone(), Vec::new(), None),
Ok(tx_id)
);
} else {
let result = storage.insert(transaction.clone(), Vec::new(), None);
if result.is_ok() {
prop_assert_eq!(
result,
Ok(tx_id)
);
} else {
prop_assert_eq!(
result,
Err(MempoolError::StorageEffectsChain(SameEffectsChainRejectionError::RandomlyEvicted))
);
}
}
}
prop_assert!(storage.transaction_count() <= MEMPOOL_TX_COUNT);
prop_assert_eq!(storage.rejected_transaction_count(), MAX_EVICTION_MEMORY_ENTRIES);
}
#[test]
fn reject_lists_are_limited_reject(
rejection_error in any::<RejectionError>(),
mut rejection_template in any::<UnminedTxId>()
) {
let mut storage: Storage = Storage::new(&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::default()
});
let unique_ids = (0..(MAX_EVICTION_MEMORY_ENTRIES + 1) as u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
});
for (index, rejection) in unique_ids.enumerate() {
storage.reject(rejection, rejection_error.clone());
if index == MAX_EVICTION_MEMORY_ENTRIES - 1 {
prop_assert_eq!(storage.rejected_transaction_count(), MAX_EVICTION_MEMORY_ENTRIES);
} else if index == MAX_EVICTION_MEMORY_ENTRIES {
match rejection_error {
RejectionError::ExactTip(_) |
RejectionError::NonStandardTransaction(_) |
RejectionError::SameEffectsTip(_) => {
prop_assert_eq!(storage.rejected_transaction_count(), 0);
},
RejectionError::SameEffectsChain(_) => {
prop_assert_eq!(storage.rejected_transaction_count(), MAX_EVICTION_MEMORY_ENTRIES);
},
}
}
}
}
#[test]
fn reject_lists_are_time_pruned(
mut rejection_template in any::<UnminedTxId>()
) {
let mut storage = Storage::new(&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: Duration::from_millis(10),
..Default::default()
});
let unique_ids: Vec<UnminedTxId> = (0..2_u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
}).collect();
storage.reject(unique_ids[0], SameEffectsChainRejectionError::RandomlyEvicted.into());
thread::sleep(Duration::from_millis(11));
storage.reject(unique_ids[1], SameEffectsChainRejectionError::RandomlyEvicted.into());
prop_assert_eq!(storage.rejected_transaction_count(), 1);
}
}
proptest! {
#[test]
fn conflicting_transactions_are_rejected(input in any::<SpendConflictTestInput>()) {
let mut storage: Storage = Storage::new(&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::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.transaction.id;
let id_to_reject = transaction_to_reject.transaction.id;
prop_assert_eq!(storage.insert(transaction_to_accept, Vec::new(), None), Ok(id_to_accept));
prop_assert_eq!(
storage.insert(transaction_to_reject, Vec::new(), None),
Err(MempoolError::StorageEffectsTip(SameEffectsTipRejectionError::SpendConflict))
);
prop_assert!(storage.contains_rejected(&id_to_reject));
storage.clear();
}
}
#[test]
fn rejected_transactions_are_properly_rolled_back(input in any::<SpendConflictTestInput>())
{
let mut storage: Storage = Storage::new(&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::default()
});
let (first_unconflicting_transaction, second_unconflicting_transaction) =
input.clone().unconflicting_transactions();
let (first_transaction, second_transaction) = input.conflicting_transactions();
let input_permutations = vec![
(
first_transaction.clone(),
second_transaction.clone(),
second_unconflicting_transaction,
),
(
second_transaction,
first_transaction,
first_unconflicting_transaction,
),
];
for (first_transaction_to_accept, transaction_to_reject, second_transaction_to_accept) in
input_permutations
{
let first_id_to_accept = first_transaction_to_accept.transaction.id;
let second_id_to_accept = second_transaction_to_accept.transaction.id;
let id_to_reject = transaction_to_reject.transaction.id;
prop_assert_eq!(
storage.insert(first_transaction_to_accept, Vec::new(), None),
Ok(first_id_to_accept)
);
prop_assert_eq!(
storage.insert(transaction_to_reject, Vec::new(), None),
Err(MempoolError::StorageEffectsTip(SameEffectsTipRejectionError::SpendConflict))
);
prop_assert!(storage.contains_rejected(&id_to_reject));
prop_assert_eq!(
storage.insert(second_transaction_to_accept, Vec::new(), None),
Ok(second_id_to_accept)
);
storage.clear();
}
}
#[test]
fn removal_of_multiple_transactions(input in any::<MultipleTransactionRemovalTestInput>()) {
let mut storage: Storage = Storage::new(&Config {
tx_cost_limit: 160_000_000,
eviction_memory_time: EVICTION_MEMORY_TIME,
..Default::default()
});
let inserted_transactions: HashSet<_> = input
.transactions()
.filter_map(|transaction| {
let id = transaction.transaction.id;
storage.insert(transaction.clone(), Vec::new(), None).ok().map(|_| id)
})
.collect();
for transaction_id in &inserted_transactions {
prop_assert!(storage.contains_transaction_exact(&transaction_id.mined_id()));
}
match &input {
RemoveExact { wtx_ids_to_remove, .. } => storage.remove_exact(wtx_ids_to_remove),
RejectAndRemoveSameEffects { mined_ids_to_remove, .. } => {
let num_removals = storage.reject_and_remove_same_effects(mined_ids_to_remove, vec![]).total_len();
for &removed_transaction_id in mined_ids_to_remove.iter() {
prop_assert_eq!(
storage.rejection_error(&UnminedTxId::Legacy(removed_transaction_id)),
Some(SameEffectsChainRejectionError::Mined.into())
);
}
num_removals
},
};
let removed_transactions = input.removed_transaction_ids();
for removed_transaction_id in &removed_transactions {
prop_assert!(!storage.contains_transaction_exact(&removed_transaction_id.mined_id()));
}
let remaining_transactions = inserted_transactions.difference(&removed_transactions);
for remaining_transaction_id in remaining_transactions {
prop_assert!(storage.contains_transaction_exact(&remaining_transaction_id.mined_id()));
}
}
}
#[derive(Arbitrary, Clone, Debug)]
enum SpendConflictTestInput {
V4 {
#[proptest(
strategy = "Transaction::v4_strategy(LedgerState::default()).prop_map(DisplayToDebug)"
)]
first: DisplayToDebug<Transaction>,
#[proptest(
strategy = "Transaction::v4_strategy(LedgerState::default()).prop_map(DisplayToDebug)"
)]
second: DisplayToDebug<Transaction>,
conflict: SpendConflictForTransactionV4,
},
V5 {
#[proptest(
strategy = "Transaction::v5_strategy(LedgerState::default()).prop_map(DisplayToDebug)"
)]
first: DisplayToDebug<Transaction>,
#[proptest(
strategy = "Transaction::v5_strategy(LedgerState::default()).prop_map(DisplayToDebug)"
)]
second: DisplayToDebug<Transaction>,
conflict: SpendConflictForTransactionV5,
},
}
impl SpendConflictTestInput {
pub fn conflicting_transactions(self) -> (VerifiedUnminedTx, VerifiedUnminedTx) {
let (mut first, mut 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)
}
};
standardize_transaction(&mut first.0);
standardize_transaction(&mut second.0);
(
VerifiedUnminedTx::new(
first.0.into(),
Amount::try_from(1_000_000).expect("valid amount"),
0,
std::sync::Arc::new(vec![]),
)
.expect("verification should pass"),
VerifiedUnminedTx::new(
second.0.into(),
Amount::try_from(1_000_000).expect("valid amount"),
0,
std::sync::Arc::new(vec![]),
)
.expect("verification should pass"),
)
}
pub fn unconflicting_transactions(self) -> (VerifiedUnminedTx, VerifiedUnminedTx) {
let (mut first, mut second) = match self {
SpendConflictTestInput::V4 { first, second, .. } => (first, second),
SpendConflictTestInput::V5 { first, second, .. } => (first, second),
};
Self::remove_transparent_conflicts(&mut first, &mut second);
Self::remove_sprout_conflicts(&mut first, &mut second);
Self::remove_sapling_conflicts(&mut first, &mut second);
Self::remove_orchard_conflicts(&mut first, &mut second);
standardize_transaction(&mut first.0);
standardize_transaction(&mut second.0);
(
VerifiedUnminedTx::new(
first.0.into(),
Amount::try_from(1_000_000).expect("valid amount"),
0,
std::sync::Arc::new(vec![]),
)
.expect("verification should pass"),
VerifiedUnminedTx::new(
second.0.into(),
Amount::try_from(1_000_000).expect("valid amount"),
0,
std::sync::Arc::new(vec![]),
)
.expect("verification should pass"),
)
}
fn remove_transparent_conflicts(first: &mut Transaction, second: &mut Transaction) {
let first_spent_outpoints: HashSet<_> = first.spent_outpoints().collect();
let second_spent_outpoints: HashSet<_> = second.spent_outpoints().collect();
let conflicts: HashSet<_> = first_spent_outpoints
.intersection(&second_spent_outpoints)
.collect();
for transaction in [first, second] {
transaction.inputs_mut().retain(|input| {
input
.outpoint()
.as_ref()
.map(|outpoint| !conflicts.contains(outpoint))
.unwrap_or(true)
});
}
}
fn remove_sprout_conflicts(first: &mut Transaction, second: &mut Transaction) {
let first_nullifiers: HashSet<_> = first.sprout_nullifiers().copied().collect();
let second_nullifiers: HashSet<_> = second.sprout_nullifiers().copied().collect();
let conflicts: HashSet<_> = first_nullifiers
.intersection(&second_nullifiers)
.copied()
.collect();
for transaction in [first, second] {
match transaction {
Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => {
Self::remove_joinsplits_with_conflicts(joinsplit_data, &conflicts)
}
Transaction::V4 { joinsplit_data, .. } => {
Self::remove_joinsplits_with_conflicts(joinsplit_data, &conflicts)
}
Transaction::V1 { .. } | Transaction::V5 { .. } => {}
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
Transaction::V6 { .. } => {}
}
}
}
fn remove_joinsplits_with_conflicts<P: ZkSnarkProof>(
maybe_joinsplit_data: &mut Option<JoinSplitData<P>>,
conflicts: &HashSet<sprout::Nullifier>,
) {
let mut should_clear_joinsplit_data = false;
if let Some(joinsplit_data) = maybe_joinsplit_data.as_mut() {
joinsplit_data.rest.retain(|joinsplit| {
!joinsplit
.nullifiers
.iter()
.any(|nullifier| conflicts.contains(nullifier))
});
let first_joinsplit_should_be_removed = joinsplit_data
.first
.nullifiers
.iter()
.any(|nullifier| conflicts.contains(nullifier));
if first_joinsplit_should_be_removed {
if joinsplit_data.rest.is_empty() {
should_clear_joinsplit_data = true;
} else {
joinsplit_data.first = joinsplit_data.rest.remove(0);
}
}
}
if should_clear_joinsplit_data {
*maybe_joinsplit_data = None;
}
}
fn remove_sapling_conflicts(first: &mut Transaction, second: &mut Transaction) {
let first_nullifiers: HashSet<_> = first.sapling_nullifiers().copied().collect();
let second_nullifiers: HashSet<_> = second.sapling_nullifiers().copied().collect();
let conflicts: HashSet<_> = first_nullifiers
.intersection(&second_nullifiers)
.copied()
.collect();
for transaction in [first, second] {
match transaction {
Transaction::V4 {
sapling_shielded_data,
..
} => {
Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts)
}
Transaction::V5 {
sapling_shielded_data,
..
} => {
Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts)
}
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
Transaction::V6 {
sapling_shielded_data,
..
} => {
Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts)
}
Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {}
}
}
}
fn remove_sapling_transfers_with_conflicts<A>(
maybe_shielded_data: &mut Option<sapling::ShieldedData<A>>,
conflicts: &HashSet<sapling::Nullifier>,
) where
A: sapling::AnchorVariant + Clone,
{
if let Some(shielded_data) = maybe_shielded_data.take() {
match shielded_data.transfers {
sapling::TransferData::JustOutputs { .. } => {
*maybe_shielded_data = Some(shielded_data)
}
sapling::TransferData::SpendsAndMaybeOutputs {
shared_anchor,
spends,
maybe_outputs,
} => {
let updated_spends: Vec<_> = spends
.to_vec()
.into_iter()
.filter(|spend| !conflicts.contains(&spend.nullifier))
.collect();
if let Ok(spends) = AtLeastOne::try_from(updated_spends) {
*maybe_shielded_data = Some(sapling::ShieldedData {
transfers: sapling::TransferData::SpendsAndMaybeOutputs {
shared_anchor,
spends,
maybe_outputs,
},
..shielded_data
});
} else if let Ok(outputs) = AtLeastOne::try_from(maybe_outputs) {
*maybe_shielded_data = Some(sapling::ShieldedData {
transfers: sapling::TransferData::JustOutputs { outputs },
..shielded_data
});
}
}
}
}
}
fn remove_orchard_conflicts(first: &mut Transaction, second: &mut Transaction) {
let first_nullifiers: HashSet<_> = first.orchard_nullifiers().copied().collect();
let second_nullifiers: HashSet<_> = second.orchard_nullifiers().copied().collect();
let conflicts: HashSet<_> = first_nullifiers
.intersection(&second_nullifiers)
.copied()
.collect();
for transaction in [first, second] {
match transaction {
Transaction::V5 {
orchard_shielded_data,
..
} => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts),
#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))]
Transaction::V6 {
orchard_shielded_data,
..
} => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts),
Transaction::V1 { .. }
| Transaction::V2 { .. }
| Transaction::V3 { .. }
| Transaction::V4 { .. } => {}
}
}
}
fn remove_orchard_actions_with_conflicts(
maybe_shielded_data: &mut Option<orchard::ShieldedData>,
conflicts: &HashSet<orchard::Nullifier>,
) {
if let Some(shielded_data) = maybe_shielded_data.take() {
let updated_actions: Vec<_> = shielded_data
.actions
.to_vec()
.into_iter()
.filter(|action| !conflicts.contains(&action.action.nullifier))
.collect();
if let Ok(actions) = AtLeastOne::try_from(updated_actions) {
*maybe_shielded_data = Some(orchard::ShieldedData {
actions,
..shielded_data
});
}
}
}
}
#[derive(Arbitrary, Clone, Debug)]
enum SpendConflictForTransactionV4 {
Transparent(Box<TransparentSpendConflict>),
Sprout(Box<SproutSpendConflict>),
Sapling(Box<SaplingSpendConflict<sapling::PerSpendAnchor>>),
}
#[derive(Arbitrary, Clone, Debug)]
enum SpendConflictForTransactionV5 {
Transparent(Box<TransparentSpendConflict>),
Sapling(Box<SaplingSpendConflict<sapling::SharedAnchor>>),
Orchard(Box<OrchardSpendConflict>),
}
#[derive(Arbitrary, Clone, Debug)]
struct TransparentSpendConflict {
new_input: DisplayToDebug<transparent::Input>,
}
#[derive(Arbitrary, Clone, Debug)]
struct SproutSpendConflict {
new_joinsplit_data: DisplayToDebug<transaction::JoinSplitData<Groth16Proof>>,
}
#[derive(Clone, Debug)]
struct SaplingSpendConflict<A: sapling::AnchorVariant + Clone> {
new_spend: DisplayToDebug<sapling::Spend<A>>,
new_shared_anchor: A::Shared,
fallback_shielded_data: DisplayToDebug<sapling::ShieldedData<A>>,
}
#[derive(Arbitrary, Clone, Debug)]
struct OrchardSpendConflict {
new_shielded_data: DisplayToDebug<orchard::ShieldedData>,
}
impl SpendConflictForTransactionV4 {
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 {
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 {
pub fn apply_to(self, inputs: &mut Vec<transparent::Input>) {
inputs.push(self.new_input.0);
}
}
impl SproutSpendConflict {
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.0);
}
}
}
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_spend.into(),
new_shared_anchor,
fallback_shielded_data: fallback_shielded_data.into(),
}
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl<A: sapling::AnchorVariant + Clone> SaplingSpendConflict<A> {
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.0);
match &mut shielded_data.transfers {
SpendsAndMaybeOutputs { ref mut spends, .. } => {
let mut spends_vec = spends.as_slice().to_vec();
spends_vec.push(self.new_spend.0);
*spends = AtLeastOne::from_vec(spends_vec)
.expect("pushing one element never breaks at least one constraints");
}
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.0],
maybe_outputs: new_outputs.to_vec(),
};
}
}
}
}
impl OrchardSpendConflict {
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
.iter_mut()
.next()
.unwrap()
.action
.nullifier = self.new_shielded_data.actions.first().action.nullifier;
} else {
*orchard_shielded_data = Some(self.new_shielded_data.0);
}
}
}
#[derive(Clone, Debug)]
pub enum MultipleTransactionRemovalTestInput {
RemoveExact {
transactions: SummaryDebug<Vec<VerifiedUnminedTx>>,
wtx_ids_to_remove: SummaryDebug<HashSet<UnminedTxId>>,
},
RejectAndRemoveSameEffects {
transactions: SummaryDebug<Vec<VerifiedUnminedTx>>,
mined_ids_to_remove: SummaryDebug<HashSet<transaction::Hash>>,
},
}
impl Arbitrary for MultipleTransactionRemovalTestInput {
type Parameters = ();
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
vec(standard_verified_unmined_tx_strategy(), 1..MEMPOOL_TX_COUNT)
.prop_flat_map(|transactions| {
let indices_to_remove =
vec(any::<bool>(), 1..=transactions.len()).prop_map(|removal_markers| {
removal_markers
.into_iter()
.enumerate()
.filter(|(_, should_remove)| *should_remove)
.map(|(index, _)| index)
.collect::<HashSet<usize>>()
});
(Just(transactions), indices_to_remove)
})
.prop_flat_map(|(transactions, indices_to_remove)| {
let wtx_ids_to_remove: HashSet<_> = indices_to_remove
.iter()
.map(|&index| transactions[index].transaction.id)
.collect();
let mined_ids_to_remove: HashSet<transaction::Hash> = wtx_ids_to_remove
.iter()
.map(|unmined_id| unmined_id.mined_id())
.collect();
prop_oneof![
Just(RejectAndRemoveSameEffects {
transactions: transactions.clone().into(),
mined_ids_to_remove: mined_ids_to_remove.into(),
}),
Just(RemoveExact {
transactions: transactions.into(),
wtx_ids_to_remove: wtx_ids_to_remove.into(),
}),
]
})
.boxed()
}
type Strategy = BoxedStrategy<Self>;
}
impl MultipleTransactionRemovalTestInput {
pub fn transactions(&self) -> impl Iterator<Item = &VerifiedUnminedTx> + '_ {
match self {
RemoveExact { transactions, .. } | RejectAndRemoveSameEffects { transactions, .. } => {
transactions.iter()
}
}
}
pub fn removed_transaction_ids(&self) -> HashSet<UnminedTxId> {
match self {
RemoveExact {
wtx_ids_to_remove, ..
} => wtx_ids_to_remove.0.clone(),
RejectAndRemoveSameEffects {
transactions,
mined_ids_to_remove,
} => transactions
.iter()
.map(|transaction| transaction.transaction.id)
.filter(|id| mined_ids_to_remove.contains(&id.mined_id()))
.collect(),
}
}
}
proptest! {
#![proptest_config(
proptest::test_runner::Config::with_cases(1)
)]
#[test]
fn eviction_list_size(
mut rejection_template in any::<UnminedTxId>()
) {
let txids: Vec<UnminedTxId> = (0..4_u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
}).collect();
let mut e = EvictionList::new(2, EVICTION_MEMORY_TIME);
for txid in txids.iter() {
e.insert(txid.mined_id());
}
prop_assert!(!e.contains_key(&txids[0].mined_id()));
prop_assert!(!e.contains_key(&txids[1].mined_id()));
prop_assert!(e.contains_key(&txids[2].mined_id()));
prop_assert!(e.contains_key(&txids[3].mined_id()));
prop_assert_eq!(e.len(), 2);
}
#[test]
fn eviction_list_time(
mut rejection_template in any::<UnminedTxId>()
) {
let txids: Vec<UnminedTxId> = (0..2_u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
}).collect();
let mut e = EvictionList::new(2, Duration::from_millis(10));
e.insert(txids[0].mined_id());
thread::sleep(Duration::from_millis(11));
prop_assert!(!e.contains_key(&txids[0].mined_id()));
prop_assert_eq!(e.len(), 0);
e.insert(txids[1].mined_id());
prop_assert!(!e.contains_key(&txids[0].mined_id()));
prop_assert!(e.contains_key(&txids[1].mined_id()));
prop_assert_eq!(e.len(), 1);
}
#[test]
fn eviction_list_time_mixed(
mut rejection_template in any::<UnminedTxId>()
) {
const EVICTION_TIME: u64 = 500;
const BEFORE_EVICTION_TIME: u64 = 250;
let txids: Vec<UnminedTxId> = (0..20_u32).map(move |index| {
let index = index.to_le_bytes();
rejection_template.mined_id_mut().0[0..4].copy_from_slice(&index);
if let Some(auth_digest) = rejection_template.auth_digest_mut() {
auth_digest.0[0..4].copy_from_slice(&index);
}
rejection_template
}).collect();
let mut e = EvictionList::new(20, Duration::from_millis(EVICTION_TIME));
for txid in txids.iter().take(10) {
e.insert(txid.mined_id());
}
thread::sleep(Duration::from_millis(BEFORE_EVICTION_TIME));
for txid in txids.iter().skip(10) {
e.insert(txid.mined_id());
}
thread::sleep(Duration::from_millis(EVICTION_TIME - BEFORE_EVICTION_TIME + 1));
for txid in txids.iter().take(10) {
prop_assert!(!e.contains_key(&txid.mined_id()));
}
for txid in txids.iter().skip(10) {
prop_assert!(e.contains_key(&txid.mined_id()));
}
prop_assert_eq!(e.len(), 10);
thread::sleep(Duration::from_millis(EVICTION_TIME + 1));
for txid in txids.iter() {
prop_assert!(!e.contains_key(&txid.mined_id()));
}
prop_assert_eq!(e.len(), 0);
}
#[test]
#[should_panic]
fn eviction_list_refresh(
txid in any::<UnminedTxId>()
) {
let mut e = EvictionList::new(2, EVICTION_MEMORY_TIME);
e.insert(txid.mined_id());
e.insert(txid.mined_id());
}
}