Reorgs, Confirmations, and Finality in DID Resolution

Sep 18, 2026

Introduction

Last week covered getting Beacon Signals out of Bitcoin. This week covers a question that follows immediately and that most implementations answer by accident: once you have found a signal, when do you believe it?

Bitcoin does not offer finality. It offers a probability that increases with depth. Every property a did:btcr2 deployment inherits from Bitcoin inherits that caveat too, including the answer to "what is the current version of this DID document." A resolver that treats the chain tip as authoritative has built something that is usually right and occasionally, silently, wrong.

The interesting part is that the standard payments answer, wait six confirmations, is not the right answer here, and the reason is worth understanding.

What a Reorg Does to a DID

Recall how ordering works. A BTCR2 Signed Update contains no block height and no transaction id. Anchoring is external, through Beacon Signals, and the resolver derives the height during resolution. Updates are ordered by targetVersionId, with block height serving as a tiebreaker.

A reorg changes derived heights. A Beacon Signal confirmed in block N can end up in block N+2 on the winning chain, or in no block at all if the transaction did not make it into the replacement blocks.

Three things follow.

An update can un-happen. A signal that was confirmed becomes unconfirmed. The document version that update produced is no longer current. A resolver that resolved five minutes ago and cached the result now holds a document that does not reflect the chain. Nothing notifies it.

Height ordering shifts. Where the ordering rules fall back to block height, a reorg that changes relative heights can change the outcome. Consider the contested-rotation case from the rotation post, where a legitimate controller and an attacker have both published updates claiming the same version. On the default resolution path the tiebreak does not pick a winner. Whichever update applies first, the other fails the update_hash_history comparison and resolution halts with LATE_PUBLISHING, regardless of which block each landed in. What a reorg changes is quieter: the confirmations value in the DID document metadata, and the outcome of a versionTime or pinned versionId request, which can resolve early once the first of the two updates has applied and therefore does depend on which landed first.

Detection is not automatic. btcr2's late-publishing checks catch inconsistent history, not stale view. A resolver holding a document one version behind the chain sees nothing wrong, because what it holds is internally consistent. It is simply out of date, and it has no way to notice from the data alone.

The Floor the Specification Sets

Before designing a policy, know what the method already decided for you. The resolve algorithm is normative on two points:

A transaction MUST be included in a Bitcoin block and have at least resolutionOptions.minConf confirmations (6 when not provided). Unconfirmed mempool transactions MUST NOT be processed.

So the depth question is already parameterized. minConf is a resolution option, the default is six, and a conforming resolver may be asked for a different number by its caller. That is the right shape: the method declines to hardcode a threshold and hands the decision to whoever knows what the resolution is for.

The mempool clause is stricter and admits no tuning. A conforming resolver may not build a document from an unconfirmed Beacon Signal, at any depth policy, for any reason. Everything below about mempool visibility is therefore about monitoring and alarms, not about resolution. Getting that boundary wrong produces an implementation that is fast, useful, and non-conforming.

Why the Payments Heuristic Does Not Transfer

Six confirmations is a payments heuristic. It comes from reasoning about an attacker who wants to reverse a payment: you released goods, they rewrote the chain, they got the goods and kept the coins. Depth makes the rewrite expensive relative to what is being stolen.

DID resolution has a different risk shape, and the difference is that the loss is not symmetric with the transaction value.

There is nothing to double-spend. An attacker who reorgs out your Beacon Signal does not gain a coin. What they gain is that a verifier somewhere continues to treat an older document version as current. Whether that is worth anything to them depends entirely on what changed between the versions, and it can be worth a great deal while the transaction itself moved dust.

Which produces the observation this post is really about: the safe direction depends on what the update does.

Consider two updates, both one confirmation deep.

Update A adds a new key to capabilityInvocation. If you act on it and it reorgs out, you have accepted a key that has no authority. That is bad. The conservative response is to wait for depth before honoring it.

Update B removes a compromised key. If you act on it and it reorgs out, you have stopped accepting a key that is in fact still authorized. That is inconvenient and safe. If you wait on it and the attacker uses the window, that is the actual harm. The conservative response is to honor it as early as the specification lets you, which means a low minConf rather than the default six.

Same depth, opposite policies. A single global confirmation threshold gets one of these two cases wrong no matter what number you pick.

The generalizable rule: grant authority at depth; remove authority at the shallowest confirmed depth you are willing to accept.

minConf cannot express that split by itself. It gates whole transactions, so it can only select a prefix of the update history, and the resolve algorithm checks the hash of the fully patched document against update.targetHash, so no conforming resolver can honor half a patch. The asymmetry has to live above the resolver: resolve twice, once at your deep threshold and once at your shallow one, and honor a key only if it appears in both. A key present only in the shallow view has been added but is not yet granted. A key missing only from the shallow view has been removed and is already gone. That holds whether the addition and the removal arrive in one patch or as separate updates, since a single low minConf would pull in both.

Note the ceiling on how early "early" can be. One confirmation is the floor the method permits, because mempool signals MUST NOT be processed. If your removal is genuinely urgent, the lever that shortens the window is fee rate at broadcast, not a resolver willing to read the mempool. That is a real limitation and it is worth naming rather than designing around.

This is more work than a constant, and it is the kind of thing that is easy to add when a resolver is being designed and very hard to retrofit once callers depend on a single resolve() that returns one document.

Choosing a Policy

For the cases where a threshold is appropriate, calibrate to the consequence of being wrong rather than to convention.

Interactive authentication. A DID auth login is low value individually, latency-sensitive, and revocable after the fact by ending the session. minConf: 1 is defensible here, and lowering it from the default is an explicit, auditable choice at the call site. The real risk is not honoring a key that turns out not to exist; it is failing to honor a removal fast enough, which the asymmetry above handles.

Credential issuance. An issuer signing with a key it believes is current, where the credential will be relied on for years, should want depth. The spec's default of six is roughly an hour, and issuance is rarely so urgent that an hour is unacceptable. This is the case where the payments heuristic transfers reasonably well, and the specification's footnote on the minConf requirement says as much: it calls six confirmations the widely accepted industry standard for treating a Bitcoin transaction as settled and notes that requests can raise or lower minConf to match their own threat model.

High-value attestations. Title transfers, regulatory filings, anything where reversal means litigation. Deeper, and the number should come from a documented risk assessment rather than from a default.

Historical resolution. Resolving a DID at a version from two years ago is not a finality question at all. Depth has already accumulated. The concern there is completeness of your view of the chain, which is an indexer trust question, not a reorg question.

How often does this actually matter on mainnet? Single-block reorgs happen from time to time, usually as a normal consequence of two blocks being found close together. Multi-block reorgs are rare enough to be individually notable. The deepest disruptions in Bitcoin's history have come from software divergence rather than mining competition, the March 2013 chain split being the canonical example: as BIP 50 documents, versions before 0.8 configured "an insufficient number of Berkeley DB locks to process large but otherwise valid blocks," so a block that 0.8 accepted under LevelDB was rejected by earlier nodes, and the split was resolved by pools downgrading. A useful reminder that depth alone is not the only variable and that a homogeneous node fleet is its own kind of risk.

The practical read: single-block reversion is common enough to design for, and deeper reversion is rare enough that a policy of six for high-value operations is a reasonable place to land.

Signals Before Confirmation

A Beacon Signal sitting in the mempool is information, and discarding it entirely is wasteful. It is also, per the spec, information a resolver MUST NOT process into a document. Both of those are true, and the way to hold them together is to keep mempool watching outside resolution.

It is weak information. It can be replaced through RBF, it can be evicted, and it can simply never confirm. But it is early, and early is worth something as a warning even when it is worth nothing as evidence.

A defensible design: a separate watcher that tracks mempool Beacon Signals for the DIDs you care about and attaches an advisory flag to resolution results. Not an input to the resolved document, which must reflect confirmed state, but metadata alongside it. "This DID has an unconfirmed update pending" is genuinely actionable, and a relying party that is about to accept a high-value assertion may reasonably decline to proceed until it has settled. Declining to act on an unconfirmed signal is always available; acting on one is not.

For a controller monitoring their own Beacon Addresses, mempool visibility is even more valuable. An unexpected unconfirmed signal against your beacon is the earliest possible warning of key compromise, and it arrives before the attacker's update is anchored, which is the only window in which fee-bumping a competing update is a strategy available to you at all.

Cache Invalidation

Reorgs break caching in a way that is easy to get wrong.

The naive cache is keyed by DID and holds the resolved document with a TTL. This is wrong in both directions: it serves stale results after a reorg, and it re-resolves unnecessarily when nothing has changed.

The fix is to key on chain state. Record, alongside every cached resolution, the block hash of the tip at resolution time and the height of the most recent Beacon Signal that contributed. When your node reports a reorg whose fork point is at or below that height, invalidate.

When the chain has only extended, the reorg risk is gone but the cached document can still be stale. A new block can carry a Beacon Signal for this DID, and a signal that previously sat below minConf crosses the threshold as depth accumulates and becomes processable. The confirmations value in the DID document metadata moves with every block too. Chain-state keying removes the wrong-answer failure mode of a TTL cache; it does not remove the need to re-check on new blocks. Invalidate on reorg, on any new block that touches a watched Beacon Address, and whenever a known pending signal crosses your minConf.

Bitcoin Core will tell you when the tip moves if you ask. Comparing the new tip against your recorded hash tells you whether the move was an extension or a reorg; the rest is checking the new block against your watched Beacon Addresses and your pending signals. It is a small amount of code that turns a correctness problem into a solved one.

Expose the Uncertainty

The most useful thing a resolver can do about finality is decline to hide it.

A resolve() that returns only a document has thrown away the information the caller needs to apply their own policy. The caller cannot distinguish a document whose last update confirmed six months ago from one whose last update confirmed ninety seconds ago, and those warrant very different treatment.

The spec anticipates part of this. Resolution options already carry minConf, versionId, and versionTime, and DID document metadata carries a REQUIRED confirmations count for the block containing the most recently applied unique update. Note which structure that is: DID resolution metadata is the thin envelope carrying contentType and error, so the confirmation count belongs in the document metadata alongside versionId and deactivated. Implement those faithfully before inventing anything, because a caller who can set minConf per request has most of what the asymmetric policy above needs.

Alongside the spec's required fields, a resolver should also return:

  • the height and block hash of the most recent Beacon Signal applied
  • confirmations at that height
  • the tip height and hash the resolution was performed against
  • whether any unconfirmed signals are pending for this DID

With that, an authentication service can accept one confirmation while an issuance service requires six, using the same resolver and the same cache. Without it, the confirmation policy is baked into infrastructure and every consumer inherits whatever the infrastructure author assumed.

It also makes resolution auditable. "We resolved this DID at height 912,345, block hash 0000..., with the latest update six confirmations deep" is a record that can be checked later. "We resolved it and it looked fine" is not.

Practical Guidance

  • Do not use one confirmation threshold for everything. Grant authority late, remove authority early, and implement that as two resolutions at different minConf values with an intersection over the authorized set, rather than as a constant buried in the resolver.
  • Return finality metadata from the resolver and let callers set policy. Support minConf, versionId, and versionTime as the spec defines them before adding anything of your own.
  • Never resolve from the mempool. It is a MUST NOT, and the correct use of an unconfirmed signal is to withhold trust, not to extend it.
  • Key caches on chain state, not time, and invalidate on reorg and on new or newly eligible signals rather than on a clock.
  • Subscribe to block notifications so you learn about reorgs instead of inferring them.
  • Track mempool signals for your own DIDs in a watcher separate from the resolver, as an early-warning channel only.
  • Write down the confirmation policy for each operation class and the reasoning, because the number will be questioned during an incident and "it was the default" is not an answer.
  • Test against regtest with deliberately induced reorgs. invalidateblock makes this straightforward, and it is the only way to know your invalidation logic works.

Conclusion

Anchoring to Bitcoin buys a canonical, immutable, auditable history. What it does not buy is instant certainty about the present. The most recent update is always the least certain one, and a resolver that presents the tip as settled fact is making a claim the chain does not support.

The useful reframing is that finality is not a threshold to pick but a property to propagate. The specification already went partway here by making depth a resolution option with a sane default rather than a constant. The resolver knows how deep each update is. Passing that along costs almost nothing and lets every consumer make a decision appropriate to what they are about to do, which is a better outcome than any single number the resolver could have chosen for them.

Next week: what all of this costs. A ten-year fee model for keeping a DID deployment anchored, across beacon types and fee regimes.

Jintek LLC