diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae825eb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +.crumb.local +*.swp +*.log diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..55c20d2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,358 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crumb-spec" +version = "1.0.0" +dependencies = [ + "bincode", + "blake3", + "serde", + "serde_json", + "thiserror", + "uuid", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..037eee1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crumb-spec" +version = "1.0.0" +edition = "2021" +license = "Apache-2.0 WITH LLVM-exception" +authors = ["AIEN ", "Drake Stapleton "] +description = "Crumb Protocol specification, cryptographic ledger, and verification test suite" +repository = "https://github.com/aien-dev/crumb-spec" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +blake3 = "1.5" +bincode = "1.3" +thiserror = "1.0" +uuid = { version = "1.7", features = ["v4", "v7", "serde"] } + +[lib] +name = "crumb_spec" +path = "src/lib.rs" diff --git a/SPEC.md b/SPEC.md index 08cfb8f..11bd2de 100644 --- a/SPEC.md +++ b/SPEC.md @@ -181,3 +181,39 @@ When opening `.crumb.local`: ## 7. Extensibility and Dialects Vendors, open-source communities, and agent teams MAY add custom fields under the top-level `extensions` object or introduce custom sub-objects prefixed with `x_`. Implementations conforming to RFC-0001 MUST preserve unrecognized extension fields during read-modify-write cycles. + +--- + +## 8. Cryptographic Action Ledger and Hash Chaining + +### 8.1 Purpose and Invariants +The Crumb Ledger provides an immutable, cryptographically verifiable record of operational action vectors executed across repositories by autonomous agents. Each event seals the physical mutation or audit with BLAKE3 cryptographic hashes, forming an unbroken tamper-evident hash chain. + +Key Invariants: +1. **Cryptographic Chaining**: Every event commits to the hash of its immediate predecessor (`parent_hash`). Modifying any historical event invalidates all downstream hashes. +2. **Genesis Anchor**: The initial event (`index: 0`) must have a `parent_hash` of 32 zero bytes (`[0u8; 32]`). +3. **Chronological Sequencing**: Event timestamps must be strictly monotonic (`timestamp >= parent.timestamp`). Backdated events must be rejected. +4. **Sequential Indexing**: Event indices must advance strictly by 1 (`index == parent.index + 1`). Missing parents or index gaps trigger immediate verification failure. +5. **Dual Canonical Formats**: Conforming implementations must support lossless round-trip serialization across canonical JSON and deterministic binary formats. + +### 8.2 Event Schema + +```typescript +interface LedgerEvent { + index: number; + timestamp: number; // Unix epoch seconds + agent: string; + action: "create" | "modify" | "delete" | "audit" | "test" | "build" | string; + target: string; + intent: string; + payload_hash: string; // 32-byte BLAKE3 hex or raw byte array + parent_hash: string; // Preceding event hash or 32 zero bytes for genesis + hash: string; // Sealed BLAKE3 event hash +} +``` + +### 8.3 Hash Computation +The event hash seals the canonical tuple: +`BLAKE3("CRUMB_LEDGER_EVENT_V1" || index || timestamp || agent || action || target || intent || payload_hash || parent_hash)`. +Any modification to intent, target, payload, or predecessor breaks chain verification. + diff --git a/src/ledger.rs b/src/ledger.rs new file mode 100644 index 0000000..661ea62 --- /dev/null +++ b/src/ledger.rs @@ -0,0 +1,319 @@ +//! The Cryptographic Crumb Ledger. +//! +//! Provides hash-chained, chronological action vector verification +//! for multi-agent stigmergic coordination and immutable repository history. + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum CrumbAction { + Create, + Modify, + Delete, + Audit, + Test, + Build, + Custom(String), +} + +impl CrumbAction { + pub fn as_str(&self) -> &str { + match self { + Self::Create => "create", + Self::Modify => "modify", + Self::Delete => "delete", + Self::Audit => "audit", + Self::Test => "test", + Self::Build => "build", + Self::Custom(s) => s.as_str(), + } + } +} + +#[derive(Error, Debug, PartialEq, Eq, Clone)] +pub enum LedgerError { + #[error("Hash mismatch at index {index}: computed {computed:?}, recorded {recorded:?}")] + HashMismatch { + index: u64, + computed: [u8; 32], + recorded: [u8; 32], + }, + #[error("Parent hash mismatch at index {index}: expected {expected:?}, found {found:?}")] + ParentHashMismatch { + index: u64, + expected: [u8; 32], + found: [u8; 32], + }, + #[error("Chronological sequencing violation at index {index}: previous timestamp {previous} > current timestamp {current}")] + ChronologicalViolation { + index: u64, + previous: u64, + current: u64, + }, + #[error("Invalid index sequence at {index}: expected {expected}")] + InvalidIndex { + index: u64, + expected: u64, + }, + #[error("Missing parent event for event at index {0}")] + MissingParent(u64), + #[error("Genesis parent hash must be all zeros, got {0:?}")] + InvalidGenesisParent([u8; 32]), + #[error("Serialization error: {0}")] + Serialization(String), + #[error("Deserialization error: {0}")] + Deserialization(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct LedgerEvent { + pub index: u64, + pub timestamp: u64, + pub agent: String, + pub action: CrumbAction, + pub target: String, + pub intent: String, + pub payload_hash: [u8; 32], + pub parent_hash: [u8; 32], + pub hash: [u8; 32], +} + +impl LedgerEvent { + pub fn compute_hash( + index: u64, + timestamp: u64, + agent: &str, + action: &CrumbAction, + target: &str, + intent: &str, + payload_hash: &[u8; 32], + parent_hash: &[u8; 32], + ) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(b"CRUMB_LEDGER_EVENT_V1"); + hasher.update(&index.to_be_bytes()); + hasher.update(×tamp.to_be_bytes()); + hasher.update(&(agent.len() as u32).to_be_bytes()); + hasher.update(agent.as_bytes()); + let action_str = action.as_str(); + hasher.update(&(action_str.len() as u32).to_be_bytes()); + hasher.update(action_str.as_bytes()); + hasher.update(&(target.len() as u32).to_be_bytes()); + hasher.update(target.as_bytes()); + hasher.update(&(intent.len() as u32).to_be_bytes()); + hasher.update(intent.as_bytes()); + hasher.update(payload_hash); + hasher.update(parent_hash); + *hasher.finalize().as_bytes() + } + + pub fn new( + index: u64, + timestamp: u64, + agent: String, + action: CrumbAction, + target: String, + intent: String, + payload: &[u8], + parent_hash: [u8; 32], + ) -> Self { + let payload_hash = *blake3::hash(payload).as_bytes(); + let hash = Self::compute_hash( + index, + timestamp, + &agent, + &action, + &target, + &intent, + &payload_hash, + &parent_hash, + ); + + Self { + index, + timestamp, + agent, + action, + target, + intent, + payload_hash, + parent_hash, + hash, + } + } + + pub fn verify_hash(&self) -> Result<(), LedgerError> { + let computed = Self::compute_hash( + self.index, + self.timestamp, + &self.agent, + &self.action, + &self.target, + &self.intent, + &self.payload_hash, + &self.parent_hash, + ); + + if computed != self.hash { + return Err(LedgerError::HashMismatch { + index: self.index, + computed, + recorded: self.hash, + }); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct CrumbLedger { + pub events: Vec, +} + +impl CrumbLedger { + pub fn new() -> Self { + Self { events: Vec::new() } + } + + pub fn len(&self) -> usize { + self.events.len() + } + + pub fn is_empty(&self) -> bool { + self.events.is_empty() + } + + pub fn last_event(&self) -> Option<&LedgerEvent> { + self.events.last() + } + + pub fn head_hash(&self) -> [u8; 32] { + self.events + .last() + .map(|e| e.hash) + .unwrap_or([0u8; 32]) + } + + pub fn append( + &mut self, + agent: String, + action: CrumbAction, + target: String, + intent: String, + payload: &[u8], + timestamp: u64, + ) -> Result<&LedgerEvent, LedgerError> { + let index = self.events.len() as u64; + let parent_hash = if let Some(last) = self.events.last() { + if timestamp < last.timestamp { + return Err(LedgerError::ChronologicalViolation { + index, + previous: last.timestamp, + current: timestamp, + }); + } + last.hash + } else { + [0u8; 32] + }; + + let event = LedgerEvent::new( + index, + timestamp, + agent, + action, + target, + intent, + payload, + parent_hash, + ); + + self.events.push(event); + Ok(self.events.last().expect("event just pushed")) + } + + pub fn verify_chain(&self) -> Result<(), LedgerError> { + for (i, event) in self.events.iter().enumerate() { + if event.index != i as u64 { + return Err(LedgerError::InvalidIndex { + index: event.index, + expected: i as u64, + }); + } + + event.verify_hash()?; + + if i == 0 { + if event.parent_hash != [0u8; 32] { + return Err(LedgerError::InvalidGenesisParent(event.parent_hash)); + } + } else { + let prev = &self.events[i - 1]; + if event.parent_hash != prev.hash { + return Err(LedgerError::ParentHashMismatch { + index: event.index, + expected: prev.hash, + found: event.parent_hash, + }); + } + if event.timestamp < prev.timestamp { + return Err(LedgerError::ChronologicalViolation { + index: event.index, + previous: prev.timestamp, + current: event.timestamp, + }); + } + } + } + + Ok(()) + } + + pub fn verify_parent(&self, event: &LedgerEvent) -> Result<(), LedgerError> { + if event.index == 0 { + if event.parent_hash != [0u8; 32] { + return Err(LedgerError::InvalidGenesisParent(event.parent_hash)); + } + return Ok(()); + } + + let parent_idx = (event.index - 1) as usize; + if parent_idx >= self.events.len() { + return Err(LedgerError::MissingParent(event.index)); + } + + let expected_parent = &self.events[parent_idx]; + if event.parent_hash != expected_parent.hash { + return Err(LedgerError::ParentHashMismatch { + index: event.index, + expected: expected_parent.hash, + found: event.parent_hash, + }); + } + + Ok(()) + } + + pub fn to_json(&self) -> Result { + serde_json::to_string_pretty(self) + .map_err(|e| LedgerError::Serialization(e.to_string())) + } + + pub fn from_json(json_str: &str) -> Result { + serde_json::from_str(json_str) + .map_err(|e| LedgerError::Deserialization(e.to_string())) + } + + pub fn to_binary(&self) -> Result, LedgerError> { + bincode::serialize(self) + .map_err(|e| LedgerError::Serialization(e.to_string())) + } + + pub fn from_binary(bytes: &[u8]) -> Result { + bincode::deserialize(bytes) + .map_err(|e| LedgerError::Deserialization(e.to_string())) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ed0d348 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,317 @@ +pub mod ledger; + +pub use ledger::*; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_crumb_ledger_cryptographic_hash_chaining() { + let mut ledger = CrumbLedger::new(); + assert!(ledger.is_empty()); + + let base_time: u64 = 1_700_000_000; + let actions = [ + (CrumbAction::Create, "crates/core/src/lib.rs", "Init core crate"), + (CrumbAction::Modify, "crates/core/src/lib.rs", "Implement protocol handler"), + (CrumbAction::Test, "crates/core/tests/test.rs", "Add unit tests"), + (CrumbAction::Build, "target/release/core", "Compile release binary"), + (CrumbAction::Audit, "crates/core/src/lib.rs", "Security audit"), + (CrumbAction::Modify, "Cargo.toml", "Bump version to 1.0.0"), + (CrumbAction::Test, "crates/core/tests/integration.rs", "Run integration tests"), + (CrumbAction::Create, "README.md", "Document architecture"), + ]; + + for (i, (action, target, intent)) in actions.iter().enumerate() { + let payload = format!("payload-content-step-{}", i); + let timestamp = base_time + (i as u64 * 10); + ledger + .append( + "agent-atlas-prime".into(), + action.clone(), + target.to_string(), + intent.to_string(), + payload.as_bytes(), + timestamp, + ) + .expect("append should succeed"); + } + + assert_eq!(ledger.len(), 8); + assert!(ledger.verify_chain().is_ok(), "Untampered chain must verify successfully"); + + // Scenario 1: Tamper with genesis event payload + { + let mut tampered = ledger.clone(); + tampered.events[0].intent = "Malicious backdoor inserted".to_string(); + let err = tampered.verify_chain(); + assert!( + matches!(err, Err(LedgerError::HashMismatch { index: 0, .. })), + "Tampering genesis event must trigger hash mismatch at index 0" + ); + + // If attacker updates event 0's hash to match tampered payload, event 1 fails parent check + tampered.events[0].hash = LedgerEvent::compute_hash( + tampered.events[0].index, + tampered.events[0].timestamp, + &tampered.events[0].agent, + &tampered.events[0].action, + &tampered.events[0].target, + &tampered.events[0].intent, + &tampered.events[0].payload_hash, + &tampered.events[0].parent_hash, + ); + let err2 = tampered.verify_chain(); + assert!( + matches!(err2, Err(LedgerError::ParentHashMismatch { index: 1, .. })), + "Downstream event 1 must reject modified genesis parent hash" + ); + } + + // Scenario 2: Tamper with historical event in the middle of the chain (index 3) + { + let mut tampered = ledger.clone(); + tampered.events[3].target = "crates/tampered/path.rs".to_string(); + let err = tampered.verify_chain(); + assert!( + matches!(err, Err(LedgerError::HashMismatch { index: 3, .. })), + "Tampering historical event 3 must trigger hash mismatch at index 3" + ); + + // Recompute event 3 hash to match tampered data + tampered.events[3].hash = LedgerEvent::compute_hash( + tampered.events[3].index, + tampered.events[3].timestamp, + &tampered.events[3].agent, + &tampered.events[3].action, + &tampered.events[3].target, + &tampered.events[3].intent, + &tampered.events[3].payload_hash, + &tampered.events[3].parent_hash, + ); + // Event 4 parent_hash still expects original event 3 hash + let err_downstream = tampered.verify_chain(); + assert!( + matches!(err_downstream, Err(LedgerError::ParentHashMismatch { index: 4, .. })), + "Downstream event 4 must fail verification due to broken parent hash link" + ); + } + + // Scenario 3: Single bit flip in recorded hash + { + let mut tampered = ledger.clone(); + tampered.events[5].hash[0] ^= 0x01; + let err = tampered.verify_chain(); + assert!( + matches!(err, Err(LedgerError::HashMismatch { index: 5, .. })), + "Single bit alteration in hash must trigger hash mismatch" + ); + } + } + + #[test] + fn test_crumb_ledger_chronological_sequencing_validation() { + let mut ledger = CrumbLedger::new(); + let base_time: u64 = 1_700_000_100; + + ledger + .append( + "agent-alpha".into(), + CrumbAction::Create, + "src/main.rs".into(), + "Initial commit".into(), + b"fn main() {}", + base_time, + ) + .expect("first append must succeed"); + + // Attempt to append an event with a past timestamp (chronological violation) + let past_time = base_time - 50; + let append_res = ledger.append( + "agent-beta".into(), + CrumbAction::Modify, + "src/main.rs".into(), + "Retroactive edit attempt".into(), + b"fn main() { println!(); }", + past_time, + ); + + assert!( + matches!( + append_res, + Err(LedgerError::ChronologicalViolation { + index: 1, + previous: 1_700_000_100, + current: 1_700_000_050 + }) + ), + "Appending past timestamp must be rejected with ChronologicalViolation" + ); + + // Identical timestamp within same batch succeeds + assert!( + ledger + .append( + "agent-gamma".into(), + CrumbAction::Audit, + "src/main.rs".into(), + "Concurrent audit in same second".into(), + b"audit-proof", + base_time, + ) + .is_ok() + ); + + // Forward timestamp succeeds + assert!( + ledger + .append( + "agent-delta".into(), + CrumbAction::Test, + "tests/unit.rs".into(), + "Pass test".into(), + b"test-proof", + base_time + 10, + ) + .is_ok() + ); + + assert!(ledger.verify_chain().is_ok()); + } + + #[test] + fn test_crumb_ledger_missing_parent_detection() { + let mut ledger = CrumbLedger::new(); + let base_time: u64 = 1_700_000_000; + + for i in 0..5 { + ledger + .append( + "agent-worker".into(), + CrumbAction::Modify, + format!("file_{}.rs", i), + format!("Intent {}", i), + format!("data-{}", i).as_bytes(), + base_time + i, + ) + .unwrap(); + } + + // Test missing parent when index 2 is deleted from the sequence + let mut gapped_ledger = CrumbLedger::new(); + gapped_ledger.events.push(ledger.events[0].clone()); + gapped_ledger.events.push(ledger.events[1].clone()); + // Skip index 2, append index 3 + gapped_ledger.events.push(ledger.events[3].clone()); + + let err = gapped_ledger.verify_chain(); + assert!( + matches!(err, Err(LedgerError::InvalidIndex { index: 3, expected: 2 })), + "Gapped chain must be rejected for index inconsistency" + ); + + // Test altered parent_hash directly + let mut corrupted_parent = ledger.clone(); + corrupted_parent.events[2].parent_hash = [0xFF; 32]; + let err_parent = corrupted_parent.verify_chain(); + // Since hash depends on parent_hash, hash verification fails first + assert!(matches!(err_parent, Err(LedgerError::HashMismatch { index: 2, .. }))); + + // If attacker recomputes hash to match corrupted parent_hash: + corrupted_parent.events[2].hash = LedgerEvent::compute_hash( + corrupted_parent.events[2].index, + corrupted_parent.events[2].timestamp, + &corrupted_parent.events[2].agent, + &corrupted_parent.events[2].action, + &corrupted_parent.events[2].target, + &corrupted_parent.events[2].intent, + &corrupted_parent.events[2].payload_hash, + &corrupted_parent.events[2].parent_hash, + ); + let err_parent2 = corrupted_parent.verify_chain(); + assert!( + matches!(err_parent2, Err(LedgerError::ParentHashMismatch { index: 2, .. })), + "Parent hash mismatch must be detected when referencing non-existent parent" + ); + + // Test verify_parent method + assert!(ledger.verify_parent(&ledger.events[1]).is_ok()); + let mut detached_event = ledger.events[3].clone(); + detached_event.parent_hash = [0xAA; 32]; + assert!(matches!( + ledger.verify_parent(&detached_event), + Err(LedgerError::ParentHashMismatch { .. }) + )); + } + + #[test] + fn test_crumb_ledger_serialization_roundtrip_json() { + let mut ledger = CrumbLedger::new(); + let base_time: u64 = 1_700_000_500; + + ledger + .append( + "agent-builder".into(), + CrumbAction::Create, + "Cargo.toml".into(), + "Define workspace manifest".into(), + b"[workspace]\nresolver = \"2\"", + base_time, + ) + .unwrap(); + + ledger + .append( + "agent-reviewer".into(), + CrumbAction::Audit, + "Cargo.toml".into(), + "Verify workspace dependencies".into(), + b"audit: clean", + base_time + 5, + ) + .unwrap(); + + let json_str = ledger.to_json().expect("JSON serialization must succeed"); + assert!(json_str.contains("agent-builder")); + assert!(json_str.contains("agent-reviewer")); + assert!(json_str.contains("payload_hash")); + + let restored = CrumbLedger::from_json(&json_str).expect("JSON deserialization must succeed"); + assert_eq!(ledger, restored, "Deserialized ledger must exactly equal original"); + assert!(restored.verify_chain().is_ok(), "Restored ledger chain must verify"); + + // Corrupted JSON must fail + assert!(CrumbLedger::from_json("invalid json payload").is_err()); + } + + #[test] + fn test_crumb_ledger_serialization_roundtrip_canonical_binary() { + let mut ledger = CrumbLedger::new(); + let base_time: u64 = 1_700_000_900; + + for i in 0..10 { + ledger + .append( + format!("agent-binary-{}", i), + CrumbAction::Modify, + format!("kernel/module_{}.bin", i), + format!("Install binary patch {}", i), + &[i as u8; 64], + base_time + (i as u64 * 10), + ) + .unwrap(); + } + + let binary_data = ledger.to_binary().expect("Binary serialization must succeed"); + assert!(!binary_data.is_empty()); + + let restored = CrumbLedger::from_binary(&binary_data).expect("Binary deserialization must succeed"); + assert_eq!(ledger, restored, "Canonical binary roundtrip must yield identical ledger"); + assert!(restored.verify_chain().is_ok(), "Restored ledger from binary must verify"); + + // Corrupted binary payload must fail + let corrupted_bytes = vec![0xFF, 0x00, 0xAA]; + assert!(CrumbLedger::from_binary(&corrupted_bytes).is_err()); + } +}