Skip to main content

immutable_logging/
lib.rs

1//! Immutable Logging - Append-only audit logs with cryptographic proof
2//!
3//! This module implements the immutable audit layer as specified in SPEC_IMMUTABLE_LOGGING.md
4//! Features:
5//! - Chained hash verification
6//! - Hourly Merkle tree roots
7//! - Daily publication
8//! - TSA timestamps
9
10pub mod chain;
11pub mod error;
12pub mod log_entry;
13pub mod merkle_service;
14pub mod publication;
15
16use serde::{Deserialize, Serialize};
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20/// Immutable log service
21pub struct ImmutableLog {
22    chain: Arc<RwLock<chain::LogChain>>,
23    merkle: Arc<RwLock<merkle_service::MerkleService>>,
24}
25
26impl ImmutableLog {
27    /// Create new immutable log
28    pub fn new() -> Self {
29        ImmutableLog {
30            chain: Arc::new(RwLock::new(chain::LogChain::new())),
31            merkle: Arc::new(RwLock::new(merkle_service::MerkleService::new())),
32        }
33    }
34
35    /// Append a new entry
36    pub async fn append(
37        &self,
38        entry: log_entry::LogEntry,
39    ) -> Result<log_entry::LogEntry, error::LogError> {
40        // Get current chain state
41        let mut chain = self.chain.write().await;
42        let entry = chain.append(entry).await?;
43
44        // Add to merkle tree
45        let mut merkle = self.merkle.write().await;
46        merkle.add_entry(entry.clone()).await?;
47
48        Ok(entry)
49    }
50
51    /// Verify chain integrity
52    pub async fn verify(&self) -> Result<bool, error::LogError> {
53        let chain = self.chain.read().await;
54        Ok(chain.verify())
55    }
56
57    /// Get number of entries currently stored in the chain.
58    pub async fn entry_count(&self) -> usize {
59        let chain = self.chain.read().await;
60        chain.len()
61    }
62
63    /// Get the current chain hash (or genesis hash if empty).
64    pub async fn current_hash(&self) -> String {
65        let chain = self.chain.read().await;
66        chain.current_hash().to_string()
67    }
68
69    /// Get current hourly root
70    pub async fn get_hourly_root(&self) -> Option<merkle_service::HourlyRoot> {
71        let merkle = self.merkle.read().await;
72        merkle.get_current_root()
73    }
74
75    /// Snapshot hourly roots (published roots + current in-progress hour root if present).
76    pub async fn hourly_roots_snapshot(&self) -> Vec<merkle_service::HourlyRoot> {
77        let merkle = self.merkle.read().await;
78        let mut roots = merkle.get_published_roots().to_vec();
79        if let Some(current) = merkle.get_current_root() {
80            let exists = roots
81                .iter()
82                .any(|r| r.hour == current.hour && r.root_hash == current.root_hash);
83            if !exists {
84                roots.push(current);
85            }
86        }
87        roots
88    }
89
90    /// Generate chain proof for an entry
91    pub async fn get_chain_proof(&self, entry_id: &str) -> Option<chain::ChainProof> {
92        let chain = self.chain.read().await;
93        let entry = chain.get_entry(entry_id)?.clone();
94        let mut proof = chain.generate_proof(entry_id)?;
95        drop(chain);
96
97        let merkle = self.merkle.read().await;
98        let merkle_proof = merkle.generate_proof(entry_id, &entry);
99        proof.attach_merkle_proof(merkle_proof);
100        Some(proof)
101    }
102}
103
104impl Default for ImmutableLog {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110/// Configuration for immutable logging
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct LogConfig {
113    /// Hash algorithm
114    pub hash_algorithm: String,
115    /// Hourly publication enabled
116    pub hourly_publication: bool,
117    /// Daily publication enabled
118    pub daily_publication: bool,
119    /// TSA server URL
120    pub tsa_url: Option<String>,
121    /// Blockchain enabled
122    pub blockchain_enabled: bool,
123}
124
125impl Default for LogConfig {
126    fn default() -> Self {
127        LogConfig {
128            hash_algorithm: "SHA256".to_string(),
129            hourly_publication: true,
130            daily_publication: true,
131            tsa_url: None,
132            blockchain_enabled: true,
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_default_config() {
143        let config = LogConfig::default();
144        assert_eq!(config.hash_algorithm, "SHA256");
145        assert!(config.hourly_publication);
146    }
147
148    #[tokio::test]
149    async fn test_append_entry() {
150        let log = ImmutableLog::new();
151
152        let entry = log_entry::LogEntry::new(
153            log_entry::EventType::AccountQuery,
154            "agent-001".to_string(),
155            "org-001".to_string(),
156        )
157        .unwrap();
158
159        let result = log.append(entry).await;
160        assert!(result.is_ok());
161    }
162
163    #[tokio::test]
164    async fn test_chain_stats() {
165        let log = ImmutableLog::new();
166        assert_eq!(log.entry_count().await, 0);
167        assert_eq!(log.current_hash().await.len(), 64);
168    }
169
170    #[tokio::test]
171    async fn test_hourly_roots_snapshot_includes_current_root() {
172        let log = ImmutableLog::new();
173        let entry = log_entry::LogEntry::new(
174            log_entry::EventType::AccountQuery,
175            "agent-001".to_string(),
176            "org-001".to_string(),
177        )
178        .unwrap();
179        log.append(entry).await.expect("append");
180
181        let roots = log.hourly_roots_snapshot().await;
182        assert_eq!(roots.len(), 1);
183        assert_eq!(roots[0].entry_count, 1);
184    }
185}