use std::{
collections::HashSet,
future::Future,
iter,
net::IpAddr,
pin::{pin, Pin},
task::{Context, Poll},
time::{Duration, Instant},
};
use futures::{future::FutureExt, stream::Stream};
use tokio::sync::{broadcast, mpsc, oneshot};
use tower::{buffer::Buffer, timeout::Timeout, util::BoxCloneService, Service, ServiceExt};
use zakura_chain::{
block::{self, Height},
chain_sync_status::ChainSyncStatus,
chain_tip::ChainTip,
parameters::{Network, NetworkUpgrade},
transaction::UnminedTxId,
};
use zakura_consensus::{error::TransactionError, transaction};
use zakura_network::{self as zn, PeerSocketAddr};
use zakura_node_services::mempool::{
CreatedOrSpent, Gossip, MempoolChange, MempoolDisabledError, MempoolTxSubscriber, QueueSource,
Request, Response,
};
use zakura_node_services::task::spawn_erased;
use zakura_state as zs;
use zakura_state::{ChainTipChange, TipAction};
use crate::components::sync::SyncStatus;
pub mod config;
mod crawler;
pub mod downloads;
mod error;
pub mod gossip;
mod peer_cooldown;
mod pending_outputs;
mod queue_checker;
mod storage;
#[cfg(feature = "mempool-bench")]
pub fn mempool_eviction_benchmarks(run: impl FnMut(&str, &mut dyn FnMut() -> std::time::Duration)) {
storage::mempool_eviction_benchmarks(run);
}
#[cfg(test)]
mod tests;
pub use crate::BoxError;
pub use config::Config;
pub use crawler::Crawler;
pub use error::MempoolError;
pub(crate) use gossip::run_mempool_transaction_id_gossip;
pub use queue_checker::QueueChecker;
pub use storage::{
ExactTipRejectionError, SameEffectsChainRejectionError, SameEffectsTipRejectionError, Storage,
};
#[cfg(test)]
pub use self::tests::UnboxMempoolError;
use downloads::{
Downloads as TxDownloads, TransactionDownloadVerifyError, TRANSACTION_DOWNLOAD_TIMEOUT,
TRANSACTION_VERIFY_TIMEOUT,
};
fn legacy_peer_log_label(addr: PeerSocketAddr, expose_peer_addresses: bool) -> String {
if expose_peer_addresses {
format!("legacy:{}", addr.remove_socket_addr_privacy())
} else {
"legacy:redacted".to_string()
}
}
fn queue_source_log_label(source: &QueueSource, expose_peer_addresses: bool) -> String {
match source {
QueueSource::LegacySocket(addr) => {
legacy_peer_log_label(PeerSocketAddr::from(*addr), expose_peer_addresses)
}
QueueSource::Zakura(peer_id) => format!("zakura:{peer_id:?}"),
}
}
fn transaction_error_peer_log_label(
error: &TransactionDownloadVerifyError,
expose_peer_addresses: bool,
) -> Option<String> {
match error {
TransactionDownloadVerifyError::Invalid {
advertiser_addr: Some(addr),
..
} => Some(legacy_peer_log_label(*addr, expose_peer_addresses)),
_ => None,
}
}
type Outbound =
Buffer<zn::Request, futures::future::BoxFuture<'static, Result<zn::Response, zn::BoxError>>>;
type State =
Buffer<zs::Request, futures::future::BoxFuture<'static, Result<zs::Response, zs::BoxError>>>;
type ReadState = BoxCloneService<zs::ReadRequest, zs::ReadResponse, zs::BoxError>;
type TxVerifier = Buffer<
transaction::Request,
futures::future::BoxFuture<'static, Result<transaction::Response, TransactionError>>,
>;
type InboundTxDownloads = TxDownloads<Timeout<Outbound>, Timeout<TxVerifier>, ReadState>;
const MAX_ESTIMATED_DISTANCE_TO_ENABLE: block::HeightDiff = 100;
pub(crate) fn is_estimated_close_to_network_tip(chain_tip_change: &ChainTipChange) -> bool {
if chain_tip_change.network().is_a_test_network() {
return chain_tip_change.best_tip_height().is_some();
}
chain_tip_change
.estimate_distance_to_network_chain_tip()
.is_some_and(|(distance, _height)| distance <= MAX_ESTIMATED_DISTANCE_TO_ENABLE)
}
fn transaction_ban_peer(error: &TransactionDownloadVerifyError) -> Option<PeerSocketAddr> {
let TransactionDownloadVerifyError::Invalid {
error,
advertiser_addr: Some(advertiser_addr),
transaction_version,
..
} = error
else {
return None;
};
let bans = match peer_action(error) {
PeerAction::Ban => true,
PeerAction::BanV5AndLater => *transaction_version >= 5,
PeerAction::Cooldown | PeerAction::Ignore => false,
};
bans.then_some(*advertiser_addr)
}
const UPGRADE_GRACE_PERIOD_BLOCKS: block::HeightDiff = 40;
fn upgrade_grace_period(network: &Network, height: block::Height) -> Option<NetworkUpgrade> {
let (upgrade, activation_height) =
NetworkUpgrade::current_with_activation_height(network, height);
if upgrade.branch_id().is_none() || height - activation_height >= UPGRADE_GRACE_PERIOD_BLOCKS {
return None;
}
let previous_height = activation_height.previous().ok()?;
Some(NetworkUpgrade::current(network, previous_height))
}
fn transaction_cooldown_peer(
error: &TransactionDownloadVerifyError,
best_tip_height: Option<block::Height>,
tip_upgrade: NetworkUpgrade,
grace_period_upgrade: Option<NetworkUpgrade>,
) -> Option<PeerSocketAddr> {
let TransactionDownloadVerifyError::Invalid {
error,
advertiser_addr: Some(advertiser_addr),
tip_height,
transaction_version,
transaction_network_upgrade,
retried_after_tip_reset,
} = error
else {
return None;
};
if *retried_after_tip_reset {
return None;
}
let cools_down = match peer_action(error) {
PeerAction::Cooldown => true,
PeerAction::BanV5AndLater => *transaction_version < 5,
PeerAction::Ban | PeerAction::Ignore => false,
};
if !cools_down || tip_height.is_none() || *tip_height != best_tip_height {
return None;
}
if transaction_network_upgrade.is_some_and(|upgrade| upgrade > tip_upgrade) {
return None;
}
if grace_period_upgrade.is_some_and(|previous_upgrade| {
transaction_network_upgrade.is_none_or(|upgrade| upgrade == previous_upgrade)
}) {
return None;
}
Some(*advertiser_addr)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PeerAction {
Ban,
BanV5AndLater,
Cooldown,
Ignore,
}
fn peer_action(error: &TransactionError) -> PeerAction {
use PeerAction::*;
use TransactionError::*;
match error {
WrongVersion
| NoInputs
| NoOutputs
| BadBalance
| IncorrectFee
| SmallOrder
| Groth16(_)
| MalformedGroth16(_)
| BothVPubsNonZero
| NotEnoughFlags
| NotEnoughIronwoodFlags
| OrchardHasEnableCrossAddress
| OrchardProofSize
| IronwoodProofSize
| CoinbaseInMempool
| NonCoinbaseHasCoinbaseInput => Ban,
DuplicateTransparentSpend(_)
| DuplicateSproutNullifier(_)
| DuplicateSaplingNullifier(_)
| DuplicateOrchardNullifier(_)
| DuplicateIronwoodNullifier(_)
| MaximumExpiryHeight { .. } => Ban,
UnshieldedTransparentCoinbaseSpend { .. } => Ban,
ValidateContextError(error) => match **error {
zs::ValidateContextError::UnshieldedTransparentCoinbaseSpend { .. } => Ban,
_ => Ignore,
},
Script(script_error) if script_error.is_script_failure() => BanV5AndLater,
SaplingVerificationFailed
| Halo2VerificationFailed
| Ed25519(_)
| RedJubjub(_)
| RedPallas(_) => BanV5AndLater,
DisabledAddToOrchardPool => BanV5AndLater,
OrchardActionsExceedBlockLimit { .. }
| IronwoodActionsExceedBlockLimit { .. }
| SaplingIOsExceedBlockLimit { .. }
| SproutJoinSplitsExceedBlockLimit { .. }
| ShieldedCostExceedsBlockBudget { .. } => BanV5AndLater,
DisabledAddToSproutPool => Cooldown,
WrongConsensusBranchId | UnsupportedByNetworkUpgrade(..) | ExpiredTransaction { .. } => {
Ignore
}
WrongConsensusBranchIdNu6_3GracePeriod
| MissingConsensusBranchId
| LockedUntilAfterBlockHeight(_)
| LockedUntilAfterBlockTime(_)
| ImmatureTransparentCoinbaseSpend { .. } => Ignore,
TransparentInputNotFound | ValidateMempoolLockTimeError(_) => Ignore,
Zip317(_)
| NonStandardScriptSigSize { .. }
| NonStandardScriptSigNotPushOnly { .. }
| NonStandardInputs => Ignore,
CoinbasePosition
| CoinbaseAfterFirst
| CoinbaseHasJoinSplit
| CoinbaseHasSpend
| CoinbaseHasOutputPreHeartwood
| CoinbaseHasEnableSpendsOrchard
| CoinbaseHasEnableSpendsIronwood
| CoinbaseHasOrchardShieldedData
| CoinbaseOutputsNotDecryptable
| NotCoinbase
| CoinbaseExpiryBlockHeight { .. }
| CoinbaseConstruction(_)
| Subsidy(_) => Ignore,
Script(_)
| InternalDowncastError(_)
| Io(_)
| TryFromSlice(_)
| Amount(_)
| Balance(_)
| Other(_) => Ignore,
}
}
fn is_zcashd_compat_peer(zcashd_compat_peer_ips: &HashSet<IpAddr>, peer: PeerSocketAddr) -> bool {
zcashd_compat_peer_ips.contains(&peer.ip().to_canonical())
}
const DISCONNECT_TIMEOUT: Duration = Duration::from_secs(5);
fn start_peer_cooldown(
peer_cooldowns: &peer_cooldown::PeerCooldowns,
outbound: &Outbound,
peer: PeerSocketAddr,
tx_id: UnminedTxId,
expose_peer_addresses: bool,
) {
let Some(cooldown) = peer_cooldowns.record_invalid_transaction(peer.ip(), Instant::now())
else {
return;
};
tracing::debug!(
?tx_id,
peer = %legacy_peer_log_label(peer, expose_peer_addresses),
?cooldown,
"ignoring peer transaction advertisements after an invalid transaction"
);
metrics::counter!("mempool.peer_cooldown.started.total").increment(1);
if cooldown > peer_cooldown::BASE_COOLDOWN {
let disconnect = outbound
.clone()
.oneshot(zn::Request::DisconnectPeer(peer))
.boxed();
spawn_erased(async move {
let result = tokio::time::timeout(DISCONNECT_TIMEOUT, disconnect).await;
if !matches!(result, Ok(Ok(_))) {
tracing::debug!("could not disconnect a peer after repeated transaction failures");
}
});
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Default)]
enum ActiveState {
#[default]
Disabled,
Enabled {
storage: Storage,
tx_downloads: Pin<Box<InboundTxDownloads>>,
pending_gossip_tx_ids: HashSet<UnminedTxId>,
last_seen_tip_hash: block::Hash,
},
}
impl ActiveState {
fn take(&mut self) -> Self {
std::mem::take(self)
}
fn transaction_retry_requests(&self) -> Vec<(Gossip, Option<QueueSource>)> {
match self {
ActiveState::Disabled => Vec::new(),
ActiveState::Enabled {
storage,
tx_downloads,
..
} => {
let mut transactions = Vec::new();
let storage = storage
.transactions()
.values()
.map(|tx| (tx.transaction.clone().into(), None));
transactions.extend(storage);
let pending = tx_downloads
.transaction_requests()
.map(|(tx, source)| (tx.clone(), source.clone()));
transactions.extend(pending);
transactions
}
}
}
}
pub struct Mempool {
config: Config,
expose_peer_addresses: bool,
active_state: ActiveState,
sync_status: SyncStatus,
debug_enable_at_height: Option<Height>,
latest_chain_tip: zs::LatestChainTip,
chain_tip_change: ChainTipChange,
outbound: Outbound,
read_state: ReadState,
_state_guard: State,
tx_verifier: TxVerifier,
transaction_sender: broadcast::Sender<MempoolChange>,
peer_cooldowns: peer_cooldown::PeerCooldowns,
misbehavior_sender: mpsc::Sender<(PeerSocketAddr, u32)>,
zcashd_compat_peer_ips: HashSet<IpAddr>,
}
impl Mempool {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
config: &Config,
expose_peer_addresses: bool,
outbound: Outbound,
state: State,
read_state: ReadState,
tx_verifier: TxVerifier,
sync_status: SyncStatus,
latest_chain_tip: zs::LatestChainTip,
chain_tip_change: ChainTipChange,
misbehavior_sender: mpsc::Sender<(PeerSocketAddr, u32)>,
zcashd_compat_peer_ips: Vec<IpAddr>,
) -> (Self, MempoolTxSubscriber) {
let (transaction_sender, _) =
tokio::sync::broadcast::channel(gossip::MEMPOOL_CHANGE_CHANNEL_CAPACITY);
let transaction_subscriber = MempoolTxSubscriber::new(transaction_sender.clone());
let mut service = Mempool {
config: config.clone(),
expose_peer_addresses,
active_state: ActiveState::Disabled,
sync_status,
debug_enable_at_height: config.debug_enable_at_height.map(Height),
latest_chain_tip,
chain_tip_change,
outbound,
read_state,
_state_guard: state,
tx_verifier,
transaction_sender,
peer_cooldowns: peer_cooldown::PeerCooldowns::default(),
misbehavior_sender,
zcashd_compat_peer_ips: zcashd_compat_peer_ips
.into_iter()
.map(|ip| ip.to_canonical())
.collect(),
};
service.update_state(None, service.is_caught_up_to_start());
(service, transaction_subscriber)
}
fn is_enabled_by_debug(&self) -> bool {
let mut is_debug_enabled = false;
if self.debug_enable_at_height.is_none() {
return is_debug_enabled;
}
let enable_at_height = self
.debug_enable_at_height
.expect("unexpected debug_enable_at_height: just checked for None");
if let Some(best_tip_height) = self.latest_chain_tip.best_tip_height() {
is_debug_enabled = best_tip_height >= enable_at_height;
if is_debug_enabled && !self.is_enabled() {
info!(
?best_tip_height,
?enable_at_height,
"enabling mempool for debugging"
);
}
}
is_debug_enabled
}
fn is_caught_up_to_start(&self) -> bool {
self.is_enabled_by_debug() || self.is_current_enough_for_mempool()
}
fn is_current_enough_for_mempool(&self) -> bool {
self.sync_status.is_close_to_tip()
&& is_estimated_close_to_network_tip(&self.chain_tip_change)
}
fn estimated_distance_to_network_tip(&self) -> Option<block::HeightDiff> {
self.chain_tip_change
.estimate_distance_to_network_chain_tip()
.map(|(distance, _height)| distance)
}
fn is_far_enough_to_disable(&self) -> bool {
!self.is_enabled_by_debug()
&& !self.chain_tip_change.network().is_a_test_network()
&& self
.estimated_distance_to_network_tip()
.is_none_or(|distance| distance > i64::from(zs::MAX_BLOCK_REORG_HEIGHT))
}
fn enable_at_tip(&mut self, tip_action: &TipAction, reason: &'static str) {
let (last_seen_tip_hash, tip_height) = tip_action.best_tip_hash_and_height();
info!(?tip_height, reason, "activating mempool");
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.read_state.clone(),
self.expose_peer_addresses,
self.config.max_transaction_bytes,
self.peer_cooldowns.clone(),
));
self.active_state = ActiveState::Enabled {
storage: storage::Storage::new(&self.config),
tx_downloads,
pending_gossip_tx_ids: HashSet::new(),
last_seen_tip_hash,
};
}
fn update_state(
&mut self,
tip_action: Option<&TipAction>,
is_caught_up_to_start: bool,
) -> bool {
let is_far_enough_to_disable = self.is_far_enough_to_disable();
match (
is_caught_up_to_start,
is_far_enough_to_disable,
self.is_enabled(),
tip_action,
) {
(false, _, false, _) | (true, _, true, _) | (true, _, false, None) => return false,
(true, _, false, Some(tip_action)) => self.enable_at_tip(
tip_action,
if self.is_enabled_by_debug() {
"debug height reached"
} else {
"estimated close to the network tip"
},
),
(_, true, true, _) => {
let estimated_distance_to_network_tip = self.estimated_distance_to_network_tip();
info!(
?estimated_distance_to_network_tip,
disable_distance = zs::MAX_BLOCK_REORG_HEIGHT,
"deactivating mempool: Zakura is far from the network tip"
);
if let ActiveState::Enabled { storage, .. } = &self.active_state {
let invalidated_ids: HashSet<_> = storage.tx_ids().collect();
if !invalidated_ids.is_empty() {
let _ = self
.transaction_sender
.send(MempoolChange::invalidated(invalidated_ids));
}
}
self.active_state = ActiveState::Disabled;
}
(false, false, true, _) => return false,
};
true
}
pub fn is_enabled(&self) -> bool {
match self.active_state {
ActiveState::Disabled => false,
ActiveState::Enabled { .. } => true,
}
}
}
impl Service<Request> for Mempool {
type Response = Response;
type Error = BoxError;
type Future =
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_caught_up_to_start = self.is_caught_up_to_start();
let should_check_tip = self.is_enabled() || is_caught_up_to_start;
let tip_action = should_check_tip
.then(|| self.chain_tip_change.last_tip_change())
.flatten();
let is_state_changed = self.update_state(tip_action.as_ref(), is_caught_up_to_start);
tracing::trace!(is_enabled = ?self.is_enabled(), ?is_state_changed, "started polling the mempool...");
if !self.is_enabled() {
return Poll::Ready(Ok(()));
}
if !is_state_changed && matches!(tip_action, Some(TipAction::Reset { .. })) {
info!(
tip_height = ?tip_action.as_ref().unwrap().best_tip_height(),
"resetting mempool: switched best chain, skipped blocks, or activated network upgrade"
);
let previous_state = self.active_state.take();
let tx_retries = previous_state.transaction_retry_requests();
std::mem::drop(previous_state);
self.enable_at_tip(
tip_action
.as_ref()
.expect("this branch only matches when tip_action is a Reset"),
"chain tip reset",
);
if let ActiveState::Enabled { tx_downloads, .. } = &mut self.active_state {
info!(
transactions = tx_retries.len(),
"re-verifying mempool transactions after a chain fork"
);
for (tx, source) in tx_retries {
let _result = tx_downloads.retry_after_tip_reset(tx, source);
}
}
return Poll::Ready(Ok(()));
}
let is_current_enough_for_mempool = self.is_current_enough_for_mempool();
if let ActiveState::Enabled {
storage,
tx_downloads,
pending_gossip_tx_ids,
last_seen_tip_hash,
} = &mut self.active_state
{
let mut send_to_peers_ids = HashSet::<_>::new();
let mut invalidated_ids = HashSet::<_>::new();
let mut mined_mempool_ids = HashSet::<_>::new();
let best_tip_height = self.latest_chain_tip.best_tip_height();
let next_height = best_tip_height
.and_then(|height| height + 1)
.unwrap_or(Height(0));
let network = self.chain_tip_change.network();
let tip_upgrade = NetworkUpgrade::current(network, next_height);
let grace_period_upgrade = upgrade_grace_period(network, next_height);
while let Poll::Ready(Some(result)) = pin!(&mut *tx_downloads).poll_next(cx) {
match result {
Ok(Ok((tx, spent_mempool_outpoints, expected_tip_height, rsp_tx))) => {
if best_tip_height == expected_tip_height {
let tx_id = tx.transaction.id();
let (insert_result, evicted_ids) = storage.insert_with_evicted_ids(
tx,
spent_mempool_outpoints,
best_tip_height,
);
tracing::trace!(
?insert_result,
?evicted_ids,
"got Ok(_) transaction verify, tried to store",
);
if let Ok(inserted_id) = insert_result {
send_to_peers_ids.insert(inserted_id);
} else {
invalidated_ids.insert(tx_id);
}
if !evicted_ids.is_empty() {
send_to_peers_ids.retain(|id| !evicted_ids.contains(id));
invalidated_ids.extend(evicted_ids);
}
if let Some(rsp_tx) = rsp_tx {
let _ = rsp_tx
.send(insert_result.map(|_| ()).map_err(|err| err.into()));
}
} else {
tracing::trace!("chain grew during tx verification, retrying ..",);
let _result = tx_downloads.download_if_needed_and_verify(
tx.transaction.into(),
None,
rsp_tx,
);
}
}
Ok(Err(boxed_err)) => {
let (tx_id, error) = *boxed_err;
let zcashd_compat_peer_ips = &self.zcashd_compat_peer_ips;
let ban_peer = transaction_ban_peer(&error)
.filter(|peer| !is_zcashd_compat_peer(zcashd_compat_peer_ips, *peer));
if let Some(peer) = ban_peer {
let _ = self
.misbehavior_sender
.try_send((peer, zn::constants::MAX_PEER_MISBEHAVIOR_SCORE));
}
let cooldown_peer = is_current_enough_for_mempool
.then(|| {
transaction_cooldown_peer(
&error,
best_tip_height,
tip_upgrade,
grace_period_upgrade,
)
})
.flatten()
.filter(|peer| !is_zcashd_compat_peer(zcashd_compat_peer_ips, *peer));
if let Some(peer) = cooldown_peer {
start_peer_cooldown(
&self.peer_cooldowns,
&self.outbound,
peer,
tx_id,
self.expose_peer_addresses,
);
}
let peer_label =
transaction_error_peer_log_label(&error, self.expose_peer_addresses);
let peer = peer_label.as_deref().unwrap_or("none");
tracing::debug!(
?tx_id,
?error,
peer = %peer,
"mempool transaction was not accepted"
);
match &error {
TransactionDownloadVerifyError::PolicyRejected(
storage::NonStandardTransactionError::TransactionTooLarge {
..
},
) => metrics::counter!(
"mempool.rejected.transactions.total",
"reason" => "transaction_too_large"
)
.increment(1),
TransactionDownloadVerifyError::PeerCoolingDown => metrics::counter!(
"mempool.peer_cooldown.ignored.transactions.total"
)
.increment(1),
_ => metrics::counter!(
"mempool.failed.verify.tasks.total",
"reason" => error.to_string()
)
.increment(1),
}
invalidated_ids.insert(tx_id);
storage.reject_if_needed(tx_id, error);
}
Err((tx_id, _elapsed)) => {
tracing::info!(
?tx_id,
"mempool transaction failed to verify due to timeout"
);
invalidated_ids.insert(tx_id);
metrics::counter!("mempool.failed.verify.tasks.total", "reason" => "timeout").increment(1);
}
};
}
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;
let mined_ids = block.transaction_hashes.iter().cloned().collect();
tx_downloads.cancel(&mined_ids);
storage.clear_mined_dependencies(&mined_ids);
let storage::RemovedTransactionIds { mined, invalidated } =
storage.reject_and_remove_same_effects(&mined_ids, block.transactions);
storage.clear_tip_rejections();
mined_mempool_ids.extend(mined);
invalidated_ids.extend(invalidated);
}
if let Some(tip_height) = best_tip_height {
let expired_transactions = storage.remove_expired_transactions(tip_height);
send_to_peers_ids.retain(|id| !expired_transactions.contains(id));
if !expired_transactions.is_empty() {
tracing::debug!(
?expired_transactions,
"removed expired transactions from the mempool",
);
invalidated_ids.extend(expired_transactions);
}
}
if !send_to_peers_ids.is_empty() {
tracing::trace!(
?send_to_peers_ids,
"sending new transactions to peers and RPC listeners"
);
pending_gossip_tx_ids.extend(send_to_peers_ids.iter().copied());
self.transaction_sender
.send(MempoolChange::added(send_to_peers_ids))?;
}
if !invalidated_ids.is_empty() || !mined_mempool_ids.is_empty() {
pending_gossip_tx_ids.retain(|tx_id| {
!invalidated_ids.contains(tx_id) && !mined_mempool_ids.contains(tx_id)
});
}
if !invalidated_ids.is_empty() {
tracing::trace!(
?invalidated_ids,
"sending invalidated transactions to RPC listeners"
);
self.transaction_sender
.send(MempoolChange::invalidated(invalidated_ids))?;
}
if !mined_mempool_ids.is_empty() {
tracing::trace!(
?mined_mempool_ids,
"sending mined transactions to RPC listeners"
);
self.transaction_sender
.send(MempoolChange::mined(mined_mempool_ids))?;
}
}
Poll::Ready(Ok(()))
}
#[instrument(name = "mempool", skip(self, req))]
fn call(&mut self, req: Request) -> Self::Future {
match &mut self.active_state {
ActiveState::Enabled {
storage,
tx_downloads,
pending_gossip_tx_ids,
last_seen_tip_hash,
} => match req {
Request::TransactionIds => {
trace!(?req, "got mempool request");
let res: HashSet<_> = storage.tx_ids().collect();
trace!(?req, res_count = ?res.len(), "answered mempool request");
async move { Ok(Response::TransactionIds(res)) }.boxed()
}
Request::TakePendingGossipTransactionIds { limit } => {
trace!(?req, "got mempool request");
let res = if pending_gossip_tx_ids.len() <= limit {
std::mem::take(pending_gossip_tx_ids)
} else {
let res: HashSet<_> =
pending_gossip_tx_ids.iter().copied().take(limit).collect();
for tx_id in &res {
pending_gossip_tx_ids.remove(tx_id);
}
res
};
trace!(?req, res_count = ?res.len(), "answered mempool request");
async move { Ok(Response::TransactionIds(res)) }.boxed()
}
Request::TransactionsById(ref ids) => {
trace!(?req, "got mempool request");
let res: Vec<_> = storage.transactions_exact(ids.clone()).cloned().collect();
trace!(?req, res_count = ?res.len(), "answered mempool request");
async move { Ok(Response::Transactions(res)) }.boxed()
}
Request::TransactionsByMinedId(ref ids) => {
trace!(?req, "got mempool request");
let res: Vec<_> = storage
.transactions_same_effects(ids.clone())
.cloned()
.collect();
trace!(?req, res_count = ?res.len(), "answered mempool request");
async move { Ok(Response::Transactions(res)) }.boxed()
}
Request::TransactionWithDepsByMinedId(tx_id) => {
trace!(?req, "got mempool request");
let res = if let Some((transaction, dependencies)) =
storage.transaction_with_deps(tx_id)
{
Ok(Response::TransactionWithDeps {
transaction,
dependencies,
})
} else {
Err("transaction not found in mempool".into())
};
trace!(?req, ?res, "answered mempool request");
async move { res }.boxed()
}
Request::AwaitOutput(outpoint) => {
trace!(?req, "got mempool request");
let response_fut = storage.pending_outputs.queue(outpoint);
if let Some(output) = storage.created_output(&outpoint) {
storage.pending_outputs.respond(&outpoint, output)
}
trace!("answered mempool request");
response_fut.boxed()
}
Request::FullTransactions => {
trace!(?req, "got mempool request");
let transactions: Vec<_> = storage.transactions().values().cloned().collect();
let transaction_dependencies = storage.transaction_dependencies().clone();
trace!(?req, transactions_count = ?transactions.len(), "answered mempool request");
let response = Response::FullTransactions {
transactions,
transaction_dependencies,
last_seen_tip_hash: *last_seen_tip_hash,
};
async move { Ok(response) }.boxed()
}
Request::RejectedTransactionIds(ref ids) => {
trace!(?req, "got mempool request");
let res = storage.rejected_transactions(ids.clone()).collect();
trace!(?req, ?res, "answered mempool request");
async move { Ok(Response::RejectedTransactionIds(res)) }.boxed()
}
Request::Queue(gossiped_txs) => {
trace!(req_count = ?gossiped_txs.len(), "got mempool Queue request");
let rsp: Vec<Result<oneshot::Receiver<Result<(), BoxError>>, BoxError>> =
gossiped_txs
.into_iter()
.map(
|gossiped_tx| -> Result<
oneshot::Receiver<Result<(), BoxError>>,
MempoolError,
> {
let (rsp_tx, rsp_rx) = oneshot::channel();
match storage.should_download_or_verify(gossiped_tx.id()) {
Ok(()) => {}
Err(
error @ MempoolError::StorageExactTip(
ExactTipRejectionError::FailedStandard(
storage::NonStandardTransactionError::TransactionTooLarge {
..
},
),
),
) => {
let _ = rsp_tx.send(Err(error.into()));
return Ok(rsp_rx);
}
Err(error) => return Err(error),
}
tx_downloads.download_if_needed_and_verify(
gossiped_tx,
None,
Some(rsp_tx),
)?;
Ok(rsp_rx)
},
)
.map(|result| result.map_err(BoxError::from))
.collect();
async move { Ok(Response::Queued(rsp)) }.boxed()
}
Request::QueueFromPeer {
transactions,
source,
} => {
trace!(
req_count = ?transactions.len(),
source = %queue_source_log_label(&source, self.expose_peer_addresses),
"got mempool QueueFromPeer request"
);
let is_cooling_down = match &source {
QueueSource::LegacySocket(addr) => self
.peer_cooldowns
.is_cooling_down(addr.ip(), Instant::now()),
QueueSource::Zakura(_) => false,
};
if is_cooling_down {
trace!(
req_count = ?transactions.len(),
"ignored transactions from a peer in a transaction cooldown"
);
metrics::counter!("mempool.peer_cooldown.ignored.transactions.total")
.increment(u64::try_from(transactions.len()).unwrap_or(u64::MAX));
} else {
for gossiped_tx in transactions {
if storage.should_download_or_verify(gossiped_tx.id()).is_err() {
continue;
}
let _ = tx_downloads.download_if_needed_and_verify(
gossiped_tx,
Some(source.clone()),
None,
);
}
}
async move { Ok(Response::Queued(Vec::new())) }.boxed()
}
Request::CheckForVerifiedTransactions => {
trace!(?req, "got mempool request");
async move { Ok(Response::CheckedForVerifiedTransactions) }.boxed()
}
Request::QueueStats => {
trace!(?req, "got mempool request");
let size = storage.transaction_count();
let bytes = storage.total_serialized_size();
let usage = bytes;
let fully_notified = None;
trace!(size, bytes, usage, "answered mempool request");
async move {
Ok(Response::QueueStats {
size,
bytes,
usage,
fully_notified,
})
}
.boxed()
}
Request::UnspentOutput(outpoint) => {
trace!(?req, "got mempool request");
if storage.has_spent_outpoint(&outpoint) {
trace!(?req, "answered mempool request");
return async move {
Ok(Response::TransparentOutput(Some(CreatedOrSpent::Spent)))
}
.boxed();
}
if let Some((tx_version, output)) = storage
.transactions()
.get(&outpoint.hash)
.map(|tx| tx.transaction.transaction().clone())
.and_then(|tx| {
tx.outputs()
.get(outpoint.index as usize)
.map(|output| (tx.version(), output.clone()))
})
{
trace!(?req, "answered mempool request");
let last_seen_hash = *last_seen_tip_hash;
return async move {
Ok(Response::TransparentOutput(Some(CreatedOrSpent::Created {
output,
tx_version,
last_seen_hash,
})))
}
.boxed();
}
trace!(?req, "answered mempool request");
async move { Ok(Response::TransparentOutput(None)) }.boxed()
}
},
ActiveState::Disabled => {
trace!("got mempool request while mempool is disabled");
let resp = match req {
Request::TransactionIds | Request::TakePendingGossipTransactionIds { .. } => {
Response::TransactionIds(Default::default())
}
Request::TransactionsById(_) => Response::Transactions(Default::default()),
Request::TransactionsByMinedId(_) => Response::Transactions(Default::default()),
Request::TransactionWithDepsByMinedId(_)
| Request::AwaitOutput(_)
| Request::UnspentOutput(_) => {
return async move { Err(MempoolDisabledError.into()) }.boxed()
}
Request::FullTransactions => {
return async move { Err(MempoolDisabledError.into()) }.boxed()
}
Request::RejectedTransactionIds(_) => {
Response::RejectedTransactionIds(Default::default())
}
Request::Queue(gossiped_txs) => Response::Queued(
iter::repeat_n(MempoolError::Disabled, gossiped_txs.len())
.map(BoxError::from)
.map(Err)
.collect(),
),
Request::QueueFromPeer { .. } => Response::Queued(Vec::new()),
Request::CheckForVerifiedTransactions => {
Response::CheckedForVerifiedTransactions
}
Request::QueueStats => Response::QueueStats {
size: 0,
bytes: 0,
usage: 0,
fully_notified: None,
},
};
async move { Ok(resp) }.boxed()
}
}
}
}