use serde::{Deserialize, Serialize};
use serde_json::Value;
use ts_rs::TS;
use super::hash;
pub const DEFAULT_BLOCK_SIZE: usize = 4096;
pub const NOISE_KEYS: &[&str] = &["request_id", "requestId", "timestamp", "nonce"];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Chain {
pub body_hash: u128,
pub len_bytes: u64,
pub block_size: usize,
pub block_hashes: Vec<u128>,
}
impl Chain {
#[must_use]
pub fn block_count(&self) -> u64 {
self.block_hashes.len() as u64
}
}
pub struct BlockChainFingerprint {
pub block_size: usize,
pub scope_salt: u64,
}
impl BlockChainFingerprint {
#[must_use]
pub fn new(block_size: usize, scope_salt: u64) -> Self {
debug_assert!(block_size >= 16, "block size too small");
Self {
block_size,
scope_salt,
}
}
#[must_use]
pub fn compute(&self, data: &[u8]) -> Chain {
let n = data.len();
let mut prev: u128 = 0;
let mut hashes = Vec::with_capacity(n / self.block_size + 1);
for chunk in data.chunks(self.block_size) {
let key = hash::mix(self.scope_salt ^ hash::low64(prev));
let h = hash::hash128(key, chunk);
hashes.push(h);
prev = h;
}
let body_hash = hash::hash128(hash::mix(self.scope_salt ^ 0x517c_c1b7_2722_0a95), data);
Chain {
body_hash,
len_bytes: n as u64,
block_size: self.block_size,
block_hashes: hashes,
}
}
}
#[must_use]
pub fn denoise(value: &Value) -> Value {
denoise_at(value, 0)
}
fn denoise_at(value: &Value, depth: usize) -> Value {
match value {
Value::Object(map) => {
let mut out = serde_json::Map::with_capacity(map.len());
for (k, v) in map {
if depth == 0 {
if NOISE_KEYS.contains(&k.as_str()) {
continue;
}
} else if k == "request_id" || k == "requestId" {
continue;
}
out.insert(k.clone(), denoise_at(v, depth + 1));
}
Value::Object(out)
}
Value::Array(items) => {
Value::Array(items.iter().map(|i| denoise_at(i, depth + 1)).collect())
}
other => other.clone(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Fingerprint {
pub body_hash: String,
pub len_bytes: u64,
pub block_size: u64,
pub block_hashes: Vec<String>,
pub token_estimate: u64,
}
impl From<&Chain> for Fingerprint {
fn from(chain: &Chain) -> Self {
Fingerprint {
body_hash: hash::hex128(chain.body_hash),
len_bytes: chain.len_bytes,
block_size: chain.block_size as u64,
block_hashes: chain
.block_hashes
.iter()
.map(|h| hash::hex128(*h))
.collect(),
token_estimate: chain.len_bytes / 4,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fingerprint_is_deterministic_and_sensitive() {
let fp = BlockChainFingerprint::new(512, 7);
let a = b"hello world".repeat(64);
let b = b"hello world".repeat(64);
let c = b"hello worle".repeat(64); assert_eq!(fp.compute(&a).body_hash, fp.compute(&b).body_hash);
assert_ne!(fp.compute(&a).body_hash, fp.compute(&c).body_hash);
}
#[test]
fn block_count_matches_chunks() {
let fp = BlockChainFingerprint::new(512, 1);
let data = vec![b'x'; 1200];
let chain = fp.compute(&data);
assert_eq!(chain.block_count(), 3);
assert_eq!(chain.len_bytes, 1200);
}
#[test]
fn denoise_strips_top_level_noise_and_nested_request_ids() {
let v = serde_json::json!({
"model": "gpt-4o",
"messages": [{"role": "user", "content": "hi"}],
"request_id": "rand-1",
"timestamp": 123,
"nonce": "n1",
"nested": {"nonce": "abc", "keep": 1},
"array": [{"requestId": "x"}, {"keep": 2}],
});
let d = denoise(&v);
assert!(d.get("request_id").is_none());
assert!(d.get("timestamp").is_none());
assert!(d.get("nonce").is_none());
assert_eq!(d["nested"]["keep"], 1);
assert_eq!(d["nested"]["nonce"], "abc");
assert!(d["array"][0].get("requestId").is_none());
assert_eq!(d["array"][1]["keep"], 2);
assert_eq!(d["model"], "gpt-4o");
assert_eq!(d["messages"][0]["content"], "hi");
}
#[test]
fn denoise_preserves_tool_arguments_with_timestamp_and_nonce() {
let v = serde_json::json!({
"messages": [{
"role": "user",
"content": "hi",
"tool_arguments": {"timestamp": 123456, "nonce": "sig-1", "data": "x"}
}]
});
let d = denoise(&v);
assert_eq!(d["messages"][0]["tool_arguments"]["timestamp"], 123456);
assert_eq!(d["messages"][0]["tool_arguments"]["nonce"], "sig-1");
}
#[test]
fn denoise_preserves_body_fingerprint_stability() {
let fp = BlockChainFingerprint::new(512, 42);
let v1 = serde_json::json!({"messages":[{"role":"user","content":"abc"}],"request_id":"r1","timestamp":1});
let v2 = serde_json::json!({"messages":[{"role":"user","content":"abc"}],"request_id":"r2","timestamp":2});
let b1 = serde_json::to_vec(&denoise(&v1)).unwrap();
let b2 = serde_json::to_vec(&denoise(&v2)).unwrap();
assert_eq!(b1, b2);
assert_eq!(fp.compute(&b1).body_hash, fp.compute(&b2).body_hash);
}
#[test]
fn fingerprint_serializes_as_hex_strings() {
let fp = BlockChainFingerprint::new(512, 3);
let chain = fp.compute(b"data");
let f = Fingerprint::from(&chain);
assert_eq!(f.body_hash.len(), 32); assert!(f.block_hashes.iter().all(|h| h.len() == 32));
let json = serde_json::to_string(&f).unwrap();
assert!(json.contains("\"body_hash\":\""));
}
}