294 lines
10 KiB
Rust
294 lines
10 KiB
Rust
//! Fixed test vectors for mempool storage.
|
|
|
|
use std::iter;
|
|
|
|
use color_eyre::eyre::Result;
|
|
|
|
use zebra_chain::{
|
|
amount::Amount,
|
|
block::{Block, Height},
|
|
parameters::Network,
|
|
transaction::{UnminedTxId, VerifiedUnminedTx},
|
|
};
|
|
|
|
use crate::components::mempool::{
|
|
config, storage::tests::unmined_transactions_in_blocks, storage::*, Mempool,
|
|
};
|
|
|
|
/// Eviction memory time used for tests. Most tests won't care about this
|
|
/// so we use a large enough value that will never be reached in the tests.
|
|
const EVICTION_MEMORY_TIME: Duration = Duration::from_secs(60 * 60);
|
|
|
|
/// Transaction count used in some tests to derive the mempool test size.
|
|
const MEMPOOL_TX_COUNT: usize = 4;
|
|
|
|
#[test]
|
|
fn mempool_storage_crud_exact_mainnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
// Create an empty storage instance
|
|
let mut storage: Storage = Storage::new(&config::Config {
|
|
tx_cost_limit: u64::MAX,
|
|
eviction_memory_time: EVICTION_MEMORY_TIME,
|
|
..Default::default()
|
|
});
|
|
|
|
// Get one (1) unmined transaction
|
|
let unmined_tx = unmined_transactions_in_blocks(.., network)
|
|
.next()
|
|
.expect("at least one unmined transaction");
|
|
|
|
// Insert unmined tx into the mempool.
|
|
let _ = storage.insert(unmined_tx.clone());
|
|
|
|
// Check that it is in the mempool, and not rejected.
|
|
assert!(storage.contains_transaction_exact(&unmined_tx.transaction.id));
|
|
|
|
// Remove tx
|
|
let removal_count = storage.remove_exact(&iter::once(unmined_tx.transaction.id).collect());
|
|
|
|
// Check that it is /not/ in the mempool.
|
|
assert_eq!(removal_count, 1);
|
|
assert!(!storage.contains_transaction_exact(&unmined_tx.transaction.id));
|
|
}
|
|
|
|
#[test]
|
|
fn mempool_storage_basic() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
// Test multiple times to catch intermittent bugs since eviction is randomized
|
|
for _ in 0..10 {
|
|
mempool_storage_basic_for_network(Network::Mainnet)?;
|
|
mempool_storage_basic_for_network(Network::Testnet)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn mempool_storage_basic_for_network(network: Network) -> Result<()> {
|
|
// Get transactions from the first 10 blocks of the Zcash blockchain
|
|
let unmined_transactions: Vec<_> = unmined_transactions_in_blocks(..=10, network).collect();
|
|
|
|
assert!(
|
|
MEMPOOL_TX_COUNT < unmined_transactions.len(),
|
|
"inconsistent MEMPOOL_TX_COUNT value for this test; decrease it"
|
|
);
|
|
|
|
// Use the sum of the costs of the first `MEMPOOL_TX_COUNT` transactions
|
|
// as the cost limit
|
|
let tx_cost_limit = unmined_transactions
|
|
.iter()
|
|
.take(MEMPOOL_TX_COUNT)
|
|
.map(|tx| tx.cost())
|
|
.sum();
|
|
|
|
// Create an empty storage
|
|
let mut storage: Storage = Storage::new(&config::Config {
|
|
tx_cost_limit,
|
|
..Default::default()
|
|
});
|
|
|
|
// Insert them all to the storage
|
|
let mut maybe_inserted_transactions = Vec::new();
|
|
let mut some_rejected_transactions = Vec::new();
|
|
for unmined_transaction in unmined_transactions.clone() {
|
|
let result = storage.insert(unmined_transaction.clone());
|
|
match result {
|
|
Ok(_) => {
|
|
// While the transaction was inserted here, it can be rejected later.
|
|
maybe_inserted_transactions.push(unmined_transaction);
|
|
}
|
|
Err(_) => {
|
|
// Other transactions can be rejected on a successful insert,
|
|
// so not all rejected transactions will be added.
|
|
// Note that `some_rejected_transactions` can be empty since `insert` only
|
|
// returns a rejection error if the transaction being inserted is the one
|
|
// that was randomly evicted.
|
|
some_rejected_transactions.push(unmined_transaction);
|
|
}
|
|
}
|
|
}
|
|
// Since transactions are rejected randomly we can't test exact numbers.
|
|
// We know the first MEMPOOL_TX_COUNT must have been inserted successfully.
|
|
assert!(maybe_inserted_transactions.len() >= MEMPOOL_TX_COUNT);
|
|
assert_eq!(
|
|
some_rejected_transactions.len() + maybe_inserted_transactions.len(),
|
|
unmined_transactions.len()
|
|
);
|
|
|
|
// Test if the actual number of inserted/rejected transactions is consistent.
|
|
assert!(storage.verified.transaction_count() <= maybe_inserted_transactions.len());
|
|
assert!(storage.rejected_transaction_count() >= some_rejected_transactions.len());
|
|
|
|
// Test if rejected transactions were actually rejected.
|
|
for tx in some_rejected_transactions.iter() {
|
|
assert!(!storage.contains_transaction_exact(&tx.transaction.id));
|
|
}
|
|
|
|
// Query all the ids we have for rejected, get back `total - MEMPOOL_SIZE`
|
|
let all_ids: HashSet<UnminedTxId> = unmined_transactions
|
|
.iter()
|
|
.map(|tx| tx.transaction.id)
|
|
.collect();
|
|
|
|
// Convert response to a `HashSet`, because the order of the response doesn't matter.
|
|
let all_rejected_ids: HashSet<UnminedTxId> = storage.rejected_transactions(all_ids).collect();
|
|
|
|
let some_rejected_ids = some_rejected_transactions
|
|
.iter()
|
|
.map(|tx| tx.transaction.id)
|
|
.collect::<HashSet<_>>();
|
|
|
|
// Test if the rejected transactions we have are a subset of the actually
|
|
// rejected transactions.
|
|
assert!(some_rejected_ids.is_subset(&all_rejected_ids));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn mempool_storage_crud_same_effects_mainnet() {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
let network = Network::Mainnet;
|
|
|
|
// Create an empty storage instance
|
|
let mut storage: Storage = Storage::new(&config::Config {
|
|
tx_cost_limit: 160_000_000,
|
|
eviction_memory_time: EVICTION_MEMORY_TIME,
|
|
..Default::default()
|
|
});
|
|
|
|
// Get one (1) unmined transaction
|
|
let unmined_tx_1 = unmined_transactions_in_blocks(.., network)
|
|
.next()
|
|
.expect("at least one unmined transaction");
|
|
|
|
// Insert unmined tx into the mempool.
|
|
let _ = storage.insert(unmined_tx_1.clone());
|
|
|
|
// Check that it is in the mempool, and not rejected.
|
|
assert!(storage.contains_transaction_exact(&unmined_tx_1.transaction.id));
|
|
|
|
// Reject and remove mined tx
|
|
let removal_count = storage.reject_and_remove_same_effects(
|
|
&iter::once(unmined_tx_1.transaction.id.mined_id()).collect(),
|
|
vec![unmined_tx_1.transaction.transaction.clone()],
|
|
);
|
|
|
|
// Check that it is /not/ in the mempool as a verified transaction.
|
|
assert_eq!(removal_count, 1);
|
|
assert!(!storage.contains_transaction_exact(&unmined_tx_1.transaction.id));
|
|
|
|
// Check that it's rejection is cached in the chain_rejected_same_effects' `Mined` eviction list.
|
|
assert_eq!(
|
|
storage.rejection_error(&unmined_tx_1.transaction.id),
|
|
Some(SameEffectsChainRejectionError::Mined.into())
|
|
);
|
|
assert_eq!(
|
|
storage.insert(unmined_tx_1),
|
|
Err(SameEffectsChainRejectionError::Mined.into())
|
|
);
|
|
|
|
// Get a different unmined transaction
|
|
let unmined_tx_2 = unmined_transactions_in_blocks(1.., network)
|
|
.find(|tx| {
|
|
tx.transaction
|
|
.transaction
|
|
.spent_outpoints()
|
|
.next()
|
|
.is_some()
|
|
})
|
|
.expect("at least one unmined transaction with at least 1 spent outpoint");
|
|
|
|
// Insert unmined tx into the mempool.
|
|
assert_eq!(
|
|
storage.insert(unmined_tx_2.clone()),
|
|
Ok(unmined_tx_2.transaction.id)
|
|
);
|
|
|
|
// Check that it is in the mempool, and not rejected.
|
|
assert!(storage.contains_transaction_exact(&unmined_tx_2.transaction.id));
|
|
|
|
// Reject and remove duplicate spend tx
|
|
let removal_count = storage.reject_and_remove_same_effects(
|
|
&HashSet::new(),
|
|
vec![unmined_tx_2.transaction.transaction.clone()],
|
|
);
|
|
|
|
// Check that it is /not/ in the mempool as a verified transaction.
|
|
assert_eq!(removal_count, 1);
|
|
assert!(!storage.contains_transaction_exact(&unmined_tx_2.transaction.id));
|
|
|
|
// Check that it's rejection is cached in the chain_rejected_same_effects' `SpendConflict` eviction list.
|
|
assert_eq!(
|
|
storage.rejection_error(&unmined_tx_2.transaction.id),
|
|
Some(SameEffectsChainRejectionError::DuplicateSpend.into())
|
|
);
|
|
assert_eq!(
|
|
storage.insert(unmined_tx_2),
|
|
Err(SameEffectsChainRejectionError::DuplicateSpend.into())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mempool_expired_basic() -> Result<()> {
|
|
let _init_guard = zebra_test::init();
|
|
|
|
mempool_expired_basic_for_network(Network::Mainnet)?;
|
|
mempool_expired_basic_for_network(Network::Testnet)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn mempool_expired_basic_for_network(network: Network) -> Result<()> {
|
|
// Create an empty storage
|
|
let mut storage: Storage = Storage::new(&config::Config {
|
|
tx_cost_limit: 160_000_000,
|
|
eviction_memory_time: EVICTION_MEMORY_TIME,
|
|
..Default::default()
|
|
});
|
|
|
|
let block: Block = network.test_block(982681, 925483).unwrap();
|
|
|
|
// Get a test transaction
|
|
let tx = &*(block.transactions[1]).clone();
|
|
|
|
// Change the expiration height of the test transaction
|
|
let mut tx = tx.clone();
|
|
*tx.expiry_height_mut() = zebra_chain::block::Height(1);
|
|
|
|
let tx_id = tx.unmined_id();
|
|
|
|
// Insert the transaction into the mempool, with a fake zero miner fee and sigops
|
|
storage.insert(
|
|
VerifiedUnminedTx::new(
|
|
tx.into(),
|
|
Amount::try_from(1_000_000).expect("invalid value"),
|
|
0,
|
|
)
|
|
.expect("verification should pass"),
|
|
)?;
|
|
|
|
assert_eq!(storage.transaction_count(), 1);
|
|
|
|
// Get everything available in mempool now
|
|
let everything_in_mempool: HashSet<UnminedTxId> = storage.tx_ids().collect();
|
|
assert_eq!(everything_in_mempool.len(), 1);
|
|
assert!(everything_in_mempool.contains(&tx_id));
|
|
|
|
// remove_expired_transactions() will return what was removed
|
|
let expired = storage.remove_expired_transactions(Height(1));
|
|
assert!(expired.contains(&tx_id));
|
|
let everything_in_mempool: HashSet<UnminedTxId> = storage.tx_ids().collect();
|
|
assert_eq!(everything_in_mempool.len(), 0);
|
|
|
|
// No transaction will be sent to peers
|
|
let send_to_peers = Mempool::remove_expired_from_peer_list(&everything_in_mempool, &expired);
|
|
assert_eq!(send_to_peers.len(), 0);
|
|
|
|
Ok(())
|
|
}
|