Skip to main content

immutable_logging/
chain.rs

1//! Log Chain - Append-only linked list with hash chaining
2
3use crate::error::LogError;
4use crate::log_entry::LogEntry;
5use crate::merkle_service::{self, MerkleProof};
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9/// Genesis hash (initial chain hash)
10pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
11
12const CHAIN_PROOF_LEAF_PREFIX: u8 = 0x02;
13const CHAIN_PROOF_NODE_PREFIX: u8 = 0x03;
14
15/// Chain proof for cryptographic verification.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ChainProof {
18    pub target_entry_id: String,
19    pub target_index: usize,
20    pub chain_length: usize,
21    pub chain_head_hash: String,
22    pub target_entry: LogEntry,
23    pub target_entry_hash: String,
24    pub chain_root_hash: String,
25    pub target_membership_proof: Vec<ChainProofPathStep>,
26    pub head_membership_proof: Vec<ChainProofPathStep>,
27    pub merkle_proof: Option<MerkleProof>,
28}
29
30/// Merkle path element for compact chain proofs.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ChainProofPathStep {
33    pub side: String, // "left" or "right"
34    pub hash: String,
35}
36
37impl ChainProof {
38    pub fn attach_merkle_proof(&mut self, proof: Option<MerkleProof>) {
39        self.merkle_proof = proof;
40    }
41}
42
43/// Log chain state
44pub struct LogChain {
45    entries: Vec<LogEntry>,
46    current_hash: String,
47    entry_index: std::collections::HashMap<String, usize>,
48}
49
50impl LogChain {
51    /// Create new chain with genesis block
52    pub fn new() -> Self {
53        LogChain {
54            entries: Vec::new(),
55            current_hash: GENESIS_HASH.to_string(),
56            entry_index: std::collections::HashMap::new(),
57        }
58    }
59
60    /// Append entry to chain
61    pub async fn append(&mut self, entry: LogEntry) -> Result<LogEntry, LogError> {
62        let entry = entry.commit_with_previous_hash(&self.current_hash)?;
63        let new_hash = entry.compute_hash(&self.current_hash)?;
64
65        let index = self.entries.len();
66        self.entry_index.insert(entry.entry_id().to_string(), index);
67        self.entries.push(entry.clone());
68        self.current_hash = new_hash;
69
70        Ok(entry)
71    }
72
73    /// Append an already-committed entry (WAL replay path).
74    pub async fn append_committed(&mut self, entry: LogEntry) -> Result<(), LogError> {
75        if !entry.verify_content_hash() {
76            return Err(LogError::ChainError(
77                "WAL replay rejected entry with invalid content hash".to_string(),
78            ));
79        }
80        if entry.previous_entry_hash() != self.current_hash {
81            return Err(LogError::ChainError(format!(
82                "WAL replay previous hash mismatch: expected {}, got {}",
83                self.current_hash,
84                entry.previous_entry_hash()
85            )));
86        }
87
88        let new_hash = entry.compute_hash(&self.current_hash)?;
89        let index = self.entries.len();
90        self.entry_index.insert(entry.entry_id().to_string(), index);
91        self.entries.push(entry);
92        self.current_hash = new_hash;
93        Ok(())
94    }
95
96    /// Verify chain integrity
97    pub fn verify(&self) -> bool {
98        let mut previous_hash = GENESIS_HASH.to_string();
99        for entry in &self.entries {
100            if !entry.verify_content_hash() {
101                return false;
102            }
103            if entry.previous_entry_hash() != previous_hash {
104                return false;
105            }
106
107            let computed = match entry.compute_hash(&previous_hash) {
108                Ok(v) => v,
109                Err(_) => return false,
110            };
111            previous_hash = computed;
112        }
113
114        previous_hash == self.current_hash
115    }
116
117    pub fn get_entry(&self, entry_id: &str) -> Option<&LogEntry> {
118        self.entry_index
119            .get(entry_id)
120            .and_then(|idx| self.entries.get(*idx))
121    }
122
123    /// Generate a compact inclusion proof for an entry.
124    ///
125    /// The proof size is O(log n):
126    /// - one Merkle path proving target entry hash membership in the chain root
127    /// - one Merkle path proving chain head hash membership in the same root
128    pub fn generate_proof(&self, entry_id: &str) -> Option<ChainProof> {
129        let &target_index = self.entry_index.get(entry_id)?;
130        if target_index >= self.entries.len() {
131            return None;
132        }
133
134        let entry_hashes = self.compute_entry_hashes()?;
135        let chain_length = entry_hashes.len();
136        if chain_length == 0 {
137            return None;
138        }
139
140        let target_entry = self.entries.get(target_index)?.clone();
141        let target_entry_hash = entry_hashes.get(target_index)?.clone();
142        let head_index = chain_length - 1;
143        let head_hash = entry_hashes.get(head_index)?;
144        if head_hash != &self.current_hash {
145            return None;
146        }
147
148        let leaves = entry_hashes
149            .iter()
150            .map(|h| Self::hex_decode(h).map(|bytes| Self::hash_chain_leaf(&bytes)))
151            .collect::<Option<Vec<_>>>()?;
152        let chain_root_hash = Self::hex_encode(&Self::build_merkle_root(&leaves));
153        let target_membership_proof = Self::build_merkle_path(&leaves, target_index)?;
154        let head_membership_proof = Self::build_merkle_path(&leaves, head_index)?;
155
156        Some(ChainProof {
157            target_entry_id: entry_id.to_string(),
158            target_index,
159            chain_length,
160            chain_head_hash: self.current_hash.clone(),
161            target_entry,
162            target_entry_hash,
163            chain_root_hash,
164            target_membership_proof,
165            head_membership_proof,
166            merkle_proof: None,
167        })
168    }
169
170    fn compute_entry_hashes(&self) -> Option<Vec<String>> {
171        let mut hashes = Vec::with_capacity(self.entries.len());
172        let mut previous_hash = GENESIS_HASH.to_string();
173        for entry in &self.entries {
174            if !entry.verify_content_hash() {
175                return None;
176            }
177            if entry.previous_entry_hash() != previous_hash {
178                return None;
179            }
180            let entry_hash = entry.compute_hash(&previous_hash).ok()?;
181            previous_hash = entry_hash.clone();
182            hashes.push(entry_hash);
183        }
184
185        if previous_hash != self.current_hash {
186            return None;
187        }
188
189        Some(hashes)
190    }
191
192    fn hash_chain_leaf(data: &[u8]) -> Vec<u8> {
193        let mut hasher = Sha256::new();
194        hasher.update([CHAIN_PROOF_LEAF_PREFIX]);
195        hasher.update(data);
196        hasher.finalize().to_vec()
197    }
198
199    fn hash_chain_node(left: &[u8], right: &[u8]) -> Vec<u8> {
200        let mut hasher = Sha256::new();
201        hasher.update([CHAIN_PROOF_NODE_PREFIX]);
202        hasher.update(left);
203        hasher.update(right);
204        hasher.finalize().to_vec()
205    }
206
207    fn next_level(level: &[Vec<u8>]) -> Vec<Vec<u8>> {
208        let mut next = Vec::with_capacity(level.len().div_ceil(2));
209        for chunk in level.chunks(2) {
210            let left = &chunk[0];
211            let right = if chunk.len() == 2 {
212                &chunk[1]
213            } else {
214                &chunk[0]
215            };
216            next.push(Self::hash_chain_node(left, right));
217        }
218        next
219    }
220
221    fn build_merkle_root(leaves: &[Vec<u8>]) -> Vec<u8> {
222        if leaves.is_empty() {
223            return vec![0u8; 32];
224        }
225        let mut level = leaves.to_vec();
226        while level.len() > 1 {
227            level = Self::next_level(&level);
228        }
229        level[0].clone()
230    }
231
232    fn build_merkle_path(leaves: &[Vec<u8>], mut index: usize) -> Option<Vec<ChainProofPathStep>> {
233        if leaves.is_empty() || index >= leaves.len() {
234            return None;
235        }
236        let mut path = Vec::new();
237        let mut level = leaves.to_vec();
238
239        while level.len() > 1 {
240            let is_right = index % 2 == 1;
241            let sibling_index = if is_right {
242                index - 1
243            } else {
244                (index + 1).min(level.len() - 1)
245            };
246            path.push(ChainProofPathStep {
247                side: if is_right {
248                    "left".to_string()
249                } else {
250                    "right".to_string()
251                },
252                hash: Self::hex_encode(&level[sibling_index]),
253            });
254            level = Self::next_level(&level);
255            index /= 2;
256        }
257
258        Some(path)
259    }
260
261    fn hex_encode(data: &[u8]) -> String {
262        data.iter().map(|b| format!("{:02x}", b)).collect()
263    }
264
265    fn hex_decode(s: &str) -> Option<Vec<u8>> {
266        if !s.len().is_multiple_of(2) {
267            return None;
268        }
269        (0..s.len())
270            .step_by(2)
271            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
272            .collect()
273    }
274
275    /// Get entry count
276    pub fn len(&self) -> usize {
277        self.entries.len()
278    }
279
280    /// Check if empty
281    pub fn is_empty(&self) -> bool {
282        self.entries.is_empty()
283    }
284
285    /// Get current hash
286    pub fn current_hash(&self) -> &str {
287        &self.current_hash
288    }
289}
290
291impl Default for LogChain {
292    fn default() -> Self {
293        Self::new()
294    }
295}
296
297/// Verify chain proof using compact O(log n) membership paths and optional Merkle proof.
298pub fn verify_chain_proof(proof: &ChainProof) -> bool {
299    if proof.chain_length == 0 || proof.target_index >= proof.chain_length {
300        return false;
301    }
302    if proof.target_entry.entry_id() != proof.target_entry_id {
303        return false;
304    }
305    if proof.chain_head_hash.len() != 64 || proof.target_entry_hash.len() != 64 {
306        return false;
307    }
308    if !proof.target_entry.verify_content_hash() {
309        return false;
310    }
311
312    let target_entry_hash = match proof
313        .target_entry
314        .compute_hash(proof.target_entry.previous_entry_hash())
315    {
316        Ok(v) => v,
317        Err(_) => return false,
318    };
319    if target_entry_hash != proof.target_entry_hash {
320        return false;
321    }
322
323    if !verify_compact_membership(
324        &proof.target_entry_hash,
325        &proof.target_membership_proof,
326        &proof.chain_root_hash,
327    ) {
328        return false;
329    }
330
331    if !verify_compact_membership(
332        &proof.chain_head_hash,
333        &proof.head_membership_proof,
334        &proof.chain_root_hash,
335    ) {
336        return false;
337    }
338
339    if let Some(merkle) = &proof.merkle_proof {
340        if merkle.entry_id != proof.target_entry_id {
341            return false;
342        }
343        if !merkle_service::verify_proof(merkle) {
344            return false;
345        }
346    }
347
348    true
349}
350
351fn verify_compact_membership(
352    entry_hash_hex: &str,
353    path: &[ChainProofPathStep],
354    expected_root_hex: &str,
355) -> bool {
356    let entry_hash = match LogChain::hex_decode(entry_hash_hex) {
357        Some(v) => v,
358        None => return false,
359    };
360    let mut current = LogChain::hash_chain_leaf(&entry_hash);
361
362    for step in path {
363        let sibling = match LogChain::hex_decode(&step.hash) {
364            Some(v) => v,
365            None => return false,
366        };
367        match step.side.as_str() {
368            "left" => current = LogChain::hash_chain_node(&sibling, &current),
369            "right" => current = LogChain::hash_chain_node(&current, &sibling),
370            _ => return false,
371        }
372    }
373
374    LogChain::hex_encode(&current) == expected_root_hex
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380    use crate::log_entry::EventType;
381
382    #[test]
383    fn test_genesis_hash() {
384        assert_eq!(GENESIS_HASH.len(), 64);
385    }
386
387    #[tokio::test]
388    async fn test_append_entry() {
389        let mut chain = LogChain::new();
390        let entry = LogEntry::new(
391            EventType::AccountQuery,
392            "AGENT_001".to_string(),
393            "DGFiP".to_string(),
394        )
395        .unwrap();
396
397        let result = chain.append(entry).await;
398        assert!(result.is_ok());
399        assert!(chain.verify());
400    }
401
402    #[tokio::test]
403    async fn test_chain_proof_detects_tampering() {
404        let mut chain = LogChain::new();
405        let e1 = chain
406            .append(
407                LogEntry::new(
408                    EventType::AuthSuccess,
409                    "AGENT_001".to_string(),
410                    "DGFiP".to_string(),
411                )
412                .unwrap(),
413            )
414            .await
415            .unwrap();
416        let _e2 = chain
417            .append(
418                LogEntry::new(
419                    EventType::DataAccess,
420                    "AGENT_002".to_string(),
421                    "DGFiP".to_string(),
422                )
423                .unwrap(),
424            )
425            .await
426            .unwrap();
427
428        let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
429        assert!(verify_chain_proof(&proof));
430
431        proof.target_entry_hash = "0".repeat(64);
432        assert!(!verify_chain_proof(&proof));
433    }
434
435    #[tokio::test]
436    async fn test_chain_proof_is_compact_logarithmic() {
437        let mut chain = LogChain::new();
438        let mut target_id = String::new();
439        let n = 64usize;
440
441        for i in 0..n {
442            let entry = chain
443                .append(
444                    LogEntry::new(
445                        EventType::DataAccess,
446                        format!("AGENT_{i:03}"),
447                        "DGFiP".to_string(),
448                    )
449                    .unwrap(),
450                )
451                .await
452                .unwrap();
453            if i == 17 {
454                target_id = entry.entry_id().to_string();
455            }
456        }
457
458        let proof = chain.generate_proof(&target_id).unwrap();
459        assert!(verify_chain_proof(&proof));
460
461        let max_depth = (usize::BITS - (n - 1).leading_zeros()) as usize;
462        assert!(proof.target_membership_proof.len() <= max_depth);
463        assert!(proof.head_membership_proof.len() <= max_depth);
464    }
465
466    #[tokio::test]
467    async fn test_chain_proof_detects_path_tampering() {
468        let mut chain = LogChain::new();
469        let e1 = chain
470            .append(
471                LogEntry::new(
472                    EventType::AuthSuccess,
473                    "AGENT_001".to_string(),
474                    "DGFiP".to_string(),
475                )
476                .unwrap(),
477            )
478            .await
479            .unwrap();
480        let _e2 = chain
481            .append(
482                LogEntry::new(
483                    EventType::DataAccess,
484                    "AGENT_002".to_string(),
485                    "DGFiP".to_string(),
486                )
487                .unwrap(),
488            )
489            .await
490            .unwrap();
491
492        let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
493        assert!(verify_chain_proof(&proof));
494
495        if let Some(first) = proof.target_membership_proof.first_mut() {
496            first.hash = "f".repeat(64);
497        } else {
498            proof.chain_root_hash = "e".repeat(64);
499        }
500        assert!(!verify_chain_proof(&proof));
501    }
502}