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//! - `verify_hash_chain`:始终返回 `true` 的误导函数("假验证"陷阱),已彻底删除。
21//!   真正的完整性验证请使用 [`compute_chain_hash`] 重算后与存储的链哈希比对,
22//!   或使用 `verify_chain` 命令读取带哈希字段的 WAL 并逐一校验。
23//!
24//! # 与 tier1/tier2 的关系
25//! - 算法源:`evorule_reactor::hash`(单一真相源)
26//! - evorule-governance 也 re-export 同一源
27//! - 三方(tier1/tier2/CLI)哈希值字节级一致,无需交叉验证快照
28
29/// 重导出 tier1 的哈希算法(单一真相源)
30///
31/// 所有哈希计算函数均在 `evorule_reactor::hash` 中实现,
32/// 本模块通过 re-export 对外暴露相同的 API。
33#[allow(unused_imports)]
34pub use evorule_reactor::{
35    compute_chain_hash, content_hash, fact_hash, fact_to_stable_json, HashError,
36};
37
38/// 验证哈希链完整性的正确姿势:用 [`compute_chain_hash`] 重算后与存储的链哈希比对,
39/// 或在 `verify_chain` 命令中读取带哈希字段的 WAL 并逐一校验。
40///
41/// # 迁移指南
42/// ```ignore
43/// // 正确做法(真正验证):
44/// let computed = compute_chain_hash(&facts)?;
45/// if computed != stored_last_hash {
46///     return Err("审计链断裂");
47/// }
48/// ```
49#[cfg(test)]
50mod tests {
51    #![allow(deprecated, clippy::unwrap_used, clippy::panic, clippy::expect_used)]
52    use super::*;
53    use evorule_reactor::{Fact, FactId, IoType};
54    use evorule_tcb::JsonValue;
55
56    #[test]
57    fn test_fact_to_stable_json_format() {
58        let command = Fact::Command {
59            id: FactId(1),
60            instruction: JsonValue::object_from_pairs(&[
61                ("type", JsonValue::string("increment")),
62                (
63                    "params",
64                    JsonValue::object_from_pairs(&[
65                        ("attr", JsonValue::string("x")),
66                        ("delta", JsonValue::Integer(5)),
67                    ]),
68                ),
69            ]),
70        };
71
72        let json_value = fact_to_stable_json(&command).unwrap();
73
74        assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
75        assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
76        assert!(json_value.get("instruction").is_some());
77    }
78
79    #[test]
80    fn test_fact_hash_all_variants() {
81        let test_facts = vec![
82            Fact::Command {
83                id: FactId(1),
84                instruction: JsonValue::empty_object(),
85            },
86            Fact::PayloadUpdate {
87                id: FactId(2),
88                path: "test.path".into(),
89                value: JsonValue::string("test_value"),
90            },
91            Fact::StateTransition {
92                id: FactId(3),
93                cause: FactId(1),
94                new_payload: JsonValue::empty_object(),
95                new_queue: vec![],
96            },
97            Fact::IoRequest {
98                id: FactId(4),
99                cause: FactId(3),
100                io_type: IoType::http_get(),
101                params: JsonValue::empty_object(),
102            },
103            Fact::IoResponse {
104                id: FactId(5),
105                request_id: FactId(4),
106                result: JsonValue::string("response"),
107                error: None,
108            },
109            Fact::IoResponse {
110                id: FactId(6),
111                request_id: FactId(4),
112                result: JsonValue::Null,
113                error: Some("timeout".to_string()),
114            },
115            Fact::Stable {
116                id: FactId(7),
117                version: 1,
118            },
119            Fact::Error {
120                id: FactId(8),
121                message: "test error".into(),
122            },
123        ];
124
125        for fact in test_facts {
126            let hash = fact_hash(&fact).unwrap();
127            assert_eq!(hash.len(), 64);
128
129            let hash2 = fact_hash(&fact).unwrap();
130            assert_eq!(
131                hash,
132                hash2,
133                "fact_hash should be deterministic for {}",
134                fact.type_name()
135            );
136        }
137    }
138
139    #[test]
140    fn test_fact_hash_identity() {
141        let fact1 = Fact::Command {
142            id: FactId(1),
143            instruction: JsonValue::string("same"),
144        };
145        let fact2 = Fact::Command {
146            id: FactId(1),
147            instruction: JsonValue::string("same"),
148        };
149
150        assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
151    }
152
153    #[test]
154    fn test_fact_hash_different_ids() {
155        let fact1 = Fact::Command {
156            id: FactId(1),
157            instruction: JsonValue::string("same"),
158        };
159        let fact2 = Fact::Command {
160            id: FactId(2),
161            instruction: JsonValue::string("same"),
162        };
163
164        assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
165    }
166
167    /// 交叉验证:CLI re-export 的哈希与 tier1 直接计算的哈希一致
168    ///
169    /// 本测试是两套 WAL 合并的核心验证点:
170    /// 确保 CLI 通过 re-export 调用的哈希函数与 tier1 直接调用的完全一致。
171    /// 由于 CLI 现在直接 re-export tier1 的函数,本测试实质上验证 re-export 正确性。
172    #[test]
173    fn test_cross_validate_with_tier1() {
174        let test_facts = [
175            Fact::Command {
176                id: FactId(1),
177                instruction: JsonValue::empty_object(),
178            },
179            Fact::PayloadUpdate {
180                id: FactId(2),
181                path: "test.path".into(),
182                value: JsonValue::string("test_value"),
183            },
184            Fact::StateTransition {
185                id: FactId(3),
186                cause: FactId(1),
187                new_payload: JsonValue::empty_object(),
188                new_queue: vec![],
189            },
190            Fact::IoRequest {
191                id: FactId(4),
192                cause: FactId(3),
193                io_type: IoType::http_get(),
194                params: JsonValue::empty_object(),
195            },
196            Fact::IoResponse {
197                id: FactId(5),
198                request_id: FactId(4),
199                result: JsonValue::string("response"),
200                error: None,
201            },
202            Fact::Stable {
203                id: FactId(6),
204                version: 1,
205            },
206            Fact::Error {
207                id: FactId(7),
208                message: "test error".into(),
209            },
210        ];
211
212        // CLI re-export 的哈希
213        let cli_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
214
215        // tier1 直接计算的哈希
216        let tier1_hashes: Vec<String> = test_facts
217            .iter()
218            .map(|f| evorule_reactor::fact_hash(f).unwrap())
219            .collect();
220
221        assert_eq!(
222            cli_hashes, tier1_hashes,
223            "CLI re-export 的哈希与 tier1 直接计算的哈希不一致!\
224             这违反了两套 WAL 合并的单一真相源原则。"
225        );
226
227        // 验证链哈希一致
228        let cli_chain = compute_chain_hash(&test_facts).unwrap();
229        let tier1_chain = evorule_reactor::compute_chain_hash(&test_facts).unwrap();
230        assert_eq!(
231            cli_chain, tier1_chain,
232            "CLI re-export 的链哈希与 tier1 直接计算的不一致!"
233        );
234    }
235
236    #[test]
237    fn test_hash_chain_stability() {
238        let facts = vec![
239            Fact::Command {
240                id: FactId(1),
241                instruction: JsonValue::empty_object(),
242            },
243            Fact::StateTransition {
244                id: FactId(2),
245                cause: FactId(1),
246                new_payload: JsonValue::empty_object(),
247                new_queue: vec![],
248            },
249        ];
250
251        // compute_chain_hash 是确定性的
252        let result1 = compute_chain_hash(&facts).unwrap();
253        let result2 = compute_chain_hash(&facts).unwrap();
254        assert_eq!(result1, result2);
255    }
256
257    #[test]
258    fn test_compute_chain_hash_empty() {
259        let facts: Vec<Fact> = vec![];
260        let chain_hash = compute_chain_hash(&facts).unwrap();
261        assert_eq!(chain_hash, "genesis");
262    }
263
264    #[test]
265    fn test_compute_chain_hash_order_sensitive() {
266        let fact1 = Fact::Command {
267            id: FactId(1),
268            instruction: JsonValue::empty_object(),
269        };
270        let fact2 = Fact::StateTransition {
271            id: FactId(2),
272            cause: FactId(1),
273            new_payload: JsonValue::empty_object(),
274            new_queue: vec![],
275        };
276
277        let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
278        let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
279        assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
280    }
281}