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}