Skip to main content

cuprate_types/
output_cache.rs

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