@did-btcr2/method 0.56.0 → 0.59.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.
- package/README.md +12 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +183 -42
- package/dist/esm/core/beacon/beacon.js +19 -2
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +7 -6
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/factory.js +6 -4
- package/dist/esm/core/beacon/factory.js.map +1 -1
- package/dist/esm/core/beacon/signal-discovery.js +24 -3
- package/dist/esm/core/beacon/signal-discovery.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +2 -2
- package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
- package/dist/esm/core/beacon/smt-beacon.js +7 -6
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/did-sender-resolver.js +8 -2
- package/dist/esm/core/did-sender-resolver.js.map +1 -1
- package/dist/esm/core/resolver.js +79 -28
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/esm/core/updater.js +17 -4
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +25 -8
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/utils/appendix.js +36 -5
- package/dist/esm/utils/appendix.js.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +10 -1
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +2 -1
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/factory.d.ts +3 -1
- package/dist/types/core/beacon/factory.d.ts.map +1 -1
- package/dist/types/core/beacon/signal-discovery.d.ts +14 -0
- package/dist/types/core/beacon/signal-discovery.d.ts.map +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +2 -1
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/did-sender-resolver.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +13 -0
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts +13 -0
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/core/updater.d.ts +11 -4
- package/dist/types/core/updater.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/utils/appendix.d.ts +30 -4
- package/dist/types/utils/appendix.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/core/beacon/beacon.ts +26 -4
- package/src/core/beacon/cas-beacon.ts +7 -6
- package/src/core/beacon/factory.ts +6 -4
- package/src/core/beacon/interfaces.ts +1 -1
- package/src/core/beacon/signal-discovery.ts +25 -3
- package/src/core/beacon/singleton-beacon.ts +2 -2
- package/src/core/beacon/smt-beacon.ts +7 -6
- package/src/core/did-sender-resolver.ts +10 -2
- package/src/core/interfaces.ts +14 -0
- package/src/core/resolver.ts +95 -29
- package/src/core/updater.ts +25 -5
- package/src/did-btcr2.ts +30 -9
- package/src/utils/appendix.ts +37 -5
|
@@ -57,9 +57,10 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
57
57
|
/**
|
|
58
58
|
* Creates an instance of CASBeacon.
|
|
59
59
|
* @param {BeaconService} service The service of the Beacon.
|
|
60
|
+
* @param {string} did The absolute did:btcr2 identifier this beacon serves.
|
|
60
61
|
*/
|
|
61
|
-
constructor(service: BeaconService) {
|
|
62
|
-
super({ ...service, type: 'CASBeacon' });
|
|
62
|
+
constructor(service: BeaconService, did: string) {
|
|
63
|
+
super({ ...service, type: 'CASBeacon' }, did);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -82,8 +83,8 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
82
83
|
const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
|
|
83
84
|
const needs = new Array<DataNeed>();
|
|
84
85
|
|
|
85
|
-
//
|
|
86
|
-
const did = this.
|
|
86
|
+
// The DID under resolution keys this beacon's announcement entry.
|
|
87
|
+
const did = this.did;
|
|
87
88
|
|
|
88
89
|
for(const signal of signals) {
|
|
89
90
|
// Signal bytes are hex, matches hex-keyed sidecar maps directly
|
|
@@ -160,8 +161,8 @@ export class CASBeacon extends SinglePartyBeacon {
|
|
|
160
161
|
bitcoin: BitcoinConnection,
|
|
161
162
|
options?: CASBroadcastOptions
|
|
162
163
|
): Promise<BroadcastResult> {
|
|
163
|
-
//
|
|
164
|
-
const did = this.
|
|
164
|
+
// The DID this beacon serves keys its announcement entry.
|
|
165
|
+
const did = this.did;
|
|
165
166
|
|
|
166
167
|
// Hash the signed update (base64urlnopad for the CAS Announcement entry per spec)
|
|
167
168
|
const updateHash = canonicalHash(signedUpdate);
|
|
@@ -14,16 +14,18 @@ export class BeaconFactory {
|
|
|
14
14
|
/**
|
|
15
15
|
* Establish a Beacon instance based on the provided service and optional sidecar data.
|
|
16
16
|
* @param {BeaconService} service The beacon service configuration.
|
|
17
|
+
* @param {string} did The absolute did:btcr2 identifier the beacon serves. Supplied
|
|
18
|
+
* by the caller because a beacon service `id` may be a relative DID URL.
|
|
17
19
|
* @returns {SinglePartyBeacon} The established Beacon instance.
|
|
18
20
|
*/
|
|
19
|
-
static establish(service: BeaconService): SinglePartyBeacon {
|
|
21
|
+
static establish(service: BeaconService, did: string): SinglePartyBeacon {
|
|
20
22
|
switch (service.type) {
|
|
21
23
|
case 'SingletonBeacon':
|
|
22
|
-
return new SingletonBeacon(service);
|
|
24
|
+
return new SingletonBeacon(service, did);
|
|
23
25
|
case 'CASBeacon':
|
|
24
|
-
return new CASBeacon(service);
|
|
26
|
+
return new CASBeacon(service, did);
|
|
25
27
|
case 'SMTBeacon':
|
|
26
|
-
return new SMTBeacon(service);
|
|
28
|
+
return new SMTBeacon(service, did);
|
|
27
29
|
default:
|
|
28
30
|
throw new MethodError('Invalid Beacon Type', 'INVALID_BEACON_ERROR', service);
|
|
29
31
|
}
|
|
@@ -76,4 +76,4 @@ export interface BeaconSignal {
|
|
|
76
76
|
*
|
|
77
77
|
* @param announcement The CAS Announcement object (DID to update hash mapping).
|
|
78
78
|
*/
|
|
79
|
-
export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
|
|
79
|
+
export type CasPublishFn = (announcement: Record<string, string>) => Promise<void>;
|
|
@@ -109,6 +109,20 @@ export class BeaconSignalDiscovery {
|
|
|
109
109
|
/**
|
|
110
110
|
* Retrieves the beacon signals for the given array of BeaconService objects
|
|
111
111
|
* using an esplora/electrs REST API connection via a bitcoin I/O driver.
|
|
112
|
+
*
|
|
113
|
+
* The address listing includes mempool transactions. The method skips a
|
|
114
|
+
* transaction whose `status.confirmed` is not `true`. A mempool transaction
|
|
115
|
+
* has no block height and no block time, so it cannot carry block metadata.
|
|
116
|
+
* The specification also says that a resolver must not process an
|
|
117
|
+
* unconfirmed transaction. An absent flag counts as unconfirmed, as it does
|
|
118
|
+
* for UTXO selection. The check runs before the OP_RETURN parse, so a
|
|
119
|
+
* mempool transaction costs no prevout fetch. The {@link fullnode} path
|
|
120
|
+
* needs no such check: it walks mined blocks only.
|
|
121
|
+
*
|
|
122
|
+
* The `confirmations` count uses the block count fetched before the listing.
|
|
123
|
+
* A block that arrives between the two calls yields a count of `0` for its
|
|
124
|
+
* transactions. The resolver then excludes them, because its minimum is at
|
|
125
|
+
* least `1`. An under-count is the safe direction, so keep that order.
|
|
112
126
|
* @param {Array<BeaconService>} beaconServices Array of BeaconService objects to retrieve signals for
|
|
113
127
|
* @param {BitcoinConnection} bitcoin Bitcoin network connection to use for REST calls
|
|
114
128
|
* @returns {Promise<Map<BeaconService, Array<BeaconSignal>>>} Map of beacon service to its discovered signals
|
|
@@ -136,6 +150,14 @@ export class BeaconSignalDiscovery {
|
|
|
136
150
|
|
|
137
151
|
// Iterate over each signal
|
|
138
152
|
for (const beaconSignal of beaconSignals) {
|
|
153
|
+
// Skip a mempool transaction before any parse or fetch. Esplora reports it
|
|
154
|
+
// as `{ confirmed: false }` with no block fields, so it has no block
|
|
155
|
+
// metadata, and the specification forbids a resolver to process it.
|
|
156
|
+
const status = beaconSignal.status;
|
|
157
|
+
if(status.confirmed !== true) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
// Get the last vout in the transaction
|
|
140
162
|
const lastSignalVout = beaconSignal.vout.slice(-1)[0];
|
|
141
163
|
|
|
@@ -169,7 +191,7 @@ export class BeaconSignalDiscovery {
|
|
|
169
191
|
}
|
|
170
192
|
|
|
171
193
|
// Use the pre-fetched block count instead of calling per-signal
|
|
172
|
-
const confirmations = currentBlockCount -
|
|
194
|
+
const confirmations = currentBlockCount - status.block_height + 1;
|
|
173
195
|
|
|
174
196
|
// Push the beacon signal object to the signals array for the beacon service
|
|
175
197
|
beaconServiceSignals.get(beaconService)?.push({
|
|
@@ -177,8 +199,8 @@ export class BeaconSignalDiscovery {
|
|
|
177
199
|
signalBytes : updateHash,
|
|
178
200
|
blockMetadata : {
|
|
179
201
|
confirmations,
|
|
180
|
-
height :
|
|
181
|
-
time :
|
|
202
|
+
height : status.block_height,
|
|
203
|
+
time : status.block_time,
|
|
182
204
|
}
|
|
183
205
|
});
|
|
184
206
|
}
|
|
@@ -20,8 +20,8 @@ export class SingletonBeacon extends SinglePartyBeacon {
|
|
|
20
20
|
* Creates an instance of SingletonBeacon.
|
|
21
21
|
* @param {BeaconService} service The BeaconService object representing the funded beacon to announce the update to.
|
|
22
22
|
*/
|
|
23
|
-
constructor(service: BeaconService) {
|
|
24
|
-
super({ ...service, type: 'SingletonBeacon' });
|
|
23
|
+
constructor(service: BeaconService, did: string) {
|
|
24
|
+
super({ ...service, type: 'SingletonBeacon' }, did);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -27,9 +27,10 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
27
27
|
/**
|
|
28
28
|
* Creates an instance of SMTBeacon.
|
|
29
29
|
* @param {BeaconService} service The Beacon service.
|
|
30
|
+
* @param {string} did The absolute did:btcr2 identifier this beacon serves.
|
|
30
31
|
*/
|
|
31
|
-
constructor(service: BeaconService) {
|
|
32
|
-
super({ ...service, type: 'SMTBeacon' });
|
|
32
|
+
constructor(service: BeaconService, did: string) {
|
|
33
|
+
super({ ...service, type: 'SMTBeacon' }, did);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -52,8 +53,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
52
53
|
const updates = new Array<[SignedBTCR2Update, BlockMetadata]>();
|
|
53
54
|
const needs = new Array<DataNeed>();
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
-
const did = this.
|
|
56
|
+
// The DID under resolution keys this beacon's leaf index.
|
|
57
|
+
const did = this.did;
|
|
57
58
|
|
|
58
59
|
for(const signal of signals) {
|
|
59
60
|
// Signal bytes are the hex-encoded SMT root hash; smtMap is keyed by proof.id (also hex)
|
|
@@ -144,8 +145,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
144
145
|
bitcoin: BitcoinConnection,
|
|
145
146
|
options?: BroadcastOptions
|
|
146
147
|
): Promise<BroadcastResult> {
|
|
147
|
-
//
|
|
148
|
-
const did = this.
|
|
148
|
+
// The DID keys this beacon's leaf index in the tree.
|
|
149
|
+
const did = this.did;
|
|
149
150
|
|
|
150
151
|
// Build a single-entry SMT from the signed update
|
|
151
152
|
const canonicalBytes = new TextEncoder().encode(canonicalize(signedUpdate));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DidDocumentError, INVALID_DID_DOCUMENT } from '@did-btcr2/common';
|
|
2
2
|
import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
|
|
3
3
|
import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
|
|
4
|
+
import { Appendix } from '../utils/appendix.js';
|
|
4
5
|
import type { DidDocument, DidVerificationMethod } from '../utils/did-document.js';
|
|
5
6
|
import { Identifier } from './identifier.js';
|
|
6
7
|
import { Resolver } from './resolver.js';
|
|
@@ -40,9 +41,16 @@ export function getAggregationCommunicationKey(
|
|
|
40
41
|
// Resolve capabilityInvocation[0] to a verification method: a string reference is
|
|
41
42
|
// dereferenced by id against verificationMethod; an embedded method is used directly.
|
|
42
43
|
// A local id lookup is used rather than getSigningMethod, which defaults to #initialKey
|
|
43
|
-
// and does not resolve embedded methods.
|
|
44
|
+
// and does not resolve embedded methods. Reference and method id are each resolved to an
|
|
45
|
+
// absolute DID URL first, because either may be spelled as a relative DID URL; an
|
|
46
|
+
// unusable reference resolves to nothing rather than to the first unusable method id.
|
|
47
|
+
const invocationId = Appendix.absoluteDidUrl(invocation, document.id);
|
|
44
48
|
const vm: DidVerificationMethod | undefined = typeof invocation === 'string'
|
|
45
|
-
?
|
|
49
|
+
? (invocationId === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: document.verificationMethod?.find(
|
|
52
|
+
method => Appendix.absoluteDidUrl(method.id, document.id) === invocationId
|
|
53
|
+
))
|
|
46
54
|
: invocation;
|
|
47
55
|
|
|
48
56
|
if(!vm) {
|
package/src/core/interfaces.ts
CHANGED
|
@@ -46,6 +46,20 @@ export interface ResolutionOptions extends DidResolutionOptions {
|
|
|
46
46
|
* document is well-formed, the resolver simply stopped at the caller's limit.
|
|
47
47
|
*/
|
|
48
48
|
maxDiscoveryRounds?: number;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Minimum number of Bitcoin block confirmations a Beacon Signal transaction
|
|
52
|
+
* must have before resolution processes it. A positive integer, minimum `1`.
|
|
53
|
+
* Defaults to `6` ({@link DEFAULT_MIN_CONF}), the value the specification
|
|
54
|
+
* mandates. A signal below the threshold is excluded from the resolution
|
|
55
|
+
* as if it did not exist yet; the rest of the signals are processed. A lower
|
|
56
|
+
* value shows a fresh update sooner and raises the exposure to a block
|
|
57
|
+
* reorganization. The `confirmations` field of the resolution metadata
|
|
58
|
+
* reports the depth of the last applied signal, so a consumer can judge it.
|
|
59
|
+
* Any other value (`0`, a negative number, a fraction, `NaN`, a string)
|
|
60
|
+
* fails with a `ResolveError` of type `INVALID_OPTIONS`.
|
|
61
|
+
*/
|
|
62
|
+
minConf?: number;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
package/src/core/resolver.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
INTERNAL_ERROR,
|
|
10
10
|
INVALID_DID_DOCUMENT,
|
|
11
11
|
INVALID_DID_UPDATE,
|
|
12
|
+
INVALID_OPTIONS,
|
|
12
13
|
JSONPatch,
|
|
13
14
|
JSONUtils,
|
|
14
15
|
LATE_PUBLISHING_ERROR,
|
|
@@ -37,6 +38,15 @@ import type { SMTProof } from './interfaces.js';
|
|
|
37
38
|
import type { CASAnnouncement, Sidecar, SidecarData } from './types.js';
|
|
38
39
|
import { equalBytes } from '@noble/curves/utils.js';
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Default minimum number of Bitcoin block confirmations a Beacon Signal
|
|
43
|
+
* transaction must have before resolution processes it. The specification
|
|
44
|
+
* mandates `6` when `ResolutionOptions.minConf` is not set: six confirmations
|
|
45
|
+
* is the accepted standard for a settled Bitcoin transaction. A resolution
|
|
46
|
+
* request can raise or lower it through `minConf`.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_MIN_CONF = 6;
|
|
49
|
+
|
|
40
50
|
/**
|
|
41
51
|
* The response object for DID Resolution.
|
|
42
52
|
*/
|
|
@@ -149,30 +159,20 @@ function isSMTProof(value: unknown): value is SMTProof {
|
|
|
149
159
|
&& Array.isArray(value.hashes);
|
|
150
160
|
}
|
|
151
161
|
|
|
152
|
-
// ─── verification relationship membership ────────────────────────────────────
|
|
153
|
-
|
|
154
162
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* absolute DID URL) or an embedded verification method object. A bare fragment
|
|
160
|
-
* is relative to the document that carries it, so it is resolved against
|
|
161
|
-
* `documentId` before comparison; every other string is returned unchanged, so
|
|
162
|
-
* a reference naming a different DID can never collapse onto this document's.
|
|
163
|
-
*
|
|
164
|
-
* Malformed entries yield `undefined` rather than a placeholder string, so two
|
|
165
|
-
* unusable values never compare equal to each other.
|
|
166
|
-
*
|
|
167
|
-
* @param {string} documentId The `id` of the DID document carrying the entry.
|
|
168
|
-
* @param {unknown} entry The relationship entry: a reference or an embedded method.
|
|
169
|
-
* @returns {string | undefined} The absolute DID URL, or undefined if unusable.
|
|
163
|
+
* Validate `ResolutionOptions.minConf`. `undefined` selects the specification
|
|
164
|
+
* default, {@link DEFAULT_MIN_CONF}. Any other value must be an integer of at
|
|
165
|
+
* least 1, as the specification defines the option.
|
|
166
|
+
* @throws {ResolveError} `INVALID_OPTIONS` for every other value.
|
|
170
167
|
*/
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
function validateMinConf(value: unknown): number {
|
|
169
|
+
if(value === undefined) return DEFAULT_MIN_CONF;
|
|
170
|
+
if(typeof value === 'number' && Number.isInteger(value) && value >= 1) return value;
|
|
171
|
+
const shown = typeof value === 'string' ? JSON.stringify(value) : String(value);
|
|
172
|
+
throw new ResolveError(
|
|
173
|
+
`Invalid resolution option minConf: expected a positive integer (minimum 1), got ${shown}.`,
|
|
174
|
+
INVALID_OPTIONS, { minConf: value }
|
|
175
|
+
);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -251,6 +251,16 @@ export class Resolver {
|
|
|
251
251
|
/** Count of beacon-discovery passes driven by updates adding new beacon services. */
|
|
252
252
|
#discoveryRounds = 0;
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Minimum block confirmations a Beacon Signal must have before this resolver
|
|
256
|
+
* processes it: `ResolutionOptions.minConf`, default {@link DEFAULT_MIN_CONF}.
|
|
257
|
+
* Applied at signal intake in the BeaconProcess phase. A signal below the
|
|
258
|
+
* threshold is excluded from the resolution; the rest of the signals are
|
|
259
|
+
* processed.
|
|
260
|
+
*/
|
|
261
|
+
readonly #minConf: number;
|
|
262
|
+
|
|
263
|
+
|
|
254
264
|
/**
|
|
255
265
|
* @internal Use {@link DidBtcr2.resolve} to create instances.
|
|
256
266
|
*/
|
|
@@ -258,7 +268,13 @@ export class Resolver {
|
|
|
258
268
|
didComponents: DidComponents,
|
|
259
269
|
sidecarData: SidecarData,
|
|
260
270
|
currentDocument: DidDocument | null,
|
|
261
|
-
options?: {
|
|
271
|
+
options?: {
|
|
272
|
+
versionId?: string;
|
|
273
|
+
versionTime?: string;
|
|
274
|
+
genesisDocument?: object;
|
|
275
|
+
maxDiscoveryRounds?: number;
|
|
276
|
+
minConf?: number;
|
|
277
|
+
}
|
|
262
278
|
) {
|
|
263
279
|
this.#didComponents = didComponents;
|
|
264
280
|
this.#sidecarData = sidecarData;
|
|
@@ -269,6 +285,9 @@ export class Resolver {
|
|
|
269
285
|
// finite resource guard. A non-positive or omitted value means no limit.
|
|
270
286
|
const rounds = options?.maxDiscoveryRounds;
|
|
271
287
|
this.#maxDiscoveryRounds = typeof rounds === 'number' && rounds > 0 ? rounds : Infinity;
|
|
288
|
+
// The signal confirmation threshold. An invalid value fails here, before any
|
|
289
|
+
// data need is emitted, so the caller does no I/O for a request it cannot serve.
|
|
290
|
+
this.#minConf = validateMinConf(options?.minConf);
|
|
272
291
|
|
|
273
292
|
// If a genesis document was provided (from sidecar), pre-seed it for validation
|
|
274
293
|
if(options?.genesisDocument) {
|
|
@@ -394,6 +413,10 @@ export class Resolver {
|
|
|
394
413
|
* Version counter and update-hash history carried from earlier discovery rounds.
|
|
395
414
|
* Standalone callers omit it and start fresh at version 1 with an empty history.
|
|
396
415
|
* @returns {DidResolutionResponse} The updated DID Document, number of confirmations, and version id.
|
|
416
|
+
*
|
|
417
|
+
* Confirmation depth is not checked here. The BeaconProcess phase excludes a
|
|
418
|
+
* signal below `ResolutionOptions.minConf` before its update reaches this method,
|
|
419
|
+
* so every tuple here comes from a block at or above the threshold.
|
|
397
420
|
*/
|
|
398
421
|
static updates(
|
|
399
422
|
currentDocument: DidDocument,
|
|
@@ -434,8 +457,6 @@ export class Resolver {
|
|
|
434
457
|
// Safely convert block.time to timestamp
|
|
435
458
|
const blocktime = DateUtils.blocktimeToTimestamp(block.time);
|
|
436
459
|
|
|
437
|
-
// TODO: How to detect if block is unconfirmed and exit gracefully or return without it
|
|
438
|
-
|
|
439
460
|
// Set the updated field to the blocktime of the current update
|
|
440
461
|
response.metadata.updated = DateUtils.toISOStringNonFractional(blocktime);
|
|
441
462
|
|
|
@@ -636,9 +657,9 @@ export class Resolver {
|
|
|
636
657
|
// without it here the read path applies an update signed by any key in the document.
|
|
637
658
|
// Checked before the method is located so an unauthorized method always fails with
|
|
638
659
|
// this typed error, whether or not it also appears in verificationMethod[].
|
|
639
|
-
const authorizedMethodId = relationshipMethodId(currentDocument.id
|
|
660
|
+
const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, currentDocument.id);
|
|
640
661
|
const authorized = authorizedMethodId !== undefined && currentDocument.capabilityInvocation?.some(
|
|
641
|
-
entry => relationshipMethodId(currentDocument.id
|
|
662
|
+
entry => Appendix.relationshipMethodId(entry, currentDocument.id) === authorizedMethodId
|
|
642
663
|
);
|
|
643
664
|
if(!authorized) {
|
|
644
665
|
throw new ResolveError(
|
|
@@ -772,9 +793,17 @@ export class Resolver {
|
|
|
772
793
|
// Skip already-processed services and services with no signals
|
|
773
794
|
if(this.#processedServices.has(service.id) || !signals.length) continue;
|
|
774
795
|
|
|
796
|
+
// Keep only the signals at or above the confirmation threshold. A
|
|
797
|
+
// service whose signals are all below it is treated like a service
|
|
798
|
+
// with no signals: it is not processed and not marked processed.
|
|
799
|
+
const eligible = this.#eligibleSignals(signals);
|
|
800
|
+
if(!eligible.length) continue;
|
|
801
|
+
|
|
775
802
|
// Establish a typed beacon and process its signals
|
|
776
|
-
|
|
777
|
-
|
|
803
|
+
// The beacon is bound to the DID under resolution: a beacon service
|
|
804
|
+
// `id` may be a relative DID URL, so it cannot supply the subject.
|
|
805
|
+
const beacon = BeaconFactory.establish(service, this.#currentDocument!.id);
|
|
806
|
+
const result = beacon.processSignals(eligible, this.#sidecarData);
|
|
778
807
|
|
|
779
808
|
if(result.needs.length > 0) {
|
|
780
809
|
// This service has unmet data needs, collect them
|
|
@@ -864,6 +893,43 @@ export class Resolver {
|
|
|
864
893
|
}
|
|
865
894
|
}
|
|
866
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Return the signals of one beacon service that resolution may process: the
|
|
898
|
+
* signals with at least `#minConf` confirmations. The specification removes a
|
|
899
|
+
* transaction below the threshold from the set of Beacon Signals, so an
|
|
900
|
+
* excluded signal emits no data need and applies no update. A signal with no
|
|
901
|
+
* integer confirmation count is excluded too: that is a mempool transaction
|
|
902
|
+
* from a driver that did not skip it.
|
|
903
|
+
*
|
|
904
|
+
* An eligible signal must carry a finite block height and block time. A
|
|
905
|
+
* signal that passes the count but lacks them is malformed. It fails fast
|
|
906
|
+
* here with a typed error, in the style of the {@link provide} guards, and
|
|
907
|
+
* not later with an invalid date inside {@link updates}.
|
|
908
|
+
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
909
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
910
|
+
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
911
|
+
*/
|
|
912
|
+
#eligibleSignals(signals: Array<BeaconSignal>): Array<BeaconSignal> {
|
|
913
|
+
const eligible: Array<BeaconSignal> = [];
|
|
914
|
+
for(const signal of signals) {
|
|
915
|
+
const block = signal.blockMetadata as Partial<BlockMetadata> | undefined;
|
|
916
|
+
const confirmations = block?.confirmations;
|
|
917
|
+
if(!Number.isInteger(confirmations) || (confirmations as number) < this.#minConf) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
if(!Number.isFinite(block?.height) || !Number.isFinite(block?.time)) {
|
|
921
|
+
throw new ResolveError(
|
|
922
|
+
`Beacon signal ${signal.signalBytes} has ${confirmations} confirmations `
|
|
923
|
+
+ 'but no valid block height or block time.',
|
|
924
|
+
INVALID_DID_UPDATE,
|
|
925
|
+
{ signalBytes: signal.signalBytes, confirmations, height: block?.height, time: block?.time }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
eligible.push(signal);
|
|
929
|
+
}
|
|
930
|
+
return eligible;
|
|
931
|
+
}
|
|
932
|
+
|
|
867
933
|
/**
|
|
868
934
|
* Provide data the resolver requested in a previous {@link resolve} call.
|
|
869
935
|
* Call once per need, then call {@link resolve} again to continue.
|
package/src/core/updater.ts
CHANGED
|
@@ -58,8 +58,8 @@ export interface FundingProof {
|
|
|
58
58
|
* The updater needs the caller to broadcast the signed update via the beacon.
|
|
59
59
|
*
|
|
60
60
|
* The caller decides how: for single-party beacons, call
|
|
61
|
-
* `Updater.announce(beaconService, signedUpdate, signer, bitcoin, options?)` or
|
|
62
|
-
* `BeaconFactory.establish(beaconService).broadcastSignal(...)`. For multi-party
|
|
61
|
+
* `Updater.announce(beaconService, did, signedUpdate, signer, bitcoin, options?)` or
|
|
62
|
+
* `BeaconFactory.establish(beaconService, did).broadcastSignal(...)`. For multi-party
|
|
63
63
|
* aggregate beacons, hand off to the aggregation protocol.
|
|
64
64
|
*
|
|
65
65
|
* Both single-party paths return a `BroadcastResult`; capture it. Beyond the txid,
|
|
@@ -76,6 +76,11 @@ export interface NeedBroadcast {
|
|
|
76
76
|
readonly beaconService: BeaconService;
|
|
77
77
|
/** The signed update ready for broadcast. */
|
|
78
78
|
readonly signedUpdate: SignedBTCR2Update;
|
|
79
|
+
/**
|
|
80
|
+
* The DID being updated. Supplied because a beacon service `id` may be a
|
|
81
|
+
* relative DID URL and so cannot be used to recover the subject.
|
|
82
|
+
*/
|
|
83
|
+
readonly did: string;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
/** Discriminated union of all data needs the updater may request from the caller. */
|
|
@@ -148,7 +153,7 @@ export interface UpdaterParams {
|
|
|
148
153
|
* // Capture the BroadcastResult: broadcast.txid, plus broadcast.announcement
|
|
149
154
|
* // (CAS beacons) and broadcast.proof (SMT beacons; must be retained, the
|
|
150
155
|
* // proof's nonce exists nowhere else).
|
|
151
|
-
* const broadcast = await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
|
|
156
|
+
* const broadcast = await Updater.announce(need.beaconService, need.did, need.signedUpdate, signer, bitcoin);
|
|
152
157
|
* updater.provide(need);
|
|
153
158
|
* break;
|
|
154
159
|
* }
|
|
@@ -278,6 +283,12 @@ export class Updater {
|
|
|
278
283
|
const id = verificationMethod.id.slice(hashIdx);
|
|
279
284
|
const multikey = SchnorrMultikey.fromSigner(id, controller, signer);
|
|
280
285
|
|
|
286
|
+
// The absolute spelling of the method id, for the proof to name it by. `hashIdx === 0`
|
|
287
|
+
// means the document writes this method's own id as a relative DID URL, which per DID
|
|
288
|
+
// Core denotes that fragment of the document's `id`: the same rule as
|
|
289
|
+
// Appendix.absoluteDidUrl, whose `undefined` case the fragment check above excludes.
|
|
290
|
+
const absoluteMethodId = hashIdx === 0 ? `${did}${id}` : verificationMethod.id;
|
|
291
|
+
|
|
281
292
|
// Fail fast if the signer's public key is not the key published in the named
|
|
282
293
|
// verification method. Signing with the wrong key yields a Data Integrity
|
|
283
294
|
// proof that cannot verify against the method, so the resulting on-chain
|
|
@@ -308,7 +319,12 @@ export class Updater {
|
|
|
308
319
|
],
|
|
309
320
|
cryptosuite : 'bip340-jcs-2025',
|
|
310
321
|
type : 'DataIntegrityProof',
|
|
311
|
-
|
|
322
|
+
// The proof names the signing method by absolute DID URL, even when the document
|
|
323
|
+
// spells that method's own id relatively: a proof travels apart from the document
|
|
324
|
+
// that defines the method, so a bare `#initialKey` in it resolves against nothing.
|
|
325
|
+
// For a document that already spells its ids absolutely this is the id unchanged,
|
|
326
|
+
// so proofs over such documents are byte-identical to before.
|
|
327
|
+
verificationMethod : absoluteMethodId,
|
|
312
328
|
proofPurpose : 'capabilityInvocation',
|
|
313
329
|
capability : `urn:zcap:root:${encodeURIComponent(did)}`,
|
|
314
330
|
capabilityAction : 'Write',
|
|
@@ -323,6 +339,8 @@ export class Updater {
|
|
|
323
339
|
* Announces a signed update to the Bitcoin blockchain via the specified beacon.
|
|
324
340
|
*
|
|
325
341
|
* @param {BeaconService} beaconService The beacon service to broadcast through.
|
|
342
|
+
* @param {string} did The DID being updated. Required because a beacon service
|
|
343
|
+
* `id` may be a relative DID URL and so cannot supply the subject.
|
|
326
344
|
* @param {SignedBTCR2Update} update The signed update to announce.
|
|
327
345
|
* @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
|
|
328
346
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
@@ -334,12 +352,13 @@ export class Updater {
|
|
|
334
352
|
*/
|
|
335
353
|
static async announce(
|
|
336
354
|
beaconService: BeaconService,
|
|
355
|
+
did: string,
|
|
337
356
|
update: SignedBTCR2Update,
|
|
338
357
|
signer: Signer,
|
|
339
358
|
bitcoin: BitcoinConnection,
|
|
340
359
|
options?: CASBroadcastOptions
|
|
341
360
|
): Promise<BroadcastResult> {
|
|
342
|
-
const beacon = BeaconFactory.establish(beaconService);
|
|
361
|
+
const beacon = BeaconFactory.establish(beaconService, did);
|
|
343
362
|
return beacon.broadcastSignal(update, signer, bitcoin, options);
|
|
344
363
|
}
|
|
345
364
|
|
|
@@ -406,6 +425,7 @@ export class Updater {
|
|
|
406
425
|
kind : 'NeedBroadcast',
|
|
407
426
|
beaconService : this.#beaconService,
|
|
408
427
|
signedUpdate : this.#state.signedUpdate,
|
|
428
|
+
did : this.#sourceDocument.id,
|
|
409
429
|
}],
|
|
410
430
|
};
|
|
411
431
|
}
|
package/src/did-btcr2.ts
CHANGED
|
@@ -131,7 +131,8 @@ export class DidBtcr2 implements DidMethod {
|
|
|
131
131
|
versionId : resolutionOptions.versionId,
|
|
132
132
|
versionTime : resolutionOptions.versionTime,
|
|
133
133
|
genesisDocument : resolutionOptions.sidecar?.genesisDocument,
|
|
134
|
-
maxDiscoveryRounds : resolutionOptions.maxDiscoveryRounds
|
|
134
|
+
maxDiscoveryRounds : resolutionOptions.maxDiscoveryRounds,
|
|
135
|
+
minConf : resolutionOptions.minConf
|
|
135
136
|
});
|
|
136
137
|
}
|
|
137
138
|
|
|
@@ -171,8 +172,16 @@ export class DidBtcr2 implements DidMethod {
|
|
|
171
172
|
verificationMethodId: string;
|
|
172
173
|
beaconId: string;
|
|
173
174
|
}): Updater {
|
|
174
|
-
// Validate that the verificationMethodId is authorized for capabilityInvocation
|
|
175
|
-
|
|
175
|
+
// Validate that the verificationMethodId is authorized for capabilityInvocation.
|
|
176
|
+
// Both sides are resolved to absolute DID URLs first, so the caller's spelling of the
|
|
177
|
+
// reference and the document's spelling of the entry may differ: either is legal per
|
|
178
|
+
// DID Core. This is the same rule the read path applies to an update's proof, so an
|
|
179
|
+
// update this factory authorizes is one the resolver will also accept.
|
|
180
|
+
const authorizedMethodId = Appendix.relationshipMethodId(verificationMethodId, sourceDocument.id);
|
|
181
|
+
const authorized = authorizedMethodId !== undefined && sourceDocument.capabilityInvocation?.some(
|
|
182
|
+
entry => Appendix.relationshipMethodId(entry, sourceDocument.id) === authorizedMethodId
|
|
183
|
+
);
|
|
184
|
+
if(!authorized) {
|
|
176
185
|
throw new UpdateError(
|
|
177
186
|
'Invalid verificationMethodId: not authorized for capabilityInvocation',
|
|
178
187
|
INVALID_DID_DOCUMENT, sourceDocument
|
|
@@ -206,9 +215,14 @@ export class DidBtcr2 implements DidMethod {
|
|
|
206
215
|
);
|
|
207
216
|
}
|
|
208
217
|
|
|
209
|
-
// Find the beacon service matching the given beaconId
|
|
218
|
+
// Find the beacon service matching the given beaconId. A service `id` may be a relative
|
|
219
|
+
// DID URL, so both sides are resolved against the document before comparison; an
|
|
220
|
+
// unusable beaconId matches nothing rather than matching an unusable service id.
|
|
221
|
+
const targetBeaconId = Appendix.absoluteDidUrl(beaconId, sourceDocument.id);
|
|
210
222
|
const beaconService = sourceDocument.service
|
|
211
|
-
.filter((service: BeaconService) =>
|
|
223
|
+
.filter((service: BeaconService) =>
|
|
224
|
+
targetBeaconId !== undefined
|
|
225
|
+
&& Appendix.absoluteDidUrl(service.id, sourceDocument.id) === targetBeaconId)
|
|
212
226
|
.filter((service: BeaconService): service is BeaconService => !!service)
|
|
213
227
|
.shift();
|
|
214
228
|
|
|
@@ -250,10 +264,17 @@ export class DidBtcr2 implements DidMethod {
|
|
|
250
264
|
}
|
|
251
265
|
|
|
252
266
|
// Attempt to find a verification method that matches the given method ID, or if not given,
|
|
253
|
-
// find the first verification method intended for signing claims.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
// find the first verification method intended for signing claims. Both sides are resolved
|
|
268
|
+
// to absolute DID URLs first: a document may spell either the verification method `id` or
|
|
269
|
+
// the reference to it as a relative DID URL (`#initialKey`), and the two must still match.
|
|
270
|
+
const targetId = Appendix.absoluteDidUrl(methodId, didDocument.id)
|
|
271
|
+
?? Appendix.relationshipMethodId(didDocument.assertionMethod?.[0], didDocument.id);
|
|
272
|
+
|
|
273
|
+
// An unusable target matches nothing: without this guard it compares equal to every
|
|
274
|
+
// method whose own id is unusable, and the document's first malformed method is
|
|
275
|
+
// returned as the signing method.
|
|
276
|
+
const verificationMethod = targetId === undefined ? undefined : didDocument.verificationMethod?.find(
|
|
277
|
+
(vm: DidVerificationMethod) => Appendix.absoluteDidUrl(vm.id, didDocument.id) === targetId
|
|
257
278
|
);
|
|
258
279
|
|
|
259
280
|
// If no verification method is found, throw an error
|
package/src/utils/appendix.ts
CHANGED
|
@@ -18,14 +18,46 @@ import type { RootCapability } from '../core/interfaces.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export class Appendix {
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* Resolves a DID URL that may be written as a relative reference against the
|
|
22
|
+
* DID it appears under. DID Core permits the `id` of a verification method or
|
|
23
|
+
* service, and the entries of a verification relationship, to be a relative
|
|
24
|
+
* DID URL such as `#initialKey`; it denotes that fragment of `did`. Absolute
|
|
25
|
+
* DID URLs are returned unchanged, so both spellings of the same reference
|
|
26
|
+
* compare equal without discarding the DID being compared.
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} input The DID URL to resolve. Non-strings yield `undefined`.
|
|
29
|
+
* @param {string} did The DID the relative reference is resolved against.
|
|
30
|
+
* @returns {string | undefined} The absolute DID URL, or `undefined` if `input` is not a usable string.
|
|
24
31
|
*/
|
|
25
|
-
public static
|
|
32
|
+
public static absoluteDidUrl(input: unknown, did: string): string | undefined {
|
|
26
33
|
if (typeof input !== 'string') return undefined;
|
|
27
34
|
if (input.length === 0) return undefined;
|
|
28
|
-
return input;
|
|
35
|
+
return input.startsWith('#') ? `${did}${input}` : input;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolves a verification relationship entry to the absolute DID URL of the method it
|
|
40
|
+
* names. A relationship entry is either a reference to a method defined elsewhere in the
|
|
41
|
+
* document, or a method embedded inline, which names itself with its own `id`; either
|
|
42
|
+
* spelling may be a relative DID URL.
|
|
43
|
+
*
|
|
44
|
+
* Every comparison of a relationship entry against a method id goes through this helper,
|
|
45
|
+
* on both the read path and the write path, so the two admit exactly the same spellings.
|
|
46
|
+
*
|
|
47
|
+
* Malformed entries yield `undefined` rather than a placeholder string, so two unusable
|
|
48
|
+
* values never compare equal to each other. Callers comparing a resolved entry must still
|
|
49
|
+
* reject an `undefined` target before comparing, or an unusable target matches an
|
|
50
|
+
* unusable entry.
|
|
51
|
+
*
|
|
52
|
+
* @param {unknown} entry The relationship entry: a reference, or an embedded method.
|
|
53
|
+
* @param {string} did The DID the relative reference is resolved against.
|
|
54
|
+
* @returns {string | undefined} The absolute DID URL, or `undefined` if unusable.
|
|
55
|
+
*/
|
|
56
|
+
public static relationshipMethodId(entry: unknown, did: string): string | undefined {
|
|
57
|
+
const id = (typeof entry === 'object' && entry !== null && !Array.isArray(entry))
|
|
58
|
+
? (entry as { id?: unknown }).id
|
|
59
|
+
: entry;
|
|
60
|
+
return Appendix.absoluteDidUrl(id, did);
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
/**
|