1use curve25519_dalek::edwards::CompressedEdwardsY;
2use indexmap::{IndexMap, IndexSet};
3use monero_serai::transaction::Transaction;
45use cuprate_helper::{cast::u64_to_usize, crypto::compute_zero_commitment};
67use crate::{OutputOnChain, VerifiedBlockInformation};
89/// 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.
13cached_outputs: IndexMap<u64, IndexMap<u64, OutputOnChain>>,
14/// A map of an output amount to the amount of outputs in the blockchain with that amount.
15number_of_outputs: IndexMap<u64, u64>,
16/// A set of outputs that were requested but were not currently in the DB.
17wanted_outputs: IndexMap<u64, IndexSet<u64>>,
18}
1920impl OutputCache {
21/// Create a new [`OutputCache`].
22pub 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 {
27Self {
28 cached_outputs,
29 number_of_outputs,
30 wanted_outputs,
31 }
32 }
3334/// 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.
40pub const fn cached_outputs(&self) -> &IndexMap<u64, IndexMap<u64, OutputOnChain>> {
41&self.cached_outputs
42 }
4344/// 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.
51pub fn number_outs_with_amount(&self, amount: u64) -> usize {
52 u64_to_usize(
53self.number_of_outputs
54 .get(&amount)
55 .copied()
56 .unwrap_or_default(),
57 )
58 }
5960/// Request an output with a given amount and amount index from the cache.
61pub fn get_output(&self, amount: u64, index: u64) -> Option<&OutputOnChain> {
62self.cached_outputs
63 .get(&amount)
64 .and_then(|map| map.get(&index))
65 }
6667/// Adds a [`Transaction`] to the cache.
68fn add_tx<const MINER_TX: bool>(&mut self, height: usize, tx: &Transaction) {
69for (i, out) in tx.prefix().outputs.iter().enumerate() {
70let amount = if MINER_TX && tx.version() == 2 {
710
72} else {
73 out.amount.unwrap_or_default()
74 };
7576let Some(outputs_with_amount) = self.number_of_outputs.get_mut(&amount) else {
77continue;
78 };
7980let amount_index_of_out = *outputs_with_amount;
81*outputs_with_amount += 1;
8283if let Some(set) = self.wanted_outputs.get_mut(&amount) {
84if set.swap_remove(&amount_index_of_out) {
85self.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 }
99100/// 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.
104pub fn add_block_to_cache(&mut self, block: &VerifiedBlockInformation) {
105self.add_tx::<true>(block.height, &block.block.miner_transaction);
106107for tx in &block.txs {
108self.add_tx::<false>(block.height, &tx.tx);
109 }
110 }
111}
112113/// Returns the amount commitment for the output at the given index `i` in the [`Transaction`]
114fn get_output_commitment(tx: &Transaction, i: usize) -> CompressedEdwardsY {
115match tx {
116 Transaction::V1 { prefix, .. } => {
117 compute_zero_commitment(prefix.outputs[i].amount.unwrap_or_default())
118 }
119 Transaction::V2 { prefix, proofs } => {
120let Some(proofs) = proofs else {
121return compute_zero_commitment(prefix.outputs[i].amount.unwrap_or_default());
122 };
123124 proofs.base.commitments[i]
125 }
126 }
127}