use core::ops::Deref; use std_shims::{ vec::Vec, io::{self, Read, Write}, }; use zeroize::{Zeroize, Zeroizing}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; pub(crate) mod hash_to_point; pub use hash_to_point::{raw_hash_to_point, hash_to_point}; /// MLSAG struct, along with verifying functionality. pub mod mlsag; /// CLSAG struct, along with signing and verifying functionality. pub mod clsag; /// BorromeanRange struct, along with verifying functionality. pub mod borromean; /// Bulletproofs(+) structs, along with proving and verifying functionality. pub mod bulletproofs; use crate::{ Protocol, serialize::*, ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs}, }; /// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`. pub fn generate_key_image(secret: &Zeroizing) -> EdwardsPoint { hash_to_point(ED25519_BASEPOINT_TABLE * secret.deref()) * secret.deref() } #[derive(Clone, PartialEq, Eq, Debug)] pub enum EncryptedAmount { Original { mask: [u8; 32], amount: [u8; 32] }, Compact { amount: [u8; 8] }, } impl EncryptedAmount { pub fn read(compact: bool, r: &mut R) -> io::Result { Ok(if !compact { EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? } } else { EncryptedAmount::Compact { amount: read_bytes(r)? } }) } pub fn write(&self, w: &mut W) -> io::Result<()> { match self { EncryptedAmount::Original { mask, amount } => { w.write_all(mask)?; w.write_all(amount) } EncryptedAmount::Compact { amount } => w.write_all(amount), } } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub enum RctType { /// No RCT proofs. Null, /// One MLSAG for a single input and a Borromean range proof (RCTTypeFull). MlsagAggregate, // One MLSAG for each input and a Borromean range proof (RCTTypeSimple). MlsagIndividual, // One MLSAG for each input and a Bulletproof (RCTTypeBulletproof). Bulletproofs, /// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact /// (RCTTypeBulletproof2). BulletproofsCompactAmount, /// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG). Clsag, /// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus). BulletproofsPlus, } impl RctType { pub fn to_byte(self) -> u8 { match self { RctType::Null => 0, RctType::MlsagAggregate => 1, RctType::MlsagIndividual => 2, RctType::Bulletproofs => 3, RctType::BulletproofsCompactAmount => 4, RctType::Clsag => 5, RctType::BulletproofsPlus => 6, } } pub fn from_byte(byte: u8) -> Option { Some(match byte { 0 => RctType::Null, 1 => RctType::MlsagAggregate, 2 => RctType::MlsagIndividual, 3 => RctType::Bulletproofs, 4 => RctType::BulletproofsCompactAmount, 5 => RctType::Clsag, 6 => RctType::BulletproofsPlus, _ => None?, }) } pub fn compact_encrypted_amounts(&self) -> bool { match self { RctType::Null => false, RctType::MlsagAggregate => false, RctType::MlsagIndividual => false, RctType::Bulletproofs => false, RctType::BulletproofsCompactAmount => true, RctType::Clsag => true, RctType::BulletproofsPlus => true, } } } #[derive(Clone, PartialEq, Eq, Debug)] pub struct RctBase { pub fee: u64, pub pseudo_outs: Vec, pub encrypted_amounts: Vec, pub commitments: Vec, } impl RctBase { pub(crate) fn fee_weight(outputs: usize, fee: u64) -> usize { // 1 byte for the RCT signature type 1 + (outputs * (8 + 32)) + varint_len(fee) } pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { w.write_all(&[rct_type.to_byte()])?; match rct_type { RctType::Null => Ok(()), _ => { write_varint(&self.fee, w)?; if rct_type == RctType::MlsagIndividual { write_raw_vec(write_point, &self.pseudo_outs, w)?; } for encrypted_amount in &self.encrypted_amounts { encrypted_amount.write(w)?; } write_raw_vec(write_point, &self.commitments, w) } } } pub fn read(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> { let rct_type = RctType::from_byte(read_byte(r)?) .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid RCT type"))?; match rct_type { RctType::Null => {} RctType::MlsagAggregate => {} RctType::MlsagIndividual => {} RctType::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => { if outputs == 0 { // Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if // Bulletproofs are in use // If there are Bulletproofs, there must be a matching amount of outputs, implicitly // banning 0 outputs // Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced Err(io::Error::new(io::ErrorKind::Other, "RCT with Bulletproofs(+) had 0 outputs"))?; } } } Ok(( if rct_type == RctType::Null { RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] } } else { RctBase { fee: read_varint(r)?, pseudo_outs: if rct_type == RctType::MlsagIndividual { read_raw_vec(read_point, inputs, r)? } else { vec![] }, encrypted_amounts: (0 .. outputs) .map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r)) .collect::>()?, commitments: read_raw_vec(read_point, outputs, r)?, } }, rct_type, )) } } #[derive(Clone, PartialEq, Eq, Debug)] pub enum RctPrunable { Null, MlsagBorromean { borromean: Vec, mlsags: Vec, }, MlsagBulletproofs { bulletproofs: Bulletproofs, mlsags: Vec, pseudo_outs: Vec, }, Clsag { bulletproofs: Bulletproofs, clsags: Vec, pseudo_outs: Vec, }, } impl RctPrunable { pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize { // 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one) 1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) + (inputs * (Clsag::fee_weight(protocol.ring_len()) + 32)) } pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { match self { RctPrunable::Null => Ok(()), RctPrunable::MlsagBorromean { borromean, mlsags } => { write_raw_vec(BorromeanRange::write, borromean, w)?; write_raw_vec(Mlsag::write, mlsags, w) } RctPrunable::MlsagBulletproofs { bulletproofs, mlsags, pseudo_outs } => { if rct_type == RctType::Bulletproofs { w.write_all(&1u32.to_le_bytes())?; } else { w.write_all(&[1])?; } bulletproofs.write(w)?; write_raw_vec(Mlsag::write, mlsags, w)?; write_raw_vec(write_point, pseudo_outs, w) } RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { w.write_all(&[1])?; bulletproofs.write(w)?; write_raw_vec(Clsag::write, clsags, w)?; write_raw_vec(write_point, pseudo_outs, w) } } } pub fn serialize(&self, rct_type: RctType) -> Vec { let mut serialized = vec![]; self.write(&mut serialized, rct_type).unwrap(); serialized } pub fn read( rct_type: RctType, decoys: &[usize], outputs: usize, r: &mut R, ) -> io::Result { // While we generally don't bother with misc consensus checks, this affects the safety of // the below defined rct_type function // The exact line preventing zero-input transactions is: // https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/ // src/ringct/rctSigs.cpp#L609 // And then for RctNull, that's only allowed for miner TXs which require one input of // Input::Gen if decoys.is_empty() { Err(io::Error::new(io::ErrorKind::Other, "transaction had no inputs"))?; } Ok(match rct_type { RctType::Null => RctPrunable::Null, RctType::MlsagAggregate | RctType::MlsagIndividual => RctPrunable::MlsagBorromean { borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, mlsags: decoys.iter().map(|d| Mlsag::read(*d, r)).collect::>()?, }, RctType::Bulletproofs | RctType::BulletproofsCompactAmount => { RctPrunable::MlsagBulletproofs { bulletproofs: { if (if rct_type == RctType::Bulletproofs { u64::from(read_u32(r)?) } else { read_varint(r)? }) != 1 { Err(io::Error::new(io::ErrorKind::Other, "n bulletproofs instead of one"))?; } Bulletproofs::read(r)? }, mlsags: decoys.iter().map(|d| Mlsag::read(*d, r)).collect::>()?, pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?, } } RctType::Clsag | RctType::BulletproofsPlus => RctPrunable::Clsag { bulletproofs: { if read_varint(r)? != 1 { Err(io::Error::new(io::ErrorKind::Other, "n bulletproofs instead of one"))?; } (if rct_type == RctType::Clsag { Bulletproofs::read } else { Bulletproofs::read_plus })( r, )? }, clsags: (0 .. decoys.len()).map(|o| Clsag::read(decoys[o], r)).collect::>()?, pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?, }, }) } pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { match self { RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"), RctPrunable::MlsagBorromean { borromean, .. } => { borromean.iter().try_for_each(|rs| rs.write(w)) } RctPrunable::MlsagBulletproofs { bulletproofs, .. } => bulletproofs.signature_write(w), RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w), } } } #[derive(Clone, PartialEq, Eq, Debug)] pub struct RctSignatures { pub base: RctBase, pub prunable: RctPrunable, } impl RctSignatures { /// RctType for a given RctSignatures struct. pub fn rct_type(&self) -> RctType { match &self.prunable { RctPrunable::Null => RctType::Null, RctPrunable::MlsagBorromean { .. } => { /* This type of RctPrunable may have no outputs, yet pseudo_outs are per input This will only be a valid RctSignatures if it's for a TX with inputs That makes this valid for any valid RctSignatures While it will be invalid for any invalid RctSignatures, potentially letting an invalid MlsagAggregate be interpreted as a valid MlsagIndividual (or vice versa), they have incompatible deserializations This means it's impossible to receive a MlsagAggregate over the wire and interpret it as a MlsagIndividual (or vice versa) That only makes manual manipulation unsafe, which will always be true since these fields are all pub TODO: Consider making them private with read-only accessors? */ if self.base.pseudo_outs.is_empty() { RctType::MlsagAggregate } else { RctType::MlsagIndividual } } // RctBase ensures there's at least one output, making the following // inferences guaranteed/expects impossible on any valid RctSignatures RctPrunable::MlsagBulletproofs { .. } => { if matches!( self .base .encrypted_amounts .get(0) .expect("MLSAG with Bulletproofs didn't have any outputs"), EncryptedAmount::Original { .. } ) { RctType::Bulletproofs } else { RctType::BulletproofsCompactAmount } } RctPrunable::Clsag { bulletproofs, .. } => { if matches!(bulletproofs, Bulletproofs::Original { .. }) { RctType::Clsag } else { RctType::BulletproofsPlus } } } } pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize, fee: u64) -> usize { RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(protocol, inputs, outputs) } pub fn write(&self, w: &mut W) -> io::Result<()> { let rct_type = self.rct_type(); self.base.write(w, rct_type)?; self.prunable.write(w, rct_type) } pub fn serialize(&self) -> Vec { let mut serialized = vec![]; self.write(&mut serialized).unwrap(); serialized } pub fn read(decoys: Vec, outputs: usize, r: &mut R) -> io::Result { let base = RctBase::read(decoys.len(), outputs, r)?; Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, &decoys, outputs, r)? }) } }