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,
}
}