Files
wifi-densepose/vendor/ruvector/crates/rvf/rvf-crypto/src/lineage.rs

273 lines
8.8 KiB
Rust

//! 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<LineageRecord, RvfError> {
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());
}
}