mirror of
https://github.com/Cuprate/cuprate.git
synced 2024-12-22 19:49:28 +00:00
Move contextual data to monero-consensus
This commit is contained in:
parent
3eea0b73bd
commit
b01314ff70
3 changed files with 354 additions and 110 deletions
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
280
consensus/rules/src/transactions/contextual_data.rs
Normal file
280
consensus/rules/src/transactions/contextual_data.rs
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue