Skip to main content

mina_signer/
schnorr.rs

1//! Mina Schnorr signature scheme
2//!
3//! An implementation of the singer interface for the Mina signature algorithm
4//!
5//! Details: <https://github.com/MinaProtocol/mina/blob/develop/docs/specs/signatures/description.md>
6
7extern crate alloc;
8
9use alloc::{boxed::Box, string::String, vec};
10use num_bigint::BigUint;
11
12use crate::{
13    BaseField, CurvePoint, Hashable, Keypair, NonceMode, PubKey, ScalarField, Signature, Signer,
14};
15use ark_ec::{
16    AffineRepr, // for generator()
17    CurveGroup,
18};
19use ark_ff::{BigInteger, Field, PrimeField, Zero};
20use blake2::{
21    digest::{Update, VariableOutput},
22    Blake2bVar,
23};
24use core::ops::{Add, Neg};
25use mina_hasher::{self, DomainParameter, Hasher, ROInput};
26use o1_utils::FieldHelpers;
27
28/// `BLAKE2b` output size in bytes for nonce derivation.
29///
30/// Using 256 bits (32 bytes) provides sufficient entropy for deriving
31/// a scalar field element after masking the top 2 bits.
32const BLAKE2B_OUTPUT_SIZE: usize = 32;
33
34/// Schnorr signer context for the Mina signature algorithm
35///
36/// For details about the signature algorithm please see the
37/// [`schnorr`](crate::schnorr) documentation
38pub struct Schnorr<H: Hashable> {
39    /// The hasher instance used to hash messages
40    pub hasher: Box<dyn Hasher<Message<H>>>,
41    /// The domain parameter used for hashing
42    pub domain_param: H::D,
43}
44
45/// Internal message structure for Schnorr signature hash computation.
46///
47/// This struct combines the user's input message with cryptographic context
48/// (public key and nonce commitment) to create the hash input for computing
49/// the Schnorr signature challenge. It implements [`Hashable`] to be
50/// compatible with Mina's Poseidon-based hashing.
51///
52/// # Schnorr Signature Context
53///
54/// In the Schnorr signature scheme, the challenge `e` is computed as:
55///
56/// ```text
57/// e = H(input || pub_key_x || pub_key_y || rx)
58/// ```
59///
60/// where:
61/// - `input` is the user's message to sign
62/// - `pub_key_x`, `pub_key_y` are the signer's public key coordinates
63/// - `rx` is the x-coordinate of the nonce commitment point `R = k·G`
64///
65/// This binding ensures the signature is tied to a specific message, public
66/// key, and nonce, preventing various attack vectors.
67///
68/// # Fields
69///
70/// - `input`: The original message being signed, implementing [`Hashable`]
71/// - `pub_key_x`: X-coordinate of the signer's public key
72/// - `pub_key_y`: Y-coordinate of the signer's public key
73/// - `rx`: X-coordinate of the nonce commitment point
74#[derive(Clone)]
75pub struct Message<H: Hashable> {
76    /// The original input message to be signed
77    input: H,
78    /// X-coordinate of the signer's public key
79    pub_key_x: BaseField,
80    /// Y-coordinate of the signer's public key
81    pub_key_y: BaseField,
82    /// X-coordinate of the nonce commitment point R = k·G
83    rx: BaseField,
84}
85
86impl<H: Hashable> Hashable for Message<H> {
87    type D = H::D;
88
89    fn to_roinput(&self) -> ROInput {
90        self.input
91            .to_roinput()
92            .append_field(self.pub_key_x)
93            .append_field(self.pub_key_y)
94            .append_field(self.rx)
95    }
96
97    fn domain_string(domain_param: Self::D) -> Option<String> {
98        H::domain_string(domain_param)
99    }
100}
101
102impl<H: 'static + Hashable> Signer<H> for Schnorr<H> {
103    fn sign(&mut self, kp: &Keypair, input: &H, nonce_mode: NonceMode) -> Signature {
104        let k: ScalarField = match nonce_mode {
105            NonceMode::Chunked => self.derive_nonce_chunked(kp, input),
106            NonceMode::Legacy => self.derive_nonce_legacy(kp, input),
107        };
108        let r: CurvePoint = CurvePoint::generator()
109            .mul_bigint(k.into_bigint())
110            .into_affine();
111        let k: ScalarField = if r.y.into_bigint().is_even() { k } else { -k };
112
113        let e: ScalarField = self.message_hash(&kp.public, r.x, input);
114        let s: ScalarField = k + e * kp.secret_key().scalar();
115
116        Signature::new(r.x, s)
117    }
118
119    fn verify(&mut self, sig: &Signature, public: &PubKey, input: &H) -> bool {
120        let ev: ScalarField = self.message_hash(public, sig.rx, input);
121
122        let sv = CurvePoint::generator()
123            .mul_bigint(sig.s.into_bigint())
124            .into_affine();
125        // Perform addition and infinity check in projective coordinates for
126        // performance
127        let rv = public.point().mul_bigint(ev.into_bigint()).neg().add(sv);
128
129        if rv.is_zero() {
130            return false;
131        }
132
133        let rv = rv.into_affine();
134
135        rv.y.into_bigint().is_even() && rv.x == sig.rx
136    }
137}
138
139pub(crate) fn create_legacy<H: 'static + Hashable>(domain_param: H::D) -> impl Signer<H> {
140    Schnorr::<H> {
141        hasher: Box::new(mina_hasher::create_legacy::<Message<H>>(
142            domain_param.clone(),
143        )),
144        domain_param,
145    }
146}
147
148pub(crate) fn create_kimchi<H: 'static + Hashable>(domain_param: H::D) -> impl Signer<H> {
149    Schnorr::<H> {
150        hasher: Box::new(mina_hasher::create_kimchi::<Message<H>>(
151            domain_param.clone(),
152        )),
153        domain_param,
154    }
155}
156
157impl<H: 'static + Hashable> Schnorr<H> {
158    /// Chunked nonce derivation for zkApp transactions.
159    ///
160    /// This function implements the deterministic nonce derivation algorithm used
161    /// by `Message.Chunked` in the OCaml implementation. Use this for zkApp
162    /// transactions that need to be compatible with o1js.
163    ///
164    /// # Compatibility
165    ///
166    /// This implementation corresponds to `Message.Chunked.derive` in the OCaml
167    /// implementation (`src/lib/crypto/signature_lib/schnorr.ml`).
168    ///
169    /// It is also compatible with the TypeScript o1js implementation:
170    /// <https://github.com/o1-labs/o1js/blob/main/src/mina-signer/src/signature.ts>
171    ///
172    /// The private key conversion replicates the "Field.project" method with unpack
173    /// from the OCaml implementation, which performs modular reduction when the
174    /// scalar field value is larger than the base field modulus.
175    ///
176    /// # Algorithm
177    ///
178    /// The nonce derivation follows this process:
179    /// 1. Create `ROInput` from: `message || public_key_x || public_key_y || private_key || network_id`
180    /// 2. Pack the `ROInput` into fields using Mina's field packing
181    /// 3. Convert packed fields to bits (255 bits per field)
182    /// 4. Convert bits to bytes for `BLAKE2b` input
183    /// 5. Hash with BLAKE2b-256
184    /// 6. Drop the top 2 bits to create a valid scalar field element
185    ///
186    /// # Parameters
187    ///
188    /// * `kp` - The keypair containing both public and private keys
189    /// * `input` - The message to be signed
190    ///
191    /// # Returns
192    ///
193    /// A deterministic nonce as a scalar field element.
194    ///
195    /// # Test Vectors
196    ///
197    /// For test vectors demonstrating this function's usage, see the
198    /// `sign_fields_test` in [`tests/signer.rs`](../../tests/signer.rs) which
199    /// uses `NonceMode::Chunked`.
200    ///
201    /// # Security
202    ///
203    /// This function generates a cryptographically secure, deterministic nonce
204    /// that:
205    /// - Depends on the private key, public key, message, and network context
206    /// - Ensures no two different messages share the same nonce (with the same
207    ///   key)
208    /// - Is compatible with existing Mina protocol implementations
209    ///
210    /// # Panics
211    ///
212    /// Panics if the `BLAKE2b` variable-output hasher cannot be created with
213    /// a 32-byte output size (should not happen).
214    pub fn derive_nonce_chunked(&self, kp: &Keypair, input: &H) -> ScalarField {
215        let mut blake_hasher =
216            Blake2bVar::new(BLAKE2B_OUTPUT_SIZE).expect("BLAKE2b output size is valid");
217
218        // Create ROInput with message + [px, py, private_key_as_field] +
219        // network_id_packed
220        let network_id_bytes = self.domain_param.clone().into_bytes();
221        let network_id_value = if network_id_bytes.is_empty() {
222            0u8
223        } else {
224            network_id_bytes[0]
225        };
226
227        let roi = input
228            .to_roinput()
229            .append_field(kp.public.point().x)
230            .append_field(kp.public.point().y)
231            .append_field({
232                // Convert scalar to base field with explicit wraparound (modular reduction)
233                // This replicates the "Field.project" method with unpack from the OCaml implementation
234
235                let secret_biguint: BigUint = kp.secret_key().scalar().into_bigint().into();
236                let modulus = BaseField::MODULUS.into();
237                if secret_biguint >= modulus {
238                    // Reduce modulo base field modulus
239                    let reduced_biguint: BigUint = secret_biguint - modulus;
240                    BaseField::from_biguint(&reduced_biguint)
241                        .expect("Reduced bigint should fit in base field")
242                } else {
243                    BaseField::from_biguint(&secret_biguint)
244                        .expect("Scalar bigint should fit in base field")
245                }
246            })
247            .append_bytes(&[network_id_value]); // Network ID as packed 8 bits
248
249        // Get packed fields
250        let packed_fields = roi.to_fields();
251
252        // Convert each field to bits and flatten
253        let mut all_bits = vec![];
254        for field in packed_fields {
255            let field_bytes = field.to_bytes();
256            let mut field_bits = 0;
257            for &byte in &field_bytes {
258                for bit_idx in 0..8 {
259                    if field_bits < 255 {
260                        let bit = (byte & (1 << bit_idx)) != 0;
261                        all_bits.push(bit);
262                        field_bits += 1;
263                    }
264                }
265            }
266        }
267
268        // Convert bits to bytes for BLAKE2b
269        let mut input_bytes = vec![0u8; all_bits.len().div_ceil(8)];
270        for (i, &bit) in all_bits.iter().enumerate() {
271            if bit {
272                input_bytes[i / 8] |= 1 << (i % 8);
273            }
274        }
275
276        // Hash with BLAKE2b and drop top 2 bits
277        blake_hasher.update(&input_bytes);
278        let mut bytes = [0; BLAKE2B_OUTPUT_SIZE];
279        blake_hasher
280            .finalize_variable(&mut bytes)
281            .expect("incorrect output size");
282        bytes[bytes.len() - 1] &= 0b0011_1111;
283
284        ScalarField::from_random_bytes(&bytes[..]).expect("failed to create scalar from bytes")
285    }
286
287    /// Legacy nonce derivation for user commands (payments, delegations).
288    ///
289    /// This function implements the deterministic nonce derivation algorithm used
290    /// by `Message.Legacy` in the OCaml implementation. Use this for legacy Mina
291    /// transactions (user commands) such as payments and delegations.
292    ///
293    /// # Compatibility
294    ///
295    /// This implementation corresponds to `Message.Legacy.derive` in the OCaml
296    /// implementation (`src/lib/crypto/signature_lib/schnorr.ml`).
297    ///
298    /// # Parameters
299    ///
300    /// * `kp` - The keypair containing both public and private keys
301    /// * `input` - The message to be signed
302    ///
303    /// # Returns
304    ///
305    /// A deterministic nonce as a scalar field element.
306    ///
307    /// # Usage
308    ///
309    /// Use this method for legacy Mina transactions (user commands) such as
310    /// payments and delegations. For zkApp transactions, use
311    /// [`derive_nonce_chunked`](Self::derive_nonce_chunked) instead.
312    ///
313    /// # Differences from `derive_nonce_chunked`
314    ///
315    /// This method differs from [`derive_nonce_chunked`](Self::derive_nonce_chunked) in several ways:
316    /// - Uses direct byte serialization (`roi.to_bytes()`) instead of field
317    ///   packing
318    /// - Appends private key as scalar field element instead of base field
319    ///   element
320    /// - Uses full network ID bytes instead of packed single byte
321    /// - Does not perform bit-level manipulation for `BLAKE2b` input
322    ///
323    /// # Security
324    ///
325    /// This function generates a cryptographically secure, deterministic nonce
326    /// that depends on the private key, public key, message, and network
327    /// context.
328    fn derive_nonce_legacy(&self, kp: &Keypair, input: &H) -> ScalarField {
329        let mut blake_hasher =
330            Blake2bVar::new(BLAKE2B_OUTPUT_SIZE).expect("BLAKE2b output size is valid");
331
332        let roi = input
333            .to_roinput()
334            .append_field(kp.public.point().x)
335            .append_field(kp.public.point().y)
336            .append_scalar(*kp.secret_key().scalar())
337            .append_bytes(&self.domain_param.clone().into_bytes());
338
339        blake_hasher.update(&roi.to_bytes());
340
341        let mut bytes = [0; BLAKE2B_OUTPUT_SIZE];
342        blake_hasher
343            .finalize_variable(&mut bytes)
344            .expect("incorrect output size");
345        // Drop the top two bits to convert into a scalar field element
346        //   N.B. Since the order of Pallas's scalar field p is very close to 2^m
347        //   for some m, truncating only creates a tiny amount of bias that should
348        //   be insignificant and better than reduction modulo p.
349        bytes[bytes.len() - 1] &= 0b0011_1111;
350
351        ScalarField::from_random_bytes(&bytes[..]).expect("failed to create scalar from bytes")
352    }
353
354    /// This function uses a cryptographic hash function (based on a sponge
355    /// construction) to convert the message to be signed (and some other
356    /// information) into a uniformly and randomly distributed scalar field
357    /// element. It uses Mina's variant of the Poseidon SNARK-friendly
358    /// cryptographic hash function.
359    /// Details:
360    /// <https://github.com/o1-labs/cryptography-rfcs/blob/httpsnapps-notary-signatures/mina/001-poseidon-sponge.md>
361    fn message_hash(&mut self, pub_key: &PubKey, rx: BaseField, input: &H) -> ScalarField {
362        let schnorr_input = Message::<H> {
363            input: input.clone(),
364            pub_key_x: pub_key.point().x,
365            pub_key_y: pub_key.point().y,
366            rx,
367        };
368
369        // Squeeze and convert from base field element to scalar field element
370        // Since the difference in modulus between the two fields is < 2^125,
371        // w.h.p., a random value from one field will fit in the other field.
372        ScalarField::from(self.hasher.hash(&schnorr_input).into_bigint())
373    }
374}