Skip to main content

frost_bluepallas/
lib.rs

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