tdln_proof/
lib.rs

1//! TDLN Proof Bundle — captures deterministic proof of translation.
2//!
3//! `ProofBundle` references canonical content by CID and may carry signatures
4//! (plain Ed25519 and/or DV25 Seal via `logline-core` feature).
5
6#![forbid(unsafe_code)]
7
8use blake3::Hasher;
9use serde::{Deserialize, Serialize};
10use tdln_ast::SemanticUnit;
11use thiserror::Error;
12
13#[cfg(feature = "ed25519")]
14use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
15#[cfg(feature = "ed25519")]
16use std::convert::TryInto;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct ProofBundle {
20    pub ast_cid: [u8; 32],
21    pub canon_cid: [u8; 32],
22    /// Rule ids applied deterministically by the compiler.
23    pub rules_applied: Vec<String>,
24    /// Hashes of relevant inputs (pre-images) to lock determinism.
25    pub preimage_hashes: Vec<[u8; 32]>,
26    /// Optional signatures over the bundle digest.
27    #[cfg(feature = "ed25519")]
28    pub signatures: Vec<Vec<u8>>,
29}
30
31#[derive(Debug, Error)]
32pub enum ProofError {
33    #[error("invalid bundle shape")]
34    Invalid,
35    #[error("signature missing")]
36    NoSignature,
37    #[error("signature verify failed")]
38    VerifyFailed,
39}
40
41/// Build a proof bundle from AST + canonical bytes + rule ids.
42pub fn build_proof(
43    ast: &SemanticUnit,
44    canon_json: &[u8],
45    rules: &[impl AsRef<str>],
46) -> ProofBundle {
47    let ast_cid = ast.cid_blake3();
48    let mut h = Hasher::new();
49    h.update(canon_json);
50    let canon_cid = h.finalize().into();
51    ProofBundle {
52        ast_cid,
53        canon_cid,
54        rules_applied: rules.iter().map(|r| r.as_ref().to_string()).collect(),
55        preimage_hashes: vec![],
56        #[cfg(feature = "ed25519")]
57        signatures: vec![],
58    }
59}
60
61/// Digest that is signed/verified (ast_cid || canon_cid || rules_applied as bytes).
62fn bundle_digest(bundle: &ProofBundle) -> [u8; 32] {
63    let mut h = Hasher::new();
64    h.update(&bundle.ast_cid);
65    h.update(&bundle.canon_cid);
66    for r in &bundle.rules_applied {
67        h.update(r.as_bytes());
68    }
69    h.finalize().into()
70}
71
72#[cfg(feature = "ed25519")]
73pub fn sign(bundle: &mut ProofBundle, sk: &SigningKey) {
74    let msg = bundle_digest(bundle);
75    let sig = sk.sign(&msg);
76    bundle.signatures.push(sig.to_bytes().to_vec());
77}
78
79/// Verifies determinism & integrity relationships within the bundle (shape-level).
80pub fn verify_proof(bundle: &ProofBundle) -> Result<(), ProofError> {
81    // Minimal sanity: CIDs are non-zero, rules list stable
82    if bundle.ast_cid == [0; 32] || bundle.canon_cid == [0; 32] {
83        return Err(ProofError::Invalid);
84    }
85    Ok(())
86}
87
88#[cfg(feature = "ed25519")]
89pub fn verify_signatures(bundle: &ProofBundle, keys: &[VerifyingKey]) -> Result<(), ProofError> {
90    if bundle.signatures.is_empty() {
91        return Err(ProofError::NoSignature);
92    }
93    let msg = bundle_digest(bundle);
94    for (sig_bytes, vk) in bundle.signatures.iter().zip(keys.iter().cycle()) {
95        let sig_array: [u8; 64] = sig_bytes
96            .as_slice()
97            .try_into()
98            .map_err(|_| ProofError::VerifyFailed)?;
99        let sig = Signature::from_bytes(&sig_array);
100        vk.verify(&msg, &sig)
101            .map_err(|_| ProofError::VerifyFailed)?;
102    }
103    Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    #[cfg(feature = "ed25519")]
110    use ed25519_dalek::{SigningKey, VerifyingKey};
111    #[test]
112    fn shape_ok() {
113        let ast = SemanticUnit::from_intent("book a table for two");
114        let canon = ast.canonical_bytes();
115        let pb = build_proof(&ast, &canon, &["normalize", "slots"]);
116        assert!(verify_proof(&pb).is_ok());
117    }
118
119    #[cfg(feature = "ed25519")]
120    #[test]
121    fn sign_and_verify() {
122        let ast = SemanticUnit::from_intent("set timer 5 minutes");
123        let canon = ast.canonical_bytes();
124        let mut pb = build_proof(&ast, &canon, &["normalize"]);
125        let sk = SigningKey::from_bytes(&[7u8; 32]);
126        let vk: VerifyingKey = (&sk).into();
127        sign(&mut pb, &sk);
128        assert!(verify_signatures(&pb, &[vk]).is_ok());
129    }
130}