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}