use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const NONCE_FILE_LEN_V1: usize = 1 + 8;
const NONCE_FORMAT_V1: u8 = 1;
#[derive(Debug, Clone)]
pub struct PersistentProducerNonce {
nonce: u64,
#[allow(dead_code)] path: PathBuf,
}
impl PersistentProducerNonce {
pub fn load_or_create(path: impl AsRef<Path>) -> io::Result<Self> {
let path = path.as_ref().to_path_buf();
match fs::read(&path) {
Ok(bytes) => {
if bytes.len() != NONCE_FILE_LEN_V1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"producer-nonce file at {} has length {} (expected {} for v1)",
path.display(),
bytes.len(),
NONCE_FILE_LEN_V1,
),
));
}
if bytes[0] != NONCE_FORMAT_V1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"producer-nonce file at {} has unknown version byte 0x{:02x} \
(expected 0x{:02x} for v1)",
path.display(),
bytes[0],
NONCE_FORMAT_V1,
),
));
}
let mut buf = [0u8; 8];
buf.copy_from_slice(&bytes[1..]);
let nonce = u64::from_le_bytes(buf);
Ok(Self { nonce, path })
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {
Self::create_new(path)
}
Err(e) => Err(e),
}
}
fn create_new(path: PathBuf) -> io::Result<Self> {
use std::hash::{Hash, Hasher};
use std::time::Instant;
let wall_nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let mono_marker = format!("{:?}", Instant::now());
let pid = std::process::id() as u64;
let stack_local: u64 = wall_nanos;
let stack_marker = (&stack_local as *const u64) as usize;
let mut tid_hasher = std::collections::hash_map::DefaultHasher::new();
std::thread::current().id().hash(&mut tid_hasher);
let tid = tid_hasher.finish();
use std::hash::BuildHasher;
let os_entropy_a = std::collections::hash_map::RandomState::new().hash_one(0u64);
let os_entropy_b = std::collections::hash_map::RandomState::new().hash_one(0u64);
let mut hash_input = [0u8; 64];
hash_input[..8].copy_from_slice(&wall_nanos.to_le_bytes());
hash_input[8..16].copy_from_slice(&pid.to_le_bytes());
hash_input[16..24].copy_from_slice(&(stack_marker as u64).to_le_bytes());
hash_input[24..32].copy_from_slice(&tid.to_le_bytes());
let mono_bytes = mono_marker.as_bytes();
let n = mono_bytes.len().min(16);
hash_input[32..32 + n].copy_from_slice(&mono_bytes[..n]);
hash_input[48..56].copy_from_slice(&os_entropy_a.to_le_bytes());
hash_input[56..64].copy_from_slice(&os_entropy_b.to_le_bytes());
let mut nonce = xxhash_rust::xxh3::xxh3_64(&hash_input);
if nonce == 0 {
nonce = 1;
}
let mut buf = [0u8; NONCE_FILE_LEN_V1];
buf[0] = NONCE_FORMAT_V1;
buf[1..].copy_from_slice(&nonce.to_le_bytes());
let tmp_path = {
use std::hash::{Hash, Hasher};
let mut p = path.clone();
let mut name = p.file_name().map(|n| n.to_os_string()).unwrap_or_default();
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let mut tid_hasher = std::collections::hash_map::DefaultHasher::new();
std::thread::current().id().hash(&mut tid_hasher);
let tid = tid_hasher.finish();
name.push(format!(".{pid}.{tid:x}.{nanos}.{nonce:x}.tmp"));
p.set_file_name(name);
p
};
let _ = fs::remove_file(&tmp_path);
{
use std::io::Write;
let mut f = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)?;
f.write_all(&buf)?;
f.sync_all()?;
}
fs::rename(&tmp_path, &path)?;
Ok(Self { nonce, path })
}
#[inline]
pub fn nonce(&self) -> u64 {
self.nonce
}
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_path(suffix: &str) -> PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
p.push(format!("net-test-nonce-{pid}-{nanos}-{suffix}"));
p
}
#[test]
fn first_load_creates_a_random_nonzero_nonce() {
let path = temp_path("first");
let nonce = PersistentProducerNonce::load_or_create(&path)
.unwrap()
.nonce();
assert_ne!(nonce, 0, "first-load must sample a nonzero nonce");
let _ = fs::remove_file(&path);
}
#[test]
fn second_load_returns_the_same_nonce() {
let path = temp_path("second");
let first = PersistentProducerNonce::load_or_create(&path)
.unwrap()
.nonce();
let second = PersistentProducerNonce::load_or_create(&path)
.unwrap()
.nonce();
assert_eq!(
first, second,
"second load against same path must return the same nonce — \
this is the load-bearing cross-restart property",
);
let _ = fs::remove_file(&path);
}
#[test]
fn corrupt_file_surfaces_invalid_data_error() {
let path = temp_path("corrupt");
fs::write(&path, b"shorty!").unwrap();
let err = PersistentProducerNonce::load_or_create(&path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("length 7"),
"error message should pin the actual length; got: {err}",
);
let _ = fs::remove_file(&path);
}
#[test]
fn missing_parent_directory_surfaces_not_found_error() {
let mut path = temp_path("missing-parent");
path.push("subdir-that-does-not-exist");
path.push("nonce");
let err = PersistentProducerNonce::load_or_create(&path).unwrap_err();
assert!(
err.kind() == io::ErrorKind::NotFound
|| err.kind() == io::ErrorKind::PermissionDenied
|| err.kind() == io::ErrorKind::Other,
"expected a clear filesystem error; got {err:?}",
);
}
#[test]
fn back_to_back_nonces_in_same_thread_differ_via_os_entropy() {
let mut nonces = std::collections::HashSet::new();
for i in 0..32 {
let path = temp_path(&format!("os_entropy_{i}"));
let nonce = PersistentProducerNonce::load_or_create(&path)
.unwrap()
.nonce();
assert!(
nonces.insert(nonce),
"regression: back-to-back nonces must differ — same-thread \
same-instant calls have identical predictable inputs, so \
OS-random entropy is the only thing that varies. \
collision at i={i}, nonce={nonce}",
);
let _ = fs::remove_file(&path);
}
}
#[test]
fn two_distinct_paths_produce_two_distinct_nonces() {
let a = temp_path("a");
let b = temp_path("b");
let n_a = PersistentProducerNonce::load_or_create(&a).unwrap().nonce();
let n_b = PersistentProducerNonce::load_or_create(&b).unwrap().nonce();
assert_ne!(
n_a, n_b,
"two distinct nonce paths must produce distinct nonces (collision \
probability is ~2^-63 — if this fires twice, suspect getrandom)",
);
let _ = fs::remove_file(&a);
let _ = fs::remove_file(&b);
}
#[test]
fn concurrent_first_load_does_not_corrupt_or_fail() {
use std::sync::Arc;
use std::thread;
const N: usize = 16;
let path = Arc::new(temp_path("concurrent-first-load"));
let barrier = Arc::new(std::sync::Barrier::new(N));
let mut handles = Vec::with_capacity(N);
for _ in 0..N {
let path = Arc::clone(&path);
let barrier = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
barrier.wait();
PersistentProducerNonce::load_or_create(&*path)
.expect("concurrent first-load must succeed")
.nonce()
}));
}
let nonces: Vec<u64> = handles
.into_iter()
.map(|h| h.join().expect("worker must not panic"))
.collect();
assert!(
nonces.iter().all(|&n| n != 0),
"every concurrent first-loader must observe a non-zero nonce, \
got: {nonces:?}",
);
let on_disk = fs::read(&*path).expect("path must exist after concurrent first-load");
assert_eq!(
on_disk.len(),
NONCE_FILE_LEN_V1,
"on-disk nonce must be exactly {} bytes (no interleaved-write corruption)",
NONCE_FILE_LEN_V1,
);
let post_load = PersistentProducerNonce::load_or_create(&*path)
.unwrap()
.nonce();
assert!(
nonces.contains(&post_load),
"post-load nonce {post_load:#x} must match one of the in-race \
samples {nonces:?} — anything else implies corruption",
);
let _ = fs::remove_file(&*path);
}
#[test]
fn cr28_legacy_8_byte_file_is_rejected() {
let path = temp_path("legacy-8byte");
let stale: u64 = 0xDEAD_BEEF_CAFE_F00D;
fs::write(&path, stale.to_le_bytes()).unwrap();
let err = PersistentProducerNonce::load_or_create(&path).unwrap_err();
assert_eq!(
err.kind(),
io::ErrorKind::InvalidData,
"legacy 8-byte file must surface InvalidData (CR-28 dropped v0 support)"
);
assert!(
err.to_string().contains("length 8"),
"error message should pin the rejected length; got: {err}"
);
let _ = fs::remove_file(&path);
}
#[test]
fn cr28_v1_versioned_9_byte_file_round_trip() {
let path = temp_path("v1-roundtrip");
let expected: u64 = 0xDEAD_BEEF_CAFE_F00D;
let mut bytes = Vec::with_capacity(9);
bytes.push(NONCE_FORMAT_V1);
bytes.extend_from_slice(&expected.to_le_bytes());
fs::write(&path, &bytes).unwrap();
let loaded = PersistentProducerNonce::load_or_create(&path)
.unwrap()
.nonce();
assert_eq!(
loaded, expected,
"CR-28: v1 file format is [VERSION=1][8 LE bytes]. Pin so a \
future refactor that flips byte order or drops the version \
byte doesn't silently produce a different nonce."
);
let _ = fs::remove_file(&path);
}
#[test]
fn cr28_unknown_version_byte_surfaces_invalid_data() {
let path = temp_path("v-unknown");
let mut bytes = Vec::with_capacity(9);
bytes.push(0xFF);
bytes.extend_from_slice(&0xDEAD_BEEFu64.to_le_bytes());
fs::write(&path, &bytes).unwrap();
let err = PersistentProducerNonce::load_or_create(&path).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("0xff") || err.to_string().contains("0xFF"),
"error message must name the unknown version byte; got: {err}"
);
let _ = fs::remove_file(&path);
}
#[test]
fn cr28_create_new_writes_v1_format() {
let path = temp_path("v1-fresh");
let _ = PersistentProducerNonce::load_or_create(&path).unwrap();
let on_disk = fs::read(&path).unwrap();
assert_eq!(
on_disk.len(),
NONCE_FILE_LEN_V1,
"CR-28: freshly-created nonce file must be v1 (9 bytes); got {} bytes",
on_disk.len()
);
assert_eq!(
on_disk[0], NONCE_FORMAT_V1,
"CR-28: freshly-created nonce file must carry version byte 0x{:02x}; got 0x{:02x}",
NONCE_FORMAT_V1, on_disk[0]
);
let _ = fs::remove_file(&path);
}
}