use crate::error::LogError;
use crate::log_entry::LogEntry;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HourlyRoot {
pub hour: String,
pub root_hash: String,
pub entry_count: u64,
pub generated_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleProofStep {
pub side: String, pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MerkleProof {
pub entry_id: String,
pub leaf_hash: String,
pub root_hash: String,
pub proof: Vec<MerkleProofStep>,
}
pub struct MerkleService {
entries: Vec<Vec<u8>>,
current_hour: String,
published_roots: Vec<HourlyRoot>,
}
impl MerkleService {
pub fn new() -> Self {
MerkleService {
entries: Vec::new(),
current_hour: Self::get_current_hour(),
published_roots: Vec::new(),
}
}
fn get_current_hour() -> String {
Utc::now().format("%Y-%m-%dT%H:00:00Z").to_string()
}
pub async fn add_entry(&mut self, entry: LogEntry) -> Result<(), LogError> {
let current = Self::get_current_hour();
if current != self.current_hour {
self.publish_current_root();
self.entries.clear();
self.current_hour = current;
}
let bytes = entry.canonical_entry_bytes()?;
let hash = Self::hash_entry(&bytes);
self.entries.push(hash);
Ok(())
}
fn hash_entry(data: &[u8]) -> Vec<u8> {
Self::hash_leaf(data)
}
fn hash_leaf(data: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update([0x00]);
hasher.update(data);
hasher.finalize().to_vec()
}
fn hash_node(left: &[u8], right: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
hasher.update([0x01]);
hasher.update(left);
hasher.update(right);
hasher.finalize().to_vec()
}
pub fn get_current_root(&self) -> Option<HourlyRoot> {
if self.entries.is_empty() {
return None;
}
let root = self.build_tree(&self.entries);
Some(HourlyRoot {
hour: self.current_hour.clone(),
root_hash: Self::hex_encode(&root),
entry_count: self.entries.len() as u64,
generated_at: Utc::now().timestamp(),
})
}
fn publish_current_root(&mut self) {
if let Some(root) = self.get_current_root() {
self.published_roots.push(root);
}
}
fn build_tree(&self, leaves: &[Vec<u8>]) -> Vec<u8> {
if leaves.is_empty() {
return vec![0u8; 32];
}
let mut level = leaves.to_vec();
while level.len() > 1 {
level = Self::next_level(&level);
}
level[0].clone()
}
fn next_level(level: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for chunk in level.chunks(2) {
let left = &chunk[0];
let right = if chunk.len() == 2 {
&chunk[1]
} else {
&chunk[0]
};
next.push(Self::hash_node(left, right));
}
next
}
pub fn generate_proof(&self, entry_id: &str, entry_data: &LogEntry) -> Option<MerkleProof> {
let entry_bytes = entry_data.canonical_entry_bytes().ok()?;
let leaf_hash_bytes = Self::hash_entry(&entry_bytes);
let leaf_hash = Self::hex_encode(&leaf_hash_bytes);
let root = self.get_current_root()?;
let index = self.entries.iter().position(|h| h == &leaf_hash_bytes)?;
let proof = self.build_proof_path(&self.entries, index)?;
Some(MerkleProof {
entry_id: entry_id.to_string(),
leaf_hash,
root_hash: root.root_hash,
proof,
})
}
fn build_proof_path(
&self,
entries: &[Vec<u8>],
mut index: usize,
) -> Option<Vec<MerkleProofStep>> {
if entries.is_empty() || index >= entries.len() {
return None;
}
let mut proof = Vec::new();
let mut level = entries.to_vec();
while level.len() > 1 {
let is_right = index % 2 == 1;
let sibling_index = if is_right {
index - 1
} else {
(index + 1).min(level.len() - 1)
};
let sibling_hash = Self::hex_encode(&level[sibling_index]);
proof.push(MerkleProofStep {
side: if is_right {
"left".to_string()
} else {
"right".to_string()
},
hash: sibling_hash,
});
level = Self::next_level(&level);
index /= 2;
}
Some(proof)
}
fn hex_encode(data: &[u8]) -> String {
data.iter().map(|b| format!("{:02x}", b)).collect()
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
pub fn get_published_roots(&self) -> &[HourlyRoot] {
&self.published_roots
}
}
impl Default for MerkleService {
fn default() -> Self {
Self::new()
}
}
pub fn verify_proof(proof: &MerkleProof) -> bool {
let mut current = match MerkleService::hex_decode(&proof.leaf_hash) {
Some(v) => v,
None => return false,
};
for step in &proof.proof {
let sibling = match MerkleService::hex_decode(&step.hash) {
Some(v) => v,
None => return false,
};
match step.side.as_str() {
"left" => {
current = MerkleService::hash_node(&sibling, ¤t);
}
"right" => {
current = MerkleService::hash_node(¤t, &sibling);
}
_ => return false,
}
}
MerkleService::hex_encode(¤t) == proof.root_hash
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log_entry::EventType;
#[test]
fn test_hourly_root() {
let service = MerkleService::new();
let root = service.get_current_root();
assert!(root.is_none());
}
#[test]
fn test_hex_encode() {
let data = b"test";
let encoded = MerkleService::hex_encode(data);
assert_eq!(encoded.len(), 8);
}
#[tokio::test]
async fn test_merkle_proof_roundtrip() {
let mut service = MerkleService::new();
let e1 = LogEntry::new(EventType::AccountQuery, "a".to_string(), "o".to_string()).unwrap();
let e2 = LogEntry::new(EventType::AuthSuccess, "b".to_string(), "o".to_string()).unwrap();
service.add_entry(e1.clone()).await.unwrap();
service.add_entry(e2.clone()).await.unwrap();
let proof = service.generate_proof(e2.entry_id(), &e2).unwrap();
assert!(verify_proof(&proof));
}
#[test]
fn test_leaf_and_node_hash_domain_separation() {
let left = vec![0xAA; 32];
let right = vec![0xBB; 32];
let mut combined = left.clone();
combined.extend_from_slice(&right);
let leaf_hash = MerkleService::hash_leaf(&combined);
let node_hash = MerkleService::hash_node(&left, &right);
assert_ne!(leaf_hash, node_hash);
}
}