diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index 52c4ab95..e156354c 100644 --- a/coins/monero/src/clsag/mod.rs +++ b/coins/monero/src/clsag/mod.rs @@ -24,7 +24,7 @@ use crate::{ #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] -pub use multisig::{TransactionData, Multisig}; +pub use multisig::Multisig; #[derive(Error, Debug)] pub enum Error { diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index 4df3e8e1..db9fd91d 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -1,4 +1,5 @@ use core::fmt::Debug; +use std::{rc::Rc, cell::RefCell}; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha12Rng; @@ -14,7 +15,7 @@ use curve25519_dalek::{ use group::Group; use dalek_ff_group as dfg; -use frost::{Curve, FrostError, algorithm::Algorithm, sign::ParamsView}; +use frost::{Curve, FrostError, algorithm::Algorithm, MultisigView}; use monero::util::ringct::{Key, Clsag}; @@ -25,11 +26,6 @@ use crate::{ clsag::{Input, sign_core, verify} }; -pub trait TransactionData: Clone + Debug { - fn msg(&self) -> [u8; 32]; - fn mask_sum(&self) -> Scalar; -} - #[allow(non_snake_case)] #[derive(Clone, Debug)] struct ClsagSignInterim { @@ -42,23 +38,26 @@ struct ClsagSignInterim { #[allow(non_snake_case)] #[derive(Clone, Debug)] -pub struct Multisig { +pub struct Multisig { entropy: Vec, AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), input: Input, image: EdwardsPoint, - data: D, + + msg: Rc>, + mask_sum: Rc>, interim: Option } -impl Multisig { +impl Multisig { pub fn new( input: Input, - data: D - ) -> Result, MultisigError> { + msg: Rc>, + mask_sum: Rc>, + ) -> Result { Ok( Multisig { entropy: vec![], @@ -67,38 +66,45 @@ impl Multisig { input, image: EdwardsPoint::identity(), - data, + + msg, + mask_sum, interim: None } ) } + + pub fn serialized_len() -> usize { + 3 * (32 + 64) + } } -impl Algorithm for Multisig { +impl Algorithm for Multisig { type Signature = (Clsag, EdwardsPoint); // We arguably don't have to commit to the nonces at all thanks to xG and yG being committed to, // both of those being proven to have the same scalar as xH and yH, yet it doesn't hurt - // As for the image, that should be committed to by the msg from TransactionData, yet putting it - // here as well ensures the security bounds of this + // As for the image, that should be committed to by the msg, yet putting it here as well ensures + // the security bounds of this fn addendum_commit_len() -> usize { 3 * 32 } fn preprocess_addendum( rng: &mut R, - view: &ParamsView, + view: &MultisigView, nonces: &[dfg::Scalar; 2] ) -> Vec { - let (mut serialized, proof) = key_image::generate_share(rng, view); + let (share, proof) = key_image::generate_share(rng, view); #[allow(non_snake_case)] let H = hash_to_point(&view.group_key().0); #[allow(non_snake_case)] let nH = (nonces[0].0 * H, nonces[1].0 * H); - serialized.reserve_exact(3 * (32 + 64)); + let mut serialized = Vec::with_capacity(Multisig::serialized_len()); + serialized.extend(share.compress().to_bytes()); serialized.extend(nH.0.compress().to_bytes()); serialized.extend(nH.1.compress().to_bytes()); serialized.extend(&DLEqProof::prove(rng, &nonces[0].0, &H, &nH.0).serialize()); @@ -109,12 +115,12 @@ impl Algorithm for Multisig { fn process_addendum( &mut self, - view: &ParamsView, + view: &MultisigView, l: usize, commitments: &[dfg::EdwardsPoint; 2], serialized: &[u8] ) -> Result<(), FrostError> { - if serialized.len() != (3 * (32 + 64)) { + if serialized.len() != Multisig::serialized_len() { // Not an optimal error but... Err(FrostError::InvalidCommitmentQuantity(l, 9, serialized.len() / 32))?; } @@ -122,7 +128,7 @@ impl Algorithm for Multisig { // Use everyone's commitments to derive a random source all signers can agree upon // Cannot be manipulated to effect and all signers must, and will, know this self.entropy.extend(&l.to_le_bytes()); - self.entropy.extend(&serialized[0 .. (3 * 32)]); + self.entropy.extend(&serialized[0 .. Multisig::addendum_commit_len()]); let (share, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; self.image += share; @@ -154,19 +160,16 @@ impl Algorithm for Multisig { } fn context(&self) -> Vec { - let mut context = vec![]; - // This should be redundant as the image should be in the addendum if using Multisig and in msg - // if signing a Transaction, yet this ensures CLSAG takes responsibility for its own security - // boundaries - context.extend(&self.image.compress().to_bytes()); - context.extend(&self.data.msg()); + let mut context = Vec::with_capacity(32 + 32 + 1 + (2 * 11 * 32)); + context.extend(&*self.msg.borrow()); + context.extend(&self.mask_sum.borrow().to_bytes()); context.extend(&self.input.context()); context } fn sign_share( &mut self, - view: &ParamsView, + view: &MultisigView, nonce_sum: dfg::EdwardsPoint, b: dfg::Scalar, nonce: dfg::Scalar, @@ -186,10 +189,10 @@ impl Algorithm for Multisig { #[allow(non_snake_case)] let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( &mut rng, - &self.data.msg(), + &self.msg.borrow(), &self.input, &self.image, - self.data.mask_sum(), + *self.mask_sum.borrow(), nonce_sum.0, self.AH.0.0 ); @@ -210,7 +213,7 @@ impl Algorithm for Multisig { let mut clsag = interim.clsag.clone(); clsag.s[self.input.i] = Key { key: (sum.0 - interim.s).to_bytes() }; - if verify(&clsag, &self.data.msg(), self.image, &self.input.ring, interim.C_out) { + if verify(&clsag, &self.msg.borrow(), self.image, &self.input.ring, interim.C_out) { return Some((clsag, interim.C_out)); } return None; diff --git a/coins/monero/src/key_image/multisig.rs b/coins/monero/src/key_image/multisig.rs index a8db96d9..978bed63 100644 --- a/coins/monero/src/key_image/multisig.rs +++ b/coins/monero/src/key_image/multisig.rs @@ -1,15 +1,15 @@ use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; -use frost::sign::ParamsView; +use frost::MultisigView; use crate::{hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}}; #[allow(non_snake_case)] pub fn generate_share( rng: &mut R, - view: &ParamsView -) -> (Vec, Vec) { + view: &MultisigView +) -> (EdwardsPoint, Vec) { let H = hash_to_point(&view.group_key().0); let image = view.secret_share().0 * H; // Includes a proof. Since: @@ -20,14 +20,11 @@ pub fn generate_share( // lagranged_secret * G is known. lagranged_secret * H is being sent // Any discrete log equality proof confirms the same secret was used, // forming a valid key_image share - ( - image.compress().to_bytes().to_vec(), - DLEqProof::prove(rng, &view.secret_share().0, &H, &image).serialize() - ) + (image, DLEqProof::prove(rng, &view.secret_share().0, &H, &image).serialize()) } pub fn verify_share( - view: &ParamsView, + view: &MultisigView, l: usize, share: &[u8] ) -> Result<(EdwardsPoint, Vec), MultisigError> { diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index 069833f8..f27ed4dc 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -26,6 +26,9 @@ use monero::{ } }; +#[cfg(feature = "multisig")] +use frost::FrostError; + use crate::{ Commitment, random_scalar, @@ -33,13 +36,12 @@ use crate::{ key_image, bulletproofs, clsag, rpc::{Rpc, RpcError} }; +#[cfg(feature = "multisig")] +use crate::frost::MultisigError; mod mixins; - #[cfg(feature = "multisig")] mod multisig; -#[cfg(feature = "multisig")] -pub use multisig::Multisig; #[derive(Error, Debug)] pub enum TransactionError { @@ -60,10 +62,16 @@ pub enum TransactionError { #[error("clsag error ({0})")] ClsagError(clsag::Error), #[error("invalid transaction ({0})")] - InvalidTransaction(RpcError) + InvalidTransaction(RpcError), + #[cfg(feature = "multisig")] + #[error("frost error {0}")] + FrostError(FrostError), + #[cfg(feature = "multisig")] + #[error("multisig error {0}")] + MultisigError(MultisigError) } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct SpendableOutput { pub tx: Hash, pub o: usize, @@ -192,157 +200,23 @@ enum Preparation<'a, R: RngCore + CryptoRng> { Follower([u8; 32], Bulletproof) } -fn prepare_outputs<'a, R: RngCore + CryptoRng>( - prep: &mut Preparation<'a, R>, - inputs: &[SpendableOutput], - payments: &[(Address, u64)], - change: Address, - fee_per_byte: u64 -) -> Result<(Vec, Scalar, Transaction), TransactionError> { - let fee = fee_per_byte * 2000; // TODO - - // TODO TX MAX SIZE - - // Make sure we have enough funds - let in_amount = inputs.iter().map(|input| input.commitment.amount).sum(); - let out_amount = fee + payments.iter().map(|payment| payment.1).sum::(); - if in_amount < out_amount { - Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?; - } - - // Add the change output - let mut payments = payments.to_vec(); - payments.push((change, in_amount - out_amount)); - - // Grab the prep - let mut entropy = [0; 32]; - let mut bp = None; - match prep { - Preparation::Leader(ref mut rng) => { - // The Leader generates the entropy for the one time keys and the bulletproof - rng.fill_bytes(&mut entropy); - }, - Preparation::Follower(e, b) => { - entropy = e.clone(); - bp = Some(b.clone()); - } - } - - let mut seed = b"StealthAddress_randomness".to_vec(); - // Leader selected entropy to prevent de-anonymization via recalculation of randomness - seed.extend(&entropy); - // This output can only be spent once. Therefore, it forces all one time keys used here to be - // unique, even if the leader reuses entropy. While another transaction could use a different - // input ordering to swap which 0 is, that input set can't contain this input without being a - // double spend - seed.extend(&inputs[0].tx.0); - seed.extend(&inputs[0].o.to_le_bytes()); - let mut rng = ChaCha12Rng::from_seed(Blake2b512::digest(seed)[0 .. 32].try_into().unwrap()); - - let mut outputs = Vec::with_capacity(payments.len()); - let mut commitments = Vec::with_capacity(payments.len()); - for o in 0 .. payments.len() { - outputs.push(Output::new(&mut rng, payments[o], o)?); - commitments.push(Commitment::new(outputs[o].mask, payments[o].1)); - } - - if bp.is_none() { - // Generate the bulletproof if leader - bp = Some(bulletproofs::generate(&commitments)?); - } else { - // Verify the bulletproof if follower - if !bulletproofs::verify( - bp.as_ref().unwrap(), - &commitments.iter().map(|c| c.calculate()).collect::>() - ) { - Err(TransactionError::InvalidPreparation("invalid bulletproof".to_string()))?; - } - } - - // Create the TX extra - let mut extra = ExtraField(vec![ - SubField::TxPublicKey(PublicKey { point: outputs[0].R.compress() }) - ]); - extra.0.push(SubField::AdditionalPublickKey( - outputs[1 .. outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect() - )); - - // Format it for monero-rs - let mut mrs_outputs = Vec::with_capacity(outputs.len()); - let mut out_pk = Vec::with_capacity(outputs.len()); - let mut ecdh_info = Vec::with_capacity(outputs.len()); - for o in 0 .. outputs.len() { - mrs_outputs.push(TxOut { - amount: VarInt(0), - target: TxOutTarget::ToKey { key: PublicKey { point: outputs[o].dest.compress() } } - }); - out_pk.push(CtKey { - mask: Key { key: commitments[o].calculate().compress().to_bytes() } - }); - ecdh_info.push(EcdhInfo::Bulletproof { amount: outputs[o].amount }); - } - - Ok(( - match prep { - // Encode the prep - Preparation::Leader(..) => { - let mut prep = entropy.to_vec(); - bp.as_ref().unwrap().consensus_encode(&mut prep).expect("Couldn't encode bulletproof"); - prep - }, - Preparation::Follower(..) => { - vec![] - } - }, - outputs.iter().map(|output| output.mask).sum(), - Transaction { - prefix: TransactionPrefix { - version: VarInt(2), - unlock_time: VarInt(0), - inputs: vec![], - outputs: mrs_outputs, - extra - }, - signatures: vec![], - rct_signatures: RctSig { - sig: Some(RctSigBase { - rct_type: RctType::Clsag, - txn_fee: VarInt(fee), - pseudo_outs: vec![], - ecdh_info, - out_pk - }), - p: Some(RctSigPrunable { - range_sigs: vec![], - bulletproofs: vec![bp.unwrap()], - MGs: vec![], - Clsags: vec![], - pseudo_outs: vec![] - }) - } - } - )) -} - async fn prepare_inputs( rpc: &Rpc, spend: &Scalar, inputs: &[SpendableOutput], tx: &mut Transaction ) -> Result, TransactionError> { - let mut mixins = Vec::with_capacity(inputs.len()); let mut signable = Vec::with_capacity(inputs.len()); for (i, input) in inputs.iter().enumerate() { // Select mixins - let (m, mix) = mixins::select( + let (m, mixins) = 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, clsag::Input::new( - rpc.get_ring(&mixins[i]).await.map_err(|e| TransactionError::RpcError(e))?, + rpc.get_ring(&mixins).await.map_err(|e| TransactionError::RpcError(e))?, m, input.commitment ).map_err(|e| TransactionError::ClsagError(e))?, @@ -351,7 +225,7 @@ async fn prepare_inputs( tx.prefix.inputs.push(TxIn::ToKey { amount: VarInt(0), - key_offsets: mixins::offset(&mixins[i]).iter().map(|x| VarInt(*x)).collect(), + key_offsets: mixins::offset(&mixins).iter().map(|x| VarInt(*x)).collect(), k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) } }); } @@ -390,19 +264,142 @@ impl SignableTransaction { ) } + fn prepare_outputs<'a, R: RngCore + CryptoRng>( + &self, + prep: &mut Preparation<'a, R> + ) -> Result<(Vec, Scalar, Transaction), TransactionError> { + let fee = self.fee_per_byte * 2000; // TODO + + // TODO TX MAX SIZE + + // Make sure we have enough funds + let in_amount = self.inputs.iter().map(|input| input.commitment.amount).sum(); + let out_amount = fee + self.payments.iter().map(|payment| payment.1).sum::(); + if in_amount < out_amount { + Err(TransactionError::NotEnoughFunds(in_amount, out_amount))?; + } + + // Add the change output + let mut payments = self.payments.clone(); + payments.push((self.change, in_amount - out_amount)); + + // Grab the prep + let mut entropy = [0; 32]; + let mut bp = None; + match prep { + Preparation::Leader(ref mut rng) => { + // The Leader generates the entropy for the one time keys and the bulletproof + rng.fill_bytes(&mut entropy); + }, + Preparation::Follower(e, b) => { + entropy = e.clone(); + bp = Some(b.clone()); + } + } + + let mut seed = b"StealthAddress_randomness".to_vec(); + // Leader selected entropy to prevent de-anonymization via recalculation of randomness + seed.extend(&entropy); + // This output can only be spent once. Therefore, it forces all one time keys used here to be + // unique, even if the leader reuses entropy. While another transaction could use a different + // input ordering to swap which 0 is, that input set can't contain this input without being a + // double spend + seed.extend(&self.inputs[0].tx.0); + seed.extend(&self.inputs[0].o.to_le_bytes()); + let mut rng = ChaCha12Rng::from_seed(Blake2b512::digest(seed)[0 .. 32].try_into().unwrap()); + + let mut outputs = Vec::with_capacity(payments.len()); + let mut commitments = Vec::with_capacity(payments.len()); + for o in 0 .. payments.len() { + outputs.push(Output::new(&mut rng, payments[o], o)?); + commitments.push(Commitment::new(outputs[o].mask, payments[o].1)); + } + + if bp.is_none() { + // Generate the bulletproof if leader + bp = Some(bulletproofs::generate(&commitments)?); + } else { + // Verify the bulletproof if follower + if !bulletproofs::verify( + bp.as_ref().unwrap(), + &commitments.iter().map(|c| c.calculate()).collect::>() + ) { + Err(TransactionError::InvalidPreparation("invalid bulletproof".to_string()))?; + } + } + + // Create the TX extra + let mut extra = ExtraField(vec![ + SubField::TxPublicKey(PublicKey { point: outputs[0].R.compress() }) + ]); + extra.0.push(SubField::AdditionalPublickKey( + outputs[1 .. outputs.len()].iter().map(|output| PublicKey { point: output.R.compress() }).collect() + )); + + // Format it for monero-rs + let mut mrs_outputs = Vec::with_capacity(outputs.len()); + let mut out_pk = Vec::with_capacity(outputs.len()); + let mut ecdh_info = Vec::with_capacity(outputs.len()); + for o in 0 .. outputs.len() { + mrs_outputs.push(TxOut { + amount: VarInt(0), + target: TxOutTarget::ToKey { key: PublicKey { point: outputs[o].dest.compress() } } + }); + out_pk.push(CtKey { + mask: Key { key: commitments[o].calculate().compress().to_bytes() } + }); + ecdh_info.push(EcdhInfo::Bulletproof { amount: outputs[o].amount }); + } + + Ok(( + match prep { + // Encode the prep + Preparation::Leader(..) => { + let mut prep = entropy.to_vec(); + bp.as_ref().unwrap().consensus_encode(&mut prep).expect("Couldn't encode bulletproof"); + prep + }, + Preparation::Follower(..) => { + vec![] + } + }, + outputs.iter().map(|output| output.mask).sum(), + Transaction { + prefix: TransactionPrefix { + version: VarInt(2), + unlock_time: VarInt(0), + inputs: vec![], + outputs: mrs_outputs, + extra + }, + signatures: vec![], + rct_signatures: RctSig { + sig: Some(RctSigBase { + rct_type: RctType::Clsag, + txn_fee: VarInt(fee), + pseudo_outs: vec![], + ecdh_info, + out_pk + }), + p: Some(RctSigPrunable { + range_sigs: vec![], + bulletproofs: vec![bp.unwrap()], + MGs: vec![], + Clsags: vec![], + pseudo_outs: vec![] + }) + } + } + )) + } + pub async fn sign( &self, rng: &mut R, rpc: &Rpc, spend: &Scalar ) -> Result { - let (_, mask_sum, mut tx) = prepare_outputs( - &mut Preparation::Leader(rng), - &self.inputs, - &self.payments, - self.change, - self.fee_per_byte - )?; + let (_, mask_sum, mut tx) = self.prepare_outputs(&mut Preparation::Leader(rng))?; let signable = prepare_inputs(rpc, spend, &self.inputs, &mut tx).await?; diff --git a/coins/monero/src/transaction/multisig.rs b/coins/monero/src/transaction/multisig.rs new file mode 100644 index 00000000..e740b5b4 --- /dev/null +++ b/coins/monero/src/transaction/multisig.rs @@ -0,0 +1,237 @@ +use std::{rc::Rc, cell::RefCell}; + +use rand_core::{RngCore, CryptoRng}; +use rand_chacha::ChaCha12Rng; + +use curve25519_dalek::{scalar::Scalar, edwards::{EdwardsPoint, CompressedEdwardsY}}; + +use frost::{FrostError, MultisigKeys, MultisigParams, sign::{State, StateMachine, AlgorithmMachine}}; + +use monero::{ + Hash, VarInt, + consensus::deserialize, + util::ringct::Key, + blockdata::transaction::{KeyImage, TxIn, Transaction} +}; + +use crate::{ + frost::Ed25519, + key_image, + clsag, + rpc::Rpc, + transaction::{TransactionError, Preparation, SignableTransaction, mixins} +}; + +pub struct TransactionMachine { + leader: bool, + signable: SignableTransaction, + our_images: Vec, + inputs: Vec, + tx: Option, + mask_sum: Rc>, + msg: Rc>, + clsags: Vec> +} + +impl SignableTransaction { + pub async fn multisig( + self, + rng: &mut R, + rpc: &Rpc, + keys: Rc>, + included: &[usize] + ) -> Result { + let mut our_images = vec![]; + let mut inputs = vec![]; + let mask_sum = Rc::new(RefCell::new(Scalar::zero())); + let msg = Rc::new(RefCell::new([0; 32])); + let mut clsags = vec![]; + for input in &self.inputs { + // Select mixins + let (m, mixins) = mixins::select( + rpc.get_o_indexes(input.tx).await.map_err(|e| TransactionError::RpcError(e))?[input.o] + ); + + let keys = keys.offset(dalek_ff_group::Scalar(input.key_offset)); + let (image, _) = key_image::generate_share( + rng, + &keys.view(included).map_err(|e| TransactionError::FrostError(e))? + ); + our_images.push(image); + clsags.push( + AlgorithmMachine::new( + clsag::Multisig::new( + clsag::Input::new( + rpc.get_ring(&mixins).await.map_err(|e| TransactionError::RpcError(e))?, + m, + input.commitment + ).map_err(|e| TransactionError::ClsagError(e))?, + msg.clone(), + mask_sum.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: mixins::offset(&mixins).iter().map(|x| VarInt(*x)).collect(), + k_image: KeyImage { image: Hash([0; 32]) } + }); + } + + // Verify these outputs by a dummy prep + self.prepare_outputs(&mut Preparation::Leader(rng))?; + + Ok(TransactionMachine { + leader: keys.params().i() == included[0], + signable: self, + our_images, + inputs, + tx: None, + mask_sum, + msg, + clsags + }) + } +} + +impl StateMachine for TransactionMachine { + type Signature = Transaction; + + fn preprocess( + &mut self, + rng: &mut R + ) -> Result, FrostError> { + if self.state() != State::Fresh { + Err(FrostError::InvalidSignTransition(State::Fresh, self.state()))?; + } + + // Iterate over each CLSAG calling preprocess + let mut serialized = vec![]; + for clsag in self.clsags.iter_mut() { + serialized.extend(&clsag.preprocess(rng)?); + } + + if self.leader { + let (prep, mask_sum, tx) = self.signable.prepare_outputs(&mut Preparation::Leader(rng)).unwrap(); + self.mask_sum.replace(mask_sum); + self.tx = Some(tx); + + serialized.extend(&prep); + } + + Ok(serialized) + } + + fn sign( + &mut self, + commitments: &[Option>], + _: &[u8] + ) -> Result, FrostError> { + if self.state() != State::Preprocessed { + Err(FrostError::InvalidSignTransition(State::Preprocessed, self.state()))?; + } + + // FROST commitments, image, commitments, and their proofs + let clsag_len = 64 + clsag::Multisig::serialized_len(); + let clsag_lens = clsag_len * self.clsags.len(); + + // Split out the prep and update the TX + let mut tx = None; + if self.leader { + tx = self.tx.take(); + } else { + for (l, prep) in commitments.iter().enumerate() { + if prep.is_none() { + continue; + } + let prep = prep.as_ref().unwrap(); + + // Handle the prep with a seeded RNG type to make rustc happy + let (_, mask_sum, tx_inner) = self.signable.prepare_outputs::( + &mut Preparation::Follower( + prep[clsag_lens .. (clsag_lens + 32)].try_into().map_err(|_| FrostError::InvalidCommitment(l))?, + deserialize(&prep[(clsag_lens + 32) .. prep.len()]).map_err(|_| FrostError::InvalidCommitment(l))? + ) + ).map_err(|_| FrostError::InvalidShare(l))?; // Not invalid outputs due to doing a dummy prep as leader + self.mask_sum.replace(mask_sum); + tx = Some(tx_inner); + break; + } + } + + // 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 + for c in 0 .. self.clsags.len() { + let mut image = self.our_images[c]; + for (l, serialized) in commitments.iter().enumerate() { + if serialized.is_none() { + continue; + } + + 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, + k_image: KeyImage { image: Hash(image.compress().to_bytes()) } + }, + _ => panic!("Signing for an input which isn't ToKey") + }; + } + + let mut tx = tx.unwrap(); + tx.prefix.inputs = self.inputs.clone(); + self.msg.replace(tx.signature_hash().unwrap().0); + self.tx = Some(tx); + + // Iterate over each CLSAG calling sign + let mut serialized = Vec::with_capacity(self.clsags.len() * 32); + for (c, clsag) in self.clsags.iter_mut().enumerate() { + serialized.extend(&clsag.sign( + &commitments.iter().map( + |commitments| commitments.clone().map( + |commitments| commitments[(c * clsag_len) .. ((c * clsag_len) + clsag_len)].to_vec() + ) + ).collect::>(), + &vec![] + )?); + } + + Ok(serialized) + } + + fn complete(&mut self, shares: &[Option>]) -> Result { + if self.state() != State::Signed { + Err(FrostError::InvalidSignTransition(State::Signed, self.state()))?; + } + + let mut tx = self.tx.take().unwrap(); + let mut prunable = tx.rct_signatures.p.unwrap(); + for (c, clsag) in self.clsags.iter_mut().enumerate() { + let (clsag, pseudo_out) = clsag.complete(&shares.iter().map( + |share| share.clone().map(|share| share[(c * 32) .. ((c * 32) + 32)].to_vec()) + ).collect::>())?; + prunable.Clsags.push(clsag); + prunable.pseudo_outs.push(Key { key: pseudo_out.compress().to_bytes() }); + } + tx.rct_signatures.p = Some(prunable); + + Ok(tx) + } + + fn multisig_params(&self) -> MultisigParams { + self.clsags[0].multisig_params() + } + + fn state(&self) -> State { + self.clsags[0].state() + } +} diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index 5f843fb4..1866da64 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -1,3 +1,5 @@ +use std::{rc::Rc, cell::RefCell}; + use rand::{RngCore, rngs::OsRng}; use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; @@ -50,20 +52,6 @@ fn test_single() { assert!(clsag::verify(&clsag, &msg, image, &ring, pseudo_out)); } -#[cfg(feature = "multisig")] -#[derive(Clone, Debug)] -struct TransactionData; -#[cfg(feature = "multisig")] -impl clsag::TransactionData for TransactionData { - fn msg(&self) -> [u8; 32] { - [1; 32] - } - - fn mask_sum(&self) -> Scalar { - Scalar::from(21u64) - } -} - #[cfg(feature = "multisig")] #[test] fn test_multisig() -> Result<(), MultisigError> { @@ -94,7 +82,8 @@ fn test_multisig() -> Result<(), MultisigError> { sign::AlgorithmMachine::new( clsag::Multisig::new( clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(), - TransactionData + Rc::new(RefCell::new([1; 32])), + Rc::new(RefCell::new(Scalar::from(42u64))) ).unwrap(), keys[i - 1].clone(), &(1 ..= THRESHOLD).collect::>() diff --git a/coins/monero/tests/frost.rs b/coins/monero/tests/frost.rs index 7eb856e5..5ff74335 100644 --- a/coins/monero/tests/frost.rs +++ b/coins/monero/tests/frost.rs @@ -2,11 +2,10 @@ use std::rc::Rc; -use rand_core::{RngCore, CryptoRng}; use rand::rngs::OsRng; use ff::Field; -use dalek_ff_group::{ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}; +use dalek_ff_group::{ED25519_BASEPOINT_TABLE, Scalar}; pub use frost::{ FrostError, MultisigParams, MultisigKeys, @@ -15,50 +14,8 @@ pub use frost::{ use monero_serai::frost::Ed25519; -pub const THRESHOLD: usize = 5; -pub const PARTICIPANTS: usize = 8; - -#[derive(Clone)] -pub struct DummyAlgorithm; -impl Algorithm for DummyAlgorithm { - type Signature = (); - - fn addendum_commit_len() -> usize { unimplemented!() } - - fn preprocess_addendum( - _: &mut R, - _: &sign::ParamsView, - _: &[Scalar; 2], - ) -> Vec { unimplemented!() } - - fn process_addendum( - &mut self, - _: &sign::ParamsView, - _: usize, - _: &[EdwardsPoint; 2], - _: &[u8], - ) -> Result<(), FrostError> { unimplemented!() } - - fn context(&self) -> Vec { unimplemented!() } - - fn sign_share( - &mut self, - _: &sign::ParamsView, - _: EdwardsPoint, - _: Scalar, - _: Scalar, - _: &[u8], - ) -> Scalar { unimplemented!() } - - fn verify(&self, _: EdwardsPoint, _: EdwardsPoint, _: Scalar) -> Option { unimplemented!() } - - fn verify_share( - &self, - _: EdwardsPoint, - _: EdwardsPoint, - _: Scalar, - ) -> bool { unimplemented!() } -} +pub const THRESHOLD: usize = 3; +pub const PARTICIPANTS: usize = 5; pub fn generate_keys() -> (Vec>>, Scalar) { let mut params = vec![]; diff --git a/coins/monero/tests/key_image.rs b/coins/monero/tests/key_image.rs index 1910ffdc..0bd1b8c7 100644 --- a/coins/monero/tests/key_image.rs +++ b/coins/monero/tests/key_image.rs @@ -4,15 +4,13 @@ use rand::{RngCore, rngs::OsRng}; use curve25519_dalek::{traits::Identity, edwards::EdwardsPoint}; -use monero_serai::{frost::MultisigError, key_image}; - -use ::frost::sign; +use monero_serai::key_image; mod frost; -use crate::frost::{THRESHOLD, PARTICIPANTS, DummyAlgorithm, generate_keys}; +use crate::frost::{THRESHOLD, PARTICIPANTS, generate_keys}; #[test] -fn test() -> Result<(), MultisigError> { +fn test() { let (keys, group_private) = generate_keys(); let image = key_image::generate(&group_private); @@ -27,16 +25,16 @@ fn test() -> Result<(), MultisigError> { for i in 1 ..= PARTICIPANTS { if included.contains(&i) { // If they were included, include their view - views.push(sign::Params::new(DummyAlgorithm, keys[i - 1].clone(), &included).unwrap().view()); + views.push(keys[i - 1].view(&included).unwrap()); let share = key_image::generate_share(&mut OsRng, &views[i - 1]); - let mut serialized = share.0; + let mut serialized = share.0.compress().to_bytes().to_vec(); serialized.extend(b"abc"); serialized.extend(&share.1); shares.push(serialized); } else { - // If they weren't included, include dummy data + // If they weren't included, include dummy data to fill the Vec // Uses the view of someone actually included as Params::new verifies inclusion - views.push(sign::Params::new(DummyAlgorithm, keys[included[0] - 1].clone(), &included).unwrap().view()); + views.push(keys[included[0] - 1].view(&included).unwrap()); shares.push(vec![]); } } @@ -50,6 +48,4 @@ fn test() -> Result<(), MultisigError> { } assert_eq!(image, multi_image); } - - Ok(()) } diff --git a/coins/monero/tests/send_multisig.rs b/coins/monero/tests/send_multisig.rs new file mode 100644 index 00000000..b6df0c62 --- /dev/null +++ b/coins/monero/tests/send_multisig.rs @@ -0,0 +1,73 @@ +#![cfg(feature = "multisig")] + +use rand::rngs::OsRng; + +use blake2::{digest::Update, Digest, Blake2b512}; + +use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; +use dalek_ff_group::Scalar; + +use monero::{ + cryptonote::hash::Hashable, + network::Network, + util::{key::PublicKey, address::Address} +}; + +use monero_serai::{transaction::{self, SignableTransaction}, rpc::Rpc}; + +mod rpc; +use crate::rpc::mine_block; + +mod frost; +use crate::frost::{THRESHOLD, generate_keys, sign}; + +#[tokio::test] +pub async fn send_multisig() { + let rpc = Rpc::new("http://127.0.0.1:18081".to_string()); + + let fee_per_byte = 50000000; + let fee = fee_per_byte * 2000; + + let (keys, _) = generate_keys(); + let t = keys[0].params().t(); + + // Generate an address + let view = Scalar::from_hash(Blake2b512::new().chain("Serai DEX")).0; + let spend = keys[0].group_key().0; + let addr = Address::standard( + Network::Mainnet, + PublicKey { point: spend.compress() }, + PublicKey { point: (&view * &ED25519_BASEPOINT_TABLE).compress() } + ); + + // Mine blocks to that address + let start = rpc.get_height().await.unwrap(); + for _ in 0 .. 7 { + mine_block(&rpc, addr.to_string()).await.unwrap(); + } + + // Get the input TX + let tx = rpc.get_block_transactions(start).await.unwrap().swap_remove(0); + let output = transaction::scan(&tx, view, spend).swap_remove(0); + let amount = output.commitment.amount - fee; + + let mut machines = Vec::with_capacity(t); + for i in 1 ..= t { + machines.push( + SignableTransaction::new( + vec![output.clone()], vec![(addr, amount)], addr, fee_per_byte + ).unwrap().multisig( + &mut OsRng, + &rpc, + keys[i - 1].clone(), + &(1 ..= THRESHOLD).collect::>() + ).await.unwrap() + ); + } + + let txs = sign(&mut machines, keys); + for s in 0 .. (t - 1) { + assert_eq!(txs[s].hash(), txs[0].hash()); + } + rpc.publish_transaction(&txs[0]).await.unwrap(); +} diff --git a/sign/frost/src/algorithm.rs b/sign/frost/src/algorithm.rs index f25c132b..054b46eb 100644 --- a/sign/frost/src/algorithm.rs +++ b/sign/frost/src/algorithm.rs @@ -4,7 +4,7 @@ use rand_core::{RngCore, CryptoRng}; use group::Group; -use crate::{Curve, FrostError, sign}; +use crate::{Curve, FrostError, MultisigView}; /// Algorithm to use FROST with pub trait Algorithm: Clone { @@ -17,14 +17,14 @@ pub trait Algorithm: Clone { /// Generate an addendum to FROST"s preprocessing stage fn preprocess_addendum( rng: &mut R, - params: &sign::ParamsView, + params: &MultisigView, nonces: &[C::F; 2], ) -> Vec; /// Proccess the addendum for the specified participant. Guaranteed to be ordered fn process_addendum( &mut self, - params: &sign::ParamsView, + params: &MultisigView, l: usize, commitments: &[C::G; 2], serialized: &[u8], @@ -39,7 +39,7 @@ pub trait Algorithm: Clone { /// The nonce will already have been processed into the combined form d + (e * p) fn sign_share( &mut self, - params: &sign::ParamsView, + params: &MultisigView, nonce_sum: C::G, b: C::F, nonce: C::F, @@ -98,7 +98,7 @@ impl> Algorithm for Schnorr { fn preprocess_addendum( _: &mut R, - _: &sign::ParamsView, + _: &MultisigView, _: &[C::F; 2], ) -> Vec { vec![] @@ -106,7 +106,7 @@ impl> Algorithm for Schnorr { fn process_addendum( &mut self, - _: &sign::ParamsView, + _: &MultisigView, _: usize, _: &[C::G; 2], _: &[u8], @@ -120,7 +120,7 @@ impl> Algorithm for Schnorr { fn sign_share( &mut self, - params: &sign::ParamsView, + params: &MultisigView, nonce_sum: C::G, _: C::F, nonce: C::F, diff --git a/sign/frost/src/lib.rs b/sign/frost/src/lib.rs index 5c4c118c..4da270e8 100644 --- a/sign/frost/src/lib.rs +++ b/sign/frost/src/lib.rs @@ -1,6 +1,6 @@ use core::{ops::Mul, fmt::Debug}; -use ff::PrimeField; +use ff::{Field, PrimeField}; use group::{Group, GroupOps, ScalarMul}; use thiserror::Error; @@ -8,6 +8,7 @@ use thiserror::Error; pub mod key_gen; pub mod algorithm; pub mod sign; +use sign::lagrange; /// Set of errors for curve-related operations, namely encoding and decoding #[derive(Error, Debug)] @@ -190,6 +191,33 @@ pub enum FrostError { InternalError(String), } +// View of keys passable to algorithm implementations +#[derive(Clone)] +pub struct MultisigView { + group_key: C::G, + included: Vec, + secret_share: C::F, + verification_shares: Vec, +} + +impl MultisigView { + pub fn group_key(&self) -> C::G { + self.group_key + } + + pub fn included(&self) -> Vec { + self.included.clone() + } + + pub fn secret_share(&self) -> C::F { + self.secret_share + } + + pub fn verification_share(&self, l: usize) -> C::G { + self.verification_shares[l] + } +} + #[derive(Clone, PartialEq, Eq, Debug)] pub struct MultisigKeys { /// Multisig Parameters @@ -229,6 +257,30 @@ impl MultisigKeys { self.verification_shares.clone() } + pub fn view(&self, included: &[usize]) -> Result, FrostError> { + if (included.len() < self.params.t) || (self.params.n < included.len()) { + Err(FrostError::InvalidSigningSet("invalid amount of participants included".to_string()))?; + } + + let secret_share = self.secret_share * lagrange::(self.params.i, &included); + let (offset, offset_share) = if self.offset.is_some() { + let offset = self.offset.unwrap(); + (offset, offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap()) + } else { + (C::F::zero(), C::F::zero()) + }; + + Ok(MultisigView { + group_key: self.group_key + (C::generator_table() * offset), + secret_share: secret_share + offset_share, + verification_shares: self.verification_shares.clone().iter().enumerate().map( + |(l, share)| (*share * lagrange::(l, &included)) + + (C::generator_table() * offset_share) + ).collect(), + included: included.to_vec(), + }) + } + pub fn serialized_len(n: usize) -> usize { 1 + usize::from(C::id_len()) + (3 * 8) + C::F_len() + C::G_len() + (n * C::G_len()) } diff --git a/sign/frost/src/sign.rs b/sign/frost/src/sign.rs index 40daf922..eddc614a 100644 --- a/sign/frost/src/sign.rs +++ b/sign/frost/src/sign.rs @@ -1,4 +1,4 @@ -use core::{convert::{TryFrom, TryInto}, cmp::min, fmt}; +use core::{convert::TryFrom, cmp::min, fmt}; use std::rc::Rc; use rand_core::{RngCore, CryptoRng}; @@ -6,7 +6,7 @@ use rand_core::{RngCore, CryptoRng}; use ff::{Field, PrimeField}; use group::Group; -use crate::{Curve, MultisigParams, MultisigKeys, FrostError, algorithm::Algorithm}; +use crate::{Curve, FrostError, MultisigParams, MultisigKeys, MultisigView, algorithm::Algorithm}; /// Calculate the lagrange coefficient pub fn lagrange( @@ -30,39 +30,12 @@ pub fn lagrange( num * denom.invert().unwrap() } -// View of params passable to algorithm implementations -#[derive(Clone)] -pub struct ParamsView { - group_key: C::G, - included: Vec, - secret_share: C::F, - verification_shares: Vec, -} - -impl ParamsView { - pub fn group_key(&self) -> C::G { - self.group_key - } - - pub fn included(&self) -> Vec { - self.included.clone() - } - - pub fn secret_share(&self) -> C::F { - self.secret_share - } - - pub fn verification_share(&self, l: usize) -> C::G { - self.verification_shares[l] - } -} - /// Pairing of an Algorithm with a MultisigKeys instance and this specific signing set #[derive(Clone)] pub struct Params> { algorithm: A, keys: Rc>, - view: ParamsView, + view: MultisigView, } // Currently public to enable more complex operations as desired, yet solely used in testing @@ -75,7 +48,7 @@ impl> Params { let mut included = included.to_vec(); (&mut included).sort_unstable(); - // included < threshold + // Included < threshold if included.len() < keys.params.t { Err(FrostError::InvalidSigningSet("not enough signers".to_string()))?; } @@ -98,37 +71,15 @@ impl> Params { Err(FrostError::InvalidSigningSet("signing despite not being included".to_string()))?; } - let secret_share = keys.secret_share * lagrange::(keys.params.i, &included); - let (offset, offset_share) = if keys.offset.is_some() { - let offset = keys.offset.unwrap(); - (offset, offset * C::F::from(included.len().try_into().unwrap()).invert().unwrap()) - } else { - (C::F::zero(), C::F::zero()) - }; - - Ok( - Params { - algorithm, - // Out of order arguments to prevent additional cloning - view: ParamsView { - group_key: keys.group_key + (C::generator_table() * offset), - secret_share: secret_share + offset_share, - verification_shares: keys.verification_shares.clone().iter().enumerate().map( - |(l, share)| (*share * lagrange::(l, &included)) + - (C::generator_table() * offset_share) - ).collect(), - included: included, - }, - keys - } - ) + // Out of order arguments to prevent additional cloning + Ok(Params { algorithm, view: keys.view(&included).unwrap(), keys }) } pub fn multisig_params(&self) -> MultisigParams { self.keys.params } - pub fn view(&self) -> ParamsView { + pub fn view(&self) -> MultisigView { self.view.clone() } }