@did-btcr2/method 0.65.0 → 0.66.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 +0 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +3 -3
- package/dist/browser.mjs +3 -3
- package/dist/cjs/index.js +68 -40
- package/dist/esm/core/beacon/smt-beacon.js +37 -31
- package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
- package/dist/esm/core/identifier.js +3 -1
- package/dist/esm/core/identifier.js.map +1 -1
- package/dist/esm/core/resolver.js +68 -28
- package/dist/esm/core/resolver.js.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +8 -6
- package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
- package/dist/types/core/identifier.d.ts.map +1 -1
- package/dist/types/core/interfaces.d.ts +27 -14
- package/dist/types/core/interfaces.d.ts.map +1 -1
- package/dist/types/core/resolver.d.ts.map +1 -1
- package/dist/types/core/types.d.ts +4 -2
- package/dist/types/core/types.d.ts.map +1 -1
- package/package.json +5 -20
- package/src/core/beacon/smt-beacon.ts +40 -34
- package/src/core/identifier.ts +4 -2
- package/src/core/interfaces.ts +27 -14
- package/src/core/resolver.ts +69 -28
- package/src/core/types.ts +4 -2
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { BitcoinConnection } from '@did-btcr2/bitcoin';
|
|
2
|
-
import {
|
|
2
|
+
import { canonicalHashBytes, INVALID_SIGNAL_DATA } from '@did-btcr2/common';
|
|
3
3
|
import type { SignedBTCR2Update } from '../btcr2-update.js';
|
|
4
4
|
import type { Signer } from '@did-btcr2/keypair';
|
|
5
|
-
import { base64UrlToHash,
|
|
5
|
+
import { base64UrlToHash, BTCR2MerkleTree, hashToHex, verifyProof } from '@did-btcr2/smt';
|
|
6
6
|
import { randomBytes } from '@noble/hashes/utils';
|
|
7
7
|
import type { BeaconProcessResult, DataNeed } from '../resolver.js';
|
|
8
|
+
import type { SMTProof } from '../interfaces.js';
|
|
8
9
|
import type { SidecarData } from '../types.js';
|
|
9
10
|
import type { BroadcastOptions, BroadcastResult } from './beacon.js';
|
|
10
11
|
import { SinglePartyBeacon } from './beacon.js';
|
|
11
12
|
import { SMTBeaconError } from './error.js';
|
|
12
13
|
import type { BeaconService, BeaconSignal, BlockMetadata } from './interfaces.js';
|
|
13
14
|
|
|
15
|
+
/** The hex of the base64url `id` of a proof, or `undefined` if the id does not decode to 32 bytes. */
|
|
16
|
+
function proofIdHex(proof: SMTProof): string | undefined {
|
|
17
|
+
try {
|
|
18
|
+
return hashToHex(base64UrlToHash(proof.id));
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/**
|
|
15
25
|
* Implements {@link https://dcdpr.github.io/did-btcr2/terminology.html#smt-beacon | SMT Beacon}.
|
|
16
26
|
*
|
|
@@ -36,15 +46,17 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
36
46
|
/**
|
|
37
47
|
* Implements {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-smt-beacon | 7.2.e.1 Process SMT Beacon}.
|
|
38
48
|
*
|
|
39
|
-
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
40
|
-
* This method looks up the SMT Proof from the sidecar by root
|
|
41
|
-
*
|
|
42
|
-
* signed update
|
|
49
|
+
* For each signal, the signalBytes contain the hex-encoded SMT root hash
|
|
50
|
+
* (`smt_root`). This method looks up the SMT Proof from the sidecar by root
|
|
51
|
+
* hash, checks that the id of the proof is the root, verifies the proof with
|
|
52
|
+
* the SMT Proof Verification algorithm, and retrieves the signed update by the
|
|
53
|
+
* proof's updateId. A proof with no updateId announces no update for the DID.
|
|
43
54
|
*
|
|
44
55
|
* @param {Array<BeaconSignal>} signals The array of Beacon Signals to process.
|
|
45
56
|
* @param {SidecarData} sidecar The sidecar data associated with the SMT Beacon.
|
|
46
57
|
* @returns {BeaconProcessResult} Successfully resolved updates and any data needs.
|
|
47
|
-
* @throws {SMTBeaconError} if
|
|
58
|
+
* @throws {SMTBeaconError} `INVALID_SIGNAL_DATA` if the id of the proof is not the
|
|
59
|
+
* signal root, or if the proof does not verify.
|
|
48
60
|
*/
|
|
49
61
|
processSignals(
|
|
50
62
|
signals: Array<BeaconSignal>,
|
|
@@ -57,7 +69,8 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
57
69
|
const did = this.did;
|
|
58
70
|
|
|
59
71
|
for(const signal of signals) {
|
|
60
|
-
//
|
|
72
|
+
// "Process SMT Beacon": the signal bytes are smt_root, the hex SMT root hash.
|
|
73
|
+
// The smtMap is keyed by the hex of proof.id. No entry = a need for the proof.
|
|
61
74
|
const smtProof = sidecar.smtMap.get(signal.signalBytes);
|
|
62
75
|
|
|
63
76
|
if(!smtProof) {
|
|
@@ -70,39 +83,32 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
70
83
|
continue;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
//
|
|
74
|
-
|
|
86
|
+
// The id of the proof must equal smt_root. The resolver keys the map by the
|
|
87
|
+
// hex of the id, so its entries pass. A caller-built map is checked here too.
|
|
88
|
+
if(proofIdHex(smtProof) !== signal.signalBytes) {
|
|
75
89
|
throw new SMTBeaconError(
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
`SMT proof id does not equal the signal root ${signal.signalBytes}.`,
|
|
91
|
+
INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes }
|
|
78
92
|
);
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
// Verify the
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
const index = didToIndex(did);
|
|
86
|
-
const nonceHash = base64UrlToHash(smtProof.nonce);
|
|
87
|
-
const candidateHash = smtProof.updateId
|
|
88
|
-
? blockHash(blockHash(nonceHash), base64UrlToHash(smtProof.updateId))
|
|
89
|
-
: blockHash(blockHash(nonceHash));
|
|
90
|
-
const valid = verifySerializedProof(smtProof, index, candidateHash);
|
|
91
|
-
|
|
92
|
-
if(!valid) {
|
|
95
|
+
// Verify the proof with the SMT Proof Verification algorithm. The nonce and
|
|
96
|
+
// updateId fields of the proof select the leaf value (four arms). A proof that
|
|
97
|
+
// does not decode, or that does not walk to the root, is INVALID_SIGNAL_DATA.
|
|
98
|
+
if(!verifyProof(smtProof, did)) {
|
|
93
99
|
throw new SMTBeaconError(
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
`SMT proof verification failed for the signal root ${signal.signalBytes}.`,
|
|
101
|
+
INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes }
|
|
96
102
|
);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
//
|
|
100
|
-
if(
|
|
105
|
+
// No updateId: the signal announces no update for this DID. No tuple.
|
|
106
|
+
if(smtProof.updateId === undefined) {
|
|
101
107
|
continue;
|
|
102
108
|
}
|
|
103
109
|
|
|
104
110
|
// Look up the signed update in sidecar updateMap (keyed by hex canonical
|
|
105
|
-
// hash). The proof's updateId is
|
|
111
|
+
// hash). The proof's updateId is the same hash in base64url.
|
|
106
112
|
const updateHashHex = hashToHex(base64UrlToHash(smtProof.updateId));
|
|
107
113
|
const signedUpdate = sidecar.updateMap.get(updateHashHex);
|
|
108
114
|
|
|
@@ -135,7 +141,7 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
135
141
|
* @param {BitcoinConnection} bitcoin The Bitcoin network connection.
|
|
136
142
|
* @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
|
|
137
143
|
* @return {Promise<BroadcastResult>} The signed update, the signal txid, and the SMT
|
|
138
|
-
*
|
|
144
|
+
* proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
|
|
139
145
|
* distribution: the nonce exists only here, so the on-chain signal is unresolvable without it.
|
|
140
146
|
* @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
|
|
141
147
|
*/
|
|
@@ -148,14 +154,14 @@ export class SMTBeacon extends SinglePartyBeacon {
|
|
|
148
154
|
// The DID keys this beacon's leaf index in the tree.
|
|
149
155
|
const did = this.did;
|
|
150
156
|
|
|
151
|
-
// Build a single-entry SMT
|
|
152
|
-
|
|
157
|
+
// Build a single-entry SMT in nonce mode: the leaf value is
|
|
158
|
+
// hash(hash(nonce) + updateId), with updateId the JSON Document Hash of the update.
|
|
153
159
|
const nonce = randomBytes(32);
|
|
154
160
|
const tree = new BTCR2MerkleTree();
|
|
155
|
-
tree.addEntries([{ did, nonce,
|
|
161
|
+
tree.addEntries([{ did, nonce, updateId: canonicalHashBytes(signedUpdate) }]);
|
|
156
162
|
tree.finalize();
|
|
157
163
|
|
|
158
|
-
// Serialize the
|
|
164
|
+
// Serialize the proof (carrying the nonce and updateId) before
|
|
159
165
|
// broadcasting: it is the only artifact that can link the on-chain root back
|
|
160
166
|
// to the update, and the nonce it embeds is irrecoverable once dropped.
|
|
161
167
|
const proof = tree.proof(did);
|
package/src/core/identifier.ts
CHANGED
|
@@ -150,8 +150,10 @@ export class Identifier {
|
|
|
150
150
|
if (typeof network !== 'string') {
|
|
151
151
|
throw new IdentifierError('Expected "network" to be a known network name', INVALID_DID, { network });
|
|
152
152
|
}
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// The numeric enum also maps a value string to a name: '5' reads as 'mutinynet', and
|
|
154
|
+
// the low nibble becomes 0 (bitcoin). Only a name maps to a number.
|
|
155
|
+
const networkValue: unknown = BitcoinNetworkNames[network as keyof typeof BitcoinNetworkNames];
|
|
156
|
+
if (typeof networkValue !== 'number') {
|
|
155
157
|
throw new IdentifierError('Invalid "network" name', INVALID_DID, { network });
|
|
156
158
|
}
|
|
157
159
|
|
package/src/core/interfaces.ts
CHANGED
|
@@ -76,22 +76,26 @@ export interface ResolutionOptions extends DidResolutionOptions {
|
|
|
76
76
|
/**
|
|
77
77
|
* {@link https://dcdpr.github.io/did-btcr2/terminology.html#smt-proof | SMT Proof}
|
|
78
78
|
* a set of SHA-256 hashes for nodes in a Sparse Merkle Tree that together form
|
|
79
|
-
* a path from a leaf in the tree to the Merkle root
|
|
79
|
+
* a path from a leaf in the tree to the Merkle root. The proof shows the value of
|
|
80
|
+
* the leaf at the index of the DID: an update, no update, or an empty index.
|
|
80
81
|
* See {@link https://dcdpr.github.io/did-btcr2/data-structures.html#smt-proof | SMT Proof (data structure)}.
|
|
81
82
|
*
|
|
82
|
-
* All
|
|
83
|
-
*
|
|
84
|
-
*
|
|
83
|
+
* All fields are "base64url" [RFC4648] encoded without padding. `id`, `updateId`,
|
|
84
|
+
* `collapsed`, and the entries of `hashes` decode to 32 bytes (43 chars each).
|
|
85
|
+
* `nonce` has any length. The presence of `nonce` and `updateId` selects the leaf
|
|
86
|
+
* value that the SMT Proof Verification algorithm walks from:
|
|
87
|
+
* `hash(hash(nonce) + updateId)`, `hash(hash(nonce))`, `updateId`, or the value of
|
|
88
|
+
* an empty leaf.
|
|
85
89
|
*
|
|
86
90
|
* @example
|
|
87
91
|
* ```json
|
|
88
92
|
* {
|
|
89
|
-
* "id": "
|
|
90
|
-
* "nonce": "
|
|
91
|
-
* "updateId": "
|
|
92
|
-
* "collapsed": "
|
|
93
|
+
* "id": "ZSN-lAyRpXG72aK1xLC9sAuRhFGsILupaQXxpkITJuo",
|
|
94
|
+
* "nonce": "WYVxNuwz3RBEhnJKM4LvVh2tOdXI9WRUPYqA_qa0klM",
|
|
95
|
+
* "updateId": "_YDKmjcnIkHDY6rnRwrO86id5H1Onycy7Bz62jYq6GA",
|
|
96
|
+
* "collapsed": "-_________________________________________8",
|
|
93
97
|
* "hashes": [
|
|
94
|
-
* "
|
|
98
|
+
* "s-2LV-dfS-x___DBpNeH4KaBBSJj0xCSpn8ZlusZwLo"
|
|
95
99
|
* ]
|
|
96
100
|
* }
|
|
97
101
|
* ```
|
|
@@ -99,23 +103,32 @@ export interface ResolutionOptions extends DidResolutionOptions {
|
|
|
99
103
|
export interface SMTProof {
|
|
100
104
|
/**
|
|
101
105
|
* base64url (no padding) SHA-256 hash of the root node of the Sparse Merkle Tree.
|
|
106
|
+
* The resolver compares it to the Signal Bytes of the SMT beacon signal.
|
|
102
107
|
*/
|
|
103
108
|
id: string;
|
|
104
109
|
/**
|
|
105
|
-
* Optional
|
|
110
|
+
* Optional nonce, one for each index in each Beacon Signal, of any length.
|
|
111
|
+
* base64url, no padding. Without the nonce that the DID controller used, the
|
|
112
|
+
* proof of that signal cannot be verified. Absent in no-nonce mode.
|
|
106
113
|
*/
|
|
107
114
|
nonce?: string;
|
|
108
115
|
/**
|
|
109
|
-
* Optional base64url (no padding)
|
|
116
|
+
* Optional base64url (no padding) JSON Document Hash of the BTCR2 Signed Update.
|
|
117
|
+
* Present when the signal announces an update for the DID. Absent when it does not.
|
|
110
118
|
*/
|
|
111
119
|
updateId?: string;
|
|
112
120
|
/**
|
|
113
|
-
* base64url (no padding) bitmap of
|
|
114
|
-
*
|
|
121
|
+
* base64url (no padding) 256-bit bitmap of the empty siblings on the path from
|
|
122
|
+
* the leaf to the root. Bit `i` set = the sibling at level `i` is an empty subtree;
|
|
123
|
+
* bit `i` clear = the next entry of `hashes` is the sibling. Bit `i` is `bitAt(i)`
|
|
124
|
+
* of the decoded value, counted from the left: bit `0` is the root level, bit
|
|
125
|
+
* `255` the leaf level. The number of entries in `hashes` plus the number of set
|
|
126
|
+
* bits is `256`.
|
|
115
127
|
*/
|
|
116
128
|
collapsed: string;
|
|
117
129
|
/**
|
|
118
|
-
* Array of
|
|
130
|
+
* Array of the SHA-256 hashes of the non-empty sibling nodes on the path from the
|
|
131
|
+
* leaf to the root, in that order.
|
|
119
132
|
*/
|
|
120
133
|
hashes: string[];
|
|
121
134
|
}
|
package/src/core/resolver.ts
CHANGED
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
INVALID_DID,
|
|
11
11
|
INVALID_DID_UPDATE,
|
|
12
12
|
INVALID_OPTIONS,
|
|
13
|
+
INVALID_SIGNAL_DATA,
|
|
13
14
|
JSONPatch,
|
|
14
15
|
JSONUtils,
|
|
15
16
|
LATE_PUBLISHING_ERROR,
|
|
17
|
+
MISSING_UPDATE_DATA,
|
|
16
18
|
NOT_FOUND,
|
|
17
19
|
ResolveError
|
|
18
20
|
} from '@did-btcr2/common';
|
|
@@ -173,12 +175,15 @@ function isSignedBTCR2Update(value: unknown): value is SignedBTCR2Update {
|
|
|
173
175
|
&& isRecord(value.proof);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
/** True if `value` has the shape of an SMT
|
|
178
|
+
/** True if `value` has the shape of an SMT proof: string fields, `hashes` an array of strings. */
|
|
177
179
|
function isSMTProof(value: unknown): value is SMTProof {
|
|
178
180
|
if(!isRecord(value)) return false;
|
|
179
181
|
return typeof value.id === 'string'
|
|
180
182
|
&& typeof value.collapsed === 'string'
|
|
181
|
-
&& Array.isArray(value.hashes)
|
|
183
|
+
&& Array.isArray(value.hashes)
|
|
184
|
+
&& value.hashes.every(h => typeof h === 'string')
|
|
185
|
+
&& (value.nonce === undefined || typeof value.nonce === 'string')
|
|
186
|
+
&& (value.updateId === undefined || typeof value.updateId === 'string');
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
/**
|
|
@@ -301,7 +306,12 @@ export class Resolver {
|
|
|
301
306
|
#currentDocument: DidDocument | null;
|
|
302
307
|
#providedGenesisDocument: object | null = null;
|
|
303
308
|
#beaconServicesSignals: Map<BeaconService, Array<BeaconSignal>> = new Map();
|
|
304
|
-
|
|
309
|
+
/**
|
|
310
|
+
* The beacon addresses whose signals became update tuples. Keyed by address, not by
|
|
311
|
+
* service id: a rotation keeps the id and changes the address, and the specification
|
|
312
|
+
* has no per-service state (ADR 118).
|
|
313
|
+
*/
|
|
314
|
+
#processedAddresses: Set<string> = new Set();
|
|
305
315
|
/** The beacon addresses the resolver requested signals for: `scanned_beacons` of the specification. */
|
|
306
316
|
#requestCache: Set<string> = new Set();
|
|
307
317
|
/**
|
|
@@ -317,16 +327,25 @@ export class Resolver {
|
|
|
317
327
|
* The state of the specification loop, carried across every pass: the version counter
|
|
318
328
|
* (`current_version_id`), the update-hash history that backs duplicate confirmation
|
|
319
329
|
* (`update_hash_history`), the confirmations of the block that contains the most
|
|
320
|
-
* recently applied unique update (`block_confirmations`),
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
330
|
+
* recently applied unique update (`block_confirmations`), the height of that block
|
|
331
|
+
* (`current_block_height`), and the header time of that block as `updated`. A pass
|
|
332
|
+
* that finds a new beacon address returns to discovery, so the state must not
|
|
333
|
+
* restart: a restart would reject a linear history whose later updates are
|
|
334
|
+
* announced on beacons that earlier updates added.
|
|
324
335
|
*/
|
|
325
336
|
#currentVersionId = 1;
|
|
326
337
|
#updateHashHistory: HashBytes[] = [];
|
|
327
338
|
#blockConfirmations = 0;
|
|
328
339
|
#updated?: string;
|
|
329
340
|
|
|
341
|
+
/**
|
|
342
|
+
* The height of the block that contains the most recently applied update
|
|
343
|
+
* (`current_block_height`). "Find Beacon Signals" keeps only the signals at or above
|
|
344
|
+
* it: a beacon address that an update added has no signals for this DID before the
|
|
345
|
+
* block of that update.
|
|
346
|
+
*/
|
|
347
|
+
#currentBlockHeight = 0;
|
|
348
|
+
|
|
330
349
|
/**
|
|
331
350
|
* Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
|
|
332
351
|
* default) leaves discovery unbounded; termination is already guaranteed by
|
|
@@ -865,8 +884,9 @@ export class Resolver {
|
|
|
865
884
|
const allNeeds: Array<DataNeed> = [];
|
|
866
885
|
|
|
867
886
|
for(const [service, signals] of this.#beaconServicesSignals) {
|
|
868
|
-
// Skip
|
|
869
|
-
|
|
887
|
+
// Skip a processed address and a service with no signals
|
|
888
|
+
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string);
|
|
889
|
+
if(this.#processedAddresses.has(address) || !signals.length) continue;
|
|
870
890
|
|
|
871
891
|
// Keep only the signals at or above the confirmation threshold. A
|
|
872
892
|
// service whose signals are all below it is treated like a service
|
|
@@ -884,13 +904,12 @@ export class Resolver {
|
|
|
884
904
|
// This service has unmet data needs, collect them
|
|
885
905
|
allNeeds.push(...result.needs);
|
|
886
906
|
} else {
|
|
887
|
-
// All signals
|
|
888
|
-
// beacon address
|
|
889
|
-
const address = BeaconUtils.parseBitcoinAddress(service.serviceEndpoint as string);
|
|
907
|
+
// All signals of this address resolved: collect the updates with the
|
|
908
|
+
// beacon address, mark the address processed.
|
|
890
909
|
this.#unsortedUpdates.push(...result.updates.map(
|
|
891
910
|
([update, block]): UpdateTuple => [update, block, address]
|
|
892
911
|
));
|
|
893
|
-
this.#
|
|
912
|
+
this.#processedAddresses.add(address);
|
|
894
913
|
}
|
|
895
914
|
}
|
|
896
915
|
|
|
@@ -953,7 +972,7 @@ export class Resolver {
|
|
|
953
972
|
);
|
|
954
973
|
if(removed) continue;
|
|
955
974
|
|
|
956
|
-
// Step
|
|
975
|
+
// Step 6, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
|
|
957
976
|
// re-announces an applied version. Confirm that it is a true duplicate, then
|
|
958
977
|
// skip it. A duplicate does not advance the version counter, does not append
|
|
959
978
|
// to the history (the slot already holds the applied update, ADR 067), and
|
|
@@ -975,7 +994,7 @@ export class Resolver {
|
|
|
975
994
|
continue;
|
|
976
995
|
}
|
|
977
996
|
|
|
978
|
-
// Step
|
|
997
|
+
// Step 6, third arm: a version was skipped, so raise LATE_PUBLISHING.
|
|
979
998
|
if(update.targetVersionId !== this.#currentVersionId + 1) {
|
|
980
999
|
throw new ResolveError(
|
|
981
1000
|
`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
|
|
@@ -986,16 +1005,18 @@ export class Resolver {
|
|
|
986
1005
|
);
|
|
987
1006
|
}
|
|
988
1007
|
|
|
989
|
-
// Step
|
|
1008
|
+
// Step 6, second arm: targetVersionId == currentVersionId + 1. Apply the update,
|
|
990
1009
|
// append the unsigned update hash to the history, increment the version.
|
|
991
1010
|
this.#currentDocument = Resolver.applyUpdate(document, update, block);
|
|
992
1011
|
const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']) as UnsignedBTCR2Update;
|
|
993
1012
|
this.#updateHashHistory.push(canonicalHashBytes(unsignedUpdate));
|
|
994
1013
|
this.#currentVersionId++;
|
|
995
1014
|
|
|
996
|
-
//
|
|
997
|
-
// path only: the stop above and the duplicate branch
|
|
1015
|
+
// "Apply Update": block_confirmations, current_block_height, and the header time
|
|
1016
|
+
// as `updated`. On the apply path only: the stop above and the duplicate branch
|
|
1017
|
+
// stamp nothing.
|
|
998
1018
|
this.#blockConfirmations = block.confirmations;
|
|
1019
|
+
this.#currentBlockHeight = block.height;
|
|
999
1020
|
this.#updated = DateUtils.toISOStringNonFractional(DateUtils.blocktimeToTimestamp(block.time));
|
|
1000
1021
|
|
|
1001
1022
|
// The applied update can add a beacon service. "Find Beacon Signals" runs at
|
|
@@ -1063,8 +1084,12 @@ export class Resolver {
|
|
|
1063
1084
|
* malformed. It fails fast here with a typed error, in the style of the
|
|
1064
1085
|
* {@link provide} guards, and not later with an invalid date or a false
|
|
1065
1086
|
* `versionTime` comparison in the ProcessUpdate phase.
|
|
1087
|
+
*
|
|
1088
|
+
* "Find Beacon Signals" finds only the transactions at or above
|
|
1089
|
+
* `current_block_height`, the height of the block of the most recently applied
|
|
1090
|
+
* update. A signal below it is excluded: it emits no data need and applies no update.
|
|
1066
1091
|
* @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
|
|
1067
|
-
* @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
|
|
1092
|
+
* @returns {Array<BeaconSignal>} The signals at or above the threshold and the height, in the given order.
|
|
1068
1093
|
* @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
|
|
1069
1094
|
*/
|
|
1070
1095
|
#eligibleSignals(signals: Array<BeaconSignal>): Array<BeaconSignal> {
|
|
@@ -1089,6 +1114,10 @@ export class Resolver {
|
|
|
1089
1114
|
}
|
|
1090
1115
|
);
|
|
1091
1116
|
}
|
|
1117
|
+
// "Find Beacon Signals" finds only the transactions at or above current_block_height.
|
|
1118
|
+
// A signal before the block of the update that added the address is not a signal
|
|
1119
|
+
// of this DID.
|
|
1120
|
+
if((block!.height as number) < this.#currentBlockHeight) continue;
|
|
1092
1121
|
eligible.push(signal);
|
|
1093
1122
|
}
|
|
1094
1123
|
return eligible;
|
|
@@ -1142,12 +1171,14 @@ export class Resolver {
|
|
|
1142
1171
|
);
|
|
1143
1172
|
}
|
|
1144
1173
|
// Fail fast if the provided announcement is not the one the on-chain
|
|
1145
|
-
// signal requested: its canonical hash must equal the need's hash.
|
|
1174
|
+
// signal requested: its canonical hash must equal the need's hash. The
|
|
1175
|
+
// specification ("Process CAS Beacon") treats an announcement whose hash is
|
|
1176
|
+
// not map_update_hash as not available from CAS: MISSING_UPDATE_DATA.
|
|
1146
1177
|
const announcementHash = canonicalHash(data, { encoding: 'hex' });
|
|
1147
1178
|
if(announcementHash !== need.announcementHash) {
|
|
1148
1179
|
throw new ResolveError(
|
|
1149
1180
|
`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
|
|
1150
|
-
|
|
1181
|
+
MISSING_UPDATE_DATA, { expected: need.announcementHash, actual: announcementHash }
|
|
1151
1182
|
);
|
|
1152
1183
|
}
|
|
1153
1184
|
this.#sidecarData.casMap.set(announcementHash, data);
|
|
@@ -1162,12 +1193,13 @@ export class Resolver {
|
|
|
1162
1193
|
);
|
|
1163
1194
|
}
|
|
1164
1195
|
// Fail fast if the provided update is not the one the on-chain signal
|
|
1165
|
-
// requested:
|
|
1196
|
+
// requested: the specification compares the JSON Document Hash of a
|
|
1197
|
+
// retrieved update to update_hash, and a mismatch is INVALID_SIGNAL_DATA.
|
|
1166
1198
|
const updateHash = canonicalHash(data, { encoding: 'hex' });
|
|
1167
1199
|
if(updateHash !== need.updateHash) {
|
|
1168
1200
|
throw new ResolveError(
|
|
1169
1201
|
`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
|
|
1170
|
-
|
|
1202
|
+
INVALID_SIGNAL_DATA, { expected: need.updateHash, actual: updateHash }
|
|
1171
1203
|
);
|
|
1172
1204
|
}
|
|
1173
1205
|
this.#sidecarData.updateMap.set(updateHash, data);
|
|
@@ -1175,18 +1207,27 @@ export class Resolver {
|
|
|
1175
1207
|
}
|
|
1176
1208
|
|
|
1177
1209
|
case 'NeedSMTProof': {
|
|
1210
|
+
// A proof of another shape is data for the signal that does not agree with
|
|
1211
|
+
// its Signal Bytes: INVALID_SIGNAL_DATA, as for the id and the walk below.
|
|
1178
1212
|
if(!isSMTProof(data)) {
|
|
1179
1213
|
throw new ResolveError(
|
|
1180
1214
|
'Provided data for NeedSMTProof is not an SMT proof.',
|
|
1181
|
-
|
|
1215
|
+
INVALID_SIGNAL_DATA, { kind: need.kind }
|
|
1182
1216
|
);
|
|
1183
1217
|
}
|
|
1184
|
-
// proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
|
|
1185
|
-
|
|
1218
|
+
// proof.id is base64url per spec; smtRootHash is the hex on-chain signal. The
|
|
1219
|
+
// specification ("Process SMT Beacon") compares the id of smt_proof to
|
|
1220
|
+
// smt_root: a mismatch, or an id that does not decode, is INVALID_SIGNAL_DATA.
|
|
1221
|
+
let proofIdHex: string | undefined;
|
|
1222
|
+
try {
|
|
1223
|
+
proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
|
|
1224
|
+
} catch {
|
|
1225
|
+
proofIdHex = undefined;
|
|
1226
|
+
}
|
|
1186
1227
|
if(proofIdHex !== need.smtRootHash) {
|
|
1187
1228
|
throw new ResolveError(
|
|
1188
|
-
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}
|
|
1189
|
-
|
|
1229
|
+
`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex ?? 'an id that does not decode'}.`,
|
|
1230
|
+
INVALID_SIGNAL_DATA, { expected: need.smtRootHash, actual: proofIdHex }
|
|
1190
1231
|
);
|
|
1191
1232
|
}
|
|
1192
1233
|
this.#sidecarData.smtMap.set(need.smtRootHash, data);
|
package/src/core/types.ts
CHANGED
|
@@ -49,8 +49,10 @@ export type Sidecar = {
|
|
|
49
49
|
casUpdates?: Array<CASAnnouncement>;
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Optional array of SMT Proofs
|
|
53
|
-
*
|
|
52
|
+
* Optional array of SMT Proofs: one proof for each SMT beacon signal that the
|
|
53
|
+
* resolver finds for the DID, with an update announced or not. The DID
|
|
54
|
+
* controller keeps every proof for the life of the DID. Sidecar is the only
|
|
55
|
+
* channel for a proof.
|
|
54
56
|
*/
|
|
55
57
|
smtProofs?: Array<SMTProof>;
|
|
56
58
|
};
|