zebra/zebrad/src/components/mempool/tests/prop.rs

346 lines
12 KiB
Rust

//! Randomised property tests for the mempool.
use std::{env, fmt, sync::Arc};
use proptest::{collection::vec, prelude::*};
use proptest_derive::Arbitrary;
use chrono::Duration;
use tokio::time;
use tower::{buffer::Buffer, util::BoxService};
use zebra_chain::{
block::{self, Block},
fmt::DisplayToDebug,
parameters::{Network, NetworkUpgrade},
serialization::ZcashDeserializeInto,
transaction::VerifiedUnminedTx,
};
use zebra_consensus::{error::TransactionError, transaction as tx};
use zebra_network as zn;
use zebra_state::{self as zs, ChainTipBlock, ChainTipSender};
use zebra_test::mock_service::{MockService, PropTestAssertion};
use zs::FinalizedBlock;
use crate::components::{
mempool::{config::Config, Mempool},
sync::{RecentSyncLengths, SyncStatus},
};
/// A [`MockService`] representing the network service.
type MockPeerSet = MockService<zn::Request, zn::Response, PropTestAssertion>;
/// A [`MockService`] representing the Zebra state service.
type MockState = MockService<zs::Request, zs::Response, PropTestAssertion>;
/// A [`MockService`] representing the Zebra transaction verifier service.
type MockTxVerifier = MockService<tx::Request, tx::Response, PropTestAssertion, TransactionError>;
const CHAIN_LENGTH: usize = 5;
const DEFAULT_MEMPOOL_PROPTEST_CASES: u32 = 8;
proptest! {
// The mempool tests can generate very verbose logs, so we use fewer cases by
// default. Set the PROPTEST_CASES env var to override this default.
#![proptest_config(proptest::test_runner::Config::with_cases(env::var("PROPTEST_CASES")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_MEMPOOL_PROPTEST_CASES)))]
/// Test if the mempool storage is cleared on a chain reset.
#[test]
fn storage_is_cleared_on_single_chain_reset(
network in any::<Network>(),
transaction in any::<DisplayToDebug<VerifiedUnminedTx>>(),
chain_tip in any::<DisplayToDebug<ChainTipBlock>>(),
) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let (
mut mempool,
_peer_set,
_state_service,
_tx_verifier,
mut recent_syncs,
mut chain_tip_sender,
) = setup(network);
time::pause();
mempool.enable(&mut recent_syncs).await;
// Insert a dummy transaction.
mempool
.storage()
.insert(transaction.0)
.expect("Inserting a transaction should succeed");
// The first call to `poll_ready` shouldn't clear the storage yet.
mempool.dummy_call().await;
prop_assert_eq!(mempool.storage().transaction_count(), 1);
// Simulate a chain reset.
chain_tip_sender.set_finalized_tip(chain_tip.0);
// This time a call to `poll_ready` should clear the storage.
mempool.dummy_call().await;
prop_assert_eq!(mempool.storage().transaction_count(), 0);
// The services might or might not get requests,
// depending on how many transactions get re-queued, and if they need downloading.
Ok(())
})?;
}
/// Test if the mempool storage is cleared on multiple chain resets.
#[test]
fn storage_is_cleared_on_multiple_chain_resets(
network in any::<Network>(),
mut previous_chain_tip in any::<DisplayToDebug<ChainTipBlock>>(),
mut transactions in vec(any::<DisplayToDebug<VerifiedUnminedTx>>(), 0..CHAIN_LENGTH),
fake_chain_tips in vec(any::<DisplayToDebug<FakeChainTip>>(), 0..CHAIN_LENGTH),
) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let (
mut mempool,
_peer_set,
_state_service,
_tx_verifier,
mut recent_syncs,
mut chain_tip_sender,
) = setup(network);
time::pause();
mempool.enable(&mut recent_syncs).await;
// Set the initial chain tip.
chain_tip_sender.set_best_non_finalized_tip(previous_chain_tip.0.clone());
// Call the mempool so that it is aware of the initial chain tip.
mempool.dummy_call().await;
for (fake_chain_tip, transaction) in fake_chain_tips.iter().zip(transactions.iter_mut()) {
// Obtain a new chain tip based on the previous one.
let chain_tip = fake_chain_tip.to_chain_tip_block(&previous_chain_tip, network);
// Adjust the transaction expiry height based on the new chain
// tip height so that the mempool does not evict the transaction
// when there is a chain growth.
if let Some(expiry_height) = transaction.transaction.transaction.expiry_height() {
if chain_tip.height >= expiry_height {
let mut tmp_tx = (*transaction.transaction.transaction).clone();
// Set a new expiry height that is greater than the
// height of the current chain tip.
*tmp_tx.expiry_height_mut() = block::Height(chain_tip.height.0 + 1);
transaction.transaction = tmp_tx.into();
}
}
// Insert the dummy transaction into the mempool.
mempool
.storage()
.insert(transaction.0.clone())
.expect("Inserting a transaction should succeed");
// Set the new chain tip.
chain_tip_sender.set_best_non_finalized_tip(chain_tip.clone());
// Call the mempool so that it is aware of the new chain tip.
mempool.dummy_call().await;
match fake_chain_tip.0 {
FakeChainTip::Grow(_) => {
// The mempool should not be empty because we had a regular chain growth.
prop_assert_ne!(mempool.storage().transaction_count(), 0);
}
FakeChainTip::Reset(_) => {
// The mempool should be empty because we had a chain tip reset.
prop_assert_eq!(mempool.storage().transaction_count(), 0);
},
}
// Remember the current chain tip so that the next one can refer to it.
previous_chain_tip = chain_tip.into();
}
// The services might or might not get requests,
// depending on how many transactions get re-queued, and if they need downloading.
Ok(())
})?;
}
/// Test if the mempool storage is cleared if the syncer falls behind and starts to catch up.
#[test]
fn storage_is_cleared_if_syncer_falls_behind(
network in any::<Network>(),
transaction in any::<VerifiedUnminedTx>(),
) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let (
mut mempool,
mut peer_set,
mut state_service,
mut tx_verifier,
mut recent_syncs,
mut chain_tip_sender,
) = setup(network);
time::pause();
mempool.enable(&mut recent_syncs).await;
// Insert a dummy transaction.
mempool
.storage()
.insert(transaction)
.expect("Inserting a transaction should succeed");
// The first call to `poll_ready` shouldn't clear the storage yet.
mempool.dummy_call().await;
prop_assert_eq!(mempool.storage().transaction_count(), 1);
// Simulate the synchronizer catching up to the network chain tip.
mempool.disable(&mut recent_syncs).await;
// This time a call to `poll_ready` should clear the storage.
mempool.dummy_call().await;
// sends a new fake chain tip so that the mempool can be enabled
chain_tip_sender.set_finalized_tip(block1_chain_tip());
// Enable the mempool again so the storage can be accessed.
mempool.enable(&mut recent_syncs).await;
prop_assert_eq!(mempool.storage().transaction_count(), 0);
peer_set.expect_no_requests().await?;
state_service.expect_no_requests().await?;
tx_verifier.expect_no_requests().await?;
Ok(())
})?;
}
}
fn genesis_chain_tip() -> Option<ChainTipBlock> {
zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.map(FinalizedBlock::from)
.map(ChainTipBlock::from)
.ok()
}
fn block1_chain_tip() -> Option<ChainTipBlock> {
zebra_test::vectors::BLOCK_MAINNET_1_BYTES
.zcash_deserialize_into::<Arc<Block>>()
.map(FinalizedBlock::from)
.map(ChainTipBlock::from)
.ok()
}
/// Create a new [`Mempool`] instance using mocked services.
fn setup(
network: Network,
) -> (
Mempool,
MockPeerSet,
MockState,
MockTxVerifier,
RecentSyncLengths,
ChainTipSender,
) {
let peer_set = MockService::build().for_prop_tests();
let state_service = MockService::build().for_prop_tests();
let tx_verifier = MockService::build().for_prop_tests();
let (sync_status, recent_syncs) = SyncStatus::new();
let (mut chain_tip_sender, latest_chain_tip, chain_tip_change) =
ChainTipSender::new(None, network);
let (mempool, _transaction_receiver) = Mempool::new(
&Config {
tx_cost_limit: 160_000_000,
..Default::default()
},
Buffer::new(BoxService::new(peer_set.clone()), 1),
Buffer::new(BoxService::new(state_service.clone()), 1),
Buffer::new(BoxService::new(tx_verifier.clone()), 1),
sync_status,
latest_chain_tip,
chain_tip_change,
);
// sends a fake chain tip so that the mempool can be enabled
chain_tip_sender.set_finalized_tip(genesis_chain_tip());
(
mempool,
peer_set,
state_service,
tx_verifier,
recent_syncs,
chain_tip_sender,
)
}
/// A helper enum for simulating either a chain reset or growth.
#[derive(Arbitrary, Clone, Debug, Eq, PartialEq)]
enum FakeChainTip {
Grow(ChainTipBlock),
Reset(ChainTipBlock),
}
impl fmt::Display for FakeChainTip {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (mut f, inner) = match self {
FakeChainTip::Grow(inner) => (f.debug_tuple("FakeChainTip::Grow"), inner),
FakeChainTip::Reset(inner) => (f.debug_tuple("FakeChainTip::Reset"), inner),
};
f.field(&inner).finish()
}
}
impl FakeChainTip {
/// Returns a new [`ChainTipBlock`] placed on top of the previous block if
/// the chain is supposed to grow. Otherwise returns a [`ChainTipBlock`]
/// that does not reference the previous one.
fn to_chain_tip_block(&self, previous: &ChainTipBlock, network: Network) -> ChainTipBlock {
match self {
Self::Grow(chain_tip_block) => {
let height = block::Height(previous.height.0 + 1);
let target_spacing = NetworkUpgrade::target_spacing_for_height(network, height);
let mock_block_time_delta = Duration::seconds(
previous.time.timestamp() % (2 * target_spacing.num_seconds()),
);
ChainTipBlock {
hash: chain_tip_block.hash,
height,
time: previous.time + mock_block_time_delta,
transactions: chain_tip_block.transactions.clone(),
transaction_hashes: chain_tip_block.transaction_hashes.clone(),
previous_block_hash: previous.hash,
}
}
Self::Reset(chain_tip_block) => chain_tip_block.clone(),
}
}
}