Skip to main content

frost_bluepallas/
lib.rs

1//! FROST BluePallas ciphersuite implementation for the Mina protocol.
2//! This library uses the mina_hasher crate for the Poseidon hash function and mina_curves for the Pallas curve implementation,
3//! and implements the FROST signature scheme as specified in the FROST paper and the Mina protocol specifications.
4#![warn(rustdoc::broken_intra_doc_links)]
5#![warn(rustdoc::bare_urls)]
6#![no_std]
7
8extern crate alloc;
9
10use alloc::{borrow::Cow, collections::BTreeMap};
11use core::marker::PhantomData;
12
13use ark_ec::{models::CurveConfig, CurveGroup, PrimeGroup};
14
15use ark_ff::{fields::Field as ArkField, UniformRand};
16use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
17pub use frost_core::{self as frost, Ciphersuite, Field, FieldError, Group, GroupError};
18use frost_core::{compute_group_commitment, BindingFactorList};
19use mina_curves::pasta::{PallasParameters, ProjectivePallas};
20
21use num_traits::identities::Zero;
22use rand_core::{CryptoRng, RngCore};
23
24pub type Error<M> = frost_core::Error<BluePallas<M>>;
25
26use crate::{
27    hasher::{hash_to_array, hash_to_scalar},
28    negate::NegateY,
29};
30
31pub mod errors;
32pub mod hasher;
33pub mod keys;
34mod negate;
35pub mod signing_utilities;
36
37/// Message contract required by the BluePallas challenge logic.
38pub trait ChallengeMessage:
39    Clone + core::fmt::Debug + PartialEq + Eq + Sized + Send + Sync + 'static
40{
41    fn challenge(
42        r: &frost_core::Element<BluePallas<Self>>,
43        verifying_key: &frost_core::VerifyingKey<BluePallas<Self>>,
44        message: &[u8],
45    ) -> Result<frost_core::Challenge<BluePallas<Self>>, frost_core::Error<BluePallas<Self>>>;
46}
47
48pub const GROUP_SIZE: usize = 33; // Size of group elements in bytes (compressed)
49pub const FIELD_SIZE: usize = 32; // Size of field elements in bytes (compressed)
50
51/// PallasScalarField implements the FROST field interface for the Pallas scalar field
52#[derive(Clone, Copy)]
53pub struct PallasScalarField;
54
55impl Field for PallasScalarField {
56    // Equivalent to Fq in mina::curves::pasta i.e. the scalar field of the Pallas curve
57    type Scalar = <PallasParameters as CurveConfig>::ScalarField;
58    type Serialization = [u8; FIELD_SIZE];
59    fn zero() -> Self::Scalar {
60        Self::Scalar::zero()
61    }
62    fn one() -> Self::Scalar {
63        Self::Scalar::ONE
64    }
65    fn invert(scalar: &Self::Scalar) -> Result<Self::Scalar, FieldError> {
66        <Self::Scalar as ArkField>::inverse(scalar).ok_or(FieldError::InvalidZeroScalar)
67    }
68    fn random<R: RngCore + CryptoRng>(rng: &mut R) -> Self::Scalar {
69        Self::Scalar::rand(rng)
70    }
71
72    fn serialize(scalar: &Self::Scalar) -> Self::Serialization {
73        // Serialize the scalar in compressed form
74        let mut buf = [0u8; FIELD_SIZE];
75        scalar
76            .serialize_compressed(&mut buf[..])
77            .expect("Serialization should not fail for valid scalars");
78
79        buf
80    }
81
82    fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization {
83        Self::serialize(scalar)
84    }
85
86    // Parse the scalar from compressed form
87    fn deserialize(buf: &Self::Serialization) -> Result<Self::Scalar, FieldError> {
88        let scalar = <Self::Scalar as CanonicalDeserialize>::deserialize_compressed(&buf[..])
89            .map_err(|_| FieldError::MalformedScalar)?;
90        Ok(scalar)
91    }
92}
93
94/// PallasGroup implements the FROST group interface for the Pallas curve
95#[derive(Clone, Copy, PartialEq, Eq)]
96pub struct PallasGroup {}
97
98impl Group for PallasGroup {
99    type Element = ProjectivePallas;
100    type Field = PallasScalarField;
101    type Serialization = [u8; GROUP_SIZE]; // Compressed byte representation of a Pallas group element
102
103    fn cofactor() -> <Self::Field as Field>::Scalar {
104        Self::Field::one()
105    }
106    fn identity() -> Self::Element {
107        Self::Element::zero()
108    }
109    fn generator() -> Self::Element {
110        Self::Element::generator()
111    }
112    fn serialize(element: &Self::Element) -> Result<Self::Serialization, GroupError> {
113        // Ensure that the element is not the identity element
114        // The FROST protocol requires that the identity element is never serialized or used in computations
115        if element.is_zero() {
116            return Err(GroupError::InvalidIdentityElement);
117        }
118
119        let mut buf: Self::Serialization = [0u8; GROUP_SIZE];
120        element
121            .serialize_compressed(&mut buf[..])
122            .map_err(|_| GroupError::MalformedElement)?;
123
124        Ok(buf)
125    }
126    fn deserialize(buf: &Self::Serialization) -> Result<Self::Element, GroupError> {
127        let point = <Self::Element as CanonicalDeserialize>::deserialize_compressed(&buf[..])
128            .map_err(|_| GroupError::MalformedElement);
129
130        // Ensure that the deserialized point is not the identity element
131        match point {
132            Ok(p) if p.is_zero() => Err(GroupError::InvalidIdentityElement),
133            Ok(p) => Ok(p),
134            Err(_) => Err(GroupError::MalformedElement),
135        }
136    }
137}
138
139// Define the ciphersuite for Pallas with Poseidon as the hash function
140// https://github.com/MinaProtocol/mina/blob/master/docs/specs/signatures/description.md
141pub const CONTEXT_STRING: &str = "bluepallas";
142const HASH_SIZE: usize = 32; // Posiedon hash output size
143
144/// The BluePallas ciphersuite, which uses the Pallas curve and Poseidon hash function.
145///
146/// Note that this ciphersuite is used for FROST signatures in the Mina protocol and has a lot of Mina-specific logic
147/// This library SHOULD not be treated as a general-purpose BluePallas ciphersuite, but rather as a Mina-specific implementation.
148#[derive(PartialEq, Eq, Debug)]
149pub struct BluePallas<M>(PhantomData<M>);
150
151// BluePallas<M> contains only PhantomData<M>, so copying it has no runtime cost or ownership risk.
152impl<M> Copy for BluePallas<M> {}
153
154impl<M> Clone for BluePallas<M> {
155    fn clone(&self) -> Self {
156        *self
157    }
158}
159
160impl<M> Ciphersuite for BluePallas<M>
161where
162    M: ChallengeMessage,
163{
164    const ID: &'static str = CONTEXT_STRING;
165
166    type Group = PallasGroup;
167    type HashOutput = [u8; HASH_SIZE];
168
169    type SignatureSerialization = [u8; HASH_SIZE];
170    fn H1(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
171        hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho", m])
172    }
173    fn H2(_m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
174        unimplemented!("H2 is not implemented on purpose, please see the `challenge` function");
175    }
176    fn H3(m: &[u8]) -> <<Self::Group as Group>::Field as Field>::Scalar {
177        hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce", m])
178    }
179    fn H4(m: &[u8]) -> Self::HashOutput {
180        hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m])
181    }
182    fn H5(m: &[u8]) -> Self::HashOutput {
183        hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m])
184    }
185
186    fn HDKG(m: &[u8]) -> Option<<<Self::Group as Group>::Field as Field>::Scalar> {
187        Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg", m]))
188    }
189
190    fn HID(m: &[u8]) -> Option<<<Self::Group as Group>::Field as Field>::Scalar> {
191        Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id", m]))
192    }
193
194    fn challenge(
195        r: &frost_core::Element<Self>,
196        verifying_key: &frost_core::VerifyingKey<Self>,
197        message: &[u8],
198    ) -> Result<frost_core::Challenge<Self>, frost_core::Error<Self>> {
199        M::challenge(r, verifying_key, message)
200    }
201
202    /// This performs the same functionality as [`Self::pre_commitment_sign`], but instead only
203    /// negates commitments because the coordinator is not able to receive any nonces.
204    /// Naturally, this is called by the coordinator in the [`crate::aggregate`] function.
205    fn pre_commitment_aggregate<'a>(
206        signing_package: &'a frost_core::SigningPackage<Self>,
207        binding_factor_list: &'a BindingFactorList<Self>,
208    ) -> Result<Cow<'a, frost_core::SigningPackage<Self>>, frost_core::Error<Self>> {
209        use ark_ff::{BigInteger, PrimeField};
210        // Compute the group commitment from signing commitments produced in round one.
211        let commit = compute_group_commitment(signing_package, binding_factor_list)?;
212
213        if commit.to_element().into_affine().y.into_bigint().is_even() {
214            return Ok(Cow::Borrowed(signing_package));
215        }
216
217        // Otherwise negate the commitments
218        let negated_commitments =
219            <frost_core::SigningPackage<Self> as NegateY>::negate_y(signing_package);
220
221        Ok(Cow::Owned(negated_commitments))
222    }
223
224    /// This functions computes the group commitment and checks whether the y-coordinate of the
225    /// group commitment is even, as required by the Mina protocol.
226    /// If the group commitment is not even, it negates the nonces and commitments
227    /// This will be called by each individual signer during [`round2::sign`]
228    fn pre_commitment_sign<'a>(
229        signing_package: &'a frost_core::SigningPackage<Self>,
230        signing_nonces: &'a frost_core::round1::SigningNonces<Self>,
231        binding_factor_list: &'a BindingFactorList<Self>,
232    ) -> Result<
233        (
234            Cow<'a, frost_core::SigningPackage<Self>>,
235            Cow<'a, frost_core::round1::SigningNonces<Self>>,
236        ),
237        frost_core::Error<Self>,
238    > {
239        use ark_ff::{BigInteger, PrimeField};
240        // Compute the group commitment from signing commitments produced in round one.
241        let commit = compute_group_commitment(signing_package, binding_factor_list)?;
242
243        if commit.to_element().into_affine().y.into_bigint().is_even() {
244            return Ok((
245                Cow::Borrowed(signing_package),
246                Cow::Borrowed(signing_nonces),
247            ));
248        }
249
250        // Otherwise negate the nonce that we know and all the commitments
251        let negated_nonce =
252            <frost_core::round1::SigningNonces<Self> as NegateY>::negate_y(signing_nonces);
253        let negated_commitments =
254            <frost_core::SigningPackage<Self> as NegateY>::negate_y(signing_package);
255
256        Ok((Cow::Owned(negated_commitments), Cow::Owned(negated_nonce)))
257    }
258}
259
260// Simply type alias for the FROST ciphersuite using Pallas with Poseidon
261pub type Identifier<M> = frost::Identifier<BluePallas<M>>;
262
263/// Generated by the coordinator of the signing operation and distributed to
264/// each signing party.
265pub type SigningPackage<M> = frost::SigningPackage<BluePallas<M>>;
266
267/// A Schnorr signature on FROST(Pallas, Posiedon).
268pub type Signature<M> = frost::Signature<BluePallas<M>>;
269
270/// A signing key for a Schnorr signature on FROST(Pallas, Posiedon).
271pub type SigningKey<M> = frost::SigningKey<BluePallas<M>>;
272
273/// A valid verifying key for Schnorr signatures on FROST(Pallas, Posiedon).
274pub type VerifyingKey<M> = frost::VerifyingKey<BluePallas<M>>;
275
276/// FROST(Pallas, Posiedon) Round 1 functionality and types.
277pub mod round1 {
278    use crate::{keys::SigningShare, ChallengeMessage};
279
280    use super::*;
281    /// Comprised of FROST(Pallas, Posiedon) hiding and binding nonces.
282    ///
283    /// Note that [`SigningNonces`] must be used *only once* for a signing
284    /// operation; re-using nonces will result in leakage of a signer's long-lived
285    /// signing key.
286    pub type SigningNonces<M> = frost::round1::SigningNonces<BluePallas<M>>;
287
288    /// Published by each participant in the first round of the signing protocol.
289    ///
290    /// This step can be batched if desired by the implementation. Each
291    /// SigningCommitment can be used for exactly *one* signature.
292    pub type SigningCommitments<M> = frost::round1::SigningCommitments<BluePallas<M>>;
293
294    /// A commitment to a signing nonce share.
295    pub type NonceCommitment<M> = frost::round1::NonceCommitment<BluePallas<M>>;
296
297    /// Performed once by each participant selected for the signing operation.
298    ///
299    /// Generates the signing nonces and commitments to be used in the signing
300    /// operation.
301    pub fn commit<M, RNG>(
302        secret: &SigningShare<M>,
303        rng: &mut RNG,
304    ) -> (SigningNonces<M>, SigningCommitments<M>)
305    where
306        M: ChallengeMessage,
307        RNG: CryptoRng + RngCore,
308    {
309        frost::round1::commit::<BluePallas<M>, RNG>(secret, rng)
310    }
311}
312
313/// FROST(Pallas, Posiedon) Round 2 functionality and types, for signature share generation.
314pub mod round2 {
315    use super::*;
316    use crate::{round1::SigningNonces, ChallengeMessage};
317
318    /// A FROST(Pallas, Posiedon) participant's signature share, which the Coordinator will aggregate with all other signer's
319    /// shares into the joint signature.
320    pub type SignatureShare<M> = frost::round2::SignatureShare<BluePallas<M>>;
321
322    pub fn sign<M>(
323        signing_package: &SigningPackage<M>,
324        signer_nonces: &SigningNonces<M>,
325        key_package: &frost::keys::KeyPackage<BluePallas<M>>,
326    ) -> Result<SignatureShare<M>, Error<M>>
327    where
328        M: ChallengeMessage,
329    {
330        frost::round2::sign::<BluePallas<M>>(signing_package, signer_nonces, key_package)
331    }
332}
333
334pub fn aggregate<M>(
335    signing_package: &SigningPackage<M>,
336    signature_shares: &BTreeMap<Identifier<M>, frost::round2::SignatureShare<BluePallas<M>>>,
337    pubkey_package: &frost::keys::PublicKeyPackage<BluePallas<M>>,
338) -> Result<Signature<M>, Error<M>>
339where
340    M: ChallengeMessage,
341{
342    frost::aggregate(signing_package, signature_shares, pubkey_package)
343}