use crate::schedule::FramePlan;
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StateHash(u64);
impl StateHash {
#[must_use]
pub const fn new() -> Self {
Self(OFFSET_BASIS)
}
#[must_use]
pub const fn absorb_bytes(mut self, bytes: &[u8]) -> Self {
let mut index = 0;
while index < bytes.len() {
#[allow(clippy::cast_lossless)]
let byte = bytes[index] as u64;
self.0 = (self.0 ^ byte).wrapping_mul(PRIME);
index += 1;
}
self
}
#[must_use]
pub const fn absorb_u64(self, value: u64) -> Self {
self.absorb_bytes(&value.to_le_bytes())
}
#[must_use]
pub const fn absorb_u32(self, value: u32) -> Self {
self.absorb_bytes(&value.to_le_bytes())
}
#[must_use]
pub const fn absorb_f32_bits(self, value: f32) -> Self {
self.absorb_u32(value.to_bits())
}
#[must_use]
pub const fn absorb_plan(self, plan: &FramePlan) -> Self {
self.absorb_u64(plan.first_tick())
.absorb_u32(plan.step_count())
.absorb_u64(plan.dropped())
.absorb_u64(plan.remainder().get())
.absorb_u64(plan.dt().nanos().get())
}
#[must_use]
pub const fn finish(self) -> u64 {
self.0
}
}
impl Default for StateHash {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::StateHash;
use crate::schedule::FrameLoop;
use crate::time::{StepBudget, Timestamp, Timestep};
use core::num::NonZeroU64;
#[test]
fn the_published_reference_vector_matches() {
assert_eq!(StateHash::new().finish(), 0xcbf2_9ce4_8422_2325);
assert_eq!(
StateHash::new().absorb_bytes(b"a").finish(),
0xaf63_dc4c_8601_ec8c
);
assert_eq!(
StateHash::new().absorb_bytes(b"foobar").finish(),
0x8594_4171_f739_67e8
);
}
#[test]
fn the_default_is_the_offset_basis() {
assert_eq!(StateHash::default(), StateHash::new());
}
#[test]
fn absorbing_nothing_changes_nothing() {
assert_eq!(StateHash::new().absorb_bytes(&[]), StateHash::new());
}
#[test]
fn absorption_order_is_part_of_the_digest() {
let forward = StateHash::new().absorb_u64(1).absorb_u64(2).finish();
let reversed = StateHash::new().absorb_u64(2).absorb_u64(1).finish();
assert_ne!(forward, reversed);
}
#[test]
fn width_is_part_of_the_digest() {
assert_ne!(
StateHash::new().absorb_u32(7).finish(),
StateHash::new().absorb_u64(7).finish()
);
assert_eq!(
StateHash::new().absorb_u32(7).finish(),
StateHash::new().absorb_bytes(&7u32.to_le_bytes()).finish()
);
}
#[test]
fn a_float_is_absorbed_by_its_bit_pattern() {
assert_eq!(
StateHash::new().absorb_f32_bits(1.5).finish(),
StateHash::new().absorb_u32(1.5f32.to_bits()).finish()
);
assert_ne!(
StateHash::new().absorb_f32_bits(0.0).finish(),
StateHash::new().absorb_f32_bits(-0.0).finish()
);
}
#[test]
fn a_plan_is_absorbed_field_by_field_in_the_documented_order() {
let mut frame = FrameLoop::new(
Timestep::HZ_60,
StepBudget::DEFAULT,
Timestamp::from_nanos(0),
);
let plan = frame.begin_frame(Timestamp::from_nanos(200_000_000));
let by_hand = StateHash::new()
.absorb_u64(plan.first_tick())
.absorb_u32(plan.step_count())
.absorb_u64(plan.dropped())
.absorb_u64(plan.remainder().get())
.absorb_u64(plan.dt().nanos().get());
assert_eq!(StateHash::new().absorb_plan(&plan), by_hand);
}
#[test]
fn every_absorbed_field_moves_the_digest() {
let mut frame = FrameLoop::new(
Timestep::HZ_60,
StepBudget::DEFAULT,
Timestamp::from_nanos(0),
);
let base = frame.begin_frame(Timestamp::from_nanos(200_000_000));
let next = frame.begin_frame(Timestamp::from_nanos(400_000_000));
assert_ne!(base.first_tick(), next.first_tick());
assert_ne!(
StateHash::new().absorb_plan(&base),
StateHash::new().absorb_plan(&next)
);
}
#[test]
fn two_timesteps_agreeing_in_every_other_field_do_not_share_a_digest() {
let plan_at = |step_nanos: u64| {
let dt = Timestep::from_nanos(NonZeroU64::new(step_nanos).expect("positive"));
let mut frame = FrameLoop::new(dt, StepBudget::DEFAULT, Timestamp::from_nanos(0));
frame.begin_frame(Timestamp::from_nanos(3 * step_nanos + 1_000_000))
};
let slow = plan_at(20_000_000);
let fast = plan_at(10_000_000);
assert_eq!(slow.first_tick(), fast.first_tick());
assert_eq!(slow.step_count(), fast.step_count());
assert_eq!(slow.dropped(), fast.dropped());
assert_eq!(slow.remainder().get(), fast.remainder().get());
assert_ne!(slow.dt().nanos(), fast.dt().nanos());
assert_ne!(
slow.remainder().get() * fast.dt().nanos().get(),
fast.remainder().get() * slow.dt().nanos().get(),
"cross-multiplied, the two ratios must differ"
);
assert_ne!(
StateHash::new().absorb_plan(&slow),
StateHash::new().absorb_plan(&fast),
"two plans differing only in timestep digested the same, so the oracle cannot see the loop's rate and alpha carries an input it does not cover"
);
}
#[test]
fn the_digest_is_computable_at_compile_time() {
const DIGEST: u64 = StateHash::new().absorb_u64(1).absorb_u32(2).finish();
assert_eq!(
DIGEST,
StateHash::new().absorb_u64(1).absorb_u32(2).finish()
);
}
}