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}