use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use tracing::{Level, error, info, span};
pub static GLOBAL_POR_METRICS: PorMetrics = PorMetrics::new();
pub struct PorMetrics {
pub audits_total: AtomicU64,
pub failures_total: AtomicU64,
}
impl PorMetrics {
#[must_use]
pub const fn new() -> Self {
Self {
audits_total: AtomicU64::new(0),
failures_total: AtomicU64::new(0),
}
}
pub fn record_audit(&self, valid: bool) {
self.audits_total.fetch_add(1, Ordering::Relaxed);
if !valid {
self.failures_total.fetch_add(1, Ordering::Relaxed);
}
}
#[must_use]
pub fn snapshot(&self) -> PorMetricsSnapshot {
PorMetricsSnapshot {
audits_total: self.audits_total.load(Ordering::Relaxed),
failures_total: self.failures_total.load(Ordering::Relaxed),
}
}
pub fn reset(&self) {
self.audits_total.store(0, Ordering::Relaxed);
self.failures_total.store(0, Ordering::Relaxed);
}
}
impl Default for PorMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PorMetricsSnapshot {
pub audits_total: u64,
pub failures_total: u64,
}
impl fmt::Display for PorMetricsSnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"por_audits={} por_failures={}",
self.audits_total, self.failures_total,
)
}
}
const POR_WITNESS_DOMAIN: &str = "fsqlite:por:witness:v1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PorChallenge {
pub page_indices: Vec<u32>,
pub nonce: [u8; 32],
}
impl PorChallenge {
#[must_use]
pub fn from_seed(seed: u64, total_pages: u32, challenge_size: u32) -> Self {
let effective_size = challenge_size.min(total_pages);
let mut hasher = blake3::Hasher::new();
hasher.update(POR_WITNESS_DOMAIN.as_bytes());
hasher.update(b":nonce:");
hasher.update(&seed.to_le_bytes());
let nonce: [u8; 32] = *hasher.finalize().as_bytes();
let mut indices = Vec::with_capacity(effective_size as usize);
let mut idx_hasher = blake3::Hasher::new();
idx_hasher.update(POR_WITNESS_DOMAIN.as_bytes());
idx_hasher.update(b":indices:");
idx_hasher.update(&seed.to_le_bytes());
idx_hasher.update(&total_pages.to_le_bytes());
let mut reader = idx_hasher.finalize_xof();
let mut visited = std::collections::HashSet::with_capacity(effective_size as usize);
let mut buf = [0u8; 4];
while indices.len() < effective_size as usize {
reader.fill(&mut buf);
let candidate = u32::from_le_bytes(buf) % total_pages;
if visited.insert(candidate) {
indices.push(candidate);
}
}
indices.sort_unstable();
Self {
page_indices: indices,
nonce,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PorProof {
pub witness: [u8; 32],
}
pub fn compute_por_proof<F>(challenge: &PorChallenge, read_page: F) -> Option<PorProof>
where
F: Fn(u32) -> Option<Vec<u8>>,
{
let mut hasher = blake3::Hasher::new();
hasher.update(POR_WITNESS_DOMAIN.as_bytes());
hasher.update(b":proof:");
hasher.update(&challenge.nonce);
for &page_idx in &challenge.page_indices {
let page_data = read_page(page_idx)?;
hasher.update(&page_idx.to_le_bytes());
hasher.update(&page_data);
}
Some(PorProof {
witness: *hasher.finalize().as_bytes(),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PorAuditResult {
pub valid: bool,
pub challenge_size: u32,
pub duration_us: u64,
}
pub fn run_por_audit<P, V>(
seed: u64,
total_pages: u32,
challenge_size: u32,
prover_read: P,
verifier_read: V,
) -> PorAuditResult
where
P: Fn(u32) -> Option<Vec<u8>>,
V: Fn(u32) -> Option<Vec<u8>>,
{
let start = Instant::now();
let challenge = PorChallenge::from_seed(seed, total_pages, challenge_size);
let challenge_len = u32::try_from(challenge.page_indices.len()).unwrap_or(0);
let prover_proof = compute_por_proof(&challenge, prover_read);
let verifier_proof = compute_por_proof(&challenge, verifier_read);
let valid = match (prover_proof, verifier_proof) {
(Some(p), Some(v)) => p.witness == v.witness,
_ => false,
};
let elapsed = start.elapsed();
#[allow(clippy::cast_possible_truncation)] let duration_us = elapsed.as_micros().min(u128::from(u64::MAX)) as u64;
let _span = span!(
Level::INFO,
"por_audit",
challenge_size = challenge_len,
proof_valid = valid,
audit_duration_us = duration_us,
)
.entered();
GLOBAL_POR_METRICS.record_audit(valid);
if valid {
info!(
seed = seed,
challenge_size = challenge_len,
duration_us = duration_us,
"PoR audit passed"
);
} else {
error!(
seed = seed,
challenge_size = challenge_len,
duration_us = duration_us,
"PoR audit FAILED — storage may be corrupted"
);
}
PorAuditResult {
valid,
challenge_size: challenge_len,
duration_us,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pages(count: u32) -> Vec<Vec<u8>> {
(0..count)
.map(|i| {
let mut page = vec![0u8; 4096];
for (j, b) in page.iter_mut().enumerate() {
#[allow(clippy::cast_possible_truncation)]
{
*b = ((i as usize * 37 + j) % 256) as u8;
}
}
page
})
.collect()
}
#[test]
fn test_challenge_deterministic() {
let c1 = PorChallenge::from_seed(42, 100, 10);
let c2 = PorChallenge::from_seed(42, 100, 10);
assert_eq!(c1, c2, "same seed should produce identical challenges");
}
#[test]
fn test_challenge_different_seeds_differ() {
let c1 = PorChallenge::from_seed(1, 100, 10);
let c2 = PorChallenge::from_seed(2, 100, 10);
assert_ne!(
c1.page_indices, c2.page_indices,
"different seeds should produce different challenges"
);
assert_ne!(c1.nonce, c2.nonce);
}
#[test]
fn test_challenge_size_capped_at_total_pages() {
let c = PorChallenge::from_seed(42, 5, 100);
assert_eq!(
c.page_indices.len(),
5,
"challenge size capped at total_pages"
);
}
#[test]
fn test_challenge_no_duplicates() {
let c = PorChallenge::from_seed(42, 1000, 50);
let mut sorted = c.page_indices.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
sorted.len(),
c.page_indices.len(),
"no duplicate page indices"
);
}
#[test]
fn test_challenge_indices_sorted() {
let c = PorChallenge::from_seed(99, 500, 20);
let mut sorted = c.page_indices.clone();
sorted.sort_unstable();
assert_eq!(c.page_indices, sorted, "indices should be sorted");
}
#[test]
fn test_proof_matches_same_data() {
let pages = make_pages(10);
let challenge = PorChallenge::from_seed(42, 10, 5);
let proof1 = compute_por_proof(&challenge, |i| pages.get(i as usize).cloned());
let proof2 = compute_por_proof(&challenge, |i| pages.get(i as usize).cloned());
assert_eq!(proof1, proof2, "same data should produce same proof");
}
#[test]
fn test_proof_differs_on_corruption() {
let pages = make_pages(10);
let challenge = PorChallenge::from_seed(42, 10, 5);
let proof_clean = compute_por_proof(&challenge, |i| pages.get(i as usize).cloned());
let target_idx = challenge.page_indices[0];
let proof_corrupt = compute_por_proof(&challenge, |i| {
let mut p = pages.get(i as usize)?.clone();
if i == target_idx {
p[0] ^= 0xFF;
}
Some(p)
});
assert_ne!(
proof_clean, proof_corrupt,
"corruption should change the proof"
);
}
#[test]
fn test_proof_none_on_missing_page() {
let proof = compute_por_proof(
&PorChallenge::from_seed(1, 10, 5),
|_| None, );
assert!(proof.is_none(), "missing page should return None");
}
#[test]
fn test_audit_passes_same_data() {
let pages = make_pages(20);
let result = run_por_audit(
42,
20,
8,
|i| pages.get(i as usize).cloned(),
|i| pages.get(i as usize).cloned(),
);
assert!(result.valid, "audit should pass with identical data");
assert_eq!(result.challenge_size, 8);
}
#[test]
fn test_audit_fails_on_corruption() {
let pages = make_pages(20);
let result = run_por_audit(
42,
20,
8,
|i| {
let mut p = pages.get(i as usize)?.clone();
if i == 0 {
p[100] ^= 0x01; }
Some(p)
},
|i| pages.get(i as usize).cloned(),
);
let _ = result; }
#[test]
fn test_audit_fails_with_missing_pages() {
let pages = make_pages(10);
let result = run_por_audit(
42,
10,
5,
|_| None, |i| pages.get(i as usize).cloned(),
);
assert!(!result.valid, "audit should fail when prover has no data");
}
#[test]
fn test_por_metrics_record_and_snapshot() {
let m = PorMetrics::new();
m.record_audit(true);
m.record_audit(true);
m.record_audit(false);
let s = m.snapshot();
assert_eq!(s.audits_total, 3);
assert_eq!(s.failures_total, 1);
}
#[test]
fn test_por_metrics_reset() {
let m = PorMetrics::new();
m.record_audit(false);
m.reset();
let s = m.snapshot();
assert_eq!(s.audits_total, 0);
assert_eq!(s.failures_total, 0);
}
#[test]
fn test_por_metrics_display() {
let m = PorMetrics::new();
m.record_audit(true);
m.record_audit(false);
let text = format!("{}", m.snapshot());
assert!(text.contains("por_audits=2"));
assert!(text.contains("por_failures=1"));
}
#[test]
fn test_por_metrics_global_delta() {
let before = GLOBAL_POR_METRICS.snapshot();
GLOBAL_POR_METRICS.record_audit(true);
GLOBAL_POR_METRICS.record_audit(false);
let after = GLOBAL_POR_METRICS.snapshot();
assert_eq!(after.audits_total - before.audits_total, 2);
assert_eq!(after.failures_total - before.failures_total, 1);
}
}