Skip to Content

API Reference

Generated by reading ahl-core 0.5.0’s src/lib.rs and src/receipt.rs directly, not projected from the specification. Every signature below is copied from source. The crate is published on crates.io  under Apache-2.0; the surface below is 0.5.0’s.

0.5.0 is additive at the API surface and changes the corpus. Two new public items, ATL_PROFILE_DOCUMENT and ATL_PROFILE_DIGEST, ship the released ahl-adaptor-atl-v1 artifact. atl-core moved from a git revision to the exact registry version =0.23.2 — the same code — so the crate carries no git dependency and can be published. 0.4.0 broke the cosignature API: cosignature_bytes now takes a &CosignedCheckpoint, the typed six-member projection, instead of a checkpoint Value.

Constants

pub const AHL_VERSION: &str = "0.4"; // I-D §2.2, §7.1 pub const LEAF_PREFIX: u8 = 0x00; // §2.7 tree domain separation pub const NODE_PREFIX: u8 = 0x01; pub const DATASET_SEPARATOR: u8 = 0x1F; // §2.6 commitment preimage pub const TEST_PROFILE_ID: &str = "ahl-test-log-v1"; pub const ATL_PROFILE_ID: &str = "ahl-adaptor-atl-v1"; pub const TEST_ATL_PROFILE_ID: &str = "ahl-test-atl-leaf-v1"; pub const ATL_PROFILE_DOCUMENT: &[u8]; // the released artifact, verbatim pub const ATL_PROFILE_DIGEST: &str; // sha256: over those exact bytes pub const ATL_METADATA: &str = r#"{"ahl_adaptor":"ahl-adaptor-atl-v1"}"#;

ATL_PROFILE_DOCUMENT is the released ahl-adaptor-atl-v1 document — 110 320 bytes — and ATL_PROFILE_DIGEST its digest. A profile’s identity is its bytes, so a client pinning {id, digest} under the real id takes both from here rather than fetching the document to learn its own digest; a unit test recomputes one from the other.

Identifiers and canonical form

pub fn jcs(value: &Value) -> Vec<u8>; pub fn sha256_hex(bytes: &[u8]) -> String; pub fn statement_id(envelope: &Value) -> AhlResult<String>; // SHA-256(JCS(payload)) pub fn entry_id(envelope: &Value) -> String; // SHA-256(JCS(envelope))

Canonicalization and Merkle node/leaf hashing are delegated to atl-core, pinned at the exact registry version =0.23.2 and never reimplemented locally — the crate’s anti-drift guarantee against its sibling implementations. jcs is atl_core::canonicalize; inclusion proofs are verified through atl_core::core::merkle::verify_inclusion; consistency proofs come from the same crate.

Commitments and descriptors

pub fn commit_plain(dataset: &str, ddig: &[u8; 32], canonical: &[u8]) -> AhlResult<String>; pub fn commit_keyed(key: &[u8], dataset: &str, ddig: &[u8; 32], canonical: &[u8]) -> AhlResult<String>;

Both build the I-D §2.6 preimage dsid || 0x1F || ddig || 0x1F || canonical bytes, and both validate dataset first — the length and printable-ASCII syntax, and, as its own check, the control-octet prohibition — because an invalid dataset id would make the preimage’s field boundaries ambiguous. Failures are AhlError::DatasetIdSyntax or AhlError::DatasetIdControlOctet; commit_keyed additionally returns AhlError::BadLength where key cannot be used as an HMAC key.

ddig comes from the descriptor, in ahl_core::descriptor:

pub struct CanonicalizationDescriptor { /* private */ } impl CanonicalizationDescriptor { pub fn new(canonicalization: impl Into<String>, media_type: Option<String>) -> AhlResult<Self>; pub fn canonicalization(&self) -> &str; pub fn media_type(&self) -> Option<&str>; pub fn canonical_bytes(&self) -> Vec<u8>; // JCS(D) pub fn ddig(&self) -> [u8; 32]; // SHA-256(JCS(D)), raw octets } pub fn validate_dataset_id(id: &str) -> AhlResult<()>; pub fn validate_dataset_id_syntax(id: &str) -> AhlResult<()>; pub fn reject_dataset_id_control_octets(id: &str) -> AhlResult<()>; pub fn validate_canonicalization_identifier(id: &str) -> AhlResult<()>; pub fn validate_media_type_production(raw: &str) -> AhlResult<String>; pub fn media_type_required(identifier: &str) -> Option<bool>;

media_type_required returns None for an identifier this build does not know: presence is a producer duty and is never a syntactic matter, so it never rejects a manifest (I-D §2.6, §6.3).

Envelopes and signatures

pub fn envelope(payload: Value, key: &TestKey) -> Value; pub enum EnvelopeCheck { Verified, SignatureInvalid, KeyNotResolved { key_id: String } } pub fn check_envelope<F>(env: &Value, resolve: F) -> AhlResult<EnvelopeCheck> where F: Fn(&str) -> Option<String>; pub fn verify_envelope<F>(env: &Value, resolve: F) -> AhlResult<bool> where F: Fn(&str) -> Option<String>; pub fn verify_signature(key: &VerifyingKey, msg: &[u8], signature: &str) -> AhlResult<bool>; pub fn decode_pubkey(pubkey: &str) -> AhlResult<VerifyingKey>;

check_envelope reports which way an envelope failed, and the precedence is SignatureInvalid > KeyNotResolved > Verified, applied as a sweep rather than an early return: a resolvable entry that fails to verify wins immediately, and an unresolved key_id is returned only once every resolvable entry has verified. Returning on the first unresolved key instead would let a producer downgrade a demonstrated forgery to a capability gap by ordering the signature array. verify_envelope is the boolean form; an envelope with no signatures is false, since unsigned objects are not AHL statements.

TestKey builds deterministic Ed25519 keys from committed seeds:

pub struct TestKey { /* private */ } impl TestKey { pub fn from_seed_hex(name: &str, seed_hex: &str) -> AhlResult<Self>; pub fn name(&self) -> &str; pub fn verifying_key(&self) -> VerifyingKey; pub fn key_id(&self) -> String; // sha256: over the raw 32 octets pub fn pubkey(&self) -> String; // base64: pub fn sign(&self, msg: &[u8]) -> String; // base64: pub fn key_object(&self, valid_from_index: u64) -> Value; // log/witness shape pub fn producer_key_object(&self) -> Value; // {key_id, pubkey} }

Every seed in test_data/keys/ is a published constant. TestKey exists to generate the deterministic conformance corpus, not to manage producer keys in a deployment.

Checkpoints and cosignatures

pub fn checkpoint(log_id: &str, tree_size: u64, root_hash: &str, checkpoint_time: &str, key: &TestKey) -> Value; pub fn checkpoint_signing_bytes(cp: &Value) -> AhlResult<Vec<u8>>; pub fn checkpoint_signing_bytes_for(checkpoint: &Value, profile_id: &str) -> AhlResult<Vec<u8>>; pub struct CosignedCheckpoint { /* private */ } impl CosignedCheckpoint { pub fn project(checkpoint: &Value) -> AhlResult<Self>; } pub fn cosignature_bytes(checkpoint: &CosignedCheckpoint, witness_id: &str) -> Vec<u8>;

CosignedCheckpoint::project is the only way to build the value cosignature_bytes accepts. It reads the six members the adaptor profile cosigns — log_id, tree_size, root_hash, checkpoint_time, key_id, signature — accepts raw and drops it, and refuses any other member rather than carrying it into bytes a second implementation would not reproduce. That is what keeps a checkpoint carrying raw cosigning and re-verifying identically to one that does not. The cosigned bytes are JCS({"checkpoint": <the six-member projection>, "witness_id": <id>}), the witness_id half binding the cosignature to one identity so it cannot be replayed for another witness.

ATL-shaped profiles sign a fixed 98-byte blob instead of canonical JSON:

pub const fn is_atl_shaped(profile_id: &str) -> bool; pub fn atl_log_id(tree_uuid: &[u8; 16]) -> String; pub fn atl_checkpoint(log_id: &str, tree_size: u64, root_hash: &str, timestamp_ns: u64, key: &TestKey) -> AhlResult<Value>; pub fn atl_checkpoint_blob(/* … */) -> /* … */; pub fn atl_checkpoint_blob_from_json(checkpoint: &Value) -> AhlResult<[u8; 98]>; pub fn atl_checkpoint_time(nanos: u64) -> String; // exactly nine fractional digits pub fn atl_checkpoint_time_nanos(value: &str) -> AhlResult<u64>; pub fn reconcile_atl_checkpoint_raw(checkpoint: &Value, raw: &str) -> AhlResult<()>; pub fn atl_metadata_hash() -> Hash;

is_atl_shaped is const and true for exactly ahl-adaptor-atl-v1 and ahl-test-atl-leaf-v1: what differs between them is which artifact a policy must hold, never how a checkpoint is signed or a leaf is built, so both dispatch on one code path.

Merkle trees

pub fn log_leaf_bytes_for(envelope: &Value, profile_id: &str) -> AhlResult<Vec<u8>>; pub fn leaf_hash(bytes: &[u8]) -> Hash; pub fn tree_root(leaves: &[Vec<u8>]) -> Hash; pub fn inclusion_proof(leaves: &[Vec<u8>], index: usize) -> AhlResult<InclusionProof>; pub fn verify_inclusion_proof(leaf: &[u8], proof: &InclusionProof, root: &Hash) -> AhlResult<bool>; pub fn consistency_proof(leaves: &[Vec<u8>], from_size: u64, to_size: u64) -> AhlResult<ConsistencyProof>; pub fn verify_consistency_proof(proof: &ConsistencyProof, old_root: &Hash, new_root: &Hash) -> AhlResult<bool>; pub fn record_sorted(leaves: Vec<Value>) -> AhlResult<Vec<Value>>; pub fn consistency_path_hex(proof: &ConsistencyProof) -> Vec<String>; pub fn proof_path_hex(proof: &InclusionProof) -> Vec<String>; pub fn hash_hex(hash: &Hash) -> String; pub fn parse_hash_hex(value: &str) -> AhlResult<Hash>;

Hash, InclusionProof and ConsistencyProof are re-exported from atl_core::core::merkle. log_leaf_bytes_for is the leaf preimage under a given profile: JCS(envelope) under ahl-test-log-v1, and the two-digest construction SHA-256(JCS(envelope)) || METADATA_HASH under an ATL-shaped one.

Committed tree material is opened rather than trusted, in ahl_core::tree:

pub struct ValidatedLeafSet { /* private */ } impl ValidatedLeafSet { pub fn open(root: &str, declared_count: u64, leaves: Vec<Value>) -> AhlResult<Self>; pub fn root(&self) -> &str; pub fn leaves(&self) -> &[Value]; } pub fn is_canonical_commitment(value: &str) -> bool;

Range proofs (ahl_core::range_proof)

pub const RANGE_PROOF_MAGIC: &[u8; 6] = b"AHLRP1"; pub struct RangeProof { /* … */ } pub fn generate(leaf_hashes: &[Hash], from_index: u64, to_index: u64) -> AhlResult<RangeProof>; pub fn verify(proof: &RangeProof, leaf_hashes: &[Hash], root: &Hash) -> AhlResult<bool>; pub fn verify_over_leaves(proof: &RangeProof, leaves: &[Vec<u8>], root: &Hash) -> AhlResult<bool>; pub fn encode(proof: &RangeProof) -> String; pub fn decode(value: &str) -> AhlResult<RangeProof>;

Range proofs are the one construction atl-core has no primitive for, so the combining recursion is local — but every hash it computes is still atl-core’s, and a width-1 range is cross-checked through verify_inclusion so the two constructions cannot diverge.

Closure (ahl_core::closure)

pub type RecordRef = (String, String); pub type TreeMaterial = BTreeMap<String, Vec<Value>>; pub struct Edge { pub entry_index: usize, pub valid_time: ValidTime, pub input: RecordRef, pub output: RecordRef } pub struct Closure { pub seeds: BTreeSet<RecordRef>, pub affected: BTreeSet<RecordRef> } pub fn edges(envelopes: &[Value], trees: &TreeMaterial, through_size: usize) -> AhlResult<Vec<Edge>>; pub fn trigger_seeds(envelopes: &[Value], trigger_index: usize) -> AhlResult<BTreeSet<RecordRef>>; pub fn affected_set(envelopes: &[Value], trees: &TreeMaterial, trigger_index: usize, through_size: usize) -> AhlResult<Closure>;

The slice index is the entry index. The seeds are never members of affected (I-D §5.1): a retraction seeds exactly {(dataset, record)}; a correction additionally seeds every earlier correction’s superseded replacement, never its own. Committed tree material referenced by a batch or input-set root is opened and validated against the anchored root and count before an edge is read — rejecting or ignoring such derivations would silently truncate the closure.

Bitemporal scope (ahl_core::bitemporal)

pub fn parse_rfc3339(field: &str, value: &str) -> AhlResult<OffsetDateTime>; pub enum ValidTime { /* … */ } impl ValidTime { pub fn from_payload(payload: &Value) -> AhlResult<Self>; pub fn intersects_ray(&self, effective_from: OffsetDateTime) -> bool; } pub struct Scope { /* … */ } impl Scope { pub fn from_payload(payload: &Value) -> AhlResult<Self>; pub fn covers(&self, valid_time: ValidTime) -> bool; }

Every RFC 3339 value in the crate is parsed and compared through one datetime type. String comparison is never used.

Evidence Receipt verification (ahl_core::receipt)

pub const RECEIPT_VERSION: &str = "2"; pub const SPEC_VERSION: &str = "0.4.0"; pub const MAX_EMBEDDED_DEPTH: usize = 4; pub const MAX_EMBEDDED_RECEIPTS: usize = 64; pub fn verify_receipt_report(receipt: &Value, policy: &TrustPolicy) -> Result<Report, ExecutionError>; pub fn verify_receipt(receipt: &Value, policy: &TrustPolicy) -> Result<Verdict, ReceiptError>;

verify_receipt_report is the primary entry point: it returns the §7.7 three-valued result together with the findings it reduces from. verify_receipt is the single-value form for callers that report one rejection — Ok if and only if the result is verified, otherwise the rejection behind the finding that decided it.

The Result error type of verify_receipt_report is not a verdict. ExecutionError is a run that did not complete — “a local execution failure; it says nothing about the receipt and MUST NOT be rendered as any of the three values”. This build reaches it nowhere today: it is handed an already-parsed receipt and an already-loaded policy, performs no I/O, and allocates nothing it does not bound. The type exists so the boundary is in the signature rather than in prose.

The result model

pub enum Outcome { Verified, Unverifiable, Invalid } // Ord: Verified < Unverifiable < Invalid impl Outcome { pub const fn name(self) -> &'static str; } pub enum Assertion { Versions, ResourceLimits, Structure, AdaptorProfile, Anchoring, Governance, CheckpointAuthentication, Witnesses, EnvelopeValidity, CrossField, ClaimMaterial, ContentBinding, } impl Assertion { pub const ORDER: [Self; 12]; pub const fn name(self) -> &'static str; } pub struct Finding { pub assertion: Assertion, pub outcome: Outcome, pub receipt_path: Vec<String>, // claim_material member names, outermost first pub detail: Option<String>, pub rests_on: Option<Assertion>, // None = a cause; Some = an inherited gap } impl Finding { pub const fn counts_toward_result(&self) -> bool; } pub struct Report { pub result: Outcome, pub findings: Vec<Finding>, pub informative: Vec<InformativeItem>, pub verdict: Option<Verdict>, // present iff result == Verified } impl Report { pub fn dominating(&self) -> Option<&Finding>; pub fn finding(&self, assertion: Assertion) -> Option<&Finding>; pub fn finding_at(&self, path: &[&str], assertion: Assertion) -> Option<&Finding>; }

Outcome’s Ord is declared in the order of the §7.7 reduction — invalid if any required finding is invalid; otherwise unverifiable if any is; otherwise verified — so the reduction is max and cannot drift from the sentence. Assertion’s variants are declared in the order the §7.5 algorithm reaches them, and a Report is sorted by that order.

rests_on is what makes a report actionable: None where the finding is what its own check produced — the rule that fired, the budget that ran out, the capability that was missing — and Some(assertion) where the check merely inherited another’s gap. Report::dominating() is the finding to lead with: the first invalid in report order, otherwise the first unverifiable whose rests_on is None. counts_toward_result encodes §7.7’s single exception — an embedded receipt’s content binding is never a required assertion of the receipt that embeds it.

pub enum VoidReason { SignatureInvalid, KeyNotActive } impl VoidReason { pub const fn name(self) -> &'static str; } pub struct InformativeItem { pub entry_index: u64, pub reason: VoidReason, pub receipt_path: Vec<String>, } pub struct ExecutionError { pub detail: String }

Informative items are not findings: they belong to no required assertion, carry no result value, never enter the reduction, and never appear in dominating(). Their number is the number of void entries the run inspected (§7.5.1 4d).

Trust policy

pub struct TrustPolicy { pub genesis_entry_id: String, pub genesis_key_ids: Option<BTreeSet<String>>, pub adaptor_profiles: BTreeMap<String, AdaptorProfile>, pub dataset_keys: BTreeMap<String, Vec<u8>>, pub trusted_witness_keys: BTreeMap<String, TrustedWitnessKey>, pub limits: Limits, } pub struct AdaptorProfile { pub document: Vec<u8>, pub capabilities: AdaptorCapabilities } impl AdaptorProfile { pub fn hash(&self) -> String; // recomputed from `document`, never cached pub const fn minimal(document: Vec<u8>) -> Self; } pub struct AdaptorCapabilities { pub checkpoint_raw: bool, pub consistency_proofs: bool } pub struct TrustedWitnessKey { pub pubkey: String, pub witness_id: String } pub struct Limits { pub max_decoded_bytes: usize, pub max_work_units: u64 } // Default: 8 MiB decoded, 100_000 work units.

Nothing in TrustPolicy may be taken from the receipt; that is the whole point of the trust anchor. A receipt carries a genesis anchor so it is self-describing, and policy decides whether that anchor is the right one.

genesis_key_ids is an Option because initial key fingerprints are optional under I-D §7.5.1 4a: None means policy holds none and no comparison arises; Some(set) means they are held and MUST match. AdaptorProfile stores the artifact itself and recomputes the digest at resolution time, never a caller-asserted hash string a caller could get wrong or leave stale. TrustedWitnessKey carries the witness_id policy trusts the key for, because the identity is inside the cosignature preimage: a key trusted for one witness is not thereby trusted to cosign as another.

Limits carries only the two verifier-local budgets. MAX_EMBEDDED_DEPTH and MAX_EMBEDDED_RECEIPTS are constants and deliberately not configurable — they are properties of the artifact, and a verifier that could lower either would report invalid over a receipt another verifier verifies.

Verdict

pub struct Verdict { pub claim_type: String, pub subject_entry_index: u64, pub subject_statement_id: String, pub assurance: Assurance, pub boundary: String, pub embedded_receipts: usize, } pub struct Assurance { pub governance: String, // "declared" | "enumerated" pub competing_triggers: String, // "not-checked" | "enumerated" pub witnessed: bool, pub continued_history: bool, pub content_binding: String, // "none" | "plain-verified" | "keyed-authorized" pub canonicalization_namespace: Option<String>, // "public" | "private-use" }

boundary is rendered from claim.type and the verified assurance only — never from the receipt’s informative note, and never stronger than what verification established. A Verdict exists only for a verified result: neither invalid nor unverifiable may be rendered as asserting or denying the property.

Rejections

#[non_exhaustive] pub enum ReceiptError { /* one variant per rule that can reject a receipt */ } impl ReceiptError { pub const fn class(&self) -> Outcome; // exhaustive over the variants pub fn assertion(&self) -> Assertion; // fallback for a rejection examined outside a run }

Each variant names the specific rule that fired rather than reporting a generic failure — among them GenesisAnchorMismatch, AdaptorUnknown, AdaptorHashMismatch, AdaptorCapabilityUnsupported, KeyNotBound, RotationProofInvalid, EnvelopeSignatureInvalid, ProducerKeyNotCarried, CompetingRangeInsufficient, ClosureMismatch, TreeMaterialInvalid, CanonicalizationUnsupported, DatasetKeyNotHeld, BudgetExhausted, WitnessKeyNotTrusted, AssuranceMismatch, SubjectManifestBindingInvalid. A test can assert which rule rejected a deliberately malformed receipt.

class() gives the §7.7 value of one rejection, and it is exhaustive rather than a default. The conditions this build reports as unverifiable rather than invalid are exactly: UnsupportedVersion, BudgetExhausted (naming the budget and the value in force), AdaptorUnknown, AdaptorCapabilityUnsupported, AdaptorProfileMisconfigured, GenesisAnchorMismatch, WitnessKeyNotTrusted, CanonicalizationUnsupported, DatasetKeyNotHeld and ProducerKeyNotCarried. Everything else is invalid.

ProducerKeyNotCarried is the one of these decided by the receipt’s governance mode rather than by this build’s capabilities. Under declared governance an envelope naming a producer key the presented chain holds nothing for is unverifiable — the transition is a key statement, and those reach a verifier only through enumeration material, so the receipt is short of material rather than defective. Under enumerated the same condition is invalid, because the range proof forecloses omission (I-D §7.4).

Error types

pub enum AhlError { /* … */ } pub type AhlResult<T> = Result<T, AhlError>;

No panic

ahl-core reaches no panicking construct on any input to its three parsers — the statement envelope (statement_id, entry_id, check_envelope, verify_envelope), the Evidence Receipt (verify_receipt_report, verify_receipt), and the governance manifest and key statement schema the §7.5.1 walk applies — under the crate’s own receipt::Limits. Malformed, hostile or simply absurd input is reported as an error or as a §7.7 finding, never as an abort of the caller’s process.

The mechanism is crate-level lints denied and satisfied in library code rather than allowed at a site: unwrap_used, expect_used, indexing_slicing, arithmetic_side_effects, panic, unreachable, todo, unimplemented, missing_panics_doc. The evidence is three libFuzzer targets under fuzz/.

The boundary, stated rather than implied. Allocation failure and stack exhaustion are out of scope — neither is a panic, and neither is something a library can decline. Nesting depth is bounded by serde_json, which refuses a document nested deeper than 128 levels with an error rather than recursing, so a Value obtained by parsing bytes is already bounded when the crate sees it, while a Value built programmatically to arbitrary depth is not and is outside the claim. Total work is bounded by the §7.8 decoded-size budget, enforced over the canonical form of the whole receipt after the top-level version read and before every remaining semantic and cryptographic check. atl-core is not covered, because the claim is about this crate’s own code.

Last updated on