From 27396a6291d54e942f4831503fdb0194d450a0b2 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 29 Apr 2022 22:03:34 -0400 Subject: [PATCH] Implement a CLSAG algorithm extension which also does key images Practically, this should be mergeable. There's little reason to do a CLSAG and not also a key image. Keeps them isolated for now. --- coins/monero/src/clsag/mod.rs | 21 ++-- coins/monero/src/clsag/multisig.rs | 153 ++++++++++++++++++++----- coins/monero/src/frost.rs | 7 +- coins/monero/src/key_image/multisig.rs | 5 +- coins/monero/src/transaction/mod.rs | 84 ++++++++------ coins/monero/tests/clsag.rs | 26 +++-- coins/monero/tests/frost.rs | 3 +- coins/monero/tests/send.rs | 8 +- sign/frost/src/algorithm.rs | 7 +- sign/frost/src/sign.rs | 2 +- 10 files changed, 213 insertions(+), 103 deletions(-) diff --git a/coins/monero/src/clsag/mod.rs b/coins/monero/src/clsag/mod.rs index d2dac818..5039de23 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::Multisig; +pub use multisig::{Msg, Multisig, InputMultisig}; #[derive(Error, Debug)] pub enum Error { @@ -38,16 +38,14 @@ pub enum Error { #[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 + pub commitment: Commitment, } impl Input { pub fn new( - image: EdwardsPoint, ring: Vec<[EdwardsPoint; 2]>, i: u8, commitment: Commitment @@ -66,16 +64,13 @@ impl Input { Err(Error::InvalidCommitment)?; } - Ok(Input { image, ring, i, commitment }) + Ok(Input { ring, i, commitment }) } #[cfg(feature = "multisig")] pub fn context(&self) -> Vec { - // image is extraneous in practice as the image should be in the msg AND the addendum when TX - // signing. This just ensures CLSAG guarantees its integrity, even when others won't - let mut context = self.image.compress().to_bytes().to_vec(); // Ring index - context.extend(&u8::try_from(self.i).unwrap().to_le_bytes()); + let mut context = u8::try_from(self.i).unwrap().to_le_bytes().to_vec(); // Ring for pair in &self.ring { // Doesn't include key offsets as CLSAG doesn't care and won't be affected by it @@ -92,6 +87,7 @@ pub(crate) fn sign_core( rng: &mut R, msg: &[u8; 32], input: &Input, + image: &EdwardsPoint, mask: Scalar, A: EdwardsPoint, AH: EdwardsPoint @@ -126,7 +122,7 @@ pub(crate) fn sign_core( let mut D = H * z; // Doesn't use a constant time table as dalek takes longer to generate those then they save - let images_precomp = VartimeEdwardsPrecomputation::new(&[input.image, D]); + let images_precomp = VartimeEdwardsPrecomputation::new([image, &D]); D = Scalar::from(8 as u8).invert() * D; let mut to_hash = vec![]; @@ -145,7 +141,7 @@ pub(crate) fn sign_core( to_hash.extend(C_non_zero[i].compress().to_bytes()); } - to_hash.extend(input.image.compress().to_bytes()); + to_hash.extend(image.compress().to_bytes()); let D_bytes = D.compress().to_bytes(); to_hash.extend(D_bytes); to_hash.extend(C_out.compress().to_bytes()); @@ -208,7 +204,7 @@ pub(crate) fn sign_core( pub fn sign( rng: &mut R, msg: [u8; 32], - inputs: &[(Scalar, Input)], + inputs: &[(Scalar, Input, EdwardsPoint)], sum_outputs: Scalar ) -> Option> { if inputs.len() == 0 { @@ -235,6 +231,7 @@ pub fn sign( rng, &msg, &inputs[i].1, + &inputs[i].2, mask, &nonce * &ED25519_BASEPOINT_TABLE, nonce * hash_to_point(&inputs[i].1.ring[inputs[i].1.i][0]) ); diff --git a/coins/monero/src/clsag/multisig.rs b/coins/monero/src/clsag/multisig.rs index afd75ffd..a6f805d5 100644 --- a/coins/monero/src/clsag/multisig.rs +++ b/coins/monero/src/clsag/multisig.rs @@ -1,3 +1,5 @@ +use core::fmt::Debug; + use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha12Rng; @@ -5,6 +7,7 @@ use blake2::{Digest, Blake2b512}; use curve25519_dalek::{ constants::ED25519_BASEPOINT_TABLE, + traits::Identity, scalar::Scalar, edwards::EdwardsPoint }; @@ -19,9 +22,14 @@ use crate::{ random_scalar, hash_to_point, frost::{MultisigError, Ed25519, DLEqProof}, + key_image, clsag::{Input, sign_core, verify} }; +pub trait Msg: Clone + Debug { + fn msg(&self, image: EdwardsPoint) -> [u8; 32]; +} + #[allow(non_snake_case)] #[derive(Clone, Debug)] struct ClsagSignInterim { @@ -34,44 +42,43 @@ struct ClsagSignInterim { #[allow(non_snake_case)] #[derive(Clone, Debug)] -pub struct Multisig { +pub struct Multisig { b: Vec, - AH0: dfg::EdwardsPoint, - AH1: dfg::EdwardsPoint, + AH: (dfg::EdwardsPoint, dfg::EdwardsPoint), input: Input, - msg: Option<[u8; 32]>, + image: Option, + msg: M, + interim: Option } -impl Multisig { +impl Multisig { pub fn new( - input: Input - ) -> Result { + input: Input, + msg: M + ) -> Result, MultisigError> { Ok( Multisig { b: vec![], - AH0: dfg::EdwardsPoint::identity(), - AH1: dfg::EdwardsPoint::identity(), + AH: (dfg::EdwardsPoint::identity(), dfg::EdwardsPoint::identity()), input, - msg: None, + image: None, + msg, interim: None } ) } - pub fn set_msg( - &mut self, - msg: [u8; 32] - ) { - self.msg = Some(msg); + pub fn set_image(&mut self, image: EdwardsPoint) { + self.image = Some(image); } } -impl Algorithm for Multisig { +impl Algorithm for Multisig { type Signature = (Clsag, EdwardsPoint); // We arguably don't have to commit to at all thanks to xG and yG being committed to, both of @@ -113,7 +120,6 @@ impl Algorithm for Multisig { let h0 = ::G_from_slice(&serialized[0 .. 32]).map_err(|_| FrostError::InvalidCommitment(l))?; DLEqProof::deserialize(&serialized[64 .. 128]).ok_or(FrostError::InvalidCommitment(l))?.verify( - l, &alt, &commitments[0], &h0 @@ -121,7 +127,6 @@ impl Algorithm for Multisig { let h1 = ::G_from_slice(&serialized[32 .. 64]).map_err(|_| FrostError::InvalidCommitment(l))?; DLEqProof::deserialize(&serialized[128 .. 192]).ok_or(FrostError::InvalidCommitment(l))?.verify( - l, &alt, &commitments[1], &h1 @@ -129,33 +134,34 @@ impl Algorithm for Multisig { self.b.extend(&l.to_le_bytes()); self.b.extend(&serialized[0 .. 64]); - self.AH0 += h0; - self.AH1 += h1; + self.AH.0 += h0; + self.AH.1 += h1; Ok(()) } fn context(&self) -> Vec { let mut context = vec![]; - context.extend(&self.msg.unwrap()); + // This should be redundant as the image should be in the addendum if using InputMultisig and + // in msg if signing a Transaction, yet this ensures CLSAG takes responsibility for its own + // security boundaries + context.extend(&self.image.unwrap().compress().to_bytes()); + context.extend(&self.msg.msg(self.image.unwrap())); context.extend(&self.input.context()); context } - fn process_binding( - &mut self, - p: &dfg::Scalar, - ) { - self.AH0 += self.AH1 * p; - } - fn sign_share( &mut self, view: &ParamsView, nonce_sum: dfg::EdwardsPoint, + b: dfg::Scalar, nonce: dfg::Scalar, _: &[u8] ) -> dfg::Scalar { + // Apply the binding factor to the H variant of the nonce + self.AH.0 += self.AH.1 * b; + // 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 // Uses the context as well to prevent passive observers of messages from being able to break @@ -170,11 +176,12 @@ impl Algorithm for Multisig { #[allow(non_snake_case)] let (clsag, c, mu_C, z, mu_P, C_out) = sign_core( &mut rng, - &self.msg.unwrap(), + &self.msg.msg(self.image.unwrap()), &self.input, + &self.image.unwrap(), mask, nonce_sum.0, - self.AH0.0 + self.AH.0.0 ); self.interim = Some(ClsagSignInterim { c: c * mu_P, s: c * mu_C * z, clsag, C_out }); @@ -193,7 +200,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.msg.unwrap(), self.input.image, &self.input.ring, interim.C_out) { + if verify(&clsag, &self.msg.msg(self.image.unwrap()), self.image.unwrap(), &self.input.ring, interim.C_out) { return Some((clsag, interim.C_out)); } return None; @@ -211,3 +218,87 @@ impl Algorithm for Multisig { ); } } + +#[allow(non_snake_case)] +#[derive(Clone, Debug)] +pub struct InputMultisig(EdwardsPoint, Multisig); + +impl InputMultisig { + pub fn new( + input: Input, + msg: M + ) -> Result, MultisigError> { + Ok(InputMultisig(EdwardsPoint::identity(), Multisig::new(input, msg)?)) + } + + pub fn image(&self) -> EdwardsPoint { + self.0 + } +} + +impl Algorithm for InputMultisig { + type Signature = (Clsag, EdwardsPoint); + + fn addendum_commit_len() -> usize { + 32 + Multisig::::addendum_commit_len() + } + + fn preprocess_addendum( + rng: &mut R, + view: &ParamsView, + nonces: &[dfg::Scalar; 2] + ) -> Vec { + let (mut serialized, end) = key_image::generate_share(rng, view); + serialized.extend(Multisig::::preprocess_addendum(rng, view, nonces)); + serialized.extend(end); + serialized + } + + fn process_addendum( + &mut self, + view: &ParamsView, + l: usize, + commitments: &[dfg::EdwardsPoint; 2], + serialized: &[u8] + ) -> Result<(), FrostError> { + let (image, serialized) = key_image::verify_share(view, l, serialized).map_err(|_| FrostError::InvalidShare(l))?; + self.0 += image; + if l == *view.included().last().unwrap() { + self.1.set_image(self.0); + } + self.1.process_addendum(view, l, commitments, &serialized) + } + + fn context(&self) -> Vec { + self.1.context() + } + + fn sign_share( + &mut self, + view: &ParamsView, + nonce_sum: dfg::EdwardsPoint, + b: dfg::Scalar, + nonce: dfg::Scalar, + msg: &[u8] + ) -> dfg::Scalar { + self.1.sign_share(view, nonce_sum, b, nonce, msg) + } + + fn verify( + &self, + group_key: dfg::EdwardsPoint, + nonce: dfg::EdwardsPoint, + sum: dfg::Scalar + ) -> Option { + self.1.verify(group_key, nonce, sum) + } + + fn verify_share( + &self, + verification_share: dfg::EdwardsPoint, + nonce: dfg::EdwardsPoint, + share: dfg::Scalar, + ) -> bool { + self.1.verify_share(verification_share, nonce, share) + } +} diff --git a/coins/monero/src/frost.rs b/coins/monero/src/frost.rs index 0d967bf1..4523665d 100644 --- a/coins/monero/src/frost.rs +++ b/coins/monero/src/frost.rs @@ -26,8 +26,8 @@ use crate::random_scalar; pub enum MultisigError { #[error("internal error ({0})")] InternalError(String), - #[error("invalid discrete log equality proof {0}")] - InvalidDLEqProof(usize), + #[error("invalid discrete log equality proof")] + InvalidDLEqProof, #[error("invalid key image {0}")] InvalidKeyImage(usize) } @@ -145,7 +145,6 @@ impl DLEqProof { pub fn verify( &self, - l: usize, H: &DPoint, primary: &DPoint, alt: &DPoint @@ -166,7 +165,7 @@ impl DLEqProof { // Take the opportunity to ensure a lack of torsion in key images/randomness commitments if (!primary.is_torsion_free()) || (!alt.is_torsion_free()) || (c != expected_c) { - Err(MultisigError::InvalidDLEqProof(l))?; + Err(MultisigError::InvalidDLEqProof)?; } Ok(()) diff --git a/coins/monero/src/key_image/multisig.rs b/coins/monero/src/key_image/multisig.rs index 3c8cba11..a8db96d9 100644 --- a/coins/monero/src/key_image/multisig.rs +++ b/coins/monero/src/key_image/multisig.rs @@ -32,16 +32,15 @@ pub fn verify_share( share: &[u8] ) -> Result<(EdwardsPoint, Vec), MultisigError> { if share.len() < 96 { - Err(MultisigError::InvalidDLEqProof(l))?; + Err(MultisigError::InvalidDLEqProof)?; } let image = CompressedEdwardsY( share[0 .. 32].try_into().unwrap() ).decompress().ok_or(MultisigError::InvalidKeyImage(l))?; let proof = DLEqProof::deserialize( &share[(share.len() - 64) .. share.len()] - ).ok_or(MultisigError::InvalidDLEqProof(l))?; + ).ok_or(MultisigError::InvalidDLEqProof)?; proof.verify( - l, &hash_to_point(&view.group_key().0), &view.verification_share(l), &image diff --git a/coins/monero/src/transaction/mod.rs b/coins/monero/src/transaction/mod.rs index cb103492..3c39498d 100644 --- a/coins/monero/src/transaction/mod.rs +++ b/coins/monero/src/transaction/mod.rs @@ -327,7 +327,7 @@ async fn prepare_inputs( spend: &Scalar, inputs: &[SpendableOutput], tx: &mut Transaction -) -> Result, TransactionError> { +) -> 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() { @@ -340,51 +340,71 @@ async fn prepare_inputs( signable.push(( spend + input.key_offset, clsag::Input::new( - key_image::generate(&(spend + input.key_offset)), rpc.get_ring(&mixins[i]).await.map_err(|e| TransactionError::RpcError(e))?, m, input.commitment - ).map_err(|e| TransactionError::ClsagError(e))? + ).map_err(|e| TransactionError::ClsagError(e))?, + key_image::generate(&(spend + input.key_offset)) )); tx.prefix.inputs.push(TxIn::ToKey { amount: VarInt(0), key_offsets: mixins::offset(&mixins[i]).iter().map(|x| VarInt(*x)).collect(), - k_image: KeyImage { image: Hash(signable[i].1.image.compress().to_bytes()) } + k_image: KeyImage { image: Hash(signable[i].2.compress().to_bytes()) } }); } Ok(signable) } -pub async fn send( - rng: &mut R, - rpc: &Rpc, - spend: &Scalar, - inputs: &[SpendableOutput], - payments: &[(Address, u64)], +pub struct SignableTransaction { + inputs: Vec, + payments: Vec<(Address, u64)>, change: Address, fee_per_byte: u64 -) -> Result { - let (_, mask_sum, mut tx) = prepare_outputs( - &mut Preparation::Leader(rng), - inputs, - payments, - change, - fee_per_byte - )?; - - let signable = prepare_inputs(rpc, spend, inputs, &mut tx).await?; - - let clsags = clsag::sign( - rng, - tx.signature_hash().expect("Couldn't get the signature hash").0, - &signable, - mask_sum - ).ok_or(TransactionError::NoInputs)?; - let mut prunable = tx.rct_signatures.p.unwrap(); - prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect(); - prunable.pseudo_outs = clsags.iter().map(|clsag| Key { key: clsag.1.compress().to_bytes() }).collect(); - tx.rct_signatures.p = Some(prunable); - Ok(tx) +} + +impl SignableTransaction { + pub fn new( + inputs: Vec, + payments: Vec<(Address, u64)>, + change: Address, + fee_per_byte: u64 + ) -> SignableTransaction { + SignableTransaction { + inputs, + payments, + change, + fee_per_byte + } + } + + 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 signable = prepare_inputs(rpc, spend, &self.inputs, &mut tx).await?; + + let clsags = clsag::sign( + rng, + tx.signature_hash().expect("Couldn't get the signature hash").0, + &signable, + mask_sum + ).ok_or(TransactionError::NoInputs)?; + let mut prunable = tx.rct_signatures.p.unwrap(); + prunable.Clsags = clsags.iter().map(|clsag| clsag.0.clone()).collect(); + prunable.pseudo_outs = clsags.iter().map(|clsag| Key { key: clsag.1.compress().to_bytes() }).collect(); + tx.rct_signatures.p = Some(prunable); + Ok(tx) + } } diff --git a/coins/monero/tests/clsag.rs b/coins/monero/tests/clsag.rs index bf9e325e..a4ba6022 100644 --- a/coins/monero/tests/clsag.rs +++ b/coins/monero/tests/clsag.rs @@ -1,6 +1,6 @@ use rand::{RngCore, rngs::OsRng}; -use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; use monero_serai::{random_scalar, Commitment, frost::MultisigError, key_image, clsag}; @@ -39,17 +39,27 @@ fn test_single() { &vec![( secrets[0], clsag::Input::new( - image, ring.clone(), RING_INDEX, Commitment::new(secrets[1], AMOUNT) - ).unwrap() + ).unwrap(), + image )], Scalar::zero() ).unwrap().swap_remove(0); assert!(clsag::verify(&clsag, &msg, image, &ring, pseudo_out)); } +#[cfg(feature = "multisig")] +#[derive(Clone, Debug)] +struct Msg([u8; 32]); +#[cfg(feature = "multisig")] +impl clsag::Msg for Msg { + fn msg(&self, _: EdwardsPoint) -> [u8; 32] { + self.0 + } +} + #[cfg(feature = "multisig")] #[test] fn test_multisig() -> Result<(), MultisigError> { @@ -58,8 +68,6 @@ fn test_multisig() -> Result<(), MultisigError> { let msg = [1; 32]; - let image = key_image::generate(&group_private.0); - let randomness = random_scalar(&mut OsRng); let mut ring = vec![]; for i in 0 .. RING_LEN { @@ -79,13 +87,13 @@ fn test_multisig() -> Result<(), MultisigError> { } let mut algorithms = Vec::with_capacity(t); - for i in 1 ..= t { + for _ in 1 ..= t { algorithms.push( - clsag::Multisig::new( - clsag::Input::new(image, ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap() + clsag::InputMultisig::new( + clsag::Input::new(ring.clone(), RING_INDEX, Commitment::new(randomness, AMOUNT)).unwrap(), + Msg(msg) ).unwrap() ); - algorithms[i - 1].set_msg(msg); } let mut signatures = sign(algorithms, keys); diff --git a/coins/monero/tests/frost.rs b/coins/monero/tests/frost.rs index f31f3538..d4c0e71c 100644 --- a/coins/monero/tests/frost.rs +++ b/coins/monero/tests/frost.rs @@ -41,13 +41,12 @@ impl Algorithm for DummyAlgorithm { fn context(&self) -> Vec { unimplemented!() } - fn process_binding(&mut self, _: &Scalar) { unimplemented!() } - fn sign_share( &mut self, _: &sign::ParamsView, _: EdwardsPoint, _: Scalar, + _: Scalar, _: &[u8], ) -> Scalar { unimplemented!() } diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs index 6fd4beb4..01c43075 100644 --- a/coins/monero/tests/send.rs +++ b/coins/monero/tests/send.rs @@ -9,7 +9,7 @@ use monero::{ use monero_serai::{ random_scalar, - transaction, + transaction::{self, SignableTransaction}, rpc::Rpc }; @@ -48,9 +48,9 @@ pub async fn send() { output = transaction::scan(&tx, view, spend_pub).swap_remove(0); // Test creating a zero change output and a non-zero change output amount = output.commitment.amount - fee - u64::try_from(i).unwrap(); - let tx = transaction::send( - &mut OsRng, &rpc, &spend, &vec![output], &vec![(addr, amount)], addr, fee_per_byte - ).await.unwrap(); + let tx = SignableTransaction::new( + vec![output], vec![(addr, amount)], addr, fee_per_byte + ).sign(&mut OsRng, &rpc, &spend).await.unwrap(); rpc.publish_transaction(&tx).await.unwrap(); } } diff --git a/sign/frost/src/algorithm.rs b/sign/frost/src/algorithm.rs index 0e14762f..f25c132b 100644 --- a/sign/frost/src/algorithm.rs +++ b/sign/frost/src/algorithm.rs @@ -33,9 +33,6 @@ pub trait Algorithm: Clone { /// Context for this algorithm to be hashed into b, and therefore committed to fn context(&self) -> Vec; - /// Process the binding factor generated from all the committed to data - fn process_binding(&mut self, p: &C::F); - /// Sign a share with the given secret/nonce /// The secret will already have been its lagrange coefficient applied so it is the necessary /// key share @@ -44,6 +41,7 @@ pub trait Algorithm: Clone { &mut self, params: &sign::ParamsView, nonce_sum: C::G, + b: C::F, nonce: C::F, msg: &[u8], ) -> C::F; @@ -120,12 +118,11 @@ impl> Algorithm for Schnorr { vec![] } - fn process_binding(&mut self, _: &C::F) {} - fn sign_share( &mut self, params: &sign::ParamsView, nonce_sum: C::G, + _: C::F, nonce: C::F, msg: &[u8], ) -> C::F { diff --git a/sign/frost/src/sign.rs b/sign/frost/src/sign.rs index a5dee144..6dcf0732 100644 --- a/sign/frost/src/sign.rs +++ b/sign/frost/src/sign.rs @@ -287,7 +287,6 @@ fn sign_with_share>( } let b = C::hash_to_F(&b); - params.algorithm.process_binding(&b); #[allow(non_snake_case)] let mut Ris = vec![]; @@ -305,6 +304,7 @@ fn sign_with_share>( let share = params.algorithm.sign_share( view, R, + b, our_preprocess.nonces[0] + (our_preprocess.nonces[1] * b), msg );