diff --git a/Cargo.lock b/Cargo.lock index b7195b90..647c0353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1147,15 +1147,20 @@ dependencies = [ "cryptonight-cuprate", "cuprate-common", "curve25519-dalek", + "dalek-ff-group", "hex", + "hex-literal", "monero-serai", + "multiexp", "primitive-types", "proptest", "proptest-derive", + "rand", "rayon", "thiserror", "tokio", "tracing", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ffb55c2d..fcc9639c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ opt-level = 3 [workspace.dependencies] async-trait = { version = "0.1.74" } -borsh = { version = "1.2.1" } +borsh = { version = "1.2.1" } bytes = { version = "1.5.0" } clap = { version = "4.4.7" } chrono = { version = "0.4.31" } diff --git a/consensus/rules/Cargo.toml b/consensus/rules/Cargo.toml index 2cc0f52b..27d25bad 100644 --- a/consensus/rules/Cargo.toml +++ b/consensus/rules/Cargo.toml @@ -13,9 +13,15 @@ cryptonight-cuprate = {path = "../../cryptonight"} cuprate-common = {path = "../../common"} monero-serai = { workspace = true } +multiexp = { workspace = true } +dalek-ff-group = { workspace = true } +zeroize = "1.7" curve25519-dalek = { workspace = true } +rand = { workspace = true } + hex = "0.4" +hex-literal = "0.4" primitive-types = { version = "0.12.2", default-features = false } tracing = { workspace = true } diff --git a/consensus/rules/src/transactions.rs b/consensus/rules/src/transactions.rs index 1a52b7af..418f0b97 100644 --- a/consensus/rules/src/transactions.rs +++ b/consensus/rules/src/transactions.rs @@ -5,6 +5,7 @@ use monero_serai::transaction::{Input, Output, Timelock}; use crate::{check_point_canonically_encoded, is_decomposed_amount, HardFork}; mod contextual_data; +mod ring_ct; pub use contextual_data::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] diff --git a/consensus/rules/src/transactions/ring_ct.rs b/consensus/rules/src/transactions/ring_ct.rs new file mode 100644 index 00000000..c6d93691 --- /dev/null +++ b/consensus/rules/src/transactions/ring_ct.rs @@ -0,0 +1,227 @@ +#![allow(dead_code)] + +use curve25519_dalek::{EdwardsPoint, Scalar}; +use hex_literal::hex; +use monero_serai::{ + ringct::{ + clsag::ClsagError, + mlsag::{AggregateRingMatrixBuilder, MlsagError, RingMatrix}, + RctPrunable, RctSignatures, RctType, + }, + transaction::{Input, Output}, + H, +}; +use multiexp::BatchVerifier; +use rand::thread_rng; +#[cfg(feature = "rayon")] +use rayon::prelude::*; + +use crate::{transactions::Rings, try_par_iter, HardFork}; + +/// This constant contains the IDs of 2 transactions that should be allowed after the fork the ringCT +/// type they used should be banned. +const GRANDFATHERED_TRANSACTIONS: [[u8; 32]; 2] = [ + hex!("c5151944f0583097ba0c88cd0f43e7fabb3881278aa2f73b3b0a007c5d34e910"), + hex!("6f2f117cde6fbcf8d4a6ef8974fcac744726574ac38cf25d3322c996b21edd4c"), +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +pub enum RingCTError { + #[error("The RingCT type used is not allowed.")] + TypeNotAllowed, + #[error("One or more of the outputs do not have a zero amount.")] + OutputNotZero, + #[error("RingCT simple: sum pseudo-outs does not equal outputs.")] + SimpleAmountDoNotBalance, + #[error("The borromean range proof is invalid.")] + BorromeanRangeInvalid, + #[error("The bulletproofs range proof is invalid.")] + BulletproofsRangeInvalid, + #[error("One or more input ring is invalid.")] + RingInvalid, + #[error("MLSAG Error: {0}.")] + MLSAGError(#[from] MlsagError), + #[error("CLSAG Error: {0}.")] + CLSAGError(#[from] ClsagError), +} + +/// Checks the RingCT type is allowed for the current hard fork. +/// +/// https://monero-book.cuprate.org/consensus_rules/ring_ct.html#type +fn check_rct_type(ty: &RctType, hf: HardFork, tx_hash: &[u8; 32]) -> Result<(), RingCTError> { + use HardFork as F; + use RctType as T; + + match ty { + T::MlsagAggregate | T::MlsagIndividual if hf >= F::V4 && hf < F::V9 => Ok(()), + T::Bulletproofs if hf >= F::V8 && hf < F::V11 => Ok(()), + T::BulletproofsCompactAmount if hf >= F::V10 && hf < F::V14 => Ok(()), + T::BulletproofsCompactAmount if GRANDFATHERED_TRANSACTIONS.contains(tx_hash) => Ok(()), + T::Clsag if hf >= F::V13 && hf < F::V16 => Ok(()), + T::BulletproofsPlus if hf >= F::V15 => Ok(()), + _ => Err(RingCTError::TypeNotAllowed), + } +} + +/// Checks all the outputs have a zero amount. +/// +/// https://monero-book.cuprate.org/consensus_rules/ring_ct.html#output-amount +fn check_output_amount(outputs: &[Output]) -> Result<(), RingCTError> { + outputs.iter().try_for_each(|out| { + if out.amount.is_none() { + Ok(()) + } else { + Err(RingCTError::OutputNotZero) + } + }) +} + +/// Checks that the pseudo-outs sum to the same point as the output commitments. +/// +/// https://monero-book.cuprate.org/consensus_rules/ring_ct.html#pseudo-outs-outpks-balance +fn simple_type_balances(rct_sig: &RctSignatures) -> Result<(), RingCTError> { + let pseudo_outs = if rct_sig.rct_type() == RctType::MlsagIndividual { + &rct_sig.base.pseudo_outs + } else { + match &rct_sig.prunable { + RctPrunable::Clsag { pseudo_outs, .. } + | RctPrunable::MlsagBulletproofs { pseudo_outs, .. } => pseudo_outs, + _ => panic!("RingCT type is not simple!"), + } + }; + + let sum_inputs = pseudo_outs.iter().sum::(); + let sum_outputs = rct_sig.base.commitments.iter().sum::() + + Scalar::from(rct_sig.base.fee) * H(); + + if sum_inputs == sum_outputs { + Ok(()) + } else { + Err(RingCTError::SimpleAmountDoNotBalance) + } +} + +/// Checks the outputs range proof(s) +/// +/// https://monero-book.cuprate.org/consensus_rules/ring_ct/borromean.html +/// https://monero-book.cuprate.org/consensus_rules/ring_ct/bulletproofs.html +/// https://monero-book.cuprate.org/consensus_rules/ring_ct/bulletproofs+.html +fn check_output_range_proofs( + rct_sig: &RctSignatures, + verifier: &mut BatchVerifier<(), dalek_ff_group::EdwardsPoint>, +) -> Result<(), RingCTError> { + let commitments = &rct_sig.base.commitments; + + match &rct_sig.prunable { + RctPrunable::Null => Err(RingCTError::TypeNotAllowed)?, + RctPrunable::MlsagBorromean { borromean, .. } + | RctPrunable::AggregateMlsagBorromean { borromean, .. } => try_par_iter(borromean) + .zip(commitments) + .try_for_each(|(borro, commitment)| { + if borro.verify(commitment) { + Ok(()) + } else { + Err(RingCTError::BorromeanRangeInvalid) + } + }), + RctPrunable::MlsagBulletproofs { bulletproofs, .. } + | RctPrunable::Clsag { bulletproofs, .. } => { + if bulletproofs.batch_verify(&mut thread_rng(), verifier, (), commitments) { + Ok(()) + } else { + Err(RingCTError::BulletproofsRangeInvalid) + } + } + } +} + +/// Check the input signatures, MLSAG, CLSAG. +/// +/// https://monero-book.cuprate.org/consensus_rules/ring_ct/mlsag.html +/// https://monero-book.cuprate.org/consensus_rules/ring_ct/clsag.html +fn check_input_signatures( + msg: &[u8; 32], + inputs: &[Input], + rct_sig: &RctSignatures, + rings: &Rings, +) -> Result<(), RingCTError> { + let Rings::RingCT(rings) = rings else { + panic!("Tried to verify RCT transaction without RCT ring"); + }; + + if rings.is_empty() { + Err(RingCTError::RingInvalid)?; + } + + match &rct_sig.prunable { + RctPrunable::Null => Err(RingCTError::TypeNotAllowed)?, + RctPrunable::AggregateMlsagBorromean { mlsag, .. } => { + let key_images = inputs + .iter() + .map(|inp| { + let Input::ToKey { key_image, .. } = inp else { + panic!("How did we build a ring with no decoys?"); + }; + *key_image + }) + .collect::>(); + + let mut matrix = + AggregateRingMatrixBuilder::new(&rct_sig.base.commitments, rct_sig.base.fee); + rings.iter().try_for_each(|ring| matrix.push_ring(ring))?; + Ok(mlsag.verify(msg, &matrix.build()?, &key_images)?) + } + RctPrunable::MlsagBorromean { mlsags, .. } + | RctPrunable::MlsagBulletproofs { mlsags, .. } => try_par_iter(mlsags) + .zip(&rct_sig.base.pseudo_outs) + .zip(inputs) + .zip(rings) + .try_for_each(|(((mlsag, pseudo_out), input), ring)| { + let Input::ToKey { key_image, .. } = input else { + panic!("How did we build a ring with no decoys?"); + }; + + Ok(mlsag.verify( + msg, + &RingMatrix::individual(ring, *pseudo_out)?, + &[*key_image], + )?) + }), + RctPrunable::Clsag { clsags, .. } => try_par_iter(clsags) + .zip(&rct_sig.base.pseudo_outs) + .zip(inputs) + .zip(rings) + .try_for_each(|(((clsags, pseudo_out), input), ring)| { + let Input::ToKey { key_image, .. } = input else { + panic!("How did we build a ring with no decoys?"); + }; + + Ok(clsags.verify(ring, key_image, pseudo_out, msg)?) + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn grandfathered_bulletproofs2() { + assert!( + check_rct_type(&RctType::BulletproofsCompactAmount, HardFork::V14, &[0; 32]).is_err() + ); + + assert!(check_rct_type( + &RctType::BulletproofsCompactAmount, + HardFork::V14, + &GRANDFATHERED_TRANSACTIONS[0] + ) + .is_ok()); + assert!(check_rct_type( + &RctType::BulletproofsCompactAmount, + HardFork::V14, + &GRANDFATHERED_TRANSACTIONS[1] + ) + .is_ok()); + } +}