Skip to main content

aztec_core/
hash.rs

1//! Poseidon2 hash functions for the Aztec protocol.
2//!
3//! Provides `poseidon2_hash_with_separator` and derived functions that mirror the
4//! TypeScript SDK's hashing utilities.
5
6use crate::abi::AbiValue;
7use crate::constants::domain_separator;
8use crate::tx::FunctionCall;
9use crate::types::{AztecAddress, Fr};
10
11/// Compute a Poseidon2 sponge hash over `inputs`.
12///
13/// Uses a rate-3 / capacity-1 sponge with the standard Aztec IV construction:
14/// `state[3] = len * 2^64`.
15///
16/// This matches barretenberg's `poseidon2_hash` and the TS SDK's `poseidon2Hash`.
17pub(crate) fn poseidon2_hash(inputs: &[Fr]) -> Fr {
18    use ark_bn254::Fr as ArkFr;
19    use taceo_poseidon2::bn254::t4::permutation;
20
21    const RATE: usize = 3;
22
23    // IV: capacity element = input_length * 2^64
24    let two_pow_64 = ArkFr::from(1u64 << 32) * ArkFr::from(1u64 << 32);
25    let iv = ArkFr::from(inputs.len() as u64) * two_pow_64;
26
27    let mut state: [ArkFr; 4] = [ArkFr::from(0u64), ArkFr::from(0u64), ArkFr::from(0u64), iv];
28    let mut cache = [ArkFr::from(0u64); RATE];
29    let mut cache_size = 0usize;
30
31    for input in inputs {
32        if cache_size == RATE {
33            for i in 0..RATE {
34                state[i] += cache[i];
35            }
36            cache = [ArkFr::from(0u64); RATE];
37            cache_size = 0;
38            state = permutation(&state);
39        }
40        cache[cache_size] = input.0;
41        cache_size += 1;
42    }
43
44    // Absorb remaining cache
45    for i in 0..cache_size {
46        state[i] += cache[i];
47    }
48    state = permutation(&state);
49
50    Fr(state[0])
51}
52
53/// Compute a Poseidon2 hash over raw bytes using the same chunking as Aztec's
54/// `poseidon2HashBytes`.
55///
56/// Bytes are split into 31-byte chunks, each chunk is placed into a 32-byte
57/// buffer, reversed, and then interpreted as a field element before hashing.
58pub(crate) fn poseidon2_hash_bytes(bytes: &[u8]) -> Fr {
59    if bytes.is_empty() {
60        return poseidon2_hash(&[]);
61    }
62
63    let inputs = bytes
64        .chunks(31)
65        .map(|chunk| {
66            let mut field_bytes = [0u8; 32];
67            field_bytes[..chunk.len()].copy_from_slice(chunk);
68            field_bytes.reverse();
69            Fr::from(field_bytes)
70        })
71        .collect::<Vec<_>>();
72
73    poseidon2_hash(&inputs)
74}
75
76/// Compute a Poseidon2 hash of `inputs` with a domain separator prepended.
77///
78/// Mirrors the TS `poseidon2HashWithSeparator(args, separator)`.
79pub fn poseidon2_hash_with_separator(inputs: &[Fr], separator: u32) -> Fr {
80    let mut full_input = Vec::with_capacity(1 + inputs.len());
81    full_input.push(Fr::from(u64::from(separator)));
82    full_input.extend_from_slice(inputs);
83    poseidon2_hash(&full_input)
84}
85
86/// Hash function arguments using Poseidon2 with the `FUNCTION_ARGS` separator.
87///
88/// Returns `Fr::zero()` if `args` is empty.
89///
90/// Mirrors TS `computeVarArgsHash(args)`.
91pub fn compute_var_args_hash(args: &[Fr]) -> Fr {
92    if args.is_empty() {
93        return Fr::zero();
94    }
95    poseidon2_hash_with_separator(args, domain_separator::FUNCTION_ARGS)
96}
97
98/// Compute the inner authwit hash — the "intent" before siloing with consumer.
99///
100/// `args` is typically `[caller, selector, args_hash]`.
101/// Uses Poseidon2 with `AUTHWIT_INNER` domain separator.
102///
103/// Mirrors TS `computeInnerAuthWitHash(args)`.
104pub fn compute_inner_auth_wit_hash(args: &[Fr]) -> Fr {
105    poseidon2_hash_with_separator(args, domain_separator::AUTHWIT_INNER)
106}
107
108/// Compute the outer authwit hash — the value the approver signs.
109///
110/// Combines consumer address, chain ID, protocol version, and inner hash.
111/// Uses Poseidon2 with `AUTHWIT_OUTER` domain separator.
112///
113/// Mirrors TS `computeOuterAuthWitHash(consumer, chainId, version, innerHash)`.
114pub fn compute_outer_auth_wit_hash(
115    consumer: &AztecAddress,
116    chain_id: &Fr,
117    version: &Fr,
118    inner_hash: &Fr,
119) -> Fr {
120    poseidon2_hash_with_separator(
121        &[consumer.0, *chain_id, *version, *inner_hash],
122        domain_separator::AUTHWIT_OUTER,
123    )
124}
125
126/// Flatten ABI values into their field element representation.
127///
128/// Handles the common types used in authwit scenarios: `Field`, `Boolean`,
129/// `Integer`, `Array`, `Struct`, and `Tuple`. Strings are encoded as
130/// one field element per byte.
131pub fn abi_values_to_fields(args: &[AbiValue]) -> Vec<Fr> {
132    let mut fields = Vec::new();
133    for arg in args {
134        flatten_abi_value(arg, &mut fields);
135    }
136    fields
137}
138
139fn flatten_abi_value(value: &AbiValue, out: &mut Vec<Fr>) {
140    match value {
141        AbiValue::Field(f) => out.push(*f),
142        AbiValue::Boolean(b) => out.push(if *b { Fr::one() } else { Fr::zero() }),
143        AbiValue::Integer(i) => {
144            // Integers are encoded as unsigned field elements.
145            // Negative values are not expected in authwit args.
146            out.push(Fr::from(*i as u64));
147        }
148        AbiValue::Array(items) => {
149            for item in items {
150                flatten_abi_value(item, out);
151            }
152        }
153        AbiValue::String(s) => {
154            for byte in s.bytes() {
155                out.push(Fr::from(u64::from(byte)));
156            }
157        }
158        AbiValue::Struct(map) => {
159            // BTreeMap iterates in key order (deterministic).
160            for value in map.values() {
161                flatten_abi_value(value, out);
162            }
163        }
164        AbiValue::Tuple(items) => {
165            for item in items {
166                flatten_abi_value(item, out);
167            }
168        }
169    }
170}
171
172/// Compute the inner authwit hash from a caller address and a function call.
173///
174/// Computes `computeInnerAuthWitHash([caller, call.selector, varArgsHash(call.args)])`.
175///
176/// Mirrors TS `computeInnerAuthWitHashFromAction(caller, action)`.
177pub fn compute_inner_auth_wit_hash_from_action(caller: &AztecAddress, call: &FunctionCall) -> Fr {
178    let args_as_fields = abi_values_to_fields(&call.args);
179    let args_hash = compute_var_args_hash(&args_as_fields);
180    compute_inner_auth_wit_hash(&[caller.0, call.selector.to_field(), args_hash])
181}
182
183/// Chain identification information.
184///
185/// This is defined in `aztec-core` so that hash functions can use it
186/// without creating a circular dependency with `aztec-wallet`.
187#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct ChainInfo {
190    /// The L2 chain ID.
191    pub chain_id: Fr,
192    /// The rollup protocol version.
193    pub version: Fr,
194}
195
196/// Either a raw message hash, a structured call intent, or a pre-computed
197/// inner hash with its consumer address.
198///
199/// Mirrors the TS distinction between `Fr`, `CallIntent`, and `IntentInnerHash`.
200#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
201#[serde(tag = "type", rename_all = "snake_case")]
202pub enum MessageHashOrIntent {
203    /// A raw message hash (already computed outer hash).
204    Hash {
205        /// The hash value.
206        hash: Fr,
207    },
208    /// A structured call intent.
209    Intent {
210        /// The caller requesting authorization.
211        caller: AztecAddress,
212        /// The function call to authorize.
213        call: FunctionCall,
214    },
215    /// A pre-computed inner hash with consumer address.
216    ///
217    /// Used when the inner hash is already known but the outer hash
218    /// (which includes chain info) still needs to be computed.
219    InnerHash {
220        /// The consumer contract address.
221        consumer: AztecAddress,
222        /// The inner hash value.
223        inner_hash: Fr,
224    },
225}
226
227/// Compute the full authwit message hash from an intent and chain info.
228///
229/// For `MessageHashOrIntent::Hash` — returns the hash directly.
230/// For `MessageHashOrIntent::Intent { caller, call }`:
231///   1. `inner_hash = compute_inner_auth_wit_hash_from_action(caller, call)`
232///   2. `consumer = call.to` (the contract being called)
233///   3. `outer_hash = compute_outer_auth_wit_hash(consumer, chain_id, version, inner_hash)`
234/// For `MessageHashOrIntent::InnerHash { consumer, inner_hash }`:
235///   1. `outer_hash = compute_outer_auth_wit_hash(consumer, chain_id, version, inner_hash)`
236///
237/// Mirrors TS `computeAuthWitMessageHash(intent, metadata)`.
238pub fn compute_auth_wit_message_hash(intent: &MessageHashOrIntent, chain_info: &ChainInfo) -> Fr {
239    match intent {
240        MessageHashOrIntent::Hash { hash } => *hash,
241        MessageHashOrIntent::Intent { caller, call } => {
242            let inner_hash = compute_inner_auth_wit_hash_from_action(caller, call);
243            compute_outer_auth_wit_hash(
244                &call.to,
245                &chain_info.chain_id,
246                &chain_info.version,
247                &inner_hash,
248            )
249        }
250        MessageHashOrIntent::InnerHash {
251            consumer,
252            inner_hash,
253        } => compute_outer_auth_wit_hash(
254            consumer,
255            &chain_info.chain_id,
256            &chain_info.version,
257            inner_hash,
258        ),
259    }
260}
261
262#[cfg(test)]
263#[allow(clippy::expect_used, clippy::panic)]
264mod tests {
265    use super::*;
266    use crate::abi::{FunctionSelector, FunctionType};
267
268    #[test]
269    fn var_args_hash_empty_returns_zero() {
270        assert_eq!(compute_var_args_hash(&[]), Fr::zero());
271    }
272
273    #[test]
274    fn poseidon2_hash_known_vector() {
275        // Test vector: hash of [1] should produce a known result.
276        // This validates the sponge construction matches barretenberg.
277        let result = poseidon2_hash(&[Fr::from(1u64)]);
278        let expected =
279            Fr::from_hex("0x168758332d5b3e2d13be8048c8011b454590e06c44bce7f702f09103eef5a373")
280                .expect("valid hex");
281        assert_eq!(
282            result, expected,
283            "Poseidon2 hash of [1] must match barretenberg test vector"
284        );
285    }
286
287    #[test]
288    fn poseidon2_hash_with_separator_prepends_separator() {
289        // hash_with_separator([a, b], sep) == hash([Fr(sep), a, b])
290        let a = Fr::from(10u64);
291        let b = Fr::from(20u64);
292        let sep = 42u32;
293
294        let result = poseidon2_hash_with_separator(&[a, b], sep);
295        let manual = poseidon2_hash(&[Fr::from(u64::from(sep)), a, b]);
296        assert_eq!(result, manual);
297    }
298
299    #[test]
300    fn var_args_hash_single_element() {
301        let result = compute_var_args_hash(&[Fr::from(42u64)]);
302        // Should be poseidon2_hash([FUNCTION_ARGS_SEP, 42])
303        let expected =
304            poseidon2_hash_with_separator(&[Fr::from(42u64)], domain_separator::FUNCTION_ARGS);
305        assert_eq!(result, expected);
306    }
307
308    #[test]
309    fn inner_auth_wit_hash_uses_correct_separator() {
310        let args = [Fr::from(1u64), Fr::from(2u64), Fr::from(3u64)];
311        let result = compute_inner_auth_wit_hash(&args);
312        let expected = poseidon2_hash_with_separator(&args, domain_separator::AUTHWIT_INNER);
313        assert_eq!(result, expected);
314    }
315
316    #[test]
317    fn outer_auth_wit_hash_uses_correct_separator() {
318        let consumer = AztecAddress(Fr::from(100u64));
319        let chain_id = Fr::from(31337u64);
320        let version = Fr::from(1u64);
321        let inner_hash = Fr::from(999u64);
322
323        let result = compute_outer_auth_wit_hash(&consumer, &chain_id, &version, &inner_hash);
324        let expected = poseidon2_hash_with_separator(
325            &[consumer.0, chain_id, version, inner_hash],
326            domain_separator::AUTHWIT_OUTER,
327        );
328        assert_eq!(result, expected);
329    }
330
331    #[test]
332    fn inner_auth_wit_hash_from_action() {
333        let caller = AztecAddress(Fr::from(1u64));
334        let call = FunctionCall {
335            to: AztecAddress(Fr::from(2u64)),
336            selector: FunctionSelector::from_hex("0xaabbccdd").expect("valid"),
337            args: vec![AbiValue::Field(Fr::from(100u64))],
338            function_type: FunctionType::Private,
339            is_static: false,
340        };
341
342        let result = compute_inner_auth_wit_hash_from_action(&caller, &call);
343
344        // Manual computation
345        let args_hash = compute_var_args_hash(&[Fr::from(100u64)]);
346        let selector_field = call.selector.to_field();
347        let expected = compute_inner_auth_wit_hash(&[caller.0, selector_field, args_hash]);
348        assert_eq!(result, expected);
349    }
350
351    #[test]
352    fn auth_wit_message_hash_passthrough() {
353        let hash = Fr::from(42u64);
354        let chain_info = ChainInfo {
355            chain_id: Fr::from(31337u64),
356            version: Fr::from(1u64),
357        };
358        let result =
359            compute_auth_wit_message_hash(&MessageHashOrIntent::Hash { hash }, &chain_info);
360        assert_eq!(result, hash);
361    }
362
363    #[test]
364    fn auth_wit_message_hash_from_intent() {
365        let caller = AztecAddress(Fr::from(10u64));
366        let consumer = AztecAddress(Fr::from(20u64));
367        let call = FunctionCall {
368            to: consumer,
369            selector: FunctionSelector::from_hex("0x11223344").expect("valid"),
370            args: vec![],
371            function_type: FunctionType::Private,
372            is_static: false,
373        };
374        let chain_info = ChainInfo {
375            chain_id: Fr::from(31337u64),
376            version: Fr::from(1u64),
377        };
378
379        let result = compute_auth_wit_message_hash(
380            &MessageHashOrIntent::Intent {
381                caller,
382                call: call.clone(),
383            },
384            &chain_info,
385        );
386
387        // Manual computation
388        let inner = compute_inner_auth_wit_hash_from_action(&caller, &call);
389        let expected = compute_outer_auth_wit_hash(
390            &consumer,
391            &chain_info.chain_id,
392            &chain_info.version,
393            &inner,
394        );
395        assert_eq!(result, expected);
396    }
397
398    #[test]
399    fn auth_wit_message_hash_from_inner_hash() {
400        let consumer = AztecAddress(Fr::from(20u64));
401        let inner_hash = Fr::from(999u64);
402        let chain_info = ChainInfo {
403            chain_id: Fr::from(31337u64),
404            version: Fr::from(1u64),
405        };
406
407        let result = compute_auth_wit_message_hash(
408            &MessageHashOrIntent::InnerHash {
409                consumer,
410                inner_hash,
411            },
412            &chain_info,
413        );
414
415        let expected = compute_outer_auth_wit_hash(
416            &consumer,
417            &chain_info.chain_id,
418            &chain_info.version,
419            &inner_hash,
420        );
421        assert_eq!(result, expected);
422    }
423
424    #[test]
425    fn abi_values_to_fields_basic_types() {
426        let values = vec![
427            AbiValue::Field(Fr::from(1u64)),
428            AbiValue::Boolean(true),
429            AbiValue::Boolean(false),
430            AbiValue::Integer(42),
431        ];
432        let fields = abi_values_to_fields(&values);
433        assert_eq!(fields.len(), 4);
434        assert_eq!(fields[0], Fr::from(1u64));
435        assert_eq!(fields[1], Fr::one());
436        assert_eq!(fields[2], Fr::zero());
437        assert_eq!(fields[3], Fr::from(42u64));
438    }
439
440    #[test]
441    fn abi_values_to_fields_nested() {
442        let values = vec![AbiValue::Array(vec![
443            AbiValue::Field(Fr::from(1u64)),
444            AbiValue::Field(Fr::from(2u64)),
445        ])];
446        let fields = abi_values_to_fields(&values);
447        assert_eq!(fields.len(), 2);
448        assert_eq!(fields[0], Fr::from(1u64));
449        assert_eq!(fields[1], Fr::from(2u64));
450    }
451
452    #[test]
453    fn message_hash_or_intent_serde_roundtrip() {
454        let variants = vec![
455            MessageHashOrIntent::Hash {
456                hash: Fr::from(42u64),
457            },
458            MessageHashOrIntent::Intent {
459                caller: AztecAddress(Fr::from(1u64)),
460                call: FunctionCall {
461                    to: AztecAddress(Fr::from(2u64)),
462                    selector: FunctionSelector::from_hex("0xaabbccdd").expect("valid"),
463                    args: vec![],
464                    function_type: FunctionType::Private,
465                    is_static: false,
466                },
467            },
468            MessageHashOrIntent::InnerHash {
469                consumer: AztecAddress(Fr::from(3u64)),
470                inner_hash: Fr::from(999u64),
471            },
472        ];
473
474        for variant in variants {
475            let json = serde_json::to_string(&variant).expect("serialize");
476            let decoded: MessageHashOrIntent = serde_json::from_str(&json).expect("deserialize");
477            assert_eq!(decoded, variant);
478        }
479    }
480
481    #[test]
482    fn chain_info_serde_roundtrip() {
483        let info = ChainInfo {
484            chain_id: Fr::from(31337u64),
485            version: Fr::from(1u64),
486        };
487        let json = serde_json::to_string(&info).expect("serialize");
488        let decoded: ChainInfo = serde_json::from_str(&json).expect("deserialize");
489        assert_eq!(decoded, info);
490    }
491}