use std::collections::HashSet;
use crate::event::Event;
use crate::event::hash_text::decode_blake3_hash;
use crate::event::writer::{WriteError, compute_event_hash};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashErrorCode {
HashMismatch,
UnknownParent,
ComputeFailure,
}
#[derive(Debug, thiserror::Error)]
pub enum HashError {
#[error("event hash mismatch: stored={stored} expected={expected}")]
HashMismatch {
stored: String,
expected: String,
},
#[error("event {event_hash} references unknown parent {parent_hash}")]
UnknownParent {
event_hash: String,
parent_hash: String,
},
#[error("failed to compute event hash: {0}")]
Compute(#[from] WriteError),
}
impl HashError {
#[must_use]
pub const fn code(&self) -> HashErrorCode {
match self {
Self::HashMismatch { .. } => HashErrorCode::HashMismatch,
Self::UnknownParent { .. } => HashErrorCode::UnknownParent,
Self::Compute(_) => HashErrorCode::ComputeFailure,
}
}
}
pub fn verify_event_hash(event: &Event) -> Result<bool, HashError> {
let expected = compute_event_hash(event)?;
let Some(stored) = decode_blake3_hash(&event.event_hash) else {
return Ok(false);
};
let Some(computed) = decode_blake3_hash(&expected) else {
return Ok(false);
};
Ok(stored == computed)
}
pub fn verify_chain(events: &[&Event]) -> Result<(), HashError> {
let known: HashSet<&str> = events.iter().map(|e| e.event_hash.as_str()).collect();
for event in events {
let expected = compute_event_hash(event)?;
let stored =
decode_blake3_hash(&event.event_hash).ok_or_else(|| HashError::HashMismatch {
stored: event.event_hash.clone(),
expected: expected.clone(),
})?;
let computed = decode_blake3_hash(&expected).ok_or_else(|| HashError::HashMismatch {
stored: event.event_hash.clone(),
expected: expected.clone(),
})?;
if stored != computed {
return Err(HashError::HashMismatch {
stored: event.event_hash.clone(),
expected,
});
}
for parent in &event.parents {
if !known.contains(parent.as_str()) {
return Err(HashError::UnknownParent {
event_hash: event.event_hash.clone(),
parent_hash: parent.clone(),
});
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::event::Event;
use crate::event::data::{CreateData, EventData, MoveData};
use crate::event::types::EventType;
use crate::event::writer::write_event;
use crate::model::item::{Kind, State, Urgency};
use crate::model::item_id::ItemId;
fn make_root(wall_ts_us: i64) -> Event {
let mut event = Event {
wall_ts_us,
agent: "agent-a".into(),
itc: "itc:AQ".into(),
parents: vec![],
event_type: EventType::Create,
item_id: ItemId::new_unchecked("bn-a1b2"),
data: EventData::Create(CreateData {
title: "Root event".into(),
kind: Kind::Task,
size: None,
urgency: Urgency::Default,
labels: vec![],
parent: None,
causation: None,
description: None,
extra: BTreeMap::new(),
}),
event_hash: "blake3:placeholder".into(),
};
write_event(&mut event).expect("write_event should not fail");
event
}
fn make_child(wall_ts_us: i64, parent_hash: &str) -> Event {
let mut event = Event {
wall_ts_us,
agent: "agent-b".into(),
itc: "itc:AQ.1".into(),
parents: vec![parent_hash.to_owned()],
event_type: EventType::Move,
item_id: ItemId::new_unchecked("bn-a1b2"),
data: EventData::Move(MoveData {
state: State::Doing,
reason: None,
extra: BTreeMap::new(),
}),
event_hash: "blake3:placeholder".into(),
};
write_event(&mut event).expect("write_event should not fail");
event
}
#[test]
fn test_verify_event_hash_valid() {
let event = make_root(1_000_000);
let result = verify_event_hash(&event).expect("should compute");
assert!(result, "freshly written event should have valid hash");
}
#[test]
fn test_verify_event_hash_tampered_content() {
let mut event = make_root(1_000_000);
event.wall_ts_us += 1;
let result = verify_event_hash(&event).expect("should compute");
assert!(!result, "tampered event should fail hash check");
}
#[test]
fn test_verify_event_hash_tampered_agent() {
let mut event = make_root(1_000_000);
event.agent = "evil-agent".into();
let result = verify_event_hash(&event).expect("should compute");
assert!(!result, "event with modified agent should fail hash check");
}
#[test]
fn test_verify_event_hash_tampered_parents() {
let root = make_root(1_000_000);
let mut child = make_child(2_000_000, &root.event_hash);
child.parents[0] = "blake3:forged_parent_hash".into();
let result = verify_event_hash(&child).expect("should compute");
assert!(!result, "event with forged parent should fail hash check");
}
#[test]
fn test_verify_event_hash_deterministic() {
let event = make_root(42_000_000);
let r1 = verify_event_hash(&event).expect("first call");
let r2 = verify_event_hash(&event).expect("second call");
assert_eq!(r1, r2, "verify_event_hash must be deterministic");
assert!(r1, "should be valid");
}
#[test]
fn test_verify_chain_single_root_event() {
let root = make_root(1_000_000);
verify_chain(&[&root]).expect("single valid root event should pass");
}
#[test]
fn test_verify_chain_linear_chain() {
let root = make_root(1_000_000);
let child = make_child(2_000_000, &root.event_hash);
let grandchild = make_child(3_000_000, &child.event_hash);
verify_chain(&[&root, &child, &grandchild]).expect("valid 3-event chain should pass");
}
#[test]
fn test_verify_chain_order_independent() {
let root = make_root(1_000_000);
let child = make_child(2_000_000, &root.event_hash);
verify_chain(&[&child, &root]).expect("order should not matter for verify_chain");
}
#[test]
fn test_verify_chain_tampered_root_detected() {
let mut root = make_root(1_000_000);
let child = make_child(2_000_000, &root.event_hash);
root.wall_ts_us += 999;
let err =
verify_chain(&[&root, &child]).expect_err("tampered root should cause chain failure");
assert_eq!(err.code(), HashErrorCode::HashMismatch);
}
#[test]
fn test_verify_chain_tampered_child_detected() {
let root = make_root(1_000_000);
let mut child = make_child(2_000_000, &root.event_hash);
child.agent = "impersonator".into();
let err =
verify_chain(&[&root, &child]).expect_err("tampered child should cause chain failure");
assert_eq!(err.code(), HashErrorCode::HashMismatch);
}
#[test]
fn test_verify_chain_unknown_parent_detected() {
let child = make_child(2_000_000, "blake3:nonexistent_parent_hash");
let err =
verify_chain(&[&child]).expect_err("unresolvable parent should cause chain failure");
assert_eq!(err.code(), HashErrorCode::UnknownParent);
}
#[test]
fn test_verify_chain_merkle_cascade_property() {
let root = make_root(1_000_000);
let child = make_child(2_000_000, &root.event_hash);
let grandchild = make_child(3_000_000, &child.event_hash);
verify_chain(&[&root, &child, &grandchild]).expect("chain is initially valid");
let mut modified_root = root.clone();
modified_root.wall_ts_us += 1;
let new_root_hash = compute_event_hash(&modified_root).expect("hash compute");
modified_root.event_hash = new_root_hash.clone();
let err = verify_chain(&[&modified_root, &child, &grandchild])
.expect_err("Merkle cascade: ancestor modification breaks descendants");
assert!(
matches!(
err.code(),
HashErrorCode::UnknownParent | HashErrorCode::HashMismatch
),
"expected Merkle violation error, got: {:?}",
err
);
}
#[test]
fn test_verify_chain_empty_slice() {
verify_chain(&[]).expect("empty slice should be valid");
}
#[test]
fn test_verify_chain_merge_event_two_parents() {
let root_a = make_root(1_000_000);
let root_b = make_root(1_100_000);
let mut parents = vec![root_a.event_hash.clone(), root_b.event_hash.clone()];
parents.sort();
let mut merge_event = Event {
wall_ts_us: 2_000_000,
agent: "agent-c".into(),
itc: "itc:AQ.2".into(),
parents,
event_type: EventType::Move,
item_id: ItemId::new_unchecked("bn-a1b2"),
data: EventData::Move(MoveData {
state: State::Done,
reason: Some("merged".into()),
extra: BTreeMap::new(),
}),
event_hash: "blake3:placeholder".into(),
};
write_event(&mut merge_event).expect("write merge event");
verify_chain(&[&root_a, &root_b, &merge_event])
.expect("DAG with merge point should be valid");
}
#[test]
fn test_hash_error_code_mismatch() {
let err = HashError::HashMismatch {
stored: "blake3:wrong".into(),
expected: "blake3:right".into(),
};
assert_eq!(err.code(), HashErrorCode::HashMismatch);
}
#[test]
fn test_hash_error_code_unknown_parent() {
let err = HashError::UnknownParent {
event_hash: "blake3:child".into(),
parent_hash: "blake3:missing".into(),
};
assert_eq!(err.code(), HashErrorCode::UnknownParent);
}
}