fix(rpc): Check that mempool transactions are valid for the state's chain info in getblocktemplate (#6416)
* check last seen tip hash from mempool in fetch_mempool_transactions() * Moves last_seen_tip_hash to ActiveState * fixes tests and tests fixes * continues to the next iteration of the loop to make fresh state and mempool requests if called with a long poll id * Update zebra-rpc/src/methods/get_block_template_rpcs.rs Co-authored-by: teor <teor@riseup.net> * adds allow[unused_variable) for linter * expects a chain tip when not(test) * Apply suggestions from code review Co-authored-by: teor <teor@riseup.net> * Addresses comments in code review * - Removes second call to `last_tip_change()` - Fixes tests using enabled mempool * Adds note about chain tip action requirement to test method `enable()` * updates doc comment * Update zebrad/src/components/mempool.rs Co-authored-by: teor <teor@riseup.net> * fixes test --------- Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
4fcb5b9de9
commit
d7842bd467
|
@ -103,7 +103,13 @@ pub enum Response {
|
|||
// TODO: make the Transactions response return VerifiedUnminedTx,
|
||||
// and remove the FullTransactions variant
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
FullTransactions(Vec<VerifiedUnminedTx>),
|
||||
FullTransactions {
|
||||
/// All [`VerifiedUnminedTx`]s in the mempool
|
||||
transactions: Vec<VerifiedUnminedTx>,
|
||||
|
||||
/// Last seen chain tip hash by mempool service
|
||||
last_seen_tip_hash: zebra_chain::block::Hash,
|
||||
},
|
||||
|
||||
/// Returns matching cached rejected [`UnminedTxId`]s from the mempool,
|
||||
RejectedTransactionIds(HashSet<UnminedTxId>),
|
||||
|
|
|
@ -798,7 +798,10 @@ where
|
|||
|
||||
match response {
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
mempool::Response::FullTransactions(mut transactions) => {
|
||||
mempool::Response::FullTransactions {
|
||||
mut transactions,
|
||||
last_seen_tip_hash: _,
|
||||
} => {
|
||||
// Sort transactions in descending order by fee/size, using hash in serialized byte order as a tie-breaker
|
||||
transactions.sort_by_cached_key(|tx| {
|
||||
// zcashd uses modified fee here but Zebra doesn't currently
|
||||
|
|
|
@ -448,8 +448,14 @@ where
|
|||
.as_ref()
|
||||
.and_then(get_block_template::JsonParameters::block_proposal_data)
|
||||
{
|
||||
return validate_block_proposal(self.chain_verifier.clone(), block_proposal_bytes)
|
||||
.boxed();
|
||||
return validate_block_proposal(
|
||||
self.chain_verifier.clone(),
|
||||
block_proposal_bytes,
|
||||
network,
|
||||
latest_chain_tip,
|
||||
sync_status,
|
||||
)
|
||||
.boxed();
|
||||
}
|
||||
|
||||
// To implement long polling correctly, we split this RPC into multiple phases.
|
||||
|
@ -505,7 +511,15 @@ where
|
|||
//
|
||||
// Optional TODO:
|
||||
// - add a `MempoolChange` type with an `async changed()` method (like `ChainTip`)
|
||||
let mempool_txs = fetch_mempool_transactions(mempool.clone()).await?;
|
||||
let Some(mempool_txs) =
|
||||
fetch_mempool_transactions(mempool.clone(), chain_tip_and_local_time.tip_hash)
|
||||
.await?
|
||||
// If the mempool and state responses are out of sync:
|
||||
// - if we are not long polling, omit mempool transactions from the template,
|
||||
// - if we are long polling, continue to the next iteration of the loop to make fresh state and mempool requests.
|
||||
.or_else(|| client_long_poll_id.is_none().then(Vec::new)) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// - Long poll ID calculation
|
||||
let server_long_poll_id = LongPollInput::new(
|
||||
|
|
|
@ -97,9 +97,12 @@ pub fn check_miner_address(
|
|||
/// usual acceptance rules (except proof-of-work).
|
||||
///
|
||||
/// Returns a `getblocktemplate` [`Response`].
|
||||
pub async fn validate_block_proposal<ChainVerifier>(
|
||||
pub async fn validate_block_proposal<ChainVerifier, Tip, SyncStatus>(
|
||||
mut chain_verifier: ChainVerifier,
|
||||
block_proposal_bytes: Vec<u8>,
|
||||
network: Network,
|
||||
latest_chain_tip: Tip,
|
||||
sync_status: SyncStatus,
|
||||
) -> Result<Response>
|
||||
where
|
||||
ChainVerifier: Service<zebra_consensus::Request, Response = block::Hash, Error = zebra_consensus::BoxError>
|
||||
|
@ -107,7 +110,11 @@ where
|
|||
+ Send
|
||||
+ Sync
|
||||
+ 'static,
|
||||
Tip: ChainTip + Clone + Send + Sync + 'static,
|
||||
SyncStatus: ChainSyncStatus + Clone + Send + Sync + 'static,
|
||||
{
|
||||
check_synced_to_tip(network, latest_chain_tip, sync_status)?;
|
||||
|
||||
let block: Block = match block_proposal_bytes.zcash_deserialize_into() {
|
||||
Ok(block) => block,
|
||||
Err(parse_error) => {
|
||||
|
@ -231,11 +238,15 @@ where
|
|||
Ok(chain_info)
|
||||
}
|
||||
|
||||
/// Returns the transactions that are currently in `mempool`.
|
||||
/// Returns the transactions that are currently in `mempool`, or None if the
|
||||
/// `last_seen_tip_hash` from the mempool response doesn't match the tip hash from the state.
|
||||
///
|
||||
/// You should call `check_synced_to_tip()` before calling this function.
|
||||
/// If the mempool is inactive because Zebra is not synced to the tip, returns no transactions.
|
||||
pub async fn fetch_mempool_transactions<Mempool>(mempool: Mempool) -> Result<Vec<VerifiedUnminedTx>>
|
||||
pub async fn fetch_mempool_transactions<Mempool>(
|
||||
mempool: Mempool,
|
||||
chain_tip_hash: block::Hash,
|
||||
) -> Result<Option<Vec<VerifiedUnminedTx>>>
|
||||
where
|
||||
Mempool: Service<
|
||||
mempool::Request,
|
||||
|
@ -253,11 +264,15 @@ where
|
|||
data: None,
|
||||
})?;
|
||||
|
||||
if let mempool::Response::FullTransactions(transactions) = response {
|
||||
Ok(transactions)
|
||||
} else {
|
||||
let mempool::Response::FullTransactions {
|
||||
transactions,
|
||||
last_seen_tip_hash,
|
||||
} = response else {
|
||||
unreachable!("unmatched response to a mempool::FullTransactions request")
|
||||
}
|
||||
};
|
||||
|
||||
// Check that the mempool and state were in sync when we made the requests
|
||||
Ok((last_seen_tip_hash == chain_tip_hash).then_some(transactions))
|
||||
}
|
||||
|
||||
// - Response processing
|
||||
|
|
|
@ -386,7 +386,10 @@ proptest! {
|
|||
mempool
|
||||
.expect_request(mempool::Request::FullTransactions)
|
||||
.await?
|
||||
.respond(mempool::Response::FullTransactions(transactions));
|
||||
.respond(mempool::Response::FullTransactions {
|
||||
transactions,
|
||||
last_seen_tip_hash: [0; 32].into(),
|
||||
});
|
||||
|
||||
expected_response
|
||||
};
|
||||
|
|
|
@ -176,7 +176,10 @@ async fn test_rpc_response_data_for_network(network: Network) {
|
|||
let mempool_req = mempool
|
||||
.expect_request_that(|request| matches!(request, mempool::Request::FullTransactions))
|
||||
.map(|responder| {
|
||||
responder.respond(mempool::Response::FullTransactions(vec![]));
|
||||
responder.respond(mempool::Response::FullTransactions {
|
||||
transactions: vec![],
|
||||
last_seen_tip_hash: blocks[blocks.len() - 1].hash(),
|
||||
});
|
||||
});
|
||||
|
||||
#[cfg(not(feature = "getblocktemplate-rpcs"))]
|
||||
|
|
|
@ -219,7 +219,11 @@ pub async fn test_responses<State, ReadState>(
|
|||
mempool
|
||||
.expect_request(mempool::Request::FullTransactions)
|
||||
.await
|
||||
.respond(mempool::Response::FullTransactions(vec![]));
|
||||
.respond(mempool::Response::FullTransactions {
|
||||
transactions: vec![],
|
||||
// tip hash needs to match chain info for long poll requests
|
||||
last_seen_tip_hash: fake_tip_hash,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1167,6 +1167,7 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
|
|||
block::{Hash, MAX_BLOCK_BYTES, ZCASH_BLOCK_VERSION},
|
||||
chain_sync_status::MockSyncStatus,
|
||||
serialization::DateTime32,
|
||||
transaction::VerifiedUnminedTx,
|
||||
work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256},
|
||||
};
|
||||
use zebra_consensus::MAX_BLOCK_SIGOPS;
|
||||
|
@ -1190,7 +1191,7 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
|
|||
|
||||
let mut mempool: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests();
|
||||
|
||||
let mut read_state = MockService::build().for_unit_tests();
|
||||
let read_state = MockService::build().for_unit_tests();
|
||||
let chain_verifier = MockService::build().for_unit_tests();
|
||||
|
||||
let mut mock_sync_status = MockSyncStatus::default();
|
||||
|
@ -1238,36 +1239,43 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
|
|||
);
|
||||
|
||||
// Fake the ChainInfo response
|
||||
let mock_read_state_request_handler = async move {
|
||||
read_state
|
||||
.expect_request_that(|req| matches!(req, ReadRequest::ChainInfo))
|
||||
.await
|
||||
.respond(ReadResponse::ChainInfo(GetBlockTemplateChainInfo {
|
||||
expected_difficulty: fake_difficulty,
|
||||
tip_height: fake_tip_height,
|
||||
tip_hash: fake_tip_hash,
|
||||
cur_time: fake_cur_time,
|
||||
min_time: fake_min_time,
|
||||
max_time: fake_max_time,
|
||||
history_tree: fake_history_tree(Mainnet),
|
||||
}));
|
||||
let make_mock_read_state_request_handler = || {
|
||||
let mut read_state = read_state.clone();
|
||||
|
||||
async move {
|
||||
read_state
|
||||
.expect_request_that(|req| matches!(req, ReadRequest::ChainInfo))
|
||||
.await
|
||||
.respond(ReadResponse::ChainInfo(GetBlockTemplateChainInfo {
|
||||
expected_difficulty: fake_difficulty,
|
||||
tip_height: fake_tip_height,
|
||||
tip_hash: fake_tip_hash,
|
||||
cur_time: fake_cur_time,
|
||||
min_time: fake_min_time,
|
||||
max_time: fake_max_time,
|
||||
history_tree: fake_history_tree(Mainnet),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let mock_mempool_request_handler = {
|
||||
let make_mock_mempool_request_handler = |transactions, last_seen_tip_hash| {
|
||||
let mut mempool = mempool.clone();
|
||||
async move {
|
||||
mempool
|
||||
.expect_request(mempool::Request::FullTransactions)
|
||||
.await
|
||||
.respond(mempool::Response::FullTransactions(vec![]));
|
||||
.respond(mempool::Response::FullTransactions {
|
||||
transactions,
|
||||
last_seen_tip_hash,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let get_block_template_fut = get_block_template_rpc.get_block_template(None);
|
||||
let (get_block_template, ..) = tokio::join!(
|
||||
get_block_template_fut,
|
||||
mock_mempool_request_handler,
|
||||
mock_read_state_request_handler,
|
||||
make_mock_mempool_request_handler(vec![], fake_tip_hash),
|
||||
make_mock_read_state_request_handler(),
|
||||
);
|
||||
|
||||
let get_block_template::Response::TemplateMode(get_block_template) = get_block_template
|
||||
|
@ -1324,8 +1332,6 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
|
|||
Amount::<NonNegative>::zero()
|
||||
);
|
||||
|
||||
mempool.expect_no_requests().await;
|
||||
|
||||
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(200));
|
||||
let get_block_template_sync_error = get_block_template_rpc
|
||||
.get_block_template(None)
|
||||
|
@ -1400,6 +1406,53 @@ async fn rpc_getblocktemplate_mining_address(use_p2pkh: bool) {
|
|||
get_block_template_sync_error.code,
|
||||
ErrorCode::ServerError(-10)
|
||||
);
|
||||
|
||||
// Try getting mempool transactions with a different tip hash
|
||||
|
||||
let tx = Arc::new(Transaction::V1 {
|
||||
inputs: vec![],
|
||||
outputs: vec![],
|
||||
lock_time: transaction::LockTime::unlocked(),
|
||||
});
|
||||
|
||||
let unmined_tx = UnminedTx {
|
||||
transaction: tx.clone(),
|
||||
id: tx.unmined_id(),
|
||||
size: tx.zcash_serialized_size(),
|
||||
conventional_fee: 0.try_into().unwrap(),
|
||||
};
|
||||
|
||||
let verified_unmined_tx = VerifiedUnminedTx {
|
||||
transaction: unmined_tx,
|
||||
miner_fee: 0.try_into().unwrap(),
|
||||
legacy_sigop_count: 0,
|
||||
unpaid_actions: 0,
|
||||
fee_weight_ratio: 1.0,
|
||||
};
|
||||
|
||||
let next_fake_tip_hash =
|
||||
Hash::from_hex("0000000000b6a5024aa412120b684a509ba8fd57e01de07bc2a84e4d3719a9f1").unwrap();
|
||||
|
||||
mock_sync_status.set_is_close_to_tip(true);
|
||||
|
||||
mock_chain_tip_sender.send_estimated_distance_to_network_chain_tip(Some(0));
|
||||
|
||||
let (get_block_template, ..) = tokio::join!(
|
||||
get_block_template_rpc.get_block_template(None),
|
||||
make_mock_mempool_request_handler(vec![verified_unmined_tx], next_fake_tip_hash),
|
||||
make_mock_read_state_request_handler(),
|
||||
);
|
||||
|
||||
let get_block_template::Response::TemplateMode(get_block_template) = get_block_template
|
||||
.expect("unexpected error in getblocktemplate RPC call") else {
|
||||
panic!("this getblocktemplate call without parameters should return the `TemplateMode` variant of the response")
|
||||
};
|
||||
|
||||
// mempool transactions should be omitted if the tip hash in the GetChainInfo response from the state
|
||||
// does not match the `last_seen_tip_hash` in the FullTransactions response from the mempool.
|
||||
assert!(get_block_template.transactions.is_empty());
|
||||
|
||||
mempool.expect_no_requests().await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
|
|
|
@ -666,6 +666,15 @@ impl TipAction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the block hash and height of this tip action,
|
||||
/// regardless of the underlying variant.
|
||||
pub fn best_tip_hash_and_height(&self) -> (block::Hash, block::Height) {
|
||||
match self {
|
||||
Grow { block } => (block.hash, block.height),
|
||||
Reset { hash, height } => (*hash, *height),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a [`Grow`] based on `block`.
|
||||
pub(crate) fn grow_with(block: ChainTipBlock) -> Self {
|
||||
Grow { block }
|
||||
|
|
|
@ -31,7 +31,9 @@ use tokio::sync::watch;
|
|||
use tower::{buffer::Buffer, timeout::Timeout, util::BoxService, Service};
|
||||
|
||||
use zebra_chain::{
|
||||
block::Height, chain_sync_status::ChainSyncStatus, chain_tip::ChainTip,
|
||||
block::{self, Height},
|
||||
chain_sync_status::ChainSyncStatus,
|
||||
chain_tip::ChainTip,
|
||||
transaction::UnminedTxId,
|
||||
};
|
||||
use zebra_consensus::{error::TransactionError, transaction};
|
||||
|
@ -104,6 +106,11 @@ enum ActiveState {
|
|||
|
||||
/// The transaction download and verify stream.
|
||||
tx_downloads: Pin<Box<InboundTxDownloads>>,
|
||||
|
||||
/// Last seen chain tip hash that mempool transactions have been verified against.
|
||||
///
|
||||
/// In some tests, this is initialized to the latest chain tip, then updated in `poll_ready()` before each request.
|
||||
last_seen_tip_hash: block::Hash,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -120,6 +127,7 @@ impl ActiveState {
|
|||
ActiveState::Enabled {
|
||||
storage,
|
||||
tx_downloads,
|
||||
..
|
||||
} => {
|
||||
let mut transactions = Vec::new();
|
||||
|
||||
|
@ -205,7 +213,7 @@ impl Mempool {
|
|||
|
||||
// Make sure `is_enabled` is accurate.
|
||||
// Otherwise, it is only updated in `poll_ready`, right before each service call.
|
||||
service.update_state();
|
||||
service.update_state(None);
|
||||
|
||||
(service, transaction_receiver)
|
||||
}
|
||||
|
@ -241,41 +249,47 @@ impl Mempool {
|
|||
/// Update the mempool state (enabled / disabled) depending on how close to
|
||||
/// the tip is the synchronization, including side effects to state changes.
|
||||
///
|
||||
/// Accepts an optional [`TipAction`] for setting the `last_seen_tip_hash` field
|
||||
/// when enabling the mempool state, it will not enable the mempool if this is None.
|
||||
///
|
||||
/// Returns `true` if the state changed.
|
||||
fn update_state(&mut self) -> bool {
|
||||
fn update_state(&mut self, tip_action: Option<&TipAction>) -> bool {
|
||||
let is_close_to_tip = self.sync_status.is_close_to_tip() || self.is_enabled_by_debug();
|
||||
|
||||
if self.is_enabled() == is_close_to_tip {
|
||||
// the active state is up to date
|
||||
return false;
|
||||
}
|
||||
match (is_close_to_tip, self.is_enabled(), tip_action) {
|
||||
// the active state is up to date, or there is no tip action to activate the mempool
|
||||
(false, false, _) | (true, true, _) | (true, false, None) => return false,
|
||||
|
||||
// Update enabled / disabled state
|
||||
if is_close_to_tip {
|
||||
info!(
|
||||
tip_height = ?self.latest_chain_tip.best_tip_height(),
|
||||
"activating mempool: Zebra is close to the tip"
|
||||
);
|
||||
// Enable state - there should be a chain tip when Zebra is close to the network tip
|
||||
(true, false, Some(tip_action)) => {
|
||||
let (last_seen_tip_hash, tip_height) = tip_action.best_tip_hash_and_height();
|
||||
|
||||
let tx_downloads = Box::pin(TxDownloads::new(
|
||||
Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT),
|
||||
Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT),
|
||||
self.state.clone(),
|
||||
));
|
||||
self.active_state = ActiveState::Enabled {
|
||||
storage: storage::Storage::new(&self.config),
|
||||
tx_downloads,
|
||||
};
|
||||
} else {
|
||||
info!(
|
||||
tip_height = ?self.latest_chain_tip.best_tip_height(),
|
||||
"deactivating mempool: Zebra is syncing lots of blocks"
|
||||
);
|
||||
info!(?tip_height, "activating mempool: Zebra is close to the tip");
|
||||
|
||||
// This drops the previous ActiveState::Enabled, cancelling its download tasks.
|
||||
// We don't preserve the previous transactions, because we are syncing lots of blocks.
|
||||
self.active_state = ActiveState::Disabled
|
||||
}
|
||||
let tx_downloads = Box::pin(TxDownloads::new(
|
||||
Timeout::new(self.outbound.clone(), TRANSACTION_DOWNLOAD_TIMEOUT),
|
||||
Timeout::new(self.tx_verifier.clone(), TRANSACTION_VERIFY_TIMEOUT),
|
||||
self.state.clone(),
|
||||
));
|
||||
self.active_state = ActiveState::Enabled {
|
||||
storage: storage::Storage::new(&self.config),
|
||||
tx_downloads,
|
||||
last_seen_tip_hash,
|
||||
};
|
||||
}
|
||||
|
||||
// Disable state
|
||||
(false, true, _) => {
|
||||
info!(
|
||||
tip_height = ?self.latest_chain_tip.best_tip_height(),
|
||||
"deactivating mempool: Zebra is syncing lots of blocks"
|
||||
);
|
||||
|
||||
// This drops the previous ActiveState::Enabled, cancelling its download tasks.
|
||||
// We don't preserve the previous transactions, because we are syncing lots of blocks.
|
||||
self.active_state = ActiveState::Disabled;
|
||||
}
|
||||
};
|
||||
|
||||
true
|
||||
}
|
||||
|
@ -307,7 +321,8 @@ impl Service<Request> for Mempool {
|
|||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
let is_state_changed = self.update_state();
|
||||
let tip_action = self.chain_tip_change.last_tip_change();
|
||||
let is_state_changed = self.update_state(tip_action.as_ref());
|
||||
|
||||
tracing::trace!(is_enabled = ?self.is_enabled(), ?is_state_changed, "started polling the mempool...");
|
||||
|
||||
|
@ -317,8 +332,6 @@ impl Service<Request> for Mempool {
|
|||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
let tip_action = self.chain_tip_change.last_tip_change();
|
||||
|
||||
// Clear the mempool and cancel downloads if there has been a chain tip reset.
|
||||
//
|
||||
// But if the mempool was just freshly enabled,
|
||||
|
@ -341,7 +354,7 @@ impl Service<Request> for Mempool {
|
|||
std::mem::drop(previous_state);
|
||||
|
||||
// Re-initialise an empty state.
|
||||
self.update_state();
|
||||
self.update_state(tip_action.as_ref());
|
||||
|
||||
// Re-verify the transactions that were pending or valid at the previous tip.
|
||||
// This saves us the time and data needed to re-download them.
|
||||
|
@ -364,6 +377,7 @@ impl Service<Request> for Mempool {
|
|||
if let ActiveState::Enabled {
|
||||
storage,
|
||||
tx_downloads,
|
||||
last_seen_tip_hash,
|
||||
} = &mut self.active_state
|
||||
{
|
||||
// Collect inserted transaction ids.
|
||||
|
@ -413,6 +427,7 @@ impl Service<Request> for Mempool {
|
|||
// Handle best chain tip changes
|
||||
if let Some(TipAction::Grow { block }) = tip_action {
|
||||
tracing::trace!(block_height = ?block.height, "handling blocks added to tip");
|
||||
*last_seen_tip_hash = block.hash;
|
||||
|
||||
// Cancel downloads/verifications/storage of transactions
|
||||
// with the same mined IDs as recently mined transactions.
|
||||
|
@ -467,6 +482,10 @@ impl Service<Request> for Mempool {
|
|||
ActiveState::Enabled {
|
||||
storage,
|
||||
tx_downloads,
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
last_seen_tip_hash,
|
||||
#[cfg(not(feature = "getblocktemplate-rpcs"))]
|
||||
last_seen_tip_hash: _,
|
||||
} => match req {
|
||||
// Queries
|
||||
Request::TransactionIds => {
|
||||
|
@ -509,11 +528,16 @@ impl Service<Request> for Mempool {
|
|||
Request::FullTransactions => {
|
||||
trace!(?req, "got mempool request");
|
||||
|
||||
let res: Vec<_> = storage.full_transactions().cloned().collect();
|
||||
let transactions: Vec<_> = storage.full_transactions().cloned().collect();
|
||||
|
||||
trace!(?req, res_count = ?res.len(), "answered mempool request");
|
||||
trace!(?req, transactions_count = ?transactions.len(), "answered mempool request");
|
||||
|
||||
async move { Ok(Response::FullTransactions(res)) }.boxed()
|
||||
let response = Response::FullTransactions {
|
||||
transactions,
|
||||
last_seen_tip_hash: *last_seen_tip_hash,
|
||||
};
|
||||
|
||||
async move { Ok(response) }.boxed()
|
||||
}
|
||||
|
||||
Request::RejectedTransactionIds(ref ids) => {
|
||||
|
@ -559,6 +583,7 @@ impl Service<Request> for Mempool {
|
|||
// We can't return an error since that will cause a disconnection
|
||||
// by the peer connection handler. Therefore, return successful
|
||||
// empty responses.
|
||||
|
||||
let resp = match req {
|
||||
// Return empty responses for queries.
|
||||
Request::TransactionIds => Response::TransactionIds(Default::default()),
|
||||
|
@ -566,7 +591,12 @@ impl Service<Request> for Mempool {
|
|||
Request::TransactionsById(_) => Response::Transactions(Default::default()),
|
||||
Request::TransactionsByMinedId(_) => Response::Transactions(Default::default()),
|
||||
#[cfg(feature = "getblocktemplate-rpcs")]
|
||||
Request::FullTransactions => Response::FullTransactions(Default::default()),
|
||||
Request::FullTransactions => {
|
||||
return async move {
|
||||
Err("mempool is not active: wait for Zebra to sync to the tip".into())
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
Request::RejectedTransactionIds(_) => {
|
||||
Response::RejectedTransactionIds(Default::default())
|
||||
|
@ -590,6 +620,7 @@ impl Service<Request> for Mempool {
|
|||
Response::CheckedForVerifiedTransactions
|
||||
}
|
||||
};
|
||||
|
||||
async move { Ok(resp) }.boxed()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ impl Mempool {
|
|||
}
|
||||
|
||||
/// Enable the mempool by pretending the synchronization is close to the tip.
|
||||
///
|
||||
/// Requires a chain tip action to enable the mempool before the future resolves.
|
||||
pub async fn enable(&mut self, recent_syncs: &mut RecentSyncLengths) {
|
||||
// Pretend we're close to tip
|
||||
SyncStatus::sync_close_to_tip(recent_syncs);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Randomised property tests for the mempool.
|
||||
|
||||
use std::{env, fmt};
|
||||
use std::{env, fmt, sync::Arc};
|
||||
|
||||
use proptest::{collection::vec, prelude::*};
|
||||
use proptest_derive::Arbitrary;
|
||||
|
@ -10,15 +10,17 @@ use tokio::time;
|
|||
use tower::{buffer::Buffer, util::BoxService};
|
||||
|
||||
use zebra_chain::{
|
||||
block,
|
||||
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},
|
||||
|
@ -193,7 +195,7 @@ proptest! {
|
|||
mut state_service,
|
||||
mut tx_verifier,
|
||||
mut recent_syncs,
|
||||
_chain_tip_sender,
|
||||
mut chain_tip_sender,
|
||||
) = setup(network);
|
||||
|
||||
time::pause();
|
||||
|
@ -217,6 +219,9 @@ proptest! {
|
|||
// 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;
|
||||
|
||||
|
@ -231,6 +236,22 @@ proptest! {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
@ -247,7 +268,8 @@ fn setup(
|
|||
let tx_verifier = MockService::build().for_prop_tests();
|
||||
|
||||
let (sync_status, recent_syncs) = SyncStatus::new();
|
||||
let (chain_tip_sender, latest_chain_tip, chain_tip_change) = ChainTipSender::new(None, network);
|
||||
let (mut chain_tip_sender, latest_chain_tip, chain_tip_change) =
|
||||
ChainTipSender::new(None, network);
|
||||
|
||||
let (mempool, _transaction_receiver) = Mempool::new(
|
||||
&Config {
|
||||
|
@ -262,6 +284,9 @@ fn setup(
|
|||
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,
|
||||
|
|
|
@ -44,7 +44,7 @@ async fn mempool_service_basic_single() -> Result<(), Report> {
|
|||
let network = Network::Mainnet;
|
||||
|
||||
// get the genesis block transactions from the Zcash blockchain.
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(..=10, network);
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(1..=10, network);
|
||||
let genesis_transaction = unmined_transactions
|
||||
.next()
|
||||
.expect("Missing genesis transaction");
|
||||
|
@ -56,7 +56,7 @@ async fn mempool_service_basic_single() -> Result<(), Report> {
|
|||
let cost_limit = more_transactions.iter().map(|tx| tx.cost()).sum();
|
||||
|
||||
let (mut service, _peer_set, _state_service, _chain_tip_change, _tx_verifier, mut recent_syncs) =
|
||||
setup(network, cost_limit).await;
|
||||
setup(network, cost_limit, true).await;
|
||||
|
||||
// Enable the mempool
|
||||
service.enable(&mut recent_syncs).await;
|
||||
|
@ -187,7 +187,7 @@ async fn mempool_queue_single() -> Result<(), Report> {
|
|||
let network = Network::Mainnet;
|
||||
|
||||
// Get transactions to use in the test
|
||||
let unmined_transactions = unmined_transactions_in_blocks(..=10, network);
|
||||
let unmined_transactions = unmined_transactions_in_blocks(1..=10, network);
|
||||
let mut transactions = unmined_transactions.collect::<Vec<_>>();
|
||||
// Split unmined_transactions into:
|
||||
// [transactions..., new_tx]
|
||||
|
@ -203,7 +203,7 @@ async fn mempool_queue_single() -> Result<(), Report> {
|
|||
.sum();
|
||||
|
||||
let (mut service, _peer_set, _state_service, _chain_tip_change, _tx_verifier, mut recent_syncs) =
|
||||
setup(network, cost_limit).await;
|
||||
setup(network, cost_limit, true).await;
|
||||
|
||||
// Enable the mempool
|
||||
service.enable(&mut recent_syncs).await;
|
||||
|
@ -277,10 +277,10 @@ async fn mempool_service_disabled() -> Result<(), Report> {
|
|||
let network = Network::Mainnet;
|
||||
|
||||
let (mut service, _peer_set, _state_service, _chain_tip_change, _tx_verifier, mut recent_syncs) =
|
||||
setup(network, u64::MAX).await;
|
||||
setup(network, u64::MAX, true).await;
|
||||
|
||||
// get the genesis block transactions from the Zcash blockchain.
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(..=10, network);
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(1..=10, network);
|
||||
let genesis_transaction = unmined_transactions
|
||||
.next()
|
||||
.expect("Missing genesis transaction");
|
||||
|
@ -398,41 +398,12 @@ async fn mempool_cancel_mined() -> Result<(), Report> {
|
|||
mut chain_tip_change,
|
||||
_tx_verifier,
|
||||
mut recent_syncs,
|
||||
) = setup(network, u64::MAX).await;
|
||||
) = setup(network, u64::MAX, true).await;
|
||||
|
||||
// Enable the mempool
|
||||
mempool.enable(&mut recent_syncs).await;
|
||||
assert!(mempool.is_enabled());
|
||||
|
||||
// Push the genesis block to the state
|
||||
let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into()
|
||||
.unwrap();
|
||||
state_service
|
||||
.ready()
|
||||
.await
|
||||
.unwrap()
|
||||
.call(zebra_state::Request::CommitFinalizedBlock(
|
||||
genesis_block.clone().into(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the chain tip update
|
||||
if let Err(timeout_error) = timeout(
|
||||
CHAIN_TIP_UPDATE_WAIT_LIMIT,
|
||||
chain_tip_change.wait_for_tip_change(),
|
||||
)
|
||||
.await
|
||||
.map(|change_result| change_result.expect("unexpected chain tip update failure"))
|
||||
{
|
||||
info!(
|
||||
timeout = ?humantime_seconds(CHAIN_TIP_UPDATE_WAIT_LIMIT),
|
||||
?timeout_error,
|
||||
"timeout waiting for chain tip change after committing block"
|
||||
);
|
||||
}
|
||||
|
||||
// Query the mempool to make it poll chain_tip_change
|
||||
mempool.dummy_call().await;
|
||||
|
||||
|
@ -542,41 +513,12 @@ async fn mempool_cancel_downloads_after_network_upgrade() -> Result<(), Report>
|
|||
mut chain_tip_change,
|
||||
_tx_verifier,
|
||||
mut recent_syncs,
|
||||
) = setup(network, u64::MAX).await;
|
||||
) = setup(network, u64::MAX, true).await;
|
||||
|
||||
// Enable the mempool
|
||||
mempool.enable(&mut recent_syncs).await;
|
||||
assert!(mempool.is_enabled());
|
||||
|
||||
// Push the genesis block to the state
|
||||
let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into()
|
||||
.unwrap();
|
||||
state_service
|
||||
.ready()
|
||||
.await
|
||||
.unwrap()
|
||||
.call(zebra_state::Request::CommitFinalizedBlock(
|
||||
genesis_block.clone().into(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the chain tip update
|
||||
if let Err(timeout_error) = timeout(
|
||||
CHAIN_TIP_UPDATE_WAIT_LIMIT,
|
||||
chain_tip_change.wait_for_tip_change(),
|
||||
)
|
||||
.await
|
||||
.map(|change_result| change_result.expect("unexpected chain tip update failure"))
|
||||
{
|
||||
info!(
|
||||
timeout = ?humantime_seconds(CHAIN_TIP_UPDATE_WAIT_LIMIT),
|
||||
?timeout_error,
|
||||
"timeout waiting for chain tip change after committing block"
|
||||
);
|
||||
}
|
||||
|
||||
// Queue transaction from block 2 for download
|
||||
let txid = block2.transactions[0].unmined_id();
|
||||
let response = mempool
|
||||
|
@ -658,7 +600,7 @@ async fn mempool_failed_verification_is_rejected() -> Result<(), Report> {
|
|||
_chain_tip_change,
|
||||
mut tx_verifier,
|
||||
mut recent_syncs,
|
||||
) = setup(network, u64::MAX).await;
|
||||
) = setup(network, u64::MAX, true).await;
|
||||
|
||||
// Get transactions to use in the test
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(1..=2, network);
|
||||
|
@ -733,7 +675,7 @@ async fn mempool_failed_download_is_not_rejected() -> Result<(), Report> {
|
|||
_chain_tip_change,
|
||||
_tx_verifier,
|
||||
mut recent_syncs,
|
||||
) = setup(network, u64::MAX).await;
|
||||
) = setup(network, u64::MAX, true).await;
|
||||
|
||||
// Get transactions to use in the test
|
||||
let mut unmined_transactions = unmined_transactions_in_blocks(1..=2, network);
|
||||
|
@ -801,9 +743,6 @@ async fn mempool_failed_download_is_not_rejected() -> Result<(), Report> {
|
|||
async fn mempool_reverifies_after_tip_change() -> Result<(), Report> {
|
||||
let network = Network::Mainnet;
|
||||
|
||||
let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into()
|
||||
.unwrap();
|
||||
let block1: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_1_BYTES
|
||||
.zcash_deserialize_into()
|
||||
.unwrap();
|
||||
|
@ -821,31 +760,12 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> {
|
|||
mut chain_tip_change,
|
||||
mut tx_verifier,
|
||||
mut recent_syncs,
|
||||
) = setup(network, u64::MAX).await;
|
||||
) = setup(network, u64::MAX, true).await;
|
||||
|
||||
// Enable the mempool
|
||||
mempool.enable(&mut recent_syncs).await;
|
||||
assert!(mempool.is_enabled());
|
||||
|
||||
// Push the genesis block to the state
|
||||
state_service
|
||||
.ready()
|
||||
.await
|
||||
.unwrap()
|
||||
.call(zebra_state::Request::CommitFinalizedBlock(
|
||||
genesis_block.clone().into(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the chain tip update without a timeout
|
||||
// (skipping the chain tip change here may cause the test to
|
||||
// pass without reverifying transactions for `TipAction::Grow`)
|
||||
chain_tip_change
|
||||
.wait_for_tip_change()
|
||||
.await
|
||||
.expect("unexpected chain tip update failure");
|
||||
|
||||
// Queue transaction from block 3 for download
|
||||
let tx = block3.transactions[0].clone();
|
||||
let txid = block3.transactions[0].unmined_id();
|
||||
|
@ -985,6 +905,7 @@ async fn mempool_reverifies_after_tip_change() -> Result<(), Report> {
|
|||
async fn setup(
|
||||
network: Network,
|
||||
tx_cost_limit: u64,
|
||||
should_commit_genesis_block: bool,
|
||||
) -> (
|
||||
Mempool,
|
||||
MockPeerSet,
|
||||
|
@ -997,9 +918,9 @@ async fn setup(
|
|||
|
||||
// UTXO verification doesn't matter here.
|
||||
let state_config = StateConfig::ephemeral();
|
||||
let (state, _read_only_state_service, latest_chain_tip, chain_tip_change) =
|
||||
let (state, _read_only_state_service, latest_chain_tip, mut chain_tip_change) =
|
||||
zebra_state::init(state_config, network, Height::MAX, 0);
|
||||
let state_service = ServiceBuilder::new().buffer(1).service(state);
|
||||
let mut state_service = ServiceBuilder::new().buffer(1).service(state);
|
||||
|
||||
let tx_verifier = MockService::build().for_unit_tests();
|
||||
|
||||
|
@ -1018,6 +939,29 @@ async fn setup(
|
|||
chain_tip_change.clone(),
|
||||
);
|
||||
|
||||
if should_commit_genesis_block {
|
||||
let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||
.zcash_deserialize_into()
|
||||
.unwrap();
|
||||
|
||||
// Push the genesis block to the state
|
||||
state_service
|
||||
.ready()
|
||||
.await
|
||||
.unwrap()
|
||||
.call(zebra_state::Request::CommitFinalizedBlock(
|
||||
genesis_block.clone().into(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for the chain tip update without a timeout
|
||||
chain_tip_change
|
||||
.wait_for_tip_change()
|
||||
.await
|
||||
.expect("unexpected chain tip update failure");
|
||||
}
|
||||
|
||||
(
|
||||
mempool,
|
||||
peer_set,
|
||||
|
|
Loading…
Reference in New Issue