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