From 1b6a720c8d00fc19c531756de025c35422a18948 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Wed, 3 Jul 2024 14:12:33 +1000 Subject: [PATCH] [frost] Convert ShareBackup to SecretShare Having a concept of a secret share seemed to be useful generally. This makes the share backup feature less boxed off. Also we now embed the share index in the human readable part when we can. --- schnorr_fun/Cargo.toml | 2 +- schnorr_fun/src/frost.rs | 38 ++-- schnorr_fun/src/frost/share.rs | 297 ++++++++++++++++++++++++++++++ schnorr_fun/src/lib.rs | 4 - schnorr_fun/src/share_backup.rs | 241 ------------------------ schnorr_fun/tests/frost_prop.rs | 103 ----------- schnorr_fun/tests/share_backup.rs | 96 ---------- 7 files changed, 320 insertions(+), 461 deletions(-) create mode 100644 schnorr_fun/src/frost/share.rs delete mode 100644 schnorr_fun/tests/frost_prop.rs delete mode 100644 schnorr_fun/tests/share_backup.rs 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 1e8cb6c5..94c23e36 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); -}