From 3e7598315cc41bde7d715d6f55f90fad35679305 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 6 May 2022 19:07:37 -0400 Subject: [PATCH] Reorganize CLSAG sign flow --- coins/monero/c/wrapper.c | 8 +- coins/monero/src/clsag/mod.rs | 66 +++++----- coins/monero/src/clsag/multisig.rs | 71 ++++++---- coins/monero/src/transaction/decoys.rs | 25 +++- coins/monero/src/transaction/mod.rs | 22 ++-- coins/monero/src/transaction/multisig.rs | 158 ++++++++++++----------- coins/monero/tests/clsag.rs | 41 ++++-- coins/monero/tests/send_multisig.rs | 2 +- 8 files changed, 227 insertions(+), 166 deletions(-) diff --git a/coins/monero/c/wrapper.c b/coins/monero/c/wrapper.c index a26dd049..4593e8a3 100644 --- a/coins/monero/c/wrapper.c +++ b/coins/monero/c/wrapper.c @@ -53,7 +53,7 @@ extern "C" { try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; } } - bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* m, uint8_t* p) { + bool c_verify_clsag(uint s_len, uint8_t* s, uint8_t* I, uint8_t k_len, uint8_t* k, uint8_t* p, uint8_t* m) { rct::clsag clsag; std::stringstream ss; std::string str; @@ -66,9 +66,6 @@ extern "C" { } memcpy(clsag.I.bytes, I, 32); - rct::key msg; - memcpy(msg.bytes, m, 32); - rct::ctkeyV keys; keys.resize(k_len); for (uint8_t i = 0; i < k_len; i++) { @@ -79,6 +76,9 @@ extern "C" { rct::key pseudo_out; memcpy(pseudo_out.bytes, p, 32); + rct::key msg; + memcpy(msg.bytes, m, 32); + try { return verRctCLSAGSimple(msg, clsag, keys, pseudo_out); } catch(...) { return false; } } } diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index cf93378c..c3579c11 100644 --- a/coins/monero/src/clsag/mod.rs +++ b/coins/monero/src/clsag/mod.rs @@ -8,23 +8,21 @@ use curve25519_dalek::{ edwards::{EdwardsPoint, VartimeEdwardsPrecomputation} }; -use monero::{ - consensus::Encodable, - util::ringct::{Key, Clsag} -}; +use monero::{consensus::Encodable, util::ringct::{Key, Clsag}}; use crate::{ Commitment, - c_verify_clsag, + transaction::decoys::Decoys, random_scalar, hash_to_scalar, - hash_to_point + hash_to_point, + c_verify_clsag }; #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] -pub use multisig::Multisig; +pub use multisig::{Details, Multisig}; #[derive(Error, Debug)] pub enum Error { @@ -36,49 +34,48 @@ pub enum Error { InvalidCommitment } -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, Debug)] pub struct Input { - // Ring, the index we're signing for, and the actual commitment behind it - pub ring: Vec<[EdwardsPoint; 2]>, - pub i: u8, - pub commitment: Commitment + // The actual commitment for the true spend + pub commitment: Commitment, + // True spend index, offsets, and ring + pub decoys: Decoys } impl Input { pub fn new( - ring: Vec<[EdwardsPoint; 2]>, - i: u8, - commitment: Commitment + commitment: Commitment, + decoys: Decoys ) -> Result { - let n = ring.len(); + let n = decoys.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))?; + if decoys.i >= (n as u8) { + Err(Error::InvalidRingMember(decoys.i, n as u8))?; } // Validate the commitment matches - if ring[usize::from(i)][1] != commitment.calculate() { + if decoys.ring[usize::from(decoys.i)][1] != commitment.calculate() { Err(Error::InvalidCommitment)?; } - Ok(Input { ring, i, commitment }) + Ok(Input { commitment, decoys }) } } #[allow(non_snake_case)] pub(crate) fn sign_core( rng: &mut R, - msg: &[u8; 32], - input: &Input, image: &EdwardsPoint, + input: &Input, mask: Scalar, + msg: &[u8; 32], A: EdwardsPoint, AH: EdwardsPoint ) -> (Clsag, Scalar, Scalar, Scalar, Scalar, EdwardsPoint) { - let n = input.ring.len(); - let r: usize = input.i.into(); + let n = input.decoys.len(); + let r: usize = input.decoys.i.into(); let C_out; @@ -94,7 +91,7 @@ pub(crate) fn sign_core( { C_out = Commitment::new(mask, input.commitment.amount).calculate(); - for member in &input.ring { + for member in &input.decoys.ring { P.push(member[0]); C_non_zero.push(member[1]); C.push(C_non_zero[C_non_zero.len() - 1] - C_out); @@ -188,9 +185,9 @@ pub(crate) fn sign_core( #[allow(non_snake_case)] pub fn sign( rng: &mut R, - msg: [u8; 32], - inputs: &[(Scalar, Input, EdwardsPoint)], - sum_outputs: Scalar + inputs: &[(Scalar, EdwardsPoint, Input)], + sum_outputs: Scalar, + msg: [u8; 32] ) -> Option> { if inputs.len() == 0 { return None; @@ -214,13 +211,14 @@ pub fn sign( rng.fill_bytes(&mut rand_source); let (mut clsag, c, mu_C, z, mu_P, C_out) = sign_core( rng, - &msg, &inputs[i].1, &inputs[i].2, mask, - &nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[usize::from(inputs[i].1.i)][0]) + &msg, + &nonce * &ED25519_BASEPOINT_TABLE, + nonce * hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0]) ); - clsag.s[inputs[i].1.i as usize] = Key { + clsag.s[inputs[i].2.decoys.i as usize] = Key { key: (nonce - (c * ((mu_C * z) + (mu_P * inputs[i].0)))).to_bytes() }; @@ -233,10 +231,10 @@ pub fn sign( // Uses Monero's C verification function to ensure compatibility with Monero pub fn verify( clsag: &Clsag, - msg: &[u8; 32], image: EdwardsPoint, ring: &[[EdwardsPoint; 2]], - pseudo_out: EdwardsPoint + pseudo_out: EdwardsPoint, + msg: &[u8; 32] ) -> bool { // Workaround for the fact monero-rs doesn't include the length of clsag.s in clsag encoding // despite it being part of clsag encoding. Reason for the patch version pin @@ -256,7 +254,7 @@ pub fn verify( unsafe { c_verify_clsag( serialized.len(), serialized.as_ptr(), image_bytes.as_ptr(), - ring.len() as u8, ring_bytes.as_ptr(), msg.as_ptr(), pseudo_out_bytes.as_ptr() + ring.len() as u8, ring_bytes.as_ptr(), pseudo_out_bytes.as_ptr(), msg.as_ptr() ) } } diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index 5cb04987..86fd3a24 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -31,13 +31,14 @@ impl Input { // Doesn't domain separate as this is considered part of the larger CLSAG proof // Ring index - transcript.append_message(b"ring_index", &[self.i]); + transcript.append_message(b"ring_index", &[self.decoys.i]); // Ring let mut ring = vec![]; - for pair in &self.ring { + for pair in &self.decoys.ring { // Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it - // They're just a mutable reference to this data + // They're just a unreliable reference to this data which will be included in the message + // if in use ring.extend(&pair[0].compress().to_bytes()); ring.extend(&pair[1].compress().to_bytes()); } @@ -49,9 +50,24 @@ impl Input { } } +// pub to enable testing +// While we could move the CLSAG test inside this crate, that'd require duplicating the FROST test +// helper, and isn't worth doing right now when this is harmless enough (semver? TODO) +#[derive(Clone, Debug)] +pub struct Details { + input: Input, + mask: Scalar +} + +impl Details { + pub fn new(input: Input, mask: Scalar) -> Details { + Details { input, mask } + } +} + #[allow(non_snake_case)] #[derive(Clone, Debug)] -struct ClsagSignInterim { +struct Interim { c: Scalar, s: Scalar, @@ -63,36 +79,33 @@ struct ClsagSignInterim { #[derive(Clone, Debug)] pub struct Multisig { transcript: Transcript, - input: Input, image: EdwardsPoint, commitments_H: Vec, AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), - msg: Rc>, - mask: Rc>, + details: Rc>>, + msg: Rc>>, - interim: Option + interim: Option } impl Multisig { pub fn new( transcript: Transcript, - input: Input, - msg: Rc>, - mask: Rc>, + details: Rc>>, + msg: Rc>>, ) -> Result { Ok( Multisig { transcript, - input, image: EdwardsPoint::identity(), commitments_H: vec![], AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), + details, msg, - mask, interim: None } @@ -102,6 +115,18 @@ impl Multisig { pub fn serialized_len() -> usize { 3 * (32 + 64) } + + fn input(&self) -> Input { + self.details.borrow().as_ref().unwrap().input.clone() + } + + fn mask(&self) -> Scalar { + self.details.borrow().as_ref().unwrap().mask + } + + fn msg(&self) -> [u8; 32] { + *self.msg.borrow().as_ref().unwrap() + } } impl Algorithm for Multisig { @@ -144,9 +169,9 @@ impl Algorithm for Multisig { if self.commitments_H.len() == 0 { self.transcript.domain_separate(b"CLSAG"); - self.input.transcript(&mut self.transcript); - self.transcript.append_message(b"message", &*self.msg.borrow()); - self.transcript.append_message(b"mask", &self.mask.borrow().to_bytes()); + self.input().transcript(&mut self.transcript); + self.transcript.append_message(b"mask", &self.mask().to_bytes()); + self.transcript.append_message(b"message", &self.msg()); } let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; @@ -156,7 +181,7 @@ impl Algorithm for Multisig { self.transcript.append_message(b"image_share", &share.compress().to_bytes()); self.image += share; - let alt = &hash_to_point(&self.input.ring[usize::from(self.input.i)][0]); + let alt = &hash_to_point(&view.group_key().0); // Uses the same format FROST does for the expected commitments (nonce * G where this is nonce * H) // Given this is guaranteed to match commitments, which FROST commits to, this also technically @@ -214,14 +239,14 @@ impl Algorithm for Multisig { #[allow(non_snake_case)] let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( &mut rng, - &self.msg.borrow(), - &self.input, &self.image, - *self.mask.borrow(), + &self.input(), + self.mask(), + &self.msg(), nonce_sum.0, self.AH.0.0 ); - self.interim = Some(ClsagSignInterim { c: c * mu_P, s: c * mu_C * z, clsag, C_out }); + self.interim = Some(Interim { c: c * mu_P, s: c * mu_C * z, clsag, C_out }); let share = dfg::Scalar(nonce.0 - (c * mu_P * view.secret_share().0)); @@ -237,8 +262,8 @@ impl Algorithm for Multisig { let interim = self.interim.as_ref().unwrap(); let mut clsag = interim.clsag.clone(); - clsag.s[usize::from(self.input.i)] = Key { key: (sum.0 - interim.s).to_bytes() }; - if verify(&clsag, &self.msg.borrow(), self.image, &self.input.ring, interim.C_out) { + clsag.s[usize::from(self.input().decoys.i)] = Key { key: (sum.0 - interim.s).to_bytes() }; + if verify(&clsag, self.image, &self.input().decoys.ring, interim.C_out, &self.msg()) { return Some((clsag, interim.C_out)); } return None; diff --git a/coins/monero/src/transaction/decoys.rs b/coins/monero/src/transaction/decoys.rs index d34bb3e6..9661221e 100644 --- a/coins/monero/src/transaction/decoys.rs +++ b/coins/monero/src/transaction/decoys.rs @@ -69,12 +69,25 @@ fn offset(decoys: &[u64]) -> Vec { res } +#[derive(Clone, Debug)] +pub struct Decoys { + pub i: u8, + pub offsets: Vec, + pub ring: Vec<[EdwardsPoint; 2]> +} + +impl Decoys { + pub fn len(&self) -> usize { + self.offsets.len() + } +} + pub(crate) async fn select( rng: &mut R, rpc: &Rpc, height: usize, inputs: &[SpendableOutput] -) -> Result, u8, Vec<[EdwardsPoint; 2]>)>, RpcError> { +) -> Result, RpcError> { // Convert the inputs in question to the raw output data let mut outputs = Vec::with_capacity(inputs.len()); for input in inputs { @@ -133,11 +146,11 @@ pub(crate) async fn select( } decoys[replace] = outputs[i]; - res.push(( - offset(&decoys.iter().map(|output| output.0).collect::>()), - u8::try_from(replace).unwrap(), - decoys.iter().map(|output| output.1).collect() - )); + res.push(Decoys { + i: u8::try_from(replace).unwrap(), + offsets: offset(&decoys.iter().map(|output| output.0).collect::>()), + ring: decoys.iter().map(|output| output.1).collect() + }); } Ok(res) diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index 08f5c8d3..49ed537b 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -37,7 +37,8 @@ use crate::{ #[cfg(feature = "multisig")] use crate::frost::MultisigError; -mod decoys; +pub mod decoys; + #[cfg(feature = "multisig")] mod multisig; @@ -198,7 +199,7 @@ async fn prepare_inputs( inputs: &[SpendableOutput], spend: &Scalar, tx: &mut Transaction -) -> Result, TransactionError> { +) -> Result, TransactionError> { // TODO sort inputs let mut signable = Vec::with_capacity(inputs.len()); @@ -214,18 +215,17 @@ async fn prepare_inputs( for (i, input) in inputs.iter().enumerate() { signable.push(( spend + input.key_offset, + key_image::generate(&(spend + input.key_offset)), clsag::Input::new( - decoys[i].2.clone(), - decoys[i].1, - input.commitment - ).map_err(|e| TransactionError::ClsagError(e))?, - key_image::generate(&(spend + input.key_offset)) + input.commitment, + decoys[i].clone() + ).map_err(|e| TransactionError::ClsagError(e))? )); tx.prefix.inputs.push(TxIn::ToKey { amount: VarInt(0), - key_offsets: decoys[i].0.clone(), - k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) } + key_offsets: decoys[i].offsets.clone(), + k_image: KeyImage { image: Hash(signable[i].1.compress().to_bytes()) } }); } @@ -370,9 +370,9 @@ impl SignableTransaction { let clsags = clsag::sign( rng, - tx.signature_hash().expect("Couldn't get the signature hash").0, &signable, - mask_sum + mask_sum, + tx.signature_hash().expect("Couldn't get the signature hash").0 ).unwrap(); // None if no inputs which new checks for let mut prunable = tx.rct_signatures.p.unwrap(); prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect(); diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/transaction/multisig.rs index 52c27143..e1b577e2 100644 --- a/coins/monero/src/transaction/multisig.rs +++ b/coins/monero/src/transaction/multisig.rs @@ -17,9 +17,9 @@ use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine use crate::{ frost::{Transcript, Ed25519}, - key_image, bulletproofs, clsag, + random_scalar, key_image, bulletproofs, clsag, rpc::Rpc, - transaction::{TransactionError, SignableTransaction, decoys} + transaction::{TransactionError, SignableTransaction, decoys::{self, Decoys}} }; pub struct TransactionMachine { @@ -27,12 +27,15 @@ pub struct TransactionMachine { signable: SignableTransaction, transcript: Transcript, + decoys: Vec, + our_images: Vec, - mask_sum: Rc>, - msg: Rc>, + output_masks: Option, + inputs: Vec>>>, + msg: Rc>>, clsags: Vec>, - inputs: Vec, - tx: Option, + + tx: Option } impl SignableTransaction { @@ -41,32 +44,30 @@ impl SignableTransaction { label: Vec, rng: &mut R, rpc: &Rpc, - keys: Rc>, height: usize, + keys: Rc>, included: &[usize] ) -> Result { let mut our_images = vec![]; - - let mask_sum = Rc::new(RefCell::new(Scalar::zero())); - let msg = Rc::new(RefCell::new([0; 32])); - let mut clsags = vec![]; - let mut inputs = vec![]; + inputs.resize(self.inputs.len(), Rc::new(RefCell::new(None))); + let msg = Rc::new(RefCell::new(None)); + let mut clsags = vec![]; // Create a RNG out of the input shared keys, which either requires the view key or being every // sender, and the payments (address and amount), which a passive adversary may be able to know // depending on how these transactions are coordinated - // The lack of dedicated entropy here is frustrating. We can probably provide entropy inclusion - // if we move CLSAG ring to a Rc RefCell like msg and mask? TODO let mut transcript = Transcript::new(label); + // Also include the spend_key as below only the key offset is included, so this confirms the sum product + // Useful as confirming the sum product confirms the key image, further guaranteeing the one time + // properties noted below + transcript.append_message(b"spend_key", &keys.group_key().0.compress().to_bytes()); for input in &self.inputs { // These outputs can only be spent once. Therefore, it forces all RNGs derived from this // transcript (such as the one used to create one time keys) to be unique transcript.append_message(b"input_hash", &input.tx.0); - // TODO: Should this be u8, u16, or u32? Right now, outputs are solely up to 16, but what - // about the future? - transcript.append_message(b"input_output_index", &u64::try_from(input.o).unwrap().to_le_bytes()); + transcript.append_message(b"input_output_index", &u16::try_from(input.o).unwrap().to_le_bytes()); // Not including this, with a doxxed list of payments, would allow brute forcing the inputs // to determine RNG seeds and therefore the true spends transcript.append_message(b"input_shared_key", &input.key_offset.to_bytes()); @@ -75,10 +76,13 @@ impl SignableTransaction { transcript.append_message(b"payment_address", &payment.0.as_bytes()); transcript.append_message(b"payment_amount", &payment.1.to_le_bytes()); } - // Not only is this an output, but this locks to the base keys to be complete with the above key offsets transcript.append_message(b"change", &self.change.as_bytes()); // Select decoys + // Ideally, this would be done post entropy, instead of now, yet doing so would require sign + // to be async which isn't feasible. This should be suitably competent though + // While this inability means we can immediately create the input, moving it out of the + // Rc RefCell, keeping it within an Rc RefCell keeps our options flexible let decoys = decoys::select( &mut ChaCha12Rng::from_seed(transcript.rng_seed(b"decoys", None)), rpc, @@ -98,24 +102,13 @@ impl SignableTransaction { AlgorithmMachine::new( clsag::Multisig::new( transcript.clone(), - clsag::Input::new( - decoys[i].2.clone(), - decoys[i].1, - input.commitment - ).map_err(|e| TransactionError::ClsagError(e))?, - msg.clone(), - mask_sum.clone() + inputs[i].clone(), + msg.clone() ).map_err(|e| TransactionError::MultisigError(e))?, Rc::new(keys), included ).map_err(|e| TransactionError::FrostError(e))? ); - - inputs.push(TxIn::ToKey { - amount: VarInt(0), - key_offsets: decoys[i].0.clone(), - k_image: KeyImage { image: Hash([0; 32]) } - }); } // Verify these outputs by a dummy prep @@ -125,11 +118,15 @@ impl SignableTransaction { leader: keys.params().i() == included[0], signable: self, transcript, + + decoys, + our_images, - mask_sum, + output_masks: None, + inputs, msg, clsags, - inputs, + tx: None }) } @@ -159,8 +156,8 @@ impl StateMachine for TransactionMachine { let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"tx_keys", Some(entropy))); // Safe to unwrap thanks to the dummy prepare - let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).unwrap(); - self.mask_sum.replace(mask_sum); + let (commitments, output_masks) = self.signable.prepare_outputs(&mut rng).unwrap(); + self.output_masks = Some(output_masks); let bp = bulletproofs::generate(&commitments).unwrap(); bp.consensus_encode(&mut serialized).unwrap(); @@ -186,68 +183,79 @@ impl StateMachine for TransactionMachine { let clsag_lens = clsag_len * self.clsags.len(); // Split out the prep and update the TX - let mut tx = None; + let mut tx; if self.leader { - tx = self.tx.take(); + tx = self.tx.take().unwrap(); } else { - for (l, prep) in commitments.iter().enumerate() { - if prep.is_none() { - continue; - } - let prep = prep.as_ref().unwrap(); + let (l, prep) = commitments.iter().enumerate().filter(|(_, prep)| prep.is_some()).next() + .ok_or(FrostError::InternalError("no participants".to_string()))?; + let prep = prep.as_ref().unwrap(); - let mut rng = ChaCha12Rng::from_seed( + // Not invalid outputs due to doing a dummy prep as leader + let (commitments, output_masks) = self.signable.prepare_outputs( + &mut ChaCha12Rng::from_seed( self.transcript.rng_seed( b"tx_keys", Some(prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidShare(l))?) ) - ); - // Not invalid outputs due to doing a dummy prep as leader - let (commitments, mask_sum) = self.signable.prepare_outputs(&mut rng).map_err(|_| FrostError::InvalidShare(l))?; - self.mask_sum.replace(mask_sum); + ) + ).map_err(|_| FrostError::InvalidShare(l))?; + self.output_masks.replace(output_masks); - // Verify the provided bulletproofs if not leader - let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?; - if !bulletproofs::verify(&bp, &commitments.iter().map(|c| c.calculate()).collect::>()) { - Err(FrostError::InvalidShare(l))?; - } - - let tx_inner = self.signable.prepare_transaction(&commitments, bp); - tx = Some(tx_inner); - break; + // Verify the provided bulletproofs if not leader + let bp = deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidShare(l))?; + if !bulletproofs::verify(&bp, &commitments.iter().map(|c| c.calculate()).collect::>()) { + Err(FrostError::InvalidShare(l))?; } + + tx = self.signable.prepare_transaction(&commitments, bp); } - // Calculate the key images and update the TX - // Multisig will parse/calculate/validate this as needed, yet doing so here as well provides - // the easiest API overall + let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks", None)); + let mut sum_pseudo_outs = Scalar::zero(); for c in 0 .. self.clsags.len() { + // Calculate the key images in order to update the TX + // Multisig will parse/calculate/validate this as needed, yet doing so here as well provides + // the easiest API overall let mut image = self.our_images[c]; - for (l, serialized) in commitments.iter().enumerate() { - if serialized.is_none() { - continue; - } - + for (l, serialized) in commitments.iter().enumerate().filter(|(_, s)| s.is_some()) { image += CompressedEdwardsY( serialized.as_ref().unwrap()[((c * clsag_len) + 64) .. ((c * clsag_len) + 96)] .try_into().map_err(|_| FrostError::InvalidCommitment(l))? ).decompress().ok_or(FrostError::InvalidCommitment(l))?; } - self.inputs[c] = match self.inputs[c].clone() { - TxIn::ToKey { amount, key_offsets, k_image: _ } => TxIn::ToKey { - amount, key_offsets, + // TODO sort inputs + + let mut mask = random_scalar(&mut rng); + if c == (self.clsags.len() - 1) { + mask = self.output_masks.unwrap() - sum_pseudo_outs; + } else { + sum_pseudo_outs += mask; + } + + self.inputs[c].replace( + Some( + clsag::Details::new( + clsag::Input::new( + self.signable.inputs[c].commitment, + self.decoys[c].clone() + ).map_err(|_| panic!("Signing an input which isn't present in the ring we created for it"))?, + mask + ) + ) + ); + + tx.prefix.inputs.push( + TxIn::ToKey { + amount: VarInt(0), + key_offsets: self.decoys[c].offsets.clone(), k_image: KeyImage { image: Hash(image.compress().to_bytes()) } - }, - _ => panic!("Signing for an input which isn't ToKey") - }; + } + ); } - // TODO sort inputs - - let mut tx = tx.unwrap(); - tx.prefix.inputs = self.inputs.clone(); - self.msg.replace(tx.signature_hash().unwrap().0); + self.msg.replace(Some(tx.signature_hash().unwrap().0)); self.tx = Some(tx); // Iterate over each CLSAG calling sign diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index 80256a2e..2584efea 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -5,7 +5,9 @@ use rand::{RngCore, rngs::OsRng}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; -use monero_serai::{random_scalar, Commitment, key_image, clsag}; +use monero::VarInt; + +use monero_serai::{random_scalar, Commitment, transaction::decoys::Decoys, key_image, clsag}; #[cfg(feature = "multisig")] use monero_serai::frost::{MultisigError, Transcript}; @@ -40,19 +42,22 @@ fn test_single() { let image = key_image::generate(&secrets[0]); let (clsag, pseudo_out) = clsag::sign( &mut OsRng, - msg, &vec![( secrets[0], + image, clsag::Input::new( - ring.clone(), - RING_INDEX, - Commitment::new(secrets[1], AMOUNT) - ).unwrap(), - image + Commitment::new(secrets[1], AMOUNT), + Decoys { + i: RING_INDEX, + offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(), + ring: ring.clone() + } + ).unwrap() )], - Scalar::zero() + random_scalar(&mut OsRng), + msg ).unwrap().swap_remove(0); - assert!(clsag::verify(&clsag, &msg, image, &ring, pseudo_out)); + assert!(clsag::verify(&clsag, image, &ring, pseudo_out, &msg)); } #[cfg(feature = "multisig")] @@ -79,15 +84,27 @@ fn test_multisig() -> Result<(), MultisigError> { ring.push([&dest * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]); } + let mask_sum = random_scalar(&mut OsRng); let mut machines = Vec::with_capacity(t); for i in 1 ..= t { machines.push( sign::AlgorithmMachine::new( clsag::Multisig::new( Transcript::new(b"Monero Serai CLSAG Test".to_vec()), - clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(), - Rc::new(RefCell::new([1; 32])), - Rc::new(RefCell::new(Scalar::from(42u64))) + Rc::new(RefCell::new(Some( + clsag::Details::new( + clsag::Input::new( + Commitment::new(randomness, AMOUNT), + Decoys { + i: RING_INDEX, + offsets: (1 ..= RING_LEN).into_iter().map(|o| VarInt(o)).collect(), + ring: ring.clone() + } + ).unwrap(), + mask_sum + ) + ))), + Rc::new(RefCell::new(Some([1; 32]))) ).unwrap(), keys[i - 1].clone(), &(1 ..= THRESHOLD).collect::>() diff --git a/coins/monero/tests/send_multisig.rs b/coins/monero/tests/send_multisig.rs index 6f2fcef6..0b2fa29e 100644 --- a/coins/monero/tests/send_multisig.rs +++ b/coins/monero/tests/send_multisig.rs @@ -60,8 +60,8 @@ pub async fn send_multisig() { b"Monero Serai Test Transaction".to_vec(), &mut OsRng, &rpc, - keys[i - 1].clone(), rpc.get_height().await.unwrap() - 10, + keys[i - 1].clone(), &(1 ..= THRESHOLD).collect::>() ).await.unwrap() );