Construction and validation of did:plc operations.
An operation is a plain map with string keys — the shape the directory serves and the shape DAG-CBOR encodes — rather than a struct, because the bytes are what a DID is derived from and what a signature covers. Keeping the map verbatim means a fetched operation round-trips without a lossy conversion in the middle.
Operation types
"plc_operation"— the modern operation, carryingalsoKnownAs,verificationMethods,rotationKeysandservices."plc_tombstone"— permanently deactivates the DID. Carries onlytype,prevandsig; it cannot record a move target."create"— the legacy genesis form, still present in real audit logs (seelog_legacy_dholms.json). Accepted for validation and normalized bynormalize/1; never emitted.
Strictness
The directory tolerates no extra fields, and neither do we: validate/1
rejects any key outside the type's fixed set. rotationKeys must hold 1–5
unique did:keys on secp256k1 or P-256 — the only two curves the method
permits for rotation — and services keys carry no # prefix.
prev is explicitly null for a genesis operation. It is present and
null, never omitted; an omitted prev changes the DAG-CBOR bytes and
therefore the DID.
Summary
Functions
Is this operation a genesis (no predecessor)?
Build an unsigned plc_operation.
Build an unsigned plc_tombstone pointing at prev.
Normalize a legacy create operation into the modern plc_operation shape.
The rotation keys that may sign the next operation after this one.
The DAG-CBOR bytes of a complete, signed operation — what its CID and, for a genesis operation, its DID are derived from.
Is this operation a tombstone?
The DAG-CBOR bytes an operation's signature covers: the operation with
sig omitted entirely (not nulled).
Validate an operation's shape.
Types
Functions
Is this operation a genesis (no predecessor)?
@spec new(build_opts()) :: {:ok, t()} | {:error, term()}
Build an unsigned plc_operation.
Returns the operation without a sig key — pass it to
Exosphere.ATProto.Identity.DID.PLC.Signer.sign/3.
Options
:also_known_as— URIs, e.g.["at://alice.example.com"](no duplicates):verification_methods— map of service id (no#) todid:key:rotation_keys— 1–5 uniquedid:keys, highest authority first:services— map of service id (no#) to%{"type" => _, "endpoint" => _}:prev— CID string of the previous operation, ornilfor genesis
Examples
iex> {:ok, op} = Operation.new(rotation_keys: ["did:key:zQ3sh..."], prev: nil)
iex> op["prev"]
nil
Build an unsigned plc_tombstone pointing at prev.
A tombstone permanently deactivates the DID. It carries no data fields, so it cannot record where an identity moved to — that record has to live somewhere else.
Normalize a legacy create operation into the modern plc_operation shape.
Modern operations are returned unchanged. The normalized form is for interpretation only — never hash or verify it, because a legacy operation's DID and signature are derived from its own original bytes.
The rotation keys that may sign the next operation after this one.
Legacy operations are normalized first, so a create yields
[recoveryKey, signingKey] in that authority order.
The DAG-CBOR bytes of a complete, signed operation — what its CID and, for a genesis operation, its DID are derived from.
Is this operation a tombstone?
The DAG-CBOR bytes an operation's signature covers: the operation with
sig omitted entirely (not nulled).
prev is left as the string CID it already is — DAG-CBOR string-encodes
it rather than emitting an IPLD link, which is the trap this function
exists to avoid re-introducing.
Validate an operation's shape.
Checks the field set is exactly the one its type permits, that prev is
present (possibly null), and — for plc_operation — the alsoKnownAs,
rotationKeys, verificationMethods and services rules.
Signature validity is a separate question; see
Exosphere.ATProto.Identity.DID.PLC.Signer.verify/3.