#![no_std]
#![forbid(unsafe_code)]
#[cfg(feature = "std")]
extern crate std;
pub const MAX_PACKED: u8 = 243;
pub const SENTINEL_COUNT: usize = 256 - MAX_PACKED as usize;
pub const LEAVES: usize = MAX_PACKED as usize;
pub const TIERS: usize = 5;
pub const ROOT_SENTINEL: usize = LEAVES;
pub const PAYLOAD_BYTES: usize = LEAVES + 1;
pub const RADIX_243_POW_8: u64 = 12_157_665_459_056_928_801;
pub const DIGITS_PER_WORD: usize = 8;
pub const FNV1A64_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
pub const FNV1A64_PRIME: u64 = 0x0000_0100_0000_01b3;
const _: () = assert!(3usize.pow(TIERS as u32) == LEAVES);
const _: () = assert!(LEAVES + SENTINEL_COUNT == 256);
const _: () = assert!(SENTINEL_COUNT == 13);
const _: () = assert!((MAX_PACKED as u64).pow(DIGITS_PER_WORD as u32) == RADIX_243_POW_8);
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sentinel {
NullNode = 243,
MersenneOverflow = 244,
Tombstone = 245,
BusIndirect = 246,
Phase5 = 247,
Phase6 = 248,
Phase7 = 249,
Phase8 = 250,
Phase9 = 251,
Phase10 = 252,
Phase11 = 253,
Phase12 = 254,
Phase13 = 255,
}
const _: () = assert!(core::mem::size_of::<Sentinel>() == 1);
impl Sentinel {
#[inline(always)]
pub const fn from_u8(b: u8) -> Option<Self> {
match b {
243 => Some(Self::NullNode),
244 => Some(Self::MersenneOverflow),
245 => Some(Self::Tombstone),
246 => Some(Self::BusIndirect),
247 => Some(Self::Phase5),
248 => Some(Self::Phase6),
249 => Some(Self::Phase7),
250 => Some(Self::Phase8),
251 => Some(Self::Phase9),
252 => Some(Self::Phase10),
253 => Some(Self::Phase11),
254 => Some(Self::Phase12),
255 => Some(Self::Phase13),
_ => None,
}
}
}
#[inline(always)]
pub const fn fnv1a64_fold(mut hash: u64, bytes: &[u8]) -> u64 {
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u64;
hash = hash.wrapping_mul(FNV1A64_PRIME);
i += 1;
}
hash
}
#[inline(always)]
pub const fn xorshift64(mut x: u64) -> u64 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
x
}
#[repr(C, align(256))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Word256 {
pub hash: u64,
pub parent: u32,
pub trits: [u8; PAYLOAD_BYTES],
}
const _: () = assert!(core::mem::size_of::<Word256>() == 256);
const _: () = assert!(core::mem::align_of::<Word256>() == 256);
const _: () = assert!(core::mem::offset_of!(Word256, hash) == 0);
const _: () = assert!(core::mem::offset_of!(Word256, parent) == 8);
const _: () = assert!(core::mem::offset_of!(Word256, trits) == 12);
const _: () = assert!(core::mem::offset_of!(Word256, trits) + ROOT_SENTINEL == 255);
const _: () = assert!(core::mem::size_of::<PertaroExpert243>() == 256);
const _: () = assert!(core::mem::align_of::<PertaroExpert243>() == 256);
impl Word256 {
pub const ZERO: Self = Self {
hash: 0,
parent: 0,
trits: [0; PAYLOAD_BYTES],
};
}
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PertaroExpert243(pub Word256);
impl PertaroExpert243 {
#[inline(always)]
pub const fn seed(&self) -> u64 {
self.0.hash
}
#[inline(always)]
pub const fn parent(&self) -> u32 {
self.0.parent
}
#[inline(always)]
pub const fn root_sentinel_byte(&self) -> u8 {
self.0.trits[ROOT_SENTINEL]
}
#[inline(always)]
pub const fn root_sentinel(&self) -> Option<Sentinel> {
Sentinel::from_u8(self.0.trits[ROOT_SENTINEL])
}
#[inline(always)]
pub const fn leaves(&self) -> &[u8; LEAVES] {
match self.0.trits.first_chunk::<LEAVES>() {
Some(c) => c,
None => unreachable!(),
}
}
#[inline(always)]
pub const fn trie_index(path: [u8; TIERS]) -> usize {
let mut idx = 0usize;
let mut i = 0;
while i < TIERS {
assert!(path[i] < 3, "pertaro trie tier outside 0..3");
idx = idx * 3 + path[i] as usize;
i += 1;
}
idx
}
#[inline(always)]
pub const fn default_field(seed: u64, out: &mut [u8; LEAVES]) {
let mut s = if seed == 0 { FNV1A64_BASIS } else { seed };
let mut idx = 0usize;
while idx < LEAVES {
s = xorshift64(s);
if s >= RADIX_243_POW_8 {
continue;
}
let mut word = s;
let mut k = 0;
while k < DIGITS_PER_WORD && idx < LEAVES {
out[idx] = (word % MAX_PACKED as u64) as u8;
word /= MAX_PACKED as u64;
idx += 1;
k += 1;
}
}
}
#[inline(always)]
pub const fn materialize(&self) -> Word256 {
let mut base = [0u8; LEAVES];
Self::default_field(self.0.hash, &mut base);
let mut out = self.0;
let mut idx = 0usize;
while idx < LEAVES {
let raw = self.0.trits[idx];
out.trits[idx] = if raw < MAX_PACKED {
((base[idx] as u16 + raw as u16) % MAX_PACKED as u16) as u8
} else if raw == Sentinel::NullNode as u8 {
base[idx]
} else {
raw
};
idx += 1;
}
out
}
#[inline(always)]
pub const fn reground_seed(&self, winner: u8, next: Sentinel) -> u64 {
assert!(
winner < MAX_PACKED,
"pertaro reground winner outside the 243 leaves"
);
let h = fnv1a64_fold(FNV1A64_BASIS, &self.0.hash.to_le_bytes());
let h = fnv1a64_fold(h, &[winner, next as u8]);
xorshift64(h)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn word(seed: u64) -> Word256 {
Word256 {
hash: seed,
parent: 7,
trits: [0; PAYLOAD_BYTES],
}
}
#[test]
fn every_byte_is_a_coordinate_or_a_sentinel() {
for b in 0..MAX_PACKED {
assert!(Sentinel::from_u8(b).is_none(), "{b}");
}
for b in MAX_PACKED..=255 {
assert_eq!(Sentinel::from_u8(b).map(|s| s as u8), Some(b));
}
}
#[test]
fn default_field_is_deterministic_and_in_range() {
let mut a = [0u8; LEAVES];
let mut b = [0u8; LEAVES];
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF0, &mut a);
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF0, &mut b);
assert_eq!(a, b);
assert!(a.iter().all(|x| *x < MAX_PACKED));
let mut c = [0u8; LEAVES];
PertaroExpert243::default_field(0x1234_5678_9ABC_DEF1, &mut c);
assert_ne!(a, c);
}
#[test]
fn zero_seed_uses_the_basis() {
let mut z = [0u8; LEAVES];
let mut b = [0u8; LEAVES];
PertaroExpert243::default_field(0, &mut z);
PertaroExpert243::default_field(FNV1A64_BASIS, &mut b);
assert_eq!(z, b);
}
#[test]
fn radix_digits_are_balanced_over_many_seeds() {
let mut hist = [0u32; 243];
let mut f = [0u8; LEAVES];
for seed in 1..=2000u64 {
PertaroExpert243::default_field(seed.wrapping_mul(FNV1A64_PRIME), &mut f);
for d in f {
hist[d as usize] += 1;
}
}
let total: u32 = hist.iter().sum();
let mean = total / 243;
for (d, n) in hist.iter().enumerate() {
assert!(
*n > mean / 2 && *n < mean * 2,
"digit {d}: {n} vs mean {mean}"
);
}
}
#[test]
fn materialize_keeps_header_and_resolves_sentinels() {
let mut w = word(99);
w.trits[0] = 5;
w.trits[1] = Sentinel::NullNode as u8;
w.trits[2] = Sentinel::Tombstone as u8;
w.trits[3] = Sentinel::BusIndirect as u8;
w.trits[ROOT_SENTINEL] = Sentinel::Phase13 as u8;
let x = PertaroExpert243(w);
let out = x.materialize();
let mut base = [0u8; LEAVES];
PertaroExpert243::default_field(99, &mut base);
assert_eq!(out.hash, 99);
assert_eq!(out.parent, 7);
assert_eq!(out.trits[0], ((base[0] as u16 + 5) % 243) as u8);
assert_eq!(out.trits[1], base[1]);
assert_eq!(out.trits[2], Sentinel::Tombstone as u8);
assert_eq!(out.trits[3], Sentinel::BusIndirect as u8);
assert_eq!(out.trits[ROOT_SENTINEL], Sentinel::Phase13 as u8);
assert_eq!(x.root_sentinel(), Some(Sentinel::Phase13));
assert_eq!(x.leaves()[0], 5);
}
#[test]
fn trie_index_spans_all_leaves() {
assert_eq!(PertaroExpert243::trie_index([0; 5]), 0);
assert_eq!(PertaroExpert243::trie_index([2; 5]), LEAVES - 1);
assert_eq!(PertaroExpert243::trie_index([1, 0, 0, 0, 0]), 81);
assert_eq!(PertaroExpert243::trie_index([0, 0, 0, 0, 1]), 1);
}
#[test]
#[should_panic]
fn trie_index_refuses_a_bad_tier() {
let _ = PertaroExpert243::trie_index([0, 0, 3, 0, 0]);
}
#[test]
fn reground_is_deterministic_and_phase_sensitive() {
let x = PertaroExpert243(word(1));
let a = x.reground_seed(0, Sentinel::NullNode);
assert_eq!(a, x.reground_seed(0, Sentinel::NullNode));
assert_ne!(a, x.reground_seed(1, Sentinel::NullNode));
assert_ne!(a, x.reground_seed(0, Sentinel::Tombstone));
assert_ne!(a, 0);
}
#[test]
#[should_panic]
fn reground_refuses_a_sentinel_as_winner() {
let _ = PertaroExpert243(word(1)).reground_seed(243, Sentinel::NullNode);
}
}