Skip to main content

lsp_max/runtime/control_plane/
receipts.rs

1use 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    /// Returns the metadata attributes formatted for OpenTelemetry / tracing.
81    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    /// Exports the receipt as an OCEL 2.0 event JSON object representation.
92    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    /// Exports the receipt as an OCEL 2.0 object JSON object representation.
110    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
122// ─────────────────────────────────────────────────────────────────────────────
123// Moniker ↔ OCEL join (cross-product #1: provenance-carrying code index)
124//
125// The bridge that makes "go to definition" (an LSIF moniker vertex) and "show
126// the receipt chain that produced this symbol" (OCEL events relating to an
127// object) the same identity. The join key is the moniker's CONTENT identity
128// `(scheme, identifier)` — NOT the LSIF numeric vertex id, which is
129// allocation-order dependent and shifts under unrelated edits. Keying on the
130// numeric id would pass a re-run determinism check yet silently break the first
131// time any source file changed; the content address is stable under unrelated
132// edits (witnessed in `moniker_join` tests below).
133// ─────────────────────────────────────────────────────────────────────────────
134
135use lsp_max_lsif::lsif_types::{MonikerKind, UniquenessLevel};
136
137/// The single authoritative OCEL `objectId` for a code symbol, derived from its
138/// moniker content identity. Both the LSIF moniker vertex and the OCEL
139/// `CodeSymbol` object resolve to this string — it is the join key. Defining it
140/// in exactly one place keeps a competing authority from minting a second,
141/// divergent id format for the same symbol.
142pub fn moniker_object_id(scheme: &str, identifier: &str) -> String {
143    format!("moniker:{scheme}:{identifier}")
144}
145
146/// Export a code symbol (identified by its moniker) as an OCEL 2.0 object.
147/// Its `id` is the moniker join key, so any receipt event that produced this
148/// symbol can reference it by the same identity an LSIF consumer would resolve.
149pub 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    /// Export the receipt as an OCEL 2.0 event that additionally relates to the
169    /// code symbol it produced, by the moniker join key. This is the load-bearing
170    /// half of cross-product #1: the receipt's operation-event and the LSIF
171    /// moniker vertex now share one OCEL object id, so navigation and provenance
172    /// are a single graph traversal.
173    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
194/// Helper function to format bytes to hex string.
195pub 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/// Iteratively verifies an array slice of cryptographic receipts.
223#[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        // 1. Verify chronological progression of execution sequence
238        if receipt.sequence != expected_sequence {
239            return Err(ChainValidationError::SequenceMismatch {
240                index,
241                expected: expected_sequence,
242                found: receipt.sequence,
243            });
244        }
245
246        // 2. Verify link integrity to previous receipt
247        if receipt.prev_hash != expected_prev_hash {
248            return Err(ChainValidationError::HashMismatch { index });
249        }
250
251        // 3. Compute and verify the payload digest
252        let payload_hash = receipt.compute_payload_hash();
253
254        // 4. Verify Ed25519 signature of the payload digest
255        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        // Prepare context for the next block evaluation
261        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
280/// A robust Key Management Keystore for Ed25519 signing and verification.
281pub struct Keystore {
282    primary_key: SigningKey,
283    trusted_keys: RwLock<HashMap<Uuid, VerifyingKey>>,
284}
285
286impl Keystore {
287    /// Generate a fresh random key pair.
288    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    /// Create from a raw seed bytes array.
299    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    /// Create from a seed slice.
308    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    /// Load primary key from file containing raw 32 seed bytes.
321    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    /// Save primary key seed to file.
327    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    /// Returns the raw 32 seed bytes of the primary key.
336    pub fn to_bytes(&self) -> [u8; 32] {
337        self.primary_key.to_bytes()
338    }
339
340    /// Returns the VerifyingKey of the primary key pair.
341    pub fn verifying_key(&self) -> VerifyingKey {
342        self.primary_key.verifying_key()
343    }
344
345    /// Sign a payload hash using the primary key.
346    pub fn sign_hash(&self, hash: &Blake3Hash) -> [u8; 64] {
347        self.primary_key.sign(hash.as_ref()).to_bytes()
348    }
349
350    /// Sign a CryptographicReceipt in place using the primary key.
351    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    /// Register a trusted verifying public key associated with a Uuid.
357    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    /// Retrieve a trusted verifying public key.
363    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    /// Verify a signature against a payload hash.
369    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    /// Verify a CryptographicReceipt's signature using the verifying key registered
380    /// for its discipline_id, or fallback to the primary verifying key if no specific key is registered.
381    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
390/// A ReplayEngine that implements Section 4 (Deterministic Replay Protocol) of the ARD.
391pub 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    /// Replays a sequence of transition inputs and asserts they match the receipt chain.
405    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        // 1. First, verify the chain cryptographically
414        verify_receipt_chain(chain, &self.verifying_key, &self.expected_genesis_hash)?;
415
416        // 2. Perform isolation of state and execute deterministic transition conformance checks
417        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;