aztec_crypto/
pedersen.rs

1//! Pedersen hash on the Grumpkin curve.
2//!
3//! Matches the Noir standard library's `pedersen_hash` implementation:
4//!
5//! ```text
6//! generators = derive_generators("pedersen_hash", N, 0)
7//! length_gen = derive_generators("pedersen_hash_length", 1, 0)[0]
8//! H = sum(inputs[i] * generators[i]) + N * length_gen
9//! result = H.x
10//! ```
11
12use ark_ec::AffineRepr;
13use ark_ff::{BigInteger, PrimeField};
14use aztec_core::grumpkin;
15use aztec_core::types::{Fq, Fr, GrumpkinScalar, Point};
16use bn254_blackbox_solver::derive_generators;
17
18/// Convert an `ark_grumpkin::Affine` point to our `Point` type.
19///
20/// Both use BN254's scalar field (Fr) as the base field for Grumpkin.
21fn affine_to_point(affine: &ark_grumpkin::Affine) -> Point {
22    if affine.is_zero() {
23        return Point {
24            x: Fr::zero(),
25            y: Fr::zero(),
26            is_infinite: true,
27        };
28    }
29    let x = affine.x().expect("non-infinity point has x");
30    let y = affine.y().expect("non-infinity point has y");
31
32    // ark_grumpkin::Fq is the same field as ark_bn254::Fr.
33    // Convert via big-endian byte representation.
34    let x_bytes = x.into_bigint().to_bytes_be();
35    let y_bytes = y.into_bigint().to_bytes_be();
36
37    let mut x_padded = [0u8; 32];
38    let mut y_padded = [0u8; 32];
39    x_padded[32 - x_bytes.len()..].copy_from_slice(&x_bytes);
40    y_padded[32 - y_bytes.len()..].copy_from_slice(&y_bytes);
41
42    Point {
43        x: Fr::from(x_padded),
44        y: Fr::from(y_padded),
45        is_infinite: false,
46    }
47}
48
49/// Compute the Pedersen hash of a slice of field elements.
50///
51/// This matches the Noir standard library's `pedersen_hash` function exactly:
52/// - Generators are derived from the `"pedersen_hash"` domain separator
53/// - A length generator is derived from `"pedersen_hash_length"`
54/// - Result is the x-coordinate of the accumulated point
55pub fn pedersen_hash(inputs: &[Fr]) -> Fr {
56    let n = inputs.len();
57
58    // Derive generators for the inputs.
59    // The Noir stdlib's pedersen_hash uses "DEFAULT_DOMAIN_SEPARATOR" with
60    // separator=0 as starting_index.
61    let generators = derive_generators(b"DEFAULT_DOMAIN_SEPARATOR", n as u32, 0);
62    // Derive the length generator
63    let length_generators = derive_generators(b"pedersen_hash_length", 1, 0);
64    let length_gen = affine_to_point(&length_generators[0]);
65
66    // Accumulate: H = sum(inputs[i] * generators[i])
67    let mut result = Point {
68        x: Fr::zero(),
69        y: Fr::zero(),
70        is_infinite: true,
71    };
72
73    for (i, input) in inputs.iter().enumerate() {
74        let gen_point = affine_to_point(&generators[i]);
75        // Convert Fr (BN254 scalar field) to GrumpkinScalar (BN254 base field = Grumpkin scalar field).
76        // Fr and Fq are different fields, but for pedersen hash the input values
77        // are small enough (or we just reinterpret the bytes).
78        // The Noir pedersen_hash takes Field elements and multiplies them with generators.
79        // In Noir, Field = BN254 scalar field = Grumpkin base field.
80        // Our Fr = BN254 scalar field (same as Noir's Field).
81        // GrumpkinScalar = Fq = BN254 base field = Grumpkin scalar field.
82        // We need to convert Fr -> GrumpkinScalar for scalar_mul.
83        let scalar = fr_to_grumpkin_scalar(input);
84        let term = grumpkin::scalar_mul(&scalar, &gen_point);
85        result = grumpkin::point_add(&result, &term);
86    }
87
88    // Add length term: N * length_gen
89    let length_scalar = GrumpkinScalar::from(n as u64);
90    let length_term = grumpkin::scalar_mul(&length_scalar, &length_gen);
91    result = grumpkin::point_add(&result, &length_term);
92
93    // Return x-coordinate
94    result.x
95}
96
97/// Convert an Fr (BN254 scalar field) to a GrumpkinScalar (BN254 base field).
98///
99/// This reinterprets the big-endian byte representation, reducing modulo the
100/// target field order. For values that fit in both fields (which is the common
101/// case for pedersen hash inputs), this is an identity operation on the integer.
102fn fr_to_grumpkin_scalar(fr: &Fr) -> GrumpkinScalar {
103    let bytes = fr.to_be_bytes();
104    Fq::from_be_bytes_mod_order(&bytes)
105}
106
107#[cfg(test)]
108#[allow(clippy::expect_used)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn pedersen_hash_single_zero() {
114        // pedersen_hash([0]) should produce a deterministic non-zero result
115        let result = pedersen_hash(&[Fr::zero()]);
116        // The result should be a valid field element (non-trivially zero because
117        // of the length generator contribution: 1 * length_gen)
118        assert!(!result.is_zero());
119    }
120
121    #[test]
122    fn pedersen_hash_deterministic() {
123        let inputs = [Fr::from(1u64), Fr::from(2u64), Fr::from(3u64)];
124        let h1 = pedersen_hash(&inputs);
125        let h2 = pedersen_hash(&inputs);
126        assert_eq!(h1, h2);
127    }
128
129    #[test]
130    fn pedersen_hash_different_inputs_differ() {
131        let h1 = pedersen_hash(&[Fr::from(1u64), Fr::from(2u64)]);
132        let h2 = pedersen_hash(&[Fr::from(2u64), Fr::from(1u64)]);
133        assert_ne!(h1, h2);
134    }
135}