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}