From 0e4ba72779154955b7333e235e07bc76a5d8f59b Mon Sep 17 00:00:00 2001 From: LLFourn Date: Fri, 26 Jul 2024 11:19:41 +1000 Subject: [PATCH] [frost] Add multiplicative tweaking by demand See: https://github.com/LLFourn/secp256kfun/issues/189 --- schnorr_fun/src/frost/frost_poly.rs | 31 ++++++++++++++++++++- schnorr_fun/src/frost/share.rs | 43 +++++++++++++++++++++-------- schnorr_fun/tests/frost_prop.rs | 26 +++++++++++++---- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/schnorr_fun/src/frost/frost_poly.rs b/schnorr_fun/src/frost/frost_poly.rs index 05225d80..b06280ea 100644 --- a/schnorr_fun/src/frost/frost_poly.rs +++ b/schnorr_fun/src/frost/frost_poly.rs @@ -1,4 +1,4 @@ -use core::marker::PhantomData; +use core::{marker::PhantomData, ops::Deref}; use alloc::vec::Vec; use secp256kfun::{poly, prelude::*}; @@ -125,6 +125,16 @@ impl FrostPoly { pub fn homomorphic_negate(&mut self) { poly::point::negate(&mut self.point_polynomial) } + + /// Multiplies the shared key by a scalar. + /// + /// In otder for a [`PairedSecretShare`] to be valid against the new key they will have to apply + /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + pub fn homomorphic_mul(&mut self, tweak: Scalar) { + for coeff in &mut self.point_polynomial { + *coeff = g!(tweak * coeff.deref()).normalize(); + } + } } impl FrostPoly { @@ -154,6 +164,25 @@ impl FrostPoly { Some(self) } + /// Multiplies the shared key by a scalar. + /// + /// In otder for a `PairedSecretShare` to be valid against the new key they will have to apply + /// [the same operation](PairedSecretShare::xonly_homomorphic_mul). + pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { + self.point_polynomial[0] = g!(tweak * self.point_polynomial[0]).normalize(); + let needs_negation = !self.point_polynomial[0] + .non_zero() + .expect("multiplication cannot be zero") + .is_y_even(); + + self.point_polynomial[0] = self.point_polynomial[0].conditional_negate(needs_negation); + tweak.conditional_negate(needs_negation); + + for coeff in &mut self.point_polynomial[1..] { + *coeff = g!(tweak * coeff.deref()).normalize(); + } + } + /// The public key that would have signatures verified against for this shared key. pub fn shared_key(&self) -> Point { let (even_y_point, _needs_negation) = self.point_polynomial[0] diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index c53a782b..d2824697 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -178,15 +178,24 @@ impl PairedSecretShare { } /// Add a `Scalar` to both the secret share and the paired shared key. - pub fn homomorphic_add( - mut self, - scalar: Scalar, - ) -> Option { - self.shared_key = g!(self.shared_key + scalar * G).normalize().non_zero()?; - self.secret_share.share = s!(self.secret_share.share + scalar); + /// If the share is a share of `secret` it becomes the share of the `secret + tweak`. + /// + /// This is useful for for deriving unhardened child frost keys from a root frost public key + /// using [BIP32] + /// + /// [BIP32]: https://bips.xyz/32 + pub fn homomorphic_add(mut self, tweak: Scalar) -> Option { + self.shared_key = g!(self.shared_key + tweak * G).normalize().non_zero()?; + self.secret_share.share = s!(self.secret_share.share + tweak); Some(self) } + /// Multiply the secret share by `scalar`. + pub fn homomorphic_mul(&mut self, tweak: Scalar) { + self.shared_key = g!(tweak * self.shared_key).normalize(); + self.secret_share.share = s!(tweak * self.secret_share.share); + } + /// Create an XOnly secert share where the paired image is always an `EvenY` point. pub fn into_xonly(mut self) -> PairedSecretShare { let (shared_key, needs_negation) = self.shared_key.into_point_with_even_y(); @@ -200,8 +209,9 @@ impl PairedSecretShare { } impl PairedSecretShare { - /// Add a scalar to the share such that share becomes a share of the previous secret plus - /// `scalar`. + /// Adds an "XOnly" tweak to the FROST public key. If the share is a share of `secret` it + /// becomes the share of the `secret + tweak` however the even-y coordinate of the shared key is + /// maintained by negating if need be. /// /// If the secret image were to become zero it returns `None` since this desinged to be /// [`BIP340`] secret share which does not allow 0. @@ -209,17 +219,28 @@ impl PairedSecretShare { /// [`BIP340`]: https://bips.xyz/bip340 pub fn xonly_homomorphic_add( mut self, - scalar: Scalar, + tweak: Scalar, ) -> Option { - let (shared_key, needs_negation) = g!(self.shared_key + scalar * G) + let (shared_key, needs_negation) = g!(self.shared_key + tweak * G) .normalize() .non_zero()? .into_point_with_even_y(); self.shared_key = shared_key; - self.secret_share.share = s!(self.secret_share.share + scalar); + self.secret_share.share = s!(self.secret_share.share + tweak); self.secret_share.share.conditional_negate(needs_negation); Some(self) } + + /// Multiply the secret share and paired key by `scalar` maintaing the paired key's even y + /// coordinate by negating them both (if need be). + pub fn xonly_homomorphic_mul(&mut self, mut tweak: Scalar) { + let (shared_key, needs_negation) = g!(tweak * self.shared_key) + .normalize() + .into_point_with_even_y(); + self.shared_key = shared_key; + tweak.conditional_negate(needs_negation); + self.secret_share.share *= tweak; + } } #[cfg(feature = "share_backup")] diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index 5dd13f98..db0c92d9 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -20,8 +20,10 @@ proptest! { #[test] fn frost_prop_test( (n_parties, threshold) in (2usize..=4).prop_flat_map(|n| (Just(n), 2usize..=n)), - plain_tweak in option::of(any::>()), - xonly_tweak in option::of(any::>()) + add_tweak in option::of(any::>()), + xonly_add_tweak in option::of(any::>()), + mul_tweak in option::of(any::>()), + xonly_mul_tweak in option::of(any::>()) ) { let proto = new_with_deterministic_nonces::(); assert!(threshold <= n_parties); @@ -30,25 +32,39 @@ proptest! { let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); let (mut frost_poly, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); - if let Some(tweak) = plain_tweak { + if let Some(tweak) = add_tweak { for secret_share in &mut secret_shares { *secret_share = secret_share.homomorphic_add(tweak).unwrap(); } frost_poly = frost_poly.homomorphic_add(tweak).unwrap(); } + if let Some(mul_tweak) = mul_tweak { + frost_poly.homomorphic_mul(mul_tweak); + for secret_share in &mut secret_shares { + secret_share.homomorphic_mul(mul_tweak); + } + } + let mut xonly_poly = frost_poly.into_xonly(); let mut xonly_secret_shares = secret_shares.into_iter().map(|secret_share| secret_share.into_xonly()).collect::>(); - if let Some(tweak) = xonly_tweak { + if let Some(tweak) = xonly_add_tweak { xonly_poly = xonly_poly.xonly_homomorphic_add(tweak).unwrap(); for secret_share in &mut xonly_secret_shares { *secret_share = secret_share.xonly_homomorphic_add(tweak).unwrap(); } } + if let Some(xonly_mul_tweak) = xonly_mul_tweak { + xonly_poly.xonly_homomorphic_mul(xonly_mul_tweak); + for secret_share in &mut xonly_secret_shares { + secret_share.xonly_homomorphic_mul(xonly_mul_tweak); + } + } + for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_poly.shared_key()); + assert_eq!(secret_share.shared_key(), xonly_poly.shared_key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers