//! Lineage witness functions for DNA-style provenance chains. //! //! Provides serialization, hashing, and verification for lineage records //! that track file derivation history through witness chain entries. use rvf_types::{ DerivationType, ErrorCode, FileIdentity, LineageRecord, RvfError, LINEAGE_RECORD_SIZE, WITNESS_DERIVATION, }; use crate::hash::shake256_256; use crate::witness::WitnessEntry; /// Serialize a `LineageRecord` to a fixed 128-byte array. pub fn lineage_record_to_bytes(record: &LineageRecord) -> [u8; LINEAGE_RECORD_SIZE] { let mut buf = [0u8; LINEAGE_RECORD_SIZE]; buf[0x00..0x10].copy_from_slice(&record.file_id); buf[0x10..0x20].copy_from_slice(&record.parent_id); buf[0x20..0x40].copy_from_slice(&record.parent_hash); buf[0x40] = record.derivation_type as u8; // 3 bytes padding at 0x41..0x44 buf[0x44..0x48].copy_from_slice(&record.mutation_count.to_le_bytes()); buf[0x48..0x50].copy_from_slice(&record.timestamp_ns.to_le_bytes()); buf[0x50] = record.description_len; let desc_len = (record.description_len as usize).min(47); buf[0x51..0x51 + desc_len].copy_from_slice(&record.description[..desc_len]); buf } /// Deserialize a `LineageRecord` from a 128-byte slice. pub fn lineage_record_from_bytes( data: &[u8; LINEAGE_RECORD_SIZE], ) -> Result { let mut file_id = [0u8; 16]; file_id.copy_from_slice(&data[0x00..0x10]); let mut parent_id = [0u8; 16]; parent_id.copy_from_slice(&data[0x10..0x20]); let mut parent_hash = [0u8; 32]; parent_hash.copy_from_slice(&data[0x20..0x40]); let derivation_type = DerivationType::try_from(data[0x40]).map_err(|v| RvfError::InvalidEnumValue { type_name: "DerivationType", value: v as u64, })?; let mutation_count = u32::from_le_bytes(data[0x44..0x48].try_into().unwrap()); let timestamp_ns = u64::from_le_bytes(data[0x48..0x50].try_into().unwrap()); let description_len = data[0x50].min(47); let mut description = [0u8; 47]; description[..description_len as usize] .copy_from_slice(&data[0x51..0x51 + description_len as usize]); Ok(LineageRecord { file_id, parent_id, parent_hash, derivation_type, mutation_count, timestamp_ns, description_len, description, }) } /// Create a witness entry for a lineage derivation event. /// /// The `action_hash` is SHAKE-256-256 of the serialized record bytes. /// Uses witness type `WITNESS_DERIVATION` (0x09). pub fn lineage_witness_entry(record: &LineageRecord, prev_hash: [u8; 32]) -> WitnessEntry { let record_bytes = lineage_record_to_bytes(record); let action_hash = shake256_256(&record_bytes); WitnessEntry { prev_hash, action_hash, timestamp_ns: record.timestamp_ns, witness_type: WITNESS_DERIVATION, } } /// Compute the SHAKE-256-256 hash of a 4096-byte manifest for use as parent_hash. pub fn compute_manifest_hash(manifest: &[u8; 4096]) -> [u8; 32] { shake256_256(manifest) } /// Verify a lineage chain: each child's parent_hash must match the /// hash of the corresponding parent's manifest bytes. /// /// Takes pairs of (FileIdentity, manifest_hash) in order from root to leaf. pub fn verify_lineage_chain(entries: &[(FileIdentity, [u8; 32])]) -> Result<(), RvfError> { if entries.is_empty() { return Ok(()); } // First entry must be root if !entries[0].0.is_root() { return Err(RvfError::Code(ErrorCode::LineageBroken)); } for i in 1..entries.len() { let child = &entries[i].0; let parent = &entries[i - 1].0; let parent_manifest_hash = &entries[i - 1].1; // Child's parent_id must match parent's file_id if child.parent_id != parent.file_id { return Err(RvfError::Code(ErrorCode::LineageBroken)); } // Child's parent_hash must match parent's manifest hash if child.parent_hash != *parent_manifest_hash { return Err(RvfError::Code(ErrorCode::ParentHashMismatch)); } // Depth must increment by 1 if child.lineage_depth != parent.lineage_depth + 1 { return Err(RvfError::Code(ErrorCode::LineageBroken)); } } Ok(()) } #[cfg(test)] mod tests { use super::*; fn sample_record() -> LineageRecord { LineageRecord::new( [1u8; 16], [2u8; 16], [3u8; 32], DerivationType::Filter, 5, 1_700_000_000_000_000_000, "test derivation", ) } #[test] fn lineage_record_round_trip() { let record = sample_record(); let bytes = lineage_record_to_bytes(&record); assert_eq!(bytes.len(), LINEAGE_RECORD_SIZE); let decoded = lineage_record_from_bytes(&bytes).unwrap(); assert_eq!(decoded.file_id, record.file_id); assert_eq!(decoded.parent_id, record.parent_id); assert_eq!(decoded.parent_hash, record.parent_hash); assert_eq!(decoded.derivation_type, record.derivation_type); assert_eq!(decoded.mutation_count, record.mutation_count); assert_eq!(decoded.timestamp_ns, record.timestamp_ns); assert_eq!(decoded.description_str(), record.description_str()); } #[test] fn lineage_record_invalid_derivation_type() { let record = sample_record(); let mut bytes = lineage_record_to_bytes(&record); bytes[0x40] = 0xFE; // invalid derivation type let result = lineage_record_from_bytes(&bytes); assert!(result.is_err()); } #[test] fn lineage_witness_entry_creates_valid_entry() { let record = sample_record(); let prev_hash = [0u8; 32]; let entry = lineage_witness_entry(&record, prev_hash); assert_eq!(entry.witness_type, WITNESS_DERIVATION); assert_eq!(entry.prev_hash, prev_hash); assert_eq!(entry.timestamp_ns, record.timestamp_ns); assert_ne!(entry.action_hash, [0u8; 32]); } #[test] fn compute_manifest_hash_deterministic() { let manifest = [0xABu8; 4096]; let h1 = compute_manifest_hash(&manifest); let h2 = compute_manifest_hash(&manifest); assert_eq!(h1, h2); assert_ne!(h1, [0u8; 32]); } #[test] fn verify_empty_chain() { assert!(verify_lineage_chain(&[]).is_ok()); } #[test] fn verify_single_root() { let root = FileIdentity::new_root([1u8; 16]); let hash = [0xAAu8; 32]; assert!(verify_lineage_chain(&[(root, hash)]).is_ok()); } #[test] fn verify_parent_child_chain() { let root_id = [1u8; 16]; let child_id = [2u8; 16]; let root_hash = [0xAAu8; 32]; let child_hash = [0xBBu8; 32]; let root = FileIdentity::new_root(root_id); let child = FileIdentity { file_id: child_id, parent_id: root_id, parent_hash: root_hash, lineage_depth: 1, }; assert!(verify_lineage_chain(&[(root, root_hash), (child, child_hash)]).is_ok()); } #[test] fn verify_broken_parent_id() { let root = FileIdentity::new_root([1u8; 16]); let root_hash = [0xAAu8; 32]; let child = FileIdentity { file_id: [2u8; 16], parent_id: [3u8; 16], // wrong parent_id parent_hash: root_hash, lineage_depth: 1, }; let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]); assert!(result.is_err()); } #[test] fn verify_hash_mismatch() { let root_id = [1u8; 16]; let root = FileIdentity::new_root(root_id); let root_hash = [0xAAu8; 32]; let child = FileIdentity { file_id: [2u8; 16], parent_id: root_id, parent_hash: [0xCCu8; 32], // wrong hash lineage_depth: 1, }; let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]); assert!(matches!( result, Err(RvfError::Code(ErrorCode::ParentHashMismatch)) )); } #[test] fn verify_non_root_first() { let non_root = FileIdentity { file_id: [1u8; 16], parent_id: [2u8; 16], parent_hash: [3u8; 32], lineage_depth: 1, }; let result = verify_lineage_chain(&[(non_root, [0u8; 32])]); assert!(result.is_err()); } #[test] fn verify_depth_mismatch() { let root_id = [1u8; 16]; let root = FileIdentity::new_root(root_id); let root_hash = [0xAAu8; 32]; let child = FileIdentity { file_id: [2u8; 16], parent_id: root_id, parent_hash: root_hash, lineage_depth: 5, // should be 1 }; let result = verify_lineage_chain(&[(root, root_hash), (child, [0xBBu8; 32])]); assert!(result.is_err()); } }