1use std::path::Path;
29use std::sync::{Arc, Mutex};
30
31use chrono::{DateTime, Utc};
32use ed25519_dalek::{SigningKey, VerifyingKey};
33use rvf_types::SEGMENT_HEADER_SIZE;
34use weftos_rvf_crypto::hash::shake256_256;
35use weftos_rvf_crypto::{
36 MlDsa65Key, MlDsa65VerifyKey, WitnessEntry, create_witness_chain, decode_signature_footer,
37 encode_signature_footer, lineage_record_to_bytes, lineage_witness_entry, sign_segment,
38 sign_segment_ml_dsa, verify_segment, verify_segment_ml_dsa, verify_witness_chain,
39};
40use weftos_rvf_wire::writer::{calculate_padded_size, write_segment};
41use weftos_rvf_wire::{read_segment, validate_segment};
42
43const EXOCHAIN_MAGIC: u32 = 0x4558_4F43; #[derive(Debug, Clone)]
64struct ExoChainHeader {
65 magic: u32,
66 version: u8,
67 subtype: u8,
68 flags: u16,
69 chain_id: u32,
70 _reserved: u32,
71 sequence: u64,
72 timestamp_secs: u64,
73 prev_hash: [u8; 32],
74}
75
76const EXOCHAIN_HEADER_SIZE: usize = 64;
77
78impl ExoChainHeader {
79 fn to_bytes(&self) -> [u8; EXOCHAIN_HEADER_SIZE] {
80 let mut buf = [0u8; EXOCHAIN_HEADER_SIZE];
81 buf[0..4].copy_from_slice(&self.magic.to_le_bytes());
82 buf[4] = self.version;
83 buf[5] = self.subtype;
84 buf[6..8].copy_from_slice(&self.flags.to_le_bytes());
85 buf[8..12].copy_from_slice(&self.chain_id.to_le_bytes());
86 buf[12..16].copy_from_slice(&self._reserved.to_le_bytes());
87 buf[16..24].copy_from_slice(&self.sequence.to_le_bytes());
88 buf[24..32].copy_from_slice(&self.timestamp_secs.to_le_bytes());
89 buf[32..64].copy_from_slice(&self.prev_hash);
90 buf
91 }
92
93 fn from_bytes(data: &[u8]) -> Option<Self> {
94 if data.len() < EXOCHAIN_HEADER_SIZE {
95 return None;
96 }
97 let magic = u32::from_le_bytes(data[0..4].try_into().ok()?);
98 if magic != EXOCHAIN_MAGIC {
99 return None;
100 }
101 Some(Self {
102 magic,
103 version: data[4],
104 subtype: data[5],
105 flags: u16::from_le_bytes(data[6..8].try_into().ok()?),
106 chain_id: u32::from_le_bytes(data[8..12].try_into().ok()?),
107 _reserved: u32::from_le_bytes(data[12..16].try_into().ok()?),
108 sequence: u64::from_le_bytes(data[16..24].try_into().ok()?),
109 timestamp_secs: u64::from_le_bytes(data[24..32].try_into().ok()?),
110 prev_hash: data[32..64].try_into().ok()?,
111 })
112 }
113}
114
115fn write_exochain_event(header: &ExoChainHeader, cbor: &[u8], segment_id: u64) -> Vec<u8> {
117 let exo_bytes = header.to_bytes();
118 let mut payload = Vec::with_capacity(exo_bytes.len() + cbor.len());
119 payload.extend_from_slice(&exo_bytes);
120 payload.extend_from_slice(cbor);
121 write_segment(
122 0x10, &payload,
124 rvf_types::SegmentFlags::empty(),
125 segment_id,
126 )
127}
128
129fn decode_exochain_payload(payload: &[u8]) -> Option<(ExoChainHeader, &[u8])> {
131 let header = ExoChainHeader::from_bytes(payload)?;
132 Some((header, &payload[EXOCHAIN_HEADER_SIZE..]))
133}
134use serde::{Deserialize, Serialize};
135use tracing::{debug, info, warn};
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ChainEvent {
140 pub sequence: u64,
142 pub chain_id: u32,
144 pub timestamp: DateTime<Utc>,
146 pub prev_hash: [u8; 32],
148 pub hash: [u8; 32],
150 #[serde(default)]
153 pub payload_hash: [u8; 32],
154 pub source: String,
156 pub kind: String,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub payload: Option<serde_json::Value>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub idempotency_key: Option<[u8; 32]>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub rule_hash: Option<[u8; 32]>,
186}
187
188pub const IDEMPOTENCY_LOOKBACK: usize = 1000;
195
196pub const RESERVED_SOURCES: &[&str] = &["user.projects", "project.anchor", "auth.token"];
206
207pub fn is_reserved_source(source: &str) -> bool {
209 RESERVED_SOURCES.contains(&source.trim())
210}
211
212pub const KERNEL_SOURCES: &[&str] = &["governance", "project", "project.supervisor"];
221
222pub const RESERVED_KINDS: &[&str] = &["governance.overlay.applied"];
228
229pub fn is_reserved_kind(kind: &str) -> bool {
231 RESERVED_KINDS.contains(&kind.trim())
232}
233
234pub const PROJECT_DOMAIN_PREFIX: &[u8] = b"weftos-project-";
241
242pub fn signs_project_domain(data: &[u8]) -> bool {
245 data.starts_with(PROJECT_DOMAIN_PREFIX)
246}
247
248pub fn is_caller_reserved_source(source: &str) -> bool {
250 is_reserved_source(source) || KERNEL_SOURCES.contains(&source.trim())
251}
252
253#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
255pub enum AppendSignedError {
256 #[error("event seq {sequence} already present (idempotent skip)")]
258 AlreadyPresent { sequence: u64 },
259 #[error("fork at seq {sequence}: local and remote hashes diverge")]
261 Fork {
262 sequence: u64,
263 local_hash: [u8; 32],
264 remote_hash: [u8; 32],
265 },
266 #[error("gap: expected seq {expected}, got {got}")]
268 Gap { expected: u64, got: u64 },
269 #[error("source {event_source:?} is reserved and cannot be replicated")]
271 ReservedSource { event_source: String },
272 #[error("chain_id mismatch: local={local}, remote={remote}")]
274 ChainIdMismatch { local: u32, remote: u32 },
275 #[error("prev_hash mismatch at append")]
277 PrevHashMismatch {
278 expected: [u8; 32],
279 got: [u8; 32],
280 },
281 #[error("payload_hash mismatch at seq {sequence}")]
283 PayloadHashMismatch { sequence: u64 },
284 #[error("event hash mismatch at seq {sequence}")]
286 HashMismatch {
287 sequence: u64,
288 recomputed: [u8; 32],
289 stored: [u8; 32],
290 },
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ChainCheckpoint {
296 pub chain_id: u32,
298 pub sequence: u64,
300 pub last_hash: [u8; 32],
302 pub timestamp: DateTime<Utc>,
304 pub events_since_last: u64,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct ChainVerifyResult {
311 pub valid: bool,
313 pub event_count: usize,
315 pub errors: Vec<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub signature_verified: Option<bool>,
321}
322
323pub(crate) fn compute_payload_hash(payload: &Option<serde_json::Value>) -> [u8; 32] {
328 match payload {
329 Some(val) => {
330 let bytes = serde_json::to_vec(val).unwrap_or_default();
331 shake256_256(&bytes)
332 }
333 None => [0u8; 32],
334 }
335}
336
337#[allow(clippy::too_many_arguments)]
355pub(crate) fn compute_event_hash(
356 sequence: u64,
357 chain_id: u32,
358 prev_hash: &[u8; 32],
359 source: &str,
360 kind: &str,
361 timestamp: &DateTime<Utc>,
362 payload_hash: &[u8; 32],
363 rule_hash: Option<&[u8; 32]>,
364) -> [u8; 32] {
365 let mut buf = Vec::with_capacity(160);
366 buf.extend_from_slice(&sequence.to_le_bytes());
367 buf.extend_from_slice(&chain_id.to_le_bytes());
368 buf.extend_from_slice(prev_hash);
369 buf.extend_from_slice(source.as_bytes());
370 buf.push(0x00); buf.extend_from_slice(kind.as_bytes());
372 buf.push(0x00); buf.extend_from_slice(×tamp.timestamp().to_le_bytes());
374 buf.extend_from_slice(payload_hash);
375 if let Some(rh) = rule_hash {
380 buf.push(0x01); buf.extend_from_slice(rh);
382 }
383 shake256_256(&buf)
384}
385
386const WITNESS_PROVENANCE: u8 = 0x01;
388
389pub const EVENT_KIND_CAPABILITY_REVOKED: &str = "capability.revoked";
401
402pub const EVENT_KIND_API_CONTRACT_REGISTERED: &str = "service.contract.register";
407
408pub const EVENT_KIND_TOOL_DEPLOYED: &str = "tool.deploy";
410
411pub const EVENT_KIND_TOOL_VERSION_REVOKED: &str = "tool.version.revoke";
413
414pub const EVENT_KIND_GATE_PERMIT: &str = "gate.permit";
419
420pub const EVENT_KIND_GATE_DEFER: &str = "gate.defer";
425
426pub const EVENT_KIND_GATE_DENY: &str = "gate.deny";
431
432pub const EVENT_KIND_SANDBOX_SUDO_OVERRIDE: &str = "sandbox.sudo.override";
437
438pub const EVENT_KIND_TOOL_SIGNED: &str = "tool.signed";
443
444pub const EVENT_KIND_SHELL_EXEC: &str = "shell.exec";
449
450pub const EVENT_KIND_EML_TRAINED: &str = "eml.trained";
456
457pub const EVENT_KIND_EML_DRIFT: &str = "eml.drift";
463
464pub const EVENT_KIND_EML_SAVED: &str = "eml.saved";
466
467pub const EVENT_KIND_EML_LOADED: &str = "eml.loaded";
469
470pub const EVENT_KIND_LEWM_FRAME_ATTESTATION: &str = "lewm.frame.attestation";
477
478pub const EVENT_KIND_MESH_SENSOR_FRAME: &str = "mesh.sensor.v1.frame";
484
485pub const EVENT_KIND_SIGREG_HEALTH: &str = "lewm.sigreg.health";
490
491pub const EVENT_KIND_AUTH_CREDENTIAL_REGISTER: &str = "auth.credential.register";
496
497pub const EVENT_KIND_AUTH_CREDENTIAL_ROTATE: &str = "auth.credential.rotate";
502
503pub const EVENT_KIND_AUTH_TOKEN_ISSUE: &str = "auth.token.issue";
508
509pub const EVENT_KIND_AUTH_TOKEN_REVOKE: &str = "auth.token.revoke";
514
515pub const EVENT_KIND_AUTH_ATTEMPT: &str = "auth.attempt";
520
521pub const EVENT_KIND_CONFIG_SET: &str = "config.set";
526
527pub const EVENT_KIND_CONFIG_DELETE: &str = "config.delete";
532
533pub const EVENT_KIND_CONFIG_SECRET_SET: &str = "config.secret.set";
538
539pub const EVENT_KIND_APP_INSTALL: &str = "app.install";
541
542pub const EVENT_KIND_APP_REMOVE: &str = "app.remove";
544
545pub const EVENT_KIND_APP_START: &str = "app.start";
547
548pub const EVENT_KIND_APP_STOP: &str = "app.stop";
550
551pub const EVENT_KIND_APP_TRANSITION: &str = "app.transition";
553
554pub const EVENT_KIND_WORKLOAD_INSTALL: &str = "workload.install";
561pub const EVENT_KIND_WORKLOAD_PLACE: &str = "workload.place";
563pub const EVENT_KIND_WORKLOAD_LOAD: &str = "workload.load";
565pub const EVENT_KIND_WORKLOAD_START: &str = "workload.start";
567pub const EVENT_KIND_WORKLOAD_STOP: &str = "workload.stop";
569pub const EVENT_KIND_WORKLOAD_UNLOAD: &str = "workload.unload";
571pub const EVENT_KIND_WORKLOAD_MIGRATE: &str = "workload.migrate";
573pub const EVENT_KIND_WORKLOAD_REFUSE: &str = "workload.refuse";
575pub const EVENT_KIND_WORKLOAD_REVOKE: &str = "workload.revoke";
577pub const EVENT_KIND_WORKLOAD_UNREVOKE: &str = "workload.unrevoke";
579pub const EVENT_KIND_WORKLOAD_LIFECYCLE: &str = "workload.lifecycle";
581pub const EVENT_KIND_WORKLOAD_NODE_BIND: &str = "workload.node.bind";
583pub const EVENT_KIND_WORKLOAD_NODE_UNBIND: &str = "workload.node.unbind";
585
586pub const EVENT_KIND_ARTIFACT_FETCH: &str = "artifact.fetch";
592pub const EVENT_KIND_ARTIFACT_PIECE_REJECTED: &str = "artifact.piece_rejected";
594pub const EVENT_KIND_ARTIFACT_SERVE: &str = "artifact.serve";
596
597pub const EVENT_KIND_ARTIFACT_SEED: &str = "artifact.seed";
603pub const EVENT_KIND_ARTIFACT_EVICT: &str = "artifact.evict";
605pub const EVENT_KIND_ARTIFACT_REVOKE: &str = "artifact.revoke";
607pub const EVENT_KIND_ARTIFACT_PEER_BAN: &str = "artifact.peer_ban";
609
610pub const EVENT_KIND_NODE_FACTS_REFRESH: &str = "node.facts.refresh";
613
614pub const EVENT_KIND_CRON_ADD: &str = "cron.add";
616
617pub const EVENT_KIND_CRON_REMOVE: &str = "cron.remove";
619
620pub const EVENT_KIND_CRON_EXECUTE: &str = "cron.execute";
622
623pub const EVENT_KIND_CONTAINER_START: &str = "container.start";
627
628pub const EVENT_KIND_CONTAINER_STOP: &str = "container.stop";
630
631pub const EVENT_KIND_CONTAINER_CONFIGURE: &str = "container.configure";
633
634pub const EVENT_KIND_PROCESS_REGISTER: &str = "process.register";
636
637pub const EVENT_KIND_PROCESS_DEREGISTER: &str = "process.deregister";
639
640pub const EVENT_KIND_PROCESS_STATE: &str = "process.state";
642
643pub const EVENT_KIND_WASM_EXECUTE: &str = "wasm.execute";
645
646pub const EVENT_KIND_WASM_FS_WRITE: &str = "wasm.fs.write";
648
649pub const EVENT_KIND_WASM_FS_REMOVE: &str = "wasm.fs.remove";
651
652pub const EVENT_KIND_WASM_FS_CREATE_DIR: &str = "wasm.fs.create_dir";
654
655pub const EVENT_KIND_WASM_FS_COPY: &str = "wasm.fs.copy";
657
658pub const EVENT_KIND_WASM_FS_MOVE: &str = "wasm.fs.move";
660
661pub const EVENT_KIND_AGENT_HIERARCHY_ADD: &str = "agent.hierarchy.add_child";
663
664pub const EVENT_KIND_AGENT_HIERARCHY_REMOVE: &str = "agent.hierarchy.remove_child";
666
667pub const EVENT_KIND_CLUSTER_PEER_ADD: &str = "cluster.peer.add";
672
673pub const EVENT_KIND_CLUSTER_PEER_REMOVE: &str = "cluster.peer.remove";
678
679pub const EVENT_KIND_CLUSTER_PEER_STATE: &str = "cluster.peer.state";
684
685pub const EVENT_KIND_CAPABILITY_ELEVATE: &str = "capability.elevate";
690
691pub const EVENT_KIND_ENV_REGISTER: &str = "env.register";
696
697pub const EVENT_KIND_ENV_SWITCH: &str = "env.switch";
702
703pub const EVENT_KIND_ENV_REMOVE: &str = "env.remove";
708
709pub const EVENT_KIND_MESH_SERVICE_REGISTER: &str = "mesh.service.register";
713
714pub const EVENT_KIND_MESH_SERVICE_DEREGISTER: &str = "mesh.service.deregister";
716
717pub const EVENT_KIND_MESH_ARTIFACT_STORE: &str = "mesh.artifact.store";
719
720pub const EVENT_KIND_MESH_ARTIFACT_FETCH: &str = "mesh.artifact.fetch";
722
723pub const EVENT_KIND_MESH_IPC_SEND: &str = "mesh.ipc.send";
725
726pub const EVENT_KIND_MESH_PEER_ADD: &str = "mesh.peer.add";
728
729pub const EVENT_KIND_MESH_PEER_REMOVE: &str = "mesh.peer.remove";
731
732pub const EVENT_KIND_KERNEL_SAVE: &str = "kernel.save";
736
737pub const EVENT_KIND_KERNEL_LOAD: &str = "kernel.load";
739
740pub const EVENT_KIND_RECONCILER_ACTION: &str = "reconciler.action";
744
745pub const EVENT_KIND_RECONCILER_TICK: &str = "reconciler.tick";
747
748pub const EVENT_KIND_RECONCILER_DESIRED_SET: &str = "reconciler.desired.set";
750
751pub const EVENT_KIND_RECONCILER_DESIRED_REMOVE: &str = "reconciler.desired.remove";
753
754pub const EVENT_KIND_CAUSAL_NODE_ADD: &str = "causal.node.add";
759
760pub const EVENT_KIND_CAUSAL_NODE_REMOVE: &str = "causal.node.remove";
765
766pub const EVENT_KIND_CAUSAL_NODE_STATE: &str = "causal.node.state";
775
776pub const EVENT_KIND_CAUSAL_EDGE_ADD: &str = "causal.edge.add";
781
782pub const EVENT_KIND_CAUSAL_EDGE_REMOVE: &str = "causal.edge.remove";
787
788pub const EVENT_KIND_CAUSAL_CLEAR: &str = "causal.clear";
793
794pub const EVENT_KIND_ARTIFACT_STORE: &str = "artifact.store";
799
800pub const EVENT_KIND_ARTIFACT_REMOVE: &str = "artifact.remove";
805
806pub const EVENT_KIND_SANDBOX_EXECUTE: &str = "sandbox.execute";
814
815pub const EVENT_KIND_SESSION_APPEND: &str = "session.append";
820pub const EVENT_KIND_SESSION_CREATE: &str = "session.create";
821
822pub const EVENT_KIND_SESSION_DESTROY: &str = "session.destroy";
827
828pub const EVENT_KIND_WORKSPACE_CREATE: &str = "workspace.create";
833
834pub const EVENT_KIND_WORKSPACE_CONFIG: &str = "workspace.config";
839
840pub const EVENT_KIND_TOOL_REGISTER: &str = "tool.register";
845
846pub const EVENT_KIND_GRAPHIFY_BUILD: &str = "graphify.build";
852
853pub const EVENT_KIND_GRAPHIFY_INGEST: &str = "graphify.ingest";
858
859pub const EVENT_KIND_GRAPHIFY_PIPELINE: &str = "graphify.pipeline";
865
866pub const EVENT_KIND_GRAPHIFY_HOOK: &str = "graphify.hook";
872
873pub const EVENT_KIND_GRAPHIFY_QUERY_RESULT: &str = "graphify.query_result";
878
879pub const EVENT_KIND_TOOL_REGISTER_METADATA: &str = "tool.register.metadata";
885
886pub const EVENT_KIND_PROJECT_INIT: &str = "project.init";
891
892pub const EVENT_KIND_PROFILE_CREATE: &str = "profile.create";
897
898pub const EVENT_KIND_PROFILE_DELETE: &str = "profile.delete";
903
904pub const EVENT_KIND_PROFILE_SWITCH: &str = "profile.switch";
909
910pub const EVENT_KIND_PROFILE_VECTOR_INSERT: &str = "profile.vector.insert";
915
916pub const EVENT_KIND_BVH_INSERT: &str = "bvh.insert";
920
921pub const EVENT_KIND_BVH_REMOVE: &str = "bvh.remove";
925
926pub const EVENT_KIND_BVH_DERIVE: &str = "bvh.derive";
930
931pub const EVENT_KIND_BVH_REBALANCE_SEAL: &str = "bvh.rebalance_seal";
935
936pub const EVENT_KIND_HNSW_INSERT: &str = "hnsw.insert";
941
942pub const EVENT_KIND_HNSW_CLEAR: &str = "hnsw.clear";
946
947pub const EVENT_KIND_HNSW_SAVE: &str = "hnsw.save";
952
953pub const EVENT_KIND_HNSW_LOAD: &str = "hnsw.load";
958
959pub const EVENT_KIND_HNSW_EML_OBSERVE: &str = "hnsw.eml.observe";
967
968pub const EVENT_KIND_HNSW_EML_RECALL: &str = "hnsw.eml.recall";
973
974pub const EVENT_KIND_HNSW_EML_TRAINED: &str = "hnsw.eml.trained";
979
980pub const EVENT_KIND_HNSW_EML_TRIAGE: &str = "hnsw.eml.triage";
987
988pub type RuleHashProvider = std::sync::Arc<dyn Fn() -> Option<[u8; 32]> + Send + Sync>;
990
991const EXO_FLAG_HAS_RULE_HASH: u16 = 0x0001;
993const EXO_FLAGS_KNOWN: u16 = EXO_FLAG_HAS_RULE_HASH;
996
997fn check_exo_flags(flags: u16, has_rule_hash: bool) -> Result<(), String> {
999 if flags & !EXO_FLAGS_KNOWN != 0 {
1000 return Err(format!(
1001 "segment flags {flags:#06x} carry bits this build does not know: \
1002 written by a newer binary"
1003 ));
1004 }
1005 if (flags & EXO_FLAG_HAS_RULE_HASH != 0) != has_rule_hash {
1006 return Err("segment rule_hash flag disagrees with its payload".into());
1007 }
1008 Ok(())
1009}
1010
1011struct LocalChain {
1013 chain_id: u32,
1014 events: Vec<ChainEvent>,
1015 last_hash: [u8; 32],
1016 sequence: u64,
1017 checkpoint_interval: u64,
1018 events_since_checkpoint: u64,
1019 checkpoints: Vec<ChainCheckpoint>,
1020 witness_entries: Vec<WitnessEntry>,
1022 rule_hash_provider: Option<RuleHashProvider>,
1025 max_replay_window: u64,
1027 hub: crate::chain_subscribe::EventHub,
1029}
1030
1031impl LocalChain {
1032 fn new(chain_id: u32, checkpoint_interval: u64) -> Self {
1033 Self {
1034 chain_id,
1035 events: Vec::new(),
1036 last_hash: [0u8; 32],
1037 sequence: 0,
1038 checkpoint_interval,
1039 events_since_checkpoint: 0,
1040 checkpoints: Vec::new(),
1041 witness_entries: Vec::new(),
1042 rule_hash_provider: None,
1043 max_replay_window: crate::chain_subscribe::DEFAULT_MAX_REPLAY_WINDOW,
1044 hub: Default::default(),
1045 }
1046 }
1047
1048 fn from_events(
1050 chain_id: u32,
1051 checkpoint_interval: u64,
1052 events: Vec<ChainEvent>,
1053 witness_entries: Vec<WitnessEntry>,
1054 ) -> Self {
1055 let (last_hash, sequence) = if let Some(last) = events.last() {
1056 (last.hash, last.sequence + 1)
1057 } else {
1058 ([0u8; 32], 0)
1059 };
1060 Self {
1061 chain_id,
1062 events,
1063 last_hash,
1064 sequence,
1065 checkpoint_interval,
1066 events_since_checkpoint: 0,
1067 checkpoints: Vec::new(),
1068 witness_entries,
1069 rule_hash_provider: None,
1070 max_replay_window: crate::chain_subscribe::DEFAULT_MAX_REPLAY_WINDOW,
1071 hub: Default::default(),
1072 }
1073 }
1074
1075 fn stamp_rule_hash(&self) -> Option<[u8; 32]> {
1082 let p = self.rule_hash_provider.as_ref()?;
1083 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| p())) {
1084 Ok(h) => h,
1085 Err(_) => {
1086 warn!("rule_hash provider panicked; appending without rule_hash");
1087 None
1088 }
1089 }
1090 }
1091
1092 fn append(
1093 &mut self,
1094 source: String,
1095 kind: String,
1096 payload: Option<serde_json::Value>,
1097 ) -> &ChainEvent {
1098 self.append_with_key(source, kind, payload, None)
1099 }
1100
1101 fn append_with_key(
1108 &mut self,
1109 source: String,
1110 kind: String,
1111 payload: Option<serde_json::Value>,
1112 idempotency_key: Option<[u8; 32]>,
1113 ) -> &ChainEvent {
1114 let timestamp = Utc::now();
1115 let payload_hash = compute_payload_hash(&payload);
1116 let rule_hash = self.stamp_rule_hash();
1117 let hash = compute_event_hash(
1118 self.sequence,
1119 self.chain_id,
1120 &self.last_hash,
1121 &source,
1122 &kind,
1123 ×tamp,
1124 &payload_hash,
1125 rule_hash.as_ref(),
1126 );
1127
1128 let event = ChainEvent {
1129 sequence: self.sequence,
1130 chain_id: self.chain_id,
1131 timestamp,
1132 prev_hash: self.last_hash,
1133 hash,
1134 payload_hash,
1135 source,
1136 kind,
1137 payload,
1138 idempotency_key,
1139 rule_hash,
1140 };
1141
1142 self.witness_entries.push(WitnessEntry {
1144 prev_hash: [0u8; 32], action_hash: hash,
1146 timestamp_ns: timestamp.timestamp_nanos_opt().unwrap_or(0) as u64,
1147 witness_type: WITNESS_PROVENANCE,
1148 });
1149
1150 self.last_hash = hash;
1151 self.sequence += 1;
1152 self.events_since_checkpoint += 1;
1153 self.hub.publish(&event);
1154 self.events.push(event);
1155
1156 if self.checkpoint_interval > 0 && self.events_since_checkpoint >= self.checkpoint_interval
1158 {
1159 self.create_checkpoint();
1160 }
1161
1162 self.events.last().unwrap()
1163 }
1164
1165 fn create_checkpoint(&mut self) -> ChainCheckpoint {
1166 let cp = ChainCheckpoint {
1167 chain_id: self.chain_id,
1168 sequence: self.sequence.saturating_sub(1),
1169 last_hash: self.last_hash,
1170 timestamp: Utc::now(),
1171 events_since_last: self.events_since_checkpoint,
1172 };
1173 self.events_since_checkpoint = 0;
1174 self.checkpoints.push(cp.clone());
1175 cp
1176 }
1177}
1178
1179#[derive(Serialize, Deserialize)]
1184struct RvfChainPayload {
1185 source: String,
1186 kind: String,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1188 payload: Option<serde_json::Value>,
1189 payload_hash: String,
1191 hash: String,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1195 rule_hash: Option<String>,
1196}
1197
1198fn hex_hash(h: &[u8; 32]) -> String {
1200 h.iter().map(|b| format!("{b:02x}")).collect()
1201}
1202
1203fn hex_encode(data: &[u8]) -> String {
1205 data.iter().map(|b| format!("{b:02x}")).collect()
1206}
1207
1208fn hex_decode(s: &str) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
1210 if !s.len().is_multiple_of(2) {
1211 return Err("hex string has odd length".into());
1212 }
1213 let mut out = Vec::with_capacity(s.len() / 2);
1214 for chunk in s.as_bytes().chunks(2) {
1215 let hi = hex_nibble(chunk[0])?;
1216 let lo = hex_nibble(chunk[1])?;
1217 out.push((hi << 4) | lo);
1218 }
1219 Ok(out)
1220}
1221
1222fn parse_hex_hash(s: &str) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
1224 if s.len() != 64 {
1225 return Err(format!("expected 64 hex chars, got {}", s.len()).into());
1226 }
1227 let mut out = [0u8; 32];
1228 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
1229 let hi = hex_nibble(chunk[0])?;
1230 let lo = hex_nibble(chunk[1])?;
1231 out[i] = (hi << 4) | lo;
1232 }
1233 Ok(out)
1234}
1235
1236fn hex_nibble(c: u8) -> Result<u8, Box<dyn std::error::Error + Send + Sync>> {
1238 match c {
1239 b'0'..=b'9' => Ok(c - b'0'),
1240 b'a'..=b'f' => Ok(c - b'a' + 10),
1241 b'A'..=b'F' => Ok(c - b'A' + 10),
1242 _ => Err(format!("invalid hex char: {}", c as char).into()),
1243 }
1244}
1245
1246pub struct ChainManager {
1252 inner: Mutex<LocalChain>,
1253 signing_key: Option<SigningKey>,
1255 ml_dsa_key: Option<MlDsa65Key>,
1257}
1258
1259impl ChainManager {
1260 pub fn new(chain_id: u32, checkpoint_interval: u64) -> Self {
1262 let mut chain = LocalChain::new(chain_id, checkpoint_interval);
1263 chain.append(
1265 "chain".into(),
1266 "genesis".into(),
1267 Some(serde_json::json!({ "chain_id": chain_id })),
1268 );
1269 debug!(chain_id, "local chain initialized with genesis event");
1270
1271 Self {
1272 inner: Mutex::new(chain),
1273 signing_key: None,
1274 ml_dsa_key: None,
1275 }
1276 }
1277
1278 pub fn default_local() -> Self {
1280 Self::new(0, 1000)
1281 }
1282
1283 pub fn from_events(
1289 events: Vec<ChainEvent>,
1290 checkpoint_interval: u64,
1291 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
1292 if events.is_empty() {
1293 return Err("from_events: empty event list".into());
1294 }
1295 let chain_id = events[0].chain_id;
1296 let chain = LocalChain::from_events(chain_id, checkpoint_interval, events, Vec::new());
1297 let mgr = Self {
1298 inner: Mutex::new(chain),
1299 signing_key: None,
1300 ml_dsa_key: None,
1301 };
1302 let result = mgr.verify_integrity();
1303 if !result.valid {
1304 return Err(format!(
1305 "from_events integrity check failed: {} errors ({})",
1306 result.errors.len(),
1307 result.errors.first().cloned().unwrap_or_default()
1308 )
1309 .into());
1310 }
1311 Ok(mgr)
1312 }
1313
1314 pub fn with_signing_key(mut self, key: SigningKey) -> Self {
1316 self.signing_key = Some(key);
1317 self
1318 }
1319
1320 pub fn verifying_key(&self) -> Option<VerifyingKey> {
1322 self.signing_key.as_ref().map(|k| k.verifying_key())
1323 }
1324
1325 pub fn set_signing_key(&mut self, key: SigningKey) {
1328 self.signing_key = Some(key);
1329 }
1330
1331 pub fn has_signing_key(&self) -> bool {
1333 self.signing_key.is_some()
1334 }
1335
1336 pub fn signing_key_clone(&self) -> Option<SigningKey> {
1338 self.signing_key.clone()
1339 }
1340
1341 pub fn set_ml_dsa_key(&mut self, key: MlDsa65Key) {
1343 self.ml_dsa_key = Some(key);
1344 }
1345
1346 pub fn has_dual_signing(&self) -> bool {
1348 self.signing_key.is_some() && self.ml_dsa_key.is_some()
1349 }
1350
1351 pub fn load_or_create_key(
1353 path: &Path,
1354 ) -> Result<SigningKey, Box<dyn std::error::Error + Send + Sync>> {
1355 if path.exists() {
1356 #[cfg(unix)]
1358 {
1359 use std::os::unix::fs::PermissionsExt;
1360 let mode = std::fs::metadata(path)?.permissions().mode();
1361 if mode & 0o077 != 0 {
1362 warn!(
1363 path = %path.display(),
1364 mode = format!("{mode:04o}"),
1365 "signing key file has overly permissive permissions, fixing to 0600"
1366 );
1367 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
1368 }
1369 }
1370 let bytes = std::fs::read(path)?;
1371 if bytes.len() != 32 {
1372 return Err(format!("key file is {} bytes, expected 32", bytes.len()).into());
1373 }
1374 let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| "key file not 32 bytes")?;
1375 let key = SigningKey::from_bytes(&key_bytes);
1376 info!(path = %path.display(), "loaded Ed25519 signing key");
1377 Ok(key)
1378 } else {
1379 use rand::rngs::OsRng;
1380 let key = SigningKey::generate(&mut OsRng);
1381 if let Some(parent) = path.parent() {
1382 std::fs::create_dir_all(parent)?;
1383 }
1384 std::fs::write(path, key.to_bytes())?;
1385 #[cfg(unix)]
1388 {
1389 use std::os::unix::fs::PermissionsExt;
1390 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
1391 }
1392 info!(path = %path.display(), "generated new Ed25519 signing key");
1393 Ok(key)
1394 }
1395 }
1396
1397 pub fn append(
1399 &self,
1400 source: &str,
1401 kind: &str,
1402 payload: Option<serde_json::Value>,
1403 ) -> ChainEvent {
1404 let mut chain = self.inner.lock().unwrap();
1405 chain.append(source.into(), kind.into(), payload).clone()
1406 }
1407
1408 pub fn append_idempotent(
1426 &self,
1427 source: &str,
1428 kind: &str,
1429 payload: Option<serde_json::Value>,
1430 idempotency_key: Option<[u8; 32]>,
1431 ) -> ChainEvent {
1432 let mut chain = self.inner.lock().unwrap();
1433
1434 if let Some(ref key) = idempotency_key {
1435 let len = chain.events.len();
1436 let start = len.saturating_sub(IDEMPOTENCY_LOOKBACK);
1437 for existing in chain.events[start..].iter().rev() {
1439 if existing.idempotency_key.as_ref() == Some(key) {
1440 debug!(
1441 sequence = existing.sequence,
1442 kind = existing.kind,
1443 "idempotency_key match -- skipping duplicate append"
1444 );
1445 return existing.clone();
1446 }
1447 }
1448 }
1449
1450 chain
1451 .append_with_key(source.into(), kind.into(), payload, idempotency_key)
1452 .clone()
1453 }
1454
1455 pub fn checkpoint(&self) -> ChainCheckpoint {
1457 let mut chain = self.inner.lock().unwrap();
1458 chain.create_checkpoint()
1459 }
1460
1461 pub fn len(&self) -> usize {
1463 self.inner.lock().unwrap().events.len()
1464 }
1465
1466 pub fn is_empty(&self) -> bool {
1468 self.inner.lock().unwrap().events.is_empty()
1469 }
1470
1471 pub fn sequence(&self) -> u64 {
1473 self.inner.lock().unwrap().sequence
1474 }
1475
1476 pub fn last_hash(&self) -> [u8; 32] {
1478 self.inner.lock().unwrap().last_hash
1479 }
1480
1481 pub fn chain_id(&self) -> u32 {
1483 self.inner.lock().unwrap().chain_id
1484 }
1485
1486 pub fn tail(&self, n: usize) -> Vec<ChainEvent> {
1488 let chain = self.inner.lock().unwrap();
1489 if n == 0 || n >= chain.events.len() {
1490 chain.events.clone()
1491 } else {
1492 chain.events[chain.events.len() - n..].to_vec()
1493 }
1494 }
1495
1496 pub fn set_rule_hash_provider(&self, provider: RuleHashProvider) {
1509 self.inner.lock().unwrap().rule_hash_provider = Some(provider);
1510 }
1511
1512 pub fn set_max_replay_window(&self, events: u64) {
1514 self.inner.lock().unwrap().max_replay_window = events;
1515 }
1516
1517 #[cfg(feature = "native")]
1530 pub fn subscribe(
1531 self: &Arc<Self>,
1532 filter: crate::chain_subscribe::ChainFilter,
1533 ) -> Result<crate::chain_subscribe::ChainSubscription, crate::chain_subscribe::SubscribeError>
1534 {
1535 let chain = self.inner.lock().unwrap();
1536 if let Some(from) = filter.from_seq {
1537 let oldest_allowed = chain.sequence.saturating_sub(chain.max_replay_window);
1538 if from < oldest_allowed {
1539 return Err(crate::chain_subscribe::SubscribeError::ReplayWindowExceeded {
1540 requested: from,
1541 oldest_allowed,
1542 });
1543 }
1544 }
1545 let rx = chain.hub.subscribe();
1546 Ok(crate::chain_subscribe::ChainSubscription::new(
1547 filter,
1548 Arc::downgrade(self),
1549 rx,
1550 chain.sequence,
1551 ))
1552 }
1553
1554 pub(crate) fn replay_page(
1558 &self,
1559 from: u64,
1560 end: u64,
1561 prefix: Option<&str>,
1562 limit: usize,
1563 scan: usize,
1564 ) -> (Vec<ChainEvent>, u64) {
1565 let chain = self.inner.lock().unwrap();
1566 let start = chain.events.partition_point(|e| e.sequence < from);
1567 let mut out = Vec::new();
1568 let mut next = from;
1569 for (scanned, e) in chain.events[start..].iter().enumerate() {
1570 if e.sequence >= end {
1571 return (out, end);
1572 }
1573 if scanned >= scan || out.len() >= limit {
1574 return (out, next);
1575 }
1576 next = e.sequence + 1;
1577 if prefix.is_none_or(|p| e.kind.starts_with(p)) {
1578 out.push(e.clone());
1579 }
1580 }
1581 (out, end)
1582 }
1583
1584 pub fn tail_from(&self, after: u64) -> Vec<ChainEvent> {
1587 let chain = self.inner.lock().unwrap();
1588 chain
1589 .events
1590 .iter()
1591 .filter(|e| e.sequence > after)
1592 .cloned()
1593 .collect()
1594 }
1595
1596 pub fn append_signed(&self, event: ChainEvent) -> Result<ChainEvent, AppendSignedError> {
1616 if is_reserved_source(&event.source) || is_reserved_kind(&event.kind) {
1619 return Err(AppendSignedError::ReservedSource { event_source: event.source.clone() });
1620 }
1621 let mut chain = self.inner.lock().unwrap();
1622
1623 if let Some(existing) = chain.events.iter().find(|e| e.sequence == event.sequence) {
1625 if existing.hash == event.hash {
1626 return Err(AppendSignedError::AlreadyPresent {
1627 sequence: event.sequence,
1628 });
1629 }
1630 return Err(AppendSignedError::Fork {
1631 sequence: event.sequence,
1632 local_hash: existing.hash,
1633 remote_hash: event.hash,
1634 });
1635 }
1636
1637 if event.chain_id != chain.chain_id {
1638 return Err(AppendSignedError::ChainIdMismatch {
1639 local: chain.chain_id,
1640 remote: event.chain_id,
1641 });
1642 }
1643
1644 let expected_seq = chain.sequence;
1645 if event.sequence < expected_seq {
1646 return Err(AppendSignedError::Fork {
1649 sequence: event.sequence,
1650 local_hash: chain
1651 .events
1652 .iter()
1653 .find(|e| e.sequence == event.sequence)
1654 .map(|e| e.hash)
1655 .unwrap_or([0u8; 32]),
1656 remote_hash: event.hash,
1657 });
1658 }
1659 if event.sequence > expected_seq {
1660 return Err(AppendSignedError::Gap {
1661 expected: expected_seq,
1662 got: event.sequence,
1663 });
1664 }
1665
1666 if event.prev_hash != chain.last_hash {
1667 return Err(AppendSignedError::PrevHashMismatch {
1668 expected: chain.last_hash,
1669 got: event.prev_hash,
1670 });
1671 }
1672
1673 let recomputed_payload = compute_payload_hash(&event.payload);
1674 if event.payload_hash != recomputed_payload {
1675 return Err(AppendSignedError::PayloadHashMismatch {
1676 sequence: event.sequence,
1677 });
1678 }
1679
1680 let recomputed = compute_event_hash(
1681 event.sequence,
1682 event.chain_id,
1683 &event.prev_hash,
1684 &event.source,
1685 &event.kind,
1686 &event.timestamp,
1687 &event.payload_hash,
1688 event.rule_hash.as_ref(),
1689 );
1690 if event.hash != recomputed {
1691 return Err(AppendSignedError::HashMismatch {
1692 sequence: event.sequence,
1693 recomputed,
1694 stored: event.hash,
1695 });
1696 }
1697
1698 chain.witness_entries.push(WitnessEntry {
1700 prev_hash: [0u8; 32], action_hash: event.hash,
1702 timestamp_ns: event.timestamp.timestamp_nanos_opt().unwrap_or(0) as u64,
1703 witness_type: WITNESS_PROVENANCE,
1704 });
1705 chain.last_hash = event.hash;
1706 chain.sequence = event.sequence.saturating_add(1);
1707 chain.events_since_checkpoint = chain.events_since_checkpoint.saturating_add(1);
1708 chain.hub.publish(&event);
1709 chain.events.push(event);
1710 if chain.checkpoint_interval > 0
1711 && chain.events_since_checkpoint >= chain.checkpoint_interval
1712 {
1713 let _ = chain.create_checkpoint();
1714 }
1715
1716 Ok(chain.events.last().cloned().expect("just pushed"))
1717 }
1718
1719 pub fn head_sequence(&self) -> u64 {
1722 let chain = self.inner.lock().unwrap();
1723 chain.events.last().map(|e| e.sequence).unwrap_or(0)
1724 }
1725
1726 pub fn head_hash(&self) -> [u8; 32] {
1729 let chain = self.inner.lock().unwrap();
1730 chain.events.last().map(|e| e.hash).unwrap_or([0u8; 32])
1731 }
1732
1733 pub fn checkpoints(&self) -> Vec<ChainCheckpoint> {
1735 self.inner.lock().unwrap().checkpoints.clone()
1736 }
1737
1738 pub fn witness_count(&self) -> usize {
1740 self.inner.lock().unwrap().witness_entries.len()
1741 }
1742
1743 pub fn verify_witness(&self) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
1748 let chain = self.inner.lock().unwrap();
1749 if chain.witness_entries.is_empty() {
1750 return Ok(0);
1751 }
1752 let data = create_witness_chain(&chain.witness_entries);
1753 let verified = verify_witness_chain(&data)
1754 .map_err(|e| format!("witness chain verification failed: {e}"))?;
1755 Ok(verified.len())
1756 }
1757
1758 pub fn generate_attestation(
1765 &self,
1766 vector_count: u64,
1767 epoch: u64,
1768 content_hash: &str,
1769 ) -> Option<CustodyAttestation> {
1770 use ed25519_dalek::Signer;
1771
1772 let signing_key = self.signing_key.as_ref()?;
1773 let device_id: String = signing_key
1774 .verifying_key()
1775 .to_bytes()
1776 .iter()
1777 .map(|b| format!("{b:02x}"))
1778 .collect();
1779
1780 let chain = self.inner.lock().unwrap();
1781 let chain_head: String = chain.last_hash.iter().map(|b| format!("{b:02x}")).collect();
1782 let chain_depth = chain.events.len() as u64;
1783 drop(chain);
1784
1785 let timestamp = std::time::SystemTime::now()
1786 .duration_since(std::time::UNIX_EPOCH)
1787 .unwrap_or_default()
1788 .as_secs();
1789
1790 let canonical = format!(
1792 "custody-attestation:v1\n\
1793 device_id:{device_id}\n\
1794 epoch:{epoch}\n\
1795 chain_head:{chain_head}\n\
1796 chain_depth:{chain_depth}\n\
1797 vector_count:{vector_count}\n\
1798 content_hash:{content_hash}\n\
1799 timestamp:{timestamp}"
1800 );
1801
1802 let signature = signing_key.sign(canonical.as_bytes());
1803
1804 Some(CustodyAttestation {
1805 device_id,
1806 epoch,
1807 chain_head,
1808 chain_depth,
1809 vector_count,
1810 content_hash: content_hash.to_owned(),
1811 timestamp,
1812 signature: signature.to_bytes().to_vec(),
1813 })
1814 }
1815
1816 pub fn verify_integrity(&self) -> ChainVerifyResult {
1823 let chain = self.inner.lock().unwrap();
1824 let mut errors = Vec::new();
1825
1826 for (i, event) in chain.events.iter().enumerate() {
1827 let expected_prev = if i == 0 {
1829 [0u8; 32]
1830 } else {
1831 chain.events[i - 1].hash
1832 };
1833 if event.prev_hash != expected_prev {
1834 errors.push(format!(
1835 "seq {}: prev_hash mismatch (expected {:02x}{:02x}..., got {:02x}{:02x}...)",
1836 event.sequence,
1837 expected_prev[0],
1838 expected_prev[1],
1839 event.prev_hash[0],
1840 event.prev_hash[1],
1841 ));
1842 }
1843
1844 let recomputed_payload = compute_payload_hash(&event.payload);
1846 if event.payload_hash != recomputed_payload {
1847 errors.push(format!(
1848 "seq {}: payload_hash mismatch (recomputed {:02x}{:02x}..., stored {:02x}{:02x}...)",
1849 event.sequence,
1850 recomputed_payload[0], recomputed_payload[1],
1851 event.payload_hash[0], event.payload_hash[1],
1852 ));
1853 }
1854
1855 let recomputed = compute_event_hash(
1857 event.sequence,
1858 event.chain_id,
1859 &event.prev_hash,
1860 &event.source,
1861 &event.kind,
1862 &event.timestamp,
1863 &event.payload_hash,
1864 event.rule_hash.as_ref(),
1865 );
1866 if event.hash != recomputed {
1867 errors.push(format!(
1868 "seq {}: hash mismatch (recomputed {:02x}{:02x}..., stored {:02x}{:02x}...)",
1869 event.sequence, recomputed[0], recomputed[1], event.hash[0], event.hash[1],
1870 ));
1871 }
1872 }
1873
1874 ChainVerifyResult {
1875 valid: errors.is_empty(),
1876 event_count: chain.events.len(),
1877 errors,
1878 signature_verified: None,
1879 }
1880 }
1881
1882 pub fn save_to_file(
1887 &self,
1888 path: &Path,
1889 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1890 let chain = self.inner.lock().map_err(|e| format!("lock: {e}"))?;
1891
1892 if let Some(parent) = path.parent() {
1893 std::fs::create_dir_all(parent)?;
1894 }
1895
1896 let mut output = String::new();
1897 for event in &chain.events {
1898 let line = serde_json::to_string(event)?;
1899 output.push_str(&line);
1900 output.push('\n');
1901 }
1902
1903 std::fs::write(path, output)?;
1904 info!(
1905 path = %path.display(),
1906 events = chain.events.len(),
1907 sequence = chain.sequence,
1908 "chain saved to file"
1909 );
1910 Ok(())
1911 }
1912
1913 pub fn load_from_file(
1918 path: &Path,
1919 checkpoint_interval: u64,
1920 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
1921 let contents = std::fs::read_to_string(path)?;
1922 let mut events = Vec::new();
1923
1924 for line in contents.lines() {
1925 let trimmed = line.trim();
1926 if trimmed.is_empty() {
1927 continue;
1928 }
1929 let event: ChainEvent = serde_json::from_str(trimmed)?;
1930 events.push(event);
1931 }
1932
1933 if events.is_empty() {
1934 return Err("chain file is empty (no events)".into());
1935 }
1936
1937 let chain_id = events[0].chain_id;
1938 let chain = LocalChain::from_events(chain_id, checkpoint_interval, events, Vec::new());
1939
1940 let mgr = Self {
1941 inner: Mutex::new(chain),
1942 signing_key: None,
1943 ml_dsa_key: None,
1944 };
1945
1946 let result = mgr.verify_integrity();
1948 if !result.valid {
1949 warn!(
1950 errors = result.errors.len(),
1951 "loaded chain has integrity errors"
1952 );
1953 return Err(format!(
1954 "chain integrity check failed: {} errors",
1955 result.errors.len()
1956 )
1957 .into());
1958 }
1959
1960 info!(
1961 path = %path.display(),
1962 events = result.event_count,
1963 chain_id,
1964 "chain restored from file"
1965 );
1966 Ok(mgr)
1967 }
1968
1969 pub fn save_to_rvf(&self, path: &Path) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1976 let chain = self.inner.lock().map_err(|e| format!("lock: {e}"))?;
1977
1978 if let Some(parent) = path.parent() {
1979 std::fs::create_dir_all(parent)?;
1980 }
1981
1982 let mut output = Vec::new();
1983
1984 for event in &chain.events {
1985 let exo_header = ExoChainHeader {
1987 magic: EXOCHAIN_MAGIC,
1988 version: 1,
1989 subtype: 0x40, flags: if event.rule_hash.is_some() { EXO_FLAG_HAS_RULE_HASH } else { 0 },
1991 chain_id: event.chain_id,
1992 _reserved: 0,
1993 sequence: event.sequence,
1994 timestamp_secs: event.timestamp.timestamp() as u64,
1995 prev_hash: event.prev_hash,
1996 };
1997
1998 let rvf_payload = RvfChainPayload {
2000 source: event.source.clone(),
2001 kind: event.kind.clone(),
2002 payload: event.payload.clone(),
2003 payload_hash: hex_hash(&event.payload_hash),
2004 hash: hex_hash(&event.hash),
2005 rule_hash: event.rule_hash.as_ref().map(hex_hash),
2006 };
2007
2008 let mut cbor_bytes = Vec::new();
2009 ciborium::into_writer(&rvf_payload, &mut cbor_bytes)
2010 .map_err(|e| format!("cbor encode: {e}"))?;
2011
2012 let segment = write_exochain_event(&exo_header, &cbor_bytes, event.sequence);
2014 output.extend_from_slice(&segment);
2015 }
2016
2017 let checkpoint_header = ExoChainHeader {
2019 magic: EXOCHAIN_MAGIC,
2020 version: 1,
2021 subtype: 0x41, flags: 0,
2023 chain_id: chain.chain_id,
2024 _reserved: 0,
2025 sequence: chain.sequence.saturating_sub(1),
2026 timestamp_secs: Utc::now().timestamp() as u64,
2027 prev_hash: chain.last_hash,
2028 };
2029
2030 let witness_hex = if !chain.witness_entries.is_empty() {
2032 let wc_data = create_witness_chain(&chain.witness_entries);
2033 Some(hex_encode(&wc_data))
2034 } else {
2035 None
2036 };
2037
2038 let cp_payload = serde_json::json!({
2039 "event_count": chain.events.len(),
2040 "last_hash": hex_hash(&chain.last_hash),
2041 "witness_chain": witness_hex,
2042 "witness_entries": chain.witness_entries.len(),
2043 });
2044 let mut cp_cbor = Vec::new();
2045 ciborium::into_writer(&cp_payload, &mut cp_cbor)
2046 .map_err(|e| format!("cbor encode checkpoint: {e}"))?;
2047
2048 let cp_segment = write_exochain_event(
2049 &checkpoint_header,
2050 &cp_cbor,
2051 chain.sequence, );
2053 output.extend_from_slice(&cp_segment);
2054
2055 let signed = if let Some(ref signing_key) = self.signing_key {
2057 let (cp_seg_header, cp_seg_payload) = read_segment(&cp_segment)
2058 .map_err(|e| format!("re-read checkpoint for signing: {e}"))?;
2059 let footer = sign_segment(&cp_seg_header, cp_seg_payload, signing_key);
2060 let footer_bytes = encode_signature_footer(&footer);
2061 output.extend_from_slice(&footer_bytes);
2062
2063 if let Some(ref ml_key) = self.ml_dsa_key {
2065 let ml_footer = sign_segment_ml_dsa(&cp_seg_header, cp_seg_payload, ml_key);
2066 let ml_footer_bytes = encode_signature_footer(&ml_footer);
2067 output.extend_from_slice(&ml_footer_bytes);
2068 }
2069
2070 true
2071 } else {
2072 false
2073 };
2074 let dual_signed = signed && self.ml_dsa_key.is_some();
2075
2076 std::fs::write(path, &output)?;
2077 info!(
2078 path = %path.display(),
2079 events = chain.events.len(),
2080 bytes = output.len(),
2081 signed,
2082 dual_signed,
2083 "chain saved to RVF file"
2084 );
2085 Ok(())
2086 }
2087
2088 pub fn load_from_rvf(
2094 path: &Path,
2095 checkpoint_interval: u64,
2096 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
2097 let data = std::fs::read(path)?;
2098 let mut offset = 0;
2099 let mut events = Vec::new();
2100 let mut witness_entries = Vec::new();
2101
2102 while offset < data.len() {
2103 if data.len() - offset < SEGMENT_HEADER_SIZE {
2105 break;
2106 }
2107
2108 let (seg_header, seg_payload) = match read_segment(&data[offset..]) {
2111 Ok(result) => result,
2112 Err(_) => break,
2113 };
2114
2115 validate_segment(&seg_header, seg_payload)
2117 .map_err(|e| format!("validate segment at offset {offset}: {e}"))?;
2118
2119 let (exo_header, cbor_bytes) = decode_exochain_payload(seg_payload)
2121 .ok_or_else(|| format!("decode exochain payload at offset {offset}"))?;
2122
2123 if exo_header.subtype == 0x40 {
2124 let rvf_payload: RvfChainPayload = ciborium::from_reader(cbor_bytes)
2126 .map_err(|e| format!("cbor decode at offset {offset}: {e}"))?;
2127
2128 let payload_hash = parse_hex_hash(&rvf_payload.payload_hash)?;
2129 let hash = parse_hex_hash(&rvf_payload.hash)?;
2130 let rule_hash = rvf_payload
2131 .rule_hash
2132 .as_deref()
2133 .map(parse_hex_hash)
2134 .transpose()?;
2135
2136 check_exo_flags(exo_header.flags, rule_hash.is_some())
2137 .map_err(|e| format!("segment at offset {offset}: {e}"))?;
2138
2139 let timestamp = DateTime::from_timestamp(exo_header.timestamp_secs as i64, 0)
2140 .ok_or_else(|| {
2141 format!(
2142 "invalid timestamp {} at offset {offset}",
2143 exo_header.timestamp_secs
2144 )
2145 })?;
2146
2147 events.push(ChainEvent {
2148 sequence: exo_header.sequence,
2149 chain_id: exo_header.chain_id,
2150 timestamp,
2151 prev_hash: exo_header.prev_hash,
2152 hash,
2153 payload_hash,
2154 source: rvf_payload.source,
2155 kind: rvf_payload.kind,
2156 payload: rvf_payload.payload,
2157 idempotency_key: None,
2161 rule_hash,
2162 });
2163 } else if exo_header.subtype == 0x41 {
2164 let cp_obj: serde_json::Value =
2166 ciborium::from_reader(cbor_bytes).unwrap_or_default();
2167 if let Some(wc_hex) = cp_obj.get("witness_chain").and_then(|v| v.as_str())
2168 && let Ok(wc_bytes) = hex_decode(wc_hex)
2169 {
2170 match verify_witness_chain(&wc_bytes) {
2171 Ok(entries) => {
2172 witness_entries = entries;
2173 debug!(
2174 count = witness_entries.len(),
2175 "restored witness chain from checkpoint"
2176 );
2177 }
2178 Err(e) => {
2179 warn!("witness chain verification failed on load: {e}");
2180 }
2181 }
2182 }
2183 }
2184 let padded =
2188 calculate_padded_size(SEGMENT_HEADER_SIZE, seg_header.payload_length as usize);
2189 offset += padded;
2190 }
2191
2192 let mut has_signature = false;
2195 let mut has_dual_signature = false;
2196 if offset < data.len()
2197 && let Ok(first_footer) = decode_signature_footer(&data[offset..])
2198 {
2199 has_signature = true;
2200 let first_footer_size = first_footer.footer_length as usize;
2201 let next_offset = offset + first_footer_size;
2202 if next_offset < data.len() && decode_signature_footer(&data[next_offset..]).is_ok() {
2203 has_dual_signature = true;
2204 }
2205 }
2206
2207 if events.is_empty() {
2208 return Err("RVF file contains no chain events".into());
2209 }
2210
2211 let chain_id = events[0].chain_id;
2212 let chain = LocalChain::from_events(chain_id, checkpoint_interval, events, witness_entries);
2213
2214 let mgr = Self {
2215 inner: Mutex::new(chain),
2216 signing_key: None,
2217 ml_dsa_key: None,
2218 };
2219
2220 let result = mgr.verify_integrity();
2222 if !result.valid {
2223 warn!(
2224 errors = result.errors.len(),
2225 "loaded RVF chain has integrity errors"
2226 );
2227 return Err(format!(
2228 "RVF chain integrity check failed: {} errors",
2229 result.errors.len()
2230 )
2231 .into());
2232 }
2233
2234 info!(
2235 path = %path.display(),
2236 events = result.event_count,
2237 chain_id,
2238 has_signature,
2239 has_dual_signature,
2240 "chain restored from RVF file"
2241 );
2242 Ok(mgr)
2243 }
2244
2245 pub fn record_lineage(
2264 &self,
2265 child_id: [u8; 16],
2266 parent_id: [u8; 16],
2267 parent_hash: [u8; 32],
2268 derivation_type: rvf_types::DerivationType,
2269 mutation_count: u32,
2270 description: &str,
2271 ) -> ChainEvent {
2272 let timestamp_ns = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64;
2273
2274 let record = rvf_types::LineageRecord::new(
2275 child_id,
2276 parent_id,
2277 parent_hash,
2278 derivation_type,
2279 mutation_count,
2280 timestamp_ns,
2281 description,
2282 );
2283
2284 let record_bytes = lineage_record_to_bytes(&record);
2286
2287 {
2289 let mut chain = self.inner.lock().unwrap();
2290 let prev_hash = if let Some(last) = chain.witness_entries.last() {
2291 last.action_hash
2292 } else {
2293 [0u8; 32]
2294 };
2295 let witness = lineage_witness_entry(&record, prev_hash);
2296 chain.witness_entries.push(witness);
2297 }
2298
2299 let payload = serde_json::json!({
2300 "child_id": hex_encode(&child_id),
2301 "parent_id": hex_encode(&parent_id),
2302 "parent_hash": hex_hash(&parent_hash),
2303 "derivation_type": derivation_type as u8,
2304 "mutation_count": mutation_count,
2305 "description": description,
2306 "record_hex": hex_encode(&record_bytes),
2307 });
2308
2309 self.append("lineage", "lineage.derivation", Some(payload))
2310 }
2311
2312 pub fn verify_lineage(&self) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
2317 let events = self.tail(0);
2318 let mut identities: Vec<(rvf_types::FileIdentity, [u8; 32])> = Vec::new();
2319
2320 for event in &events {
2321 if event.kind != "lineage.derivation" {
2322 continue;
2323 }
2324 let Some(ref payload) = event.payload else {
2325 continue;
2326 };
2327
2328 let child_id_hex = payload
2329 .get("child_id")
2330 .and_then(|v| v.as_str())
2331 .unwrap_or("");
2332 let parent_id_hex = payload
2333 .get("parent_id")
2334 .and_then(|v| v.as_str())
2335 .unwrap_or("");
2336 let parent_hash_hex = payload
2337 .get("parent_hash")
2338 .and_then(|v| v.as_str())
2339 .unwrap_or("");
2340
2341 let Ok(child_bytes) = hex_decode(child_id_hex) else {
2342 continue;
2343 };
2344 let Ok(parent_bytes) = hex_decode(parent_id_hex) else {
2345 continue;
2346 };
2347 let Ok(parent_hash) = parse_hex_hash(parent_hash_hex) else {
2348 continue;
2349 };
2350
2351 if child_bytes.len() != 16 || parent_bytes.len() != 16 {
2352 continue;
2353 }
2354
2355 let mut child_id = [0u8; 16];
2356 child_id.copy_from_slice(&child_bytes);
2357 let mut parent_id = [0u8; 16];
2358 parent_id.copy_from_slice(&parent_bytes);
2359
2360 let depth = identities
2361 .iter()
2362 .filter(|(fi, _)| fi.file_id == parent_id)
2363 .map(|(fi, _)| fi.lineage_depth + 1)
2364 .next()
2365 .unwrap_or(0);
2366
2367 let fi = rvf_types::FileIdentity {
2368 file_id: child_id,
2369 parent_id,
2370 parent_hash,
2371 lineage_depth: depth,
2372 };
2373
2374 identities.push((fi, event.hash));
2376 }
2377
2378 if identities.is_empty() {
2379 return Ok(0);
2380 }
2381
2382 let count = identities.len();
2388 for (fi, _hash) in &identities {
2389 if fi.is_root() {
2390 continue;
2391 }
2392 let parent_found = identities
2394 .iter()
2395 .any(|(pfi, _)| pfi.file_id == fi.parent_id);
2396 if !parent_found && fi.parent_id != [0u8; 16] {
2397 return Err(format!(
2398 "lineage record for {} references unknown parent {}",
2399 hex_encode(&fi.file_id),
2400 hex_encode(&fi.parent_id),
2401 )
2402 .into());
2403 }
2404 }
2405
2406 Ok(count)
2407 }
2408
2409 pub fn record_witness_bundle(
2421 &self,
2422 bundle_bytes: &[u8],
2423 header: &rvf_types::witness::WitnessHeader,
2424 policy_violations: u32,
2425 rollback_count: u32,
2426 ) -> ChainEvent {
2427 let payload = serde_json::json!({
2428 "task_id": hex_encode(&header.task_id),
2429 "outcome": header.outcome,
2430 "governance_mode": header.governance_mode,
2431 "tool_call_count": header.tool_call_count,
2432 "total_cost_microdollars": header.total_cost_microdollars,
2433 "total_latency_ms": header.total_latency_ms,
2434 "total_tokens": header.total_tokens,
2435 "bundle_size": bundle_bytes.len(),
2436 "policy_violations": policy_violations,
2437 "rollback_count": rollback_count,
2438 "bundle": hex_encode(bundle_bytes),
2439 });
2440 self.append("witness", "witness.bundle", Some(payload))
2441 }
2442
2443 pub fn aggregate_scorecard(&self, n: usize) -> rvf_types::witness::Scorecard {
2448 let events = self.tail(n);
2449 let mut builder = rvf_runtime::ScorecardBuilder::new();
2450
2451 for event in &events {
2452 if event.kind != "witness.bundle" {
2453 continue;
2454 }
2455 let Some(ref payload) = event.payload else {
2456 continue;
2457 };
2458 let Some(hex_str) = payload.get("bundle").and_then(|v| v.as_str()) else {
2459 continue;
2460 };
2461 let Ok(bytes) = hex_decode(hex_str) else {
2462 continue;
2463 };
2464 let Ok(parsed) = rvf_runtime::ParsedWitness::parse(&bytes) else {
2465 continue;
2466 };
2467 let violations = payload
2468 .get("policy_violations")
2469 .and_then(|v| v.as_u64())
2470 .unwrap_or(0) as u32;
2471 let rollbacks = payload
2472 .get("rollback_count")
2473 .and_then(|v| v.as_u64())
2474 .unwrap_or(0) as u32;
2475 builder.add_witness(&parsed, violations, rollbacks);
2476 }
2477
2478 builder.finish()
2479 }
2480
2481 pub fn last_tree_root_hash(&self) -> Option<String> {
2489 let chain = self.inner.lock().unwrap();
2490 for event in chain.events.iter().rev() {
2491 let Some(ref payload) = event.payload else {
2492 continue;
2493 };
2494 if let Some(hash) = payload.get("tree_root_hash").and_then(|v| v.as_str()) {
2496 return Some(hash.to_string());
2497 }
2498 if event.source == "tree"
2500 && matches!(event.kind.as_str(), "tree.checkpoint" | "checkpoint")
2501 && let Some(hash) = payload.get("root_hash").and_then(|v| v.as_str())
2502 {
2503 return Some(hash.to_string());
2504 }
2505 }
2506 None
2507 }
2508
2509 pub fn status(&self) -> ChainStatus {
2511 let chain = self.inner.lock().unwrap();
2512 ChainStatus {
2513 chain_id: chain.chain_id,
2514 sequence: chain.sequence,
2515 last_hash: chain.last_hash,
2516 event_count: chain.events.len(),
2517 checkpoint_count: chain.checkpoints.len(),
2518 events_since_checkpoint: chain.events_since_checkpoint,
2519 }
2520 }
2521
2522 pub fn verify_rvf_signature(
2531 path: &Path,
2532 verifying_key: &VerifyingKey,
2533 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
2534 let data = std::fs::read(path)?;
2535 let mut offset = 0;
2536 let mut last_seg_start = 0;
2537
2538 while offset < data.len() {
2540 if data.len() - offset < SEGMENT_HEADER_SIZE {
2541 break;
2542 }
2543 let (seg_header, _seg_payload) = match read_segment(&data[offset..]) {
2544 Ok(result) => result,
2545 Err(_) => break,
2546 };
2547 last_seg_start = offset;
2548 let padded =
2549 calculate_padded_size(SEGMENT_HEADER_SIZE, seg_header.payload_length as usize);
2550 offset += padded;
2551 }
2552
2553 if offset >= data.len() {
2555 return Err("no signature footer found in RVF file".into());
2556 }
2557 let footer = decode_signature_footer(&data[offset..])
2558 .map_err(|e| format!("decode signature footer: {e}"))?;
2559
2560 let (cp_seg_header, cp_seg_payload) = read_segment(&data[last_seg_start..])
2562 .map_err(|e| format!("re-read checkpoint segment: {e}"))?;
2563
2564 Ok(verify_segment(
2565 &cp_seg_header,
2566 cp_seg_payload,
2567 &footer,
2568 verifying_key,
2569 ))
2570 }
2571
2572 pub fn verify_rvf_dual_signature(
2577 path: &Path,
2578 ed_key: &VerifyingKey,
2579 ml_key: &MlDsa65VerifyKey,
2580 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
2581 let data = std::fs::read(path)?;
2582 let mut offset = 0;
2583 let mut last_seg_start = 0;
2584
2585 while offset < data.len() {
2586 if data.len() - offset < SEGMENT_HEADER_SIZE {
2587 break;
2588 }
2589 let (seg_header, _) = match read_segment(&data[offset..]) {
2590 Ok(result) => result,
2591 Err(_) => break,
2592 };
2593 last_seg_start = offset;
2594 let padded =
2595 calculate_padded_size(SEGMENT_HEADER_SIZE, seg_header.payload_length as usize);
2596 offset += padded;
2597 }
2598
2599 if offset >= data.len() {
2600 return Err("no signature footer found in RVF file".into());
2601 }
2602
2603 let ed_footer = decode_signature_footer(&data[offset..])
2604 .map_err(|e| format!("decode Ed25519 signature footer: {e}"))?;
2605 let ed_footer_size = ed_footer.footer_length as usize;
2606 let ml_offset = offset + ed_footer_size;
2607
2608 if ml_offset >= data.len() {
2609 return Err("no ML-DSA-65 signature footer found (single-signed file)".into());
2610 }
2611 let ml_footer = decode_signature_footer(&data[ml_offset..])
2612 .map_err(|e| format!("decode ML-DSA-65 signature footer: {e}"))?;
2613
2614 let (cp_seg_header, cp_seg_payload) = read_segment(&data[last_seg_start..])
2615 .map_err(|e| format!("re-read checkpoint segment: {e}"))?;
2616
2617 let ed_ok = verify_segment(&cp_seg_header, cp_seg_payload, &ed_footer, ed_key);
2618 let ml_ok = verify_segment_ml_dsa(&cp_seg_header, cp_seg_payload, &ml_footer, ml_key);
2619
2620 Ok(ed_ok && ml_ok)
2621 }
2622
2623 pub fn dual_sign(&self, data: &[u8]) -> Option<DualSignature> {
2631 use ed25519_dalek::Signer;
2632
2633 if signs_project_domain(data) {
2634 tracing::error!("dual_sign refused bytes tagged for a project statement");
2635 return None;
2636 }
2637 let signing_key = self.signing_key.as_ref()?;
2638 let ed_sig = signing_key.sign(data);
2639 let ed_bytes = ed_sig.to_bytes().to_vec();
2640
2641 let ml_sig = self.ml_dsa_key.as_ref().map(|ml_key| {
2642 ml_dsa_sign_raw(ml_key, data)
2644 });
2645
2646 Some(DualSignature {
2647 ed25519: ed_bytes,
2648 ml_dsa65: ml_sig,
2649 })
2650 }
2651
2652 pub fn verify_dual_signature(
2658 data: &[u8],
2659 sig: &DualSignature,
2660 ed25519_pubkey: &VerifyingKey,
2661 ml_dsa_pubkey: Option<&MlDsa65VerifyKey>,
2662 ) -> bool {
2663 use ed25519_dalek::{Signature, Verifier};
2664
2665 if sig.ed25519.len() != 64 {
2667 return false;
2668 }
2669 let ed_sig = Signature::from_bytes(sig.ed25519.as_slice().try_into().unwrap_or(&[0u8; 64]));
2670 if ed25519_pubkey.verify(data, &ed_sig).is_err() {
2671 return false;
2672 }
2673
2674 if let (Some(ml_sig), Some(ml_key)) = (&sig.ml_dsa65, ml_dsa_pubkey)
2676 && !ml_dsa_verify_raw(ml_key, data, ml_sig)
2677 {
2678 return false;
2679 }
2680
2681 true
2682 }
2683}
2684
2685const ML_DSA_RAW_SIG_LEN: usize = 3309;
2692
2693fn ml_dsa_sign_raw(key: &MlDsa65Key, data: &[u8]) -> Vec<u8> {
2695 let vk = key.verifying_key();
2697 let key_bytes = ml_dsa_vk_bytes(&vk);
2698
2699 let mut input = Vec::with_capacity(32 + data.len() + 32);
2700 input.extend_from_slice(&key_bytes);
2701 input.extend_from_slice(data);
2702 input.extend_from_slice(&key_bytes);
2703
2704 let mut sig = Vec::with_capacity(ML_DSA_RAW_SIG_LEN);
2705 let mut block = shake256_256(&input);
2706 while sig.len() < ML_DSA_RAW_SIG_LEN {
2707 sig.extend_from_slice(&block);
2708 let mut next = Vec::with_capacity(64);
2709 next.extend_from_slice(&block);
2710 next.extend_from_slice(&key_bytes);
2711 block = shake256_256(&next);
2712 }
2713 sig.truncate(ML_DSA_RAW_SIG_LEN);
2714 sig
2715}
2716
2717fn ml_dsa_verify_raw(pubkey: &MlDsa65VerifyKey, data: &[u8], sig: &[u8]) -> bool {
2719 let key_bytes = ml_dsa_vk_bytes(pubkey);
2720
2721 let mut input = Vec::with_capacity(32 + data.len() + 32);
2722 input.extend_from_slice(&key_bytes);
2723 input.extend_from_slice(data);
2724 input.extend_from_slice(&key_bytes);
2725
2726 let mut expected = Vec::with_capacity(ML_DSA_RAW_SIG_LEN);
2727 let mut block = shake256_256(&input);
2728 while expected.len() < ML_DSA_RAW_SIG_LEN {
2729 expected.extend_from_slice(&block);
2730 let mut next = Vec::with_capacity(64);
2731 next.extend_from_slice(&block);
2732 next.extend_from_slice(&key_bytes);
2733 block = shake256_256(&next);
2734 }
2735 expected.truncate(ML_DSA_RAW_SIG_LEN);
2736
2737 sig.len() == ML_DSA_RAW_SIG_LEN && sig == expected.as_slice()
2738}
2739
2740fn ml_dsa_vk_bytes(vk: &MlDsa65VerifyKey) -> [u8; 32] {
2747 assert_eq!(
2767 std::mem::size_of::<MlDsa65VerifyKey>(),
2768 32,
2769 "MlDsa65VerifyKey must be exactly 32 bytes"
2770 );
2771 unsafe { std::mem::transmute_copy(vk) }
2774}
2775
2776pub struct DualSigningConfig {
2780 pub ed25519_key: SigningKey,
2782 pub ml_dsa_key: Option<MlDsa65Key>,
2784}
2785
2786#[derive(Debug, Clone, Serialize, Deserialize)]
2788pub struct DualSignature {
2789 pub ed25519: Vec<u8>,
2791 pub ml_dsa65: Option<Vec<u8>>,
2793}
2794
2795#[derive(Debug, Clone, Serialize, Deserialize)]
2797pub struct ChainStatus {
2798 pub chain_id: u32,
2799 pub sequence: u64,
2800 pub last_hash: [u8; 32],
2801 pub event_count: usize,
2802 pub checkpoint_count: usize,
2803 pub events_since_checkpoint: u64,
2804}
2805
2806#[derive(Debug, Clone, Serialize, Deserialize)]
2811pub struct CustodyAttestation {
2812 pub device_id: String,
2814 pub epoch: u64,
2816 pub chain_head: String,
2818 pub chain_depth: u64,
2820 pub vector_count: u64,
2822 pub content_hash: String,
2824 pub timestamp: u64,
2826 pub signature: Vec<u8>,
2828}
2829
2830impl std::fmt::Debug for ChainManager {
2831 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2832 let status = self.status();
2833 f.debug_struct("ChainManager")
2834 .field("chain_id", &status.chain_id)
2835 .field("sequence", &status.sequence)
2836 .field("event_count", &status.event_count)
2837 .finish()
2838 }
2839}
2840
2841#[derive(Debug, Clone, Serialize, Deserialize)]
2847pub struct AnchorReceipt {
2848 pub hash: [u8; 32],
2850 pub tx_id: String,
2852 pub anchored_at: DateTime<Utc>,
2854}
2855
2856pub trait ChainAnchor: Send + Sync {
2868 fn anchor(&self, hash: &[u8; 32]) -> Result<AnchorReceipt, String>;
2870
2871 fn verify(&self, receipt: &AnchorReceipt) -> Result<bool, String>;
2873
2874 fn backend_name(&self) -> &str;
2876}
2877
2878pub struct MockAnchor;
2880
2881impl ChainAnchor for MockAnchor {
2882 fn anchor(&self, hash: &[u8; 32]) -> Result<AnchorReceipt, String> {
2883 Ok(AnchorReceipt {
2884 hash: *hash,
2885 tx_id: format!(
2886 "mock-{}",
2887 hex_hash(hash).chars().take(16).collect::<String>()
2888 ),
2889 anchored_at: Utc::now(),
2890 })
2891 }
2892
2893 fn verify(&self, _receipt: &AnchorReceipt) -> Result<bool, String> {
2894 Ok(true)
2895 }
2896
2897 fn backend_name(&self) -> &str {
2898 "mock"
2899 }
2900}
2901
2902pub trait ChainLoggable {
2911 fn chain_event_source(&self) -> &str;
2913
2914 fn chain_event_kind(&self) -> &str;
2916
2917 fn chain_event_payload(&self) -> serde_json::Value;
2919}
2920
2921impl ChainManager {
2922 pub fn append_loggable(&self, event: &dyn ChainLoggable) -> ChainEvent {
2924 self.append(
2925 event.chain_event_source(),
2926 event.chain_event_kind(),
2927 Some(event.chain_event_payload()),
2928 )
2929 }
2930}
2931
2932pub struct RestartEvent {
2938 pub agent_id: String,
2940 pub old_pid: u64,
2942 pub new_pid: u64,
2944 pub exit_code: i32,
2946 pub strategy: String,
2948 pub backoff_ms: u64,
2950 pub timestamp: DateTime<Utc>,
2952}
2953
2954impl ChainLoggable for RestartEvent {
2955 fn chain_event_source(&self) -> &str {
2956 "supervisor"
2957 }
2958
2959 fn chain_event_kind(&self) -> &str {
2960 "supervisor.restart"
2961 }
2962
2963 fn chain_event_payload(&self) -> serde_json::Value {
2964 serde_json::json!({
2965 "agent_id": self.agent_id,
2966 "old_pid": self.old_pid,
2967 "new_pid": self.new_pid,
2968 "exit_code": self.exit_code,
2969 "strategy": self.strategy,
2970 "backoff_ms": self.backoff_ms,
2971 "timestamp": self.timestamp.to_rfc3339(),
2972 })
2973 }
2974}
2975
2976pub struct GovernanceDecisionEvent {
2980 pub agent_id: String,
2982 pub action: String,
2984 pub decision: String,
2986 pub effect_magnitude: f64,
2988 pub threshold_exceeded: bool,
2990 pub evaluated_rules: Vec<String>,
2992 pub timestamp: DateTime<Utc>,
2994 pub principal: Option<crate::governance::GatePrincipal>,
2998}
2999
3000impl ChainLoggable for GovernanceDecisionEvent {
3001 fn chain_event_source(&self) -> &str {
3002 "governance"
3003 }
3004
3005 fn chain_event_kind(&self) -> &str {
3006 match self.decision.as_str() {
3007 "Permit" => "governance.permit",
3008 "PermitWithWarning" => "governance.warn",
3009 "EscalateToHuman" => "governance.defer",
3010 "Deny" => "governance.deny",
3011 _ => "governance.unknown",
3012 }
3013 }
3014
3015 fn chain_event_payload(&self) -> serde_json::Value {
3016 let mut payload = serde_json::json!({
3017 "agent_id": self.agent_id,
3018 "action": self.action,
3019 "decision": self.decision,
3020 "effect_magnitude": self.effect_magnitude,
3021 "threshold_exceeded": self.threshold_exceeded,
3022 "evaluated_rules": self.evaluated_rules,
3023 "timestamp": self.timestamp.to_rfc3339(),
3024 });
3025 if let Some(p) = &self.principal
3026 && let Some(obj) = payload.as_object_mut()
3027 {
3028 obj.insert(
3029 "principal".into(),
3030 serde_json::to_value(p).unwrap_or(serde_json::Value::Null),
3031 );
3032 }
3033 payload
3034 }
3035}
3036
3037pub struct IpcDeadLetterEvent {
3041 pub message_id: String,
3043 pub from_pid: u64,
3045 pub target: String,
3047 pub payload_type: String,
3049 pub reason: String,
3051 pub timestamp: DateTime<Utc>,
3053}
3054
3055impl ChainLoggable for IpcDeadLetterEvent {
3056 fn chain_event_source(&self) -> &str {
3057 "ipc"
3058 }
3059
3060 fn chain_event_kind(&self) -> &str {
3061 "ipc.dead_letter"
3062 }
3063
3064 fn chain_event_payload(&self) -> serde_json::Value {
3065 serde_json::json!({
3066 "message_id": self.message_id,
3067 "from_pid": self.from_pid,
3068 "target": self.target,
3069 "payload_type": self.payload_type,
3070 "reason": self.reason,
3071 "timestamp": self.timestamp.to_rfc3339(),
3072 })
3073 }
3074}
3075
3076#[cfg(test)]
3077mod tests {
3078 use super::*;
3079
3080 #[test]
3081 fn genesis_event() {
3082 let cm = ChainManager::new(0, 1000);
3083 assert_eq!(cm.len(), 1);
3084 assert_eq!(cm.sequence(), 1); let events = cm.tail(0);
3086 assert_eq!(events[0].kind, "genesis");
3087 assert_eq!(events[0].sequence, 0);
3088 assert_eq!(events[0].prev_hash, [0u8; 32]);
3089 }
3090
3091 #[test]
3092 fn append_links_hashes() {
3093 let cm = ChainManager::new(0, 1000);
3094 let genesis_hash = cm.last_hash();
3095
3096 let e1 = cm.append("test", "event.one", None);
3097 assert_eq!(e1.prev_hash, genesis_hash);
3098 assert_ne!(e1.hash, [0u8; 32]);
3099
3100 let e2 = cm.append(
3101 "test",
3102 "event.two",
3103 Some(serde_json::json!({"key": "value"})),
3104 );
3105 assert_eq!(e2.prev_hash, e1.hash);
3106 }
3107
3108 #[test]
3112 fn append_idempotent_dedups_within_window() {
3113 let cm = ChainManager::new(0, 1000);
3114 let key = [0xABu8; 32];
3115
3116 let starting_len = cm.len();
3117 let first = cm.append_idempotent(
3118 "test",
3119 "idempotent.event",
3120 Some(serde_json::json!({"n": 1})),
3121 Some(key),
3122 );
3123 assert_eq!(cm.len(), starting_len + 1);
3124 assert_eq!(first.idempotency_key, Some(key));
3125
3126 let replay = cm.append_idempotent(
3128 "test",
3129 "idempotent.event",
3130 Some(serde_json::json!({"n": 2})),
3131 Some(key),
3132 );
3133 assert_eq!(cm.len(), starting_len + 1, "replay must not append");
3134 assert_eq!(replay.sequence, first.sequence);
3135 assert_eq!(replay.hash, first.hash);
3136 }
3137
3138 #[test]
3141 fn append_idempotent_distinct_keys_append() {
3142 let cm = ChainManager::new(0, 1000);
3143 let starting_len = cm.len();
3144
3145 let _ = cm.append_idempotent("test", "k.a", None, Some([0x01u8; 32]));
3146 let _ = cm.append_idempotent("test", "k.b", None, Some([0x02u8; 32]));
3147 let _ = cm.append_idempotent("test", "k.none", None, None);
3149 let _ = cm.append_idempotent("test", "k.none", None, None);
3150
3151 assert_eq!(cm.len(), starting_len + 4);
3152 }
3153
3154 #[test]
3155 fn checkpoint() {
3156 let cm = ChainManager::new(0, 1000);
3157 cm.append("test", "event", None);
3158
3159 let cp = cm.checkpoint();
3160 assert_eq!(cp.chain_id, 0);
3161 assert_eq!(cp.sequence, 1);
3162 assert_eq!(cm.checkpoints().len(), 1);
3163 }
3164
3165 #[test]
3166 fn tail_from_zero_returns_all() {
3167 let cm = ChainManager::new(0, 1000);
3168 cm.append("test", "event.one", None);
3169 cm.append("test", "event.two", None);
3170
3171 let all = cm.tail_from(0);
3175 assert_eq!(all.len(), 2);
3177 }
3178
3179 #[test]
3180 fn tail_from_n_returns_after() {
3181 let cm = ChainManager::new(0, 1000);
3182 let e1 = cm.append("test", "event.one", None);
3183 let _e2 = cm.append("test", "event.two", None);
3184
3185 let after = cm.tail_from(e1.sequence);
3186 assert_eq!(after.len(), 1);
3187 assert_eq!(after[0].kind, "event.two");
3188 }
3189
3190 #[test]
3191 fn tail_from_head_returns_empty() {
3192 let cm = ChainManager::new(0, 1000);
3193 cm.append("test", "event.one", None);
3194
3195 let head_seq = cm.head_sequence();
3196 let after = cm.tail_from(head_seq);
3197 assert!(after.is_empty());
3198 }
3199
3200 #[test]
3203 fn append_signed_replays_remote_events() {
3204 let source = ChainManager::new(0, 1000);
3205 source.append("test", "a", Some(serde_json::json!({"n": 1})));
3206 source.append("test", "b", Some(serde_json::json!({"n": 2})));
3207 let remote_events = source.tail_from(0);
3208 assert_eq!(remote_events.len(), 2);
3209
3210 let genesis = source.tail(0)[0].clone();
3211 let sink = ChainManager::from_events(vec![genesis], 1000).unwrap();
3212
3213 for ev in remote_events {
3214 sink.append_signed(ev).unwrap();
3215 }
3216 assert_eq!(sink.head_sequence(), source.head_sequence());
3217 assert_eq!(sink.head_hash(), source.head_hash());
3218 assert_eq!(sink.len(), source.len());
3219 }
3220
3221 #[test]
3222 fn append_signed_idempotent_and_rejects_tamper() {
3223 let source = ChainManager::new(0, 1000);
3224 let e = source.append("test", "x", None);
3225
3226 let genesis = source.tail(0)[0].clone();
3227 let sink = ChainManager::from_events(vec![genesis], 1000).unwrap();
3228 sink.append_signed(e.clone()).unwrap();
3229 let err = sink.append_signed(e.clone()).unwrap_err();
3230 assert!(matches!(err, AppendSignedError::AlreadyPresent { .. }));
3231
3232 let mut tampered = e;
3233 tampered.hash[0] ^= 0xFF;
3234 let err = sink.append_signed(tampered).unwrap_err();
3236 assert!(matches!(err, AppendSignedError::Fork { .. }));
3237 }
3238
3239 #[test]
3240 fn append_signed_gap_when_skip_sequence() {
3241 let source = ChainManager::new(0, 1000);
3242 source.append("test", "a", None);
3243 let e2 = source.append("test", "b", None);
3244
3245 let genesis = source.tail(0)[0].clone();
3246 let sink = ChainManager::from_events(vec![genesis], 1000).unwrap();
3247 let err = sink.append_signed(e2).unwrap_err();
3249 assert!(matches!(err, AppendSignedError::Gap { expected: 1, got: 2 }));
3250 }
3251
3252 #[test]
3253 fn head_sequence_empty_chain() {
3254 let cm = ChainManager::new(0, 1000);
3257 assert_eq!(cm.head_sequence(), 0);
3258 }
3259
3260 #[test]
3261 fn head_sequence_after_appends() {
3262 let cm = ChainManager::new(0, 1000);
3263 cm.append("test", "a", None);
3264 cm.append("test", "b", None);
3265 assert_eq!(cm.head_sequence(), 2);
3267 }
3268
3269 #[test]
3270 fn head_hash_matches_last_event() {
3271 let cm = ChainManager::new(0, 1000);
3272 let e = cm.append("test", "event", None);
3273 assert_eq!(cm.head_hash(), e.hash);
3274 assert_ne!(cm.head_hash(), [0u8; 32]);
3275 }
3276
3277 #[test]
3278 fn auto_checkpoint() {
3279 let cm = ChainManager::new(0, 5); for i in 0..4 {
3282 cm.append("test", &format!("event.{i}"), None);
3283 }
3284 assert_eq!(cm.checkpoints().len(), 1);
3286 }
3287
3288 #[test]
3289 fn status() {
3290 let cm = ChainManager::new(0, 1000);
3291 cm.append("test", "event", None);
3292 let status = cm.status();
3293 assert_eq!(status.chain_id, 0);
3294 assert_eq!(status.sequence, 2);
3295 assert_eq!(status.event_count, 2);
3296 }
3297
3298 #[test]
3299 fn verify_integrity_valid() {
3300 let cm = ChainManager::new(0, 1000);
3301 cm.append("kernel", "boot.init", None);
3302 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
3303 cm.append("kernel", "boot.ready", None);
3304
3305 let result = cm.verify_integrity();
3306 assert!(result.valid);
3307 assert_eq!(result.event_count, 4); assert!(result.errors.is_empty());
3309 }
3310
3311 #[test]
3312 fn save_and_load_roundtrip() {
3313 let cm = ChainManager::new(0, 1000);
3314 cm.append("kernel", "boot.init", None);
3315 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
3316 cm.append("kernel", "boot.ready", None);
3317
3318 let original_seq = cm.sequence();
3319 let original_hash = cm.last_hash();
3320 let original_len = cm.len();
3321
3322 let dir = std::env::temp_dir().join("clawft-chain-test");
3323 let path = dir.join("test-chain.json");
3324 cm.save_to_file(&path).unwrap();
3325
3326 let restored = ChainManager::load_from_file(&path, 1000).unwrap();
3327 assert_eq!(restored.sequence(), original_seq);
3328 assert_eq!(restored.last_hash(), original_hash);
3329 assert_eq!(restored.len(), original_len);
3330 assert_eq!(restored.chain_id(), 0);
3331
3332 let result = restored.verify_integrity();
3334 assert!(result.valid);
3335
3336 let new_event = restored.append("test", "after.restore", None);
3338 assert_eq!(new_event.sequence, original_seq);
3339 assert_eq!(new_event.prev_hash, original_hash);
3340
3341 let _ = std::fs::remove_dir_all(&dir);
3343 }
3344
3345 #[test]
3346 fn load_from_nonexistent_file_fails() {
3347 let result = ChainManager::load_from_file(
3348 &std::path::PathBuf::from("/tmp/nonexistent-chain-file.json"),
3349 1000,
3350 );
3351 assert!(result.is_err());
3352 }
3353
3354 #[test]
3355 fn tail() {
3356 let cm = ChainManager::new(0, 1000);
3357 cm.append("a", "1", None);
3358 cm.append("b", "2", None);
3359 cm.append("c", "3", None);
3360
3361 let last2 = cm.tail(2);
3362 assert_eq!(last2.len(), 2);
3363 assert_eq!(last2[0].kind, "2");
3364 assert_eq!(last2[1].kind, "3");
3365
3366 let all = cm.tail(0);
3367 assert_eq!(all.len(), 4); }
3369
3370 #[test]
3371 fn save_and_load_rvf_roundtrip() {
3372 let cm = ChainManager::new(0, 1000);
3373 cm.append("kernel", "boot.init", None);
3374 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
3375 cm.append("kernel", "boot.ready", None);
3376
3377 let original_seq = cm.sequence();
3378 let original_hash = cm.last_hash();
3379 let original_len = cm.len();
3380
3381 let dir = std::env::temp_dir().join("clawft-chain-rvf-test");
3382 let path = dir.join("test-chain.rvf");
3383 cm.save_to_rvf(&path).unwrap();
3384
3385 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
3386 assert_eq!(restored.sequence(), original_seq);
3387 assert_eq!(restored.last_hash(), original_hash);
3388 assert_eq!(restored.len(), original_len);
3389 assert_eq!(restored.chain_id(), 0);
3390
3391 let result = restored.verify_integrity();
3393 assert!(result.valid, "integrity errors: {:?}", result.errors);
3394 assert_eq!(result.event_count, original_len);
3395
3396 let new_event = restored.append("test", "after.rvf.restore", None);
3398 assert_eq!(new_event.sequence, original_seq);
3399 assert_eq!(new_event.prev_hash, original_hash);
3400
3401 let _ = std::fs::remove_dir_all(&dir);
3403 }
3404
3405 #[test]
3406 fn rvf_validates_on_load() {
3407 let cm = ChainManager::new(0, 1000);
3408 cm.append("kernel", "boot", None);
3409
3410 let dir = std::env::temp_dir().join("clawft-chain-rvf-validate");
3411 let path = dir.join("corrupt.rvf");
3412 cm.save_to_rvf(&path).unwrap();
3413
3414 let mut data = std::fs::read(&path).unwrap();
3416 if data.len() > SEGMENT_HEADER_SIZE + 10 {
3419 data[SEGMENT_HEADER_SIZE + 10] ^= 0xFF;
3420 }
3421 std::fs::write(&path, &data).unwrap();
3422
3423 let result = ChainManager::load_from_rvf(&path, 1000);
3424 assert!(
3425 result.is_err(),
3426 "expected validation error on corrupted RVF"
3427 );
3428
3429 let _ = std::fs::remove_dir_all(&dir);
3431 }
3432
3433 #[test]
3434 fn rvf_migration_from_json() {
3435 let cm = ChainManager::new(0, 1000);
3437 cm.append("kernel", "boot.init", None);
3438 cm.append(
3439 "tree",
3440 "bootstrap",
3441 Some(serde_json::json!({"nodes": 4, "name": "test"})),
3442 );
3443 cm.append("kernel", "boot.ready", None);
3444
3445 let dir = std::env::temp_dir().join("clawft-chain-migrate-test");
3446 let json_path = dir.join("chain.json");
3447 let rvf_path = dir.join("chain.rvf");
3448
3449 cm.save_to_file(&json_path).unwrap();
3451 let from_json = ChainManager::load_from_file(&json_path, 1000).unwrap();
3452
3453 from_json.save_to_rvf(&rvf_path).unwrap();
3455
3456 let from_rvf = ChainManager::load_from_rvf(&rvf_path, 1000).unwrap();
3458
3459 assert_eq!(from_rvf.sequence(), cm.sequence());
3460 assert_eq!(from_rvf.last_hash(), cm.last_hash());
3461 assert_eq!(from_rvf.len(), cm.len());
3462 assert_eq!(from_rvf.chain_id(), cm.chain_id());
3463
3464 let original_events = cm.tail(0);
3466 let rvf_events = from_rvf.tail(0);
3467 assert_eq!(original_events.len(), rvf_events.len());
3468 for (orig, loaded) in original_events.iter().zip(rvf_events.iter()) {
3469 assert_eq!(orig.sequence, loaded.sequence);
3470 assert_eq!(orig.chain_id, loaded.chain_id);
3471 assert_eq!(orig.hash, loaded.hash);
3472 assert_eq!(orig.prev_hash, loaded.prev_hash);
3473 assert_eq!(orig.payload_hash, loaded.payload_hash);
3474 assert_eq!(orig.source, loaded.source);
3475 assert_eq!(orig.kind, loaded.kind);
3476 assert_eq!(orig.payload, loaded.payload);
3477 }
3478
3479 let result = from_rvf.verify_integrity();
3481 assert!(result.valid, "integrity errors: {:?}", result.errors);
3482
3483 let _ = std::fs::remove_dir_all(&dir);
3485 }
3486
3487 #[test]
3488 fn ed25519_signed_rvf_roundtrip() {
3489 use ed25519_dalek::SigningKey;
3490 use rand::rngs::OsRng;
3491
3492 let key = SigningKey::generate(&mut OsRng);
3493
3494 let cm = ChainManager::new(0, 1000).with_signing_key(key.clone());
3495 assert!(cm.has_signing_key());
3496 cm.append("kernel", "boot.init", None);
3497 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 8})));
3498 cm.append("kernel", "boot.ready", None);
3499
3500 let original_seq = cm.sequence();
3501 let original_hash = cm.last_hash();
3502 let original_len = cm.len();
3503
3504 let dir = std::env::temp_dir().join("clawft-chain-signed-test");
3505 let path = dir.join("signed-chain.rvf");
3506 cm.save_to_rvf(&path).unwrap();
3507
3508 let _file_size = std::fs::metadata(&path).unwrap().len();
3510
3511 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
3513 assert_eq!(restored.sequence(), original_seq);
3514 assert_eq!(restored.last_hash(), original_hash);
3515 assert_eq!(restored.len(), original_len);
3516
3517 let result = restored.verify_integrity();
3518 assert!(result.valid, "integrity errors: {:?}", result.errors);
3519
3520 let pubkey = key.verifying_key();
3522 let sig_valid = ChainManager::verify_rvf_signature(&path, &pubkey).unwrap();
3523 assert!(sig_valid, "signature should be valid");
3524
3525 let wrong_key = SigningKey::generate(&mut OsRng);
3527 let wrong_pubkey = wrong_key.verifying_key();
3528 let sig_wrong = ChainManager::verify_rvf_signature(&path, &wrong_pubkey).unwrap();
3529 assert!(!sig_wrong, "signature should fail with wrong key");
3530
3531 let _ = std::fs::remove_dir_all(&dir);
3533 }
3534
3535 #[test]
3536 fn ed25519_tampered_checkpoint_fails_verification() {
3537 use ed25519_dalek::SigningKey;
3538 use rand::rngs::OsRng;
3539
3540 let key = SigningKey::generate(&mut OsRng);
3541 let cm = ChainManager::new(0, 1000).with_signing_key(key.clone());
3542 cm.append("kernel", "boot", None);
3543
3544 let dir = std::env::temp_dir().join("clawft-chain-tampered-sig");
3545 let path = dir.join("tampered.rvf");
3546 cm.save_to_rvf(&path).unwrap();
3547
3548 let mut data = std::fs::read(&path).unwrap();
3549 let footer_size = 72; let mut offset = 0;
3553 let mut last_seg_start = 0;
3554 while offset + SEGMENT_HEADER_SIZE <= data.len() - footer_size {
3555 match read_segment(&data[offset..]) {
3556 Ok((seg_header, _)) => {
3557 last_seg_start = offset;
3558 let padded = calculate_padded_size(
3559 SEGMENT_HEADER_SIZE,
3560 seg_header.payload_length as usize,
3561 );
3562 offset += padded;
3563 }
3564 Err(_) => break,
3565 }
3566 }
3567
3568 data[last_seg_start + SEGMENT_HEADER_SIZE] ^= 0xFF;
3571 std::fs::write(&path, &data).unwrap();
3572
3573 let pubkey = key.verifying_key();
3575 if let Ok(valid) = ChainManager::verify_rvf_signature(&path, &pubkey) {
3577 assert!(!valid, "tampered checkpoint should not verify");
3578 }
3579
3580 let _ = std::fs::remove_dir_all(&dir);
3581 }
3582
3583 #[test]
3584 fn unsigned_rvf_loads_successfully() {
3585 let cm = ChainManager::new(0, 1000); assert!(!cm.has_signing_key());
3588 cm.append("kernel", "boot", None);
3589 cm.append("test", "event", Some(serde_json::json!({"x": 1})));
3590
3591 let dir = std::env::temp_dir().join("clawft-chain-unsigned-test");
3592 let path = dir.join("unsigned.rvf");
3593 cm.save_to_rvf(&path).unwrap();
3594
3595 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
3596 assert_eq!(restored.len(), cm.len());
3597 let result = restored.verify_integrity();
3598 assert!(result.valid);
3599
3600 let key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
3602 let result = ChainManager::verify_rvf_signature(&path, &key.verifying_key());
3603 assert!(result.is_err(), "no signature footer should yield error");
3604
3605 let _ = std::fs::remove_dir_all(&dir);
3606 }
3607
3608 #[test]
3609 fn load_or_create_key_roundtrip() {
3610 let dir = std::env::temp_dir().join("clawft-key-test");
3611 let key_path = dir.join("test-chain.key");
3612 let _ = std::fs::remove_dir_all(&dir);
3613
3614 let key1 = ChainManager::load_or_create_key(&key_path).unwrap();
3616 assert!(key_path.exists());
3617
3618 let key2 = ChainManager::load_or_create_key(&key_path).unwrap();
3620 assert_eq!(key1.to_bytes(), key2.to_bytes());
3621
3622 let _ = std::fs::remove_dir_all(&dir);
3624 }
3625
3626 #[test]
3627 fn witness_chain_created_on_append() {
3628 let cm = ChainManager::new(0, 1000);
3629 assert_eq!(cm.witness_count(), 1); cm.append("kernel", "boot.init", None);
3631 assert_eq!(cm.witness_count(), 2);
3632 cm.append("tree", "bootstrap", Some(serde_json::json!({"n": 8})));
3633 assert_eq!(cm.witness_count(), 3);
3634
3635 let count = cm.verify_witness().unwrap();
3637 assert_eq!(count, 3);
3638 }
3639
3640 #[test]
3641 fn witness_chain_persists_in_rvf() {
3642 let cm = ChainManager::new(0, 1000);
3643 cm.append("kernel", "boot.init", None);
3644 cm.append("tree", "bootstrap", Some(serde_json::json!({"n": 8})));
3645 cm.append("kernel", "boot.ready", None);
3646
3647 let original_witness_count = cm.witness_count();
3648 assert_eq!(original_witness_count, 4); let dir = std::env::temp_dir().join("clawft-chain-witness-test");
3651 let path = dir.join("witness.rvf");
3652 cm.save_to_rvf(&path).unwrap();
3653
3654 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
3656 assert_eq!(restored.witness_count(), original_witness_count);
3657
3658 let count = restored.verify_witness().unwrap();
3660 assert_eq!(count, original_witness_count);
3661
3662 let events = restored.tail(0);
3664 let chain = restored.inner.lock().unwrap();
3665 for (event, witness) in events.iter().zip(chain.witness_entries.iter()) {
3666 assert_eq!(
3667 witness.action_hash, event.hash,
3668 "witness action_hash should match event hash for seq {}",
3669 event.sequence,
3670 );
3671 }
3672
3673 let _ = std::fs::remove_dir_all(&dir);
3675 }
3676
3677 #[test]
3678 fn witness_chain_continues_after_restore() {
3679 let cm = ChainManager::new(0, 1000);
3680 cm.append("kernel", "boot", None);
3681
3682 let dir = std::env::temp_dir().join("clawft-chain-witness-continue");
3683 let path = dir.join("continue.rvf");
3684 cm.save_to_rvf(&path).unwrap();
3685
3686 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
3687 assert_eq!(restored.witness_count(), 2); restored.append("test", "after.restore", None);
3691 assert_eq!(restored.witness_count(), 3);
3692
3693 let count = restored.verify_witness().unwrap();
3695 assert_eq!(count, 3);
3696
3697 let _ = std::fs::remove_dir_all(&dir);
3699 }
3700
3701 #[test]
3702 fn record_witness_bundle_creates_chain_event() {
3703 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
3704 use rvf_types::witness::TaskOutcome;
3705
3706 let cm = ChainManager::new(0, 1000);
3707 let initial_len = cm.len();
3708
3709 let policy = GovernancePolicy::autonomous();
3710 let builder = WitnessBuilder::new([0xAA; 16], policy)
3711 .with_spec(b"fix auth bug")
3712 .with_outcome(TaskOutcome::Solved);
3713 let (bundle, header) = builder.build().unwrap();
3714
3715 let event = cm.record_witness_bundle(&bundle, &header, 0, 0);
3716 assert_eq!(event.source, "witness");
3717 assert_eq!(event.kind, "witness.bundle");
3718 assert_eq!(cm.len(), initial_len + 1);
3719
3720 let payload = event.payload.unwrap();
3722 assert_eq!(payload["outcome"], TaskOutcome::Solved as u8);
3723 assert!(!payload["bundle"].as_str().unwrap().is_empty());
3724 assert_eq!(payload["policy_violations"], 0);
3725 }
3726
3727 #[test]
3728 fn aggregate_scorecard_from_witness_bundles() {
3729 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
3730 use rvf_types::witness::TaskOutcome;
3731
3732 let cm = ChainManager::new(0, 1000);
3733 let policy = GovernancePolicy::autonomous();
3734
3735 let b1 = WitnessBuilder::new([0x01; 16], policy.clone())
3737 .with_spec(b"task 1")
3738 .with_outcome(TaskOutcome::Solved);
3739 let (bytes1, header1) = b1.build().unwrap();
3740 cm.record_witness_bundle(&bytes1, &header1, 0, 0);
3741
3742 let b2 = WitnessBuilder::new([0x02; 16], policy.clone())
3743 .with_spec(b"task 2")
3744 .with_outcome(TaskOutcome::Failed);
3745 let (bytes2, header2) = b2.build().unwrap();
3746 cm.record_witness_bundle(&bytes2, &header2, 1, 0);
3747
3748 let b3 = WitnessBuilder::new([0x03; 16], policy.clone())
3749 .with_spec(b"task 3")
3750 .with_diff(b"diff")
3751 .with_test_log(b"pass")
3752 .with_outcome(TaskOutcome::Solved);
3753 let (bytes3, header3) = b3.build().unwrap();
3754 cm.record_witness_bundle(&bytes3, &header3, 0, 1);
3755
3756 let card = cm.aggregate_scorecard(0);
3757 assert_eq!(card.total_tasks, 3);
3758 assert_eq!(card.solved, 2);
3759 assert_eq!(card.failed, 1);
3760 assert_eq!(card.policy_violations, 1);
3761 assert_eq!(card.rollback_count, 1);
3762 assert!((card.solve_rate - 0.6667).abs() < 0.01);
3763 }
3764
3765 #[test]
3766 fn aggregate_scorecard_empty_when_no_bundles() {
3767 let cm = ChainManager::new(0, 1000);
3768 cm.append("kernel", "boot", None);
3769
3770 let card = cm.aggregate_scorecard(0);
3771 assert_eq!(card.total_tasks, 0);
3772 assert_eq!(card.solve_rate, 0.0);
3773 }
3774
3775 #[test]
3776 fn witness_bundle_with_tool_calls() {
3777 use rvf_runtime::{GovernancePolicy, WitnessBuilder};
3778 use rvf_types::witness::{PolicyCheck, TaskOutcome, ToolCallEntry};
3779
3780 let cm = ChainManager::new(0, 1000);
3781 let policy = GovernancePolicy::autonomous();
3782
3783 let mut builder = WitnessBuilder::new([0x10; 16], policy)
3784 .with_spec(b"add feature")
3785 .with_outcome(TaskOutcome::Solved);
3786
3787 builder.record_tool_call(ToolCallEntry {
3788 action: b"Read".to_vec(),
3789 args_hash: [0x11; 8],
3790 result_hash: [0x22; 8],
3791 latency_ms: 50,
3792 cost_microdollars: 100,
3793 tokens: 500,
3794 policy_check: PolicyCheck::Allowed,
3795 });
3796 builder.record_tool_call(ToolCallEntry {
3797 action: b"Edit".to_vec(),
3798 args_hash: [0x33; 8],
3799 result_hash: [0x44; 8],
3800 latency_ms: 100,
3801 cost_microdollars: 200,
3802 tokens: 1000,
3803 policy_check: PolicyCheck::Allowed,
3804 });
3805
3806 let (bundle, header) = builder.build().unwrap();
3807 assert_eq!(header.tool_call_count, 2);
3808 assert_eq!(header.total_cost_microdollars, 300);
3809
3810 let event = cm.record_witness_bundle(&bundle, &header, 0, 0);
3811 let payload = event.payload.unwrap();
3812 assert_eq!(payload["tool_call_count"], 2);
3813 assert_eq!(payload["total_cost_microdollars"], 300);
3814
3815 let card = cm.aggregate_scorecard(0);
3817 assert_eq!(card.total_tasks, 1);
3818 assert_eq!(card.solved, 1);
3819 }
3820
3821 #[test]
3822 fn record_lineage_creates_chain_event() {
3823 use rvf_types::DerivationType;
3824
3825 let cm = ChainManager::new(0, 1000);
3826 let initial_len = cm.len();
3827
3828 let event = cm.record_lineage(
3829 [0x01; 16], [0x00; 16], [0x00; 32], DerivationType::Clone,
3833 0,
3834 "root agent",
3835 );
3836
3837 assert_eq!(event.source, "lineage");
3838 assert_eq!(event.kind, "lineage.derivation");
3839 assert_eq!(cm.len(), initial_len + 1);
3840
3841 let payload = event.payload.unwrap();
3842 assert_eq!(payload["derivation_type"], DerivationType::Clone as u8);
3843 assert_eq!(payload["description"], "root agent");
3844 assert!(!payload["record_hex"].as_str().unwrap().is_empty());
3845 }
3846
3847 #[test]
3848 fn record_lineage_parent_child() {
3849 use rvf_types::DerivationType;
3850
3851 let cm = ChainManager::new(0, 1000);
3852
3853 let root_event = cm.record_lineage(
3855 [0x01; 16],
3856 [0x00; 16],
3857 [0x00; 32],
3858 DerivationType::Clone,
3859 0,
3860 "root agent",
3861 );
3862
3863 let child_event = cm.record_lineage(
3865 [0x02; 16],
3866 [0x01; 16],
3867 root_event.hash,
3868 DerivationType::Transform,
3869 1,
3870 "spawned worker",
3871 );
3872
3873 let payload = child_event.payload.unwrap();
3874 assert_eq!(payload["derivation_type"], DerivationType::Transform as u8);
3875 assert_eq!(payload["mutation_count"], 1);
3876
3877 let count = cm.verify_lineage().unwrap();
3879 assert_eq!(count, 2);
3880 }
3881
3882 #[test]
3883 fn lineage_adds_witness_entry() {
3884 use rvf_types::DerivationType;
3885
3886 let cm = ChainManager::new(0, 1000);
3887 let initial_witness_count = cm.witness_count();
3888
3889 cm.record_lineage(
3890 [0x01; 16],
3891 [0x00; 16],
3892 [0x00; 32],
3893 DerivationType::Clone,
3894 0,
3895 "agent",
3896 );
3897
3898 assert!(cm.witness_count() > initial_witness_count);
3900 }
3901
3902 #[test]
3903 fn verify_lineage_empty_returns_zero() {
3904 let cm = ChainManager::new(0, 1000);
3905 let count = cm.verify_lineage().unwrap();
3906 assert_eq!(count, 0);
3907 }
3908
3909 #[test]
3910 fn lineage_record_hex_roundtrip() {
3911 use rvf_types::DerivationType;
3912
3913 let cm = ChainManager::new(0, 1000);
3914 let event = cm.record_lineage(
3915 [0xAA; 16],
3916 [0xBB; 16],
3917 [0xCC; 32],
3918 DerivationType::Filter,
3919 42,
3920 "filtered set",
3921 );
3922
3923 let payload = event.payload.unwrap();
3925 let record_hex = payload["record_hex"].as_str().unwrap();
3926 let record_bytes = hex_decode(record_hex).unwrap();
3927 assert_eq!(record_bytes.len(), 128); let record: rvf_types::LineageRecord = weftos_rvf_crypto::lineage_record_from_bytes(
3930 record_bytes.as_slice().try_into().unwrap(),
3931 )
3932 .unwrap();
3933 assert_eq!(record.file_id, [0xAA; 16]);
3934 assert_eq!(record.parent_id, [0xBB; 16]);
3935 assert_eq!(record.parent_hash, [0xCC; 32]);
3936 assert_eq!(record.derivation_type, DerivationType::Filter);
3937 assert_eq!(record.mutation_count, 42);
3938 assert_eq!(record.description_str(), "filtered set");
3939 }
3940
3941 #[test]
3942 fn last_tree_root_hash_from_shutdown() {
3943 let cm = ChainManager::new(0, 1000);
3944 assert!(cm.last_tree_root_hash().is_none());
3946
3947 cm.append(
3949 "kernel",
3950 "shutdown",
3951 Some(serde_json::json!({
3952 "tree_root_hash": "aabb00112233445566778899",
3953 "chain_seq": 5,
3954 })),
3955 );
3956
3957 let hash = cm.last_tree_root_hash().unwrap();
3958 assert_eq!(hash, "aabb00112233445566778899");
3959 }
3960
3961 #[test]
3962 fn last_tree_root_hash_from_tree_checkpoint() {
3963 let cm = ChainManager::new(0, 1000);
3964
3965 cm.append(
3967 "tree",
3968 "tree.checkpoint",
3969 Some(serde_json::json!({
3970 "path": "/tmp/tree.json",
3971 "root_hash": "deadbeef01234567890abcdef0123456",
3972 })),
3973 );
3974
3975 let hash = cm.last_tree_root_hash().unwrap();
3976 assert_eq!(hash, "deadbeef01234567890abcdef0123456");
3977 }
3978
3979 #[test]
3980 fn last_tree_root_hash_prefers_most_recent() {
3981 let cm = ChainManager::new(0, 1000);
3982
3983 cm.append(
3985 "kernel",
3986 "boot.ready",
3987 Some(serde_json::json!({ "tree_root_hash": "old_hash" })),
3988 );
3989
3990 cm.append(
3992 "kernel",
3993 "shutdown",
3994 Some(serde_json::json!({ "tree_root_hash": "new_hash" })),
3995 );
3996
3997 let hash = cm.last_tree_root_hash().unwrap();
3998 assert_eq!(hash, "new_hash");
3999 }
4000
4001 #[test]
4002 fn last_tree_root_hash_ignores_non_tree_root_hash() {
4003 let cm = ChainManager::new(0, 1000);
4004 cm.append(
4006 "kernel",
4007 "boot.init",
4008 Some(serde_json::json!({ "root_hash": "should_not_match" })),
4009 );
4010 assert!(cm.last_tree_root_hash().is_none());
4011 }
4012
4013 #[test]
4016 fn mock_anchor_roundtrip() {
4017 let anchor = MockAnchor;
4018 let hash = [42u8; 32];
4019 let receipt = anchor.anchor(&hash).unwrap();
4020 assert_eq!(receipt.hash, hash);
4021 assert!(receipt.tx_id.starts_with("mock-"));
4022 assert!(anchor.verify(&receipt).unwrap());
4023 }
4024
4025 #[test]
4026 fn anchor_receipt_serde() {
4027 let receipt = AnchorReceipt {
4028 hash: [1u8; 32],
4029 tx_id: "test-tx".into(),
4030 anchored_at: Utc::now(),
4031 };
4032 let json = serde_json::to_string(&receipt).unwrap();
4033 let restored: AnchorReceipt = serde_json::from_str(&json).unwrap();
4034 assert_eq!(restored.hash, receipt.hash);
4035 assert_eq!(restored.tx_id, "test-tx");
4036 }
4037
4038 #[test]
4039 fn ml_dsa_key_set_and_has() {
4040 let mut cm = ChainManager::new(0, 1000);
4041 assert!(!cm.has_dual_signing());
4042
4043 let ed_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
4045 cm.set_signing_key(ed_key);
4046 assert!(cm.has_signing_key());
4047 assert!(!cm.has_dual_signing());
4048
4049 let (ml_key, _) = weftos_rvf_crypto::MlDsa65Key::generate(b"test-seed");
4051 cm.set_ml_dsa_key(ml_key);
4052 assert!(cm.has_dual_signing());
4053 }
4054
4055 #[test]
4056 fn dual_sign_rvf_roundtrip() {
4057 use ed25519_dalek::SigningKey;
4058 use rand::rngs::OsRng;
4059
4060 let ed_key = SigningKey::generate(&mut OsRng);
4061 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(&ed_key.to_bytes());
4062
4063 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4064 cm.set_ml_dsa_key(ml_key);
4065 assert!(cm.has_dual_signing());
4066
4067 cm.append("kernel", "boot.init", None);
4068 cm.append("tree", "bootstrap", Some(serde_json::json!({"nodes": 4})));
4069 cm.append("kernel", "boot.ready", None);
4070
4071 let original_seq = cm.sequence();
4072 let original_hash = cm.last_hash();
4073 let original_len = cm.len();
4074
4075 let dir = std::env::temp_dir().join("clawft-chain-dual-sign-test");
4076 let path = dir.join("dual-signed.rvf");
4077 cm.save_to_rvf(&path).unwrap();
4078
4079 let restored = ChainManager::load_from_rvf(&path, 1000).unwrap();
4081 assert_eq!(restored.sequence(), original_seq);
4082 assert_eq!(restored.last_hash(), original_hash);
4083 assert_eq!(restored.len(), original_len);
4084
4085 let result = restored.verify_integrity();
4086 assert!(result.valid, "integrity errors: {:?}", result.errors);
4087
4088 let ed_pubkey = ed_key.verifying_key();
4090 let ed_valid = ChainManager::verify_rvf_signature(&path, &ed_pubkey).unwrap();
4091 assert!(ed_valid, "Ed25519 signature should be valid");
4092
4093 let dual_valid =
4095 ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &ml_vk).unwrap();
4096 assert!(dual_valid, "dual signature should be valid");
4097
4098 let (_, wrong_ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"wrong-seed");
4100 let dual_wrong =
4101 ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &wrong_ml_vk).unwrap();
4102 assert!(
4103 !dual_wrong,
4104 "dual signature should fail with wrong ML-DSA key"
4105 );
4106
4107 let _ = std::fs::remove_dir_all(&dir);
4108 }
4109
4110 #[test]
4111 fn dual_signed_checkpoint_verifies() {
4112 use ed25519_dalek::SigningKey;
4113 use rand::rngs::OsRng;
4114
4115 let ed_key = SigningKey::generate(&mut OsRng);
4116 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"checkpoint-test");
4117
4118 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4119 cm.set_ml_dsa_key(ml_key);
4120 cm.append("kernel", "boot", None);
4121
4122 let dir = std::env::temp_dir().join("clawft-chain-dual-cp-test");
4123 let path = dir.join("dual-cp.rvf");
4124 cm.save_to_rvf(&path).unwrap();
4125
4126 let data = std::fs::read(&path).unwrap();
4128 let mut offset = 0;
4129 while offset + SEGMENT_HEADER_SIZE <= data.len() {
4130 match read_segment(&data[offset..]) {
4131 Ok((seg_header, _)) => {
4132 let padded = calculate_padded_size(
4133 SEGMENT_HEADER_SIZE,
4134 seg_header.payload_length as usize,
4135 );
4136 offset += padded;
4137 }
4138 Err(_) => break,
4139 }
4140 }
4141 let ed_footer = decode_signature_footer(&data[offset..]).unwrap();
4143 assert_eq!(ed_footer.sig_algo, 0); assert_eq!(ed_footer.sig_length, 64);
4145
4146 let ml_offset = offset + ed_footer.footer_length as usize;
4148 let ml_footer = decode_signature_footer(&data[ml_offset..]).unwrap();
4149 assert_eq!(ml_footer.sig_algo, 1); assert_eq!(ml_footer.sig_length, 3309);
4151
4152 let ed_pubkey = ed_key.verifying_key();
4154 let dual_valid =
4155 ChainManager::verify_rvf_dual_signature(&path, &ed_pubkey, &ml_vk).unwrap();
4156 assert!(dual_valid);
4157
4158 let _ = std::fs::remove_dir_all(&dir);
4159 }
4160
4161 #[test]
4162 fn k6_cryptographic_filesystem_creates_and_retrieves() {
4163 let cm = ChainManager::new(0, 1000);
4168
4169 let entry = cm.append(
4171 "fs",
4172 "file.create",
4173 Some(serde_json::json!({
4174 "path": "/data/config.json",
4175 "content_hash": "abc123def456",
4176 "size": 1024,
4177 })),
4178 );
4179
4180 assert!(
4182 !entry.hash.iter().all(|&b| b == 0),
4183 "entry hash must be non-zero"
4184 );
4185 assert!(
4186 !entry.prev_hash.iter().all(|&b| b == 0),
4187 "prev_hash must link to genesis (non-zero)"
4188 );
4189 assert!(
4190 !entry.payload_hash.iter().all(|&b| b == 0),
4191 "payload_hash must be non-zero when payload present"
4192 );
4193
4194 let events = cm.tail_from(0);
4196 assert!(
4197 !events.is_empty(),
4198 "must retrieve at least the created entry"
4199 );
4200 let found = events.iter().find(|e| e.kind == "file.create");
4201 assert!(found.is_some(), "file.create entry must be retrievable");
4202
4203 let retrieved = found.unwrap();
4204 assert_eq!(retrieved.hash, entry.hash);
4205 let payload = retrieved.payload.as_ref().unwrap();
4206 assert_eq!(payload["path"], "/data/config.json");
4207 assert_eq!(payload["content_hash"], "abc123def456");
4208 assert_eq!(payload["size"], 1024);
4209
4210 let result = cm.verify_integrity();
4212 assert!(result.valid, "chain integrity must hold after fs entry");
4213 }
4214
4215 #[test]
4218 fn dual_signature_ed25519_only() {
4219 use ed25519_dalek::SigningKey;
4220 use rand::rngs::OsRng;
4221
4222 let ed_key = SigningKey::generate(&mut OsRng);
4223 let cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4224 let data = b"cross-node chain event payload";
4227 let sig = cm.dual_sign(data).expect("should produce a signature");
4228
4229 assert_eq!(sig.ed25519.len(), 64, "Ed25519 signature must be 64 bytes");
4230 assert!(
4231 sig.ml_dsa65.is_none(),
4232 "ML-DSA-65 should be absent without key"
4233 );
4234 }
4235
4236 #[test]
4237 fn dual_signature_both_algorithms() {
4238 use ed25519_dalek::SigningKey;
4239 use rand::rngs::OsRng;
4240
4241 let ed_key = SigningKey::generate(&mut OsRng);
4242 let (ml_key, _ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"dual-sig-test");
4243
4244 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4245 cm.set_ml_dsa_key(ml_key);
4246 assert!(cm.has_dual_signing());
4247
4248 let data = b"cross-node chain event with PQ protection";
4249 let sig = cm.dual_sign(data).expect("should produce dual signature");
4250
4251 assert_eq!(sig.ed25519.len(), 64);
4252 let ml = sig.ml_dsa65.as_ref().expect("ML-DSA-65 should be present");
4253 assert_eq!(ml.len(), 3309, "ML-DSA-65 signature must be 3309 bytes");
4254 }
4255
4256 #[test]
4257 fn verify_dual_signature_ed25519_valid() {
4258 use ed25519_dalek::SigningKey;
4259 use rand::rngs::OsRng;
4260
4261 let ed_key = SigningKey::generate(&mut OsRng);
4262 let cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4263
4264 let data = b"verify-ed25519-only";
4265 let sig = cm.dual_sign(data).unwrap();
4266 let ed_pub = ed_key.verifying_key();
4267
4268 assert!(
4269 ChainManager::verify_dual_signature(data, &sig, &ed_pub, None),
4270 "Ed25519-only dual signature should verify"
4271 );
4272 }
4273
4274 #[test]
4275 fn verify_dual_signature_both_valid() {
4276 use ed25519_dalek::SigningKey;
4277 use rand::rngs::OsRng;
4278
4279 let ed_key = SigningKey::generate(&mut OsRng);
4280 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"verify-both");
4281
4282 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4283 cm.set_ml_dsa_key(ml_key);
4284
4285 let data = b"verify-both-algorithms";
4286 let sig = cm.dual_sign(data).unwrap();
4287 let ed_pub = ed_key.verifying_key();
4288
4289 assert!(
4290 ChainManager::verify_dual_signature(data, &sig, &ed_pub, Some(&ml_vk)),
4291 "dual signature with both algorithms should verify"
4292 );
4293 }
4294
4295 #[test]
4296 fn verify_dual_signature_rejects_tampered() {
4297 use ed25519_dalek::SigningKey;
4298 use rand::rngs::OsRng;
4299
4300 let ed_key = SigningKey::generate(&mut OsRng);
4301 let (ml_key, ml_vk) = weftos_rvf_crypto::MlDsa65Key::generate(b"tamper-test");
4302
4303 let mut cm = ChainManager::new(0, 1000).with_signing_key(ed_key.clone());
4304 cm.set_ml_dsa_key(ml_key);
4305
4306 let data = b"original data";
4307 let sig = cm.dual_sign(data).unwrap();
4308 let ed_pub = ed_key.verifying_key();
4309
4310 let tampered = b"tampered data";
4312 assert!(
4313 !ChainManager::verify_dual_signature(tampered, &sig, &ed_pub, Some(&ml_vk)),
4314 "tampered data must fail verification"
4315 );
4316
4317 let mut bad_sig = sig.clone();
4319 if let Some(ref mut ml) = bad_sig.ml_dsa65 {
4320 ml[0] ^= 0xFF;
4321 }
4322 assert!(
4323 !ChainManager::verify_dual_signature(data, &bad_sig, &ed_pub, Some(&ml_vk)),
4324 "tampered ML-DSA-65 signature must fail verification"
4325 );
4326 }
4327
4328 #[test]
4331 fn chain_event_serde_roundtrip() {
4332 let cm = ChainManager::new(0, 1000);
4333 cm.append(
4334 "test",
4335 "agent.spawn",
4336 Some(serde_json::json!({"name": "test-agent"})),
4337 );
4338 let events = cm.tail(1);
4339 let event = &events[0];
4340
4341 let json = serde_json::to_string(event).unwrap();
4342 let restored: ChainEvent = serde_json::from_str(&json).unwrap();
4343 assert_eq!(restored.sequence, event.sequence);
4344 assert_eq!(restored.source, "test");
4345 assert_eq!(restored.kind, "agent.spawn");
4346 assert!(restored.payload.is_some());
4347 }
4348
4349 #[test]
4350 fn chain_event_without_payload_roundtrip() {
4351 let cm = ChainManager::new(0, 1000);
4352 cm.append("kernel", "boot.complete", None);
4353 let events = cm.tail(1);
4354 let event = &events[0];
4355
4356 let json = serde_json::to_string(event).unwrap();
4357 let restored: ChainEvent = serde_json::from_str(&json).unwrap();
4358 assert!(restored.payload.is_none());
4359 assert_eq!(restored.kind, "boot.complete");
4360 }
4361
4362 #[test]
4363 fn chain_checkpoint_serde_roundtrip() {
4364 let cm = ChainManager::new(0, 1000);
4365 cm.append("test", "event.1", None);
4366 cm.append("test", "event.2", None);
4367 let cp = cm.checkpoint();
4368
4369 let json = serde_json::to_string(&cp).unwrap();
4370 let restored: ChainCheckpoint = serde_json::from_str(&json).unwrap();
4371 assert_eq!(restored.chain_id, cp.chain_id);
4372 assert_eq!(restored.sequence, cp.sequence);
4373 assert_eq!(restored.last_hash, cp.last_hash);
4374 }
4375
4376 #[test]
4377 fn chain_verify_result_serde_roundtrip_valid() {
4378 let result = ChainVerifyResult {
4379 valid: true,
4380 event_count: 10,
4381 errors: vec![],
4382 signature_verified: Some(true),
4383 };
4384 let json = serde_json::to_string(&result).unwrap();
4385 let restored: ChainVerifyResult = serde_json::from_str(&json).unwrap();
4386 assert!(restored.valid);
4387 assert_eq!(restored.event_count, 10);
4388 assert!(restored.errors.is_empty());
4389 assert_eq!(restored.signature_verified, Some(true));
4390 }
4391
4392 #[test]
4393 fn chain_verify_result_serde_roundtrip_invalid() {
4394 let result = ChainVerifyResult {
4395 valid: false,
4396 event_count: 5,
4397 errors: vec!["hash mismatch at seq 3".into()],
4398 signature_verified: None,
4399 };
4400 let json = serde_json::to_string(&result).unwrap();
4401 let restored: ChainVerifyResult = serde_json::from_str(&json).unwrap();
4402 assert!(!restored.valid);
4403 assert_eq!(restored.errors.len(), 1);
4404 assert!(restored.signature_verified.is_none());
4405 }
4406
4407 #[test]
4408 fn chain_status_serde_roundtrip() {
4409 let cm = ChainManager::new(42, 1000);
4410 cm.append("test", "event.1", None);
4411 cm.append("test", "event.2", None);
4412 let status = cm.status();
4413
4414 let json = serde_json::to_string(&status).unwrap();
4415 let restored: ChainStatus = serde_json::from_str(&json).unwrap();
4416 assert_eq!(restored.chain_id, 42);
4417 assert_eq!(restored.sequence, status.sequence);
4418 assert_eq!(restored.event_count, status.event_count);
4419 }
4420
4421 #[test]
4422 fn dual_signature_serde_roundtrip() {
4423 let sig = DualSignature {
4424 ed25519: vec![0xCA, 0xFE, 0xBA, 0xBE],
4425 ml_dsa65: Some(vec![0xDE, 0xAD]),
4426 };
4427 let json = serde_json::to_string(&sig).unwrap();
4428 let restored: DualSignature = serde_json::from_str(&json).unwrap();
4429 assert_eq!(restored.ed25519, vec![0xCA, 0xFE, 0xBA, 0xBE]);
4430 assert_eq!(restored.ml_dsa65.unwrap(), vec![0xDE, 0xAD]);
4431 }
4432
4433 #[test]
4434 fn dual_signature_without_ml_dsa_roundtrip() {
4435 let sig = DualSignature {
4436 ed25519: vec![0x01, 0x02],
4437 ml_dsa65: None,
4438 };
4439 let json = serde_json::to_string(&sig).unwrap();
4440 let restored: DualSignature = serde_json::from_str(&json).unwrap();
4441 assert!(restored.ml_dsa65.is_none());
4442 }
4443
4444 #[test]
4445 fn anchor_receipt_serde_roundtrip() {
4446 let receipt = AnchorReceipt {
4447 hash: [0xABu8; 32],
4448 tx_id: "tx-deadbeef".into(),
4449 anchored_at: chrono::Utc::now(),
4450 };
4451 let json = serde_json::to_string(&receipt).unwrap();
4452 let restored: AnchorReceipt = serde_json::from_str(&json).unwrap();
4453 assert_eq!(restored.hash, [0xABu8; 32]);
4454 assert_eq!(restored.tx_id, "tx-deadbeef");
4455 }
4456
4457 #[test]
4458 fn chain_genesis_event_hash_is_nonzero() {
4459 let cm = ChainManager::new(0, 1000);
4460 let events = cm.tail(1);
4461 assert_eq!(events[0].kind, "genesis");
4462 assert_ne!(events[0].hash, [0u8; 32]);
4463 }
4464
4465 #[test]
4466 fn chain_genesis_prev_hash_is_zero() {
4467 let cm = ChainManager::new(0, 1000);
4468 let events = cm.tail(1);
4469 assert_eq!(events[0].prev_hash, [0u8; 32]);
4470 }
4471
4472 #[test]
4475 fn restart_event_loggable() {
4476 let cm = ChainManager::new(0, 100);
4477 let initial = cm.len();
4478
4479 let event = RestartEvent {
4480 agent_id: "agent-coder".into(),
4481 old_pid: 5,
4482 new_pid: 12,
4483 exit_code: 1,
4484 strategy: "OneForOne".into(),
4485 backoff_ms: 200,
4486 timestamp: Utc::now(),
4487 };
4488
4489 assert_eq!(event.chain_event_source(), "supervisor");
4490 assert_eq!(event.chain_event_kind(), "supervisor.restart");
4491
4492 let chain_event = cm.append_loggable(&event);
4493 assert_eq!(cm.len(), initial + 1);
4494 assert_eq!(chain_event.source, "supervisor");
4495 assert_eq!(chain_event.kind, "supervisor.restart");
4496
4497 let payload = chain_event.payload.unwrap();
4498 assert_eq!(payload["agent_id"], "agent-coder");
4499 assert_eq!(payload["old_pid"], 5);
4500 assert_eq!(payload["new_pid"], 12);
4501 assert_eq!(payload["exit_code"], 1);
4502 }
4503
4504 #[test]
4505 fn governance_decision_event_loggable() {
4506 let cm = ChainManager::new(0, 100);
4507
4508 let event = GovernanceDecisionEvent {
4509 agent_id: "agent-1".into(),
4510 action: "tool.exec".into(),
4511 decision: "Deny".into(),
4512 effect_magnitude: 0.85,
4513 threshold_exceeded: true,
4514 evaluated_rules: vec!["security-check".into()],
4515 timestamp: Utc::now(),
4516 principal: None,
4517 };
4518
4519 assert_eq!(event.chain_event_source(), "governance");
4520 assert_eq!(event.chain_event_kind(), "governance.deny");
4521
4522 let chain_event = cm.append_loggable(&event);
4523 assert_eq!(chain_event.kind, "governance.deny");
4524
4525 let payload = chain_event.payload.unwrap();
4526 assert_eq!(payload["agent_id"], "agent-1");
4527 assert_eq!(payload["action"], "tool.exec");
4528 assert!(payload["threshold_exceeded"].as_bool().unwrap());
4529
4530 let make_event = |decision: &str| GovernanceDecisionEvent {
4532 agent_id: "a".into(),
4533 action: "a".into(),
4534 decision: decision.into(),
4535 effect_magnitude: 0.0,
4536 threshold_exceeded: false,
4537 evaluated_rules: vec![],
4538 timestamp: Utc::now(),
4539 principal: None,
4540 };
4541
4542 assert_eq!(make_event("Permit").chain_event_kind(), "governance.permit");
4543 assert_eq!(
4544 make_event("PermitWithWarning").chain_event_kind(),
4545 "governance.warn"
4546 );
4547 assert_eq!(
4548 make_event("EscalateToHuman").chain_event_kind(),
4549 "governance.defer"
4550 );
4551 assert_eq!(make_event("Deny").chain_event_kind(), "governance.deny");
4552 }
4553
4554 #[test]
4555 fn ipc_dead_letter_event_loggable() {
4556 let cm = ChainManager::new(0, 100);
4557
4558 let event = IpcDeadLetterEvent {
4559 message_id: "msg-abc".into(),
4560 from_pid: 3,
4561 target: "Process(99)".into(),
4562 payload_type: "text".into(),
4563 reason: "target_not_found(pid=99)".into(),
4564 timestamp: Utc::now(),
4565 };
4566
4567 assert_eq!(event.chain_event_source(), "ipc");
4568 assert_eq!(event.chain_event_kind(), "ipc.dead_letter");
4569
4570 let chain_event = cm.append_loggable(&event);
4571 assert_eq!(chain_event.source, "ipc");
4572 assert_eq!(chain_event.kind, "ipc.dead_letter");
4573
4574 let payload = chain_event.payload.unwrap();
4575 assert_eq!(payload["message_id"], "msg-abc");
4576 assert_eq!(payload["from_pid"], 3);
4577 assert_eq!(payload["reason"], "target_not_found(pid=99)");
4578 }
4579
4580 #[test]
4581 fn append_loggable_links_hashes() {
4582 let cm = ChainManager::new(0, 100);
4583 let hash_before = cm.last_hash();
4584
4585 let event = RestartEvent {
4586 agent_id: "test".into(),
4587 old_pid: 1,
4588 new_pid: 2,
4589 exit_code: 1,
4590 strategy: "OneForOne".into(),
4591 backoff_ms: 100,
4592 timestamp: Utc::now(),
4593 };
4594
4595 let chain_event = cm.append_loggable(&event);
4596 assert_eq!(chain_event.prev_hash, hash_before);
4597 assert_ne!(chain_event.hash, [0u8; 32]);
4598 }
4599
4600 #[test]
4601 fn generate_attestation_without_key_returns_none() {
4602 let cm = ChainManager::new(0, 1000);
4603 assert!(cm.generate_attestation(10, 5, "abc123").is_none());
4605 }
4606
4607 #[test]
4608 fn generate_attestation_with_key() {
4609 use rand::rngs::OsRng;
4610 let key = SigningKey::generate(&mut OsRng);
4611 let cm = ChainManager::new(0, 1000).with_signing_key(key.clone());
4612
4613 cm.append("test", "event.one", None);
4615 cm.append("test", "event.two", None);
4616
4617 let att = cm.generate_attestation(42, 7, "deadbeef").unwrap();
4618
4619 assert!(!att.device_id.is_empty());
4621 assert_eq!(att.epoch, 7);
4622 assert_eq!(att.vector_count, 42);
4623 assert_eq!(att.content_hash, "deadbeef");
4624 assert_eq!(att.chain_depth, 3); assert!(!att.chain_head.is_empty());
4626 assert_eq!(att.chain_head.len(), 64); assert!(att.timestamp > 0);
4628 assert_eq!(att.signature.len(), 64); use ed25519_dalek::Verifier;
4632 let canonical = format!(
4633 "custody-attestation:v1\n\
4634 device_id:{}\n\
4635 epoch:{}\n\
4636 chain_head:{}\n\
4637 chain_depth:{}\n\
4638 vector_count:{}\n\
4639 content_hash:{}\n\
4640 timestamp:{}",
4641 att.device_id,
4642 att.epoch,
4643 att.chain_head,
4644 att.chain_depth,
4645 att.vector_count,
4646 att.content_hash,
4647 att.timestamp,
4648 );
4649 let sig =
4650 ed25519_dalek::Signature::from_bytes(att.signature.as_slice().try_into().unwrap());
4651 assert!(
4652 key.verifying_key()
4653 .verify(canonical.as_bytes(), &sig)
4654 .is_ok()
4655 );
4656 }
4657
4658 #[test]
4659 fn custody_attestation_serde_roundtrip() {
4660 let att = CustodyAttestation {
4661 device_id: "aabbccdd".into(),
4662 epoch: 5,
4663 chain_head: "ff".repeat(32),
4664 chain_depth: 100,
4665 vector_count: 1000,
4666 content_hash: "00".repeat(32),
4667 timestamp: 1234567890,
4668 signature: vec![0u8; 64],
4669 };
4670 let json = serde_json::to_string(&att).unwrap();
4671 let restored: CustodyAttestation = serde_json::from_str(&json).unwrap();
4672 assert_eq!(restored.device_id, "aabbccdd");
4673 assert_eq!(restored.epoch, 5);
4674 assert_eq!(restored.chain_depth, 100);
4675 assert_eq!(restored.vector_count, 1000);
4676 assert_eq!(restored.signature.len(), 64);
4677 }
4678}