Skip to main content

frost_bluepallas/
hasher.rs

1//! Mina-compatible hashing utilities for FROST using the Pallas curve.
2
3use alloc::string::{String, ToString};
4use ark_ff::PrimeField;
5use frost_core::Field;
6use mina_hasher::{create_legacy, Hashable, Hasher, ROInput};
7
8use crate::PallasScalarField;
9
10/// This is a Hashable interface for an array of bytes
11/// This allows us to provide a easy-to-read interface for hashing FROST elements in H1, H3, H4, H5
12#[derive(Clone, Debug)]
13pub(crate) struct PallasHashElement<'a> {
14    value: &'a [&'a [u8]],
15}
16
17const HASH_ELEMENT_STRING: &str = "PallasHashElement";
18
19// Implement a hashable trait for a u8 slice
20impl Hashable for PallasHashElement<'_> {
21    type D = ();
22
23    fn to_roinput(&self) -> ROInput {
24        let mut roi = ROInput::new();
25        let count_bytes = (self.value.len() as u64).to_le_bytes();
26        roi = roi.append_bytes(&count_bytes);
27        for segment in self.value {
28            let len_bytes = (segment.len() as u64).to_le_bytes();
29            roi = roi.append_bytes(&len_bytes);
30            roi = roi.append_bytes(segment);
31        }
32
33        roi
34    }
35
36    // Use a fixed domain string for PallasHashElement hashing
37    fn domain_string(_domain_param: Self::D) -> Option<String> {
38        HASH_ELEMENT_STRING.to_string().into()
39    }
40}
41
42type Fq = <PallasScalarField as Field>::Scalar;
43
44// Maps poseidon hash of input to a scalar field element
45pub fn hash_to_scalar(input: &[&[u8]]) -> Fq {
46    // Hash via PallasHashElement, which length-prefixes the segment count and each segment
47    // to prevent padding and segmentation-based collision attacks.
48    let wrap = PallasHashElement { value: input };
49    let mut hasher = create_legacy::<PallasHashElement>(());
50
51    // Convert from base field to scalar field
52    // This is performed in the mina-signer crate
53    // https://github.com/o1-labs/proof-systems/blob/6d2ac796205456d314d7ea2a3db6e0e816d60a99/signer/src/schnorr.rs#L145-L158
54    Fq::from(hasher.hash(&wrap).into_bigint())
55}
56
57// Maps poseidon hash of input to a 32-byte array
58pub fn hash_to_array(input: &[&[u8]]) -> <PallasScalarField as frost_core::Field>::Serialization {
59    let scalar = hash_to_scalar(input);
60
61    PallasScalarField::serialize(&scalar)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_hash_to_scalar_is_deterministic_and_differs() {
70        let input = &[&b"abc"[..]];
71        let s1 = hash_to_scalar(input);
72        let s2 = hash_to_scalar(input);
73        assert_eq!(s1, s2, "same input must yield same scalar");
74
75        let other = &[&b"def"[..]];
76        let s3 = hash_to_scalar(other);
77        assert_ne!(s1, s3, "different input must yield a different scalar");
78    }
79
80    #[test]
81    fn test_hash_to_array_length() {
82        let arr = hash_to_array(&[&b"hello"[..]]);
83        // Serialization for PallasScalarField is 32 bytes
84        assert_eq!(arr.len(), 32);
85    }
86
87    #[test]
88    fn test_padding_attack_resistance() {
89        let base = &[&b"1"[..]];
90        let padded = &[&b"1"[..], &[0u8][..]];
91        let h_base = hash_to_scalar(base);
92        let h_padded = hash_to_scalar(padded);
93        assert_ne!(
94            h_base, h_padded,
95            "trailing zero-byte padding collides for this variable-length encoding",
96        );
97    }
98
99    #[test]
100    fn test_segment_boundary_collision() {
101        let a = hash_to_scalar(&[b"ab", b"c"]);
102        let b = hash_to_scalar(&[b"a", b"bc"]);
103        assert_ne!(a, b, "different segmentation can yield the same hash");
104    }
105}