mina_hasher/lib.rs
1#![deny(missing_docs)]
2#![deny(unsafe_code)]
3#![deny(clippy::all)]
4#![deny(clippy::pedantic)]
5#![deny(clippy::nursery)]
6#![doc = include_str!("../README.md")]
7#![no_std]
8
9extern crate alloc;
10
11use alloc::{format, string::String, vec, vec::Vec};
12
13pub mod poseidon;
14pub mod roinput;
15pub use mina_curves::pasta::Fp;
16pub use poseidon::{PoseidonHasherKimchi, PoseidonHasherLegacy};
17pub use roinput::ROInput;
18
19use ark_ff::PrimeField;
20use o1_utils::FieldHelpers;
21
22/// Maximum length for domain strings used in hashing.
23const MAX_DOMAIN_STRING_LEN: usize = 20;
24
25/// The domain parameter trait is used during hashing to convey extra
26/// arguments to domain string generation. It is also used by generic signing
27/// code.
28pub trait DomainParameter: Clone {
29 /// Conversion into vector of bytes
30 fn into_bytes(self) -> Vec<u8>;
31}
32
33impl DomainParameter for () {
34 fn into_bytes(self) -> Vec<u8> {
35 vec![]
36 }
37}
38
39impl DomainParameter for u32 {
40 fn into_bytes(self) -> Vec<u8> {
41 self.to_le_bytes().to_vec()
42 }
43}
44
45impl DomainParameter for u64 {
46 fn into_bytes(self) -> Vec<u8> {
47 self.to_le_bytes().to_vec()
48 }
49}
50
51/// Interface for hashable objects
52///
53/// Mina uses fixed-length hashing with domain separation for each type of
54/// object hashed. The prior means that `Hashable` only supports types whose
55/// size is not variable.
56///
57/// **Important:** The developer MUST assure that all domain strings used
58/// throughout the system are unique and that all structures hashed are of fixed
59/// size.
60///
61/// Here is an example of how to implement the `Hashable` trait for am `Example`
62/// type.
63///
64/// ```rust
65/// use mina_hasher::{Hashable, ROInput};
66///
67/// #[derive(Clone)]
68/// struct Example;
69///
70/// impl Hashable for Example {
71/// type D = ();
72///
73/// fn to_roinput(&self) -> ROInput {
74/// let roi = ROInput::new();
75/// // Serialize example members
76/// // ...
77/// roi
78/// }
79///
80/// fn domain_string(_: Self::D) -> Option<String> {
81/// format!("Example").into()
82/// }
83/// }
84/// ```
85///
86/// See example in [`ROInput`] documentation
87pub trait Hashable: Clone {
88 /// Generic domain string argument type
89 type D: DomainParameter;
90
91 /// Serialization to random oracle input
92 fn to_roinput(&self) -> ROInput;
93
94 /// Generate unique domain string of length `<= 20`.
95 ///
96 /// The length bound is guarded by an assertion, but uniqueness must be
97 /// enforced by the developer implementing the traits (see [`Hashable`] for
98 /// more details). The domain string may be parameterized by the contents of
99 /// the generic `domain_param` argument.
100 ///
101 /// **Note:** You should always return `Some(String)`. A `None` return value
102 /// is only used for testing.
103 fn domain_string(domain_param: Self::D) -> Option<String>;
104}
105
106/// Interface for hashing [`Hashable`] inputs
107///
108/// Mina uses a unique hasher configured with domain separation for each type of
109/// object hashed.
110/// The underlying hash parameters are large and costly to initialize, so the
111/// [`Hasher`] interface provides a reusable context for efficient hashing with
112/// domain separation.
113///
114/// Example usage
115///
116/// ```rust
117/// use mina_hasher::{create_legacy, Fp, Hashable, Hasher, ROInput};
118///
119/// #[derive(Clone)]
120/// struct Something;
121///
122/// impl Hashable for Something {
123/// type D = u32;
124///
125/// fn to_roinput(&self) -> ROInput {
126/// let mut roi = ROInput::new();
127/// // ... serialize contents of self
128/// roi
129/// }
130///
131/// fn domain_string(id: Self::D) -> Option<String> {
132/// format!("Something {}", id).into()
133/// }
134/// }
135///
136/// let mut hasher = create_legacy::<Something>(123);
137/// let output: Fp = hasher.hash(&Something { });
138/// ```
139///
140pub trait Hasher<H: Hashable> {
141 /// Set the initial state based on domain separation string generated from
142 /// `H::domain_string(domain_param)`
143 fn init(&mut self, domain_param: H::D) -> &mut dyn Hasher<H>;
144
145 /// Restore the initial state that was set most recently
146 fn reset(&mut self) -> &mut dyn Hasher<H>;
147
148 /// Consume hash `input`
149 fn update(&mut self, input: &H) -> &mut dyn Hasher<H>;
150
151 /// Obtain has result output
152 fn digest(&mut self) -> Fp;
153
154 /// Hash input and obtain result output
155 fn hash(&mut self, input: &H) -> Fp {
156 self.reset();
157 self.update(input);
158 let output = self.digest();
159 self.reset();
160 output
161 }
162
163 /// Initialize state, hash input and obtain result output
164 fn init_and_hash(&mut self, domain_param: H::D, input: &H) -> Fp {
165 self.init(domain_param);
166 self.update(input);
167 let output = self.digest();
168 self.reset();
169 output
170 }
171}
172
173/// Transform domain prefix string to field element.
174///
175/// The prefix must be at most 20 characters. Shorter strings are
176/// right-padded with asterisks (`*`) to reach 20 characters before
177/// conversion to a field element. For example, `"CodaSignature"` becomes
178/// `"CodaSignature*******"`.
179fn domain_prefix_to_field<F: PrimeField>(prefix: &str) -> F {
180 assert!(prefix.len() <= MAX_DOMAIN_STRING_LEN);
181 let prefix = &prefix[..core::cmp::min(prefix.len(), MAX_DOMAIN_STRING_LEN)];
182 let mut bytes = format!("{prefix:*<MAX_DOMAIN_STRING_LEN$}")
183 .as_bytes()
184 .to_vec();
185 bytes.resize(F::size_in_bytes(), 0);
186 F::from_bytes(&bytes).expect("invalid domain bytes")
187}
188
189/// Create a legacy hasher context
190pub fn create_legacy<H: Hashable>(domain_param: H::D) -> PoseidonHasherLegacy<H> {
191 poseidon::new_legacy::<H>(domain_param)
192}
193
194/// Create a kimchi hasher context for `ZkApp` signing (Berkeley upgrade)
195pub fn create_kimchi<H: Hashable>(domain_param: H::D) -> PoseidonHasherKimchi<H>
196where
197 H::D: DomainParameter,
198{
199 poseidon::new_kimchi::<H>(domain_param)
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_domain_prefix_padding_short_string() {
208 // "CodaSignature" (13 chars) should be padded to "CodaSignature*******"
209 let result: Fp = domain_prefix_to_field("CodaSignature");
210 let bytes = result.to_bytes();
211 let padded = &bytes[..MAX_DOMAIN_STRING_LEN];
212 assert_eq!(padded, b"CodaSignature*******");
213 }
214
215 #[test]
216 fn test_domain_prefix_padding_exact_length() {
217 // Exactly 20 chars should not be padded
218 let result: Fp = domain_prefix_to_field("MinaSignatureMainnet");
219 let bytes = result.to_bytes();
220 let padded = &bytes[..MAX_DOMAIN_STRING_LEN];
221 assert_eq!(padded, b"MinaSignatureMainnet");
222 }
223
224 #[test]
225 fn test_domain_prefix_padding_empty_string() {
226 // Empty string should become 20 asterisks
227 let result: Fp = domain_prefix_to_field("");
228 let bytes = result.to_bytes();
229 let padded = &bytes[..MAX_DOMAIN_STRING_LEN];
230 assert_eq!(padded, b"********************");
231 }
232
233 #[test]
234 fn test_domain_prefix_same_result_with_or_without_padding() {
235 // Pre-padded and un-padded versions should produce the same result
236 let unpadded: Fp = domain_prefix_to_field("CodaSignature");
237 let prepadded: Fp = domain_prefix_to_field("CodaSignature*******");
238 assert_eq!(unpadded, prepadded);
239 }
240
241 #[test]
242 #[should_panic(expected = "assertion failed")]
243 fn test_domain_prefix_too_long() {
244 // Strings longer than 20 chars should panic
245 let _: Fp = domain_prefix_to_field("ThisStringIsTooLongForDomain");
246 }
247}