Validation of a did:plc audit log — the operation history the directory
serves from /:did/log/audit.
A log is a chain, not a list. Each operation names its predecessor by CID, the first derives the DID from its own bytes, and every operation is signed by one of the rotation keys its predecessor declared. Validating a log means walking that chain and checking each link.
Nullification
Rotation keys are ordered by descending authority, and a higher-authority key may fork the log from an earlier point, nullifying the operations it displaces. Two rules bound that power:
- the forking operation must be signed by a strictly higher-authority key than the one that signed the first operation it nullifies; and
- it must land within 72 hours of that operation — inclusive, as
log_nullification_at_exactly_72h.jsonpins down.
Nullified operations stay in the log, flagged; they simply stop being part of the active chain, and nothing may build on them.
Tombstones
A plc_tombstone permanently deactivates the DID. Nothing may follow one —
though a tombstone may itself be nullified, which is what
log_nullified_tombstone.json covers.
Why validation is two passes
Signatures and chaining are checked for every operation; the shape rules
(rotationKeys of 1–5, no duplicates, and so on) are checked only for
operations that survive on the active chain.
That split is not fussiness — it is what the corpus requires.
log_nullification.json is a valid log whose middle operation carries
rotationKeys: []: someone emptied their rotation keys, locking the
identity, and a higher-authority key forked it away inside the recovery
window. That is precisely the situation the window exists for, so the log
is valid even though a displaced operation in it is not something we would
ever emit. log_empty_rotation_keys.json is the same operation left
standing on the active chain, and is invalid.
(The directory has historically accepted operations that do not satisfy its own rules — see did-method-plc issue #109 — so "the directory stored it" is not evidence of validity.)
Strictness vs the directory
Shape rules follow the spec, which in places is stricter than what the
production directory actually enforces: the spec allows at most 5 rotation
keys while the directory accepts up to 10 (MAX_ROTATION_ENTRIES in its
constraints.ts), and the spec's 7,500-byte operation cap is enforced at
4,000. A legitimate directory-stored log can therefore fail validation
here. That is deliberate — this module validates what an operation
should be, not everything the directory has ever admitted — but callers
screening arbitrary directory logs should know the divergence exists.
Malformed input
Entries are validated before they are trusted: cid must be present (and
is recomputed from the operation bytes — a log is a content-addressed
chain, so a reported CID that does not match its own operation is
malformed however well it links), operation must be a map, every
entry's did must agree with the DID the genesis operation derives, and
timestamps must be strictly increasing along the chain — the reference
implementation enforces the same.
Summary
Functions
Validate a complete audit log.
Validate a log and check the computed nullification against the flags the directory reported, so a disagreement is an error rather than a silent divergence.
Types
Functions
Validate a complete audit log.
Returns {:ok, nullified_cids} — the set of operation CIDs the log's own
rules nullify — or {:error, reason}.
The returned set is what lets a caller cross-check the directory's own
nullified flags rather than trusting them.
Validate a log and check the computed nullification against the flags the directory reported, so a disagreement is an error rather than a silent divergence.