immutable_logging/
chain.rs1use crate::log_entry::LogEntry;
4use crate::error::LogError;
5use serde::{Deserialize, Serialize};
6
7pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ChainProof {
13 pub entry_id: String,
14 pub entry_hash: String,
15 pub path: Vec<ChainProofStep>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ChainProofStep {
21 pub previous_hash: String,
22}
23
24pub struct LogChain {
26 entries: Vec<LogEntry>,
27 current_hash: String,
28 entry_index: std::collections::HashMap<String, usize>,
29}
30
31impl LogChain {
32 pub fn new() -> Self {
34 LogChain {
35 entries: Vec::new(),
36 current_hash: GENESIS_HASH.to_string(),
37 entry_index: std::collections::HashMap::new(),
38 }
39 }
40
41 pub async fn append(&mut self, mut entry: LogEntry) -> Result<LogEntry, LogError> {
43 entry.update_previous_hash(&self.current_hash);
45
46 let new_hash = entry.compute_hash(&self.current_hash);
48
49 let index = self.entries.len();
51 self.entry_index.insert(entry.entry_id.clone(), index);
52 self.entries.push(entry.clone());
53
54 self.current_hash = new_hash;
56
57 Ok(entry)
58 }
59
60 pub fn verify(&self) -> bool {
62 if self.entries.is_empty() {
63 return true;
64 }
65
66 let mut previous_hash = GENESIS_HASH.to_string();
67
68 for entry in &self.entries {
69 if entry.integrity.previous_entry_hash != previous_hash {
71 return false;
72 }
73
74 let computed = entry.compute_hash(&previous_hash);
76 if computed != self.get_entry_hash(&entry.entry_id) {
77 return false;
78 }
79
80 previous_hash = computed;
81 }
82
83 true
84 }
85
86 fn get_entry_hash(&self, entry_id: &str) -> String {
88 let index = self.entry_index.get(entry_id).copied();
91
92 if let Some(idx) = index {
93 let entry = &self.entries[idx];
94 entry.compute_hash(&entry.integrity.previous_entry_hash)
95 } else {
96 String::new()
97 }
98 }
99
100 pub fn generate_proof(&self, entry_id: &str) -> Option<ChainProof> {
102 let index = self.entry_index.get(entry_id)?;
103
104 if *index >= self.entries.len() {
105 return None;
106 }
107
108 let entry = &self.entries[*index];
109 let entry_hash = entry.compute_hash(&entry.integrity.previous_entry_hash);
110
111 let mut path = Vec::new();
112
113 for i in 0..=*index {
115 if i == 0 {
116 path.push(ChainProofStep {
117 previous_hash: GENESIS_HASH.to_string(),
118 });
119 } else {
120 let prev_entry = &self.entries[i - 1];
121 let hash = prev_entry.compute_hash(&prev_entry.integrity.previous_entry_hash);
122 path.push(ChainProofStep {
123 previous_hash: hash,
124 });
125 }
126 }
127
128 Some(ChainProof {
129 entry_id: entry_id.to_string(),
130 entry_hash,
131 path,
132 })
133 }
134
135 pub fn len(&self) -> usize {
137 self.entries.len()
138 }
139
140 pub fn is_empty(&self) -> bool {
142 self.entries.is_empty()
143 }
144
145 pub fn current_hash(&self) -> &str {
147 &self.current_hash
148 }
149}
150
151impl Default for LogChain {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157pub fn verify_chain_proof(proof: &ChainProof) -> bool {
159 !proof.entry_hash.is_empty() && !proof.path.is_empty()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::log_entry::EventType;
167
168 #[test]
169 fn test_genesis_hash() {
170 assert_eq!(GENESIS_HASH.len(), 64);
171 }
172
173 #[tokio::test]
174 async fn test_append_entry() {
175 let mut chain = LogChain::new();
176
177 let mut entry = LogEntry::new(
178 EventType::AccountQuery,
179 "AGENT_001".to_string(),
180 "DGFiP".to_string(),
181 );
182 entry.update_previous_hash(GENESIS_HASH);
183
184 let result = chain.append(entry).await;
185 assert!(result.is_ok());
186 assert!(chain.verify());
187 }
188
189 #[tokio::test]
190 async fn test_chain_proof() {
191 let mut chain = LogChain::new();
192
193 let entry = LogEntry::new(
194 EventType::AuthSuccess,
195 "AGENT_001".to_string(),
196 "DGFiP".to_string(),
197 );
198
199 let entry = chain.append(entry).await.unwrap();
200 let proof = chain.generate_proof(&entry.entry_id);
201
202 assert!(proof.is_some());
203 }
204}