use crate::chain::VerificationReceipt;
use crate::identity::narrowing::NarrowingMatrix;
use crate::provenance::{ProvenanceRoot, ProvenanceStepProof, ReasoningTrace};
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ProvableReceipt {
pub inner: VerificationReceipt,
pub passport_namespace: String,
pub narrowing_commitment: [u8; 32],
pub capability_mask_hex: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub provenance: Option<ProvenanceRoot>,
}
impl ProvableReceipt {
pub(crate) fn new(
inner: VerificationReceipt,
passport_namespace: String,
mask: &NarrowingMatrix,
) -> Self {
Self {
inner,
passport_namespace,
narrowing_commitment: mask.commitment(),
capability_mask_hex: mask.to_hex(),
provenance: None,
}
}
pub fn fingerprint_hex(&self) -> String {
self.inner.fingerprint_hex()
}
pub fn verify_commitment(&self) -> bool {
match NarrowingMatrix::from_hex(&self.capability_mask_hex) {
Ok(m) => m.commitment() == self.narrowing_commitment,
Err(_) => false,
}
}
#[must_use]
pub fn with_provenance(mut self, root: ProvenanceRoot) -> Self {
self.provenance = Some(root);
self
}
pub fn bind_reasoning_trace(
self,
trace: &ReasoningTrace,
finalized_at_unix: u64,
) -> Result<Self, crate::error::A1Error> {
let root = trace.finalize(finalized_at_unix, &self.inner.chain_fingerprint)?;
Ok(self.with_provenance(root))
}
pub fn verify_provenance_step(&self, proof: &ProvenanceStepProof) -> bool {
match &self.provenance {
Some(root) => proof.verify(root),
None => false,
}
}
#[inline]
pub fn has_provenance(&self) -> bool {
self.provenance.is_some()
}
}
impl std::fmt::Display for ProvableReceipt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ProvableReceipt {{ namespace={}, depth={}, fingerprint={}, provenance={}, system=a1_dyolo_v2.8.0 }}",
self.passport_namespace,
self.inner.chain_depth,
self.fingerprint_hex(),
if self.has_provenance() { "attached" } else { "none" },
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::narrowing::NarrowingMatrix;
use crate::provenance::{ReasoningStepKind, ReasoningTrace};
fn dummy_receipt() -> VerificationReceipt {
VerificationReceipt {
chain_depth: 1,
verified_scope_root: [0u8; 32],
intent: [1u8; 32],
verified_at_unix: 1_700_000_000,
chain_fingerprint: [2u8; 32],
namespace: Some("test".into()),
}
}
fn make_receipt() -> ProvableReceipt {
let mask = NarrowingMatrix::from_capabilities(&["trade.equity"]);
ProvableReceipt::new(dummy_receipt(), "test-ns".into(), &mask)
}
#[test]
fn commitment_roundtrip() {
let receipt = make_receipt();
assert!(receipt.verify_commitment());
}
#[test]
fn tampered_mask_fails_commitment() {
let mut receipt = make_receipt();
receipt.capability_mask_hex = NarrowingMatrix::FULL.to_hex();
assert!(!receipt.verify_commitment());
}
#[test]
fn no_provenance_by_default() {
let receipt = make_receipt();
assert!(!receipt.has_provenance());
assert!(receipt.provenance.is_none());
}
#[test]
fn with_provenance_attaches_root() {
let receipt = make_receipt();
let fp = receipt.inner.chain_fingerprint;
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(
ReasoningStepKind::Thought,
b"analyzing the request",
1_700_000_001,
);
trace.record(
ReasoningStepKind::FinalAction,
b"execute trade",
1_700_000_002,
);
let root = trace.finalize(1_700_000_003, &fp).unwrap();
let receipt = receipt.with_provenance(root);
assert!(receipt.has_provenance());
let pr = receipt.provenance.as_ref().unwrap();
assert_eq!(pr.step_count, 2);
assert!(pr.verify_chain_binding(&fp));
}
#[test]
fn bind_reasoning_trace_convenience() {
let receipt = make_receipt();
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(ReasoningStepKind::Thought, b"step one", 1_700_000_001);
let receipt = receipt.bind_reasoning_trace(&trace, 1_700_000_002).unwrap();
assert!(receipt.has_provenance());
}
#[test]
fn bind_empty_trace_returns_err() {
let receipt = make_receipt();
let trace = ReasoningTrace::new(1_700_000_000);
assert!(receipt.bind_reasoning_trace(&trace, 1_700_000_001).is_err());
}
#[test]
fn verify_provenance_step_without_provenance_is_false() {
let receipt = make_receipt();
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(ReasoningStepKind::Thought, b"only thought", 1_700_000_001);
let proof = trace.step_proof(0).unwrap();
assert!(!receipt.verify_provenance_step(&proof));
}
#[test]
fn verify_provenance_step_roundtrip() {
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(ReasoningStepKind::Thought, b"checking price", 1_700_000_001);
trace.record_tool_call("get_price", b"{\"symbol\":\"AAPL\"}", 1_700_000_002);
trace.record(ReasoningStepKind::Observation, b"182.50", 1_700_000_003);
trace.record(
ReasoningStepKind::FinalAction,
b"buy AAPL x10",
1_700_000_004,
);
let receipt = make_receipt();
let fp = receipt.inner.chain_fingerprint;
let root = trace.finalize(1_700_000_005, &fp).unwrap();
let receipt = receipt.with_provenance(root);
for i in 0..4 {
let proof = trace.step_proof(i).unwrap();
assert!(receipt.verify_provenance_step(&proof), "step {i} failed");
}
}
#[test]
fn tampered_step_fails_verify() {
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(ReasoningStepKind::Thought, b"step a", 1_700_000_001);
trace.record(ReasoningStepKind::Thought, b"step b", 1_700_000_002);
let receipt = make_receipt();
let fp = receipt.inner.chain_fingerprint;
let root = trace.finalize(1_700_000_003, &fp).unwrap();
let receipt = receipt.with_provenance(root);
let mut proof = trace.step_proof(0).unwrap();
proof.step.content_hash[0] ^= 0xFF;
assert!(!receipt.verify_provenance_step(&proof));
}
#[test]
fn display_shows_provenance_state() {
let receipt = make_receipt();
assert!(format!("{receipt}").contains("provenance=none"));
let mut trace = ReasoningTrace::new(1_700_000_000);
trace.record(ReasoningStepKind::Thought, b"x", 1_700_000_001);
let fp = receipt.inner.chain_fingerprint;
let root = trace.finalize(1_700_000_002, &fp).unwrap();
let receipt = receipt.with_provenance(root);
assert!(format!("{receipt}").contains("provenance=attached"));
}
}