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