use ark_babyjubjub::Fq;
use ark_ff::AdditiveGroup as _;
use crate::FieldElement;
const MAX_TAG_LEN_BYTES: usize = 31;
pub mod ds {
use super::{DomainSeparator, VariableLengthDomainSeparator};
pub const CREDENTIAL_V1: DomainSeparator<7> = DomainSeparator::new(b"POSEIDON2+EDDSA-BJJ");
pub const CREDENTIAL_SUB: DomainSeparator<2> = DomainSeparator::new(b"H_CS(id, r)");
pub const SESSION_COMMITMENT: DomainSeparator<2> = DomainSeparator::new(b"H(id, r)");
pub const AUTHENTICATOR_KEY_SET: DomainSeparator<14> = DomainSeparator::new(b"World ID PK");
pub const OPRF_QUERY: DomainSeparator<3> = DomainSeparator::new(b"World ID Query");
pub const OPRF_PROOF: DomainSeparator<3> = DomainSeparator::new(b"World ID Proof");
pub const OWNERSHIP_PROOF: DomainSeparator<3> = DomainSeparator::new(b"WIP103");
pub const TRUST_ANCHOR_KEY_TOKEN: DomainSeparator<7> =
DomainSeparator::new(b"WORLD_ID_TAKT_V1");
pub const CLAIMS_HASH_V1: VariableLengthDomainSeparator =
VariableLengthDomainSeparator::new(b"CLAIMS_HASH_V1");
pub const ASSOCIATED_DATA_V1: VariableLengthDomainSeparator =
VariableLengthDomainSeparator::new(b"ASSOCIATED_DATA_HASH_V1");
}
const fn checked_tag(tag: &'static [u8]) -> &'static [u8] {
assert!(
!tag.is_empty() && tag.len() <= MAX_TAG_LEN_BYTES && tag[0] != 0,
"a domain separator must be between 1 and 31 bytes, and not start with a zero byte"
);
tag
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DomainSeparator<const N: usize>(&'static [u8]);
impl<const N: usize> DomainSeparator<N> {
const ARITY_SUPPORTED: () = assert!(
N >= 1 && N <= 15,
"a domain-separated Poseidon2 hash takes between 1 and 15 inputs"
);
#[must_use]
pub const fn new(tag: &'static [u8]) -> Self {
Self(checked_tag(tag))
}
#[must_use]
pub const fn as_bytes(self) -> &'static [u8] {
self.0
}
#[must_use]
pub fn as_field_element(self) -> FieldElement {
FieldElement::from_be_bytes_mod_order(self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VariableLengthDomainSeparator(&'static [u8]);
impl VariableLengthDomainSeparator {
#[must_use]
pub const fn new(tag: &'static [u8]) -> Self {
Self(checked_tag(tag))
}
#[must_use]
pub const fn as_bytes(self) -> &'static [u8] {
self.0
}
}
#[must_use]
pub fn hash<const N: usize>(ds: DomainSeparator<N>, inputs: [FieldElement; N]) -> FieldElement {
let () = DomainSeparator::<N>::ARITY_SUPPORTED;
let ds = *ds.as_field_element();
match N {
1 => hash_padded(ds, &inputs, poseidon2::bn254::t2::permutation_in_place),
2 => hash_padded(ds, &inputs, poseidon2::bn254::t3::permutation_in_place),
3 => hash_padded(ds, &inputs, poseidon2::bn254::t4::permutation_in_place),
4..=7 => hash_padded(ds, &inputs, poseidon2::bn254::t8::permutation_in_place),
8..=11 => hash_padded(ds, &inputs, poseidon2::bn254::t12::permutation_in_place),
_ => hash_padded(ds, &inputs, poseidon2::bn254::t16::permutation_in_place),
}
}
#[must_use]
pub fn compress(left: FieldElement, right: FieldElement) -> FieldElement {
let mut state = poseidon2::bn254::t2::permutation(&[*left, *right]);
state[0] += *left;
state[0].into()
}
fn hash_padded<const T: usize>(
ds: Fq,
inputs: &[FieldElement],
permute: fn(&mut [Fq; T]),
) -> FieldElement {
debug_assert!(inputs.len() < T, "inputs do not fit the rate");
let mut state = [Fq::ZERO; T];
state[0] = ds;
for (slot, input) in state[1..].iter_mut().zip(inputs) {
*slot = **input;
}
permute(&mut state);
state[1].into()
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use ark_ff::AdditiveGroup as _;
use super::{DomainSeparator, FieldElement, Fq, MAX_TAG_LEN_BYTES, ds, hash};
const TEST_TAG: &[u8] = b"TEST_DS";
const TEST_DS: DomainSeparator<2> = DomainSeparator::new(TEST_TAG);
const ALL_TAGS: [&[u8]; 10] = [
ds::CREDENTIAL_V1.as_bytes(),
ds::CREDENTIAL_SUB.as_bytes(),
ds::SESSION_COMMITMENT.as_bytes(),
ds::AUTHENTICATOR_KEY_SET.as_bytes(),
ds::OPRF_QUERY.as_bytes(),
ds::OPRF_PROOF.as_bytes(),
ds::OWNERSHIP_PROOF.as_bytes(),
ds::TRUST_ANCHOR_KEY_TOKEN.as_bytes(),
ds::CLAIMS_HASH_V1.as_bytes(),
ds::ASSOCIATED_DATA_V1.as_bytes(),
];
#[test]
fn domain_separators_are_distinct_and_unreduced() {
let mut seen = HashSet::new();
for tag in ALL_TAGS {
assert!(!tag.is_empty() && tag.len() <= MAX_TAG_LEN_BYTES);
assert!(
seen.insert(FieldElement::from_be_bytes_mod_order(tag)),
"duplicate domain separator: {tag:?}"
);
}
}
#[test]
fn domain_separators_match_circuit_literals() {
assert_eq!(
ds::OPRF_QUERY.as_field_element(),
FieldElement::from(1_773_399_373_884_719_043_551_600_379_785_849_u128),
"see oprf_nullifier.circom / oprf_query.circom"
);
assert_eq!(
ds::AUTHENTICATOR_KEY_SET.as_field_element(),
FieldElement::from(105_702_839_725_298_824_521_994_315_u128),
);
}
#[test]
fn hash_matches_hand_rolled_layout() {
let inputs: [FieldElement; 15] = std::array::from_fn(|i| FieldElement::from(i as u64 + 1));
let ds_element = *FieldElement::from_be_bytes_mod_order(TEST_TAG);
macro_rules! assert_width {
($n:literal, $t:literal, $module:ident) => {{
let mut state = [Fq::ZERO; $t];
state[0] = ds_element;
for (slot, input) in state[1..].iter_mut().zip(&inputs[..$n]) {
*slot = **input;
}
poseidon2::bn254::$module::permutation_in_place(&mut state);
let expected = FieldElement::from(state[1]);
let actual = hash(
DomainSeparator::<$n>::new(TEST_TAG),
<[FieldElement; $n]>::try_from(&inputs[..$n]).unwrap(),
);
assert_eq!(actual, expected, "width {} mismatch", $t);
}};
}
assert_width!(2, 3, t3);
assert_width!(3, 4, t4);
assert_width!(7, 8, t8);
assert_width!(14, 16, t16);
assert_width!(1, 2, t2);
assert_width!(4, 8, t8);
assert_width!(6, 8, t8);
assert_width!(8, 12, t12);
assert_width!(11, 12, t12);
assert_width!(12, 16, t16);
assert_width!(15, 16, t16);
}
#[test]
fn hash_separates_domains_arities_and_inputs() {
let a = FieldElement::from(1u64);
let b = FieldElement::from(2u64);
let base = hash(TEST_DS, [a, b]);
assert_ne!(base, FieldElement::ZERO);
assert_eq!(base, hash(TEST_DS, [a, b]));
assert_ne!(base, hash(ds::CREDENTIAL_SUB, [a, b]));
assert_ne!(base, hash(TEST_DS, [b, a]));
}
}