use std::fmt;
use evorule_tcb::JsonValue;
use tracing::{debug, trace};
use crate::fact::Fact;
#[derive(Debug)]
pub struct HashError {
message: String,
}
impl HashError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for HashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "哈希计算错误: {}", self.message)
}
}
impl std::error::Error for HashError {}
fn tcb_to_serde(value: &JsonValue) -> serde_json::Value {
match value {
JsonValue::Null => serde_json::Value::Null,
JsonValue::Bool(b) => serde_json::Value::Bool(*b),
JsonValue::Integer(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
JsonValue::String(s) => serde_json::Value::String(s.clone()),
JsonValue::Array(arr) => serde_json::Value::Array(arr.iter().map(tcb_to_serde).collect()),
JsonValue::Object(obj) => {
let mut map = serde_json::Map::new();
for (k, v) in obj.iter() {
map.insert(k.clone(), tcb_to_serde(v));
}
serde_json::Value::Object(map)
}
}
}
#[allow(clippy::cognitive_complexity, clippy::too_many_lines)]
pub fn fact_to_stable_json(fact: &Fact) -> Result<serde_json::Value, HashError> {
let fact_type = fact.type_name();
let fact_id = fact.id();
trace!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"序列化开始"
);
let mut obj = serde_json::Map::new();
match fact {
Fact::Command { id, instruction } => {
trace!(
事实ID = ?id,
指令大小 = instruction.to_string().len(),
"处理命令类型事实"
);
obj.insert("type".into(), serde_json::Value::String("Command".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("instruction".into(), tcb_to_serde(instruction));
}
Fact::PayloadUpdate { id, path, value } => {
trace!(
事实ID = ?id,
路径 = %path,
"处理载荷更新类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("PayloadUpdate".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("path".into(), serde_json::Value::String(path.clone()));
obj.insert("value".into(), tcb_to_serde(value));
}
Fact::StateTransition {
id,
cause,
new_payload,
new_queue,
} => {
trace!(
事实ID = ?id,
原因ID = ?cause,
队列长度 = new_queue.len(),
"处理状态转换类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("StateTransition".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
obj.insert("new_payload".into(), tcb_to_serde(new_payload));
obj.insert(
"new_queue".into(),
serde_json::Value::Array(new_queue.iter().map(tcb_to_serde).collect()),
);
}
Fact::IoRequest {
id,
cause,
io_type,
params,
} => {
trace!(
事实ID = ?id,
原因ID = ?cause,
IO类型 = %io_type.as_str(),
"处理IO请求类型事实"
);
obj.insert("type".into(), serde_json::Value::String("IoRequest".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("cause".into(), serde_json::Value::Number(cause.0.into()));
obj.insert(
"io_type".into(),
serde_json::Value::String(io_type.as_str().into()),
);
obj.insert("params".into(), tcb_to_serde(params));
}
Fact::IoResponse {
id,
request_id,
result,
error,
} => {
trace!(
事实ID = ?id,
请求ID = ?request_id,
是否有错误 = error.is_some(),
"处理IO响应类型事实"
);
obj.insert(
"type".into(),
serde_json::Value::String("IoResponse".into()),
);
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert(
"request_id".into(),
serde_json::Value::Number(request_id.0.into()),
);
obj.insert("result".into(), tcb_to_serde(result));
obj.insert(
"error".into(),
error
.as_ref()
.map(|e| serde_json::Value::String(e.clone()))
.unwrap_or(serde_json::Value::Null),
);
}
Fact::Stable { id, final_snapshot } => {
trace!(
事实ID = ?id,
快照大小 = final_snapshot.to_string().len(),
"处理稳定状态类型事实"
);
obj.insert("type".into(), serde_json::Value::String("Stable".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("final_snapshot".into(), tcb_to_serde(final_snapshot));
}
Fact::Error { id, message } => {
trace!(
事实ID = ?id,
消息 = %message,
"处理错误类型事实"
);
obj.insert("type".into(), serde_json::Value::String("Error".into()));
obj.insert("id".into(), serde_json::Value::Number(id.0.into()));
obj.insert("message".into(), serde_json::Value::String(message.clone()));
}
}
let value = serde_json::Value::Object(obj);
trace!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"序列化完成"
);
Ok(value)
}
#[allow(dead_code)] pub fn content_hash(value: &JsonValue) -> Result<String, HashError> {
#[cfg(kani)]
{
Ok(String::from("c"))
}
#[cfg(not(kani))]
{
let serde_value = tcb_to_serde(value);
let serialized = serde_json::to_string(&serde_value)
.map_err(|e| HashError::new(format!("内容哈希序列化失败: {}", e)))?;
let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
trace!(
序列化长度 = serialized.len(),
哈希值 = %hash,
"内容哈希计算完成"
);
Ok(hash)
}
}
pub fn fact_hash(fact: &Fact) -> Result<String, HashError> {
#[cfg(kani)]
{
let id = fact.id().0;
let h = match id % 7 {
0 => "a",
1 => "b",
2 => "c",
3 => "d",
4 => "e",
5 => "f",
_ => "g",
};
Ok(String::from(h))
}
#[cfg(not(kani))]
{
let fact_type = fact.type_name();
let fact_id = fact.id();
debug!(
事实ID = ?fact_id,
事实类型 = %fact_type,
"开始计算事实哈希"
);
let value = fact_to_stable_json(fact)?;
let serialized = serde_json::to_string(&value)
.map_err(|e| HashError::new(format!("序列化失败: {}", e)))?;
let hash = blake3::hash(serialized.as_bytes()).to_hex().to_string();
debug!(
事实ID = ?fact_id,
事实类型 = %fact_type,
哈希值 = %hash,
序列化长度 = serialized.len(),
"事实哈希计算完成"
);
Ok(hash)
}
}
pub fn compute_chain_hash(facts: &[Fact]) -> Result<String, HashError> {
let fact_count = facts.len();
debug!(事实数量 = fact_count, "开始计算链哈希");
if fact_count == 0 {
debug!("事实列表为空,返回 genesis 哈希");
return Ok(String::from("genesis"));
}
let mut prev_hash = String::from("genesis");
trace!(
初始前序哈希 = %prev_hash,
"初始化前序哈希为创世值"
);
for (index, fact) in facts.iter().enumerate() {
let fact_type = fact.type_name();
let fact_id = fact.id();
trace!(
索引 = index,
事实ID = ?fact_id,
事实类型 = %fact_type,
前序哈希 = %prev_hash,
"处理事实"
);
let fh = fact_hash(fact)?;
trace!(
索引 = index,
事实ID = ?fact_id,
事实哈希 = %fh,
"事实哈希计算完成"
);
let current = chain_step(&prev_hash, &fh);
trace!(
索引 = index,
事实ID = ?fact_id,
当前哈希 = %current,
"计算当前哈希(前序哈希+事实哈希)"
);
prev_hash = current;
}
debug!(
事实数量 = fact_count,
最终哈希 = %prev_hash,
"链哈希计算完成"
);
Ok(prev_hash)
}
pub fn chain_step(prev_hash: &str, content_hash: &str) -> String {
#[cfg(kani)]
{
let mut s = String::from(prev_hash);
s.push_str(content_hash);
s
}
#[cfg(not(kani))]
{
let combined = format!("{}{}", prev_hash, content_hash);
blake3::hash(combined.as_bytes()).to_hex().to_string()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic, clippy::expect_used)]
use super::*;
use crate::fact::{Fact, FactId, IoType};
use evorule_tcb::JsonValue;
#[test]
fn test_fact_to_stable_json_format() {
let command = Fact::Command {
id: FactId(1),
instruction: JsonValue::object_from_pairs(&[
("type", JsonValue::string("increment")),
(
"params",
JsonValue::object_from_pairs(&[
("attr", JsonValue::string("x")),
("delta", JsonValue::Integer(5)),
]),
),
]),
};
let json_value = fact_to_stable_json(&command).unwrap();
assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
assert!(json_value.get("instruction").is_some());
}
#[test]
fn test_fact_hash_all_variants() {
let test_facts = vec![
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::IoResponse {
id: FactId(6),
request_id: FactId(4),
result: JsonValue::Null,
error: Some("timeout".to_string()),
},
Fact::Stable {
id: FactId(7),
final_snapshot: JsonValue::empty_object(),
},
Fact::Error {
id: FactId(8),
message: "test error".into(),
},
];
for fact in test_facts {
let hash = fact_hash(&fact).unwrap();
assert_eq!(hash.len(), 64);
let hash2 = fact_hash(&fact).unwrap();
assert_eq!(
hash,
hash2,
"fact_hash should be deterministic for {}",
fact.type_name()
);
}
}
#[test]
fn test_fact_hash_identity() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
let fact2 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
}
#[test]
fn test_fact_hash_different_ids() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::string("same"),
};
let fact2 = Fact::Command {
id: FactId(2),
instruction: JsonValue::string("same"),
};
assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
}
#[test]
fn test_fact_hash_snapshot() {
let test_facts = [
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::Stable {
id: FactId(6),
final_snapshot: JsonValue::empty_object(),
},
Fact::Error {
id: FactId(7),
message: "test error".into(),
},
];
let current_hashes: Vec<String> =
test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
let snapshot_file = env!("CARGO_MANIFEST_DIR").to_string() + "/hash_snapshot.txt";
if std::path::Path::new(&snapshot_file).exists() {
let snapshot = std::fs::read_to_string(&snapshot_file).unwrap();
let expected_hashes: Vec<String> = snapshot.lines().map(|s| s.to_string()).collect();
assert_eq!(
current_hashes, expected_hashes,
"Hash snapshot mismatch! If this is an expected change (e.g., Fact struct modification), delete {} to regenerate.",
snapshot_file
);
} else {
let snapshot_content = current_hashes.join("\n");
std::fs::write(&snapshot_file, snapshot_content).unwrap();
println!("Created hash snapshot: {}", snapshot_file);
}
}
#[test]
fn test_compute_chain_hash_empty() {
let facts: Vec<Fact> = vec![];
let chain_hash = compute_chain_hash(&facts).unwrap();
assert_eq!(chain_hash, "genesis");
}
#[test]
fn test_compute_chain_hash_deterministic() {
let facts = vec![
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::StateTransition {
id: FactId(2),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
];
let result1 = compute_chain_hash(&facts).unwrap();
let result2 = compute_chain_hash(&facts).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn test_compute_chain_hash_order_sensitive() {
let fact1 = Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
};
let fact2 = Fact::StateTransition {
id: FactId(2),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
};
let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
}
#[test]
fn test_cross_validate_with_tier2() {
let test_facts = [
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
},
Fact::PayloadUpdate {
id: FactId(2),
path: "test.path".into(),
value: JsonValue::string("test_value"),
},
Fact::StateTransition {
id: FactId(3),
cause: FactId(1),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
},
Fact::IoRequest {
id: FactId(4),
cause: FactId(3),
io_type: IoType::http_get(),
params: JsonValue::empty_object(),
},
Fact::IoResponse {
id: FactId(5),
request_id: FactId(4),
result: JsonValue::string("response"),
error: None,
},
Fact::Stable {
id: FactId(6),
final_snapshot: JsonValue::empty_object(),
},
Fact::Error {
id: FactId(7),
message: "test error".into(),
},
];
let tier1_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
let tier2_snapshot_path = "../../evorule-governance/src/hash_snapshot.txt".to_string();
if std::path::Path::new(&tier2_snapshot_path).exists() {
let tier2_snapshot = std::fs::read_to_string(&tier2_snapshot_path).unwrap();
let tier2_hashes: Vec<String> = tier2_snapshot.lines().map(|s| s.to_string()).collect();
assert_eq!(
tier1_hashes, tier2_hashes,
"tier1 和 tier2 的 fact_hash 不一致!\
如果这是预期变更(如 Fact 结构修改),\
请删除 evorule-governance/src/hash_snapshot.txt 重新生成。"
);
}
}
}