@did-btcr2/method 0.23.0 → 0.25.0

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.
Files changed (160) hide show
  1. package/README.md +96 -50
  2. package/dist/browser.js +36332 -37280
  3. package/dist/browser.mjs +36331 -37279
  4. package/dist/cjs/core/beacon/aggregation/communication/adapter/did-comm.js +1 -1
  5. package/dist/cjs/core/beacon/aggregation/communication/adapter/did-comm.js.map +1 -1
  6. package/dist/cjs/core/beacon/aggregation/communication/adapter/nostr.js +1 -1
  7. package/dist/cjs/core/beacon/aggregation/communication/adapter/nostr.js.map +1 -1
  8. package/dist/cjs/core/beacon/aggregation/coordinator.js +40 -44
  9. package/dist/cjs/core/beacon/aggregation/coordinator.js.map +1 -1
  10. package/dist/cjs/core/beacon/aggregation/participant.js +35 -38
  11. package/dist/cjs/core/beacon/aggregation/participant.js.map +1 -1
  12. package/dist/cjs/core/beacon/aggregation/session/index.js +3 -4
  13. package/dist/cjs/core/beacon/aggregation/session/index.js.map +1 -1
  14. package/dist/cjs/core/beacon/beacon.js.map +1 -1
  15. package/dist/cjs/core/beacon/cas-beacon.js +119 -7
  16. package/dist/cjs/core/beacon/cas-beacon.js.map +1 -1
  17. package/dist/cjs/core/beacon/factory.js +1 -1
  18. package/dist/cjs/core/beacon/factory.js.map +1 -1
  19. package/dist/cjs/core/beacon/{singleton.js → singleton-beacon.js} +19 -27
  20. package/dist/cjs/core/beacon/singleton-beacon.js.map +1 -0
  21. package/dist/cjs/core/beacon/smt-beacon.js +1 -1
  22. package/dist/cjs/core/beacon/smt-beacon.js.map +1 -1
  23. package/dist/cjs/core/identifier.js +1 -1
  24. package/dist/cjs/core/identifier.js.map +1 -1
  25. package/dist/{esm/core/resolve.js → cjs/core/resolver.js} +244 -92
  26. package/dist/cjs/core/resolver.js.map +1 -0
  27. package/dist/cjs/core/update.js +7 -7
  28. package/dist/cjs/core/update.js.map +1 -1
  29. package/dist/cjs/did-btcr2.js +34 -94
  30. package/dist/cjs/did-btcr2.js.map +1 -1
  31. package/dist/cjs/index.js +2 -3
  32. package/dist/cjs/index.js.map +1 -1
  33. package/dist/cjs/utils/did-document.js +9 -19
  34. package/dist/cjs/utils/did-document.js.map +1 -1
  35. package/dist/esm/core/beacon/aggregation/communication/adapter/did-comm.js +1 -1
  36. package/dist/esm/core/beacon/aggregation/communication/adapter/did-comm.js.map +1 -1
  37. package/dist/esm/core/beacon/aggregation/communication/adapter/nostr.js +1 -1
  38. package/dist/esm/core/beacon/aggregation/communication/adapter/nostr.js.map +1 -1
  39. package/dist/esm/core/beacon/aggregation/coordinator.js +40 -44
  40. package/dist/esm/core/beacon/aggregation/coordinator.js.map +1 -1
  41. package/dist/esm/core/beacon/aggregation/participant.js +35 -38
  42. package/dist/esm/core/beacon/aggregation/participant.js.map +1 -1
  43. package/dist/esm/core/beacon/aggregation/session/index.js +3 -4
  44. package/dist/esm/core/beacon/aggregation/session/index.js.map +1 -1
  45. package/dist/esm/core/beacon/beacon.js.map +1 -1
  46. package/dist/esm/core/beacon/cas-beacon.js +119 -7
  47. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  48. package/dist/esm/core/beacon/factory.js +1 -1
  49. package/dist/esm/core/beacon/factory.js.map +1 -1
  50. package/dist/esm/core/beacon/{singleton.js → singleton-beacon.js} +19 -27
  51. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -0
  52. package/dist/esm/core/beacon/smt-beacon.js +1 -1
  53. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  54. package/dist/esm/core/identifier.js +1 -1
  55. package/dist/esm/core/identifier.js.map +1 -1
  56. package/dist/{cjs/core/resolve.js → esm/core/resolver.js} +244 -92
  57. package/dist/esm/core/resolver.js.map +1 -0
  58. package/dist/esm/core/update.js +7 -7
  59. package/dist/esm/core/update.js.map +1 -1
  60. package/dist/esm/did-btcr2.js +34 -94
  61. package/dist/esm/did-btcr2.js.map +1 -1
  62. package/dist/esm/index.js +2 -3
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/utils/did-document.js +9 -19
  65. package/dist/esm/utils/did-document.js.map +1 -1
  66. package/dist/types/core/beacon/aggregation/cohort/index.d.ts +1 -0
  67. package/dist/types/core/beacon/aggregation/cohort/messages/base.d.ts +1 -0
  68. package/dist/types/core/beacon/aggregation/cohort/messages/constants.d.ts +1 -0
  69. package/dist/types/core/beacon/aggregation/cohort/messages/index.d.ts +1 -0
  70. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/cohort-advert.d.ts +1 -0
  71. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/cohort-ready.d.ts +2 -2
  72. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/cohort-ready.d.ts.map +1 -1
  73. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/opt-in-accept.d.ts +1 -0
  74. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/opt-in.d.ts +1 -0
  75. package/dist/types/core/beacon/aggregation/cohort/messages/keygen/subscribe.d.ts +1 -0
  76. package/dist/types/core/beacon/aggregation/cohort/messages/sign/aggregated-nonce.d.ts +1 -0
  77. package/dist/types/core/beacon/aggregation/cohort/messages/sign/authorization-request.d.ts +1 -0
  78. package/dist/types/core/beacon/aggregation/cohort/messages/sign/nonce-contribution.d.ts +1 -0
  79. package/dist/types/core/beacon/aggregation/cohort/messages/sign/request-signature.d.ts +1 -0
  80. package/dist/types/core/beacon/aggregation/cohort/messages/sign/signature-authorization.d.ts +1 -0
  81. package/dist/types/core/beacon/aggregation/cohort/status.d.ts +1 -0
  82. package/dist/types/core/beacon/aggregation/communication/adapter/did-comm.d.ts +4 -3
  83. package/dist/types/core/beacon/aggregation/communication/adapter/did-comm.d.ts.map +1 -1
  84. package/dist/types/core/beacon/aggregation/communication/adapter/nostr.d.ts +5 -3
  85. package/dist/types/core/beacon/aggregation/communication/adapter/nostr.d.ts.map +1 -1
  86. package/dist/types/core/beacon/aggregation/communication/error.d.ts +1 -0
  87. package/dist/types/core/beacon/aggregation/communication/factory.d.ts +1 -0
  88. package/dist/types/core/beacon/aggregation/communication/service.d.ts +3 -2
  89. package/dist/types/core/beacon/aggregation/communication/service.d.ts.map +1 -1
  90. package/dist/types/core/beacon/aggregation/coordinator.d.ts +1 -0
  91. package/dist/types/core/beacon/aggregation/coordinator.d.ts.map +1 -1
  92. package/dist/types/core/beacon/aggregation/participant.d.ts +1 -0
  93. package/dist/types/core/beacon/aggregation/participant.d.ts.map +1 -1
  94. package/dist/types/core/beacon/aggregation/session/index.d.ts +1 -0
  95. package/dist/types/core/beacon/aggregation/session/index.d.ts.map +1 -1
  96. package/dist/types/core/beacon/aggregation/session/status.d.ts +1 -0
  97. package/dist/types/core/beacon/beacon.d.ts +10 -4
  98. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  99. package/dist/types/core/beacon/cas-beacon.d.ts +27 -7
  100. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  101. package/dist/types/core/beacon/error.d.ts +1 -0
  102. package/dist/types/core/beacon/factory.d.ts +1 -0
  103. package/dist/types/core/beacon/interfaces.d.ts +1 -0
  104. package/dist/types/core/beacon/signal-discovery.d.ts +1 -0
  105. package/dist/types/core/beacon/{singleton.d.ts → singleton-beacon.d.ts} +7 -5
  106. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -0
  107. package/dist/types/core/beacon/smt-beacon.d.ts +5 -3
  108. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  109. package/dist/types/core/beacon/utils.d.ts +1 -0
  110. package/dist/types/core/identifier.d.ts +1 -0
  111. package/dist/types/core/interfaces.d.ts +6 -15
  112. package/dist/types/core/interfaces.d.ts.map +1 -1
  113. package/dist/types/core/resolver.d.ts +167 -0
  114. package/dist/types/core/resolver.d.ts.map +1 -0
  115. package/dist/types/core/types.d.ts +1 -0
  116. package/dist/types/core/update.d.ts +4 -3
  117. package/dist/types/core/update.d.ts.map +1 -1
  118. package/dist/types/did-btcr2.d.ts +17 -16
  119. package/dist/types/did-btcr2.d.ts.map +1 -1
  120. package/dist/types/index.d.ts +3 -3
  121. package/dist/types/index.d.ts.map +1 -1
  122. package/dist/types/utils/appendix.d.ts +1 -0
  123. package/dist/types/utils/did-document-builder.d.ts +1 -0
  124. package/dist/types/utils/did-document.d.ts +2 -6
  125. package/dist/types/utils/did-document.d.ts.map +1 -1
  126. package/package.json +5 -5
  127. package/src/core/beacon/aggregation/cohort/messages/keygen/cohort-ready.ts +1 -1
  128. package/src/core/beacon/aggregation/communication/adapter/did-comm.ts +4 -3
  129. package/src/core/beacon/aggregation/communication/adapter/nostr.ts +4 -3
  130. package/src/core/beacon/aggregation/communication/service.ts +2 -2
  131. package/src/core/beacon/aggregation/coordinator.ts +40 -44
  132. package/src/core/beacon/aggregation/participant.ts +38 -40
  133. package/src/core/beacon/aggregation/session/index.ts +3 -4
  134. package/src/core/beacon/beacon.ts +9 -5
  135. package/src/core/beacon/cas-beacon.ts +156 -10
  136. package/src/core/beacon/factory.ts +1 -1
  137. package/src/core/beacon/{singleton.ts → singleton-beacon.ts} +20 -36
  138. package/src/core/beacon/smt-beacon.ts +4 -3
  139. package/src/core/identifier.ts +1 -1
  140. package/src/core/interfaces.ts +5 -16
  141. package/src/core/resolver.ts +706 -0
  142. package/src/core/update.ts +9 -9
  143. package/src/did-btcr2.ts +37 -130
  144. package/src/index.ts +2 -3
  145. package/src/utils/did-document.ts +10 -18
  146. package/dist/cjs/core/beacon/singleton.js.map +0 -1
  147. package/dist/cjs/core/resolve.js.map +0 -1
  148. package/dist/cjs/utils/general.js +0 -195
  149. package/dist/cjs/utils/general.js.map +0 -1
  150. package/dist/esm/core/beacon/singleton.js.map +0 -1
  151. package/dist/esm/core/resolve.js.map +0 -1
  152. package/dist/esm/utils/general.js +0 -195
  153. package/dist/esm/utils/general.js.map +0 -1
  154. package/dist/types/core/beacon/singleton.d.ts.map +0 -1
  155. package/dist/types/core/resolve.d.ts +0 -92
  156. package/dist/types/core/resolve.d.ts.map +0 -1
  157. package/dist/types/utils/general.d.ts +0 -85
  158. package/dist/types/utils/general.d.ts.map +0 -1
  159. package/src/core/resolve.ts +0 -474
  160. package/src/utils/general.ts +0 -204
@@ -0,0 +1,706 @@
1
+ import { getNetwork } from '@did-btcr2/bitcoin';
2
+ import {
3
+ canonicalHash,
4
+ canonicalize,
5
+ DateUtils,
6
+ encode as encodeHash,
7
+ decode as decodeHash,
8
+ INVALID_DID_DOCUMENT,
9
+ INVALID_DID_UPDATE,
10
+ JSONPatch,
11
+ JSONUtils,
12
+ LATE_PUBLISHING_ERROR,
13
+ ResolveError
14
+ } from '@did-btcr2/common';
15
+ import {
16
+ BIP340Cryptosuite,
17
+ BIP340DataIntegrityProof,
18
+ SchnorrMultikey,
19
+ SignedBTCR2Update,
20
+ UnsignedBTCR2Update
21
+ } from '@did-btcr2/cryptosuite';
22
+ import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
23
+ import { DidBtcr2 } from '../did-btcr2.js';
24
+ import { Appendix } from '../utils/appendix.js';
25
+ import { DidDocument, ID_PLACEHOLDER_VALUE } from '../utils/did-document.js';
26
+ import { BeaconFactory } from './beacon/factory.js';
27
+ import { BeaconService, BeaconSignal, BlockMetadata } from './beacon/interfaces.js';
28
+ import { BeaconUtils } from './beacon/utils.js';
29
+ import { DidComponents, Identifier } from './identifier.js';
30
+ import { SMTProof } from './interfaces.js';
31
+ import { CASAnnouncement, Sidecar, SidecarData } from './types.js';
32
+
33
+ /**
34
+ * The response object for DID Resolution.
35
+ */
36
+ export interface DidResolutionResponse {
37
+ didDocument: DidDocument;
38
+ metadata: {
39
+ confirmations?: number;
40
+ versionId: string;
41
+ updated?: string;
42
+ deactivated?: boolean;
43
+ }
44
+ }
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
+
89
+ /**
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.
93
+ */
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
+
177
+ /**
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}.
179
+ * @param {DidComponents} didComponents The decoded components of the did.
180
+ * @returns {DidDocument} The resolved DID Document object.
181
+ */
182
+ static deterministic(didComponents: DidComponents): DidDocument {
183
+ // Deconstruct the bytes from the given components
184
+ const genesisBytes = didComponents.genesisBytes;
185
+
186
+ // Encode the did from the didComponents
187
+ const did = Identifier.encode(genesisBytes, didComponents);
188
+
189
+ // Construct a new CompressedSecp256k1PublicKey and deconstruct the publicKey and publicKeyMultibase
190
+ const { multibase } = new CompressedSecp256k1PublicKey(genesisBytes);
191
+
192
+ // Generate the service field for the DID Document
193
+ const service = BeaconUtils.generateBeaconServices({
194
+ id : did,
195
+ publicKey : genesisBytes,
196
+ network : getNetwork(didComponents.network),
197
+ beaconType : 'SingletonBeacon'
198
+ });
199
+
200
+ return new DidDocument({
201
+ id : did,
202
+ verificationMethod : [{
203
+ id : `${did}#initialKey`,
204
+ type : 'Multikey',
205
+ controller : did,
206
+ publicKeyMultibase : multibase.encoded
207
+ }],
208
+ service
209
+ });
210
+ }
211
+
212
+ /**
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}.
214
+ * @param {DidComponents} didComponents BTCR2 DID components used to resolve the DID Document
215
+ * @param {object} genesisDocument The genesis document for resolving the DID Document.
216
+ * @returns {DidDocument} The resolved DID Document object
217
+ * @throws {ResolveError} InvalidDidDocument if not conformant to DID Core v1.1
218
+ */
219
+ static external(
220
+ didComponents: DidComponents,
221
+ genesisDocument: object,
222
+ ): DidDocument {
223
+ // Encode the genesis bytes from the did components
224
+ const genesisBytes = encodeHash(didComponents.genesisBytes);
225
+
226
+ // Canonicalize and sha256 hash the currentDocument
227
+ const genesisDocumentBytes = canonicalHash(genesisDocument);
228
+
229
+ // If the genesisBytes do not match the hashBytes, throw an error
230
+ if (genesisBytes !== genesisDocumentBytes) {
231
+ throw new ResolveError(
232
+ `Initial document mismatch: genesisBytes !== genesisDocumentBytes`,
233
+ INVALID_DID_DOCUMENT, { genesisBytes, genesisDocumentBytes }
234
+ );
235
+ }
236
+
237
+ // Encode the did from the didComponents
238
+ const did = Identifier.encode(decodeHash(genesisBytes), didComponents);
239
+
240
+ // Replace the placeholder did with the did throughout the currentDocument.
241
+ const currentDocument = JSON.parse(
242
+ JSON.stringify(genesisDocument).replaceAll(ID_PLACEHOLDER_VALUE, did)
243
+ );
244
+
245
+ // Return a W3C conformant DID Document
246
+ return new DidDocument(currentDocument);
247
+ }
248
+
249
+ /**
250
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-sidecar-data | Process Sidecar Data}
251
+ * @param {Sidecar} sidecar The sidecar data to process.
252
+ * @returns {SidecarData} The processed sidecar data containing maps of updates, CAS announcements, and SMT proofs.
253
+ */
254
+ static sidecarData(sidecar: Sidecar = {} as Sidecar): SidecarData {
255
+ // BTCR2 Signed Updates map
256
+ const updateMap = new Map<string, SignedBTCR2Update>();
257
+ if(sidecar.updates?.length)
258
+ for(const update of sidecar.updates) {
259
+ updateMap.set(canonicalHash(update), update);
260
+ }
261
+
262
+ // CAS Announcements map
263
+ const casMap = new Map<string, CASAnnouncement>();
264
+ if(sidecar.casUpdates?.length)
265
+ for(const update of sidecar.casUpdates) {
266
+ casMap.set(canonicalHash(update), update);
267
+ }
268
+
269
+ // SMT Proofs map
270
+ const smtMap = new Map<string, SMTProof>();
271
+ if(sidecar.smtProofs?.length)
272
+ for(const proof of sidecar.smtProofs) {
273
+ smtMap.set(proof.id, proof);
274
+ }
275
+
276
+ return { updateMap, casMap, smtMap };
277
+ }
278
+
279
+ /**
280
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-updates | 7.2.f Process updates Array}.
281
+ * @param {DidDocument} currentDocument The current DID Document to apply the updates to.
282
+ * @param {Array<[SignedBTCR2Update, BlockMetadata]>} unsortedUpdates The unsorted array of BTCR2 Signed Updates and their associated Block Metadata.
283
+ * @param {string} [versionTime] The optional version time to limit updates to.
284
+ * @param {string} [versionId] The optional version id to limit updates to.
285
+ * @returns {DidResolutionResponse} The updated DID Document, number of confirmations, and version id.
286
+ */
287
+ static updates(
288
+ currentDocument: DidDocument,
289
+ unsortedUpdates: Array<[SignedBTCR2Update, BlockMetadata]>,
290
+ versionTime?: string,
291
+ versionId?: string
292
+ ): DidResolutionResponse {
293
+ // Start the version number being processed at 1
294
+ let currentVersionId = 1;
295
+
296
+ // Initialize an empty array to hold the update hashes
297
+ const updateHashHistory: string[] = [];
298
+
299
+ // 1. Sort updates by targetVersionId (ascending), using blockheight as tie-breaker
300
+ const updates = unsortedUpdates.sort(([upd0, blk0], [upd1, blk1]) =>
301
+ upd0.targetVersionId - upd1.targetVersionId || blk0.height - blk1.height
302
+ );
303
+
304
+ // Create a default response object
305
+ const response: DidResolutionResponse = {
306
+ didDocument : currentDocument,
307
+ metadata : {
308
+ versionId : `${currentVersionId}`,
309
+ confirmations : 0,
310
+ updated : '',
311
+ deactivated : currentDocument.deactivated || false
312
+ }
313
+ };
314
+
315
+ // Iterate over each (update block) pair
316
+ for(const [update, block] of updates) {
317
+ // Get the hash of the current document
318
+ const currentDocumentHash = canonicalHash(response.didDocument);
319
+
320
+ // Safely convert block.time to timestamp
321
+ const blocktime = DateUtils.blocktimeToTimestamp(block.time);
322
+
323
+ // TODO: How to detect if block is unconfirmed and exit gracefully or return without it
324
+
325
+ // Set the updated field to the blocktime of the current update
326
+ response.metadata.updated = DateUtils.toISOStringNonFractional(blocktime);
327
+
328
+ // Set confirmations to the block confirmations
329
+ response.metadata.confirmations = block.confirmations;
330
+
331
+ // if resolutionOptions.versionTime is defined and the blocktime is more recent, return currentDocument
332
+ if(versionTime) {
333
+ // Safely convert versionTime to timestamp
334
+ if(blocktime > DateUtils.dateStringToTimestamp(versionTime)) {
335
+ return response;
336
+ }
337
+ }
338
+
339
+ // Check update.targetVersionId against currentVersionId
340
+ // If update.targetVersionId <= currentVersionId, confirm duplicate update
341
+ if(update.targetVersionId <= currentVersionId) {
342
+ updateHashHistory.push(currentDocumentHash);
343
+ this.confirmDuplicate(update, updateHashHistory);
344
+ }
345
+
346
+ // If update.targetVersionId == currentVersionId + 1, apply the update
347
+ else if (update.targetVersionId === currentVersionId + 1) {
348
+ // Check if update.sourceHash !== currentDocumentHash
349
+ if (update.sourceHash !== currentDocumentHash) {
350
+ // Raise an INVALID_DID_UPDATE error if they do not match
351
+ throw new ResolveError(
352
+ `Hash mismatch: update.sourceHash !== currentDocumentHash`,
353
+ INVALID_DID_UPDATE, { sourceHash: update.sourceHash, currentDocumentHash }
354
+ );
355
+ }
356
+ // Apply the update to the currentDocument and set it in the response
357
+ response.didDocument = this.applyUpdate(response.didDocument, update);
358
+
359
+ // Create unsigned_update by removing the proof property from update.
360
+ const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']) as UnsignedBTCR2Update;
361
+ // Push the canonicalized unsigned update hash to the updateHashHistory
362
+ updateHashHistory.push(canonicalHash(unsignedUpdate));
363
+ }
364
+
365
+ // If update.targetVersionId > currentVersionId + 1, throw LATE_PUBLISHING error
366
+ else if(update.targetVersionId > currentVersionId + 1) {
367
+ throw new ResolveError(
368
+ `Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
369
+ 'LATE_PUBLISHING_ERROR', {
370
+ targetVersionId : update.targetVersionId,
371
+ currentVersionId : currentVersionId + 1
372
+ }
373
+ );
374
+ }
375
+
376
+ // Increment currentVersionId
377
+ currentVersionId++;
378
+
379
+ // Set response.versionId to be the new currentVersionId
380
+ response.metadata.versionId = `${currentVersionId}`;
381
+
382
+ // If resolutionOptions.versionId is defined and <= currentVersionId, return currentDocument
383
+ const versionIdNumber = Number(versionId);
384
+ if(!isNaN(versionIdNumber) && versionIdNumber <= currentVersionId) {
385
+ return response;
386
+ }
387
+
388
+ // Check if the current document is deactivated before further processing
389
+ if(response.didDocument.deactivated) {
390
+ // Set the response deactivated flag to true
391
+ response.metadata.deactivated = response.didDocument.deactivated;
392
+ // If deactivated, stop processing further updates and return the response
393
+ return response;
394
+ }
395
+ }
396
+
397
+ // Return response data
398
+ return response;
399
+ }
400
+
401
+ // ─── Private static: update internals ──────────────────────────────
402
+
403
+ /**
404
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/#confirm-duplicate-update | 7.2.f.1 Confirm Duplicate Update}.
405
+ * This step confirms that an update with a lower-than-expected targetVersionId is a true duplicate.
406
+ * @param {SignedBTCR2Update} update The BTCR2 Signed Update to confirm as a duplicate.
407
+ * @param {string[]} updateHashHistory The accumulated hash history for comparison.
408
+ * @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
409
+ */
410
+ private static confirmDuplicate(update: SignedBTCR2Update, updateHashHistory: string[]): void {
411
+ // Create unsigned_update by removing the proof property from update.
412
+ const { proof: _, ...unsignedUpdate } = update;
413
+
414
+ // Hash unsignedUpdate with JSON Document Hashing algorithm
415
+ const unsignedUpdateHash = canonicalHash(unsignedUpdate);
416
+
417
+ // Let historicalUpdateHash equal updateHashHistory[updateHashIndex].
418
+ const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
419
+
420
+ // Check if the updateHash matches the historical hash
421
+ if (updateHashHistory[update.targetVersionId - 2] !== unsignedUpdateHash) {
422
+ throw new ResolveError(
423
+ `Invalid duplicate: ${unsignedUpdateHash} does not match ${historicalUpdateHash}`,
424
+ LATE_PUBLISHING_ERROR, { unsignedUpdateHash, updateHashHistory }
425
+ );
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Implements subsection {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#apply-update | 7.2.f.3 Apply Update}.
431
+ * @param {DidDocument} currentDocument The current DID Document to apply the update to.
432
+ * @param {SignedBTCR2Update} update The BTCR2 Signed Update to apply.
433
+ * @returns {DidDocument} The updated DID Document after applying the update.
434
+ * @throws {ResolveError} If the update is invalid or cannot be applied.
435
+ */
436
+ private static applyUpdate(
437
+ currentDocument: DidDocument,
438
+ update: SignedBTCR2Update
439
+ ): DidDocument {
440
+ // Get the capability id from the to update proof.
441
+ const capabilityId = update.proof?.capability;
442
+ // Since this field is optional, check that it exists
443
+ if (!capabilityId) {
444
+ // If it does not exist, throw INVALID_DID_UPDATE error
445
+ throw new ResolveError('No root capability found in update', INVALID_DID_UPDATE, update);
446
+ }
447
+
448
+ // Get the root capability object by dereferencing the capabilityId
449
+ const rootCapability = Appendix.dereferenceZcapId(capabilityId);
450
+
451
+ // Deconstruct the invocationTarget and controller from the root capability
452
+ const { invocationTarget, controller: rootController } = rootCapability;
453
+ // Check that both invocationTarget and rootController equal currentDocument.id
454
+ if (![invocationTarget, rootController].every((id) => id === currentDocument.id)) {
455
+ // If they do not all match, throw INVALID_DID_UPDATE error
456
+ throw new ResolveError(
457
+ 'Invalid root capability',
458
+ INVALID_DID_UPDATE, { rootCapability, currentDocument }
459
+ );
460
+ }
461
+
462
+ // Get the verificationMethod field from the update proof as verificationMethodId.
463
+ const verificationMethodId = update.proof?.verificationMethod;
464
+ // Since this field is optional, check that it exists
465
+ if(!verificationMethodId) {
466
+ // If it does not exist, throw INVALID_DID_UPDATE error
467
+ throw new ResolveError('No verificationMethod found in update', INVALID_DID_UPDATE, update);
468
+ }
469
+
470
+ // Get the verificationMethod from the DID Document using the verificationMethodId.
471
+ const vm = DidBtcr2.getSigningMethod(currentDocument, verificationMethodId);
472
+
473
+ // Construct a new SchnorrMultikey.
474
+ const multikey = SchnorrMultikey.fromVerificationMethod(vm);
475
+
476
+ // Construct a new BIP340Cryptosuite with the SchnorrMultikey.
477
+ const cryptosuite = new BIP340Cryptosuite(multikey);
478
+
479
+ // Canonicalize the update
480
+ const canonicalUpdate = canonicalize(update);
481
+
482
+ // Construct a DataIntegrityProof with the cryptosuite
483
+ const diProof = new BIP340DataIntegrityProof(cryptosuite);
484
+
485
+ // Call the verifyProof method
486
+ const verificationResult = diProof.verifyProof(canonicalUpdate, 'capabilityInvocation');
487
+
488
+ // If the result is not verified, throw INVALID_DID_UPDATE error
489
+ if (!verificationResult.verified) {
490
+ throw new ResolveError(
491
+ 'Invalid update: proof not verified',
492
+ INVALID_DID_UPDATE, verificationResult
493
+ );
494
+ }
495
+
496
+ // Apply the update.patch to the currentDocument to get the updatedDocument.
497
+ const updatedDocument = JSONPatch.apply(currentDocument, update.patch) as DidDocument;
498
+
499
+ // Verify that updatedDocument is conformant to DID Core v1.1.
500
+ DidDocument.validate(updatedDocument);
501
+
502
+ // Canonicalize and hash the updatedDocument to get the currentDocumentHash.
503
+ const currentDocumentHash = canonicalHash(updatedDocument);
504
+
505
+ // Prepare the update targetHash for comparison with currentDocumentHash.
506
+ const updateTargetHash = update.targetHash;
507
+
508
+ // Make sure the update.targetHash equals currentDocumentHash.
509
+ if (update.targetHash !== currentDocumentHash) {
510
+ // If they do not match, throw INVALID_DID_UPDATE error.
511
+ throw new ResolveError(
512
+ `Invalid update: update.targetHash !== currentDocumentHash`,
513
+ INVALID_DID_UPDATE, { updateTargetHash, currentDocumentHash }
514
+ );
515
+ }
516
+
517
+ // Return final updatedDocument.
518
+ return updatedDocument;
519
+ }
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
+ }