use blake3::Hasher;
use serde::{Deserialize, Serialize};
use subtle::ConstantTimeEq;
use crate::chain::DyoloChain;
use crate::error::A1Error;
use crate::identity::Signer;
use crate::intent::IntentHash;
const DOMAIN_ZK_COMMIT: &str = "a1::dyolo::zk::commit::v2.8.0";
const DOMAIN_ZK_BIND: &str = "a1::dyolo::zk::bind::v2.8.0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ZkProofMode {
Blake3Commit = 1,
ExternalZkvm = 2,
}
impl ZkProofMode {
pub fn as_u8(&self) -> u8 {
match self {
Self::Blake3Commit => 1,
Self::ExternalZkvm => 2,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZkChainCommitment {
pub commitment: [u8; 32],
pub intent: IntentHash,
pub sealed_at_unix: u64,
pub chain_fingerprint_hex: String,
pub authority_signature: String,
pub authority_did: String,
pub mode: ZkProofMode,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub zk_proof_hex: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub passport_namespace: Option<String>,
}
impl ZkChainCommitment {
pub fn seal(
chain: &DyoloChain,
intent: &IntentHash,
narrowing_commitment: &[u8; 32],
sealed_at_unix: u64,
authority: &dyn Signer,
passport_namespace: Option<&str>,
) -> Self {
let chain_fp = chain.fingerprint();
let commitment =
compute_commitment(&chain_fp, intent, narrowing_commitment, sealed_at_unix);
let sig = authority.sign_message(&commitment);
let authority_did = format!(
"did:a1:{}",
hex::encode(authority.verifying_key().as_bytes())
);
Self {
commitment,
intent: *intent,
sealed_at_unix,
chain_fingerprint_hex: hex::encode(chain_fp),
authority_signature: hex::encode(sig.to_bytes()),
authority_did,
mode: ZkProofMode::Blake3Commit,
zk_proof_hex: String::new(),
passport_namespace: passport_namespace.map(String::from),
}
}
pub fn verify_commitment(
&self,
narrowing_commitment: &[u8; 32],
now_unix: u64,
max_age_secs: Option<u64>,
) -> Result<(), A1Error> {
let chain_fp_bytes = hex::decode(&self.chain_fingerprint_hex)
.map_err(|_| A1Error::WireFormatError("invalid chain_fingerprint_hex".into()))?;
let chain_fp: [u8; 32] = chain_fp_bytes
.try_into()
.map_err(|_| A1Error::WireFormatError("chain fingerprint must be 32 bytes".into()))?;
let expected = compute_commitment(
&chain_fp,
&self.intent,
narrowing_commitment,
self.sealed_at_unix,
);
if expected[..].ct_eq(&self.commitment[..]).unwrap_u8() == 0 {
return Err(A1Error::InvalidSubScopeProof);
}
if let Some(max_age) = max_age_secs {
let age = now_unix.saturating_sub(self.sealed_at_unix);
if age > max_age {
return Err(A1Error::Expired(0, self.sealed_at_unix + max_age, now_unix));
}
}
let pk_hex = self
.authority_did
.strip_prefix("did:a1:")
.ok_or_else(|| A1Error::WireFormatError("invalid authority DID".into()))?;
let pk_bytes = hex::decode(pk_hex)
.map_err(|_| A1Error::WireFormatError("invalid authority DID hex".into()))?;
let pk_arr: [u8; 32] = pk_bytes
.try_into()
.map_err(|_| A1Error::WireFormatError("authority key must be 32 bytes".into()))?;
let authority_vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
.map_err(|_| A1Error::WireFormatError("invalid authority Ed25519 key".into()))?;
let sig_bytes = hex::decode(&self.authority_signature)
.map_err(|_| A1Error::WireFormatError("invalid authority_signature hex".into()))?;
let sig_arr: [u8; 64] = sig_bytes
.try_into()
.map_err(|_| A1Error::WireFormatError("signature must be 64 bytes".into()))?;
let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
use ed25519_dalek::Verifier;
authority_vk
.verify(&self.commitment, &sig)
.map_err(|_| A1Error::HybridSignatureInvalid {
component: "zk-commitment",
})
}
pub fn with_zk_proof(mut self, proof_bytes: &[u8]) -> Self {
self.zk_proof_hex = hex::encode(proof_bytes);
self.mode = ZkProofMode::ExternalZkvm;
self
}
pub fn has_zk_proof(&self) -> bool {
self.mode == ZkProofMode::ExternalZkvm && !self.zk_proof_hex.is_empty()
}
}
fn compute_commitment(
chain_fp: &[u8; 32],
intent: &IntentHash,
narrowing_commitment: &[u8; 32],
sealed_at: u64,
) -> [u8; 32] {
let mut h = Hasher::new_derive_key(DOMAIN_ZK_COMMIT);
h.update(chain_fp);
h.update(intent);
h.update(narrowing_commitment);
h.update(&sealed_at.to_le_bytes());
h.finalize().into()
}
pub fn anchor_hash(commitment: &ZkChainCommitment) -> [u8; 32] {
let mut h = Hasher::new_derive_key(DOMAIN_ZK_BIND);
h.update(&commitment.commitment);
h.update(&commitment.sealed_at_unix.to_le_bytes());
h.update(commitment.authority_did.as_bytes());
h.finalize().into()
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ZkTraceProof {
pub chain_commitment: ZkChainCommitment,
pub trace_root: crate::provenance::ProvenanceRoot,
#[cfg_attr(feature = "serde", serde(with = "crate::zk::hex_32_serde"))]
pub combined_commitment: [u8; 32],
pub authority_signature: String,
pub authority_did: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "String::is_empty")
)]
pub zk_proof_hex: String,
}
impl ZkTraceProof {
pub fn seal(
chain_commitment: ZkChainCommitment,
trace_root: crate::provenance::ProvenanceRoot,
authority: &dyn crate::identity::Signer,
) -> Self {
let combined =
trace_combined_commitment(&chain_commitment.commitment, &trace_root.merkle_root);
let sig = authority.sign_message(&combined);
let authority_did = format!(
"did:a1:{}",
hex::encode(authority.verifying_key().as_bytes())
);
Self {
chain_commitment,
trace_root,
combined_commitment: combined,
authority_signature: hex::encode(sig.to_bytes()),
authority_did,
zk_proof_hex: String::new(),
}
}
pub fn verify(&self) -> Result<(), crate::error::A1Error> {
let expected = trace_combined_commitment(
&self.chain_commitment.commitment,
&self.trace_root.merkle_root,
);
use subtle::ConstantTimeEq;
if expected[..]
.ct_eq(&self.combined_commitment[..])
.unwrap_u8()
== 0
{
return Err(crate::error::A1Error::InvalidSubScopeProof);
}
let pk_hex = self.authority_did.strip_prefix("did:a1:").ok_or_else(|| {
crate::error::A1Error::WireFormatError("invalid authority DID".into())
})?;
let pk_bytes = hex::decode(pk_hex)
.map_err(|_| crate::error::A1Error::WireFormatError("invalid DID hex".into()))?;
let pk_arr: [u8; 32] = pk_bytes.try_into().map_err(|_| {
crate::error::A1Error::WireFormatError("authority key must be 32 bytes".into())
})?;
let vk = ed25519_dalek::VerifyingKey::from_bytes(&pk_arr)
.map_err(|_| crate::error::A1Error::WireFormatError("invalid Ed25519 key".into()))?;
let sig_bytes = hex::decode(&self.authority_signature)
.map_err(|_| crate::error::A1Error::WireFormatError("invalid signature hex".into()))?;
let sig_arr: [u8; 64] = sig_bytes.try_into().map_err(|_| {
crate::error::A1Error::WireFormatError("signature must be 64 bytes".into())
})?;
let sig = ed25519_dalek::Signature::from_bytes(&sig_arr);
use ed25519_dalek::Verifier;
vk.verify(&self.combined_commitment, &sig).map_err(|_| {
crate::error::A1Error::HybridSignatureInvalid {
component: "zk-trace",
}
})
}
#[must_use]
pub fn with_zk_proof(mut self, proof_bytes: &[u8]) -> Self {
self.zk_proof_hex = hex::encode(proof_bytes);
self
}
pub fn has_zk_proof(&self) -> bool {
!self.zk_proof_hex.is_empty()
}
}
fn trace_combined_commitment(chain_commit: &[u8; 32], merkle_root: &[u8; 32]) -> [u8; 32] {
let mut h = Hasher::new_derive_key("a1::dyolo::zk::trace::v2.8.0");
h.update(chain_commit);
h.update(merkle_root);
h.finalize().into()
}
pub(crate) mod hex_32_serde {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(v: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(v))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
let h = String::deserialize(d)?;
hex::decode(&h)
.map_err(serde::de::Error::custom)?
.try_into()
.map_err(|_| serde::de::Error::custom("expected 32 bytes"))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{cert::CertBuilder, identity::DyoloIdentity, intent::Intent};
#[test]
fn seal_and_verify() {
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("trade.equity").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let commitment =
ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, Some("acme-bot"));
assert!(commitment
.verify_commitment(&narrowing, now, Some(86400))
.is_ok());
assert_eq!(commitment.mode, ZkProofMode::Blake3Commit);
assert!(!commitment.has_zk_proof());
}
#[test]
fn tampered_commitment_fails() {
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("read").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let mut commitment =
ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
commitment.commitment[0] ^= 0xFF;
assert!(commitment.verify_commitment(&narrowing, now, None).is_err());
}
#[test]
fn expired_commitment_fails() {
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("read").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
assert!(commitment
.verify_commitment(&narrowing, now + 7200, Some(3600))
.is_err());
}
#[test]
fn with_zk_proof_upgrades_mode() {
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("read").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None)
.with_zk_proof(b"placeholder-proof-bytes");
assert_eq!(commitment.mode, ZkProofMode::ExternalZkvm);
assert!(commitment.has_zk_proof());
assert!(commitment.verify_commitment(&narrowing, now, None).is_ok());
}
#[test]
fn anchor_hash_is_deterministic() {
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("read").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let c = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
assert_eq!(anchor_hash(&c), anchor_hash(&c));
}
}
#[test]
fn zk_trace_proof_seal_verify() {
use crate::{
cert::CertBuilder,
identity::DyoloIdentity,
intent::Intent,
provenance::{ReasoningStepKind, ReasoningTrace},
};
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("trade.equity").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let narrowing = [0u8; 32];
let chain_fp = chain.fingerprint();
let commitment = ZkChainCommitment::seal(&chain, &intent, &narrowing, now, &human, None);
let mut trace = ReasoningTrace::new(now);
trace.record(ReasoningStepKind::Thought, b"analyzing trade", now + 1);
trace.record(
ReasoningStepKind::FinalAction,
b"execute trade.equity AAPL 100",
now + 2,
);
let root = trace.finalize(now + 3, &chain_fp).unwrap();
let proof = ZkTraceProof::seal(commitment, root, &human);
assert!(proof.verify().is_ok());
assert!(!proof.has_zk_proof());
}
#[test]
fn zk_trace_proof_tampered_fails() {
use crate::{
cert::CertBuilder,
identity::DyoloIdentity,
intent::Intent,
provenance::{ReasoningStepKind, ReasoningTrace},
};
let human = DyoloIdentity::generate();
let agent = DyoloIdentity::generate();
let now = 1_700_000_000u64;
let intent = Intent::new("read").unwrap().hash();
let cert = CertBuilder::new(agent.verifying_key(), intent, now, now + 3600).sign(&human);
let mut chain = DyoloChain::new(human.verifying_key(), intent);
chain.push(cert);
let chain_fp = chain.fingerprint();
let mut trace = ReasoningTrace::new(now);
trace.record(ReasoningStepKind::Thought, b"step one", now + 1);
let root = trace.finalize(now + 2, &chain_fp).unwrap();
let commitment = ZkChainCommitment::seal(&chain, &intent, &[0u8; 32], now, &human, None);
let mut proof = ZkTraceProof::seal(commitment, root, &human);
proof.combined_commitment[0] ^= 0xFF;
assert!(proof.verify().is_err());
}