1use blake3::Hasher;
2use serde::{Deserialize, Serialize};
3use subtle::ConstantTimeEq;
4
5use crate::chain::DyoloChain;
6use crate::error::A1Error;
7use crate::identity::Signer;
8use crate::intent::IntentHash;
9
10const DOMAIN_ZK_COMMIT: &str = "a1::dyolo::zk::commit::v2.8.0";
11const DOMAIN_ZK_BIND: &str = "a1::dyolo::zk::bind::v2.8.0";
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[repr(u8)]
30pub enum ZkProofMode {
31 Blake3Commit = 1,
32 ExternalZkvm = 2,
33}
34
35impl ZkProofMode {
36 pub fn as_u8(&self) -> u8 {
37 match self {
38 Self::Blake3Commit => 1,
39 Self::ExternalZkvm => 2,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ZkChainCommitment {
73 pub commitment: [u8; 32],
75
76 pub intent: IntentHash,
78
79 pub sealed_at_unix: u64,
81
82 pub chain_fingerprint_hex: String,
84
85 pub authority_signature: String,
87
88 pub authority_did: String,
90
91 pub mode: ZkProofMode,
93
94 #[serde(default, skip_serializing_if = "String::is_empty")]
96 pub zk_proof_hex: String,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub passport_namespace: Option<String>,
101}
102
103impl ZkChainCommitment {
104 pub fn seal(
110 chain: &DyoloChain,
111 intent: &IntentHash,
112 narrowing_commitment: &[u8; 32],
113 sealed_at_unix: u64,
114 authority: &dyn Signer,
115 passport_namespace: Option<&str>,
116 ) -> Self {
117 let chain_fp = chain.fingerprint();
118 let commitment =
119 compute_commitment(&chain_fp, intent, narrowing_commitment, sealed_at_unix);
120 let sig = authority.sign_message(&commitment);
121 let authority_did = format!(
122 "did:a1:{}",
123 hex::encode(authority.verifying_key().as_bytes())
124 );
125
126 Self {
127 commitment,
128 intent: *intent,
129 sealed_at_unix,
130 chain_fingerprint_hex: hex::encode(chain_fp),
131 authority_signature: hex::encode(sig.to_bytes()),
132 authority_did,
133 mode: ZkProofMode::Blake3Commit,
134 zk_proof_hex: String::new(),
135 passport_namespace: passport_namespace.map(String::from),
136 }
137 }
138
139 pub fn verify_commitment(
144 &self,
145 narrowing_commitment: &[u8; 32],
146 now_unix: u64,
147 max_age_secs: Option<u64>,
148 ) -> Result<(), A1Error> {
149 let chain_fp_bytes = hex::decode(&self.chain_fingerprint_hex)
150 .map_err(|_| A1Error::WireFormatError("invalid chain_fingerprint_hex".into()))?;
151 let chain_fp: [u8; 32] = chain_fp_bytes
152 .try_into()
153 .map_err(|_| A1Error::WireFormatError("chain fingerprint must be 32 bytes".into()))?;
154
155 let expected = compute_commitment(
156 &chain_fp,
157 &self.intent,
158 narrowing_commitment,
159 self.sealed_at_unix,
160 );
161
162 if expected[..].ct_eq(&self.commitment[..]).unwrap_u8() == 0 {
163 return Err(A1Error::InvalidSubScopeProof);
164 }
165
166 if let Some(max_age) = max_age_secs {
167 let age = now_unix.saturating_sub(self.sealed_at_unix);
168 if age > max_age {
169 return Err(A1Error::Expired(0, self.sealed_at_unix + max_age, now_unix));
170 }
171 }
172
173 let pk_hex = self
174 .authority_did
175 .strip_prefix("did:a1:")
176 .ok_or_else(|| A1Error::WireFormatError("invalid authority DID".into()))?;
177 let pk_bytes = hex::decode(pk_hex)
178 .map_err(|_| A1Error::WireFormatError("invalid authority DID hex".into()))?;
179 let pk_arr: [u8; 32] = pk_bytes
180 .try_into()
181 .map_err(|_| A1Error::WireFormatError("authority key must be 32 bytes".into()))?;
182 let authority_vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
183 .map_err(|_| A1Error::WireFormatError("invalid authority Ed25519 key".into()))?;
184
185 let sig_bytes = hex::decode(&self.authority_signature)
186 .map_err(|_| A1Error::WireFormatError("invalid authority_signature hex".into()))?;
187 let sig_arr: [u8; 64] = sig_bytes
188 .try_into()
189 .map_err(|_| A1Error::WireFormatError("signature must be 64 bytes".into()))?;
190 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
191
192 use ed25519_dalek::Verifier;
193 authority_vk
194 .verify(&self.commitment, &sig)
195 .map_err(|_| A1Error::HybridSignatureInvalid {
196 component: "zk-commitment",
197 })
198 }
199
200 pub fn with_zk_proof(mut self, proof_bytes: &[u8]) -> Self {
207 self.zk_proof_hex = hex::encode(proof_bytes);
208 self.mode = ZkProofMode::ExternalZkvm;
209 self
210 }
211
212 pub fn has_zk_proof(&self) -> bool {
214 self.mode == ZkProofMode::ExternalZkvm && !self.zk_proof_hex.is_empty()
215 }
216}
217
218fn compute_commitment(
219 chain_fp: &[u8; 32],
220 intent: &IntentHash,
221 narrowing_commitment: &[u8; 32],
222 sealed_at: u64,
223) -> [u8; 32] {
224 let mut h = Hasher::new_derive_key(DOMAIN_ZK_COMMIT);
225 h.update(chain_fp);
226 h.update(intent);
227 h.update(narrowing_commitment);
228 h.update(&sealed_at.to_le_bytes());
229 h.finalize().into()
230}
231
232pub fn anchor_hash(commitment: &ZkChainCommitment) -> [u8; 32] {
238 let mut h = Hasher::new_derive_key(DOMAIN_ZK_BIND);
239 h.update(&commitment.commitment);
240 h.update(&commitment.sealed_at_unix.to_le_bytes());
241 h.update(commitment.authority_did.as_bytes());
242 h.finalize().into()
243}
244
245#[derive(Debug, Clone)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub struct ZkTraceProof {
275 pub chain_commitment: ZkChainCommitment,
277 pub trace_root: crate::provenance::ProvenanceRoot,
279 #[cfg_attr(feature = "serde", serde(with = "crate::zk::hex_32_serde"))]
281 pub combined_commitment: [u8; 32],
282 pub authority_signature: String,
284 pub authority_did: String,
286 #[cfg_attr(
288 feature = "serde",
289 serde(default, skip_serializing_if = "String::is_empty")
290 )]
291 pub zk_proof_hex: String,
292}
293
294impl ZkTraceProof {
295 pub fn seal(
301 chain_commitment: ZkChainCommitment,
302 trace_root: crate::provenance::ProvenanceRoot,
303 authority: &dyn crate::identity::Signer,
304 ) -> Self {
305 let combined =
306 trace_combined_commitment(&chain_commitment.commitment, &trace_root.merkle_root);
307 let sig = authority.sign_message(&combined);
308 let authority_did = format!(
309 "did:a1:{}",
310 hex::encode(authority.verifying_key().as_bytes())
311 );
312 Self {
313 chain_commitment,
314 trace_root,
315 combined_commitment: combined,
316 authority_signature: hex::encode(sig.to_bytes()),
317 authority_did,
318 zk_proof_hex: String::new(),
319 }
320 }
321
322 pub fn verify(&self) -> Result<(), crate::error::A1Error> {
324 let expected = trace_combined_commitment(
325 &self.chain_commitment.commitment,
326 &self.trace_root.merkle_root,
327 );
328 use subtle::ConstantTimeEq;
329 if expected[..]
330 .ct_eq(&self.combined_commitment[..])
331 .unwrap_u8()
332 == 0
333 {
334 return Err(crate::error::A1Error::InvalidSubScopeProof);
335 }
336
337 let pk_hex = self.authority_did.strip_prefix("did:a1:").ok_or_else(|| {
338 crate::error::A1Error::WireFormatError("invalid authority DID".into())
339 })?;
340 let pk_bytes = hex::decode(pk_hex)
341 .map_err(|_| crate::error::A1Error::WireFormatError("invalid DID hex".into()))?;
342 let pk_arr: [u8; 32] = pk_bytes.try_into().map_err(|_| {
343 crate::error::A1Error::WireFormatError("authority key must be 32 bytes".into())
344 })?;
345 let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
346 .map_err(|_| crate::error::A1Error::WireFormatError("invalid Ed25519 key".into()))?;
347
348 let sig_bytes = hex::decode(&self.authority_signature)
349 .map_err(|_| crate::error::A1Error::WireFormatError("invalid signature hex".into()))?;
350 let sig_arr: [u8; 64] = sig_bytes.try_into().map_err(|_| {
351 crate::error::A1Error::WireFormatError("signature must be 64 bytes".into())
352 })?;
353 let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
354
355 use ed25519_dalek::Verifier;
356 vk.verify(&self.combined_commitment, &sig).map_err(|_| {
357 crate::error::A1Error::HybridSignatureInvalid {
358 component: "zk-trace",
359 }
360 })
361 }
362
363 #[must_use]
365 pub fn with_zk_proof(mut self, proof_bytes: &[u8]) -> Self {
366 self.zk_proof_hex = hex::encode(proof_bytes);
367 self
368 }
369
370 pub fn has_zk_proof(&self) -> bool {
372 !self.zk_proof_hex.is_empty()
373 }
374}
375
376fn trace_combined_commitment(chain_commit: &[u8; 32], merkle_root: &[u8; 32]) -> [u8; 32] {
377 let mut h = Hasher::new_derive_key("a1::dyolo::zk::trace::v2.8.0");
378 h.update(chain_commit);
379 h.update(merkle_root);
380 h.finalize().into()
381}
382
383pub(crate) mod hex_32_serde {
384 use serde::{Deserialize, Deserializer, Serializer};
385 pub fn serialize<S: Serializer>(v: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
386 s.serialize_str(&hex::encode(v))
387 }
388 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
389 let h = String::deserialize(d)?;
390 hex::decode(&h)
391 .map_err(serde::de::Error::custom)?
392 .try_into()
393 .map_err(|_| serde::de::Error::custom("expected 32 bytes"))
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::{cert::CertBuilder, identity::DyoloIdentity, intent::Intent};
401
402 #[test]
403 fn seal_and_verify() {
404 let human = DyoloIdentity::generate();
405 let agent = DyoloIdentity::generate();
406 let now = 1_700_000_000u64;
407
408 let intent = Intent::new("trade.equity").unwrap().hash();
409 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
410 let mut chain = DyoloChain::new(human.verifying_key(), intent);
411 chain.push(cert);
412
413 let narrowing = [0u8; 32];
414 let commitment =
415 ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, Some("acme-bot"));
416
417 assert!(commitment
418 .verify_commitment(&narrowing, now, Some(86400))
419 .is_ok());
420 assert_eq!(commitment.mode, ZkProofMode::Blake3Commit);
421 assert!(!commitment.has_zk_proof());
422 }
423
424 #[test]
425 fn tampered_commitment_fails() {
426 let human = DyoloIdentity::generate();
427 let agent = DyoloIdentity::generate();
428 let now = 1_700_000_000u64;
429 let intent = Intent::new("read").unwrap().hash();
430 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
431 let mut chain = DyoloChain::new(human.verifying_key(), intent);
432 chain.push(cert);
433
434 let narrowing = [0u8; 32];
435 let mut commitment =
436 ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
437 commitment.commitment[0] ^= 0xFF;
438 assert!(commitment.verify_commitment(&narrowing, now, None).is_err());
439 }
440
441 #[test]
442 fn expired_commitment_fails() {
443 let human = DyoloIdentity::generate();
444 let agent = DyoloIdentity::generate();
445 let now = 1_700_000_000u64;
446 let intent = Intent::new("read").unwrap().hash();
447 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
448 let mut chain = DyoloChain::new(human.verifying_key(), intent);
449 chain.push(cert);
450
451 let narrowing = [0u8; 32];
452 let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
453 assert!(commitment
454 .verify_commitment(&narrowing, now + 7200, Some(3600))
455 .is_err());
456 }
457
458 #[test]
459 fn with_zk_proof_upgrades_mode() {
460 let human = DyoloIdentity::generate();
461 let agent = DyoloIdentity::generate();
462 let now = 1_700_000_000u64;
463 let intent = Intent::new("read").unwrap().hash();
464 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
465 let mut chain = DyoloChain::new(human.verifying_key(), intent);
466 chain.push(cert);
467
468 let narrowing = [0u8; 32];
469 let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None)
470 .with_zk_proof(b"placeholder-proof-bytes");
471
472 assert_eq!(commitment.mode, ZkProofMode::ExternalZkvm);
473 assert!(commitment.has_zk_proof());
474 assert!(commitment.verify_commitment(&narrowing, now, None).is_ok());
475 }
476
477 #[test]
478 fn anchor_hash_is_deterministic() {
479 let human = DyoloIdentity::generate();
480 let agent = DyoloIdentity::generate();
481 let now = 1_700_000_000u64;
482 let intent = Intent::new("read").unwrap().hash();
483 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
484 let mut chain = DyoloChain::new(human.verifying_key(), intent);
485 chain.push(cert);
486
487 let narrowing = [0u8; 32];
488 let c = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
489 assert_eq!(anchor_hash(&c), anchor_hash(&c));
490 }
491}
492
493#[test]
494fn zk_trace_proof_seal_verify() {
495 use crate::{
496 cert::CertBuilder,
497 identity::DyoloIdentity,
498 intent::Intent,
499 provenance::{ReasoningStepKind, ReasoningTrace},
500 };
501
502 let human = DyoloIdentity::generate();
503 let agent = DyoloIdentity::generate();
504 let now = 1_700_000_000u64;
505 let intent = Intent::new("trade.equity").unwrap().hash();
506 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
507 let mut chain = DyoloChain::new(human.verifying_key(), intent);
508 chain.push(cert);
509
510 let narrowing = [0u8; 32];
511 let chain_fp = chain.fingerprint();
512 let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
513
514 let mut trace = ReasoningTrace::new(now);
515 trace.record(ReasoningStepKind::Thought, b"analyzing trade", now + 1);
516 trace.record(
517 ReasoningStepKind::FinalAction,
518 b"execute trade.equity AAPL 100",
519 now + 2,
520 );
521 let root = trace.finalize(now + 3, &chain_fp).unwrap();
522
523 let proof = ZkTraceProof::seal(commitment, root, &human);
524 assert!(proof.verify().is_ok());
525 assert!(!proof.has_zk_proof());
526}
527
528#[test]
529fn zk_trace_proof_tampered_fails() {
530 use crate::{
531 cert::CertBuilder,
532 identity::DyoloIdentity,
533 intent::Intent,
534 provenance::{ReasoningStepKind, ReasoningTrace},
535 };
536
537 let human = DyoloIdentity::generate();
538 let agent = DyoloIdentity::generate();
539 let now = 1_700_000_000u64;
540 let intent = Intent::new("read").unwrap().hash();
541 let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
542 let mut chain = DyoloChain::new(human.verifying_key(), intent);
543 chain.push(cert);
544 let chain_fp = chain.fingerprint();
545
546 let mut trace = ReasoningTrace::new(now);
547 trace.record(ReasoningStepKind::Thought, b"step one", now + 1);
548 let root = trace.finalize(now + 2, &chain_fp).unwrap();
549
550 let commitment = ZkChainCommitment::seal(&chain, &intent, &[0u8; 32], now, &human, None);
551 let mut proof = ZkTraceProof::seal(commitment, root, &human);
552 proof.combined_commitment[0] ^= 0xFF;
553 assert!(proof.verify().is_err());
554}