did-btcr2-js

ADR 112: The Update Paths Check the Proof Fields and the Proof Time Window, Accept an Embedded Verification Method, Apply the JSON Patch Strictly, and Raise INVALID_DID_UPDATE

Context

Four specification pull requests define the checks of the update read path and the update write path after ADR 109 and ADR 111:

The implementation at method 0.63.0 differed on every point:

Decision

The read path checks each proof field by string equality before it verifies the signature. applyUpdate raises a ResolveError of type INVALID_DID_UPDATE unless proof.type equals "DataIntegrityProof", proof.cryptosuite equals "bip340-jcs-2025", proof.proofPurpose equals "capabilityInvocation", proof.capabilityAction equals "Write", and proof.capability equals urn:zcap:root:${encodeURIComponent(currentDocument.id)}. The checks run after the @context checks (ADR 109) and before the method lookup. The error message names the field. The root capability is not derived and not invoked: the specification makes the derivation optional, and string equality is the rule it states. Appendix.dereferenceZcapId stays as a public utility, with a length check that counts the components of the split.

Both paths locate the verification method through the entry of capabilityInvocation. Two helpers in Appendix implement the rule of the specification. capabilityInvocationEntry(document, methodId) returns the entry that identifies the method id, in the reference form or the embedded form, or undefined. verificationMethodOfEntry(document, entry) returns the embedded object, or the member of verificationMethod whose id equals the reference, or undefined. Both spellings of a DID URL match (ADR 091). The read path raises INVALID_DID_UPDATE with the message “not authorized for capabilityInvocation” when no entry identifies the method, and INVALID_DID_UPDATE with the message “not found” when a reference names no member. The write path (DidBtcr2.update) raises an UpdateError of type INVALID_DID_UPDATE for the same two conditions; before, the type was INVALID_DID_DOCUMENT. DidBtcr2.getSigningMethod also searches the embedded methods of the verification relationships, after verificationMethod; its error type does not change.

The read path checks the proof time window against the block of the Beacon Signal. applyUpdate takes the block metadata of the tuple. When proof.created is present, it must be an XML Datetime and must not be after the header time of the block. When proof.expires is present, it must be an XML Datetime and must not be before the block mediantime. When both are present, expires must not be before created. Each comparison is in milliseconds with no tolerance. Each failure raises INVALID_DID_UPDATE. The write path sets neither field; this decision does not add them.

Both paths apply the JSON Patch strictly and raise INVALID_DID_UPDATE. JSONPatch.apply in the common package takes a strict option. With strict: true, the operation validation of the common package requires an RFC 6902 op, a value for add, replace, and test, and a from for move and copy; and fast-json-patch validates each operation against the document, so a remove or a replace of a missing path, a move or a copy from a missing path, an add under a missing parent, and a failed test fail the patch at the first failing operation. The default stays false, so the other callers of the common package do not change; the default flips at the next major version of the common package. Both method paths pass strict: true and wrap the failure: UpdateError on the write path, ResolveError on the read path, both of type INVALID_DID_UPDATE, with the message of the failing operation. After the patch, both paths check that the id did not change and that the document conforms to DID Core; the read path now raises ResolveError of type INVALID_DID_UPDATE for both, as the write path did.

Every failure of an update path that the specification names raises INVALID_DID_UPDATE. The method package wraps the errors of the cryptosuite, the multikey, the hash decoder, and the common package inside applyUpdate, Updater.construct, and Updater.sign. The wrapped error carries the type and the message of the inner error in data.cause. The cryptosuite stays method-agnostic (ADR 054) and keeps its own error types. The typed-error policy of ADR 085 applies: no raw Error on either path.

Updater.sign verifies the proof before it returns. After addProof, the method builds a multikey from the verification method (the published publicKeyMultibase, not the signer) and verifies the signed update with the capabilityInvocation purpose. A failure raises an UpdateError of type INVALID_DID_UPDATE before the state machine emits NeedFunding, so a signer that returns a wrong signature spends no beacon UTXO.

The method package exposes deactivate. DidBtcr2.deactivate({ sourceDocument, sourceVersionId, verificationMethodId, beaconId }) returns the Updater of DidBtcr2.update with the patch [{ "op": "add", "path": "/deactivated", "value": true }]. The method package exports the constant DEACTIVATION_PATCH, and DidMethodApi.DEACTIVATION_PATCH becomes a reference to it. The method-level operation does not refuse a deactivated source: ADR 100 keeps that product guard in the api, at the chokepoint that every api write path passes through.

capability and capabilityAction are required in the update proof types. Btcr2DataIntegrityProof and Btcr2DataIntegrityConfig require both fields, as the Data Integrity Config data structure does.

DidBtcr2.update validates sourceVersionId. The value must be an integer that is at least 1. Every other value raises an UpdateError of type INVALID_DID_UPDATE before any other check.

Scope boundary

Consequences

Positive. The read path applies the rule of “Check update.proof” field by field: a proof that names the "Read" action, a capability in another spelling, or a wrong type fails with the typed error and a message that names the field. A document that embeds its update key in capabilityInvocation resolves and updates. A patch that a lenient library applies with no effect fails at construction, before it is signed and announced; before, such an update resolved here and failed on a strict resolver. A proof with a time window outside the block fails. A signer that returns a wrong signature is refused before funding. An api or cli consumer can match one error type, INVALID_DID_UPDATE, for every update failure that the specification names.

Negative (breaking, method MINOR at 0.x). The write path raises INVALID_DID_UPDATE where it raised INVALID_DID_DOCUMENT for a method that is not authorized or not found. The read path raises ResolveError of type INVALID_DID_UPDATE where it raised MethodError of type JSON_PATCH_APPLY_ERROR, DidDocumentError of type INVALID_DID_DOCUMENT, or a cryptosuite error. An update whose patch removes or replaces a missing path fails on both paths; before, it passed. An update whose proof carries created or expires outside the block window fails to resolve; the packages of this repository never emitted those fields. The proof types require capability and capabilityAction. A sourceVersionId that is not a positive integer is refused. The common package change is additive (MINOR): the strict option defaults to false.

Unchanged. The signed update format, the signal bytes, the proofs, and the test-suite vectors: this decision changes what the paths check, not what they emit. The @context pin (ADR 109). The membership rule and the relative DID URL matching (ADR 091). The signing-key comparison (ADR 051). The resolver loop and the resolution options (ADR 111). The api deactivate guard (ADR 100).

Superseded. The rule that ADR 088 quotes (“locate the method in current_document.verificationMethod”) is superseded by pull request 351; the decision of ADR 088 (membership before signature verification) stands. The placement of the deactivation patch constant in the api (ADR 094) is superseded by the method export; the api operation and its guard stand.

Implementation

References