#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::missing_panics_doc,
clippy::redundant_clone,
clippy::cast_possible_truncation,
clippy::doc_markdown,
clippy::needless_borrows_for_generic_args
)]
use ant_node::replication::commitment::{
commitment_hash, leaf_hash, sign_commitment, verify_commitment_signature, MerkleTree,
StorageCommitment,
};
use ant_node::replication::commitment_state::{BuiltCommitment, ResponderCommitmentState};
use ant_node::replication::config::AUDIT_SPOTCHECK_COUNT;
use ant_node::replication::subtree::{
build_subtree_proof, nonced_leaf_hash, select_spotcheck_indices, select_subtree_path,
verify_subtree_proof, StructureVerdict, SubtreeProof,
};
use rand::Rng;
use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaPublicKey, MlDsaSecretKey};
fn keypair() -> (MlDsaPublicKey, MlDsaSecretKey) {
ml_dsa_65().generate_keypair().unwrap()
}
fn content(i: u32) -> Vec<u8> {
let mut v = key(i).to_vec();
v.extend_from_slice(b"subtree-audit-chunk-body");
v.extend_from_slice(&i.to_le_bytes());
v
}
fn content_hash(i: u32) -> [u8; 32] {
*blake3::hash(&content(i)).as_bytes()
}
fn key(i: u32) -> [u8; 32] {
let mut k = [0u8; 32];
k[..4].copy_from_slice(&i.to_be_bytes());
k
}
struct Responder {
state: ResponderCommitmentState,
public_key: MlDsaPublicKey,
secret_key: MlDsaSecretKey,
peer_id_bytes: [u8; 32],
}
impl Responder {
fn new() -> Self {
let (public_key, secret_key) = keypair();
let peer_id_bytes = *blake3::hash(&public_key.to_bytes()).as_bytes();
Self {
state: ResponderCommitmentState::new(),
public_key,
secret_key,
peer_id_bytes,
}
}
fn commit_to_range(&self, n: u32) -> [u8; 32] {
let entries: Vec<_> = (0..n).map(|i| (key(i), content_hash(i))).collect();
let built = BuiltCommitment::build(
entries,
&self.peer_id_bytes,
&self.secret_key,
&self.public_key.to_bytes(),
)
.unwrap();
let h = built.hash();
self.state.rotate(built);
h
}
}
fn honest_bytes(k: &[u8; 32]) -> Option<Vec<u8>> {
for i in 0..4096u32 {
if &key(i) == k {
return Some(content(i));
}
}
None
}
fn auditor_accepts(
challenged_peer_id: &[u8; 32],
expected_commitment_hash: &[u8; 32],
nonce: &[u8; 32],
commitment: &StorageCommitment,
proof: &SubtreeProof,
responder_serves: impl Fn(&[u8; 32]) -> Option<Vec<u8>>,
) -> Result<usize, AuditError> {
if commitment.sender_peer_id != *challenged_peer_id {
return Err(AuditError::SenderPeerIdMismatch);
}
let derived = *blake3::hash(&commitment.sender_public_key).as_bytes();
if derived != commitment.sender_peer_id {
return Err(AuditError::PeerIdKeyMismatch);
}
match commitment_hash(commitment) {
Some(h) if &h == expected_commitment_hash => {}
_ => return Err(AuditError::CommitmentHashMismatch),
}
if !verify_commitment_signature(commitment) {
return Err(AuditError::SignatureInvalid);
}
if let StructureVerdict::Invalid(why) = verify_subtree_proof(proof, nonce, commitment) {
return Err(AuditError::StructureInvalid(why));
}
let spot = random_sample_indices(
proof.leaves.len(),
AUDIT_SPOTCHECK_COUNT.clamp(3, 5) as usize,
);
if spot.is_empty() {
return Err(AuditError::StructureInvalid("empty spot-check sample"));
}
let mut checked = 0usize;
for idx in spot {
let leaf = proof
.leaves
.get(idx)
.ok_or(AuditError::StructureInvalid("spot index out of range"))?;
let Some(bytes) = responder_serves(&leaf.key) else {
return Err(AuditError::CommittedKeyUnserved);
};
let plain = *blake3::hash(&bytes).as_bytes();
let nonced = nonced_leaf_hash(nonce, &commitment.sender_peer_id, &leaf.key, &bytes);
if leaf.bytes_hash != plain || leaf.nonced_hash != nonced {
return Err(AuditError::RealBytesMismatch);
}
checked += 1;
}
Ok(checked)
}
fn random_sample_indices(n: usize, count: usize) -> Vec<usize> {
if n == 0 {
return Vec::new();
}
let want = count.min(n);
let mut rng = rand::thread_rng();
let mut picked = std::collections::BTreeSet::new();
while picked.len() < want {
picked.insert(rng.gen_range(0..n));
}
picked.into_iter().collect()
}
#[derive(Debug, PartialEq, Eq)]
enum AuditError {
SenderPeerIdMismatch,
PeerIdKeyMismatch,
CommitmentHashMismatch,
SignatureInvalid,
StructureInvalid(&'static str),
RealBytesMismatch,
CommittedKeyUnserved,
}
fn honest_proof_and_commitment(
r: &Responder,
nonce: &[u8; 32],
) -> (SubtreeProof, StorageCommitment) {
let built = r.state.current().unwrap();
let proof = build_subtree_proof(built.tree(), nonce, &r.peer_id_bytes, honest_bytes).unwrap();
(proof, built.commitment().clone())
}
#[test]
fn honest_responder_passes_audit() {
let nonce = [0xCD; 32];
let honest = Responder::new();
let pin = honest.commit_to_range(64);
let (proof, commitment) = honest_proof_and_commitment(&honest, &nonce);
let res = auditor_accepts(
&honest.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert!(res.is_ok(), "honest path must pass, got {res:?}");
assert!(res.unwrap() >= 1, "must byte-check at least one leaf");
}
#[test]
fn relay_holding_only_addresses_caught_by_real_bytes_check() {
let nonce = [0x77; 32];
let honest_keyset = Responder::new();
let pin = honest_keyset.commit_to_range(100);
let built = honest_keyset.state.current().unwrap();
let path = select_subtree_path(&nonce, built.commitment().key_count).unwrap();
let mut leaves = Vec::new();
for idx in path.leaf_start..path.leaf_end {
let k = built.tree().key_at(idx as usize).unwrap();
let forged_nonced = *blake3::hash(b"i-do-not-have-the-bytes").as_bytes();
leaves.push(ant_node::replication::subtree::SubtreeLeaf {
key: k,
bytes_hash: content_hash(idx),
nonced_hash: forged_nonced,
});
}
let plan = ant_node::replication::subtree::subtree_plan(built.tree(), &nonce).unwrap();
let forged = SubtreeProof {
leaves,
sibling_cut_hashes: plan.sibling_cut_hashes,
};
assert_eq!(
verify_subtree_proof(&forged, &nonce, built.commitment()),
StructureVerdict::Valid,
"address-only proof rebuilds the root (structure cannot bind possession)"
);
let res = auditor_accepts(
&honest_keyset.peer_id_bytes,
&pin,
&nonce,
built.commitment(),
&forged,
|_k| None, );
assert_eq!(
res,
Err(AuditError::CommittedKeyUnserved),
"a relay that cannot serve sampled bytes must fail round 2, got {res:?}"
);
let res = auditor_accepts(
&honest_keyset.peer_id_bytes,
&pin,
&nonce,
built.commitment(),
&forged,
|_k| Some(b"fabricated-not-the-chunk".to_vec()),
);
assert_eq!(
res,
Err(AuditError::RealBytesMismatch),
"fabricated served bytes must fail the content-address check, got {res:?}"
);
}
#[test]
fn predict_and_fetch_relay_is_caught_by_fresh_random_sample() {
let r = Responder::new();
let n: u32 = 400;
let pin = r.commit_to_range(n);
let built = r.state.current().unwrap();
let mut escaped = 0u32;
let trials = 200u32;
for t in 0..trials {
let mut nonce = [0u8; 32];
nonce[..4].copy_from_slice(&t.to_le_bytes());
let plan = ant_node::replication::subtree::subtree_plan(built.tree(), &nonce).unwrap();
let path = select_subtree_path(&nonce, n).unwrap();
let mut leaves = Vec::new();
for idx in path.leaf_start..path.leaf_end {
let k = built.tree().key_at(idx as usize).unwrap();
leaves.push(ant_node::replication::subtree::SubtreeLeaf {
key: k,
bytes_hash: content_hash(idx),
nonced_hash: *blake3::hash(b"forged").as_bytes(),
});
}
let forged = SubtreeProof {
leaves,
sibling_cut_hashes: plan.sibling_cut_hashes,
};
let predicted: std::collections::HashSet<[u8; 32]> =
select_spotcheck_indices(&nonce, &path, AUDIT_SPOTCHECK_COUNT.clamp(3, 5))
.into_iter()
.filter_map(|i| forged.leaves.get(i as usize).map(|l| l.key))
.collect();
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
built.commitment(),
&forged,
|k| {
if predicted.contains(k) {
(0..n).find(|&i| &key(i) == k).map(content)
} else {
None
}
},
);
if res.is_ok() {
escaped += 1;
}
}
assert!(
escaped <= trials / 20,
"fresh-random sampling let the predict-and-fetch relay pass too often: \
{escaped}/{trials} (the §1 fix should make this ~0)"
);
}
#[test]
fn fabricated_fraction_is_caught_when_a_forged_leaf_is_sampled() {
let nonce = [0x31; 32];
let r = Responder::new();
let pin = r.commit_to_range(400);
let (mut proof, commitment) = honest_proof_and_commitment(&r, &nonce);
for leaf in &mut proof.leaves {
leaf.nonced_hash[0] ^= 0xFF;
}
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert_eq!(
res,
Err(AuditError::RealBytesMismatch),
"a forged leaf landing under the byte challenge must fail, got {res:?}"
);
}
#[test]
fn relay_unable_to_serve_bytes_fails_deterministically_regardless_of_auditor_overlap() {
let nonce = [0x19; 32];
let r = Responder::new();
let pin = r.commit_to_range(100);
let (proof, commitment) = honest_proof_and_commitment(&r, &nonce);
let relay_serves_nothing = |_k: &[u8; 32]| -> Option<Vec<u8>> { None };
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
relay_serves_nothing,
);
assert_eq!(
res,
Err(AuditError::CommittedKeyUnserved),
"an unserved sampled key ⇒ deterministic confirmed failure, got {res:?}"
);
}
#[test]
fn fresh_commitment_substitution_rejected_by_pin() {
let nonce = [0xCD; 32];
let original = Responder::new();
let pinned_hash = original.commit_to_range(64);
let fresh_hash = original.commit_to_range(32);
assert_ne!(pinned_hash, fresh_hash);
let (proof, fresh_commitment) = honest_proof_and_commitment(&original, &nonce);
let res = auditor_accepts(
&original.peer_id_bytes,
&pinned_hash, &nonce,
&fresh_commitment,
&proof,
honest_bytes,
);
assert_eq!(
res,
Err(AuditError::CommitmentHashMismatch),
"fresh-commitment substitution must trip the pin, got {res:?}"
);
}
#[test]
fn cross_peer_commitment_substitution_rejected_by_sender_id() {
let nonce = [0xCD; 32];
let real_p = Responder::new();
let p_hash = real_p.commit_to_range(64);
let (p_proof, p_commitment) = honest_proof_and_commitment(&real_p, &nonce);
let q_peer_id = [0xCC; 32];
let res = auditor_accepts(
&q_peer_id, &p_hash,
&nonce,
&p_commitment, &p_proof,
honest_bytes,
);
assert_eq!(
res,
Err(AuditError::SenderPeerIdMismatch),
"cross-peer substitution must trip the sender-id binding, got {res:?}"
);
}
#[test]
#[allow(clippy::similar_names)]
fn throwaway_key_substitution_rejected_by_pubkey_binding() {
let nonce = [0xCD; 32];
let (p_pubkey, _p_secret) = keypair();
let p_peer_id = *blake3::hash(&p_pubkey.to_bytes()).as_bytes();
let (throwaway_pk, throwaway_sk) = keypair();
let throwaway_pk_bytes = throwaway_pk.to_bytes();
let entries: Vec<_> = (0..8u32).map(|i| (key(i), content_hash(i))).collect();
let tree = MerkleTree::build(entries).unwrap();
let root = tree.root();
let key_count = tree.key_count();
let sig = sign_commitment(
&throwaway_sk,
&root,
key_count,
&p_peer_id, &throwaway_pk_bytes,
)
.unwrap();
let bad_commit = StorageCommitment {
root,
key_count,
sender_peer_id: p_peer_id,
sender_public_key: throwaway_pk_bytes,
signature: sig,
};
let pin = commitment_hash(&bad_commit).unwrap();
let proof = build_subtree_proof(&tree, &nonce, &p_peer_id, honest_bytes).unwrap();
let res = auditor_accepts(&p_peer_id, &pin, &nonce, &bad_commit, &proof, honest_bytes);
assert_eq!(
res,
Err(AuditError::PeerIdKeyMismatch),
"throwaway-key attack must trip the peer-id↔key binding, got {res:?}"
);
}
#[test]
fn wrong_signer_rejected_at_signature_gate() {
let nonce = [0xCD; 32];
let responder = Responder::new();
responder.commit_to_range(16);
let (proof, commitment) = honest_proof_and_commitment(&responder, &nonce);
let (wrong_pk, _wrong_sk) = keypair();
let wrong_pk_bytes = wrong_pk.to_bytes();
let wrong_peer_id = *blake3::hash(&wrong_pk_bytes).as_bytes();
let mut bad_commit = commitment.clone();
bad_commit.sender_public_key = wrong_pk_bytes;
bad_commit.sender_peer_id = wrong_peer_id;
let new_pin = commitment_hash(&bad_commit).unwrap();
let res = auditor_accepts(
&wrong_peer_id,
&new_pin,
&nonce,
&bad_commit,
&proof,
honest_bytes,
);
assert_eq!(
res,
Err(AuditError::SignatureInvalid),
"swapped embedded key must trip the signature gate, got {res:?}"
);
}
#[test]
fn audit_response_replay_blocked_by_fresh_nonce() {
let old_nonce = [0xCD; 32];
let fresh_nonce = [0xEF; 32];
let r = Responder::new();
let pin = r.commit_to_range(256);
let (stale_proof, commitment) = honest_proof_and_commitment(&r, &old_nonce);
assert_eq!(
verify_subtree_proof(&stale_proof, &old_nonce, &commitment),
StructureVerdict::Valid
);
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&fresh_nonce, &commitment,
&stale_proof,
honest_bytes,
);
assert!(
matches!(res, Err(AuditError::StructureInvalid(_))),
"replay under a fresh nonce must fail the structural gate, got {res:?}"
);
}
#[test]
fn tampered_cut_hash_rejected() {
let nonce = [0x0B; 32];
let r = Responder::new();
let pin = r.commit_to_range(256);
let (mut proof, commitment) = honest_proof_and_commitment(&r, &nonce);
assert!(
!proof.sibling_cut_hashes.is_empty(),
"a 256-leaf tree selects a deep subtree with cut-hashes"
);
if let Some(c) = proof.sibling_cut_hashes.first_mut() {
c[0] ^= 0x01;
}
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert!(
matches!(res, Err(AuditError::StructureInvalid(_))),
"tampered cut-hash must fail structure, got {res:?}"
);
}
#[test]
fn wrong_leaf_count_rejected() {
let nonce = [0x0C; 32];
let r = Responder::new();
let pin = r.commit_to_range(100);
let (mut proof, commitment) = honest_proof_and_commitment(&r, &nonce);
proof.leaves.pop();
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert_eq!(
res,
Err(AuditError::StructureInvalid("wrong leaf count")),
"dropped leaf must fail the leaf-count check, got {res:?}"
);
}
#[test]
fn reordered_leaves_rejected() {
let nonce = [0x0D; 32];
let r = Responder::new();
let pin = r.commit_to_range(100);
let (mut proof, commitment) = honest_proof_and_commitment(&r, &nonce);
assert!(proof.leaves.len() >= 2);
proof.leaves.swap(0, 1);
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert!(
matches!(res, Err(AuditError::StructureInvalid(_))),
"reordered leaves must fail structure, got {res:?}"
);
}
#[test]
fn tampered_leaf_bytes_hash_rejected() {
let nonce = [0x0E; 32];
let r = Responder::new();
let pin = r.commit_to_range(100);
let (mut proof, commitment) = honest_proof_and_commitment(&r, &nonce);
proof.leaves[0].bytes_hash[0] ^= 0x01;
let res = auditor_accepts(
&r.peer_id_bytes,
&pin,
&nonce,
&commitment,
&proof,
honest_bytes,
);
assert!(
matches!(res, Err(AuditError::StructureInvalid(_))),
"tampered bytes_hash must fail structure, got {res:?}"
);
}
#[test]
fn repudiating_a_gossiped_pin_is_detectable_via_lookup_miss() {
let r = Responder::new();
let state = &r.state;
let h1 = r.commit_to_range(8);
state.mark_gossiped(h1);
assert!(
state.lookup_by_hash(&h1).is_some(),
"gossiped pin must be answerable immediately"
);
let h2 = r.commit_to_range(16);
state.mark_gossiped(h2);
assert!(
state.lookup_by_hash(&h1).is_some(),
"a gossiped commitment must survive one rotation (no false repudiation)"
);
let h3 = r.commit_to_range(24);
state.mark_gossiped(h3);
assert!(
state.lookup_by_hash(&h1).is_none(),
"h1 aged out of the gossip window"
);
assert!(state.lookup_by_hash(&h2).is_some());
assert!(state.lookup_by_hash(&h3).is_some());
let r2 = Responder::new();
let ungossiped = r2.commit_to_range(8);
assert!(r2.state.lookup_by_hash(&ungossiped).is_some());
let _next = r2.commit_to_range(16); assert!(
r2.state.lookup_by_hash(&ungossiped).is_none(),
"an ungossiped commitment is dropped on the next rotation"
);
}
#[test]
fn commitment_hash_is_field_sensitive() {
let (pk, sk) = keypair();
let pk_bytes = pk.to_bytes();
let sig = sign_commitment(&sk, &[0; 32], 1, &[0; 32], &pk_bytes).unwrap();
let c1 = StorageCommitment {
root: [0; 32],
key_count: 1,
sender_peer_id: [0; 32],
sender_public_key: pk_bytes,
signature: sig,
};
let h1 = commitment_hash(&c1).unwrap();
for mutate in 0..5u8 {
let mut c = c1.clone();
match mutate {
0 => c.root[0] ^= 1,
1 => c.key_count += 1,
2 => c.sender_peer_id[0] ^= 1,
3 => c.signature[0] ^= 1,
4 => c.sender_public_key[0] ^= 1,
_ => unreachable!(),
}
let h = commitment_hash(&c).unwrap();
assert_ne!(h, h1, "mutation {mutate} should change commitment_hash");
}
}
#[test]
fn leaf_hash_binds_key_and_bytes() {
let h1 = leaf_hash(&key(1), &content_hash(1));
let h2 = leaf_hash(&key(1), &content_hash(2));
let h3 = leaf_hash(&key(2), &content_hash(1));
assert_ne!(h1, h2);
assert_ne!(h1, h3);
assert_ne!(h2, h3);
}
#[test]
fn signature_round_trips_correctly() {
let (pk1, sk1) = keypair();
let (pk2, _sk2) = keypair();
let pk1_bytes = pk1.to_bytes();
let pk2_bytes = pk2.to_bytes();
let sig = sign_commitment(&sk1, &[7; 32], 42, &[3; 32], &pk1_bytes).unwrap();
let c = StorageCommitment {
root: [7; 32],
key_count: 42,
sender_peer_id: [3; 32],
sender_public_key: pk1_bytes,
signature: sig,
};
assert!(verify_commitment_signature(&c));
let mut c2 = c.clone();
c2.sender_public_key = pk2_bytes;
assert!(!verify_commitment_signature(&c2));
}
#[test]
fn nonced_leaf_hash_binds_all_inputs() {
let base = nonced_leaf_hash(&[1; 32], &[2; 32], &key(3), b"chunk");
assert_ne!(
base,
nonced_leaf_hash(&[9; 32], &[2; 32], &key(3), b"chunk")
);
assert_ne!(
base,
nonced_leaf_hash(&[1; 32], &[9; 32], &key(3), b"chunk")
);
assert_ne!(
base,
nonced_leaf_hash(&[1; 32], &[2; 32], &key(9), b"chunk")
);
assert_ne!(
base,
nonced_leaf_hash(&[1; 32], &[2; 32], &key(3), b"other")
);
}