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