cuprate_types/
output_cache.rs

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