Skip to main content

lsp_max/runtime/
ledger.rs

1use crate::runtime::mesh::AutonomicMesh;
2use crate::runtime::mesh_types::LspPhase;
3use crate::runtime::sha256::sha256;
4
5impl AutonomicMesh {
6    pub fn verify_instance_ledger(&self, instance_id: &str) -> Result<(), String> {
7        let instance = self
8            .instances
9            .get(instance_id)
10            .ok_or_else(|| format!("Instance {} not found", instance_id))?;
11
12        let history = &instance.receipts;
13        if history.is_empty() {
14            return Err("Ledger is empty".to_string());
15        }
16
17        if instance_id == "LSP_1" {
18            let r0 = &history[0];
19            if r0.receipt_id != "rcpt-uninitialized" {
20                return Err(format!("Invalid initial receipt ID: {}", r0.receipt_id));
21            }
22            let mut expected_hash = sha256(r0.receipt_id.as_bytes());
23            if r0.hash != expected_hash {
24                return Err(format!(
25                    "Hash mismatch at index 0: expected {}, got {}",
26                    expected_hash, r0.hash
27                ));
28            }
29
30            if history.len() > 1 {
31                let r1 = &history[1];
32                if !r1
33                    .receipt_id
34                    .starts_with("rcpt-uninitialized-to-initializing:")
35                {
36                    return Err(format!("Invalid receipt ID at index 1: {}", r1.receipt_id));
37                }
38                let prefix_len = "rcpt-uninitialized-to-initializing:".len();
39                let json_str = &r1.receipt_id[prefix_len..];
40                if serde_json::from_str::<serde_json::Value>(json_str).is_err() {
41                    return Err("Failed to parse client capabilities in receipt 1".to_string());
42                }
43
44                expected_hash = sha256(format!("{}:{}", expected_hash, r1.receipt_id).as_bytes());
45                if r1.hash != expected_hash {
46                    return Err(format!(
47                        "Hash mismatch at index 1: expected {}, got {}",
48                        expected_hash, r1.hash
49                    ));
50                }
51            }
52
53            if history.len() > 2 {
54                let r2 = &history[2];
55                if !r2
56                    .receipt_id
57                    .starts_with("rcpt-initializing-to-initialized:")
58                {
59                    return Err(format!("Invalid receipt ID at index 2: {}", r2.receipt_id));
60                }
61                let prefix_len = "rcpt-initializing-to-initialized:".len();
62                let json_str = &r2.receipt_id[prefix_len..];
63                if serde_json::from_str::<serde_json::Value>(json_str).is_err() {
64                    return Err("Failed to parse server capabilities in receipt 2".to_string());
65                }
66
67                expected_hash = sha256(format!("{}:{}", expected_hash, r2.receipt_id).as_bytes());
68                if r2.hash != expected_hash {
69                    return Err(format!(
70                        "Hash mismatch at index 2: expected {}, got {}",
71                        expected_hash, r2.hash
72                    ));
73                }
74            }
75
76            if history.len() > 3 {
77                let r3 = &history[3];
78                if r3.receipt_id != "rcpt-initialized-to-shutdown" {
79                    return Err(format!("Invalid receipt ID at index 3: {}", r3.receipt_id));
80                }
81                expected_hash = sha256(format!("{}:{}", expected_hash, r3.receipt_id).as_bytes());
82                if r3.hash != expected_hash {
83                    return Err(format!(
84                        "Hash mismatch at index 3: expected {}, got {}",
85                        expected_hash, r3.hash
86                    ));
87                }
88            }
89
90            if history.len() > 4 {
91                let r4 = &history[4];
92                if r4.receipt_id != "rcpt-shutdown-to-exited" {
93                    return Err(format!("Invalid receipt ID at index 4: {}", r4.receipt_id));
94                }
95                expected_hash = sha256(format!("{}:{}", expected_hash, r4.receipt_id).as_bytes());
96                if r4.hash != expected_hash {
97                    return Err(format!(
98                        "Hash mismatch at index 4: expected {}, got {}",
99                        expected_hash, r4.hash
100                    ));
101                }
102            }
103
104            if history.len() > 5 {
105                return Err("Ledger contains unexpected items beyond Exited state".to_string());
106            }
107
108            let expected_phase = match history.len() {
109                1 => LspPhase::Uninitialized,
110                2 => LspPhase::Initializing,
111                3 => LspPhase::Initialized,
112                4 => LspPhase::ShutDown,
113                5 => LspPhase::Exited,
114                _ => unreachable!(),
115            };
116
117            if instance.phase != expected_phase {
118                return Err(format!(
119                    "Phase mismatch: instance.phase is '{}' but ledger shows '{}'",
120                    instance.phase, expected_phase
121                ));
122            }
123        } else {
124            for (idx, r) in history.iter().enumerate() {
125                if r.receipt_id.is_empty() {
126                    return Err(format!("Empty receipt ID at index {}", idx));
127                }
128                if r.hash.is_empty() {
129                    return Err(format!("Empty receipt hash at index {}", idx));
130                }
131            }
132        }
133
134        Ok(())
135    }
136
137    pub fn get_ledger_diagnostic_report(&self, instance_id: &str) -> String {
138        let mut report = format!("Ledger Diagnostic Report for Instance: {}\n", instance_id);
139        match self.verify_instance_ledger(instance_id) {
140            Ok(()) => {
141                report.push_str("Status: ADMITTED (Cryptographic integrity confirmed)\n");
142            }
143            Err(e) => {
144                report.push_str(&format!(
145                    "Status: REFUSED (Ledger verification refused: {})\n",
146                    e
147                ));
148            }
149        }
150
151        if let Some(instance) = self.instances.get(instance_id) {
152            report.push_str(&format!("Active Phase: {}\n", instance.phase));
153            report.push_str(&format!("Policy State: {:?}\n", instance.policy_state));
154            report.push_str(&format!("Receipts count: {}\n", instance.receipts.len()));
155            for (idx, r) in instance.receipts.iter().enumerate() {
156                report.push_str(&format!(
157                    "  [{}] ID: {} | Hash: {}\n",
158                    idx, r.receipt_id, r.hash
159                ));
160            }
161        } else {
162            report.push_str("Instance not found in mesh registry.\n");
163        }
164        report
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::runtime::mesh_types::{LspInstance, Receipt};
172
173    fn make_mesh_with_instance(instance: LspInstance) -> AutonomicMesh {
174        let mut mesh = AutonomicMesh::new();
175        mesh.add_instance(instance);
176        mesh
177    }
178
179    // ---- verify_instance_ledger: unknown instance ----
180
181    #[test]
182    fn ledger_unknown_instance_is_err() {
183        let mesh = AutonomicMesh::new();
184        let result = mesh.verify_instance_ledger("NONEXISTENT");
185        assert!(result.is_err());
186        assert!(result.unwrap_err().contains("not found"));
187    }
188
189    // ---- verify_instance_ledger: empty receipt ledger ----
190
191    #[test]
192    fn ledger_empty_receipts_is_err() {
193        let instance = LspInstance::new("ANY_ID");
194        // No receipts attached — ledger is empty.
195        let mesh = make_mesh_with_instance(instance);
196        let result = mesh.verify_instance_ledger("ANY_ID");
197        assert!(result.is_err());
198        assert!(result.unwrap_err().contains("empty"));
199    }
200
201    // ---- verify_instance_ledger: non-LSP_1 path (generic id check) ----
202
203    #[test]
204    fn ledger_generic_instance_valid_receipt() {
205        // For instances other than LSP_1 the verifier only checks that
206        // receipt_id and hash are non-empty.
207        let mut instance = LspInstance::new("LSP_2");
208        instance.receipts.push(Receipt {
209            receipt_id: "rcpt-some-event".to_string(),
210            hash: "deadbeef".to_string(),
211            prev_receipt_hash: None,
212        });
213        let mesh = make_mesh_with_instance(instance);
214        let result = mesh.verify_instance_ledger("LSP_2");
215        assert!(
216            result.is_ok(),
217            "non-empty receipt on generic instance must pass"
218        );
219    }
220
221    #[test]
222    fn ledger_generic_instance_empty_receipt_id_is_err() {
223        let mut instance = LspInstance::new("LSP_3");
224        instance.receipts.push(Receipt {
225            receipt_id: String::new(), // empty — violates the check
226            hash: "deadbeef".to_string(),
227            prev_receipt_hash: None,
228        });
229        let mesh = make_mesh_with_instance(instance);
230        let result = mesh.verify_instance_ledger("LSP_3");
231        assert!(result.is_err(), "empty receipt_id must return Err");
232        assert!(result.unwrap_err().contains("Empty receipt ID"));
233    }
234
235    #[test]
236    fn ledger_generic_instance_empty_hash_is_err() {
237        let mut instance = LspInstance::new("LSP_4");
238        instance.receipts.push(Receipt {
239            receipt_id: "rcpt-something".to_string(),
240            hash: String::new(), // empty — violates the check
241            prev_receipt_hash: None,
242        });
243        let mesh = make_mesh_with_instance(instance);
244        let result = mesh.verify_instance_ledger("LSP_4");
245        assert!(result.is_err(), "empty receipt hash must return Err");
246        assert!(result.unwrap_err().contains("Empty receipt hash"));
247    }
248
249    // ---- verify_instance_ledger: LSP_1 path — genesis receipt ----
250
251    #[test]
252    fn ledger_lsp1_valid_genesis() {
253        let mut instance = LspInstance::new("LSP_1");
254        instance.phase = LspPhase::Uninitialized;
255        let h0 = sha256(b"rcpt-uninitialized");
256        instance.receipts.push(Receipt {
257            receipt_id: "rcpt-uninitialized".to_string(),
258            hash: h0,
259            prev_receipt_hash: None,
260        });
261        let mesh = make_mesh_with_instance(instance);
262        let result = mesh.verify_instance_ledger("LSP_1");
263        assert!(result.is_ok(), "valid genesis receipt for LSP_1 must pass");
264    }
265
266    #[test]
267    fn ledger_lsp1_wrong_genesis_id_is_err() {
268        let mut instance = LspInstance::new("LSP_1");
269        instance.phase = LspPhase::Uninitialized;
270        instance.receipts.push(Receipt {
271            receipt_id: "rcpt-wrong".to_string(),
272            hash: sha256(b"rcpt-wrong"),
273            prev_receipt_hash: None,
274        });
275        let mesh = make_mesh_with_instance(instance);
276        let result = mesh.verify_instance_ledger("LSP_1");
277        assert!(result.is_err(), "wrong genesis receipt_id must return Err");
278    }
279
280    #[test]
281    fn ledger_lsp1_tampered_genesis_hash_is_err() {
282        let mut instance = LspInstance::new("LSP_1");
283        instance.phase = LspPhase::Uninitialized;
284        instance.receipts.push(Receipt {
285            receipt_id: "rcpt-uninitialized".to_string(),
286            hash: "0000000000000000000000000000000000000000000000000000000000000000".to_string(),
287            prev_receipt_hash: None,
288        });
289        let mesh = make_mesh_with_instance(instance);
290        let result = mesh.verify_instance_ledger("LSP_1");
291        assert!(result.is_err(), "tampered genesis hash must return Err");
292    }
293}