cuprate_types/
output_cache.rs

1use indexmap::{IndexMap, IndexSet};
2use monero_oxide::{io::CompressedPoint, transaction::Transaction};
3
4use cuprate_helper::{cast::u64_to_usize, crypto::compute_zero_commitment};
5
6use crate::{OutputOnChain, VerifiedBlockInformation};
7
8/// A cache of outputs from the blockchain database.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct OutputCache {
11    /// A map of (amount, amount idx) -> output.
12    cached_outputs: IndexMap<u64, IndexMap<u64, OutputOnChain>>,
13    /// A map of an output amount to the amount of outputs in the blockchain with that amount.
14    number_of_outputs: IndexMap<u64, u64>,
15    /// A set of outputs that were requested but were not currently in the DB.
16    wanted_outputs: IndexMap<u64, IndexSet<u64>>,
17}
18
19impl OutputCache {
20    /// Create a new [`OutputCache`].
21    pub const fn new(
22        cached_outputs: IndexMap<u64, IndexMap<u64, OutputOnChain>>,
23        number_of_outputs: IndexMap<u64, u64>,
24        wanted_outputs: IndexMap<u64, IndexSet<u64>>,
25    ) -> Self {
26        Self {
27            cached_outputs,
28            number_of_outputs,
29            wanted_outputs,
30        }
31    }
32
33    /// Returns the set of currently cached outputs.
34    ///
35    /// # Warning
36    ///
37    /// [`Self::get_output`] should be preferred over this when possible, this will not contain all outputs
38    /// asked for necessarily.
39    pub const fn cached_outputs(&self) -> &IndexMap<u64, IndexMap<u64, OutputOnChain>> {
40        &self.cached_outputs
41    }
42
43    /// Returns the number of outputs in the blockchain with the given amount.
44    ///
45    /// # Warning
46    ///
47    /// The cache will only track the amount of outputs with a given amount for the requested outputs.
48    /// So if you do not request an output with `amount` when generating the cache the amount of outputs
49    /// with value `amount` will not be tracked.
50    pub fn number_outs_with_amount(&self, amount: u64) -> usize {
51        u64_to_usize(
52            self.number_of_outputs
53                .get(&amount)
54                .copied()
55                .unwrap_or_default(),
56        )
57    }
58
59    /// Request an output with a given amount and amount index from the cache.
60    pub fn get_output(&self, amount: u64, index: u64) -> Option<&OutputOnChain> {
61        self.cached_outputs
62            .get(&amount)
63            .and_then(|map| map.get(&index))
64    }
65
66    /// Adds a [`Transaction`] to the cache.
67    fn add_tx<const MINER_TX: bool>(&mut self, height: usize, tx: &Transaction) {
68        for (i, out) in tx.prefix().outputs.iter().enumerate() {
69            let amount = if MINER_TX && tx.version() == 2 {
70                0
71            } else {
72                out.amount.unwrap_or_default()
73            };
74
75            let Some(outputs_with_amount) = self.number_of_outputs.get_mut(&amount) else {
76                continue;
77            };
78
79            let amount_index_of_out = *outputs_with_amount;
80            *outputs_with_amount += 1;
81
82            if let Some(set) = self.wanted_outputs.get_mut(&amount) {
83                if set.swap_remove(&amount_index_of_out) {
84                    self.cached_outputs.entry(amount).or_default().insert(
85                        amount_index_of_out,
86                        OutputOnChain {
87                            height,
88                            time_lock: tx.prefix().additional_timelock,
89                            key: out.key,
90                            commitment: get_output_commitment(tx, i),
91                            txid: None,
92                        },
93                    );
94                }
95            }
96        }
97    }
98
99    /// Adds a block to the cache.
100    ///
101    /// This function will add any outputs to the cache that were requested when building the cache
102    /// but were not in the DB, if they are in the block.
103    pub fn add_block_to_cache(&mut self, block: &VerifiedBlockInformation) {
104        self.add_tx::<true>(block.height, &block.block.miner_transaction);
105
106        for tx in &block.txs {
107            self.add_tx::<false>(block.height, &tx.tx);
108        }
109    }
110}
111
112/// Returns the amount commitment for the output at the given index `i` in the [`Transaction`]
113fn get_output_commitment(tx: &Transaction, i: usize) -> CompressedPoint {
114    match tx {
115        Transaction::V1 { prefix, .. } => {
116            compute_zero_commitment(prefix.outputs[i].amount.unwrap_or_default())
117        }
118        Transaction::V2 { prefix, proofs } => {
119            let Some(proofs) = proofs else {
120                return compute_zero_commitment(prefix.outputs[i].amount.unwrap_or_default());
121            };
122
123            proofs.base.commitments[i]
124        }
125    }
126}