//! ledger.rs — WORM append-only audit ledger //! //! Every entailment attestation is sealed into an append-only chain. //! Each record contains: //! - BLAKE3 hash of the attestation payload //! - BLAKE3 hash of the previous record (chain link) //! - The raw bincode payload //! //! The chain is stored as a length-prefixed binary flat file. //! A corrupt or tampered record breaks the chain hash and is immediately detectable. use std::io::{self, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::sync::Arc; use tokio::sync::mpsc; /// One sealed record in the WORM chain. #[derive(Debug)] pub struct LedgerRecord { pub sequence: u64, pub prev_hash: [u8; 32], pub payload_hash: [u8; 32], pub payload: Vec, } impl LedgerRecord { /// Serialise to bytes: [seq 8B][prev 32B][hash 32B][len 8B][payload]. fn to_bytes(&self) -> Vec { let mut out = Vec::with_capacity(80 + self.payload.len()); out.extend_from_slice(&self.sequence.to_le_bytes()); out.extend_from_slice(&self.prev_hash); out.extend_from_slice(&self.payload_hash); out.extend_from_slice(&(self.payload.len() as u64).to_le_bytes()); out.extend_from_slice(&self.payload); out } } /// Append-only WORM ledger backed by a flat binary file. pub struct WormLedger { file: std::fs::File, sequence: u64, last_hash: [u8; 32], } impl WormLedger { pub fn open(path: impl AsRef) -> io::Result { let path = path.as_ref(); if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } let mut file = std::fs::OpenOptions::new() .create(true) .append(true) .read(true) .open(path)?; // Replay existing records to find last hash and sequence number. let (sequence, last_hash) = Self::replay(&mut file)?; log::info!( "[ledger] opened {} — {} existing records", path.display(), sequence ); Ok(Self { file, sequence, last_hash }) } fn replay(file: &mut std::fs::File) -> io::Result<(u64, [u8; 32])> { file.seek(SeekFrom::Start(0))?; let mut seq: u64 = 0; let mut last: [u8; 32] = [0u8; 32]; loop { let mut seq_buf = [0u8; 8]; match file.read_exact(&mut seq_buf) { Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, Err(e) => return Err(e), Ok(_) => {} } let mut prev = [0u8; 32]; let mut hash = [0u8; 32]; file.read_exact(&mut prev)?; file.read_exact(&mut hash)?; let mut len_buf = [0u8; 8]; file.read_exact(&mut len_buf)?; let len = u64::from_le_bytes(len_buf) as usize; let mut payload = vec![0u8; len]; file.read_exact(&mut payload)?; seq = u64::from_le_bytes(seq_buf) + 1; last = hash; } Ok((seq, last)) } /// Append a sealed record. Returns the receipt (sequence + hash). pub fn append(&mut self, payload_hash: [u8; 32], payload: Vec) -> io::Result<(u64, [u8; 32])> { let record = LedgerRecord { sequence: self.sequence, prev_hash: self.last_hash, payload_hash, payload, }; let bytes = record.to_bytes(); self.file.write_all(&bytes)?; self.file.flush()?; self.last_hash = payload_hash; let seq = self.sequence; self.sequence += 1; Ok((seq, payload_hash)) } } // ── Background WORM worker ─────────────────────────────────────────────────── /// Runs as a dedicated Tokio task. /// Receives (hash, payload) from the inference loop and appends to the ledger. /// Does not block the GPU batching thread. pub async fn run_ledger_worker( mut rx: mpsc::Receiver<([u8; 32], Vec)>, ledger_path: String, ) { let mut ledger = WormLedger::open(&ledger_path) .unwrap_or_else(|e| panic!("cannot open ledger {}: {}", ledger_path, e)); while let Some((hash, payload)) = rx.recv().await { match ledger.append(hash, payload) { Ok((seq, _)) => log::debug!("[ledger] sealed record seq={}", seq), Err(e) => log::error!("[ledger] write fault: {}", e), } } log::info!("[ledger] worker shutting down"); }