From 86d675da77242e22beae9b86f10517c4b75043c7 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Tue, 14 Nov 2023 20:55:07 -0800 Subject: [PATCH 1/3] Delete experimental FROST stub code We are now doing this in a separate crate with Protobuf serialization (cc @cronokirby). --- Cargo.toml | 10 +-- src/frost.rs | 155 ---------------------------------------- src/frost/keys.rs | 97 ------------------------- src/frost/keys/dkg.rs | 90 ----------------------- src/frost/traits.rs | 155 ---------------------------------------- src/lib.rs | 1 - tests/frost.rs | 161 ------------------------------------------ 7 files changed, 1 insertion(+), 668 deletions(-) delete mode 100644 src/frost.rs delete mode 100644 src/frost/keys.rs delete mode 100644 src/frost/keys/dkg.rs delete mode 100644 src/frost/traits.rs delete mode 100644 tests/frost.rs diff --git a/Cargo.toml b/Cargo.toml index c7d1e58..dad0cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "decaf377-rdsa" edition = "2021" -version = "0.8.1" +version = "0.9.0" authors = ["Penumbra Labs "] readme = "README.md" license = "MIT OR Apache-2.0" @@ -20,14 +20,6 @@ ark-serialize = "0.4" ark-ff = { version = "0.4", default-features = false } hex = "0.4" -frost-core = { git = "https://github.com/ZcashFoundation/frost/", rev = "60d9942f360e36e347513590a62cd7b850966848", features = [ - "serde", -] } -frost-rerandomized = { git = "https://github.com/ZcashFoundation/frost/", rev = "60d9942f360e36e347513590a62cd7b850966848", features = [ - "serde", -] } - - # Frost deps # TODO: make optional ? use published versions? [dev-dependencies] diff --git a/src/frost.rs b/src/frost.rs deleted file mode 100644 index bfa92c9..0000000 --- a/src/frost.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Threshold signing for `decaf377-rdsa` signatures via FROST. -//! -//! This implementation only supports producing `SpendAuth` signatures, which -//! use the conventional `decaf377` basepoint. - -use frost_core::frost; -use std::collections::HashMap; - -/// A FROST-related error. -pub type Error = frost_core::Error; - -use rand_core::{self, CryptoRng, RngCore}; - -pub mod keys; -mod traits; - -use crate::{Signature, SpendAuth}; - -// TODO: properly factor this code into leaf modules - -// Below code copied from frost-ed25519 ("MIT or Apache-2.0") - -type E = traits::Decaf377Rdsa; - -/// A FROST participant identifier. -pub type Identifier = frost::Identifier; - -/// Signing round 1 functionality and types. -pub mod round1 { - use crate::frost::keys::SigningShare; - - use super::*; - - /// The nonces used for a single FROST signing ceremony. - /// - /// Note that [`SigningNonces`] must be used *only once* for a signing - /// operation; re-using nonces will result in leakage of a signer's long-lived - /// signing key. - pub type SigningNonces = frost::round1::SigningNonces; - - /// Published by each participant in the first round of the signing protocol. - /// - /// This step can be batched if desired by the implementation. Each - /// SigningCommitment can be used for exactly *one* signature. - pub type SigningCommitments = frost::round1::SigningCommitments; - - /* - // TODO: doesn't seem like this is used directly? - /// A commitment to a signing nonce share. - pub type NonceCommitment = frost::round1::NonceCommitment; - */ - - /// Performed once by each participant selected for the signing operation. - /// - /// Generates the signing nonces and commitments to be used in the signing - /// operation. - pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) - where - RNG: CryptoRng + RngCore, - { - frost::round1::commit::(secret, rng) - } -} - -/// Generated by the coordinator of the signing operation and distributed to -/// each signing party. -pub type SigningPackage = frost::SigningPackage; - -/// Signing Round 2 functionality and types. -pub mod round2 { - use frost_rerandomized::Randomizer; - - use super::*; - - /// A FROST participant's signature share, which the Coordinator will - /// aggregate with all other signer's shares into the joint signature. - pub type SignatureShare = frost::round2::SignatureShare; - - /// Performed once by each participant selected for the signing operation. - /// - /// Receives the message to be signed and a set of signing commitments and a set - /// of randomizing commitments to be used in that signing operation, including - /// that for this participant. - /// - /// Assumes the participant has already determined which nonce corresponds with - /// the commitment that was assigned by the coordinator in the SigningPackage. - pub fn sign( - signing_package: &SigningPackage, - signer_nonces: &round1::SigningNonces, - key_package: &keys::KeyPackage, - ) -> Result { - frost::round2::sign(signing_package, signer_nonces, key_package) - } - - /// Like [`sign`], but for producing signatures with a randomized verification key. - pub fn sign_randomized( - signing_package: &SigningPackage, - signer_nonces: &round1::SigningNonces, - key_package: &keys::KeyPackage, - randomizer: crate::Fr, - ) -> Result { - frost_rerandomized::sign( - signing_package, - signer_nonces, - key_package, - Randomizer::from_scalar(randomizer), - ) - } -} - -/// Verifies each FROST participant's signature share, and if all are valid, -/// aggregates the shares into a signature to publish. -/// -/// The resulting signature is an ordinary Schnorr signature with normal -/// verification. -/// -/// This operation is performed by a coordinator that can communicate with all -/// the signing participants before publishing the final signature. The -/// coordinator can be one of the participants or a semi-trusted third party -/// (who is trusted to not perform denial of service attacks, but does not learn -/// any secret information). -/// -/// Note that because the coordinator is trusted to report misbehaving parties -/// in order to avoid publishing an invalid signature, if the coordinator -/// themselves is a signer and misbehaves, they can avoid that step. However, at -/// worst, this results in a denial of service attack due to publishing an -/// invalid signature. -pub fn aggregate( - signing_package: &SigningPackage, - signature_shares: &HashMap, - pubkeys: &keys::PublicKeyPackage, -) -> Result, Error> { - let frost_sig = frost::aggregate(signing_package, signature_shares, pubkeys)?; - Ok(frost_sig.serialize()) -} - -/// Like [`aggregate`], but for generating signatures with a randomized -/// verification key. -pub fn aggregate_randomized( - signing_package: &SigningPackage, - signature_shares: &HashMap, - pubkeys: &keys::PublicKeyPackage, - randomizer: crate::Fr, -) -> Result, Error> { - let frost_sig = frost_rerandomized::aggregate( - signing_package, - signature_shares, - pubkeys, - &frost_rerandomized::RandomizedParams::from_randomizer( - pubkeys.group_public(), - frost_rerandomized::Randomizer::from_scalar(randomizer), - ), - )?; - Ok(frost_sig.serialize()) -} diff --git a/src/frost/keys.rs b/src/frost/keys.rs deleted file mode 100644 index 9c7df31..0000000 --- a/src/frost/keys.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! FROST key shares and key generation. - -use crate::{SigningKey, SpendAuth}; - -use std::collections::HashMap; - -use rand_core::RngCore; - -use super::*; - -pub mod dkg; - -/// The identifier list to use when generating key shares. -pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, E>; - -/// Allows all participants' keys to be generated using a central, trusted -/// dealer. -pub fn generate_with_dealer( - max_signers: u16, - min_signers: u16, - identifiers: IdentifierList, - mut rng: RNG, -) -> Result<(HashMap, PublicKeyPackage), Error> { - frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) -} - -/// Splits an existing key into FROST shares. -/// -/// This is identical to [`generate_with_dealer`] but receives an existing key -/// instead of generating a fresh one. This is useful in scenarios where -/// the key needs to be generated externally or must be derived from e.g. a -/// seed phrase. -pub fn split( - secret: &SigningKey, - max_signers: u16, - min_signers: u16, - identifiers: IdentifierList, - rng: &mut R, -) -> Result<(HashMap, PublicKeyPackage), Error> { - // https://github.com/ZcashFoundation/frost/issues/497 - let frost_secret = frost_core::SigningKey::deserialize(secret.to_bytes())?; - frost::keys::split(&frost_secret, max_signers, min_signers, identifiers, rng) -} - -/// Recompute the secret from t-of-n secret shares using Lagrange interpolation. -/// -/// This can be used if for some reason the original key must be restored; e.g. -/// if threshold signing is not required anymore. -/// -/// This is NOT required to sign with FROST; the whole point of FROST is being -/// able to generate signatures only using the shares, without having to -/// reconstruct the original key. -/// -/// The caller is responsible for providing at least `min_signers` shares; -/// if less than that is provided, a different key will be returned. -pub fn reconstruct(secret_shares: &[SecretShare]) -> Result, Error> { - // https://github.com/ZcashFoundation/frost/issues/497 - let frost_secret = frost::keys::reconstruct(secret_shares)?; - Ok(SigningKey::try_from(frost_secret.serialize()).expect("serialization is valid")) -} - -/// Secret and public key material generated by a dealer performing -/// [`generate_with_dealer`]. -pub type SecretShare = frost::keys::SecretShare; - -/// A secret scalar value representing a signer's share of the group secret. -pub type SigningShare = frost::keys::SigningShare; - -/// A public group element that represents a single signer's public verification share. -pub type VerifyingShare = frost::keys::VerifyingShare; - -/// A FROST keypair, which can be generated either by a trusted dealer or using a DKG. -/// -/// When using a central dealer, [`SecretShare`]s are distributed to -/// participants, who then perform verification, before deriving -/// [`KeyPackage`]s, which they store to later use during signing. -pub type KeyPackage = frost::keys::KeyPackage; - -/// Public data that contains all the signers' public keys as well as the -/// group public key. -/// -/// Used for verification purposes before publishing a signature. -pub type PublicKeyPackage = frost::keys::PublicKeyPackage; - -/// Contains the commitments to the coefficients for our secret polynomial _f_, -/// used to generate participants' key shares. -/// -/// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which -/// themselves are scalars) for a secret polynomial f, where f is used to -/// generate each ith participant's key share f(i). Participants use this set of -/// commitments to perform verifiable secret sharing. -/// -/// Note that participants MUST be assured that they have the *same* -/// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using -/// some agreed-upon public location for publication, where each participant can -/// ensure that they received the correct (and same) value. -pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; diff --git a/src/frost/keys/dkg.rs b/src/frost/keys/dkg.rs deleted file mode 100644 index f54178e..0000000 --- a/src/frost/keys/dkg.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Distributed key generation without a trusted dealer. - -// Copied from frost-ed25519 ("MIT or Apache-2.0") - -use super::*; - -/// DKG Round 1 structures. -pub mod round1 { - use super::*; - - /// The secret package that must be kept in memory by the participant - /// between the first and second parts of the DKG protocol (round 1). - /// - /// # Security - /// - /// This package MUST NOT be sent to other participants! - pub type SecretPackage = frost::keys::dkg::round1::SecretPackage; - - /// The package that must be broadcast by each participant to all other participants - /// between the first and second parts of the DKG protocol (round 1). - pub type Package = frost::keys::dkg::round1::Package; -} - -/// DKG Round 2 structures. -pub mod round2 { - use super::*; - - /// The secret package that must be kept in memory by the participant - /// between the second and third parts of the DKG protocol (round 2). - /// - /// # Security - /// - /// This package MUST NOT be sent to other participants! - pub type SecretPackage = frost::keys::dkg::round2::SecretPackage; - - /// A package that must be sent by each participant to some other participants - /// in Round 2 of the DKG protocol. Note that there is one specific package - /// for each specific recipient, in contrast to Round 1. - /// - /// # Security - /// - /// The package must be sent on an *confidential* and *authenticated* channel. - pub type Package = frost::keys::dkg::round2::Package; -} - -/// Performs the first part of the distributed key generation protocol -/// for the given participant. -/// -/// It returns the [`round1::SecretPackage`] that must be kept in memory -/// by the participant for the other steps, and the [`round1::Package`] that -/// must be sent to other participants. -pub fn part1( - identifier: Identifier, - max_signers: u16, - min_signers: u16, - mut rng: R, -) -> Result<(round1::SecretPackage, round1::Package), Error> { - frost::keys::dkg::part1(identifier, max_signers, min_signers, &mut rng) -} - -/// Performs the second part of the distributed key generation protocol -/// for the participant holding the given [`round1::SecretPackage`], -/// given the received [`round1::Package`]s received from the other participants. -/// -/// It returns the [`round2::SecretPackage`] that must be kept in memory -/// by the participant for the final step, and the [`round2::Package`]s that -/// must be sent to other participants. -pub fn part2( - secret_package: round1::SecretPackage, - round1_packages: &HashMap, -) -> Result<(round2::SecretPackage, HashMap), Error> { - frost::keys::dkg::part2(secret_package, round1_packages) -} - -/// Performs the third and final part of the distributed key generation protocol -/// for the participant holding the given [`round2::SecretPackage`], -/// given the received [`round1::Package`]s and [`round2::Package`]s received from -/// the other participants. -/// -/// It returns the [`KeyPackage`] that has the long-lived key share for the -/// participant, and the [`PublicKeyPackage`]s that has public information -/// about all participants; both of which are required to compute FROST -/// signatures. -pub fn part3( - round2_secret_package: &round2::SecretPackage, - round1_packages: &HashMap, - round2_packages: &HashMap, -) -> Result<(KeyPackage, PublicKeyPackage), Error> { - frost::keys::dkg::part3(round2_secret_package, round1_packages, round2_packages) -} diff --git a/src/frost/traits.rs b/src/frost/traits.rs deleted file mode 100644 index 1541f2b..0000000 --- a/src/frost/traits.rs +++ /dev/null @@ -1,155 +0,0 @@ -use ark_ff::{Field as _, One, UniformRand, Zero}; - -pub use frost_core::{frost, Ciphersuite, Field, FieldError, Group, GroupError}; - -use rand_core; - -use decaf377::{Element, FieldExt, Fr}; - -use crate::{hash::HStar, SpendAuth}; - -#[derive(Copy, Clone)] -pub struct Decaf377ScalarField; - -impl Field for Decaf377ScalarField { - type Scalar = Fr; - - type Serialization = [u8; 32]; - - fn zero() -> Self::Scalar { - Fr::zero() - } - - fn one() -> Self::Scalar { - Fr::one() - } - - fn invert(scalar: &Self::Scalar) -> Result { - scalar.inverse().ok_or(FieldError::InvalidZeroScalar) - } - - fn random(rng: &mut R) -> Self::Scalar { - Fr::rand(rng) - } - - fn serialize(scalar: &Self::Scalar) -> Self::Serialization { - scalar.to_bytes() - } - - fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { - scalar.to_bytes() - } - - fn deserialize(buf: &Self::Serialization) -> Result { - Fr::from_bytes(*buf).map_err(|_| FieldError::MalformedScalar) - } -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub struct Decaf377Group; - -impl Group for Decaf377Group { - type Field = Decaf377ScalarField; - - type Element = Element; - - type Serialization = [u8; 32]; - - fn cofactor() -> ::Scalar { - Fr::one() - } - - fn identity() -> Self::Element { - Element::default() - } - - fn generator() -> Self::Element { - decaf377::basepoint() - } - - fn serialize(element: &Self::Element) -> Self::Serialization { - element.vartime_compress().0 - } - - fn deserialize(buf: &Self::Serialization) -> Result { - decaf377::Encoding(*buf) - .vartime_decompress() - .map_err(|_| GroupError::MalformedElement) - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct Decaf377Rdsa; - -const CONTEXT_STRING: &str = "FROST-decaf377-rdsa-v1"; - -#[allow(non_snake_case)] -impl Ciphersuite for Decaf377Rdsa { - const ID: &'static str = CONTEXT_STRING; - - type Group = Decaf377Group; - - type HashOutput = [u8; 32]; - - type SignatureSerialization = crate::Signature; - - fn H1(m: &[u8]) -> <::Field as Field>::Scalar { - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"rho") - .update(m) - .finalize() - } - - fn H2(m: &[u8]) -> <::Field as Field>::Scalar { - HStar::default().update(m).finalize() - } - - fn H3(m: &[u8]) -> <::Field as Field>::Scalar { - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"nonce") - .update(m) - .finalize() - } - - fn H4(m: &[u8]) -> Self::HashOutput { - // TODO: dont - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"msg") - .update(m) - .finalize() - .to_bytes() - } - - fn H5(m: &[u8]) -> Self::HashOutput { - // TODO: dont - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"com") - .update(m) - .finalize() - .to_bytes() - } - - fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some( - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"dkg") - .update(m) - .finalize(), - ) - } - - fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { - Some( - HStar::default() - .update(CONTEXT_STRING.as_bytes()) - .update(b"id") - .update(m) - .finalize(), - ) - } -} diff --git a/src/lib.rs b/src/lib.rs index c9219d4..d9a6762 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #![doc = include_str!("../README.md")] pub mod batch; -pub mod frost; mod domain; mod error; diff --git a/tests/frost.rs b/tests/frost.rs deleted file mode 100644 index fb7b992..0000000 --- a/tests/frost.rs +++ /dev/null @@ -1,161 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; - -use rand::thread_rng; - -use ark_ff::UniformRand; -use decaf377_rdsa::frost::Identifier; -use decaf377_rdsa::*; - -#[test] -fn simple_dkg_and_signing_flow() -> anyhow::Result<()> { - const T: u16 = 2; - const N: u16 = 3; - - // For convenience, store an indexable list of IDs. - let ids = (0..N) - .map(|id| Identifier::derive(&id.to_le_bytes()).unwrap()) - .collect::>(); - - // First, run a DKG with three participants - - let mut round1_secrets = HashMap::new(); - let mut round1_packages = HashMap::new(); - - for id in &ids { - let (secret, package) = frost::keys::dkg::part1(*id, N, T, thread_rng())?; - round1_secrets.insert(*id, secret); - round1_packages.insert(*id, package); - } - - // Round 1 is a broadcast, so it's enough to copy all the round 1 packages. - let mut round2_secrets = HashMap::new(); - let mut round2_packages = HashMap::new(); - - for id in &ids { - let round1_secret = round1_secrets.remove(id).unwrap(); - - let mut round1_packages_except_us = round1_packages.clone(); - round1_packages_except_us.remove(id); - - let (secret, packages) = - frost::keys::dkg::part2(round1_secret, &round1_packages_except_us)?; - round2_secrets.insert(*id, secret); - round2_packages.insert(*id, packages); - } - - // Round 2 is point-to-point (but we're faking it), so we need to - // build a map of messages received by each participant. - - let mut shares = HashMap::new(); - let mut public_key_packages = HashMap::new(); - - for id in &ids { - let mut recvd_packages = HashMap::new(); - for (other_id, its_packages) in &round2_packages { - if other_id == id { - continue; - } - recvd_packages.insert(*other_id, its_packages.get(id).unwrap().clone()); - } - - let mut round1_packages_except_us = round1_packages.clone(); - round1_packages_except_us.remove(id); - - let round2_secret = round2_secrets.remove(id).unwrap(); - let (key_package, public_key_package) = - frost::keys::dkg::part3(&round2_secret, &round1_packages_except_us, &recvd_packages)?; - - shares.insert(id, key_package); - public_key_packages.insert(*id, public_key_package); - } - - // Now try signing. - const MSG: &[u8] = b"hello world"; - - // Signing round 1 - let mut signing_commitments = BTreeMap::new(); - let mut sign_round1_nonces = HashMap::new(); - - for id in &ids { - let (nonce, commitment) = - frost::round1::commit(&shares[id].secret_share(), &mut thread_rng()); - signing_commitments.insert(*id, commitment); - sign_round1_nonces.insert(*id, nonce); - } - - let signing_package = frost::SigningPackage::new(signing_commitments, MSG); - - let mut sign_round2_signature_shares = HashMap::new(); - - for id in &ids { - let share = frost::round2::sign( - &signing_package, - &sign_round1_nonces.get(id).unwrap(), - &shares.get(id).unwrap(), - )?; - sign_round2_signature_shares.insert(*id, share); - } - - // Aggregate the signature shares - - let signature = frost::aggregate( - &signing_package, - &sign_round2_signature_shares, - public_key_packages.values().next().unwrap(), - )?; - - let vk_bytes = public_key_packages - .values() - .next() - .unwrap() - .group_public() - .serialize(); - let vk = VerificationKey::::try_from(vk_bytes)?; - - // Verify the signature - vk.verify(MSG, &signature)?; - - // Now try randomized signing. - - let mut signing_commitments = BTreeMap::new(); - let mut sign_round1_nonces = HashMap::new(); - - for id in &ids { - let (nonce, commitment) = - frost::round1::commit(&shares[id].secret_share(), &mut thread_rng()); - signing_commitments.insert(*id, commitment); - sign_round1_nonces.insert(*id, nonce); - } - - let r = Fr::rand(&mut thread_rng()); - - let signing_package = frost::SigningPackage::new(signing_commitments, MSG); - - let mut sign_round2_signature_shares = HashMap::new(); - - for id in &ids { - let share = frost::round2::sign_randomized( - &signing_package, - &sign_round1_nonces.get(id).unwrap(), - &shares.get(id).unwrap(), - r.clone(), - )?; - sign_round2_signature_shares.insert(*id, share); - } - - // Aggregate the signature shares - - let signature = frost::aggregate_randomized( - &signing_package, - &sign_round2_signature_shares, - public_key_packages.values().next().unwrap(), - r.clone(), - )?; - - // Use r to randomize the verification key independently of FROST code - let r_vk = vk.randomize(&r); - // ... and verify with the (externally) randomized key - r_vk.verify(MSG, &signature)?; - - Ok(()) -} From f47c63d3e9d029df4b2becd814be7c8a8f9d5838 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Tue, 14 Nov 2023 20:55:40 -0800 Subject: [PATCH 2/3] Add Serde traits to marker types. --- src/domain.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/domain.rs b/src/domain.rs index 1f4d072..618ca04 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -11,11 +11,13 @@ pub trait Domain: private::Sealed {} /// A type variable corresponding to Zcash's `BindingSig`. #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Binding {} impl Domain for Binding {} /// A type variable corresponding to Zcash's `SpendAuthSig`. #[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum SpendAuth {} impl Domain for SpendAuth {} From e546e8bec4cbf20f36afbb4e5d87d008d683a7a1 Mon Sep 17 00:00:00 2001 From: Henry de Valence Date: Tue, 14 Nov 2023 20:57:46 -0800 Subject: [PATCH 3/3] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 803eaab..de1cc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Entries are listed in reverse chronological order. +# 0.9.0 + +* Delete experimental FROST support (will move to another crate). +* Add missing Serde traits on Domain marker types. + # 0.8.1 * Improve nonce generation and add `sign_deterministic`.