Skip to main content

evorule_reactor/
hash.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! 审计链哈希工具(BLAKE3)
5//!
6//! 这是 evorule 审计链的核心完整性保证机制。
7//!
8//! # 设计
9//! - 使用 `blake3` crate(1.x)计算 256 位哈希
10//! - 序列化采用显式 JSON 格式:确保序列化格式的稳定性,不受 Debug 实现变更影响
11//! - 哈希以十六进制字符串形式返回
12//!
13//! # 哈希链算法
14//! - `prev_hash` 初始为 `"genesis"`
15//! - 对每个 Fact,计算 `content_hash = blake3(fact_to_stable_json(fact))`
16//! - 链哈希:`chain_hash = blake3(prev_hash + content_hash)`
17//! - 更新 `prev_hash = chain_hash`,继续处理下一个 Fact
18//!
19//! # 两套 WAL 合并
20//! 本模块是哈希算法的**单一真相源**(single source of truth)。
21//! - evorule-governance/src/hash.rs re-export 本模块
22//! - evorule-cli/src/hash.rs re-export 本模块
23//! - 通过 `test_cross_validate_with_tier2` 测试保证三方一致
24
25use std::fmt;
26
27use evorule_tcb::JsonValue;
28use tracing::{debug, trace};
29
30use crate::fact::Fact;
31
32/// 哈希计算错误类型
33///
34/// 包含错误消息,便于排查哈希计算过程中的异常。
35#[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
56/// 将 `JsonValue` 转换为 `serde_json::Value`
57///
58/// 内部函数,用于 Fact 序列化前的类型转换。
59/// 保证转换过程确定性,不受 JsonValue 内部实现影响。
60fn 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
77/// 域归因条目 → 稳定 JSON(None 字段省略,键序由 BTreeMap 固定)
78///
79/// 与 `wal.rs` 的同名私有函数保持同构:哈希链序列化与 WAL 持久化
80/// 必须产出一致形状,否则回放重算哈希将失配。
81fn 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/// 将 Fact 序列化为稳定的 JSON 格式
103///
104/// 使用显式的 JSON 序列化而非依赖 Debug trait,确保:
105/// 1. 序列化格式不受 Rust 版本或 Debug 实现变更影响
106/// 2. 字段顺序固定且可预测(serde_json::Map 基于 BTreeMap,字母序)
107/// 3. 跨版本兼容性有保障
108///
109/// # 参数
110/// - `fact`: 要序列化的事实
111///
112/// # 返回值
113/// - `Ok(serde_json::Value)`: 序列化后的 JSON 值
114/// - `Err(HashError)`: 序列化失败
115// 7 种 Fact 变体扁平 match + 嵌套, 拆函数需共享中间变量。详见 GATE_REFERENCE.md §六(豁免索引)
116#[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            // 归因轨迹参与哈希链(审计链防篡改覆盖归因事实)
253            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                    // domain_attr 仅在 Some 时插入键(None 省略):
264                    // 存量无归因事实的哈希不变,增量归因事实纳入哈希链防篡改。
265                    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            // 违规拦截事实参与哈希链(审计链防篡改覆盖拦截记录;回归验证)
288            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/// 计算内容的 BLAKE3 哈希
313///
314/// 将 `JsonValue` 序列化为确定性字符串后计算哈希。
315/// 使用 `tcb_to_serde` 进行显式 JSON 序列化,确保格式稳定性。
316///
317/// # 参数
318/// - `value`: 要计算哈希的 JSON 值
319///
320/// # 返回值
321/// - `Ok(String)`: 64 字符的十六进制哈希字符串
322/// - `Err(HashError)`: 序列化失败
323#[allow(dead_code)] // 供 evorule-governance re-export 使用
324pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
325    #[cfg(kani)]
326    {
327        // Kani 模式:blake3 的位操作循环会导致 CBMC memcmp 状态爆炸
328        // (4357+ 次循环展开未止)。用确定性简化哈希替代,保持幂等性。
329        // 注意:不用 format!(触发 core::unicode::skip_search 状态爆炸)。
330        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
349/// 计算 Fact 的哈希(基于稳定的 JSON 序列化)
350///
351/// 使用显式 JSON 序列化格式而非 Debug trait,确保序列化格式的稳定性。
352///
353/// # 参数
354/// - `fact`: 要计算哈希的事实
355///
356/// # 返回值
357/// - `Ok(String)`: 64 字符的十六进制哈希字符串
358/// - `Err(HashError)`: 哈希计算失败
359///
360/// # 示例
361/// ```
362/// use evorule_reactor::{Fact, FactId, fact_hash};
363/// use evorule_tcb::JsonValue;
364///
365/// let fact = Fact::Command {
366///     id: FactId(1),
367///     instruction: JsonValue::empty_object(),
368/// };
369/// let hash = fact_hash(&fact).unwrap();
370/// assert_eq!(hash.len(), 64);
371/// ```
372pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
373    #[cfg(kani)]
374    {
375        // Kani 模式:blake3 的位操作循环会导致 CBMC memcmp 状态爆炸。
376        // 用确定性简化哈希替代:基于 id 的单字符映射(a-g),
377        // 保持幂等性(相同 id → 相同 hash)和区分性(不同 id → 不同 hash)。
378        // 不用 format!/to_string()/type_name(触发 Unicode 状态爆炸)。
379        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
420/// 计算哈希链的最终链哈希
421///
422/// 这是审计链的核心计算函数。
423///
424/// # 算法
425/// - `prev_hash` 初始为 `"genesis"`
426/// - 对每个 Fact,计算 `content_hash = fact_hash(fact)`
427/// - 链哈希:`chain_hash = blake3(prev_hash + content_hash)`
428/// - 更新 `prev_hash = chain_hash`,继续处理下一个 Fact
429/// - 返回最终的 `prev_hash`(即整条链的链哈希)
430///
431/// # 参数
432/// - `facts`: 事实列表(按因果顺序)
433///
434/// # 返回值
435/// - `Ok(String)`: 最终链哈希(64 字符十六进制)
436/// - `Err(HashError)`: 任一 Fact 哈希计算失败
437///
438/// # 注意
439/// 本函数**只计算**链哈希,**不验证**完整性。
440/// 验证逻辑由调用方比对存储的哈希与重算的哈希。
441/// 空列表返回 `"genesis"`。
442pub 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
501/// 计算单步链哈希:chain_hash = blake3(prev_hash + content_hash)
502///
503/// Kani 模式下用确定性简化版本(字符串拼接)替代 blake3,
504/// 避免 CBMC 对 blake3 位操作循环的 memcmp 状态爆炸。
505/// 保持链步的结合性:`chain_step(chain_step(a, b), c)` 可归纳分解。
506///
507/// # 参数
508/// - `prev_hash`: 前序链哈希(初始为 `"genesis"`)
509/// - `content_hash`: 当前 Fact 的内容哈希
510///
511/// # 返回值
512/// 新的链哈希
513pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
514    #[cfg(kani)]
515    {
516        // Kani 模式:确定性简化,保持链步结构。
517        // 不用 format!(触发 core::unicode::skip_search 状态爆炸),
518        // 用 String::from + push_str(字节级操作,短字符串)。
519        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        // 存量哈希兼容:domain_attr 为 None 时 stable JSON 命中条目与
563        // 旧实现逐键一致(无 domain_attr 键)→ 存量事实哈希不变
564        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        // 归因落账进哈希链(防篡改覆盖归因事实):Some 时键出现、
588        // None 省略字段不出现,且与无归因版本哈希不同
589        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    /// 交叉验证测试:与 evorule-governance/src/hash.rs 的快照比对
831    ///
832    /// 本测试确保 tier1 和 tier2 的哈希算法产生相同的哈希值。
833    /// 如果 tier2 hash.rs 仍维护独立实现,此测试会检测到不一致。
834    #[test]
835    fn test_cross_validate_with_tier2() {
836        // 与 evorule-governance/src/hash.rs 的 test_fact_hash_snapshot 使用相同的 Fact
837        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        // 计算 tier1 的哈希
876        let tier1_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
877
878        // 读取 tier2 的快照文件(如果存在)
879        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}