Skip to main content

cuprate_blockchain/service/
read.rs

1//! Database reader thread-pool definitions and logic.
2
3//---------------------------------------------------------------------------------------------------- Import
4use std::{
5    cmp::min,
6    collections::{BTreeMap, HashMap, HashSet},
7    ops::Range,
8    sync::Arc,
9    task::{Context, Poll},
10};
11
12use bytes::Bytes;
13use fjall::Readable;
14use futures::channel::oneshot;
15use indexmap::{IndexMap, IndexSet};
16use rayon::{
17    iter::{Either, IntoParallelIterator, ParallelIterator},
18    prelude::*,
19    ThreadPool,
20};
21use tapes::TapesRead;
22use tower::Service;
23
24use cuprate_helper::{
25    asynch::InfallibleOneshotReceiver,
26    cast::{u64_to_usize, usize_to_u64},
27    map::{combine_low_high_bits_to_u128, split_u128_into_low_high_bits},
28};
29use cuprate_types::{
30    blockchain::{BlockchainReadRequest, BlockchainResponse},
31    output_cache::OutputCache,
32    rpc::{
33        ChainInfo, CoinbaseTxSum, OutputDistributionData, OutputHistogramEntry,
34        OutputHistogramInput,
35    },
36    Chain, ChainId, ExtendedBlockHeader, PreRctOutputDistributionInput, TransactionBlobs,
37    TxInBlockchain, TxsInBlock,
38};
39
40use crate::{
41    error::{BlockchainError, DbResult},
42    ops::{
43        alt_block::{
44            alt_block_height, get_alt_block, get_alt_block_extended_header_from_height,
45            get_alt_block_hash, get_alt_block_information, get_alt_chain_history_ranges,
46        },
47        block::{
48            block_exists, block_height, get_block, get_block_by_hash, get_block_complete_entry,
49            get_block_complete_entry_from_height, get_block_extended_header_from_height,
50        },
51        blockchain::find_split_point,
52        output::{
53            get_num_outputs_with_amount, id_to_output_on_chain, unlocked_and_recent_instances,
54        },
55        tx::{get_split_tx_blobs, get_tx_blob_from_id},
56    },
57    service::{
58        free::{compact_history_genesis_not_included, compact_history_index_to_height_offset},
59        ResponseResult,
60    },
61    types::{
62        AltBlockHeight, AltChainInfo, Amount, AmountIndex, BlockHash, BlockHeight,
63        CompactAltBlockInfo, KeyImage, Output, PreRctOutputId, RawChainId,
64    },
65    BlockchainDatabase,
66};
67
68/// The [`tower::Service`] handle to read from the database.
69#[derive(Clone)]
70pub struct BlockchainReadHandle {
71    /// Handle to the custom `rayon` DB reader thread-pool.
72    ///
73    /// Requests are [`rayon::ThreadPool::spawn`]ed in this thread-pool,
74    /// and responses are returned via a channel we (the caller) provide.
75    pub pool: Arc<ThreadPool>,
76
77    pub blockchain: Arc<BlockchainDatabase>,
78}
79
80impl Service<BlockchainReadRequest> for BlockchainReadHandle {
81    type Response = BlockchainResponse;
82    type Error = BlockchainError;
83    type Future = InfallibleOneshotReceiver<Result<Self::Response, Self::Error>>;
84
85    fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
86        Poll::Ready(Ok(()))
87    }
88
89    fn call(&mut self, req: BlockchainReadRequest) -> Self::Future {
90        let (tx, rx) = oneshot::channel();
91
92        let db = Arc::clone(&self.blockchain);
93        self.pool.spawn(move || {
94            let res = map_request(&db, req);
95
96            let _ = tx.send(res);
97        });
98
99        InfallibleOneshotReceiver::from(rx)
100    }
101}
102
103//---------------------------------------------------------------------------------------------------- Request Mapping
104// This function maps [`Request`]s to function calls
105// executed by the rayon DB reader threadpool.
106
107/// Map [`Request`]'s to specific database handler functions.
108///
109/// This is the main entrance into all `Request` handler functions.
110/// The basic structure is:
111/// 1. `Request` is mapped to a handler function
112/// 2. Handler function is called
113/// 3. [`BlockchainResponse`] is returned
114fn map_request(
115    env: &BlockchainDatabase,       // Access to the database
116    request: BlockchainReadRequest, // The request we must fulfill
117) -> Result<BlockchainResponse, BlockchainError> {
118    use BlockchainReadRequest as R;
119
120    /* SOMEDAY: pre-request handling, run some code for each request? */
121
122    match request {
123        R::BlockCompleteEntries(block_hashes) => block_complete_entries(env, block_hashes),
124        R::BlockCompleteEntriesByHeight(heights) => block_complete_entries_by_height(env, heights),
125        R::BlockCompleteEntriesAboveSplitPoint {
126            chain,
127            start_height,
128            no_miner_tx,
129            len,
130            pruned,
131        } => block_complete_entries_above_split_point(
132            env,
133            &chain,
134            start_height,
135            no_miner_tx,
136            len,
137            pruned,
138        ),
139        R::BlockExtendedHeader(block) => block_extended_header(env, block),
140        R::BlockHash(block, chain) => block_hash(env, block, chain),
141        R::BlockHashInRange(blocks, chain) => block_hash_in_range(env, blocks, chain),
142        R::FindBlock(block_hash) => find_block(env, block_hash),
143        R::FilterUnknownHashes(hashes) => filter_unknown_hashes(env, hashes),
144        R::BlockExtendedHeaderInRange(range, chain) => {
145            block_extended_header_in_range(env, range, chain)
146        }
147        R::ChainHeight => chain_height(env),
148        R::GeneratedCoins(height) => generated_coins(env, height),
149        R::CumulativeRctOutsInRange(range) => cumulative_rct_outs_in_range(env, range),
150        R::Outputs {
151            outputs: map,
152            get_txid,
153        } => outputs(env, map, get_txid),
154        R::OutputsVec { outputs, get_txid } => outputs_vec(env, outputs, get_txid),
155        R::NumberOutputsWithAmount(vec) => number_outputs_with_amount(env, vec),
156        R::KeyImagesSpent(set) => key_images_spent(env, set),
157        R::KeyImagesSpentVec(set) => key_images_spent_vec(env, set),
158        R::CompactChainHistory => compact_chain_history(env),
159        R::NextChainEntry(block_hashes, amount) => next_chain_entry(env, &block_hashes, amount),
160        R::FindFirstUnknown(block_ids) => find_first_unknown(env, &block_ids),
161        R::TxsInBlock {
162            block_hash,
163            tx_indexes,
164        } => txs_in_block(env, block_hash, tx_indexes),
165        R::AltBlocksInChain(chain_id) => alt_blocks_in_chain(env, chain_id),
166        R::Block { height } => block(env, height),
167        R::BlockByHash(hash) => block_by_hash(env, hash),
168        R::TotalTxCount => Ok(total_tx_count(env)),
169        R::DatabaseSize => Ok(database_size(env)),
170        R::OutputHistogram(input) => output_histogram(env, &input),
171        R::CoinbaseTxSum { height, count } => coinbase_tx_sum(env, height, count),
172        R::AltChains => alt_chains(env),
173        R::AltChainCount => alt_chain_count(env),
174        R::Transactions { tx_hashes } => transactions(env, tx_hashes),
175        R::TotalRctOutputs => Ok(total_rct_outputs(env)),
176        R::TxOutputIndexes { tx_hash } => tx_output_indexes(env, &tx_hash),
177        R::PreRctOutputDistribution(input) => pre_rct_output_distribution(env, &input),
178    }
179
180    /* SOMEDAY: post-request handling, run some code for each request? */
181}
182
183//---------------------------------------------------------------------------------------------------- Handler functions
184// These are the actual functions that do stuff according to the incoming [`Request`].
185//
186// Each function name is a 1-1 mapping (from CamelCase -> snake_case) to
187// the enum variant name, e.g: `BlockExtendedHeader` -> `block_extended_header`.
188//
189// Each function will return the [`Response`] that we
190// should send back to the caller in [`map_request()`].
191//
192// INVARIANT:
193// These functions are called above in `tower::Service::call()`
194// using a custom threadpool which means any call to `par_*()` functions
195// will be using the custom rayon DB reader thread-pool, not the global one.
196//
197// All functions below assume that this is the case, such that
198// `par_*()` functions will not block the _global_ rayon thread-pool.
199
200// TODO: The overhead of parallelism may be too much for every request, perfomace test to find optimal
201// amount of parallelism.
202
203/// [`BlockchainReadRequest::BlockCompleteEntries`].
204fn block_complete_entries(db: &BlockchainDatabase, block_hashes: Vec<BlockHash>) -> ResponseResult {
205    let (tx_ro, tapes) = db.read_transactions()?;
206
207    let (missing_hashes, blocks) = block_hashes
208        .into_par_iter()
209        .map(
210            |block_hash| match get_block_complete_entry(db, &block_hash, false, &tx_ro, &tapes) {
211                Err(BlockchainError::NotFound) => Ok(Either::Left(block_hash)),
212                res => res.map(Either::Right),
213            },
214        )
215        .collect::<DbResult<_>>()?;
216
217    let blockchain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
218
219    Ok(BlockchainResponse::BlockCompleteEntries {
220        blocks,
221        missing_hashes,
222        blockchain_height,
223    })
224}
225
226/// [`BlockchainReadRequest::BlockCompleteEntriesAboveSplitPoint`].
227fn block_complete_entries_above_split_point(
228    db: &BlockchainDatabase,
229    chain: &[[u8; 32]],
230    start_height: Option<usize>,
231    no_miner_tx: bool,
232    len: usize,
233    pruned: bool,
234) -> ResponseResult {
235    /// Total size of all block/tx blobs to return before stopping early.
236    ///
237    /// This is lower than monerod, as monerod packs too close to the epee size limit.
238    const MAX_TOTAL_SIZE: usize = 50 * 1024 * 1024;
239    /// Total tx count to return before stopping early.
240    ///
241    /// This is lower than monerod, as monerod packs too close to the epee size limit.
242    const MAX_TOTAL_TXS: usize = 10_000;
243
244    let (tx_ro, tapes) = db.read_transactions()?;
245
246    let blockchain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
247    let top_hash = tapes
248        .read_entry(&db.block_infos, usize_to_u64(blockchain_height - 1))?
249        .ok_or(BlockchainError::NotFound)?
250        .block_hash;
251
252    // If a specific start height was requested, use it directly. Otherwise, scan to find the split point.
253    let height = if let Some(h) = start_height {
254        if h >= blockchain_height {
255            return Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
256                blocks: vec![],
257                output_indices: vec![],
258                blockchain_height,
259                start_height: h,
260                top_hash,
261            });
262        }
263        h
264    } else {
265        let split = find_split_point(db, chain, false, false, &tx_ro)?;
266
267        if split == chain.len() {
268            return Err(BlockchainError::NotFound);
269        }
270
271        block_height(db, &tx_ro, &chain[split])?.ok_or(BlockchainError::NotFound)?
272    };
273
274    if height == blockchain_height {
275        return Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
276            blocks: vec![],
277            output_indices: vec![],
278            blockchain_height,
279            start_height: height,
280            top_hash,
281        });
282    }
283
284    let mut tx_count = 0;
285    let mut total_size = 0;
286
287    let blocks: Vec<_> = (height..min(height + len, blockchain_height))
288        .map_while(|height| {
289            if total_size >= MAX_TOTAL_SIZE || tx_count >= MAX_TOTAL_TXS {
290                return None;
291            }
292
293            let block = match get_block_complete_entry_from_height(height, pruned, &tapes, db) {
294                Ok(v) => v,
295                Err(e) => return Some(Err(e)),
296            };
297
298            tx_count += block.txs.len() + 1;
299
300            let tx_blobs_size = match &block.txs {
301                TransactionBlobs::None => 0,
302                TransactionBlobs::Normal(b) => b.iter().map(Bytes::len).sum(),
303                TransactionBlobs::Pruned(p) => p.iter().map(|p| p.blob.len() + 32).sum(),
304            };
305
306            total_size += block.block.len() + tx_blobs_size;
307
308            Some(Ok(block))
309        })
310        .collect::<DbResult<_>>()?;
311
312    let first_tx_idx = tapes
313        .read_entry(&db.block_infos, usize_to_u64(height))?
314        .ok_or(BlockchainError::NotFound)?
315        .mining_tx_index;
316
317    let mut output_indices = Vec::with_capacity(blocks.len());
318    output_indices.push(Vec::with_capacity(8));
319
320    let mut last_height = height;
321    let mut miner_tx = true;
322
323    for (i, tx_info) in tapes.iter_from(&db.tx_infos, first_tx_idx)?.enumerate() {
324        let tx_info = tx_info?;
325
326        if tx_info.height != last_height {
327            if tx_info.height == height + blocks.len() {
328                // We have gone past all txs in the blocks we need
329                break;
330            }
331            last_height = tx_info.height;
332            miner_tx = true;
333            output_indices.push(Vec::with_capacity(8));
334        }
335
336        // monerod replaces the miner tx's indices with an empty
337        // placeholder when `no_miner_tx` is set.
338        if no_miner_tx && miner_tx {
339            miner_tx = false;
340            output_indices.last_mut().unwrap().push(vec![]);
341            continue;
342        }
343        miner_tx = false;
344
345        let o_indexes = if tx_info.is_v1_tx() {
346            // For v1 txs we need to look up indexes.
347            let res = tx_ro
348                .get(
349                    &db.v1_tx_outputs,
350                    (first_tx_idx + usize_to_u64(i)).to_le_bytes(),
351                )?
352                .ok_or(BlockchainError::NotFound)?;
353
354            res.chunks(8)
355                .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
356                .collect::<Vec<_>>()
357        } else {
358            // For v2 we can use the data in the tx_info.
359            (0..tx_info.numb_rct_outputs)
360                .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
361                .collect()
362        };
363
364        output_indices.last_mut().unwrap().push(o_indexes);
365    }
366
367    Ok(BlockchainResponse::BlockCompleteEntriesAboveSplitPoint {
368        blocks,
369        output_indices,
370        blockchain_height,
371        start_height: height,
372        top_hash,
373    })
374}
375
376/// [`BlockchainReadRequest::BlockCompleteEntriesByHeight`].
377fn block_complete_entries_by_height(
378    db: &BlockchainDatabase,
379    block_heights: Vec<BlockHeight>,
380) -> ResponseResult {
381    let tapes = db.linear_tapes.reader();
382
383    let blocks = block_heights
384        .into_par_iter()
385        .map(|height| get_block_complete_entry_from_height(height, false, &tapes, db))
386        .collect::<DbResult<_>>()?;
387
388    Ok(BlockchainResponse::BlockCompleteEntriesByHeight(blocks))
389}
390
391/// [`BlockchainReadRequest::BlockExtendedHeader`].
392#[inline]
393fn block_extended_header(db: &BlockchainDatabase, block_height: BlockHeight) -> ResponseResult {
394    let tapes = db.linear_tapes.reader();
395
396    Ok(BlockchainResponse::BlockExtendedHeader(
397        get_block_extended_header_from_height(block_height, &tapes, db)?,
398    ))
399}
400
401/// [`BlockchainReadRequest::BlockHash`].
402#[inline]
403fn block_hash(db: &BlockchainDatabase, block_height: BlockHeight, chain: Chain) -> ResponseResult {
404    let (tx_ro, tapes) = db.read_transactions()?;
405
406    let block_hash = match chain {
407        Chain::Main => {
408            tapes
409                .read_entry(&db.block_infos, usize_to_u64(block_height))?
410                .ok_or(BlockchainError::NotFound)?
411                .block_hash
412        }
413        Chain::Alt(chain) => get_alt_block_hash(db, &block_height, chain, &tx_ro, &tapes)?,
414    };
415
416    Ok(BlockchainResponse::BlockHash(block_hash))
417}
418
419/// [`BlockchainReadRequest::BlockHashInRange`].
420#[inline]
421fn block_hash_in_range(
422    db: &BlockchainDatabase,
423    range: Range<usize>,
424    chain: Chain,
425) -> ResponseResult {
426    let (tx_ro, tapes) = db.read_transactions()?;
427
428    if range.is_empty() {
429        return Ok(BlockchainResponse::BlockHashInRange(vec![]));
430    }
431
432    let block_hash = match chain {
433        Chain::Main => tapes
434            .iter_from(&db.block_infos, usize_to_u64(range.start))?
435            .map(|info| Ok(info?.block_hash))
436            .take(range.len())
437            .collect::<Result<_, BlockchainError>>()?,
438        Chain::Alt(chain) => range
439            .into_par_iter()
440            .map(|block_height| get_alt_block_hash(db, &block_height, chain, &tx_ro, &tapes))
441            .collect::<DbResult<Vec<_>>>()?,
442    };
443
444    Ok(BlockchainResponse::BlockHashInRange(block_hash))
445}
446
447/// [`BlockchainReadRequest::FindBlock`]
448fn find_block(db: &BlockchainDatabase, block_hash: BlockHash) -> ResponseResult {
449    let tx_ro = db.fjall.snapshot();
450
451    // Check the main chain first, then alt chains.
452    let location = if let Some(height) = block_height(db, &tx_ro, &block_hash)? {
453        Some((Chain::Main, height))
454    } else {
455        alt_block_height(db, &tx_ro, &block_hash)?
456            .map(|alt| (Chain::Alt(alt.chain_id.into()), alt.height))
457    };
458
459    Ok(BlockchainResponse::FindBlock(location))
460}
461
462/// [`BlockchainReadRequest::FilterUnknownHashes`].
463#[inline]
464fn filter_unknown_hashes(
465    db: &BlockchainDatabase,
466    mut hashes: HashSet<BlockHash>,
467) -> ResponseResult {
468    let tx_ro = db.fjall.snapshot();
469
470    let mut err = None;
471
472    hashes.retain(|block_hash| match block_exists(db, block_hash, &tx_ro) {
473        Ok(exists) => exists,
474        Err(e) => {
475            err.get_or_insert(e);
476            false
477        }
478    });
479
480    if let Some(e) = err {
481        Err(e)
482    } else {
483        Ok(BlockchainResponse::FilterUnknownHashes(hashes))
484    }
485}
486
487/// [`BlockchainReadRequest::BlockExtendedHeaderInRange`].
488#[inline]
489fn block_extended_header_in_range(
490    db: &BlockchainDatabase,
491    range: Range<BlockHeight>,
492    chain: Chain,
493) -> ResponseResult {
494    let (tx_ro, tapes) = db.read_transactions()?;
495
496    // Collect results using `rayon`.
497    let vec = match chain {
498        Chain::Main => range
499            .into_iter()
500            .map(|block_height| get_block_extended_header_from_height(block_height, &tapes, db))
501            .collect::<DbResult<Vec<ExtendedBlockHeader>>>()?,
502        Chain::Alt(chain_id) => {
503            let ranges = { get_alt_chain_history_ranges(db, range, chain_id, &tx_ro)? };
504
505            ranges
506                .iter()
507                .rev()
508                .flat_map(|(chain, range)| {
509                    range.clone().map(|height| match *chain {
510                        Chain::Main => get_block_extended_header_from_height(height, &tapes, db),
511                        Chain::Alt(chain_id) => get_alt_block_extended_header_from_height(
512                            db,
513                            &AltBlockHeight {
514                                chain_id: chain_id.into(),
515                                height,
516                            },
517                            &tx_ro,
518                        ),
519                    })
520                })
521                .collect::<DbResult<Vec<_>>>()?
522        }
523    };
524
525    Ok(BlockchainResponse::BlockExtendedHeaderInRange(vec))
526}
527
528/// [`BlockchainReadRequest::ChainHeight`].
529#[inline]
530fn chain_height(db: &BlockchainDatabase) -> ResponseResult {
531    let tapes = db.linear_tapes.reader();
532
533    let chain_height = tapes
534        .fixed_sized_tape_len(&db.block_infos)
535        .expect("Required tape not found");
536
537    if chain_height == 0 {
538        return Err(BlockchainError::NotFound);
539    }
540
541    let block_hash = tapes
542        .read_entry(&db.block_infos, chain_height - 1)?
543        .ok_or(BlockchainError::NotFound)?
544        .block_hash;
545
546    Ok(BlockchainResponse::ChainHeight(
547        chain_height.try_into().unwrap(),
548        block_hash,
549    ))
550}
551
552/// [`BlockchainReadRequest::GeneratedCoins`].
553#[inline]
554fn generated_coins(db: &BlockchainDatabase, height: usize) -> ResponseResult {
555    let tapes = db.linear_tapes.reader();
556
557    Ok(BlockchainResponse::GeneratedCoins(
558        tapes
559            .read_entry(&db.block_infos, usize_to_u64(height))?
560            .map_or(0, |info| info.cumulative_generated_coins),
561    ))
562}
563
564/// [`BlockchainReadRequest::CumulativeRctOutsInRange`].
565#[inline]
566fn cumulative_rct_outs_in_range(db: &BlockchainDatabase, range: Range<usize>) -> ResponseResult {
567    let tapes = db.linear_tapes.reader();
568
569    let cumulative_rct_outs = tapes
570        .iter_from(&db.block_infos, usize_to_u64(range.start))?
571        .take(range.len())
572        .map(|info| Ok(info?.cumulative_rct_outs))
573        .collect::<DbResult<Vec<u64>>>()?;
574
575    if cumulative_rct_outs.len() < range.len() {
576        return Err(BlockchainError::NotFound);
577    }
578
579    Ok(BlockchainResponse::CumulativeRctOutsInRange(
580        cumulative_rct_outs,
581    ))
582}
583
584/// [`BlockchainReadRequest::Outputs`].
585#[inline]
586fn outputs(
587    db: &BlockchainDatabase,
588    outputs: IndexMap<Amount, IndexSet<AmountIndex>>,
589    get_txid: bool,
590) -> ResponseResult {
591    // Prepare tx/tables in `ThreadLocal`.
592
593    let (tx_ro, tapes) = db.read_transactions()?;
594
595    let amount_of_outs = outputs
596        .par_iter()
597        .map(|(&amount, _)| {
598            if amount == 0 {
599                Ok((
600                    amount,
601                    tapes
602                        .fixed_sized_tape_len(&db.rct_outputs)
603                        .expect("Required tape not found"),
604                ))
605            } else {
606                // v1 transactions.
607                match get_num_outputs_with_amount(db, &tx_ro, amount) {
608                    Ok(count) => Ok((amount, count)),
609                    Err(e) => Err(e),
610                }
611            }
612        })
613        .collect::<Result<_, _>>()?;
614
615    // The 2nd mapping function.
616    // This is pulled out from the below `map()` for readability.
617    let inner_map = |amount, amount_index| {
618        let id = PreRctOutputId {
619            amount,
620            amount_index,
621        };
622
623        let output_on_chain = match id_to_output_on_chain(db, &id, get_txid, &tx_ro, &tapes) {
624            Ok(output) => output,
625            Err(BlockchainError::NotFound) => return Ok(Either::Right(amount_index)),
626            Err(e) => return Err(e),
627        };
628
629        Ok(Either::Left((amount_index, output_on_chain)))
630    };
631
632    let (map, wanted_outputs) = outputs
633        .into_par_iter()
634        .map(|(amount, amount_index_set)| {
635            let (left, right) = amount_index_set
636                .into_par_iter()
637                .map(|amount_index| inner_map(amount, amount_index))
638                .collect::<Result<_, _>>()?;
639
640            Ok(((amount, left), (amount, right)))
641        })
642        .collect::<DbResult<(IndexMap<_, IndexMap<_, _>>, IndexMap<_, IndexSet<_>>)>>()?;
643
644    let cache = OutputCache::new(map, amount_of_outs, wanted_outputs);
645
646    Ok(BlockchainResponse::Outputs(cache))
647}
648
649/// [`BlockchainReadRequest::OutputsVec`].
650#[inline]
651fn outputs_vec(
652    db: &BlockchainDatabase,
653    outputs: Vec<(Amount, AmountIndex)>,
654    get_txid: bool,
655) -> ResponseResult {
656    let (tx_ro, tapes) = db.read_transactions()?;
657
658    let result = outputs
659        .into_iter()
660        .map(|(amount, amount_index)| {
661            let id = PreRctOutputId {
662                amount,
663                amount_index,
664            };
665            let output = id_to_output_on_chain(db, &id, get_txid, &tx_ro, &tapes)?;
666            Ok((amount, vec![(amount_index, output)]))
667        })
668        .collect::<DbResult<Vec<_>>>()?;
669
670    Ok(BlockchainResponse::OutputsVec(result))
671}
672
673/// [`BlockchainReadRequest::NumberOutputsWithAmount`].
674#[inline]
675fn number_outputs_with_amount(db: &BlockchainDatabase, amounts: Vec<Amount>) -> ResponseResult {
676    let (tx_ro, tapes) = db.read_transactions()?;
677
678    // Cache the amount of RCT outputs once.
679    let num_rct_outputs = u64_to_usize(
680        tapes
681            .fixed_sized_tape_len(&db.rct_outputs)
682            .expect("Required tape not found"),
683    );
684
685    // Collect results using `rayon`.
686    let map = amounts
687        .into_par_iter()
688        .map(|amount| {
689            if amount == 0 {
690                // v2 transactions.
691                Ok((amount, num_rct_outputs))
692            } else {
693                // v1 transactions.
694                match get_num_outputs_with_amount(db, &tx_ro, amount) {
695                    Ok(count) => Ok((amount, u64_to_usize(count))),
696                    Err(e) => Err(e),
697                }
698            }
699        })
700        .collect::<DbResult<HashMap<Amount, usize>>>()?;
701
702    Ok(BlockchainResponse::NumberOutputsWithAmount(map))
703}
704
705/// [`BlockchainReadRequest::KeyImagesSpent`].
706#[inline]
707fn key_images_spent(db: &BlockchainDatabase, key_images: HashSet<KeyImage>) -> ResponseResult {
708    let tx_ro = db.fjall.snapshot();
709
710    // FIXME:
711    // Create/use `enum cuprate_types::Exist { Does, DoesNot }`
712    // or similar instead of `bool` for clarity.
713    // <https://github.com/Cuprate/cuprate/pull/113#discussion_r1581536526>
714    //
715    // Collect results using `rayon`.
716    match key_images
717        .into_par_iter()
718        .map(|ki| tx_ro.contains_key(&db.key_images, ki))
719        // If the result is either:
720        // `Ok(true)` => a key image was found, return early
721        // `Err` => an error was found, return early
722        //
723        // Else, `Ok(false)` will continue the iterator.
724        .find_any(|result| !matches!(result, Ok(false)))
725    {
726        None => Ok(BlockchainResponse::KeyImagesSpent(false)), // Key image was NOT found.
727        Some(Ok(true)) => Ok(BlockchainResponse::KeyImagesSpent(true)), // Key image was found.
728        Some(Err(e)) => Err(e.into()),                         // A database error occurred.
729        Some(Ok(false)) => unreachable!(),
730    }
731}
732
733/// [`BlockchainReadRequest::KeyImagesSpentVec`].
734fn key_images_spent_vec(db: &BlockchainDatabase, key_images: Vec<KeyImage>) -> ResponseResult {
735    let tx_ro = db.fjall.snapshot();
736
737    // Collect results using `rayon`.
738    Ok(BlockchainResponse::KeyImagesSpentVec(
739        key_images
740            .into_par_iter()
741            .map(|ki| tx_ro.contains_key(&db.key_images, ki))
742            .collect::<Result<_, _>>()?,
743    ))
744}
745
746/// [`BlockchainReadRequest::CompactChainHistory`]
747fn compact_chain_history(db: &BlockchainDatabase) -> ResponseResult {
748    let tapes = db.linear_tapes.reader();
749
750    let get_block_info = |height| -> Result<_, BlockchainError> {
751        tapes
752            .read_entry(&db.block_infos, height)?
753            .ok_or(BlockchainError::NotFound)
754    };
755
756    let top_block_height = tapes
757        .fixed_sized_tape_len(&db.block_infos)
758        .expect("Required tape not open")
759        - 1;
760
761    let top_block_info = get_block_info(top_block_height)?;
762    let cumulative_difficulty = combine_low_high_bits_to_u128(
763        top_block_info.cumulative_difficulty_low,
764        top_block_info.cumulative_difficulty_high,
765    );
766
767    /// The amount of top block IDs in the compact chain.
768    const INITIAL_BLOCKS: usize = 11;
769
770    // rayon is not used here because the amount of block IDs is expected to be small.
771    let mut block_ids = (0..)
772        .map(compact_history_index_to_height_offset::<INITIAL_BLOCKS>)
773        .map_while(|i| top_block_height.checked_sub(usize_to_u64(i)))
774        .map(|height| Ok(get_block_info(height)?.block_hash))
775        .collect::<DbResult<Vec<_>>>()?;
776
777    if compact_history_genesis_not_included::<INITIAL_BLOCKS>(u64_to_usize(top_block_height)) {
778        block_ids.push(get_block_info(0)?.block_hash);
779    }
780
781    Ok(BlockchainResponse::CompactChainHistory {
782        cumulative_difficulty,
783        block_ids,
784    })
785}
786
787/// [`BlockchainReadRequest::NextChainEntry`]
788///
789/// # Invariant
790/// `block_ids` must be sorted in reverse chronological block order, or else
791/// the returned result is unspecified and meaningless, as this function
792/// performs a binary search.
793fn next_chain_entry(
794    db: &BlockchainDatabase,
795    block_ids: &[BlockHash],
796    next_entry_size: usize,
797) -> ResponseResult {
798    let (tx_ro, tapes) = db.read_transactions()?;
799
800    let idx = find_split_point(db, block_ids, false, false, &tx_ro)?;
801
802    // This will happen if we have a different genesis block.
803    if idx == block_ids.len() {
804        return Ok(BlockchainResponse::NextChainEntry {
805            start_height: None,
806            chain_height: 0,
807            block_ids: vec![],
808            block_weights: vec![],
809            cumulative_difficulty: 0,
810            first_block_blob: None,
811        });
812    }
813
814    // The returned chain entry must overlap with one of the blocks  we were told about.
815    let first_known_block_hash = block_ids[idx];
816    let first_known_height =
817        block_height(db, &tx_ro, &first_known_block_hash)?.ok_or(BlockchainError::NotFound)?;
818
819    let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
820    let last_height_in_chain_entry = min(first_known_height + next_entry_size, chain_height);
821
822    let entry_count = last_height_in_chain_entry - first_known_height;
823    let mut block_infos = vec![crate::types::BlockInfo::default(); entry_count];
824    tapes.read_entries(
825        &db.block_infos,
826        usize_to_u64(first_known_height),
827        &mut block_infos,
828    )?;
829
830    let (block_ids, block_weights) = block_infos
831        .iter()
832        .map(|info| (info.block_hash, info.weight))
833        .unzip::<_, _, Vec<_>, Vec<_>>();
834
835    let top_block_info = tapes
836        .read_entry(&db.block_infos, usize_to_u64(chain_height) - 1)?
837        .ok_or(BlockchainError::NotFound)?;
838
839    let first_block_blob = if block_ids.len() >= 2 {
840        Some(get_block(&(first_known_height + 1), None, &tapes, db)?.serialize())
841    } else {
842        None
843    };
844
845    Ok(BlockchainResponse::NextChainEntry {
846        start_height: Some(first_known_height),
847        chain_height,
848        block_ids,
849        block_weights,
850        cumulative_difficulty: combine_low_high_bits_to_u128(
851            top_block_info.cumulative_difficulty_low,
852            top_block_info.cumulative_difficulty_high,
853        ),
854        first_block_blob,
855    })
856}
857
858/// [`BlockchainReadRequest::FindFirstUnknown`]
859///
860/// # Invariant
861/// `block_ids` must be sorted in chronological block order, or else
862/// the returned result is unspecified and meaningless, as this function
863/// performs a binary search.
864fn find_first_unknown(db: &BlockchainDatabase, block_ids: &[BlockHash]) -> ResponseResult {
865    let tx_ro = db.fjall.snapshot();
866    let idx = find_split_point(db, block_ids, true, true, &tx_ro)?;
867
868    Ok(if idx == block_ids.len() {
869        BlockchainResponse::FindFirstUnknown(None)
870    } else if idx == 0 {
871        BlockchainResponse::FindFirstUnknown(Some((0, 0)))
872    } else {
873        let last_known_height = usize::from_le_bytes(
874            tx_ro
875                .get(&db.block_heights, block_ids[idx - 1])?
876                .unwrap()
877                .as_ref()
878                .try_into()
879                .unwrap(),
880        );
881
882        BlockchainResponse::FindFirstUnknown(Some((idx, last_known_height + 1)))
883    })
884}
885
886/// [`BlockchainReadRequest::TxsInBlock`]
887fn txs_in_block(
888    db: &BlockchainDatabase,
889    block_hash: BlockHash,
890    missing_txs: Vec<u64>,
891) -> ResponseResult {
892    let (tx_ro, tapes) = db.read_transactions()?;
893
894    // Check the main chain first, fall back to alt blocks if not found.
895    let (block, txs) = if let Some(block_height) = block_height(db, &tx_ro, &block_hash)? {
896        let block_info = tapes
897            .read_entry(&db.block_infos, usize_to_u64(block_height))?
898            .ok_or(BlockchainError::NotFound)?;
899
900        let block = get_block(&block_height, Some(&block_info), &tapes, db)?;
901        let first_tx_index = block_info.mining_tx_index + 1;
902
903        if block.transactions.len() < missing_txs.len()
904            || missing_txs
905                .iter()
906                .any(|&i| i >= usize_to_u64(block.transactions.len()))
907        {
908            return Ok(BlockchainResponse::TxsInBlock(None));
909        }
910
911        let txs = missing_txs
912            .into_iter()
913            .map(|index_offset| get_tx_blob_from_id(&(first_tx_index + index_offset), &tapes, db))
914            .collect::<DbResult<_>>()?;
915
916        (block, txs)
917    } else {
918        let alt_block_height =
919            alt_block_height(db, &tx_ro, &block_hash)?.ok_or(BlockchainError::NotFound)?;
920
921        let block = get_alt_block(db, &alt_block_height, &tx_ro)?;
922
923        if block.transactions.len() < missing_txs.len()
924            || missing_txs
925                .iter()
926                .any(|&i| i >= usize_to_u64(block.transactions.len()))
927        {
928            return Ok(BlockchainResponse::TxsInBlock(None));
929        }
930
931        let alt_transaction_blobs = db.alt_transaction_blobs.load();
932        let txs = missing_txs
933            .into_iter()
934            .map(|index_offset| {
935                let tx_hash = &block.transactions[u64_to_usize(index_offset)];
936
937                tx_ro
938                    .get(&**alt_transaction_blobs, tx_hash)?
939                    .map(|blob| blob.to_vec())
940                    .ok_or(BlockchainError::NotFound)
941            })
942            .collect::<DbResult<_>>()?;
943
944        (block, txs)
945    };
946
947    Ok(BlockchainResponse::TxsInBlock(Some(TxsInBlock {
948        block: block.serialize(),
949        txs,
950    })))
951}
952
953/// [`BlockchainReadRequest::AltBlocksInChain`]
954fn alt_blocks_in_chain(db: &BlockchainDatabase, chain_id: ChainId) -> ResponseResult {
955    let tx_ro = db.fjall.snapshot();
956
957    // Get the history of this alt-chain.
958    let history = { get_alt_chain_history_ranges(db, 0..usize::MAX, chain_id, &tx_ro)? };
959
960    // Get all the blocks until we join the main-chain.
961    let blocks = history
962        .iter()
963        .rev()
964        .skip(1)
965        .flat_map(|(chain_id, range)| {
966            let Chain::Alt(chain_id) = chain_id else {
967                panic!("Should not have main chain blocks here we skipped last range");
968            };
969
970            range.clone().map(|height| {
971                get_alt_block_information(
972                    db,
973                    &AltBlockHeight {
974                        chain_id: (*chain_id).into(),
975                        height,
976                    },
977                    &tx_ro,
978                )
979            })
980        })
981        .collect::<DbResult<_>>()?;
982
983    Ok(BlockchainResponse::AltBlocksInChain(blocks))
984}
985
986/// [`BlockchainReadRequest::Block`]
987fn block(db: &BlockchainDatabase, block_height: BlockHeight) -> ResponseResult {
988    let tapes = db.linear_tapes.reader();
989
990    Ok(BlockchainResponse::Block(get_block(
991        &block_height,
992        None,
993        &tapes,
994        db,
995    )?))
996}
997
998/// [`BlockchainReadRequest::BlockByHash`]
999fn block_by_hash(db: &BlockchainDatabase, block_hash: BlockHash) -> ResponseResult {
1000    let (tx_ro, tapes) = db.read_transactions()?;
1001
1002    Ok(BlockchainResponse::Block(get_block_by_hash(
1003        db,
1004        &block_hash,
1005        &tx_ro,
1006        &tapes,
1007    )?))
1008}
1009
1010/// [`BlockchainReadRequest::TotalTxCount`]
1011fn total_tx_count(db: &BlockchainDatabase) -> BlockchainResponse {
1012    let tapes = db.linear_tapes.reader();
1013    let count = u64_to_usize(
1014        tapes
1015            .fixed_sized_tape_len(&db.tx_infos)
1016            .expect("tx_infos tape exists"),
1017    );
1018
1019    BlockchainResponse::TotalTxCount(count)
1020}
1021
1022/// Walk a directory recursively and sum the sizes of all files.
1023fn dir_size(path: &std::path::Path) -> u64 {
1024    let Ok(entries) = std::fs::read_dir(path) else {
1025        return 0;
1026    };
1027    entries
1028        .filter_map(Result::ok)
1029        .map(|e| match e.file_type() {
1030            Ok(ft) if ft.is_dir() => dir_size(&e.path()),
1031            Ok(ft) if ft.is_file() => e.metadata().map_or(0, |m| m.len()),
1032            _ => 0,
1033        })
1034        .sum()
1035}
1036
1037/// [`BlockchainReadRequest::DatabaseSize`]
1038fn database_size(db: &BlockchainDatabase) -> BlockchainResponse {
1039    // Sum file sizes in both data directories (blob and index).
1040    let blob_size = dir_size(&db.config.blob_dir);
1041    let index_size = if db.config.index_dir == db.config.blob_dir {
1042        0
1043    } else {
1044        dir_size(&db.config.index_dir)
1045    };
1046    let database_size = blob_size + index_size;
1047
1048    // TODO:
1049    let free_space = u64::MAX;
1050
1051    BlockchainResponse::DatabaseSize {
1052        database_size,
1053        free_space,
1054    }
1055}
1056
1057/// [`BlockchainReadRequest::OutputHistogram`]
1058fn output_histogram(db: &BlockchainDatabase, input: &OutputHistogramInput) -> ResponseResult {
1059    let (tx_ro, tapes) = db.read_transactions()?;
1060
1061    let num_rct_outputs = tapes
1062        .fixed_sized_tape_len(&db.rct_outputs)
1063        .expect("rct_outputs tape exists");
1064
1065    let amounts_and_counts: BTreeMap<Amount, u64> = if input.amounts.is_empty() {
1066        let mut result = BTreeMap::new();
1067
1068        // RCT outputs are represented as amount = 0 and live in a separate tape.
1069        if num_rct_outputs > 0 {
1070            result.insert(0_u64, num_rct_outputs);
1071        }
1072
1073        // We need to get the amount of outputs for each amount. We do this by first finding the next
1074        // `amount` value in the sorted table, then calling `get_num_outputs_with_amount`, then using
1075        // fjalls `range` to find the next amount value.
1076        let mut next = tx_ro.first_key_value(&db.pre_rct_outputs);
1077        while let Some(guard) = next {
1078            let amount = Amount::from_be_bytes(guard.key()?[..8].try_into().unwrap());
1079
1080            result.insert(amount, get_num_outputs_with_amount(db, &tx_ro, amount)?);
1081
1082            // Seek the first key of the next amount group, stopping on overflow/end.
1083            next = match amount.checked_add(1) {
1084                Some(next_amount) => tx_ro
1085                    .range(&db.pre_rct_outputs, next_amount.to_be_bytes()..)
1086                    .next(),
1087                None => None,
1088            };
1089        }
1090
1091        result
1092    } else {
1093        // Use the caller-specified amounts.
1094        input
1095            .amounts
1096            .iter()
1097            .map(|&amount| {
1098                let count = if amount == 0 {
1099                    num_rct_outputs
1100                } else {
1101                    get_num_outputs_with_amount(db, &tx_ro, amount)?
1102                };
1103                Ok((amount, count))
1104            })
1105            .collect::<DbResult<_>>()?
1106    };
1107
1108    let current_height = tapes
1109        .fixed_sized_tape_len(&db.block_infos)
1110        .expect("block_infos tape exists");
1111
1112    let histogram = amounts_and_counts
1113        .into_iter()
1114        .filter(|&(_, total_instances)| {
1115            // filter the amounts that have to many or too little outputs
1116            (input.min_count == 0 || total_instances >= input.min_count)
1117                && (input.max_count == 0 || total_instances <= input.max_count)
1118        })
1119        .map(|(amount, total_instances)| {
1120            let (unlocked_instances, recent_instances) =
1121                if input.unlocked || input.recent_cutoff > 0 {
1122                    unlocked_and_recent_instances(
1123                        db,
1124                        &tx_ro,
1125                        &tapes,
1126                        amount,
1127                        total_instances,
1128                        current_height,
1129                        input.recent_cutoff,
1130                    )?
1131                } else {
1132                    (0, 0)
1133                };
1134
1135            Ok(OutputHistogramEntry {
1136                amount,
1137                total_instances,
1138                unlocked_instances,
1139                recent_instances,
1140            })
1141        })
1142        .collect::<DbResult<_>>()?;
1143
1144    Ok(BlockchainResponse::OutputHistogram(histogram))
1145}
1146
1147/// [`BlockchainReadRequest::CoinbaseTxSum`]
1148fn coinbase_tx_sum(db: &BlockchainDatabase, height: usize, count: u64) -> ResponseResult {
1149    let tapes = db.linear_tapes.reader();
1150
1151    let start_cumulative = if height == 0 {
1152        0_u64
1153    } else {
1154        tapes
1155            .read_entry(&db.block_infos, usize_to_u64(height - 1))?
1156            .ok_or(BlockchainError::NotFound)?
1157            .cumulative_generated_coins
1158    };
1159
1160    let (emission_amount, fee_amount, _) = (height..)
1161        .zip(tapes.iter_from(&db.block_infos, usize_to_u64(height))?)
1162        .take(u64_to_usize(count))
1163        .try_fold(
1164            (0_u128, 0_u128, start_cumulative),
1165            |(emission_amount, fee_amount, prev_cumulative), (h, block_info)| {
1166                let block_info = block_info?;
1167
1168                // base_reward = newly minted coins for this block (does not include fees).
1169                let base_reward = block_info
1170                    .cumulative_generated_coins
1171                    .saturating_sub(prev_cumulative);
1172
1173                // coinbase_output = sum of miner_tx output amounts = base_reward + block_fees.
1174                let block = get_block(&h, Some(&block_info), &tapes, db)?;
1175                let coinbase_output: u64 = block
1176                    .miner_transaction()
1177                    .prefix()
1178                    .outputs
1179                    .iter()
1180                    .map(|o| o.amount.unwrap_or(0))
1181                    .sum();
1182
1183                DbResult::Ok((
1184                    emission_amount + u128::from(base_reward),
1185                    fee_amount + u128::from(coinbase_output.saturating_sub(base_reward)),
1186                    block_info.cumulative_generated_coins,
1187                ))
1188            },
1189        )?;
1190
1191    let (emission_amount, emission_amount_top64) = split_u128_into_low_high_bits(emission_amount);
1192    let (fee_amount, fee_amount_top64) = split_u128_into_low_high_bits(fee_amount);
1193
1194    Ok(BlockchainResponse::CoinbaseTxSum(CoinbaseTxSum {
1195        emission_amount,
1196        emission_amount_top64,
1197        fee_amount,
1198        fee_amount_top64,
1199    }))
1200}
1201
1202/// [`BlockchainReadRequest::AltChains`]
1203fn alt_chains(db: &BlockchainDatabase) -> ResponseResult {
1204    let (tx_ro, tapes) = db.read_transactions()?;
1205    let alt_chain_infos = db.alt_chain_infos.load();
1206    let alt_block_infos = db.alt_block_infos.load();
1207
1208    let mut chains = Vec::new();
1209
1210    for guard in alt_chain_infos.iter() {
1211        let (chain_id_bytes, chain_info_bytes) = guard.into_inner()?;
1212
1213        let chain_id = RawChainId(u64::from_le_bytes(
1214            chain_id_bytes.as_ref().try_into().unwrap(),
1215        ));
1216        let chain_info: AltChainInfo = bytemuck::pod_read_unaligned(chain_info_bytes.as_ref());
1217
1218        let tip = chain_info.chain_height - 1;
1219        let history =
1220            get_alt_chain_history_ranges(db, 0..chain_info.chain_height, chain_id.into(), &tx_ro)?;
1221
1222        let tip_height = AltBlockHeight {
1223            chain_id,
1224            height: tip,
1225        };
1226        let tip_info_bytes = tx_ro
1227            .get(&**alt_block_infos, bytemuck::bytes_of(&tip_height))?
1228            .ok_or(BlockchainError::NotFound)?;
1229        let tip_info: CompactAltBlockInfo = bytemuck::pod_read_unaligned(tip_info_bytes.as_ref());
1230
1231        // The last segment is the main-chain portion. Its range starts at 0 and ends at the
1232        // fork height, so the last main chain block = range.end - 1.
1233        let main_fork_height = history
1234            .last()
1235            .map(|(_, r)| r.end.saturating_sub(1))
1236            .ok_or(BlockchainError::NotFound)?;
1237
1238        // Build the hashes of the alt chain all the way to the main chain split point.
1239        let mut block_hashes = Vec::new();
1240        for (segment_chain, height_range) in &history {
1241            let Chain::Alt(segment_chain_id) = segment_chain else {
1242                break;
1243            };
1244            let raw_id = RawChainId::from(*segment_chain_id);
1245            for height in height_range.clone().rev() {
1246                let alt_h = AltBlockHeight {
1247                    chain_id: raw_id,
1248                    height,
1249                };
1250                let info_bytes = tx_ro
1251                    .get(&**alt_block_infos, bytemuck::bytes_of(&alt_h))?
1252                    .ok_or(BlockchainError::NotFound)?;
1253                let info: CompactAltBlockInfo = bytemuck::pod_read_unaligned(info_bytes.as_ref());
1254                block_hashes.push(info.block_hash);
1255            }
1256        }
1257
1258        // Get the main chain block hash at the fork point.
1259        let main_chain_parent_block = tapes
1260            .read_entry(&db.block_infos, usize_to_u64(main_fork_height))?
1261            .map(|info| info.block_hash)
1262            .ok_or(BlockchainError::NotFound)?;
1263
1264        let length = usize_to_u64(block_hashes.len());
1265
1266        chains.push(ChainInfo {
1267            block_hash: tip_info.block_hash,
1268            block_hashes,
1269            difficulty: tip_info.cumulative_difficulty_low,
1270            difficulty_top64: tip_info.cumulative_difficulty_high,
1271            height: usize_to_u64(tip),
1272            length,
1273            main_chain_parent_block,
1274        });
1275    }
1276
1277    Ok(BlockchainResponse::AltChains(chains))
1278}
1279
1280/// [`BlockchainReadRequest::AltChainCount`]
1281fn alt_chain_count(db: &BlockchainDatabase) -> ResponseResult {
1282    let count = db.alt_chain_infos.load().len()?;
1283    Ok(BlockchainResponse::AltChainCount(count))
1284}
1285
1286/// [`BlockchainReadRequest::Transactions`]
1287fn transactions(db: &BlockchainDatabase, tx_hashes: Vec<[u8; 32]>) -> ResponseResult {
1288    let (tx_ro, tapes) = db.read_transactions()?;
1289    let chain_height = crate::ops::blockchain::chain_height(db, &tapes)?;
1290
1291    let mut txs = Vec::with_capacity(tx_hashes.len());
1292    let mut missed_txs = Vec::new();
1293    let mut block_timestamps = HashMap::new();
1294
1295    for tx_hash in tx_hashes {
1296        let Some(tx_id) = tx_ro.get(&db.tx_ids, tx_hash)? else {
1297            missed_txs.push(tx_hash);
1298            continue;
1299        };
1300
1301        let tx_id = u64::from_le_bytes(tx_id.as_ref().try_into().unwrap());
1302        let tx_info = tapes
1303            .read_entry(&db.tx_infos, tx_id)?
1304            .ok_or(BlockchainError::NotFound)?;
1305        let block_info = tapes
1306            .read_entry(&db.block_infos, usize_to_u64(tx_info.height))?
1307            .ok_or(BlockchainError::NotFound)?;
1308        let is_miner_tx = tx_id == block_info.mining_tx_index;
1309
1310        let (pruned_blob, prunable_blob, prunable_hash) =
1311            get_split_tx_blobs(&tx_info, is_miner_tx, &tapes, db)?;
1312
1313        let block_timestamp = if let Some(timestamp) = block_timestamps.get(&tx_info.height) {
1314            *timestamp
1315        } else {
1316            let timestamp =
1317                get_block_extended_header_from_height(tx_info.height, &tapes, db)?.timestamp;
1318            block_timestamps.insert(tx_info.height, timestamp);
1319            timestamp
1320        };
1321
1322        let output_indices = if tx_info.is_v1_tx() {
1323            let output_indices = tx_ro
1324                .get(&db.v1_tx_outputs, tx_id.to_le_bytes())?
1325                .ok_or(BlockchainError::NotFound)?;
1326
1327            output_indices
1328                .chunks(8)
1329                .map(|chunk| u64::from_le_bytes(chunk.try_into().unwrap()))
1330                .collect()
1331        } else {
1332            (0..tx_info.numb_rct_outputs)
1333                .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
1334                .collect()
1335        };
1336
1337        txs.push(TxInBlockchain {
1338            block_height: usize_to_u64(tx_info.height),
1339            block_timestamp,
1340            confirmations: usize_to_u64(chain_height - tx_info.height),
1341            output_indices,
1342            tx_hash,
1343            pruned_blob,
1344            prunable_blob,
1345            prunable_hash,
1346        });
1347    }
1348
1349    Ok(BlockchainResponse::Transactions { txs, missed_txs })
1350}
1351
1352/// [`BlockchainReadRequest::TotalRctOutputs`]
1353fn total_rct_outputs(db: &BlockchainDatabase) -> BlockchainResponse {
1354    let tapes = db.linear_tapes.reader();
1355
1356    let len = tapes
1357        .fixed_sized_tape_len(&db.rct_outputs)
1358        .expect("Required tape not found");
1359
1360    BlockchainResponse::TotalRctOutputs(len)
1361}
1362
1363/// [`BlockchainReadRequest::TxOutputIndexes`]
1364fn tx_output_indexes(db: &BlockchainDatabase, tx_hash: &[u8; 32]) -> ResponseResult {
1365    let (tx_ro, tapes) = db.read_transactions()?;
1366
1367    let tx_id = tx_ro
1368        .get(&db.tx_ids, tx_hash)?
1369        .ok_or(BlockchainError::NotFound)?;
1370
1371    let tx_id = u64::from_le_bytes(tx_id.as_ref().try_into().unwrap());
1372
1373    let tx_info = tapes
1374        .read_entry(&db.tx_infos, tx_id)?
1375        .ok_or(BlockchainError::NotFound)?;
1376
1377    let o_indexes = if tx_info.is_v1_tx() {
1378        let bytes = tx_ro
1379            .get(&db.v1_tx_outputs, tx_id.to_le_bytes())?
1380            .ok_or(BlockchainError::NotFound)?;
1381
1382        bytemuck::pod_collect_to_vec(bytes.as_ref())
1383    } else {
1384        (0..tx_info.numb_rct_outputs)
1385            .map(|i| usize_to_u64(i) + tx_info.rct_output_start_idx)
1386            .collect()
1387    };
1388
1389    Ok(BlockchainResponse::TxOutputIndexes(o_indexes))
1390}
1391
1392/// [`BlockchainReadRequest::PreRctOutputDistribution`]
1393fn pre_rct_output_distribution(
1394    db: &BlockchainDatabase,
1395    input: &PreRctOutputDistributionInput,
1396) -> ResponseResult {
1397    let (tx_ro, tapes) = db.read_transactions()?;
1398    let chain_height = u64_to_usize(
1399        tapes
1400            .fixed_sized_tape_len(&db.block_infos)
1401            .expect("block_infos tape exists"),
1402    );
1403
1404    if input.to_height.is_some_and(|h| h.get() < input.from_height) {
1405        return Err(BlockchainError::NotFound);
1406    }
1407
1408    let to_height = input.to_height.map_or(chain_height - 1, |h| {
1409        let h = h.get();
1410        u64_to_usize(h)
1411    });
1412
1413    if to_height >= chain_height {
1414        return Err(BlockchainError::NotFound);
1415    }
1416
1417    let mut result = Vec::with_capacity(input.amounts.len());
1418
1419    for &amount in &input.amounts {
1420        let amount = amount.get();
1421
1422        let start_height = u64_to_usize(input.from_height);
1423
1424        if start_height > to_height {
1425            return Err(BlockchainError::NotFound);
1426        }
1427
1428        let mut per_block: Vec<u64> = vec![0; to_height - start_height + 1];
1429        let mut below_start: u64 = 0;
1430
1431        // loop over all outputs with this amount.
1432        for guard in tx_ro.prefix(&db.pre_rct_outputs, amount.to_be_bytes()) {
1433            let output: Output = bytemuck::pod_read_unaligned(guard.value()?.as_ref());
1434            let h = output.height;
1435            // Add the output to the block it says it is in.
1436            if h < start_height {
1437                below_start += 1;
1438            } else if h <= to_height {
1439                per_block[h - start_height] += 1;
1440            } else {
1441                // The outputs are sorted by (amount, height), so we can stop
1442                // once we see a height above the range.
1443                break;
1444            }
1445        }
1446
1447        // monerod folds the below `start_height` count into the first bucket and
1448        // reports `base = 0` for pre-RCT amounts.
1449        per_block[0] += below_start;
1450
1451        let distribution = if input.cumulative {
1452            let mut cumulative = per_block;
1453            for i in 1..cumulative.len() {
1454                cumulative[i] += cumulative[i - 1];
1455            }
1456            cumulative
1457        } else {
1458            per_block
1459        };
1460
1461        result.push(OutputDistributionData {
1462            amount,
1463            distribution,
1464            start_height: usize_to_u64(start_height),
1465            base: 0,
1466        });
1467    }
1468
1469    Ok(BlockchainResponse::PreRctOutputDistribution(result))
1470}