Skip to Content

Examples

These examples use the real exported surface of ahl-core 0.5.0 — see the API Reference, generated from src/lib.rs and src/receipt.rs. The crate is published on crates.io  under Apache-2.0.

Verify an Evidence Receipt

verify_receipt_report is the primary entry point: it returns the §7.7 three-valued result together with the per-assertion findings it reduces from.

use ahl_core::receipt::{verify_receipt_report, Outcome, TrustPolicy}; use serde_json::Value; use std::collections::BTreeSet; use std::fs; fn main() -> Result<(), Box<dyn std::error::Error>> { let receipt: Value = serde_json::from_str(&fs::read_to_string("decision-d142.ahl")?)?; // TrustPolicy is the verifier's locally configured trust anchor — nothing in it // is taken from the receipt itself. let policy = TrustPolicy { genesis_entry_id: "sha256:<published genesis entry id>".to_owned(), // Initial key fingerprints are OPTIONAL (I-D §7.5.1 4a): `None` means policy // holds none and no comparison arises. `Some(set)` means they MUST match. genesis_key_ids: Some(BTreeSet::from([ "sha256:<published producer key fingerprint>".to_owned(), ])), ..Default::default() }; // The Err arm is NOT a verdict: it is a run that did not complete. let report = verify_receipt_report(&receipt, &policy)?; println!("result: {}", report.result); for finding in &report.findings { println!( " {} = {}{}", finding.assertion, finding.outcome, finding.detail.as_deref().map(|d| format!(" ({d})")).unwrap_or_default() ); } for item in &report.informative { println!(" informative: entry {} void, {}", item.entry_index, item.reason); } match report.result { // A boundary is rendered only for `verified`. Outcome::Verified => { let verdict = report.verdict.as_ref().expect("verified implies a verdict"); println!("boundary: {}", verdict.boundary); println!("claim type: {}", verdict.claim_type); println!("subject: {} @ {}", verdict.subject_statement_id, verdict.subject_entry_index); } // Neither of these may be rendered as asserting OR denying the property. Outcome::Invalid | Outcome::Unverifiable => { if let Some(cause) = report.dominating() { println!("decided by `{}`: {:?}", cause.assertion, cause.detail); } } } Ok(()) }

report.dominating() is the finding to lead with — the first invalid, otherwise the first unverifiable whose rests_on is None. Taking the first unverifiable finding in report order instead would report “versions rests on resource-limits” where what a reader needs — which budget was exhausted, and its value — is on the resource-limits finding.

Configure a policy that holds the released ATL profile

A profile is resolved from local possession by {id, digest}, and the digest is recomputed over the artifact held rather than trusted from any value carried alongside it. ahl-core ships the released ahl-adaptor-atl-v1 bytes, so a client need not fetch a document to learn its own digest.

use ahl_core::receipt::{AdaptorCapabilities, AdaptorProfile, TrustPolicy}; fn atl_policy(genesis_entry_id: String) -> TrustPolicy { let profile = AdaptorProfile { document: ahl_core::ATL_PROFILE_DOCUMENT.to_vec(), capabilities: AdaptorCapabilities { checkpoint_raw: true, consistency_proofs: true }, }; // The digest the crate ships is the digest of the bytes it ships. assert_eq!(profile.hash(), ahl_core::ATL_PROFILE_DIGEST); let mut policy = TrustPolicy { genesis_entry_id, ..Default::default() }; policy.adaptor_profiles.insert(ahl_core::ATL_PROFILE_ID.to_owned(), profile); policy }

A receipt pinning a profile id the policy holds nothing for is unverifiable — a capability the verifier lacks. A receipt pinning an id the policy does hold, at a digest the held document does not recompute to, is invalid — a disagreement decidable from the bytes in hand.

Compute a record commitment

The commitment preimage binds the dataset’s canonicalization descriptor, not just its bytes, so a verifier can check the descriptor it was handed by recomputation.

use ahl_core::descriptor::CanonicalizationDescriptor; use ahl_core::{commit_keyed, commit_plain, jcs}; use serde_json::json; fn main() -> ahl_core::AhlResult<()> { // `jcs` requires no media type; `exact-bytes` does. let descriptor = CanonicalizationDescriptor::new("jcs", None)?; let ddig = descriptor.ddig(); // SHA-256(JCS(D)), raw 32 octets let record = json!({ "customer_id": "C-1001", "score": 712 }); let canonical = jcs(&record); // plain = SHA-256( dsid || 0x1F || ddig || 0x1F || canonical bytes ) let plain = commit_plain("scores", &ddig, &canonical)?; println!("plain commitment: {plain}"); // `keyed` is REQUIRED for personal or sensitive data. The dataset key is held by // the producer and is never packaged in a receipt. let dataset_key = [0x11u8; 32]; let keyed = commit_keyed(&dataset_key, "customers", &ddig, &canonical)?; println!("keyed commitment: {keyed}"); // The dataset id is validated inside both: a control octet would make the // preimage's field boundaries ambiguous. assert!(commit_plain("bad\u{1f}id", &ddig, &canonical).is_err()); Ok(()) }

Build and verify a signed statement envelope

use ahl_core::{check_envelope, envelope, EnvelopeCheck, TestKey}; use serde_json::json; fn main() -> ahl_core::AhlResult<()> { // TestKey builds deterministic Ed25519 keys from committed seeds. Every seed in // `test_data/keys/` is a published constant: this is test-vector machinery, not // production key handling. let producer = TestKey::from_seed_hex( "producer-1", "0101010101010101010101010101010101010101010101010101010101010101", )?; let payload = json!({ "ahl_version": "0.4", "type": "ingestion", "producer": "acme-lending", "manifest": "sha256:437053c5...", "dataset": "customers", "record": "hmac-sha256:9f86d081...", "valid_time": "2026-08-16T12:00:00Z", "issued_at": "2026-08-16T12:00:00Z", }); let env = envelope(payload, &producer); // A real verifier resolves the key set active at THIS envelope's own entry index, // under the manifest snapshot rule (I-D §6.2) plus later `key` statements. let resolve = |key_id: &str| (key_id == producer.key_id()).then(|| producer.pubkey()); // `check_envelope` says WHICH way it failed. The two failures are not alternatives: // one resolvable entry that does not verify is conclusive however many others do. match check_envelope(&env, resolve)? { EnvelopeCheck::Verified => println!("envelope valid"), EnvelopeCheck::SignatureInvalid => println!("a signature entry did not verify"), EnvelopeCheck::KeyNotResolved { key_id } => { println!("no key held for `{key_id}` at this entry index"); } } Ok(()) }

Cosign a checkpoint

cosignature_bytes takes the typed six-member projection, not a checkpoint Value — so a checkpoint’s optional raw framing cannot enter the preimage, and producer and verifier reach the same bytes through the same constructor.

use ahl_core::{cosignature_bytes, CosignedCheckpoint, TestKey}; use serde_json::Value; fn cosign(checkpoint: &Value, witness: &TestKey, witness_id: &str) -> ahl_core::AhlResult<String> { // `project` accepts `raw` and drops it, and refuses any member other than the six // the adaptor profile cosigns. let projected = CosignedCheckpoint::project(checkpoint)?; let bytes = cosignature_bytes(&projected, witness_id); Ok(witness.sign(&bytes)) }

Compute a revocation closure

use ahl_core::closure::affected_set; use std::collections::BTreeMap; fn main() -> ahl_core::AhlResult<()> { // `envelopes` is the corpus in entry-index order — the slice index IS the entry // index, AHL's only ordering primitive. let envelopes: Vec<serde_json::Value> = load_corpus(); let trigger_index = 12; // entry index of a `retraction` or `correction` let through_size = envelopes.len(); // tree_size of the checkpoint C committing it // Committed batch / input-set trees, keyed by anchored root. Each is opened and // validated against its root and count before an edge is read; ignoring them // would silently truncate the closure. let trees = BTreeMap::new(); let closure = affected_set(&envelopes, &trees, trigger_index, through_size)?; println!("seeds: {:?}", closure.seeds); println!("affected: {:?}", closure.affected); // The seeds are never members of `affected` (I-D §5.1): a seed is the record the // trigger acts on, and the affected set is what derives from the seeds. Ok(()) } fn load_corpus() -> Vec<serde_json::Value> { vec![] }

Handle rejections

verify_receipt is the single-value form, for callers that report one rejection rather than a report.

use ahl_core::receipt::{verify_receipt, Outcome, ReceiptError, TrustPolicy}; use serde_json::Value; fn handle(receipt: &Value, policy: &TrustPolicy) { match verify_receipt(receipt, policy) { Ok(verdict) => println!("verified: {}", verdict.boundary), Err(err) => { // `class()` is exhaustive over the variants, so a caller can separate a // defect in the artifact from a gap in its own configuration without // matching every rule. let kind = match err.class() { Outcome::Invalid => "the receipt does not verify", Outcome::Unverifiable => "this verifier could not tell", Outcome::Verified => unreachable!("an Err never carries `verified`"), }; eprintln!("{kind} — on assertion `{}`", err.assertion()); match err { ReceiptError::GenesisAnchorMismatch => { eprintln!("the receipt is evidence about a corpus this policy does not accept"); } ReceiptError::BudgetExhausted { budget, in_force } => { // §7.8 requires naming the budget AND the value in force. eprintln!("budget `{budget}` exhausted at {in_force}"); } ReceiptError::CanonicalizationUnsupported { dataset, identifier } => { eprintln!("dataset `{dataset}` declares `{identifier}`, unimplemented here"); } ReceiptError::KeyNotBound { key_id, entry_index } => { eprintln!("key `{key_id}` matches no key object in the manifest at {entry_index}"); } ReceiptError::CompetingRangeInsufficient { got_from, got_to, tree_size, .. } => { eprintln!("competing range [{got_from}, {got_to}) does not cover [0, {tree_size})"); } ReceiptError::ClosureMismatch(detail) => { eprintln!("recomputed closure disagrees with the anchored dispositions: {detail}"); } other => eprintln!("rejected: {other}"), } } } }

ReceiptError is #[non_exhaustive], and each variant names the specific rule that fired, so a test can assert which rule rejected a deliberately malformed receipt rather than that verification “failed somehow”. GenesisAnchorMismatch, BudgetExhausted and CanonicalizationUnsupported are all unverifiable: none of them is a demonstrated defect in the artifact, and reporting one as invalid would put this verifier in contradiction with a better-configured one over the same bytes.

Last updated on