diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index d2c0dcb0..9f2e97dd 100644 --- a/coins/monero/src/clsag/mod.rs +++ b/coins/monero/src/clsag/mod.rs @@ -1,6 +1,8 @@ use rand_core::{RngCore, CryptoRng}; use ff::Field; +use thiserror::Error; + use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, @@ -15,7 +17,6 @@ use monero::{ use crate::{ Commitment, - transaction::SignableInput, c_verify_clsag, random_scalar, hash_to_scalar, @@ -27,11 +28,68 @@ mod multisig; #[cfg(feature = "multisig")] pub use multisig::Multisig; +#[derive(Error, Debug)] +pub enum Error { + #[error("internal error ({0})")] + InternalError(String), + #[error("invalid ring member (member {0}, ring size {1})")] + InvalidRingMember(u8, u8), + #[error("invalid commitment")] + InvalidCommitment +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Input { + pub image: EdwardsPoint, + // Ring, the index we're signing for, and the actual commitment behind it + pub ring: Vec<[EdwardsPoint; 2]>, + pub i: usize, + pub commitment: Commitment +} + +impl Input { + pub fn new( + image: EdwardsPoint, + ring: Vec<[EdwardsPoint; 2]>, + i: u8, + commitment: Commitment +) -> Result { + let n = ring.len(); + if n > u8::MAX.into() { + Err(Error::InternalError("max ring size in this library is u8 max".to_string()))?; + } + if i >= (n as u8) { + Err(Error::InvalidRingMember(i, n as u8))?; + } + let i: usize = i.into(); + + // Validate the commitment matches + if ring[i][1] != commitment.calculate() { + Err(Error::InvalidCommitment)?; + } + + Ok(Input { image, ring, i, commitment }) + } + + #[cfg(feature = "multisig")] + pub fn context(&self) -> Vec { + let mut context = self.image.compress().to_bytes().to_vec(); + for pair in &self.ring { + // Doesn't include mixins[i] as CLSAG doesn't care and won't be affected by it + context.extend(&pair[0].compress().to_bytes()); + context.extend(&pair[1].compress().to_bytes()); + } + context.extend(&u8::try_from(self.i).unwrap().to_le_bytes()); + // Doesn't include commitment as the above ring + index includes the commitment + context + } +} + #[allow(non_snake_case)] pub(crate) fn sign_core( rng: &mut R, msg: &[u8; 32], - input: &SignableInput, + input: &Input, mask: Scalar, A: EdwardsPoint, AH: EdwardsPoint @@ -148,7 +206,7 @@ pub(crate) fn sign_core( pub fn sign( rng: &mut R, msg: [u8; 32], - inputs: &[(Scalar, SignableInput)], + inputs: &[(Scalar, Input)], sum_outputs: Scalar ) -> Option> { if inputs.len() == 0 { diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index 423e94cf..fd1221ff 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -19,7 +19,7 @@ use monero::util::ringct::{Key, Clsag}; use crate::{ hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}, - clsag::{SignableInput, sign_core, verify} + clsag::{Input, sign_core, verify} }; #[allow(non_snake_case)] @@ -40,7 +40,7 @@ pub struct Multisig { AH: dfg::EdwardsPoint, msg: [u8; 32], - input: SignableInput, + input: Input, interim: Option } @@ -49,7 +49,7 @@ impl Multisig { pub fn new( rng: &mut R, msg: [u8; 32], - input: SignableInput + input: Input ) -> Result { let mut seed = [0; 32]; rng.fill_bytes(&mut seed); diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index afa9f793..54618055 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -35,12 +35,6 @@ mod mixins; #[derive(Error, Debug)] pub enum TransactionError { - #[error("internal error ({0})")] - InternalError(String), - #[error("invalid ring member (member {0}, ring size {1})")] - InvalidRingMember(u8, u8), - #[error("invalid commitment")] - InvalidCommitment, #[error("no inputs")] NoInputs, #[error("too many outputs")] @@ -51,6 +45,8 @@ pub enum TransactionError { InvalidAddress, #[error("rpc error ({0})")] RpcError(RpcError), + #[error("clsag error ({0})")] + ClsagError(clsag::Error), #[error("invalid transaction ({0})")] InvalidTransaction(RpcError) } @@ -122,55 +118,6 @@ pub fn scan_tx(tx: &Transaction, view: Scalar, spend: EdwardsPoint) -> Vec, - // Ring, the index we're signing for, and the actual commitment behind it - pub(crate) ring: Vec<[EdwardsPoint; 2]>, - pub(crate) i: usize, - pub(crate) commitment: Commitment -} - -impl SignableInput { - pub fn new( - image: EdwardsPoint, - mixins: Vec, - ring: Vec<[EdwardsPoint; 2]>, - i: u8, - commitment: Commitment - ) -> Result { - let n = ring.len(); - if n > u8::MAX.into() { - Err(TransactionError::InternalError("max ring size in this library is u8 max".to_string()))?; - } - if i >= (n as u8) { - Err(TransactionError::InvalidRingMember(i, n as u8))?; - } - let i: usize = i.into(); - - // Validate the commitment matches - if ring[i][1] != commitment.calculate() { - Err(TransactionError::InvalidCommitment)?; - } - - Ok(SignableInput { image, mixins, ring, i, commitment }) - } - - #[cfg(feature = "multisig")] - pub fn context(&self) -> Vec { - let mut context = self.image.compress().to_bytes().to_vec(); - for pair in &self.ring { - // Doesn't include mixins[i] as CLSAG doesn't care and won't be affected by it - context.extend(&pair[0].compress().to_bytes()); - context.extend(&pair[1].compress().to_bytes()); - } - context.extend(&u8::try_from(self.i).unwrap().to_le_bytes()); - // Doesn't include commitment as the above ring + index includes the commitment - context - } -} - #[allow(non_snake_case)] fn shared_key(s: Scalar, P: &EdwardsPoint, o: usize) -> Scalar { let mut shared = (s * P).mul_by_cofactor().compress().to_bytes().to_vec(); @@ -269,29 +216,30 @@ pub async fn send( )); // Handle inputs + let mut mixins = Vec::with_capacity(inputs.len()); let mut signable = Vec::with_capacity(inputs.len()); - for input in inputs { - let (m, mixins) = mixins::select( + for (i, input) in inputs.iter().enumerate() { + let (m, mix) = mixins::select( rpc.get_o_indexes(input.tx).await.map_err(|e| TransactionError::RpcError(e))?[input.o] ); + mixins.push(mix); signable.push(( spend + input.key_offset, - SignableInput::new( + clsag::Input::new( key_image::generate(&(spend + input.key_offset)), - mixins.clone(), - rpc.get_ring(&mixins).await.map_err(|e| TransactionError::RpcError(e))?, + rpc.get_ring(&mixins[i]).await.map_err(|e| TransactionError::RpcError(e))?, m, input.commitment - )? + ).map_err(|e| TransactionError::ClsagError(e))? )); } let prefix = TransactionPrefix { version: VarInt(2), unlock_time: VarInt(0), - inputs: signable.iter().map(|input| TxIn::ToKey { + inputs: signable.iter().enumerate().map(|(i, input)| TxIn::ToKey { amount: VarInt(0), - key_offsets: mixins::offset(&input.1.mixins).iter().map(|x| VarInt(*x)).collect(), + key_offsets: mixins::offset(&mixins[i]).iter().map(|x| VarInt(*x)).collect(), k_image: KeyImage { image: Hash(input.1.image.compress().to_bytes()) } diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index 361bb320..ba1dd4f1 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -3,7 +3,7 @@ use rand_chacha::ChaCha12Rng; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; -use monero_serai::{random_scalar, Commitment, frost::MultisigError, key_image, clsag, transaction::SignableInput}; +use monero_serai::{random_scalar, Commitment, frost::MultisigError, key_image, clsag}; #[cfg(feature = "multisig")] use ::frost::sign; @@ -47,9 +47,8 @@ fn test_single() { msg, &vec![( secrets[0], - SignableInput::new( + clsag::Input::new( image, - [0; RING_LEN as usize].to_vec(), ring.clone(), RING_INDEX, Commitment::new(secrets[1], AMOUNT) @@ -113,7 +112,7 @@ fn test_multisig() -> Result<(), MultisigError> { clsag::Multisig::new( &mut ChaCha12Rng::seed_from_u64(1), msg, - SignableInput::new(image, vec![], ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap() + clsag::Input::new(image, ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap() ).unwrap(), keys[i - 1].clone(), &(1 ..= t).collect::>()