diff --git a/schnorr_fun/src/frost/mod.rs b/schnorr_fun/src/frost/mod.rs index a69a8d0c..cd1a0d99 100644 --- a/schnorr_fun/src/frost/mod.rs +++ b/schnorr_fun/src/frost/mod.rs @@ -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,28 +85,38 @@ //! // ⚠️ 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 coordiantor (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 these //! 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.shared_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 = frost.sign(&sign_session, &xonly_my_secret_share, my_nonce); +//! # let sig_share3 = frost.sign(&sign_session, &xonly_secret_share3, nonce3); +//! // 🐙 receive the partial signature(s) from the other participant(s) and verify. +//! assert!(frost.verify_signature_share(&xonly_shared_key, &coord_session, party_index3, sig_share3)); +//! // 🐙 combine signature shares into a single signature that is valid under the FROST key +//! // It won't be necessarily be valid unless you verified each share. +//! let combined_sig = frost.combine_signature_shares(&coord_session, vec![my_sig_share, sig_share3]); //! assert!(frost.schnorr.verify( -//! &xonly_frost_key.public_key(), +//! &xonly_shared_key.key(), //! message, //! &combined_sig //! )); @@ -114,7 +124,7 @@ //! //! # 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`). //! @@ -175,10 +185,12 @@ //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar -mod frost_poly; -pub use frost_poly::*; +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::{binonce, Message, Schnorr, Signature}; @@ -301,7 +313,7 @@ where /// [`Frost::new_keygen`] #[derive(Clone, Debug)] pub struct KeyGen { - frost_poly: FrostPoly, + frost_poly: SharedKey, point_polys: BTreeMap>, } @@ -450,14 +462,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, - ) -> (FrostPoly, Vec>) { + ) -> (SharedKey, Vec>) { let scalar_polys = (0..n_parties) .map(|i| { ( @@ -538,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. @@ -598,7 +610,7 @@ impl + Clone, NG> Frost { } } - let frost_poly = FrostPoly::from_poly( + let frost_poly = SharedKey::from_poly( joint_poly .into_iter() .map(|coef| coef.normalize()) @@ -618,7 +630,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) @@ -647,14 +659,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<(PairedSecretShare, FrostPoly), FinishKeyGenError> { + ) -> Result<(PairedSecretShare, SharedKey), FinishKeyGenError> { let mut total_secret_share = s!(0); for (party_index, poly) in &keygen.point_polys { @@ -682,14 +694,13 @@ impl + Clone, NG> Frost { share: total_secret_share, }; - let secret_share_with_image = - PairedSecretShare::new(secret_share, keygen.frost_poly.shared_key()); + let secret_share_with_image = PairedSecretShare::new(secret_share, keygen.frost_poly.key()); Ok((secret_share_with_image, keygen.frost_poly)) } /// Start party signing session - pub fn start_party_sign_session( + pub fn party_sign_session( &self, shared_key: Point, parties: BTreeSet, @@ -718,9 +729,9 @@ impl + Clone, NG> Frost { /// # Panics /// /// If the number of nonces is less than the threshold. - pub fn start_coordinator_sign_session( + pub fn coordinator_sign_session( &self, - frost_poly: &FrostPoly, + frost_poly: &SharedKey, mut nonces: BTreeMap, message: Message, ) -> CoordinatorSignSession { @@ -730,12 +741,12 @@ impl + Clone, NG> Frost { let agg_binonce = binonce::Nonce::aggregate(nonces.values().cloned()); - let binding_coeff = self.binding_coefficient(frost_poly.shared_key(), agg_binonce, message); + let binding_coeff = self.binding_coefficient(frost_poly.key(), agg_binonce, message); let (final_nonce, binonce_needs_negation) = agg_binonce.bind(binding_coeff); let challenge = self .schnorr - .challenge(&final_nonce, &frost_poly.shared_key(), message); + .challenge(&final_nonce, &frost_poly.key(), message); for nonce in nonces.values_mut() { nonce.conditional_negate(binonce_needs_negation); @@ -806,7 +817,7 @@ impl + Clone, NG> Frost { /// Returns `bool`, true if partial signature is valid. pub fn verify_signature_share( &self, - frost_poly: &FrostPoly, + shared_key: &SharedKey, session: &CoordinatorSignSession, index: PartyIndex, signature_share: Scalar, @@ -815,7 +826,7 @@ impl + Clone, NG> Frost { let lambda = poly::eval_basis_poly_at_0(index, session.nonces.keys().cloned()); let c = &session.challenge; let b = &session.binding_coeff; - let X = frost_poly.verification_share(index); + let X = shared_key.verification_share(index); let [R1, R2] = session .nonces .get(&index) @@ -849,71 +860,6 @@ impl + Clone, NG> Frost { } } -/// 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 CoordinatorSignSession { - binding_coeff: Scalar, - agg_binonce: binonce::Nonce, - final_nonce: Point, - challenge: Scalar, - nonces: BTreeMap, -} - -impl CoordinatorSignSession { - /// Fetch the participant indices for this signing session. - /// - /// ## Return value - /// - /// An iterator of participant indices - pub fn participants(&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 go on the blockchain - pub fn final_nonce(&self) -> Point { - self.final_nonce - } -} - -/// The session that is used to sign a message. -#[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 { - shared_key: Point, - parties: BTreeSet>, - challenge: Scalar, - binonce_needs_negation: bool, - binding_coeff: Scalar, -} - /// Constructor for a Frost instance using deterministic nonce generation. /// /// If you use deterministic nonce generation you will have to provide a unique session id to every signing session. @@ -973,7 +919,7 @@ mod test { let mut malicious_nonce = nonce; malicious_nonce.conditional_negate(true); - let session = frost.start_coordinator_sign_session( + 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"), diff --git a/schnorr_fun/src/frost/session.rs b/schnorr_fun/src/frost/session.rs new file mode 100644 index 00000000..d73541ba --- /dev/null +++ b/schnorr_fun/src/frost/session.rs @@ -0,0 +1,71 @@ +use crate::{binonce, frost::PartyIndex}; +use alloc::collections::{BTreeMap, BTreeSet}; +use secp256kfun::prelude::*; +/// A FROST signing session +/// +/// 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) binding_coeff: Scalar, + pub(crate) agg_binonce: binonce::Nonce, + pub(crate) final_nonce: Point, + pub(crate) challenge: Scalar, + 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 go on the blockchain + pub fn final_nonce(&self) -> Point { + self.final_nonce + } +} + +/// 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) shared_key: Point, + pub(crate) parties: BTreeSet>, + pub(crate) challenge: Scalar, + pub(crate) binonce_needs_negation: bool, + pub(crate) binding_coeff: Scalar, +} diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs index d2824697..f29e68b8 100644 --- a/schnorr_fun/src/frost/share.rs +++ b/schnorr_fun/src/frost/share.rs @@ -477,7 +477,7 @@ mod test { 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_poly.shared_key()); + prop_assert_eq!(g!(secret * G), frost_poly.key()); } } } diff --git a/schnorr_fun/src/frost/frost_poly.rs b/schnorr_fun/src/frost/shared_key.rs similarity index 81% rename from schnorr_fun/src/frost/frost_poly.rs rename to schnorr_fun/src/frost/shared_key.rs index b06280ea..d380c26d 100644 --- a/schnorr_fun/src/frost/frost_poly.rs +++ b/schnorr_fun/src/frost/shared_key.rs @@ -16,14 +16,14 @@ use super::PartyIndex; derive(crate::fun::bincode::Encode), bincode(crate = "crate::fun::bincode",) )] -pub struct FrostPoly { +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, } -impl FrostPoly { +impl SharedKey { /// The verification shares of each party in the key. /// /// The verification share is the image of their secret share. @@ -45,14 +45,15 @@ impl FrostPoly { } } -impl FrostPoly { +impl SharedKey { /// The key that was shared with this polynomial defining the sharing. /// /// This is the first coefficient of the polynomial. - pub fn shared_key(&self) -> Point { + pub fn key(&self) -> Point { self.point_polynomial[0].non_zero().expect("invariant") } - /// Constructor to create a `FrostPoly` from a vector of points. + /// Constructor to create a from a vector of points where each item represent a polynomial + /// coefficient. /// /// Returns `None` if the first coefficient is [`Point::zero`]. pub fn from_poly(poly: Vec>) -> Option { @@ -70,7 +71,15 @@ impl FrostPoly { }) } - /// Create a `FrostPoly` from a set of verification shares. + /// Create a shared key from a subset of verification shares. + /// + /// If all the verification shares are correct and you have at least a threshold of them then + /// you'll get the right answer. If you put in a wrong share you won't get the right answer! + /// + /// ## Security + /// + /// ⚠ You can't just take any random 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_verification_shares( shares: &[(PartyIndex, Point)], ) -> Self { @@ -80,18 +89,18 @@ impl FrostPoly { ty: PhantomData, } } - /// Convert the key into a BIP340 FrostKey. + /// 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) -> FrostPoly { - let needs_negation = !self.shared_key().is_y_even(); + pub fn into_xonly(mut self) -> SharedKey { + let needs_negation = !self.key().is_y_even(); if needs_negation { self.homomorphic_negate(); - debug_assert!(self.shared_key().is_y_even()); + debug_assert!(self.key().is_y_even()); } - FrostPoly { + SharedKey { point_polynomial: self.point_polynomial, ty: PhantomData, } @@ -106,7 +115,7 @@ impl FrostPoly { /// /// ## Return value /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// Returns a new [`SharedKey`] 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`. /// @@ -128,8 +137,10 @@ impl FrostPoly { /// 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). + /// In order for a [`PairedSecretShare`] to be valid against the new key they will have to apply + /// [the same operation](super::PairedSecretShare::xonly_homomorphic_mul). + /// + /// [`PairedSecretShare`]: super::PairedSecretShare pub fn homomorphic_mul(&mut self, tweak: Scalar) { for coeff in &mut self.point_polynomial { *coeff = g!(tweak * coeff.deref()).normalize(); @@ -137,7 +148,7 @@ impl FrostPoly { } } -impl FrostPoly { +impl SharedKey { /// Applies an "XOnly" tweak to the FROST public key. /// This is how you embed a taproot commitment into a frost public key /// @@ -147,7 +158,7 @@ impl FrostPoly { /// /// ## Return value /// - /// Returns a new [`FrostKey`] with the same parties but a different frost public key. + /// Returns a new [`SharedKey`] 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 xonly_homomorphic_add( @@ -167,7 +178,7 @@ impl FrostPoly { /// 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). + /// [the same operation](super::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] @@ -184,7 +195,7 @@ impl FrostPoly { } /// The public key that would have signatures verified against for this shared key. - pub fn shared_key(&self) -> Point { + pub fn key(&self) -> Point { let (even_y_point, _needs_negation) = self.point_polynomial[0] .non_zero() .expect("invariant") @@ -195,7 +206,7 @@ impl FrostPoly { } #[cfg(feature = "bincode")] -impl crate::fun::bincode::Decode for FrostPoly { +impl crate::fun::bincode::Decode for SharedKey { fn decode( decoder: &mut D, ) -> Result { @@ -207,7 +218,7 @@ impl crate::fun::bincode::Decode for FrostPoly { )); } - Ok(FrostPoly { + Ok(SharedKey { point_polynomial: poly, ty: PhantomData, }) @@ -215,7 +226,7 @@ impl crate::fun::bincode::Decode for FrostPoly { } #[cfg(feature = "serde")] -impl<'de> crate::fun::serde::Deserialize<'de> for FrostPoly { +impl<'de> crate::fun::serde::Deserialize<'de> for SharedKey { fn deserialize(deserializer: D) -> Result where D: secp256kfun::serde::Deserializer<'de>, @@ -236,4 +247,4 @@ impl<'de> crate::fun::serde::Deserialize<'de> for FrostPoly { } #[cfg(feature = "bincode")] -crate::fun::bincode::impl_borrow_decode!(FrostPoly); +crate::fun::bincode::impl_borrow_decode!(SharedKey); diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs index db0c92d9..e08f1e1f 100644 --- a/schnorr_fun/tests/frost_prop.rs +++ b/schnorr_fun/tests/frost_prop.rs @@ -64,7 +64,7 @@ proptest! { } for secret_share in &xonly_secret_shares { - assert_eq!(secret_share.shared_key(), xonly_poly.shared_key(), "shared key doesn't match"); + assert_eq!(secret_share.shared_key(), xonly_poly.key(), "shared key doesn't match"); } // use a boolean mask for which t participants are signers @@ -91,15 +91,15 @@ proptest! { let public_nonces = secret_nonces.iter().map(|(signer_index, sn)| (*signer_index, sn.public())).collect::>(); - let coord_signing_session = proto.start_coordinator_sign_session( + let coord_signing_session = proto.coordinator_sign_session( &xonly_poly, public_nonces, message ); - let party_signing_session = proto.start_party_sign_session( - xonly_poly.shared_key(), - coord_signing_session.participants(), + let party_signing_session = proto.party_sign_session( + xonly_poly.key(), + coord_signing_session.parties(), coord_signing_session.agg_binonce(), message, ); @@ -125,7 +125,7 @@ proptest! { ); assert!(proto.schnorr.verify( - &xonly_poly.shared_key(), + &xonly_poly.key(), message, &combined_sig )); 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