@did-btcr2/method 0.24.1 → 0.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +1541 -26561
- package/dist/browser.mjs +1541 -26561
- package/dist/cjs/core/beacon/aggregation/cohort/index.js +2 -1
- package/dist/cjs/core/beacon/aggregation/cohort/index.js.map +1 -1
- package/dist/cjs/core/beacon/aggregation/communication/adapter/nostr.js +3 -2
- package/dist/cjs/core/beacon/aggregation/communication/adapter/nostr.js.map +1 -1
- package/dist/cjs/core/beacon/aggregation/coordinator.js +2 -1
- package/dist/cjs/core/beacon/aggregation/coordinator.js.map +1 -1
- package/dist/cjs/core/beacon/aggregation/participant.js +3 -2
- package/dist/cjs/core/beacon/aggregation/participant.js.map +1 -1
- package/dist/cjs/core/beacon/beacon.js.map +1 -1
- package/dist/cjs/core/beacon/cas-beacon.js +120 -7
- package/dist/cjs/core/beacon/cas-beacon.js.map +1 -1
- package/dist/cjs/core/beacon/factory.js +1 -1
- package/dist/cjs/core/beacon/factory.js.map +1 -1
- package/dist/cjs/core/beacon/signal-discovery.js +1 -1
- package/dist/cjs/core/beacon/signal-discovery.js.map +1 -1
- package/dist/cjs/core/beacon/{singleton.js → singleton-beacon.js} +20 -27
- package/dist/cjs/core/beacon/singleton-beacon.js.map +1 -0
- package/dist/cjs/core/beacon/smt-beacon.js +1 -1
- package/dist/cjs/core/beacon/smt-beacon.js.map +1 -1
- package/dist/{esm/core/resolve.js → cjs/core/resolver.js} +241 -93
- package/dist/cjs/core/resolver.js.map +1 -0
- package/dist/cjs/core/update.js +5 -5
- package/dist/cjs/core/update.js.map +1 -1
- package/dist/cjs/did-btcr2.js +39 -96
- package/dist/cjs/did-btcr2.js.map +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/core/beacon/aggregation/cohort/index.js +2 -1
- package/dist/esm/core/beacon/aggregation/cohort/index.js.map +1 -1
- package/dist/esm/core/beacon/aggregation/communication/adapter/nostr.js +3 -2
- package/dist/esm/core/beacon/aggregation/communication/adapter/nostr.js.map +1 -1
- package/dist/esm/core/beacon/aggregation/coordinator.js +2 -1
- package/dist/esm/core/beacon/aggregation/coordinator.js.map +1 -1
- package/dist/esm/core/beacon/aggregation/participant.js +3 -2
- package/dist/esm/core/beacon/aggregation/participant.js.map +1 -1
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +120 -7
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/factory.js +1 -1
- package/dist/esm/core/beacon/factory.js.map +1 -1
- package/dist/esm/core/beacon/signal-discovery.js +1 -1
- package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
- package/dist/esm/core/beacon/{singleton.js → singleton-beacon.js} +20 -27
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -0
- package/dist/esm/core/beacon/smt-beacon.js +1 -1
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/{cjs/core/resolve.js → esm/core/resolver.js} +241 -93
- package/dist/esm/core/resolver.js.map +1 -0
- package/dist/esm/core/update.js +5 -5
- package/dist/esm/core/update.js.map +1 -1
- package/dist/esm/did-btcr2.js +39 -96
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/types/core/beacon/aggregation/cohort/index.d.ts.map +1 -1
- package/dist/types/core/beacon/aggregation/communication/adapter/nostr.d.ts.map +1 -1
- package/dist/types/core/beacon/aggregation/coordinator.d.ts.map +1 -1
- package/dist/types/core/beacon/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +9 -4
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +26 -7
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/{singleton.d.ts → singleton-beacon.d.ts} +7 -5
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -0
- package/dist/types/core/beacon/smt-beacon.d.ts +4 -3
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +5 -15
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts +167 -0
- package/dist/types/core/resolver.d.ts.map +1 -0
- package/dist/types/core/update.d.ts +3 -3
- package/dist/types/core/update.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts +16 -16
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/core/beacon/aggregation/cohort/index.ts +2 -1
- package/src/core/beacon/aggregation/communication/adapter/nostr.ts +3 -2
- package/src/core/beacon/aggregation/coordinator.ts +2 -1
- package/src/core/beacon/aggregation/participant.ts +3 -2
- package/src/core/beacon/beacon.ts +9 -5
- package/src/core/beacon/cas-beacon.ts +157 -10
- package/src/core/beacon/factory.ts +1 -1
- package/src/core/beacon/signal-discovery.ts +1 -1
- package/src/core/beacon/{singleton.ts → singleton-beacon.ts} +21 -36
- package/src/core/beacon/smt-beacon.ts +4 -3
- package/src/core/interfaces.ts +5 -16
- package/src/core/{resolve.ts → resolver.ts} +355 -130
- package/src/core/update.ts +7 -7
- package/src/did-btcr2.ts +42 -132
- package/src/index.ts +2 -2
- package/dist/cjs/core/beacon/singleton.js.map +0 -1
- package/dist/cjs/core/resolve.js.map +0 -1
- package/dist/esm/core/beacon/singleton.js.map +0 -1
- package/dist/esm/core/resolve.js.map +0 -1
- package/dist/types/core/beacon/singleton.d.ts.map +0 -1
- package/dist/types/core/resolve.d.ts +0 -93
- package/dist/types/core/resolve.d.ts.map +0 -1
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BitcoinConnection,
|
|
3
|
-
getNetwork
|
|
4
|
-
} from '@did-btcr2/bitcoin';
|
|
1
|
+
import { getNetwork } from '@did-btcr2/bitcoin';
|
|
5
2
|
import {
|
|
6
3
|
canonicalHash,
|
|
7
4
|
canonicalize,
|
|
8
5
|
DateUtils,
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
encode as encodeHash,
|
|
7
|
+
decode as decodeHash,
|
|
11
8
|
INVALID_DID_DOCUMENT,
|
|
12
9
|
INVALID_DID_UPDATE,
|
|
13
10
|
JSONPatch,
|
|
14
11
|
JSONUtils,
|
|
15
12
|
LATE_PUBLISHING_ERROR,
|
|
16
|
-
MISSING_UPDATE_DATA,
|
|
17
13
|
ResolveError
|
|
18
14
|
} from '@did-btcr2/common';
|
|
19
15
|
import {
|
|
@@ -24,13 +20,11 @@ import {
|
|
|
24
20
|
UnsignedBTCR2Update
|
|
25
21
|
} from '@did-btcr2/cryptosuite';
|
|
26
22
|
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
27
|
-
import { hex } from '@scure/base';
|
|
28
23
|
import { DidBtcr2 } from '../did-btcr2.js';
|
|
29
24
|
import { Appendix } from '../utils/appendix.js';
|
|
30
25
|
import { DidDocument, ID_PLACEHOLDER_VALUE } from '../utils/did-document.js';
|
|
31
26
|
import { BeaconFactory } from './beacon/factory.js';
|
|
32
|
-
import { BeaconService, BlockMetadata } from './beacon/interfaces.js';
|
|
33
|
-
import { BeaconSignalDiscovery } from './beacon/signal-discovery.js';
|
|
27
|
+
import { BeaconService, BeaconSignal, BlockMetadata } from './beacon/interfaces.js';
|
|
34
28
|
import { BeaconUtils } from './beacon/utils.js';
|
|
35
29
|
import { DidComponents, Identifier } from './identifier.js';
|
|
36
30
|
import { SMTProof } from './interfaces.js';
|
|
@@ -49,16 +43,137 @@ export interface DidResolutionResponse {
|
|
|
49
43
|
}
|
|
50
44
|
}
|
|
51
45
|
|
|
46
|
+
/** The resolver needs a genesis document whose hash matches genesisHash. */
|
|
47
|
+
export interface NeedGenesisDocument {
|
|
48
|
+
readonly kind: 'NeedGenesisDocument';
|
|
49
|
+
/** Base64url-encoded SHA-256 hash from the DID identifier's genesisBytes. */
|
|
50
|
+
readonly genesisHash: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The resolver needs beacon signals for these beacon service addresses. */
|
|
54
|
+
export interface NeedBeaconSignals {
|
|
55
|
+
readonly kind: 'NeedBeaconSignals';
|
|
56
|
+
/** The beacon services that need signal data. Pass directly to BeaconSignalDiscovery. */
|
|
57
|
+
readonly beaconServices: ReadonlyArray<BeaconService>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The resolver needs a CAS Announcement whose canonical hash matches announcementHash. */
|
|
61
|
+
export interface NeedCASAnnouncement {
|
|
62
|
+
readonly kind: 'NeedCASAnnouncement';
|
|
63
|
+
/** Base64url-encoded canonical hash of the CAS Announcement. */
|
|
64
|
+
readonly announcementHash: string;
|
|
65
|
+
/** The beacon service that produced this signal. */
|
|
66
|
+
readonly beaconServiceId: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The resolver needs a SignedBTCR2Update whose canonical hash matches updateHash. */
|
|
70
|
+
export interface NeedSignedUpdate {
|
|
71
|
+
readonly kind: 'NeedSignedUpdate';
|
|
72
|
+
/** Base64url-encoded canonical hash of the signed update. */
|
|
73
|
+
readonly updateHash: string;
|
|
74
|
+
/** The beacon service that produced this signal. */
|
|
75
|
+
readonly beaconServiceId: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Discriminated union of all data the resolver may request from the caller. */
|
|
79
|
+
export type DataNeed = NeedGenesisDocument | NeedBeaconSignals | NeedCASAnnouncement | NeedSignedUpdate;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Output of {@link Resolver.resolve}. Analogous to Rust's `ResolverState` enum.
|
|
83
|
+
* Either the resolver needs data from the caller, or resolution is complete.
|
|
84
|
+
*/
|
|
85
|
+
export type ResolverState =
|
|
86
|
+
| { status: 'action-required'; needs: ReadonlyArray<DataNeed> }
|
|
87
|
+
| { status: 'resolved'; result: DidResolutionResponse };
|
|
88
|
+
|
|
52
89
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Beacon Signals. The Initial DID Document is either deterministically created from the DID or
|
|
57
|
-
* provided by Sidecar Data.
|
|
58
|
-
* @class Resolve
|
|
59
|
-
* @type {Resolve}
|
|
90
|
+
* Return type from {@link Beacon.processSignals}.
|
|
91
|
+
* Contains successfully resolved updates and any data needs that must be
|
|
92
|
+
* satisfied before the remaining signals can be processed.
|
|
60
93
|
*/
|
|
61
|
-
export
|
|
94
|
+
export interface BeaconProcessResult {
|
|
95
|
+
updates: Array<[SignedBTCR2Update, BlockMetadata]>;
|
|
96
|
+
needs: Array<DataNeed>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Different possible Resolver states representing phases in the resolution process.
|
|
101
|
+
*/
|
|
102
|
+
enum ResolverPhase {
|
|
103
|
+
GenesisDocument = 'GenesisDocument',
|
|
104
|
+
BeaconDiscovery = 'BeaconDiscovery',
|
|
105
|
+
BeaconProcess = 'BeaconProcess',
|
|
106
|
+
ApplyUpdates = 'ApplyUpdates',
|
|
107
|
+
Complete = 'Complete',
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sans-I/O state machine for did:btcr2 resolution.
|
|
112
|
+
*
|
|
113
|
+
* Created by {@link DidBtcr2.resolve} (the factory). The caller drives resolution
|
|
114
|
+
* by repeatedly calling {@link resolve} and {@link provide}:
|
|
115
|
+
*
|
|
116
|
+
* ```typescript
|
|
117
|
+
* const resolver = DidBtcr2.resolve(did, { sidecar });
|
|
118
|
+
* let state = resolver.resolve();
|
|
119
|
+
*
|
|
120
|
+
* while (state.status === 'action-required') {
|
|
121
|
+
* for (const need of state.needs) { ... fetch & provide ... }
|
|
122
|
+
* state = resolver.resolve();
|
|
123
|
+
* }
|
|
124
|
+
* const { didDocument, metadata } = state.result;
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* The Resolver performs **zero I/O**. All external data (Bitcoin signals, CAS
|
|
128
|
+
* data, genesis documents) flows through the advance/provide protocol.
|
|
129
|
+
*
|
|
130
|
+
* @class Resolver
|
|
131
|
+
*/
|
|
132
|
+
export class Resolver {
|
|
133
|
+
// --- Immutable inputs ---
|
|
134
|
+
readonly #didComponents: DidComponents;
|
|
135
|
+
readonly #versionId?: string;
|
|
136
|
+
readonly #versionTime?: string;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The specific phase the Resolver is current in.
|
|
140
|
+
*/
|
|
141
|
+
#phase: ResolverPhase;
|
|
142
|
+
#sidecarData: SidecarData;
|
|
143
|
+
#currentDocument: DidDocument | null;
|
|
144
|
+
#providedGenesisDocument: object | null = null;
|
|
145
|
+
#beaconServicesSignals: Map<BeaconService, Array<BeaconSignal>> = new Map();
|
|
146
|
+
#processedServices: Set<string> = new Set();
|
|
147
|
+
#requestCache: Set<string> = new Set();
|
|
148
|
+
#unsortedUpdates: Array<[SignedBTCR2Update, BlockMetadata]> = [];
|
|
149
|
+
#resolvedResponse: DidResolutionResponse | null = null;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @internal — Use {@link DidBtcr2.resolve} to create instances.
|
|
153
|
+
*/
|
|
154
|
+
constructor(
|
|
155
|
+
didComponents: DidComponents,
|
|
156
|
+
sidecarData: SidecarData,
|
|
157
|
+
currentDocument: DidDocument | null,
|
|
158
|
+
options?: { versionId?: string; versionTime?: string; genesisDocument?: object }
|
|
159
|
+
) {
|
|
160
|
+
this.#didComponents = didComponents;
|
|
161
|
+
this.#sidecarData = sidecarData;
|
|
162
|
+
this.#currentDocument = currentDocument;
|
|
163
|
+
this.#versionId = options?.versionId;
|
|
164
|
+
this.#versionTime = options?.versionTime;
|
|
165
|
+
|
|
166
|
+
// If a genesis document was provided (from sidecar), pre-seed it for validation
|
|
167
|
+
if(options?.genesisDocument) {
|
|
168
|
+
this.#providedGenesisDocument = options.genesisDocument;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// If current document was established by the factory, skip GenesisDocument phase
|
|
172
|
+
this.#phase = currentDocument
|
|
173
|
+
? ResolverPhase.BeaconDiscovery
|
|
174
|
+
: ResolverPhase.GenesisDocument;
|
|
175
|
+
}
|
|
176
|
+
|
|
62
177
|
/**
|
|
63
178
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-secp256k1-public-key | 7.2.d.1 if genesis bytes is a secp256k1 Public Key}.
|
|
64
179
|
* @param {DidComponents} didComponents The decoded components of the did.
|
|
@@ -66,13 +181,13 @@ export class Resolve {
|
|
|
66
181
|
*/
|
|
67
182
|
static deterministic(didComponents: DidComponents): DidDocument {
|
|
68
183
|
// Deconstruct the bytes from the given components
|
|
69
|
-
const
|
|
184
|
+
const genesisBytes = didComponents.genesisBytes;
|
|
70
185
|
|
|
71
186
|
// Encode the did from the didComponents
|
|
72
187
|
const did = Identifier.encode(genesisBytes, didComponents);
|
|
73
188
|
|
|
74
189
|
// Construct a new CompressedSecp256k1PublicKey and deconstruct the publicKey and publicKeyMultibase
|
|
75
|
-
const { multibase
|
|
190
|
+
const { multibase } = new CompressedSecp256k1PublicKey(genesisBytes);
|
|
76
191
|
|
|
77
192
|
// Generate the service field for the DID Document
|
|
78
193
|
const service = BeaconUtils.generateBeaconServices({
|
|
@@ -88,7 +203,7 @@ export class Resolve {
|
|
|
88
203
|
id : `${did}#initialKey`,
|
|
89
204
|
type : 'Multikey',
|
|
90
205
|
controller : did,
|
|
91
|
-
publicKeyMultibase :
|
|
206
|
+
publicKeyMultibase : multibase.encoded
|
|
92
207
|
}],
|
|
93
208
|
service
|
|
94
209
|
});
|
|
@@ -97,33 +212,30 @@ export class Resolve {
|
|
|
97
212
|
/**
|
|
98
213
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#if-genesis_bytes-is-a-sha-256-hash | 7.2.d.2 if genesis_bytes is a SHA-256 Hash}.
|
|
99
214
|
* @param {DidComponents} didComponents BTCR2 DID components used to resolve the DID Document
|
|
100
|
-
* @param {
|
|
101
|
-
* @returns {
|
|
215
|
+
* @param {object} genesisDocument The genesis document for resolving the DID Document.
|
|
216
|
+
* @returns {DidDocument} The resolved DID Document object
|
|
102
217
|
* @throws {ResolveError} InvalidDidDocument if not conformant to DID Core v1.1
|
|
103
218
|
*/
|
|
104
|
-
static
|
|
219
|
+
static external(
|
|
105
220
|
didComponents: DidComponents,
|
|
106
221
|
genesisDocument: object,
|
|
107
|
-
):
|
|
108
|
-
//
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
// Convert the genesis bytes to a hex string
|
|
112
|
-
const genesisHex = hex.encode(genesisBytes);
|
|
222
|
+
): DidDocument {
|
|
223
|
+
// Encode the genesis bytes from the did components
|
|
224
|
+
const genesisBytes = encodeHash(didComponents.genesisBytes);
|
|
113
225
|
|
|
114
226
|
// Canonicalize and sha256 hash the currentDocument
|
|
115
|
-
const
|
|
227
|
+
const genesisDocumentBytes = canonicalHash(genesisDocument);
|
|
116
228
|
|
|
117
229
|
// If the genesisBytes do not match the hashBytes, throw an error
|
|
118
|
-
if (
|
|
230
|
+
if (genesisBytes !== genesisDocumentBytes) {
|
|
119
231
|
throw new ResolveError(
|
|
120
|
-
`Initial document mismatch: genesisBytes
|
|
121
|
-
INVALID_DID_DOCUMENT, { genesisBytes,
|
|
232
|
+
`Initial document mismatch: genesisBytes !== genesisDocumentBytes`,
|
|
233
|
+
INVALID_DID_DOCUMENT, { genesisBytes, genesisDocumentBytes }
|
|
122
234
|
);
|
|
123
235
|
}
|
|
124
236
|
|
|
125
237
|
// Encode the did from the didComponents
|
|
126
|
-
const did = Identifier.encode(genesisBytes, didComponents);
|
|
238
|
+
const did = Identifier.encode(decodeHash(genesisBytes), didComponents);
|
|
127
239
|
|
|
128
240
|
// Replace the placeholder did with the did throughout the currentDocument.
|
|
129
241
|
const currentDocument = JSON.parse(
|
|
@@ -137,21 +249,21 @@ export class Resolve {
|
|
|
137
249
|
/**
|
|
138
250
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-sidecar-data | Process Sidecar Data}
|
|
139
251
|
* @param {Sidecar} sidecar The sidecar data to process.
|
|
140
|
-
* @returns {
|
|
252
|
+
* @returns {SidecarData} The processed sidecar data containing maps of updates, CAS announcements, and SMT proofs.
|
|
141
253
|
*/
|
|
142
254
|
static sidecarData(sidecar: Sidecar = {} as Sidecar): SidecarData {
|
|
143
255
|
// BTCR2 Signed Updates map
|
|
144
256
|
const updateMap = new Map<string, SignedBTCR2Update>();
|
|
145
257
|
if(sidecar.updates?.length)
|
|
146
258
|
for(const update of sidecar.updates) {
|
|
147
|
-
updateMap.set(canonicalHash(update
|
|
259
|
+
updateMap.set(canonicalHash(update), update);
|
|
148
260
|
}
|
|
149
261
|
|
|
150
262
|
// CAS Announcements map
|
|
151
263
|
const casMap = new Map<string, CASAnnouncement>();
|
|
152
264
|
if(sidecar.casUpdates?.length)
|
|
153
265
|
for(const update of sidecar.casUpdates) {
|
|
154
|
-
casMap.set(canonicalHash(update
|
|
266
|
+
casMap.set(canonicalHash(update), update);
|
|
155
267
|
}
|
|
156
268
|
|
|
157
269
|
// SMT Proofs map
|
|
@@ -164,97 +276,20 @@ export class Resolve {
|
|
|
164
276
|
return { updateMap, casMap, smtMap };
|
|
165
277
|
}
|
|
166
278
|
|
|
167
|
-
/**
|
|
168
|
-
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#establish-current-document | 7.2.d Establish current_document}.
|
|
169
|
-
* Resolution begins by creating an Initial Did Document called current_document (Current DID Document).
|
|
170
|
-
* The current_document is iteratively patched with BTCR2 Signed Updates announced by Authorized Beacon Signals.
|
|
171
|
-
* @param {DidComponents} didComponents The decoded components of the did.
|
|
172
|
-
* @param {GenesisDocument} genesisDocument The genesis document for resolving the DID Document.
|
|
173
|
-
* @returns {Promise<DidDocument>} The resolved DID Document object.
|
|
174
|
-
* @throws {ResolveError} if the DID hrp is invalid, no sidecarData passed and hrp = "x".
|
|
175
|
-
*/
|
|
176
|
-
|
|
177
|
-
static async currentDocument(
|
|
178
|
-
didComponents: DidComponents,
|
|
179
|
-
genesisDocument?: object,
|
|
180
|
-
): Promise<DidDocument> {
|
|
181
|
-
// Deconstruct the hrp from the components
|
|
182
|
-
const { hrp, genesisBytes } = didComponents;
|
|
183
|
-
|
|
184
|
-
// If hrp `x`, perform external resolution
|
|
185
|
-
if (hrp === IdentifierHrp.x) {
|
|
186
|
-
if(!genesisDocument)
|
|
187
|
-
throw new ResolveError(
|
|
188
|
-
'External resolution requires genesisDocument',
|
|
189
|
-
MISSING_UPDATE_DATA, { didComponents }
|
|
190
|
-
);
|
|
191
|
-
return await this.external(didComponents, genesisDocument);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check for hrp `k`
|
|
195
|
-
if(hrp === IdentifierHrp.k){
|
|
196
|
-
// Validate genesis bytes as a compressed secp256k1 public key
|
|
197
|
-
if(!CompressedSecp256k1PublicKey.isValid(genesisBytes)) {
|
|
198
|
-
throw new ResolveError(
|
|
199
|
-
'Deterministic resolution requires valid secp256k1 public key',
|
|
200
|
-
INVALID_DID, { genesisBytes }
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
// Perform deterministic resolution
|
|
204
|
-
return this.deterministic(didComponents);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Else, throw an error for unsupported hrp
|
|
208
|
-
throw new ResolveError(`Unsupported DID hrp ${hrp}`, INVALID_DID, { hrp });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Finds uses the beacon services in the currentDocument to scan for onchain Beacon Signals (transactions) containing
|
|
213
|
-
* Signal Bytes (last output in OP_RETURN transaction).
|
|
214
|
-
* @param {Array<BeaconService>} beaconServices The array of BeaconService objects to search for signals.
|
|
215
|
-
* @param {SidecarData} sidecarData The sidecar data containing maps of updates, CAS announcements, and SMT proofs.
|
|
216
|
-
* @param {BitcoinConnection} bitcoin The bitcoin network connection used to fetch beacon signals
|
|
217
|
-
* @returns {Promise<Array<[SignedBTCR2Update, BlockMetadata]>>} The array of BTCR2 Signed Updates announced by the Beacon Signals.
|
|
218
|
-
*/
|
|
219
|
-
static async beaconSignals(
|
|
220
|
-
beaconServices: Array<BeaconService>,
|
|
221
|
-
sidecarData: SidecarData,
|
|
222
|
-
bitcoin: BitcoinConnection
|
|
223
|
-
): Promise<Array<[SignedBTCR2Update, BlockMetadata]>> {
|
|
224
|
-
// Discover Beacon Signals via indexer server (esplora/electrs) or full node
|
|
225
|
-
const beaconServicesSignals = bitcoin.rest
|
|
226
|
-
? await BeaconSignalDiscovery.indexer(beaconServices, bitcoin)
|
|
227
|
-
: await BeaconSignalDiscovery.fullnode(beaconServices, bitcoin);
|
|
228
|
-
|
|
229
|
-
// Process each beacon's signals in parallel
|
|
230
|
-
const promises = await Promise.all(
|
|
231
|
-
Array.from(beaconServicesSignals.entries()).map(
|
|
232
|
-
async ([service, signals]) => {
|
|
233
|
-
// Skip beacons with no signals
|
|
234
|
-
if (!signals.length) return [];
|
|
235
|
-
// Establish a typed beacon and process its signals
|
|
236
|
-
return BeaconFactory.establish(service).processSignals(signals, sidecarData);
|
|
237
|
-
}
|
|
238
|
-
)
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
return promises.flat();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
279
|
/**
|
|
245
280
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-updates | 7.2.f Process updates Array}.
|
|
246
281
|
* @param {DidDocument} currentDocument The current DID Document to apply the updates to.
|
|
247
282
|
* @param {Array<[SignedBTCR2Update, BlockMetadata]>} unsortedUpdates The unsorted array of BTCR2 Signed Updates and their associated Block Metadata.
|
|
248
283
|
* @param {string} [versionTime] The optional version time to limit updates to.
|
|
249
284
|
* @param {string} [versionId] The optional version id to limit updates to.
|
|
250
|
-
* @returns {
|
|
285
|
+
* @returns {DidResolutionResponse} The updated DID Document, number of confirmations, and version id.
|
|
251
286
|
*/
|
|
252
|
-
static
|
|
287
|
+
static updates(
|
|
253
288
|
currentDocument: DidDocument,
|
|
254
289
|
unsortedUpdates: Array<[SignedBTCR2Update, BlockMetadata]>,
|
|
255
290
|
versionTime?: string,
|
|
256
291
|
versionId?: string
|
|
257
|
-
):
|
|
292
|
+
): DidResolutionResponse {
|
|
258
293
|
// Start the version number being processed at 1
|
|
259
294
|
let currentVersionId = 1;
|
|
260
295
|
|
|
@@ -280,7 +315,7 @@ export class Resolve {
|
|
|
280
315
|
// Iterate over each (update block) pair
|
|
281
316
|
for(const [update, block] of updates) {
|
|
282
317
|
// Get the hash of the current document
|
|
283
|
-
const currentDocumentHash = canonicalHash(response.didDocument
|
|
318
|
+
const currentDocumentHash = canonicalHash(response.didDocument);
|
|
284
319
|
|
|
285
320
|
// Safely convert block.time to timestamp
|
|
286
321
|
const blocktime = DateUtils.blocktimeToTimestamp(block.time);
|
|
@@ -319,12 +354,12 @@ export class Resolve {
|
|
|
319
354
|
);
|
|
320
355
|
}
|
|
321
356
|
// Apply the update to the currentDocument and set it in the response
|
|
322
|
-
response.didDocument =
|
|
357
|
+
response.didDocument = this.applyUpdate(response.didDocument, update);
|
|
323
358
|
|
|
324
359
|
// Create unsigned_update by removing the proof property from update.
|
|
325
360
|
const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']) as UnsignedBTCR2Update;
|
|
326
361
|
// Push the canonicalized unsigned update hash to the updateHashHistory
|
|
327
|
-
updateHashHistory.push(canonicalHash(unsignedUpdate
|
|
362
|
+
updateHashHistory.push(canonicalHash(unsignedUpdate));
|
|
328
363
|
}
|
|
329
364
|
|
|
330
365
|
// If update.targetVersionId > currentVersionId + 1, throw LATE_PUBLISHING error
|
|
@@ -345,14 +380,15 @@ export class Resolve {
|
|
|
345
380
|
response.metadata.versionId = `${currentVersionId}`;
|
|
346
381
|
|
|
347
382
|
// If resolutionOptions.versionId is defined and <= currentVersionId, return currentDocument
|
|
348
|
-
|
|
383
|
+
const versionIdNumber = Number(versionId);
|
|
384
|
+
if(!isNaN(versionIdNumber) && versionIdNumber <= currentVersionId) {
|
|
349
385
|
return response;
|
|
350
386
|
}
|
|
351
387
|
|
|
352
388
|
// Check if the current document is deactivated before further processing
|
|
353
|
-
if(
|
|
389
|
+
if(response.didDocument.deactivated) {
|
|
354
390
|
// Set the response deactivated flag to true
|
|
355
|
-
response.metadata.deactivated =
|
|
391
|
+
response.metadata.deactivated = response.didDocument.deactivated;
|
|
356
392
|
// If deactivated, stop processing further updates and return the response
|
|
357
393
|
return response;
|
|
358
394
|
}
|
|
@@ -362,13 +398,16 @@ export class Resolve {
|
|
|
362
398
|
return response;
|
|
363
399
|
}
|
|
364
400
|
|
|
401
|
+
// ─── Private static: update internals ──────────────────────────────
|
|
402
|
+
|
|
365
403
|
/**
|
|
366
404
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/#confirm-duplicate-update | 7.2.f.1 Confirm Duplicate Update}.
|
|
367
405
|
* This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
|
|
368
406
|
* @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
|
|
407
|
+
* @param {string[]} updateHashHistory The accumulated hash history for comparison.
|
|
369
408
|
* @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
|
|
370
409
|
*/
|
|
371
|
-
static confirmDuplicate(update: SignedBTCR2Update, updateHashHistory: string[]): void {
|
|
410
|
+
private static confirmDuplicate(update: SignedBTCR2Update, updateHashHistory: string[]): void {
|
|
372
411
|
// Create unsigned_update by removing the proof property from update.
|
|
373
412
|
const { proof: _, ...unsignedUpdate } = update;
|
|
374
413
|
|
|
@@ -391,13 +430,13 @@ export class Resolve {
|
|
|
391
430
|
* Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#apply-update | 7.2.f.3 Apply Update}.
|
|
392
431
|
* @param {DidDocument} currentDocument The current DID Document to apply the update to.
|
|
393
432
|
* @param {SignedBTCR2Update} update The BTCR2 Signed Update to apply.
|
|
394
|
-
* @returns {
|
|
433
|
+
* @returns {DidDocument} The updated DID Document after applying the update.
|
|
395
434
|
* @throws {ResolveError} If the update is invalid or cannot be applied.
|
|
396
435
|
*/
|
|
397
|
-
static
|
|
436
|
+
private static applyUpdate(
|
|
398
437
|
currentDocument: DidDocument,
|
|
399
438
|
update: SignedBTCR2Update
|
|
400
|
-
):
|
|
439
|
+
): DidDocument {
|
|
401
440
|
// Get the capability id from the to update proof.
|
|
402
441
|
const capabilityId = update.proof?.capability;
|
|
403
442
|
// Since this field is optional, check that it exists
|
|
@@ -461,7 +500,7 @@ export class Resolve {
|
|
|
461
500
|
DidDocument.validate(updatedDocument);
|
|
462
501
|
|
|
463
502
|
// Canonicalize and hash the updatedDocument to get the currentDocumentHash.
|
|
464
|
-
const currentDocumentHash = canonicalHash(updatedDocument
|
|
503
|
+
const currentDocumentHash = canonicalHash(updatedDocument);
|
|
465
504
|
|
|
466
505
|
// Prepare the update targetHash for comparison with currentDocumentHash.
|
|
467
506
|
const updateTargetHash = update.targetHash;
|
|
@@ -478,4 +517,190 @@ export class Resolve {
|
|
|
478
517
|
// Return final updatedDocument.
|
|
479
518
|
return updatedDocument;
|
|
480
519
|
}
|
|
481
|
-
|
|
520
|
+
|
|
521
|
+
// ─── Instance: state machine ───────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Advance the state machine. Returns either:
|
|
525
|
+
* - `{ status: 'action-required', needs }` — caller must provide data via {@link provide}
|
|
526
|
+
* - `{ status: 'resolved', result }` — resolution complete
|
|
527
|
+
*
|
|
528
|
+
* Analogous to Rust's `Resolver::resolve()`.
|
|
529
|
+
*/
|
|
530
|
+
resolve(): ResolverState {
|
|
531
|
+
// Internal loop — keeps advancing through phases until data is needed or done
|
|
532
|
+
while(true) {
|
|
533
|
+
switch(this.#phase) {
|
|
534
|
+
|
|
535
|
+
// Phase: GenesisDocument
|
|
536
|
+
// Only entered for EXTERNAL (x HRP) identifiers when genesis doc was not in sidecar.
|
|
537
|
+
case ResolverPhase.GenesisDocument: {
|
|
538
|
+
if(this.#providedGenesisDocument) {
|
|
539
|
+
// Genesis doc was provided — establish the current document
|
|
540
|
+
this.#currentDocument = Resolver.external(
|
|
541
|
+
this.#didComponents, this.#providedGenesisDocument
|
|
542
|
+
);
|
|
543
|
+
this.#providedGenesisDocument = null;
|
|
544
|
+
this.#phase = ResolverPhase.BeaconDiscovery;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Need genesis document from caller
|
|
549
|
+
const genesisHash = encodeHash(this.#didComponents.genesisBytes);
|
|
550
|
+
return {
|
|
551
|
+
status : 'action-required',
|
|
552
|
+
needs : [{ kind: 'NeedGenesisDocument', genesisHash }]
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Phase: BeaconDiscovery
|
|
557
|
+
// Extract beacon services, emit NeedBeaconSignals for addresses not yet queried.
|
|
558
|
+
case ResolverPhase.BeaconDiscovery: {
|
|
559
|
+
const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument!);
|
|
560
|
+
|
|
561
|
+
// Filter to services whose addresses haven't been requested yet
|
|
562
|
+
const newServices = beaconServices.filter(service => {
|
|
563
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string);
|
|
564
|
+
return !this.#requestCache.has(address);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if(newServices.length > 0) {
|
|
568
|
+
// Mark addresses as requested so we don't re-request on subsequent rounds
|
|
569
|
+
for(const service of newServices) {
|
|
570
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string);
|
|
571
|
+
this.#requestCache.add(address);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return {
|
|
575
|
+
status : 'action-required',
|
|
576
|
+
needs : [{ kind: 'NeedBeaconSignals', beaconServices: newServices }]
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// No new beacon services to query — move to processing
|
|
581
|
+
this.#phase = ResolverPhase.BeaconProcess;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Phase: BeaconProcess
|
|
586
|
+
// Process each beacon's signals. Collect updates and data needs.
|
|
587
|
+
case ResolverPhase.BeaconProcess: {
|
|
588
|
+
const allNeeds: Array<DataNeed> = [];
|
|
589
|
+
|
|
590
|
+
for(const [service, signals] of this.#beaconServicesSignals) {
|
|
591
|
+
// Skip already-processed services and services with no signals
|
|
592
|
+
if(this.#processedServices.has(service.id) || !signals.length) continue;
|
|
593
|
+
|
|
594
|
+
// Establish a typed beacon and process its signals
|
|
595
|
+
const beacon = BeaconFactory.establish(service);
|
|
596
|
+
const result = beacon.processSignals(signals, this.#sidecarData);
|
|
597
|
+
|
|
598
|
+
if(result.needs.length > 0) {
|
|
599
|
+
// This service has unmet data needs — collect them
|
|
600
|
+
allNeeds.push(...result.needs);
|
|
601
|
+
} else {
|
|
602
|
+
// All signals for this service resolved — collect updates, mark processed
|
|
603
|
+
this.#unsortedUpdates.push(...result.updates);
|
|
604
|
+
this.#processedServices.add(service.id);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if(allNeeds.length > 0) {
|
|
609
|
+
return { status: 'action-required', needs: allNeeds };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.#phase = ResolverPhase.ApplyUpdates;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Phase: ApplyUpdates
|
|
617
|
+
// Apply collected updates, then check for new beacon services (multi-round).
|
|
618
|
+
case ResolverPhase.ApplyUpdates: {
|
|
619
|
+
if(this.#unsortedUpdates.length > 0) {
|
|
620
|
+
// Apply all collected updates to the current document
|
|
621
|
+
this.#resolvedResponse = Resolver.updates(
|
|
622
|
+
this.#currentDocument!,
|
|
623
|
+
this.#unsortedUpdates,
|
|
624
|
+
this.#versionTime,
|
|
625
|
+
this.#versionId
|
|
626
|
+
);
|
|
627
|
+
this.#currentDocument = this.#resolvedResponse.didDocument;
|
|
628
|
+
this.#unsortedUpdates = [];
|
|
629
|
+
|
|
630
|
+
// Check for new beacon services added by updates (multi-round discovery)
|
|
631
|
+
const beaconServices = BeaconUtils.getBeaconServices(this.#currentDocument);
|
|
632
|
+
const hasNewServices = beaconServices.some(service => {
|
|
633
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string);
|
|
634
|
+
return !this.#requestCache.has(address);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
if(hasNewServices) {
|
|
638
|
+
// Loop back to discover signals for new beacon services
|
|
639
|
+
this.#phase = ResolverPhase.BeaconDiscovery;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
this.#phase = ResolverPhase.Complete;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Phase: Complete
|
|
649
|
+
case ResolverPhase.Complete: {
|
|
650
|
+
return {
|
|
651
|
+
status : 'resolved',
|
|
652
|
+
result : this.#resolvedResponse ?? {
|
|
653
|
+
didDocument : this.#currentDocument!,
|
|
654
|
+
metadata : {
|
|
655
|
+
versionId : this.#versionId ?? '1',
|
|
656
|
+
deactivated : this.#currentDocument!.deactivated || false
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Provide data the resolver requested in a previous {@link resolve} call.
|
|
667
|
+
* Call once per need, then call {@link resolve} again to continue.
|
|
668
|
+
*
|
|
669
|
+
* Analogous to Rust's `Resolver::process_responses()`.
|
|
670
|
+
*
|
|
671
|
+
* @param need The DataNeed being fulfilled (from the `needs` array).
|
|
672
|
+
* @param data The data payload corresponding to the need kind.
|
|
673
|
+
*/
|
|
674
|
+
provide(need: NeedGenesisDocument, data: object): void;
|
|
675
|
+
provide(need: NeedBeaconSignals, data: Map<BeaconService, Array<BeaconSignal>>): void;
|
|
676
|
+
provide(need: NeedCASAnnouncement, data: CASAnnouncement): void;
|
|
677
|
+
provide(need: NeedSignedUpdate, data: SignedBTCR2Update): void;
|
|
678
|
+
provide(need: DataNeed, data: object | Map<BeaconService, Array<BeaconSignal>> | CASAnnouncement | SignedBTCR2Update): void {
|
|
679
|
+
switch(need.kind) {
|
|
680
|
+
case 'NeedGenesisDocument': {
|
|
681
|
+
this.#providedGenesisDocument = data;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
case 'NeedBeaconSignals': {
|
|
686
|
+
const signals = data as Map<BeaconService, Array<BeaconSignal>>;
|
|
687
|
+
for(const [service, serviceSignals] of signals) {
|
|
688
|
+
this.#beaconServicesSignals.set(service, serviceSignals);
|
|
689
|
+
}
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
case 'NeedCASAnnouncement': {
|
|
694
|
+
const announcement = data as CASAnnouncement;
|
|
695
|
+
this.#sidecarData.casMap.set(canonicalHash(announcement), announcement);
|
|
696
|
+
break;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
case 'NeedSignedUpdate': {
|
|
700
|
+
const update = data as SignedBTCR2Update;
|
|
701
|
+
this.#sidecarData.updateMap.set(canonicalHash(update), update);
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|