1#![expect(
2 unreachable_code,
3 unused_variables,
4 clippy::unnecessary_wraps,
5 clippy::needless_pass_by_value,
6 reason = "TODO: finish implementing the signatures from <https://github.com/Cuprate/cuprate/pull/297>"
7)]
8use std::{
9 collections::{HashMap, HashSet},
10 num::NonZero,
11 sync::Arc,
12 task::{Context, Poll},
13};
14
15use fjall::Readable;
16use futures::channel::oneshot;
17use rayon::ThreadPool;
18use tower::Service;
19
20use cuprate_helper::asynch::InfallibleOneshotReceiver;
21use cuprate_types::TxInPool;
22
23use crate::{
24 error::TxPoolError,
25 ops::{get_transaction_verification_data, in_stem_pool},
26 service::interface::{TxpoolReadRequest, TxpoolReadResponse},
27 txpool::TxpoolDatabase,
28 types::{TransactionBlobHash, TransactionHash, TransactionInfo, TxStateFlags},
29 TxEntry,
30};
31
32#[derive(Clone)]
34pub struct TxpoolReadHandle {
35 pub(crate) pool: Arc<ThreadPool>,
36
37 pub(crate) txpool: Arc<TxpoolDatabase>,
38}
39
40impl Service<TxpoolReadRequest> for TxpoolReadHandle {
41 type Response = TxpoolReadResponse;
42 type Error = TxPoolError;
43 type Future = InfallibleOneshotReceiver<Result<Self::Response, Self::Error>>;
44
45 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
46 Poll::Ready(Ok(()))
47 }
48
49 fn call(&mut self, req: TxpoolReadRequest) -> Self::Future {
50 let (tx, rx) = oneshot::channel();
51
52 let db = Arc::clone(&self.txpool);
53 self.pool.spawn(move || {
54 let res = map_request(&db, req);
55
56 let _ = tx.send(res);
57 });
58
59 InfallibleOneshotReceiver::from(rx)
60 }
61}
62
63fn map_request(
75 db: &TxpoolDatabase, request: TxpoolReadRequest, ) -> Result<TxpoolReadResponse, TxPoolError> {
78 match request {
79 TxpoolReadRequest::TxBlob(tx_hash) => tx_blob(db, &tx_hash),
80 TxpoolReadRequest::TxVerificationData(tx_hash) => tx_verification_data(db, &tx_hash),
81 TxpoolReadRequest::FilterKnownTxBlobHashes(blob_hashes) => {
82 filter_known_tx_blob_hashes(db, blob_hashes)
83 }
84 TxpoolReadRequest::TxsForBlock(txs_needed) => txs_for_block(db, txs_needed),
85 TxpoolReadRequest::Backlog => backlog(db),
86 TxpoolReadRequest::Size {
87 include_sensitive_txs,
88 } => size(db, include_sensitive_txs),
89 TxpoolReadRequest::PoolInfo {
90 include_sensitive_txs,
91 max_tx_count,
92 start_time,
93 } => pool_info(db, include_sensitive_txs, max_tx_count, start_time),
94 TxpoolReadRequest::TxsByHash {
95 tx_hashes,
96 include_sensitive_txs,
97 } => txs_by_hash(db, tx_hashes, include_sensitive_txs),
98 TxpoolReadRequest::KeyImagesSpent {
99 key_images,
100 include_sensitive_txs,
101 } => key_images_spent(db, key_images, include_sensitive_txs),
102 TxpoolReadRequest::KeyImagesSpentVec {
103 key_images,
104 include_sensitive_txs,
105 } => key_images_spent_vec(db, key_images, include_sensitive_txs),
106 TxpoolReadRequest::Pool {
107 include_sensitive_txs,
108 } => pool(db, include_sensitive_txs),
109 TxpoolReadRequest::PoolStats {
110 include_sensitive_txs,
111 } => pool_stats(db, include_sensitive_txs),
112 TxpoolReadRequest::AllHashes {
113 include_sensitive_txs,
114 } => all_hashes(db, include_sensitive_txs),
115 }
116}
117
118#[inline]
137fn tx_blob(
138 db: &TxpoolDatabase,
139 tx_hash: &TransactionHash,
140) -> Result<TxpoolReadResponse, TxPoolError> {
141 let snapshot = db.fjall_database.snapshot();
142
143 let tx_blob = snapshot
144 .get(&db.tx_blobs, tx_hash)?
145 .ok_or(TxPoolError::NotFound)?
146 .to_vec();
147
148 Ok(TxpoolReadResponse::TxBlob {
149 tx_blob,
150 state_stem: in_stem_pool(tx_hash, &snapshot, db)?,
151 })
152}
153
154#[inline]
156fn tx_verification_data(
157 db: &TxpoolDatabase,
158 tx_hash: &TransactionHash,
159) -> Result<TxpoolReadResponse, TxPoolError> {
160 let snapshot = db.fjall_database.snapshot();
161
162 get_transaction_verification_data(tx_hash, &snapshot, db)
163 .map(TxpoolReadResponse::TxVerificationData)
164}
165
166fn filter_known_tx_blob_hashes(
168 db: &TxpoolDatabase,
169 mut blob_hashes: HashSet<TransactionBlobHash>,
170) -> Result<TxpoolReadResponse, TxPoolError> {
171 let snapshot = db.fjall_database.snapshot();
172
173 let mut stem_pool_hashes = Vec::new();
174
175 let mut tx_unknown = |blob_hash| -> Result<bool, TxPoolError> {
178 match snapshot.get(&db.known_blob_hashes, blob_hash)? {
179 Some(tx_hash) => {
180 let tx_hash = tx_hash.as_ref().try_into().unwrap();
181
182 if in_stem_pool(&tx_hash, &snapshot, db)? {
183 stem_pool_hashes.push(tx_hash);
184 }
185 Ok(false)
186 }
187 None => Ok(true),
188 }
189 };
190
191 let mut err = None;
192 blob_hashes.retain(|blob_hash| match tx_unknown(*blob_hash) {
193 Ok(res) => res,
194 Err(e) => {
195 err = Some(e);
196 false
197 }
198 });
199
200 if let Some(e) = err {
201 return Err(e);
202 }
203
204 Ok(TxpoolReadResponse::FilterKnownTxBlobHashes {
205 unknown_blob_hashes: blob_hashes,
206 stem_pool_hashes,
207 })
208}
209
210fn txs_for_block(
212 db: &TxpoolDatabase,
213 txs: Vec<TransactionHash>,
214) -> Result<TxpoolReadResponse, TxPoolError> {
215 let snapshot = db.fjall_database.snapshot();
216
217 let mut missing_tx_indexes = Vec::with_capacity(txs.len());
218 let mut txs_verification_data = HashMap::with_capacity(txs.len());
219
220 for (i, tx_hash) in txs.into_iter().enumerate() {
221 match get_transaction_verification_data(&tx_hash, &snapshot, db) {
222 Ok(tx) => {
223 txs_verification_data.insert(tx_hash, tx);
224 }
225 Err(TxPoolError::NotFound) => missing_tx_indexes.push(i),
226 Err(e) => return Err(e),
227 }
228 }
229
230 Ok(TxpoolReadResponse::TxsForBlock {
231 txs: txs_verification_data,
232 missing: missing_tx_indexes,
233 })
234}
235
236#[inline]
238fn backlog(db: &TxpoolDatabase) -> Result<TxpoolReadResponse, TxPoolError> {
239 let snapshot = db.fjall_database.snapshot();
240
241 let backlog = snapshot
242 .iter(&db.tx_infos)
243 .map(|info| {
244 let (id, tx_info) = info.into_inner()?;
245
246 let tx_info: TransactionInfo = bytemuck::pod_read_unaligned(tx_info.as_ref());
247
248 Ok(TxEntry {
249 id: id.as_ref().try_into().unwrap(),
250 weight: tx_info.weight,
251 fee: tx_info.fee,
252 private: tx_info.flags.private(),
253 received_at: tx_info.received_at,
254 })
255 })
256 .collect::<Result<_, TxPoolError>>()?;
257
258 Ok(TxpoolReadResponse::Backlog(backlog))
259}
260
261#[inline]
263fn size(
264 db: &TxpoolDatabase,
265 include_sensitive_txs: bool,
266) -> Result<TxpoolReadResponse, TxPoolError> {
267 let count = if include_sensitive_txs {
268 db.tx_infos.len()?
269 } else {
270 let mut n = 0_usize;
271 for guard in db.tx_infos.iter() {
272 let info: TransactionInfo = bytemuck::pod_read_unaligned(guard.value()?.as_ref());
273 if !info.flags.private() {
274 n += 1;
275 }
276 }
277 n
278 };
279 Ok(TxpoolReadResponse::Size(count))
280}
281
282fn pool_info(
284 db: &TxpoolDatabase,
285 include_sensitive_txs: bool,
286 max_tx_count: usize,
287 start_time: Option<NonZero<usize>>,
288) -> Result<TxpoolReadResponse, TxPoolError> {
289 Ok(TxpoolReadResponse::PoolInfo(todo!()))
290}
291
292fn txs_by_hash(
294 db: &TxpoolDatabase,
295 tx_hashes: Vec<[u8; 32]>,
296 include_sensitive_txs: bool,
297) -> Result<TxpoolReadResponse, TxPoolError> {
298 let snapshot = db.fjall_database.snapshot();
299 let mut txs = Vec::with_capacity(tx_hashes.len());
300
301 for tx_hash in tx_hashes {
302 let Some(info_bytes) = snapshot.get(&db.tx_infos, tx_hash)? else {
303 continue;
304 };
305 let tx_info: TransactionInfo = bytemuck::pod_read_unaligned(info_bytes.as_ref());
306
307 if !include_sensitive_txs && tx_info.flags.private() {
308 continue;
309 }
310
311 let Some(blob) = snapshot.get(&db.tx_blobs, tx_hash)? else {
312 continue;
313 };
314
315 txs.push(TxInPool {
316 tx_hash,
317 tx_blob: blob.to_vec(),
318 double_spend_seen: tx_info.flags.contains(TxStateFlags::DOUBLE_SPENT),
319 received_timestamp: tx_info.received_at,
320 relayed: !tx_info.flags.private(),
321 });
322 }
323
324 Ok(TxpoolReadResponse::TxsByHash(txs))
325}
326
327fn key_image_spent_in_pool(
329 db: &TxpoolDatabase,
330 snapshot: &fjall::Snapshot,
331 key_image: &[u8; 32],
332 include_sensitive_txs: bool,
333) -> Result<bool, TxPoolError> {
334 let Some(tx_hash) = snapshot.get(&db.spent_key_images, key_image)? else {
335 return Ok(false);
336 };
337
338 if include_sensitive_txs {
339 return Ok(true);
340 }
341
342 let tx_hash: TransactionHash = tx_hash.as_ref().try_into().unwrap();
343 Ok(!in_stem_pool(&tx_hash, snapshot, db)?)
344}
345
346fn key_images_spent(
348 db: &TxpoolDatabase,
349 key_images: HashSet<[u8; 32]>,
350 include_sensitive_txs: bool,
351) -> Result<TxpoolReadResponse, TxPoolError> {
352 let snapshot = db.fjall_database.snapshot();
353
354 #[expect(
355 clippy::iter_over_hash_type,
356 reason = "ordering does not matter, this returns whether any key image is spent"
357 )]
358 for key_image in &key_images {
359 if key_image_spent_in_pool(db, &snapshot, key_image, include_sensitive_txs)? {
360 return Ok(TxpoolReadResponse::KeyImagesSpent(true));
361 }
362 }
363
364 Ok(TxpoolReadResponse::KeyImagesSpent(false))
365}
366
367fn key_images_spent_vec(
369 db: &TxpoolDatabase,
370 key_images: Vec<[u8; 32]>,
371 include_sensitive_txs: bool,
372) -> Result<TxpoolReadResponse, TxPoolError> {
373 let snapshot = db.fjall_database.snapshot();
374
375 Ok(TxpoolReadResponse::KeyImagesSpentVec(
376 key_images
377 .iter()
378 .map(|ki| key_image_spent_in_pool(db, &snapshot, ki, include_sensitive_txs))
379 .collect::<Result<_, _>>()?,
380 ))
381}
382
383fn pool(
385 db: &TxpoolDatabase,
386 include_sensitive_txs: bool,
387) -> Result<TxpoolReadResponse, TxPoolError> {
388 Ok(TxpoolReadResponse::Pool {
389 txs: todo!(),
390 spent_key_images: todo!(),
391 })
392}
393
394fn pool_stats(
396 db: &TxpoolDatabase,
397 include_sensitive_txs: bool,
398) -> Result<TxpoolReadResponse, TxPoolError> {
399 Ok(TxpoolReadResponse::PoolStats(todo!()))
400}
401
402fn all_hashes(
404 db: &TxpoolDatabase,
405 include_sensitive_txs: bool,
406) -> Result<TxpoolReadResponse, TxPoolError> {
407 let mut hashes = Vec::new();
408
409 for guard in db.tx_infos.iter() {
410 let (tx_hash, info) = guard.into_inner()?;
411
412 if !include_sensitive_txs {
413 let info: TransactionInfo = bytemuck::pod_read_unaligned(info.as_ref());
414 if info.flags.private() {
415 continue;
416 }
417 }
418
419 hashes.push(tx_hash.as_ref().try_into().unwrap());
420 }
421
422 Ok(TxpoolReadResponse::AllHashes(hashes))
423}