immutable_logging/
chain.rs1use crate::error::LogError;
4use crate::log_entry::LogEntry;
5use crate::merkle_service::{self, MerkleProof};
6use serde::{Deserialize, Serialize};
7
8pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ChainProof {
14 pub target_entry_id: String,
15 pub target_index: usize,
16 pub chain_head_hash: String,
17 pub steps: Vec<ChainProofStep>,
18 pub merkle_proof: Option<MerkleProof>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ChainProofStep {
24 pub entry_hash: String,
25 pub entry: LogEntry,
26}
27
28impl ChainProof {
29 pub fn attach_merkle_proof(&mut self, proof: Option<MerkleProof>) {
30 self.merkle_proof = proof;
31 }
32}
33
34pub struct LogChain {
36 entries: Vec<LogEntry>,
37 current_hash: String,
38 entry_index: std::collections::HashMap<String, usize>,
39}
40
41impl LogChain {
42 pub fn new() -> Self {
44 LogChain {
45 entries: Vec::new(),
46 current_hash: GENESIS_HASH.to_string(),
47 entry_index: std::collections::HashMap::new(),
48 }
49 }
50
51 pub async fn append(&mut self, entry: LogEntry) -> Result<LogEntry, LogError> {
53 let entry = entry.commit_with_previous_hash(&self.current_hash)?;
54 let new_hash = entry.compute_hash(&self.current_hash)?;
55
56 let index = self.entries.len();
57 self.entry_index.insert(entry.entry_id().to_string(), index);
58 self.entries.push(entry.clone());
59 self.current_hash = new_hash;
60
61 Ok(entry)
62 }
63
64 pub fn verify(&self) -> bool {
66 let mut previous_hash = GENESIS_HASH.to_string();
67 for entry in &self.entries {
68 if !entry.verify_content_hash() {
69 return false;
70 }
71 if entry.previous_entry_hash() != previous_hash {
72 return false;
73 }
74
75 let computed = match entry.compute_hash(&previous_hash) {
76 Ok(v) => v,
77 Err(_) => return false,
78 };
79 previous_hash = computed;
80 }
81
82 previous_hash == self.current_hash
83 }
84
85 pub fn get_entry(&self, entry_id: &str) -> Option<&LogEntry> {
86 self.entry_index
87 .get(entry_id)
88 .and_then(|idx| self.entries.get(*idx))
89 }
90
91 pub fn generate_proof(&self, entry_id: &str) -> Option<ChainProof> {
93 let &target_index = self.entry_index.get(entry_id)?;
94 if target_index >= self.entries.len() {
95 return None;
96 }
97
98 let mut steps = Vec::with_capacity(self.entries.len());
99 let mut previous_hash = GENESIS_HASH.to_string();
100 for entry in &self.entries {
101 let entry_hash = entry.compute_hash(&previous_hash).ok()?;
102 steps.push(ChainProofStep {
103 entry_hash: entry_hash.clone(),
104 entry: entry.clone(),
105 });
106 previous_hash = entry_hash;
107 }
108
109 Some(ChainProof {
110 target_entry_id: entry_id.to_string(),
111 target_index,
112 chain_head_hash: self.current_hash.clone(),
113 steps,
114 merkle_proof: None,
115 })
116 }
117
118 pub fn len(&self) -> usize {
120 self.entries.len()
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.entries.is_empty()
126 }
127
128 pub fn current_hash(&self) -> &str {
130 &self.current_hash
131 }
132}
133
134impl Default for LogChain {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140pub fn verify_chain_proof(proof: &ChainProof) -> bool {
142 if proof.steps.is_empty() || proof.target_index >= proof.steps.len() {
143 return false;
144 }
145 if proof.steps[proof.target_index].entry.entry_id() != proof.target_entry_id {
146 return false;
147 }
148
149 let mut previous_hash = GENESIS_HASH.to_string();
150 for step in &proof.steps {
151 if !step.entry.verify_content_hash() {
152 return false;
153 }
154 if step.entry.previous_entry_hash() != previous_hash {
155 return false;
156 }
157 let recomputed = match step.entry.compute_hash(&previous_hash) {
158 Ok(v) => v,
159 Err(_) => return false,
160 };
161 if recomputed != step.entry_hash {
162 return false;
163 }
164 previous_hash = recomputed;
165 }
166
167 if previous_hash != proof.chain_head_hash {
168 return false;
169 }
170
171 if let Some(merkle) = &proof.merkle_proof {
172 if merkle.entry_id != proof.target_entry_id {
173 return false;
174 }
175 if !merkle_service::verify_proof(merkle) {
176 return false;
177 }
178 }
179
180 true
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::log_entry::EventType;
187
188 #[test]
189 fn test_genesis_hash() {
190 assert_eq!(GENESIS_HASH.len(), 64);
191 }
192
193 #[tokio::test]
194 async fn test_append_entry() {
195 let mut chain = LogChain::new();
196 let entry = LogEntry::new(
197 EventType::AccountQuery,
198 "AGENT_001".to_string(),
199 "DGFiP".to_string(),
200 )
201 .unwrap();
202
203 let result = chain.append(entry).await;
204 assert!(result.is_ok());
205 assert!(chain.verify());
206 }
207
208 #[tokio::test]
209 async fn test_chain_proof_detects_tampering() {
210 let mut chain = LogChain::new();
211 let e1 = chain
212 .append(
213 LogEntry::new(
214 EventType::AuthSuccess,
215 "AGENT_001".to_string(),
216 "DGFiP".to_string(),
217 )
218 .unwrap(),
219 )
220 .await
221 .unwrap();
222 let _e2 = chain
223 .append(
224 LogEntry::new(
225 EventType::DataAccess,
226 "AGENT_002".to_string(),
227 "DGFiP".to_string(),
228 )
229 .unwrap(),
230 )
231 .await
232 .unwrap();
233
234 let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
235 assert!(verify_chain_proof(&proof));
236
237 proof.steps[0].entry_hash = "0".repeat(64);
238 assert!(!verify_chain_proof(&proof));
239 }
240}