@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.
@@ -1,16 +1,26 @@
1
1
  import type { BitcoinConnection } from '@did-btcr2/bitcoin';
2
- import { canonicalize } from '@did-btcr2/common';
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, blockHash, BTCR2MerkleTree, didToIndex, hashToHex, verifySerializedProof } from '@did-btcr2/smt';
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 hash,
41
- * validates the Merkle inclusion proof, and retrieves the corresponding
42
- * signed update using the proof's updateId.
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 proof verification fails or proof is malformed.
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
- // Signal bytes are the hex-encoded SMT root hash; smtMap is keyed by proof.id (also hex)
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
- // Nonce is required for proof verification (inclusion and non-inclusion).
74
- if(!smtProof.nonce) {
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
- 'SMT proof missing required nonce field.',
77
- 'INVALID_SMT_PROOF', { smtProof, did }
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 SMT proof against the on-chain root. Leaf value per spec:
82
- // inclusion = hash(hash(nonce) || updateId); non-inclusion = hash(hash(nonce)).
83
- // Hash fields are base64url (no padding) per the SMT Proof spec. A
84
- // non-inclusion proof (absent updateId) is verified too, not trusted.
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
- 'SMT proof verification failed.',
95
- 'INVALID_SMT_PROOF', { smtProof, did }
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
- // Non-inclusion proof verified: no update for this DID this epoch, skip.
100
- if(!smtProof.updateId) {
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 base64url, so convert to hex to match.
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
- * inclusion proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
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 from the signed update
152
- const canonicalBytes = new TextEncoder().encode(canonicalize(signedUpdate));
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, signedUpdate: canonicalBytes }]);
161
+ tree.addEntries([{ did, nonce, updateId: canonicalHashBytes(signedUpdate) }]);
156
162
  tree.finalize();
157
163
 
158
- // Serialize the inclusion proof (carrying the nonce and updateId) before
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);
@@ -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
- const networkValue = BitcoinNetworkNames[network as keyof typeof BitcoinNetworkNames] as number | undefined;
154
- if (networkValue === undefined) {
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
 
@@ -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, proving that the leaf is in the tree.
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 SHA-256 hash fields (`id`, `nonce`, `updateId`, `hashes`) are "base64url"
83
- * [RFC4648] encoded without padding (43 chars each). `collapsed` is the 256-bit
84
- * zero-node bitmap, also base64url no-pad (43 chars).
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": "q1H_iaYG0Oq6gbrycYL-r7FjUsJLnIpHDn49TLeONNA",
90
- * "nonce": "99jndCBWHpZfmObXlIvRGHaPMgoQKXIETdD4H-XqryE",
91
- * "updateId": "njYNViJq2OmhSw1fLfARPCj12RY3VXKGWdS3-7OQ2BE",
92
- * "collapsed": "v_________________________________________8",
93
+ * "id": "ZSN-lAyRpXG72aK1xLC9sAuRhFGsILupaQXxpkITJuo",
94
+ * "nonce": "WYVxNuwz3RBEhnJKM4LvVh2tOdXI9WRUPYqA_qa0klM",
95
+ * "updateId": "_YDKmjcnIkHDY6rnRwrO86id5H1Onycy7Bz62jYq6GA",
96
+ * "collapsed": "-_________________________________________8",
93
97
  * "hashes": [
94
- * "8JWXL7chPKJXwg-i9O1EFTHan_oOO_RmglDpu_ugax0"
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 256-bit nonce generated for each update. base64url, no padding (43 chars).
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) canonical hash of the BTCR2 Signed Update.
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 zero nodes within the path (see: collapsed
114
- * leaves). Bit set = empty/zero sibling; bit clear = a sibling hash is present.
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 SHA-256 hashes representing the sibling SMT nodes from the leaf, containing the SHA-256 hash of the BTCR2 Signed Update or the “zero identity”, to the root.
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
  }
@@ -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 inclusion / non-inclusion proof. */
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
- #processedServices: Set<string> = new Set();
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`), and the header time of that
321
- * block as `updated`. A pass that finds a new beacon address returns to discovery, so
322
- * the state must not restart: a restart would reject a linear history whose later
323
- * updates are announced on beacons that earlier updates added.
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 already-processed services and services with no signals
869
- if(this.#processedServices.has(service.id) || !signals.length) continue;
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 for this service resolved: collect the updates with the
888
- // beacon address of the service, mark the service processed.
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.#processedServices.add(service.id);
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 7, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
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 7, third arm: a version was skipped, so raise LATE_PUBLISHING.
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 7, second arm: targetVersionId == currentVersionId + 1. Apply the update,
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
- // Step 6: block_confirmations, and the header time as `updated`. On the apply
997
- // path only: the stop above and the duplicate branch stamp nothing.
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
- INVALID_DID_UPDATE, { expected: need.announcementHash, actual: announcementHash }
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: its canonical hash must equal the need's hash.
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
- INVALID_DID_UPDATE, { expected: need.updateHash, actual: updateHash }
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
- INVALID_DID_UPDATE, { kind: need.kind }
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
- const proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
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
- INVALID_DID_UPDATE, { expected: need.smtRootHash, actual: proofIdHex }
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. Required if the DID being resolved has used
53
- * an SMT Beacon to publish a BTCR2 Update.
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
  };