use p3_field::{PrimeCharacteristicRing, PrimeField64};
use p3_goldilocks::{Goldilocks, default_goldilocks_poseidon2_12};
use p3_symmetric::Permutation;
use shake::{ExtendableOutput, Shake256, Update, XofReader};
pub(crate) const JV_SEED: [u8; 8] = *b"JV-SEED ";
pub(crate) const JV_RZK: [u8; 8] = *b"JV-RZK ";
pub(crate) const JV_POSN: [u8; 8] = *b"JV-POSN ";
pub(crate) const JV_FSCH: [u8; 8] = *b"JV-FSCH ";
pub(crate) const JV_WHIR: [u8; 8] = *b"JV-WHIR ";
pub(crate) const JV_OPEN: [u8; 8] = *b"JV-OPEN ";
pub(crate) const JV_OPRD: [u8; 8] = *b"JV-OPRD ";
pub(crate) const JV_OOD: [u8; 8] = *b"JV-OOD ";
const G_WHIR_U64: u64 = u64::from_le_bytes(JV_WHIR);
const _: () = assert!(
G_WHIR_U64 < Goldilocks::ORDER_U64,
"JV-WHIR tag bytes as u64-LE must fit canonically in Goldilocks"
);
fn h_vc_ivs() -> &'static ([Goldilocks; 4], [Goldilocks; 4]) {
use std::sync::OnceLock;
static CELL: OnceLock<([Goldilocks; 4], [Goldilocks; 4])> = OnceLock::new();
CELL.get_or_init(|| {
let g = Goldilocks::from_u64(G_WHIR_U64);
let z = Goldilocks::ZERO;
let leaf = [g, z, z, z];
let node = [g, Goldilocks::from_u64(1), z, z];
(leaf, node)
})
}
pub(crate) fn hash(tag: [u8; 8], inputs: &[&[u8]], out_len: usize) -> Vec<u8> {
let mut reader = shake256_xof(tag, inputs);
let mut out = vec![0u8; out_len];
reader.read(&mut out);
out
}
pub(crate) fn shake_field_elements(
tag: [u8; 8],
inputs: &[&[u8]],
count: usize,
) -> Vec<crate::field::Goldilocks4> {
if count == 0 {
return Vec::new();
}
let mut reader = shake256_xof(tag, inputs);
let mut out = Vec::with_capacity(count);
let mut chunk = [0u8; 32];
while out.len() < count {
reader.read(&mut chunk);
if let Some(g) = crate::field::Goldilocks4::from_bytes(&chunk) {
out.push(g);
}
}
out
}
pub(crate) fn shake256_xof(tag: [u8; 8], inputs: &[&[u8]]) -> impl XofReader + use<> {
let mut hasher = Shake256::default();
hasher.update(&tag);
for input in inputs {
hasher.update(&(input.len() as u64).to_le_bytes());
hasher.update(input);
}
hasher.finalize_xof()
}
const POSEIDON2_WIDTH: usize = 12;
const POSEIDON2_RATE: usize = 8;
#[cfg(test)]
const POSEIDON2_RATE_BYTES: usize = POSEIDON2_RATE * 8;
#[inline]
fn bytes_to_goldilocks(chunk: &[u8]) -> Goldilocks {
let raw = u64::from_le_bytes(chunk.try_into().unwrap());
let v = if raw >= Goldilocks::ORDER_U64 {
raw - Goldilocks::ORDER_U64
} else {
raw
};
Goldilocks::from_u64(v)
}
pub(crate) fn hash_vc_leaf(symbols: &[crate::field::Goldilocks4]) -> [u8; 32] {
let perm = default_goldilocks_poseidon2_12();
let (iv_leaf, _) = h_vc_ivs();
let mut state = [Goldilocks::ZERO; POSEIDON2_WIDTH];
state[POSEIDON2_RATE..].copy_from_slice(iv_leaf.as_slice());
let total_limbs = symbols.len() * 4;
let padded_limbs = total_limbs.div_ceil(POSEIDON2_RATE) * POSEIDON2_RATE;
let mut flat: Vec<Goldilocks> = Vec::with_capacity(padded_limbs);
for s in symbols {
flat.extend_from_slice(&s.c);
}
flat.resize(padded_limbs, Goldilocks::ZERO);
for chunk in flat.chunks_exact(POSEIDON2_RATE) {
for (i, &elem) in chunk.iter().enumerate() {
state[i] += elem;
}
perm.permute_mut(&mut state);
}
let mut out = [0u8; 32];
for (i, elem) in state[..4].iter().enumerate() {
out[i * 8..(i + 1) * 8].copy_from_slice(&elem.as_canonical_u64().to_le_bytes());
}
out
}
pub(crate) fn hash_vc_node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let perm = default_goldilocks_poseidon2_12();
let (_, iv_node) = h_vc_ivs();
let mut state = [Goldilocks::ZERO; POSEIDON2_WIDTH];
for (i, chunk) in left.chunks_exact(8).enumerate() {
state[i] = bytes_to_goldilocks(chunk);
}
for (i, chunk) in right.chunks_exact(8).enumerate() {
state[4 + i] = bytes_to_goldilocks(chunk);
}
state[POSEIDON2_RATE..].copy_from_slice(iv_node.as_slice());
perm.permute_mut(&mut state);
let mut out = [0u8; 32];
for (i, elem) in state[..4].iter().enumerate() {
out[i * 8..(i + 1) * 8].copy_from_slice(&elem.as_canonical_u64().to_le_bytes());
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xof_is_deterministic() {
let a = hash(JV_POSN, &[b"hello"], 32);
let b = hash(JV_POSN, &[b"hello"], 32);
assert_eq!(a, b);
}
#[test]
fn domain_tags_separate() {
let a = hash(JV_SEED, &[b"x"], 32);
let b = hash(JV_POSN, &[b"x"], 32);
assert_ne!(a, b);
}
#[test]
fn variable_output_length() {
let a = hash(JV_POSN, &[b"x"], 16);
let b = hash(JV_POSN, &[b"x"], 64);
assert_eq!(a.len(), 16);
assert_eq!(b.len(), 64);
assert_eq!(&a[..], &b[..16]);
}
#[test]
fn length_prefix_is_injective_under_concat() {
let a = hash(JV_POSN, &[b"abcd", b"ef"], 32);
let b = hash(JV_POSN, &[b"abc", b"def"], 32);
assert_ne!(a, b);
}
#[test]
fn spec_tags_are_present() {
assert_eq!(&JV_SEED, b"JV-SEED ");
assert_eq!(&JV_RZK, b"JV-RZK ");
assert_eq!(&JV_POSN, b"JV-POSN ");
assert_eq!(&JV_FSCH, b"JV-FSCH ");
assert_eq!(&JV_WHIR, b"JV-WHIR ");
assert_eq!(&JV_OPEN, b"JV-OPEN ");
assert_eq!(&JV_OPRD, b"JV-OPRD ");
assert_eq!(&JV_OOD, b"JV-OOD ");
let tags: [&[u8; 8]; 8] = [
&JV_SEED, &JV_RZK, &JV_POSN, &JV_FSCH, &JV_WHIR, &JV_OPEN, &JV_OPRD, &JV_OOD,
];
for i in 0..tags.len() {
for j in (i + 1)..tags.len() {
assert_ne!(tags[i], tags[j]);
}
}
}
#[test]
fn hash_vc_leaf_is_deterministic() {
use crate::field::Goldilocks4;
let leaf = vec![
Goldilocks4::new([
Goldilocks::new(1),
Goldilocks::new(2),
Goldilocks::new(3),
Goldilocks::new(4),
]),
Goldilocks4::new([
Goldilocks::new(5),
Goldilocks::new(6),
Goldilocks::new(7),
Goldilocks::new(8),
]),
];
let a = hash_vc_leaf(&leaf);
let b = hash_vc_leaf(&leaf);
assert_eq!(a, b);
}
#[test]
fn hash_vc_leaf_distinguishes_inputs() {
use crate::field::Goldilocks4;
let a = vec![Goldilocks4::new([Goldilocks::new(1); 4])];
let b = vec![Goldilocks4::new([Goldilocks::new(2); 4])];
assert_ne!(hash_vc_leaf(&a), hash_vc_leaf(&b));
}
#[test]
fn hash_vc_node_is_deterministic_and_input_sensitive() {
let l = [7u8; 32];
let r = [9u8; 32];
assert_eq!(hash_vc_node(&l, &r), hash_vc_node(&l, &r));
assert_ne!(hash_vc_node(&l, &r), hash_vc_node(&r, &l));
}
fn legacy_hash_arith(tag: [u8; 8], inputs: &[&[u8]]) -> [u8; 32] {
let total = 8 + inputs.iter().map(|x| 8 + x.len()).sum::<usize>();
let mut buf = Vec::with_capacity(total);
buf.extend_from_slice(&tag);
for input in inputs {
buf.extend_from_slice(&(input.len() as u64).to_le_bytes());
buf.extend_from_slice(input);
}
let padded_len = {
let raw = buf.len() + 1;
if raw.is_multiple_of(POSEIDON2_RATE_BYTES) {
raw
} else {
raw + (POSEIDON2_RATE_BYTES - raw % POSEIDON2_RATE_BYTES)
}
};
let mut padded = vec![0u8; padded_len];
padded[..buf.len()].copy_from_slice(&buf);
padded[buf.len()] = 0x01;
let perm = default_goldilocks_poseidon2_12();
let mut state = [Goldilocks::ZERO; POSEIDON2_WIDTH];
for chunk in padded.chunks_exact(POSEIDON2_RATE_BYTES) {
for (i, elem_bytes) in chunk.chunks_exact(8).enumerate() {
state[i] += bytes_to_goldilocks(elem_bytes);
}
perm.permute_mut(&mut state);
}
let mut out = [0u8; 32];
for (i, elem) in state[..4].iter().enumerate() {
out[i * 8..(i + 1) * 8].copy_from_slice(&elem.as_canonical_u64().to_le_bytes());
}
out
}
#[test]
fn hash_vc_leaf_differs_from_legacy_sponge() {
use crate::field::Goldilocks4;
let leaf: Vec<Goldilocks4> = (1u64..=4)
.map(|n| {
Goldilocks4::new([
Goldilocks::new(n),
Goldilocks::new(n + 100),
Goldilocks::new(n + 200),
Goldilocks::new(n + 300),
])
})
.collect();
let new_out = hash_vc_leaf(&leaf);
let mut leaf_buf = Vec::with_capacity(leaf.len() * 32);
for s in &leaf {
leaf_buf.extend_from_slice(&s.to_bytes());
}
let legacy_out = legacy_hash_arith(JV_WHIR, &[&leaf_buf]);
assert_ne!(
new_out, legacy_out,
"H_VC^leaf must differ from legacy sponge framing"
);
}
#[test]
fn hash_vc_node_differs_from_legacy_sponge() {
let left = [0x11u8; 32];
let right = [0x22u8; 32];
let new_out = hash_vc_node(&left, &right);
let legacy_out = legacy_hash_arith(JV_WHIR, &[&left, &right]);
assert_ne!(
new_out, legacy_out,
"H_VC^node must differ from legacy sponge framing"
);
}
#[test]
fn hash_vc_leaf_and_node_are_domain_separated() {
use crate::field::Goldilocks4;
let leaf_symbols = vec![
Goldilocks4::new([
Goldilocks::new(1),
Goldilocks::new(2),
Goldilocks::new(3),
Goldilocks::new(4),
]),
Goldilocks4::new([
Goldilocks::new(5),
Goldilocks::new(6),
Goldilocks::new(7),
Goldilocks::new(8),
]),
];
let leaf_out = hash_vc_leaf(&leaf_symbols);
let mut left = [0u8; 32];
let mut right = [0u8; 32];
for (i, v) in (1u64..=4).enumerate() {
left[i * 8..(i + 1) * 8].copy_from_slice(&v.to_le_bytes());
}
for (i, v) in (5u64..=8).enumerate() {
right[i * 8..(i + 1) * 8].copy_from_slice(&v.to_le_bytes());
}
let node_out = hash_vc_node(&left, &right);
assert_ne!(leaf_out, node_out, "IV domain separation failed");
}
}