diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml index c0f41bd0..08fabe41 100644 --- a/coins/monero/Cargo.toml +++ b/coins/monero/Cargo.toml @@ -46,6 +46,7 @@ monero-generators = { path = "generators", version = "0.3", default-features = f futures = { version = "0.3", default-features = false, features = ["alloc"], optional = true } +hex-literal = "0.4" hex = { version = "0.4", default-features = false, features = ["alloc"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = { version = "1", default-features = false, features = ["alloc"] } @@ -61,8 +62,6 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3", defaul monero-generators = { path = "generators", version = "0.3", default-features = false } [dev-dependencies] -hex-literal = "0.4" - tokio = { version = "1", features = ["full"] } monero-rpc = "0.3" @@ -96,6 +95,9 @@ std = [ "serde/std", "serde_json/std", ] + http_rpc = ["digest_auth", "reqwest"] multisig = ["transcript", "frost", "dleq", "std"] +experimental = [] + default = ["std", "http_rpc"] diff --git a/coins/monero/generators/src/lib.rs b/coins/monero/generators/src/lib.rs index d25d927f..7f630f36 100644 --- a/coins/monero/generators/src/lib.rs +++ b/coins/monero/generators/src/lib.rs @@ -5,8 +5,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use core::cell::OnceCell; -use std_shims::sync::Mutex; +use std_shims::sync::OnceLock; use sha3::{Digest, Keccak256}; @@ -25,11 +24,11 @@ fn hash(data: &[u8]) -> [u8; 32] { Keccak256::digest(data).into() } -/// Monero alternate generator `H`, used for amounts in Pedersen commitments. -static H_CELL: Mutex> = Mutex::new(OnceCell::new()); +static H_CELL: OnceLock = OnceLock::new(); +/// Monero's alternate generator `H`, used for amounts in Pedersen commitments. #[allow(non_snake_case)] pub fn H() -> DalekPoint { - *H_CELL.lock().get_or_init(|| { + *H_CELL.get_or_init(|| { CompressedEdwardsY(hash(&EdwardsPoint::generator().to_bytes())) .decompress() .unwrap() @@ -37,6 +36,19 @@ pub fn H() -> DalekPoint { }) } +static H_POW_2_CELL: OnceLock<[DalekPoint; 64]> = OnceLock::new(); +/// Monero's alternate generator `H`, multiplied by 2**i for i in 1 ..= 64. +#[allow(non_snake_case)] +pub fn H_pow_2() -> &'static [DalekPoint; 64] { + H_POW_2_CELL.get_or_init(|| { + let mut res = [H(); 64]; + for i in 1 .. 64 { + res[i] = res[i - 1] + res[i - 1]; + } + res + }) +} + const MAX_M: usize = 16; const N: usize = 64; const MAX_MN: usize = MAX_M * N; diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs index 4a340b27..751b04f7 100644 --- a/coins/monero/src/block.rs +++ b/coins/monero/src/block.rs @@ -4,10 +4,17 @@ use std_shims::{ }; use crate::{ + hash, + merkle::merkle_root, serialize::*, transaction::{Input, Transaction}, }; +const CORRECT_BLOCK_HASH_202612: [u8; 32] = + hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a"); +const EXISTING_BLOCK_HASH_202612: [u8; 32] = + hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"); + #[derive(Clone, PartialEq, Eq, Debug)] pub struct BlockHeader { pub major_version: u64, @@ -68,6 +75,31 @@ impl Block { Ok(()) } + fn tx_merkle_root(&self) -> [u8; 32] { + merkle_root(self.miner_tx.hash(), &self.txs) + } + + fn serialize_hashable(&self) -> Vec { + let mut blob = self.header.serialize(); + blob.extend_from_slice(&self.tx_merkle_root()); + write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap(); + + let mut out = Vec::with_capacity(8 + blob.len()); + write_varint(&u64::try_from(blob.len()).unwrap(), &mut out).unwrap(); + out.append(&mut blob); + + out + } + + pub fn hash(&self) -> [u8; 32] { + let hash = hash(&self.serialize_hashable()); + if hash == CORRECT_BLOCK_HASH_202612 { + return EXISTING_BLOCK_HASH_202612; + }; + + hash + } + pub fn serialize(&self) -> Vec { let mut serialized = vec![]; self.write(&mut serialized).unwrap(); diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs index 83a0af0d..74b42f81 100644 --- a/coins/monero/src/lib.rs +++ b/coins/monero/src/lib.rs @@ -18,11 +18,14 @@ use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwar pub use monero_generators::H; +mod merkle; + mod serialize; use serialize::{read_byte, read_u16}; /// RingCT structs and functionality. pub mod ringct; +use ringct::RctType; /// Transaction structs. pub mod transaction; @@ -43,14 +46,16 @@ pub(crate) fn INV_EIGHT() -> Scalar { *INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert()) } -/// Monero protocol version. v15 is omitted as v15 was simply v14 and v16 being active at the same -/// time, with regards to the transactions supported. Accordingly, v16 should be used during v15. +/// Monero protocol version. +/// +/// v15 is omitted as v15 was simply v14 and v16 being active at the same time, with regards to the +/// transactions supported. Accordingly, v16 should be used during v15. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] #[allow(non_camel_case_types)] pub enum Protocol { v14, v16, - Custom { ring_len: usize, bp_plus: bool }, + Custom { ring_len: usize, bp_plus: bool, optimal_rct_type: RctType }, } impl Protocol { @@ -64,6 +69,7 @@ impl Protocol { } /// Whether or not the specified version uses Bulletproofs or Bulletproofs+. + /// /// This method will likely be reworked when versions not using Bulletproofs at all are added. pub fn bp_plus(&self) -> bool { match self { @@ -73,15 +79,25 @@ impl Protocol { } } + // TODO: Make this an Option when we support pre-RCT protocols + pub fn optimal_rct_type(&self) -> RctType { + match self { + Protocol::v14 => RctType::Clsag, + Protocol::v16 => RctType::BulletproofsPlus, + Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type, + } + } + pub(crate) fn write(&self, w: &mut W) -> io::Result<()> { match self { Protocol::v14 => w.write_all(&[0, 14]), Protocol::v16 => w.write_all(&[0, 16]), - Protocol::Custom { ring_len, bp_plus } => { + Protocol::Custom { ring_len, bp_plus, optimal_rct_type } => { // Custom, version 0 w.write_all(&[1, 0])?; w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?; - w.write_all(&[u8::from(*bp_plus)]) + w.write_all(&[u8::from(*bp_plus)])?; + w.write_all(&[optimal_rct_type.to_byte()]) } } } @@ -103,6 +119,8 @@ impl Protocol { 1 => true, _ => Err(io::Error::new(io::ErrorKind::Other, "invalid bool serialization"))?, }, + optimal_rct_type: RctType::from_byte(read_byte(r)?) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid RctType serialization"))?, }, _ => { Err(io::Error::new(io::ErrorKind::Other, "unrecognized custom protocol serialization"))? diff --git a/coins/monero/src/merkle.rs b/coins/monero/src/merkle.rs new file mode 100644 index 00000000..1671adf2 --- /dev/null +++ b/coins/monero/src/merkle.rs @@ -0,0 +1,55 @@ +use std_shims::vec::Vec; + +use crate::hash; + +pub fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] { + match leafs.len() { + 0 => root, + 1 => hash(&[root, leafs[0]].concat()), + _ => { + let mut hashes = Vec::with_capacity(1 + leafs.len()); + hashes.push(root); + hashes.extend(leafs); + + // Monero preprocess this so the length is a power of 2 + let mut high_pow_2 = 4; // 4 is the lowest value this can be + while high_pow_2 < hashes.len() { + high_pow_2 *= 2; + } + let low_pow_2 = high_pow_2 / 2; + + // Merge right-most hashes until we're at the low_pow_2 + { + let overage = hashes.len() - low_pow_2; + let mut rightmost = hashes.drain((low_pow_2 - overage) ..); + // This is true since we took overage from beneath and above low_pow_2, taking twice as + // many elements as overage + debug_assert_eq!(rightmost.len() % 2, 0); + + let mut paired_hashes = Vec::with_capacity(overage); + while let Some(left) = rightmost.next() { + let right = rightmost.next().unwrap(); + paired_hashes.push(hash(&[left.as_ref(), &right].concat())); + } + drop(rightmost); + + hashes.extend(paired_hashes); + assert_eq!(hashes.len(), low_pow_2); + } + + // Do a traditional pairing off + let mut new_hashes = Vec::with_capacity(hashes.len() / 2); + while hashes.len() > 1 { + let mut i = 0; + while i < hashes.len() { + new_hashes.push(hash(&[hashes[i], hashes[i + 1]].concat())); + i += 2; + } + + hashes = new_hashes; + new_hashes = Vec::with_capacity(hashes.len() / 2); + } + hashes[0] + } + } +} diff --git a/coins/monero/src/ringct/borromean.rs b/coins/monero/src/ringct/borromean.rs new file mode 100644 index 00000000..d0bf5b2a --- /dev/null +++ b/coins/monero/src/ringct/borromean.rs @@ -0,0 +1,108 @@ +use core::fmt::Debug; +use std_shims::io::{self, Read, Write}; + +use curve25519_dalek::edwards::EdwardsPoint; +#[cfg(feature = "experimental")] +use curve25519_dalek::{traits::Identity, scalar::Scalar}; + +#[cfg(feature = "experimental")] +use monero_generators::H_pow_2; +#[cfg(feature = "experimental")] +use crate::hash_to_scalar; +use crate::serialize::*; + +/// 64 Borromean ring signatures. +/// +/// This type keeps the data as raw bytes as Monero has some transactions with unreduced scalars in +/// this field. While we could use `from_bytes_mod_order`, we'd then not be able to encode this +/// back into it's original form. +/// +/// Those scalars also have a custom reduction algorithm... +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct BorromeanSignatures { + pub s0: [[u8; 32]; 64], + pub s1: [[u8; 32]; 64], + pub ee: [u8; 32], +} + +impl BorromeanSignatures { + pub fn read(r: &mut R) -> io::Result { + Ok(BorromeanSignatures { + s0: read_array(read_bytes, r)?, + s1: read_array(read_bytes, r)?, + ee: read_bytes(r)?, + }) + } + + pub fn write(&self, w: &mut W) -> io::Result<()> { + for s0 in self.s0.iter() { + w.write_all(s0)?; + } + for s1 in self.s1.iter() { + w.write_all(s1)?; + } + w.write_all(&self.ee) + } + + #[cfg(feature = "experimental")] + fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool { + let mut transcript = [0; 2048]; + for i in 0 .. 64 { + // TODO: These aren't the correct reduction + // TODO: Can either of these be tightened? + #[allow(non_snake_case)] + let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint( + &Scalar::from_bytes_mod_order(self.ee), + &keys_a[i], + &Scalar::from_bytes_mod_order(self.s0[i]), + ); + #[allow(non_snake_case)] + let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint( + &hash_to_scalar(LL.compress().as_bytes()), + &keys_b[i], + &Scalar::from_bytes_mod_order(self.s1[i]), + ); + transcript[i .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes()); + } + + // TODO: This isn't the correct reduction + // TODO: Can this be tightened to from_canonical_bytes? + hash_to_scalar(&transcript) == Scalar::from_bytes_mod_order(self.ee) + } +} + +/// A range proof premised on Borromean ring signatures. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct BorromeanRange { + pub sigs: BorromeanSignatures, + pub bit_commitments: [EdwardsPoint; 64], +} + +impl BorromeanRange { + pub fn read(r: &mut R) -> io::Result { + Ok(BorromeanRange { + sigs: BorromeanSignatures::read(r)?, + bit_commitments: read_array(read_point, r)?, + }) + } + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.sigs.write(w)?; + write_raw_vec(write_point, &self.bit_commitments, w) + } + + #[cfg(feature = "experimental")] + pub fn verify(&self, commitment: &EdwardsPoint) -> bool { + if &self.bit_commitments.iter().sum::() != commitment { + return false; + } + + #[allow(non_snake_case)] + let H_pow_2 = H_pow_2(); + let mut commitments_sub_one = [EdwardsPoint::identity(); 64]; + for i in 0 .. 64 { + commitments_sub_one[i] = self.bit_commitments[i] - H_pow_2[i]; + } + + self.sigs.verify(&self.bit_commitments, &commitments_sub_one) + } +} diff --git a/coins/monero/src/ringct/mlsag.rs b/coins/monero/src/ringct/mlsag.rs new file mode 100644 index 00000000..6999251f --- /dev/null +++ b/coins/monero/src/ringct/mlsag.rs @@ -0,0 +1,71 @@ +use std_shims::{ + vec::Vec, + io::{self, Read, Write}, +}; + +use curve25519_dalek::scalar::Scalar; +#[cfg(feature = "experimental")] +use curve25519_dalek::edwards::EdwardsPoint; + +use crate::serialize::*; +#[cfg(feature = "experimental")] +use crate::{hash_to_scalar, ringct::hash_to_point}; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Mlsag { + pub ss: Vec<[Scalar; 2]>, + pub cc: Scalar, +} + +impl Mlsag { + pub fn write(&self, w: &mut W) -> io::Result<()> { + for ss in self.ss.iter() { + write_raw_vec(write_scalar, ss, w)?; + } + write_scalar(&self.cc, w) + } + + pub fn read(mixins: usize, r: &mut R) -> io::Result { + Ok(Mlsag { + ss: (0 .. mixins).map(|_| read_array(read_scalar, r)).collect::>()?, + cc: read_scalar(r)?, + }) + } + + #[cfg(feature = "experimental")] + pub fn verify( + &self, + msg: &[u8; 32], + ring: &[[EdwardsPoint; 2]], + key_image: &EdwardsPoint, + ) -> bool { + if ring.is_empty() { + return false; + } + + let mut buf = Vec::with_capacity(6 * 32); + let mut ci = self.cc; + for (i, ring_member) in ring.iter().enumerate() { + buf.extend_from_slice(msg); + + #[allow(non_snake_case)] + let L = + |r| EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, &ring_member[r], &self.ss[i][r]); + + buf.extend_from_slice(ring_member[0].compress().as_bytes()); + buf.extend_from_slice(L(0).compress().as_bytes()); + + #[allow(non_snake_case)] + let R = (self.ss[i][0] * hash_to_point(ring_member[0])) + (ci * key_image); + buf.extend_from_slice(R.compress().as_bytes()); + + buf.extend_from_slice(ring_member[1].compress().as_bytes()); + buf.extend_from_slice(L(1).compress().as_bytes()); + + ci = hash_to_scalar(&buf); + buf.clear(); + } + + ci == self.cc + } +} diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs index d823a03c..eeadfa07 100644 --- a/coins/monero/src/ringct/mod.rs +++ b/coins/monero/src/ringct/mod.rs @@ -4,22 +4,26 @@ use std_shims::{ io::{self, Read, Write}, }; -use zeroize::Zeroizing; +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::{clsag::Clsag, bulletproofs::Bulletproofs}, + ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs}, }; /// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`. @@ -27,10 +31,95 @@ 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 ecdh_info: Vec<[u8; 8]>, + pub pseudo_outs: Vec, + pub encrypted_amounts: Vec, pub commitments: Vec, } @@ -39,30 +128,60 @@ impl RctBase { 1 + 8 + (outputs * (8 + 32)) } - pub fn write(&self, w: &mut W, rct_type: u8) -> io::Result<()> { - w.write_all(&[rct_type])?; + pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { + w.write_all(&[rct_type.to_byte()])?; match rct_type { - 0 => Ok(()), - 5 | 6 => { + RctType::Null => Ok(()), + _ => { write_varint(&self.fee, w)?; - for ecdh in &self.ecdh_info { - w.write_all(ecdh)?; + 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) } - _ => panic!("Serializing unknown RctType's Base"), } } - pub fn read(outputs: usize, r: &mut R) -> io::Result<(RctBase, u8)> { - let rct_type = read_byte(r)?; + 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 == 0 { - RctBase { fee: 0, ecdh_info: vec![], commitments: vec![] } + if rct_type == RctType::Null { + RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] } } else { RctBase { fee: read_varint(r)?, - ecdh_info: (0 .. outputs).map(|_| read_bytes(r)).collect::>()?, + 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)?, } }, @@ -74,67 +193,114 @@ impl RctBase { #[derive(Clone, PartialEq, Eq, Debug)] pub enum RctPrunable { Null, - Clsag { bulletproofs: Vec, clsags: Vec, pseudo_outs: Vec }, + MlsagBorromean { + borromean: Vec, + mlsags: Vec, + }, + MlsagBulletproofs { + bulletproofs: Bulletproofs, + mlsags: Vec, + pseudo_outs: Vec, + }, + Clsag { + bulletproofs: Bulletproofs, + clsags: Vec, + pseudo_outs: Vec, + }, } impl RctPrunable { - /// RCT Type byte for a given RctPrunable struct. - pub fn rct_type(&self) -> u8 { - match self { - RctPrunable::Null => 0, - RctPrunable::Clsag { bulletproofs, .. } => { - if matches!(bulletproofs[0], Bulletproofs::Original { .. }) { - 5 - } else { - 6 - } - } - } - } - pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize { 1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) + (inputs * (Clsag::fee_weight(protocol.ring_len()) + 32)) } - pub fn write(&self, w: &mut W) -> io::Result<()> { + pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { match self { RctPrunable::Null => Ok(()), - RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs, .. } => { - write_vec(Bulletproofs::write, bulletproofs, w)?; + 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) -> Vec { + pub fn serialize(&self, rct_type: RctType) -> Vec { let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); + self.write(&mut serialized, rct_type).unwrap(); serialized } - pub fn read(rct_type: u8, decoys: &[usize], r: &mut R) -> io::Result { + pub fn read( + rct_type: RctType, + decoys: &[usize], + outputs: usize, + r: &mut R, + ) -> io::Result { Ok(match rct_type { - 0 => RctPrunable::Null, - 5 | 6 => RctPrunable::Clsag { - bulletproofs: read_vec( - if rct_type == 5 { Bulletproofs::read } else { Bulletproofs::read_plus }, - r, - )?, + 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)?, }, - _ => Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown RCT type"))?, }) } pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { match self { RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"), - RctPrunable::Clsag { bulletproofs, .. } => { - bulletproofs.iter().try_for_each(|bp| bp.signature_write(w)) + 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), } } } @@ -146,13 +312,68 @@ pub struct RctSignatures { } 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) -> usize { RctBase::fee_weight(outputs) + RctPrunable::fee_weight(protocol, inputs, outputs) } pub fn write(&self, w: &mut W) -> io::Result<()> { - self.base.write(w, self.prunable.rct_type())?; - self.prunable.write(w) + let rct_type = self.rct_type(); + self.base.write(w, rct_type)?; + self.prunable.write(w, rct_type) } pub fn serialize(&self) -> Vec { @@ -162,7 +383,7 @@ impl RctSignatures { } pub fn read(decoys: Vec, outputs: usize, r: &mut R) -> io::Result { - let base = RctBase::read(outputs, r)?; - Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, &decoys, r)? }) + let base = RctBase::read(decoys.len(), outputs, r)?; + Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, &decoys, outputs, r)? }) } } diff --git a/coins/monero/src/rpc/mod.rs b/coins/monero/src/rpc/mod.rs index 5874b0e8..daae57a2 100644 --- a/coins/monero/src/rpc/mod.rs +++ b/coins/monero/src/rpc/mod.rs @@ -278,8 +278,12 @@ impl Rpc { let res: BlockResponse = self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?; - // TODO: Verify the TXs included are actually committed to by the header - Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()).map_err(|_| RpcError::InvalidNode) + let block = + Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()).map_err(|_| RpcError::InvalidNode)?; + if block.hash() != hash { + Err(RpcError::InvalidNode)?; + } + Ok(block) } pub async fn get_block_by_number(&self, number: usize) -> Result { diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs index 89f72d68..d3e983b0 100644 --- a/coins/monero/src/serialize.rs +++ b/coins/monero/src/serialize.rs @@ -1,3 +1,4 @@ +use core::fmt::Debug; use std_shims::{ vec::Vec, io::{self, Read, Write}, @@ -140,6 +141,13 @@ pub(crate) fn read_raw_vec io::Result>( Ok(res) } +pub(crate) fn read_array io::Result, const N: usize>( + f: F, + r: &mut R, +) -> io::Result<[T; N]> { + read_raw_vec(f, N, r).map(|vec| vec.try_into().unwrap()) +} + pub(crate) fn read_vec io::Result>( f: F, r: &mut R, diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs index c5c9d0ee..882c8322 100644 --- a/coins/monero/src/transaction.rs +++ b/coins/monero/src/transaction.rs @@ -231,13 +231,17 @@ impl TransactionPrefix { prefix.extra = read_vec(read_byte, r)?; Ok(prefix) } + + pub fn hash(&self) -> [u8; 32] { + hash(&self.serialize()) + } } /// Monero transaction. For version 1, rct_signatures still contains an accurate fee value. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Transaction { pub prefix: TransactionPrefix, - pub signatures: Vec<(Scalar, Scalar)>, + pub signatures: Vec>, pub rct_signatures: RctSignatures, } @@ -255,9 +259,11 @@ impl Transaction { pub fn write(&self, w: &mut W) -> io::Result<()> { self.prefix.write(w)?; if self.prefix.version == 1 { - for sig in &self.signatures { - write_scalar(&sig.0, w)?; - write_scalar(&sig.1, w)?; + for sigs in &self.signatures { + for sig in sigs { + write_scalar(&sig.0, w)?; + write_scalar(&sig.1, w)?; + } } Ok(()) } else if self.prefix.version == 2 { @@ -277,14 +283,25 @@ impl Transaction { let prefix = TransactionPrefix::read(r)?; let mut signatures = vec![]; let mut rct_signatures = RctSignatures { - base: RctBase { fee: 0, ecdh_info: vec![], commitments: vec![] }, + base: RctBase { fee: 0, encrypted_amounts: vec![], pseudo_outs: vec![], commitments: vec![] }, prunable: RctPrunable::Null, }; if prefix.version == 1 { - for _ in 0 .. prefix.inputs.len() { - signatures.push((read_scalar(r)?, read_scalar(r)?)); - } + signatures = prefix + .inputs + .iter() + .filter_map(|input| match input { + Input::ToKey { key_offsets, .. } => Some( + key_offsets + .iter() + .map(|_| Ok((read_scalar(r)?, read_scalar(r)?))) + .collect::>(), + ), + _ => None, + }) + .collect::>()?; + rct_signatures.base.fee = prefix .inputs .iter() @@ -322,18 +339,16 @@ impl Transaction { } else { let mut hashes = Vec::with_capacity(96); - self.prefix.write(&mut buf).unwrap(); - hashes.extend(hash(&buf)); - buf.clear(); + hashes.extend(self.prefix.hash()); - self.rct_signatures.base.write(&mut buf, self.rct_signatures.prunable.rct_type()).unwrap(); + self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); hashes.extend(hash(&buf)); buf.clear(); match self.rct_signatures.prunable { RctPrunable::Null => buf.resize(32, 0), _ => { - self.rct_signatures.prunable.write(&mut buf).unwrap(); + self.rct_signatures.prunable.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); buf = hash(&buf).to_vec(); } } @@ -348,11 +363,9 @@ impl Transaction { let mut buf = Vec::with_capacity(2048); let mut sig_hash = Vec::with_capacity(96); - self.prefix.write(&mut buf).unwrap(); - sig_hash.extend(hash(&buf)); - buf.clear(); + sig_hash.extend(self.prefix.hash()); - self.rct_signatures.base.write(&mut buf, self.rct_signatures.prunable.rct_type()).unwrap(); + self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); sig_hash.extend(hash(&buf)); buf.clear(); diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs index acab7ca1..f953edd1 100644 --- a/coins/monero/src/wallet/mod.rs +++ b/coins/monero/src/wallet/mod.rs @@ -9,7 +9,9 @@ use curve25519_dalek::{ edwards::{EdwardsPoint, CompressedEdwardsY}, }; -use crate::{hash, hash_to_scalar, serialize::write_varint, transaction::Input}; +use crate::{ + hash, hash_to_scalar, serialize::write_varint, ringct::EncryptedAmount, transaction::Input, +}; pub mod extra; pub(crate) use extra::{PaymentId, ExtraField, Extra}; @@ -86,20 +88,49 @@ pub(crate) fn shared_key( (view_tag, hash_to_scalar(&shared_key), payment_id_xor) } +pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar { + let mut mask = b"commitment_mask".to_vec(); + mask.extend(shared_key.to_bytes()); + hash_to_scalar(&mask) +} + pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] { let mut amount_mask = b"amount".to_vec(); amount_mask.extend(key.to_bytes()); (amount ^ u64::from_le_bytes(hash(&amount_mask)[.. 8].try_into().unwrap())).to_le_bytes() } -fn amount_decryption(amount: [u8; 8], key: Scalar) -> u64 { - u64::from_le_bytes(amount_encryption(u64::from_le_bytes(amount), key)) -} +// TODO: Move this under EncryptedAmount? +fn amount_decryption(amount: &EncryptedAmount, key: Scalar) -> (Scalar, u64) { + match amount { + EncryptedAmount::Original { mask, amount } => { + #[cfg(feature = "experimental")] + { + let mask_shared_sec = hash(key.as_bytes()); + let mask = + Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec); -pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar { - let mut mask = b"commitment_mask".to_vec(); - mask.extend(shared_key.to_bytes()); - hash_to_scalar(&mask) + let amount_shared_sec = hash(&mask_shared_sec); + let amount_scalar = + Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec); + // d2b from rctTypes.cpp + let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap()); + + (mask, amount) + } + + #[cfg(not(feature = "experimental"))] + { + let _ = mask; + let _ = amount; + todo!("decrypting a legacy monero transaction's amount") + } + } + EncryptedAmount::Compact { amount } => ( + commitment_mask(key), + u64::from_le_bytes(amount_encryption(u64::from_le_bytes(*amount), key)), + ), + } } /// The private view key and public spend key, enabling scanning transactions. diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs index 5f217fda..1a29d21e 100644 --- a/coins/monero/src/wallet/scan.rs +++ b/coins/monero/src/wallet/scan.rs @@ -16,7 +16,6 @@ use crate::{ rpc::{RpcError, RpcConnection, Rpc}, wallet::{ PaymentId, Extra, address::SubaddressIndex, Scanner, uniqueness, shared_key, amount_decryption, - commitment_mask, }, }; @@ -379,15 +378,15 @@ impl Scanner { commitment.amount = amount; // Regular transaction } else { - let amount = match tx.rct_signatures.base.ecdh_info.get(o) { - Some(amount) => amount_decryption(*amount, shared_key), + let (mask, amount) = match tx.rct_signatures.base.encrypted_amounts.get(o) { + Some(amount) => amount_decryption(amount, shared_key), // This should never happen, yet it may be possible with miner transactions? // Using get just decreases the possibility of a panic and lets us move on in that case None => break, }; // Rebuild the commitment to verify it - commitment = Commitment::new(commitment_mask(shared_key), amount); + commitment = Commitment::new(mask, amount); // If this is a malicious commitment, move to the next output // Any other R value will calculate to a different spend key and are therefore ignorable if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) { diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs index e722df2a..7eb1fcf2 100644 --- a/coins/monero/src/wallet/send/mod.rs +++ b/coins/monero/src/wallet/send/mod.rs @@ -53,6 +53,7 @@ pub use builder::SignableTransactionBuilder; mod multisig; #[cfg(feature = "multisig")] pub use multisig::TransactionMachine; +use crate::ringct::EncryptedAmount; #[allow(non_snake_case)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] @@ -629,7 +630,7 @@ impl SignableTransaction { let mut fee = self.inputs.iter().map(|input| input.commitment().amount).sum::(); let mut tx_outputs = Vec::with_capacity(outputs.len()); - let mut ecdh_info = Vec::with_capacity(outputs.len()); + let mut encrypted_amounts = Vec::with_capacity(outputs.len()); for output in &outputs { fee -= output.commitment.amount; tx_outputs.push(Output { @@ -637,7 +638,7 @@ impl SignableTransaction { key: output.dest.compress(), view_tag: Some(output.view_tag).filter(|_| matches!(self.protocol, Protocol::v16)), }); - ecdh_info.push(output.amount); + encrypted_amounts.push(EncryptedAmount::Compact { amount: output.amount }); } ( @@ -653,14 +654,11 @@ impl SignableTransaction { rct_signatures: RctSignatures { base: RctBase { fee, - ecdh_info, + encrypted_amounts, + pseudo_outs: vec![], commitments: commitments.iter().map(|commitment| commitment.calculate()).collect(), }, - prunable: RctPrunable::Clsag { - bulletproofs: vec![bp], - clsags: vec![], - pseudo_outs: vec![], - }, + prunable: RctPrunable::Clsag { bulletproofs: bp, clsags: vec![], pseudo_outs: vec![] }, }, }, sum, @@ -706,6 +704,7 @@ impl SignableTransaction { clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::>()); pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::>()); } + _ => unreachable!("attempted to sign a TX which wasn't CLSAG"), } Ok(tx) } @@ -747,6 +746,16 @@ impl Eventuality { uniqueness(&tx.prefix.inputs), ); + let rct_type = tx.rct_signatures.rct_type(); + if rct_type != self.protocol.optimal_rct_type() { + return false; + } + + // TODO: Remove this when the following for loop is updated + if !rct_type.compact_encrypted_amounts() { + panic!("created an Eventuality for a very old RctType we don't support proving for"); + } + for (o, (expected, actual)) in outputs.iter().zip(tx.prefix.outputs.iter()).enumerate() { // Verify the output, commitment, and encrypted amount. if (&Output { @@ -755,7 +764,8 @@ impl Eventuality { view_tag: Some(expected.view_tag).filter(|_| matches!(self.protocol, Protocol::v16)), } != actual) || (Some(&expected.commitment.calculate()) != tx.rct_signatures.base.commitments.get(o)) || - (Some(&expected.amount) != tx.rct_signatures.base.ecdh_info.get(o)) + (Some(&EncryptedAmount::Compact { amount: expected.amount }) != + tx.rct_signatures.base.encrypted_amounts.get(o)) { return false; } diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs index 42c1073b..3694d507 100644 --- a/coins/monero/src/wallet/send/multisig.rs +++ b/coins/monero/src/wallet/send/multisig.rs @@ -430,6 +430,7 @@ impl SignatureMachine for TransactionSignatureMachine { pseudo_outs.push(pseudo_out); } } + _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"), } Ok(tx) } diff --git a/processor/src/coins/monero.rs b/processor/src/coins/monero.rs index 20582080..5cdd8163 100644 --- a/processor/src/coins/monero.rs +++ b/processor/src/coins/monero.rs @@ -13,7 +13,7 @@ use frost::{curve::Ed25519, ThresholdKeys}; use monero_serai::{ Protocol, transaction::Transaction, - block::Block as MBlock, + block::Block, rpc::{RpcError, HttpRpc, Rpc}, wallet::{ ViewPair, Scanner, @@ -134,20 +134,18 @@ pub struct SignableTransaction { actual: MSignableTransaction, } -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Block([u8; 32], MBlock); impl BlockTrait for Block { type Id = [u8; 32]; fn id(&self) -> Self::Id { - self.0 + self.hash() } fn parent(&self) -> Self::Id { - self.1.header.previous + self.header.previous } fn time(&self) -> u64 { - self.1.header.timestamp + self.header.timestamp } fn median_fee(&self) -> Fee { @@ -256,18 +254,22 @@ impl Coin for Monero { } async fn get_block(&self, number: usize) -> Result { - let hash = self.rpc.get_block_hash(number).await.map_err(|_| CoinError::ConnectionError)?; - let block = self.rpc.get_block(hash).await.map_err(|_| CoinError::ConnectionError)?; - Ok(Block(hash, block)) + Ok( + self + .rpc + .get_block(self.rpc.get_block_hash(number).await.map_err(|_| CoinError::ConnectionError)?) + .await + .map_err(|_| CoinError::ConnectionError)?, + ) } async fn get_outputs( &self, - block: &Self::Block, + block: &Block, key: EdwardsPoint, ) -> Result, CoinError> { let mut txs = Self::scanner(key) - .scan(&self.rpc, &block.1) + .scan(&self.rpc, block) .await .map_err(|_| CoinError::ConnectionError)? .iter() @@ -305,10 +307,8 @@ impl Coin for Monero { async fn get_eventuality_completions( &self, eventualities: &mut EventualitiesTracker, - block: &Self::Block, + block: &Block, ) -> HashMap<[u8; 32], [u8; 32]> { - let block = &block.1; - let mut res = HashMap::new(); if eventualities.map.is_empty() { return res; @@ -317,7 +317,7 @@ impl Coin for Monero { async fn check_block( coin: &Monero, eventualities: &mut EventualitiesTracker, - block: &MBlock, + block: &Block, res: &mut HashMap<[u8; 32], [u8; 32]>, ) { for hash in &block.txs { @@ -357,7 +357,7 @@ impl Coin for Monero { block.unwrap() }; - check_block(self, eventualities, &block.1, &mut res).await; + check_block(self, eventualities, &block, &mut res).await; } // Also check the current block