@did-btcr2/method 0.28.0 → 0.32.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 (193) hide show
  1. package/README.md +13 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +34125 -44647
  4. package/dist/browser.mjs +26409 -36931
  5. package/dist/cjs/index.js +2869 -679
  6. package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
  8. package/dist/esm/core/aggregation/cohort.js +31 -8
  9. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  10. package/dist/esm/core/aggregation/logger.js +15 -0
  11. package/dist/esm/core/aggregation/logger.js.map +1 -0
  12. package/dist/esm/core/aggregation/messages/base.js +12 -1
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +90 -0
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
  16. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  17. package/dist/esm/core/aggregation/messages/index.js +1 -0
  18. package/dist/esm/core/aggregation/messages/index.js.map +1 -1
  19. package/dist/esm/core/aggregation/participant.js +39 -46
  20. package/dist/esm/core/aggregation/participant.js.map +1 -1
  21. package/dist/esm/core/aggregation/runner/participant-runner.js +33 -7
  22. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  23. package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
  24. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  25. package/dist/esm/core/aggregation/service.js +143 -15
  26. package/dist/esm/core/aggregation/service.js.map +1 -1
  27. package/dist/esm/core/aggregation/signing-session.js +44 -5
  28. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  29. package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
  30. package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
  31. package/dist/esm/core/aggregation/transport/factory.js +15 -6
  32. package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
  33. package/dist/esm/core/aggregation/transport/http/client.js +350 -0
  34. package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
  35. package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
  36. package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
  37. package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
  38. package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
  39. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
  40. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
  41. package/dist/esm/core/aggregation/transport/http/index.js +12 -0
  42. package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
  43. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
  44. package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
  45. package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
  46. package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
  47. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
  48. package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
  49. package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
  50. package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
  51. package/dist/esm/core/aggregation/transport/http/server.js +481 -0
  52. package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
  53. package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
  54. package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
  55. package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
  56. package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
  57. package/dist/esm/core/aggregation/transport/index.js +1 -0
  58. package/dist/esm/core/aggregation/transport/index.js.map +1 -1
  59. package/dist/esm/core/aggregation/transport/nostr.js +245 -16
  60. package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
  61. package/dist/esm/core/beacon/beacon.js +295 -63
  62. package/dist/esm/core/beacon/beacon.js.map +1 -1
  63. package/dist/esm/core/beacon/cas-beacon.js +3 -3
  64. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  65. package/dist/esm/core/beacon/singleton-beacon.js +3 -3
  66. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  67. package/dist/esm/core/beacon/smt-beacon.js +3 -3
  68. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  69. package/dist/esm/core/beacon/utils.js +14 -9
  70. package/dist/esm/core/beacon/utils.js.map +1 -1
  71. package/dist/esm/core/updater.js +63 -55
  72. package/dist/esm/core/updater.js.map +1 -1
  73. package/dist/esm/did-btcr2.js +0 -4
  74. package/dist/esm/did-btcr2.js.map +1 -1
  75. package/dist/esm/index.js +2 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/utils/did-document.js +2 -2
  78. package/dist/esm/utils/did-document.js.map +1 -1
  79. package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
  80. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
  81. package/dist/types/core/aggregation/cohort.d.ts +20 -3
  82. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  83. package/dist/types/core/aggregation/logger.d.ts +22 -0
  84. package/dist/types/core/aggregation/logger.d.ts.map +1 -0
  85. package/dist/types/core/aggregation/messages/base.d.ts +13 -1
  86. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  87. package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
  88. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
  89. package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
  90. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  91. package/dist/types/core/aggregation/messages/index.d.ts +1 -0
  92. package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
  93. package/dist/types/core/aggregation/participant.d.ts +2 -0
  94. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  95. package/dist/types/core/aggregation/runner/events.d.ts +32 -6
  96. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  97. package/dist/types/core/aggregation/runner/participant-runner.d.ts +7 -5
  98. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  99. package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
  100. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  101. package/dist/types/core/aggregation/service.d.ts +33 -2
  102. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  103. package/dist/types/core/aggregation/signing-session.d.ts +5 -1
  104. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  105. package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
  106. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
  107. package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
  108. package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
  109. package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
  110. package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
  111. package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
  112. package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
  113. package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
  114. package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
  115. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
  116. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
  117. package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
  118. package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
  119. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
  120. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
  121. package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
  122. package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
  123. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
  124. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
  125. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
  126. package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
  127. package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
  128. package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
  129. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
  130. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
  131. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
  132. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
  133. package/dist/types/core/aggregation/transport/index.d.ts +1 -0
  134. package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
  135. package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
  136. package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
  137. package/dist/types/core/aggregation/transport/transport.d.ts +26 -1
  138. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  139. package/dist/types/core/beacon/beacon.d.ts +149 -22
  140. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  141. package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
  142. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  143. package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
  144. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  145. package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
  146. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  147. package/dist/types/core/beacon/utils.d.ts +2 -2
  148. package/dist/types/core/beacon/utils.d.ts.map +1 -1
  149. package/dist/types/core/updater.d.ts +27 -12
  150. package/dist/types/core/updater.d.ts.map +1 -1
  151. package/dist/types/did-btcr2.d.ts.map +1 -1
  152. package/dist/types/index.d.ts +2 -0
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/package.json +5 -7
  155. package/src/core/aggregation/beacon-strategy.ts +123 -0
  156. package/src/core/aggregation/cohort.ts +34 -8
  157. package/src/core/aggregation/logger.ts +33 -0
  158. package/src/core/aggregation/messages/base.ts +20 -5
  159. package/src/core/aggregation/messages/bodies.ts +223 -0
  160. package/src/core/aggregation/messages/factories.ts +1 -0
  161. package/src/core/aggregation/messages/index.ts +1 -0
  162. package/src/core/aggregation/participant.ts +40 -46
  163. package/src/core/aggregation/runner/events.ts +27 -3
  164. package/src/core/aggregation/runner/participant-runner.ts +41 -7
  165. package/src/core/aggregation/runner/service-runner.ts +227 -19
  166. package/src/core/aggregation/service.ts +189 -20
  167. package/src/core/aggregation/signing-session.ts +65 -7
  168. package/src/core/aggregation/transport/didcomm.ts +17 -0
  169. package/src/core/aggregation/transport/factory.ts +48 -12
  170. package/src/core/aggregation/transport/http/client.ts +409 -0
  171. package/src/core/aggregation/transport/http/envelope.ts +204 -0
  172. package/src/core/aggregation/transport/http/errors.ts +11 -0
  173. package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
  174. package/src/core/aggregation/transport/http/index.ts +11 -0
  175. package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
  176. package/src/core/aggregation/transport/http/protocol.ts +57 -0
  177. package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
  178. package/src/core/aggregation/transport/http/request-auth.ts +164 -0
  179. package/src/core/aggregation/transport/http/server.ts +615 -0
  180. package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
  181. package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
  182. package/src/core/aggregation/transport/index.ts +1 -0
  183. package/src/core/aggregation/transport/nostr.ts +266 -23
  184. package/src/core/aggregation/transport/transport.ts +34 -1
  185. package/src/core/beacon/beacon.ts +411 -79
  186. package/src/core/beacon/cas-beacon.ts +4 -4
  187. package/src/core/beacon/singleton-beacon.ts +4 -4
  188. package/src/core/beacon/smt-beacon.ts +4 -4
  189. package/src/core/beacon/utils.ts +16 -11
  190. package/src/core/updater.ts +113 -67
  191. package/src/did-btcr2.ts +0 -5
  192. package/src/index.ts +2 -0
  193. package/src/utils/did-document.ts +2 -2
@@ -1,13 +1,14 @@
1
- import { canonicalHash, canonicalize } from '@did-btcr2/common';
1
+ import { canonicalHash } from '@did-btcr2/common';
2
2
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
3
3
  import type { SchnorrKeyPair } from '@did-btcr2/keypair';
4
4
  import type { SerializedSMTProof} from '@did-btcr2/smt';
5
- import { blockHash, didToIndex, hashToHex, hexToHash, verifySerializedProof } from '@did-btcr2/smt';
6
- import { bytesToHex } from '@noble/hashes/utils';
7
- import { Transaction } from 'bitcoinjs-lib';
5
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
6
+ import { Transaction } from '@scure/btc-signer';
7
+ import { getBeaconStrategy } from './beacon-strategy.js';
8
8
  import { AggregationCohort } from './cohort.js';
9
9
  import { AggregationParticipantError } from './errors.js';
10
10
  import type { BaseMessage } from './messages/base.js';
11
+ import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
11
12
  import {
12
13
  AGGREGATED_NONCE,
13
14
  AUTHORIZATION_REQUEST,
@@ -61,6 +62,8 @@ export interface PendingSigningRequest {
61
62
  cohortId: string;
62
63
  sessionId: string;
63
64
  pendingTxHex: string;
65
+ /** Hex-encoded scriptPubKey of the UTXO being spent. Required for BIP-341 sighash. */
66
+ prevOutScriptHex: string;
64
67
  prevOutValue: string;
65
68
  }
66
69
 
@@ -110,6 +113,10 @@ export class AggregationParticipant {
110
113
  * outgoing messages — those come exclusively from action methods.
111
114
  */
112
115
  public receive(message: BaseMessage): void {
116
+ // Reject messages whose wire version doesn't match what this build speaks.
117
+ if(message.version === undefined || message.version !== AGGREGATION_WIRE_VERSION) {
118
+ return;
119
+ }
113
120
  const type = message.type;
114
121
  switch(type) {
115
122
  case COHORT_ADVERT:
@@ -291,46 +298,28 @@ export class AggregationParticipant {
291
298
  if(!state.submittedUpdate) return;
292
299
 
293
300
  const beaconType = message.body?.beaconType;
301
+ if(!beaconType) return;
302
+ const strategy = getBeaconStrategy(beaconType);
303
+ if(!strategy) return;
304
+
294
305
  const signalBytesHex = message.body?.signalBytesHex ?? '';
295
306
  const expectedHash = canonicalHash(state.submittedUpdate);
296
- let matches = false;
297
-
298
- if(beaconType === 'CASBeacon') {
299
- const casAnnouncement = message.body?.casAnnouncement;
300
- if(casAnnouncement) {
301
- matches = casAnnouncement[this.did] === expectedHash;
302
- state.validation = {
303
- cohortId,
304
- beaconType,
305
- signalBytesHex,
306
- casAnnouncement,
307
- expectedHash,
308
- matches,
309
- };
310
- }
311
- } else if(beaconType === 'SMTBeacon') {
312
- const smtProof = message.body?.smtProof as unknown as SerializedSMTProof | undefined;
313
- if(smtProof?.updateId && smtProof?.nonce) {
314
- // Verify updateId matches the canonicalized update hash
315
- const canonicalBytes = new TextEncoder().encode(canonicalize(state.submittedUpdate));
316
- const expectedUpdateId = hashToHex(blockHash(canonicalBytes));
317
- if(smtProof.updateId === expectedUpdateId) {
318
- // Verify Merkle inclusion
319
- const index = didToIndex(this.did);
320
- const candidateHash = blockHash(blockHash(hexToHash(smtProof.nonce)), hexToHash(smtProof.updateId));
321
- matches = verifySerializedProof(smtProof, index, candidateHash);
322
- }
323
- state.validation = {
324
- cohortId,
325
- beaconType,
326
- signalBytesHex,
327
- smtProof,
328
- expectedHash,
329
- matches,
330
- };
331
- }
332
- }
307
+ const result = strategy.validateParticipantView({
308
+ participantDid : this.did,
309
+ submittedUpdate : state.submittedUpdate,
310
+ expectedHash,
311
+ body : message.body!,
312
+ });
333
313
 
314
+ state.validation = {
315
+ cohortId,
316
+ beaconType,
317
+ signalBytesHex,
318
+ expectedHash,
319
+ matches : result.matches,
320
+ casAnnouncement : result.casAnnouncement,
321
+ smtProof : result.smtProof,
322
+ };
334
323
  state.phase = ParticipantCohortPhase.AwaitingValidation;
335
324
  }
336
325
 
@@ -395,13 +384,15 @@ export class AggregationParticipant {
395
384
 
396
385
  const sessionId = message.body?.sessionId;
397
386
  const pendingTxHex = message.body?.pendingTx;
387
+ const prevOutScriptHex = message.body?.prevOutScriptHex;
398
388
  const prevOutValue = message.body?.prevOutValue;
399
- if(!sessionId || !pendingTxHex || !prevOutValue) return;
389
+ if(!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
400
390
 
401
391
  state.signingRequest = {
402
392
  cohortId,
403
393
  sessionId,
404
394
  pendingTxHex,
395
+ prevOutScriptHex,
405
396
  prevOutValue,
406
397
  };
407
398
  state.phase = ParticipantCohortPhase.AwaitingSigning;
@@ -425,11 +416,14 @@ export class AggregationParticipant {
425
416
  );
426
417
  }
427
418
 
428
- const tx = Transaction.fromHex(state.signingRequest.pendingTxHex);
419
+ const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
429
420
 
430
- // Derive UTXO metadata for Taproot sighash (BIP-341).
431
- // The beacon TX's change output (index 0) uses the same Taproot address as the input.
432
- const prevOutScripts = tx.outs.length > 0 ? [tx.outs[0].script] : [];
421
+ // Derive UTXO metadata for Taproot sighash (BIP-341). Use the script
422
+ // supplied by the service in AUTHORIZATION_REQUEST rather than reading
423
+ // the change output: input and change may use different scripts in future
424
+ // beacon designs, and the prevOutScript must be the UTXO script, not the
425
+ // change script.
426
+ const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
433
427
  const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
434
428
 
435
429
  const session = new BeaconSigningSession({
@@ -1,5 +1,6 @@
1
+ import type { SerializedSMTProof } from '@did-btcr2/smt';
1
2
  import type { CohortAdvert, PendingSigningRequest, PendingValidation } from '../participant.js';
2
- import type { AggregationResult, PendingOptIn } from '../service.js';
3
+ import type { AggregationResult, PendingOptIn, Rejection } from '../service.js';
3
4
 
4
5
  /**
5
6
  * AggregationServiceRunner events are emitted by the AggregationServiceRunner to signal important
@@ -22,6 +23,13 @@ export type AggregationServiceEvents = {
22
23
  /** A participant has submitted a signed update. */
23
24
  'update-received': [{ participantDid: string }];
24
25
 
26
+ /**
27
+ * An inbound message was silently dropped by the state machine (bad proof,
28
+ * oversized payload, wrong wire version, etc.). Fires for *any* rejection,
29
+ * not just SUBMIT_UPDATE.
30
+ */
31
+ 'message-rejected': [Rejection & { cohortId: string }];
32
+
25
33
  /** Aggregated data has been distributed to all participants for validation. */
26
34
  'data-distributed': [{ cohortId: string }];
27
35
 
@@ -37,6 +45,9 @@ export type AggregationServiceEvents = {
37
45
  /** Signing complete — final aggregated signature is ready to broadcast. */
38
46
  'signing-complete': [AggregationResult];
39
47
 
48
+ /** Cohort transitioned to Failed phase (e.g. a participant rejected validation). */
49
+ 'cohort-failed': [{ cohortId: string; reason: string }];
50
+
40
51
  /** A non-fatal error occurred. Fatal errors reject the run() promise. */
41
52
  'error': [Error];
42
53
  };
@@ -66,8 +77,21 @@ export type AggregationParticipantEvents = {
66
77
  /** Signing request has arrived. Fires before the sign approval callback. */
67
78
  'signing-requested': [PendingSigningRequest];
68
79
 
69
- /** Cohort signing is complete from this participant's perspective. */
70
- 'cohort-complete': [{ cohortId: string; beaconAddress: string }];
80
+ /**
81
+ * Cohort signing is complete from this participant's perspective.
82
+ * Includes the aggregated sidecar data the participant needs to keep for
83
+ * future DID resolution: the CAS Announcement map (for CAS beacons) or the
84
+ * SMT inclusion proof (for SMT beacons).
85
+ */
86
+ 'cohort-complete': [{
87
+ cohortId: string;
88
+ beaconAddress: string;
89
+ beaconType: string;
90
+ /** DID → base64url update hash. Populated only for CAS beacons. */
91
+ casAnnouncement?: Record<string, string>;
92
+ /** Merkle inclusion proof for this participant's slot. Populated only for SMT beacons. */
93
+ smtProof?: SerializedSMTProof;
94
+ }];
71
95
 
72
96
  /** Cohort failed (rejected validation, signing error, etc.). */
73
97
  'cohort-failed': [{ cohortId: string; reason: string }];
@@ -1,5 +1,6 @@
1
1
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
2
2
  import type { SchnorrKeyPair } from '@did-btcr2/keypair';
3
+ import type { SerializedSMTProof } from '@did-btcr2/smt';
3
4
  import type { BaseMessage } from '../messages/base.js';
4
5
  import {
5
6
  AGGREGATED_NONCE,
@@ -87,11 +88,7 @@ export interface AggregationParticipantRunnerOptions {
87
88
  * keys: myKeys,
88
89
  * shouldJoin: async (advert) => advert.beaconType === 'CASBeacon',
89
90
  * onProvideUpdate: async ({ beaconAddress }) => {
90
- <<<<<<< Updated upstream
91
- * return Update.sign(myDid, unsigned, vm, secretKey);
92
- =======
93
91
  * return Updater.sign(myDid, unsigned, vm, secretKey);
94
- >>>>>>> Stashed changes
95
92
  * },
96
93
  * });
97
94
  *
@@ -143,9 +140,29 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
143
140
  this.#registerHandlers();
144
141
  }
145
142
 
146
- /** Stop the runner. Does not unregister transport handlers. */
143
+ /** Stop the runner and detach transport handlers. Safe to call repeatedly. */
147
144
  stop(): void {
148
145
  this.#stopped = true;
146
+ this.#unregisterHandlers();
147
+ }
148
+
149
+ /** Message types this runner listens for on the transport. */
150
+ static readonly #HANDLED_MESSAGE_TYPES: readonly string[] = [
151
+ COHORT_ADVERT,
152
+ COHORT_OPT_IN_ACCEPT,
153
+ COHORT_READY,
154
+ DISTRIBUTE_AGGREGATED_DATA,
155
+ AUTHORIZATION_REQUEST,
156
+ AGGREGATED_NONCE,
157
+ ];
158
+
159
+ /** Internal: detach from the transport. Safe to call repeatedly. */
160
+ #unregisterHandlers(): void {
161
+ if(!this.#handlersRegistered) return;
162
+ this.#handlersRegistered = false;
163
+ for(const type of AggregationParticipantRunner.#HANDLED_MESSAGE_TYPES) {
164
+ this.#transport.unregisterMessageHandler(this.#did, type);
165
+ }
149
166
  }
150
167
 
151
168
  /**
@@ -154,7 +171,15 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
154
171
  */
155
172
  static async joinFirst(
156
173
  options: AggregationParticipantRunnerOptions
157
- ): Promise<{ cohortId: string; beaconAddress: string }> {
174
+ ): Promise<{
175
+ cohortId: string;
176
+ beaconAddress: string;
177
+ beaconType: string;
178
+ /** DID → base64url update hash. Populated only for CAS beacons. */
179
+ casAnnouncement?: Record<string, string>;
180
+ /** Merkle inclusion proof for this participant's slot. Populated only for SMT beacons. */
181
+ smtProof?: SerializedSMTProof;
182
+ }> {
158
183
  return new Promise((resolve, reject) => {
159
184
  const runner = new AggregationParticipantRunner(options);
160
185
  runner.once('cohort-complete', (info) => {
@@ -341,7 +366,16 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
341
366
  if (this.session.getCohortPhase(cohortId) === ParticipantCohortPhase.Complete) {
342
367
  const info = this.session.joinedCohorts.get(cohortId);
343
368
  if (info) {
344
- this.emit('cohort-complete', { cohortId, beaconAddress: info.beaconAddress });
369
+ // Surface the sidecar data the participant will need for future resolutions:
370
+ // the CAS Announcement map (CAS beacons) or their SMT inclusion proof.
371
+ const validation = this.session.pendingValidations.get(cohortId);
372
+ this.emit('cohort-complete', {
373
+ cohortId,
374
+ beaconAddress : info.beaconAddress,
375
+ beaconType : validation?.beaconType ?? '',
376
+ casAnnouncement : validation?.casAnnouncement,
377
+ smtProof : validation?.smtProof,
378
+ });
345
379
  }
346
380
  }
347
381
  } catch (err) {
@@ -65,8 +65,44 @@ export interface AggregationServiceRunnerOptions {
65
65
  * REQUIRED — no sensible default.
66
66
  */
67
67
  onProvideTxData: OnProvideTxData;
68
+
69
+ /**
70
+ * Maximum canonicalized byte-length of a signed update body. Submissions
71
+ * above this cap are rejected and surfaced via the `message-rejected` event.
72
+ * Defaults to {@link DEFAULT_MAX_UPDATE_SIZE_BYTES} (256 KiB).
73
+ */
74
+ maxUpdateSizeBytes?: number;
75
+
76
+ /**
77
+ * Overall wall-clock budget for the cohort, from run() to signing-complete.
78
+ * On expiry the cohort is dropped, `cohort-failed` is emitted, and run()
79
+ * rejects with a timeout error. Leave undefined to disable.
80
+ */
81
+ cohortTtlMs?: number;
82
+
83
+ /**
84
+ * Maximum time allowed between phase transitions. Protects against stalled
85
+ * cohorts (e.g. a participant vanishing mid-protocol). Reset automatically
86
+ * on every observed phase change. Leave undefined to disable.
87
+ */
88
+ phaseTimeoutMs?: number;
89
+
90
+ /**
91
+ * Re-publish COHORT_ADVERT on this interval until keygen is finalized.
92
+ * Works around relays that don't backfill historical events to late
93
+ * subscribers — a republish gives late joiners a window to discover the
94
+ * advert without protocol changes. The first publish is immediate;
95
+ * subsequent publishes fire every `advertRepeatIntervalMs` until
96
+ * keygen-complete, fail, or stop(). Defaults to
97
+ * {@link DEFAULT_ADVERT_REPEAT_INTERVAL_MS} (60 s). Set to 0 to publish
98
+ * once and never retry.
99
+ */
100
+ advertRepeatIntervalMs?: number;
68
101
  }
69
102
 
103
+ /** Default cadence for re-publishing COHORT_ADVERT until keygen completes: 60 seconds. */
104
+ export const DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 60_000;
105
+
70
106
  /**
71
107
  * High-level facade for running an Aggregation Service over a Transport.
72
108
  *
@@ -111,12 +147,27 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
111
147
  readonly #onOptInReceived: OnOptInReceived;
112
148
  readonly #onReadyToFinalize: OnReadyToFinalize;
113
149
  readonly #onProvideTxData: OnProvideTxData;
150
+ readonly #cohortTtlMs?: number;
151
+ readonly #phaseTimeoutMs?: number;
152
+ readonly #advertRepeatIntervalMs: number;
114
153
 
115
154
  #cohortId?: string;
116
155
  #handlersRegistered = false;
117
156
  #stopped = false;
157
+ /**
158
+ * Guard against the async race where two concurrent #handleOptIn invocations
159
+ * both pass the `participants.length >= minParticipants` check before either
160
+ * mutates the cohort phase. Set synchronously before any `await` so subsequent
161
+ * handlers observe it on their next resumption.
162
+ */
163
+ #finalizing = false;
118
164
  #resolveRun?: (result: AggregationResult) => void;
119
165
  #rejectRun?: (err: Error) => void;
166
+ #cohortTtlTimer?: ReturnType<typeof setTimeout>;
167
+ #phaseTimer?: ReturnType<typeof setTimeout>;
168
+ #lastObservedPhase?: string;
169
+ /** Stop handle for the repeating COHORT_ADVERT publish loop. */
170
+ #stopAdvertRepeat?: () => void;
120
171
 
121
172
  constructor(options: AggregationServiceRunnerOptions) {
122
173
  super();
@@ -128,8 +179,27 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
128
179
  finalize : acceptedCount >= minRequired,
129
180
  }));
130
181
  this.#onProvideTxData = options.onProvideTxData;
182
+ this.#cohortTtlMs = options.cohortTtlMs;
183
+ this.#phaseTimeoutMs = options.phaseTimeoutMs;
184
+ this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
185
+
186
+ this.session = new AggregationService({
187
+ did : options.did,
188
+ keys : options.keys,
189
+ maxUpdateSizeBytes : options.maxUpdateSizeBytes,
190
+ });
191
+ }
131
192
 
132
- this.session = new AggregationService({ did: options.did, keys: options.keys });
193
+ /**
194
+ * Drain any silent rejections the state machine recorded during the most
195
+ * recent receive() and surface them as `message-rejected` events. Safe to
196
+ * call even before a cohortId is assigned.
197
+ */
198
+ #drainRejections(): void {
199
+ if(!this.#cohortId) return;
200
+ for(const r of this.session.drainRejections(this.#cohortId)) {
201
+ this.emit('message-rejected', { cohortId: this.#cohortId, ...r });
202
+ }
133
203
  }
134
204
 
135
205
  /**
@@ -146,10 +216,20 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
146
216
  try {
147
217
  this.#registerHandlers();
148
218
  this.#cohortId = this.session.createCohort(this.#config);
219
+ this.#startTimers();
149
220
  // Emit cohort-advertised BEFORE the send so the event fires before any downstream cascade
150
221
  const advertMsgs = this.session.advertise(this.#cohortId);
222
+ this.#onPhaseMaybeChanged();
151
223
  this.emit('cohort-advertised', { cohortId: this.#cohortId });
152
- this.#sendAll(advertMsgs).catch(err => this.#fail(err));
224
+ // Publish the advert. If advertRepeatIntervalMs > 0 we republish on
225
+ // that cadence until keygen-complete / fail / stop — works around
226
+ // relays that don't backfill historical events to late subscribers.
227
+ // Otherwise fall back to a single send.
228
+ if(this.#advertRepeatIntervalMs > 0) {
229
+ this.#startAdvertRepeat(advertMsgs);
230
+ } else {
231
+ this.#sendAll(advertMsgs).catch(err => this.#fail(err));
232
+ }
153
233
  } catch(err) {
154
234
  this.#fail(err as Error);
155
235
  }
@@ -157,14 +237,95 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
157
237
  }
158
238
 
159
239
  /**
160
- * Stop the runner early. Cleans up internal state.
161
- * Note: does not unregister transport handlers (the transport interface
162
- * does not currently expose unregister).
240
+ * Begin publishing the cohort advert immediately and on a repeating interval
241
+ * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
242
+ * (no recipient) via the transport's `publishRepeating` primitive.
243
+ */
244
+ #startAdvertRepeat(advertMsgs: BaseMessage[]): void {
245
+ // COHORT_ADVERT is always a single broadcast message in the current
246
+ // protocol, but iterate for generality.
247
+ const stops: Array<() => void> = [];
248
+ for(const msg of advertMsgs) {
249
+ stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
250
+ }
251
+ this.#stopAdvertRepeat = () => {
252
+ for(const stop of stops) {
253
+ try { stop(); } catch { /* ignore */ }
254
+ }
255
+ };
256
+ }
257
+
258
+ /** Stop the advert republish loop. Idempotent. */
259
+ #stopAdvertRepeating(): void {
260
+ if(!this.#stopAdvertRepeat) return;
261
+ const stop = this.#stopAdvertRepeat;
262
+ this.#stopAdvertRepeat = undefined;
263
+ stop();
264
+ }
265
+
266
+ /** Schedule cohort TTL + phase timeout at the start of a run. */
267
+ #startTimers(): void {
268
+ if(this.#cohortTtlMs !== undefined) {
269
+ this.#cohortTtlTimer = setTimeout(() => {
270
+ const reason = `Cohort ${this.#cohortId ?? ''} exceeded TTL of ${this.#cohortTtlMs}ms`;
271
+ this.emit('cohort-failed', { cohortId: this.#cohortId ?? '', reason });
272
+ this.#fail(new Error(reason));
273
+ }, this.#cohortTtlMs);
274
+ }
275
+ this.#resetPhaseTimer();
276
+ }
277
+
278
+ /** Reset the per-phase stall timer. Called when a phase transition is observed. */
279
+ #resetPhaseTimer(): void {
280
+ if(this.#phaseTimer) clearTimeout(this.#phaseTimer);
281
+ this.#phaseTimer = undefined;
282
+ if(this.#phaseTimeoutMs === undefined) return;
283
+ this.#phaseTimer = setTimeout(() => {
284
+ const reason = `Cohort ${this.#cohortId ?? ''} stalled in phase ${this.#lastObservedPhase ?? '?'} for ${this.#phaseTimeoutMs}ms`;
285
+ this.emit('cohort-failed', { cohortId: this.#cohortId ?? '', reason });
286
+ this.#fail(new Error(reason));
287
+ }, this.#phaseTimeoutMs);
288
+ }
289
+
290
+ /** Detect a phase change since the last observation and reset the phase timer. */
291
+ #onPhaseMaybeChanged(): void {
292
+ if(!this.#cohortId) return;
293
+ const phase = this.session.getCohortPhase(this.#cohortId);
294
+ if(phase !== this.#lastObservedPhase) {
295
+ this.#lastObservedPhase = phase;
296
+ this.#resetPhaseTimer();
297
+ }
298
+ }
299
+
300
+ /** Clear both timers. Called on successful completion, stop(), and #fail. */
301
+ #clearTimers(): void {
302
+ if(this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
303
+ if(this.#phaseTimer) clearTimeout(this.#phaseTimer);
304
+ this.#cohortTtlTimer = undefined;
305
+ this.#phaseTimer = undefined;
306
+ }
307
+
308
+ /**
309
+ * Stop the runner early. Marks the runner stopped and detaches transport
310
+ * handlers so a restart or a new runner doesn't inherit stale dispatch.
163
311
  */
164
312
  stop(): void {
165
313
  this.#stopped = true;
314
+ this.#stopAdvertRepeating();
315
+ this.#clearTimers();
316
+ this.#unregisterHandlers();
317
+ if(this.#cohortId) this.session.removeCohort(this.#cohortId);
166
318
  }
167
319
 
320
+ /** Message types this runner listens for on the transport. */
321
+ static readonly #HANDLED_MESSAGE_TYPES: readonly string[] = [
322
+ COHORT_OPT_IN,
323
+ SUBMIT_UPDATE,
324
+ VALIDATION_ACK,
325
+ NONCE_CONTRIBUTION,
326
+ SIGNATURE_AUTHORIZATION,
327
+ ];
328
+
168
329
  /**
169
330
  * Internal: handler registration with the transport. Idempotent.
170
331
  */
@@ -179,6 +340,15 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
179
340
  this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
180
341
  }
181
342
 
343
+ /** Internal: detach from the transport. Safe to call repeatedly. */
344
+ #unregisterHandlers(): void {
345
+ if(!this.#handlersRegistered) return;
346
+ this.#handlersRegistered = false;
347
+ for(const type of AggregationServiceRunner.#HANDLED_MESSAGE_TYPES) {
348
+ this.#transport.unregisterMessageHandler(this.#did, type);
349
+ }
350
+ }
351
+
182
352
  /**
183
353
  * Internal: message handlers for each protocol step. Each handler:
184
354
  * 1) feeds the message into the state machine via session.receive()
@@ -195,6 +365,8 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
195
365
  if(this.#stopped) return;
196
366
  try {
197
367
  this.session.receive(msg);
368
+ this.#drainRejections();
369
+ this.#onPhaseMaybeChanged();
198
370
 
199
371
  const optIn = this.session.pendingOptIns(this.#cohortId!).get(msg.from);
200
372
  if(!optIn) return;
@@ -211,25 +383,35 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
211
383
  await this.#sendAll(this.session.acceptParticipant(this.#cohortId!, msg.from));
212
384
  this.emit('participant-accepted', { participantDid: msg.from });
213
385
 
214
- // Check if it's time to finalize
386
+ // Check if it's time to finalize. The `#finalizing` flag is set synchronously
387
+ // before the first await so concurrent opt-in handlers observe it and skip —
388
+ // otherwise two handlers could both pass the minParticipants check and both
389
+ // call finalizeKeygen, the second of which would throw (phase mismatch).
215
390
  const cohort = this.session.getCohort(this.#cohortId!)!;
216
- if(cohort.participants.length >= this.#config.minParticipants) {
391
+ if(cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
392
+ this.#finalizing = true;
217
393
  const finalizeDecision = await this.#onReadyToFinalize({
218
394
  acceptedCount : cohort.participants.length,
219
395
  minRequired : this.#config.minParticipants,
220
396
  });
221
- if(finalizeDecision.finalize) {
222
- // finalizeKeygen() computes the beacon address synchronously
223
- // emit BEFORE awaiting sendAll. Otherwise the downstream cascade
224
- // (which can run all the way to signing-complete) would resolve the
225
- // run() promise before this event fires.
226
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId!);
227
- this.emit('keygen-complete', {
228
- cohortId : this.#cohortId!,
229
- beaconAddress : cohort.beaconAddress,
230
- });
231
- await this.#sendAll(readyMsgs);
397
+ if(!finalizeDecision.finalize) {
398
+ // Operator declined — reset the flag so a later opt-in can retry.
399
+ this.#finalizing = false;
400
+ return;
232
401
  }
402
+ // finalizeKeygen() computes the beacon address synchronously
403
+ // emit BEFORE awaiting sendAll. Otherwise the downstream cascade
404
+ // (which can run all the way to signing-complete) would resolve the
405
+ // run() promise before this event fires.
406
+ const readyMsgs = this.session.finalizeKeygen(this.#cohortId!);
407
+ // Keygen done — stop re-advertising the cohort. New participants
408
+ // arriving after this point would be rejected anyway.
409
+ this.#stopAdvertRepeating();
410
+ this.emit('keygen-complete', {
411
+ cohortId : this.#cohortId!,
412
+ beaconAddress : cohort.beaconAddress,
413
+ });
414
+ await this.#sendAll(readyMsgs);
233
415
  }
234
416
  } catch(err) {
235
417
  this.#fail(err as Error);
@@ -248,6 +430,8 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
248
430
  if(this.#stopped) return;
249
431
  try {
250
432
  this.session.receive(msg);
433
+ this.#drainRejections();
434
+ this.#onPhaseMaybeChanged();
251
435
  this.emit('update-received', { participantDid: msg.from });
252
436
 
253
437
  // When all updates collected, build and distribute
@@ -273,11 +457,25 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
273
457
  if(this.#stopped) return;
274
458
  try {
275
459
  this.session.receive(msg);
460
+ this.#drainRejections();
461
+ this.#onPhaseMaybeChanged();
276
462
  const approved = !!msg.body?.approved;
277
463
  this.emit('validation-received', { participantDid: msg.from, approved });
278
464
 
465
+ const phase = this.session.getCohortPhase(this.#cohortId!);
466
+
467
+ // A participant rejection flips the cohort to Failed. Emit a structured
468
+ // event so the runner/caller sees the failure instead of the cohort
469
+ // silently stalling.
470
+ if(phase === ServiceCohortPhase.Failed) {
471
+ const reason = `Validation rejected by participant ${msg.from}`;
472
+ this.emit('cohort-failed', { cohortId: this.#cohortId!, reason });
473
+ this.#fail(new Error(reason));
474
+ return;
475
+ }
476
+
279
477
  // When all validations received, request tx data and start signing
280
- if(this.session.getCohortPhase(this.#cohortId!) === ServiceCohortPhase.Validated) {
478
+ if(phase === ServiceCohortPhase.Validated) {
281
479
  const cohort = this.session.getCohort(this.#cohortId!)!;
282
480
  const txData = await this.#onProvideTxData({
283
481
  cohortId : this.#cohortId!,
@@ -306,6 +504,8 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
306
504
  if(this.#stopped) return;
307
505
  try {
308
506
  this.session.receive(msg);
507
+ this.#drainRejections();
508
+ this.#onPhaseMaybeChanged();
309
509
  this.emit('nonce-received', { participantDid: msg.from });
310
510
 
311
511
  // When all nonces collected, send aggregated nonce
@@ -329,10 +529,14 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
329
529
  if(this.#stopped) return;
330
530
  try {
331
531
  this.session.receive(msg);
532
+ this.#drainRejections();
533
+ this.#onPhaseMaybeChanged();
332
534
 
333
535
  // The state machine auto-completes when all partial sigs received
334
536
  const result = this.session.getResult(this.#cohortId!);
335
537
  if(result) {
538
+ this.#clearTimers();
539
+ this.#unregisterHandlers();
336
540
  this.emit('signing-complete', result);
337
541
  this.#resolveRun?.(result);
338
542
  }
@@ -359,6 +563,10 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
359
563
  * @param {Error} err - The error to handle.
360
564
  */
361
565
  #fail(err: Error): void {
566
+ this.#stopAdvertRepeating();
567
+ this.#clearTimers();
568
+ this.#unregisterHandlers();
569
+ if(this.#cohortId) this.session.removeCohort(this.#cohortId);
362
570
  this.emit('error', err);
363
571
  this.#rejectRun?.(err);
364
572
  }