zakura_node_services/mempool.rs
1//! The Zakura mempool.
2//!
3//! A service that manages known unmined Zcash transactions.
4
5use std::{collections::HashSet, fmt, net::SocketAddr};
6
7use tokio::sync::oneshot;
8use zakura_chain::{
9 block,
10 transaction::{self, UnminedTx, UnminedTxId, VerifiedUnminedTx},
11 transparent,
12};
13
14use crate::BoxError;
15
16mod gossip;
17mod mempool_change;
18mod service_trait;
19mod transaction_dependencies;
20
21pub use self::{
22 gossip::Gossip,
23 mempool_change::{MempoolChange, MempoolChangeKind, MempoolTxSubscriber},
24 service_trait::MempoolService,
25 transaction_dependencies::TransactionDependencies,
26};
27
28/// The mempool is disabled until Zakura is close to the chain tip.
29#[derive(Debug)]
30pub struct MempoolDisabledError;
31
32impl fmt::Display for MempoolDisabledError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str("mempool is not active: wait for Zakura to sync to the tip")
35 }
36}
37
38impl std::error::Error for MempoolDisabledError {}
39
40#[cfg(test)]
41mod tests {
42 use super::MempoolDisabledError;
43
44 #[test]
45 fn mempool_disabled_error_uses_zakura_branding() {
46 assert_eq!(
47 MempoolDisabledError.to_string(),
48 "mempool is not active: wait for Zakura to sync to the tip"
49 );
50 }
51}
52
53/// A peer source for per-peer mempool download accounting.
54#[derive(Clone, Debug, Eq, Hash, PartialEq)]
55pub enum QueueSource {
56 /// A transaction advertisement from the legacy TCP transport.
57 LegacySocket(SocketAddr),
58
59 /// A transaction advertisement from an authenticated Zakura peer.
60 ///
61 /// Stored as the encoded Zakura peer id to keep this service-interface crate
62 /// independent from the `zakura-network` transport types.
63 Zakura(Vec<u8>),
64}
65
66impl From<SocketAddr> for QueueSource {
67 fn from(source: SocketAddr) -> Self {
68 Self::LegacySocket(source)
69 }
70}
71
72/// A mempool service request.
73///
74/// Requests can query the current set of mempool transactions,
75/// queue transactions to be downloaded and verified, or
76/// run the mempool to check for newly verified transactions.
77///
78/// Requests can't modify the mempool directly,
79/// because all mempool transactions must be verified.
80#[derive(Debug, Eq, PartialEq)]
81pub enum Request {
82 /// Query all [`UnminedTxId`]s in the mempool.
83 TransactionIds,
84
85 /// Return and clear up to `limit` transaction IDs awaiting proactive
86 /// advertisement through the peer set.
87 ///
88 /// This pending set is separate from the full mempool inventory returned
89 /// by [`Request::TransactionIds`] in response to peer `mempool` requests.
90 TakePendingGossipTransactionIds {
91 /// Maximum number of transaction IDs to return.
92 limit: usize,
93 },
94
95 /// Query matching [`UnminedTx`] in the mempool,
96 /// using a unique set of [`UnminedTxId`]s.
97 TransactionsById(HashSet<UnminedTxId>),
98
99 /// Query matching [`UnminedTx`] in the mempool,
100 /// using a unique set of [`transaction::Hash`]es. Pre-V5 transactions are matched
101 /// directly; V5 transaction are matched just by the Hash, disregarding
102 /// the [`AuthDigest`](zakura_chain::transaction::AuthDigest).
103 TransactionsByMinedId(HashSet<transaction::Hash>),
104
105 /// Request a [`transparent::Output`] identified by the given [`OutPoint`](transparent::OutPoint),
106 /// waiting until it becomes available if it is unknown.
107 ///
108 /// This request is purely informational, and there are no guarantees about
109 /// whether the UTXO remains unspent or is on the best chain, or any chain.
110 /// Its purpose is to allow orphaned mempool transaction verification.
111 ///
112 /// # Correctness
113 ///
114 /// Output requests should be wrapped in a timeout, so that
115 /// out-of-order and invalid requests do not hang indefinitely.
116 ///
117 /// Outdated requests are pruned on a regular basis.
118 AwaitOutput(transparent::OutPoint),
119
120 /// Request a [`VerifiedUnminedTx`] and its dependencies by its mined id.
121 TransactionWithDepsByMinedId(transaction::Hash),
122
123 /// Get all the [`VerifiedUnminedTx`] in the mempool.
124 ///
125 /// Equivalent to `TransactionsById(TransactionIds)`,
126 /// but each transaction also includes the `miner_fee` and `legacy_sigop_count` fields.
127 //
128 // TODO: make the Transactions response return VerifiedUnminedTx,
129 // and remove the FullTransactions variant
130 FullTransactions,
131
132 /// Query matching cached rejected transaction IDs in the mempool,
133 /// using a unique set of [`UnminedTxId`]s.
134 RejectedTransactionIds(HashSet<UnminedTxId>),
135
136 /// Queue a list of gossiped transactions or transaction IDs, or
137 /// crawled transaction IDs.
138 ///
139 /// The transaction downloader checks for duplicates across IDs and transactions.
140 Queue(Vec<Gossip>),
141
142 /// Queue transactions or transaction IDs received from a specific peer,
143 /// tagging each one with the peer so the downloader can enforce a per-peer
144 /// queue cap. See `GHSA-4fc2-h7jh-287c`.
145 QueueFromPeer {
146 /// The gossiped transaction candidates received from the peer.
147 transactions: Vec<Gossip>,
148 /// The peer that advertised them.
149 source: QueueSource,
150 },
151
152 /// Check for newly verified transactions.
153 ///
154 /// The transaction downloader does not push transactions into the mempool.
155 /// So a task should send this request regularly (every 5-10 seconds).
156 ///
157 /// These checks also happen for other request variants,
158 /// but we can't rely on peers to send queries regularly,
159 /// and crawler queue requests depend on peer responses.
160 /// Also, crawler requests aren't frequent enough for transaction propagation.
161 ///
162 /// # Correctness
163 ///
164 /// This request is required to avoid hangs in the mempool.
165 ///
166 /// The queue checker task can't call `poll_ready` directly on the mempool
167 /// service, because the service is wrapped in a `Buffer`. Calling
168 /// `Buffer::poll_ready` reserves a buffer slot, which can cause hangs
169 /// when too many slots are reserved but unused:
170 /// <https://docs.rs/tower/0.4.10/tower/buffer/struct.Buffer.html#a-note-on-choosing-a-bound>
171 CheckForVerifiedTransactions,
172
173 /// Request summary statistics from the mempool for `getmempoolinfo`.
174 QueueStats,
175
176 /// Check whether a transparent output is spent in the mempool.
177 UnspentOutput(transparent::OutPoint),
178}
179
180/// A response to a mempool service request.
181///
182/// Responses can read the current set of mempool transactions,
183/// check the queued status of transactions to be downloaded and verified, or
184/// confirm that the mempool has been checked for newly verified transactions.
185#[derive(Debug)]
186pub enum Response {
187 /// Returns all [`UnminedTxId`]s from the mempool.
188 TransactionIds(HashSet<UnminedTxId>),
189
190 /// Returns matching [`UnminedTx`] from the mempool.
191 ///
192 /// Since the [`Request::TransactionsById`] request is unique,
193 /// the response transactions are also unique. The same applies to
194 /// [`Request::TransactionsByMinedId`] requests, since the mempool does not allow
195 /// different transactions with different mined IDs.
196 Transactions(Vec<UnminedTx>),
197
198 /// Response to [`Request::AwaitOutput`] with the transparent output
199 UnspentOutput(transparent::Output),
200
201 /// Response to [`Request::TransactionWithDepsByMinedId`].
202 TransactionWithDeps {
203 /// The queried transaction
204 transaction: VerifiedUnminedTx,
205 /// A list of dependencies of the queried transaction.
206 dependencies: HashSet<transaction::Hash>,
207 },
208
209 /// Returns all [`VerifiedUnminedTx`] in the mempool.
210 //
211 // TODO: make the Transactions response return VerifiedUnminedTx,
212 // and remove the FullTransactions variant
213 FullTransactions {
214 /// All [`VerifiedUnminedTx`]s in the mempool
215 transactions: Vec<VerifiedUnminedTx>,
216
217 /// All transaction dependencies in the mempool
218 transaction_dependencies: TransactionDependencies,
219
220 /// Last seen chain tip hash by mempool service
221 last_seen_tip_hash: zakura_chain::block::Hash,
222 },
223
224 /// Returns matching cached rejected [`UnminedTxId`]s from the mempool,
225 RejectedTransactionIds(HashSet<UnminedTxId>),
226
227 /// Returns a list of initial queue checks results and a oneshot receiver
228 /// for awaiting download and/or verification results.
229 ///
230 /// Each result matches the request at the corresponding vector index.
231 Queued(Vec<Result<oneshot::Receiver<Result<(), BoxError>>, BoxError>>),
232
233 /// Confirms that the mempool has checked for recently verified transactions.
234 CheckedForVerifiedTransactions,
235
236 /// Summary statistics for the mempool: count, total size, memory usage, and regtest info.
237 QueueStats {
238 /// Number of transactions currently in the mempool
239 size: usize,
240 /// Total size in bytes of all transactions
241 bytes: usize,
242 /// Estimated memory usage in bytes
243 usage: usize,
244 /// Whether all transactions have been fully notified (regtest only)
245 fully_notified: Option<bool>,
246 },
247
248 /// Returns whether a transparent output is created or spent in the mempool, if present.
249 TransparentOutput(Option<CreatedOrSpent>),
250}
251
252/// Indicates whether an output was created or spent by a mempool transaction.
253#[derive(Debug)]
254pub enum CreatedOrSpent {
255 /// An unspent output that was created by a transaction in the mempool and not spent by any other mempool tx.
256 Created {
257 /// The output
258 output: transparent::Output,
259 /// The version
260 tx_version: u32,
261 /// The last seen hash
262 last_seen_hash: block::Hash,
263 },
264 /// Indicates that an output was spent by a mempool transaction.
265 Spent,
266}