cuprate_consensus_rules/transactions/
contextual_data.rs

1use std::cmp::{max, min};
2
3use indexmap::{IndexMap, IndexSet};
4use monero_oxide::{
5    io::CompressedPoint,
6    transaction::{Input, Timelock},
7};
8
9use crate::{transactions::TransactionError, HardFork};
10
11/// Gets the absolute offsets from the relative offsets.
12///
13/// This function will return an error if the relative offsets are empty.
14/// <https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys>
15pub fn get_absolute_offsets(relative_offsets: &[u64]) -> Result<Vec<u64>, TransactionError> {
16    if relative_offsets.is_empty() {
17        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
18    }
19
20    let mut offsets = Vec::with_capacity(relative_offsets.len());
21    offsets.push(relative_offsets[0]);
22
23    for i in 1..relative_offsets.len() {
24        offsets.push(offsets[i - 1] + relative_offsets[i]);
25    }
26    Ok(offsets)
27}
28
29/// Inserts the output IDs that are needed to verify the transaction inputs into the provided `HashMap`.
30///
31/// This will error if the inputs are empty
32/// <https://cuprate.github.io/monero-book/consensus_rules/transactions.html#no-empty-inputs>
33///
34pub fn insert_ring_member_ids(
35    inputs: &[Input],
36    output_ids: &mut IndexMap<u64, IndexSet<u64>>,
37) -> Result<(), TransactionError> {
38    if inputs.is_empty() {
39        return Err(TransactionError::NoInputs);
40    }
41
42    for input in inputs {
43        match input {
44            Input::ToKey {
45                amount,
46                key_offsets,
47                ..
48            } => output_ids
49                .entry(amount.unwrap_or(0))
50                .or_default()
51                .extend(get_absolute_offsets(key_offsets)?),
52            Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
53        }
54    }
55    Ok(())
56}
57
58/// Represents the ring members of all the inputs.
59#[derive(Debug)]
60pub enum Rings {
61    /// Legacy, pre-ringCT, rings.
62    Legacy(Vec<Vec<CompressedPoint>>),
63    /// `RingCT` rings, (outkey, amount commitment).
64    RingCT(Vec<Vec<[CompressedPoint; 2]>>),
65}
66
67/// Information on the outputs the transaction is referencing for inputs (ring members).
68#[derive(Debug)]
69pub struct TxRingMembersInfo {
70    pub rings: Rings,
71    /// Information on the structure of the decoys, must be [`None`] for txs before [`HardFork::V1`]
72    pub decoy_info: Option<DecoyInfo>,
73    pub youngest_used_out_height: usize,
74    pub time_locked_outs: Vec<Timelock>,
75}
76
77/// A struct holding information about the inputs and their decoys. This data can vary by block so
78/// this data needs to be retrieved after every change in the blockchain.
79///
80/// This data *does not* need to be refreshed if one of these are true:
81/// - The input amounts are *ALL* 0 (RCT)
82/// - The top block hash is the same as when this data was retrieved (the blockchain state is unchanged).
83///
84/// <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html>
85#[derive(Debug, Copy, Clone)]
86pub struct DecoyInfo {
87    /// The number of inputs that have enough outputs on the chain to mix with.
88    pub mixable: usize,
89    /// The number of inputs that don't have enough outputs on the chain to mix with.
90    pub not_mixable: usize,
91    /// The minimum amount of decoys used in the transaction.
92    pub min_decoys: usize,
93    /// The maximum amount of decoys used in the transaction.
94    pub max_decoys: usize,
95}
96
97impl DecoyInfo {
98    /// Creates a new [`DecoyInfo`] struct relating to the passed in inputs, This is only needed from
99    /// hf 2 onwards.
100    ///
101    /// `outputs_with_amount` is a list of the amount of outputs currently on the chain with the same amount
102    /// as the `inputs` amount at the same index. For RCT inputs it instead should be [`None`].
103    ///
104    /// So:
105    ///
106    /// `amount_outs_on_chain(inputs[X]) == outputs_with_amount[X]`
107    ///
108    /// Do not rely on this function to do consensus checks!
109    ///
110    pub fn new(
111        inputs: &[Input],
112        outputs_with_amount: impl Fn(u64) -> usize,
113        hf: HardFork,
114    ) -> Result<Self, TransactionError> {
115        let mut min_decoys = usize::MAX;
116        let mut max_decoys = usize::MIN;
117        let mut mixable = 0;
118        let mut not_mixable = 0;
119
120        let minimum_decoys = minimum_decoys(hf);
121
122        for inp in inputs {
123            match inp {
124                Input::ToKey {
125                    key_offsets,
126                    amount,
127                    ..
128                } => {
129                    if let Some(amount) = amount {
130                        let outs_with_amt = outputs_with_amount(*amount);
131
132                        // <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs>
133                        if outs_with_amt <= minimum_decoys {
134                            not_mixable += 1;
135                        } else {
136                            mixable += 1;
137                        }
138                    } else {
139                        // ringCT amounts are always mixable.
140                        mixable += 1;
141                    }
142
143                    let numb_decoys = key_offsets
144                        .len()
145                        .checked_sub(1)
146                        .ok_or(TransactionError::InputDoesNotHaveExpectedNumbDecoys)?;
147
148                    // <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-and-maximum-decoys-used>
149                    min_decoys = min(min_decoys, numb_decoys);
150                    max_decoys = max(max_decoys, numb_decoys);
151                }
152                Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
153            }
154        }
155
156        Ok(Self {
157            mixable,
158            not_mixable,
159            min_decoys,
160            max_decoys,
161        })
162    }
163}
164
165/// Returns the default minimum amount of decoys for a hard-fork.
166/// **There are exceptions to this always being the minimum decoys**
167///
168/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#default-minimum-decoys>
169pub(crate) fn minimum_decoys(hf: HardFork) -> usize {
170    use HardFork as HF;
171    match hf {
172        HF::V1 => panic!("hard-fork 1 does not use these rules!"),
173        HF::V2 | HF::V3 | HF::V4 | HF::V5 => 2,
174        HF::V6 => 4,
175        HF::V7 => 6,
176        HF::V8 | HF::V9 | HF::V10 | HF::V11 | HF::V12 | HF::V13 | HF::V14 => 10,
177        HF::V15 | HF::V16 => 15,
178    }
179}