diff --git a/schnorr_fun/Cargo.toml b/schnorr_fun/Cargo.toml index 76699103..e4be82c6 100644 --- a/schnorr_fun/Cargo.toml +++ b/schnorr_fun/Cargo.toml @@ -15,7 +15,7 @@ keywords = ["bitcoin", "schnorr"] [dependencies] secp256kfun = { path = "../secp256kfun", version = "0.10", default-features = false } -bech32 = { version = "0.9", optional = true, default-features = false } +bech32 = { version = "0.11", optional = true, default-features = false, features = ["alloc"] } [dev-dependencies] secp256kfun = { path = "../secp256kfun", version = "0.10", features = ["proptest"] } diff --git a/schnorr_fun/src/frost.rs b/schnorr_fun/src/frost.rs index 7629815d..d552281e 100644 --- a/schnorr_fun/src/frost.rs +++ b/schnorr_fun/src/frost.rs @@ -90,7 +90,7 @@ //! // Generate nonces for this signing session. //! // ⚠️ 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, session_id); +//! let mut nonce_rng: ChaCha20Rng = frost.seed_nonce_rng(&xonly_frost_key, &my_secret_share.secret, 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 @@ -99,8 +99,8 @@ //! // start a sign session with these nonces for a message //! let session = frost.start_sign_session(&xonly_frost_key, nonces, message); //! // create a partial signature using our secret share and secret nonce -//! let my_sig_share = frost.sign(&xonly_frost_key, &session, my_index, &my_secret_share, my_nonce); -//! # let sig_share3 = frost.sign(&xonly_frost_key, &session, party_index3, &secret_share3, nonce3); +//! 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 @@ -174,11 +174,14 @@ //! [Security of Multi- and Threshold Signatures]: //! [`musig`]: crate::musig //! [`Scalar`]: crate::fun::Scalar -use core::num::NonZeroU32; + +mod share; +pub use share::*; pub use crate::binonce::{Nonce, NonceKeyPair}; use crate::{Message, Schnorr, Signature}; use alloc::{collections::BTreeMap, vec::Vec}; +use core::num::NonZeroU32; use secp256kfun::{ derive_nonce_rng, digest::{generic_array::typenum::U32, Digest}, @@ -199,9 +202,6 @@ use secp256kfun::{ /// 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. -/// See [`share_backup`] for backup sizes. -/// -/// [`share_backup`]: crate::share_backup pub type PartyIndex = Scalar; /// The FROST context. @@ -591,7 +591,7 @@ impl + Clone, NG: NonceGen> Frost { threshold: usize, n_parties: usize, rng: &mut impl RngCore, - ) -> (FrostKey, BTreeMap>) { + ) -> (FrostKey, Vec) { let scalar_polys = (0..n_parties) .map(|i| { ( @@ -649,7 +649,7 @@ impl + Clone, NG: NonceGen> Frost { .unwrap(); frost_key = Some(_frost_key); - (party_index, secret_share) + secret_share }) .collect(); @@ -793,7 +793,7 @@ impl + Clone, NG> Frost { my_index: PartyIndex, secret_shares: BTreeMap, Signature)>, proof_of_possession_msg: Message, - ) -> Result<(Scalar, FrostKey), FinishKeyGenError> { + ) -> Result<(SecretShare, FrostKey), FinishKeyGenError> { let mut total_secret_share = s!(0); for (party_index, poly) in &keygen.point_polys { @@ -816,7 +816,13 @@ impl + Clone, NG> Frost { total_secret_share += secret_share; } - Ok((total_secret_share, keygen.frost_key)) + Ok(( + SecretShare { + index: my_index, + secret: total_secret_share, + }, + keygen.frost_key, + )) } /// Start a FROST signing session. @@ -890,15 +896,15 @@ impl + Clone, NG> Frost { &self, frost_key: &FrostKey, session: &SignSession, - my_index: PartyIndex, - secret_share: &Scalar, + secret_share: &SecretShare, secret_nonce: NonceKeyPair, ) -> Scalar { - let mut lambda = poly::eval_basis_poly_at_0(my_index, session.nonces.keys().cloned()); + let mut lambda = + poly::eval_basis_poly_at_0(secret_share.index, session.nonces.keys().cloned()); assert_eq!( *session .nonces - .get(&my_index) + .get(&secret_share.index) .expect("my_index was not in session"), secret_nonce.public(), "secret nonce didn't match previously provided public nonce" @@ -909,7 +915,7 @@ impl + Clone, NG> Frost { r2.conditional_negate(session.nonces_need_negation); let b = &session.binding_coeff; - let x = secret_share; + let x = &secret_share.secret; let c = &session.challenge; s!(r1 + (r2 * b) + lambda * x * c).public() } diff --git a/schnorr_fun/src/frost/share.rs b/schnorr_fun/src/frost/share.rs new file mode 100644 index 00000000..7bf1ffac --- /dev/null +++ b/schnorr_fun/src/frost/share.rs @@ -0,0 +1,297 @@ +use secp256kfun::{marker::*, poly, Scalar}; +/// A *[Shamir secret share]*. +/// +/// Each share is an `(x,y)` pair where `y = p(x)` for some polynomial `p`. With a sufficient +/// number of unique pairs you can recontruct `p` as a vector of `Scalar`s where each `Scalar` is a +/// coefficient `p`. This structure is useful for hiding a secret `y*` by having `p(0) = y*` until a +/// sufficient number of shares come together to resconstruct `y*`. +/// +/// Signing keys in FROST are also shamir secert shares which is why this is here. +/// +/// ## Backup format (bech32 chars) +/// +/// We decided to encode each share as a [`bech32m`] string in order to back them up. There are two +/// forms, one where the share index goes in the human readable part and one where that goes into +/// the payload. +/// +/// We optionally have the index in the human readable part since users can more easily identify +/// shares that way. Share identification can help for keeping track of them and distinguishing them +/// there are only a small numbner of shares. +/// +/// The backuip format is enabled with the `share_backup` feature and accessed with the `FromStr` +/// and `Display`. +/// +/// ### Index in human readable part +/// +/// human readable: `"frost[]"` // (8+) +/// separator: `"1"` // (1) +/// payload: `[u5; 53]`, // (53) +/// checksum: `[u5; 6]`, // (6) +/// +/// The payload consists of: +/// +/// - `secret_share` (`[u8;32]`): the 32 bytes that reperesents the secret share scalar in big-endian encoding +/// +/// ### Index in payload +/// +/// human readable: "frost" // (5) +/// separator: "1" // (1) +/// payload: [u5; 53..103], // (53..103) +/// checksum: [u5; 6], // (6) +/// +/// The payload consists of: +/// +/// - `secret_share` (`[u8;32]`): the 32 bytes that reperesents the secret share scalar in big-endian encoding +/// - `share_index`: [u8;1..32] which is the index where the polynomial was evalulated to create the share. This is also a big-endian scalar except that the leading zero bytes are dropped so the smaller the index the smaller the encoding. +/// +/// [Shamir secret share]: https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing +/// [`bech32m`]: https://bips.xyz/350 + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +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, + /// The secret scalar which is the output of the polynomial evaluated at `index` + pub secret: Scalar, +} + +impl SecretShare { + /// From (at least) a threshold number of backups, restores the shared secret. + pub fn recover_secret(shares: &[SecretShare]) -> Scalar { + let index_and_secret = shares + .iter() + .map(|share| (share.index, share.secret)) + .collect::>(); + + poly::scalar::interpolate_and_eval_poly_at_0(&index_and_secret[..]) + } +} + +#[cfg(feature = "share_backup")] +mod share_backup { + use super::*; + use bech32::{primitives::decode::CheckedHrpstring, Bech32m, ByteIterExt, Fe32IterExt, Hrp}; + use core::{fmt, str::FromStr}; + + /// the threshold under which we encode the share index in the human readable section. + const HUMAN_READABLE_THRESHOLD: u32 = 1000; + + impl fmt::Display for SecretShare { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut share_index_bytes = None; + let hrp = if self.index < Scalar::::from(HUMAN_READABLE_THRESHOLD) { + let bytes = self.index.to_bytes(); + let mut u32_index_bytes = [0u8; 4]; + u32_index_bytes.copy_from_slice(&bytes[28..]); + let u32_index = u32::from_be_bytes(u32_index_bytes); + Hrp::parse(&format!("frost[{}]", u32_index)).unwrap() + } else { + share_index_bytes = Some( + self.index + .to_bytes() + .into_iter() + .skip_while(|byte| *byte == 0x00), + ); + Hrp::parse("frost").unwrap() + }; + + let chars = self + .secret + .to_bytes() + .into_iter() + .chain(share_index_bytes.into_iter().flatten()) + .bytes_to_fes() + .with_checksum::(&hrp) + .chars(); + + for c in chars { + write!(f, "{}", c)?; + } + Ok(()) + } + } + + impl FromStr for SecretShare { + type Err = ShareDecodeError; + fn from_str(encoded: &str) -> Result { + let checked_hrpstring = &CheckedHrpstring::new::(encoded) + .map_err(ShareDecodeError::Bech32DecodeError)?; + let hrp = checked_hrpstring.hrp(); + + let tail = hrp + .as_str() + .strip_prefix("frost") + .ok_or(ShareDecodeError::InvalidHumanReadablePrefix)?; + + let has_parenthetical = !tail.is_empty(); + dbg!(has_parenthetical); + let hr_index = if has_parenthetical { + let tail = tail + .strip_prefix('[') + .ok_or(ShareDecodeError::InvalidHumanReadablePrefix)?; + let tail = tail + .strip_suffix(']') + .ok_or(ShareDecodeError::InvalidHumanReadablePrefix)?; + let u32_scalar = u32::from_str(tail) + .map_err(|_| ShareDecodeError::InvalidHumanReadablePrefix)?; + + Some(Scalar::::from(u32_scalar)) + } else { + None + }; + + let mut byte_iter = checked_hrpstring.byte_iter(); + let mut secret_share = [0u8; 32]; + for byte in &mut secret_share { + *byte = byte_iter + .next() + .ok_or(ShareDecodeError::InvalidSecretShareScalar)?; + } + + let secret_share = Scalar::from_bytes(secret_share) + .ok_or(ShareDecodeError::InvalidSecretShareScalar)?; + + let share_index = match hr_index { + Some(share_index) => share_index, + None => { + let mut share_index = [0u8; 32]; + let mut i = 0; + for byte in byte_iter { + if i >= 32 { + return Err(ShareDecodeError::InvalidShareIndexScalar); + } + share_index[i] = byte; + i += 1; + } + + if i == 0 { + return Err(ShareDecodeError::InvalidShareIndexScalar)?; + } + share_index.rotate_right(32 - i); + Scalar::::from_bytes(share_index) + .ok_or(ShareDecodeError::InvalidShareIndexScalar)? + } + }; + + let share_index = share_index + .public() + .non_zero() + .ok_or(ShareDecodeError::InvalidShareIndexScalar)?; + + Ok(SecretShare { + secret: secret_share, + index: share_index, + }) + } + } + + /// An error encountered when encoding a Frostsnap backup. + #[derive(Debug, Clone, PartialEq)] + pub enum ShareDecodeError { + /// Decode error from bech32 library + Bech32DecodeError(bech32::primitives::decode::CheckedHrpstringError), + /// Decoded secret share is not a valid secp256k1 scalar + InvalidSecretShareScalar, + /// Decoded share index is not a valid secp256k1 scalar + InvalidShareIndexScalar, + /// Tried to decode backup with unknown prefix + InvalidHumanReadablePrefix, + } + + #[cfg(feature = "std")] + impl std::error::Error for ShareDecodeError {} + + impl fmt::Display for ShareDecodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + ShareDecodeError::Bech32DecodeError(e) => { + write!(f, "Failed to decode bech32m string: {e}") + } + ShareDecodeError::InvalidSecretShareScalar => { + write!( + f, + "Invalid secret share scalar value, not on secp256k1 curve." + ) + } + ShareDecodeError::InvalidHumanReadablePrefix => { + write!(f, "Expected human readable prefix `frost`",) + } + ShareDecodeError::InvalidShareIndexScalar => { + write!(f, "Share index scalar was not a valid secp256k1 scalar.",) + } + } + } + } + + #[cfg(test)] + mod test { + use super::*; + use crate::frost::SecretShare; + use alloc::string::ToString; + use core::str::FromStr; + use secp256kfun::{proptest::prelude::*, Scalar}; + + proptest! { + #[test] + fn share_backup_roundtrip(index in any::>(), secret in any::>()) { + let orig = SecretShare { secret, index }; + let orig_encoded = orig.to_string(); + let decoded = SecretShare::from_str(&orig_encoded).unwrap(); + assert_eq!(orig, decoded) + } + + + #[test] + fn short_backup_length(secret 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, + }; + let backup = secret_share + .to_string(); + + if share_index_u32 >= HUMAN_READABLE_THRESHOLD { + assert!(backup.starts_with("frost1")); + } else { + assert!(backup.starts_with(&format!("frost[{}]", share_index_u32))); + } + + assert_eq!(SecretShare::from_str(&backup), Ok(secret_share)) + } + } + } +} + +#[cfg(feature = "share_backup")] +pub use share_backup::ShareDecodeError; + +#[cfg(test)] +mod test { + use super::*; + use crate::frost; + use alloc::vec::Vec; + use secp256kfun::{ + g, + proptest::{ + prelude::*, + test_runner::{RngAlgorithm, TestRng}, + }, + G, + }; + proptest! { + #[test] + fn recover_secret(parties in 1usize..10, threshold in 1usize..5) { + use rand::seq::SliceRandom; + let frost = frost::new_with_deterministic_nonces::(); + let parties = parties.max(threshold); + + 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 secret = SecretShare::recover_secret(&chosen); + prop_assert_eq!(g!(secret * G), frost_key.public_key()); + } + } +} diff --git a/schnorr_fun/src/lib.rs b/schnorr_fun/src/lib.rs index 11f869ff..cfd4bd4b 100755 --- a/schnorr_fun/src/lib.rs +++ b/schnorr_fun/src/lib.rs @@ -26,10 +26,6 @@ pub mod musig; #[cfg(feature = "alloc")] pub mod frost; -/// bech32m secret share backup scheme -#[cfg(feature = "share_backup")] -pub mod share_backup; - mod signature; pub use signature::Signature; pub mod adaptor; diff --git a/schnorr_fun/src/share_backup.rs b/schnorr_fun/src/share_backup.rs index 65426616..8b137891 100644 --- a/schnorr_fun/src/share_backup.rs +++ b/schnorr_fun/src/share_backup.rs @@ -1,242 +1 @@ -//! Backup scheme for Shamir Secret Shares -//! -//! # Description -//! -//! Based on -//! -//! ## Backup format (bech32 chars) -//! -//! human readable: "frost" // (5) -//! separator: "1" // (1) -//! secret_share: [u5; 52], // (52) -//! share_index: u5 or [u5; 52], // (1 or 52) -//! checksum: [u5; 6], // (6) -//! -//! The total length of the backup is 65 characters if using 'small' share indicies (1, 2, ..., 32), -//! or 116 bech32m characters if using general scalars for participant indicies. -//! -//! ## Rationale -//! -//! ### Human Readable - (5 bech32 characters) -//! -//! `frost` looks cool. -//! Most of the time we will be using this backup scheme for FROST related shamir secret shares. -//! -//! ### Secret Share - (52) -//! -//! A secret share scalar is fixed length scalar of 32 bytes. -//! This is 32 * 8 / 5 = 51.2 -> 52 bech32 characters -//! -//! ### Share Index - (1 or 52) -//! -//! Can be any scalar, but will often be a small integer for simplicity and smaller backups. -//! By leaving this data piece at the end, we can use the length of the remaining data to -//! easily decode either a single bech32 char into integer, or 52 chars into a 32 byte scalar. -use alloc::{fmt, string::String, vec::Vec}; -use bech32::{u5, FromBase32, ToBase32, Variant::Bech32m}; -use core::str::FromStr; -use secp256kfun::{ - digest::{generic_array::typenum::U32, Digest}, - g, - hash::HashAdd, - marker::*, - poly, Point, Scalar, G, -}; - -/// Create an identifier that's used to determine compatibility of shamir secret shares. -/// The first 4 bech32 chars from a hash of the polynomial coefficients. -/// Collision expected once in (32)^4 = 2^20. -pub fn polynomial_identifier>( - polynomial: &[Point], -) -> [u8; 4] { - let hash = H::default(); - hash.add(polynomial).finalize()[0..4] - .try_into() - .expect("4 chars must fit 4 character arry") -} - -/// A Shamir Share Backup -/// -/// Can be encoded and displayed as bech32m characters. -/// -/// If using 'small' share indices (1, 2, ..., 32) the encoded backup will be 71 bech32 characters. -/// If using general scalar participant indicies, the encoded backup will be 122 bech32 characters. -/// -/// Encoding requires that the threshold is no greater than 1024. -#[derive(Debug, Copy, Clone)] -pub struct ShareBackup { - /// The Shamir Secret Share that is being backed up. - pub secret_share: Scalar, - /// The scalar index for this secret share, generally a simple participant index (1, 2, ..., 32). - pub share_index: Scalar, -} - -impl ShareBackup { - /// Create a new share backup. - /// - /// ## Panics - /// - /// The threshold must be greater than 0 and less than 1024. - /// The secret share is checked to confirm that its image lies on the public point polynomial. - pub fn new>( - polynomial: &[Point], - secret_share: Scalar, - share_index: Scalar, - ) -> Self { - let threshold = polynomial.len() as u16; - assert_ne!(threshold, 0, "Polynomial threshold can not be zero"); - assert!(threshold <= 32, "PPolynomial has too high of a threshold"); - - assert_eq!( - poly::point::eval(polynomial, share_index), - g!(secret_share * G), - "Secret share is not valid with respect to the polynomial!" - ); - - Self { - secret_share, - share_index, - } - } -} - -impl fmt::Display for ShareBackup { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut data = [u5::default(); 52 + 52]; - - let secret_share_u5 = self.secret_share.to_bytes().to_vec().to_base32(); - for (i, byte) in secret_share_u5.into_iter().enumerate() { - data[i] = byte; - } - - let is_small = self.share_index.to_bytes()[0..31].iter().all(|b| *b == 0) - && self.share_index.to_bytes()[31] < 32; - - let n_unused_bytes = if is_small { - let share_index_u5 = - u5::try_from_u8(self.share_index.to_bytes()[31]).expect("must be less than 32"); - data[52] = share_index_u5; - 52 - 1 - } else { - let share_index_u5 = self.share_index.to_bytes().to_base32(); - for (i, byte) in share_index_u5.iter().enumerate() { - data[52 + i] = *byte; - } - 0 - }; - - bech32::encode_to_fmt(f, "frost", &data[..(data.len() - n_unused_bytes)], Bech32m) - .expect("hrp is valid and is standard") - } -} - -impl FromStr for ShareBackup { - type Err = FrostBackupDecodeError; - fn from_str(encoded: &str) -> Result { - let (hrp, data, variant) = - bech32::decode(encoded).map_err(FrostBackupDecodeError::Bech32DecodeError)?; - - if hrp != "frost" { - return Err(FrostBackupDecodeError::InvalidHumanReadablePrefix); - } - - if !matches!(variant, bech32::Variant::Bech32m) { - return Err(FrostBackupDecodeError::WrongBech32Variant(variant)); - } - - let secret_share = Scalar::from_bytes( - Vec::::from_base32(&data[..52]) - .map_err(FrostBackupDecodeError::Bech32DecodeError)? - .try_into() - .expect("52 bech32 chars corresponds to 32 bytes"), - ) - .ok_or(FrostBackupDecodeError::InvalidSecretShareScalar)?; - - let share_index = if data[52..].len() == 52 { - Scalar::from_bytes( - Vec::::from_base32(&data[52..]) - .map_err(FrostBackupDecodeError::Bech32DecodeError)? - .try_into() - .expect("remaining 52 bech32 chars corresponds to 32 bytes"), - ) - .ok_or(FrostBackupDecodeError::InvalidShareIndexScalar)? - .non_zero() - .ok_or(FrostBackupDecodeError::ShareIndexIsZero)? - } else if data[52..].len() == 1 { - Scalar::from_non_zero_u32( - core::num::NonZeroU32::new(data[52].to_u8() as u32) - .ok_or(FrostBackupDecodeError::ShareIndexIsZero)?, - ) - .public() - } else { - return Err(FrostBackupDecodeError::UnknownShareIndexLength); - }; - - Ok(ShareBackup { - secret_share, - share_index, - }) - } -} - -/// Decode a bech32m secret share backup -pub fn decode_backup(encoded: String) -> Result { - ShareBackup::from_str(&encoded) -} - -/// An error encountered when encoding a Frostsnap backup. -#[derive(Debug, Copy, Clone)] -pub enum FrostBackupDecodeError { - /// Decode error from bech32 library - Bech32DecodeError(bech32::Error), - /// Tried to decode a bech32 variant that was not bech32m - WrongBech32Variant(bech32::Variant), - /// Decoded secret share is not a valid secp256k1 scalar - InvalidSecretShareScalar, - /// Tried to decode backup with unknown prefix - InvalidHumanReadablePrefix, - /// The share index data length doesn't match expected for SmallIndex nor Scalar - UnknownShareIndexLength, - /// Decoded share index is not a valid secp256k1 scalar - InvalidShareIndexScalar, - /// Decoded share index is zero - ShareIndexIsZero, -} - -#[cfg(feature = "std")] -impl std::error::Error for FrostBackupDecodeError {} - -impl fmt::Display for FrostBackupDecodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self { - FrostBackupDecodeError::Bech32DecodeError(e) => { - write!(f, "Failed to decode bech32m string: {e}") - } - FrostBackupDecodeError::WrongBech32Variant(e) => { - write!(f, "Expected bech32m but decoded {e:?}") - } - FrostBackupDecodeError::InvalidSecretShareScalar => { - write!( - f, - "Invalid secret share scalar value, not on secp256k1 curve." - ) - } - FrostBackupDecodeError::InvalidHumanReadablePrefix => { - write!(f, "Expected human readable prefix `frost`",) - } - FrostBackupDecodeError::UnknownShareIndexLength => { - write!(f, "Expected share index of 1 character or 52 characters.",) - } - FrostBackupDecodeError::InvalidShareIndexScalar => { - write!(f, "Share index scalar was not a valid secp256k1 scalar.",) - } - FrostBackupDecodeError::ShareIndexIsZero => { - write!( - f, - "Can not have a share index of zero since this immediately implies the secret.", - ) - } - } - } -} diff --git a/schnorr_fun/tests/frost_prop.rs b/schnorr_fun/tests/frost_prop.rs deleted file mode 100644 index 15b9058d..00000000 --- a/schnorr_fun/tests/frost_prop.rs +++ /dev/null @@ -1,103 +0,0 @@ -#![cfg(feature = "alloc")] -use rand::seq::SliceRandom; -use rand_chacha::ChaCha20Rng; -use schnorr_fun::{ - frost::*, - fun::{marker::*, Scalar}, - Message, -}; -use secp256kfun::proptest::{ - arbitrary::any, - option, proptest, - strategy::{Just, Strategy}, - test_runner::{RngAlgorithm, TestRng}, -}; -use sha2::Sha256; -use std::collections::BTreeMap; - -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::>()) - ) { - 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); - - if let Some(tweak) = plain_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); - } - - let mut frost_key = frost_key.into_xonly_key(); - - if let Some(tweak) = xonly_tweak { - frost_key = frost_key.tweak(tweak).unwrap(); - } - - // use a boolean mask for which t participants are signers - let mut signer_mask = vec![true; threshold]; - signer_mask.append(&mut vec![false; n_parties - threshold]); - // 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) - .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(|(signer_index, secret_share)| { - (*signer_index, proto.gen_nonce::( - &mut proto.seed_nonce_rng( - &frost_key, - 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, - public_nonces, - message - ); - - let mut signatures = vec![]; - for (signer_index, secret_share) in secret_shares { - let sig = proto.sign( - &frost_key, - &signing_session, - signer_index, - &secret_share, - secret_nonces.remove(&signer_index).unwrap() - ); - assert!(proto.verify_signature_share( - &frost_key, - &signing_session, - signer_index, - sig) - ); - signatures.push(sig); - } - let combined_sig = proto.combine_signature_shares( - &frost_key, - &signing_session, - signatures); - - assert!(proto.schnorr.verify( - &frost_key.public_key(), - message, - &combined_sig - )); - } -} diff --git a/schnorr_fun/tests/share_backup.rs b/schnorr_fun/tests/share_backup.rs deleted file mode 100644 index 03f5974b..00000000 --- a/schnorr_fun/tests/share_backup.rs +++ /dev/null @@ -1,96 +0,0 @@ -#![cfg(feature = "share_backup")] -use core::str::FromStr; -use schnorr_fun::share_backup::{decode_backup, ShareBackup}; -use secp256kfun::{marker::*, poly, s, Scalar}; -use std::dbg; - -#[test] -fn short_backup() { - let secret_poly = vec![Scalar::::from_str( - "91dbab9f62660e95258480d2f2cff6dcfdb513f28a85fa4fb55ee993a5b46809", - ) - .unwrap()]; - let polynomial: Vec<_> = poly::scalar::to_point_poly(&secret_poly); - let share_index = s!(12).public(); - let secret_share = poly::scalar::eval(&secret_poly, share_index); - - let share_backup = ShareBackup::new::(&polynomial, secret_share, share_index); - let share_backup_bech32 = format!("{}", share_backup); - - dbg!(&share_backup_bech32); - let decoded_share_backup = decode_backup(share_backup_bech32).unwrap(); - - assert_eq!(secret_share, decoded_share_backup.secret_share); - assert_eq!(share_index, decoded_share_backup.share_index); -} - -#[test] -fn long_backup() { - let secret_poly = vec![s!(1), s!(2), s!(3)]; - let polynomial: Vec<_> = poly::scalar::to_point_poly(&secret_poly); - let share_index = Scalar::::from_str( - "34f7ce653cfa8454b3463726a599ef2925736442d2d06455974d6feae9450d90", - ) - .unwrap() - .public(); - let secret_share = poly::scalar::eval(&secret_poly, share_index); - - let share_backup = ShareBackup::new::(&polynomial, secret_share, share_index); - let share_backup_bech32 = format!("{}", share_backup); - dbg!(&share_backup_bech32); - - let decoded_share_backup = decode_backup(share_backup_bech32).unwrap(); - - assert_eq!(secret_share, decoded_share_backup.secret_share); - assert_eq!(share_index, decoded_share_backup.share_index); -} - -#[test] -#[should_panic(expected = "too high of a threshold")] -fn threshold_too_high() { - let secret_poly: Vec = (0..33).map(|_| s!(1)).collect(); - let polynomial: Vec<_> = poly::scalar::to_point_poly(&secret_poly); - let share_index = Scalar::::from_str( - "91dbab9f62660e95258480d2f2cff6dcfdb513f28a85fa4fb55ee993a5b46809", - ) - .unwrap() - .public(); - let secret_share = poly::scalar::eval(&secret_poly, share_index); - ShareBackup::new::(&polynomial, secret_share, share_index); -} - -#[test] -#[should_panic(expected = "threshold can not be zero")] -fn threshold_zero() { - let secret_poly: Vec = vec![]; - let polynomial: Vec<_> = poly::scalar::to_point_poly(&secret_poly); - let share_index = Scalar::::from_str( - "000000000000000000000000000000000000000000000066726F7374736E6170", - ) - .unwrap() - .public(); - let secret_share = Scalar::::from_str( - "00000000000000000000000000656C656374726F6E696320707972616D696473", - ) - .unwrap(); - - ShareBackup::new::(&polynomial, secret_share, share_index); -} - -#[test] -#[should_panic(expected = "Secret share is not valid with respect to the polynomial")] -fn share_not_on_poly() { - let secret_poly: Vec = vec![s!(1), s!(2), s!(3)]; - let polynomial: Vec<_> = poly::scalar::to_point_poly(&secret_poly); - let share_index = Scalar::::from_str( - "00000000000000000000000000000000000000000000006672656520726F7373", - ) - .unwrap() - .public(); - let secret_share = Scalar::::from_str( - "0000000000000000626974636F696E2068616C76696E6720746F6F2066617374", - ) - .unwrap(); - - ShareBackup::new::(&polynomial, secret_share, share_index); -}