use cyber_hemera::field::{Goldilocks, P};
use cyber_hemera::permutation::{permute, permute_ct};
use cyber_hemera::{Hasher, derive_key, hash, hash_secret, keyed_hash, reference};
fn n() -> usize {
std::env::var("HEMERA_DIFF_N")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(if cfg!(debug_assertions) {
10_000
} else {
1_000_000
})
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn elem(&mut self) -> u64 {
match self.next() % 16 {
0 => [0, 1, P - 1, P, P + 1, u64::MAX, 1 << 32, (1 << 32) - 1]
[(self.next() % 8) as usize],
1 => P + self.next() % (u64::MAX - P + 1),
2 => self.next() >> 32,
_ => self.next(),
}
}
fn bytes(&mut self, max: u64) -> Vec<u8> {
let len = (self.next() % (max + 1)) as usize;
(0..len).map(|_| self.next() as u8).collect()
}
}
fn parallel(total: usize, f: impl Fn(u64, usize) + Sync) {
let threads = std::thread::available_parallelism().map_or(4, |n| n.get());
let per = total.div_ceil(threads);
std::thread::scope(|sc| {
for t in 0..threads {
let f = &f;
let count = per.min(total.saturating_sub(t * per));
sc.spawn(move || f(t as u64 * 0x1000_0001 + 3, count));
}
});
}
#[test]
fn ct_kernel_equals_fast_kernel_bit_for_bit() {
let total = n();
parallel(total, |seed, count| {
let mut r = Rng(seed);
for _ in 0..count {
let raw: [u64; 16] = match r.next() % 64 {
0 => [0; 16],
1 => [P; 16],
_ => core::array::from_fn(|_| r.elem()),
};
let mut fast = raw.map(Goldilocks::new);
let mut ct = fast;
permute(&mut fast);
permute_ct(&mut ct);
assert_eq!(ct, fast, "permute_ct differs on {raw:x?}");
}
});
println!("permute_ct == permute (raw limbs): {total} random states");
}
fn ref_sponge(mut st: [u64; 16], msg: &[u8], absorbed: u64) -> [u64; 16] {
let absorb = |st: &mut [u64; 16], block: &[u8]| {
for i in 0..8 {
let mut b = [0u8; 8];
b[..7].copy_from_slice(&block[7 * i..7 * i + 7]);
st[i] = reference::add(st[i], u64::from_le_bytes(b));
}
};
let mut chunks = msg.chunks_exact(56);
for c in &mut chunks {
absorb(&mut st, c);
reference::permute(&mut st);
}
let rem = chunks.remainder();
let mut last = [0u8; 56];
last[..rem.len()].copy_from_slice(rem);
last[rem.len()] = 0x01;
absorb(&mut st, &last);
st[10] = absorbed;
reference::permute(&mut st);
st
}
fn digest(st: &[u64; 16]) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0..4 {
out[8 * i..8 * i + 8].copy_from_slice(&(st[i] % P).to_le_bytes());
}
out
}
fn domain(d: u64) -> [u64; 16] {
let mut st = [0u64; 16];
st[11] = d;
st
}
#[test]
fn secret_entry_points_match_reference() {
let total = n() / 10;
parallel(total, |seed, count| {
let mut r = Rng(seed ^ 0x5EC);
for _ in 0..count {
let msg = r.bytes(200);
let want = digest(&ref_sponge(domain(0), &msg, msg.len() as u64));
assert_eq!(*hash_secret(&msg).as_bytes(), want);
assert_eq!(*hash(&msg).as_bytes(), want);
let key: [u8; 32] = core::array::from_fn(|_| r.next() as u8);
let data = r.bytes(150);
let joined: Vec<u8> = key.iter().chain(&data).copied().collect();
let want = digest(&ref_sponge(domain(1), &joined, joined.len() as u64));
assert_eq!(*keyed_hash(&key, &data).as_bytes(), want);
let ctx: String = (0..(r.next() % 80))
.map(|_| char::from(b'a' + (r.next() % 26) as u8))
.collect();
let material = r.bytes(150);
let c = ref_sponge(domain(2), ctx.as_bytes(), ctx.len() as u64);
let mut st = domain(3);
for i in 0..4 {
st[i] = c[i] % P;
}
reference::permute(&mut st);
let want = digest(&ref_sponge(st, &material, material.len() as u64));
assert_eq!(derive_key(&ctx, &material), want);
let mut h = Hasher::new_secret();
h.update(&msg);
let mut out = [0u8; 64];
h.finalize_xof().fill(&mut out);
let mut st = ref_sponge(domain(0), &msg, msg.len() as u64);
assert_eq!(out[..32], digest(&st));
reference::permute(&mut st);
assert_eq!(out[32..], digest(&st));
}
});
println!("hash_secret / keyed_hash / derive_key / secret XOF: {total} random inputs each");
}