From d7842bd46731488b3408a7282468cc1752f870c5 Mon Sep 17 00:00:00 2001 From: Arya Date: Mon, 3 Apr 2023 19:22:07 -0400 Subject: [PATCH] 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 * adds allow[unused_variable) for linter * expects a chain tip when not(test) * Apply suggestions from code review Co-authored-by: teor * 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 * fixes test --------- Co-authored-by: teor --- zebra-node-services/src/mempool.rs | 8 +- zebra-rpc/src/methods.rs | 5 +- .../src/methods/get_block_template_rpcs.rs | 20 ++- .../get_block_template.rs | 29 +++- zebra-rpc/src/methods/tests/prop.rs | 5 +- zebra-rpc/src/methods/tests/snapshot.rs | 5 +- .../tests/snapshot/get_block_template_rpcs.rs | 6 +- zebra-rpc/src/methods/tests/vectors.rs | 93 ++++++++++--- zebra-state/src/service/chain_tip.rs | 9 ++ zebrad/src/components/mempool.rs | 109 +++++++++------ zebrad/src/components/mempool/tests.rs | 2 + zebrad/src/components/mempool/tests/prop.rs | 33 ++++- zebrad/src/components/mempool/tests/vector.rs | 130 +++++------------- 13 files changed, 283 insertions(+), 171 deletions(-) diff --git a/zebra-node-services/src/mempool.rs b/zebra-node-services/src/mempool.rs index f4c77ddd9..98c1969bb 100644 --- a/zebra-node-services/src/mempool.rs +++ b/zebra-node-services/src/mempool.rs @@ -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), + FullTransactions { + /// All [`VerifiedUnminedTx`]s in the mempool + transactions: Vec, + + /// 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), diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 21f8ee607..f76847a1c 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -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 diff --git a/zebra-rpc/src/methods/get_block_template_rpcs.rs b/zebra-rpc/src/methods/get_block_template_rpcs.rs index 87539cde9..34d8cb2f5 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs.rs @@ -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( diff --git a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs index f7ba6adfd..8439808fe 100644 --- a/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs +++ b/zebra-rpc/src/methods/get_block_template_rpcs/get_block_template.rs @@ -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( +pub async fn validate_block_proposal( mut chain_verifier: ChainVerifier, block_proposal_bytes: Vec, + network: Network, + latest_chain_tip: Tip, + sync_status: SyncStatus, ) -> Result where ChainVerifier: Service @@ -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) -> Result> +pub async fn fetch_mempool_transactions( + mempool: Mempool, + chain_tip_hash: block::Hash, +) -> Result>> 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 diff --git a/zebra-rpc/src/methods/tests/prop.rs b/zebra-rpc/src/methods/tests/prop.rs index 8f780af08..9d2e6610b 100644 --- a/zebra-rpc/src/methods/tests/prop.rs +++ b/zebra-rpc/src/methods/tests/prop.rs @@ -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 }; diff --git a/zebra-rpc/src/methods/tests/snapshot.rs b/zebra-rpc/src/methods/tests/snapshot.rs index d02bac10e..21a8c584a 100644 --- a/zebra-rpc/src/methods/tests/snapshot.rs +++ b/zebra-rpc/src/methods/tests/snapshot.rs @@ -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"))] diff --git a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs index 48fcfd697..9ae6d70f4 100644 --- a/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs +++ b/zebra-rpc/src/methods/tests/snapshot/get_block_template_rpcs.rs @@ -219,7 +219,11 @@ pub async fn test_responses( 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, + }); } }; diff --git a/zebra-rpc/src/methods/tests/vectors.rs b/zebra-rpc/src/methods/tests/vectors.rs index a3b137401..c2f2c0c25 100644 --- a/zebra-rpc/src/methods/tests/vectors.rs +++ b/zebra-rpc/src/methods/tests/vectors.rs @@ -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::::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")] diff --git a/zebra-state/src/service/chain_tip.rs b/zebra-state/src/service/chain_tip.rs index 2dd53b905..80675609a 100644 --- a/zebra-state/src/service/chain_tip.rs +++ b/zebra-state/src/service/chain_tip.rs @@ -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 } diff --git a/zebrad/src/components/mempool.rs b/zebrad/src/components/mempool.rs index 0d1b7d4d3..f7d8853fc 100644 --- a/zebrad/src/components/mempool.rs +++ b/zebrad/src/components/mempool.rs @@ -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>, + + /// 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 for Mempool { Pin> + Send + 'static>>; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - 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 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 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 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 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 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 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 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 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 for Mempool { Response::CheckedForVerifiedTransactions } }; + async move { Ok(resp) }.boxed() } } diff --git a/zebrad/src/components/mempool/tests.rs b/zebrad/src/components/mempool/tests.rs index 5eb6b6f68..60a210910 100644 --- a/zebrad/src/components/mempool/tests.rs +++ b/zebrad/src/components/mempool/tests.rs @@ -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); diff --git a/zebrad/src/components/mempool/tests/prop.rs b/zebrad/src/components/mempool/tests/prop.rs index 562747a3f..41523013a 100644 --- a/zebrad/src/components/mempool/tests/prop.rs +++ b/zebrad/src/components/mempool/tests/prop.rs @@ -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 { + zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES + .zcash_deserialize_into::>() + .map(FinalizedBlock::from) + .map(ChainTipBlock::from) + .ok() +} + +fn block1_chain_tip() -> Option { + zebra_test::vectors::BLOCK_MAINNET_1_BYTES + .zcash_deserialize_into::>() + .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, diff --git a/zebrad/src/components/mempool/tests/vector.rs b/zebrad/src/components/mempool/tests/vector.rs index aa4b8b363..3dad0f1a4 100644 --- a/zebrad/src/components/mempool/tests/vector.rs +++ b/zebrad/src/components/mempool/tests/vector.rs @@ -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::>(); // 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 = 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 = 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 = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES - .zcash_deserialize_into() - .unwrap(); let block1: Arc = 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 = 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,