Introduction
Last week, we examined how a resolver gets Beacon Signals from Bitcoin. This week, we examine when a resolver can trust a signal that it found. Many implementations answer this question by accident.
Bitcoin does not give deterministic finality. It gives a probability that increases with depth. Each property that a did:btcr2 deployment gets from Bitcoin also has that limit. This includes the answer to the question "what is the current version of this DID document." A resolver that treats the chain tip as final is usually right, but sometimes it is wrong with no sign of the error.
The usual answer for payments is to wait for six confirmations. That answer does not transfer well to DIDs, and this post explains the reason.
What a Reorg Does to a DID
First, remember how the order of updates works. A BTCR2 Signed Update contains no block height and no transaction ID. The anchor is external, through Beacon Signals, and the resolver derives the height at resolution time. The resolve algorithm sorts updates by targetVersionId and uses the block height as a tiebreaker.
A reorg changes the derived heights. A Beacon Signal that confirmed in block N can move to block N+2 on the chain that wins. If the replacement blocks do not include the transaction, the signal is in no block at all.
This has three results.
An update can become unconfirmed. A signal that was confirmed goes back to the unconfirmed state. The document version from that update is no longer current. A resolver that resolved the DID five minutes ago and cached the result now holds a document that does not match the chain. Nothing tells the resolver about this change.
The height order can change. The order rules use the block height as a tiebreaker. When the order depends on the tiebreaker, a reorg that changes the relative heights can change the result. Consider the contested rotation from the rotation post: a legitimate controller and an attacker both published updates for the same version. On the default resolution path, the tiebreaker does not select a winner. The update in the lower block applies first.
The other update then fails the update_hash_history comparison, and resolution stops with LATE_PUBLISHING. The block of each update does not change this result. But a reorg changes other results that are less visible. It changes the confirmations value in the DID document metadata. It can also change the result of a request with a pinned versionId for the contested version. That request stops after the first of the two updates applies, so its result depends on which update landed first.
A versionTime request can also give a different result after a reorg. The reason is that a reorg can move an update to a block on the other side of versionTime.
Detection is not automatic. The late publishing checks of btcr2 detect an inconsistent history, not a stale view. A resolver that holds a document one version behind the chain sees no problem, because the document is internally consistent. The document is only out of date, and the data alone does not show this.
The Minimum That the Specification Sets
Before you design a policy, know what the method already decided for you. The resolve algorithm sets two normative rules:
A transaction MUST be included in a Bitcoin block and have at least
resolutionOptions.minConfconfirmations (6 when not provided). Unconfirmed mempool transactions MUST NOT be processed.
Thus the spec already makes depth a parameter. minConf is a resolution option with a default of six, and a caller can ask a conformant resolver for a different number. The Resolution Options structure defines minConf as a positive integer with a minimum of 1. This design is good: the method does not hard-code a threshold. It gives the decision to the caller, who knows the purpose of the resolution.
The mempool clause is stricter and has no parameter. A conformant resolver must not build a document from an unconfirmed Beacon Signal, with any depth policy, for any reason. Thus all later text in this post about mempool visibility is about monitors and alarms, not about resolution. If you put that boundary in the wrong place, your implementation is fast and useful, but it does not conform.
Why the Payments Heuristic Does Not Transfer
Six confirmations is a heuristic for payments. It comes from an analysis of an attacker who wants to reverse a payment (Bitcoin whitepaper, section 11). In that attack, you release goods, the attacker rewrites the chain, and the attacker gets the goods and keeps the coins. Depth makes the rewrite expensive, compared to the value that the attacker steals.
DID resolution has a different risk profile: the loss does not scale with the transaction value.
A Beacon Signal gives an attacker nothing to double-spend. An attacker who removes your Beacon Signal with a reorg does not get a coin. The attacker gets a different result: a verifier continues to treat an older document version as current. The value of that result depends on the change between the two versions. That value can be very high, even if the transaction itself moved only a dust amount.
This gives the main point of this post: the safe direction depends on what the update does.
Consider two updates, each with one confirmation.
Update A adds a new key to capabilityInvocation. If you act on it and a reorg removes it, you accepted a key that has no authority. That result is bad. The conservative response is to wait for depth before you honor the update.
Update B removes a compromised key. If you act on it and a reorg removes it, you rejected a key that still has authority. That result is inconvenient but safe. If you wait and the attacker uses that time, the harm is real. The conservative response is to honor the update as early as the specification permits. That means a low minConf, not the default of six.
The depth is the same, but the correct policies are opposite. A single global confirmation threshold gets one of these two cases wrong, for each number that you select.
The general rule is: grant authority at depth, and remove authority at the lowest confirmed depth that you accept.
minConf alone cannot express that split. It filters whole transactions, so it can only select a prefix of the update history. Also, the resolve algorithm compares the hash of the fully patched document with update.targetHash, so a conformant resolver cannot honor half of a patch. Thus the asymmetry must be above the resolver. Resolve twice: once at your deep threshold and once at your shallow threshold. Honor a key only if it is in both results.
If a key is only in the shallow result, the controller added it, but you do not grant it yet. If a key is absent only from the shallow result, the controller removed it, and you no longer honor it. This rule works if the addition and the removal arrive in one patch or in separate updates, because a single low minConf gets both.
There is a limit on how early "early" can be. The method permits one confirmation as the minimum, because unconfirmed mempool transactions "MUST NOT be processed." If your removal is urgent, a higher fee rate at broadcast makes the delay shorter. A resolver that reads the mempool is not a conformant solution. This is a real limitation. Accept it, and do not try to avoid it with a design that does not conform.
This design needs more work than a constant. It is easy to add at design time. It is very hard to add later, after callers depend on a single resolve() that returns one document.
Select a Policy
For the cases where a threshold is appropriate, set it by the consequence of an error, not by convention.
Interactive authentication. Each DID auth login has low value, and it is sensitive to latency. You can also revoke it later when you end the session. minConf: 1 is defensible here. A value lower than the default is an explicit choice at the call site, and an auditor can see it. The real risk is not that you honor a key that a reorg later removes. The real risk is a removal that you do not honor fast enough, and the asymmetry above solves that problem.
Credential issuance. An issuer signs with a key that it believes is current, and relying parties use the credential for years. We recommend depth for this case. The default of six in the spec is about one hour, because Bitcoin targets one block each 10 minutes. Issuance is rarely so urgent that one hour is unacceptable.
The payments heuristic transfers reasonably well to issuance, and a footnote on the minConf requirement in the resolve algorithm agrees. The footnote states: "Six confirmations is the widely accepted industry standard for treating a Bitcoin transaction as settled." It also states that requests "can raise or lower minConf to match their own threat and security model."
High-value attestations. Examples are title transfers, regulatory filings, and each case where a reversal causes litigation. Use a greater depth. Take the number from a documented risk assessment, not from a default.
Historical resolution. Resolution of a DID at a version from two years ago is not a finality question. That version already has a large depth. The concern there is the completeness of your view of the chain. That is an indexer trust question, not a reorg question.
On mainnet, single-block reorgs occur from time to time, usually when two miners find blocks at almost the same time. Multi-block reorgs are rare, and each one gets attention. For example, the public stale-blocks dataset records a two-block stale branch at heights 941,881 and 941,882 on 23 March 2026.
Some of the deepest disruptions in the history of Bitcoin came from software divergence, not from mining competition. An example is the chain split of March 2013. BIP 50 documents that versions before 0.8 configured "an insufficient number of Berkeley DB locks to process large but otherwise valid blocks." Thus the earlier nodes rejected a block that 0.8 accepted with LevelDB. Two mining pools, BTCGuild and Slush, downgraded their nodes to 0.7, and this action resolved the split. This event shows that depth is not the only variable, and that a homogeneous fleet of nodes has its own risk.
A more recent example is the chain split of August 2026. BIP 110 is a temporary soft fork that limits the size of data fields. Its specification has a mandatory signaling period from height 961,632 to 963,647. In that period, nodes that enforce BIP 110 reject each block that does not signal bit 4. The main-chain block at height 961,632, from the AntPool mining pool, did not signal bit 4. Nodes that enforce BIP 110 rejected that block and followed a different branch.
The stale-blocks dataset records eight linked blocks on that branch, at heights 961,632 to 961,639. Their timestamps go from 8 August to 28 August 2026, and each block signals bit 4. In the same period, the main chain grew to height 964,462. A soft fork only adds restrictions to the current rules. Thus nodes without the BIP 110 rules can accept the blocks of that branch. These nodes stayed on the main chain, because it has more proof-of-work (Bitcoin whitepaper).
For DID resolution, this event shows that the consensus rules of a node select the chain that its resolver sees. Two resolvers on nodes with different rules can see different blocks at the same height. On each branch, a Beacon Signal can have a different height, or it can be in no block at all.
In practice, single-block reorgs are common enough that your design must handle them. Deeper reorgs are rare enough that a policy of six confirmations for high-value operations is a reasonable choice.
Signals Before Confirmation
A Beacon Signal in the mempool is information, and it is wasteful to discard it. But the spec also says that a resolver MUST NOT process it into a document. Both statements are true. To satisfy both, keep the mempool watcher outside resolution.
This information is weak. Another transaction can replace it through RBF. Bitcoin Core 28.0 made full replace-by-fee the default, and Bitcoin Core 29.0 removed the option to turn it off. As a result, a replacement does not need the BIP 125 signal.
The mempool can also evict the transaction, and the transaction possibly never confirms. But the information is early. As an alarm, early information has value, even when it has no value as evidence.
A defensible design uses a separate watcher. The watcher tracks mempool Beacon Signals for the DIDs that you monitor, and it attaches an advisory flag to resolution results. The flag is not an input to the resolved document, because that document must show the confirmed state. The flag is metadata next to the document. "This DID has an unconfirmed update" is a useful statement, because a relying party can then wait before it accepts a high-value assertion.
A relying party can always decline to act on an unconfirmed signal. It cannot act on one through a conformant resolver.
For a controller that monitors its own Beacon Addresses, mempool visibility has even more value. An unexpected unconfirmed signal on your beacon is the earliest possible alarm for a key compromise. The alarm arrives before the update of the attacker has an anchor in a block. Only in this time can you send your own update with a higher fee to replace the transaction of the attacker.
Cache Invalidation
Reorgs affect caches in a way that is easy to get wrong.
A naive cache uses the DID as the key and holds the resolved document with a TTL. This design fails in two directions. It serves stale results after a reorg, and it resolves again when nothing changed.
The solution is to use the chain state as the key. For each cached resolution, record the block hash of the tip at resolution time. Also record the height of the most recent Beacon Signal that the resolution used. If your node reports a reorg with a fork point at or below that height, invalidate the entry.
If the chain only extends, there is no reorg risk, but the cached document can still be stale. A new block can contain a Beacon Signal for this DID. Also, a signal below minConf gets one more confirmation with each block, and at minConf the resolver can process it. The confirmations value in the DID document metadata also changes with each block. A chain-state key removes the risk of a wrong answer that a TTL cache has, but you must still check again on new blocks.
Invalidate a cache entry on each of these events:
- a reorg with a fork point at or below the recorded height
- a new block that touches a watched Beacon Address
- a known signal below
minConfthat reaches yourminConf
Bitcoin Core tells you when the tip moves, if you subscribe to its ZMQ notifications. The hashblock topic notifies you of each new tip. After a reorg, this topic reports only the new tip. Compare the new tip with your recorded hash to find out if the change was an extension or a reorg. The sequence topic publishes each block connection and each disconnection, so it shows a reorg directly. Then check the new blocks against your watched Beacon Addresses and your known signals below minConf.
This is a small amount of code, and it solves a correctness problem.
Expose the Uncertainty
One of the most useful things that a resolver can do about finality is to show it, not hide it.
A resolve() that returns only a document discards the information that the caller needs for its own policy. For example, the last update of one document confirmed six months ago. The last update of another document confirmed ninety seconds ago. The caller cannot tell the difference, but these documents need very different treatment.
The spec already covers part of this. Resolution options contain minConf, versionId, and versionTime. DID document metadata contains a REQUIRED confirmations count for the block that contains the most recently applied unique update. Note which structure that is. DID resolution metadata is the thin envelope for contentType and error. The confirmation count is in the document metadata, together with versionId and deactivated.
Implement these fields exactly as the spec defines them before you add new fields. A caller that can set minConf for each request has most of what the asymmetric policy above needs.
In addition to the required fields of the spec, we recommend that a resolver also returns these values:
- the height and block hash of the most recent Beacon Signal that the resolver applied
- the number of confirmations at that height
- the tip height and tip hash at resolution time
- an indication if unconfirmed signals for this DID are in the mempool
With these values, an authentication service can accept one confirmation, and an issuance service can require six. Both services use the same resolver and the same cache. Without these values, the confirmation policy is fixed in the infrastructure. Each consumer then gets the assumptions of the infrastructure author.
These values also make resolution auditable. "We resolved this DID at height 912,345, block hash 0000..., with the latest update six confirmations deep" is a record that an auditor can check. "We resolved it and it looked fine" is not such a record.
Practical Guidance
- Do not use one confirmation threshold for all operations. Grant authority late, and remove authority early. Implement this rule as two resolutions at different
minConfvalues, and honor only the keys in both results. Do not implement it as a constant inside the resolver. - Return finality metadata from the resolver, and let callers set the policy. Support
minConf,versionId, andversionTimeas the spec defines them before you add your own fields. - Never resolve from the mempool. The spec says MUST NOT. Use an unconfirmed signal only to withhold trust, not to extend it.
- Use the chain state as the cache key, not the time. Invalidate on a reorg, on a new signal, and on a signal that reaches
minConf, not on a clock. - Subscribe to the ZMQ
sequencetopic, because it reports each block disconnection. Then a reorg is explicit, and you do not need to infer it from the new tip. - Track mempool signals for your own DIDs in a watcher that is separate from the resolver. Use the watcher only as an early alarm.
- Write down the confirmation policy for each class of operation, and write down the reason. In an incident, people will ask about the number, and "it was the default" is not an answer.
- Test on regtest with reorgs that you cause on purpose. Use
invalidateblockto cause them. Afterinvalidateblock, the ZMQhashblocktopic sends no notification, but thesequencetopic reports each disconnection (ZMQ docs). Such a test shows if your invalidation logic works.
Conclusion
An anchor in Bitcoin gives a canonical, immutable, and auditable history. It does not give instant certainty about the present. The most recent update is always the least certain update. A resolver that shows the tip as a settled fact makes a claim that the chain does not support.
Thus, finality is not a threshold that you select, but a property that you pass on to the caller. The specification already does part of this: it makes depth a resolution option with a sensible default, not a constant. The resolver knows the depth of each update, and it costs almost nothing to pass that depth to each consumer. Then each consumer can make a decision that suits its operation. That decision is better than one number that the resolver selects for all consumers.
Next week: the cost of all of this. We give a ten-year fee model to keep a DID deployment anchored, for each beacon type and for different fee regimes.