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}