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};
7
8/// Genesis hash (initial chain hash)
9pub const GENESIS_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
10
11/// Chain proof for cryptographic verification.
12#[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/// Single step in chain proof.
22#[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
34/// Log chain state
35pub struct LogChain {
36    entries: Vec<LogEntry>,
37    current_hash: String,
38    entry_index: std::collections::HashMap<String, usize>,
39}
40
41impl LogChain {
42    /// Create new chain with genesis block
43    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    /// Append entry to chain
52    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    /// Verify chain integrity
65    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    /// Generate proof for entry (includes all steps up to current head).
92    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    /// Get entry count
119    pub fn len(&self) -> usize {
120        self.entries.len()
121    }
122
123    /// Check if empty
124    pub fn is_empty(&self) -> bool {
125        self.entries.is_empty()
126    }
127
128    /// Get current hash
129    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
140/// Verify chain proof by recomputing the hash chain and optional Merkle proof.
141pub 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
202        let result = chain.append(entry).await;
203        assert!(result.is_ok());
204        assert!(chain.verify());
205    }
206
207    #[tokio::test]
208    async fn test_chain_proof_detects_tampering() {
209        let mut chain = LogChain::new();
210        let e1 = chain
211            .append(LogEntry::new(
212                EventType::AuthSuccess,
213                "AGENT_001".to_string(),
214                "DGFiP".to_string(),
215            ))
216            .await
217            .unwrap();
218        let _e2 = chain
219            .append(LogEntry::new(
220                EventType::DataAccess,
221                "AGENT_002".to_string(),
222                "DGFiP".to_string(),
223            ))
224            .await
225            .unwrap();
226
227        let mut proof = chain.generate_proof(e1.entry_id()).unwrap();
228        assert!(verify_chain_proof(&proof));
229
230        proof.steps[0].entry_hash = "0".repeat(64);
231        assert!(!verify_chain_proof(&proof));
232    }
233}