Agent Context Distribution Protocol (ACDP) — Chain Verification and Evidence Bundles
RFC-ACDP-0017
Document: RFC-ACDP-0017 Version: 0.5.0 Status: Community Standards Track (Final; §10–§11 registry amendments (0.5.0) Draft)
This RFC specifies chain verification: one consumer-side algorithm that verifies a whole derived_from graph, offline, as of a past instant T, and emits a deterministic, digestible verdict. It defines three objects:
- the evidence bundle (
acdp-evidence/1): a self-describing, untrusted container for everything the algorithm consumes; - the chain verdict (
acdp-chain-verdict/1): a JCS-canonical report with two digests, one sealable at T and one over the full result; - the event receipt (
acdp-event-receipt/1): a registry-signed acceptance time for a lifecycle event. This one is a wire amendment to RFC-ACDP-0013.
The algorithm runs identically online and offline. The two modes differ only in where the evidence comes from. It depends on RFC-ACDP-0001, -0004, -0006, -0010, -0012, -0013, -0014 and -0015. It amends RFC-ACDP-0013 (§10 of this document) and RFC-ACDP-0004 §5.1 (§11).
1. Status of This Memo
This document is a Final ACDP specification at version 0.5.0, promoted 2026-10-26. It is the first Final document on the acdp/0.5.0 line, which as a whole remains Draft (VERSIONING.md). Breaking changes now require a new RFC and a version bump.
Promotion evidence. The consumer-side conformance fixtures (chain-001..chain-011, profile acdp-consumer-chain) pass against two independent interoperating implementations:
- acdp-rs
acdp-chain0.17.1; - acdp-verifier-py at
c95cf7b, written from this text alone.
Both reproduce every known-answer vector byte for byte. That is 31 verdicts: chain-001, 002, 003 and 010 scenario A, plus the 27 cross-implementation edge vectors of chain-011. Both also pass the behavioral consumer scenarios. The two implementations' disagreements were resolved in this text before promotion (CHANGELOG, 2026-10-26).
Still Draft. The registry-side amendments in §10 (lifecycle event receipts) and §11 (receipts on lineage members) remain (0.5.0) Draft, not frozen. Only one registry implementation (acdp-registry-rs, in production since 0.5.6) exercises their registry-side scenarios (chain-009 A–B). They are promoted when a second registry does. The consumer-side verification of event receipts (§10.3) is Final.
The acdp-consumer-chain profile (§13) is OPTIONAL. A consumer that never verifies chains is unaffected. The registry-side amendments in §10 and §11 bind only registries that advertise acdp-registry-receipts and declare acdp_version ≥ 0.5.0.
Nothing in this document invalidates any existing body, signature, content_hash, receipt, checkpoint or cosignature. §10 adds a new signed object; it changes none.
2. Conventions and Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in BCP 14 ([RFC 2119], [RFC 8174]) when, and only when, they appear in all capitals.
| Term | Definition |
|---|---|
| Root | A ctx_id the walk starts from. A walk has one or more roots. |
| Node | A context reached from a root through zero or more derived_from edges. |
| Evidence | Any input a check consumes: bodies, registry receipts, lifecycle events, event receipts, log inclusions, DID-document snapshots, revocation-discovery records, checkpoints, cosignatures. |
| Evidence time | The earliest instant at which a piece of evidence is provably known to have existed (§5.1). Some evidence has no evidence time. |
| As-of instant (T) | A caller-supplied instant. The as-of pass uses only evidence whose evidence time is ≤ T. |
| Horizon | The greatest evidence time in the bundle (§5.4). The latest pass evaluates as of the horizon, using all evidence. |
| Pass | One evaluation of every node: the as-of pass (filtered to ≤ T) or the latest pass (unfiltered). |
| Trust basis | The kind of trust a stage result rests on. Exactly one of: self (hash or signature arithmetic only, e.g. did:key, content_hash); pinned (a key or document the caller supplied out of band, §6.3); registry (signed by a registry key resolved from a pinned or observed basis); observed (an unsigned snapshot that an exporter asserts it saw at a stated time). |
| Drift | A per-node difference between the as-of and latest passes (§8.4). |
Timestamps are canonical millisecond-precision RFC 3339 UTC (RFC-ACDP-0001 §5.3). Every comparison in this document is between such timestamps, and compares instants.
3. Motivation
Before this RFC, ACDP could verify one body, online, now. An auditor of an agent decision needs to verify the graph of contexts behind it, offline, at the instant the decision was made. There were four gaps:
- No chain primitive. RFC-ACDP-0006 §4.1 step 8 bounds a
derived_fromwalk and requires partial-walk reporting (fed-010). But it defines no consumer report shape, and no reusable algorithm. - No offline evidence format. Every trust check reaches the network: did:web resolution (RFC-ACDP-0001 §5.11), receipt keys (RFC-ACDP-0010 §8), revocation discovery (RFC-ACDP-0014 §8), lifecycle state (RFC-ACDP-0013 §4.1). A party holding only an exported bundle could run none of them.
- No time other than "now". RFC-ACDP-0014 compromise windows are retroactive by design. A revocation published after a decision makes a body invalid now that was correctly accepted then, and before this RFC both cases collapsed into a single
revoked. Retractions had no provable time at all, becauseoccurred_atis claimed by the actor and array order carries no clock. - No deterministic verdict. There was nothing a decision system could seal, and nothing an auditor could recompute and byte-compare.
4. The Evidence Bundle: acdp-evidence/1 (NORMATIVE)
An evidence bundle is a single JSON object, validated by acdp-evidence.schema.json.
{
"evidence_version": "acdp-evidence/1",
"roots": ["acdp://registry.example.com/11111111-1111-4111-8111-111111111111"],
"exported_at": "2026-10-24T12:00:00.000Z",
"contexts": {
"acdp://registry.example.com/11111111-1111-4111-8111-111111111111": {
"body": { "...": "..." },
"registry_receipt": { "...": "..." },
"lifecycle_events": [],
"event_receipts": [],
"log_inclusion": { "...": "..." }
}
},
"did_documents": {
"did:web:agents.example.com:producer": [
{
"document": { "...": "..." },
"observed_at": "2026-10-24T11:59:58.000Z",
"observed_by": "seam-runtime",
"source": "https://agents.example.com/producer/did.json"
}
]
},
"revocation_discovery": {
"did:web:agents.example.com:producer": [
{
"vantage": "registry.example.com",
"outcome": "not-revoked-complete",
"completed_at": "2026-10-24T11:59:59.000Z"
}
]
},
"checkpoints": [ { "checkpoint": { "...": "..." }, "cosignatures": [] } ],
"omitted": [
{ "ctx_id": "acdp://registry.example.com/22222222-2222-4222-8222-222222222222", "reason": "withheld" }
]
}4.1 Posture
The bundle is unsigned and untrusted. A verifier MUST NOT believe any member because it is present. Every stage (§7) re-derives its result from content, and the trust basis of every result says what that result ultimately rests on. A bundle that fails schema validation MUST be rejected as a whole. A verifier MUST NOT verify the parts of it that happen to parse.
4.2 Members
contextsis keyed byctx_id. Each entry carries the context'sbodyand, where the exporter obtained them, the context'sregistry_receipt,lifecycle_events,event_receipts(§10) andlog_inclusion. The map key is a claim: the identity stage (§7.2) checks it againstbody.ctx_idandregistry_receipt.ctx_id. Key-revocation contexts (RFC-ACDP-0014 §4) that the verifier should apply are carried here like any other context. They do not need to be reachable throughderived_from.did_documentsis keyed by DID and holds a history: an array of snapshots, each with thedocumentas fetched,observed_at, and optionallyobserved_byandsource. Several snapshots per DID let the as-of pass choose the one in force at T (§6.2).did:keyDIDs MUST NOT appear, because they resolve arithmetically. Registry DID documents (for receipt, event-receipt and checkpoint keys) and witness DID documents go in the same map.revocation_discoveryis keyed by producer DID and holds observed RFC-ACDP-0014 §8 discovery records. Each record has avantage(the registry authority queried), anoutcome(revoked,not-revoked-completeorunknown) and acompleted_at. A producer with no record has discovery outcomeunknown. Its outcome is nevernot-revoked.checkpointsholds RFC-ACDP-0012 checkpoints with any RFC-ACDP-0015 cosignatures.consistency_proofsMAY carry RFC-ACDP-0012 §9.2 proofs between them.omittedis the exporter's honest list of contexts it deliberately left out, each with areason:withheld(left out by policy, e.g. visibility),not_retrievable(the fetch failed) orlimit(an export budget fired).rootsandexported_atare informational. The verifier takes its roots from the caller (§6), andexported_atplays no part in any pass.
4.3 Exporting
An exporter collects a bundle by walking online under the §6.1 rules. It fetches each node with its receipt, lifecycle events, event receipts and log inclusion; snapshots every DID document it resolved, recording observed_at; and runs RFC-ACDP-0014 §8 discovery once per distinct producer, recording the outcome. Collecting and verifying are separate. An exporter MAY collect without trusting what it collects.
Exporters SHOULD prefer the receipt-bearing lineage listing (§11) to per-context retrieval, and SHOULD include every member of any key-revocation lineage they discovered (RFC-ACDP-0014 §7 requires the whole lineage for the earliest-compromised_since fold).
4.4 Embedding
A host format MAY carry an acdp-evidence/1 object as a file. Seam's evidence bundle does this, for example, with an acdp-evidence.json listed in its manifest. The host's own integrity mechanism, not ACDP's, protects the file. The verdict (§9) binds to the bundle bytes through evidence_digest.
5. Time Semantics (NORMATIVE)
5.1 Evidence time, by kind
| Evidence | Evidence time | Basis |
|---|---|---|
| Body with a receipt that verifies (RFC-ACDP-0010 §8) | registry_receipt.created_at | registry |
| Body with no receipt, or whose receipt fails | none (a bare created_at is unsigned, RFC-ACDP-0008 §9.1) | — |
| Key-revocation context | the evidence time of its own body, per the two rows above | registry |
| Lifecycle event with a verified event receipt (§10) | event_receipt.accepted_at | registry |
| Lifecycle event with no verified event receipt | none (occurred_at is claimed by the actor) | — |
| DID-document snapshot | observed_at | observed (pinned when the caller pins it, §6.3) |
| Revocation-discovery record | completed_at | observed |
| Checkpoint (and the leaves it covers) | checkpoint.timestamp, with cosignature witnessed_at values as independent bounds | registry (+ witnesses) |
5.2 The two passes
- As-of pass (T). Every stage is evaluated using only evidence whose evidence time is ≤ T. Evidence with no evidence time counts as not known at T, with the two exceptions §7 states: bodies, and unattested events when
require_attested_eventsis false. This conservative rule makes a backdated retraction harmless, and a later revocation non-retroactive in the as-of answer. - Latest pass. Every stage is evaluated using all evidence.
The as-of pass runs only when the caller supplies T. The latest pass always runs.
5.3 Revocation at T
A key-revocation context is applied in the as-of pass only if its own evidence time is ≤ T. Once it is applied, RFC-ACDP-0014 §7 runs unchanged: the compromise-boundary comparison, the earliest-compromised_since fold over the applied members of the revocation lineage, producer scope, and attribution precedence. A revocation that is not applied in the as-of pass is reported as drift (§8.4) when the latest pass applies it.
5.4 Horizon
The horizon is the greatest evidence time over all evidence in the bundle that has one. (NORMATIVE) Concretely, it is the maximum of these values:
- the
created_atof every registry receipt that verifies; - the
accepted_atof every event receipt that passes §10.3 on its own (every step except step 8, which is a check across receipts); - the
timestampof every checkpoint whose signature verifies (carried or in alog_inclusion); - the
observed_atof everydid_documentssnapshot; - the
completed_atof every discovery record, whatever its outcome.
Cosignature witnessed_at values are bounds, not evidence times, and do not count. Neither do pinned snapshot instants, which are caller input, or exported_at. The horizon is computed once, over the whole bundle, independent of T. A bundle with no timed evidence has a null horizon. The verifier MUST NOT read a wall clock in either pass. Offline there is no "now", and the report states the horizon in its place.
6. The Algorithm (NORMATIVE)
verify_chain(roots, evidence, options) -> acdp-chain-verdict/1
options = { as_of: T | null, limits, pinned, require_attested_events }6.1 Walk
The verifier walks breadth-first from the roots over derived_from. The order is deterministic:
-
The roots are deduplicated, sorted by
ctx_idin ascending byte order of their UTF-8 encoding, and assigned depth 0. -
Nodes are expanded in order of increasing depth. Within one depth, they are expanded in ascending
ctx_idbyte order. -
A node's
derived_fromentries are considered in array order. Actx_idalready admitted is a DAG join: it adds an edge and admits no new node. -
The RFC-ACDP-0006 §4.1 controls apply, combined with AND.
limitscarriesmax_depth,max_nodesandmax_fanout, which default to 10, 500 and 100. A caller MAY lower them and MUST NOT raise them above those defaults. A verifier MUST reject options carrying a limit above its default, or below the schema minimum (max_depth0,max_nodes1,max_fanout0), with an error and emit no verdict. Each limit truncates rather than aborts, as follows:max_depth. A node at depthmax_depthis verified but not expanded.max_fanout. Only the firstmax_fanoutdistinct entries of a node'sderived_fromare admitted.max_nodes. Oncemax_nodesnodes have been admitted, no further node is admitted.
Every
ctx_idthat is referenced but not admitted joins the frontier. Frontier entries are not nodes: they appear infrontieronly, never innodes. The report setscomplete: falseand names the first control that fired, in walk order. -
The verifier MAY also enforce a wall-clock time budget. A verdict produced under a fired time budget MUST set
control: "timeout"andcomplete: false, and is exempt from the determinism requirement (§9.3). -
A cycle is a
derived_frompath from a node back to itself. It is reported as stagepresence: cycleon every node that lies on the cycle. Honest registries cannot produce one, becausederived_fromcan name onlyctx_ids that already exist, so a cycle is evidence of a forged body or a forged bundle. (NORMATIVE) Cycles (here) and tainted paths (§8.3) are computed over the walk graph: the edges the walk considered, i.e. a node's firstmax_fanoutdistinctderived_fromentries, from nodes that were expanded. An edge the walk did not consider plays no part in either.
A node is resolved from evidence.contexts. A ctx_id the walk admits but contexts does not carry has presence withheld when omitted lists it with reason withheld, unresolved when omitted lists it with any other reason, and absent when omitted does not list it. An online verifier collects first (§4.3) and then runs the same algorithm over what it collected. It MUST NOT fetch evidence during a pass.
6.2 DID-document selection
For a did:web key:
- In the as-of pass, the verifier uses the snapshot with the greatest
observed_at≤ T (for a pinned snapshot, itsinstant). Ties are broken as follows: a pinned snapshot beats an observed one; among observed snapshots, the later array position wins. With no such snapshot, the key stage isunknownwith reasonno_evidence_at_as_of. - In the latest pass, it uses the snapshot with the greatest
observed_at.
The chosen snapshot is evaluated under RFC-ACDP-0001 §5.11 as though it were the fetched document. There is no forced re-fetch offline. An online verifier performs the §5.11 re-fetch while collecting, and the bundle records the result.
did:key resolves arithmetically (RFC-ACDP-0001 §5.11.1) with basis self in both passes.
6.3 Pinned trust
pinned is a caller-supplied set containing any of:
- DID-document snapshots, each bound to the instant at which the caller vouches for it;
- registry DIDs whose receipt keys the caller trusts;
- witness DIDs;
- discovery vantages: hostnames whose RFC-ACDP-0014 §8 discovery records the caller vouches for. A
revocation_discoveryrecord whosevantageis pinned has basispinnedrather thanobserved; - (0.5.0) keyless attesters: registry DIDs the caller accepts as RFC-ACDP-0021 key attesters (RFC-ACDP-0021 §10, §11). This member is Draft; a verifier without RFC-ACDP-0021 support rejects it as an unknown member, as for any options that fail the shape (§6.1(4)).
(NORMATIVE) pinned is a JSON object with these members, each OPTIONAL:
{
"snapshots": [{ "did": "did:web:…", "document": { }, "instant": "2026-09-20T00:00:00.000Z" }],
"registries": ["did:web:…"],
"witnesses": ["did:web:…"],
"vantages": ["registry.example.com"],
"keyless_attesters": ["did:web:…"]
}registries, witnesses, vantages and keyless_attesters are sorted ascending by UTF-8 bytes and deduplicated. snapshots are sorted by did, then instant, and deduplicated. Two pinned snapshots with the same did and instant but different documents are an error under §6.1(4). An empty array member is omitted. A caller that pins nothing passes pinned absent, null or {}; all three are equivalent. The shape is #/$defs/pinned in acdp-evidence.schema.json, and options that fail it are rejected as in §6.1(4).
A pinned snapshot takes part in §6.2 selection like an observed one, but its basis is pinned. Pinning promotes a basis. It never overrides a failed check: a pinned key that does not verify a signature is still signature: invalid.
pinned contributes to the verdict through options_digest (§9.1), computed over the normalized object above.
6.4 Attested events
require_attested_events defaults to true. When true, the as-of pass considers only lifecycle events with a verified event receipt (§10) whose accepted_at ≤ T, and whose signature verifies under the key located as of T (§6.2). For a producer actor, that key MUST also be in the as-of snapshot's assertionMethod. (NORMATIVE) The one exception is an unsigned event whose actor is the origin registry's DID (RFC-ACDP-0013, registries not advertising receipts). It is admitted on its verified event receipt alone, and never without one. When false, an as-of pass MAY also consider an event without one, provided its signature verifies and its occurred_at is ≤ T. Such an event is labelled basis observed.
7. Per-Node Stages (NORMATIVE)
Each stage reports a value. Where the stage defines them, it also reports a basis, an evidence_time and a reason. (NORMATIVE) acdp-chain-verdict/1 defines one reason token, no_evidence_at_as_of, emitted exactly where this document names it (§6.2, §7.5, §7.6). A verifier MUST NOT emit any other reason, since the verdict bytes are compared across implementations (§9.3). Stages run in the order below, in both passes, over the evidence that pass admits. Stage values are the closed vocabularies in acdp-chain-verdict.schema.json.
7.1 presence
present, withheld, unresolved, absent, cycle, or not_yet_published. A truncated walk is reported through complete, control and frontier (§6.1), never as a node presence.
not_yet_publishedapplies in the as-of pass only: the node's receipt verifies and itscreated_atis later than T. A chain that is honest at T cannot reference such a node.- A node with no evidence time is
presentin the as-of pass, with the caveatundated(§8.1). Its content verifies arithmetically, but its existence at T is unproven.
A node that is not present runs no further stages. A cycle presence carries value only. A node's derived_from member (§9.1) is emitted exactly when its latest presence is present.
7.2 identity
ok, unbound, or mismatch.
mismatchwhen the bundle key ≠body.ctx_id, when a present receipt'sctx_id,lineage_id,origin_registryorcreated_atdisagrees with the body, or when the RFC-ACDP-0004 §2.1 first-versionlineage_idderivation fails. This is thefed-011binding, carried offline.unboundwhen the body has no verified receipt. Its identity fields are then unattested.
7.3 content_hash
ok or mismatch, by recomputing under RFC-ACDP-0001 §5.7 over the exclusion-stripped body. Basis self.
7.4 signature
ok, invalid, or unresolvable, under RFC-ACDP-0001 §5.8. It uses the key the key stage resolves. (NORMATIVE) It verifies the signature over the body's claimed content_hash, as RFC-ACDP-0001 §5.8 defines the preimage. A tampered body is caught by content_hash: mismatch, not by this stage. (NORMATIVE) When the key stage is unknown (as-of pass, no_evidence_at_as_of), the signature stage is omitted: there is no key to verify against, and the key: unknown verdict rule (§8.1) already decides the node.
7.5 registry_receipt
absent, ok, or invalid, under RFC-ACDP-0010 §8, with the registry DID document chosen per §6.2. An ok value carries key_status: current or historical.
A receipt that fails is treated as absent everywhere downstream, as RFC-ACDP-0010 §8 requires.
(NORMATIVE) An ok receipt has basis pinned when its registry_did is pinned (§6.3), and registry otherwise. The registry DID snapshot that supplied the receipt key does not make the stage rest on basis observed: the receipt is the registry's own signed attestation, and trusting the registry for it is the RFC-ACDP-0010 trust model. The observed snapshot is still what the pass selects under §6.2. (NORMATIVE) RFC-ACDP-0010 §8 step 5 (key binding) runs only when the key stage locates a producer key. When it locates none (key is unknown, or unresolvable), step 5 is deferred: the receipt is evaluated on its other steps, and the key stage decides the node (§8.1). A receipt that verifies this way keeps its key_fingerprint as the registry's attestation of the signing key, and RFC-ACDP-0014 §7 placement uses it (§7.9). So a node whose key cannot be resolved, but whose receipt names a revoked key, is key_revocation: revoked (and key: revoked, §7.6), not not-revoked. A present receipt for which no registry DID snapshot is selectable is invalid. In the as-of pass, when the only selectable snapshots are dated after T, the value is invalid with reason no_evidence_at_as_of.
7.6 key
current, historical_receipt_attested, revoked, unplaceable, unresolvable, or unknown.
current: the signing key is in the chosen snapshot'sassertionMethod, or the key is adid:key.historical_receipt_attested: RFC-ACDP-0010 §10 applies against the chosen snapshot.revokedandunplaceable: these come fromkey_revocation(§7.9). A revocation verdict overrides this stage.unresolvable: no snapshot, or a snapshot that cannot be evaluated under §5.11 or lacks the signing key, in the latest pass. The as-of-pass equivalent isunknown, with reasonno_evidence_at_as_of. (NORMATIVE) That mapping covers only outcomes that depend on which snapshot is selected. A key that cannot resolve whatever the evidence isunresolvablein both passes, withsignature: unresolvable. Examples are akey_idwhose DID ≠body.agent_id, an empty fragment, or adid:keythat does not decode. Such a body is not a schema failure, so the bundle is not rejected (§4.1).
(0.5.0, Draft) A keyless-signed node (RFC-ACDP-0021) is evaluated with these same values: the bundle carries its acdp:key-attestation in contexts, and the stage is current or historical_receipt_attested with basis registry per RFC-ACDP-0021 §10. A verifier without RFC-ACDP-0021 support reaches unresolvable. This note adds no stage value and changes no Final rule.
7.7 log
absent, included, cannot_verify, or invalid, under RFC-ACDP-0012 §9 and §9.1. An included value carries tree_size, checkpoint_time and witnesses. witnesses is the count of distinct trusted witness DIDs whose cosignatures verify under RFC-ACDP-0015 §8, over a verified checkpoint. A witness is trusted when it is pinned (§6.3).
In the as-of pass, a checkpoint counts only if its timestamp ≤ T. (NORMATIVE) The verifier rebuilds the leaf with key_fingerprint taken from the node's verified registry_receipt (RFC-ACDP-0012 §4 requires them equal), never from the key the key stage locates. The log stage therefore does not depend on the key stage, and with no verified receipt the leaf cannot be rebuilt, which gives cannot_verify.
7.8 retraction
not_retracted, retracted, invalid, or unknown.
- Admitting events. The pass admits a prefix of
lifecycle_events: the longest prefix whose every event is admitted under §6.4 (as-of) or carries a verified signature (latest, RFC-ACDP-0013 §5). In the latest pass, an event with a verified event receipt (§10) has basisregistry. (NORMATIVE) Latest-pass admission rules:- A producer-actor event is verified under the latest snapshot.
- Its key MUST be in that snapshot's
assertionMethod, or else in itsverificationMethodand carry a verified event receipt. That second case is the receipt-attested historical key, by analogy with RFC-ACDP-0010 §10: the receipt'sevent_hashcovers the signature as it was accepted. - An unsigned event whose
actoris the origin registry's DID is admitted only with a verified event receipt, in either pass (§6.4). An event admitted on its signature alone has basisobserved, and theretractionstage takes the weakest basis among the admitted events.
- Deriving the state. RFC-ACDP-0013 §7.1 retraction state is derived over the admitted prefix. (NORMATIVE) The stage's
evidence_timeis theaccepted_atof the last admitted event when that event's receipt verifies; otherwise the stage has noevidence_time. With no admitted events, the stage carries neitherbasisnorevidence_time. unknownin the as-of pass when an event that is not admitted precedes an admitted one. The prefix is then unprovable.invalidwhen §10.3 step 8 fails: the verified event receipts'accepted_atvalues decrease with index. A receipt failing any other §10.3 step (a mismatchedindex,event_hash,registry_didor signature, or step 9) is treated as absent, and its event has no evidence time; that alone never makes the stageinvalid.
7.9 key_revocation
not-revoked, pre-compromise, revoked, unplaceable, or unknown. RFC-ACDP-0014 §7–§8 applies, with these rules:
- Applying revocations. The verifier applies the key-revocation contexts the pass admits (§5.3), under RFC-ACDP-0014 §7 producer scope. (NORMATIVE) A registry-attested revocation (RFC-ACDP-0014 §6) is applied to a node only under that section's strict default scope, read offline: the revocation's
agent_idisdid:web:<authority>, where<authority>is the revocation's ownorigin_registry, and that authority equals the node'sorigin_registryor theregistry_didauthority of the node's verified receipt. Itsmetadata.revoked_key_controllerMUST equal the node'sbody.agent_id. - Discovery records. It considers the discovery records the pass admits. A record is admitted when its
completed_at≤ T in the as-of pass, and always in the latest pass. - Producer outcomes. A producer has a
revokeddiscovery outcome when any admitted revocation applies to it. It hasnot-revokedonly when an admitted record saysnot-revoked-complete. Otherwise its outcome isunknown. (NORMATIVE) The rules for discovery records:- A record never makes the stage
revoked; only an applied revocation context does. - A record with outcome
unknownis ignored. - An admitted record with outcome
revokedmakes the outcomeunknownwhen no admitted revocation applies, because a vantage saw a revocation the bundle does not carry. - Otherwise, the deciding record is the admitted
not-revoked-completerecord that is greatest by (vantage pinned,completed_at, array position). It supplies the stage'sevidence_time(itscompleted_at) and itsbasis(pinnedorobserved). (NORMATIVE) Revocation contexts in the bundle that do not name the key are not evidence of not-revoked: the bundle is untrusted (§4.1), and its exporter can leave the revoking context out. Only a discovery record can decidenot-revoked.
- A record never makes the stage
- Precedence is RFC-ACDP-0014's own: refusal, then
unknown, thenpre-compromise, thennot-revoked. - Basis. The value carries the basis of the evidence that decided it. A revocation that decided it is
registry. A discovery record that decided it isobserved.
8. Verdicts and Aggregation (NORMATIVE)
8.1 Node verdict
Each pass yields one node verdict. The first rule that matches decides it:
-
refusedwhen any of these hold:presenceiscycleornot_yet_published;identity: mismatch;content_hash: mismatch;signatureisinvalidorunresolvable;keyisrevoked,unplaceableorunresolvable;key_revocationisrevokedorunplaceable.
-
unknownwhen any of these hold:presenceiswithheld,unresolvedorabsent;key: unknown;key_revocation: unknown;retractionisunknownorinvalid;registry_receipt: invalidorlog: invalid.
-
ok_with_caveatswhen any caveat holds. The caveats are:undated;identity: unbound;keyishistorical_receipt_attested;key_revocation: pre-compromise;registry_receipt.key_status: historical;retraction: retracted;log: cannot_verify;- any stage resting on basis
observed.
The verdict lists the caveats that hold, as the schema's tokens, sorted ascending by UTF-8 bytes. (NORMATIVE)
caveatsis computed from the stages independently of which rule decides the verdict, so arefusedorunknownnode also lists every caveat that holds for it. In the order of the list above, the tokens areundated,identity_unbound,historical_key,pre_compromise,historical_receipt_key,retracted,log_cannot_verifyandobserved_basis. -
okotherwise.
retracted is a caveat, not a refusal. A retracted body stays valid and retrievable (RFC-ACDP-0013 §8.1). Whether to rely on it is the consumer's policy, and §8.3 supplies what that policy needs.
8.2 Chain verdict
Each pass also yields one chain verdict: the worst node verdict, in the order refused > unknown > ok_with_caveats > ok. A pass with complete: false has a chain verdict no better than unknown.
8.3 Tainted paths
For each node whose node verdict is refused, or whose retraction is retracted, the verdict lists one tainted path: the shortest path over the walk graph (§6.1(6)) from any root to that node, as a ctx_id array. Ties are broken by comparing the arrays lexicographically, element by element, in UTF-8 byte order.
Tainted paths do not change any node verdict. A descendant of a retracted or refused node keeps its own verdict, and policy acts on ancestry without re-walking.
8.4 Drift
When both passes run, the verifier emits one drift entry, {ctx_id, kind}, for each kind below that holds for a node. A stage difference that matches no kind and leaves the node verdict unchanged emits no entry. (NORMATIVE) A stage the as-of pass did not run (because the node was not present as of T) counts as not having reached the latest value, so for example a node that is not_yet_published as of T and retracted latest emits both published_after_as_of and retracted_after_as_of. The kinds are:
| Kind | Meaning |
|---|---|
published_after_as_of | The node is not_yet_published as of T. |
revoked_after_as_of | key_revocation becomes revoked or unplaceable only in the latest pass. |
retracted_after_as_of | retraction becomes retracted only in the latest pass. |
republished_after_as_of | retraction is retracted as of T and not_retracted in the latest pass. A stage the as-of pass did not run does not count as retracted, and unknown or invalid on either side never triggers it. |
backdated_event | An event whose occurred_at ≤ T but whose accepted_at > T. This is reported in addition to the retraction kinds; the backdating is reported, not believed. |
as_of_unprovable | A stage carries reason no_evidence_at_as_of as of T (only key or registry_receipt can, §7), and is decided in the latest pass. A key_revocation: unknown stage carries no reason and never triggers it. |
verdict_changed | The node verdict differs between the passes and no kind above holds for the node. |
A clean key rotation after T is not drift. Rotation is not compromise (RFC-ACDP-0014 §9). It shows up as key: current as of T and historical_receipt_attested latest, which is a caveat change and therefore verdict_changed only if the node verdict itself changes.
9. The Verdict Object: acdp-chain-verdict/1 (NORMATIVE)
9.1 Shape
The verdict is validated by acdp-chain-verdict.schema.json. Its members are:
verdict_version: the literalacdp-chain-verdict/1.roots: sorted and deduplicated.as_of: T, ornull.horizon.limits:max_depth,max_nodesandmax_fanout.require_attested_events.options_digest:sha256:+ hex(SHA-256(JCS(pinned))) over the normalized object of §6.3, ornullwhenpinnedis absent or{}.evidence_digest:sha256:+ hex(SHA-256(JCS(bundle))).complete,controlandfrontier(sorted).nodes: sorted byctx_id. Each node carriesdepth(the shortest distance from a root),derived_from(as in the body), and anas_ofand alatestobject. Each pass object holds the §7 stages and the §8.1 verdict and caveats.chain:{as_of, latest}chain verdicts.tainted_paths: underas_ofandlatest, each sorted.drift: sorted byctx_id, then bykind.as_of_digestandverdict_digest.
When as_of is null, every as_of member is null and drift is empty. A member that does not apply (for example, the stages of a node that is not present, or key_status on a failed receipt) is omitted, never null. The only nullable members are those the schema declares nullable. This keeps the JCS bytes unique.
9.2 Digests
-
verdict_digest=sha256:+ hex(SHA-256(JCS(verdict withoutas_of_digestandverdict_digest))). It identifies the full result, over this exact bundle. -
as_of_digest=sha256:+ hex(SHA-256(JCS(the as-of projection))). The projection is the object made of these members only:verdict_version,roots,as_of,limits,require_attested_eventsandoptions_digest;complete,controlandfrontier;nodes, each reduced toctx_id,depth,derived_fromandas_of;chainandtainted_paths, each reduced to itsas_ofmember and kept nested:{"chain": {"as_of": …}, "tainted_paths": {"as_of": …}}.
It is
nullwhenas_ofisnull.
as_of_digest is the value to seal at T. It omits evidence_digest, horizon and everything from the latest pass, so it is stable as evidence accumulates after T. The bodies themselves are immutable, and later receipts, events, revocations and snapshots fall outside the as-of pass. An auditor holding a later bundle reruns verify_chain with the same T, limits and options, and compares as_of_digest.
A mismatch means the auditor's bundle differs from the sealer's in evidence dated ≤ T. The as-of projection pins exactly the stages that rest on that evidence, so the mismatch names what to examine. Evidence dated ≤ T that rests on basis observed can be fabricated by whoever produced the bundle. That is why such evidence is labelled, and why a sealer that witnessed the evidence SHOULD pin it (§6.3).
9.3 Determinism (NORMATIVE)
Given the same roots, bundle bytes and options, and no fired time budget, every conformant implementation MUST emit a verdict whose JCS serialization is byte-identical. In particular, its as_of_digest and verdict_digest MUST be equal.
Implementations MUST NOT add members to the verdict object. Diagnostics, durations and fetch logs belong outside it.
10. Lifecycle Event Receipts (amends RFC-ACDP-0013, NORMATIVE) (0.5.0, Draft; §10.3 consumer verification Final)
10.1 Issuance
A registry that advertises both acdp-registry-lifecycle and acdp-registry-receipts, and declares acdp_version ≥ 0.5.0, MUST mint an event receipt when it appends any lifecycle event: producer-initiated (RFC-ACDP-0013 §6) or registry-initiated. The receipt is minted atomically with the append.
{
"receipt_type": "acdp-event-receipt/1",
"registry_did": "did:web:registry.example.com",
"ctx_id": "acdp://registry.example.com/12345678-1234-4321-8123-123456781234",
"event_id": "018f6d0a-7b2e-7c4d-9e1f-3a5b7c9d1e2f",
"event_hash": "sha256:…",
"index": 0,
"accepted_at": "2026-07-04T09:15:43.120Z",
"signature": { "algorithm": "ed25519", "key_id": "did:web:registry.example.com#receipt-key-1", "value": "…" }
}The members are:
event_hash:sha256:+ hex(SHA-256(JCS(the event, including itssignature))). It binds the exact event object as stored.index: the event's zero-based position inlifecycle_events.accepted_at: the registry's clock when it accepted the event. The registry MUST NOT backdate it; it MUST be ≥ theaccepted_atof the event atindex - 1, and ≥ the context'screated_at.signature: under the RFC-ACDP-0010 §5 construction, verbatim, with the receipt signing key. The preimage is the JCS canonicalization of the object minussignature.
The object is closed (acdp-event-receipt.schema.json).
10.2 Serving
The registry serves event receipts in registry_state.event_receipts, an array parallel to lifecycle_events: element i is the receipt for event i. A registry bound by §10.1 MUST include event_receipts wherever it serves lifecycle_events.
The array is append-only under the same discipline as lifecycle_events (RFC-ACDP-0013 §4.1). An idempotent retry (RFC-ACDP-0013 §6) returns the receipt already minted. It MUST NOT mint a second one.
10.3 Verification
A consumer verifies event receipt i against event i:
- The schema validates and
receipt_typematches. registry_didisdid:web:<authority>, where<authority>equals the context'sorigin_registry.ctx_idequals the context'sctx_id.event_idequals the event'sevent_id.indexequals i.event_hashrecomputes.- The signature verifies under a key in the registry DID document's
verificationMethod, chosen per §6.2. The RFC-ACDP-0010 §9 receipt-key lifecycle applies. accepted_atis non-decreasing in i, compared over the receipts that pass steps 1–7 and 9. A receipt failing any of those is treated as absent first and takes no part in this comparison.accepted_at≥ the context'screated_at.
A receipt that fails any step is treated as absent. Step 8 failing on any pair makes the context's retraction stage invalid (§7.8).
10.4 What this fixes
An event receipt gives a lifecycle event a provable evidence time (§5.1). With it, as-of retraction state has a meaning. Without it, occurred_at is a claim by the actor and array order carries no clock, so as-of retraction state is unknown under the default require_attested_events: true.
11. Receipts on Lineage Members (amends RFC-ACDP-0004 §5.1, NORMATIVE) (0.5.0, Draft)
A registry that advertises acdp-registry-receipts and declares acdp_version ≥ 0.5.0 MUST include registry_receipt in every member of GET /lineages/{lineage_id}, exactly as in full retrieval (RFC-ACDP-0004 §2.1).
A consumer folding a key-revocation lineage (RFC-ACDP-0014 §7), or an exporter (§4.3), then needs one request instead of one per member. The RFC-ACDP-0014 §8 re-retrieve-by-ctx_id path remains the fallback for registries below 0.5.0.
12. Reserved: event leaves in the transparency log (NON-NORMATIVE)
A future amendment to RFC-ACDP-0012 MAY add a leaf kind that binds an event receipt's hash. That would make lifecycle events visible to log auditors as publishes already are. It is deliberately not specified here: the as-of semantics of this document depend only on §10. It is recorded in RFC-ACDP-0009 terms as deferred, for this reason: a leaf-format change regenerates the log-* goldens and should travel with its own fixtures.
13. Profile and Capabilities
- Profile.
acdp-consumer-chain, registered inregistries/profiles.md, with prerequisiteacdp-consumer. It is OPTIONAL. Claiming it commits a consumer to:- §4 bundle handling;
- §6 walk determinism;
- the §7 stages;
- the §8 aggregation;
- the §9 verdict and digests.
- Registries. The §10 and §11 obligations bind registries through
acdp_version≥0.5.0together with their existing profiles. There is no new registry profile and no new capability field. - Errors. None. Chain verification is consumer-side and has no wire surface beyond §10 and §11.
14. Conformance Fixtures
| Fixture | Covers |
|---|---|
chain-001 | 3-node DAG with a join, all did:key, receipts and complete discovery: both passes ok_with_caveats [observed_basis], because discovery is observed; with the vantage pinned (B), ok; no drift (§6.1, §6.3, §8) |
chain-002 | Revocation published after T: as-of ok_with_caveats (observed discovery), latest refused, drift revoked_after_as_of (§5.3) |
chain-003 | Retraction with an event receipt after T: as-of not_retracted, latest retracted, drift retracted_after_as_of (§7.8, §10) |
chain-004 | Backdated retraction (occurred_at ≤ T < accepted_at): same as chain-003, plus backdated_event (§8.4) |
chain-005 | A cycle is presence: cycle, refused; a DAG join is not a cycle (§6.1) |
chain-006 | Fanout truncation: complete: false, control: max_fanout, frontier pinned (§6.1; offline fed-010) |
chain-007 | did:web snapshot selection before and after T; observed vs pinned basis; no_evidence_at_as_of (§6.2, §6.3) |
chain-008 | Tampered body, key ≠ body.ctx_id, and a node missing from contexts and absent from omitted: content_hash: mismatch, identity: mismatch, absent vs withheld (§4.2, §7) |
chain-009 | Event-receipt verification (consumer): wrong index, wrong event_hash, wrong registry, decreasing accepted_at. Issuance and the idempotent retry that mints no second receipt are registry-side only (acdp-registry-lifecycle) (§10) |
chain-010 | Digest stability: the same bundle and options give the same verdict_digest across implementations and across online/offline modes; evidence added after T changes verdict_digest but not as_of_digest (§9) |
chain-011 | 27 cross-implementation edge vectors: each pinned bundle and options reproduce the pinned verdict byte for byte (§9.3) |
chain-001, chain-002, chain-003 and chain-010 scenario A, and every vector of chain-011, carry known-answer vectors: a pinned bundle (examples/evidence/), a pinned verdict (examples/chain-verdict/) and their evidence_digest, as_of_digest and verdict_digest. scripts/conformance-runner.py recomputes the three digests and the as-of projection, and make json-validate validates both files against their schemas. A conformant implementation MUST reproduce each pinned verdict byte for byte from the pinned bundle and options. The chain-001..010 known answers come from the first implementation (acdp-rs acdp-chain). The chain-011 known answers are verdicts that acdp-rs and acdp-verifier-py each produced byte-identically from the same bundles. Every other scenario is behavioral.
15. Security Considerations
- Bundles are adversarial input. Size limits matter. Verifiers SHOULD bound the bundle byte size and the number of snapshots per DID, and MUST apply the §6.1 walk limits regardless of how many contexts the bundle carries.
observedis the weak point, and it is labelled. A DID document has no signature and no history, so ACDP cannot prove what adid:webdocument said at T. Whoever produced the bundle can fabricate a snapshot dated ≤ T. Mitigations:- the
observedcaveat; - pinning by a party that actually witnessed the snapshot (§6.3);
did:keyproducers, whose keys areself.
- the
- Backdating is reported, not believed.
occurred_atnever orders anything (RFC-ACDP-0013 §4.1). §10 gives events a registry-attested clock. A registry that backdatesaccepted_atis caught by the §10.1 monotonicity rule only relative to its own earlier receipts. The transparency-log leaf kind reserved in §12 would close the rest of that gap. - No wall clock. A verifier that substitutes its own clock for the horizon makes verdicts depend on when they are computed. That breaks §9.3, and with it the auditor's ability to recompute.
16. References
- RFC-ACDP-0001 §5.3, §5.6, §5.7, §5.8, §5.11 — time, lineage, content hash, signature, key resolution
- RFC-ACDP-0004 §2.1, §5.1 — retrieval, lineage listing
- RFC-ACDP-0006 §4.1 — traversal controls, partial-walk reporting
- RFC-ACDP-0010 §5, §8, §9, §10 — receipt construction, verification, key lifecycle, historical keys
- RFC-ACDP-0012 §9 — log verification
- RFC-ACDP-0013 §4–§8 — lifecycle events and retraction
- RFC-ACDP-0014 §7–§9 — revocation semantics, discovery
- RFC-ACDP-0015 §8 — witness cosignature verification
- [RFC 8785] JSON Canonicalization Scheme
- [RFC 2119], [RFC 8174]