Resolve
Resolving a did:btcr2 identifier iteratively builds a DID document by applying BTCR2 Updates committed to the Bitcoin blockchain by Authorized Beacon Signals to an Initial DID Document. The Initial DID Document is either deterministically created from the DID or provided by Sidecar Data.
DID resolution is defined by DID Resolution v1 DID-RESOLUTION.
The resolve operation has the following function signature:
fn resolve(did, resolutionOptions) ->
(didResolutionMetadata, didDocument, didDocumentMetadata)
Process
Input values MUST first go through Decoding the DID and Processing Sidecar Data.
Raise an INVALID_OPTIONS error if resolutionOptions contains both versionId and versionTime. 1
When provided, resolutionOptions.versionId MUST be parsed as an integer and resolutionOptions.versionTime SHOULD be parsed as an XML Datetime. Raise an INVALID_OPTIONS error if either value does not parse.
Resolution maintains the following state while building the DID document:
updates: a list of tuples, each containing Bitcoin block metadata (height, mediantime, confirmations), a Beacon Address, and a BTCR2 Signed Update (data structure).scanned_beacons: a list of Beacon Addresses that Find Beacon Signals scanned (starts empty).current_document: the DID document being assembled.current_version_id: the version number being processed (starts at1).update_hash_history: a list of BTCR2 Unsigned Update hashes used to detect duplicates.block_confirmations: confirmations for the Bitcoin block that contains the most recently applied unique update (starts at0).block_mediantime: themediantimeof the Bitcoin block that contains the most recently applied update.current_block_height: the height of the Bitcoin block that contains the most recently applied update (starts at0).
The resolver:
- Establishes
current_documentfrom the DID or from Sidecar Data. - Repeats the following loop:
- Find Beacon Signals to add tuples to
updatesfrom the beacon services incurrent_document. - Process Next Update to apply one update to
current_document. - The loop terminates when Process Next Update resolves
didDocumentor an error occurs.
- Find Beacon Signals to add tuples to
The resolver returns:
didResolutionMetadata: a DID Resolution Metadata (data structure) (MAY be empty).didDocument: the final DID document (data structure).didDocumentMetadata: a DID document metadata (data structure) with REQUIRED fields:versionId:current_version_idas an ASCII string.confirmations:block_confirmationsas an integer. 2deactivated:current_document.deactivated.
If current_version_id is more than 1, didDocumentMetadata also contains updated: block_mediantime as an XML Datetime.
If resolutionOptions has no versionId and no versionTime, Sidecar Data that the resolver did not use has no effect on the result. It can show that the resolver and the DID controller do not read the same Bitcoin blocks. Examples: a Beacon Signal has less than minConf confirmations, or the resolver reads a different chain. It can also show a problem with the Sidecar Data, for example Sidecar Data that is not for did. Implementations MAY tell the caller which Sidecar Data they did not use.
Decode the DID
The did MUST be parsed with the DID-BTCR2 Identifier Decoding algorithm to retrieve version,
network, and genesis_bytes. An INVALID_DID error MUST be raised in response to any errors
raised while decoding.
Process Sidecar Data
resolutionOptions contains a sidecar property (Sidecar Data (data structure)) which SHOULD be prepared as lookup tables:
- Hash each BTCR2 Signed Update (data structure) in
sidecar.updateswith the JSON Document Hashing algorithm and build a map from hash to update (update_lookup_table). - Hash each CAS Announcement (data structure) in
sidecar.casUpdateswith the JSON Document Hashing algorithm and build a map from hash to announcement (cas_lookup_table). - Build a map from
sidecar.smtProofskeyed by proofid(smt_lookup_table).
If genesis_bytes is a SHA-256 hash, hash sidecar.genesisDocument with the JSON Document Hashing algorithm. If sidecar.genesisDocument is not provided, retrieve it from CAS using genesis_bytes as described in BTCR2 Update Data Distribution. Raise a NOT_FOUND error if the Genesis Document cannot be retrieved. Raise an INVALID_DID error if the computed hash does not match genesis_bytes.
When data is not available in Sidecar Data, implementations are RECOMMENDED to retrieve it from a Content Addressable Storage (CAS) service. To retrieve a document from CAS, construct a CID from the document’s SHA-256 hash bytes as described in BTCR2 Update Data Distribution.
Establish current_document
Resolution begins by creating an Initial Did Document called current_document (Current DID Document). The current_document is iteratively patched with BTCR2 Signed Updates announced by Authorized Beacon Signals.
Choose how to establish current_document based on the type of genesis_bytes retrieved from the decoded did:
If genesis_bytes is a SHA-256 Hash
Process the Genesis Document provided in sidecar.genesisDocument by replacing the identifier placeholder ("did:btcr2:_") with the did. A simple string replacement is sufficient. Parse the result as JSON to form current_document. The resulting DID Document (data structure) MUST be conformant to DID Core v1.1 DID-CORE.
If genesis_bytes is a secp256k1 Public Key
Render the Initial DID Document template with these values (Bitcoin addresses MUST use the Bitcoin URI Scheme BIP321):
did: Thedid.public-key-multikey: Public key as a Multibase"base-58-btc"CONTROLLED-IDENTIFIERS encoded string.p2pkh-bitcoin-address: Pay-to-Public-Key-Hash (P2PKH) Bitcoin address produced from the public key.p2wpkh-bitcoin-address: Pay-to-Witness-Public-Key-Hash (P2WPKH) Bitcoin address produced from the public key.p2tr-bitcoin-address: Pay-to-Taproot (P2TR) Bitcoin address produced from the public key.
{
"@context": [
"https://www.w3.org/ns/did/v1.1",
"https://btcr2.dev/context/v1"
],
"id": "{{did}}",
"verificationMethod": [
{
"id": "{{did}}#initialKey",
"type": "Multikey",
"controller": "{{did}}",
"publicKeyMultibase": "{{public-key-multikey}}"
}
],
"authentication": [
"{{did}}#initialKey"
],
"assertionMethod": [
"{{did}}#initialKey"
],
"capabilityInvocation": [
"{{did}}#initialKey"
],
"capabilityDelegation": [
"{{did}}#initialKey"
],
"service": [
{
"id": "{{did}}#initialP2PKH",
"type": "SingletonBeacon",
"serviceEndpoint": "{{p2pkh-bitcoin-address}}"
},
{
"id": "{{did}}#initialP2WPKH",
"type": "SingletonBeacon",
"serviceEndpoint": "{{p2wpkh-bitcoin-address}}"
},
{
"id": "{{did}}#initialP2TR",
"type": "SingletonBeacon",
"serviceEndpoint": "{{p2tr-bitcoin-address}}"
}
]
}
Parse the rendered template as JSON to form current_document. The resulting DID Document (data structure) MUST be conformant to DID Core v1.1 DID-CORE.
Find Beacon Signals
Scan the service entries in current_document (DID Document (data structure)) and identify BTCR2 Beacons by matching service type to Beacons Table 1: Beacon Types. Parse each beacon serviceEndpoint as a Beacon Address.
For each Beacon Address that is not in scanned_beacons:
- Find the Bitcoin transactions for which all of the following conditions are true:
- The transaction spends from the Beacon Address.
- The last output script of the transaction contains Signal Bytes.
- The block height of the transaction is equal to or more than
current_block_height.
- Add the Beacon Address to
scanned_beacons.
Implementations are RECOMMENDED to query an indexed Bitcoin blockchain Remote Procedure Call (RPC) service such as electrs or Esplora. Implementations MAY instead traverse blocks from the genesis block.
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. 3
For each transaction found:
- Derive
update_hashfrom the transaction’s Signal Bytes based on the Beacon Type:- Singleton Beacon:
update_hashis the Signal Bytes. - CAS Beacon: use Process CAS Beacon.
- SMT Beacon: use Process SMT Beacon.
- Singleton Beacon:
- If the Beacon Signal announces no update for
did, do not build a tuple. Continue with the next transaction. - Build a tuple with:
- The transaction’s block metadata (height, mediantime, and confirmations).
- The Beacon Address of the transaction.
- The BTCR2 Signed Update (data structure) retrieved from
update_lookup_table[update_hash].- If the update is not in
update_lookup_table, retrieve it from CAS usingupdate_hashas described in BTCR2 Update Data Distribution. - Raise a
MISSING_UPDATE_DATAerror if the update is not available from either source. - The resolver MUST hash the update with the JSON Document Hashing algorithm. The resolver MUST compare the hash to
update_hash. Raise anINVALID_SIGNAL_DATAerror if the two hashes are not equal.
- If the update is not in
- Append the tuple to
updates.
Process CAS Beacon
Treat Signal Bytes as map_update_hash. Look up map_update_hash in cas_lookup_table to retrieve a CAS Announcement (data structure). If the CAS Announcement (data structure) is not in cas_lookup_table, retrieve it from CAS using map_update_hash as described in BTCR2 Update Data Distribution. Raise a MISSING_UPDATE_DATA error if the announcement is not in cas_lookup_table and not available from CAS. The announcement is not available from CAS if the hash of the retrieved content is not equal to map_update_hash (BTCR2 Update Data Distribution).
Read update_hash from the announcement entry keyed by did. If the announcement has no entry for did, the Beacon Signal announces no update for did.
Process SMT Beacon
Treat Signal Bytes as smt_root. Look up smt_root in smt_lookup_table to retrieve an SMT Proof (data structure) as smt_proof. Raise a MISSING_UPDATE_DATA error if smt_lookup_table has no entry for smt_root. Raise an INVALID_SIGNAL_DATA error if the id of smt_proof is not equal to smt_root.
Verify smt_proof with the SMT Proof Verification algorithm. Raise an INVALID_SIGNAL_DATA error if the result of the algorithm is false. If smt_proof has an updateId, use it as update_hash. If smt_proof has no updateId, the Beacon Signal announces no update for did.
Process Next Update
- If
resolutionOptions.versionIdis provided andcurrent_version_idis equal to the parsedresolutionOptions.versionId, resolvecurrent_documentasdidDocument. - If
updatesis empty orcurrent_document.deactivatedistrue:- Raise a
NOT_FOUNDerror ifresolutionOptions.versionIdis provided. - Otherwise, resolve
current_documentasdidDocument.
- Raise a
- Sort
updatesby BTCR2 Signed Update (data structure)targetVersionId(ascending) with the tuple’s block height as a tiebreaker. Remove the first tuple fromupdates. - If
current_documenthas no BTCR2 Beacon with the tuple’s Beacon Address, ignore the tuple. Continue with the next tuple. - Resolve
current_documentasdidDocumentif all of the following conditions are true:- The tuple’s
targetVersionIdis more thancurrent_version_id. 4 resolutionOptions.versionTimeis provided.- The tuple’s block
mediantimeBitcoin-Core is afterresolutionOptions.versionTime. 5
- The tuple’s
- Set
updateto the tuple’s BTCR2 Signed Update (data structure) and checkupdate.targetVersionId.
Check update.targetVersionId
Compare update.targetVersionId to current_version_id. Only one of three possible conditions will occur:
update.targetVersionId <= current_version_id:update.targetVersionId == current_version_id + 1:update.targetVersionId > current_version_id + 1:LATE_PUBLISHINGerror MUST be raised.
Confirm Duplicate Update
This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
Raise an INVALID_DID_UPDATE error if update.targetVersionId is less than 2.
Create unsigned_update by removing the proof property from update. Hash unsigned_update with the JSON Document Hashing algorithm and compare it to update_hash_history[update.targetVersionId - 2]. Raise a LATE_PUBLISHING error if the hashes differ.
Apply update
Hash current_document with the JSON Document Hashing algorithm. Raise an INVALID_DID_UPDATE error if the result does not match the decoded update.sourceHash.
Apply the update.patch JSON Patch RFC6902 to current_document. Raise an INVALID_DID_UPDATE error if update.patch is malformed or fails to apply. JSON Patch operations are evaluated in order; the first operation that fails, including a failed test operation, fails the whole patch.
Verify that current_document conforms to DID Core v1.1 DID-CORE and that current_document.id equals did. Otherwise raise INVALID_DID_UPDATE.
Hash the patched current_document with the JSON Document Hashing algorithm. Raise an INVALID_DID_UPDATE error if the result does not match the decoded update.targetHash.
Create unsigned_update by removing the proof property from update, hash it with the JSON Document Hashing algorithm, and append the hash to update_hash_history.
Set block_confirmations to the tuple’s block confirmations. Set block_mediantime to the tuple’s block mediantime. Set current_block_height to the tuple’s block height.
Increment current_version_id.
Check update.proof
Raise an INVALID_DID_UPDATE error if the @context of update is not the array that the BTCR2 Unsigned Update (data structure) specifies. Raise an INVALID_DID_UPDATE error if the @context of update.proof is not equal to the @context of update. Two @context arrays are equal when they contain the same context URLs in the same order.
Raise an INVALID_DID_UPDATE error if any of the following conditions are not true:
update.proof.proofPurposeequals"capabilityInvocation".update.proof.capabilityActionequals"Write".update.proof.capabilityequals thecapabilityURN that Data Integrity Config (data structure) specifies fordid.update.proof.invocationTargetequalsdid.
Implementations MAY derive a Root Capability (data structure) from update.proof and invoke it according to Authorization Capabilities for Linked Data v0.3 ZCAP-LD.
The resolver MUST find the entry of current_document.capabilityInvocation that identifies update.proof.verificationMethod. A reference entry identifies it when the two values are equal. An embedded verification method object identifies it when the id of the object is equal. Raise an INVALID_DID_UPDATE error if no entry identifies it.
Read publicKeyMultibase from that entry. When the entry is an embedded verification method object, read publicKeyMultibase from the object. When the entry is a reference, find the verification method in current_document.verificationMethod with an id that is equal to the reference. Read publicKeyMultibase from that verification method. Raise an INVALID_DID_UPDATE error if there is no verification method with that id.
If update.proof.created or update.proof.expires is present, check each value against the Bitcoin block that contains the Beacon Signal that announced update. 6 Raise an INVALID_DID_UPDATE error if any of the following conditions are true:
update.proof.createdis after the timestamp in the block header.update.proof.expiresis before the blockmediantimeBitcoin-Core.update.proof.expiresis beforeupdate.proof.created, when both values are present.
Use a BIP340 Cryptosuite BIP340-Cryptosuite instance with publicKeyMultibase and the "bip340-jcs-2025" cryptosuite to verify update. Raise INVALID_DID_UPDATE if verification fails.
-
DID Resolution v1 DID-RESOLUTION defines the two options as mutually exclusive. ↩
-
The number of confirmations for the Bitcoin block that contains the most recently applied unique update that yielded the resolved DID document. “Unique” refers to handling duplicated updates. When deduplicating, use the lowest block height to determine confirmations. ↩
-
Six confirmations is the widely accepted industry standard for treating a Bitcoin transaction as settled. Resolution requests can raise or lower
minConfto match their own threat and security model; lowering it increases exposure to Bitcoin block reorganizations, which consumers can evaluate from the returnedconfirmationsmetadata. ↩ -
This condition is necessary because the resolver accepts a duplicate update (Confirm Duplicate Update). The block of a duplicate can be after
versionTimewhile the block of a subsequent version is beforeversionTime. Without this condition, the resolver stops at the duplicate and does not apply the subsequent version. ↩ -
The resolver applies an update whose block
mediantimeis equal toversionTime. The comparison has no tolerance.mediantimedoes not decrease from one block to the next. Each resolver reads the same value from the block chain, so each resolver selects the same version. ↩ -
In Data Integrity VC-DATA-INTEGRITY, each method selects the time of interest for
createdandexpires. On mainnet, the timestamp in the block header is approximately one hour later thanmediantime. A controller signs a proof a short time before the block that contains it, so a check ofcreatedagainstmediantimerejects valid updates. For this reason,createduses the timestamp in the block header. A miner sets the timestamp in the header of its own block and can increase that value, but a single miner cannot changemediantime. Theexpiresvalue limits the time between the signature and a replay of the update, soexpiresusesmediantime. ↩