diff --git a/coins/monero/wallet/src/old_send/builder.rs b/coins/monero/wallet/src/old_send/builder.rs index 053b0588..8e4a689c 100644 --- a/coins/monero/wallet/src/old_send/builder.rs +++ b/coins/monero/wallet/src/old_send/builder.rs @@ -11,7 +11,6 @@ use crate::{ struct SignableTransactionBuilderInternal { protocol: WalletProtocol, fee_rate: FeeRate, - r_seed: Option>, inputs: Vec<(SpendableOutput, Decoys)>, payments: Vec<(MoneroAddress, u64)>, diff --git a/coins/monero/wallet/src/old_send/multisig.rs b/coins/monero/wallet/src/old_send/multisig.rs deleted file mode 100644 index 8eb279e0..00000000 --- a/coins/monero/wallet/src/old_send/multisig.rs +++ /dev/null @@ -1,414 +0,0 @@ -use std_shims::{ - vec::Vec, - io::{self, Read}, - collections::HashMap, -}; - -use zeroize::Zeroizing; - -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha20Rng; - -use group::ff::Field; -use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint}; -use dalek_ff_group as dfg; - -use transcript::{Transcript, RecommendedTranscript}; -use frost::{ - curve::Ed25519, - Participant, FrostError, ThresholdKeys, - dkg::lagrange, - sign::{ - Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, - SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, - }, -}; - -use monero_serai::{ - ringct::{ - clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig}, - RctPrunable, RctProofs, - }, - transaction::{Input, Transaction}, -}; -use crate::{TransactionError, InternalPayment, SignableTransaction, key_image_sort, uniqueness}; - -/// FROST signing machine to produce a signed transaction. -pub struct TransactionMachine { - signable: SignableTransaction, - - i: Participant, - transcript: RecommendedTranscript, - - // Hashed key and scalar offset - key_images: Vec<(EdwardsPoint, Scalar)>, - clsag_mask_sends: Vec, - clsags: Vec>, -} - -pub struct TransactionSignMachine { - signable: SignableTransaction, - - i: Participant, - transcript: RecommendedTranscript, - - key_images: Vec<(EdwardsPoint, Scalar)>, - clsag_mask_sends: Vec, - clsags: Vec>, - - our_preprocess: Vec>, -} - -pub struct TransactionSignatureMachine { - tx: Transaction, - clsags: Vec>, -} - -impl SignableTransaction { - /// Create a FROST signing machine out of this signable transaction. - /// The height is the Monero blockchain height to synchronize around. - pub fn multisig( - self, - keys: &ThresholdKeys, - mut transcript: RecommendedTranscript, - ) -> Result { - let mut clsag_mask_sends = vec![]; - 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 - // Being every sender would already let you note rings which happen to use your transactions - // multiple times, already breaking privacy there - - transcript.domain_separate(b"monero_transaction"); - - // Also include the spend_key as below only the key offset is included, so this transcripts the - // sum product - // Useful as transcripting the sum product effectively transcripts the key image, further - // guaranteeing the one time properties noted below - transcript.append_message(b"spend_key", keys.group_key().0.compress().to_bytes()); - - if let Some(r_seed) = &self.r_seed { - transcript.append_message(b"r_seed", r_seed); - } - - for (input, decoys) 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.output.absolute.tx); - transcript.append_message(b"input_output_index", [input.output.absolute.o]); - // 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()); - - // Ensure all signers are signing the same rings - transcript.append_message(b"real_spend", [decoys.signer_index()]); - for (i, ring_member) in decoys.ring().iter().enumerate() { - transcript - .append_message(b"ring_member", [u8::try_from(i).expect("ring size exceeded 255")]); - transcript.append_message(b"ring_member_offset", decoys.offsets()[i].to_le_bytes()); - transcript.append_message(b"ring_member_key", ring_member[0].compress().to_bytes()); - transcript.append_message(b"ring_member_commitment", ring_member[1].compress().to_bytes()); - } - } - - for payment in &self.payments { - match payment { - InternalPayment::Payment(payment, need_dummy_payment_id) => { - transcript.append_message(b"payment_address", payment.0.to_string().as_bytes()); - transcript.append_message(b"payment_amount", payment.1.to_le_bytes()); - transcript.append_message( - b"need_dummy_payment_id", - [if *need_dummy_payment_id { 1u8 } else { 0u8 }], - ); - } - InternalPayment::Change(change, change_view) => { - transcript.append_message(b"change_address", change.0.to_string().as_bytes()); - transcript.append_message(b"change_amount", change.1.to_le_bytes()); - if let Some(view) = change_view.as_ref() { - transcript.append_message(b"change_view_key", Zeroizing::new(view.to_bytes())); - } - } - } - } - - let mut key_images = vec![]; - for (i, (input, decoys)) in self.inputs.iter().enumerate() { - // Check this the right set of keys - let offset = keys.offset(dfg::Scalar(input.key_offset())); - if offset.group_key().0 != input.key() { - Err(TransactionError::WrongPrivateKey)?; - } - - let context = ClsagContext::new(decoys.clone(), input.commitment()) - .map_err(TransactionError::ClsagError)?; - let (clsag, clsag_mask_send) = ClsagMultisig::new(transcript.clone(), context); - clsag_mask_sends.push(clsag_mask_send); - key_images.push(( - clsag.key_image_generator(), - keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + self.inputs[i].0.key_offset(), - )); - clsags.push(AlgorithmMachine::new(clsag, offset)); - } - - Ok(TransactionMachine { - signable: self, - i: keys.params().i(), - transcript, - key_images, - clsag_mask_sends, - clsags, - }) - } -} - -impl PreprocessMachine for TransactionMachine { - type Preprocess = Vec>; - type Signature = Transaction; - type SignMachine = TransactionSignMachine; - - fn preprocess( - mut self, - rng: &mut R, - ) -> (TransactionSignMachine, Self::Preprocess) { - // Iterate over each CLSAG calling preprocess - let mut preprocesses = Vec::with_capacity(self.clsags.len()); - let clsags = self - .clsags - .drain(..) - .map(|clsag| { - let (clsag, preprocess) = clsag.preprocess(rng); - preprocesses.push(preprocess); - clsag - }) - .collect(); - let our_preprocess = preprocesses.clone(); - - // We could add further entropy here, and previous versions of this library did so - // As of right now, the multisig's key, the inputs being spent, and the FROST data itself - // will be used for RNG seeds. In order to recreate these RNG seeds, breaking privacy, - // counterparties must have knowledge of the multisig, either the view key or access to the - // coordination layer, and then access to the actual FROST signing process - // If the commitments are sent in plain text, then entropy here also would be, making it not - // increase privacy. If they're not sent in plain text, or are otherwise inaccessible, they - // already offer sufficient entropy. That's why further entropy is not included - - ( - TransactionSignMachine { - signable: self.signable, - - i: self.i, - transcript: self.transcript, - - key_images: self.key_images, - clsag_mask_sends: self.clsag_mask_sends, - clsags, - - our_preprocess, - }, - preprocesses, - ) - } -} - -impl SignMachine for TransactionSignMachine { - type Params = (); - type Keys = ThresholdKeys; - type Preprocess = Vec>; - type SignatureShare = Vec>; - type SignatureMachine = TransactionSignatureMachine; - - fn cache(self) -> CachedPreprocess { - unimplemented!( - "Monero transactions don't support caching their preprocesses due to {}", - "being already bound to a specific transaction" - ); - } - - fn from_cache( - (): (), - _: ThresholdKeys, - _: CachedPreprocess, - ) -> (Self, Self::Preprocess) { - unimplemented!( - "Monero transactions don't support caching their preprocesses due to {}", - "being already bound to a specific transaction" - ); - } - - fn read_preprocess(&self, reader: &mut R) -> io::Result { - self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect() - } - - fn sign( - mut self, - mut commitments: HashMap, - msg: &[u8], - ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { - if !msg.is_empty() { - panic!("message was passed to the TransactionMachine when it generates its own"); - } - - // Find out who's included - // This may not be a valid set of signers yet the algorithm machine will error if it's not - commitments.remove(&self.i); // Remove, if it was included for some reason - let mut included = commitments.keys().copied().collect::>(); - included.push(self.i); - included.sort_unstable(); - - // Start calculating the key images, as needed on the TX level - let mut images = vec![EdwardsPoint::identity(); self.clsags.len()]; - for (image, (generator, offset)) in images.iter_mut().zip(&self.key_images) { - *image = generator * offset; - } - - // Convert the serialized nonces commitments to a parallelized Vec - let mut commitments = (0 .. self.clsags.len()) - .map(|c| { - included - .iter() - .map(|l| { - // Add all commitments to the transcript for their entropy - // While each CLSAG will do this as they need to for security, they have their own - // transcripts cloned from this TX's initial premise's transcript. For our TX - // transcript to have the CLSAG data for entropy, it'll have to be added ourselves here - self.transcript.append_message(b"participant", (*l).to_bytes()); - - let preprocess = if *l == self.i { - self.our_preprocess[c].clone() - } else { - commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone() - }; - - { - let mut buf = vec![]; - preprocess.write(&mut buf).unwrap(); - self.transcript.append_message(b"preprocess", buf); - } - - // While here, calculate the key image - // Clsag will parse/calculate/validate this as needed, yet doing so here as well - // provides the easiest API overall, as this is where the TX is (which needs the key - // images in its message), along with where the outputs are determined (where our - // outputs may need these in order to guarantee uniqueness) - images[c] += - preprocess.addendum.key_image_share().0 * lagrange::(*l, &included).0; - - Ok((*l, preprocess)) - }) - .collect::, _>>() - }) - .collect::, _>>()?; - - // Remove our preprocess which shouldn't be here. It was just the easiest way to implement the - // above - for map in &mut commitments { - map.remove(&self.i); - } - - // Create the actual transaction - let (mut tx, output_masks) = { - let mut sorted_images = images.clone(); - sorted_images.sort_by(key_image_sort); - - self.signable.prepare_transaction( - // Technically, r_seed is used for the transaction keys if it's provided - &mut ChaCha20Rng::from_seed(self.transcript.rng_seed(b"transaction_keys_bulletproofs")), - uniqueness( - &sorted_images - .iter() - .map(|image| Input::ToKey { amount: None, key_offsets: vec![], key_image: *image }) - .collect::>(), - ), - ) - }; - - // Sort the inputs, as expected - let mut sorted = Vec::with_capacity(self.clsags.len()); - while !self.clsags.is_empty() { - sorted.push(( - images.swap_remove(0), - self.signable.inputs.swap_remove(0).1, - self.clsag_mask_sends.swap_remove(0), - self.clsags.swap_remove(0), - commitments.swap_remove(0), - )); - } - sorted.sort_by(|x, y| key_image_sort(&x.0, &y.0)); - - let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks")); - let mut sum_pseudo_outs = Scalar::ZERO; - while !sorted.is_empty() { - let value = sorted.remove(0); - - let mut mask = Scalar::random(&mut rng); - if sorted.is_empty() { - mask = output_masks - sum_pseudo_outs; - } else { - sum_pseudo_outs += mask; - } - value.2.send(mask); - - tx.prefix_mut().inputs.push(Input::ToKey { - amount: None, - key_offsets: value.1.offsets().to_vec(), - key_image: value.0, - }); - - self.clsags.push(value.3); - commitments.push(value.4); - } - - let msg = tx.signature_hash().unwrap(); - - // Iterate over each CLSAG calling sign - let mut shares = Vec::with_capacity(self.clsags.len()); - let clsags = self - .clsags - .drain(..) - .map(|clsag| { - let (clsag, share) = clsag.sign(commitments.remove(0), &msg)?; - shares.push(share); - Ok(clsag) - }) - .collect::>()?; - - Ok((TransactionSignatureMachine { tx, clsags }, shares)) - } -} - -impl SignatureMachine for TransactionSignatureMachine { - type SignatureShare = Vec>; - - fn read_share(&self, reader: &mut R) -> io::Result { - self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect() - } - - fn complete( - mut self, - shares: HashMap, - ) -> Result { - let mut tx = self.tx; - match tx { - Transaction::V2 { - proofs: - Some(RctProofs { - prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. }, - .. - }), - .. - } => { - for (c, clsag) in self.clsags.drain(..).enumerate() { - let (clsag, pseudo_out) = clsag.complete( - shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::>(), - )?; - clsags.push(clsag); - pseudo_outs.push(pseudo_out); - } - } - _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"), - } - Ok(tx) - } -} diff --git a/coins/monero/wallet/src/send/mod.rs b/coins/monero/wallet/src/send/mod.rs index 27d5a24e..c07f61a3 100644 --- a/coins/monero/wallet/src/send/mod.rs +++ b/coins/monero/wallet/src/send/mod.rs @@ -31,6 +31,13 @@ mod tx; mod eventuality; pub use eventuality::Eventuality; +#[cfg(feature = "multisig")] +mod multisig; + +pub(crate) fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering { + x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() +} + #[derive(Clone, PartialEq, Eq, Zeroize)] enum ChangeEnum { None, @@ -406,9 +413,6 @@ impl SignableTransaction { debug_assert_eq!(self.inputs.len(), key_images.len()); // Sort the inputs by their key images - fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering { - x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() - } let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::>(); sorted_inputs .sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b)); @@ -461,12 +465,7 @@ impl SignableTransaction { } // Get the output commitments' mask sum - let mask_sum = tx - .intent - .commitments_and_encrypted_amounts(&tx.key_images) - .into_iter() - .map(|(commitment, _)| commitment.mask) - .sum::(); + let mask_sum = tx.intent.sum_output_masks(&tx.key_images); // Get the actual TX, just needing the CLSAGs let mut tx = tx.transaction_without_signatures(); diff --git a/coins/monero/wallet/src/send/multisig.rs b/coins/monero/wallet/src/send/multisig.rs new file mode 100644 index 00000000..4efe4480 --- /dev/null +++ b/coins/monero/wallet/src/send/multisig.rs @@ -0,0 +1,302 @@ +use std_shims::{ + vec::Vec, + io::{self, Read}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use group::ff::Field; +use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint}; +use dalek_ff_group as dfg; + +use transcript::{Transcript, RecommendedTranscript}; +use frost::{ + curve::Ed25519, + Participant, FrostError, ThresholdKeys, + dkg::lagrange, + sign::{ + Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine, + AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, + }, +}; + +use monero_serai::{ + ringct::{ + clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig}, + RctPrunable, RctProofs, + }, + transaction::Transaction, +}; +use crate::send::{SendError, SignableTransaction, key_image_sort}; + +/// FROST signing machine to produce a signed transaction. +pub struct TransactionMachine { + signable: SignableTransaction, + + i: Participant, + + // The key image generator, and the scalar offset from the spend key + key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>, + clsags: Vec<(ClsagMultisigMaskSender, AlgorithmMachine)>, +} + +pub struct TransactionSignMachine { + signable: SignableTransaction, + + i: Participant, + + key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>, + clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine)>, + + our_preprocess: Vec>, +} + +pub struct TransactionSignatureMachine { + tx: Transaction, + clsags: Vec>, +} + +impl SignableTransaction { + /// Create a FROST signing machine out of this signable transaction. + pub fn multisig(self, keys: &ThresholdKeys) -> Result { + let mut clsags = vec![]; + + let mut key_image_generators_and_offsets = vec![]; + for (i, (input, decoys)) in self.inputs.iter().enumerate() { + // Check this is the right set of keys + let offset = keys.offset(dfg::Scalar(input.key_offset())); + if offset.group_key().0 != input.key() { + Err(SendError::WrongPrivateKey)?; + } + + let context = + ClsagContext::new(decoys.clone(), input.commitment()).map_err(SendError::ClsagError)?; + let (clsag, clsag_mask_send) = ClsagMultisig::new( + RecommendedTranscript::new(b"Monero Multisignature Transaction"), + context, + ); + key_image_generators_and_offsets.push(( + clsag.key_image_generator(), + keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + self.inputs[i].0.key_offset(), + )); + clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset))); + } + + Ok(TransactionMachine { + signable: self, + i: keys.params().i(), + key_image_generators_and_offsets, + clsags, + }) + } +} + +impl PreprocessMachine for TransactionMachine { + type Preprocess = Vec>; + type Signature = Transaction; + type SignMachine = TransactionSignMachine; + + fn preprocess( + mut self, + rng: &mut R, + ) -> (TransactionSignMachine, Self::Preprocess) { + // Iterate over each CLSAG calling preprocess + let mut preprocesses = Vec::with_capacity(self.clsags.len()); + let clsags = self + .clsags + .drain(..) + .map(|(clsag_mask_send, clsag)| { + let (clsag, preprocess) = clsag.preprocess(rng); + preprocesses.push(preprocess); + (clsag_mask_send, clsag) + }) + .collect(); + let our_preprocess = preprocesses.clone(); + + ( + TransactionSignMachine { + signable: self.signable, + + i: self.i, + + key_image_generators_and_offsets: self.key_image_generators_and_offsets, + clsags, + + our_preprocess, + }, + preprocesses, + ) + } +} + +impl SignMachine for TransactionSignMachine { + type Params = (); + type Keys = ThresholdKeys; + type Preprocess = Vec>; + type SignatureShare = Vec>; + type SignatureMachine = TransactionSignatureMachine; + + fn cache(self) -> CachedPreprocess { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn from_cache( + (): (), + _: ThresholdKeys, + _: CachedPreprocess, + ) -> (Self, Self::Preprocess) { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn read_preprocess(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.1.read_preprocess(reader)).collect() + } + + fn sign( + self, + mut commitments: HashMap, + msg: &[u8], + ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { + if !msg.is_empty() { + panic!("message was passed to the TransactionMachine when it generates its own"); + } + + // We do not need to be included here, yet this set of signers has yet to be validated + // We explicitly remove ourselves to ensure we aren't included twice, if we were redundantly + // included + commitments.remove(&self.i); + + // Find out who's included + let mut included = commitments.keys().copied().collect::>(); + // This push won't duplicate due to the above removal + included.push(self.i); + // unstable sort may reorder elements of equal order + // Given our lack of duplicates, we should have no elements of equal order + included.sort_unstable(); + + // Start calculating the key images, as needed on the TX level + let mut key_images = vec![EdwardsPoint::identity(); self.clsags.len()]; + for (image, (generator, offset)) in + key_images.iter_mut().zip(&self.key_image_generators_and_offsets) + { + *image = generator * offset; + } + + // Convert the serialized nonces commitments to a parallelized Vec + let mut commitments = (0 .. self.clsags.len()) + .map(|c| { + included + .iter() + .map(|l| { + let preprocess = if *l == self.i { + self.our_preprocess[c].clone() + } else { + commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone() + }; + + // While here, calculate the key image as needed to call sign + // The CLSAG algorithm will independently calculate the key image/verify these shares + key_images[c] += + preprocess.addendum.key_image_share().0 * lagrange::(*l, &included).0; + + Ok((*l, preprocess)) + }) + .collect::, _>>() + }) + .collect::, _>>()?; + + // The above inserted our own preprocess into these maps (which is unnecessary) + // Remove it now + for map in &mut commitments { + map.remove(&self.i); + } + + // The actual TX will have sorted its inputs by key image + // We apply the same sort now to our CLSAG machines + let mut clsags = Vec::with_capacity(self.clsags.len()); + for ((key_image, clsag), commitments) in key_images.iter().zip(self.clsags).zip(commitments) { + clsags.push((key_image, clsag, commitments)); + } + clsags.sort_by(|x, y| key_image_sort(x.0, y.0)); + let clsags = + clsags.into_iter().map(|(_, clsag, commitments)| (clsag, commitments)).collect::>(); + + // Specify the TX's key images + let tx = self.signable.with_key_images(key_images); + + // We now need to decide the masks for each CLSAG + let clsag_len = clsags.len(); + let output_masks = tx.intent.sum_output_masks(&tx.key_images); + let mut rng = tx.intent.seeded_rng(b"multisig_pseudo_out_masks"); + let mut sum_pseudo_outs = Scalar::ZERO; + let mut to_sign = Vec::with_capacity(clsag_len); + for (i, ((clsag_mask_send, clsag), commitments)) in clsags.into_iter().enumerate() { + let mut mask = Scalar::random(&mut rng); + if i == (clsag_len - 1) { + mask = output_masks - sum_pseudo_outs; + } else { + sum_pseudo_outs += mask; + } + clsag_mask_send.send(mask); + to_sign.push((clsag, commitments)); + } + + let tx = tx.transaction_without_signatures(); + let msg = tx.signature_hash().unwrap(); + + // Iterate over each CLSAG calling sign + let mut shares = Vec::with_capacity(to_sign.len()); + let clsags = to_sign + .drain(..) + .map(|(clsag, commitments)| { + let (clsag, share) = clsag.sign(commitments, &msg)?; + shares.push(share); + Ok(clsag) + }) + .collect::>()?; + + Ok((TransactionSignatureMachine { tx, clsags }, shares)) + } +} + +impl SignatureMachine for TransactionSignatureMachine { + type SignatureShare = Vec>; + + fn read_share(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect() + } + + fn complete( + mut self, + shares: HashMap, + ) -> Result { + let mut tx = self.tx; + match tx { + Transaction::V2 { + proofs: + Some(RctProofs { + prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. }, + .. + }), + .. + } => { + for (c, clsag) in self.clsags.drain(..).enumerate() { + let (clsag, pseudo_out) = clsag.complete( + shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::>(), + )?; + clsags.push(clsag); + pseudo_outs.push(pseudo_out); + } + } + _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"), + } + Ok(tx) + } +} diff --git a/coins/monero/wallet/src/send/tx_keys.rs b/coins/monero/wallet/src/send/tx_keys.rs index 4409b50d..f8eac0e4 100644 --- a/coins/monero/wallet/src/send/tx_keys.rs +++ b/coins/monero/wallet/src/send/tx_keys.rs @@ -228,4 +228,12 @@ impl SignableTransaction { } res } + + pub(crate) fn sum_output_masks(&self, key_images: &[EdwardsPoint]) -> Scalar { + self + .commitments_and_encrypted_amounts(key_images) + .into_iter() + .map(|(commitment, _)| commitment.mask) + .sum() + } }