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.