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