1#![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 pub rules_applied: Vec<String>,
27 pub preimage_hashes: Vec<[u8; 32]>,
29 #[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
44pub 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
64fn 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
82pub fn verify_proof(bundle: &ProofBundle) -> Result<(), ProofError> {
88 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")]
96pub 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}