use blake3::Hasher;
use saorsa_pqc::api::sig::{ml_dsa_65, MlDsaPublicKey, MlDsaSignature, MlDsaVariant};
use serde::{Deserialize, Serialize};
pub const DOMAIN_COMMITMENT: &[u8] = b"autonomi.ant.replication.storage_commitment.v1";
pub const DOMAIN_COMMITMENT_HASH: &[u8] = b"autonomi.ant.replication.commitment_hash.v1";
pub const MAX_COMMITMENT_KEY_COUNT: u32 = 1_000_000;
pub const MAX_COMMITMENT_SIDECAR_BYTES: usize = 8 * 1024;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct StorageCommitment {
pub root: [u8; 32],
pub key_count: u32,
pub sender_peer_id: [u8; 32],
pub sender_public_key: Vec<u8>,
pub signature: Vec<u8>,
}
#[must_use]
pub fn commitment_hash(c: &StorageCommitment) -> Option<[u8; 32]> {
let serialized = postcard::to_allocvec(c).ok()?;
let mut h = Hasher::new();
h.update(DOMAIN_COMMITMENT_HASH);
h.update(&serialized);
Some(*h.finalize().as_bytes())
}
fn commitment_signed_payload(
root: &[u8; 32],
key_count: u32,
sender_peer_id: &[u8; 32],
sender_public_key: &[u8],
) -> Vec<u8> {
let mut v = Vec::with_capacity(32 + 4 + 32 + 4 + sender_public_key.len());
v.extend_from_slice(root);
v.extend_from_slice(&key_count.to_le_bytes());
v.extend_from_slice(sender_peer_id);
let pk_len = u32::try_from(sender_public_key.len()).unwrap_or(u32::MAX);
v.extend_from_slice(&pk_len.to_le_bytes());
v.extend_from_slice(sender_public_key);
v
}
#[must_use]
pub fn verify_commitment_signature(c: &StorageCommitment) -> bool {
let Ok(public_key) = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &c.sender_public_key)
else {
return false;
};
let payload = commitment_signed_payload(
&c.root,
c.key_count,
&c.sender_peer_id,
&c.sender_public_key,
);
let Ok(sig) = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &c.signature) else {
return false;
};
ml_dsa_65()
.verify_with_context(&public_key, &payload, &sig, DOMAIN_COMMITMENT)
.unwrap_or(false)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use saorsa_pqc::api::sig::ml_dsa_65;
fn signed_commitment(root: [u8; 32], key_count: u32, peer_id: [u8; 32]) -> StorageCommitment {
let (pk, sk) = ml_dsa_65().generate_keypair().unwrap();
let pk_bytes = pk.to_bytes();
let payload = commitment_signed_payload(&root, key_count, &peer_id, &pk_bytes);
let sig = ml_dsa_65()
.sign_with_context(&sk, &payload, DOMAIN_COMMITMENT)
.unwrap();
StorageCommitment {
root,
key_count,
sender_peer_id: peer_id,
sender_public_key: pk_bytes,
signature: sig.to_bytes(),
}
}
#[test]
fn verify_rejects_malformed_without_panic() {
let c = StorageCommitment {
root: [0u8; 32],
key_count: 1,
sender_peer_id: [0u8; 32],
sender_public_key: vec![0u8; 10],
signature: vec![0u8; 10],
};
assert!(!verify_commitment_signature(&c));
assert!(commitment_hash(&c).is_some());
}
#[test]
fn commitment_hash_is_deterministic_and_field_sensitive() {
let c = StorageCommitment {
root: [1u8; 32],
key_count: 5,
sender_peer_id: [2u8; 32],
sender_public_key: vec![3u8; 20],
signature: vec![4u8; 20],
};
let h1 = commitment_hash(&c).unwrap();
let h2 = commitment_hash(&c).unwrap();
assert_eq!(h1, h2, "same commitment -> same pin");
let mut c2 = c.clone();
c2.key_count = 6;
assert_ne!(
commitment_hash(&c2).unwrap(),
h1,
"changing key_count must change the pin"
);
}
#[test]
fn verify_accepts_a_correctly_signed_commitment() {
let c = signed_commitment([7u8; 32], 42, [9u8; 32]);
assert!(
verify_commitment_signature(&c),
"a genuinely signed commitment must verify"
);
}
#[test]
fn verify_rejects_any_field_tampered_after_signing() {
let mut c = signed_commitment([1u8; 32], 100, [2u8; 32]);
c.key_count = c.key_count.wrapping_add(1);
assert!(
!verify_commitment_signature(&c),
"tampered key_count must fail"
);
let mut c = signed_commitment([1u8; 32], 100, [2u8; 32]);
c.root[0] ^= 0xff;
assert!(!verify_commitment_signature(&c), "tampered root must fail");
let mut c = signed_commitment([1u8; 32], 100, [2u8; 32]);
c.sender_peer_id[0] ^= 0xff;
assert!(
!verify_commitment_signature(&c),
"tampered peer_id must fail"
);
let mut c = signed_commitment([1u8; 32], 100, [2u8; 32]);
let (other_pk, _) = ml_dsa_65().generate_keypair().unwrap();
c.sender_public_key = other_pk.to_bytes();
assert!(
!verify_commitment_signature(&c),
"swapping the embedded pubkey must fail"
);
}
}