Move contextual data to monero-consensus

This commit is contained in:
Boog900 2023-12-14 19:09:24 +00:00
parent 3eea0b73bd
commit b01314ff70
No known key found for this signature in database
GPG key ID: 5401367FB7302004
3 changed files with 354 additions and 110 deletions

View file

@ -1,6 +1,7 @@
use curve25519_dalek::EdwardsPoint;
use monero_serai::transaction::Transaction;
use crate::transactions::Rings;
mod ring_signatures;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
@ -11,15 +12,6 @@ pub enum SignatureError {
IncorrectSignature,
}
/// 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]>>),
}
pub fn verify_contextual_signatures(tx: &Transaction, rings: &Rings) -> Result<(), SignatureError> {
match rings {
Rings::Legacy(_) => ring_signatures::verify_inputs_signatures(

View file

@ -4,6 +4,9 @@ use monero_serai::transaction::{Input, Output, Timelock};
use crate::{check_point, is_decomposed_amount, HardFork, TxVersion};
mod contextual_data;
pub use contextual_data::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum TransactionError {
//-------------------------------------------------------- OUTPUTS
@ -38,6 +41,8 @@ pub enum TransactionError {
InputsOverflow,
#[error("The transaction has no inputs.")]
NoInputs,
#[error("Ring member not in database")]
RingMemberNotFound,
}
//----------------------------------------------------------------------------------------------------------- OUTPUTS
@ -136,44 +141,75 @@ pub fn check_outputs(
}
}
//----------------------------------------------------------------------------------------------------------- INPUTS
//----------------------------------------------------------------------------------------------------------- TIME LOCKS
/// 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.
/// Checks all the time locks are unlocked.
///
/// 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).
/// `current_time_lock_timestamp` must be: https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html
#[derive(Debug)]
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,
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
pub fn check_all_time_locks(
time_locks: &[Timelock],
current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> Result<(), TransactionError> {
time_locks.iter().try_for_each(|time_lock| {
if !output_unlocked(
time_lock,
current_chain_height,
current_time_lock_timestamp,
hf,
) {
Err(TransactionError::OneOrMoreDecoysLocked)
} else {
Ok(())
}
})
}
/// Returns the default minimum amount of decoys for a hard-fork.
/// **There are exceptions to this always being the minimum decoys**
/// Checks if an outputs unlock time has passed.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-amount-of-decoys
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,
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
fn output_unlocked(
time_lock: &Timelock,
current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> bool {
match *time_lock {
Timelock::None => true,
Timelock::Block(unlock_height) => {
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
}
Timelock::Time(unlock_time) => {
check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
}
}
}
/// Returns if a locked output, which uses a block height, can be spend.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#block-height
fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool {
// current_chain_height = 1 + top height
unlock_height <= current_chain_height
}
/// ///
/// Returns if a locked output, which uses a block height, can be spend.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
fn check_timestamp_time_lock(
unlock_timestamp: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> bool {
current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
}
//----------------------------------------------------------------------------------------------------------- INPUTS
/// Checks the decoys are allowed.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions.html#minimum-decoys
@ -348,9 +384,8 @@ fn sum_inputs_v1(inputs: &[Input]) -> Result<u64, TransactionError> {
///
pub fn check_inputs(
inputs: &[Input],
youngest_used_out_height: u64,
tx_ring_members_info: &TxRingMembersInfo,
current_chain_height: u64,
decoys_info: Option<&DecoyInfo>,
hf: &HardFork,
tx_version: &TxVersion,
spent_kis: Arc<std::sync::Mutex<HashSet<[u8; 32]>>>,
@ -359,9 +394,13 @@ pub fn check_inputs(
return Err(TransactionError::NoInputs);
}
check_10_block_lock(youngest_used_out_height, current_chain_height, hf)?;
check_10_block_lock(
tx_ring_members_info.youngest_used_out_height,
current_chain_height,
hf,
)?;
if let Some(decoys_info) = decoys_info {
if let Some(decoys_info) = &tx_ring_members_info.decoy_info {
check_decoy_info(decoys_info, hf)?;
} else {
assert_eq!(hf, &HardFork::V1);
@ -387,70 +426,3 @@ pub fn check_inputs(
_ => panic!("TODO: RCT"),
}
}
//----------------------------------------------------------------------------------------------------------- TIME LOCKS
/// Checks all the time locks are unlocked.
///
/// `current_time_lock_timestamp` must be: https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#getting-the-current-time
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
pub fn check_all_time_locks(
time_locks: &[Timelock],
current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> Result<(), TransactionError> {
time_locks.iter().try_for_each(|time_lock| {
if !output_unlocked(
time_lock,
current_chain_height,
current_time_lock_timestamp,
hf,
) {
Err(TransactionError::OneOrMoreDecoysLocked)
} else {
Ok(())
}
})
}
/// Checks if an outputs unlock time has passed.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#unlock-time
fn output_unlocked(
time_lock: &Timelock,
current_chain_height: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> bool {
match *time_lock {
Timelock::None => true,
Timelock::Block(unlock_height) => {
check_block_time_lock(unlock_height.try_into().unwrap(), current_chain_height)
}
Timelock::Time(unlock_time) => {
check_timestamp_time_lock(unlock_time, current_time_lock_timestamp, hf)
}
}
}
/// Returns if a locked output, which uses a block height, can be spend.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#block-height
fn check_block_time_lock(unlock_height: u64, current_chain_height: u64) -> bool {
// current_chain_height = 1 + top height
unlock_height <= current_chain_height
}
/// ///
/// Returns if a locked output, which uses a block height, can be spend.
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/unlock_time.html#timestamp
fn check_timestamp_time_lock(
unlock_timestamp: u64,
current_time_lock_timestamp: u64,
hf: &HardFork,
) -> bool {
current_time_lock_timestamp + hf.block_time().as_secs() >= unlock_timestamp
}

View file

@ -0,0 +1,280 @@
use std::{
cmp::{max, min},
collections::{HashMap, HashSet},
};
use curve25519_dalek::EdwardsPoint;
use monero_serai::transaction::{Input, Timelock};
use crate::{transactions::TransactionError, HardFork, TxVersion};
/// An already approved previous transaction output.
#[derive(Debug)]
pub struct OutputOnChain {
height: u64,
time_lock: Timelock,
key: EdwardsPoint,
mask: EdwardsPoint,
}
/// 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
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)?),
_ => return Err(TransactionError::IncorrectInputType),
}
}
Ok(())
}
/// Get the ring members for the inputs from the outputs on the chain.
///
/// Will error if `outputs` does not contain the outputs needed.
fn get_ring_members_for_inputs<'a>(
outputs: &'a HashMap<u64, HashMap<u64, OutputOnChain>>,
inputs: &[Input],
) -> Result<Vec<Vec<&'a OutputOnChain>>, TransactionError> {
inputs
.iter()
.map(|inp| match inp {
Input::ToKey {
amount,
key_offsets,
..
} => {
let offsets = get_absolute_offsets(key_offsets)?;
Ok(offsets
.iter()
.map(|offset| {
// get the hashmap for this amount.
outputs
.get(&amount.unwrap_or(0))
// get output at the index from the amount hashmap.
.and_then(|amount_map| amount_map.get(offset))
.ok_or(TransactionError::RingMemberNotFound)
})
.collect::<Result<_, TransactionError>>()?)
}
_ => Err(TransactionError::IncorrectInputType),
})
.collect::<Result<_, TransactionError>>()
}
/// 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]>>),
}
impl Rings {
/// Builds the rings for the transaction inputs, from the given outputs.
fn new(outputs: Vec<Vec<&OutputOnChain>>, tx_version: TxVersion) -> Rings {
match tx_version {
TxVersion::RingSignatures => Rings::Legacy(
outputs
.into_iter()
.map(|inp_outs| inp_outs.into_iter().map(|out| out.key).collect())
.collect(),
),
TxVersion::RingCT => Rings::RingCT(
outputs
.into_iter()
.map(|inp_outs| {
inp_outs
.into_iter()
.map(|out| [out.key, out.mask])
.collect()
})
.collect(),
),
}
}
}
/// Information on the outputs the transaction is is referencing for inputs (ring members).
#[derive(Debug)]
pub struct TxRingMembersInfo {
pub rings: Rings,
/// Information on the structure of the decoys, will be [`None`] for txs before [`HardFork::V1`]
pub decoy_info: Option<DecoyInfo>,
pub youngest_used_out_height: u64,
pub time_locked_outs: Vec<Timelock>,
}
impl TxRingMembersInfo {
/// Construct a [`TxRingMembersInfo`] struct.
///
/// The used outs must be all the ring members used in the transactions inputs.
fn new(
used_outs: Vec<Vec<&OutputOnChain>>,
decoy_info: Option<DecoyInfo>,
tx_version: TxVersion,
) -> TxRingMembersInfo {
TxRingMembersInfo {
youngest_used_out_height: used_outs
.iter()
.map(|inp_outs| {
inp_outs
.iter()
// the output with the highest height is the youngest
.map(|out| out.height)
.max()
.expect("Input must have ring members")
})
.max()
.expect("Tx must have inputs"),
time_locked_outs: used_outs
.iter()
.flat_map(|inp_outs| {
inp_outs
.iter()
.filter_map(|out| match out.time_lock {
Timelock::None => None,
lock => Some(lock),
})
.collect::<Vec<_>>()
})
.collect(),
rings: Rings::new(used_outs, tx_version),
decoy_info,
}
}
}
/// 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)]
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: &[Option<usize>],
hf: &HardFork,
) -> Result<DecoyInfo, 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, outs_with_amt) in inputs.iter().zip(outputs_with_amount) {
match inp {
Input::ToKey { key_offsets, .. } => {
if let Some(outs_with_amt) = *outs_with_amt {
// 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);
}
_ => return Err(TransactionError::IncorrectInputType),
}
}
Ok(DecoyInfo {
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**
///
/// https://cuprate.github.io/monero-book/consensus_rules/transactions/decoys.html#minimum-amount-of-decoys
pub 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,
}
}