Skip to main content

evorule_cli/
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)—— re-export tier1 单一真相源
5//!
6//! # 两套 WAL 合并
7//! 自 0.2.0 起,本模块已重构为 `evorule_reactor::hash` 的**重导出层**。
8//! 哈希算法的单一真相源在 `evorule_reactor::hash`,本模块仅 re-export。
9//!
10//! ## 重导出的符号
11//! - [`HashError`]:哈希计算错误类型
12//! - [`fact_to_stable_json`]:Fact → 稳定 JSON 序列化
13//! - [`fact_hash`]:Fact 的 BLAKE3 哈希
14//! - [`compute_chain_hash`]:哈希链最终链哈希计算
15//! - [`content_hash`]:JsonValue 的 BLAKE3 哈希
16//!
17//! ## 已移除的符号
18//! - `tcb_to_serde`:内部函数,已统一到 tier1
19//! - `log_hash_error`:依赖 `Backtrace` 的日志函数,已移除
20//!
21//! ## 向后兼容
22//! [`verify_hash_chain`] 作为 deprecated 包装器保留,内部调用 [`compute_chain_hash`]
23//! 并返回 `true`,保持原有 API 语义(自洽重算,不报错)。
24//!
25//! # 与 tier1/tier2 的关系
26//! - 算法源:`evorule_reactor::hash`(单一真相源)
27//! - evorule-governance 也 re-export 同一源
28//! - 三方(tier1/tier2/CLI)哈希值字节级一致,无需交叉验证快照
29
30/// 重导出 tier1 的哈希算法(单一真相源)
31///
32/// 所有哈希计算函数均在 `evorule_reactor::hash` 中实现,
33/// 本模块通过 re-export 对外暴露相同的 API。
34#[allow(unused_imports)]
35pub use evorule_reactor::{
36    compute_chain_hash, content_hash, fact_hash, fact_to_stable_json, HashError,
37};
38
39/// 验证哈希链完整性(deprecated,保留向后兼容)
40///
41/// # ⚠️ 已废弃
42/// 本函数始终返回 `true`,仅自洽地重算链式哈希,**不验证**已存储的哈希。
43/// 真正的完整性验证应使用 [`compute_chain_hash`] 比对存储的链哈希,
44/// 或在 `verify_chain` 命令中读取带哈希字段的 WAL 并逐一校验。
45///
46/// # 算法
47/// - `prev_hash` 初始为 `"genesis"`
48/// - 对每个 Fact,计算 `blake3(prev_hash + fact_hash)` 作为当前哈希
49/// - 更新 `prev_hash` 为当前哈希,继续处理下一个 Fact
50///
51/// # 返回值
52/// 始终返回 `true`:本函数仅以 Fact 列表为输入,按上述算法自洽地重算链式哈希。
53/// 空列表视为完整(`true`)。
54///
55/// # 迁移指南
56/// ```ignore
57/// // 旧代码(不验证):
58/// let ok = verify_hash_chain(&facts);
59///
60/// // 新代码(真正验证):
61/// let computed = compute_chain_hash(&facts)?;
62/// if computed != stored_last_hash {
63///     return Err("审计链断裂");
64/// }
65/// ```
66#[deprecated(
67    since = "0.2.0",
68    note = "verify_hash_chain 始终返回 true,不验证存储的哈希。请使用 compute_chain_hash 比对存储的链哈希"
69)]
70#[allow(deprecated, dead_code)]
71pub fn verify_hash_chain(facts: &[evorule_reactor::Fact]) -> bool {
72    // 调用 compute_chain_hash 重算链哈希(忽略结果,仅验证算法可执行)
73    let _ = compute_chain_hash(facts);
74    true
75}
76
77#[cfg(test)]
78mod tests {
79    #![allow(deprecated, clippy::unwrap_used, clippy::panic, clippy::expect_used)]
80    use super::*;
81    use evorule_reactor::{Fact, FactId, IoType};
82    use evorule_tcb::JsonValue;
83
84    #[test]
85    fn test_fact_to_stable_json_format() {
86        let command = Fact::Command {
87            id: FactId(1),
88            instruction: JsonValue::object_from_pairs(&[
89                ("type", JsonValue::string("increment")),
90                (
91                    "params",
92                    JsonValue::object_from_pairs(&[
93                        ("attr", JsonValue::string("x")),
94                        ("delta", JsonValue::Integer(5)),
95                    ]),
96                ),
97            ]),
98        };
99
100        let json_value = fact_to_stable_json(&command).unwrap();
101
102        assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
103        assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
104        assert!(json_value.get("instruction").is_some());
105    }
106
107    #[test]
108    fn test_fact_hash_all_variants() {
109        let test_facts = vec![
110            Fact::Command {
111                id: FactId(1),
112                instruction: JsonValue::empty_object(),
113            },
114            Fact::PayloadUpdate {
115                id: FactId(2),
116                path: "test.path".into(),
117                value: JsonValue::string("test_value"),
118            },
119            Fact::StateTransition {
120                id: FactId(3),
121                cause: FactId(1),
122                new_payload: JsonValue::empty_object(),
123                new_queue: vec![],
124            },
125            Fact::IoRequest {
126                id: FactId(4),
127                cause: FactId(3),
128                io_type: IoType::http_get(),
129                params: JsonValue::empty_object(),
130            },
131            Fact::IoResponse {
132                id: FactId(5),
133                request_id: FactId(4),
134                result: JsonValue::string("response"),
135                error: None,
136            },
137            Fact::IoResponse {
138                id: FactId(6),
139                request_id: FactId(4),
140                result: JsonValue::Null,
141                error: Some("timeout".to_string()),
142            },
143            Fact::Stable {
144                id: FactId(7),
145                final_snapshot: JsonValue::empty_object(),
146            },
147            Fact::Error {
148                id: FactId(8),
149                message: "test error".into(),
150            },
151        ];
152
153        for fact in test_facts {
154            let hash = fact_hash(&fact).unwrap();
155            assert_eq!(hash.len(), 64);
156
157            let hash2 = fact_hash(&fact).unwrap();
158            assert_eq!(
159                hash,
160                hash2,
161                "fact_hash should be deterministic for {}",
162                fact.type_name()
163            );
164        }
165    }
166
167    #[test]
168    fn test_fact_hash_identity() {
169        let fact1 = Fact::Command {
170            id: FactId(1),
171            instruction: JsonValue::string("same"),
172        };
173        let fact2 = Fact::Command {
174            id: FactId(1),
175            instruction: JsonValue::string("same"),
176        };
177
178        assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
179    }
180
181    #[test]
182    fn test_fact_hash_different_ids() {
183        let fact1 = Fact::Command {
184            id: FactId(1),
185            instruction: JsonValue::string("same"),
186        };
187        let fact2 = Fact::Command {
188            id: FactId(2),
189            instruction: JsonValue::string("same"),
190        };
191
192        assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
193    }
194
195    /// 交叉验证:CLI re-export 的哈希与 tier1 直接计算的哈希一致
196    ///
197    /// 本测试是两套 WAL 合并的核心验证点:
198    /// 确保 CLI 通过 re-export 调用的哈希函数与 tier1 直接调用的完全一致。
199    /// 由于 CLI 现在直接 re-export tier1 的函数,本测试实质上验证 re-export 正确性。
200    #[test]
201    fn test_cross_validate_with_tier1() {
202        let test_facts = [
203            Fact::Command {
204                id: FactId(1),
205                instruction: JsonValue::empty_object(),
206            },
207            Fact::PayloadUpdate {
208                id: FactId(2),
209                path: "test.path".into(),
210                value: JsonValue::string("test_value"),
211            },
212            Fact::StateTransition {
213                id: FactId(3),
214                cause: FactId(1),
215                new_payload: JsonValue::empty_object(),
216                new_queue: vec![],
217            },
218            Fact::IoRequest {
219                id: FactId(4),
220                cause: FactId(3),
221                io_type: IoType::http_get(),
222                params: JsonValue::empty_object(),
223            },
224            Fact::IoResponse {
225                id: FactId(5),
226                request_id: FactId(4),
227                result: JsonValue::string("response"),
228                error: None,
229            },
230            Fact::Stable {
231                id: FactId(6),
232                final_snapshot: JsonValue::empty_object(),
233            },
234            Fact::Error {
235                id: FactId(7),
236                message: "test error".into(),
237            },
238        ];
239
240        // CLI re-export 的哈希
241        let cli_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
242
243        // tier1 直接计算的哈希
244        let tier1_hashes: Vec<String> = test_facts
245            .iter()
246            .map(|f| evorule_reactor::fact_hash(f).unwrap())
247            .collect();
248
249        assert_eq!(
250            cli_hashes, tier1_hashes,
251            "CLI re-export 的哈希与 tier1 直接计算的哈希不一致!\
252             这违反了两套 WAL 合并的单一真相源原则。"
253        );
254
255        // 验证链哈希一致
256        let cli_chain = compute_chain_hash(&test_facts).unwrap();
257        let tier1_chain = evorule_reactor::compute_chain_hash(&test_facts).unwrap();
258        assert_eq!(
259            cli_chain, tier1_chain,
260            "CLI re-export 的链哈希与 tier1 直接计算的不一致!"
261        );
262    }
263
264    #[test]
265    fn test_hash_chain_stability() {
266        let facts = vec![
267            Fact::Command {
268                id: FactId(1),
269                instruction: JsonValue::empty_object(),
270            },
271            Fact::StateTransition {
272                id: FactId(2),
273                cause: FactId(1),
274                new_payload: JsonValue::empty_object(),
275                new_queue: vec![],
276            },
277        ];
278
279        // compute_chain_hash 是确定性的
280        let result1 = compute_chain_hash(&facts).unwrap();
281        let result2 = compute_chain_hash(&facts).unwrap();
282        assert_eq!(result1, result2);
283
284        // verify_hash_chain(deprecated)仍可调用,返回 true
285        assert!(verify_hash_chain(&facts));
286    }
287
288    #[test]
289    fn test_compute_chain_hash_empty() {
290        let facts: Vec<Fact> = vec![];
291        let chain_hash = compute_chain_hash(&facts).unwrap();
292        assert_eq!(chain_hash, "genesis");
293    }
294
295    #[test]
296    fn test_compute_chain_hash_order_sensitive() {
297        let fact1 = Fact::Command {
298            id: FactId(1),
299            instruction: JsonValue::empty_object(),
300        };
301        let fact2 = Fact::StateTransition {
302            id: FactId(2),
303            cause: FactId(1),
304            new_payload: JsonValue::empty_object(),
305            new_queue: vec![],
306        };
307
308        let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
309        let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
310        assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
311    }
312}