did-btcr2-js

ADR 060: Carry the Version Counter and Update-Hash History Across Resolver Discovery Rounds

Status: Accepted

Date: 2026-06-29

Branch / PR: fix/resolver-cross-round-versioning

References: ADR 016, ADR 059, did:btcr2 Resolve, W3C DID Resolution

Context

The did:btcr2 read algorithm resolves a DID by processing beacon signals in a single loop. It keeps two pieces of state for the whole loop: a monotonic current_version_id (starting at 1) and an update_hash_history (appended to as each update is applied, and indexed when confirming a duplicate). On every pass it re-derives the beacon set from the contemporary DID document, so a beacon added by one update is searched on the next pass. The version counter and the history are therefore loop-invariant: they span the entire resolution, including signals found on beacons that earlier updates introduced.

This implementation’s Resolver (ADR 016) is a sans-I/O state machine. It cannot block to fetch signals, so it splits that one loop into discovery rounds: apply the updates found so far, look for beacon services those updates added, then loop back to request their signals. The per-round update application lived entirely inside the static Resolver.updates(), which initialized currentVersionId = 1 and a fresh updateHashHistory = [] on every call. The two pieces of state the spec keeps for the whole loop were being reset once per round.

The consequence is a real resolution failure. Consider a legitimate linear history:

Round one finds the v2 update, applies it (currentVersionId 1 to 2), and discovers beacon B. Round two finds the v3 update, but Resolver.updates() has reset currentVersionId to 1, so it sees targetVersionId 3 against a counter of 1, takes the targetVersionId > currentVersionId + 1 branch, and raises LATE_PUBLISHING. A conformant DID fails to resolve the moment its history is spread across the beacons it builds up. ADR 059 noted this under its Context and left it for its own change; this is that change.

The reset also meant metadata.versionId reported only the last round’s local count rather than the document’s true version, and updateHashHistory was empty in every round after the first, so a duplicate update republished on a later-round beacon could not be confirmed against its history.

Decision

1. Lift the version counter and update-hash history to resolution-wide state

currentVersionId and updateHashHistory are now instance fields on the Resolver (#currentVersionId, initialized to 1; #updateHashHistory, initialized to []). They model exactly what the spec models: state that persists for the whole resolution, not for a single batch of signals.

2. Thread that state through Resolver.updates()

Resolver.updates() takes an optional fifth parameter, resolutionState: { currentVersionId, updateHashHistory }, defaulting to { currentVersionId: 1, updateHashHistory: [] }. It continues the counter from the carried value and appends to the carried history array (shared by reference, so appends are visible to the next round). The ApplyUpdates phase passes the instance fields in and carries the reached version forward by reading it back from the response’s metadata.versionId, which the algorithm already sets at every return point. Standalone callers (for example test-vector generation) omit the parameter and get the spec’s fresh start, so their behavior is unchanged.

The net effect: a resolution split across N discovery rounds now behaves identically to processing the same signals in one continuous loop. The rounds are an I/O-scheduling detail, no longer a semantic boundary.

Consequences

Rejected alternatives

Out of scope

The per-tuple currentVersionId increment in Resolver.updates() is unconditional, which matches the spec’s “Process updates Array” step that increments the counter after the duplicate-or-apply check rather than only on apply. This change does not alter that behavior; it only stops the counter and history from resetting between rounds. Any question about how the read algorithm increments on a confirmed duplicate is a separate, pre-existing matter that this fix neither introduces nor worsens, and is left for its own review.