cuprate_consensus_rules/transactions/
contextual_data.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
use std::{
    cmp::{max, min},
    collections::{HashMap, HashSet},
};

use curve25519_dalek::EdwardsPoint;
use monero_serai::transaction::{Input, Timelock};

use crate::{transactions::TransactionError, HardFork};

/// Gets the absolute offsets from the relative offsets.
///
/// This function will return an error if the relative offsets are empty.
/// <https://cuprate.github.io/monero-book/consensus_rules/transactions.html#inputs-must-have-decoys>
pub fn get_absolute_offsets(relative_offsets: &[u64]) -> Result<Vec<u64>, TransactionError> {
    if relative_offsets.is_empty() {
        return Err(TransactionError::InputDoesNotHaveExpectedNumbDecoys);
    }

    let mut offsets = Vec::with_capacity(relative_offsets.len());
    offsets.push(relative_offsets[0]);

    for i in 1..relative_offsets.len() {
        offsets.push(offsets[i - 1] + relative_offsets[i]);
    }
    Ok(offsets)
}

/// Inserts the output IDs that are needed to verify the transaction inputs into the provided `HashMap`.
///
/// This will error if the inputs are empty
/// <https://cuprate.github.io/monero-book/consensus_rules/transactions.html#no-empty-inputs>
///
pub fn insert_ring_member_ids(
    inputs: &[Input],
    output_ids: &mut HashMap<u64, HashSet<u64>>,
) -> Result<(), TransactionError> {
    if inputs.is_empty() {
        return Err(TransactionError::NoInputs);
    }

    for input in inputs {
        match input {
            Input::ToKey {
                amount,
                key_offsets,
                ..
            } => output_ids
                .entry(amount.unwrap_or(0))
                .or_default()
                .extend(get_absolute_offsets(key_offsets)?),
            Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
        }
    }
    Ok(())
}

/// Represents the ring members of all the inputs.
#[derive(Debug)]
pub enum Rings {
    /// Legacy, pre-ringCT, rings.
    Legacy(Vec<Vec<EdwardsPoint>>),
    /// `RingCT` rings, (outkey, amount commitment).
    RingCT(Vec<Vec<[EdwardsPoint; 2]>>),
}

/// Information on the outputs the transaction is referencing for inputs (ring members).
#[derive(Debug)]
pub struct TxRingMembersInfo {
    pub rings: Rings,
    /// Information on the structure of the decoys, must be [`None`] for txs before [`HardFork::V1`]
    pub decoy_info: Option<DecoyInfo>,
    pub youngest_used_out_height: usize,
    pub time_locked_outs: Vec<Timelock>,
}

/// A struct holding information about the inputs and their decoys. This data can vary by block so
/// this data needs to be retrieved after every change in the blockchain.
///
/// This data *does not* need to be refreshed if one of these are true:
/// - The input amounts are *ALL* 0 (RCT)
/// - The top block hash is the same as when this data was retrieved (the blockchain state is unchanged).
///
/// <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html>
#[derive(Debug, Copy, Clone)]
pub struct DecoyInfo {
    /// The number of inputs that have enough outputs on the chain to mix with.
    pub mixable: usize,
    /// The number of inputs that don't have enough outputs on the chain to mix with.
    pub not_mixable: usize,
    /// The minimum amount of decoys used in the transaction.
    pub min_decoys: usize,
    /// The maximum amount of decoys used in the transaction.
    pub max_decoys: usize,
}

impl DecoyInfo {
    /// Creates a new [`DecoyInfo`] struct relating to the passed in inputs, This is only needed from
    /// hf 2 onwards.
    ///
    /// `outputs_with_amount` is a list of the amount of outputs currently on the chain with the same amount
    /// as the `inputs` amount at the same index. For RCT inputs it instead should be [`None`].
    ///
    /// So:
    ///
    /// `amount_outs_on_chain(inputs[X]) == outputs_with_amount[X]`
    ///
    /// Do not rely on this function to do consensus checks!
    ///
    pub fn new(
        inputs: &[Input],
        outputs_with_amount: impl Fn(u64) -> usize,
        hf: HardFork,
    ) -> Result<Self, TransactionError> {
        let mut min_decoys = usize::MAX;
        let mut max_decoys = usize::MIN;
        let mut mixable = 0;
        let mut not_mixable = 0;

        let minimum_decoys = minimum_decoys(hf);

        for inp in inputs {
            match inp {
                Input::ToKey {
                    key_offsets,
                    amount,
                    ..
                } => {
                    if let Some(amount) = amount {
                        let outs_with_amt = outputs_with_amount(*amount);

                        // <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#mixable-and-unmixable-inputs>
                        if outs_with_amt <= minimum_decoys {
                            not_mixable += 1;
                        } else {
                            mixable += 1;
                        }
                    } else {
                        // ringCT amounts are always mixable.
                        mixable += 1;
                    }

                    let numb_decoys = key_offsets
                        .len()
                        .checked_sub(1)
                        .ok_or(TransactionError::InputDoesNotHaveExpectedNumbDecoys)?;

                    // <https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-and-maximum-decoys-used>
                    min_decoys = min(min_decoys, numb_decoys);
                    max_decoys = max(max_decoys, numb_decoys);
                }
                Input::Gen(_) => return Err(TransactionError::IncorrectInputType),
            }
        }

        Ok(Self {
            mixable,
            not_mixable,
            min_decoys,
            max_decoys,
        })
    }
}

/// Returns the default minimum amount of decoys for a hard-fork.
/// **There are exceptions to this always being the minimum decoys**
///
/// ref: <https://monero-book.cuprate.org/consensus_rules/transactions/inputs.html#default-minimum-decoys>
pub(crate) fn minimum_decoys(hf: HardFork) -> usize {
    use HardFork as HF;
    match hf {
        HF::V1 => panic!("hard-fork 1 does not use these rules!"),
        HF::V2 | HF::V3 | HF::V4 | HF::V5 => 2,
        HF::V6 => 4,
        HF::V7 => 6,
        HF::V8 | HF::V9 | HF::V10 | HF::V11 | HF::V12 | HF::V13 | HF::V14 => 10,
        HF::V15 | HF::V16 => 15,
    }
}