@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.
package/dist/cjs/index.js CHANGED
@@ -589,6 +589,13 @@ var SingletonBeacon = class extends SinglePartyBeacon {
589
589
  var import_common4 = require("@did-btcr2/common");
590
590
  var import_smt = require("@did-btcr2/smt");
591
591
  var import_utils2 = require("@noble/hashes/utils");
592
+ function proofIdHex(proof) {
593
+ try {
594
+ return (0, import_smt.hashToHex)((0, import_smt.base64UrlToHash)(proof.id));
595
+ } catch {
596
+ return void 0;
597
+ }
598
+ }
592
599
  var SMTBeacon = class extends SinglePartyBeacon {
593
600
  /**
594
601
  * Creates an instance of SMTBeacon.
@@ -601,15 +608,17 @@ var SMTBeacon = class extends SinglePartyBeacon {
601
608
  /**
602
609
  * Implements {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-smt-beacon | 7.2.e.1 Process SMT Beacon}.
603
610
  *
604
- * For each signal, the signalBytes contain the hex-encoded SMT root hash.
605
- * This method looks up the SMT Proof from the sidecar by root hash,
606
- * validates the Merkle inclusion proof, and retrieves the corresponding
607
- * signed update using the proof's updateId.
611
+ * For each signal, the signalBytes contain the hex-encoded SMT root hash
612
+ * (`smt_root`). This method looks up the SMT Proof from the sidecar by root
613
+ * hash, checks that the id of the proof is the root, verifies the proof with
614
+ * the SMT Proof Verification algorithm, and retrieves the signed update by the
615
+ * proof's updateId. A proof with no updateId announces no update for the DID.
608
616
  *
609
617
  * @param {Array<BeaconSignal>} signals The array of Beacon Signals to process.
610
618
  * @param {SidecarData} sidecar The sidecar data associated with the SMT Beacon.
611
619
  * @returns {BeaconProcessResult} Successfully resolved updates and any data needs.
612
- * @throws {SMTBeaconError} if proof verification fails or proof is malformed.
620
+ * @throws {SMTBeaconError} `INVALID_SIGNAL_DATA` if the id of the proof is not the
621
+ * signal root, or if the proof does not verify.
613
622
  */
614
623
  processSignals(signals, sidecar) {
615
624
  const updates = new Array();
@@ -625,25 +634,21 @@ var SMTBeacon = class extends SinglePartyBeacon {
625
634
  });
626
635
  continue;
627
636
  }
628
- if (!smtProof.nonce) {
637
+ if (proofIdHex(smtProof) !== signal.signalBytes) {
629
638
  throw new SMTBeaconError(
630
- "SMT proof missing required nonce field.",
631
- "INVALID_SMT_PROOF",
632
- { smtProof, did }
639
+ `SMT proof id does not equal the signal root ${signal.signalBytes}.`,
640
+ import_common4.INVALID_SIGNAL_DATA,
641
+ { smtProof, did, smtRootHash: signal.signalBytes }
633
642
  );
634
643
  }
635
- const index = (0, import_smt.didToIndex)(did);
636
- const nonceHash = (0, import_smt.base64UrlToHash)(smtProof.nonce);
637
- const candidateHash = smtProof.updateId ? (0, import_smt.blockHash)((0, import_smt.blockHash)(nonceHash), (0, import_smt.base64UrlToHash)(smtProof.updateId)) : (0, import_smt.blockHash)((0, import_smt.blockHash)(nonceHash));
638
- const valid = (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash);
639
- if (!valid) {
644
+ if (!(0, import_smt.verifyProof)(smtProof, did)) {
640
645
  throw new SMTBeaconError(
641
- "SMT proof verification failed.",
642
- "INVALID_SMT_PROOF",
643
- { smtProof, did }
646
+ `SMT proof verification failed for the signal root ${signal.signalBytes}.`,
647
+ import_common4.INVALID_SIGNAL_DATA,
648
+ { smtProof, did, smtRootHash: signal.signalBytes }
644
649
  );
645
650
  }
646
- if (!smtProof.updateId) {
651
+ if (smtProof.updateId === void 0) {
647
652
  continue;
648
653
  }
649
654
  const updateHashHex = (0, import_smt.hashToHex)((0, import_smt.base64UrlToHash)(smtProof.updateId));
@@ -673,16 +678,15 @@ var SMTBeacon = class extends SinglePartyBeacon {
673
678
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
674
679
  * @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
675
680
  * @return {Promise<BroadcastResult>} The signed update, the signal txid, and the SMT
676
- * inclusion proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
681
+ * proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
677
682
  * distribution: the nonce exists only here, so the on-chain signal is unresolvable without it.
678
683
  * @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
679
684
  */
680
685
  async broadcastSignal(signedUpdate, signer, bitcoin, options) {
681
686
  const did = this.did;
682
- const canonicalBytes = new TextEncoder().encode((0, import_common4.canonicalize)(signedUpdate));
683
687
  const nonce = (0, import_utils2.randomBytes)(32);
684
688
  const tree = new import_smt.BTCR2MerkleTree();
685
- tree.addEntries([{ did, nonce, signedUpdate: canonicalBytes }]);
689
+ tree.addEntries([{ did, nonce, updateId: (0, import_common4.canonicalHashBytes)(signedUpdate) }]);
686
690
  tree.finalize();
687
691
  const proof = tree.proof(did);
688
692
  const txid = await this.buildSignAndBroadcast(tree.rootHash, signer, bitcoin, options);
@@ -2058,7 +2062,7 @@ function isSignedBTCR2Update(value) {
2058
2062
  }
2059
2063
  function isSMTProof(value) {
2060
2064
  if (!isRecord(value)) return false;
2061
- return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes);
2065
+ return typeof value.id === "string" && typeof value.collapsed === "string" && Array.isArray(value.hashes) && value.hashes.every((h) => typeof h === "string") && (value.nonce === void 0 || typeof value.nonce === "string") && (value.updateId === void 0 || typeof value.updateId === "string");
2062
2066
  }
2063
2067
  function validateMinConf(value) {
2064
2068
  if (value === void 0) return DEFAULT_MIN_CONF;
@@ -2133,15 +2137,23 @@ var Resolver = class _Resolver {
2133
2137
  * The state of the specification loop, carried across every pass: the version counter
2134
2138
  * (`current_version_id`), the update-hash history that backs duplicate confirmation
2135
2139
  * (`update_hash_history`), the confirmations of the block that contains the most
2136
- * recently applied unique update (`block_confirmations`), and the header time of that
2137
- * block as `updated`. A pass that finds a new beacon address returns to discovery, so
2138
- * the state must not restart: a restart would reject a linear history whose later
2139
- * updates are announced on beacons that earlier updates added.
2140
+ * recently applied unique update (`block_confirmations`), the height of that block
2141
+ * (`current_block_height`), and the header time of that block as `updated`. A pass
2142
+ * that finds a new beacon address returns to discovery, so the state must not
2143
+ * restart: a restart would reject a linear history whose later updates are
2144
+ * announced on beacons that earlier updates added.
2140
2145
  */
2141
2146
  #currentVersionId = 1;
2142
2147
  #updateHashHistory = [];
2143
2148
  #blockConfirmations = 0;
2144
2149
  #updated;
2150
+ /**
2151
+ * The height of the block that contains the most recently applied update
2152
+ * (`current_block_height`). "Find Beacon Signals" keeps only the signals at or above
2153
+ * it: a beacon address that an update added has no signals for this DID before the
2154
+ * block of that update.
2155
+ */
2156
+ #currentBlockHeight = 0;
2145
2157
  /**
2146
2158
  * Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
2147
2159
  * default) leaves discovery unbounded; termination is already guaranteed by
@@ -2637,6 +2649,7 @@ var Resolver = class _Resolver {
2637
2649
  this.#updateHashHistory.push((0, import_common12.canonicalHashBytes)(unsignedUpdate));
2638
2650
  this.#currentVersionId++;
2639
2651
  this.#blockConfirmations = block.confirmations;
2652
+ this.#currentBlockHeight = block.height;
2640
2653
  this.#updated = import_common12.DateUtils.toISOStringNonFractional(import_common12.DateUtils.blocktimeToTimestamp(block.time));
2641
2654
  if (this.#hasUnscannedBeacons()) {
2642
2655
  if (++this.#discoveryRounds > this.#maxDiscoveryRounds) {
@@ -2692,8 +2705,12 @@ var Resolver = class _Resolver {
2692
2705
  * malformed. It fails fast here with a typed error, in the style of the
2693
2706
  * {@link provide} guards, and not later with an invalid date or a false
2694
2707
  * `versionTime` comparison in the ProcessUpdate phase.
2708
+ *
2709
+ * "Find Beacon Signals" finds only the transactions at or above
2710
+ * `current_block_height`, the height of the block of the most recently applied
2711
+ * update. A signal below it is excluded: it emits no data need and applies no update.
2695
2712
  * @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
2696
- * @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
2713
+ * @returns {Array<BeaconSignal>} The signals at or above the threshold and the height, in the given order.
2697
2714
  * @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
2698
2715
  */
2699
2716
  #eligibleSignals(signals) {
@@ -2717,6 +2734,7 @@ var Resolver = class _Resolver {
2717
2734
  }
2718
2735
  );
2719
2736
  }
2737
+ if (block.height < this.#currentBlockHeight) continue;
2720
2738
  eligible.push(signal);
2721
2739
  }
2722
2740
  return eligible;
@@ -2759,7 +2777,7 @@ var Resolver = class _Resolver {
2759
2777
  if (announcementHash !== need.announcementHash) {
2760
2778
  throw new import_common12.ResolveError(
2761
2779
  `CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`,
2762
- import_common12.INVALID_DID_UPDATE,
2780
+ import_common12.MISSING_UPDATE_DATA,
2763
2781
  { expected: need.announcementHash, actual: announcementHash }
2764
2782
  );
2765
2783
  }
@@ -2778,7 +2796,7 @@ var Resolver = class _Resolver {
2778
2796
  if (updateHash !== need.updateHash) {
2779
2797
  throw new import_common12.ResolveError(
2780
2798
  `Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`,
2781
- import_common12.INVALID_DID_UPDATE,
2799
+ import_common12.INVALID_SIGNAL_DATA,
2782
2800
  { expected: need.updateHash, actual: updateHash }
2783
2801
  );
2784
2802
  }
@@ -2789,16 +2807,21 @@ var Resolver = class _Resolver {
2789
2807
  if (!isSMTProof(data)) {
2790
2808
  throw new import_common12.ResolveError(
2791
2809
  "Provided data for NeedSMTProof is not an SMT proof.",
2792
- import_common12.INVALID_DID_UPDATE,
2810
+ import_common12.INVALID_SIGNAL_DATA,
2793
2811
  { kind: need.kind }
2794
2812
  );
2795
2813
  }
2796
- const proofIdHex = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
2797
- if (proofIdHex !== need.smtRootHash) {
2814
+ let proofIdHex2;
2815
+ try {
2816
+ proofIdHex2 = (0, import_common12.encode)((0, import_common12.decode)(data.id, "base64urlnopad"), "hex");
2817
+ } catch {
2818
+ proofIdHex2 = void 0;
2819
+ }
2820
+ if (proofIdHex2 !== need.smtRootHash) {
2798
2821
  throw new import_common12.ResolveError(
2799
- `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`,
2800
- import_common12.INVALID_DID_UPDATE,
2801
- { expected: need.smtRootHash, actual: proofIdHex }
2822
+ `SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex2 ?? "an id that does not decode"}.`,
2823
+ import_common12.INVALID_SIGNAL_DATA,
2824
+ { expected: need.smtRootHash, actual: proofIdHex2 }
2802
2825
  );
2803
2826
  }
2804
2827
  this.#sidecarData.smtMap.set(need.smtRootHash, data);
@@ -1,8 +1,17 @@
1
- import { canonicalize } from '@did-btcr2/common';
2
- import { base64UrlToHash, blockHash, BTCR2MerkleTree, didToIndex, hashToHex, verifySerializedProof } from '@did-btcr2/smt';
1
+ import { canonicalHashBytes, INVALID_SIGNAL_DATA } from '@did-btcr2/common';
2
+ import { base64UrlToHash, BTCR2MerkleTree, hashToHex, verifyProof } from '@did-btcr2/smt';
3
3
  import { randomBytes } from '@noble/hashes/utils';
4
4
  import { SinglePartyBeacon } from './beacon.js';
5
5
  import { SMTBeaconError } from './error.js';
6
+ /** The hex of the base64url `id` of a proof, or `undefined` if the id does not decode to 32 bytes. */
7
+ function proofIdHex(proof) {
8
+ try {
9
+ return hashToHex(base64UrlToHash(proof.id));
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
6
15
  /**
7
16
  * Implements {@link https://dcdpr.github.io/did-btcr2/terminology.html#smt-beacon | SMT Beacon}.
8
17
  *
@@ -27,15 +36,17 @@ export class SMTBeacon extends SinglePartyBeacon {
27
36
  /**
28
37
  * Implements {@link https://dcdpr.github.io/did-btcr2/operations/resolve.html#process-smt-beacon | 7.2.e.1 Process SMT Beacon}.
29
38
  *
30
- * For each signal, the signalBytes contain the hex-encoded SMT root hash.
31
- * This method looks up the SMT Proof from the sidecar by root hash,
32
- * validates the Merkle inclusion proof, and retrieves the corresponding
33
- * signed update using the proof's updateId.
39
+ * For each signal, the signalBytes contain the hex-encoded SMT root hash
40
+ * (`smt_root`). This method looks up the SMT Proof from the sidecar by root
41
+ * hash, checks that the id of the proof is the root, verifies the proof with
42
+ * the SMT Proof Verification algorithm, and retrieves the signed update by the
43
+ * proof's updateId. A proof with no updateId announces no update for the DID.
34
44
  *
35
45
  * @param {Array<BeaconSignal>} signals The array of Beacon Signals to process.
36
46
  * @param {SidecarData} sidecar The sidecar data associated with the SMT Beacon.
37
47
  * @returns {BeaconProcessResult} Successfully resolved updates and any data needs.
38
- * @throws {SMTBeaconError} if proof verification fails or proof is malformed.
48
+ * @throws {SMTBeaconError} `INVALID_SIGNAL_DATA` if the id of the proof is not the
49
+ * signal root, or if the proof does not verify.
39
50
  */
40
51
  processSignals(signals, sidecar) {
41
52
  const updates = new Array();
@@ -43,7 +54,8 @@ export class SMTBeacon extends SinglePartyBeacon {
43
54
  // The DID under resolution keys this beacon's leaf index.
44
55
  const did = this.did;
45
56
  for (const signal of signals) {
46
- // Signal bytes are the hex-encoded SMT root hash; smtMap is keyed by proof.id (also hex)
57
+ // "Process SMT Beacon": the signal bytes are smt_root, the hex SMT root hash.
58
+ // The smtMap is keyed by the hex of proof.id. No entry = a need for the proof.
47
59
  const smtProof = sidecar.smtMap.get(signal.signalBytes);
48
60
  if (!smtProof) {
49
61
  // SMT Proof not available, emit a need
@@ -54,29 +66,23 @@ export class SMTBeacon extends SinglePartyBeacon {
54
66
  });
55
67
  continue;
56
68
  }
57
- // Nonce is required for proof verification (inclusion and non-inclusion).
58
- if (!smtProof.nonce) {
59
- throw new SMTBeaconError('SMT proof missing required nonce field.', 'INVALID_SMT_PROOF', { smtProof, did });
69
+ // The id of the proof must equal smt_root. The resolver keys the map by the
70
+ // hex of the id, so its entries pass. A caller-built map is checked here too.
71
+ if (proofIdHex(smtProof) !== signal.signalBytes) {
72
+ throw new SMTBeaconError(`SMT proof id does not equal the signal root ${signal.signalBytes}.`, INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes });
60
73
  }
61
- // Verify the SMT proof against the on-chain root. Leaf value per spec:
62
- // inclusion = hash(hash(nonce) || updateId); non-inclusion = hash(hash(nonce)).
63
- // Hash fields are base64url (no padding) per the SMT Proof spec. A
64
- // non-inclusion proof (absent updateId) is verified too, not trusted.
65
- const index = didToIndex(did);
66
- const nonceHash = base64UrlToHash(smtProof.nonce);
67
- const candidateHash = smtProof.updateId
68
- ? blockHash(blockHash(nonceHash), base64UrlToHash(smtProof.updateId))
69
- : blockHash(blockHash(nonceHash));
70
- const valid = verifySerializedProof(smtProof, index, candidateHash);
71
- if (!valid) {
72
- throw new SMTBeaconError('SMT proof verification failed.', 'INVALID_SMT_PROOF', { smtProof, did });
74
+ // Verify the proof with the SMT Proof Verification algorithm. The nonce and
75
+ // updateId fields of the proof select the leaf value (four arms). A proof that
76
+ // does not decode, or that does not walk to the root, is INVALID_SIGNAL_DATA.
77
+ if (!verifyProof(smtProof, did)) {
78
+ throw new SMTBeaconError(`SMT proof verification failed for the signal root ${signal.signalBytes}.`, INVALID_SIGNAL_DATA, { smtProof, did, smtRootHash: signal.signalBytes });
73
79
  }
74
- // Non-inclusion proof verified: no update for this DID this epoch, skip.
75
- if (!smtProof.updateId) {
80
+ // No updateId: the signal announces no update for this DID. No tuple.
81
+ if (smtProof.updateId === undefined) {
76
82
  continue;
77
83
  }
78
84
  // Look up the signed update in sidecar updateMap (keyed by hex canonical
79
- // hash). The proof's updateId is base64url, so convert to hex to match.
85
+ // hash). The proof's updateId is the same hash in base64url.
80
86
  const updateHashHex = hashToHex(base64UrlToHash(smtProof.updateId));
81
87
  const signedUpdate = sidecar.updateMap.get(updateHashHex);
82
88
  if (!signedUpdate) {
@@ -105,20 +111,20 @@ export class SMTBeacon extends SinglePartyBeacon {
105
111
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
106
112
  * @param {BroadcastOptions} [options] Optional broadcast configuration (e.g. fee estimator).
107
113
  * @return {Promise<BroadcastResult>} The signed update, the signal txid, and the SMT
108
- * inclusion proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
114
+ * proof (with the leaf nonce embedded). The proof MUST be captured for sidecar
109
115
  * distribution: the nonce exists only here, so the on-chain signal is unresolvable without it.
110
116
  * @throws {BeaconError} if the bitcoin address is invalid, unfunded, or UTXO cannot cover the fee.
111
117
  */
112
118
  async broadcastSignal(signedUpdate, signer, bitcoin, options) {
113
119
  // The DID keys this beacon's leaf index in the tree.
114
120
  const did = this.did;
115
- // Build a single-entry SMT from the signed update
116
- const canonicalBytes = new TextEncoder().encode(canonicalize(signedUpdate));
121
+ // Build a single-entry SMT in nonce mode: the leaf value is
122
+ // hash(hash(nonce) + updateId), with updateId the JSON Document Hash of the update.
117
123
  const nonce = randomBytes(32);
118
124
  const tree = new BTCR2MerkleTree();
119
- tree.addEntries([{ did, nonce, signedUpdate: canonicalBytes }]);
125
+ tree.addEntries([{ did, nonce, updateId: canonicalHashBytes(signedUpdate) }]);
120
126
  tree.finalize();
121
- // Serialize the inclusion proof (carrying the nonce and updateId) before
127
+ // Serialize the proof (carrying the nonce and updateId) before
122
128
  // broadcasting: it is the only artifact that can link the on-chain root back
123
129
  // to the update, and the nonce it embeds is irrecoverable once dropped.
124
130
  const proof = tree.proof(did);
@@ -1 +1 @@
1
- {"version":3,"file":"smt-beacon.js","sourceRoot":"","sources":["../../../../src/core/beacon/smt-beacon.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"smt-beacon.js","sourceRoot":"","sources":["../../../../src/core/beacon/smt-beacon.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { getNetwork } from '@did-btcr2/bitcoin';
2
- import { canonicalHash, canonicalHashBytes, canonicalize, DateUtils, encode as encodeHash, decode as decodeHash, INTERNAL_ERROR, INVALID_DID, INVALID_DID_UPDATE, INVALID_OPTIONS, JSONPatch, JSONUtils, LATE_PUBLISHING_ERROR, NOT_FOUND, ResolveError } from '@did-btcr2/common';
2
+ import { canonicalHash, canonicalHashBytes, canonicalize, DateUtils, encode as encodeHash, decode as decodeHash, INTERNAL_ERROR, INVALID_DID, INVALID_DID_UPDATE, INVALID_OPTIONS, INVALID_SIGNAL_DATA, JSONPatch, JSONUtils, LATE_PUBLISHING_ERROR, MISSING_UPDATE_DATA, NOT_FOUND, ResolveError } from '@did-btcr2/common';
3
3
  import { BTCR2_UPDATE_CONTEXT, isBtcr2UpdateContext } from './btcr2-update.js';
4
4
  import { BIP340Cryptosuite, BIP340DataIntegrityProof, SchnorrMultikey } from '@did-btcr2/cryptosuite';
5
5
  import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair';
@@ -44,13 +44,16 @@ function isSignedBTCR2Update(value) {
44
44
  && value.targetVersionId >= 2
45
45
  && isRecord(value.proof);
46
46
  }
47
- /** True if `value` has the shape of an SMT inclusion / non-inclusion proof. */
47
+ /** True if `value` has the shape of an SMT proof: string fields, `hashes` an array of strings. */
48
48
  function isSMTProof(value) {
49
49
  if (!isRecord(value))
50
50
  return false;
51
51
  return typeof value.id === 'string'
52
52
  && typeof value.collapsed === 'string'
53
- && Array.isArray(value.hashes);
53
+ && Array.isArray(value.hashes)
54
+ && value.hashes.every(h => typeof h === 'string')
55
+ && (value.nonce === undefined || typeof value.nonce === 'string')
56
+ && (value.updateId === undefined || typeof value.updateId === 'string');
54
57
  }
55
58
  /**
56
59
  * Validate `ResolutionOptions.minConf`. `undefined` selects the specification
@@ -180,15 +183,23 @@ export class Resolver {
180
183
  * The state of the specification loop, carried across every pass: the version counter
181
184
  * (`current_version_id`), the update-hash history that backs duplicate confirmation
182
185
  * (`update_hash_history`), the confirmations of the block that contains the most
183
- * recently applied unique update (`block_confirmations`), and the header time of that
184
- * block as `updated`. A pass that finds a new beacon address returns to discovery, so
185
- * the state must not restart: a restart would reject a linear history whose later
186
- * updates are announced on beacons that earlier updates added.
186
+ * recently applied unique update (`block_confirmations`), the height of that block
187
+ * (`current_block_height`), and the header time of that block as `updated`. A pass
188
+ * that finds a new beacon address returns to discovery, so the state must not
189
+ * restart: a restart would reject a linear history whose later updates are
190
+ * announced on beacons that earlier updates added.
187
191
  */
188
192
  #currentVersionId = 1;
189
193
  #updateHashHistory = [];
190
194
  #blockConfirmations = 0;
191
195
  #updated;
196
+ /**
197
+ * The height of the block that contains the most recently applied update
198
+ * (`current_block_height`). "Find Beacon Signals" keeps only the signals at or above
199
+ * it: a beacon address that an update added has no signals for this DID before the
200
+ * block of that update.
201
+ */
202
+ #currentBlockHeight = 0;
192
203
  /**
193
204
  * Opt-in upper bound on multi-round beacon-discovery passes. `Infinity` (the
194
205
  * default) leaves discovery unbounded; termination is already guaranteed by
@@ -670,7 +681,7 @@ export class Resolver {
670
681
  const removed = !BeaconUtils.getBeaconServices(document).some(service => BeaconUtils.parseBitcoinAddress(service.serviceEndpoint) === address);
671
682
  if (removed)
672
683
  continue;
673
- // Step 7, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
684
+ // Step 6, "Check targetVersionId", first arm: targetVersionId <= currentVersionId
674
685
  // re-announces an applied version. Confirm that it is a true duplicate, then
675
686
  // skip it. A duplicate does not advance the version counter, does not append
676
687
  // to the history (the slot already holds the applied update, ADR 067), and
@@ -690,22 +701,24 @@ export class Resolver {
690
701
  this.#phase = ResolverPhase.Complete;
691
702
  continue;
692
703
  }
693
- // Step 7, third arm: a version was skipped, so raise LATE_PUBLISHING.
704
+ // Step 6, third arm: a version was skipped, so raise LATE_PUBLISHING.
694
705
  if (update.targetVersionId !== this.#currentVersionId + 1) {
695
706
  throw new ResolveError(`Version Id Mismatch: targetVersionId cannot be > currentVersionId + 1`, LATE_PUBLISHING_ERROR, {
696
707
  targetVersionId: update.targetVersionId,
697
708
  currentVersionId: this.#currentVersionId + 1
698
709
  });
699
710
  }
700
- // Step 7, second arm: targetVersionId == currentVersionId + 1. Apply the update,
711
+ // Step 6, second arm: targetVersionId == currentVersionId + 1. Apply the update,
701
712
  // append the unsigned update hash to the history, increment the version.
702
713
  this.#currentDocument = Resolver.applyUpdate(document, update, block);
703
714
  const unsignedUpdate = JSONUtils.deleteKeys(update, ['proof']);
704
715
  this.#updateHashHistory.push(canonicalHashBytes(unsignedUpdate));
705
716
  this.#currentVersionId++;
706
- // Step 6: block_confirmations, and the header time as `updated`. On the apply
707
- // path only: the stop above and the duplicate branch stamp nothing.
717
+ // "Apply Update": block_confirmations, current_block_height, and the header time
718
+ // as `updated`. On the apply path only: the stop above and the duplicate branch
719
+ // stamp nothing.
708
720
  this.#blockConfirmations = block.confirmations;
721
+ this.#currentBlockHeight = block.height;
709
722
  this.#updated = DateUtils.toISOStringNonFractional(DateUtils.blocktimeToTimestamp(block.time));
710
723
  // The applied update can add a beacon service. "Find Beacon Signals" runs at
711
724
  // the top of every pass for the addresses that are not scanned yet, so the
@@ -763,8 +776,12 @@ export class Resolver {
763
776
  * malformed. It fails fast here with a typed error, in the style of the
764
777
  * {@link provide} guards, and not later with an invalid date or a false
765
778
  * `versionTime` comparison in the ProcessUpdate phase.
779
+ *
780
+ * "Find Beacon Signals" finds only the transactions at or above
781
+ * `current_block_height`, the height of the block of the most recently applied
782
+ * update. A signal below it is excluded: it emits no data need and applies no update.
766
783
  * @param {Array<BeaconSignal>} signals The signals the caller provided for one service.
767
- * @returns {Array<BeaconSignal>} The signals at or above the threshold, in the given order.
784
+ * @returns {Array<BeaconSignal>} The signals at or above the threshold and the height, in the given order.
768
785
  * @throws {ResolveError} `INVALID_DID_UPDATE` for an eligible signal with no valid block metadata.
769
786
  */
770
787
  #eligibleSignals(signals) {
@@ -785,6 +802,11 @@ export class Resolver {
785
802
  mediantime: block?.mediantime
786
803
  });
787
804
  }
805
+ // "Find Beacon Signals" finds only the transactions at or above current_block_height.
806
+ // A signal before the block of the update that added the address is not a signal
807
+ // of this DID.
808
+ if (block.height < this.#currentBlockHeight)
809
+ continue;
788
810
  eligible.push(signal);
789
811
  }
790
812
  return eligible;
@@ -812,10 +834,12 @@ export class Resolver {
812
834
  throw new ResolveError('Provided data for NeedCASAnnouncement is not a CAS announcement.', INVALID_DID_UPDATE, { kind: need.kind });
813
835
  }
814
836
  // Fail fast if the provided announcement is not the one the on-chain
815
- // signal requested: its canonical hash must equal the need's hash.
837
+ // signal requested: its canonical hash must equal the need's hash. The
838
+ // specification ("Process CAS Beacon") treats an announcement whose hash is
839
+ // not map_update_hash as not available from CAS: MISSING_UPDATE_DATA.
816
840
  const announcementHash = canonicalHash(data, { encoding: 'hex' });
817
841
  if (announcementHash !== need.announcementHash) {
818
- throw new ResolveError(`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`, INVALID_DID_UPDATE, { expected: need.announcementHash, actual: announcementHash });
842
+ throw new ResolveError(`CAS announcement hash mismatch: expected ${need.announcementHash}, got ${announcementHash}.`, MISSING_UPDATE_DATA, { expected: need.announcementHash, actual: announcementHash });
819
843
  }
820
844
  this.#sidecarData.casMap.set(announcementHash, data);
821
845
  break;
@@ -825,22 +849,33 @@ export class Resolver {
825
849
  throw new ResolveError('Provided data for NeedSignedUpdate is not a signed BTCR2 update.', INVALID_DID_UPDATE, { kind: need.kind });
826
850
  }
827
851
  // Fail fast if the provided update is not the one the on-chain signal
828
- // requested: its canonical hash must equal the need's hash.
852
+ // requested: the specification compares the JSON Document Hash of a
853
+ // retrieved update to update_hash, and a mismatch is INVALID_SIGNAL_DATA.
829
854
  const updateHash = canonicalHash(data, { encoding: 'hex' });
830
855
  if (updateHash !== need.updateHash) {
831
- throw new ResolveError(`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`, INVALID_DID_UPDATE, { expected: need.updateHash, actual: updateHash });
856
+ throw new ResolveError(`Signed update hash mismatch: expected ${need.updateHash}, got ${updateHash}.`, INVALID_SIGNAL_DATA, { expected: need.updateHash, actual: updateHash });
832
857
  }
833
858
  this.#sidecarData.updateMap.set(updateHash, data);
834
859
  break;
835
860
  }
836
861
  case 'NeedSMTProof': {
862
+ // A proof of another shape is data for the signal that does not agree with
863
+ // its Signal Bytes: INVALID_SIGNAL_DATA, as for the id and the walk below.
837
864
  if (!isSMTProof(data)) {
838
- throw new ResolveError('Provided data for NeedSMTProof is not an SMT proof.', INVALID_DID_UPDATE, { kind: need.kind });
865
+ throw new ResolveError('Provided data for NeedSMTProof is not an SMT proof.', INVALID_SIGNAL_DATA, { kind: need.kind });
866
+ }
867
+ // proof.id is base64url per spec; smtRootHash is the hex on-chain signal. The
868
+ // specification ("Process SMT Beacon") compares the id of smt_proof to
869
+ // smt_root: a mismatch, or an id that does not decode, is INVALID_SIGNAL_DATA.
870
+ let proofIdHex;
871
+ try {
872
+ proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
873
+ }
874
+ catch {
875
+ proofIdHex = undefined;
839
876
  }
840
- // proof.id is base64url per spec; smtRootHash is the hex on-chain signal.
841
- const proofIdHex = encodeHash(decodeHash(data.id, 'base64urlnopad'), 'hex');
842
877
  if (proofIdHex !== need.smtRootHash) {
843
- throw new ResolveError(`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex}`, INVALID_DID_UPDATE, { expected: need.smtRootHash, actual: proofIdHex });
878
+ throw new ResolveError(`SMT proof root hash mismatch: expected ${need.smtRootHash}, got ${proofIdHex ?? 'an id that does not decode'}.`, INVALID_SIGNAL_DATA, { expected: need.smtRootHash, actual: proofIdHex });
844
879
  }
845
880
  this.#sidecarData.smtMap.set(need.smtRootHash, data);
846
881
  break;