Cancel download and verify tasks when the mempool is deactivated (#2764)
* Cancel download and verify tasks when the mempool is deactivated * Refactor enable/disable logic to use a state enum * Add helper test functions to enable/disable the mempool * Add documentation about errors on service calls * Improvements from review * Improve documentation * Fix bug in test * Apply suggestions from code review Co-authored-by: teor <teor@riseup.net> Co-authored-by: teor <teor@riseup.net>
This commit is contained in:
parent
1601c9fbb3
commit
c6878d9b63
|
@ -237,6 +237,10 @@ impl Service<zn::Request> for Inbound {
|
||||||
Poll::Ready(result)
|
Poll::Ready(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call the inbound service.
|
||||||
|
///
|
||||||
|
/// Errors indicate that the peer has done something wrong or unexpected,
|
||||||
|
/// and will cause callers to disconnect from the remote peer.
|
||||||
#[instrument(name = "inbound", skip(self, req))]
|
#[instrument(name = "inbound", skip(self, req))]
|
||||||
fn call(&mut self, req: zn::Request) -> Self::Future {
|
fn call(&mut self, req: zn::Request) -> Self::Future {
|
||||||
match req {
|
match req {
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crate::components::sync::SyncStatus;
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tower::{
|
use tower::{
|
||||||
buffer::Buffer, builder::ServiceBuilder, load_shed::LoadShed, util::BoxService, ServiceExt,
|
buffer::Buffer, builder::ServiceBuilder, load_shed::LoadShed, util::BoxService, Service,
|
||||||
|
ServiceExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tracing::Span;
|
use tracing::Span;
|
||||||
|
@ -34,15 +35,16 @@ async fn mempool_requests_for_transactions() {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Test `Request::MempoolTransactionIds`
|
// Test `Request::MempoolTransactionIds`
|
||||||
let request = inbound_service
|
let response = inbound_service
|
||||||
.clone()
|
.clone()
|
||||||
.oneshot(Request::MempoolTransactionIds)
|
.oneshot(Request::MempoolTransactionIds)
|
||||||
.await;
|
.await;
|
||||||
match request {
|
match response {
|
||||||
Ok(Response::TransactionIds(response)) => assert_eq!(response, added_transaction_ids),
|
Ok(Response::TransactionIds(response)) => assert_eq!(response, added_transaction_ids),
|
||||||
_ => unreachable!(
|
_ => unreachable!(format!(
|
||||||
"`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId>)`"
|
"`MempoolTransactionIds` requests should always respond `Ok(Vec<UnminedTxId>)`, got {:?}",
|
||||||
),
|
response
|
||||||
|
)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test `Request::TransactionsById`
|
// Test `Request::TransactionsById`
|
||||||
|
@ -51,11 +53,11 @@ async fn mempool_requests_for_transactions() {
|
||||||
.copied()
|
.copied()
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
let request = inbound_service
|
let response = inbound_service
|
||||||
.oneshot(Request::TransactionsById(hash_set))
|
.oneshot(Request::TransactionsById(hash_set))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match request {
|
match response {
|
||||||
Ok(Response::Transactions(response)) => assert_eq!(response, added_transactions.unwrap()),
|
Ok(Response::Transactions(response)) => assert_eq!(response, added_transactions.unwrap()),
|
||||||
_ => unreachable!("`TransactionsById` requests should always respond `Ok(Vec<UnminedTx>)`"),
|
_ => unreachable!("`TransactionsById` requests should always respond `Ok(Vec<UnminedTx>)`"),
|
||||||
};
|
};
|
||||||
|
@ -184,12 +186,11 @@ async fn setup(
|
||||||
let state_config = StateConfig::ephemeral();
|
let state_config = StateConfig::ephemeral();
|
||||||
let address_book = AddressBook::new(SocketAddr::from_str("0.0.0.0:0").unwrap(), Span::none());
|
let address_book = AddressBook::new(SocketAddr::from_str("0.0.0.0:0").unwrap(), Span::none());
|
||||||
let address_book = Arc::new(std::sync::Mutex::new(address_book));
|
let address_book = Arc::new(std::sync::Mutex::new(address_book));
|
||||||
let (sync_status, _recent_syncs) = SyncStatus::new();
|
let (sync_status, mut recent_syncs) = SyncStatus::new();
|
||||||
let (_state_service, _latest_chain_tip, chain_tip_change) =
|
let (state, _latest_chain_tip, chain_tip_change) =
|
||||||
zebra_state::init(state_config.clone(), network);
|
zebra_state::init(state_config.clone(), network);
|
||||||
|
|
||||||
let (state, _, _) = zebra_state::init(state_config, network);
|
let mut state_service = ServiceBuilder::new().buffer(1).service(state);
|
||||||
let state_service = ServiceBuilder::new().buffer(1).service(state);
|
|
||||||
|
|
||||||
let (block_verifier, _transaction_verifier) =
|
let (block_verifier, _transaction_verifier) =
|
||||||
zebra_consensus::chain::init(consensus_config.clone(), network, state_service.clone())
|
zebra_consensus::chain::init(consensus_config.clone(), network, state_service.clone())
|
||||||
|
@ -201,6 +202,22 @@ async fn setup(
|
||||||
let mock_tx_verifier = MockService::build().for_unit_tests();
|
let mock_tx_verifier = MockService::build().for_unit_tests();
|
||||||
let buffered_tx_verifier = Buffer::new(BoxService::new(mock_tx_verifier.clone()), 10);
|
let buffered_tx_verifier = Buffer::new(BoxService::new(mock_tx_verifier.clone()), 10);
|
||||||
|
|
||||||
|
// Push the genesis block to the state.
|
||||||
|
// This must be done before creating the mempool to avoid `chain_tip_change`
|
||||||
|
// returning "reset" which would clear the mempool.
|
||||||
|
let genesis_block: Arc<Block> = zebra_test::vectors::BLOCK_MAINNET_GENESIS_BYTES
|
||||||
|
.zcash_deserialize_into()
|
||||||
|
.unwrap();
|
||||||
|
state_service
|
||||||
|
.ready_and()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.call(zebra_state::Request::CommitFinalizedBlock(
|
||||||
|
genesis_block.clone().into(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut mempool_service = Mempool::new(
|
let mut mempool_service = Mempool::new(
|
||||||
network,
|
network,
|
||||||
buffered_peer_set.clone(),
|
buffered_peer_set.clone(),
|
||||||
|
@ -210,6 +227,9 @@ async fn setup(
|
||||||
chain_tip_change,
|
chain_tip_change,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enable the mempool
|
||||||
|
let _ = mempool_service.enable(&mut recent_syncs).await;
|
||||||
|
|
||||||
let mut added_transactions = None;
|
let mut added_transactions = None;
|
||||||
if add_transactions {
|
if add_transactions {
|
||||||
added_transactions = Some(add_some_stuff_to_mempool(&mut mempool_service, network));
|
added_transactions = Some(add_some_stuff_to_mempool(&mut mempool_service, network));
|
||||||
|
@ -233,17 +253,6 @@ async fn setup(
|
||||||
// We can't expect or unwrap because the returned Result does not implement Debug
|
// We can't expect or unwrap because the returned Result does not implement Debug
|
||||||
assert!(r.is_ok());
|
assert!(r.is_ok());
|
||||||
|
|
||||||
// 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
|
|
||||||
.oneshot(zebra_state::Request::CommitFinalizedBlock(
|
|
||||||
genesis_block.clone().into(),
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
(
|
(
|
||||||
inbound_service,
|
inbound_service,
|
||||||
added_transactions,
|
added_transactions,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
iter,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
@ -38,6 +39,8 @@ use self::downloads::{
|
||||||
Downloads as TxDownloads, Gossip, TRANSACTION_DOWNLOAD_TIMEOUT, TRANSACTION_VERIFY_TIMEOUT,
|
Downloads as TxDownloads, Gossip, TRANSACTION_DOWNLOAD_TIMEOUT, TRANSACTION_VERIFY_TIMEOUT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
use super::sync::RecentSyncLengths;
|
||||||
use super::sync::SyncStatus;
|
use super::sync::SyncStatus;
|
||||||
|
|
||||||
type Outbound = Buffer<BoxService<zn::Request, zn::Response, zn::BoxError>, zn::Request>;
|
type Outbound = Buffer<BoxService<zn::Request, zn::Response, zn::BoxError>, zn::Request>;
|
||||||
|
@ -65,20 +68,33 @@ pub enum Response {
|
||||||
Queued(Vec<Result<(), MempoolError>>),
|
Queued(Vec<Result<(), MempoolError>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The state of the mempool.
|
||||||
|
///
|
||||||
|
/// Indicates wether it is enabled or disabled and, if enabled, contains
|
||||||
|
/// the necessary data to run it.
|
||||||
|
enum ActiveState {
|
||||||
|
/// The Mempool is disabled.
|
||||||
|
Disabled,
|
||||||
|
/// The Mempool is enabled.
|
||||||
|
Enabled {
|
||||||
|
/// The Mempool storage itself.
|
||||||
|
///
|
||||||
|
/// ##: Correctness: only components internal to the [`Mempool`] struct are allowed to
|
||||||
|
/// inject transactions into `storage`, as transactions must be verified beforehand.
|
||||||
|
storage: storage::Storage,
|
||||||
|
/// The transaction dowload and verify stream.
|
||||||
|
tx_downloads: Pin<Box<InboundTxDownloads>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
/// Mempool async management and query service.
|
/// Mempool async management and query service.
|
||||||
///
|
///
|
||||||
/// The mempool is the set of all verified transactions that this node is aware
|
/// The mempool is the set of all verified transactions that this node is aware
|
||||||
/// of that have yet to be confirmed by the Zcash network. A transaction is
|
/// of that have yet to be confirmed by the Zcash network. A transaction is
|
||||||
/// confirmed when it has been included in a block ('mined').
|
/// confirmed when it has been included in a block ('mined').
|
||||||
pub struct Mempool {
|
pub struct Mempool {
|
||||||
/// The Mempool storage itself.
|
/// The state of the mempool.
|
||||||
///
|
active_state: ActiveState,
|
||||||
/// ##: Correctness: only components internal to the [`Mempool`] struct are allowed to
|
|
||||||
/// inject transactions into `storage`, as transactions must be verified beforehand.
|
|
||||||
storage: storage::Storage,
|
|
||||||
|
|
||||||
/// The transaction dowload and verify stream.
|
|
||||||
tx_downloads: Pin<Box<InboundTxDownloads>>,
|
|
||||||
|
|
||||||
/// Allows checking if we are near the tip to enable/disable the mempool.
|
/// Allows checking if we are near the tip to enable/disable the mempool.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -87,6 +103,18 @@ pub struct Mempool {
|
||||||
/// Allows the detection of chain tip resets.
|
/// Allows the detection of chain tip resets.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
chain_tip_change: ChainTipChange,
|
chain_tip_change: ChainTipChange,
|
||||||
|
|
||||||
|
/// Handle to the outbound service.
|
||||||
|
/// Used to construct the transaction downloader.
|
||||||
|
outbound: Outbound,
|
||||||
|
|
||||||
|
/// Handle to the state service.
|
||||||
|
/// Used to construct the transaction downloader.
|
||||||
|
state: State,
|
||||||
|
|
||||||
|
/// Handle to the transaction verifier service.
|
||||||
|
/// Used to construct the transaction downloader.
|
||||||
|
tx_verifier: TxVerifier,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mempool {
|
impl Mempool {
|
||||||
|
@ -99,36 +127,100 @@ impl Mempool {
|
||||||
sync_status: SyncStatus,
|
sync_status: SyncStatus,
|
||||||
chain_tip_change: ChainTipChange,
|
chain_tip_change: ChainTipChange,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let tx_downloads = Box::pin(TxDownloads::new(
|
|
||||||
Timeout::new(outbound, TRANSACTION_DOWNLOAD_TIMEOUT),
|
|
||||||
Timeout::new(tx_verifier, TRANSACTION_VERIFY_TIMEOUT),
|
|
||||||
state,
|
|
||||||
));
|
|
||||||
|
|
||||||
Mempool {
|
Mempool {
|
||||||
storage: Default::default(),
|
active_state: ActiveState::Disabled,
|
||||||
tx_downloads,
|
|
||||||
sync_status,
|
sync_status,
|
||||||
chain_tip_change,
|
chain_tip_change,
|
||||||
|
outbound,
|
||||||
|
state,
|
||||||
|
tx_verifier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the mempool state (enabled / disabled) depending on how close to
|
||||||
|
/// the tip is the synchronization, including side effects to state changes.
|
||||||
|
fn update_state(&mut self) {
|
||||||
|
let is_close_to_tip = self.sync_status.is_close_to_tip();
|
||||||
|
if self.is_enabled() == is_close_to_tip {
|
||||||
|
// the active state is up to date
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update enabled / disabled state
|
||||||
|
if is_close_to_tip {
|
||||||
|
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: Default::default(),
|
||||||
|
tx_downloads,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.active_state = ActiveState::Disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return whether the mempool is enabled or not.
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
match self.active_state {
|
||||||
|
ActiveState::Disabled => false,
|
||||||
|
ActiveState::Enabled { .. } => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the storage field of the mempool for testing purposes.
|
/// Get the storage field of the mempool for testing purposes.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn storage(&mut self) -> &mut storage::Storage {
|
pub fn storage(&mut self) -> &mut storage::Storage {
|
||||||
&mut self.storage
|
match &mut self.active_state {
|
||||||
|
ActiveState::Disabled => panic!("mempool must be enabled"),
|
||||||
|
ActiveState::Enabled { storage, .. } => storage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the transaction downloader of the mempool for testing purposes.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn tx_downloads(&self) -> &Pin<Box<InboundTxDownloads>> {
|
||||||
|
match &self.active_state {
|
||||||
|
ActiveState::Disabled => panic!("mempool must be enabled"),
|
||||||
|
ActiveState::Enabled { tx_downloads, .. } => tx_downloads,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable the mempool by pretending the synchronization is close to the tip.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub async fn enable(&mut self, recent_syncs: &mut RecentSyncLengths) {
|
||||||
|
use tower::ServiceExt;
|
||||||
|
// Pretend we're close to tip
|
||||||
|
SyncStatus::sync_close_to_tip(recent_syncs);
|
||||||
|
// Make a dummy request to poll the mempool and make it enable itself
|
||||||
|
let _ = self.oneshot(Request::TransactionIds).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disable the mempool by pretending the synchronization is far from the tip.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub async fn disable(&mut self, recent_syncs: &mut RecentSyncLengths) {
|
||||||
|
use tower::ServiceExt;
|
||||||
|
// Pretend we're far from the tip
|
||||||
|
SyncStatus::sync_far_from_tip(recent_syncs);
|
||||||
|
// Make a dummy request to poll the mempool and make it disable itself
|
||||||
|
let _ = self.oneshot(Request::TransactionIds).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if transaction should be downloaded and/or verified.
|
/// Check if transaction should be downloaded and/or verified.
|
||||||
///
|
///
|
||||||
/// If it is already in the mempool (or in its rejected list)
|
/// If it is already in the mempool (or in its rejected list)
|
||||||
/// then it shouldn't be downloaded/verified.
|
/// then it shouldn't be downloaded/verified.
|
||||||
fn should_download_or_verify(&mut self, txid: UnminedTxId) -> Result<(), MempoolError> {
|
fn should_download_or_verify(
|
||||||
|
storage: &mut storage::Storage,
|
||||||
|
txid: UnminedTxId,
|
||||||
|
) -> Result<(), MempoolError> {
|
||||||
// Check if the transaction is already in the mempool.
|
// Check if the transaction is already in the mempool.
|
||||||
if self.storage.contains(&txid) {
|
if storage.contains(&txid) {
|
||||||
return Err(MempoolError::InMempool);
|
return Err(MempoolError::InMempool);
|
||||||
}
|
}
|
||||||
if self.storage.contains_rejected(&txid) {
|
if storage.contains_rejected(&txid) {
|
||||||
return Err(MempoolError::Rejected);
|
return Err(MempoolError::Rejected);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -142,48 +234,90 @@ impl Service<Request> for Mempool {
|
||||||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
|
||||||
|
|
||||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
// Clear the mempool if there has been a chain tip reset.
|
self.update_state();
|
||||||
if let Some(TipAction::Reset { .. }) = self.chain_tip_change.last_tip_change() {
|
|
||||||
self.storage.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up completed download tasks and add to mempool if successful
|
match &mut self.active_state {
|
||||||
while let Poll::Ready(Some(r)) = self.tx_downloads.as_mut().poll_next(cx) {
|
ActiveState::Enabled {
|
||||||
if let Ok(tx) = r {
|
storage,
|
||||||
// TODO: should we do something with the result?
|
tx_downloads,
|
||||||
let _ = self.storage.insert(tx);
|
} => {
|
||||||
|
// Clear the mempool if there has been a chain tip reset.
|
||||||
|
if let Some(TipAction::Reset { .. }) = self.chain_tip_change.last_tip_change() {
|
||||||
|
storage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up completed download tasks and add to mempool if successful
|
||||||
|
while let Poll::Ready(Some(r)) = tx_downloads.as_mut().poll_next(cx) {
|
||||||
|
if let Ok(tx) = r {
|
||||||
|
// Storage handles conflicting transactions or a full mempool internally,
|
||||||
|
// so just ignore the storage result here
|
||||||
|
let _ = storage.insert(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActiveState::Disabled => {
|
||||||
|
// When the mempool is disabled we still return that the service is ready.
|
||||||
|
// Otherwise, callers could block waiting for the mempool to be enabled,
|
||||||
|
// which may not be the desired behaviour.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Ready(Ok(()))
|
Poll::Ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Call the mempool service.
|
||||||
|
///
|
||||||
|
/// Errors indicate that the peer has done something wrong or unexpected,
|
||||||
|
/// and will cause callers to disconnect from the remote peer.
|
||||||
#[instrument(name = "mempool", skip(self, req))]
|
#[instrument(name = "mempool", skip(self, req))]
|
||||||
fn call(&mut self, req: Request) -> Self::Future {
|
fn call(&mut self, req: Request) -> Self::Future {
|
||||||
match req {
|
match &mut self.active_state {
|
||||||
Request::TransactionIds => {
|
ActiveState::Enabled {
|
||||||
let res = self.storage.tx_ids();
|
storage,
|
||||||
async move { Ok(Response::TransactionIds(res)) }.boxed()
|
tx_downloads,
|
||||||
}
|
} => match req {
|
||||||
Request::TransactionsById(ids) => {
|
Request::TransactionIds => {
|
||||||
let rsp = Ok(self.storage.transactions(ids)).map(Response::Transactions);
|
let res = storage.tx_ids();
|
||||||
async move { rsp }.boxed()
|
async move { Ok(Response::TransactionIds(res)) }.boxed()
|
||||||
}
|
}
|
||||||
Request::RejectedTransactionIds(ids) => {
|
Request::TransactionsById(ids) => {
|
||||||
let rsp = Ok(self.storage.rejected_transactions(ids))
|
let rsp = Ok(storage.transactions(ids)).map(Response::Transactions);
|
||||||
.map(Response::RejectedTransactionIds);
|
async move { rsp }.boxed()
|
||||||
async move { rsp }.boxed()
|
}
|
||||||
}
|
Request::RejectedTransactionIds(ids) => {
|
||||||
Request::Queue(gossiped_txs) => {
|
let rsp = Ok(storage.rejected_transactions(ids))
|
||||||
let rsp: Vec<Result<(), MempoolError>> = gossiped_txs
|
.map(Response::RejectedTransactionIds);
|
||||||
.into_iter()
|
async move { rsp }.boxed()
|
||||||
.map(|gossiped_tx| {
|
}
|
||||||
self.should_download_or_verify(gossiped_tx.id())?;
|
Request::Queue(gossiped_txs) => {
|
||||||
self.tx_downloads
|
let rsp: Vec<Result<(), MempoolError>> = gossiped_txs
|
||||||
.download_if_needed_and_verify(gossiped_tx)?;
|
.into_iter()
|
||||||
Ok(())
|
.map(|gossiped_tx| {
|
||||||
})
|
Self::should_download_or_verify(storage, gossiped_tx.id())?;
|
||||||
.collect();
|
tx_downloads.download_if_needed_and_verify(gossiped_tx)?;
|
||||||
async move { Ok(Response::Queued(rsp)) }.boxed()
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
async move { Ok(Response::Queued(rsp)) }.boxed()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ActiveState::Disabled => {
|
||||||
|
// 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 {
|
||||||
|
Request::TransactionIds => Response::TransactionIds(Default::default()),
|
||||||
|
Request::TransactionsById(_) => Response::Transactions(Default::default()),
|
||||||
|
Request::RejectedTransactionIds(_) => {
|
||||||
|
Response::RejectedTransactionIds(Default::default())
|
||||||
|
}
|
||||||
|
// Special case; we can signal the error inside the response.
|
||||||
|
Request::Queue(gossiped_txs) => Response::Queued(
|
||||||
|
iter::repeat(Err(MempoolError::Disabled))
|
||||||
|
.take(gossiped_txs.len())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
async move { Ok(resp) }.boxed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -315,6 +315,13 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the number of currently in-flight download tasks.
|
||||||
|
// Note: copied from zebrad/src/components/sync/downloads.rs
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn in_flight(&self) -> usize {
|
||||||
|
self.pending.len()
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if transaction is already in the state.
|
/// Check if transaction is already in the state.
|
||||||
async fn transaction_in_state(state: &mut ZS, txid: UnminedTxId) -> Result<(), BoxError> {
|
async fn transaction_in_state(state: &mut ZS, txid: UnminedTxId) -> Result<(), BoxError> {
|
||||||
// Check if the transaction is already in the state.
|
// Check if the transaction is already in the state.
|
||||||
|
|
|
@ -50,4 +50,7 @@ pub enum MempoolError {
|
||||||
its inputs"
|
its inputs"
|
||||||
)]
|
)]
|
||||||
SpendConflict,
|
SpendConflict,
|
||||||
|
|
||||||
|
#[error("mempool is disabled since synchronization is behind the chain tip")]
|
||||||
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
let consensus_config = ConsensusConfig::default();
|
let consensus_config = ConsensusConfig::default();
|
||||||
let state_config = StateConfig::ephemeral();
|
let state_config = StateConfig::ephemeral();
|
||||||
let peer_set = MockService::build().for_unit_tests();
|
let peer_set = MockService::build().for_unit_tests();
|
||||||
let (sync_status, _recent_syncs) = SyncStatus::new();
|
let (sync_status, mut recent_syncs) = SyncStatus::new();
|
||||||
let (state, _latest_chain_tip, chain_tip_change) =
|
let (state, _latest_chain_tip, chain_tip_change) =
|
||||||
zebra_state::init(state_config.clone(), network);
|
zebra_state::init(state_config.clone(), network);
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
let genesis_transaction = unmined_transactions
|
let genesis_transaction = unmined_transactions
|
||||||
.next()
|
.next()
|
||||||
.expect("Missing genesis transaction");
|
.expect("Missing genesis transaction");
|
||||||
let mut more_transactions = unmined_transactions;
|
let txid = unmined_transactions.next_back().unwrap().id;
|
||||||
let last_transaction = more_transactions.next_back().unwrap();
|
let more_transactions = unmined_transactions;
|
||||||
|
|
||||||
// Start the mempool service
|
// Start the mempool service
|
||||||
let mut service = Mempool::new(
|
let mut service = Mempool::new(
|
||||||
|
@ -41,8 +41,12 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
sync_status,
|
sync_status,
|
||||||
chain_tip_change,
|
chain_tip_change,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enable the mempool
|
||||||
|
let _ = service.enable(&mut recent_syncs).await;
|
||||||
|
|
||||||
// Insert the genesis block coinbase transaction into the mempool storage.
|
// Insert the genesis block coinbase transaction into the mempool storage.
|
||||||
service.storage.insert(genesis_transaction.clone())?;
|
service.storage().insert(genesis_transaction.clone())?;
|
||||||
|
|
||||||
// Test `Request::TransactionIds`
|
// Test `Request::TransactionIds`
|
||||||
let response = service
|
let response = service
|
||||||
|
@ -84,7 +88,7 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
// This will cause the genesis transaction to be moved into rejected.
|
// This will cause the genesis transaction to be moved into rejected.
|
||||||
// Skip the last (will be used later)
|
// Skip the last (will be used later)
|
||||||
for tx in more_transactions {
|
for tx in more_transactions {
|
||||||
service.storage.insert(tx.clone())?;
|
service.storage().insert(tx.clone())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test `Request::RejectedTransactionIds`
|
// Test `Request::RejectedTransactionIds`
|
||||||
|
@ -110,7 +114,7 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
.ready_and()
|
.ready_and()
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.call(Request::Queue(vec![last_transaction.id.into()]))
|
.call(Request::Queue(vec![txid.into()]))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let queued_responses = match response {
|
let queued_responses = match response {
|
||||||
|
@ -119,6 +123,7 @@ async fn mempool_service_basic() -> Result<(), Report> {
|
||||||
};
|
};
|
||||||
assert_eq!(queued_responses.len(), 1);
|
assert_eq!(queued_responses.len(), 1);
|
||||||
assert!(queued_responses[0].is_ok());
|
assert!(queued_responses[0].is_ok());
|
||||||
|
assert_eq!(service.tx_downloads().in_flight(), 1);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -130,7 +135,7 @@ async fn mempool_queue() -> Result<(), Report> {
|
||||||
let consensus_config = ConsensusConfig::default();
|
let consensus_config = ConsensusConfig::default();
|
||||||
let state_config = StateConfig::ephemeral();
|
let state_config = StateConfig::ephemeral();
|
||||||
let peer_set = MockService::build().for_unit_tests();
|
let peer_set = MockService::build().for_unit_tests();
|
||||||
let (sync_status, _recent_syncs) = SyncStatus::new();
|
let (sync_status, mut recent_syncs) = SyncStatus::new();
|
||||||
let (state, _latest_chain_tip, chain_tip_change) =
|
let (state, _latest_chain_tip, chain_tip_change) =
|
||||||
zebra_state::init(state_config.clone(), network);
|
zebra_state::init(state_config.clone(), network);
|
||||||
|
|
||||||
|
@ -162,15 +167,19 @@ async fn mempool_queue() -> Result<(), Report> {
|
||||||
sync_status,
|
sync_status,
|
||||||
chain_tip_change,
|
chain_tip_change,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Enable the mempool
|
||||||
|
let _ = service.enable(&mut recent_syncs).await;
|
||||||
|
|
||||||
// Insert [rejected_tx, transactions..., stored_tx] into the mempool storage.
|
// Insert [rejected_tx, transactions..., stored_tx] into the mempool storage.
|
||||||
// Insert the genesis block coinbase transaction into the mempool storage.
|
// Insert the genesis block coinbase transaction into the mempool storage.
|
||||||
service.storage.insert(rejected_tx.clone())?;
|
service.storage().insert(rejected_tx.clone())?;
|
||||||
// Insert more transactions into the mempool storage.
|
// Insert more transactions into the mempool storage.
|
||||||
// This will cause the `rejected_tx` to be moved into rejected.
|
// This will cause the `rejected_tx` to be moved into rejected.
|
||||||
for tx in transactions {
|
for tx in transactions {
|
||||||
service.storage.insert(tx.clone())?;
|
service.storage().insert(tx.clone())?;
|
||||||
}
|
}
|
||||||
service.storage.insert(stored_tx.clone())?;
|
service.storage().insert(stored_tx.clone())?;
|
||||||
|
|
||||||
// Test `Request::Queue` for a new transaction
|
// Test `Request::Queue` for a new transaction
|
||||||
let response = service
|
let response = service
|
||||||
|
@ -219,3 +228,120 @@ async fn mempool_queue() -> Result<(), Report> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mempool_service_disabled() -> Result<(), Report> {
|
||||||
|
// Using the mainnet for now
|
||||||
|
let network = Network::Mainnet;
|
||||||
|
let consensus_config = ConsensusConfig::default();
|
||||||
|
let state_config = StateConfig::ephemeral();
|
||||||
|
let peer_set = MockService::build().for_unit_tests();
|
||||||
|
let (sync_status, mut recent_syncs) = SyncStatus::new();
|
||||||
|
|
||||||
|
let (state, _latest_chain_tip, chain_tip_change) = zebra_state::init(state_config, network);
|
||||||
|
let state_service = ServiceBuilder::new().buffer(1).service(state);
|
||||||
|
let (_chain_verifier, tx_verifier) =
|
||||||
|
zebra_consensus::chain::init(consensus_config.clone(), network, state_service.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// get the genesis block transactions from the Zcash blockchain.
|
||||||
|
let mut unmined_transactions = unmined_transactions_in_blocks(..=10, network);
|
||||||
|
let genesis_transaction = unmined_transactions
|
||||||
|
.next()
|
||||||
|
.expect("Missing genesis transaction");
|
||||||
|
let more_transactions = unmined_transactions;
|
||||||
|
|
||||||
|
// Start the mempool service
|
||||||
|
let mut service = Mempool::new(
|
||||||
|
network,
|
||||||
|
Buffer::new(BoxService::new(peer_set), 1),
|
||||||
|
state_service.clone(),
|
||||||
|
tx_verifier,
|
||||||
|
sync_status,
|
||||||
|
chain_tip_change,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test if mempool is disabled (it should start disabled)
|
||||||
|
assert!(!service.is_enabled());
|
||||||
|
|
||||||
|
// Enable the mempool
|
||||||
|
let _ = service.enable(&mut recent_syncs).await;
|
||||||
|
|
||||||
|
assert!(service.is_enabled());
|
||||||
|
|
||||||
|
// Insert the genesis block coinbase transaction into the mempool storage.
|
||||||
|
service.storage().insert(genesis_transaction.clone())?;
|
||||||
|
|
||||||
|
// Test if the mempool answers correctly (i.e. is enabled)
|
||||||
|
let response = service
|
||||||
|
.ready_and()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.call(Request::TransactionIds)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let _genesis_transaction_ids = match response {
|
||||||
|
Response::TransactionIds(ids) => ids,
|
||||||
|
_ => unreachable!("will never happen in this test"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Queue a transaction for download
|
||||||
|
// Use the ID of the last transaction in the list
|
||||||
|
let txid = more_transactions.last().unwrap().id;
|
||||||
|
let response = service
|
||||||
|
.ready_and()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.call(Request::Queue(vec![txid.into()]))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let queued_responses = match response {
|
||||||
|
Response::Queued(queue_responses) => queue_responses,
|
||||||
|
_ => unreachable!("will never happen in this test"),
|
||||||
|
};
|
||||||
|
assert_eq!(queued_responses.len(), 1);
|
||||||
|
assert!(queued_responses[0].is_ok());
|
||||||
|
assert_eq!(service.tx_downloads().in_flight(), 1);
|
||||||
|
|
||||||
|
// Disable the mempool
|
||||||
|
let _ = service.disable(&mut recent_syncs).await;
|
||||||
|
|
||||||
|
// Test if mempool is disabled again
|
||||||
|
assert!(!service.is_enabled());
|
||||||
|
|
||||||
|
// Test if the mempool returns no transactions when disabled
|
||||||
|
let response = service
|
||||||
|
.ready_and()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.call(Request::TransactionIds)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
match response {
|
||||||
|
Response::TransactionIds(ids) => {
|
||||||
|
assert_eq!(
|
||||||
|
ids.len(),
|
||||||
|
0,
|
||||||
|
"mempool should return no transactions when disabled"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => unreachable!("will never happen in this test"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if the mempool returns to Queue requests correctly when disabled
|
||||||
|
let response = service
|
||||||
|
.ready_and()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.call(Request::Queue(vec![txid.into()]))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let queued_responses = match response {
|
||||||
|
Response::Queued(queue_responses) => queue_responses,
|
||||||
|
_ => unreachable!("will never happen in this test"),
|
||||||
|
};
|
||||||
|
assert_eq!(queued_responses.len(), 1);
|
||||||
|
assert_eq!(queued_responses[0], Err(MempoolError::Disabled));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ mod status;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use downloads::{AlwaysHedge, Downloads};
|
use downloads::{AlwaysHedge, Downloads};
|
||||||
use recent_sync_lengths::RecentSyncLengths;
|
pub use recent_sync_lengths::RecentSyncLengths;
|
||||||
pub use status::SyncStatus;
|
pub use status::SyncStatus;
|
||||||
|
|
||||||
/// Controls the number of peers used for each ObtainTips and ExtendTips request.
|
/// Controls the number of peers used for each ObtainTips and ExtendTips request.
|
||||||
|
|
|
@ -69,4 +69,22 @@ impl SyncStatus {
|
||||||
// average sync length falls below the threshold.
|
// average sync length falls below the threshold.
|
||||||
avg < Self::MIN_DIST_FROM_TIP
|
avg < Self::MIN_DIST_FROM_TIP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Feed the given [`RecentSyncLengths`] it order to make the matching
|
||||||
|
/// [`SyncStatus`] report that it's close to the tip.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn sync_close_to_tip(recent_syncs: &mut RecentSyncLengths) {
|
||||||
|
for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS {
|
||||||
|
recent_syncs.push_extend_tips_length(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feed the given [`RecentSyncLengths`] it order to make the matching
|
||||||
|
/// [`SyncStatus`] report that it's not close to the tip.
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn sync_far_from_tip(recent_syncs: &mut RecentSyncLengths) {
|
||||||
|
for _ in 0..RecentSyncLengths::MAX_RECENT_LENGTHS {
|
||||||
|
recent_syncs.push_extend_tips_length(Self::MIN_DIST_FROM_TIP * 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue