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