cuprate_consensus_rules/transactions/
contextual_data.rs

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