1pub mod chain;
11pub mod error;
12pub mod log_entry;
13pub mod merkle_service;
14pub mod publication;
15
16use serde::{Deserialize, Serialize};
17use std::{io::Write, path::Path, path::PathBuf, sync::Arc};
18use tokio::sync::{Mutex, RwLock};
19
20pub struct ImmutableLog {
22 chain: Arc<RwLock<chain::LogChain>>,
23 merkle: Arc<RwLock<merkle_service::MerkleService>>,
24 wal: Option<Arc<Mutex<WalStore>>>,
25}
26
27struct WalStore {
28 path: PathBuf,
29}
30
31impl WalStore {
32 fn open(path: PathBuf) -> Result<Self, error::LogError> {
33 if let Some(parent) = path.parent() {
34 std::fs::create_dir_all(parent)
35 .map_err(|e| error::LogError::PublicationError(e.to_string()))?;
36 }
37 if !path.exists() {
38 std::fs::File::create(&path)
39 .map_err(|e| error::LogError::PublicationError(e.to_string()))?;
40 }
41 Ok(Self { path })
42 }
43
44 fn load_entries(&self) -> Result<Vec<log_entry::LogEntry>, error::LogError> {
45 let raw = std::fs::read_to_string(&self.path)
46 .map_err(|e| error::LogError::PublicationError(e.to_string()))?;
47 let mut out = Vec::new();
48 for (line_no, line) in raw.lines().enumerate() {
49 let line = line.trim();
50 if line.is_empty() {
51 continue;
52 }
53 let entry = serde_json::from_str::<log_entry::LogEntry>(line).map_err(|e| {
54 error::LogError::PublicationError(format!(
55 "WAL parse error at line {}: {}",
56 line_no + 1,
57 e
58 ))
59 })?;
60 out.push(entry);
61 }
62 Ok(out)
63 }
64
65 fn append_entry(&mut self, entry: &log_entry::LogEntry) -> Result<(), error::LogError> {
66 let mut file = std::fs::OpenOptions::new()
67 .create(true)
68 .append(true)
69 .open(&self.path)
70 .map_err(|e| error::LogError::PublicationError(e.to_string()))?;
71 let encoded = serde_json::to_string(entry)
72 .map_err(|e| error::LogError::SerializationError(e.to_string()))?;
73 writeln!(file, "{}", encoded).map_err(|e| error::LogError::PublicationError(e.to_string()))
74 }
75}
76
77impl ImmutableLog {
78 pub fn new() -> Self {
80 ImmutableLog {
81 chain: Arc::new(RwLock::new(chain::LogChain::new())),
82 merkle: Arc::new(RwLock::new(merkle_service::MerkleService::new())),
83 wal: None,
84 }
85 }
86
87 pub async fn with_wal_path<P: AsRef<Path>>(path: P) -> Result<Self, error::LogError> {
89 let wal = WalStore::open(path.as_ref().to_path_buf())?;
90 let entries = wal.load_entries()?;
91 let log = ImmutableLog {
92 chain: Arc::new(RwLock::new(chain::LogChain::new())),
93 merkle: Arc::new(RwLock::new(merkle_service::MerkleService::new())),
94 wal: Some(Arc::new(Mutex::new(wal))),
95 };
96
97 if !entries.is_empty() {
98 let mut chain = log.chain.write().await;
99 let mut merkle = log.merkle.write().await;
100 for entry in entries {
101 chain.append_committed(entry.clone()).await?;
102 merkle.add_entry(entry).await?;
103 }
104 }
105
106 Ok(log)
107 }
108
109 pub async fn append(
111 &self,
112 entry: log_entry::LogEntry,
113 ) -> Result<log_entry::LogEntry, error::LogError> {
114 let mut chain = self.chain.write().await;
116 let entry = chain.append(entry).await?;
117
118 let mut merkle = self.merkle.write().await;
120 merkle.add_entry(entry.clone()).await?;
121
122 if let Some(wal) = &self.wal {
123 let mut wal = wal.lock().await;
124 wal.append_entry(&entry)?;
125 }
126
127 Ok(entry)
128 }
129
130 pub async fn verify(&self) -> Result<bool, error::LogError> {
132 let chain = self.chain.read().await;
133 Ok(chain.verify())
134 }
135
136 pub async fn entry_count(&self) -> usize {
138 let chain = self.chain.read().await;
139 chain.len()
140 }
141
142 pub async fn current_hash(&self) -> String {
144 let chain = self.chain.read().await;
145 chain.current_hash().to_string()
146 }
147
148 pub async fn get_hourly_root(&self) -> Option<merkle_service::HourlyRoot> {
150 let merkle = self.merkle.read().await;
151 merkle.get_current_root()
152 }
153
154 pub async fn hourly_roots_snapshot(&self) -> Vec<merkle_service::HourlyRoot> {
156 let merkle = self.merkle.read().await;
157 let mut roots = merkle.get_published_roots().to_vec();
158 if let Some(current) = merkle.get_current_root() {
159 let exists = roots
160 .iter()
161 .any(|r| r.hour == current.hour && r.root_hash == current.root_hash);
162 if !exists {
163 roots.push(current);
164 }
165 }
166 roots
167 }
168
169 pub async fn get_chain_proof(&self, entry_id: &str) -> Option<chain::ChainProof> {
171 let chain = self.chain.read().await;
172 let entry = chain.get_entry(entry_id)?.clone();
173 let mut proof = chain.generate_proof(entry_id)?;
174 drop(chain);
175
176 let merkle = self.merkle.read().await;
177 let merkle_proof = merkle.generate_proof(entry_id, &entry);
178 proof.attach_merkle_proof(merkle_proof);
179 Some(proof)
180 }
181}
182
183impl Default for ImmutableLog {
184 fn default() -> Self {
185 Self::new()
186 }
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct LogConfig {
192 pub hash_algorithm: String,
194 pub hourly_publication: bool,
196 pub daily_publication: bool,
198 pub tsa_url: Option<String>,
200 pub blockchain_enabled: bool,
202}
203
204impl Default for LogConfig {
205 fn default() -> Self {
206 LogConfig {
207 hash_algorithm: "SHA256".to_string(),
208 hourly_publication: true,
209 daily_publication: true,
210 tsa_url: None,
211 blockchain_enabled: true,
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_default_config() {
222 let config = LogConfig::default();
223 assert_eq!(config.hash_algorithm, "SHA256");
224 assert!(config.hourly_publication);
225 }
226
227 #[tokio::test]
228 async fn test_append_entry() {
229 let log = ImmutableLog::new();
230
231 let entry = log_entry::LogEntry::new(
232 log_entry::EventType::AccountQuery,
233 "agent-001".to_string(),
234 "org-001".to_string(),
235 )
236 .unwrap();
237
238 let result = log.append(entry).await;
239 assert!(result.is_ok());
240 }
241
242 #[tokio::test]
243 async fn test_chain_stats() {
244 let log = ImmutableLog::new();
245 assert_eq!(log.entry_count().await, 0);
246 assert_eq!(log.current_hash().await.len(), 64);
247 }
248
249 #[tokio::test]
250 async fn test_hourly_roots_snapshot_includes_current_root() {
251 let log = ImmutableLog::new();
252 let entry = log_entry::LogEntry::new(
253 log_entry::EventType::AccountQuery,
254 "agent-001".to_string(),
255 "org-001".to_string(),
256 )
257 .unwrap();
258 log.append(entry).await.expect("append");
259
260 let roots = log.hourly_roots_snapshot().await;
261 assert_eq!(roots.len(), 1);
262 assert_eq!(roots[0].entry_count, 1);
263 }
264
265 #[tokio::test]
266 async fn test_wal_replay_rebuilds_chain() {
267 let tmp = tempfile::tempdir().expect("tempdir");
268 let wal_path = tmp.path().join("log.wal");
269
270 let log = ImmutableLog::with_wal_path(&wal_path)
271 .await
272 .expect("wal init");
273 let entry = log_entry::LogEntry::new(
274 log_entry::EventType::AccountQuery,
275 "agent-001".to_string(),
276 "org-001".to_string(),
277 )
278 .unwrap();
279 log.append(entry).await.expect("append");
280 assert_eq!(log.entry_count().await, 1);
281 drop(log);
282
283 let reloaded = ImmutableLog::with_wal_path(&wal_path)
284 .await
285 .expect("wal reload");
286 assert_eq!(reloaded.entry_count().await, 1);
287 assert!(reloaded.verify().await.expect("verify"));
288 }
289}