@did-btcr2/method 0.52.0 → 0.54.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.
Files changed (37) hide show
  1. package/README.md +6 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +3 -3
  4. package/dist/browser.mjs +3 -3
  5. package/dist/cjs/index.js +50 -20
  6. package/dist/esm/core/beacon/beacon.js.map +1 -1
  7. package/dist/esm/core/beacon/cas-beacon.js +15 -8
  8. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  9. package/dist/esm/core/beacon/singleton-beacon.js +3 -3
  10. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  11. package/dist/esm/core/beacon/smt-beacon.js +9 -3
  12. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  13. package/dist/esm/core/resolver.js +40 -10
  14. package/dist/esm/core/resolver.js.map +1 -1
  15. package/dist/esm/core/updater.js +13 -5
  16. package/dist/esm/core/updater.js.map +1 -1
  17. package/dist/types/core/beacon/beacon.d.ts +31 -3
  18. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  19. package/dist/types/core/beacon/cas-beacon.d.ts +15 -8
  20. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  21. package/dist/types/core/beacon/interfaces.d.ts +5 -2
  22. package/dist/types/core/beacon/interfaces.d.ts.map +1 -1
  23. package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
  24. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  25. package/dist/types/core/beacon/smt-beacon.d.ts +5 -3
  26. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  27. package/dist/types/core/resolver.d.ts.map +1 -1
  28. package/dist/types/core/updater.d.ts +20 -5
  29. package/dist/types/core/updater.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/core/beacon/beacon.ts +32 -3
  32. package/src/core/beacon/cas-beacon.ts +21 -13
  33. package/src/core/beacon/interfaces.ts +5 -2
  34. package/src/core/beacon/singleton-beacon.ts +5 -5
  35. package/src/core/beacon/smt-beacon.ts +12 -5
  36. package/src/core/resolver.ts +49 -11
  37. package/src/core/updater.ts +23 -7
@@ -126,13 +126,18 @@ function isCASAnnouncement(value: unknown): value is CASAnnouncement {
126
126
  return isRecord(value) && Object.values(value).every(v => typeof v === 'string');
127
127
  }
128
128
 
129
- /** True if `value` has the shape of a signed BTCR2 update. */
129
+ /**
130
+ * True if `value` has the shape of a signed BTCR2 update. targetVersionId must be an
131
+ * integer of at least 2: an update targets the version after the one it patches, and
132
+ * genesis is version 1, so no conformant update can target a lower version (ADR 068).
133
+ */
130
134
  function isSignedBTCR2Update(value: unknown): value is SignedBTCR2Update {
131
135
  if(!isRecord(value)) return false;
132
136
  return Array.isArray(value.patch)
133
137
  && typeof value.sourceHash === 'string'
134
138
  && typeof value.targetHash === 'string'
135
- && typeof value.targetVersionId === 'number'
139
+ && Number.isInteger(value.targetVersionId)
140
+ && (value.targetVersionId as number) >= 2
136
141
  && isRecord(value.proof);
137
142
  }
138
143
 
@@ -411,14 +416,6 @@ export class Resolver {
411
416
  // Set confirmations to the block confirmations
412
417
  response.metadata.confirmations = block.confirmations;
413
418
 
414
- // if resolutionOptions.versionTime is defined and the blocktime is more recent, return currentDocument
415
- if(versionTime) {
416
- // Safely convert versionTime to timestamp
417
- if(blocktime > DateUtils.dateStringToTimestamp(versionTime)) {
418
- return response;
419
- }
420
- }
421
-
422
419
  // Check update.targetVersionId against currentVersionId.
423
420
  // If update.targetVersionId <= currentVersionId, this update re-announces a version
424
421
  // that has already been applied. Confirm it is a true duplicate, then skip it: a
@@ -428,12 +425,28 @@ export class Resolver {
428
425
  // history already holds the applied update at updateHashHistory[targetVersionId - 2].
429
426
  // Holding the increment off the duplicate path is the deliberate did:btcr2 deviation
430
427
  // recorded in ADR 067: the read algorithm's "Increment current_version_id" belongs
431
- // to the apply branch, not to every tuple.
428
+ // to the apply branch, not to every tuple. Duplicates are confirmed whatever their
429
+ // blocktime, before the versionTime check below, so a re-announcement mined after
430
+ // versionTime can neither truncate the in-window history nor dodge late-publishing
431
+ // detection (ADR 068).
432
432
  if(update.targetVersionId <= currentVersionId) {
433
433
  this.confirmDuplicate(update, updateHashHistory);
434
434
  continue;
435
435
  }
436
436
 
437
+ // if resolutionOptions.versionTime is defined and the blocktime is more recent, return
438
+ // currentDocument. Evaluated only for tuples that would change state (apply or late
439
+ // publishing). The spec places this check before the duplicate branch, where the sort
440
+ // by targetVersionId lets a duplicate of an early version mined after versionTime end
441
+ // resolution before genuine in-window updates are processed; checking it here is the
442
+ // deliberate deviation recorded in ADR 068.
443
+ if(versionTime) {
444
+ // Safely convert versionTime to timestamp
445
+ if(blocktime > DateUtils.dateStringToTimestamp(versionTime)) {
446
+ return response;
447
+ }
448
+ }
449
+
437
450
  // If update.targetVersionId == currentVersionId + 1, apply the update
438
451
  if (update.targetVersionId === currentVersionId + 1) {
439
452
  // Check if update.sourceHash !== currentDocumentHash (byte comparison)
@@ -501,6 +514,17 @@ export class Resolver {
501
514
  * @returns {void} Does not return a value, but throws an error if the update is not a valid duplicate.
502
515
  */
503
516
  private static confirmDuplicate(update: SignedBTCR2Update, updateHashHistory: HashBytes[]): void {
517
+ // A conformant update targets the version after the one it patches, so targetVersionId
518
+ // is an integer of at least 2 (genesis is version 1). Anything else cannot name an
519
+ // applied update: it is a malformed update, not a duplicate, and without this guard it
520
+ // would read a nonexistent history slot below and crash on the byte comparison (ADR 068).
521
+ if (!Number.isInteger(update.targetVersionId) || update.targetVersionId < 2) {
522
+ throw new ResolveError(
523
+ `Invalid duplicate: targetVersionId must be an integer >= 2`,
524
+ INVALID_DID_UPDATE, { targetVersionId: update.targetVersionId }
525
+ );
526
+ }
527
+
504
528
  // Create unsigned_update by removing the proof property from update.
505
529
  const { proof: _, ...unsignedUpdate } = update;
506
530
 
@@ -510,6 +534,20 @@ export class Resolver {
510
534
  // Let historicalUpdateHash equal updateHashHistory[updateHashIndex].
511
535
  const historicalUpdateHash = updateHashHistory[update.targetVersionId - 2];
512
536
 
537
+ // The resolver's own loop records one history entry per applied version, so this slot
538
+ // always exists on that path; a standalone caller, however, can pass a resolutionState
539
+ // whose version counter outruns its history. A duplicate that cannot be checked against
540
+ // an applied update is unconfirmable, which is late-publishing evidence, not a pass.
541
+ if (historicalUpdateHash === undefined) {
542
+ throw new ResolveError(
543
+ `Invalid duplicate: no applied update in history for targetVersionId`,
544
+ LATE_PUBLISHING_ERROR, {
545
+ targetVersionId : update.targetVersionId,
546
+ historyLength : updateHashHistory.length
547
+ }
548
+ );
549
+ }
550
+
513
551
  // Check if the updateHash matches the historical hash (byte comparison)
514
552
  if (!equalBytes(historicalUpdateHash, unsignedUpdateHash)) {
515
553
  throw new ResolveError(
@@ -5,6 +5,8 @@ import { SchnorrMultikey } from '@did-btcr2/cryptosuite';
5
5
  import type { Signer } from '@did-btcr2/keypair';
6
6
  import type { Btcr2DataIntegrityConfig, SignedBTCR2Update, UnsignedBTCR2Update } from './btcr2-update.js';
7
7
  import { DidDocument, type Btcr2DidDocument, type DidVerificationMethod } from '../utils/did-document.js';
8
+ import type { BroadcastResult } from './beacon/beacon.js';
9
+ import type { CASBroadcastOptions } from './beacon/cas-beacon.js';
8
10
  import { BeaconFactory } from './beacon/factory.js';
9
11
  import type { BeaconService } from './beacon/interfaces.js';
10
12
 
@@ -56,10 +58,15 @@ export interface FundingProof {
56
58
  * The updater needs the caller to broadcast the signed update via the beacon.
57
59
  *
58
60
  * The caller decides how: for single-party beacons, call
59
- * `Updater.announce(beaconService, signedUpdate, secretKey, bitcoin)` or
61
+ * `Updater.announce(beaconService, signedUpdate, signer, bitcoin, options?)` or
60
62
  * `BeaconFactory.establish(beaconService).broadcastSignal(...)`. For multi-party
61
63
  * aggregate beacons, hand off to the aggregation protocol.
62
64
  *
65
+ * Both single-party paths return a `BroadcastResult`; capture it. Beyond the txid,
66
+ * it carries the per-beacon-type sidecar artifacts: the CAS Announcement (CAS
67
+ * beacons) and the SMT inclusion proof (SMT beacons), whose embedded nonce exists
68
+ * nowhere else, so a discarded proof makes the signal permanently unresolvable.
69
+ *
63
70
  * After the broadcast succeeds, the caller calls `updater.provide(need)` (with no
64
71
  * data) to transition the updater to Complete.
65
72
  */
@@ -137,10 +144,14 @@ export interface UpdaterParams {
137
144
  * // Check UTXOs at need.beaconAddress, fund if needed
138
145
  * updater.provide(need);
139
146
  * break;
140
- * case 'NeedBroadcast':
141
- * await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
147
+ * case 'NeedBroadcast': {
148
+ * // Capture the BroadcastResult: broadcast.txid, plus broadcast.announcement
149
+ * // (CAS beacons) and broadcast.proof (SMT beacons; must be retained, the
150
+ * // proof's nonce exists nowhere else).
151
+ * const broadcast = await Updater.announce(need.beaconService, need.signedUpdate, signer, bitcoin);
142
152
  * updater.provide(need);
143
153
  * break;
154
+ * }
144
155
  * }
145
156
  * }
146
157
  * state = updater.advance();
@@ -315,16 +326,21 @@ export class Updater {
315
326
  * @param {SignedBTCR2Update} update The signed update to announce.
316
327
  * @param {Signer} signer Signer that produces the ECDSA signature for the Bitcoin transaction.
317
328
  * @param {BitcoinConnection} bitcoin The Bitcoin network connection.
318
- * @returns {Promise<SignedBTCR2Update>} The signed update that was broadcast.
329
+ * @param {CASBroadcastOptions} [options] Optional broadcast configuration (fee estimator,
330
+ * change address, and, for CAS beacons, a `casPublish` callback invoked before the
331
+ * transaction broadcast; other beacon types ignore `casPublish`).
332
+ * @returns {Promise<BroadcastResult>} The broadcast artifacts: the signed update, the signal
333
+ * txid, and any per-beacon-type sidecar data (CAS announcement / SMT proof).
319
334
  */
320
335
  static async announce(
321
336
  beaconService: BeaconService,
322
337
  update: SignedBTCR2Update,
323
338
  signer: Signer,
324
- bitcoin: BitcoinConnection
325
- ): Promise<SignedBTCR2Update> {
339
+ bitcoin: BitcoinConnection,
340
+ options?: CASBroadcastOptions
341
+ ): Promise<BroadcastResult> {
326
342
  const beacon = BeaconFactory.establish(beaconService);
327
- return beacon.broadcastSignal(update, signer, bitcoin);
343
+ return beacon.broadcastSignal(update, signer, bitcoin, options);
328
344
  }
329
345
 
330
346
  // Private instance wrappers