diff --git a/ecdsa_fun/src/adaptor/mod.rs b/ecdsa_fun/src/adaptor/mod.rs index c5ecf11c..06727eb2 100644 --- a/ecdsa_fun/src/adaptor/mod.rs +++ b/ecdsa_fun/src/adaptor/mod.rs @@ -234,9 +234,9 @@ impl, NG> Adaptor { /// There are two crucial things to understand when calling this: /// /// 1. You should be certain that the encrypted signature is what you think it is by calling - /// [`verify_encrypted_signature`] on it first. + /// [`verify_encrypted_signature`] on it first. /// 2. Once you give the decrypted signature to anyone who has seen `encrypted_signature` they will be - /// able to learn `decryption_key` by calling [`recover_decryption_key`]. + /// able to learn `decryption_key` by calling [`recover_decryption_key`]. /// /// See [synopsis] for an example /// diff --git a/schnorr_fun/Cargo.toml b/schnorr_fun/Cargo.toml index 939f8ad3..751ca2b5 100644 --- a/schnorr_fun/Cargo.toml +++ b/schnorr_fun/Cargo.toml @@ -18,7 +18,7 @@ secp256kfun = { path = "../secp256kfun", version = "0.10", default-features = f bech32 = { version = "0.11", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] -secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest"] } +secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest", "bincode", "alloc"] } rand = { version = "0.8" } lazy_static = "1.4" bincode = "1.0" diff --git a/schnorr_fun/benches/bench_schnorr.rs b/schnorr_fun/benches/bench_schnorr.rs index 38d4989a..926f04f3 100755 --- a/schnorr_fun/benches/bench_schnorr.rs +++ b/schnorr_fun/benches/bench_schnorr.rs @@ -49,7 +49,7 @@ fn verify_schnorr(c: &mut Criterion) { }); { - let sig = sig.clone().set_secrecy::(); + let sig = sig.set_secrecy::(); group.bench_function("fun::schnorr_verify_ct", |b| { b.iter(|| schnorr.verify(verification_key, message, &sig)) }); diff --git a/schnorr_fun/src/adaptor/mod.rs b/schnorr_fun/src/adaptor/mod.rs index 660bc5c2..a188e478 100644 --- a/schnorr_fun/src/adaptor/mod.rs +++ b/schnorr_fun/src/adaptor/mod.rs @@ -153,9 +153,9 @@ pub trait Adaptor { /// There are two crucial things to understand when calling this: /// /// 1. You should be certain that the encrypted signature is what you think it is by calling - /// [`verify_encrypted_signature`] on it first. + /// [`verify_encrypted_signature`] on it first. /// 2. Once you give the decrypted signature to anyone who has seen `encrypted_signature` they will be - /// able to learn `decryption_key` by calling [`recover_decryption_key`]. + /// able to learn `decryption_key` by calling [`recover_decryption_key`]. /// /// See [synopsis] for an example /// diff --git a/schnorr_fun/src/binonce.rs b/schnorr_fun/src/binonce.rs index a16ac65e..1d7d27ce 100644 --- a/schnorr_fun/src/binonce.rs +++ b/schnorr_fun/src/binonce.rs @@ -4,7 +4,7 @@ //! Your public nonces are derived from scalars which must be kept secret. //! Derived binonces should be unique and and must not be reused for signing under any circumstances //! as this can leak your secret key. -use secp256kfun::{g, marker::*, rand_core::RngCore, Point, Scalar, G}; +use secp256kfun::{g, hash::HashInto, marker::*, rand_core::RngCore, Point, Scalar, G}; /// A nonce (pair of points) that each party must share with the others in the first stage of signing. /// @@ -26,7 +26,7 @@ impl Nonce { } } -impl Nonce { +impl Nonce { /// Negate the two nonces pub fn conditional_negate(&mut self, needs_negation: bool) { self.0[0] = self.0[0].conditional_negate(needs_negation); @@ -42,6 +42,33 @@ impl Nonce { bytes[33..].copy_from_slice(self.0[1].to_bytes().as_ref()); bytes } + + /// Binds an aggregated binonce to a it's binding coefficient (which is produced differently for + /// different schemes) and produces the final nonce (the one that will go into the signature). + pub fn bind(&self, binding_coeff: Scalar) -> (Point, bool) { + g!(self.0[0] + binding_coeff * self.0[1]) + .normalize() + .non_zero() + .unwrap_or(Point::generator()) + .into_point_with_even_y() + } +} + +impl HashInto for Nonce { + fn hash_into(self, hash: &mut impl secp256kfun::digest::Digest) { + self.0.hash_into(hash) + } +} + +impl Nonce { + /// Adds a bunch of binonces together (one for each party signing usually). + pub fn aggregate(nonces: impl IntoIterator) -> Self { + let agg = nonces.into_iter().fold([Point::zero(); 2], |acc, nonce| { + [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] + }); + + Self([agg[0].normalize(), agg[1].normalize()]) + } } secp256kfun::impl_fromstr_deserialize! { @@ -52,7 +79,7 @@ secp256kfun::impl_fromstr_deserialize! { } secp256kfun::impl_display_serialize! { - fn to_bytes(nonce: &Nonce) -> [u8;66] { + fn to_bytes(nonce: &Nonce) -> [u8;66] { nonce.to_bytes() } } diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index d552281e..7beaae46 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -28,7 +28,7 @@ //! # let public_poly2 = poly::scalar::to_point_poly(&secret_poly2); //! # let public_poly3 = poly::scalar::to_point_poly(&secret_poly3); //! -//! // Party indicies can be any non-zero scalar +//! // Party indices can be any non-zero scalar //! let my_index = s!(1).public(); //! let party_index2 = s!(2).public(); //! let party_index3 = s!(3).public(); @@ -74,7 +74,7 @@ //! # Message::raw(&frost.keygen_id(&keygen)), //! # ) //! # .unwrap(); -//! let (my_secret_share, frost_key) = frost +//! let (my_secret_share, shared_key) = frost //! .finish_keygen( //! keygen, //! my_index, @@ -85,36 +85,49 @@ //! // ⚠️ At this point you probably want to check out of band that all the other parties //! // received their secret shares correctly and have the same view of the protocol //! // (e.g same keygen_id). If they all give the OK then we're ready to use the key and do some signing! -//! let xonly_frost_key = frost_key.into_xonly_key(); +//! // With signing we'll have at least one party be the "coordinator" (steps marked with 🐙) +//! // In this example we'll be the coordinator (but it doesn't have to be on eof the signing parties) +//! let xonly_shared_key = shared_key.into_xonly(); // this is the key signatures will be valid under +//! let xonly_my_secret_share = my_secret_share.into_xonly(); +//! # let xonly_secret_share3 = secret_share3.into_xonly(); //! let message = Message::plain("my-app", b"chancellor on brink of second bailout for banks"); -//! // Generate nonces for this signing session. +//! // Generate nonces for this signing session (and send them to coordinator somehow) //! // ⚠️ session_id MUST be different for every signing attempt to avoid nonce reuse (if using deterministic nonces). //! let session_id = b"signing-ominous-message-about-banks-attempt-1".as_slice(); -//! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(&xonly_frost_key, &my_secret_share.secret, session_id); +//! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(my_secret_share, session_id); //! let my_nonce = frost.gen_nonce(&mut nonce_rng); //! # let nonce3 = NonceKeyPair::random(&mut rand::thread_rng()); //! // share your public nonce with the other signing participant(s) receive public nonces //! # let received_nonce3 = nonce3.public(); +//! // 🐙 the coordinator has received the nonces //! let nonces = BTreeMap::from_iter([(my_index, my_nonce.public()), (party_index3, received_nonce3)]); +//! let coord_session = frost.coordinator_sign_session(&xonly_shared_key, nonces, message); +//! // Parties receive the agg_nonce from the coordiantor and the list of perties +//! let agg_binonce = coord_session.agg_binonce(); +//! let parties = coord_session.parties(); //! // start a sign session with these nonces for a message -//! let session = frost.start_sign_session(&xonly_frost_key, nonces, message); +//! let sign_session = frost.party_sign_session(xonly_my_secret_share.public_key(),parties, agg_binonce, message); //! // create a partial signature using our secret share and secret nonce -//! let my_sig_share = frost.sign(&xonly_frost_key, &session, &my_secret_share, my_nonce); -//! # let sig_share3 = frost.sign(&xonly_frost_key, &session, &secret_share3, nonce3); -//! // receive the partial signature(s) from the other participant(s) and verify -//! assert!(frost.verify_signature_share(&xonly_frost_key, &session, party_index3, sig_share3)); -//! // combine signature shares into a single signature that is valid under the FROST key -//! let combined_sig = frost.combine_signature_shares(&xonly_frost_key, &session, vec![my_sig_share, sig_share3]); +//! let my_sig_share = sign_session.sign(&xonly_my_secret_share, my_nonce); +//! # let sig_share3 = sign_session.sign(&xonly_secret_share3, nonce3); +//! // 🐙 receive the partial signature(s) from the other participant(s). +//! // 🐙 combine signature shares into a single signature that is valid under the FROST key +//! let combined_sig = coord_session.verify_and_combine_signature_shares( +//! &xonly_shared_key, +//! [(my_index, my_sig_share), (party_index3, sig_share3)].into() +//! )?; //! assert!(frost.schnorr.verify( -//! &xonly_frost_key.public_key(), +//! &xonly_shared_key.public_key(), //! message, //! &combined_sig //! )); +//! +//! # Ok::<(), schnorr_fun::frost::VerifySignatureSharesError>(()) //! ``` //! //! # Description //! -//! In FROST, multiple parties cooperatively generate a single joint public key ([`FrostKey`]) for +//! In FROST, multiple parties cooperatively generate a single joint public key ([`SharedKey`]) for //! creating Schnorr signatures. Unlike in [`musig`], only some threshold `t` of the `n` signers are //! required to generate a signature under the key (rather than all `n`). //! @@ -126,7 +139,7 @@ //! Signatures]*. //! //! > ⚠️ At this stage this implementation is for API exploration purposes only. The way it is -//! currently implemented is not proven secure. +//! > currently implemented is not proven secure. //! //! ## Polynomial Generation //! @@ -155,7 +168,7 @@ //! let mut poly_rng = derive_nonce_rng! { //! // use synthetic nonces that add system randomness in //! nonce_gen => nonce_gen, -//! // Use your static secret key to add further protectoin +//! // Use your static secret key to add further protection //! secret => static_secret_key, //! // session id should be unique for each key generation session //! public => ["frost_key_session_1053"], @@ -175,12 +188,19 @@ //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar +mod shared_key; +pub use shared_key::*; mod share; pub use share::*; +mod session; +pub use session::*; pub use crate::binonce::{Nonce, NonceKeyPair}; -use crate::{Message, Schnorr, Signature}; -use alloc::{collections::BTreeMap, vec::Vec}; +use crate::{binonce, Message, Schnorr, Signature}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec::Vec, +}; use core::num::NonZeroU32; use secp256kfun::{ derive_nonce_rng, @@ -200,8 +220,8 @@ use secp256kfun::{ /// It is used in interpolation and computation of the shared secret. /// /// This index can be any non-zero [`Scalar`], but must be unique between parties. -/// In most cases it will make sense to use simple indicies `s!(1), s!(2), ...` for smaller backups. -/// Other applications may desire to use indicies corresponding to pre-existing keys or identifiers. +/// In most cases it will make sense to use simple indices `s!(1), s!(2), ...` for smaller backups. +/// Other applications may desire to use indices corresponding to pre-existing keys or identifiers. pub type PartyIndex = Scalar; /// The FROST context. @@ -248,7 +268,7 @@ impl Frost { &self.nonce_gen } - /// Create our secret shares to be shared with other participants using pre-existing indicies + /// Create our secret shares to be shared with other participants using pre-existing indices /// /// Each secret share needs to be securely communicated to the intended participant. /// @@ -296,7 +316,7 @@ where /// [`Frost::new_keygen`] #[derive(Clone, Debug)] pub struct KeyGen { - frost_key: FrostKey, + frost_poly: SharedKey, point_polys: BTreeMap>, } @@ -364,147 +384,10 @@ impl core::fmt::Display for FinishKeyGenError { #[cfg(feature = "std")] impl std::error::Error for FinishKeyGenError {} -/// A FROST key -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FrostKey { - /// The joint public key of the frost multisignature. - /// - /// This public key will change as you add tweaks or convert [`into_xonly_key`]. - /// Only initially will the public key match the first coefficient of the public polynomial. - tweaked_public_key: Point, - /// The public point polynomial that defines the access structure to the FROST key. - point_polynomial: Vec>, - /// The tweak applied to this frost key, tracks the aggregate tweak. - tweak: Scalar, - /// Whether the secret keys need to be negated during signing (only used for EvenY keys). - needs_negation: bool, -} - -impl FrostKey { - /// The public key with all tweaks applied - pub fn public_key(&self) -> Point { - self.tweaked_public_key - } - - /// The verification shares of each party in the key. - /// - /// The verification share is the image of their secret share. - pub fn verification_share(&self, index: &PartyIndex) -> Point { - poly::point::eval(&self.point_polynomial, *index) - } - - /// The threshold number of participants required in a signing coalition to produce a valid signature. - pub fn threshold(&self) -> usize { - self.point_polynomial.len() - } - - /// The public image of the key's polynomial on the elliptic curve. - /// - /// Note: the first coefficient (index `0`) is guaranteed to be non-zero but the coefficients - /// may be. - pub fn point_polynomial(&self) -> Vec> { - self.point_polynomial.clone() - } -} - -impl FrostKey { - /// Convert the key into a BIP340 FrostKey. - /// - /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. - /// - /// [BIP340]: https://bips.xyz/340 - pub fn into_xonly_key(self) -> FrostKey { - let (tweaked_public_key, needs_negation) = self.public_key().into_point_with_even_y(); - let mut tweak = self.tweak; - tweak.conditional_negate(needs_negation); - FrostKey { - tweaked_public_key, - point_polynomial: self.point_polynomial, - tweak, - needs_negation, - } - } - - /// Apply a plain tweak to the frost public key. - /// - /// This is useful for deriving unhardened child frost keys from a master frost public key using [BIP32]. - /// - /// Tweak the frost public key with a scalar so that the resulting key is equal to the - /// existing key plus `tweak * G`. The tweak mutates the public key while still allowing - /// the original set of signers to sign under the new key. - /// - /// ## Return value - /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. - /// - /// [BIP32]: https://bips.xyz/32 - pub fn tweak(self, tweak: Scalar) -> Option { - let tweaked_public_key = g!(self.tweaked_public_key + tweak * G) - .normalize() - .non_zero()?; - let tweak = s!(self.tweak + tweak).public(); - - Some(FrostKey { - tweaked_public_key, - point_polynomial: self.point_polynomial, - tweak, - needs_negation: self.needs_negation, - }) - } - - /// Put the `FrostKey` into a form that can be encoded/decode. - /// - /// Note this encoding **ignores the tweaks that have been applied to the `FrostKey`**. To - /// restore tweaks you must reapply them after it's been [`decode`]d. - /// - /// [`decode`]: Self::decode - pub fn encode(&self) -> EncodedFrostKey { - EncodedFrostKey::from(self.clone()) - } - - /// Decode the `FrostKey` from an `EncodedFrostKey`. - pub fn decode(encoded_frost_key: EncodedFrostKey) -> Self { - Self::from(encoded_frost_key) - } -} - -impl FrostKey { - /// Applies an "XOnly" tweak to the FROST public key. - /// This is how you embed a taproot commitment into a frost public key - /// - /// Tweak the frost public key with a scalar so that the resulting key is equal to the - /// existing key plus `tweak * G` as an [`EvenY`] point. The tweak mutates the public key while still allowing - /// the original set of signers to sign under the new key. - /// - /// ## Return value - /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. - /// In the erroneous case that the tweak is exactly equal to the negation of the aggregate - /// secret key it returns `None`. - pub fn tweak(self, tweak: Scalar) -> Option { - let (new_public_key, needs_negation) = g!(self.tweaked_public_key + tweak * G) - .normalize() - .non_zero()? - .into_point_with_even_y(); - let mut new_tweak = s!(self.tweak + tweak).public(); - new_tweak.conditional_negate(needs_negation); - let needs_negation = self.needs_negation ^ needs_negation; - - Some(Self { - tweaked_public_key: new_public_key, - point_polynomial: self.point_polynomial, - needs_negation, - tweak: new_tweak, - }) - } -} - impl + Clone, NG: NonceGen> Frost { - /// Convienence method to generate secret shares and proof-of-possession to be shared with other + /// Convenience method to generate secret shares and proof-of-possession to be shared with other /// participants. Each secret share needs to be securely communicated to the intended - /// participant but the proof of possession (shnorr signature) can be publically shared with + /// participant but the proof of possession (schnorr signature) can be publically shared with /// everyone. pub fn create_shares_and_pop( &self, @@ -543,7 +426,7 @@ impl + Clone, NG: NonceGen> Frost { /// /// Parameters: /// - /// - `frost_key`: the joint public key we are signing under. This can be an `XOnly` or `Normal` + /// - `frost_key`: the joint public key we are signing under. This can be an `EvenY` or `Normal` /// It will return the same nonce regardless. /// - `secret`: you're secret key share for the `frost_key` /// - `session_id`: a string of bytes that is **unique for each signing attempt**. @@ -554,7 +437,7 @@ impl + Clone, NG: NonceGen> Frost { /// must not reuse the session id, the resulting rng or anything derived from that rng again. /// /// 💡 Before using this function with a deterministic rng write a short justification as to why - /// you beleive your session id will be unique per signing attempt. Perhaps include it as a + /// you believe your session id will be unique per signing attempt. Perhaps include it as a /// comment next to the call. Note **it must be unique even across signing attempts for the same /// or different messages**. /// @@ -564,18 +447,16 @@ impl + Clone, NG: NonceGen> Frost { /// resulting rng for each input. pub fn seed_nonce_rng>( &self, - frost_key: &FrostKey, - secret: &Scalar, + paired_secret_share: PairedSecretShare, session_id: &[u8], ) -> R { let sid_len = (session_id.len() as u64).to_be_bytes(); - let threshold_bytes = (frost_key.threshold() as u64).to_be_bytes(); - let pk_bytes = frost_key.public_key().to_xonly_bytes(); + let pk_bytes = paired_secret_share.public_key().to_xonly_bytes(); let rng: R = derive_nonce_rng!( nonce_gen => self.nonce_gen(), - secret => &secret, - public => [pk_bytes, threshold_bytes, sid_len, session_id], + secret => paired_secret_share.share(), + public => [pk_bytes, sid_len, session_id], seedable_rng => R ); rng @@ -584,14 +465,14 @@ impl + Clone, NG: NonceGen> Frost { /// Run the key generation protocol while simulating the parties internally. /// /// This can be used to do generate a "trusted setup" FROST key (but it is extremely inefficient - /// for this purpose). It returns the joint `FrostKey` along with the secret keys for each + /// for this purpose). It returns the joint `SharedKey` along with the secret keys for each /// party. pub fn simulate_keygen( &self, threshold: usize, n_parties: usize, rng: &mut impl RngCore, - ) -> (FrostKey, Vec) { + ) -> (SharedKey, Vec>) { let scalar_polys = (0..n_parties) .map(|i| { ( @@ -622,10 +503,7 @@ impl + Clone, NG: NonceGen> Frost { .map(|(gen_party_index, (party_shares, pop))| { ( *gen_party_index, - ( - party_shares.remove(receiver_party_index).unwrap(), - pop.clone(), - ), + (party_shares.remove(receiver_party_index).unwrap(), *pop), ) }) .collect::>(); @@ -658,7 +536,7 @@ impl + Clone, NG: NonceGen> Frost { } impl + Clone, NG> Frost { - /// Generate an id for the key generation by hashing the party indicies and their point + /// Generate an id for the key generation by hashing the party indices and their point /// polynomials pub fn keygen_id(&self, keygen: &KeyGen) -> [u8; 32] { let mut keygen_hash = self.keygen_id_hash.clone(); @@ -672,7 +550,7 @@ impl + Clone, NG> Frost { keygen_hash.finalize().into() } - /// Collect all the public polynomials commitments into a [`KeyGen`] to produce a [`FrostKey`]. + /// Collect all the public polynomials commitments into a [`KeyGen`] to produce a [`SharedKey`]. /// /// It is crucial that at least one of these polynomials was not adversarially produced /// otherwise the adversary will know the eventual secret key. @@ -732,22 +610,18 @@ impl + Clone, NG> Frost { } } - let public_key = joint_poly[0] - .normalize() - .non_zero() - .ok_or(NewKeyGenError::ZeroFrostKey)?; + let frost_poly = SharedKey::from_poly( + joint_poly + .into_iter() + .map(|coef| coef.normalize()) + .collect(), + ) + .non_zero() + .ok_or(NewKeyGenError::ZeroFrostKey)?; Ok(KeyGen { point_polys, - frost_key: FrostKey { - tweaked_public_key: public_key, - point_polynomial: joint_poly - .into_iter() - .map(|coef| coef.normalize()) - .collect(), - tweak: Scalar::zero(), - needs_negation: false, - }, + frost_poly, }) } @@ -757,7 +631,7 @@ impl + Clone, NG> Frost { keygen: KeyGen, proofs_of_possession: BTreeMap, proof_of_possession_msg: Message, - ) -> Result, FinishKeyGenError> { + ) -> Result, FinishKeyGenError> { for (party_index, poly) in &keygen.point_polys { let pop = proofs_of_possession .get(party_index) @@ -772,10 +646,10 @@ impl + Clone, NG> Frost { } } - Ok(keygen.frost_key) + Ok(keygen.frost_poly) } - /// Combine all receieved shares into your long-lived secret share. + /// Combine all received shares into your long-lived secret share. /// /// The `secret_shares` includes your own share as well as shares from each of the other /// parties. The `secret_shares` are validated to match the expected result by evaluating their @@ -786,14 +660,14 @@ impl + Clone, NG> Frost { /// /// # Return value /// - /// Your secret share and the [`FrostKey`] + /// Your secret share and the [`SharedKey`] pub fn finish_keygen( &self, keygen: KeyGen, my_index: PartyIndex, secret_shares: BTreeMap, Signature)>, proof_of_possession_msg: Message, - ) -> Result<(SecretShare, FrostKey), FinishKeyGenError> { + ) -> Result<(PairedSecretShare, SharedKey), FinishKeyGenError> { let mut total_secret_share = s!(0); for (party_index, poly) in &keygen.point_polys { @@ -816,197 +690,105 @@ impl + Clone, NG> Frost { total_secret_share += secret_share; } - Ok(( - SecretShare { - index: my_index, - secret: total_secret_share, - }, - keygen.frost_key, - )) + let secret_share = SecretShare { + index: my_index, + share: total_secret_share, + }; + + let secret_share_with_image = + PairedSecretShare::new(secret_share, keygen.frost_poly.public_key()); + + Ok((secret_share_with_image, keygen.frost_poly)) + } + + /// Aggregate the nonces of the signers so you can start a [`party_sign_session`] without a + /// coordinator. + /// + /// [`party_sign_session`]: Self::party_sign_session + pub fn aggregate_binonces( + &self, + nonces: impl IntoIterator, + ) -> binonce::Nonce { + binonce::Nonce::aggregate(nonces) + } + + /// Start party signing session + pub fn party_sign_session( + &self, + public_key: Point, + parties: BTreeSet, + agg_binonce: binonce::Nonce, + message: Message, + ) -> PartySignSession { + let binding_coeff = self.binding_coefficient(public_key, agg_binonce, message); + let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); + let challenge = self.schnorr.challenge(&final_nonce, &public_key, message); + + PartySignSession { + public_key, + parties, + binding_coeff, + challenge, + binonce_needs_negation, + final_nonce, + } } - /// Start a FROST signing session. + /// Start a FROST signing session as a *coordinator*. /// - /// Each signing party must call this with the same arguments for it to succeeed. This means you - /// must all agree on each other's nonces before starting the sign session. In `nonces` each - /// item is the signer's index and their `Nonce`. It's length must be at least `threshold`. - /// Generating your own nonces can be done with [`Frost::gen_nonce`]. + /// The corodinator must have collected nonces from each of the signers and pass them in as `nonces`. + /// From there /// /// # Panics /// /// If the number of nonces is less than the threshold. - pub fn start_sign_session( + pub fn coordinator_sign_session( &self, - frost_key: &FrostKey, - nonces: BTreeMap, + shared_key: &SharedKey, + mut nonces: BTreeMap, message: Message, - ) -> SignSession { - let nonce_map = nonces; - - if nonce_map.len() < frost_key.threshold() { + ) -> CoordinatorSignSession { + if nonces.len() < shared_key.threshold() { panic!("nonces' length was less than the threshold"); } - let agg_nonce = nonce_map - .iter() - .fold([Point::zero(); 2], |acc, (_, nonce)| { - [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] - }); + let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let agg_nonce = [agg_nonce[0].normalize(), agg_nonce[1].normalize()]; - - let binding_coeff = Scalar::from_hash( - self.binding_hash - .clone() - .add(agg_nonce[0]) - .add(agg_nonce[1]) - .add(frost_key.public_key()) - .add(message), - ); - let (agg_nonce, nonces_need_negation) = g!(agg_nonce[0] + binding_coeff * agg_nonce[1]) - .normalize() - .non_zero() - .unwrap_or(Point::generator()) - .into_point_with_even_y(); + let binding_coeff = self.binding_coefficient(shared_key.public_key(), agg_binonce, message); + let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&agg_nonce, &frost_key.public_key(), message); + .challenge(&final_nonce, &shared_key.public_key(), message); + + for nonce in nonces.values_mut() { + nonce.conditional_negate(binonce_needs_negation); + } - SignSession { + CoordinatorSignSession { binding_coeff, - nonces_need_negation, - agg_nonce, + agg_binonce, + final_nonce, challenge, - nonces: nonce_map, + nonces, + public_key: shared_key.public_key(), } } - /// Generates a partial signature share under the frost key using a secret share. - /// - /// ## Return value - /// - /// Returns a signature share - /// - /// ## Panics - /// - /// Panics if the `secret_nonce` does not match the previously provided public nonce in the - /// `session`. - pub fn sign( - &self, - frost_key: &FrostKey, - session: &SignSession, - secret_share: &SecretShare, - secret_nonce: NonceKeyPair, - ) -> Scalar { - let mut lambda = - poly::eval_basis_poly_at_0(secret_share.index, session.nonces.keys().cloned()); - assert_eq!( - *session - .nonces - .get(&secret_share.index) - .expect("my_index was not in session"), - secret_nonce.public(), - "secret nonce didn't match previously provided public nonce" - ); - lambda.conditional_negate(frost_key.needs_negation); - let [mut r1, mut r2] = secret_nonce.secret; - r1.conditional_negate(session.nonces_need_negation); - r2.conditional_negate(session.nonces_need_negation); - - let b = &session.binding_coeff; - let x = &secret_share.secret; - let c = &session.challenge; - s!(r1 + (r2 * b) + lambda * x * c).public() - } - - /// Verify a partial signature for a participant at `index` (from zero). - /// - /// ## Return Value - /// - /// Returns `bool`, true if partial signature is valid. - pub fn verify_signature_share( - &self, - frost_key: &FrostKey, - session: &SignSession, - index: PartyIndex, - signature_share: Scalar, - ) -> bool { - let s = signature_share; - let mut lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); - lambda.conditional_negate(frost_key.needs_negation); - let c = &session.challenge; - let b = &session.binding_coeff; - let X = frost_key.verification_share(&index); - let [R1, R2] = session - .nonces - .get(&index) - .expect("verifying party index that is not part of frost signing coalition") - .0; - let R1 = R1.conditional_negate(session.nonces_need_negation); - let R2 = R2.conditional_negate(session.nonces_need_negation); - g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero() - } - - /// Combine a vector of signatures shares into an aggregate signature. - /// - /// This method does not check the validity of the `signature_shares` but if you have verified - /// each signautre share individually the output will be a valid siganture under the `frost_key` - /// and message provided when starting the session. - /// - /// ## Return value - /// - /// Returns a combined schnorr [`Signature`] on the message - pub fn combine_signature_shares( + fn binding_coefficient( &self, - frost_key: &FrostKey, - session: &SignSession, - signature_shares: Vec>, - ) -> Signature { - let ck = s!(session.challenge * frost_key.tweak); - let sum_s = signature_shares - .into_iter() - .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) - .unwrap_or(Scalar::zero()); - Signature { - R: session.agg_nonce, - s: s!(sum_s + ck).public(), - } - } -} - -/// A FROST signing session -/// -/// Created using [`Frost::start_sign_session`]. -/// -/// [`Frost::start_sign_session`] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr( - feature = "bincode", - derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), - bincode(crate = "crate::fun::bincode") -)] -#[cfg_attr( - feature = "serde", - derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), - serde(crate = "crate::fun::serde") -)] -pub struct SignSession { - binding_coeff: Scalar, - nonces_need_negation: bool, - agg_nonce: Point, - challenge: Scalar, - nonces: BTreeMap, -} - -impl SignSession { - /// Fetch the participant indices for this signing session. - /// - /// ## Return value - /// - /// An iterator of participant indices - pub fn participants(&self) -> impl DoubleEndedIterator + '_ { - self.nonces.keys().copied() + public_key: Point, + agg_binonce: Nonce, + message: Message, + ) -> Scalar { + Scalar::from_hash( + self.binding_hash + .clone() + .add(agg_binonce) + .add(public_key) + .add(message), + ) + .public() } } @@ -1055,106 +837,6 @@ where Frost::default() } -/// An encoded FROST key -/// -/// This encodes only stores the joint public polynomial. **It does not encode tweaks applied to the -/// `FrostKey`**. -#[derive(Clone, Debug, PartialEq, Eq)] -#[cfg_attr( - feature = "serde", - derive(crate::fun::serde::Serialize), - serde(crate = "crate::fun::serde") -)] -#[cfg_attr( - feature = "bincode", - derive(crate::fun::bincode::Encode), - bincode(crate = "crate::fun::bincode",) -)] -pub struct EncodedFrostKey { - /// The public point polynomial that defines the access structure to the FROST key. - point_polynomial: Vec>, -} - -impl EncodedFrostKey { - /// Traverse back to a FROST key to be used in signing - pub fn into_frost_key(&self) -> FrostKey { - self.clone().into() - } - - /// The threshold number of participants required in a signing coalition to produce a valid signature. - pub fn threshold(&self) -> usize { - self.point_polynomial.len() - } - - /// The public polynomial that defines the access structure to the FROST key. - pub fn point_polynomial(&self) -> Vec> { - self.point_polynomial.clone() - } -} - -#[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for EncodedFrostKey { - fn decode( - decoder: &mut D, - ) -> Result { - let poly = Vec::>::decode(decoder)?; - - if poly[0].is_zero() { - return Err(secp256kfun::bincode::error::DecodeError::Other( - "first coefficient of a frost polynomial can't be zero", - )); - } - - Ok(EncodedFrostKey { - point_polynomial: poly, - }) - } -} - -#[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(EncodedFrostKey); - -#[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for EncodedFrostKey { - fn deserialize(deserializer: D) -> Result - where - D: secp256kfun::serde::Deserializer<'de>, - { - let poly = Vec::>::deserialize(deserializer)?; - - if poly[0].is_zero() { - return Err(crate::fun::serde::de::Error::custom( - "first coefficient of a frost polynomial can't be zero", - )); - } - - Ok(EncodedFrostKey { - point_polynomial: poly, - }) - } -} - -impl From for FrostKey { - fn from(from: EncodedFrostKey) -> Self { - FrostKey { - tweaked_public_key: from.point_polynomial[0] - .non_zero() - .expect("the frost public key can not be zero"), - point_polynomial: from.point_polynomial, - tweak: Scalar::zero(), - needs_negation: false, - } - } -} - -impl From> for EncodedFrostKey { - fn from(from: FrostKey) -> Self { - EncodedFrostKey { - point_polynomial: from.point_polynomial, - } - } -} - #[cfg(test)] mod test { @@ -1164,17 +846,17 @@ mod test { #[test] fn zero_agg_nonce_results_in_G() { let frost = new_with_deterministic_nonces::(); - let (frost_key, _shares) = frost.simulate_keygen(2, 3, &mut rand::thread_rng()); + let (frost_poly, _shares) = frost.simulate_keygen(2, 3, &mut rand::thread_rng()); let nonce = NonceKeyPair::random(&mut rand::thread_rng()).public(); let mut malicious_nonce = nonce; malicious_nonce.conditional_negate(true); - let session = frost.start_sign_session( - &frost_key.into_xonly_key(), + let session = frost.coordinator_sign_session( + &frost_poly.into_xonly(), BTreeMap::from_iter([(s!(1).public(), nonce), (s!(2).public(), malicious_nonce)]), Message::::plain("test", b"hello"), ); - assert_eq!(session.agg_nonce, *G); + assert_eq!(session.final_nonce(), *G); } } diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs new file mode 100644 index 00000000..87cd9b29 --- /dev/null +++ b/schnorr_fun/src/frost/session.rs @@ -0,0 +1,273 @@ +use crate::{binonce, frost::PartyIndex, Signature}; +use alloc::collections::{BTreeMap, BTreeSet}; +use secp256kfun::{poly, prelude::*}; + +use super::{NonceKeyPair, PairedSecretShare, SharedKey, VerificationShare}; +/// A FROST signing session used to *verify* signatures. +/// +/// Created using [`coordinator_sign_session`]. +/// +/// [`coordinator_sign_session`]: super::Frost::coordinator_sign_session +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") +)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") +)] +pub struct CoordinatorSignSession { + pub(crate) public_key: Point, + pub(crate) binding_coeff: Scalar, + pub(crate) final_nonce: Point, + pub(crate) challenge: Scalar, + + pub(crate) agg_binonce: binonce::Nonce, + pub(crate) nonces: BTreeMap, +} + +impl CoordinatorSignSession { + /// Fetch the participant indices for this signing session. + /// + /// ## Return value + /// + /// An iterator of participant indices + pub fn parties(&self) -> BTreeSet { + self.nonces.keys().cloned().collect() + } + + /// The aggregated nonce used to sign + pub fn agg_binonce(&self) -> binonce::Nonce { + self.agg_binonce + } + + /// The final nonce that will actually appear in the signature + pub fn final_nonce(&self) -> Point { + self.final_nonce + } + + /// The public key this session was started under + pub fn public_key(&self) -> Point { + self.public_key + } + + /// Verify a partial signature for a participant for a particular session. + /// + /// The `verification_share` is usually derived from either [`SharedKey::verification_share`] or + /// [`PairedSecretShare::verification_share`]. + /// + /// ## Return Value + /// + /// Returns `true` if signature share is valid. + pub fn verify_signature_share( + &self, + verification_share: VerificationShare, + signature_share: Scalar, + ) -> Result<(), SignatureShareInvalid> { + let X = verification_share.share_image; + let index = verification_share.index; + + // We need to know the verification share was generated against the session's key for + // further validity to have any meaning. + if verification_share.public_key != self.public_key() { + return Err(SignatureShareInvalid { index }); + } + + let s = signature_share; + let lambda = + poly::eval_basis_poly_at_0(verification_share.index, self.nonces.keys().cloned()); + let c = &self.challenge; + let b = &self.binding_coeff; + debug_assert!( + self.parties().contains(&index), + "the party is not part of the session" + ); + let [R1, R2] = self + .nonces + .get(&index) + .ok_or(SignatureShareInvalid { index })? + .0; + let valid = g!(R1 + b * R2 + (c * lambda) * X - s * G).is_zero(); + if valid { + Ok(()) + } else { + Err(SignatureShareInvalid { index }) + } + } + + /// Combines signature shares from each party into the final signature. + /// + /// You can use this instead of calling [`verify_signature_share`] on each share. + /// + /// [`verify_signature_share`]: Self::verify_signature_share + pub fn verify_and_combine_signature_shares( + &self, + shared_key: &SharedKey, + signature_shares: BTreeMap>, + ) -> Result { + if signature_shares.len() < shared_key.threshold() { + return Err(VerifySignatureSharesError::NotEnough { + missing: shared_key.threshold() - signature_shares.len(), + }); + } + for (party_index, signature_share) in &signature_shares { + self.verify_signature_share( + shared_key.verification_share(*party_index), + *signature_share, + ) + .map_err(VerifySignatureSharesError::Invalid)?; + } + + let signature = + self.combine_signature_shares(self.final_nonce(), signature_shares.values().cloned()); + + Ok(signature) + } + + /// Combine a vector of signatures shares into an aggregate signature given the final nonce. + /// + /// You can get `final_nonce` from either of the [`CoordinatorSignSession`] or the [`PartySignSession`]. + /// + /// This method does not check the validity of the `signature_shares` + /// but if you have verified each signature share + /// individually the output will be a valid siganture under the `frost_key` and message provided + /// when starting the session. + /// + /// Alternatively you can use [`verify_and_combine_signature_shares`] which checks and combines + /// the signature shares. + /// + /// ## Return value + /// + /// Returns a schnorr [`Signature`] on the message + /// + /// [`CoordinatorSignSession`]: CoordinatorSignSession::final_nonce + /// [`PartySignSession`]: PartySignSession::final_nonce + /// [`verify_and_combine_signature_shares`]: Self::verify_and_combine_signature_shares + pub fn combine_signature_shares( + &self, + final_nonce: Point, + signature_shares: impl IntoIterator>, + ) -> Signature { + let sum_s = signature_shares + .into_iter() + .reduce(|acc, partial_sig| s!(acc + partial_sig).public()) + .unwrap_or(Scalar::zero()); + Signature { + R: final_nonce, + s: sum_s, + } + } +} + +/// The session that is used to sign a message. +/// +/// Created using [`party_sign_session`] +/// +/// [`party_sign_session`]: super::Frost::party_sign_session +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode(crate = "crate::fun::bincode") +)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") +)] +pub struct PartySignSession { + pub(crate) public_key: Point, + pub(crate) binding_coeff: Scalar, + pub(crate) final_nonce: Point, + pub(crate) challenge: Scalar, + + pub(crate) parties: BTreeSet>, + pub(crate) binonce_needs_negation: bool, +} + +impl PartySignSession { + /// The final nonce that will actually appear in the signature + pub fn final_nonce(&self) -> Point { + self.final_nonce + } + + /// The public key the session was started under + pub fn public_key(&self) -> Point { + self.public_key + } + + /// Generates a signature share under the frost key using a secret share. + /// + /// The `secret_share` is taken as a `PairedSecretShare` to guarantee that the secret is aligned with an `EvenY` point. + /// + /// ## Return value + /// + /// Returns a signature share + /// + /// ## Panics + /// + /// Panics if the `secret_share` was not part of the signing session + pub fn sign( + &self, + secret_share: &PairedSecretShare, + secret_nonce: NonceKeyPair, + ) -> Scalar { + if self.public_key != secret_share.public_key() { + panic!("the share's shared key is not the same as the shared key of the session"); + } + let secret_share = secret_share.secret_share(); + let lambda = poly::eval_basis_poly_at_0(secret_share.index, self.parties.iter().cloned()); + let [mut r1, mut r2] = secret_nonce.secret; + r1.conditional_negate(self.binonce_needs_negation); + r2.conditional_negate(self.binonce_needs_negation); + + let b = &self.binding_coeff; + let x = secret_share.share; + let c = &self.challenge; + s!(r1 + (r2 * b) + lambda * x * c).public() + } +} + +/// Error for a signature share being invalid +#[derive(Clone, Debug, PartialEq)] +pub struct SignatureShareInvalid { + index: PartyIndex, +} + +impl core::fmt::Display for SignatureShareInvalid { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + write!(f, "signature share from party {} was invalid", self.index) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SignatureShareInvalid {} + +/// Error returned by [`CoordinatorSignSession::verify_and_combine_signature_shares`] +#[derive(Clone, Debug, PartialEq)] +pub enum VerifySignatureSharesError { + /// Not enough signature shares to compelte the signature + NotEnough { + /// How many are missing + missing: usize, + }, + /// One of the signature shars was invalid + Invalid(SignatureShareInvalid), +} + +impl core::fmt::Display for VerifySignatureSharesError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + VerifySignatureSharesError::NotEnough { missing } => { + write!(f, "not enough signature shares have been collected to finish the signature. You need {missing} more.") + } + VerifySignatureSharesError::Invalid(invalid) => write!(f, "{invalid}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for VerifySignatureSharesError {} diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index 151acd16..b1c0f374 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -1,4 +1,4 @@ -use secp256kfun::{marker::*, poly, Scalar}; +use secp256kfun::{poly, prelude::*}; /// A *[Shamir secret share]*. /// /// Each share is an `(x,y)` pair where `y = p(x)` for some polynomial `p`. With a sufficient @@ -20,8 +20,7 @@ use secp256kfun::{marker::*, poly, Scalar}; /// shares that way. Share identification can help for keeping track of them and distinguishing shares /// when there are only a small number of them. /// -/// The backup format is enabled with the `share_backup` feature and accessed with the `FromStr` -/// and `Display`. +/// The backup format is enabled with the `share_backup` feature and accessed with the feature enabled methods. /// /// ### Index in human readable part /// @@ -53,9 +52,9 @@ use secp256kfun::{marker::*, poly, Scalar}; pub struct SecretShare { /// The scalar index for this secret share, usually this is a small number but it can take any /// value (other than 0). - pub index: Scalar, + pub index: PartyIndex, /// The secret scalar which is the output of the polynomial evaluated at `index` - pub secret: Scalar, + pub share: Scalar, } impl SecretShare { @@ -63,7 +62,7 @@ impl SecretShare { pub fn recover_secret(shares: &[SecretShare]) -> Scalar { let index_and_secret = shares .iter() - .map(|share| (share.index, share.secret)) + .map(|share| (share.index, share.share)) .collect::>(); poly::scalar::interpolate_and_eval_poly_at_0(&index_and_secret[..]) @@ -74,7 +73,7 @@ impl SecretShare { pub fn to_bytes(&self) -> [u8; 64] { let mut bytes = [0u8; 64]; bytes[..32].copy_from_slice(self.index.to_bytes().as_ref()); - bytes[32..].copy_from_slice(self.secret.to_bytes().as_ref()); + bytes[32..].copy_from_slice(self.share.to_bytes().as_ref()); bytes } @@ -83,9 +82,14 @@ impl SecretShare { pub fn from_bytes(bytes: [u8; 64]) -> Option { Some(Self { index: Scalar::from_slice(&bytes[..32])?, - secret: Scalar::from_slice(&bytes[32..])?, + share: Scalar::from_slice(&bytes[32..])?, }) } + + /// Get the image of the secret share. + pub fn share_image(&self) -> Point { + g!(self.share * G) + } } secp256kfun::impl_fromstr_deserialize! { @@ -101,6 +105,190 @@ secp256kfun::impl_display_debug_serialize! { } } +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode, crate::fun::bincode::Decode), + bincode( + crate = "crate::fun::bincode", + encode_bounds = "Point: crate::fun::bincode::Encode", + decode_bounds = "Point: crate::fun::bincode::Decode", + borrow_decode_bounds = "Point: crate::fun::bincode::BorrowDecode<'__de>" + ) +)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Deserialize, crate::fun::serde::Serialize), + serde( + crate = "crate::fun::serde", + bound( + deserialize = "Point: crate::fun::serde::de::Deserialize<'de>", + serialize = "Point: crate::fun::serde::Serialize" + ) + ) +)] +/// A secret share paired with the image of the secret for which it is a share of. +/// +/// This is useful so you can keep track of tweaks to the secret value and tweaks to the shared key +/// in tandem. +pub struct PairedSecretShare { + secret_share: SecretShare, + public_key: Point, +} + +impl PairedSecretShare { + /// The index of the secret share + pub fn index(&self) -> PartyIndex { + self.secret_share.index + } + + /// The secret bit of the share + pub fn share(&self) -> Scalar { + self.secret_share.share + } + + /// The public key that this secert share is a part of + pub fn public_key(&self) -> Point { + self.public_key + } + + /// The inner un-paired secret share. + /// + /// This exists since when you do a physical paper backup of a secret share you usually don't + /// record explicitly the entire shared key (maybe just a short identifier). + pub fn secret_share(&self) -> &SecretShare { + &self.secret_share + } +} + +impl PairedSecretShare { + /// Pair a secret share to a shared key. + /// + /// Users are meant to use [`pair_secret_share`] to create this + /// + /// [`pair_secret_share`]: SharedKey::pair_secret_share + pub(crate) fn new(secret_share: SecretShare, public_key: Point) -> Self { + Self { + secret_share, + public_key, + } + } + + /// Adds a scalar `tweak` to the paired secret share. + /// + /// The returned `PairedSecretShare` represents a sharing of the original value + `tweak`. + /// + /// This is useful for deriving unhardened child frost keys from a master frost public key using + /// [BIP32]. In cases like this since you know that the tweak was computed from a hash of the + /// original key you call [`non_zero`] and unwrap the `Option` since zero is computationally + /// unreachable. + /// + /// If you want to apply an "x-only" tweak you need to call this then [`non_zero`] and finally [`into_xonly`]. + /// + /// See also: [`SharedKey::homomorphic_add`] + /// + /// [BIP32]: https://bips.xyz/32 + /// [`non_zero`]: Self::non_zero + /// [`into_xonly`]: Self::into_xonly + /// [`SharedKey::homomorphic_add`]: crate::frost::SharedKey::homomorphic_add + #[must_use] + pub fn homomorphic_add( + self, + tweak: Scalar, + ) -> PairedSecretShare { + let PairedSecretShare { + mut secret_share, + public_key: shared_key, + } = self; + let shared_key = g!(shared_key + tweak * G).normalize(); + secret_share.share = s!(secret_share.share + tweak); + PairedSecretShare { + public_key: shared_key, + secret_share, + } + } + + /// Multiply the secret share by `scalar`. + #[must_use] + pub fn homomorphic_mul(self, tweak: Scalar) -> PairedSecretShare { + let PairedSecretShare { + public_key: shared_key, + mut secret_share, + } = self; + + let shared_key = g!(tweak * shared_key).normalize(); + secret_share.share = s!(tweak * self.secret_share.share); + PairedSecretShare { + secret_share, + public_key: shared_key, + } + } + + /// Converts a `PairedSecretShare` to a `PairedSecretShare`. + /// + /// If the paired shared key *was* actually zero ([`is_zero`] returns true) it returns `None`. + /// + /// [`is_zero`]: Point::is_zero + #[must_use] + pub fn non_zero(self) -> Option> { + Some(PairedSecretShare { + secret_share: self.secret_share, + public_key: self.public_key.non_zero()?, + }) + } + + /// Is the key this is a share of zero + pub fn is_zero(&self) -> bool { + self.public_key.is_zero() + } +} + +impl PairedSecretShare { + /// Create an XOnly secert share where the paired image is always an `EvenY` point. + #[must_use] + pub fn into_xonly(mut self) -> PairedSecretShare { + let (shared_key, needs_negation) = self.public_key.into_point_with_even_y(); + self.secret_share.share.conditional_negate(needs_negation); + + PairedSecretShare { + secret_share: self.secret_share, + public_key: shared_key, + } + } +} + +impl PairedSecretShare { + /// Get the verification for the inner secret share. + pub fn verification_share(&self) -> VerificationShare { + VerificationShare { + index: self.index(), + share_image: self.secret_share.share_image(), + public_key: self.public_key, + } + } +} + +/// This is the public image of a [`SecretShare`]. You can't sign with it but you can verify +/// signature shares created by the secret share. +/// +/// A `VerificationShare` is the same as a [`share_image`] except it's generated against an `EvenY` +/// key that can actually have signatures verified against it. +/// +/// A `VerificationShare` is not designed to be persisted. The verification share will only be able +/// to verify signatures against the key that it was generated from. Tweaking a key with +/// `homomorphic_add` etc will invalidate the verification share. +/// +/// [`share_image`]: SecretShare::share_image +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct VerificationShare { + /// The index of the share in the secret sharing + pub index: PartyIndex, + /// The image of the secret share + pub share_image: Point, + /// The public key that this is a share of + pub public_key: Point, +} + #[cfg(feature = "share_backup")] mod share_backup { use super::*; @@ -140,7 +328,7 @@ mod share_backup { }; let chars = self - .secret + .share .to_bytes() .into_iter() .chain(share_index_bytes.into_iter().flatten()) @@ -222,7 +410,7 @@ mod share_backup { .ok_or(BackupDecodeError::InvalidShareIndexScalar)?; Ok(SecretShare { - secret: secret_share, + share: secret_share, index: share_index, }) } @@ -275,8 +463,8 @@ mod share_backup { proptest! { #[test] - fn share_backup_roundtrip(index in any::>(), secret in any::>()) { - let orig = SecretShare { secret, index }; + fn share_backup_roundtrip(index in any::>(), share in any::>()) { + let orig = SecretShare { share, index }; let orig_encoded = orig.to_bech32_backup(); let decoded = SecretShare::from_bech32_backup(&orig_encoded).unwrap(); assert_eq!(orig, decoded) @@ -284,11 +472,11 @@ mod share_backup { #[test] - fn short_backup_length(secret in any::>(), share_index_u32 in 1u32..200) { + fn short_backup_length(share in any::>(), share_index_u32 in 1u32..200) { let index = Scalar::::from(share_index_u32).non_zero().unwrap().public(); let secret_share = SecretShare { index, - secret, + share, }; let backup = secret_share.to_bech32_backup(); @@ -307,6 +495,8 @@ mod share_backup { #[cfg(feature = "share_backup")] pub use share_backup::BackupDecodeError; +use super::PartyIndex; + #[cfg(test)] mod test { use super::*; @@ -329,10 +519,11 @@ mod test { let frost = frost::new_with_deterministic_nonces::(); let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (frost_key, shares) = frost.simulate_keygen(threshold, parties, &mut rng); - let chosen = shares.choose_multiple(&mut rng, threshold).cloned().collect::>(); + let (frost_poly, shares) = frost.simulate_keygen(threshold, parties, &mut rng); + let chosen = shares.choose_multiple(&mut rng, threshold).cloned() + .map(|paired_share| paired_share.secret_share).collect::>(); let secret = SecretShare::recover_secret(&chosen); - prop_assert_eq!(g!(secret * G), frost_key.public_key()); + prop_assert_eq!(g!(secret * G), frost_poly.public_key()); } } } diff --git a/schnorr_fun/src/frost/shared_key.rs b/schnorr_fun/src/frost/shared_key.rs new file mode 100644 index 00000000..c16f368d --- /dev/null +++ b/schnorr_fun/src/frost/shared_key.rs @@ -0,0 +1,426 @@ +use core::{marker::PhantomData, ops::Deref}; + +use super::{PairedSecretShare, PartyIndex, SecretShare, VerificationShare}; +use alloc::vec::Vec; +use secp256kfun::{poly, prelude::*}; + +/// A polynomial where the first coefficient (constant term) is the image of a secret `Scalar` that +/// has been shared in a [Shamir's secret sharing] structure. +/// +/// [Shamir's secret sharing]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr( + feature = "serde", + derive(crate::fun::serde::Serialize), + serde(crate = "crate::fun::serde") +)] +#[cfg_attr( + feature = "bincode", + derive(crate::fun::bincode::Encode), + bincode(crate = "crate::fun::bincode",) +)] +pub struct SharedKey { + /// The public point polynomial that defines the access structure to the FROST key. + point_polynomial: Vec>, + #[cfg_attr(feature = "serde", serde(skip))] + ty: PhantomData<(T, Z)>, +} + +impl SharedKey { + /// "pair" a secret share that belongs to this shared key so you can keep track of tweaks to the + /// public key and the secret share together. + /// + /// Returns `None` if the secret share is not a valid share of this key. + pub fn pair_secret_share(&self, secret_share: SecretShare) -> Option> { + let share_image = poly::point::eval(&self.point_polynomial, secret_share.index); + if share_image != g!(secret_share.share * G) { + return None; + } + + Some(PairedSecretShare::new(secret_share, self.public_key())) + } + + /// The threshold number of participants required in a signing coalition to produce a valid signature. + pub fn threshold(&self) -> usize { + self.point_polynomial.len() + } + + /// The internal public polynomial coefficients that defines the public key and the share structure. + /// + /// To get the first coefficient of the polynomial typed correctly call [`public_key`]. + /// + /// [`public_key`]: Self::public_key + pub fn point_polynomial(&self) -> &[Point] { + &self.point_polynomial + } + + /// ☠ Type unsafe: you have to make sure the polynomial fits the type parameters + fn from_inner(point_polynomial: Vec>) -> Self { + SharedKey { + point_polynomial, + ty: PhantomData, + } + } + + /// Converts a `SharedKey` that's was marked as `Zero` to `NonZero`. + /// + /// If the shared key *was* actually zero ([`is_zero`] returns true) it returns `None`. + /// + /// [`is_zero`]: Self::is_zero + pub fn non_zero(self) -> Option> { + if self.point_polynomial[0].is_zero() { + return None; + } + + Some(SharedKey::from_inner(self.point_polynomial)) + } + + /// Whether the shared key is actually zero. i.e. the first coefficient of the sharing polynomial [`is_zero`]. + /// + /// [`is_zero`]: secp256kfun::Point::is_zero + pub fn is_zero(&self) -> bool { + self.point_polynomial[0].is_zero() + } + + /// Adds a scalar `tweak` to the shared key. + /// + /// The returned `SharedKey` represents a sharing of the original value + `tweak`. + /// + /// This is useful for deriving unhardened child frost keys from a master frost public key using + /// [BIP32]. In cases like this since you know that the tweak was computed from a hash of the + /// original key you call [`non_zero`] and unwrap the `Option` since zero is computationally + /// unreachable. + /// + /// In order for `PairedSecretShare` s to be valid against the new key they will have to apply the same operation. + /// + /// If you want to apply an "x-only" tweak you need to call this then [`non_zero`] and finally [`into_xonly`]. + /// + /// [BIP32]: https://bips.xyz/32 + /// [`non_zero`]: Self::non_zero + /// [`into_xonly`]: Self::into_xonly + #[must_use] + pub fn homomorphic_add( + mut self, + tweak: Scalar, + ) -> SharedKey { + self.point_polynomial[0] = g!(self.point_polynomial[0] + tweak * G).normalize(); + SharedKey::from_inner(self.point_polynomial) + } + + /// Negates the polynomial + #[must_use] + pub fn homomorphic_negate(mut self) -> SharedKey { + poly::point::negate(&mut self.point_polynomial); + SharedKey::from_inner(self.point_polynomial) + } + + /// Multiplies the shared key by a scalar. + /// + /// In order for a [`PairedSecretShare`] to be valid against the new key they will have to apply + /// [the same operation](super::PairedSecretShare::homomorphic_mul). + /// + /// [`PairedSecretShare`]: super::PairedSecretShare + #[must_use] + pub fn homomorphic_mul(mut self, tweak: Scalar) -> SharedKey { + for coeff in &mut self.point_polynomial { + *coeff = g!(tweak * coeff.deref()).normalize(); + } + SharedKey::from_inner(self.point_polynomial) + } + + /// The public key that has been shared. + /// + /// This is using *public key* in a rather loose sense. Unless it's a `SharedKey` then it + /// won't be usable as an actual Schnorr [BIP340] public key. + /// + /// [BIP340]: https://bips.xyz/340 + pub fn public_key(&self) -> Point { + // SAFETY: we hold the first coefficient to match the type parameters always + let public_key = Z::cast_point(self.point_polynomial[0]).expect("invariant"); + T::cast_point(public_key).expect("invariant") + } + + /// Encodes a `SharedKey` as the compressed encoding of each underlying polynomial coefficient + /// + /// i.e. call [`Point::to_bytes`] on each coefficient starting with the constant term. Note that + /// even if it's a `SharedKey` the first coefficient (A.K.A the public key) will still be + /// encoded as 33 bytes. + /// + /// ⚠ Unlike other secp256kfun things this doesn't exactly match the serde/bincode + /// implementations which will length prefix the list of points. + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(self.point_polynomial.len() * 33); + for coeff in &self.point_polynomial { + bytes.extend(coeff.to_bytes()) + } + bytes + } + + /// Decodes a `SharedKey` (for any `T` and `Z`) from a slice. + /// + /// Returns `None` if the bytes don't represent points or if the first coefficient doesn't + /// satisfy the constraints of `T` and `Z`. + pub fn from_slice(bytes: &[u8]) -> Option { + let mut poly = vec![]; + for point_bytes in bytes.chunks(33) { + poly.push(Point::from_slice(point_bytes)?); + } + + // check first coefficient satisfies both type parameters + let first_coeff = Z::cast_point(poly[0])?; + let _check = T::cast_point(first_coeff)?; + + Some(Self::from_inner(poly)) + } +} + +impl SharedKey { + /// Convert the key into a BIP340 "x-only" SharedKey. + /// + /// This is the [BIP340] compatible version of the key which you can put in a segwitv1 output. + /// + /// [BIP340]: https://bips.xyz/340 + pub fn into_xonly(mut self) -> SharedKey { + let needs_negation = !self.public_key().is_y_even(); + if needs_negation { + self = self.homomorphic_negate(); + debug_assert!(self.public_key().is_y_even()); + } + + SharedKey::from_inner(self.point_polynomial) + } +} + +impl SharedKey { + /// Constructor to create a shared key from a vector of points where each item represent a polynomial + /// coefficient. + /// + /// The resulting shared key will be `SharedKey`. It's up to the caller to do the zero check with [`non_zero`] + /// + /// [`non_zero`]: Self::non_zero + pub fn from_poly(poly: Vec>) -> Self { + if poly.is_empty() { + // an empty polynomial is represented as a vector with a single zero item to avoid + // panics + return Self::from_poly(vec![Point::zero()]); + } + + SharedKey::from_inner(poly) + } + + /// Create a shared key from a subset of share images. + /// + /// If all the share images are correct and you have at least a threshold of them then you'll + /// get the original shared key. If you put in a wrong share you won't get the right answer and + /// there will be no error. + /// + /// Note that a "share image" is not a concept that we really use in the core of this library + /// but you can get one from a share with [`SecretShare::share_image`]. + /// + /// ## Security + /// + /// ⚠ You can't just take any points you want and pass them in here and hope it's secure. + /// They need to be from a securely generated key. + pub fn from_share_images( + shares: &[(PartyIndex, Point)], + ) -> Self { + let poly = poly::point::interpolate(shares); + let poly = poly::point::normalize(poly); + SharedKey::from_inner(poly.collect()) + } +} + +impl SharedKey { + /// The verification shares of each party in the key. + /// + /// The verification share is the image of their secret share. + pub fn verification_share(&self, index: PartyIndex) -> VerificationShare { + let share_image = poly::point::eval(&self.point_polynomial, index); + VerificationShare { + index, + share_image, + public_key: self.public_key(), + } + } +} + +#[cfg(feature = "bincode")] +impl crate::fun::bincode::Decode for SharedKey { + fn decode( + decoder: &mut D, + ) -> Result { + use secp256kfun::bincode::error::DecodeError; + let poly = Vec::>::decode(decoder)?; + let first_coeff = Z::cast_point(poly[0]).ok_or(DecodeError::Other( + "zero public key for non-zero shared key", + ))?; + let _check = T::cast_point(first_coeff) + .ok_or(DecodeError::Other("odd-y public key for even-y shared key"))?; + + Ok(SharedKey { + point_polynomial: poly, + ty: PhantomData, + }) + } +} + +#[cfg(feature = "serde")] +impl<'de, T: PointType, Z: ZeroChoice> crate::fun::serde::Deserialize<'de> for SharedKey { + fn deserialize(deserializer: D) -> Result + where + D: secp256kfun::serde::Deserializer<'de>, + { + let poly = Vec::>::deserialize(deserializer)?; + + let first_coeff = Z::cast_point(poly[0]).ok_or(crate::fun::serde::de::Error::custom( + "zero public key for non-zero shared key", + ))?; + + let _check = T::cast_point(first_coeff).ok_or(crate::fun::serde::de::Error::custom( + "odd-y public key for even-y shared key", + ))?; + + Ok(Self { + point_polynomial: poly, + ty: PhantomData, + }) + } +} + +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); +#[cfg(feature = "bincode")] +crate::fun::bincode::impl_borrow_decode!(SharedKey); + +#[cfg(test)] +mod test { + use super::*; + + #[cfg(feature = "bincode")] + #[test] + fn bincode_encoding_decoding_roundtrip() { + use crate::fun::bincode; + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = + bincode::encode_to_vec(&poly_zero, bincode::config::standard()).unwrap(); + let bytes_poly_one = + bincode::encode_to_vec(&poly_one, bincode::config::standard()).unwrap(); + let bytes_poly_minus_one = + bincode::encode_to_vec(&poly_minus_one, bincode::config::standard()).unwrap(); + + let (poly_zero_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .unwrap(); + let (poly_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_one, + bincode::config::standard(), + ) + .unwrap(); + + let (poly_minus_one_got, _) = bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .unwrap(); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_zero, + bincode::config::standard(), + ) + .is_err()); + + assert!(bincode::decode_from_slice::, _>( + &bytes_poly_minus_one, + bincode::config::standard(), + ) + .is_err()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } + + #[test] + fn to_bytes_from_slice_roudtrip() { + let poly_zero = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(0 * G), + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + ]) + .collect(), + ); + let poly_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap() + .into_xonly(); + + let poly_minus_one = SharedKey::::from_poly( + poly::point::normalize(vec![ + g!(-1 * G).mark_zero(), + g!(2 * G).mark_zero(), + g!(3 * G).mark_zero(), + ]) + .collect(), + ) + .non_zero() + .unwrap(); + + let bytes_poly_zero = poly_zero.to_bytes(); + let bytes_poly_one = poly_one.to_bytes(); + let bytes_poly_minus_one = poly_minus_one.to_bytes(); + + let poly_zero_got = SharedKey::::from_slice(&bytes_poly_zero[..]).unwrap(); + let poly_one_got = SharedKey::::from_slice(&bytes_poly_one).unwrap(); + let poly_minus_one_got = + SharedKey::::from_slice(&bytes_poly_minus_one[..]).unwrap(); + + assert!(SharedKey::::from_slice(&bytes_poly_zero[..]).is_none()); + assert!(SharedKey::::from_slice(&bytes_poly_minus_one[..]).is_none()); + + assert_eq!(poly_zero_got, poly_zero); + assert_eq!(poly_one_got, poly_one); + assert_eq!(poly_minus_one_got, poly_minus_one); + } +} diff --git a/schnorr_fun/src/musig.rs b/schnorr_fun/src/musig.rs index 6706c41f..78142478 100644 --- a/schnorr_fun/src/musig.rs +++ b/schnorr_fun/src/musig.rs @@ -71,8 +71,7 @@ //! //! [the excellent paper]: https://eprint.iacr.org/2020/1261.pdf //! [secp256k1-zkp]: https://github.com/ElementsProject/secp256k1-zkp/pull/131 -pub use crate::binonce::{Nonce, NonceKeyPair}; -use crate::{adaptor::EncryptedSignature, Message, Schnorr, Signature}; +use crate::{adaptor::EncryptedSignature, binonce, Message, Schnorr, Signature}; use alloc::vec::Vec; use secp256kfun::{ digest::{generic_array::typenum::U32, Digest}, @@ -116,8 +115,8 @@ impl MuSig { /// Generate nonces for creating signatures shares. /// /// ⚠ You must use a CAREFULLY CHOSEN nonce rng, see [`MuSig::seed_nonce_rng`] - pub fn gen_nonce(&self, nonce_rng: &mut R) -> NonceKeyPair { - NonceKeyPair::random(nonce_rng) + pub fn gen_nonce(&self, nonce_rng: &mut R) -> binonce::NonceKeyPair { + binonce::NonceKeyPair::random(nonce_rng) } } @@ -340,8 +339,8 @@ where /// - `agg_key`: the joint public key we are signing under. This can be an `XOnly` or `Normal`. /// It will return the same nonce regardless. /// - `secret`: you're secret key as part of `agg_key`. This **must be the secret key you are - /// going to sign with**. It cannot be an "untweaked" version of the signing key. It must be - /// exactly equal to the secret key you pass to [`sign`] (the MuSig specification requires this). + /// going to sign with**. It cannot be an "untweaked" version of the signing key. It must be + /// exactly equal to the secret key you pass to [`sign`] (the MuSig specification requires this). /// - `session_id`: a string of bytes that is **unique for each signing attempt**. /// /// The application should decide upon a unique `session_id` per call to this function. If the @@ -430,9 +429,9 @@ pub struct Adaptor { bincode(crate = "crate::fun::bincode") )] pub struct SignSession { - b: Scalar, + b: Scalar, c: Scalar, - public_nonces: Vec, + public_nonces: Vec, R: Point, nonce_needs_negation: bool, signing_type: T, @@ -455,15 +454,11 @@ impl + Clone, NG> MuSig { pub fn start_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + nonces: Vec, message: Message<'_, Public>, ) -> SignSession { - let (b, c, public_nonces, R, nonce_needs_negation) = self._start_sign_session( - agg_key, - nonces, - message, - &Point::::zero(), - ); + let (b, c, public_nonces, R, nonce_needs_negation) = + self._start_sign_session(agg_key, nonces, message, Point::::zero()); SignSession { b, c, @@ -497,9 +492,9 @@ impl + Clone, NG> MuSig { pub fn start_encrypted_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + nonces: Vec, message: Message<'_, Public>, - encryption_key: &Point, + encryption_key: Point, ) -> Option> { let (b, c, public_nonces, R, nonce_needs_negation) = self._start_sign_session(agg_key, nonces, message, encryption_key); @@ -519,51 +514,41 @@ impl + Clone, NG> MuSig { fn _start_sign_session( &self, agg_key: &AggKey, - nonces: Vec, + mut nonces: Vec, message: Message<'_, Public>, - encryption_key: &Point, + encryption_key: Point, ) -> ( + Scalar, Scalar, - Scalar, - Vec, + Vec, Point, bool, ) { - let mut Rs = nonces; - let agg_Rs = Rs.iter().fold([Point::zero(); 2], |acc, nonce| { - [g!(acc[0] + nonce.0[0]), g!(acc[1] + nonce.0[1])] - }); - let agg_Rs = Nonce::([ - g!(agg_Rs[0] + encryption_key).normalize(), - agg_Rs[1].normalize(), - ]); - - let b = { + let mut agg_binonce = binonce::Nonce::aggregate(nonces.iter().cloned()); + agg_binonce.0[0] = g!(agg_binonce.0[0] + encryption_key).normalize(); + + let binding_coeff = { let H = self.nonce_coeff_hash.clone(); Scalar::from_hash( - H.add(agg_Rs.to_bytes()) + H.add(agg_binonce) .add(agg_key.agg_public_key()) .add(message), ) } - .public() - .mark_zero(); + .public(); - let (R, r_needs_negation) = g!(agg_Rs.0[0] + b * agg_Rs.0[1]) - .normalize() - .non_zero() - .unwrap_or(Point::generator()) - .into_point_with_even_y(); - - for R_i in &mut Rs { - R_i.conditional_negate(r_needs_negation); - } + let (R, nonces_need_negation) = agg_binonce.bind(binding_coeff); let c = self .schnorr .challenge(&R, &agg_key.agg_public_key(), message); - (b, c, Rs, R, r_needs_negation) + // we may as well eagerly do + for nonce in &mut nonces { + nonce.conditional_negate(nonces_need_negation); + } + + (binding_coeff, c, nonces, R, nonces_need_negation) } /// Generates a partial signature (or partial encrypted signature depending on `T`) for the local_secret_nonce. @@ -573,7 +558,7 @@ impl + Clone, NG> MuSig { session: &SignSession, my_index: usize, keypair: &KeyPair, - local_secret_nonce: NonceKeyPair, + local_secret_nonce: binonce::NonceKeyPair, ) -> Scalar { assert_eq!( keypair.public_key(), @@ -898,7 +883,7 @@ mod test { &agg_key1, nonces.clone(), message, - &encryption_key + encryption_key ) .unwrap(); let p2_session = musig @@ -906,7 +891,7 @@ mod test { &agg_key2, nonces.clone(), message, - &encryption_key + encryption_key ) .unwrap(); let p3_session = musig @@ -914,7 +899,7 @@ mod test { &agg_key3, nonces, message, - &encryption_key + encryption_key ) .unwrap(); let p1_sig = musig.sign(&agg_key1, &p1_session, 0, &keypair1, p1_nonce); diff --git a/schnorr_fun/src/signature.rs b/schnorr_fun/src/signature.rs index 0cfecfeb..fc519d78 100644 --- a/schnorr_fun/src/signature.rs +++ b/schnorr_fun/src/signature.rs @@ -1,7 +1,7 @@ use crate::fun::{marker::*, rand_core::RngCore, Point, Scalar}; /// A Schnorr signature. -#[derive(Clone, Eq)] +#[derive(Clone, Eq, Copy)] pub struct Signature { /// The signature's public nonce /// diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index adfd9b56..20e426df 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -20,24 +20,51 @@ 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); // // create some scalar polynomial for each party let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha); - let (mut frost_key, secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); + let (mut shared_key, mut secret_shares) = proto.simulate_keygen(threshold, n_parties, &mut rng); - if let Some(tweak) = plain_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); + if let Some(tweak) = add_tweak { + for secret_share in &mut secret_shares { + *secret_share = secret_share.homomorphic_add(tweak).non_zero().unwrap(); + } + shared_key = shared_key.homomorphic_add(tweak).non_zero().unwrap(); } - let mut frost_key = frost_key.into_xonly_key(); + if let Some(mul_tweak) = mul_tweak { + shared_key = shared_key.homomorphic_mul(mul_tweak); + for secret_share in &mut secret_shares { + *secret_share = secret_share.homomorphic_mul(mul_tweak); + } + } + + let mut xonly_shared_key = shared_key.into_xonly(); + let mut xonly_secret_shares = secret_shares.into_iter().map(|secret_share| secret_share.into_xonly()).collect::>(); + + if let Some(tweak) = xonly_add_tweak { + xonly_shared_key = xonly_shared_key.homomorphic_add(tweak).non_zero().unwrap().into_xonly(); + for secret_share in &mut xonly_secret_shares { + *secret_share = secret_share.homomorphic_add(tweak).non_zero().unwrap().into_xonly(); + } + } + + if let Some(xonly_mul_tweak) = xonly_mul_tweak { + xonly_shared_key = xonly_shared_key.homomorphic_mul(xonly_mul_tweak).into_xonly(); + for secret_share in &mut xonly_secret_shares { + *secret_share = secret_share.homomorphic_mul(xonly_mul_tweak).into_xonly(); + } + } - if let Some(tweak) = xonly_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); + for secret_share in &xonly_secret_shares { + assert_eq!(secret_share.public_key(), xonly_shared_key.public_key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers @@ -46,57 +73,60 @@ proptest! { // shuffle the mask for random signers signer_mask.shuffle(&mut rng); - let secret_shares = signer_mask.into_iter().zip(secret_shares.into_iter()).filter(|(is_signer, _)| *is_signer) + let secret_shares_of_signers = signer_mask.into_iter().zip(xonly_secret_shares.into_iter()).filter(|(is_signer, _)| *is_signer) .map(|(_, secret_share)| secret_share).collect::>(); let sid = b"frost-prop-test".as_slice(); let message = Message::plain("test", b"test"); - let mut secret_nonces: BTreeMap<_, _> = secret_shares.iter().map(|secret_share| { - (secret_share.index, proto.gen_nonce::( + let mut secret_nonces: BTreeMap<_, _> = secret_shares_of_signers.iter().map(|paired_secret_share| { + (paired_secret_share.secret_share().index, proto.gen_nonce::( &mut proto.seed_nonce_rng( - &frost_key, - &secret_share.secret, + *paired_secret_share, sid, ))) }).collect(); let public_nonces = secret_nonces.iter().map(|(signer_index, sn)| (*signer_index, sn.public())).collect::>(); - dbg!(&public_nonces); - let signing_session = proto.start_sign_session( - &frost_key, + let coord_signing_session = proto.coordinator_sign_session( + &xonly_shared_key, public_nonces, message ); - let mut signatures = vec![]; - for secret_share in secret_shares { - let sig = proto.sign( - &frost_key, - &signing_session, + let party_signing_session = proto.party_sign_session( + xonly_shared_key.public_key(), + coord_signing_session.parties(), + coord_signing_session.agg_binonce(), + message, + ); + + let mut signatures = BTreeMap::default(); + for secret_share in secret_shares_of_signers { + let sig = party_signing_session.sign( &secret_share, - secret_nonces.remove(&secret_share.index).unwrap() + secret_nonces.remove(&secret_share.index()).unwrap() ); - assert!(proto.verify_signature_share( - &frost_key, - &signing_session, - secret_share.index, - sig) + assert_eq!(coord_signing_session.verify_signature_share( + secret_share.verification_share(), + sig), Ok(()) ); - signatures.push(sig); + signatures.insert(secret_share.index(), sig); } - let combined_sig = proto.combine_signature_shares( - &frost_key, - &signing_session, - signatures); + let combined_sig = coord_signing_session.combine_signature_shares( + coord_signing_session.final_nonce(), + signatures.values().cloned() + ); + assert_eq!(coord_signing_session.verify_and_combine_signature_shares(&xonly_shared_key, signatures), Ok(combined_sig)); assert!(proto.schnorr.verify( - &frost_key.public_key(), + &xonly_shared_key.public_key(), message, &combined_sig )); + } } diff --git a/schnorr_fun/tests/musig_sign_verify.rs b/schnorr_fun/tests/musig_sign_verify.rs index 17827c62..c0c4abd1 100644 --- a/schnorr_fun/tests/musig_sign_verify.rs +++ b/schnorr_fun/tests/musig_sign_verify.rs @@ -1,9 +1,9 @@ #![cfg(feature = "serde")] use schnorr_fun::{ binonce, + binonce::NonceKeyPair, fun::{marker::*, serde, Point, Scalar}, - musig::{self, NonceKeyPair}, - Message, + musig, Message, }; static TEST_JSON: &str = include_str!("musig/sign_verify_vectors.json"); use secp256kfun::hex; diff --git a/schnorr_fun/tests/musig_tweak.rs b/schnorr_fun/tests/musig_tweak.rs index 4306a37d..b20e458b 100644 --- a/schnorr_fun/tests/musig_tweak.rs +++ b/schnorr_fun/tests/musig_tweak.rs @@ -4,9 +4,9 @@ use std::{rc::Rc, sync::Arc}; use schnorr_fun::{ binonce, + binonce::NonceKeyPair, fun::{marker::*, serde, Point, Scalar}, - musig::{self, NonceKeyPair}, - Message, + musig, Message, }; static TEST_JSON: &'static str = include_str!("musig/tweak_vectors.json"); use secp256kfun::hex; diff --git a/secp256kfun/src/macros.rs b/secp256kfun/src/macros.rs index 14243e84..13797b44 100644 --- a/secp256kfun/src/macros.rs +++ b/secp256kfun/src/macros.rs @@ -30,8 +30,8 @@ macro_rules! s { /// - ` + ` adds two points /// - ` - ` subtracts one point from another /// - ` .* ` does a [dot product](https://en.wikipedia.org/wiki/Dot_product) -/// between a list of points and scalars. If one list is shorter than the other then the excess -/// points or scalars will be multiplied by 0. See [`op::point_scalar_dot_product`]. +/// between a list of points and scalars. If one list is shorter than the other then the excess +/// points or scalars will be multiplied by 0. See [`op::point_scalar_dot_product`]. /// /// The terms of the expression can be any variable followed by simple method calls, attribute /// access etc. If your term involves more expressions (anything involving specifying types using diff --git a/secp256kfun/src/marker/point_type.rs b/secp256kfun/src/marker/point_type.rs index 894b06af..c57d1fbe 100644 --- a/secp256kfun/src/marker/point_type.rs +++ b/secp256kfun/src/marker/point_type.rs @@ -1,3 +1,7 @@ +use crate::Point; + +use super::ZeroChoice; + /// Every `T` of a [`Point`] implements the `PointType` trait. /// /// There are several different point types. @@ -16,6 +20,14 @@ pub trait PointType: /// Whether the point type is normalized or not (i.e. not [`NonNormal`]) fn is_normalized() -> bool; + + /// Cast a point that is not of this type to this type. + /// + /// This is useful internally for doing very generic things and shouldn't be used in + /// applications. + fn cast_point( + point: Point, + ) -> Option>; } /// A Fully Normalized Point. Internally `Normal` points are represented using @@ -72,13 +84,47 @@ impl Normalized for EvenY {} impl Normalized for Normal {} impl Normalized for BasePoint {} -impl PointType for N { +impl PointType for Normal { type NegationType = Normal; #[inline(always)] fn is_normalized() -> bool { true } + + fn cast_point( + point: Point, + ) -> Option> { + Some(point.normalize()) + } +} + +impl PointType for EvenY { + type NegationType = Normal; + + fn is_normalized() -> bool { + true + } + + /// ⚠ This will always return `None` if trying to cast from a `Zero` marked point (even if the actual point is not `Zero`) + fn cast_point( + point: Point, + ) -> Option> { + let (point, needs_negation) = point.non_zero()?.into_point_with_even_y(); + if needs_negation { + return None; + } + + // we don't want to allow creating Point + if Z::is_zero() { + return None; + } + + // we already checked it's not zero + let point = Z::cast_point(point).expect("infallible"); + + Some(point) + } } impl PointType for NonNormal { @@ -88,4 +134,24 @@ impl PointType for NonNormal { fn is_normalized() -> bool { false } + + fn cast_point( + point: Point, + ) -> Option> { + Some(point.non_normal()) + } +} + +impl PointType for BasePoint { + type NegationType = Normal; + + fn is_normalized() -> bool { + true + } + + fn cast_point( + _point: Point, + ) -> Option> { + None + } } diff --git a/secp256kfun/src/marker/zero_choice.rs b/secp256kfun/src/marker/zero_choice.rs index e001fc15..fceb0580 100644 --- a/secp256kfun/src/marker/zero_choice.rs +++ b/secp256kfun/src/marker/zero_choice.rs @@ -1,3 +1,4 @@ +use crate::Point; /// Something marked with Zero might be `0` i.e. the additive identity #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -30,17 +31,29 @@ pub trait ZeroChoice: { /// Returns whether the type is `Zero` fn is_zero() -> bool; + + /// Casts a point from one zeroness to another. + fn cast_point(point: Point) -> Option>; } impl ZeroChoice for Zero { fn is_zero() -> bool { true } + + fn cast_point(point: Point) -> Option> { + Some(point.mark_zero()) + } } + impl ZeroChoice for NonZero { fn is_zero() -> bool { false } + + fn cast_point(point: Point) -> Option> { + point.non_zero() + } } /// A trait to figure out whether the result of a multiplication should be [`Zero`] or [`NonZero`] at compile time. diff --git a/secp256kfun/src/point.rs b/secp256kfun/src/point.rs index a4d20e76..71491ebc 100644 --- a/secp256kfun/src/point.rs +++ b/secp256kfun/src/point.rs @@ -251,6 +251,19 @@ impl Point { backend::BackendPoint::is_zero(&self.0) } + /// Convert a point that is marked as `Zero` to `NonZero`. + /// + /// If the point *was* actually zero ([`is_zero`] returns true) it returns `None`. + /// + /// [`is_zero`]: Point::is_zero + pub fn non_zero(self) -> Option> { + if self.is_zero() { + None + } else { + Some(Point::from_inner(self.0, self.1)) + } + } + pub(crate) const fn from_inner(backend_point: backend::Point, point_type: T) -> Self { Point(backend_point, point_type, PhantomData) } @@ -421,19 +434,6 @@ impl Point { } impl Point { - /// Convert a point that is marked as `Zero` to `NonZero`. - /// - /// If the point *was* actually zero ([`is_zero`] returns true) it returns `None`. - /// - /// [`is_zero`]: Point::is_zero - pub fn non_zero(self) -> Option> { - if self.is_zero() { - None - } else { - Some(Point::from_inner(self.0, self.1)) - } - } - /// Returns the [`identity element`] of the group A.K.A. the point at infinity. /// /// # Example diff --git a/secp256kfun/src/poly.rs b/secp256kfun/src/poly.rs index 7f1b95f7..fb99deb5 100644 --- a/secp256kfun/src/poly.rs +++ b/secp256kfun/src/poly.rs @@ -53,11 +53,11 @@ pub mod scalar { pub fn interpolate_and_eval_poly_at_0( x_and_y: &[(Scalar, Scalar)], ) -> Scalar { - let indicies = x_and_y.iter().map(|(index, _)| *index); + let indices = x_and_y.iter().map(|(index, _)| *index); x_and_y .iter() .map(|(index, secret)| { - let lambda = eval_basis_poly_at_0(*index, indicies.clone()); + let lambda = eval_basis_poly_at_0(*index, indices.clone()); s!(secret * lambda) }) .fold(s!(0), |interpolated_poly, scaled_basis_poly| { @@ -133,6 +133,13 @@ pub mod scalar { _ => None, }) } + + /// Negates a scalar polynomial + pub fn negate(poly: &mut [Scalar]) { + for coeff in poly { + *coeff = -*coeff; + } + } } /// Functions for dealing with point polynomials @@ -182,11 +189,15 @@ pub mod point { /// Find the coefficients of the polynomial that interpolates a set of points (index, point). /// - /// Panics if the indicies are not unique. + /// Panics if the indices are not unique. /// /// A vector with a tail of zero coefficients means the interpolation was overdetermined. + #[allow(clippy::type_complexity)] pub fn interpolate( - index_and_point: &[(Scalar, Point)], + index_and_point: &[( + Scalar, + Point, + )], ) -> Vec> { let x_ms = index_and_point.iter().map(|(index, _)| *index); let mut interpolating_polynomial: Vec> = vec![]; @@ -199,8 +210,31 @@ pub mod point { self::add_in_place(&mut interpolating_polynomial, point_scaled_basis_poly); } + while interpolating_polynomial.len() > 1 + && interpolating_polynomial.last().unwrap().is_zero() + { + interpolating_polynomial.pop(); + } + interpolating_polynomial } + + /// Negates a scalar polynomial + pub fn negate(poly: &mut [Point]) + where + T: PointType, + { + for coeff in poly { + *coeff = -*coeff; + } + } + + /// Normalizes the points in a polynomial + pub fn normalize( + poly: impl IntoIterator>, + ) -> impl Iterator> { + poly.into_iter().map(|point| point.normalize()) + } } /// Returns an iterator of 1, x, x², x³ ... fn powers(x: Scalar) -> impl Iterator> { diff --git a/secp256kfun/tests/poly.rs b/secp256kfun/tests/poly.rs index 47f5ff93..f0d341ca 100644 --- a/secp256kfun/tests/poly.rs +++ b/secp256kfun/tests/poly.rs @@ -1,5 +1,5 @@ #![cfg(feature = "alloc")] -use secp256kfun::{g, marker::*, poly, s, Point, G}; +use secp256kfun::{poly, prelude::*}; #[test] fn test_lagrange_lambda() { @@ -58,8 +58,8 @@ fn test_add_scalar_poly() { #[test] fn test_recover_public_poly() { let poly = vec![g!(1 * G), g!(2 * G), g!(3 * G)]; - let indicies = vec![s!(1).public(), s!(3).public(), s!(2).public()]; - let points = indicies + let indices = vec![s!(1).public(), s!(3).public(), s!(2).public()]; + let points = indices .clone() .into_iter() .map(|index| { @@ -80,46 +80,44 @@ fn test_recover_public_poly() { #[test] fn test_recover_overdetermined_poly() { let poly = vec![g!(1 * G), g!(2 * G), g!(3 * G)]; - let indicies = vec![ + let indices = vec![ s!(1).public(), s!(2).public(), s!(3).public(), s!(4).public(), s!(5).public(), ]; - let points = indicies + let points = indices .clone() .into_iter() - .map(|index| { - ( - index, - poly::point::eval(&poly, index.public()) - .normalize() - .non_zero() - .unwrap(), - ) - }) + .map(|index| (index, poly::point::eval(&poly, index.public()).normalize())) .collect::>(); let interpolation = poly::point::interpolate(&points); - let (interpolated_coeffs, zero_coeffs) = interpolation.split_at(poly.len()); - let n_extra_points = indicies.len() - poly.len(); + assert_eq!(interpolation, poly); +} + +#[test] +fn test_recover_zero_poly() { + let interpolation = poly::point::interpolate(&[ + (s!(1).public(), Point::::zero()), + (s!(2).public(), Point::::zero()), + ]); + assert_eq!( - (0..n_extra_points) - .map(|_| Point::::zero().public().normalize()) - .collect::>(), - zero_coeffs.to_vec() + interpolation, + vec![Point::::zero()], + "should not be empty vector" ); - assert_eq!(interpolated_coeffs, poly); } #[test] fn test_reconstruct_shared_secret() { - let indicies = vec![s!(1).public(), s!(2).public(), s!(3).public()]; + let indices = vec![s!(1).public(), s!(2).public(), s!(3).public()]; let scalar_poly = vec![s!(42), s!(53), s!(64)]; - let secret_shares: Vec<_> = indicies + let secret_shares: Vec<_> = indices .clone() .into_iter() .map(|index| (index, poly::scalar::eval(&scalar_poly, index)))