Skip to main content

cuprate_txpool/service/
read.rs

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/// The txpool [`Service`] read handle.
33#[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
63//---------------------------------------------------------------------------------------------------- Request Mapping
64// This function maps [`Request`]s to function calls
65// executed by the rayon DB reader threadpool.
66
67/// Map [`TxpoolReadRequest`]'s to specific database handler functions.
68///
69/// This is the main entrance into all `Request` handler functions.
70/// The basic structure is:
71/// 1. `Request` is mapped to a handler function
72/// 2. Handler function is called
73/// 3. [`TxpoolReadResponse`] is returned
74fn map_request(
75    db: &TxpoolDatabase,        // Access to the database
76    request: TxpoolReadRequest, // The request we must fulfill
77) -> 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//---------------------------------------------------------------------------------------------------- Handler functions
119// These are the actual functions that do stuff according to the incoming [`TxpoolReadRequest`].
120//
121// Each function name is a 1-1 mapping (from CamelCase -> snake_case) to
122// the enum variant name, e.g: `TxBlob` -> `tx_blob`.
123//
124// Each function will return the [`TxpoolReadResponse`] that we
125// should send back to the caller in [`map_request()`].
126//
127// INVARIANT:
128// These functions are called above in `tower::Service::call()`
129// using a custom threadpool which means any call to `par_*()` functions
130// will be using the custom rayon DB reader thread-pool, not the global one.
131//
132// All functions below assume that this is the case, such that
133// `par_*()` functions will not block the _global_ rayon thread-pool.
134
135/// [`TxpoolReadRequest::TxBlob`].
136#[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/// [`TxpoolReadRequest::TxVerificationData`].
155#[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
166/// [`TxpoolReadRequest::FilterKnownTxBlobHashes`].
167fn 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    // A closure that returns `true` if a tx with a certain blob hash is unknown.
176    // This also fills in `stem_tx_hashes`.
177    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
210/// [`TxpoolReadRequest::TxsForBlock`].
211fn 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/// [`TxpoolReadRequest::Backlog`].
237#[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/// [`TxpoolReadRequest::Size`].
262#[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
282/// [`TxpoolReadRequest::PoolInfo`].
283fn 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
292/// [`TxpoolReadRequest::TxsByHash`].
293fn 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
327/// Returns whether a key image is spent by a transaction in the pool.
328fn 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
346/// [`TxpoolReadRequest::KeyImagesSpent`].
347fn 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
367/// [`TxpoolReadRequest::KeyImagesSpentVec`].
368fn 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
383/// [`TxpoolReadRequest::Pool`].
384fn 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
394/// [`TxpoolReadRequest::PoolStats`].
395fn pool_stats(
396    db: &TxpoolDatabase,
397    include_sensitive_txs: bool,
398) -> Result<TxpoolReadResponse, TxPoolError> {
399    Ok(TxpoolReadResponse::PoolStats(todo!()))
400}
401
402/// [`TxpoolReadRequest::AllHashes`].
403fn 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}