pub mod chain;
pub mod error;
pub mod log_entry;
pub mod merkle_service;
pub mod publication;
use serde::{Deserialize, Serialize};
use std::{io::Write, path::Path, path::PathBuf, sync::Arc};
use tokio::sync::{Mutex, RwLock};
pub struct ImmutableLog {
chain: Arc<RwLock<chain::LogChain>>,
merkle: Arc<RwLock<merkle_service::MerkleService>>,
wal: Option<Arc<Mutex<WalStore>>>,
}
struct WalStore {
path: PathBuf,
}
impl WalStore {
fn open(path: PathBuf) -> Result<Self, error::LogError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| error::LogError::PublicationError(e.to_string()))?;
}
if !path.exists() {
std::fs::File::create(&path)
.map_err(|e| error::LogError::PublicationError(e.to_string()))?;
}
Ok(Self { path })
}
fn load_entries(&self) -> Result<Vec<log_entry::LogEntry>, error::LogError> {
let raw = std::fs::read_to_string(&self.path)
.map_err(|e| error::LogError::PublicationError(e.to_string()))?;
let mut out = Vec::new();
for (line_no, line) in raw.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let entry = serde_json::from_str::<log_entry::LogEntry>(line).map_err(|e| {
error::LogError::PublicationError(format!(
"WAL parse error at line {}: {}",
line_no + 1,
e
))
})?;
out.push(entry);
}
Ok(out)
}
fn append_entry(&mut self, entry: &log_entry::LogEntry) -> Result<(), error::LogError> {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.map_err(|e| error::LogError::PublicationError(e.to_string()))?;
let encoded = serde_json::to_string(entry)
.map_err(|e| error::LogError::SerializationError(e.to_string()))?;
writeln!(file, "{}", encoded).map_err(|e| error::LogError::PublicationError(e.to_string()))
}
}
impl ImmutableLog {
pub fn new() -> Self {
ImmutableLog {
chain: Arc::new(RwLock::new(chain::LogChain::new())),
merkle: Arc::new(RwLock::new(merkle_service::MerkleService::new())),
wal: None,
}
}
pub async fn with_wal_path<P: AsRef<Path>>(path: P) -> Result<Self, error::LogError> {
let wal = WalStore::open(path.as_ref().to_path_buf())?;
let entries = wal.load_entries()?;
let log = ImmutableLog {
chain: Arc::new(RwLock::new(chain::LogChain::new())),
merkle: Arc::new(RwLock::new(merkle_service::MerkleService::new())),
wal: Some(Arc::new(Mutex::new(wal))),
};
if !entries.is_empty() {
let mut chain = log.chain.write().await;
let mut merkle = log.merkle.write().await;
for entry in entries {
chain.append_committed(entry.clone()).await?;
merkle.add_entry(entry).await?;
}
}
Ok(log)
}
pub async fn append(
&self,
entry: log_entry::LogEntry,
) -> Result<log_entry::LogEntry, error::LogError> {
let mut chain = self.chain.write().await;
let entry = chain.append(entry).await?;
let mut merkle = self.merkle.write().await;
merkle.add_entry(entry.clone()).await?;
if let Some(wal) = &self.wal {
let mut wal = wal.lock().await;
wal.append_entry(&entry)?;
}
Ok(entry)
}
pub async fn verify(&self) -> Result<bool, error::LogError> {
let chain = self.chain.read().await;
Ok(chain.verify())
}
pub async fn entry_count(&self) -> usize {
let chain = self.chain.read().await;
chain.len()
}
pub async fn current_hash(&self) -> String {
let chain = self.chain.read().await;
chain.current_hash().to_string()
}
pub async fn get_hourly_root(&self) -> Option<merkle_service::HourlyRoot> {
let merkle = self.merkle.read().await;
merkle.get_current_root()
}
pub async fn hourly_roots_snapshot(&self) -> Vec<merkle_service::HourlyRoot> {
let merkle = self.merkle.read().await;
let mut roots = merkle.get_published_roots().to_vec();
if let Some(current) = merkle.get_current_root() {
let exists = roots
.iter()
.any(|r| r.hour == current.hour && r.root_hash == current.root_hash);
if !exists {
roots.push(current);
}
}
roots
}
pub async fn get_chain_proof(&self, entry_id: &str) -> Option<chain::ChainProof> {
let chain = self.chain.read().await;
let entry = chain.get_entry(entry_id)?.clone();
let mut proof = chain.generate_proof(entry_id)?;
drop(chain);
let merkle = self.merkle.read().await;
let merkle_proof = merkle.generate_proof(entry_id, &entry);
proof.attach_merkle_proof(merkle_proof);
Some(proof)
}
}
impl Default for ImmutableLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub hash_algorithm: String,
pub hourly_publication: bool,
pub daily_publication: bool,
pub tsa_url: Option<String>,
pub blockchain_enabled: bool,
}
impl Default for LogConfig {
fn default() -> Self {
LogConfig {
hash_algorithm: "SHA256".to_string(),
hourly_publication: true,
daily_publication: true,
tsa_url: None,
blockchain_enabled: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = LogConfig::default();
assert_eq!(config.hash_algorithm, "SHA256");
assert!(config.hourly_publication);
}
#[tokio::test]
async fn test_append_entry() {
let log = ImmutableLog::new();
let entry = log_entry::LogEntry::new(
log_entry::EventType::AccountQuery,
"agent-001".to_string(),
"org-001".to_string(),
)
.unwrap();
let result = log.append(entry).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_chain_stats() {
let log = ImmutableLog::new();
assert_eq!(log.entry_count().await, 0);
assert_eq!(log.current_hash().await.len(), 64);
}
#[tokio::test]
async fn test_hourly_roots_snapshot_includes_current_root() {
let log = ImmutableLog::new();
let entry = log_entry::LogEntry::new(
log_entry::EventType::AccountQuery,
"agent-001".to_string(),
"org-001".to_string(),
)
.unwrap();
log.append(entry).await.expect("append");
let roots = log.hourly_roots_snapshot().await;
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].entry_count, 1);
}
#[tokio::test]
async fn test_wal_replay_rebuilds_chain() {
let tmp = tempfile::tempdir().expect("tempdir");
let wal_path = tmp.path().join("log.wal");
let log = ImmutableLog::with_wal_path(&wal_path)
.await
.expect("wal init");
let entry = log_entry::LogEntry::new(
log_entry::EventType::AccountQuery,
"agent-001".to_string(),
"org-001".to_string(),
)
.unwrap();
log.append(entry).await.expect("append");
assert_eq!(log.entry_count().await, 1);
drop(log);
let reloaded = ImmutableLog::with_wal_path(&wal_path)
.await
.expect("wal reload");
assert_eq!(reloaded.entry_count().await, 1);
assert!(reloaded.verify().await.expect("verify"));
}
}