Skip to main content

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}