@did-btcr2/method 0.65.1 → 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.
@@ -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
  /**
@@ -322,16 +327,25 @@ export class Resolver {
322
327
  * The state of the specification loop, carried across every pass: the version counter
323
328
  * (`current_version_id`), the update-hash history that backs duplicate confirmation
324
329
  * (`update_hash_history`), the confirmations of the block that contains the most
325
- * recently applied unique update (`block_confirmations`), and the header time of that
326
- * block as `updated`. A pass that finds a new beacon address returns to discovery, so
327
- * the state must not restart: a restart would reject a linear history whose later
328
- * 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.
329
335
  */
330
336
  #currentVersionId = 1;
331
337
  #updateHashHistory: HashBytes[] = [];
332
338
  #blockConfirmations = 0;
333
339
  #updated?: string;
334
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
+
335
349
  /**
336
350
  * Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
337
351
  * default) leaves discovery unbounded; termination is already guaranteed by
@@ -958,7 +972,7 @@ export class Resolver {
958
972
  );
959
973
  if(removed) continue;
960
974
 
961
- // Step 7, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
975
+ // Step 6, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
962
976
  // re-announces an applied version. Confirm that it is a true duplicate, then
963
977
  // skip it. A duplicate does not advance the version counter, does not append
964
978
  // to the history (the slot already holds the applied update, ADR 067), and
@@ -980,7 +994,7 @@ export class Resolver {
980
994
  continue;
981
995
  }
982
996
 
983
- // 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.
984
998
  if(update.targetVersionId !== this.#currentVersionId + 1) {
985
999
  throw new ResolveError(
986
1000
  `Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`,
@@ -991,16 +1005,18 @@ export class Resolver {
991
1005
  );
992
1006
  }
993
1007
 
994
- // Step 7, second arm: targetVersionId == currentVersionId + 1. Apply the update,
1008
+ // Step 6, second arm: targetVersionId == currentVersionId + 1. Apply the update,
995
1009
  // append the unsigned update hash to the history, increment the version.
996
1010
  this.#currentDocument = Resolver.applyUpdate(document, update, block);
997
1011
  const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']) as UnsignedBTCR2Update;
998
1012
  this.#updateHashHistory.push(canonicalHashBytes(unsignedUpdate));
999
1013
  this.#currentVersionId++;
1000
1014
 
1001
- // Step 6: block_confirmations, and the header time as `updated`. On the apply
1002
- // 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.
1003
1018
  this.#blockConfirmations = block.confirmations;
1019
+ this.#currentBlockHeight = block.height;
1004
1020
  this.#updated = DateUtils.toISOStringNonFractional(DateUtils.blocktimeToTimestamp(block.time));
1005
1021
 
1006
1022
  // The applied update can add a beacon service. "Find Beacon Signals" runs at
@@ -1068,8 +1084,12 @@ export class Resolver {
1068
1084
  * malformed. It fails fast here with a typed error, in the style of the
1069
1085
  * {@link provide} guards, and not later with an invalid date or a false
1070
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.
1071
1091
  * @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
1072
- * @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.
1073
1093
  * @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
1074
1094
  */
1075
1095
  #eligibleSignals(signals: Array<BeaconSignal>): Array<BeaconSignal> {
@@ -1094,6 +1114,10 @@ export class Resolver {
1094
1114
  }
1095
1115
  );
1096
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;
1097
1121
  eligible.push(signal);
1098
1122
  }
1099
1123
  return eligible;
@@ -1147,12 +1171,14 @@ export class Resolver {
1147
1171
  );
1148
1172
  }
1149
1173
  // Fail fast if the provided announcement is not the one the on-chain
1150
- // 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.
1151
1177
  const announcementHash = canonicalHash(data, { encoding: 'hex' });
1152
1178
  if(announcementHash !== need.announcementHash) {
1153
1179
  throw new ResolveError(
1154
1180
  `CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
1155
- INVALID_DID_UPDATE, { expected: need.announcementHash, actual: announcementHash }
1181
+ MISSING_UPDATE_DATA, { expected: need.announcementHash, actual: announcementHash }
1156
1182
  );
1157
1183
  }
1158
1184
  this.#sidecarData.casMap.set(announcementHash, data);
@@ -1167,12 +1193,13 @@ export class Resolver {
1167
1193
  );
1168
1194
  }
1169
1195
  // Fail fast if the provided update is not the one the on-chain signal
1170
- // 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.
1171
1198
  const updateHash = canonicalHash(data, { encoding: 'hex' });
1172
1199
  if(updateHash !== need.updateHash) {
1173
1200
  throw new ResolveError(
1174
1201
  `Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
1175
- INVALID_DID_UPDATE, { expected: need.updateHash, actual: updateHash }
1202
+ INVALID_SIGNAL_DATA, { expected: need.updateHash, actual: updateHash }
1176
1203
  );
1177
1204
  }
1178
1205
  this.#sidecarData.updateMap.set(updateHash, data);
@@ -1180,18 +1207,27 @@ export class Resolver {
1180
1207
  }
1181
1208
 
1182
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.
1183
1212
  if(!isSMTProof(data)) {
1184
1213
  throw new ResolveError(
1185
1214
  'Provided data for NeedSMTProof is not an SMT proof.',
1186
- INVALID_DID_UPDATE, { kind: need.kind }
1215
+ INVALID_SIGNAL_DATA, { kind: need.kind }
1187
1216
  );
1188
1217
  }
1189
- // proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
1190
- 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
+ }
1191
1227
  if(proofIdHex !== need.smtRootHash) {
1192
1228
  throw new ResolveError(
1193
- `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
1194
- 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 }
1195
1231
  );
1196
1232
  }
1197
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
  };