zebra/zebra-rpc/src/methods/tests/prop.rs

968 lines
35 KiB
Rust

//! Randomised property tests for RPC methods.
use std::collections::HashSet;
use futures::{join, FutureExt, TryFutureExt};
use hex::ToHex;
use jsonrpc_core::{Error, ErrorCode};
use proptest::{collection::vec, prelude::*};
use thiserror::Error;
use tower::buffer::Buffer;
use zebra_chain::{
amount::{Amount, NonNegative},
block::{Block, Height},
chain_tip::{mock::MockChainTip, NoChainTip},
parameters::{
Network::{self, *},
NetworkUpgrade,
},
serialization::{ZcashDeserialize, ZcashSerialize},
transaction::{self, Transaction, UnminedTx, VerifiedUnminedTx},
transparent,
};
use zebra_node_services::mempool;
use zebra_state::BoxError;
use zebra_test::mock_service::MockService;
use super::super::{
AddressBalance, AddressStrings, NetworkUpgradeStatus, Rpc, RpcImpl, SentTransactionHash,
};
proptest! {
/// Test that when sending a raw transaction, it is received by the mempool service.
#[test]
fn mempool_receives_raw_transaction(transaction in any::<Transaction>()) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let hash = SentTransactionHash(transaction.hash());
let transaction_bytes = transaction
.zcash_serialize_to_vec()
.expect("Transaction serializes successfully");
let transaction_hex = hex::encode(&transaction_bytes);
let send_task = tokio::spawn(rpc.send_raw_transaction(transaction_hex));
let unmined_transaction = UnminedTx::from(transaction);
let expected_request = mempool::Request::Queue(vec![unmined_transaction.into()]);
let response = mempool::Response::Queued(vec![Ok(())]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert_eq!(result, Ok(hash));
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that mempool errors are forwarded to the caller.
///
/// Mempool service errors should become server errors.
#[test]
fn mempool_errors_are_forwarded(transaction in any::<Transaction>()) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let transaction_bytes = transaction
.zcash_serialize_to_vec()
.expect("Transaction serializes successfully");
let transaction_hex = hex::encode(&transaction_bytes);
let send_task = tokio::spawn(rpc.send_raw_transaction(transaction_hex));
let unmined_transaction = UnminedTx::from(transaction);
let expected_request = mempool::Request::Queue(vec![unmined_transaction.into()]);
mempool
.expect_request(expected_request)
.await?
.respond(Err(DummyError));
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::ServerError(_),
..
})
),
"Result is not a server error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that when the mempool rejects a transaction the caller receives an error.
#[test]
fn rejected_transactions_are_reported(transaction in any::<Transaction>()) {
let (runtime, _init_guard) = zebra_test::init_async();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let transaction_bytes = transaction
.zcash_serialize_to_vec()
.expect("Transaction serializes successfully");
let transaction_hex = hex::encode(&transaction_bytes);
let send_task = tokio::spawn(rpc.send_raw_transaction(transaction_hex));
let unmined_transaction = UnminedTx::from(transaction);
let expected_request = mempool::Request::Queue(vec![unmined_transaction.into()]);
let response = mempool::Response::Queued(vec![Err(DummyError.into())]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::ServerError(_),
..
})
),
"Result is not a server error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that the method rejects non-hexadecimal characters.
///
/// Try to call `send_raw_transaction` using a string parameter that has at least one
/// non-hexadecimal character, and check that it fails with an expected error.
#[test]
fn non_hexadecimal_string_results_in_an_error(non_hex_string in ".*[^0-9A-Fa-f].*") {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let send_task = tokio::spawn(rpc.send_raw_transaction(non_hex_string));
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that the method rejects an input that's not a transaction.
///
/// Try to call `send_raw_transaction` using random bytes that fail to deserialize as a
/// transaction, and check that it fails with an expected error.
#[test]
fn invalid_transaction_results_in_an_error(random_bytes in any::<Vec<u8>>()) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();
prop_assume!(Transaction::zcash_deserialize(&*random_bytes).is_err());
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let send_task = tokio::spawn(rpc.send_raw_transaction(hex::encode(random_bytes)));
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that the `getrawmempool` method forwards the transactions in the mempool.
///
/// Make the mock mempool service return a list of transaction IDs, and check that the RPC call
/// returns those IDs as hexadecimal strings.
#[test]
fn mempool_transactions_are_sent_to_caller(transactions in any::<Vec<VerifiedUnminedTx>>()) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let call_task = tokio::spawn(rpc.get_raw_mempool());
#[cfg(not(feature = "getblocktemplate-rpcs"))]
let expected_response = {
let transaction_ids: HashSet<_> = transactions
.iter()
.map(|tx| tx.transaction.id)
.collect();
let mut expected_response: Vec<String> = transaction_ids
.iter()
.map(|id| id.mined_id().encode_hex())
.collect();
expected_response.sort();
mempool
.expect_request(mempool::Request::TransactionIds)
.await?
.respond(mempool::Response::TransactionIds(transaction_ids));
expected_response
};
// Note: this depends on `SHOULD_USE_ZCASHD_ORDER` being true.
#[cfg(feature = "getblocktemplate-rpcs")]
let expected_response = {
let mut expected_response = transactions.clone();
expected_response.sort_by_cached_key(|tx| {
// zcashd uses modified fee here but Zebra doesn't currently
// support prioritizing transactions
std::cmp::Reverse((
i64::from(tx.miner_fee) as u128 * zebra_chain::block::MAX_BLOCK_BYTES as u128
/ tx.transaction.size as u128,
// transaction hashes are compared in their serialized byte-order.
tx.transaction.id.mined_id(),
))
});
let expected_response = expected_response
.iter()
.map(|tx| tx.transaction.id.mined_id().encode_hex())
.collect();
mempool
.expect_request(mempool::Request::FullTransactions)
.await?
.respond(mempool::Response::FullTransactions {
transactions,
last_seen_tip_hash: [0; 32].into(),
});
expected_response
};
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
let result = call_task
.await
.expect("Sending raw transactions should not panic");
prop_assert_eq!(result, Ok(expected_response));
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that the method rejects non-hexadecimal characters.
///
/// Try to call `get_raw_transaction` using a string parameter that has at least one
/// non-hexadecimal character, and check that it fails with an expected error.
#[test]
fn get_raw_transaction_non_hexadecimal_string_results_in_an_error(
non_hex_string in ".*[^0-9A-Fa-f].*",
) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let send_task = tokio::spawn(rpc.get_raw_transaction(non_hex_string, 0));
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test that the method rejects an input that's not a transaction.
///
/// Try to call `get_raw_transaction` using random bytes that fail to deserialize as a
/// transaction, and check that it fails with an expected error.
#[test]
fn get_raw_transaction_invalid_transaction_results_in_an_error(
random_bytes in any::<Vec<u8>>(),
) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
// CORRECTNESS: Nothing in this test depends on real time, so we can speed it up.
tokio::time::pause();
prop_assume!(transaction::Hash::zcash_deserialize(&*random_bytes).is_err());
runtime.block_on(async move {
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let send_task = tokio::spawn(rpc.get_raw_transaction(hex::encode(random_bytes), 0));
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
let result = send_task
.await
.expect("Sending raw transactions should not panic");
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not an invalid parameters error: {result:?}"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test the `get_blockchain_info` response when Zebra's state is empty.
#[test]
fn get_blockchain_info_response_without_a_chain_tip(network in any::<Network>()) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
// look for an error with a `NoChainTip`
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let response = rpc.get_blockchain_info();
prop_assert_eq!(
&response.err().unwrap().message,
"No Chain tip available yet"
);
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
runtime.block_on(async move {
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
Ok::<_, TestCaseError>(())
})?;
}
/// Test the `get_blockchain_info` response using an arbitrary block as the `ChainTip`.
#[test]
fn get_blockchain_info_response_with_an_arbitrary_chain_tip(
network in any::<Network>(),
block in any::<Block>(),
) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
// get block data
let block_height = block.coinbase_height().unwrap();
let block_hash = block.hash();
let block_time = block.header.time;
// create a mocked `ChainTip`
let (chain_tip, mock_chain_tip_sender) = MockChainTip::new();
mock_chain_tip_sender.send_best_tip_height(block_height);
mock_chain_tip_sender.send_best_tip_hash(block_hash);
mock_chain_tip_sender.send_best_tip_block_time(block_time);
// Start RPC with the mocked `ChainTip`
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
);
let response = rpc.get_blockchain_info();
// Check response
match response {
Ok(info) => {
prop_assert_eq!(info.chain, network.bip70_network_name());
prop_assert_eq!(info.blocks, block_height);
prop_assert_eq!(info.best_block_hash, block_hash);
prop_assert!(info.estimated_height < Height::MAX);
prop_assert_eq!(
info.consensus.chain_tip.0,
NetworkUpgrade::current(network, block_height)
.branch_id()
.unwrap()
);
prop_assert_eq!(
info.consensus.next_block.0,
NetworkUpgrade::current(network, (block_height + 1).unwrap())
.branch_id()
.unwrap()
);
for u in info.upgrades {
let mut status = NetworkUpgradeStatus::Active;
if block_height < u.1.activation_height {
status = NetworkUpgradeStatus::Pending;
}
prop_assert_eq!(u.1.status, status);
}
}
Err(_) => {
unreachable!("Test should never error with the data we are feeding it")
}
};
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
// check no requests were made during this test
runtime.block_on(async move {
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
Ok::<_, TestCaseError>(())
})?;
}
/// Test the `get_address_balance` RPC using an arbitrary set of addresses.
#[test]
fn queries_balance_for_valid_addresses(
network in any::<Network>(),
addresses in any::<HashSet<transparent::Address>>(),
balance in any::<Amount<NonNegative>>(),
) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
// Create a mocked `ChainTip`
let (chain_tip, _mock_chain_tip_sender) = MockChainTip::new();
// Prepare the list of addresses.
let address_strings = AddressStrings {
addresses: addresses
.iter()
.map(|address| address.to_string())
.collect(),
};
tokio::time::pause();
// Start RPC with the mocked `ChainTip`
runtime.block_on(async move {
let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
);
// Build the future to call the RPC
let call = rpc.get_address_balance(address_strings);
// The RPC should perform a state query
let state_query = state
.expect_request(zebra_state::ReadRequest::AddressBalance(addresses))
.map_ok(|responder| {
responder.respond(zebra_state::ReadResponse::AddressBalance(balance))
});
// Await the RPC call and the state query
let (response, state_query_result) = join!(call, state_query);
state_query_result?;
// Check that response contains the expected balance
let received_balance = response?;
prop_assert_eq!(received_balance, AddressBalance { balance: balance.into() });
// Check no further requests were made during this test
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
Ok::<_, TestCaseError>(())
})?;
}
/// Test the `get_address_balance` RPC using an invalid list of addresses.
///
/// An error should be returned.
#[test]
fn does_not_query_balance_for_invalid_addresses(
network in any::<Network>(),
at_least_one_invalid_address in vec(".*", 1..10),
) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
prop_assume!(at_least_one_invalid_address
.iter()
.any(|string| string.parse::<transparent::Address>().is_err()));
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
// Create a mocked `ChainTip`
let (chain_tip, _mock_chain_tip_sender) = MockChainTip::new();
tokio::time::pause();
// Start RPC with the mocked `ChainTip`
runtime.block_on(async move {
let (rpc, _rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
network,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
chain_tip,
);
let address_strings = AddressStrings {
addresses: at_least_one_invalid_address,
};
// Build the future to call the RPC
let result = rpc.get_address_balance(address_strings).await;
// Check that the invalid addresses lead to an error
prop_assert!(
matches!(
result,
Err(Error {
code: ErrorCode::InvalidParams,
..
})
),
"Result is not a server error: {result:?}"
);
// Check no requests were made during this test
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
Ok::<_, TestCaseError>(())
})?;
}
/// Test the queue functionality using `send_raw_transaction`
#[test]
fn rpc_queue_main_loop(tx in any::<Transaction>()) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
let transaction_hash = tx.hash();
runtime.block_on(async move {
tokio::time::pause();
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
// send a transaction
let tx_bytes = tx
.zcash_serialize_to_vec()
.expect("Transaction serializes successfully");
let tx_hex = hex::encode(&tx_bytes);
let send_task = tokio::spawn(rpc.send_raw_transaction(tx_hex));
let tx_unmined = UnminedTx::from(tx);
let expected_request = mempool::Request::Queue(vec![tx_unmined.clone().into()]);
// fail the mempool insertion
mempool
.expect_request(expected_request)
.await
.unwrap()
.respond(Err(DummyError));
let _ = send_task
.await
.expect("Sending raw transactions should not panic");
// advance enough time to have a new runner iteration
let spacing = chrono::Duration::seconds(150);
tokio::time::advance(spacing.to_std().unwrap()).await;
// the runner will made a new call to TransactionsById
let mut transactions_hash_set = HashSet::new();
transactions_hash_set.insert(tx_unmined.id);
let expected_request = mempool::Request::TransactionsById(transactions_hash_set);
let response = mempool::Response::Transactions(vec![]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
// the runner will also query the state again for the transaction
let expected_request = zebra_state::ReadRequest::Transaction(transaction_hash);
let response = zebra_state::ReadResponse::Transaction(None);
state
.expect_request(expected_request)
.await?
.respond(response);
// now a retry will be sent to the mempool
let expected_request =
mempool::Request::Queue(vec![mempool::Gossip::Tx(tx_unmined.clone())]);
let response = mempool::Response::Queued(vec![Ok(())]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
// no more requests are done
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
/// Test we receive all transactions that are sent in a channel
#[test]
fn rpc_queue_receives_all_transactions_from_channel(txs in any::<[Transaction; 2]>()) {
let (runtime, _init_guard) = zebra_test::init_async();
let _guard = runtime.enter();
runtime.block_on(async move {
tokio::time::pause();
let mut mempool = MockService::build().for_prop_tests();
let mut state: MockService<_, _, _, BoxError> = MockService::build().for_prop_tests();
let (rpc, rpc_tx_queue_task_handle) = RpcImpl::new(
"RPC test",
Mainnet,
false,
true,
Buffer::new(mempool.clone(), 1),
Buffer::new(state.clone(), 1),
NoChainTip,
);
let mut transactions_hash_set = HashSet::new();
for tx in txs.clone() {
// send a transaction
let tx_bytes = tx
.zcash_serialize_to_vec()
.expect("Transaction serializes successfully");
let tx_hex = hex::encode(&tx_bytes);
let send_task = tokio::spawn(rpc.send_raw_transaction(tx_hex));
let tx_unmined = UnminedTx::from(tx.clone());
let expected_request = mempool::Request::Queue(vec![tx_unmined.clone().into()]);
// insert to hs we will use later
transactions_hash_set.insert(tx_unmined.id);
// fail the mempool insertion
mempool
.clone()
.expect_request(expected_request)
.await
.unwrap()
.respond(Err(DummyError));
let _ = send_task
.await
.expect("Sending raw transactions should not panic");
}
// advance enough time to have a new runner iteration
let spacing = chrono::Duration::seconds(150);
tokio::time::advance(spacing.to_std().unwrap()).await;
// the runner will made a new call to TransactionsById querying with both transactions
let expected_request = mempool::Request::TransactionsById(transactions_hash_set);
let response = mempool::Response::Transactions(vec![]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
// the runner will also query the state again for each transaction
for _tx in txs.clone() {
let response = zebra_state::ReadResponse::Transaction(None);
// we use `expect_request_that` because we can't guarantee the state request order
state
.expect_request_that(|request| {
matches!(request, zebra_state::ReadRequest::Transaction(_))
})
.await?
.respond(response);
}
// each transaction will be retried
for tx in txs.clone() {
let expected_request =
mempool::Request::Queue(vec![mempool::Gossip::Tx(UnminedTx::from(tx))]);
let response = mempool::Response::Queued(vec![Ok(())]);
mempool
.expect_request(expected_request)
.await?
.respond(response);
}
// no more requests are done
mempool.expect_no_requests().await?;
state.expect_no_requests().await?;
// The queue task should continue without errors or panics
let rpc_tx_queue_task_result = rpc_tx_queue_task_handle.now_or_never();
prop_assert!(rpc_tx_queue_task_result.is_none());
Ok::<_, TestCaseError>(())
})?;
}
}
#[derive(Clone, Copy, Debug, Error)]
#[error("a dummy error type")]
pub struct DummyError;