Late-Publishing Attacks and Immutable History: How did:btcr2 Protects DID Integrity

Jul 10, 2026

Late-Publishing Attacks and Immutable History: How did:btcr2 Protects DID Integrity

Introduction

A decentralized identifier is useful only if you can trust its history. If an attacker can insert, reorder, or hide operations in the history of a DID at a later time, the trust model fails. The did:btcr2 method has specific rules against one threat: the late-publishing attack. This post explains the threat and the defense of btcr2. You must understand both if you build on the method or evaluate it.

What Is a Late-Publishing Attack?

The did:btcr2 specification gives this definition (Terminology):

Late Publishing is the ability for DID updates to be revealed at a later point in time, which alters the history of a DID document such that a state, that appeared valid before the reveal, appears after Late Publishing to never have been valid.

In an attack, a party makes a valid DID operation but does not publish it. The party publishes it later to change the state of the DID that other parties see.

Scenario

  1. Alice makes a DID.
  2. Alice makes Update A, which adds a new service endpoint. She anchors it to Bitcoin at block height 800,000.
  3. Bob verifies the DID of Alice and sees the service endpoint from Update A.
  4. Later, Alice shows Update B. She signed Update B before Update A, but she did not publish it. Update B removes the access of Bob.
  5. If the system accepts Update B, Alice can change the history of the DID at a later time.

Why It Matters

Late-publishing attacks weaken:

  • Credential verification: A late-published update can make a credential invalid at a later time, although the issuer signed it under a valid DID state.
  • Access control: A late-published update can change the permissions that a DID document gave.
  • Non-repudiation: If a party can change a DID state at a later time, a party can dispute the actions under that state. The specification states that "The Late Publishing problem breaks Non-Repudiation" (Non-Repudiation).

The Defense of btcr2: One Canonical History

btcr2 makes sure that each DID has exactly one canonical operation history. The specification states: "Resolvers MUST enforce strict ordering of the BTCR2 Updates and process all relevant Beacon Signals" (Security Considerations). The mechanisms in the sections that follow do this work.

Ordered Anchors

Each signed update contains these values (BTCR2 Unsigned Update):

  • A source hash: the SHA-256 hash of the DID document before the update. The hash is over the JCS canonical form (RFC 8785) of the document (JSON Document Hashing).
  • A target hash: the SHA-256 hash of the JCS canonical form of the DID document after the update.
  • A target version ID: the version number that this update makes. It must be exactly one more than the version of the document that it updates.

The signed update itself contains no block height and no transaction ID. The anchor is external: a Beacon Signal announces the update. A Beacon Signal is a Bitcoin transaction that spends from a Beacon Address. Its last output is an OP_RETURN script with 32 Signal Bytes.

For a Singleton Beacon, the Signal Bytes are the hash of the signed update. For CAS Beacons and SMT Beacons, the Signal Bytes commit to a CAS Announcement or an SMT root. That data then gives the update hash (Find Beacon Signals). During resolution, the resolver finds these signals on-chain. It gets the block height of each update from the transaction that announces it.

This design makes a hash-linked chain: each operation names the document state that it changes. The resolver applies an update only if its source hash matches the current document (Apply update). Thus, Update B cannot apply to a document state other than the state that Alice signed it against.

Bitcoin Order

Bitcoin gives a total order of transactions (Bitcoin whitepaper). The resolver sorts updates by targetVersionId and uses the block height of the Beacon Signal as the tiebreaker (Process Next Update). If two updates have the same targetVersionId (a fork), the resolver applies the update from the earlier block first. Then it checks the later update against the recorded hash for that version.

The resolver removes the proof of the later update and hashes the rest. If the hash is equal, the later update is a duplicate, and the resolver ignores it. If the hash is different (same targetVersionId, different content), the resolver raises a LATE_PUBLISHING error and returns no DID document (Confirm Duplicate Update).

At this point, the immutability of Bitcoin directly protects DID integrity. To reorder Bitcoin blocks at a later time, an attacker must control a majority of the hash rate of the network (Bitcoin whitepaper).

Duplicate Detection

The resolution algorithm expects each update to declare a targetVersionId. It finds conflicts through that version sequence (Check update.targetVersionId):

  1. Each update declares a targetVersionId. The resolver expects exactly current_version_id + 1.
  2. If an update has a targetVersionId that the resolver already processed, the resolver confirms it as a duplicate. The resolver removes the proof, hashes the unsigned update, and compares the result with the recorded update hash for that version.
  3. A version gap (targetVersionId > current_version_id + 1) or a hash mismatch raises a LATE_PUBLISHING error. This error is evidence of an operation that someone withheld, or of an operation that conflicts with the history.

Late-Publishing Rejection

An operation arrives "late" if a party publishes it to Bitcoin after the resolver processed a later operation. The resolver finds such an operation because:

  1. The operation declares a targetVersionId that the resolver already passed. The resolver checks it against the recorded version hash, and the check fails because the content is different.
  2. The canonical chain (ordered by targetVersionId, with Bitcoin block height as the tiebreaker) already moved past the version that this operation claims.
  3. The resolver raises a LATE_PUBLISHING error, regardless of when the controller signed the operation.

The signature time does not set the order. The targetVersionId and the Bitcoin block of the Beacon Signal set the order. Thus, an attacker cannot sign an operation early and publish it late to get priority. The resolver also checks the proof time against the block that contains the Beacon Signal. If proof.created is after the timestamp in the block header, the update is invalid (Check update.proof).

The Resolution Algorithm

When a resolver resolves a btcr2 DID, it does these steps (Resolve):

  1. Start from the initial document: For a key-based DID, the resolver makes the initial document from the DID. For a DID with a genesis document, the resolver uses the genesis document from sidecar data or CAS.
  2. Find Beacon Signals: The resolver reads the Beacon services in the current DID document and parses each service endpoint as a Beacon Address. It finds the Bitcoin transactions that spend from those addresses and have Signal Bytes in the last output. Each transaction must have at least minConf confirmations (6 if the request does not set a value).
  3. Order the operations: The resolver sorts the updates by targetVersionId, from low to high. The block height of the Beacon Signal is the tiebreaker.
  4. Validate each operation in order:
    • Check the targetVersionId of the update. It must be current_version_id + 1, and a gap raises LATE_PUBLISHING. If the resolver already processed that version, it confirms the update as a duplicate against the recorded version hash.
    • Confirm that the source hash matches the current document state.
    • Verify the proof: a BIP340 Schnorr signature with the bip340-jcs-2025 cryptosuite. The key must be in the capabilityInvocation relationship of the current document, not of the final document.
    • Apply the JSON Patch to make the new document. The new document must conform to DID Core v1.1 and keep the same id.
    • Verify that the target hash matches the patched document.
  5. Return the final state: the document after the resolver applies all valid operations.

Each step is deterministic. If two resolvers read the same Bitcoin blocks, get the same update data, and use the same resolution options, they produce the same result.

Threat Model Summary

ThreatDefense
Insert a fake operationBIP340 proof verification against keys in the capabilityInvocation relationship of the current document
Reorder operationsThe targetVersionId sequence and the source hash chain, with the Bitcoin block order as the tiebreaker
Fork the historyDuplicate detection (the targetVersionId sequence and the recorded update hashes)
Late-publish a withheld operationThe source hash chain, the targetVersionId checks, and the Bitcoin order
Hide an operationThe Beacon Signal stays on Bitcoin. If a resolver cannot get the announced update, it raises MISSING_UPDATE_DATA and returns no document
Change an anchored operationThe SHA-256 hash commitment in the Bitcoin transaction. A hash mismatch raises INVALID_SIGNAL_DATA

What btcr2 Cannot Defend Against

No system defends against all threats. The security model of btcr2 has limits:

  • Key compromise: If an attacker gets the private key of the controller, the attacker can make valid operations. The btcr2 method does not define separate update keys and recovery keys, as Sidetree-based methods such as ION do. An update invokes a ZCAP root capability (urn:zcap:root:<did>, with the DID URL-encoded), and a key in the capabilityInvocation relationship signs it with BIP340. Thus, key compromise stays a key management problem. A controller can rotate or remove a compromised key in a later update, if the attacker did not take control with the key first. The specification notes that the controller can see such an attack as a beacon spend that the controller did not authorize (Key Compromise).
  • 51% attack: If an attacker controls a majority of the Bitcoin hash rate, the attacker can reorganize the chain and reorder anchors. Here, btcr2 depends on the security assumption of Bitcoin. The specification also notes that a 51% attack alone does not give an attacker control of a DID. The attacker also needs a key compromise (Bitcoin Blockchain Compromise). The resolver uses only Beacon Signals with at least minConf confirmations (6 by default), which reduces the risk from short reorganizations.
  • Sidecar loss: If the controller loses the sidecar data and no other party (and no CAS) has it, resolvers cannot get the updates. The resolver raises MISSING_UPDATE_DATA, and the DID does not resolve (Data Retention).

Conclusion

Late-publishing attacks are a basic threat to any identity system with asynchronous updates. btcr2 addresses this threat with three rules:

  1. The resolver orders updates by targetVersionId, with the Bitcoin block height as the tiebreaker.
  2. Each update links to the previous document state through its source hash.
  3. The resolver rejects a second, different update for a version that it already applied.

The result is a DID with one auditable history. A party with a controller key cannot change that history silently: an update that conflicts with the history makes resolution fail with LATE_PUBLISHING. To reorder the history, an attacker must rewrite the Bitcoin blocks that anchor it. The limits above still apply: key management, the security assumptions of Bitcoin, and the availability of sidecar data.

Jintek LLC