did-btcr2-js

ADR 025: Sans-I/O Updater State Machine for the DID Write Path

Status: Accepted

Date: 2026-04-13

Commit: d82a13f

Context

ADR 016 introduced the sans-I/O state-machine pattern for the read path (Resolver). The DID write path: DidBtcr2.update({...}): was still a monolithic async function that directly:

This had the same problems the Resolver pattern was created to solve: I/O and protocol logic interleaved, hard to test without mocks, and consumers couldn’t insert their own key-handling semantics (hardware wallets, multisig prompts) without monkey-patching the library.

Options considered

  1. Keep the monolithic async update() and live with the coupling.
  2. Split update() into free functions (construct, sign, fund, broadcast) the caller composes manually. Would work but puts the phase-sequencing burden on every caller.
  3. Sans-I/O state machine mirroring Resolver: emit typed DataNeed requests for the caller to fulfill, sequence phases internally.

Decision

Option 3. Add Updater in packages/method/src/core/updater.ts. It mirrors the Resolver pattern with a write-path-appropriate phase sequence and its own DataNeed discriminated union:

Phases: Construct to Sign to Fund to Broadcast to Complete.

Data needs:

Static utility methods on Updater: Updater.construct(), Updater.sign(), Updater.announce(): expose each step individually for scripts that don’t want the full state machine (e.g. test-vector generation in lib/generate-vector.ts).

Factory validation. DidBtcr2.update({...}) validates that verificationMethodId is in capabilityInvocation and that beaconId matches a service in the source document before returning an Updater. Invalid inputs fail fast at the factory, not mid-state-machine.

Consequences

Positive

Negative

Explicitly accepted trade-offs

References