1use core::fmt;
29
30use evorule_tcb::JsonValue;
31
32use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35
36use crate::fact::Fact;
37
38#[derive(Debug)]
42pub struct HashError {
43 message: String,
44}
45
46impl HashError {
47 fn new(message: impl Into<String>) -> Self {
48 Self {
49 message: message.into(),
50 }
51 }
52}
53
54impl fmt::Display for HashError {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "哈希计算错误: {}", self.message)
57 }
58}
59
60impl core::error::Error for HashError {}
61
62fn tcb_to_serde(value: &JsonValue) -> serde_json::Value {
67 match value {
68 JsonValue::Null => serde_json::Value::Null,
69 JsonValue::Bool(b) => serde_json::Value::Bool(*b),
70 JsonValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
71 JsonValue::U64(v) => serde_json::Value::Number(serde_json::Number::from(*v)),
74 JsonValue::String(s) => serde_json::Value::String(s.to_string()),
75 JsonValue::Array(arr) => serde_json::Value::Array(arr.iter().map(tcb_to_serde).collect()),
76 JsonValue::Object(obj) => {
77 let mut map = serde_json::Map::new();
78 for (k, v) in obj.iter() {
79 map.insert(k.clone(), tcb_to_serde(v));
80 }
81 serde_json::Value::Object(map)
82 }
83 }
84}
85
86fn trace_attr_to_json(attr: &crate::fact::TraceDomainAttribution) -> serde_json::Value {
91 let mut attr_obj = serde_json::Map::new();
92 attr_obj.insert(
93 "outcome".into(),
94 serde_json::Value::String(attr.outcome.clone()),
95 );
96 if let Some(reason) = &attr.missing_reason {
97 attr_obj.insert(
98 "missing_reason".into(),
99 serde_json::Value::String(reason.clone()),
100 );
101 }
102 if let Some(policy) = &attr.on_missing {
103 attr_obj.insert(
104 "on_missing".into(),
105 serde_json::Value::String(policy.clone()),
106 );
107 }
108 serde_json::Value::Object(attr_obj)
109}
110
111#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
126pub fn fact_to_stable_json(fact: &Fact) -> Result<serde_json::Value, HashError> {
127 let mut obj = serde_json::Map::new();
128 match fact {
129 Fact::Command { id, instruction } => {
130 obj.insert("type".into(), serde_json::Value::String("Command".into()));
131 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
132 obj.insert("instruction".into(), tcb_to_serde(instruction));
133 }
134 Fact::PayloadUpdate { id, path, value } => {
135 obj.insert(
136 "type".into(),
137 serde_json::Value::String("PayloadUpdate".into()),
138 );
139 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
140 obj.insert("path".into(), serde_json::Value::String(path.clone()));
141 obj.insert("value".into(), tcb_to_serde(value));
142 }
143 Fact::StateTransition {
144 id,
145 cause,
146 new_payload,
147 new_queue,
148 } => {
149 obj.insert(
150 "type".into(),
151 serde_json::Value::String("StateTransition".into()),
152 );
153 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
154 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
155 obj.insert("new_payload".into(), tcb_to_serde(new_payload));
156 obj.insert(
157 "new_queue".into(),
158 serde_json::Value::Array(new_queue.iter().map(tcb_to_serde).collect()),
159 );
160 }
161 Fact::IoRequest {
162 id,
163 cause,
164 io_type,
165 params,
166 } => {
167 obj.insert("type".into(), serde_json::Value::String("IoRequest".into()));
168 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
169 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
170 obj.insert(
171 "io_type".into(),
172 serde_json::Value::String(io_type.as_str().into()),
173 );
174 obj.insert("params".into(), tcb_to_serde(params));
175 }
176 Fact::IoResponse {
177 id,
178 request_id,
179 result,
180 error,
181 } => {
182 obj.insert(
183 "type".into(),
184 serde_json::Value::String("IoResponse".into()),
185 );
186 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
187 obj.insert(
188 "request_id".into(),
189 serde_json::Value::Number(request_id.0.into()),
190 );
191 obj.insert("result".into(), tcb_to_serde(result));
192 obj.insert(
193 "error".into(),
194 error
195 .as_ref()
196 .map(|e| serde_json::Value::String(e.clone()))
197 .unwrap_or(serde_json::Value::Null),
198 );
199 }
200 Fact::Stable { id, version } => {
201 obj.insert("type".into(), serde_json::Value::String("Stable".into()));
202 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
203 obj.insert(
204 "version".into(),
205 serde_json::Value::Number((*version).into()),
206 );
207 }
208 Fact::Error { id, message } => {
209 obj.insert("type".into(), serde_json::Value::String("Error".into()));
210 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
211 obj.insert("message".into(), serde_json::Value::String(message.clone()));
212 }
213 Fact::TransitionTrace {
214 id,
215 cause,
216 rule_hits,
217 } => {
218 obj.insert(
220 "type".into(),
221 serde_json::Value::String("TransitionTrace".into()),
222 );
223 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
224 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
225 let hits: Vec<serde_json::Value> = rule_hits
226 .iter()
227 .map(|h| {
228 let mut hit_obj = serde_json::Map::new();
231 hit_obj.insert("index".into(), serde_json::json!(h.index));
232 hit_obj.insert(
233 "instr_type".into(),
234 serde_json::Value::String(h.instr_type.clone()),
235 );
236 hit_obj.insert("hit".into(), serde_json::Value::Bool(h.hit));
237 if let Some(attr) = &h.domain_attr {
238 hit_obj.insert("domain_attr".into(), trace_attr_to_json(attr));
239 }
240 serde_json::Value::Object(hit_obj)
241 })
242 .collect();
243 obj.insert("rule_hits".into(), serde_json::Value::Array(hits));
244 }
245 Fact::Violation {
246 id,
247 cause,
248 rule_index,
249 reason,
250 instruction,
251 } => {
252 obj.insert("type".into(), serde_json::Value::String("Violation".into()));
254 obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
255 obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
256 obj.insert(
257 "rule_index".into(),
258 serde_json::Value::Number((*rule_index).into()),
259 );
260 obj.insert("reason".into(), serde_json::Value::String(reason.clone()));
261 obj.insert("instruction".into(), tcb_to_serde(instruction));
262 }
263 }
264
265 Ok(serde_json::Value::Object(obj))
266}
267
268#[allow(dead_code)] pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
281 #[cfg(kani)]
282 {
283 Ok(String::from("c"))
287 }
288 #[cfg(not(kani))]
289 {
290 let serde_value = tcb_to_serde(value);
291 let serialized = serde_json::to_string(&serde_value)
292 .map_err(|e| HashError::new(alloc::format!("内容哈希序列化失败: {}", e)))?;
293 let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
294
295 Ok(hash)
296 }
297}
298
299pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
324 #[cfg(kani)]
325 {
326 let id = fact.id().0;
331 let h = match id % 7 {
332 0 => "a",
333 1 => "b",
334 2 => "c",
335 3 => "d",
336 4 => "e",
337 5 => "f",
338 _ => "g",
339 };
340 Ok(String::from(h))
341 }
342 #[cfg(not(kani))]
343 {
344 let value = fact_to_stable_json(fact)?;
345 let serialized = serde_json::to_string(&value)
346 .map_err(|e| HashError::new(alloc::format!("序列化失败: {}", e)))?;
347
348 let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
349
350 Ok(hash)
351 }
352}
353
354pub fn compute_chain_hash(facts: &[Fact]) -> Result<String, HashError> {
377 if facts.is_empty() {
378 return Ok(String::from("genesis"));
379 }
380
381 let mut prev_hash = String::from("genesis");
382
383 for fact in facts {
384 let fh = fact_hash(fact)?;
385 prev_hash = chain_step(&prev_hash, &fh);
386 }
387
388 Ok(prev_hash)
389}
390
391pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
404 #[cfg(kani)]
405 {
406 let mut s = String::from(prev_hash);
410 s.push_str(content_hash);
411 s
412 }
413 #[cfg(not(kani))]
414 {
415 let combined = alloc::format!("{}{}", prev_hash, content_hash);
416 blake3::hash(combined.as_bytes()).to_hex().to_string()
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 #![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
423 use super::*;
424 use crate::fact::{Fact, FactId, IoType, TraceDomainAttribution, TraceHit};
425 use evorule_tcb::JsonValue;
426 use std::{println, vec};
428
429 #[test]
430 fn test_fact_to_stable_json_format() {
431 let command = Fact::Command {
432 id: FactId(1),
433 instruction: JsonValue::object_from_pairs(&[
434 ("type", JsonValue::string("increment")),
435 (
436 "params",
437 JsonValue::object_from_pairs(&[
438 ("attr", JsonValue::string("x")),
439 ("delta", JsonValue::Integer(5)),
440 ]),
441 ),
442 ]),
443 };
444
445 let json_value = fact_to_stable_json(&command).unwrap();
446
447 assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
448 assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
449 assert!(json_value.get("instruction").is_some());
450 }
451
452 #[test]
455 fn test_tcb_to_serde_u64_lossless() {
456 let sentinel = u64::MAX - 1;
457 let v = JsonValue::u64(sentinel);
458 let serde_v = tcb_to_serde(&v);
459 assert_eq!(serde_v.as_u64(), Some(sentinel));
460 let obj = JsonValue::object_from_pairs(&[("version", JsonValue::u64(sentinel))]);
462 let serde_obj = tcb_to_serde(&obj);
463 assert_eq!(
464 serde_obj.get("version").and_then(|x| x.as_u64()),
465 Some(sentinel)
466 );
467 }
468
469 #[test]
470 fn test_transition_trace_hash_without_attr_matches_legacy_shape() {
471 let fact = Fact::TransitionTrace {
474 id: FactId(9),
475 cause: FactId(2),
476 rule_hits: vec![TraceHit {
477 index: 0,
478 instr_type: "branch".into(),
479 hit: false,
480 domain_attr: None,
481 }],
482 };
483 let json = fact_to_stable_json(&fact).unwrap();
484 let hits = json.get("rule_hits").unwrap().as_array().unwrap();
485 assert_eq!(hits.len(), 1);
486 let hit = hits.first().unwrap();
487 assert_eq!(
488 hit,
489 &serde_json::json!({"index": 0, "instr_type": "branch", "hit": false}),
490 "无归因条目必须与旧格式逐键一致(None 省略,存量哈希不变)"
491 );
492 }
493
494 #[test]
495 fn test_transition_trace_hash_with_attr_shape_and_distinct() {
496 let fact_with = Fact::TransitionTrace {
499 id: FactId(9),
500 cause: FactId(2),
501 rule_hits: vec![TraceHit {
502 index: 0,
503 instr_type: "branch".into(),
504 hit: false,
505 domain_attr: Some(TraceDomainAttribution {
506 outcome: "missing".into(),
507 missing_reason: Some("path_not_found".into()),
508 on_missing: Some("unsat".into()),
509 }),
510 }],
511 };
512 let fact_without = Fact::TransitionTrace {
513 id: FactId(9),
514 cause: FactId(2),
515 rule_hits: vec![TraceHit {
516 index: 0,
517 instr_type: "branch".into(),
518 hit: false,
519 domain_attr: None,
520 }],
521 };
522 let json = fact_to_stable_json(&fact_with).unwrap();
523 let hit = json
524 .get("rule_hits")
525 .unwrap()
526 .as_array()
527 .unwrap()
528 .first()
529 .unwrap();
530 assert_eq!(
531 hit.get("domain_attr").unwrap(),
532 &serde_json::json!({
533 "outcome": "missing",
534 "missing_reason": "path_not_found",
535 "on_missing": "unsat"
536 }),
537 "归因条目形状(None 字段省略)"
538 );
539 let hash_with = fact_hash(&fact_with).unwrap();
540 let hash_without = fact_hash(&fact_without).unwrap();
541 assert_ne!(
542 hash_with, hash_without,
543 "归因事实必须纳入哈希链(有归因 vs 无归因哈希不同)"
544 );
545 }
546
547 #[test]
548 fn test_fact_hash_all_variants() {
549 let test_facts = vec![
550 Fact::Command {
551 id: FactId(1),
552 instruction: JsonValue::empty_object(),
553 },
554 Fact::PayloadUpdate {
555 id: FactId(2),
556 path: "test.path".into(),
557 value: JsonValue::string("test_value"),
558 },
559 Fact::StateTransition {
560 id: FactId(3),
561 cause: FactId(1),
562 new_payload: JsonValue::empty_object(),
563 new_queue: vec![],
564 },
565 Fact::IoRequest {
566 id: FactId(4),
567 cause: FactId(3),
568 io_type: IoType::http_get(),
569 params: JsonValue::empty_object(),
570 },
571 Fact::IoResponse {
572 id: FactId(5),
573 request_id: FactId(4),
574 result: JsonValue::string("response"),
575 error: None,
576 },
577 Fact::IoResponse {
578 id: FactId(6),
579 request_id: FactId(4),
580 result: JsonValue::Null,
581 error: Some("timeout".to_string()),
582 },
583 Fact::Stable {
584 id: FactId(7),
585 version: 0,
586 },
587 Fact::Error {
588 id: FactId(8),
589 message: "test error".into(),
590 },
591 ];
592
593 for fact in test_facts {
594 let hash = fact_hash(&fact).unwrap();
595 assert_eq!(hash.len(), 64);
596
597 let hash2 = fact_hash(&fact).unwrap();
598 assert_eq!(
599 hash,
600 hash2,
601 "fact_hash should be deterministic for {}",
602 fact.type_name()
603 );
604 }
605 }
606
607 #[test]
608 fn test_fact_hash_identity() {
609 let fact1 = Fact::Command {
610 id: FactId(1),
611 instruction: JsonValue::string("same"),
612 };
613 let fact2 = Fact::Command {
614 id: FactId(1),
615 instruction: JsonValue::string("same"),
616 };
617
618 assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
619 }
620
621 #[test]
622 fn test_fact_hash_different_ids() {
623 let fact1 = Fact::Command {
624 id: FactId(1),
625 instruction: JsonValue::string("same"),
626 };
627 let fact2 = Fact::Command {
628 id: FactId(2),
629 instruction: JsonValue::string("same"),
630 };
631
632 assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
633 }
634
635 #[test]
636 fn test_fact_hash_snapshot() {
637 let test_facts = [
638 Fact::Command {
639 id: FactId(1),
640 instruction: JsonValue::empty_object(),
641 },
642 Fact::PayloadUpdate {
643 id: FactId(2),
644 path: "test.path".into(),
645 value: JsonValue::string("test_value"),
646 },
647 Fact::StateTransition {
648 id: FactId(3),
649 cause: FactId(1),
650 new_payload: JsonValue::empty_object(),
651 new_queue: vec![],
652 },
653 Fact::IoRequest {
654 id: FactId(4),
655 cause: FactId(3),
656 io_type: IoType::http_get(),
657 params: JsonValue::empty_object(),
658 },
659 Fact::IoResponse {
660 id: FactId(5),
661 request_id: FactId(4),
662 result: JsonValue::string("response"),
663 error: None,
664 },
665 Fact::Stable {
666 id: FactId(6),
667 version: 0,
668 },
669 Fact::Error {
670 id: FactId(7),
671 message: "test error".into(),
672 },
673 ];
674
675 let current_hashes: Vec<String> =
676 test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
677 let snapshot_file =
678 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("hash_snapshot.txt");
679
680 if snapshot_file.exists() {
681 let snapshot = std::fs::read_to_string(&snapshot_file).unwrap();
682 let expected_hashes: Vec<String> = snapshot.lines().map(|s| s.to_string()).collect();
683 assert_eq!(
684 current_hashes, expected_hashes,
685 "Hash snapshot mismatch! If this is an expected change (e.g., Fact struct modification), delete {} to regenerate.",
686 snapshot_file.display()
687 );
688 } else {
689 let snapshot_content = current_hashes.join("\n");
690 std::fs::write(&snapshot_file, snapshot_content).unwrap();
691 println!("Created hash snapshot: {}", snapshot_file.display());
692 }
693 }
694
695 #[test]
696 fn test_compute_chain_hash_empty() {
697 let facts: Vec<Fact> = vec![];
698 let chain_hash = compute_chain_hash(&facts).unwrap();
699 assert_eq!(chain_hash, "genesis");
700 }
701
702 #[test]
703 fn test_compute_chain_hash_deterministic() {
704 let facts = vec![
705 Fact::Command {
706 id: FactId(1),
707 instruction: JsonValue::empty_object(),
708 },
709 Fact::StateTransition {
710 id: FactId(2),
711 cause: FactId(1),
712 new_payload: JsonValue::empty_object(),
713 new_queue: vec![],
714 },
715 ];
716
717 let result1 = compute_chain_hash(&facts).unwrap();
718 let result2 = compute_chain_hash(&facts).unwrap();
719 assert_eq!(result1, result2);
720 }
721
722 #[test]
723 fn test_compute_chain_hash_order_sensitive() {
724 let fact1 = Fact::Command {
725 id: FactId(1),
726 instruction: JsonValue::empty_object(),
727 };
728 let fact2 = Fact::StateTransition {
729 id: FactId(2),
730 cause: FactId(1),
731 new_payload: JsonValue::empty_object(),
732 new_queue: vec![],
733 };
734
735 let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
736 let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
737 assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
738 }
739
740 #[test]
745 fn test_cross_validate_with_tier2() {
746 let test_facts = [
747 Fact::Command {
748 id: FactId(1),
749 instruction: JsonValue::empty_object(),
750 },
751 Fact::PayloadUpdate {
752 id: FactId(2),
753 path: "test.path".into(),
754 value: JsonValue::string("test_value"),
755 },
756 Fact::StateTransition {
757 id: FactId(3),
758 cause: FactId(1),
759 new_payload: JsonValue::empty_object(),
760 new_queue: vec![],
761 },
762 Fact::IoRequest {
763 id: FactId(4),
764 cause: FactId(3),
765 io_type: IoType::http_get(),
766 params: JsonValue::empty_object(),
767 },
768 Fact::IoResponse {
769 id: FactId(5),
770 request_id: FactId(4),
771 result: JsonValue::string("response"),
772 error: None,
773 },
774 Fact::Stable {
775 id: FactId(6),
776 version: 0,
777 },
778 Fact::Error {
779 id: FactId(7),
780 message: "test error".into(),
781 },
782 ];
783
784 let ssot_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
786
787 let tier2_snapshot_path = "../../evorule-governance/src/hash_snapshot.txt".to_string();
789 if std::path::Path::new(&tier2_snapshot_path).exists() {
790 let tier2_snapshot = std::fs::read_to_string(&tier2_snapshot_path).unwrap();
791 let tier2_hashes: Vec<String> = tier2_snapshot.lines().map(|s| s.to_string()).collect();
792
793 assert_eq!(
794 ssot_hashes, tier2_hashes,
795 "SSOT 与 tier2 的 fact_hash 不一致!\
796 如果这是预期变更(如 Fact 结构修改),\
797 请删除 evorule-governance/src/hash_snapshot.txt 重新生成。"
798 );
799 }
800 }
801}