lsp_max/runtime/control_plane/
receipts.rs1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::sync::RwLock;
5use uuid::Uuid;
6
7#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
8pub struct Blake3Hash(pub [u8; 32]);
9
10impl AsRef<[u8; 32]> for Blake3Hash {
11 fn as_ref(&self) -> &[u8; 32] {
12 &self.0
13 }
14}
15
16impl AsRef<[u8]> for Blake3Hash {
17 fn as_ref(&self) -> &[u8] {
18 &self.0
19 }
20}
21
22impl From<[u8; 32]> for Blake3Hash {
23 fn from(bytes: [u8; 32]) -> Self {
24 Self(bytes)
25 }
26}
27
28impl From<blake3::Hash> for Blake3Hash {
29 fn from(hash: blake3::Hash) -> Self {
30 Self(*hash.as_bytes())
31 }
32}
33
34mod signature_serde {
35 use serde::{Deserializer, Serializer};
36 pub fn serialize<S>(bytes: &[u8; 64], serializer: S) -> Result<S::Ok, S::Error>
37 where
38 S: Serializer,
39 {
40 serializer.collect_seq(bytes.iter())
41 }
42
43 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 64], D::Error>
44 where
45 D: Deserializer<'de>,
46 {
47 let v: Vec<u8> = serde::Deserialize::deserialize(deserializer)?;
48 if v.len() == 64 {
49 let mut array = [0u8; 64];
50 array.copy_from_slice(&v);
51 Ok(array)
52 } else {
53 Err(serde::de::Error::custom("expected an array of length 64"))
54 }
55 }
56}
57
58#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
59pub struct CryptographicReceipt {
60 pub prev_hash: Blake3Hash,
61 pub discipline_id: Uuid,
62 pub law_id: Uuid,
63 pub consequence_hash: Blake3Hash,
64 pub sequence: u64,
65 #[serde(with = "signature_serde")]
66 pub signature: [u8; 64],
67}
68
69impl CryptographicReceipt {
70 pub fn compute_payload_hash(&self) -> Blake3Hash {
71 let mut hasher = blake3::Hasher::new();
72 hasher.update(&self.prev_hash.0);
73 hasher.update(self.discipline_id.as_bytes());
74 hasher.update(self.law_id.as_bytes());
75 hasher.update(&self.consequence_hash.0);
76 hasher.update(&self.sequence.to_le_bytes());
77 Blake3Hash(*hasher.finalize().as_bytes())
78 }
79
80 pub fn trace_attributes(&self) -> Vec<(&'static str, String)> {
82 vec![
83 ("ostar.prev_hash", to_hex(&self.prev_hash.0)),
84 ("ostar.discipline_id", self.discipline_id.to_string()),
85 ("ostar.law_id", self.law_id.to_string()),
86 ("ostar.consequence_hash", to_hex(&self.consequence_hash.0)),
87 ("ostar.sequence", self.sequence.to_string()),
88 ]
89 }
90
91 pub fn to_ocel_event(&self, event_id: &str, timestamp: &str) -> serde_json::Value {
93 serde_json::json!({
94 "id": event_id,
95 "type": "TransitionExecution",
96 "time": timestamp,
97 "attributes": {
98 "sequence": self.sequence,
99 "consequence_hash": to_hex(&self.consequence_hash.0)
100 },
101 "relationships": [
102 { "objectId": format!("obj_discipline_{}", self.discipline_id), "qualifier": "discipline" },
103 { "objectId": format!("obj_law_{}", self.law_id), "qualifier": "governing_law" },
104 { "objectId": format!("receipt_{}", self.sequence), "qualifier": "attestation" }
105 ]
106 })
107 }
108
109 pub fn to_ocel_object(&self) -> serde_json::Value {
111 serde_json::json!({
112 "id": format!("receipt_{}", self.sequence),
113 "type": "Receipt",
114 "attributes": {
115 "prev_hash": to_hex(&self.prev_hash.0),
116 "signature": to_hex(&self.signature)
117 }
118 })
119 }
120}
121
122use lsp_max_lsif::lsif_types::{MonikerKind, UniquenessLevel};
136
137pub fn moniker_object_id(scheme: &str, identifier: &str) -> String {
143 format!("moniker:{scheme}:{identifier}")
144}
145
146pub fn moniker_to_ocel_object(
150 scheme: &str,
151 identifier: &str,
152 kind: &MonikerKind,
153 unique: &UniquenessLevel,
154) -> serde_json::Value {
155 serde_json::json!({
156 "id": moniker_object_id(scheme, identifier),
157 "type": "CodeSymbol",
158 "attributes": {
159 "scheme": scheme,
160 "identifier": identifier,
161 "kind": kind,
162 "unique": unique,
163 }
164 })
165}
166
167impl CryptographicReceipt {
168 pub fn to_ocel_event_for_symbol(
174 &self,
175 event_id: &str,
176 timestamp: &str,
177 scheme: &str,
178 identifier: &str,
179 ) -> serde_json::Value {
180 let mut event = self.to_ocel_event(event_id, timestamp);
181 if let Some(rels) = event
182 .get_mut("relationships")
183 .and_then(|r| r.as_array_mut())
184 {
185 rels.push(serde_json::json!({
186 "objectId": moniker_object_id(scheme, identifier),
187 "qualifier": "produced_symbol"
188 }));
189 }
190 event
191 }
192}
193
194pub fn to_hex(bytes: &[u8]) -> String {
196 let mut s = String::with_capacity(bytes.len() * 2);
197 for &b in bytes {
198 use std::fmt::Write;
199 write!(&mut s, "{:02x}", b).unwrap();
200 }
201 s
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
205pub enum ChainValidationError {
206 #[error("Chain is empty")]
207 EmptyChain,
208 #[error("Sequence mismatch at index {index}: expected {expected}, found {found}")]
209 SequenceMismatch {
210 index: usize,
211 expected: u64,
212 found: u64,
213 },
214 #[error("Hash mismatch at index {index}")]
215 HashMismatch { index: usize },
216 #[error("Signature verification failed at index {index}")]
217 SignatureVerificationFailed { index: usize },
218 #[error("Genesis link broken")]
219 GenesisLinkBroken,
220}
221
222#[allow(clippy::explicit_counter_loop)]
224pub fn verify_receipt_chain(
225 chain: &[CryptographicReceipt],
226 verifying_key: &VerifyingKey,
227 expected_genesis_hash: &Blake3Hash,
228) -> Result<(), ChainValidationError> {
229 if chain.is_empty() {
230 return Err(ChainValidationError::EmptyChain);
231 }
232
233 let mut expected_prev_hash = *expected_genesis_hash;
234 let mut expected_sequence = chain[0].sequence;
235
236 for (index, receipt) in chain.iter().enumerate() {
237 if receipt.sequence != expected_sequence {
239 return Err(ChainValidationError::SequenceMismatch {
240 index,
241 expected: expected_sequence,
242 found: receipt.sequence,
243 });
244 }
245
246 if receipt.prev_hash != expected_prev_hash {
248 return Err(ChainValidationError::HashMismatch { index });
249 }
250
251 let payload_hash = receipt.compute_payload_hash();
253
254 let sig = Signature::from_bytes(&receipt.signature);
256 if verifying_key.verify(&payload_hash.0, &sig).is_err() {
257 return Err(ChainValidationError::SignatureVerificationFailed { index });
258 }
259
260 expected_prev_hash = payload_hash;
262 expected_sequence += 1;
263 }
264
265 Ok(())
266}
267
268#[derive(Debug, thiserror::Error)]
269pub enum KeyManagementError {
270 #[error("I/O error: {0}")]
271 Io(#[from] std::io::Error),
272 #[error("Key parse error: {0}")]
273 KeyParse(String),
274 #[error("Signature error: {0}")]
275 Signature(#[from] ed25519_dalek::SignatureError),
276 #[error("Key not found: {0}")]
277 KeyNotFound(Uuid),
278}
279
280pub struct Keystore {
282 primary_key: SigningKey,
283 trusted_keys: RwLock<HashMap<Uuid, VerifyingKey>>,
284}
285
286impl Keystore {
287 pub fn generate() -> Self {
289 use rand_core::OsRng;
290 let mut csprng = OsRng;
291 let primary_key = SigningKey::generate(&mut csprng);
292 Self {
293 primary_key,
294 trusted_keys: RwLock::new(HashMap::new()),
295 }
296 }
297
298 pub fn from_seed(seed: &[u8; 32]) -> Self {
300 let primary_key = SigningKey::from_bytes(seed);
301 Self {
302 primary_key,
303 trusted_keys: RwLock::new(HashMap::new()),
304 }
305 }
306
307 pub fn from_bytes(bytes: &[u8]) -> Result<Self, KeyManagementError> {
309 if bytes.len() != 32 {
310 return Err(KeyManagementError::KeyParse(format!(
311 "invalid seed length: expected 32 bytes, got {}",
312 bytes.len()
313 )));
314 }
315 let mut seed = [0u8; 32];
316 seed.copy_from_slice(bytes);
317 Ok(Self::from_seed(&seed))
318 }
319
320 pub fn load_from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, KeyManagementError> {
322 let bytes = std::fs::read(path)?;
323 Self::from_bytes(&bytes)
324 }
325
326 pub fn save_to_file<P: AsRef<std::path::Path>>(
328 &self,
329 path: P,
330 ) -> Result<(), KeyManagementError> {
331 std::fs::write(path, self.primary_key.to_bytes())?;
332 Ok(())
333 }
334
335 pub fn to_bytes(&self) -> [u8; 32] {
337 self.primary_key.to_bytes()
338 }
339
340 pub fn verifying_key(&self) -> VerifyingKey {
342 self.primary_key.verifying_key()
343 }
344
345 pub fn sign_hash(&self, hash: &Blake3Hash) -> [u8; 64] {
347 self.primary_key.sign(hash.as_ref()).to_bytes()
348 }
349
350 pub fn sign_receipt(&self, receipt: &mut CryptographicReceipt) {
352 let hash = receipt.compute_payload_hash();
353 receipt.signature = self.sign_hash(&hash);
354 }
355
356 pub fn register_trusted_key(&self, id: Uuid, key: VerifyingKey) {
358 let mut keys = self.trusted_keys.write().unwrap();
359 keys.insert(id, key);
360 }
361
362 pub fn get_trusted_key(&self, id: &Uuid) -> Option<VerifyingKey> {
364 let keys = self.trusted_keys.read().unwrap();
365 keys.get(id).copied()
366 }
367
368 pub fn verify_signature(
370 verifying_key: &VerifyingKey,
371 hash: &Blake3Hash,
372 signature_bytes: &[u8; 64],
373 ) -> Result<(), KeyManagementError> {
374 let signature = Signature::from_bytes(signature_bytes);
375 verifying_key.verify(hash.as_ref(), &signature)?;
376 Ok(())
377 }
378
379 pub fn verify_receipt(&self, receipt: &CryptographicReceipt) -> Result<(), KeyManagementError> {
382 let payload_hash = receipt.compute_payload_hash();
383 let verifying_key = self
384 .get_trusted_key(&receipt.discipline_id)
385 .unwrap_or_else(|| self.verifying_key());
386 Self::verify_signature(&verifying_key, &payload_hash, &receipt.signature)
387 }
388}
389
390pub struct ReplayEngine {
392 expected_genesis_hash: Blake3Hash,
393 verifying_key: VerifyingKey,
394}
395
396impl ReplayEngine {
397 pub fn new(expected_genesis_hash: Blake3Hash, verifying_key: VerifyingKey) -> Self {
398 Self {
399 expected_genesis_hash,
400 verifying_key,
401 }
402 }
403
404 pub fn replay<F>(
406 &self,
407 chain: &[CryptographicReceipt],
408 mut transition_function: F,
409 ) -> Result<(), ChainValidationError>
410 where
411 F: FnMut(&CryptographicReceipt) -> Blake3Hash,
412 {
413 verify_receipt_chain(chain, &self.verifying_key, &self.expected_genesis_hash)?;
415
416 for (index, receipt) in chain.iter().enumerate() {
418 let computed_consequence = transition_function(receipt);
419 if computed_consequence.0 != receipt.consequence_hash.0 {
420 return Err(ChainValidationError::HashMismatch { index });
421 }
422 }
423
424 Ok(())
425 }
426}
427
428#[cfg(test)]
429mod tests;