Skip to main content

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}