use crate::atp::manifest::{GraphCommit, HashAlgorithm, MerkleRoot};
use crate::atp::object::ObjectId;
use crate::atp::proof::serde_types::{
SerializableContentId, SerializableGraphCommit, SerializableHashAlgorithm,
SerializableMerkleRoot, SerializableObjectId, SerializableVerificationEvidence,
};
use crate::atp::verifier::VerificationEvidence;
use crate::security::AuthKey;
use franken_decision::DecisionAuditEntry;
use franken_evidence::EvidenceLedger;
use franken_kernel::{DecisionId, TraceId};
use hmac::{Hmac, KeyInit, Mac};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ProofBundleVersion(pub u32);
impl ProofBundleVersion {
pub const CURRENT: Self = Self(1);
#[must_use]
pub const fn is_supported(self) -> bool {
self.0 <= Self::CURRENT.0
}
}
impl Default for ProofBundleVersion {
fn default() -> Self {
Self::CURRENT
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AtpProofBundle {
pub version: ProofBundleVersion,
pub created_at_micros: u64,
pub transfer_id: String,
pub metadata: AtpProofBundleMetadata,
pub manifest_root: MerkleRoot,
pub object_roots: Vec<ObjectId>,
pub commit_record: Option<GraphCommit>,
pub chunk_hash_algorithm: HashAlgorithm,
pub chunk_bitmap: ChunkBitmap,
pub verification_evidence: Vec<VerificationEvidence>,
pub raptorq_metadata: Option<RaptorQDecodeMetadata>,
pub repair_groups: Vec<RepairGroupMetadata>,
pub peer_identity: PeerIdentityInfo,
pub path_summary: TransferPathSummary,
pub journal: TransferJournal,
pub replay_pointers: BTreeMap<String, super::replay::AtpReplayPointer>,
pub extensions: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SerializableAtpProofBundle {
pub version: ProofBundleVersion,
pub created_at_micros: u64,
pub transfer_id: String,
pub metadata: AtpProofBundleMetadata,
pub manifest_root: SerializableMerkleRoot,
pub object_roots: Vec<SerializableObjectId>,
pub commit_record: Option<SerializableGraphCommit>,
pub chunk_hash_algorithm: SerializableHashAlgorithm,
pub chunk_bitmap: ChunkBitmap,
pub verification_evidence: Vec<SerializableVerificationEvidence>,
pub raptorq_metadata: Option<RaptorQDecodeMetadata>,
pub repair_groups: Vec<RepairGroupMetadata>,
pub peer_identity: PeerIdentityInfo,
pub path_summary: TransferPathSummary,
pub journal: TransferJournal,
pub replay_pointers: BTreeMap<String, super::replay::AtpReplayPointer>,
pub extensions: BTreeMap<String, serde_json::Value>,
}
impl From<&AtpProofBundle> for SerializableAtpProofBundle {
fn from(bundle: &AtpProofBundle) -> Self {
Self {
version: bundle.version,
created_at_micros: bundle.created_at_micros,
transfer_id: bundle.transfer_id.clone(),
metadata: bundle.metadata.clone(),
manifest_root: SerializableMerkleRoot::from(&bundle.manifest_root),
object_roots: bundle
.object_roots
.iter()
.map(SerializableObjectId::from)
.collect(),
commit_record: bundle
.commit_record
.as_ref()
.map(SerializableGraphCommit::from),
chunk_hash_algorithm: SerializableHashAlgorithm::from(&bundle.chunk_hash_algorithm),
chunk_bitmap: bundle.chunk_bitmap.clone(),
verification_evidence: bundle
.verification_evidence
.iter()
.map(SerializableVerificationEvidence::from)
.collect(),
raptorq_metadata: bundle.raptorq_metadata.clone(),
repair_groups: bundle.repair_groups.clone(),
peer_identity: bundle.peer_identity.clone(),
path_summary: bundle.path_summary.clone(),
journal: bundle.journal.clone(),
replay_pointers: bundle.replay_pointers.clone(),
extensions: bundle.extensions.clone(),
}
}
}
impl TryFrom<SerializableAtpProofBundle> for AtpProofBundle {
type Error = AtpProofBundleError;
fn try_from(bundle: SerializableAtpProofBundle) -> Result<Self, Self::Error> {
let verification_evidence = bundle
.verification_evidence
.into_iter()
.map(VerificationEvidence::try_from)
.collect::<Result<Vec<_>, _>>()
.map_err(AtpProofBundleError::InvalidVerificationEvidence)?;
Ok(Self {
version: bundle.version,
created_at_micros: bundle.created_at_micros,
transfer_id: bundle.transfer_id,
metadata: bundle.metadata,
manifest_root: MerkleRoot::from(bundle.manifest_root),
object_roots: bundle
.object_roots
.into_iter()
.map(ObjectId::from)
.collect(),
commit_record: None, chunk_hash_algorithm: HashAlgorithm::from(bundle.chunk_hash_algorithm),
chunk_bitmap: bundle.chunk_bitmap,
verification_evidence,
raptorq_metadata: bundle.raptorq_metadata,
repair_groups: bundle.repair_groups,
peer_identity: bundle.peer_identity,
path_summary: bundle.path_summary,
journal: bundle.journal,
replay_pointers: bundle.replay_pointers,
extensions: bundle.extensions,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AtpProofBundleMetadata {
pub description: String,
pub created_by: String,
pub required_proof_strength: ProofStrength,
pub require_repair_evidence: bool,
pub require_mailbox_evidence: bool,
pub verification_policies: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ProofStrength {
Basic,
Enhanced,
Cryptographic,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CryptographicSignature {
pub signer_id: String,
pub key_fingerprint: String,
pub signature: Vec<u8>,
pub signed_at_micros: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CryptographicSignatures {
pub signatures: Vec<CryptographicSignature>,
pub hash_algorithm: String,
pub bundle_hash: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChunkBitmap {
pub total_chunks: u64,
pub bitmap_data: Vec<u8>,
pub received_count: u64,
pub failed_chunks: BTreeSet<u64>,
}
impl ChunkBitmap {
#[must_use]
pub fn new(total_chunks: u64) -> Self {
let bitmap_bytes = (total_chunks + 7) / 8;
Self {
total_chunks,
bitmap_data: vec![0; bitmap_bytes as usize],
received_count: 0,
failed_chunks: BTreeSet::new(),
}
}
pub fn mark_received(&mut self, chunk_index: u64) {
if chunk_index < self.total_chunks {
let byte_index = (chunk_index / 8) as usize;
let bit_index = chunk_index % 8;
if byte_index < self.bitmap_data.len() {
let mask = 1u8 << bit_index;
if (self.bitmap_data[byte_index] & mask) == 0 {
self.bitmap_data[byte_index] |= mask; self.received_count += 1;
}
}
}
}
#[must_use]
pub fn is_received(&self, chunk_index: u64) -> bool {
if chunk_index < self.total_chunks {
let byte_index = (chunk_index / 8) as usize;
let bit_index = chunk_index % 8;
if byte_index < self.bitmap_data.len() {
let mask = 1u8 << bit_index;
return (self.bitmap_data[byte_index] & mask) != 0; }
}
false
}
#[must_use]
pub fn completion_ratio(&self) -> f64 {
if self.total_chunks == 0 {
1.0
} else {
self.received_count as f64 / self.total_chunks as f64
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RaptorQDecodeMetadata {
pub source_blocks: Vec<RaptorQSourceBlock>,
pub repair_symbols_received: u32,
pub repair_symbols_used: u32,
pub decode_success_rate: f64,
pub average_overhead_ratio: f64,
pub hard_regime_stats: Option<HardRegimeStats>,
pub proof_hash: Option<String>,
pub fallback_reasons: Vec<String>,
pub conformance_validation: Option<RaptorQConformanceResult>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RaptorQSourceBlock {
pub block_index: u32,
pub source_symbols: u32,
pub repair_symbols: u32,
pub decode_success: bool,
pub overhead_ratio: f64,
pub symbol_size: u32,
pub seed: u64,
pub k_prime_boundary: bool,
pub excess_repair_symbols: u32,
pub padding_truncated_bytes: u32,
pub random_loss_pattern: Option<Vec<u32>>,
pub corrupted_symbols: u32,
pub pivot_events: u32,
pub failure_reason: Option<String>,
pub block_proof_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HardRegimeStats {
pub regime_type: String,
pub loss_rate: f64,
pub burst_loss_events: u32,
pub tail_repair_activations: u32,
pub lossy_repair_activations: u32,
pub resume_repair_operations: u32,
pub relay_expensive_activations: u32,
pub mobile_unstable_activations: u32,
pub total_fallback_triggers: u32,
pub repair_roi: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RaptorQConformanceResult {
pub rfc6330_compliant: bool,
pub verified_guarantees: Vec<String>,
pub systematic_encoding_valid: bool,
pub repair_equation_correct: bool,
pub inactivation_decode_conformant: bool,
pub linear_algebra_valid: bool,
pub gf256_operations_correct: bool,
pub test_suite_version: String,
pub validated_at: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RepairGroupMetadata {
pub group_id: String,
pub covered_objects: Vec<SerializableObjectId>,
pub repair_strategy: String,
pub redundancy_factor: f64,
pub repair_activated: bool,
pub repair_completed_at: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PeerIdentityInfo {
pub source_peer_id: String,
pub destination_peer_id: String,
pub auth_method: String,
pub key_fingerprints: Vec<String>,
pub authenticated_at_micros: u64,
pub mutual_auth: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransferPathSummary {
pub primary_protocol: String,
pub fallback_protocols: Vec<String>,
pub rtt_millis: Option<f64>,
pub bandwidth_bps: Option<u64>,
pub relay_used: bool,
pub relay_nodes: Vec<String>,
pub path_setup_duration_millis: u64,
pub path_switches: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransferJournal {
pub digest: SerializableContentId,
pub format_version: u32,
pub entry_count: u64,
pub size_bytes: u64,
pub is_complete: bool,
pub created_at_micros: u64,
pub finalized_at_micros: Option<u64>,
}
pub const ATP_PROOF_FRANKEN_COMPONENT: &str = "atp.proof_bundle";
pub const ATP_PROOF_DECISION_CONTRACT: &str = "atp.proof_bundle_gate";
const ATP_PROOF_ACCEPT_ACTION: &str = "accept_proof_bundle";
const ATP_PROOF_QUARANTINE_ACTION: &str = "quarantine_proof_bundle";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AtpProofValidationStatus {
Accepted,
Quarantined {
reason: String,
},
}
impl AtpProofValidationStatus {
#[must_use]
pub const fn is_accepted(&self) -> bool {
matches!(self, Self::Accepted)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AtpAuditArtifactRef {
pub key: String,
pub artifact_kind: String,
pub schema: String,
pub digest: Option<String>,
pub replay_range: Option<(u64, u64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AtpFrankenProofExport {
pub decision_audit: DecisionAuditEntry,
pub evidence_ledger: EvidenceLedger,
pub audit_artifacts: Vec<AtpAuditArtifactRef>,
pub validation_status: AtpProofValidationStatus,
}
#[derive(Debug, Clone)]
pub struct AtpProofBundleBuilder {
transfer_id: String,
metadata: AtpProofBundleMetadata,
manifest_root: Option<MerkleRoot>,
object_roots: Vec<ObjectId>,
commit_record: Option<GraphCommit>,
chunk_hash_algorithm: HashAlgorithm,
chunk_bitmap: Option<ChunkBitmap>,
verification_evidence: Vec<VerificationEvidence>,
raptorq_metadata: Option<RaptorQDecodeMetadata>,
repair_groups: Vec<RepairGroupMetadata>,
peer_identity: Option<PeerIdentityInfo>,
path_summary: Option<TransferPathSummary>,
journal: Option<TransferJournal>,
replay_pointers: BTreeMap<String, super::replay::AtpReplayPointer>,
extensions: BTreeMap<String, serde_json::Value>,
}
impl AtpProofBundleBuilder {
#[must_use]
pub fn new(transfer_id: impl Into<String>) -> Self {
Self {
transfer_id: transfer_id.into(),
metadata: AtpProofBundleMetadata {
description: String::new(),
created_by: String::new(),
required_proof_strength: ProofStrength::Basic,
require_repair_evidence: false,
require_mailbox_evidence: false,
verification_policies: BTreeMap::new(),
},
manifest_root: None,
object_roots: Vec::new(),
commit_record: None,
chunk_hash_algorithm: HashAlgorithm::Sha256,
chunk_bitmap: None,
verification_evidence: Vec::new(),
raptorq_metadata: None,
repair_groups: Vec::new(),
peer_identity: None,
path_summary: None,
journal: None,
replay_pointers: BTreeMap::new(),
extensions: BTreeMap::new(),
}
}
pub fn metadata(mut self, metadata: AtpProofBundleMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn manifest_root(mut self, root: MerkleRoot) -> Self {
self.manifest_root = Some(root);
self
}
pub fn object_roots(mut self, roots: Vec<ObjectId>) -> Self {
self.object_roots = roots;
self
}
pub fn commit_record(mut self, commit: GraphCommit) -> Self {
self.commit_record = Some(commit);
self
}
pub fn chunk_hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
self.chunk_hash_algorithm = algorithm;
self
}
pub fn chunk_bitmap(mut self, bitmap: ChunkBitmap) -> Self {
self.chunk_bitmap = Some(bitmap);
self
}
pub fn add_verification_evidence(mut self, evidence: VerificationEvidence) -> Self {
self.verification_evidence.push(evidence);
self
}
pub fn raptorq_metadata(mut self, metadata: RaptorQDecodeMetadata) -> Self {
self.raptorq_metadata = Some(metadata);
self
}
pub fn add_repair_group(mut self, group: RepairGroupMetadata) -> Self {
self.repair_groups.push(group);
self
}
pub fn peer_identity(mut self, identity: PeerIdentityInfo) -> Self {
self.peer_identity = Some(identity);
self
}
pub fn path_summary(mut self, summary: TransferPathSummary) -> Self {
self.path_summary = Some(summary);
self
}
pub fn journal(mut self, journal: TransferJournal) -> Self {
self.journal = Some(journal);
self
}
pub fn add_replay_pointer(
mut self,
key: impl Into<String>,
pointer: super::replay::AtpReplayPointer,
) -> Self {
self.replay_pointers.insert(key.into(), pointer);
self
}
pub fn add_extension(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.extensions.insert(key.into(), value);
self
}
pub fn build(self) -> Result<AtpProofBundle, AtpProofBundleError> {
let manifest_root = self
.manifest_root
.ok_or(AtpProofBundleError::MissingRequiredField("manifest_root"))?;
let chunk_bitmap = self
.chunk_bitmap
.ok_or(AtpProofBundleError::MissingRequiredField("chunk_bitmap"))?;
let peer_identity = self
.peer_identity
.ok_or(AtpProofBundleError::MissingRequiredField("peer_identity"))?;
let path_summary = self
.path_summary
.ok_or(AtpProofBundleError::MissingRequiredField("path_summary"))?;
let journal = self
.journal
.ok_or(AtpProofBundleError::MissingRequiredField("journal"))?;
let now_micros =
system_time_micros_since_unix_epoch(SystemTime::now(), "created_at_micros")?;
Ok(AtpProofBundle {
version: ProofBundleVersion::CURRENT,
created_at_micros: now_micros,
transfer_id: self.transfer_id,
metadata: self.metadata,
manifest_root,
object_roots: self.object_roots,
commit_record: self.commit_record,
chunk_hash_algorithm: self.chunk_hash_algorithm,
chunk_bitmap,
verification_evidence: self.verification_evidence,
raptorq_metadata: self.raptorq_metadata,
repair_groups: self.repair_groups,
peer_identity,
path_summary,
journal,
replay_pointers: self.replay_pointers,
extensions: self.extensions,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AtpProofBundleError {
MissingRequiredField(&'static str),
UnsupportedVersion(ProofBundleVersion),
InsufficientProofStrength {
required: ProofStrength,
found: ProofStrength,
},
InvalidVerificationEvidence(String),
InvalidRaptorQMetadata(String),
InvalidRepairGroup(String),
InvalidPeerIdentity(String),
InvalidJournal(String),
InvalidReplayPointer(String),
InvalidSystemTime {
field: &'static str,
},
TimestampOutOfRange {
field: &'static str,
micros: u128,
},
SemanticValidationFailed(String),
}
impl fmt::Display for AtpProofBundleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingRequiredField(field) => {
write!(f, "missing required field: {field}")
}
Self::UnsupportedVersion(version) => {
write!(f, "unsupported proof bundle version: {}", version.0)
}
Self::InsufficientProofStrength { required, found } => {
write!(
f,
"insufficient proof strength: required {required:?}, found {found:?}"
)
}
Self::InvalidVerificationEvidence(msg) => {
write!(f, "invalid verification evidence: {msg}")
}
Self::InvalidRaptorQMetadata(msg) => {
write!(f, "invalid RaptorQ metadata: {msg}")
}
Self::InvalidRepairGroup(msg) => {
write!(f, "invalid repair group: {msg}")
}
Self::InvalidPeerIdentity(msg) => {
write!(f, "invalid peer identity: {msg}")
}
Self::InvalidJournal(msg) => {
write!(f, "invalid journal: {msg}")
}
Self::InvalidReplayPointer(msg) => {
write!(f, "invalid replay pointer: {msg}")
}
Self::InvalidSystemTime { field } => {
write!(f, "invalid system time while populating {field}")
}
Self::TimestampOutOfRange { field, micros } => {
write!(f, "timestamp for {field} is out of range: {micros} micros")
}
Self::SemanticValidationFailed(msg) => {
write!(f, "semantic validation failed: {msg}")
}
}
}
}
impl std::error::Error for AtpProofBundleError {}
fn duration_micros_to_u64(
duration: Duration,
field: &'static str,
) -> Result<u64, AtpProofBundleError> {
let micros = duration.as_micros();
u64::try_from(micros).map_err(|_| AtpProofBundleError::TimestampOutOfRange { field, micros })
}
fn system_time_micros_since_unix_epoch(
time: SystemTime,
field: &'static str,
) -> Result<u64, AtpProofBundleError> {
let duration = time
.duration_since(UNIX_EPOCH)
.map_err(|_| AtpProofBundleError::InvalidSystemTime { field })?;
duration_micros_to_u64(duration, field)
}
fn stable_franken_random_bits(domain: &[u8], bundle_hash: &[u8]) -> u128 {
let mut hasher = Sha256::new();
hasher.update(domain);
hasher.update(bundle_hash);
let digest = hasher.finalize();
let mut bytes = [0_u8; 16];
bytes.copy_from_slice(&digest[..16]);
u128::from_be_bytes(bytes)
}
impl AtpProofBundle {
pub fn to_json_bytes(&self) -> Result<Vec<u8>, AtpProofBundleError> {
let serializable = SerializableAtpProofBundle::from(self);
serde_json::to_vec(&serializable).map_err(|e| {
AtpProofBundleError::SemanticValidationFailed(format!("JSON serialization failed: {e}"))
})
}
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, AtpProofBundleError> {
let serializable: SerializableAtpProofBundle =
serde_json::from_slice(bytes).map_err(|e| {
AtpProofBundleError::SemanticValidationFailed(format!(
"JSON deserialization failed: {e}"
))
})?;
AtpProofBundle::try_from(serializable)
}
pub fn validate(&self) -> Result<(), AtpProofBundleError> {
if !self.version.is_supported() {
return Err(AtpProofBundleError::UnsupportedVersion(self.version));
}
self.validate_proof_strength()?;
self.validate_verification_evidence()?;
if let Some(ref metadata) = self.raptorq_metadata {
self.validate_raptorq_metadata(metadata)?;
}
self.validate_repair_groups()?;
self.validate_peer_identity()?;
self.validate_journal()?;
self.validate_semantic_consistency()?;
Ok(())
}
#[must_use]
fn compute_canonical_bundle_hash(&self) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update(self.version.0.to_be_bytes());
hasher.update(self.created_at_micros.to_be_bytes());
hasher.update(self.transfer_id.as_bytes());
hasher.update(self.manifest_root.hash());
for object_id in &self.object_roots {
match object_id {
crate::atp::object::ObjectId::Content(content_id) => {
hasher.update(b"content:");
hasher.update(content_id.hash());
}
crate::atp::object::ObjectId::Manifest(manifest_id) => {
hasher.update(b"manifest:");
hasher.update(manifest_id.hash());
}
}
}
hasher.update(self.chunk_bitmap.total_chunks.to_be_bytes());
hasher.update(self.chunk_bitmap.received_count.to_be_bytes());
hasher.update(&self.chunk_bitmap.bitmap_data);
hasher.update(self.peer_identity.source_peer_id.as_bytes());
hasher.update(self.peer_identity.destination_peer_id.as_bytes());
hasher.update(self.peer_identity.auth_method.as_bytes());
hasher.finalize().to_vec()
}
#[must_use]
pub fn stable_trace_id(&self) -> TraceId {
TraceId::from_parts(
self.created_at_micros / 1_000,
stable_franken_random_bits(b"atp.proof.trace", &self.compute_canonical_bundle_hash()),
)
}
#[must_use]
pub fn stable_decision_id(&self) -> DecisionId {
DecisionId::from_parts(
self.created_at_micros / 1_000,
stable_franken_random_bits(
b"atp.proof.decision",
&self.compute_canonical_bundle_hash(),
),
)
}
#[must_use]
pub fn to_franken_proof_export(&self) -> AtpFrankenProofExport {
let validation_result = self.validate();
let validation_status = match &validation_result {
Ok(()) => AtpProofValidationStatus::Accepted,
Err(err) => AtpProofValidationStatus::Quarantined {
reason: err.to_string(),
},
};
let (action, posterior, expected_loss_by_action, expected_loss, calibration, fallback) =
if validation_status.is_accepted() {
let mut expected_loss_by_action = BTreeMap::new();
expected_loss_by_action.insert(ATP_PROOF_ACCEPT_ACTION.to_string(), 0.01);
expected_loss_by_action.insert(ATP_PROOF_QUARANTINE_ACTION.to_string(), 0.25);
(
ATP_PROOF_ACCEPT_ACTION.to_string(),
vec![0.99, 0.01],
expected_loss_by_action,
0.01,
1.0,
false,
)
} else {
let mut expected_loss_by_action = BTreeMap::new();
expected_loss_by_action.insert(ATP_PROOF_ACCEPT_ACTION.to_string(), 1.0);
expected_loss_by_action.insert(ATP_PROOF_QUARANTINE_ACTION.to_string(), 0.05);
(
ATP_PROOF_QUARANTINE_ACTION.to_string(),
vec![0.02, 0.98],
expected_loss_by_action,
0.05,
0.99,
true,
)
};
let decision_audit = DecisionAuditEntry {
decision_id: self.stable_decision_id(),
trace_id: self.stable_trace_id(),
contract_name: ATP_PROOF_DECISION_CONTRACT.to_string(),
action_chosen: action,
expected_loss,
calibration_score: calibration,
fallback_active: fallback,
posterior_snapshot: posterior,
expected_loss_by_action,
ts_unix_ms: self.created_at_micros / 1_000,
};
let evidence_ledger = decision_audit.to_evidence_ledger();
let canonical_hash = self.compute_canonical_bundle_hash();
let audit_artifacts = self.audit_artifact_refs(&canonical_hash);
AtpFrankenProofExport {
decision_audit,
evidence_ledger,
audit_artifacts,
validation_status,
}
}
#[must_use]
pub fn to_evidence_ledger(&self) -> EvidenceLedger {
self.to_franken_proof_export().evidence_ledger
}
fn audit_artifact_refs(&self, canonical_hash: &[u8]) -> Vec<AtpAuditArtifactRef> {
let mut artifacts = vec![
AtpAuditArtifactRef {
key: "proof_bundle".to_string(),
artifact_kind: ATP_PROOF_FRANKEN_COMPONENT.to_string(),
schema: "atp-proof-bundle-v1".to_string(),
digest: Some(format!("sha256:{}", hex::encode(canonical_hash))),
replay_range: None,
},
AtpAuditArtifactRef {
key: "manifest_root".to_string(),
artifact_kind: "atp.manifest_root".to_string(),
schema: "atp-merkle-root-v1".to_string(),
digest: Some(format!("sha256:{}", hex::encode(self.manifest_root.hash()))),
replay_range: None,
},
AtpAuditArtifactRef {
key: "transfer_journal".to_string(),
artifact_kind: "atp.transfer_journal".to_string(),
schema: "atp-transfer-journal-v1".to_string(),
digest: Some(format!("sha256:{}", self.journal.digest)),
replay_range: None,
},
AtpAuditArtifactRef {
key: "path_summary".to_string(),
artifact_kind: "atp.path_summary".to_string(),
schema: "atp-transfer-path-summary-v1".to_string(),
digest: None,
replay_range: None,
},
AtpAuditArtifactRef {
key: "peer_identity".to_string(),
artifact_kind: "atp.peer_identity".to_string(),
schema: "atp-peer-identity-v1".to_string(),
digest: None,
replay_range: None,
},
];
if self.commit_record.is_some() {
artifacts.push(AtpAuditArtifactRef {
key: "final_commit_record".to_string(),
artifact_kind: "atp.graph_commit".to_string(),
schema: "atp-graph-commit-v1".to_string(),
digest: None,
replay_range: None,
});
}
for group in &self.repair_groups {
artifacts.push(AtpAuditArtifactRef {
key: format!("repair_group:{}", group.group_id),
artifact_kind: "atp.repair_group".to_string(),
schema: "atp-repair-group-v1".to_string(),
digest: None,
replay_range: None,
});
}
for (key, pointer) in &self.replay_pointers {
artifacts.push(AtpAuditArtifactRef {
key: format!("replay:{key}"),
artifact_kind: "atp.replay_pointer".to_string(),
schema: "atp-replay-pointer-v1".to_string(),
digest: Some(format!("sha256:{}", pointer.stream_checksum)),
replay_range: Some((pointer.start_position, pointer.end_position)),
});
}
artifacts
}
fn verify_cryptographic_signatures(&self) -> Result<bool, Box<dyn std::error::Error>> {
let signatures_ext = match self.extensions.get("cryptographic_signatures") {
Some(ext) => ext,
None => return Ok(false), };
let signatures: CryptographicSignatures = serde_json::from_value(signatures_ext.clone())
.map_err(|_| "Invalid cryptographic_signatures extension format")?;
if signatures.signatures.is_empty() {
return Ok(false);
}
let canonical_hash = self.compute_canonical_bundle_hash();
let hashes_match: bool =
subtle::ConstantTimeEq::ct_eq(&signatures.bundle_hash[..], &canonical_hash[..]).into();
if !hashes_match {
return Ok(false); }
let valid_peer_ids = [
&self.peer_identity.source_peer_id,
&self.peer_identity.destination_peer_id,
];
let mut valid_signature_count = 0;
for signature in &signatures.signatures {
if !valid_peer_ids.contains(&&signature.signer_id) {
continue; }
if !self
.peer_identity
.key_fingerprints
.contains(&signature.key_fingerprint)
{
continue; }
if signature.signature.len() != 32 || signature.signed_at_micros == 0 {
continue; }
valid_signature_count += 1;
}
Ok(valid_signature_count > 0)
}
#[must_use]
pub fn calculate_proof_strength(&self) -> ProofStrength {
let mut strength = ProofStrength::Basic;
if self.raptorq_metadata.is_some() || !self.repair_groups.is_empty() {
if !self.peer_identity.key_fingerprints.is_empty() {
strength = ProofStrength::Enhanced;
}
}
if let Ok(valid_signatures) = self.verify_cryptographic_signatures() {
if valid_signatures {
strength = ProofStrength::Cryptographic;
}
}
strength
}
pub fn sign_bundle(
&mut self,
signer_id: &str,
key_fingerprint: &str,
auth_key: &AuthKey,
) -> Result<(), Box<dyn std::error::Error>> {
let is_source: bool = subtle::ConstantTimeEq::ct_eq(
signer_id.as_bytes(),
self.peer_identity.source_peer_id.as_bytes(),
)
.into();
let is_dest: bool = subtle::ConstantTimeEq::ct_eq(
signer_id.as_bytes(),
self.peer_identity.destination_peer_id.as_bytes(),
)
.into();
if !is_source && !is_dest {
return Err("Signer is not a participant in this transfer".into());
}
if !self
.peer_identity
.key_fingerprints
.contains(&key_fingerprint.to_string())
{
return Err("Key fingerprint not found in peer identity".into());
}
let canonical_hash = self.compute_canonical_bundle_hash();
let signature_data = {
let mut mac = Hmac::<Sha256>::new_from_slice(auth_key.as_bytes())
.map_err(|_| "Invalid auth key")?;
mac.update(&canonical_hash);
mac.finalize().into_bytes().to_vec()
};
let signature = CryptographicSignature {
signer_id: signer_id.to_string(),
key_fingerprint: key_fingerprint.to_string(),
signature: signature_data,
signed_at_micros: system_time_micros_since_unix_epoch(
SystemTime::now(), "signed_at_micros",
)?,
};
let signatures = if let Some(existing) = self.extensions.get("cryptographic_signatures") {
let mut sigs: CryptographicSignatures = serde_json::from_value(existing.clone())?;
sigs.signatures.push(signature);
sigs
} else {
CryptographicSignatures {
signatures: vec![signature],
hash_algorithm: "SHA-256".to_string(),
bundle_hash: canonical_hash,
}
};
self.extensions.insert(
"cryptographic_signatures".to_string(),
serde_json::to_value(signatures)?,
);
Ok(())
}
#[must_use]
pub fn meets_policy_requirements(&self) -> bool {
let actual_strength = self.calculate_proof_strength();
if actual_strength < self.metadata.required_proof_strength {
return false;
}
if self.metadata.require_repair_evidence
&& self.raptorq_metadata.is_none()
&& self.repair_groups.is_empty()
{
return false;
}
if self.metadata.require_mailbox_evidence {
if !self.path_summary.relay_used && !self.extensions.contains_key("mailbox_evidence") {
return false;
}
}
true
}
fn validate_proof_strength(&self) -> Result<(), AtpProofBundleError> {
let actual = self.calculate_proof_strength();
if actual < self.metadata.required_proof_strength {
return Err(AtpProofBundleError::InsufficientProofStrength {
required: self.metadata.required_proof_strength,
found: actual,
});
}
Ok(())
}
fn validate_verification_evidence(&self) -> Result<(), AtpProofBundleError> {
let mut has_chunk_evidence = false;
let mut has_manifest_evidence = false;
for evidence in &self.verification_evidence {
match evidence.stage {
crate::atp::verifier::VerificationStage::ChunkHash => {
has_chunk_evidence = true;
}
crate::atp::verifier::VerificationStage::Manifest => {
has_manifest_evidence = true;
}
_ => {}
}
}
if !has_chunk_evidence {
return Err(AtpProofBundleError::InvalidVerificationEvidence(
"missing chunk hash evidence".to_string(),
));
}
if !has_manifest_evidence {
return Err(AtpProofBundleError::InvalidVerificationEvidence(
"missing manifest evidence".to_string(),
));
}
Ok(())
}
fn validate_raptorq_metadata(
&self,
metadata: &RaptorQDecodeMetadata,
) -> Result<(), AtpProofBundleError> {
if metadata.decode_success_rate < 0.0 || metadata.decode_success_rate > 1.0 {
return Err(AtpProofBundleError::InvalidRaptorQMetadata(
"decode success rate must be between 0.0 and 1.0".to_string(),
));
}
if metadata.average_overhead_ratio < 0.0 {
return Err(AtpProofBundleError::InvalidRaptorQMetadata(
"average overhead ratio cannot be negative".to_string(),
));
}
for block in &metadata.source_blocks {
if block.overhead_ratio < 0.0 {
return Err(AtpProofBundleError::InvalidRaptorQMetadata(format!(
"block {} has negative overhead ratio",
block.block_index
)));
}
}
Ok(())
}
fn validate_repair_groups(&self) -> Result<(), AtpProofBundleError> {
for group in &self.repair_groups {
if group.redundancy_factor < 1.0 {
return Err(AtpProofBundleError::InvalidRepairGroup(format!(
"repair group {} has invalid redundancy factor: {}",
group.group_id, group.redundancy_factor
)));
}
if group.covered_objects.is_empty() {
return Err(AtpProofBundleError::InvalidRepairGroup(format!(
"repair group {} covers no objects",
group.group_id
)));
}
}
Ok(())
}
fn validate_peer_identity(&self) -> Result<(), AtpProofBundleError> {
if self.peer_identity.source_peer_id.is_empty() {
return Err(AtpProofBundleError::InvalidPeerIdentity(
"source peer ID cannot be empty".to_string(),
));
}
if self.peer_identity.destination_peer_id.is_empty() {
return Err(AtpProofBundleError::InvalidPeerIdentity(
"destination peer ID cannot be empty".to_string(),
));
}
Ok(())
}
fn validate_journal(&self) -> Result<(), AtpProofBundleError> {
if self.journal.entry_count == 0 {
return Err(AtpProofBundleError::InvalidJournal(
"journal cannot be empty".to_string(),
));
}
if self.journal.size_bytes == 0 {
return Err(AtpProofBundleError::InvalidJournal(
"journal size cannot be zero".to_string(),
));
}
Ok(())
}
fn validate_semantic_consistency(&self) -> Result<(), AtpProofBundleError> {
let total_verified_chunks = self
.verification_evidence
.iter()
.filter(|e| e.stage == crate::atp::verifier::VerificationStage::ChunkHash)
.count() as u64;
if total_verified_chunks > self.chunk_bitmap.received_count {
return Err(AtpProofBundleError::SemanticValidationFailed(
"more chunks verified than marked as received in bitmap".to_string(),
));
}
let repair_activated = self.repair_groups.iter().any(|g| g.repair_activated);
let has_repair_symbols = self
.raptorq_metadata
.as_ref()
.is_some_and(|m| m.repair_symbols_used > 0);
if repair_activated != has_repair_symbols {
return Err(AtpProofBundleError::SemanticValidationFailed(
"repair activation inconsistent with RaptorQ metadata".to_string(),
));
}
Ok(())
}
}
impl RaptorQDecodeMetadata {
pub fn from_decode_proof(
proof: &crate::raptorq::proof::DecodeProof,
telemetry: Option<&RaptorQTelemetry>,
) -> Self {
let decode_success = matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Success { .. }
);
let overhead_ratio = if proof.received.source_count > 0 {
proof.received.total as f64 / proof.received.source_count as f64
} else {
0.0
};
let excess_repair_symbols = proof.received.repair_count.saturating_sub(proof.config.k);
let failure_reason = match &proof.outcome {
crate::raptorq::proof::ProofOutcome::Success { .. } => None,
crate::raptorq::proof::ProofOutcome::Failure { reason } => {
Some(format!("{:?}", reason))
}
};
let source_blocks = vec![RaptorQSourceBlock {
block_index: u32::from(proof.config.sbn),
source_symbols: proof.config.k as u32,
repair_symbols: proof.received.repair_count as u32,
decode_success,
overhead_ratio,
symbol_size: proof.config.symbol_size as u32,
seed: proof.config.seed,
k_prime_boundary: proof.config.k >= 1024, excess_repair_symbols: excess_repair_symbols as u32,
padding_truncated_bytes: 0, random_loss_pattern: None, corrupted_symbols: 0, pivot_events: proof.elimination.pivot_events.len() as u32,
failure_reason,
block_proof_hash: Some(proof.content_hash().to_hex()),
}];
let hard_regime_stats = telemetry.map(|t| HardRegimeStats {
regime_type: t.regime_type.clone(),
loss_rate: t.loss_rate,
burst_loss_events: t.burst_loss_events,
tail_repair_activations: t.tail_repair_activations,
lossy_repair_activations: t.lossy_repair_activations,
resume_repair_operations: t.resume_repair_operations,
relay_expensive_activations: t.relay_expensive_activations,
mobile_unstable_activations: t.mobile_unstable_activations,
total_fallback_triggers: t.total_fallback_triggers,
repair_roi: t.repair_roi,
});
let success = matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Success { .. }
);
let overhead_ratio = if proof.received.source_count > 0 {
proof.received.total as f64 / proof.received.source_count as f64
} else {
0.0
};
Self {
source_blocks,
repair_symbols_received: proof.received.repair_count as u32,
repair_symbols_used: proof.received.repair_count as u32,
decode_success_rate: if success { 1.0 } else { 0.0 },
average_overhead_ratio: overhead_ratio,
hard_regime_stats,
proof_hash: Some(proof.content_hash().to_hex()),
fallback_reasons: Vec::new(), conformance_validation: Some(RaptorQConformanceResult::from_proof(proof)),
}
}
pub fn with_hard_regime_testing(
mut self,
regime_type: &str,
loss_rate: f64,
burst_events: u32,
) -> Self {
self.hard_regime_stats = Some(HardRegimeStats {
regime_type: regime_type.to_string(),
loss_rate,
burst_loss_events: burst_events,
tail_repair_activations: u32::from(loss_rate > 0.3),
lossy_repair_activations: u32::from(loss_rate > 0.2),
resume_repair_operations: u32::from(regime_type == "mobile-unstable"),
relay_expensive_activations: u32::from(regime_type == "relay-expensive"),
mobile_unstable_activations: u32::from(regime_type == "mobile-unstable"),
total_fallback_triggers: burst_events,
repair_roi: if loss_rate > 0.5 { 0.8 } else { 0.95 },
});
self
}
}
impl RaptorQConformanceResult {
pub fn from_proof(proof: &crate::raptorq::proof::DecodeProof) -> Self {
let verified_guarantees = vec![
"RFC6330_systematic_encoding".to_string(),
"repair_equation_validation".to_string(),
"inactivation_decode_correctness".to_string(),
"linear_algebra_gf256".to_string(),
];
let success = matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Success { .. }
);
let corruption_detected = matches!(
proof.outcome,
crate::raptorq::proof::ProofOutcome::Failure {
reason: crate::raptorq::proof::FailureReason::SymbolEquationArityMismatch { .. }
}
);
Self {
rfc6330_compliant: success,
verified_guarantees,
systematic_encoding_valid: proof.config.k > 0,
repair_equation_correct: proof.elimination.pivot_events.len() <= proof.config.k,
inactivation_decode_conformant: proof.elimination.pivots > 0 || proof.config.k == 0,
linear_algebra_valid: proof.peeling.solved > 0 || proof.config.k == 0,
gf256_operations_correct: !corruption_detected,
test_suite_version: "ATP-G5-v1.0".to_string(),
validated_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_micros() as u64,
}
}
}
#[derive(Debug, Clone)]
pub struct RaptorQTelemetry {
pub regime_type: String,
pub loss_rate: f64,
pub burst_loss_events: u32,
pub tail_repair_activations: u32,
pub lossy_repair_activations: u32,
pub resume_repair_operations: u32,
pub relay_expensive_activations: u32,
pub mobile_unstable_activations: u32,
pub total_fallback_triggers: u32,
pub repair_roi: f64,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::atp::verifier::{VerificationEvidence, VerificationStage};
#[test]
fn proof_timestamp_rejects_clock_before_unix_epoch() {
let err = system_time_micros_since_unix_epoch(
UNIX_EPOCH - Duration::from_micros(1),
"created_at_micros",
)
.expect_err("pre-epoch system time must fail closed");
assert_eq!(
err,
AtpProofBundleError::InvalidSystemTime {
field: "created_at_micros",
}
);
}
#[test]
fn proof_timestamp_rejects_microsecond_overflow() {
let err = duration_micros_to_u64(Duration::from_secs(u64::MAX), "signed_at_micros")
.expect_err("oversized duration must not truncate");
assert!(matches!(
err,
AtpProofBundleError::TimestampOutOfRange {
field: "signed_at_micros",
..
}
));
}
#[test]
fn chunk_bitmap_basic_operations() {
let mut bitmap = ChunkBitmap::new(10);
assert_eq!(bitmap.total_chunks, 10);
assert_eq!(bitmap.received_count, 0);
assert!(!bitmap.is_received(0));
bitmap.mark_received(0);
bitmap.mark_received(5);
bitmap.mark_received(9);
assert!(bitmap.is_received(0));
assert!(bitmap.is_received(5));
assert!(bitmap.is_received(9));
assert!(!bitmap.is_received(1));
assert!(!bitmap.is_received(8));
assert_eq!(bitmap.received_count, 3);
assert_eq!(bitmap.completion_ratio(), 0.3);
}
#[test]
fn chunk_bitmap_duplicate_marking() {
let mut bitmap = ChunkBitmap::new(5);
bitmap.mark_received(2);
bitmap.mark_received(2); assert_eq!(bitmap.received_count, 1);
}
#[test]
fn chunk_bitmap_out_of_bounds() {
let mut bitmap = ChunkBitmap::new(5);
bitmap.mark_received(10); assert_eq!(bitmap.received_count, 0);
assert!(!bitmap.is_received(10));
}
fn franken_export_test_bundle(include_manifest_evidence: bool) -> AtpProofBundle {
use crate::atp::object::Object;
use crate::atp::proof::replay::AtpReplayPointer;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = Object::file(b"test".to_vec()).id;
let mut chunk_bitmap = ChunkBitmap::new(1);
chunk_bitmap.mark_received(0);
let peer_identity = PeerIdentityInfo {
source_peer_id: "source".to_string(),
destination_peer_id: "dest".to_string(),
auth_method: "ed25519".to_string(),
key_fingerprints: vec!["key1".to_string()],
authenticated_at_micros: 12345,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec!["tcp".to_string()],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1_000_000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12345,
finalized_at_micros: Some(12400),
};
let replay_pointer = AtpReplayPointer::new(
"transfer-replay",
10,
42,
SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(b"replay")),
);
let mut builder = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id])
.chunk_bitmap(chunk_bitmap)
.peer_identity(peer_identity)
.path_summary(path_summary)
.journal(journal)
.add_replay_pointer("failure-trace", replay_pointer)
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
});
if include_manifest_evidence {
builder = builder.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::Manifest,
summary: "manifest verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"manifest")),
});
}
builder
.build()
.expect("franken export test bundle should build")
}
#[test]
fn franken_export_accepts_valid_bundle_with_stable_ids() {
let bundle = franken_export_test_bundle(true);
let export = bundle.to_franken_proof_export();
assert_eq!(export.validation_status, AtpProofValidationStatus::Accepted);
assert_eq!(export.decision_audit.action_chosen, ATP_PROOF_ACCEPT_ACTION);
assert_eq!(
export.decision_audit.contract_name,
ATP_PROOF_DECISION_CONTRACT
);
assert_eq!(
export.decision_audit.decision_id.as_u128(),
bundle.stable_decision_id().as_u128()
);
assert_eq!(
export.decision_audit.trace_id.as_u128(),
bundle.stable_trace_id().as_u128()
);
assert!(export.evidence_ledger.is_valid());
assert_eq!(export.evidence_ledger.action, ATP_PROOF_ACCEPT_ACTION);
assert_eq!(
bundle.to_evidence_ledger().action,
export.evidence_ledger.action
);
assert!(export.audit_artifacts.iter().any(|artifact| {
artifact.key == "proof_bundle"
&& artifact
.digest
.as_deref()
.is_some_and(|digest| digest.starts_with("sha256:"))
}));
assert!(export.audit_artifacts.iter().any(|artifact| {
artifact.key == "replay:failure-trace" && artifact.replay_range == Some((10, 42))
}));
}
#[test]
fn franken_export_quarantines_invalid_bundle_with_evidence() {
let bundle = franken_export_test_bundle(false);
let export = bundle.to_franken_proof_export();
match &export.validation_status {
AtpProofValidationStatus::Quarantined { reason } => {
assert!(reason.contains("missing manifest evidence"));
}
AtpProofValidationStatus::Accepted => panic!("invalid bundle must be quarantined"),
}
assert_eq!(
export.decision_audit.action_chosen,
ATP_PROOF_QUARANTINE_ACTION
);
assert!(export.decision_audit.fallback_active);
assert!(export.evidence_ledger.is_valid());
assert_eq!(export.evidence_ledger.action, ATP_PROOF_QUARANTINE_ACTION);
}
#[test]
fn proof_bundle_builder_minimal() {
use crate::atp::object::Object;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = Object::file(b"test".to_vec()).id;
let mut chunk_bitmap = ChunkBitmap::new(1);
chunk_bitmap.mark_received(0);
let peer_identity = PeerIdentityInfo {
source_peer_id: "source".to_string(),
destination_peer_id: "dest".to_string(),
auth_method: "ed25519".to_string(),
key_fingerprints: vec!["key1".to_string()],
authenticated_at_micros: 12345,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1_000_000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12345,
finalized_at_micros: Some(12400),
};
let bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id])
.chunk_bitmap(chunk_bitmap)
.peer_identity(peer_identity)
.path_summary(path_summary)
.journal(journal)
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::Manifest,
summary: "manifest verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"manifest")),
})
.build()
.expect("minimal bundle should build");
bundle.validate().expect("minimal bundle should validate");
assert_eq!(bundle.transfer_id, "test-transfer");
assert_eq!(bundle.calculate_proof_strength(), ProofStrength::Basic);
assert!(bundle.meets_policy_requirements());
}
#[test]
fn proof_bundle_validation_fails_for_missing_evidence() {
use crate::atp::object::Object;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = Object::file(b"test".to_vec()).id;
let chunk_bitmap = ChunkBitmap::new(1);
let peer_identity = PeerIdentityInfo {
source_peer_id: "source".to_string(),
destination_peer_id: "dest".to_string(),
auth_method: "ed25519".to_string(),
key_fingerprints: vec![],
authenticated_at_micros: 12345,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1_000_000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12345,
finalized_at_micros: Some(12400),
};
let bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id])
.chunk_bitmap(chunk_bitmap)
.peer_identity(peer_identity)
.path_summary(path_summary)
.journal(journal)
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
})
.build()
.expect("bundle should build");
let err = bundle.validate().expect_err("validation should fail");
assert!(matches!(
err,
AtpProofBundleError::InvalidVerificationEvidence(_)
));
}
#[test]
fn proof_strength_calculation() {
use crate::atp::object::Object;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = Object::file(b"test".to_vec()).id.clone();
let mut bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id.clone()])
.chunk_bitmap(ChunkBitmap::new(1))
.peer_identity(PeerIdentityInfo {
source_peer_id: "source".to_string(),
destination_peer_id: "dest".to_string(),
auth_method: "ed25519".to_string(),
key_fingerprints: vec!["key1".to_string()],
authenticated_at_micros: 12345,
mutual_auth: true,
})
.path_summary(TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1_000_000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
})
.journal(TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12345,
finalized_at_micros: Some(12400),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::Manifest,
summary: "manifest verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"manifest")),
})
.add_repair_group(RepairGroupMetadata {
group_id: "group1".to_string(),
covered_objects: vec![SerializableObjectId::from(&object_id)],
repair_strategy: "raptorq".to_string(),
redundancy_factor: 1.5,
repair_activated: true,
repair_completed_at: Some(12345),
})
.build()
.expect("enhanced bundle should build");
assert_eq!(bundle.calculate_proof_strength(), ProofStrength::Enhanced);
bundle
.sign_bundle("source", "key1", &AuthKey::from_seed(12_345))
.expect("source signature should satisfy cryptographic proof strength");
assert_eq!(
bundle.calculate_proof_strength(),
ProofStrength::Cryptographic
);
}
#[test]
fn semantic_validation_detects_inconsistencies() {
use crate::atp::object::Object;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = Object::file(b"test".to_vec()).id;
let bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id.clone()])
.chunk_bitmap(ChunkBitmap::new(1))
.peer_identity(PeerIdentityInfo {
source_peer_id: "source".to_string(),
destination_peer_id: "dest".to_string(),
auth_method: "ed25519".to_string(),
key_fingerprints: vec![],
authenticated_at_micros: 12345,
mutual_auth: true,
})
.path_summary(TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1_000_000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
})
.journal(TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12345,
finalized_at_micros: Some(12400),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::Manifest,
summary: "manifest verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"manifest")),
})
.add_repair_group(RepairGroupMetadata {
group_id: "group1".to_string(),
covered_objects: vec![SerializableObjectId::from(&object_id)],
repair_strategy: "raptorq".to_string(),
redundancy_factor: 1.5,
repair_activated: true, repair_completed_at: Some(12345),
})
.build()
.expect("bundle should build");
let err = bundle
.validate()
.expect_err("semantic validation should fail");
assert!(matches!(
err,
AtpProofBundleError::SemanticValidationFailed(_)
));
}
#[test]
fn cryptographic_signature_verification() {
use crate::atp::object::ObjectId;
use crate::security::AuthKey;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = ObjectId::content(crate::atp::object::ContentId::from_bytes(b"test"));
let chunk_bitmap = ChunkBitmap::new(1);
let peer_identity = PeerIdentityInfo {
source_peer_id: "peer1".to_string(),
destination_peer_id: "peer2".to_string(),
auth_method: "hmac".to_string(),
key_fingerprints: vec!["test-key-fp".to_string()],
authenticated_at_micros: 12000,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1000000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12000,
finalized_at_micros: Some(12500),
};
let mut bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root.clone())
.object_roots(vec![object_id.clone()])
.chunk_bitmap(chunk_bitmap.clone())
.peer_identity(peer_identity.clone())
.path_summary(path_summary.clone())
.journal(journal.clone())
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::ChunkHash,
summary: "chunk verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"chunk")),
})
.add_verification_evidence(VerificationEvidence {
stage: VerificationStage::Manifest,
summary: "manifest verified".to_string(),
digest: Some(crate::atp::object::ContentId::from_bytes(b"manifest")),
})
.build()
.expect("bundle should build");
assert_eq!(bundle.calculate_proof_strength(), ProofStrength::Basic);
let auth_key = AuthKey::from_seed(12345);
bundle
.sign_bundle("peer1", "test-key-fp", &auth_key)
.expect("signing should succeed");
assert_eq!(
bundle.calculate_proof_strength(),
ProofStrength::Cryptographic
);
}
#[test]
fn cryptographic_signature_validation_rejects_tampering() {
use crate::atp::object::ObjectId;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = ObjectId::content(crate::atp::object::ContentId::from_bytes(b"test"));
let repair_object = SerializableObjectId::from(&object_id);
let chunk_bitmap = ChunkBitmap::new(1);
let peer_identity = PeerIdentityInfo {
source_peer_id: "peer1".to_string(),
destination_peer_id: "peer2".to_string(),
auth_method: "hmac".to_string(),
key_fingerprints: vec!["test-key-fp".to_string()],
authenticated_at_micros: 12000,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1000000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12000,
finalized_at_micros: Some(12500),
};
let mut bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id])
.chunk_bitmap(chunk_bitmap)
.peer_identity(peer_identity)
.path_summary(path_summary)
.journal(journal)
.add_repair_group(RepairGroupMetadata {
group_id: "group1".to_string(),
covered_objects: vec![repair_object],
repair_strategy: "raptorq".to_string(),
redundancy_factor: 1.5,
repair_activated: true,
repair_completed_at: Some(12_500),
})
.build()
.expect("bundle should build");
let tampered_signatures = CryptographicSignatures {
signatures: vec![CryptographicSignature {
signer_id: "peer1".to_string(),
key_fingerprint: "test-key-fp".to_string(),
signature: vec![0u8; 32], signed_at_micros: 12345,
}],
hash_algorithm: "SHA-256".to_string(),
bundle_hash: vec![0u8; 32], };
bundle.extensions.insert(
"cryptographic_signatures".to_string(),
serde_json::to_value(tampered_signatures).unwrap(),
);
assert_eq!(bundle.calculate_proof_strength(), ProofStrength::Enhanced);
}
#[test]
fn cryptographic_signature_rejects_unauthorized_signers() {
use crate::atp::object::ObjectId;
use crate::security::AuthKey;
let manifest_root = crate::atp::manifest::MerkleRoot::new([1; 32]);
let object_id = ObjectId::content(crate::atp::object::ContentId::from_bytes(b"test"));
let chunk_bitmap = ChunkBitmap::new(1);
let peer_identity = PeerIdentityInfo {
source_peer_id: "peer1".to_string(),
destination_peer_id: "peer2".to_string(),
auth_method: "hmac".to_string(),
key_fingerprints: vec!["test-key-fp".to_string()],
authenticated_at_micros: 12000,
mutual_auth: true,
};
let path_summary = TransferPathSummary {
primary_protocol: "quic".to_string(),
fallback_protocols: vec![],
rtt_millis: Some(50.0),
bandwidth_bps: Some(1000000),
relay_used: false,
relay_nodes: vec![],
path_setup_duration_millis: 100,
path_switches: 0,
};
let journal = TransferJournal {
digest: SerializableContentId::from(&crate::atp::object::ContentId::from_bytes(
b"journal",
)),
format_version: 1,
entry_count: 10,
size_bytes: 1024,
is_complete: true,
created_at_micros: 12000,
finalized_at_micros: Some(12500),
};
let mut bundle = AtpProofBundleBuilder::new("test-transfer")
.manifest_root(manifest_root)
.object_roots(vec![object_id])
.chunk_bitmap(chunk_bitmap)
.peer_identity(peer_identity)
.path_summary(path_summary)
.journal(journal)
.build()
.expect("bundle should build");
let auth_key = AuthKey::from_seed(12345);
let result = bundle.sign_bundle("peer3", "test-key-fp", &auth_key);
assert!(result.is_err());
let result = bundle.sign_bundle("peer1", "unknown-key-fp", &auth_key);
assert!(result.is_err());
}
}