add ringCT rules

This commit is contained in:
Boog900 2023-12-24 21:07:28 +00:00
parent 84343a8297
commit f037532610
No known key found for this signature in database
GPG key ID: 5401367FB7302004
5 changed files with 240 additions and 1 deletions

5
Cargo.lock generated
View file

@ -1147,15 +1147,20 @@ dependencies = [
"cryptonight-cuprate", "cryptonight-cuprate",
"cuprate-common", "cuprate-common",
"curve25519-dalek", "curve25519-dalek",
"dalek-ff-group",
"hex", "hex",
"hex-literal",
"monero-serai", "monero-serai",
"multiexp",
"primitive-types", "primitive-types",
"proptest", "proptest",
"proptest-derive", "proptest-derive",
"rand",
"rayon", "rayon",
"thiserror", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"zeroize",
] ]
[[package]] [[package]]

View file

@ -35,7 +35,7 @@ opt-level = 3
[workspace.dependencies] [workspace.dependencies]
async-trait = { version = "0.1.74" } async-trait = { version = "0.1.74" }
borsh = { version = "1.2.1" } borsh = { version = "1.2.1" }
bytes = { version = "1.5.0" } bytes = { version = "1.5.0" }
clap = { version = "4.4.7" } clap = { version = "4.4.7" }
chrono = { version = "0.4.31" } chrono = { version = "0.4.31" }

View file

@ -13,9 +13,15 @@ cryptonight-cuprate = {path = "../../cryptonight"}
cuprate-common = {path = "../../common"} cuprate-common = {path = "../../common"}
monero-serai = { workspace = true } monero-serai = { workspace = true }
multiexp = { workspace = true }
dalek-ff-group = { workspace = true }
zeroize = "1.7"
curve25519-dalek = { workspace = true } curve25519-dalek = { workspace = true }
rand = { workspace = true }
hex = "0.4" hex = "0.4"
hex-literal = "0.4"
primitive-types = { version = "0.12.2", default-features = false } primitive-types = { version = "0.12.2", default-features = false }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -5,6 +5,7 @@ use monero_serai::transaction::{Input, Output, Timelock};
use crate::{check_point_canonically_encoded, is_decomposed_amount, HardFork}; use crate::{check_point_canonically_encoded, is_decomposed_amount, HardFork};
mod contextual_data; mod contextual_data;
mod ring_ct;
pub use contextual_data::*; pub use contextual_data::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]

View file

@ -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::<EdwardsPoint>();
let sum_outputs = rct_sig.base.commitments.iter().sum::<EdwardsPoint>()
+ 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::<Vec<_>>();
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());
}
}