1use std::fmt;
26
27use evorule_tcb::JsonValue;
28use tracing::{debug, trace};
29
30use crate::fact::Fact;
31
32#[derive(Debug)]
36pub struct HashError {
37 message: String,
38}
39
40impl HashError {
41 fn new(message: impl Into<String>) -> Self {
42 Self {
43 message: message.into(),
44 }
45 }
46}
47
48impl fmt::Display for HashError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "哈希计算错误: {}", self.message)
51 }
52}
53
54impl std::error::Error for HashError {}
55
56fn tcb_to_serde(value: &JsonValue) -> serde_json::Value {
61 match value {
62 JsonValue::Null => serde_json::Value::Null,
63 JsonValue::Bool(b) => serde_json::Value::Bool(*b),
64 JsonValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
65 JsonValue::String(s) => serde_json::Value::String(s.to_string()),
66 JsonValue::Array(arr) => serde_json::Value::Array(arr.iter().map(tcb_to_serde).collect()),
67 JsonValue::Object(obj) => {
68 let mut map = serde_json::Map::new();
69 for (k, v) in obj.iter() {
70 map.insert(k.clone(), tcb_to_serde(v));
71 }
72 serde_json::Value::Object(map)
73 }
74 }
75}
76
77fn trace_attr_to_json(attr: &crate::fact::TraceDomainAttribution) -> serde_json::Value {
82 let mut attr_obj = serde_json::Map::new();
83 attr_obj.insert(
84 "outcome".into(),
85 serde_json::Value::String(attr.outcome.clone()),
86 );
87 if let Some(reason) = &attr.missing_reason {
88 attr_obj.insert(
89 "missing_reason".into(),
90 serde_json::Value::String(reason.clone()),
91 );
92 }
93 if let Some(policy) = &attr.on_missing {
94 attr_obj.insert(
95 "on_missing".into(),
96 serde_json::Value::String(policy.clone()),
97 );
98 }
99 serde_json::Value::Object(attr_obj)
100}
101
102#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
117pub fn fact_to_stable_json(fact: &Fact) -> Result<serde_json::Value, HashError> {
118 let fact_type = fact.type_name();
119 let fact_id = fact.id();
120
121 trace!(
122 事实ID = ?fact_id,
123 事实类型 = %fact_type,
124 "序列化开始"
125 );
126
127 let mut obj = serde_json::Map::new();
128 match fact {
129 Fact::Command { id, instruction } => {
130 trace!(
131 事实ID = ?id,
132 指令大小 = instruction.to_string().len(),
133 "处理命令类型事实"
134 );
135 obj.insert("type".into(), serde_json::Value::String("Command".into()));
136 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
137 obj.insert("instruction".into(), tcb_to_serde(instruction));
138 }
139 Fact::PayloadUpdate { id, path, value } => {
140 trace!(
141 事实ID = ?id,
142 路径 = %path,
143 "处理载荷更新类型事实"
144 );
145 obj.insert(
146 "type".into(),
147 serde_json::Value::String("PayloadUpdate".into()),
148 );
149 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
150 obj.insert("path".into(), serde_json::Value::String(path.clone()));
151 obj.insert("value".into(), tcb_to_serde(value));
152 }
153 Fact::StateTransition {
154 id,
155 cause,
156 new_payload,
157 new_queue,
158 } => {
159 trace!(
160 事实ID = ?id,
161 原因ID = ?cause,
162 队列长度 = new_queue.len(),
163 "处理状态转换类型事实"
164 );
165 obj.insert(
166 "type".into(),
167 serde_json::Value::String("StateTransition".into()),
168 );
169 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
170 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
171 obj.insert("new_payload".into(), tcb_to_serde(new_payload));
172 obj.insert(
173 "new_queue".into(),
174 serde_json::Value::Array(new_queue.iter().map(tcb_to_serde).collect()),
175 );
176 }
177 Fact::IoRequest {
178 id,
179 cause,
180 io_type,
181 params,
182 } => {
183 trace!(
184 事实ID = ?id,
185 原因ID = ?cause,
186 IO类型 = %io_type.as_str(),
187 "处理IO请求类型事实"
188 );
189 obj.insert("type".into(), serde_json::Value::String("IoRequest".into()));
190 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
191 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
192 obj.insert(
193 "io_type".into(),
194 serde_json::Value::String(io_type.as_str().into()),
195 );
196 obj.insert("params".into(), tcb_to_serde(params));
197 }
198 Fact::IoResponse {
199 id,
200 request_id,
201 result,
202 error,
203 } => {
204 trace!(
205 事实ID = ?id,
206 请求ID = ?request_id,
207 是否有错误 = error.is_some(),
208 "处理IO响应类型事实"
209 );
210 obj.insert(
211 "type".into(),
212 serde_json::Value::String("IoResponse".into()),
213 );
214 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
215 obj.insert(
216 "request_id".into(),
217 serde_json::Value::Number(request_id.0.into()),
218 );
219 obj.insert("result".into(), tcb_to_serde(result));
220 obj.insert(
221 "error".into(),
222 error
223 .as_ref()
224 .map(|e| serde_json::Value::String(e.clone()))
225 .unwrap_or(serde_json::Value::Null),
226 );
227 }
228 Fact::Stable { id, version } => {
229 trace!(事实ID = ?id, 版本 = version, "处理稳定状态类型事实");
230 obj.insert("type".into(), serde_json::Value::String("Stable".into()));
231 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
232 obj.insert(
233 "version".into(),
234 serde_json::Value::Number((*version).into()),
235 );
236 }
237 Fact::Error { id, message } => {
238 trace!(
239 事实ID = ?id,
240 消息 = %message,
241 "处理错误类型事实"
242 );
243 obj.insert("type".into(), serde_json::Value::String("Error".into()));
244 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
245 obj.insert("message".into(), serde_json::Value::String(message.clone()));
246 }
247 Fact::TransitionTrace {
248 id,
249 cause,
250 rule_hits,
251 } => {
252 trace!(事实ID = ?id, 原因ID = ?cause, 命中数 = rule_hits.len(), "处理命中归因轨迹事实");
254 obj.insert(
255 "type".into(),
256 serde_json::Value::String("TransitionTrace".into()),
257 );
258 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
259 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
260 let hits: Vec<serde_json::Value> = rule_hits
261 .iter()
262 .map(|h| {
263 let mut hit_obj = serde_json::Map::new();
266 hit_obj.insert("index".into(), serde_json::json!(h.index));
267 hit_obj.insert(
268 "instr_type".into(),
269 serde_json::Value::String(h.instr_type.clone()),
270 );
271 hit_obj.insert("hit".into(), serde_json::Value::Bool(h.hit));
272 if let Some(attr) = &h.domain_attr {
273 hit_obj.insert("domain_attr".into(), trace_attr_to_json(attr));
274 }
275 serde_json::Value::Object(hit_obj)
276 })
277 .collect();
278 obj.insert("rule_hits".into(), serde_json::Value::Array(hits));
279 }
280 Fact::Violation {
281 id,
282 cause,
283 rule_index,
284 reason,
285 instruction,
286 } => {
287 trace!(事实ID = ?id, 原因ID = ?cause, 规则下标 = rule_index, "处理违规拦截事实");
289 obj.insert("type".into(), serde_json::Value::String("Violation".into()));
290 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
291 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
292 obj.insert(
293 "rule_index".into(),
294 serde_json::Value::Number((*rule_index).into()),
295 );
296 obj.insert("reason".into(), serde_json::Value::String(reason.clone()));
297 obj.insert("instruction".into(), tcb_to_serde(instruction));
298 }
299 }
300
301 let value = serde_json::Value::Object(obj);
302
303 trace!(
304 事实ID = ?fact_id,
305 事实类型 = %fact_type,
306 "序列化完成"
307 );
308
309 Ok(value)
310}
311
312#[allow(dead_code)] pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
325 #[cfg(kani)]
326 {
327 Ok(String::from("c"))
331 }
332 #[cfg(not(kani))]
333 {
334 let serde_value = tcb_to_serde(value);
335 let serialized = serde_json::to_string(&serde_value)
336 .map_err(|e| HashError::new(format!("内容哈希序列化失败: {}", e)))?;
337 let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
338
339 trace!(
340 序列化长度 = serialized.len(),
341 哈希值 = %hash,
342 "内容哈希计算完成"
343 );
344
345 Ok(hash)
346 }
347}
348
349pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
373 #[cfg(kani)]
374 {
375 let id = fact.id().0;
380 let h = match id % 7 {
381 0 => "a",
382 1 => "b",
383 2 => "c",
384 3 => "d",
385 4 => "e",
386 5 => "f",
387 _ => "g",
388 };
389 Ok(String::from(h))
390 }
391 #[cfg(not(kani))]
392 {
393 let fact_type = fact.type_name();
394 let fact_id = fact.id();
395
396 debug!(
397 事实ID = ?fact_id,
398 事实类型 = %fact_type,
399 "开始计算事实哈希"
400 );
401
402 let value = fact_to_stable_json(fact)?;
403 let serialized = serde_json::to_string(&value)
404 .map_err(|e| HashError::new(format!("序列化失败: {}", e)))?;
405
406 let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
407
408 debug!(
409 事实ID = ?fact_id,
410 事实类型 = %fact_type,
411 哈希值 = %hash,
412 序列化长度 = serialized.len(),
413 "事实哈希计算完成"
414 );
415
416 Ok(hash)
417 }
418}
419
420pub fn compute_chain_hash(facts: &[Fact]) -> Result<String, HashError> {
443 let fact_count = facts.len();
444
445 debug!(事实数量 = fact_count, "开始计算链哈希");
446
447 if fact_count == 0 {
448 debug!("事实列表为空,返回 genesis 哈希");
449 return Ok(String::from("genesis"));
450 }
451
452 let mut prev_hash = String::from("genesis");
453
454 trace!(
455 初始前序哈希 = %prev_hash,
456 "初始化前序哈希为创世值"
457 );
458
459 for (index, fact) in facts.iter().enumerate() {
460 let fact_type = fact.type_name();
461 let fact_id = fact.id();
462
463 trace!(
464 索引 = index,
465 事实ID = ?fact_id,
466 事实类型 = %fact_type,
467 前序哈希 = %prev_hash,
468 "处理事实"
469 );
470
471 let fh = fact_hash(fact)?;
472
473 trace!(
474 索引 = index,
475 事实ID = ?fact_id,
476 事实哈希 = %fh,
477 "事实哈希计算完成"
478 );
479
480 let current = chain_step(&prev_hash, &fh);
481
482 trace!(
483 索引 = index,
484 事实ID = ?fact_id,
485 当前哈希 = %current,
486 "计算当前哈希(前序哈希+事实哈希)"
487 );
488
489 prev_hash = current;
490 }
491
492 debug!(
493 事实数量 = fact_count,
494 最终哈希 = %prev_hash,
495 "链哈希计算完成"
496 );
497
498 Ok(prev_hash)
499}
500
501pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
514 #[cfg(kani)]
515 {
516 let mut s = String::from(prev_hash);
520 s.push_str(content_hash);
521 s
522 }
523 #[cfg(not(kani))]
524 {
525 let combined = format!("{}{}", prev_hash, content_hash);
526 blake3::hash(combined.as_bytes()).to_hex().to_string()
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
533 use super::*;
534 use crate::fact::{Fact, FactId, IoType, TraceDomainAttribution, TraceHit};
535 use evorule_tcb::JsonValue;
536
537 #[test]
538 fn test_fact_to_stable_json_format() {
539 let command = Fact::Command {
540 id: FactId(1),
541 instruction: JsonValue::object_from_pairs(&[
542 ("type", JsonValue::string("increment")),
543 (
544 "params",
545 JsonValue::object_from_pairs(&[
546 ("attr", JsonValue::string("x")),
547 ("delta", JsonValue::Integer(5)),
548 ]),
549 ),
550 ]),
551 };
552
553 let json_value = fact_to_stable_json(&command).unwrap();
554
555 assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
556 assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
557 assert!(json_value.get("instruction").is_some());
558 }
559
560 #[test]
561 fn test_transition_trace_hash_without_attr_matches_legacy_shape() {
562 let fact = Fact::TransitionTrace {
565 id: FactId(9),
566 cause: FactId(2),
567 rule_hits: vec![TraceHit {
568 index: 0,
569 instr_type: "branch".into(),
570 hit: false,
571 domain_attr: None,
572 }],
573 };
574 let json = fact_to_stable_json(&fact).unwrap();
575 let hits = json.get("rule_hits").unwrap().as_array().unwrap();
576 assert_eq!(hits.len(), 1);
577 let hit = hits.first().unwrap();
578 assert_eq!(
579 hit,
580 &serde_json::json!({"index": 0, "instr_type": "branch", "hit": false}),
581 "无归因条目必须与旧格式逐键一致(None 省略,存量哈希不变)"
582 );
583 }
584
585 #[test]
586 fn test_transition_trace_hash_with_attr_shape_and_distinct() {
587 let fact_with = Fact::TransitionTrace {
590 id: FactId(9),
591 cause: FactId(2),
592 rule_hits: vec![TraceHit {
593 index: 0,
594 instr_type: "branch".into(),
595 hit: false,
596 domain_attr: Some(TraceDomainAttribution {
597 outcome: "missing".into(),
598 missing_reason: Some("path_not_found".into()),
599 on_missing: Some("unsat".into()),
600 }),
601 }],
602 };
603 let fact_without = Fact::TransitionTrace {
604 id: FactId(9),
605 cause: FactId(2),
606 rule_hits: vec![TraceHit {
607 index: 0,
608 instr_type: "branch".into(),
609 hit: false,
610 domain_attr: None,
611 }],
612 };
613 let json = fact_to_stable_json(&fact_with).unwrap();
614 let hit = json
615 .get("rule_hits")
616 .unwrap()
617 .as_array()
618 .unwrap()
619 .first()
620 .unwrap();
621 assert_eq!(
622 hit.get("domain_attr").unwrap(),
623 &serde_json::json!({
624 "outcome": "missing",
625 "missing_reason": "path_not_found",
626 "on_missing": "unsat"
627 }),
628 "归因条目形状(None 字段省略)"
629 );
630 let hash_with = fact_hash(&fact_with).unwrap();
631 let hash_without = fact_hash(&fact_without).unwrap();
632 assert_ne!(
633 hash_with, hash_without,
634 "归因事实必须纳入哈希链(有归因 vs 无归因哈希不同)"
635 );
636 }
637
638 #[test]
639 fn test_fact_hash_all_variants() {
640 let test_facts = vec![
641 Fact::Command {
642 id: FactId(1),
643 instruction: JsonValue::empty_object(),
644 },
645 Fact::PayloadUpdate {
646 id: FactId(2),
647 path: "test.path".into(),
648 value: JsonValue::string("test_value"),
649 },
650 Fact::StateTransition {
651 id: FactId(3),
652 cause: FactId(1),
653 new_payload: JsonValue::empty_object(),
654 new_queue: vec![],
655 },
656 Fact::IoRequest {
657 id: FactId(4),
658 cause: FactId(3),
659 io_type: IoType::http_get(),
660 params: JsonValue::empty_object(),
661 },
662 Fact::IoResponse {
663 id: FactId(5),
664 request_id: FactId(4),
665 result: JsonValue::string("response"),
666 error: None,
667 },
668 Fact::IoResponse {
669 id: FactId(6),
670 request_id: FactId(4),
671 result: JsonValue::Null,
672 error: Some("timeout".to_string()),
673 },
674 Fact::Stable {
675 id: FactId(7),
676 version: 0,
677 },
678 Fact::Error {
679 id: FactId(8),
680 message: "test error".into(),
681 },
682 ];
683
684 for fact in test_facts {
685 let hash = fact_hash(&fact).unwrap();
686 assert_eq!(hash.len(), 64);
687
688 let hash2 = fact_hash(&fact).unwrap();
689 assert_eq!(
690 hash,
691 hash2,
692 "fact_hash should be deterministic for {}",
693 fact.type_name()
694 );
695 }
696 }
697
698 #[test]
699 fn test_fact_hash_identity() {
700 let fact1 = Fact::Command {
701 id: FactId(1),
702 instruction: JsonValue::string("same"),
703 };
704 let fact2 = Fact::Command {
705 id: FactId(1),
706 instruction: JsonValue::string("same"),
707 };
708
709 assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
710 }
711
712 #[test]
713 fn test_fact_hash_different_ids() {
714 let fact1 = Fact::Command {
715 id: FactId(1),
716 instruction: JsonValue::string("same"),
717 };
718 let fact2 = Fact::Command {
719 id: FactId(2),
720 instruction: JsonValue::string("same"),
721 };
722
723 assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
724 }
725
726 #[test]
727 fn test_fact_hash_snapshot() {
728 let test_facts = [
729 Fact::Command {
730 id: FactId(1),
731 instruction: JsonValue::empty_object(),
732 },
733 Fact::PayloadUpdate {
734 id: FactId(2),
735 path: "test.path".into(),
736 value: JsonValue::string("test_value"),
737 },
738 Fact::StateTransition {
739 id: FactId(3),
740 cause: FactId(1),
741 new_payload: JsonValue::empty_object(),
742 new_queue: vec![],
743 },
744 Fact::IoRequest {
745 id: FactId(4),
746 cause: FactId(3),
747 io_type: IoType::http_get(),
748 params: JsonValue::empty_object(),
749 },
750 Fact::IoResponse {
751 id: FactId(5),
752 request_id: FactId(4),
753 result: JsonValue::string("response"),
754 error: None,
755 },
756 Fact::Stable {
757 id: FactId(6),
758 version: 0,
759 },
760 Fact::Error {
761 id: FactId(7),
762 message: "test error".into(),
763 },
764 ];
765
766 let current_hashes: Vec<String> =
767 test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
768 let snapshot_file = env!("CARGO_MANIFEST_DIR").to_string() + "/hash_snapshot.txt";
769
770 if std::path::Path::new(&snapshot_file).exists() {
771 let snapshot = std::fs::read_to_string(&snapshot_file).unwrap();
772 let expected_hashes: Vec<String> = snapshot.lines().map(|s| s.to_string()).collect();
773 assert_eq!(
774 current_hashes, expected_hashes,
775 "Hash snapshot mismatch! If this is an expected change (e.g., Fact struct modification), delete {} to regenerate.",
776 snapshot_file
777 );
778 } else {
779 let snapshot_content = current_hashes.join("\n");
780 std::fs::write(&snapshot_file, snapshot_content).unwrap();
781 println!("Created hash snapshot: {}", snapshot_file);
782 }
783 }
784
785 #[test]
786 fn test_compute_chain_hash_empty() {
787 let facts: Vec<Fact> = vec![];
788 let chain_hash = compute_chain_hash(&facts).unwrap();
789 assert_eq!(chain_hash, "genesis");
790 }
791
792 #[test]
793 fn test_compute_chain_hash_deterministic() {
794 let facts = vec![
795 Fact::Command {
796 id: FactId(1),
797 instruction: JsonValue::empty_object(),
798 },
799 Fact::StateTransition {
800 id: FactId(2),
801 cause: FactId(1),
802 new_payload: JsonValue::empty_object(),
803 new_queue: vec![],
804 },
805 ];
806
807 let result1 = compute_chain_hash(&facts).unwrap();
808 let result2 = compute_chain_hash(&facts).unwrap();
809 assert_eq!(result1, result2);
810 }
811
812 #[test]
813 fn test_compute_chain_hash_order_sensitive() {
814 let fact1 = Fact::Command {
815 id: FactId(1),
816 instruction: JsonValue::empty_object(),
817 };
818 let fact2 = Fact::StateTransition {
819 id: FactId(2),
820 cause: FactId(1),
821 new_payload: JsonValue::empty_object(),
822 new_queue: vec![],
823 };
824
825 let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
826 let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
827 assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
828 }
829
830 #[test]
835 fn test_cross_validate_with_tier2() {
836 let test_facts = [
838 Fact::Command {
839 id: FactId(1),
840 instruction: JsonValue::empty_object(),
841 },
842 Fact::PayloadUpdate {
843 id: FactId(2),
844 path: "test.path".into(),
845 value: JsonValue::string("test_value"),
846 },
847 Fact::StateTransition {
848 id: FactId(3),
849 cause: FactId(1),
850 new_payload: JsonValue::empty_object(),
851 new_queue: vec![],
852 },
853 Fact::IoRequest {
854 id: FactId(4),
855 cause: FactId(3),
856 io_type: IoType::http_get(),
857 params: JsonValue::empty_object(),
858 },
859 Fact::IoResponse {
860 id: FactId(5),
861 request_id: FactId(4),
862 result: JsonValue::string("response"),
863 error: None,
864 },
865 Fact::Stable {
866 id: FactId(6),
867 version: 0,
868 },
869 Fact::Error {
870 id: FactId(7),
871 message: "test error".into(),
872 },
873 ];
874
875 let tier1_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
877
878 let tier2_snapshot_path = "../../evorule-governance/src/hash_snapshot.txt".to_string();
880 if std::path::Path::new(&tier2_snapshot_path).exists() {
881 let tier2_snapshot = std::fs::read_to_string(&tier2_snapshot_path).unwrap();
882 let tier2_hashes: Vec<String> = tier2_snapshot.lines().map(|s| s.to_string()).collect();
883
884 assert_eq!(
885 tier1_hashes, tier2_hashes,
886 "tier1 和 tier2 的 fact_hash 不一致!\
887 如果这是预期变更(如 Fact 结构修改),\
888 请删除 evorule-governance/src/hash_snapshot.txt 重新生成。"
889 );
890 }
891 }
892}