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}