@did-btcr2/method 0.36.0 → 0.37.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 (164) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/browser.js +1371 -260
  3. package/dist/browser.mjs +1371 -260
  4. package/dist/cjs/index.js +1464 -343
  5. package/dist/esm/core/aggregation/beacon-strategy.js +19 -6
  6. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -1
  7. package/dist/esm/core/aggregation/cohort.js +142 -33
  8. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  9. package/dist/esm/core/aggregation/conditions.js +27 -0
  10. package/dist/esm/core/aggregation/conditions.js.map +1 -1
  11. package/dist/esm/core/aggregation/fallback-spend.js +132 -0
  12. package/dist/esm/core/aggregation/fallback-spend.js.map +1 -0
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +46 -1
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
  16. package/dist/esm/core/aggregation/messages/constants.js +11 -0
  17. package/dist/esm/core/aggregation/messages/constants.js.map +1 -1
  18. package/dist/esm/core/aggregation/messages/factories.js +27 -1
  19. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  20. package/dist/esm/core/aggregation/messages/guards.js +4 -1
  21. package/dist/esm/core/aggregation/messages/guards.js.map +1 -1
  22. package/dist/esm/core/aggregation/participant.js +218 -18
  23. package/dist/esm/core/aggregation/participant.js.map +1 -1
  24. package/dist/esm/core/aggregation/phases.js +6 -0
  25. package/dist/esm/core/aggregation/phases.js.map +1 -1
  26. package/dist/esm/core/aggregation/recovery-policy.js +156 -0
  27. package/dist/esm/core/aggregation/recovery-policy.js.map +1 -0
  28. package/dist/esm/core/aggregation/recovery-spend.js +106 -0
  29. package/dist/esm/core/aggregation/recovery-spend.js.map +1 -0
  30. package/dist/esm/core/aggregation/runner/aggregation-runner.js +11 -3
  31. package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -1
  32. package/dist/esm/core/aggregation/runner/participant-runner.js +106 -19
  33. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  34. package/dist/esm/core/aggregation/runner/service-runner.js +409 -195
  35. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  36. package/dist/esm/core/aggregation/service.js +221 -11
  37. package/dist/esm/core/aggregation/service.js.map +1 -1
  38. package/dist/esm/core/aggregation/signing-session.js +3 -3
  39. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  40. package/dist/esm/core/aggregation/transport/http/envelope.js +3 -3
  41. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +1 -1
  42. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +1 -1
  43. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +1 -1
  44. package/dist/esm/core/aggregation/transport/http/request-auth.js +1 -1
  45. package/dist/esm/core/aggregation/transport/http/server.js +1 -1
  46. package/dist/esm/core/aggregation/transport/http/sse-stream.js +1 -1
  47. package/dist/esm/core/aggregation/transport/http/sse-writer.js +1 -1
  48. package/dist/esm/core/aggregation/transport/in-memory.js +1 -1
  49. package/dist/esm/core/aggregation/transport/nostr.js +6 -6
  50. package/dist/esm/core/beacon/beacon.js +15 -10
  51. package/dist/esm/core/beacon/beacon.js.map +1 -1
  52. package/dist/esm/core/beacon/cas-beacon.js +6 -6
  53. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  54. package/dist/esm/core/beacon/singleton-beacon.js +2 -2
  55. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  56. package/dist/esm/core/beacon/smt-beacon.js +3 -3
  57. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  58. package/dist/esm/core/resolver.js +8 -8
  59. package/dist/esm/core/resolver.js.map +1 -1
  60. package/dist/esm/core/updater.js +6 -6
  61. package/dist/esm/core/updater.js.map +1 -1
  62. package/dist/esm/did-btcr2.js +2 -2
  63. package/dist/esm/index.js +3 -0
  64. package/dist/esm/index.js.map +1 -1
  65. package/dist/types/core/aggregation/beacon-strategy.d.ts +8 -4
  66. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -1
  67. package/dist/types/core/aggregation/cohort.d.ts +88 -17
  68. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  69. package/dist/types/core/aggregation/conditions.d.ts +28 -0
  70. package/dist/types/core/aggregation/conditions.d.ts.map +1 -1
  71. package/dist/types/core/aggregation/fallback-spend.d.ts +94 -0
  72. package/dist/types/core/aggregation/fallback-spend.d.ts.map +1 -0
  73. package/dist/types/core/aggregation/logger.d.ts +1 -1
  74. package/dist/types/core/aggregation/messages/base.d.ts +6 -0
  75. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  76. package/dist/types/core/aggregation/messages/bodies.d.ts +50 -2
  77. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
  78. package/dist/types/core/aggregation/messages/constants.d.ts +8 -0
  79. package/dist/types/core/aggregation/messages/constants.d.ts.map +1 -1
  80. package/dist/types/core/aggregation/messages/factories.d.ts +40 -0
  81. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  82. package/dist/types/core/aggregation/messages/guards.d.ts.map +1 -1
  83. package/dist/types/core/aggregation/participant.d.ts +47 -4
  84. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  85. package/dist/types/core/aggregation/phases.d.ts +8 -2
  86. package/dist/types/core/aggregation/phases.d.ts.map +1 -1
  87. package/dist/types/core/aggregation/recovery-policy.d.ts +132 -0
  88. package/dist/types/core/aggregation/recovery-policy.d.ts.map +1 -0
  89. package/dist/types/core/aggregation/recovery-spend.d.ts +94 -0
  90. package/dist/types/core/aggregation/recovery-spend.d.ts.map +1 -0
  91. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts +13 -3
  92. package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -1
  93. package/dist/types/core/aggregation/runner/events.d.ts +48 -16
  94. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  95. package/dist/types/core/aggregation/runner/participant-runner.d.ts +23 -14
  96. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  97. package/dist/types/core/aggregation/runner/service-runner.d.ts +102 -23
  98. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  99. package/dist/types/core/aggregation/service.d.ts +26 -2
  100. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  101. package/dist/types/core/aggregation/signing-session.d.ts +2 -2
  102. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  103. package/dist/types/core/aggregation/transport/http/envelope.d.ts +3 -3
  104. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +1 -1
  105. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +1 -1
  106. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +1 -1
  107. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +1 -1
  108. package/dist/types/core/aggregation/transport/http/server.d.ts +1 -1
  109. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +2 -2
  110. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +1 -1
  111. package/dist/types/core/aggregation/transport/in-memory.d.ts +1 -1
  112. package/dist/types/core/aggregation/transport/nostr.d.ts +4 -4
  113. package/dist/types/core/aggregation/transport/transport.d.ts +4 -4
  114. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  115. package/dist/types/core/beacon/beacon.d.ts +7 -7
  116. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  117. package/dist/types/core/beacon/cas-beacon.d.ts +2 -2
  118. package/dist/types/core/beacon/interfaces.d.ts +1 -1
  119. package/dist/types/core/resolver.d.ts +3 -3
  120. package/dist/types/core/updater.d.ts +4 -4
  121. package/dist/types/did-btcr2.d.ts +2 -2
  122. package/dist/types/index.d.ts +3 -0
  123. package/dist/types/index.d.ts.map +1 -1
  124. package/package.json +4 -4
  125. package/src/core/aggregation/beacon-strategy.ts +27 -9
  126. package/src/core/aggregation/cohort.ts +171 -35
  127. package/src/core/aggregation/conditions.ts +56 -0
  128. package/src/core/aggregation/fallback-spend.ts +223 -0
  129. package/src/core/aggregation/logger.ts +1 -1
  130. package/src/core/aggregation/messages/base.ts +6 -0
  131. package/src/core/aggregation/messages/bodies.ts +94 -1
  132. package/src/core/aggregation/messages/constants.ts +12 -0
  133. package/src/core/aggregation/messages/factories.ts +57 -0
  134. package/src/core/aggregation/messages/guards.ts +6 -0
  135. package/src/core/aggregation/participant.ts +264 -20
  136. package/src/core/aggregation/phases.ts +9 -0
  137. package/src/core/aggregation/recovery-policy.ts +237 -0
  138. package/src/core/aggregation/recovery-spend.ts +192 -0
  139. package/src/core/aggregation/runner/aggregation-runner.ts +27 -5
  140. package/src/core/aggregation/runner/events.ts +47 -19
  141. package/src/core/aggregation/runner/participant-runner.ts +113 -31
  142. package/src/core/aggregation/runner/service-runner.ts +484 -203
  143. package/src/core/aggregation/service.ts +260 -14
  144. package/src/core/aggregation/signing-session.ts +3 -3
  145. package/src/core/aggregation/transport/http/envelope.ts +3 -3
  146. package/src/core/aggregation/transport/http/inbox-buffer.ts +1 -1
  147. package/src/core/aggregation/transport/http/nonce-cache.ts +1 -1
  148. package/src/core/aggregation/transport/http/rate-limiter.ts +1 -1
  149. package/src/core/aggregation/transport/http/request-auth.ts +1 -1
  150. package/src/core/aggregation/transport/http/server.ts +1 -1
  151. package/src/core/aggregation/transport/http/sse-stream.ts +2 -2
  152. package/src/core/aggregation/transport/http/sse-writer.ts +1 -1
  153. package/src/core/aggregation/transport/in-memory.ts +1 -1
  154. package/src/core/aggregation/transport/nostr.ts +6 -6
  155. package/src/core/aggregation/transport/transport.ts +4 -4
  156. package/src/core/beacon/beacon.ts +16 -11
  157. package/src/core/beacon/cas-beacon.ts +7 -7
  158. package/src/core/beacon/interfaces.ts +1 -1
  159. package/src/core/beacon/singleton-beacon.ts +2 -2
  160. package/src/core/beacon/smt-beacon.ts +3 -3
  161. package/src/core/resolver.ts +8 -8
  162. package/src/core/updater.ts +6 -6
  163. package/src/did-btcr2.ts +2 -2
  164. package/src/index.ts +3 -0
package/dist/browser.js CHANGED
@@ -93180,9 +93180,11 @@ a=end-of-candidates
93180
93180
  DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
93181
93181
  DEFAULT_BROADCAST_LOOKBACK_MS: () => DEFAULT_BROADCAST_LOOKBACK_MS,
93182
93182
  DEFAULT_CLOCK_SKEW_SEC: () => DEFAULT_CLOCK_SKEW_SEC,
93183
+ DEFAULT_FUNDING_MODEL: () => DEFAULT_FUNDING_MODEL,
93183
93184
  DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
93184
93185
  DEFAULT_NONCE_LEN_BYTES: () => DEFAULT_NONCE_LEN_BYTES,
93185
93186
  DEFAULT_NOSTR_RELAYS: () => DEFAULT_NOSTR_RELAYS,
93187
+ DEFAULT_RECOVERY_SEQUENCE: () => DEFAULT_RECOVERY_SEQUENCE,
93186
93188
  DID_REGEX: () => DID_REGEX,
93187
93189
  DISTRIBUTE_AGGREGATED_DATA: () => DISTRIBUTE_AGGREGATED_DATA,
93188
93190
  DidBtcr2: () => DidBtcr2,
@@ -93191,6 +93193,8 @@ a=end-of-candidates
93191
93193
  DidDocumentBuilder: () => DidDocumentBuilder,
93192
93194
  DidVerificationMethod: () => DidVerificationMethod,
93193
93195
  Document: () => Document,
93196
+ FALLBACK_AUTHORIZATION_REQUEST: () => FALLBACK_AUTHORIZATION_REQUEST,
93197
+ FALLBACK_SIGNATURE: () => FALLBACK_SIGNATURE,
93194
93198
  GenesisDocument: () => GenesisDocument,
93195
93199
  HTTP_ENVELOPE_VERSION: () => HTTP_ENVELOPE_VERSION,
93196
93200
  HTTP_ROUTE: () => HTTP_ROUTE,
@@ -93204,7 +93208,9 @@ a=end-of-candidates
93204
93208
  InMemoryTransport: () => InMemoryTransport,
93205
93209
  InboxBuffer: () => InboxBuffer,
93206
93210
  KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
93211
+ KNOWN_FUNDING_MODELS: () => KNOWN_FUNDING_MODELS,
93207
93212
  KeyPairAggregationSigner: () => KeyPairAggregationSigner,
93213
+ MAX_RECOVERY_SEQUENCE: () => MAX_RECOVERY_SEQUENCE,
93208
93214
  NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
93209
93215
  NonceCache: () => NonceCache,
93210
93216
  NostrTransport: () => NostrTransport,
@@ -93221,6 +93227,7 @@ a=end-of-candidates
93221
93227
  SMTBeacon: () => SMTBeacon,
93222
93228
  SMTBeaconError: () => SMTBeaconError,
93223
93229
  SSE_EVENT: () => SSE_EVENT,
93230
+ SUBMIT_NONINCLUDED: () => SUBMIT_NONINCLUDED,
93224
93231
  SUBMIT_UPDATE: () => SUBMIT_UPDATE,
93225
93232
  ServiceCohortPhase: () => ServiceCohortPhase,
93226
93233
  SigningSessionError: () => SigningSessionError,
@@ -93229,6 +93236,7 @@ a=end-of-candidates
93229
93236
  SingletonBeacon: () => SingletonBeacon,
93230
93237
  SingletonBeaconError: () => SingletonBeaconError,
93231
93238
  StaticFeeEstimator: () => StaticFeeEstimator,
93239
+ TAPROOT_LEAF_VERSION: () => TAPROOT_LEAF_VERSION,
93232
93240
  TransportAdapterError: () => TransportAdapterError,
93233
93241
  TransportError: () => TransportError,
93234
93242
  TransportFactory: () => TransportFactory,
@@ -93236,6 +93244,11 @@ a=end-of-candidates
93236
93244
  Updater: () => Updater,
93237
93245
  VALIDATION_ACK: () => VALIDATION_ACK,
93238
93246
  buildAggregationBeaconTx: () => buildAggregationBeaconTx,
93247
+ buildFallbackLeaf: () => buildFallbackLeaf,
93248
+ buildFallbackSpend: () => buildFallbackSpend,
93249
+ buildRecoveryLeaves: () => buildRecoveryLeaves,
93250
+ buildRecoveryScript: () => buildRecoveryScript,
93251
+ buildRecoverySpend: () => buildRecoverySpend,
93239
93252
  buildRequestAuth: () => buildRequestAuth,
93240
93253
  createAggregatedNonceMessage: () => createAggregatedNonceMessage,
93241
93254
  createAuthorizationRequestMessage: () => createAuthorizationRequestMessage,
@@ -93244,13 +93257,17 @@ a=end-of-candidates
93244
93257
  createCohortOptInMessage: () => createCohortOptInMessage,
93245
93258
  createCohortReadyMessage: () => createCohortReadyMessage,
93246
93259
  createDistributeAggregatedDataMessage: () => createDistributeAggregatedDataMessage,
93260
+ createFallbackAuthorizationRequestMessage: () => createFallbackAuthorizationRequestMessage,
93261
+ createFallbackSignatureMessage: () => createFallbackSignatureMessage,
93247
93262
  createNonceContributionMessage: () => createNonceContributionMessage,
93248
93263
  createSignatureAuthorizationMessage: () => createSignatureAuthorizationMessage,
93264
+ createSubmitNonIncludedMessage: () => createSubmitNonIncludedMessage,
93249
93265
  createSubmitUpdateMessage: () => createSubmitUpdateMessage,
93250
93266
  createValidationAckMessage: () => createValidationAckMessage,
93251
93267
  defaultReconnectBackoff: () => defaultReconnectBackoff,
93252
93268
  deriveSingletonAddress: () => deriveSingletonAddress,
93253
93269
  detectSingletonScriptKind: () => detectSingletonScriptKind,
93270
+ fallbackSighash: () => fallbackSighash,
93254
93271
  formatSseComment: () => formatSseComment,
93255
93272
  formatSseEvent: () => formatSseEvent,
93256
93273
  getBeaconStrategy: () => getBeaconStrategy,
@@ -93262,10 +93279,13 @@ a=end-of-candidates
93262
93279
  isCohortOptInMessage: () => isCohortOptInMessage,
93263
93280
  isCohortReadyMessage: () => isCohortReadyMessage,
93264
93281
  isDistributeAggregatedDataMessage: () => isDistributeAggregatedDataMessage,
93282
+ isFallbackAuthorizationRequestMessage: () => isFallbackAuthorizationRequestMessage,
93283
+ isFallbackSignatureMessage: () => isFallbackSignatureMessage,
93265
93284
  isKeygenMessageType: () => isKeygenMessageType,
93266
93285
  isNonceContributionMessage: () => isNonceContributionMessage,
93267
93286
  isSignMessageType: () => isSignMessageType,
93268
93287
  isSignatureAuthorizationMessage: () => isSignatureAuthorizationMessage,
93288
+ isSubmitNonIncludedMessage: () => isSubmitNonIncludedMessage,
93269
93289
  isSubmitUpdateMessage: () => isSubmitUpdateMessage,
93270
93290
  isUpdateMessageType: () => isUpdateMessageType,
93271
93291
  isValidationAckMessage: () => isValidationAckMessage,
@@ -93274,8 +93294,10 @@ a=end-of-candidates
93274
93294
  parseRequestAuth: () => parseRequestAuth,
93275
93295
  parseSseStream: () => parseSseStream,
93276
93296
  registerBeaconStrategy: () => registerBeaconStrategy,
93297
+ resolveFallbackThreshold: () => resolveFallbackThreshold,
93277
93298
  reviveFromWire: () => reviveFromWire,
93278
93299
  signEnvelope: () => signEnvelope,
93300
+ tapLeafHash: () => tapLeafHash2,
93279
93301
  validateCohortConditions: () => validateCohortConditions,
93280
93302
  verifyEnvelope: () => verifyEnvelope,
93281
93303
  verifyRequestAuth: () => verifyRequestAuth
@@ -100261,7 +100283,7 @@ a=end-of-candidates
100261
100283
  * @readonly
100262
100284
  * Get signing ability of the Multikey: true if a local Secp256k1SecretKey
100263
100285
  * is present (note: the SchnorrKeyPair.secretKey getter throws when absent;
100264
- * that throw is the historical error contract see multikey tests) or if
100286
+ * that throw is the historical error contract - see multikey tests) or if
100265
100287
  * an external signer is available.
100266
100288
  */
100267
100289
  get signer() {
@@ -100366,6 +100388,7 @@ a=end-of-candidates
100366
100388
  init_shim();
100367
100389
 
100368
100390
  // src/core/aggregation/service.ts
100391
+ init_secp256k1();
100369
100392
  init_utils();
100370
100393
 
100371
100394
  // src/core/aggregation/beacon-strategy.ts
@@ -100666,9 +100689,12 @@ a=end-of-candidates
100666
100689
  getDistributePayload(cohort) {
100667
100690
  return { casAnnouncement: cohort.casAnnouncement };
100668
100691
  },
100669
- validateParticipantView({ participantDid, expectedHash, body }) {
100692
+ validateParticipantView({ participantDid, included, expectedHash, body }) {
100670
100693
  const casAnnouncement = body.casAnnouncement;
100671
100694
  if (!casAnnouncement) return { matches: false };
100695
+ if (!included) {
100696
+ return { matches: !(participantDid in casAnnouncement), casAnnouncement };
100697
+ }
100672
100698
  return {
100673
100699
  matches: casAnnouncement[participantDid] === expectedHash,
100674
100700
  casAnnouncement
@@ -100684,15 +100710,20 @@ a=end-of-candidates
100684
100710
  const proof = cohort.smtProofs?.get(participantDid);
100685
100711
  return { smtProof: proof };
100686
100712
  },
100687
- validateParticipantView({ participantDid, submittedUpdate, body }) {
100713
+ validateParticipantView({ participantDid, included, submittedUpdate, body }) {
100688
100714
  const smtProof = body.smtProof;
100689
- if (!smtProof?.updateId || !smtProof?.nonce) return { matches: false };
100715
+ const index = didToIndex(participantDid);
100716
+ if (!included) {
100717
+ if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
100718
+ const candidateHash2 = blockHash(blockHash(base64UrlToHash(smtProof.nonce)));
100719
+ return { matches: verifySerializedProof(smtProof, index, candidateHash2), smtProof };
100720
+ }
100721
+ if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
100690
100722
  const canonicalBytes = new TextEncoder().encode(canonicalize2(submittedUpdate));
100691
100723
  const expectedUpdateId = hashToBase64Url(blockHash(canonicalBytes));
100692
100724
  if (smtProof.updateId !== expectedUpdateId) {
100693
100725
  return { matches: false, smtProof };
100694
100726
  }
100695
- const index = didToIndex(participantDid);
100696
100727
  const candidateHash = blockHash(blockHash(base64UrlToHash(smtProof.nonce)), base64UrlToHash(smtProof.updateId));
100697
100728
  return {
100698
100729
  matches: verifySerializedProof(smtProof, index, candidateHash),
@@ -101691,6 +101722,15 @@ a=end-of-candidates
101691
101722
  checkWSH(r2, witnessScript);
101692
101723
  }
101693
101724
  }
101725
+ function uniqPubkey(pubkeys) {
101726
+ const map3 = {};
101727
+ for (const pub of pubkeys) {
101728
+ const key = hex.encode(pub);
101729
+ if (map3[key])
101730
+ throw new Error(`Multisig: non-uniq pubkey: ${pubkeys.map(hex.encode)}`);
101731
+ map3[key] = true;
101732
+ }
101733
+ }
101694
101734
  var p2pkh = (publicKey2, network = NETWORK) => {
101695
101735
  if (!isValidPubkey(publicKey2, PubT.ecdsa))
101696
101736
  throw new Error("P2PKH: invalid publicKey");
@@ -101863,6 +101903,14 @@ a=end-of-candidates
101863
101903
  };
101864
101904
  }
101865
101905
  }
101906
+ function p2tr_ms(m2, pubkeys, allowSamePubkeys = false) {
101907
+ if (!allowSamePubkeys)
101908
+ uniqPubkey(pubkeys);
101909
+ return {
101910
+ type: "tr_ms",
101911
+ script: OutScript.encode({ type: "tr_ms", pubkeys, m: m2 })
101912
+ };
101913
+ }
101866
101914
  var base58check = createBase58check(sha256);
101867
101915
  function validateWitness(version3, data) {
101868
101916
  if (data.length < 2 || data.length > 40)
@@ -103338,6 +103386,104 @@ a=end-of-candidates
103338
103386
  }
103339
103387
  };
103340
103388
 
103389
+ // src/core/aggregation/recovery-policy.ts
103390
+ init_shim();
103391
+ init_secp256k1();
103392
+ init_utils();
103393
+ var DEFAULT_FUNDING_MODEL = "operator-funded";
103394
+ var DEFAULT_RECOVERY_SEQUENCE = 144;
103395
+ var MAX_RECOVERY_SEQUENCE = 65535;
103396
+ var TAPROOT_LEAF_VERSION = 192;
103397
+ function compactSize(n2) {
103398
+ if (n2 < 253) return new Uint8Array([n2]);
103399
+ if (n2 <= 65535) return new Uint8Array([253, n2 & 255, n2 >> 8 & 255]);
103400
+ return new Uint8Array([254, n2 & 255, n2 >> 8 & 255, n2 >> 16 & 255, n2 >> 24 & 255]);
103401
+ }
103402
+ function tapLeafHash2(script, leafVersion = TAPROOT_LEAF_VERSION) {
103403
+ return schnorr2.utils.taggedHash(
103404
+ "TapLeaf",
103405
+ concatBytes(new Uint8Array([leafVersion]), compactSize(script.length), script)
103406
+ );
103407
+ }
103408
+ function resolveFallbackThreshold(advertised, n2) {
103409
+ return advertised ?? Math.max(1, n2 - 1);
103410
+ }
103411
+ function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
103412
+ const n2 = cohortKeys.length;
103413
+ if (n2 === 0) {
103414
+ throw new AggregationCohortError(
103415
+ "Cannot build fallback leaf: no cohort keys.",
103416
+ "NO_COHORT_KEYS"
103417
+ );
103418
+ }
103419
+ for (const key of cohortKeys) {
103420
+ if (key.length !== 33) {
103421
+ throw new AggregationCohortError(
103422
+ `Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
103423
+ "INVALID_COHORT_KEY",
103424
+ { length: key.length }
103425
+ );
103426
+ }
103427
+ }
103428
+ if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n2) {
103429
+ throw new AggregationCohortError(
103430
+ `Fallback threshold must be an integer in [1, ${n2}] (k-of-n), got ${fallbackThreshold}.`,
103431
+ "INVALID_FALLBACK_THRESHOLD",
103432
+ { fallbackThreshold, n: n2 }
103433
+ );
103434
+ }
103435
+ }
103436
+ function buildFallbackLeaf(params) {
103437
+ assertFallbackParams(params);
103438
+ const xOnlyKeys = sortKeys(params.cohortKeys).map((k) => k.slice(1));
103439
+ return p2tr_ms(params.fallbackThreshold, xOnlyKeys).script;
103440
+ }
103441
+ function assertRecoveryParams({ recoveryKey, recoverySequence }) {
103442
+ if (recoveryKey.length !== 32) {
103443
+ throw new AggregationCohortError(
103444
+ `Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
103445
+ "INVALID_RECOVERY_KEY"
103446
+ );
103447
+ }
103448
+ if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
103449
+ throw new AggregationCohortError(
103450
+ `Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
103451
+ "INVALID_RECOVERY_SEQUENCE"
103452
+ );
103453
+ }
103454
+ }
103455
+ function buildRecoveryScript(params) {
103456
+ assertRecoveryParams(params);
103457
+ return Script.encode([
103458
+ params.recoverySequence,
103459
+ "CHECKSEQUENCEVERIFY",
103460
+ "DROP",
103461
+ params.recoveryKey,
103462
+ "CHECKSIG"
103463
+ ]);
103464
+ }
103465
+ function buildRecoveryLeaves(fundingModel, params) {
103466
+ switch (fundingModel) {
103467
+ case "operator-funded":
103468
+ return [
103469
+ { script: buildFallbackLeaf(params) },
103470
+ { script: buildRecoveryScript(params) }
103471
+ ];
103472
+ case "participant-funded":
103473
+ throw new AggregationCohortError(
103474
+ "Funding model 'participant-funded' is reserved and not yet implemented.",
103475
+ "UNSUPPORTED_FUNDING_MODEL",
103476
+ { fundingModel }
103477
+ );
103478
+ default:
103479
+ throw new AggregationCohortError(
103480
+ `Unknown funding model: ${fundingModel}.`,
103481
+ "UNKNOWN_FUNDING_MODEL",
103482
+ { fundingModel }
103483
+ );
103484
+ }
103485
+ }
103486
+
103341
103487
  // src/core/aggregation/cohort.ts
103342
103488
  var AggregationCohort = class {
103343
103489
  /** Unique identifier for the cohort. */
@@ -103353,8 +103499,8 @@ a=end-of-candidates
103353
103499
  /** List of participant DIDs that have been accepted into the cohort. */
103354
103500
  participants = [];
103355
103501
  /**
103356
- * Mapping from participant DID their compressed secp256k1 public key.
103357
- * Distinct from {@link cohortKeys} (which is sorted per BIP-327) this lets
103502
+ * Mapping from participant DID to their compressed secp256k1 public key.
103503
+ * Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
103358
103504
  * callers look up a participant's key without knowing their position in the
103359
103505
  * sorted array. Populated by the service at `acceptParticipant` time.
103360
103506
  */
@@ -103362,16 +103508,41 @@ a=end-of-candidates
103362
103508
  /** Sorted list of cohort participants' compressed public keys. */
103363
103509
  #cohortKeys = [];
103364
103510
  /**
103365
- * BIP-341 TapTweak `taggedHash("TapTweak", internalPubkey)` for a key-path-only
103366
- * Taproot output. Despite prior naming, this is NOT a Merkle root: key-path-only
103367
- * spends have no script tree.
103511
+ * BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
103512
+ * The beacon output is an internal key (the MuSig2 aggregate) plus a script
103513
+ * tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
103514
+ * The MuSig2 signing session applies this as an x-only tweak; a value that does
103515
+ * not match the root the funded address was derived from silently yields an
103516
+ * invalid key-path signature.
103368
103517
  */
103369
103518
  tapTweak = new Uint8Array();
103370
- /** The n-of-n MuSig2 Taproot beacon address. */
103519
+ /** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
103520
+ internalKey = new Uint8Array();
103521
+ /** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
103522
+ tapMerkleRoot = new Uint8Array();
103523
+ /** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
103524
+ recoveryKey;
103525
+ /** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
103526
+ recoverySequence;
103527
+ /** Funding model governing the recovery leaves (default 'operator-funded'). */
103528
+ fundingModel;
103529
+ /**
103530
+ * Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
103531
+ * address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
103532
+ */
103533
+ fallbackThreshold;
103534
+ /** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
103371
103535
  beaconAddress = "";
103372
103536
  /** Pending DID updates submitted by participants, keyed by DID. */
103373
103537
  pendingUpdates = /* @__PURE__ */ new Map();
103374
- /** CAS Beacon Announcement Map (DID → updateHash), set by buildCASAnnouncement(). */
103538
+ /**
103539
+ * Participant DIDs that explicitly declined to submit an update this round
103540
+ * (cooperative non-inclusion). A decliner is absent from the CAS Announcement
103541
+ * Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
103542
+ * disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
103543
+ */
103544
+ nonIncluded = /* @__PURE__ */ new Set();
103545
+ /** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
103375
103546
  casAnnouncement;
103376
103547
  /** Per-participant SMT proofs, set by buildSMTTree(). */
103377
103548
  smtProofs;
@@ -103381,12 +103552,16 @@ a=end-of-candidates
103381
103552
  validationAcks = /* @__PURE__ */ new Set();
103382
103553
  /** Set of participant DIDs that have rejected the aggregated data. */
103383
103554
  validationRejections = /* @__PURE__ */ new Set();
103384
- constructor({ id, minParticipants, serviceDid, network, beaconType }) {
103555
+ constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
103385
103556
  this.id = id || crypto.randomUUID();
103386
103557
  this.minParticipants = minParticipants ?? 2;
103387
103558
  this.serviceDid = serviceDid || "";
103388
103559
  this.network = network;
103389
103560
  this.beaconType = beaconType || "CASBeacon";
103561
+ this.recoveryKey = recoveryKey;
103562
+ this.recoverySequence = recoverySequence;
103563
+ this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
103564
+ this.fallbackThreshold = fallbackThreshold;
103390
103565
  }
103391
103566
  /** Sorted cohort keys (sorted on assignment per BIP-327). */
103392
103567
  get cohortKeys() {
@@ -103396,8 +103571,26 @@ a=end-of-candidates
103396
103571
  this.#cohortKeys = sortKeys(keys);
103397
103572
  }
103398
103573
  /**
103399
- * Computes the n-of-n MuSig2 Taproot beacon address from cohort keys.
103400
- * Sets `tapTweak` to the BIP-341 key-path-only tweak.
103574
+ * The resolved k of the k-of-n fallback leaf: the advertised
103575
+ * {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
103576
+ * the current cohort size. This is the value the beacon address commits to and
103577
+ * the spend builders must reproduce. Returns 0 before any cohort keys are set.
103578
+ */
103579
+ get effectiveFallbackThreshold() {
103580
+ if (this.#cohortKeys.length === 0) return 0;
103581
+ return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
103582
+ }
103583
+ /**
103584
+ * Computes the Taproot beacon address from the cohort keys and recovery params.
103585
+ *
103586
+ * The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
103587
+ * script path is the recovery tree (a CSV recovery leaf so a missing signer
103588
+ * cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
103589
+ * the `tapTweak` the MuSig2 session must apply for the key-path spend.
103590
+ *
103591
+ * The tweak is derived from the Merkle root the address was built with (read
103592
+ * back from the payment), never recomputed by hand, so the MuSig2 key-path
103593
+ * signature is guaranteed to validate against the funded address.
103401
103594
  */
103402
103595
  computeBeaconAddress() {
103403
103596
  if (this.#cohortKeys.length === 0) {
@@ -103407,10 +103600,25 @@ a=end-of-candidates
103407
103600
  { cohortId: this.id }
103408
103601
  );
103409
103602
  }
103603
+ if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
103604
+ throw new AggregationCohortError(
103605
+ "Cannot compute beacon address: missing recovery key or sequence.",
103606
+ "NO_RECOVERY_PARAMS",
103607
+ { cohortId: this.id }
103608
+ );
103609
+ }
103410
103610
  const keyAggContext = keyAggregate(this.#cohortKeys);
103411
103611
  const aggPubkey = keyAggExport(keyAggContext);
103412
- const payment = p2tr(aggPubkey, void 0, getNetwork(this.network));
103413
- this.tapTweak = schnorr2.utils.taggedHash("TapTweak", aggPubkey);
103612
+ const leaves = buildRecoveryLeaves(this.fundingModel, {
103613
+ recoveryKey: this.recoveryKey,
103614
+ recoverySequence: this.recoverySequence,
103615
+ cohortKeys: this.#cohortKeys,
103616
+ fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
103617
+ });
103618
+ const payment = p2tr(aggPubkey, leaves, getNetwork(this.network), true);
103619
+ this.internalKey = aggPubkey;
103620
+ this.tapMerkleRoot = payment.tapMerkleRoot;
103621
+ this.tapTweak = schnorr2.utils.taggedHash("TapTweak", concatBytes(aggPubkey, payment.tapMerkleRoot));
103414
103622
  if (!payment.address) {
103415
103623
  throw new AggregationCohortError(
103416
103624
  "Failed to compute Taproot address",
@@ -103466,20 +103674,54 @@ a=end-of-candidates
103466
103674
  }
103467
103675
  this.pendingUpdates.set(participantDid, signedUpdate);
103468
103676
  }
103677
+ /**
103678
+ * Record that a participant declined to submit an update this round
103679
+ * (cooperative non-inclusion). The member stays in the cohort and still signs.
103680
+ */
103681
+ addNonInclusion(participantDid) {
103682
+ if (!this.participants.includes(participantDid)) {
103683
+ throw new AggregationCohortError(
103684
+ `Participant ${participantDid} is not in cohort ${this.id}.`,
103685
+ "UNKNOWN_PARTICIPANT",
103686
+ { cohortId: this.id, participantDid }
103687
+ );
103688
+ }
103689
+ if (this.pendingUpdates.has(participantDid)) {
103690
+ throw new AggregationCohortError(
103691
+ `Participant ${participantDid} already submitted an update; cannot also decline.`,
103692
+ "CONFLICTING_RESPONSE",
103693
+ { cohortId: this.id, participantDid }
103694
+ );
103695
+ }
103696
+ this.nonIncluded.add(participantDid);
103697
+ }
103469
103698
  hasAllUpdates() {
103470
103699
  return this.pendingUpdates.size === this.participants.length;
103471
103700
  }
103701
+ /**
103702
+ * True when every participant has responded for this round, either with an
103703
+ * update or with an explicit non-inclusion. This is the aggregation gate when
103704
+ * non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
103705
+ * {@link hasAllValidationResponses} generalizes a unanimous ack.
103706
+ */
103707
+ hasAllResponses() {
103708
+ return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
103709
+ }
103472
103710
  /**
103473
103711
  * Builds a CAS Announcement Map from collected updates.
103474
- * Maps each participant DID base64url canonical hash of their signed update.
103712
+ * Maps each participant DID to base64url canonical hash of their signed update.
103475
103713
  * Computes signal bytes as SHA-256 of canonicalized announcement.
103714
+ *
103715
+ * Members who declined (cooperative non-inclusion) are naturally absent from
103716
+ * the map: the body iterates {@link pendingUpdates}, which never holds a
103717
+ * decliner. Absence from the map is exactly the CAS non-inclusion signal.
103476
103718
  */
103477
103719
  buildCASAnnouncement() {
103478
- if (!this.hasAllUpdates()) {
103720
+ if (!this.hasAllResponses()) {
103479
103721
  throw new AggregationCohortError(
103480
- "Cannot build CAS Announcement: not all updates collected.",
103481
- "INCOMPLETE_UPDATES",
103482
- { cohortId: this.id, collected: this.pendingUpdates.size, total: this.participants.length }
103722
+ "Cannot build CAS Announcement: not all participants have responded.",
103723
+ "INCOMPLETE_RESPONSES",
103724
+ { cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
103483
103725
  );
103484
103726
  }
103485
103727
  const announcement = {};
@@ -103491,31 +103733,41 @@ a=end-of-candidates
103491
103733
  return announcement;
103492
103734
  }
103493
103735
  /**
103494
- * Builds an SMT tree from collected updates.
103495
- * Each entry uses a random 32-byte nonce + canonicalized signed update bytes.
103496
- * Stores per-participant proofs and the SMT root as signalBytes.
103736
+ * Builds an SMT tree with one leaf per participant.
103737
+ *
103738
+ * A member who submitted an update gets an inclusion leaf
103739
+ * (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
103740
+ * non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
103741
+ * omitted). The cohort mints each member's nonce and returns it inside that
103742
+ * member's serialized proof, so a decliner can self-validate its own
103743
+ * non-inclusion slot and the resolver can recompute the leaf. Stores
103744
+ * per-participant proofs and the SMT root as signalBytes.
103497
103745
  */
103498
103746
  buildSMTTree() {
103499
- if (!this.hasAllUpdates()) {
103747
+ if (!this.hasAllResponses()) {
103500
103748
  throw new AggregationCohortError(
103501
- "Cannot build SMT tree: not all updates collected.",
103502
- "INCOMPLETE_UPDATES",
103503
- { cohortId: this.id }
103749
+ "Cannot build SMT tree: not all participants have responded.",
103750
+ "INCOMPLETE_RESPONSES",
103751
+ { cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
103504
103752
  );
103505
103753
  }
103506
103754
  const tree2 = new BTCR2MerkleTree();
103507
103755
  const entries = [];
103508
103756
  const encoder4 = new TextEncoder();
103509
- for (const [did, signedUpdate] of this.pendingUpdates) {
103510
- const canonicalBytes = encoder4.encode(canonicalize2(signedUpdate));
103757
+ for (const did of this.participants) {
103511
103758
  const nonce = randomBytes(32);
103512
- entries.push({ did, nonce, signedUpdate: canonicalBytes });
103759
+ const signedUpdate = this.pendingUpdates.get(did);
103760
+ if (signedUpdate) {
103761
+ entries.push({ did, nonce, signedUpdate: encoder4.encode(canonicalize2(signedUpdate)) });
103762
+ } else {
103763
+ entries.push({ did, nonce });
103764
+ }
103513
103765
  }
103514
103766
  tree2.addEntries(entries);
103515
103767
  tree2.finalize();
103516
103768
  this.signalBytes = tree2.rootHash;
103517
103769
  this.smtProofs = /* @__PURE__ */ new Map();
103518
- for (const [did] of this.pendingUpdates) {
103770
+ for (const did of this.participants) {
103519
103771
  this.smtProofs.set(did, tree2.proof(did));
103520
103772
  }
103521
103773
  return this.smtProofs;
@@ -103541,7 +103793,7 @@ a=end-of-candidates
103541
103793
  return this.validationAcks.size + this.validationRejections.size === this.participants.length;
103542
103794
  }
103543
103795
  /**
103544
- * True when all participants approved. Note: differs from {@link hasAllValidationResponses}
103796
+ * True when all participants approved. Note: differs from {@link hasAllValidationResponses},
103545
103797
  * this returns false if any participant rejected, even if all responses are in.
103546
103798
  */
103547
103799
  isFullyValidated() {
@@ -103552,6 +103804,7 @@ a=end-of-candidates
103552
103804
  // src/core/aggregation/conditions.ts
103553
103805
  init_shim();
103554
103806
  var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
103807
+ var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
103555
103808
  function checkPair(problems, label, min, max) {
103556
103809
  if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
103557
103810
  problems.push(`min${label} must be an integer >= 0`);
@@ -103595,11 +103848,128 @@ a=end-of-candidates
103595
103848
  if (c2.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c2.pendingUpdateTrigger) || c2.pendingUpdateTrigger < 1)) {
103596
103849
  problems.push("pendingUpdateTrigger must be an integer >= 1");
103597
103850
  }
103851
+ if (c2.fallbackThreshold !== void 0) {
103852
+ if (!Number.isInteger(c2.fallbackThreshold) || c2.fallbackThreshold < 1) {
103853
+ problems.push("fallbackThreshold must be an integer >= 1");
103854
+ } else if (c2.maxParticipants !== void 0 && Number.isInteger(c2.maxParticipants) && c2.fallbackThreshold > c2.maxParticipants) {
103855
+ problems.push("fallbackThreshold must be <= maxParticipants");
103856
+ }
103857
+ }
103598
103858
  checkCost(problems, "costOfEnrollment", c2.costOfEnrollment);
103599
103859
  checkCost(problems, "costPerAnnouncement", c2.costPerAnnouncement);
103860
+ if (typeof c2.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c2.recoveryKey)) {
103861
+ problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
103862
+ }
103863
+ if (!Number.isInteger(c2.recoverySequence) || c2.recoverySequence < 1 || c2.recoverySequence > MAX_RECOVERY_SEQUENCE) {
103864
+ problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
103865
+ }
103866
+ if (c2.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c2.fundingModel)) {
103867
+ problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
103868
+ }
103600
103869
  return problems;
103601
103870
  }
103602
103871
 
103872
+ // src/core/aggregation/fallback-spend.ts
103873
+ init_shim();
103874
+ init_secp256k1();
103875
+ function bytesEqual(a2, b) {
103876
+ return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
103877
+ }
103878
+ function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
103879
+ return tx.preimageWitnessV1(
103880
+ inputIndex,
103881
+ [prevOutScript],
103882
+ SigHash.DEFAULT,
103883
+ [prevOutValue],
103884
+ void 0,
103885
+ fallbackLeafScript,
103886
+ TAPROOT_LEAF_VERSION
103887
+ );
103888
+ }
103889
+ function buildFallbackSpend(params) {
103890
+ const {
103891
+ pendingTx,
103892
+ cohortKeys,
103893
+ recoveryKey,
103894
+ recoverySequence,
103895
+ fundingModel,
103896
+ network,
103897
+ prevOutScript,
103898
+ prevOutValue,
103899
+ signatures
103900
+ } = params;
103901
+ const inputIndex = params.inputIndex ?? 0;
103902
+ if (cohortKeys.length === 0) {
103903
+ throw new AggregationCohortError(
103904
+ "Cannot build fallback spend: no cohort keys.",
103905
+ "NO_COHORT_KEYS"
103906
+ );
103907
+ }
103908
+ const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
103909
+ const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
103910
+ const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
103911
+ recoveryKey,
103912
+ recoverySequence,
103913
+ cohortKeys,
103914
+ fallbackThreshold
103915
+ });
103916
+ const net = getNetwork(network);
103917
+ const payment = p2tr(internalKey, leaves, net, true);
103918
+ if (!bytesEqual(payment.script, prevOutScript)) {
103919
+ throw new AggregationCohortError(
103920
+ "Reconstructed beacon output script does not match the spent prevout script.",
103921
+ "PREVOUT_SCRIPT_MISMATCH"
103922
+ );
103923
+ }
103924
+ const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
103925
+ const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
103926
+ const script = scriptVer.slice(0, scriptVer.length - 1);
103927
+ return bytesEqual(script, fallbackLeaf);
103928
+ });
103929
+ if (!leafEntry) {
103930
+ throw new AggregationCohortError(
103931
+ "Could not locate the fallback leaf in the reconstructed beacon output.",
103932
+ "FALLBACK_LEAF_NOT_FOUND"
103933
+ );
103934
+ }
103935
+ const cohortXOnly = sortKeys(cohortKeys).map((k) => k.slice(1));
103936
+ const tx = pendingTx;
103937
+ const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
103938
+ const leafHash = tapLeafHash2(fallbackLeaf);
103939
+ const accepted = /* @__PURE__ */ new Map();
103940
+ for (const { pubKey, signature } of signatures) {
103941
+ if (pubKey.length !== 32 || signature.length !== 64) continue;
103942
+ const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
103943
+ if (!isCohortKey) continue;
103944
+ const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
103945
+ if (accepted.has(hex2)) continue;
103946
+ let ok = false;
103947
+ try {
103948
+ ok = schnorr2.verify(signature, sighash, pubKey);
103949
+ } catch {
103950
+ ok = false;
103951
+ }
103952
+ if (ok) accepted.set(hex2, signature);
103953
+ }
103954
+ if (accepted.size < fallbackThreshold) {
103955
+ throw new AggregationCohortError(
103956
+ `Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
103957
+ "NOT_ENOUGH_FALLBACK_SIGNATURES",
103958
+ { have: accepted.size, need: fallbackThreshold }
103959
+ );
103960
+ }
103961
+ const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
103962
+ tx.updateInput(inputIndex, {
103963
+ tapLeafScript: [leafEntry],
103964
+ tapScriptSig: chosen.map((pubKey) => {
103965
+ const hex2 = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
103966
+ return [{ pubKey, leafHash }, accepted.get(hex2)];
103967
+ })
103968
+ });
103969
+ tx.finalize();
103970
+ return tx;
103971
+ }
103972
+
103603
103973
  // src/core/aggregation/messages/base.ts
103604
103974
  init_shim();
103605
103975
  var AGGREGATION_WIRE_VERSION = 1;
@@ -103639,12 +104009,15 @@ a=end-of-candidates
103639
104009
  var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
103640
104010
  var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
103641
104011
  var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
104012
+ var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
103642
104013
  var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
103643
104014
  var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
103644
104015
  var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
103645
104016
  var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
103646
104017
  var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
103647
104018
  var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
104019
+ var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
104020
+ var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
103648
104021
 
103649
104022
  // src/core/aggregation/messages/factories.ts
103650
104023
  init_shim();
@@ -103668,6 +104041,10 @@ a=end-of-candidates
103668
104041
  const { from: from8, to, ...body } = fields;
103669
104042
  return new BaseMessage({ type: SUBMIT_UPDATE, from: from8, to, body });
103670
104043
  }
104044
+ function createSubmitNonIncludedMessage(fields) {
104045
+ const { from: from8, to, ...body } = fields;
104046
+ return new BaseMessage({ type: SUBMIT_NONINCLUDED, from: from8, to, body });
104047
+ }
103671
104048
  function createDistributeAggregatedDataMessage(fields) {
103672
104049
  const { from: from8, to, ...body } = fields;
103673
104050
  return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from: from8, to, body });
@@ -103692,6 +104069,14 @@ a=end-of-candidates
103692
104069
  const { from: from8, to, ...body } = fields;
103693
104070
  return new BaseMessage({ type: SIGNATURE_AUTHORIZATION, from: from8, to, body });
103694
104071
  }
104072
+ function createFallbackAuthorizationRequestMessage(fields) {
104073
+ const { from: from8, to, ...body } = fields;
104074
+ return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from: from8, to, body });
104075
+ }
104076
+ function createFallbackSignatureMessage(fields) {
104077
+ const { from: from8, to, ...body } = fields;
104078
+ return new BaseMessage({ type: FALLBACK_SIGNATURE, from: from8, to, body });
104079
+ }
103695
104080
 
103696
104081
  // src/core/aggregation/phases.ts
103697
104082
  init_shim();
@@ -103706,6 +104091,7 @@ a=end-of-candidates
103706
104091
  ServiceCohortPhase2["SigningStarted"] = "SigningStarted";
103707
104092
  ServiceCohortPhase2["NoncesCollected"] = "NoncesCollected";
103708
104093
  ServiceCohortPhase2["AwaitingPartialSigs"] = "AwaitingPartialSigs";
104094
+ ServiceCohortPhase2["FallbackRequested"] = "FallbackRequested";
103709
104095
  ServiceCohortPhase2["Complete"] = "Complete";
103710
104096
  ServiceCohortPhase2["Failed"] = "Failed";
103711
104097
  return ServiceCohortPhase2;
@@ -103715,11 +104101,13 @@ a=end-of-candidates
103715
104101
  ParticipantCohortPhase2["OptedIn"] = "OptedIn";
103716
104102
  ParticipantCohortPhase2["CohortReady"] = "CohortReady";
103717
104103
  ParticipantCohortPhase2["UpdateSubmitted"] = "UpdateSubmitted";
104104
+ ParticipantCohortPhase2["NonIncluded"] = "NonIncluded";
103718
104105
  ParticipantCohortPhase2["AwaitingValidation"] = "AwaitingValidation";
103719
104106
  ParticipantCohortPhase2["ValidationSent"] = "ValidationSent";
103720
104107
  ParticipantCohortPhase2["AwaitingSigning"] = "AwaitingSigning";
103721
104108
  ParticipantCohortPhase2["NonceSent"] = "NonceSent";
103722
104109
  ParticipantCohortPhase2["AwaitingPartialSig"] = "AwaitingPartialSig";
104110
+ ParticipantCohortPhase2["AwaitingFallbackSig"] = "AwaitingFallbackSig";
103723
104111
  ParticipantCohortPhase2["Complete"] = "Complete";
103724
104112
  ParticipantCohortPhase2["Failed"] = "Failed";
103725
104113
  return ParticipantCohortPhase2;
@@ -103747,11 +104135,11 @@ a=end-of-candidates
103747
104135
  prevOutScripts;
103748
104136
  /** Previous output values for Taproot sighash computation. */
103749
104137
  prevOutValues;
103750
- /** Map of participant publicKey-hex public nonce contribution. */
104138
+ /** Map of participant publicKey-hex to public nonce contribution. */
103751
104139
  nonceContributions = /* @__PURE__ */ new Map();
103752
104140
  /** Aggregated MuSig2 nonce (66 bytes). */
103753
104141
  aggregatedNonce;
103754
- /** Map of participant DID partial signature. */
104142
+ /** Map of participant DID to partial signature. */
103755
104143
  partialSignatures = /* @__PURE__ */ new Map();
103756
104144
  /** Final 64-byte Schnorr signature. */
103757
104145
  signature;
@@ -103934,7 +104322,7 @@ a=end-of-candidates
103934
104322
  throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
103935
104323
  }
103936
104324
  if (!this.#secretNonce) {
103937
- throw new SigningSessionError("Secret nonce not available \u2014 generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
104325
+ throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
103938
104326
  }
103939
104327
  const session = new Session(
103940
104328
  this.aggregatedNonce,
@@ -104004,6 +104392,9 @@ a=end-of-candidates
104004
104392
  case SUBMIT_UPDATE:
104005
104393
  this.#handleSubmitUpdate(message2);
104006
104394
  break;
104395
+ case SUBMIT_NONINCLUDED:
104396
+ this.#handleSubmitNonInclusion(message2);
104397
+ break;
104007
104398
  case VALIDATION_ACK:
104008
104399
  this.#handleValidationAck(message2);
104009
104400
  break;
@@ -104013,6 +104404,9 @@ a=end-of-candidates
104013
104404
  case SIGNATURE_AUTHORIZATION:
104014
104405
  this.#handleSignatureAuthorization(message2);
104015
104406
  break;
104407
+ case FALLBACK_SIGNATURE:
104408
+ this.#handleFallbackSignature(message2);
104409
+ break;
104016
104410
  default:
104017
104411
  break;
104018
104412
  }
@@ -104031,7 +104425,7 @@ a=end-of-candidates
104031
104425
  }
104032
104426
  /**
104033
104427
  * Create a new cohort with the given config. Returns the cohort ID.
104034
- * Cohort starts in `Created` phase call `advertise()` to broadcast.
104428
+ * Cohort starts in `Created` phase - call `advertise()` to broadcast.
104035
104429
  */
104036
104430
  createCohort(config) {
104037
104431
  const problems = validateCohortConditions(config);
@@ -104046,7 +104440,11 @@ a=end-of-candidates
104046
104440
  serviceDid: this.did,
104047
104441
  minParticipants: config.minParticipants,
104048
104442
  network: config.network,
104049
- beaconType: config.beaconType
104443
+ beaconType: config.beaconType,
104444
+ recoveryKey: hexToBytes(config.recoveryKey),
104445
+ recoverySequence: config.recoverySequence,
104446
+ fundingModel: config.fundingModel,
104447
+ fallbackThreshold: config.fallbackThreshold
104050
104448
  });
104051
104449
  this.#cohortStates.set(cohort.id, {
104052
104450
  phase: "Created" /* Created */,
@@ -104247,11 +104645,64 @@ a=end-of-candidates
104247
104645
  });
104248
104646
  return;
104249
104647
  }
104648
+ if (state.cohort.nonIncluded.has(message2.from)) {
104649
+ state.rejections.push({
104650
+ from: message2.from,
104651
+ code: "UPDATE_MALFORMED",
104652
+ reason: "Participant already declined this round; cannot also submit an update"
104653
+ });
104654
+ return;
104655
+ }
104656
+ if (state.cohort.pendingUpdates.has(message2.from)) {
104657
+ state.rejections.push({
104658
+ from: message2.from,
104659
+ code: "UPDATE_MALFORMED",
104660
+ reason: "Participant already submitted an update this round; cannot resubmit"
104661
+ });
104662
+ return;
104663
+ }
104250
104664
  state.cohort.addUpdate(message2.from, signedUpdate);
104251
104665
  if (state.phase === "CohortSet" /* CohortSet */) {
104252
104666
  state.phase = "CollectingUpdates" /* CollectingUpdates */;
104253
104667
  }
104254
- if (state.cohort.hasAllUpdates()) {
104668
+ if (state.cohort.hasAllResponses()) {
104669
+ state.phase = "UpdatesCollected" /* UpdatesCollected */;
104670
+ }
104671
+ }
104672
+ /**
104673
+ * Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
104674
+ * update this round (cooperative non-inclusion). Membership is proven by the
104675
+ * signed transport envelope, so the body carries only the cohortId. The member
104676
+ * stays in the cohort and still signs; it is absent from the CAS map and
104677
+ * carries a non-inclusion leaf in the SMT.
104678
+ */
104679
+ #handleSubmitNonInclusion(message2) {
104680
+ const cohortId = message2.body?.cohortId;
104681
+ if (!cohortId) return;
104682
+ const state = this.#cohortStates.get(cohortId);
104683
+ if (!state) return;
104684
+ if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
104685
+ if (!state.cohort.participants.includes(message2.from)) {
104686
+ state.rejections.push({
104687
+ from: message2.from,
104688
+ code: "UPDATE_MALFORMED",
104689
+ reason: "Sender is not a member of this cohort"
104690
+ });
104691
+ return;
104692
+ }
104693
+ if (state.cohort.pendingUpdates.has(message2.from) || state.cohort.nonIncluded.has(message2.from)) {
104694
+ state.rejections.push({
104695
+ from: message2.from,
104696
+ code: "UPDATE_MALFORMED",
104697
+ reason: "Participant already responded this round"
104698
+ });
104699
+ return;
104700
+ }
104701
+ state.cohort.addNonInclusion(message2.from);
104702
+ if (state.phase === "CohortSet" /* CohortSet */) {
104703
+ state.phase = "CollectingUpdates" /* CollectingUpdates */;
104704
+ }
104705
+ if (state.cohort.hasAllResponses()) {
104255
104706
  state.phase = "UpdatesCollected" /* UpdatesCollected */;
104256
104707
  }
104257
104708
  }
@@ -104361,7 +104812,7 @@ a=end-of-candidates
104361
104812
  }
104362
104813
  /**
104363
104814
  * Start a signing session by creating auth requests for all participants.
104364
- * The caller provides the transaction data typically built via
104815
+ * The caller provides the transaction data - typically built via
104365
104816
  * `buildBeaconTransaction()` against a Bitcoin connection.
104366
104817
  */
104367
104818
  startSigning(cohortId, txData) {
@@ -104468,11 +104919,144 @@ a=end-of-candidates
104468
104919
  state.result = {
104469
104920
  cohortId,
104470
104921
  signature,
104471
- signedTx: state.signingSession.pendingTx
104922
+ signedTx: state.signingSession.pendingTx,
104923
+ path: "key-path"
104472
104924
  };
104473
104925
  state.phase = "Complete" /* Complete */;
104474
104926
  }
104475
104927
  }
104928
+ /**
104929
+ * Abandon the optimistic n-of-n key path and ask members to authorize the
104930
+ * k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
104931
+ * liveness, ADR 042). Reuses the in-flight signing session's transaction and
104932
+ * spent output, so the announcement and its outputs are unchanged: only the
104933
+ * witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
104934
+ * participant.
104935
+ *
104936
+ * Callable once optimistic signing has started (the session and its tx exist)
104937
+ * and before it completes. A cohort can take exactly one of the two paths: the
104938
+ * caller (runner) must commit to fallback and stop driving the optimistic path.
104939
+ */
104940
+ startFallbackSigning(cohortId) {
104941
+ const state = this.#cohortStates.get(cohortId);
104942
+ if (!state) {
104943
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
104944
+ }
104945
+ if (!state.signingSession) {
104946
+ throw new AggregationServiceError(
104947
+ `Cannot start fallback for cohort ${cohortId}: no signing session.`,
104948
+ "NO_SIGNING_SESSION",
104949
+ { cohortId }
104950
+ );
104951
+ }
104952
+ const signingPhases = [
104953
+ "SigningStarted" /* SigningStarted */,
104954
+ "NoncesCollected" /* NoncesCollected */,
104955
+ "AwaitingPartialSigs" /* AwaitingPartialSigs */
104956
+ ];
104957
+ if (!signingPhases.includes(state.phase)) {
104958
+ throw new AggregationServiceError(
104959
+ `Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
104960
+ "INVALID_PHASE",
104961
+ { cohortId, phase: state.phase }
104962
+ );
104963
+ }
104964
+ const session = state.signingSession;
104965
+ const prevOutScript = session.prevOutScripts[0];
104966
+ const prevOutValue = session.prevOutValues[0];
104967
+ if (!prevOutScript || prevOutValue === void 0) {
104968
+ throw new AggregationServiceError(
104969
+ `Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
104970
+ "MISSING_PREV_OUT",
104971
+ { cohortId }
104972
+ );
104973
+ }
104974
+ const fallbackLeaf = buildFallbackLeaf({
104975
+ cohortKeys: state.cohort.cohortKeys,
104976
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold
104977
+ });
104978
+ state.fallbackSignatures = /* @__PURE__ */ new Map();
104979
+ state.phase = "FallbackRequested" /* FallbackRequested */;
104980
+ const messages2 = [];
104981
+ for (const participantDid of state.cohort.participants) {
104982
+ messages2.push(createFallbackAuthorizationRequestMessage({
104983
+ from: this.did,
104984
+ to: participantDid,
104985
+ cohortId,
104986
+ sessionId: session.id,
104987
+ pendingTx: session.pendingTx.hex,
104988
+ prevOutScriptHex: bytesToHex(prevOutScript),
104989
+ prevOutValue: prevOutValue.toString(),
104990
+ fallbackLeafScriptHex: bytesToHex(fallbackLeaf)
104991
+ }));
104992
+ }
104993
+ return messages2;
104994
+ }
104995
+ /**
104996
+ * Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
104997
+ * signature over the fallback script-path sighash. The signature is
104998
+ * authenticated to the sender (its `signerPk` must be the sender's own cohort
104999
+ * key and the signature must verify against the sighash) and collected. Once k
105000
+ * valid signatures are in, the k-of-n fallback spend is assembled and the
105001
+ * cohort completes via the script path.
105002
+ */
105003
+ #handleFallbackSignature(message2) {
105004
+ const cohortId = message2.body?.cohortId;
105005
+ if (!cohortId) return;
105006
+ const state = this.#cohortStates.get(cohortId);
105007
+ if (!state || !state.signingSession || !state.fallbackSignatures) return;
105008
+ if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
105009
+ const sessionId = message2.body?.sessionId;
105010
+ if (sessionId !== state.signingSession.id) return;
105011
+ const signerPk = message2.body?.signerPk;
105012
+ const fallbackSignature = message2.body?.fallbackSignature;
105013
+ if (!signerPk || !fallbackSignature) return;
105014
+ const prevOutScript = state.signingSession.prevOutScripts[0];
105015
+ const prevOutValue = state.signingSession.prevOutValues[0];
105016
+ if (!prevOutScript || prevOutValue === void 0) return;
105017
+ const memberKey = state.cohort.participantKeys.get(message2.from);
105018
+ if (!memberKey) {
105019
+ state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
105020
+ return;
105021
+ }
105022
+ const memberXOnly = memberKey.slice(1);
105023
+ if (signerPk.length !== 32 || !memberXOnly.every((b, i4) => b === signerPk[i4])) {
105024
+ state.rejections.push({ from: message2.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
105025
+ return;
105026
+ }
105027
+ const fallbackLeaf = buildFallbackLeaf({
105028
+ cohortKeys: state.cohort.cohortKeys,
105029
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold
105030
+ });
105031
+ const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
105032
+ let valid = false;
105033
+ try {
105034
+ valid = fallbackSignature.length === 64 && schnorr2.verify(fallbackSignature, sighash, signerPk);
105035
+ } catch {
105036
+ valid = false;
105037
+ }
105038
+ if (!valid) {
105039
+ state.rejections.push({ from: message2.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
105040
+ return;
105041
+ }
105042
+ state.fallbackSignatures.set(message2.from, { pubKey: signerPk, signature: fallbackSignature });
105043
+ if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
105044
+ const signedTx = buildFallbackSpend({
105045
+ pendingTx: state.signingSession.pendingTx,
105046
+ cohortKeys: state.cohort.cohortKeys,
105047
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold,
105048
+ recoveryKey: state.cohort.recoveryKey,
105049
+ recoverySequence: state.cohort.recoverySequence,
105050
+ fundingModel: state.cohort.fundingModel,
105051
+ network: state.cohort.network,
105052
+ prevOutScript,
105053
+ prevOutValue,
105054
+ signatures: [...state.fallbackSignatures.values()]
105055
+ });
105056
+ state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
105057
+ state.phase = "Complete" /* Complete */;
105058
+ }
105059
+ }
104476
105060
  getResult(cohortId) {
104477
105061
  return this.#cohortStates.get(cohortId)?.result;
104478
105062
  }
@@ -104504,6 +105088,7 @@ a=end-of-candidates
104504
105088
 
104505
105089
  // src/core/aggregation/participant.ts
104506
105090
  init_shim();
105091
+ init_secp256k1();
104507
105092
  init_utils();
104508
105093
 
104509
105094
  // src/core/aggregation/messages/bodies.ts
@@ -104517,6 +105102,14 @@ a=end-of-candidates
104517
105102
  const v = b ? b[k] : void 0;
104518
105103
  return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
104519
105104
  };
105105
+ var intInRange = (b, k, min, max) => {
105106
+ const v = b ? b[k] : void 0;
105107
+ return typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
105108
+ };
105109
+ var optStrOneOf = (b, k, allowed) => {
105110
+ const v = b ? b[k] : void 0;
105111
+ return v === void 0 || typeof v === "string" && allowed.includes(v);
105112
+ };
104520
105113
  var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
104521
105114
  var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
104522
105115
  var hasBytesArray = (b, k) => {
@@ -104524,7 +105117,7 @@ a=end-of-candidates
104524
105117
  return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
104525
105118
  };
104526
105119
  function isCohortAdvertMessage(m2) {
104527
- return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasBytes(m2.body, "communicationPk");
105120
+ return m2.type === COHORT_ADVERT && hasStr(m2.body, "cohortId") && hasIntMin(m2.body, "minParticipants", 1) && optIntMin(m2.body, "maxParticipants", 1) && hasStr(m2.body, "beaconType") && hasStr(m2.body, "network") && hasStr(m2.body, "recoveryKey") && intInRange(m2.body, "recoverySequence", 1, MAX_RECOVERY_SEQUENCE) && optStrOneOf(m2.body, "fundingModel", KNOWN_FUNDING_MODELS) && optIntMin(m2.body, "fallbackThreshold", 1) && hasBytes(m2.body, "communicationPk");
104528
105121
  }
104529
105122
  function isCohortOptInMessage(m2) {
104530
105123
  return m2.type === COHORT_OPT_IN && hasStr(m2.body, "cohortId") && hasBytes(m2.body, "participantPk") && hasBytes(m2.body, "communicationPk");
@@ -104538,6 +105131,9 @@ a=end-of-candidates
104538
105131
  function isSubmitUpdateMessage(m2) {
104539
105132
  return m2.type === SUBMIT_UPDATE && hasStr(m2.body, "cohortId") && !!m2.body && typeof m2.body.signedUpdate === "object";
104540
105133
  }
105134
+ function isSubmitNonIncludedMessage(m2) {
105135
+ return m2.type === SUBMIT_NONINCLUDED && hasStr(m2.body, "cohortId");
105136
+ }
104541
105137
  function isDistributeAggregatedDataMessage(m2) {
104542
105138
  return m2.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m2.body, "cohortId") && hasStr(m2.body, "beaconType") && hasStr(m2.body, "signalBytesHex");
104543
105139
  }
@@ -104556,8 +105152,38 @@ a=end-of-candidates
104556
105152
  function isSignatureAuthorizationMessage(m2) {
104557
105153
  return m2.type === SIGNATURE_AUTHORIZATION && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "partialSignature");
104558
105154
  }
105155
+ function isFallbackAuthorizationRequestMessage(m2) {
105156
+ return m2.type === FALLBACK_AUTHORIZATION_REQUEST && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasStr(m2.body, "pendingTx") && hasStr(m2.body, "prevOutScriptHex") && hasStr(m2.body, "prevOutValue") && hasStr(m2.body, "fallbackLeafScriptHex");
105157
+ }
105158
+ function isFallbackSignatureMessage(m2) {
105159
+ return m2.type === FALLBACK_SIGNATURE && hasStr(m2.body, "cohortId") && hasStr(m2.body, "sessionId") && hasBytes(m2.body, "signerPk") && hasBytes(m2.body, "fallbackSignature");
105160
+ }
104559
105161
 
104560
105162
  // src/core/aggregation/participant.ts
105163
+ function txEmbedsSignal(tx, signalHex) {
105164
+ let expected;
105165
+ try {
105166
+ expected = hexToBytes(signalHex);
105167
+ } catch {
105168
+ return false;
105169
+ }
105170
+ if (expected.length === 0) return false;
105171
+ for (let i4 = 0; i4 < tx.outputsLength; i4++) {
105172
+ const script = tx.getOutput(i4)?.script;
105173
+ if (!script) continue;
105174
+ let decoded;
105175
+ try {
105176
+ decoded = Script.decode(script);
105177
+ } catch {
105178
+ continue;
105179
+ }
105180
+ if (decoded.length === 2 && decoded[0] === "RETURN" && decoded[1] instanceof Uint8Array) {
105181
+ const payload = decoded[1];
105182
+ if (payload.length === expected.length && payload.every((b, j) => b === expected[j])) return true;
105183
+ }
105184
+ }
105185
+ return false;
105186
+ }
104561
105187
  var AggregationParticipant = class {
104562
105188
  did;
104563
105189
  /** MuSig2 signing capability. The raw secret never lives as a field here. */
@@ -104574,7 +105200,7 @@ a=end-of-candidates
104574
105200
  }
104575
105201
  /**
104576
105202
  * Process an incoming message. Updates internal state but never produces
104577
- * outgoing messages those come exclusively from action methods.
105203
+ * outgoing messages: those come exclusively from action methods.
104578
105204
  */
104579
105205
  receive(message2) {
104580
105206
  if (message2.version === void 0 || message2.version !== AGGREGATION_WIRE_VERSION) {
@@ -104600,6 +105226,9 @@ a=end-of-candidates
104600
105226
  case AGGREGATED_NONCE:
104601
105227
  this.#handleAggregatedNonce(message2);
104602
105228
  break;
105229
+ case FALLBACK_AUTHORIZATION_REQUEST:
105230
+ this.#handleFallbackAuthorizationRequest(message2);
105231
+ break;
104603
105232
  default:
104604
105233
  break;
104605
105234
  }
@@ -104645,12 +105274,17 @@ a=end-of-candidates
104645
105274
  { cohortId, phase: state?.phase }
104646
105275
  );
104647
105276
  }
105277
+ const advert = state.advert;
104648
105278
  const cohort = new AggregationCohort({
104649
105279
  id: cohortId,
104650
105280
  serviceDid: state.serviceDid,
104651
- minParticipants: state.advert.minParticipants,
104652
- network: state.advert.network,
104653
- beaconType: state.advert.beaconType
105281
+ minParticipants: advert.minParticipants,
105282
+ network: advert.network,
105283
+ beaconType: advert.beaconType,
105284
+ recoveryKey: advert.recoveryKey ? hexToBytes(advert.recoveryKey) : void 0,
105285
+ recoverySequence: advert.recoverySequence,
105286
+ fundingModel: advert.fundingModel,
105287
+ fallbackThreshold: advert.fallbackThreshold
104654
105288
  });
104655
105289
  state.cohort = cohort;
104656
105290
  state.phase = "OptedIn" /* OptedIn */;
@@ -104665,7 +105299,7 @@ a=end-of-candidates
104665
105299
  }
104666
105300
  #handleOptInAccept(message2) {
104667
105301
  }
104668
- /** Cohorts that have been finalized beacon address available. */
105302
+ /** Cohorts that have been finalized: beacon address available. */
104669
105303
  get joinedCohorts() {
104670
105304
  const map3 = /* @__PURE__ */ new Map();
104671
105305
  for (const [id, state] of this.#cohortStates) {
@@ -104708,6 +105342,7 @@ a=end-of-candidates
104708
105342
  );
104709
105343
  }
104710
105344
  state.submittedUpdate = signedUpdate;
105345
+ state.included = true;
104711
105346
  state.phase = "UpdateSubmitted" /* UpdateSubmitted */;
104712
105347
  const message2 = createSubmitUpdateMessage({
104713
105348
  from: this.did,
@@ -104717,6 +105352,30 @@ a=end-of-candidates
104717
105352
  });
104718
105353
  return [message2];
104719
105354
  }
105355
+ /**
105356
+ * User action: decline to submit an update this round (cooperative
105357
+ * non-inclusion). The member stays in the cohort and still signs; it will be
105358
+ * absent from the CAS Announcement Map, or carry a non-inclusion leaf in the
105359
+ * SMT. Returns the SUBMIT_NONINCLUDED message to send.
105360
+ */
105361
+ declineUpdate(cohortId) {
105362
+ const state = this.#cohortStates.get(cohortId);
105363
+ if (!state || state.phase !== "CohortReady" /* CohortReady */) {
105364
+ throw new AggregationParticipantError(
105365
+ `Cannot decline in cohort ${cohortId}: not in CohortReady phase.`,
105366
+ "INVALID_PHASE",
105367
+ { cohortId, phase: state?.phase }
105368
+ );
105369
+ }
105370
+ state.included = false;
105371
+ state.phase = "NonIncluded" /* NonIncluded */;
105372
+ const message2 = createSubmitNonIncludedMessage({
105373
+ from: this.did,
105374
+ to: state.serviceDid,
105375
+ cohortId
105376
+ });
105377
+ return [message2];
105378
+ }
104720
105379
  /** Aggregated data awaiting user validation. */
104721
105380
  get pendingValidations() {
104722
105381
  const map3 = /* @__PURE__ */ new Map();
@@ -104727,22 +105386,35 @@ a=end-of-candidates
104727
105386
  }
104728
105387
  return map3;
104729
105388
  }
105389
+ /**
105390
+ * The validated aggregated data retained for a cohort, regardless of phase.
105391
+ * Unlike {@link pendingValidations} (which lists only cohorts still awaiting
105392
+ * the validate decision), this returns the stored validation, including the
105393
+ * participant's sidecar (the CAS Announcement map or its SMT inclusion proof),
105394
+ * so it is still readable once the cohort reaches Complete. Returns
105395
+ * undefined before aggregated data has been received.
105396
+ */
105397
+ getValidation(cohortId) {
105398
+ return this.#cohortStates.get(cohortId)?.validation;
105399
+ }
104730
105400
  #handleDistributeAggregatedData(message2) {
104731
105401
  const cohortId = message2.body?.cohortId;
104732
105402
  if (!cohortId) return;
104733
105403
  const state = this.#cohortStates.get(cohortId);
104734
- if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */) return;
104735
- if (!state.submittedUpdate) return;
105404
+ if (!state || state.phase !== "UpdateSubmitted" /* UpdateSubmitted */ && state.phase !== "NonIncluded" /* NonIncluded */) return;
105405
+ const declined = state.included === false;
105406
+ if (!declined && !state.submittedUpdate) return;
104736
105407
  const beaconType = message2.body?.beaconType;
104737
105408
  if (!beaconType) return;
104738
105409
  const strategy = getBeaconStrategy(beaconType);
104739
105410
  if (!strategy) return;
104740
105411
  const signalBytesHex = message2.body?.signalBytesHex ?? "";
104741
- const expectedHash = canonicalHash(state.submittedUpdate);
105412
+ const expectedHash = declined ? "" : canonicalHash(state.submittedUpdate);
104742
105413
  const result = strategy.validateParticipantView({
104743
105414
  participantDid: this.did,
104744
- submittedUpdate: state.submittedUpdate,
104745
- expectedHash,
105415
+ included: !declined,
105416
+ submittedUpdate: declined ? void 0 : state.submittedUpdate,
105417
+ expectedHash: declined ? void 0 : expectedHash,
104746
105418
  body: message2.body
104747
105419
  });
104748
105420
  state.validation = {
@@ -104752,7 +105424,8 @@ a=end-of-candidates
104752
105424
  expectedHash,
104753
105425
  matches: result.matches,
104754
105426
  casAnnouncement: result.casAnnouncement,
104755
- smtProof: result.smtProof
105427
+ smtProof: result.smtProof,
105428
+ included: !declined
104756
105429
  };
104757
105430
  state.phase = "AwaitingValidation" /* AwaitingValidation */;
104758
105431
  }
@@ -104826,6 +105499,31 @@ a=end-of-candidates
104826
105499
  };
104827
105500
  state.phase = "AwaitingSigning" /* AwaitingSigning */;
104828
105501
  }
105502
+ /**
105503
+ * Bind a signing approval to the announcement the member validated: a beacon
105504
+ * transaction MUST carry an OP_RETURN with the exact 32-byte signal stored when
105505
+ * the aggregated data was distributed. Both the optimistic nonce approval and
105506
+ * the fallback approval sign with SIGHASH_DEFAULT (committing to every output)
105507
+ * while the coordinator drives output selection, so without this check a
105508
+ * coordinator could anchor a different signal under the member's signature.
105509
+ */
105510
+ #assertTxAnchorsValidatedSignal(cohortId, state, tx) {
105511
+ const signalHex = state.validation?.signalBytesHex;
105512
+ if (!signalHex) {
105513
+ throw new AggregationParticipantError(
105514
+ `Cohort ${cohortId} has no validated signal to bind the signature to.`,
105515
+ "MISSING_STATE",
105516
+ { cohortId }
105517
+ );
105518
+ }
105519
+ if (!txEmbedsSignal(tx, signalHex)) {
105520
+ throw new AggregationParticipantError(
105521
+ `Transaction for cohort ${cohortId} does not anchor the validated signal.`,
105522
+ "SIGNAL_MISMATCH",
105523
+ { cohortId }
105524
+ );
105525
+ }
105526
+ }
104829
105527
  /**
104830
105528
  * User action: approve signing and generate nonce contribution.
104831
105529
  */
@@ -104845,7 +105543,8 @@ a=end-of-candidates
104845
105543
  { cohortId }
104846
105544
  );
104847
105545
  }
104848
- const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
105546
+ const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex), { allowUnknownOutputs: true });
105547
+ this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
104849
105548
  const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
104850
105549
  const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
104851
105550
  const session = new BeaconSigningSession({
@@ -104881,7 +105580,7 @@ a=end-of-candidates
104881
105580
  }
104882
105581
  /**
104883
105582
  * User action: generate and return the partial signature.
104884
- * In most UIs this is automatic after AwaitingPartialSig but exposing it
105583
+ * In most UIs this is automatic after AwaitingPartialSig, but exposing it
104885
105584
  * as an explicit action lets the client UI confirm before signing if desired.
104886
105585
  */
104887
105586
  generatePartialSignature(cohortId) {
@@ -104913,6 +105612,83 @@ a=end-of-candidates
104913
105612
  partialSignature: partialSig
104914
105613
  })];
104915
105614
  }
105615
+ /** Fallback signing requests awaiting user approval (UI: review the fallback spend). */
105616
+ get pendingFallbackRequests() {
105617
+ const map3 = /* @__PURE__ */ new Map();
105618
+ for (const [id, state] of this.#cohortStates) {
105619
+ if (state.phase === "AwaitingFallbackSig" /* AwaitingFallbackSig */ && state.fallbackRequest) {
105620
+ map3.set(id, state.fallbackRequest);
105621
+ }
105622
+ }
105623
+ return map3;
105624
+ }
105625
+ #handleFallbackAuthorizationRequest(message2) {
105626
+ const cohortId = message2.body?.cohortId;
105627
+ if (!cohortId) return;
105628
+ const state = this.#cohortStates.get(cohortId);
105629
+ if (!state || !state.cohort) return;
105630
+ const acceptFrom = [
105631
+ "ValidationSent" /* ValidationSent */,
105632
+ "AwaitingSigning" /* AwaitingSigning */,
105633
+ "NonceSent" /* NonceSent */,
105634
+ "AwaitingPartialSig" /* AwaitingPartialSig */,
105635
+ "Complete" /* Complete */
105636
+ ];
105637
+ if (!acceptFrom.includes(state.phase)) return;
105638
+ const sessionId = message2.body?.sessionId;
105639
+ const pendingTxHex = message2.body?.pendingTx;
105640
+ const prevOutScriptHex = message2.body?.prevOutScriptHex;
105641
+ const prevOutValue = message2.body?.prevOutValue;
105642
+ const fallbackLeafScriptHex = message2.body?.fallbackLeafScriptHex;
105643
+ if (!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue || !fallbackLeafScriptHex) return;
105644
+ state.fallbackRequest = { cohortId, sessionId, pendingTxHex, prevOutScriptHex, prevOutValue, fallbackLeafScriptHex };
105645
+ state.signingSession?.clearSecrets();
105646
+ state.phase = "AwaitingFallbackSig" /* AwaitingFallbackSig */;
105647
+ }
105648
+ /**
105649
+ * User action: authorize the fallback spend. Recomputes the k-of-n fallback
105650
+ * leaf from the member's OWN cohort state (not the service-provided script),
105651
+ * computes the BIP-341 script-path sighash over the requested transaction, and
105652
+ * returns a standalone BIP-340 signature (no nonce round). The member completes
105653
+ * once it has contributed; the service needs only k of these.
105654
+ */
105655
+ approveFallback(cohortId) {
105656
+ const state = this.#cohortStates.get(cohortId);
105657
+ if (!state || state.phase !== "AwaitingFallbackSig" /* AwaitingFallbackSig */) {
105658
+ throw new AggregationParticipantError(
105659
+ `Cannot approve fallback for cohort ${cohortId}: not in AwaitingFallbackSig phase.`,
105660
+ "INVALID_PHASE",
105661
+ { cohortId, phase: state?.phase }
105662
+ );
105663
+ }
105664
+ if (!state.fallbackRequest || !state.cohort) {
105665
+ throw new AggregationParticipantError(
105666
+ `Cohort ${cohortId} missing fallback request or cohort state.`,
105667
+ "MISSING_STATE",
105668
+ { cohortId }
105669
+ );
105670
+ }
105671
+ const req = state.fallbackRequest;
105672
+ const tx = Transaction.fromRaw(hexToBytes(req.pendingTxHex), { allowUnknownOutputs: true });
105673
+ const prevOutScript = hexToBytes(req.prevOutScriptHex);
105674
+ const prevOutValue = BigInt(req.prevOutValue);
105675
+ this.#assertTxAnchorsValidatedSignal(cohortId, state, tx);
105676
+ const fallbackLeaf = buildFallbackLeaf({
105677
+ cohortKeys: state.cohort.cohortKeys,
105678
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold
105679
+ });
105680
+ const sighash = fallbackSighash(tx, 0, prevOutScript, prevOutValue, fallbackLeaf);
105681
+ const signature = this.#signer.withSecret((secretKey) => schnorr2.sign(sighash, secretKey));
105682
+ state.phase = "Complete" /* Complete */;
105683
+ return [createFallbackSignatureMessage({
105684
+ from: this.did,
105685
+ to: state.serviceDid,
105686
+ cohortId,
105687
+ sessionId: req.sessionId,
105688
+ signerPk: this.publicKey.slice(1),
105689
+ fallbackSignature: signature
105690
+ })];
105691
+ }
104916
105692
  getCohortPhase(cohortId) {
104917
105693
  return this.#cohortStates.get(cohortId)?.phase;
104918
105694
  }
@@ -104949,6 +105725,85 @@ a=end-of-candidates
104949
105725
  }
104950
105726
  };
104951
105727
 
105728
+ // src/core/aggregation/recovery-spend.ts
105729
+ init_shim();
105730
+ init_secp256k1();
105731
+ var DUST_LIMIT_SATS = 546n;
105732
+ function bytesEqual2(a2, b) {
105733
+ return a2.length === b.length && a2.every((x, i4) => x === b[i4]);
105734
+ }
105735
+ function buildRecoverySpend(params) {
105736
+ const {
105737
+ cohortKeys,
105738
+ recoverySecretKey,
105739
+ recoveryKey,
105740
+ recoverySequence,
105741
+ fallbackThreshold,
105742
+ fundingModel,
105743
+ network,
105744
+ utxo,
105745
+ destinationAddress,
105746
+ fee,
105747
+ beaconAddress
105748
+ } = params;
105749
+ if (cohortKeys.length === 0) {
105750
+ throw new AggregationCohortError(
105751
+ "Cannot build recovery spend: no cohort keys.",
105752
+ "NO_COHORT_KEYS"
105753
+ );
105754
+ }
105755
+ const derivedPub = schnorr2.getPublicKey(recoverySecretKey);
105756
+ if (!bytesEqual2(derivedPub, recoveryKey)) {
105757
+ throw new AggregationCohortError(
105758
+ "Recovery secret key does not correspond to the committed recovery key.",
105759
+ "RECOVERY_KEY_MISMATCH"
105760
+ );
105761
+ }
105762
+ const out = utxo.value - fee;
105763
+ if (out <= 0n) {
105764
+ throw new AggregationCohortError(
105765
+ `Recovery fee ${fee} exceeds UTXO value ${utxo.value}.`,
105766
+ "FEE_EXCEEDS_VALUE",
105767
+ { fee: fee.toString(), value: utxo.value.toString() }
105768
+ );
105769
+ }
105770
+ if (out < DUST_LIMIT_SATS) {
105771
+ throw new AggregationCohortError(
105772
+ `Recovered output ${out} is below the dust limit ${DUST_LIMIT_SATS}; the spend would not relay.`,
105773
+ "DUST_OUTPUT",
105774
+ { output: out.toString(), dustLimit: DUST_LIMIT_SATS.toString() }
105775
+ );
105776
+ }
105777
+ const internalKey = keyAggExport(keyAggregate(sortKeys(cohortKeys)));
105778
+ const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
105779
+ recoveryKey,
105780
+ recoverySequence,
105781
+ cohortKeys,
105782
+ fallbackThreshold: resolveFallbackThreshold(fallbackThreshold, cohortKeys.length)
105783
+ });
105784
+ const net = getNetwork(network);
105785
+ const payment = p2tr(internalKey, leaves, net, true);
105786
+ if (beaconAddress !== void 0 && payment.address !== beaconAddress) {
105787
+ throw new AggregationCohortError(
105788
+ `Reconstructed beacon address ${payment.address} does not match the cohort's funded address ${beaconAddress}.`,
105789
+ "BEACON_ADDRESS_MISMATCH",
105790
+ { reconstructed: payment.address, expected: beaconAddress }
105791
+ );
105792
+ }
105793
+ const tx = new Transaction({ version: 2, allowUnknownInputs: true });
105794
+ tx.addInput({
105795
+ txid: utxo.txid,
105796
+ index: utxo.vout,
105797
+ witnessUtxo: { script: payment.script, amount: utxo.value },
105798
+ tapLeafScript: payment.tapLeafScript,
105799
+ sequence: recoverySequence
105800
+ });
105801
+ tx.addOutputAddress(destinationAddress, out, net);
105802
+ tx.signIdx(recoverySecretKey, 0);
105803
+ tx.finalize();
105804
+ return tx;
105805
+ }
105806
+
104952
105807
  // src/core/aggregation/logger.ts
104953
105808
  init_shim();
104954
105809
  var CONSOLE_LOGGER = {
@@ -104981,6 +105836,7 @@ a=end-of-candidates
104981
105836
  ]);
104982
105837
  var UPDATE_VALUES = /* @__PURE__ */ new Set([
104983
105838
  SUBMIT_UPDATE,
105839
+ SUBMIT_NONINCLUDED,
104984
105840
  DISTRIBUTE_AGGREGATED_DATA,
104985
105841
  VALIDATION_ACK
104986
105842
  ]);
@@ -104988,7 +105844,9 @@ a=end-of-candidates
104988
105844
  AUTHORIZATION_REQUEST,
104989
105845
  NONCE_CONTRIBUTION,
104990
105846
  AGGREGATED_NONCE,
104991
- SIGNATURE_AUTHORIZATION
105847
+ SIGNATURE_AUTHORIZATION,
105848
+ FALLBACK_AUTHORIZATION_REQUEST,
105849
+ FALLBACK_SIGNATURE
104992
105850
  ]);
104993
105851
  function isAggregationMessageType(type) {
104994
105852
  return KEYGEN_VALUES.has(type) || UPDATE_VALUES.has(type) || SIGN_VALUES.has(type);
@@ -111149,7 +112007,7 @@ ${value2}`;
111149
112007
  * backfill historical events to late subscribers: republishing gives late
111150
112008
  * joiners a window to discover the message without requiring protocol
111151
112009
  * changes. Relay rate-limit / publish failures inside the interval are
111152
- * caught and logged rather than propagated the caller should stop the
112010
+ * caught and logged rather than propagated - the caller should stop the
111153
112011
  * repeater once the protocol condition is satisfied.
111154
112012
  */
111155
112013
  publishRepeating(message2, sender, intervalMs, recipient) {
@@ -111603,35 +112461,29 @@ ${value2}`;
111603
112461
  session;
111604
112462
  #transport;
111605
112463
  #did;
111606
- #config;
112464
+ #defaultConfig;
111607
112465
  #onOptInReceived;
111608
112466
  #onReadyToFinalize;
111609
112467
  #onProvideTxData;
111610
112468
  #cohortTtlMs;
111611
112469
  #phaseTimeoutMs;
111612
112470
  #advertRepeatIntervalMs;
111613
- #cohortId;
112471
+ #autoFallbackOnStall;
112472
+ /** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
112473
+ static #SIGNING_PHASES = [
112474
+ "SigningStarted" /* SigningStarted */,
112475
+ "NoncesCollected" /* NoncesCollected */,
112476
+ "AwaitingPartialSigs" /* AwaitingPartialSigs */
112477
+ ];
112478
+ /** Per-cohort run state, keyed by cohortId. */
112479
+ #contexts = /* @__PURE__ */ new Map();
111614
112480
  #handlersRegistered = false;
111615
112481
  #stopped = false;
111616
- /**
111617
- * Guard against the async race where two concurrent #handleOptIn invocations
111618
- * both pass the `participants.length >= minParticipants` check before either
111619
- * mutates the cohort phase. Set synchronously before any `await` so subsequent
111620
- * handlers observe it on their next resumption.
111621
- */
111622
- #finalizing = false;
111623
- #resolveRun;
111624
- #rejectRun;
111625
- #cohortTtlTimer;
111626
- #phaseTimer;
111627
- #lastObservedPhase;
111628
- /** Stop handle for the repeating COHORT_ADVERT publish loop. */
111629
- #stopAdvertRepeat;
111630
112482
  constructor(options2) {
111631
112483
  super();
111632
112484
  this.#transport = options2.transport;
111633
112485
  this.#did = options2.did;
111634
- this.#config = options2.config;
112486
+ this.#defaultConfig = options2.config;
111635
112487
  this.#onOptInReceived = options2.onOptInReceived ?? (async () => ({ accepted: true }));
111636
112488
  this.#onReadyToFinalize = options2.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
111637
112489
  finalize: acceptedCount >= minRequired
@@ -111640,6 +112492,7 @@ ${value2}`;
111640
112492
  this.#cohortTtlMs = options2.cohortTtlMs;
111641
112493
  this.#phaseTimeoutMs = options2.phaseTimeoutMs;
111642
112494
  this.#advertRepeatIntervalMs = options2.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
112495
+ this.#autoFallbackOnStall = options2.autoFallbackOnStall ?? false;
111643
112496
  this.session = new AggregationService({
111644
112497
  // The coordinator never signs, so the state machine receives only the
111645
112498
  // public half of the operator's keypair (see ADR 038). The full keypair
@@ -111649,55 +112502,126 @@ ${value2}`;
111649
112502
  maxUpdateSizeBytes: options2.maxUpdateSizeBytes
111650
112503
  });
111651
112504
  }
112505
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
112506
+ #contextFor(msg) {
112507
+ const cohortId = msg.body?.cohortId;
112508
+ if (!cohortId) return void 0;
112509
+ return this.#contexts.get(cohortId);
112510
+ }
111652
112511
  /**
111653
- * Drain any silent rejections the state machine recorded during the most
111654
- * recent receive() and surface them as `message-rejected` events. Safe to
111655
- * call even before a cohortId is assigned.
112512
+ * Drain any silent rejections the state machine recorded for a cohort during
112513
+ * the most recent receive() and surface them as `message-rejected` events.
111656
112514
  */
111657
- #drainRejections() {
111658
- if (!this.#cohortId) return;
111659
- for (const r2 of this.session.drainRejections(this.#cohortId)) {
111660
- this.emit("message-rejected", { cohortId: this.#cohortId, ...r2 });
112515
+ #drainRejections(ctx) {
112516
+ for (const r2 of this.session.drainRejections(ctx.cohortId)) {
112517
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r2 });
111661
112518
  }
111662
112519
  }
111663
112520
  /**
111664
- * Run the protocol to completion. Resolves with the final aggregation result
111665
- * (signature + signed transaction) once signing is complete.
112521
+ * Advertise a new cohort and begin driving it to completion. Callable many
112522
+ * times on one runner; each cohort runs concurrently and independently.
112523
+ *
112524
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
112525
+ * @returns The new cohort's id and a `completion` promise that resolves with
112526
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
112527
+ * @throws If the runner has been stopped, or the config is invalid
112528
+ * (fail-fast via `createCohort`).
112529
+ */
112530
+ advertiseCohort(config) {
112531
+ if (this.#stopped) {
112532
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
112533
+ }
112534
+ this.#registerHandlers();
112535
+ const cohortId = this.session.createCohort(config);
112536
+ let resolve;
112537
+ let reject;
112538
+ const completion = new Promise((res, rej) => {
112539
+ resolve = res;
112540
+ reject = rej;
112541
+ });
112542
+ const ctx = {
112543
+ cohortId,
112544
+ config,
112545
+ resolve,
112546
+ reject,
112547
+ completion,
112548
+ finalizing: false,
112549
+ settled: false
112550
+ };
112551
+ this.#contexts.set(cohortId, ctx);
112552
+ try {
112553
+ this.#startTimers(ctx);
112554
+ const advertMsgs = this.session.advertise(cohortId);
112555
+ this.#onPhaseMaybeChanged(ctx);
112556
+ this.emit("cohort-advertised", { cohortId });
112557
+ if (this.#advertRepeatIntervalMs > 0) {
112558
+ this.#startAdvertRepeat(ctx, advertMsgs);
112559
+ } else {
112560
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
112561
+ }
112562
+ } catch (err) {
112563
+ this.#failCohort(ctx, err);
112564
+ }
112565
+ return { cohortId, completion };
112566
+ }
112567
+ /**
112568
+ * Run a single cohort to completion using the `config` supplied in the
112569
+ * runner options. Thin convenience over {@link advertiseCohort} for the
112570
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
111666
112571
  *
111667
112572
  * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
111668
112573
  */
111669
112574
  run() {
111670
- return new Promise((resolve, reject) => {
111671
- this.#resolveRun = resolve;
111672
- this.#rejectRun = reject;
111673
- try {
111674
- this.#registerHandlers();
111675
- this.#cohortId = this.session.createCohort(this.#config);
111676
- this.#startTimers();
111677
- const advertMsgs = this.session.advertise(this.#cohortId);
111678
- this.#onPhaseMaybeChanged();
111679
- this.emit("cohort-advertised", { cohortId: this.#cohortId });
111680
- if (this.#advertRepeatIntervalMs > 0) {
111681
- this.#startAdvertRepeat(advertMsgs);
111682
- } else {
111683
- this.#sendAll(advertMsgs).catch((err) => this.#fail(err));
111684
- }
111685
- } catch (err) {
111686
- this.#fail(err);
112575
+ if (!this.#defaultConfig) {
112576
+ return Promise.reject(new AggregationServiceError(
112577
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
112578
+ "MISSING_COHORT_CONFIG",
112579
+ {}
112580
+ ));
112581
+ }
112582
+ try {
112583
+ return this.advertiseCohort(this.#defaultConfig).completion;
112584
+ } catch (err) {
112585
+ return Promise.reject(err);
112586
+ }
112587
+ }
112588
+ /**
112589
+ * Wait for every currently-outstanding cohort to settle and return the
112590
+ * successful results. Dynamic drain: cohorts advertised while this is pending
112591
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
112592
+ * are surfaced via `error` / `cohort-failed` events and their rejected
112593
+ * `completion` promises; they are omitted from the returned array (this
112594
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
112595
+ * `phaseTimeoutMs` or this may never resolve.
112596
+ *
112597
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
112598
+ */
112599
+ async runAll() {
112600
+ const collected = /* @__PURE__ */ new Map();
112601
+ const onComplete = (result) => {
112602
+ collected.set(result.cohortId, result);
112603
+ };
112604
+ this.on("signing-complete", onComplete);
112605
+ try {
112606
+ while (this.#contexts.size > 0) {
112607
+ await Promise.allSettled([...this.#contexts.values()].map((c2) => c2.completion));
111687
112608
  }
111688
- });
112609
+ } finally {
112610
+ this.off("signing-complete", onComplete);
112611
+ }
112612
+ return [...collected.values()];
111689
112613
  }
111690
112614
  /**
111691
- * Begin publishing the cohort advert immediately and on a repeating interval
111692
- * until {@link #stopAdvertRepeating} is called. Each advert is broadcast
111693
- * (no recipient) via the transport's `publishRepeating` primitive.
112615
+ * Begin publishing a cohort's advert immediately and on a repeating interval
112616
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
112617
+ * recipient) via the transport's `publishRepeating` primitive.
111694
112618
  */
111695
- #startAdvertRepeat(advertMsgs) {
112619
+ #startAdvertRepeat(ctx, advertMsgs) {
111696
112620
  const stops = [];
111697
112621
  for (const msg of advertMsgs) {
111698
112622
  stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
111699
112623
  }
111700
- this.#stopAdvertRepeat = () => {
112624
+ ctx.stopAdvertRepeat = () => {
111701
112625
  for (const stop2 of stops) {
111702
112626
  try {
111703
112627
  stop2();
@@ -111706,81 +112630,176 @@ ${value2}`;
111706
112630
  }
111707
112631
  };
111708
112632
  }
111709
- /** Stop the advert republish loop. Idempotent. */
111710
- #stopAdvertRepeating() {
111711
- if (!this.#stopAdvertRepeat) return;
111712
- const stop2 = this.#stopAdvertRepeat;
111713
- this.#stopAdvertRepeat = void 0;
112633
+ /** Stop a cohort's advert republish loop. Idempotent. */
112634
+ #stopAdvertRepeating(ctx) {
112635
+ if (!ctx.stopAdvertRepeat) return;
112636
+ const stop2 = ctx.stopAdvertRepeat;
112637
+ ctx.stopAdvertRepeat = void 0;
111714
112638
  stop2();
111715
112639
  }
111716
- /** Schedule cohort TTL + phase timeout at the start of a run. */
111717
- #startTimers() {
112640
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
112641
+ #startTimers(ctx) {
111718
112642
  if (this.#cohortTtlMs !== void 0) {
111719
- this.#cohortTtlTimer = setTimeout(() => {
111720
- const reason = `Cohort ${this.#cohortId ?? ""} exceeded TTL of ${this.#cohortTtlMs}ms`;
111721
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111722
- this.#fail(new Error(reason));
112643
+ ctx.cohortTtlTimer = setTimeout(() => {
112644
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
112645
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
112646
+ this.#failCohort(ctx, new Error(reason));
111723
112647
  }, this.#cohortTtlMs);
111724
112648
  }
111725
- this.#resetPhaseTimer();
112649
+ this.#resetPhaseTimer(ctx);
111726
112650
  }
111727
- /** Reset the per-phase stall timer. Called when a phase transition is observed. */
111728
- #resetPhaseTimer() {
111729
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111730
- this.#phaseTimer = void 0;
112651
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
112652
+ #resetPhaseTimer(ctx) {
112653
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
112654
+ ctx.phaseTimer = void 0;
111731
112655
  if (this.#phaseTimeoutMs === void 0) return;
111732
- this.#phaseTimer = setTimeout(() => {
111733
- const reason = `Cohort ${this.#cohortId ?? ""} stalled in phase ${this.#lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
111734
- this.emit("cohort-failed", { cohortId: this.#cohortId ?? "", reason });
111735
- this.#fail(new Error(reason));
112656
+ ctx.phaseTimer = setTimeout(() => {
112657
+ const phase = this.session.getCohortPhase(ctx.cohortId);
112658
+ const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
112659
+ if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
112660
+ this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
112661
+ return;
112662
+ }
112663
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
112664
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
112665
+ this.#failCohort(ctx, new Error(reason));
111736
112666
  }, this.#phaseTimeoutMs);
111737
112667
  }
111738
- /** Detect a phase change since the last observation and reset the phase timer. */
111739
- #onPhaseMaybeChanged() {
111740
- if (!this.#cohortId) return;
111741
- const phase = this.session.getCohortPhase(this.#cohortId);
111742
- if (phase !== this.#lastObservedPhase) {
111743
- this.#lastObservedPhase = phase;
111744
- this.#resetPhaseTimer();
111745
- }
112668
+ /**
112669
+ * Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
112670
+ * fallback (script-path) signatures instead (ADR 042). Idempotent and safe
112671
+ * against the optimistic completion: it commits the cohort to the fallback
112672
+ * path synchronously (the `committedPath` latch) before sending anything, so a
112673
+ * late optimistic signature can no longer complete-and-broadcast a competing
112674
+ * spend of the same UTXO. No-op if the cohort is unknown, already settled, or
112675
+ * already committed to a path.
112676
+ *
112677
+ * Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
112678
+ * set; otherwise call it from an operator decision (a UI "fall back now"
112679
+ * action). Throws only if the underlying state machine rejects the transition
112680
+ * (e.g. signing has not started).
112681
+ */
112682
+ async triggerFallback(cohortId) {
112683
+ const ctx = this.#contexts.get(cohortId);
112684
+ if (!ctx || ctx.settled || ctx.committedPath) return;
112685
+ const messages2 = this.session.startFallbackSigning(cohortId);
112686
+ ctx.committedPath = "fallback";
112687
+ this.#stopAdvertRepeating(ctx);
112688
+ this.#onPhaseMaybeChanged(ctx);
112689
+ const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
112690
+ this.emit("fallback-started", { cohortId, sessionId });
112691
+ await this.#sendAll(messages2);
112692
+ }
112693
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
112694
+ #onPhaseMaybeChanged(ctx) {
112695
+ const phase = this.session.getCohortPhase(ctx.cohortId);
112696
+ if (phase !== ctx.lastObservedPhase) {
112697
+ ctx.lastObservedPhase = phase;
112698
+ this.#resetPhaseTimer(ctx);
112699
+ }
112700
+ }
112701
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
112702
+ #clearTimers(ctx) {
112703
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
112704
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
112705
+ ctx.cohortTtlTimer = void 0;
112706
+ ctx.phaseTimer = void 0;
111746
112707
  }
111747
- /** Clear both timers. Called on successful completion, stop(), and #fail. */
111748
- #clearTimers() {
111749
- if (this.#cohortTtlTimer) clearTimeout(this.#cohortTtlTimer);
111750
- if (this.#phaseTimer) clearTimeout(this.#phaseTimer);
111751
- this.#cohortTtlTimer = void 0;
111752
- this.#phaseTimer = void 0;
112708
+ /**
112709
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
112710
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
112711
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
112712
+ * state machine; whether that cohort's `session` state is also removed is the
112713
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
112714
+ */
112715
+ #disposeCohort(ctx) {
112716
+ this.#stopAdvertRepeating(ctx);
112717
+ this.#clearTimers(ctx);
112718
+ this.#contexts.delete(ctx.cohortId);
112719
+ }
112720
+ /**
112721
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
112722
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
112723
+ * via `session.getCohort(result.cohortId)`; reclaim it with
112724
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
112725
+ */
112726
+ #completeCohort(ctx, result) {
112727
+ if (ctx.settled) return;
112728
+ ctx.settled = true;
112729
+ this.#disposeCohort(ctx);
112730
+ this.emit("signing-complete", result);
112731
+ ctx.resolve(result);
112732
+ }
112733
+ /**
112734
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
112735
+ * the state machine, and rejects only its completion; siblings keep running
112736
+ * and the shared transport handlers stay registered. Idempotent via
112737
+ * `ctx.settled`.
112738
+ */
112739
+ #failCohort(ctx, err) {
112740
+ if (ctx.settled) return;
112741
+ ctx.settled = true;
112742
+ this.#disposeCohort(ctx);
112743
+ this.session.removeCohort(ctx.cohortId);
112744
+ this.emit("error", err);
112745
+ ctx.reject(err);
112746
+ }
112747
+ /**
112748
+ * Stop a single cohort early without affecting the rest of the runner. Drops
112749
+ * the cohort's state machine state; its `completion` promise rejects with a
112750
+ * stopped error.
112751
+ */
112752
+ stopCohort(cohortId) {
112753
+ const ctx = this.#contexts.get(cohortId);
112754
+ if (!ctx || ctx.settled) return;
112755
+ ctx.settled = true;
112756
+ this.#disposeCohort(ctx);
112757
+ this.session.removeCohort(cohortId);
112758
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
111753
112759
  }
111754
112760
  /**
111755
- * Stop the runner early. Marks the runner stopped and detaches transport
111756
- * handlers so a restart or a new runner doesn't inherit stale dispatch.
112761
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
112762
+ * shared transport handlers so a restart or a new runner doesn't inherit
112763
+ * stale dispatch. Safe to call repeatedly.
111757
112764
  */
111758
112765
  stop() {
111759
112766
  this.#stopped = true;
111760
- this.#stopAdvertRepeating();
111761
- this.#clearTimers();
112767
+ for (const ctx of [...this.#contexts.values()]) {
112768
+ if (ctx.settled) continue;
112769
+ ctx.settled = true;
112770
+ this.#disposeCohort(ctx);
112771
+ this.session.removeCohort(ctx.cohortId);
112772
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
112773
+ }
112774
+ this.#contexts.clear();
111762
112775
  this.#unregisterHandlers();
111763
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111764
112776
  }
111765
112777
  /** Message types this runner listens for on the transport. */
111766
112778
  static #HANDLED_MESSAGE_TYPES = [
111767
112779
  COHORT_OPT_IN,
111768
112780
  SUBMIT_UPDATE,
112781
+ SUBMIT_NONINCLUDED,
111769
112782
  VALIDATION_ACK,
111770
112783
  NONCE_CONTRIBUTION,
111771
- SIGNATURE_AUTHORIZATION
112784
+ SIGNATURE_AUTHORIZATION,
112785
+ FALLBACK_SIGNATURE
111772
112786
  ];
111773
112787
  /**
111774
- * Internal: handler registration with the transport. Idempotent.
112788
+ * Internal: handler registration with the transport. Idempotent. Handlers
112789
+ * are DID-scoped and cohort-agnostic - one registration serves every cohort
112790
+ * this runner drives; demux to the right {@link RunContext} happens in each
112791
+ * handler via the inbound message's cohortId.
111775
112792
  */
111776
112793
  #registerHandlers() {
111777
112794
  if (this.#handlersRegistered) return;
111778
112795
  this.#handlersRegistered = true;
111779
112796
  this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
111780
112797
  this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
112798
+ this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
111781
112799
  this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
111782
112800
  this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
111783
112801
  this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
112802
+ this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
111784
112803
  }
111785
112804
  /** Internal: detach from the transport. Safe to call repeatedly. */
111786
112805
  #unregisterHandlers() {
@@ -111792,23 +112811,25 @@ ${value2}`;
111792
112811
  }
111793
112812
  /**
111794
112813
  * Internal: message handlers for each protocol step. Each handler:
111795
- * 1) feeds the message into the state machine via session.receive()
111796
- * 2) emits a high-level event for external observers
111797
- * 3) checks if the new state triggers any automatic next steps, and if so:
112814
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
112815
+ * 2) feeds the message into the state machine via session.receive()
112816
+ * 3) emits a high-level event (carrying cohortId) for external observers
112817
+ * 4) checks if the new state triggers any automatic next steps, and if so:
111798
112818
  * a) calls the appropriate decision callback(s)
111799
112819
  * b) sends any resulting messages from the state machine
112820
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
111800
112821
  * @param {BaseMessage} msg - The incoming message to handle.
111801
112822
  * @returns {Promise<void>} Resolves when handling is complete.
111802
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111803
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111804
112823
  */
111805
112824
  async #handleOptIn(msg) {
111806
112825
  if (this.#stopped) return;
112826
+ const ctx = this.#contextFor(msg);
112827
+ if (!ctx) return;
111807
112828
  try {
111808
112829
  this.session.receive(msg);
111809
- this.#drainRejections();
111810
- this.#onPhaseMaybeChanged();
111811
- const optIn = this.session.pendingOptIns(this.#cohortId).get(msg.from);
112830
+ this.#drainRejections(ctx);
112831
+ this.#onPhaseMaybeChanged(ctx);
112832
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
111812
112833
  if (!optIn) return;
111813
112834
  this.emit("opt-in-received", optIn);
111814
112835
  if (optIn.communicationPk) {
@@ -111816,34 +112837,34 @@ ${value2}`;
111816
112837
  }
111817
112838
  const decision = await this.#onOptInReceived(optIn);
111818
112839
  if (!decision.accepted) return;
111819
- const maxParticipants = this.#config.maxParticipants;
111820
- const cohortNow = this.session.getCohort(this.#cohortId);
112840
+ const maxParticipants = ctx.config.maxParticipants;
112841
+ const cohortNow = this.session.getCohort(ctx.cohortId);
111821
112842
  if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
111822
112843
  return;
111823
112844
  }
111824
- await this.#sendAll(this.session.acceptParticipant(this.#cohortId, msg.from));
111825
- this.emit("participant-accepted", { participantDid: msg.from });
111826
- const cohort = this.session.getCohort(this.#cohortId);
111827
- if (cohort.participants.length >= this.#config.minParticipants && !this.#finalizing) {
111828
- this.#finalizing = true;
112845
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
112846
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
112847
+ const cohort = this.session.getCohort(ctx.cohortId);
112848
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
112849
+ ctx.finalizing = true;
111829
112850
  const finalizeDecision = await this.#onReadyToFinalize({
111830
112851
  acceptedCount: cohort.participants.length,
111831
- minRequired: this.#config.minParticipants
112852
+ minRequired: ctx.config.minParticipants
111832
112853
  });
111833
112854
  if (!finalizeDecision.finalize) {
111834
- this.#finalizing = false;
112855
+ ctx.finalizing = false;
111835
112856
  return;
111836
112857
  }
111837
- const readyMsgs = this.session.finalizeKeygen(this.#cohortId);
111838
- this.#stopAdvertRepeating();
112858
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
112859
+ this.#stopAdvertRepeating(ctx);
111839
112860
  this.emit("keygen-complete", {
111840
- cohortId: this.#cohortId,
112861
+ cohortId: ctx.cohortId,
111841
112862
  beaconAddress: cohort.beaconAddress
111842
112863
  });
111843
112864
  await this.#sendAll(readyMsgs);
111844
112865
  }
111845
112866
  } catch (err) {
111846
- this.#fail(err);
112867
+ this.#failCohort(ctx, err);
111847
112868
  }
111848
112869
  }
111849
112870
  /**
@@ -111851,23 +112872,23 @@ ${value2}`;
111851
112872
  * and distributes the data for validation.
111852
112873
  * @param {BaseMessage} msg - The incoming message to handle.
111853
112874
  * @returns {Promise<void>} Resolves when handling is complete.
111854
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111855
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111856
112875
  */
111857
112876
  async #handleSubmitUpdate(msg) {
111858
112877
  if (this.#stopped) return;
112878
+ const ctx = this.#contextFor(msg);
112879
+ if (!ctx) return;
111859
112880
  try {
111860
112881
  this.session.receive(msg);
111861
- this.#drainRejections();
111862
- this.#onPhaseMaybeChanged();
111863
- this.emit("update-received", { participantDid: msg.from });
111864
- if (this.session.getCohortPhase(this.#cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
111865
- const distributeMsgs = this.session.buildAndDistribute(this.#cohortId);
111866
- this.emit("data-distributed", { cohortId: this.#cohortId });
112882
+ this.#drainRejections(ctx);
112883
+ this.#onPhaseMaybeChanged(ctx);
112884
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
112885
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
112886
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
112887
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
111867
112888
  await this.#sendAll(distributeMsgs);
111868
112889
  }
111869
112890
  } catch (err) {
111870
- this.#fail(err);
112891
+ this.#failCohort(ctx, err);
111871
112892
  }
111872
112893
  }
111873
112894
  /**
@@ -111875,111 +112896,121 @@ ${value2}`;
111875
112896
  * automatically requests tx data and starts signing.
111876
112897
  * @param {BaseMessage} msg - The incoming message to handle.
111877
112898
  * @returns {Promise<void>} Resolves when handling is complete.
111878
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111879
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111880
112899
  */
111881
112900
  async #handleValidationAck(msg) {
111882
112901
  if (this.#stopped) return;
112902
+ const ctx = this.#contextFor(msg);
112903
+ if (!ctx) return;
111883
112904
  try {
111884
112905
  this.session.receive(msg);
111885
- this.#drainRejections();
111886
- this.#onPhaseMaybeChanged();
112906
+ this.#drainRejections(ctx);
112907
+ this.#onPhaseMaybeChanged(ctx);
111887
112908
  const approved = !!msg.body?.approved;
111888
- this.emit("validation-received", { participantDid: msg.from, approved });
111889
- const phase = this.session.getCohortPhase(this.#cohortId);
112909
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
112910
+ const phase = this.session.getCohortPhase(ctx.cohortId);
111890
112911
  if (phase === "Failed" /* Failed */) {
111891
112912
  const reason = `Validation rejected by participant ${msg.from}`;
111892
- this.emit("cohort-failed", { cohortId: this.#cohortId, reason });
111893
- this.#fail(new Error(reason));
112913
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
112914
+ this.#failCohort(ctx, new Error(reason));
111894
112915
  return;
111895
112916
  }
111896
112917
  if (phase === "Validated" /* Validated */) {
111897
- const cohort = this.session.getCohort(this.#cohortId);
112918
+ const cohort = this.session.getCohort(ctx.cohortId);
111898
112919
  const txData = await this.#onProvideTxData({
111899
- cohortId: this.#cohortId,
112920
+ cohortId: ctx.cohortId,
111900
112921
  beaconAddress: cohort.beaconAddress,
111901
112922
  signalBytes: cohort.signalBytes
111902
112923
  });
111903
- const authMsgs = this.session.startSigning(this.#cohortId, txData);
111904
- const sessionId = this.session.getSigningSessionId(this.#cohortId) ?? "";
111905
- this.emit("signing-started", { sessionId });
112924
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
112925
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
112926
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
111906
112927
  await this.#sendAll(authMsgs);
111907
112928
  }
111908
112929
  } catch (err) {
111909
- this.#fail(err);
112930
+ this.#failCohort(ctx, err);
111910
112931
  }
111911
112932
  }
111912
112933
  /**
111913
- * Handler for receiving nonce contributions and signature authorizations. When all nonces or
111914
- * signatures are received,
112934
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
112935
+ * nonce back to the cohort.
111915
112936
  * @param {BaseMessage} msg - The incoming message to handle.
111916
112937
  * @returns {Promise<void>} Resolves when handling is complete.
111917
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111918
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111919
112938
  */
111920
112939
  async #handleNonceContribution(msg) {
111921
112940
  if (this.#stopped) return;
112941
+ const ctx = this.#contextFor(msg);
112942
+ if (!ctx) return;
111922
112943
  try {
111923
112944
  this.session.receive(msg);
111924
- this.#drainRejections();
111925
- this.#onPhaseMaybeChanged();
111926
- this.emit("nonce-received", { participantDid: msg.from });
111927
- if (this.session.getCohortPhase(this.#cohortId) === "NoncesCollected" /* NoncesCollected */) {
111928
- await this.#sendAll(this.session.sendAggregatedNonce(this.#cohortId));
112945
+ this.#drainRejections(ctx);
112946
+ this.#onPhaseMaybeChanged(ctx);
112947
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
112948
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
112949
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
111929
112950
  }
111930
112951
  } catch (err) {
111931
- this.#fail(err);
112952
+ this.#failCohort(ctx, err);
111932
112953
  }
111933
112954
  }
111934
112955
  /**
111935
112956
  * Handler for receiving signature authorizations. When all partial signatures are received, the
111936
- * session automatically completes and the final result is emitted and the run() promise is resolved.
112957
+ * session automatically completes; the final result is emitted and the cohort's completion
112958
+ * promise resolves.
111937
112959
  * @param {BaseMessage} msg - The incoming message to handle.
111938
112960
  * @returns {Promise<void>} Resolves when handling is complete.
111939
- * @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
111940
- * Note: if the runner has been stopped, handlers will ignore incoming messages.
111941
112961
  */
111942
112962
  async #handleSignatureAuthorization(msg) {
111943
112963
  if (this.#stopped) return;
112964
+ const ctx = this.#contextFor(msg);
112965
+ if (!ctx) return;
112966
+ try {
112967
+ this.session.receive(msg);
112968
+ this.#drainRejections(ctx);
112969
+ this.#onPhaseMaybeChanged(ctx);
112970
+ if (ctx.committedPath === "fallback") return;
112971
+ const result = this.session.getResult(ctx.cohortId);
112972
+ if (result) {
112973
+ ctx.committedPath = "optimistic";
112974
+ this.#completeCohort(ctx, result);
112975
+ }
112976
+ } catch (err) {
112977
+ this.#failCohort(ctx, err);
112978
+ }
112979
+ }
112980
+ /**
112981
+ * Handler for receiving fallback (k-of-n script-path) signatures. The state
112982
+ * machine assembles and finalizes the fallback spend once k valid signatures
112983
+ * are in; the result is then emitted and the cohort's completion resolves. The
112984
+ * cohort is already committed to the fallback path (via {@link triggerFallback}).
112985
+ * @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
112986
+ * @returns {Promise<void>} Resolves when handling is complete.
112987
+ */
112988
+ async #handleFallbackSignature(msg) {
112989
+ if (this.#stopped) return;
112990
+ const ctx = this.#contextFor(msg);
112991
+ if (!ctx) return;
111944
112992
  try {
111945
112993
  this.session.receive(msg);
111946
- this.#drainRejections();
111947
- this.#onPhaseMaybeChanged();
111948
- const result = this.session.getResult(this.#cohortId);
112994
+ this.#drainRejections(ctx);
112995
+ this.#onPhaseMaybeChanged(ctx);
112996
+ const result = this.session.getResult(ctx.cohortId);
111949
112997
  if (result) {
111950
- this.#clearTimers();
111951
- this.#unregisterHandlers();
111952
- this.emit("signing-complete", result);
111953
- this.#resolveRun?.(result);
112998
+ this.#completeCohort(ctx, result);
111954
112999
  }
111955
113000
  } catch (err) {
111956
- this.#fail(err);
113001
+ this.#failCohort(ctx, err);
111957
113002
  }
111958
113003
  }
111959
113004
  /**
111960
113005
  * Internal: helper to send all messages sequentially. Catches and propagates errors.
111961
113006
  * @param {BaseMessage[]} msgs - The messages to send.
111962
113007
  * @returns {Promise<void>} Resolves when all messages have been sent.
111963
- * @throws {Error} If sending any message fails, the error is emitted and the run promise is
111964
- * rejected.
111965
113008
  */
111966
113009
  async #sendAll(msgs) {
111967
113010
  for (const m2 of msgs) {
111968
113011
  await this.#transport.sendMessage(m2, this.#did, m2.to);
111969
113012
  }
111970
113013
  }
111971
- /**
111972
- * Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
111973
- * @param {Error} err - The error to handle.
111974
- */
111975
- #fail(err) {
111976
- this.#stopAdvertRepeating();
111977
- this.#clearTimers();
111978
- this.#unregisterHandlers();
111979
- if (this.#cohortId) this.session.removeCohort(this.#cohortId);
111980
- this.emit("error", err);
111981
- this.#rejectRun?.(err);
111982
- }
111983
113014
  };
111984
113015
 
111985
113016
  // src/core/aggregation/runner/participant-runner.ts
@@ -112028,7 +113059,8 @@ ${value2}`;
112028
113059
  COHORT_READY,
112029
113060
  DISTRIBUTE_AGGREGATED_DATA,
112030
113061
  AUTHORIZATION_REQUEST,
112031
- AGGREGATED_NONCE
113062
+ AGGREGATED_NONCE,
113063
+ FALLBACK_AUTHORIZATION_REQUEST
112032
113064
  ];
112033
113065
  /** Internal: detach from the transport. Safe to call repeatedly. */
112034
113066
  #unregisterHandlers() {
@@ -112040,7 +113072,8 @@ ${value2}`;
112040
113072
  }
112041
113073
  /**
112042
113074
  * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
112043
- * drive it to completion, and resolve. Convenient for tests and demos.
113075
+ * drive it to completion, and resolve. Convenient for tests and demos. The
113076
+ * single-cohort special case of {@link joinMatching} (count = 1).
112044
113077
  */
112045
113078
  static async joinFirst(options2) {
112046
113079
  return new Promise((resolve, reject) => {
@@ -112053,6 +113086,37 @@ ${value2}`;
112053
113086
  runner.start().catch(reject);
112054
113087
  });
112055
113088
  }
113089
+ /**
113090
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
113091
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
113092
+ * `count` cohorts have completed (the runner stops at that point). The
113093
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
113094
+ * several cohorts advertised by one service.
113095
+ *
113096
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
113097
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
113098
+ * `start()`, and listen for `cohort-complete` - the runner already drives
113099
+ * any number of cohorts concurrently.
113100
+ *
113101
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
113102
+ * @param count Number of completed cohorts to collect before resolving.
113103
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
113104
+ */
113105
+ static async joinMatching(options2, count) {
113106
+ return new Promise((resolve, reject) => {
113107
+ const runner = new _AggregationParticipantRunner(options2);
113108
+ const completed = [];
113109
+ runner.on("cohort-complete", (info) => {
113110
+ completed.push(info);
113111
+ if (completed.length >= count) {
113112
+ runner.stop();
113113
+ resolve(completed);
113114
+ }
113115
+ });
113116
+ runner.on("error", reject);
113117
+ runner.start().catch(reject);
113118
+ });
113119
+ }
112056
113120
  /**
112057
113121
  * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
112058
113122
  * but only registers handlers once.
@@ -112066,6 +113130,7 @@ ${value2}`;
112066
113130
  this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
112067
113131
  this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
112068
113132
  this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
113133
+ this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
112069
113134
  }
112070
113135
  /**
112071
113136
  * Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
@@ -112121,8 +113186,13 @@ ${value2}`;
112121
113186
  cohortId,
112122
113187
  beaconAddress: info.beaconAddress
112123
113188
  });
112124
- await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
112125
- this.emit("update-submitted", { cohortId });
113189
+ if (signedUpdate === null) {
113190
+ await this.#sendAll(this.session.declineUpdate(cohortId));
113191
+ this.emit("update-declined", { cohortId });
113192
+ } else {
113193
+ await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
113194
+ this.emit("update-submitted", { cohortId });
113195
+ }
112126
113196
  } catch (err) {
112127
113197
  this.emit("error", err);
112128
113198
  }
@@ -112198,23 +113268,62 @@ ${value2}`;
112198
113268
  const cohortId = msg.body?.cohortId;
112199
113269
  if (!cohortId) return;
112200
113270
  await this.#sendAll(this.session.generatePartialSignature(cohortId));
112201
- if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
112202
- const info = this.session.joinedCohorts.get(cohortId);
112203
- if (info) {
112204
- const validation = this.session.pendingValidations.get(cohortId);
112205
- this.emit("cohort-complete", {
112206
- cohortId,
112207
- beaconAddress: info.beaconAddress,
112208
- beaconType: validation?.beaconType ?? "",
112209
- casAnnouncement: validation?.casAnnouncement,
112210
- smtProof: validation?.smtProof
112211
- });
112212
- }
113271
+ this.#emitCohortCompleteIfDone(cohortId);
113272
+ } catch (err) {
113273
+ this.emit("error", err);
113274
+ }
113275
+ }
113276
+ /**
113277
+ * Internal: handler for fallback authorization requests. The service abandoned
113278
+ * the optimistic key path; the member authorizes the k-of-n script-path spend
113279
+ * of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
113280
+ * the decision, signs the fallback, and completes once it has contributed (the
113281
+ * service needs only k members).
113282
+ * @param {BaseMessage} msg - The received fallback authorization request message.
113283
+ * @returns {Promise<void>} Resolves when processing is complete.
113284
+ */
113285
+ async #handleFallbackAuthorizationRequest(msg) {
113286
+ if (this.#stopped) return;
113287
+ try {
113288
+ this.session.receive(msg);
113289
+ const cohortId = msg.body?.cohortId;
113290
+ if (!cohortId) return;
113291
+ const req = this.session.pendingFallbackRequests.get(cohortId);
113292
+ if (!req) return;
113293
+ this.emit("fallback-requested", req);
113294
+ const decision = await this.#onApproveSigning(req);
113295
+ if (!decision.approved) {
113296
+ this.emit("cohort-failed", { cohortId, reason: "Fallback signing rejected by participant" });
113297
+ return;
112213
113298
  }
113299
+ await this.#sendAll(this.session.approveFallback(cohortId));
113300
+ this.#emitCohortCompleteIfDone(cohortId);
112214
113301
  } catch (err) {
112215
113302
  this.emit("error", err);
112216
113303
  }
112217
113304
  }
113305
+ /**
113306
+ * Internal: emit `cohort-complete` with the participant's retained sidecar once
113307
+ * the cohort has reached the Complete phase. Surfaces the CAS Announcement map
113308
+ * (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
113309
+ * DID resolution. Read via getValidation (not pendingValidations, which lists
113310
+ * only the AwaitingValidation phase) so the sidecar is still available at
113311
+ * Complete. Shared by the optimistic and fallback completion paths.
113312
+ */
113313
+ #emitCohortCompleteIfDone(cohortId) {
113314
+ if (this.session.getCohortPhase(cohortId) !== "Complete" /* Complete */) return;
113315
+ const info = this.session.joinedCohorts.get(cohortId);
113316
+ if (!info) return;
113317
+ const validation = this.session.getValidation(cohortId);
113318
+ this.emit("cohort-complete", {
113319
+ cohortId,
113320
+ beaconAddress: info.beaconAddress,
113321
+ beaconType: validation?.beaconType ?? "",
113322
+ included: validation?.included ?? true,
113323
+ casAnnouncement: validation?.casAnnouncement,
113324
+ smtProof: validation?.smtProof
113325
+ });
113326
+ }
112218
113327
  /**
112219
113328
  * Internal: send helper to ensure messages are sent sequentially. This is important for protocol
112220
113329
  * correctness, as some transports may not guarantee message order if sent in parallel.
@@ -112231,6 +113340,7 @@ ${value2}`;
112231
113340
 
112232
113341
  // src/core/aggregation/runner/aggregation-runner.ts
112233
113342
  init_shim();
113343
+ init_utils();
112234
113344
  var AggregationRunner = class {
112235
113345
  /**
112236
113346
  * Run a cohort of ONE participant entirely in-process and return the
@@ -112238,8 +113348,8 @@ ${value2}`;
112238
113348
  *
112239
113349
  * One party plays both the coordinating service and the lone participant,
112240
113350
  * connected over an {@link InMemoryTransport} (no relay or HTTP server). This
112241
- * makes the single-participant aggregate-beacon path the N=1 corner of the
112242
- * two-axis beacon matrix (see ADR 037) first-class, useful for generating
113351
+ * makes the single-participant aggregate-beacon path (the N=1 corner of the
113352
+ * two-axis beacon matrix, see ADR 037) first-class, useful for generating
112243
113353
  * and reproducing single-participant aggregate test vectors.
112244
113354
  *
112245
113355
  * The service advertises a cohort with `minParticipants: 1`; the participant
@@ -112257,11 +113367,13 @@ ${value2}`;
112257
113367
  transport.registerPeer(options2.participant.did, options2.participant.keys.publicKey.compressed);
112258
113368
  transport.registerPeer(options2.service.did, options2.service.keys.publicKey.compressed);
112259
113369
  transport.start();
113370
+ const recoveryKey = options2.config.recoveryKey ?? bytesToHex(options2.service.keys.publicKey.compressed.slice(1));
113371
+ const recoverySequence = options2.config.recoverySequence ?? DEFAULT_RECOVERY_SEQUENCE;
112260
113372
  const service = new AggregationServiceRunner({
112261
113373
  transport,
112262
113374
  did: options2.service.did,
112263
113375
  keys: options2.service.keys,
112264
- config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType },
113376
+ config: { minParticipants: 1, network: options2.config.network, beaconType: options2.config.beaconType, recoveryKey, recoverySequence, fundingModel: DEFAULT_FUNDING_MODEL },
112265
113377
  onProvideTxData: options2.onProvideTxData,
112266
113378
  cohortTtlMs: options2.cohortTtlMs,
112267
113379
  phaseTimeoutMs: options2.phaseTimeoutMs,
@@ -112391,8 +113503,7 @@ ${value2}`;
112391
113503
  async function buildAggregationBeaconTx(opts) {
112392
113504
  const feeEstimator = opts.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
112393
113505
  const { utxo, prevTxBytes } = await fetchSpendableUtxo(opts.beaconAddress, opts.bitcoin);
112394
- const tapOut = p2tr(opts.internalPubkey, void 0, opts.network);
112395
- const witnessScript = tapOut.script;
113506
+ const witnessScript = OutScript.encode(Address(opts.network).decode(opts.beaconAddress));
112396
113507
  const feeSats = await feeEstimator.estimateFee(P2TR_BEACON_TX_VSIZE);
112397
113508
  if (BigInt(utxo.value) <= feeSats) {
112398
113509
  throw new BeaconError(
@@ -112471,7 +113582,7 @@ ${value2}`;
112471
113582
  *
112472
113583
  * Composed from the three extracted phases ({@link buildSinglePartyTx},
112473
113584
  * {@link signSinglePartyTx}, {@link broadcastRawTx}) so each piece can be exercised
112474
- * in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead
113585
+ * in isolation. Aggregation beacons use {@link buildAggregationBeaconTx} instead:
112475
113586
  * the multi-party path can't share the signing phase, but the tx-construction
112476
113587
  * plumbing (UTXO fetch + OP_RETURN output + change output) is shared.
112477
113588
  *
@@ -112503,7 +113614,7 @@ ${value2}`;
112503
113614
  *
112504
113615
  * Detects the beacon address script kind (P2PKH / P2WPKH / P2TR) and configures
112505
113616
  * the input accordingly. Validates that the signer's pubkey produces the beacon
112506
- * address under that script kind without this check, a misconfigured caller
113617
+ * address under that script kind: without this check, a misconfigured caller
112507
113618
  * would burn a real UTXO on a tx that fails at broadcast. Fees are computed from
112508
113619
  * the per-kind {@link SINGLETON_BEACON_TX_VSIZE} constant, avoiding any probe-sign
112509
113620
  * round-trip.
@@ -128390,7 +129501,7 @@ ${value2}`;
128390
129501
  #verificationMethod;
128391
129502
  #beaconService;
128392
129503
  /**
128393
- * @internal Use {@link DidBtcr2.update} to create instances.
129504
+ * @internal Use {@link DidBtcr2.update} to create instances.
128394
129505
  */
128395
129506
  constructor(params) {
128396
129507
  this.#sourceDocument = params.sourceDocument;
@@ -128427,7 +129538,7 @@ ${value2}`;
128427
129538
  const targetDocument = JSONPatch.apply(sourceDocument, patches);
128428
129539
  if (targetDocument.id !== sourceDocument.id) {
128429
129540
  throw new UpdateError(
128430
- `Patches must not change the DID document id (source "${sourceDocument.id}" \u2192 target "${targetDocument.id}").`,
129541
+ `Patches must not change the DID document id (source "${sourceDocument.id}" to target "${targetDocument.id}").`,
128431
129542
  INVALID_DID_UPDATE,
128432
129543
  { sourceId: sourceDocument.id, targetId: targetDocument.id }
128433
129544
  );
@@ -128511,8 +129622,8 @@ ${value2}`;
128511
129622
  }
128512
129623
  /**
128513
129624
  * Advance the state machine. Returns either:
128514
- * - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
128515
- * - `{ status: 'complete', result }` update is signed and broadcast
129625
+ * - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
129626
+ * - `{ status: 'complete', result }` update is signed and broadcast
128516
129627
  */
128517
129628
  advance() {
128518
129629
  while (true) {
@@ -128525,7 +129636,7 @@ ${value2}`;
128525
129636
  continue;
128526
129637
  }
128527
129638
  // Phase: Sign
128528
- // Emit NeedSigningKey the caller supplies the secret key (or a KMS signature).
129639
+ // Emit NeedSigningKey: the caller supplies the secret key (or a KMS signature).
128529
129640
  case "Sign": {
128530
129641
  return {
128531
129642
  status: "action-required",
@@ -128708,8 +129819,8 @@ ${value2}`;
128708
129819
  *
128709
129820
  * Factory method that validates the update parameters and returns a sans-I/O
128710
129821
  * {@link Updater} state machine. The caller drives the updater through its
128711
- * phases (Construct Sign Broadcast Complete) by calling `advance()` and
128712
- * `provide()`. The method package performs **zero I/O** signing key retrieval
129822
+ * phases (Construct -> Sign -> Broadcast -> Complete) by calling `advance()` and
129823
+ * `provide()`. The method package performs **zero I/O**: signing key retrieval
128713
129824
  * (or KMS delegation) and the on-chain broadcast are the caller's responsibility.
128714
129825
  *
128715
129826
  * For a fully-wired version with Bitcoin broadcast and key handling, see
@@ -128827,7 +129938,7 @@ ${value2}`;
128827
129938
  #unsortedUpdates = [];
128828
129939
  #resolvedResponse = null;
128829
129940
  /**
128830
- * @internal Use {@link DidBtcr2.resolve} to create instances.
129941
+ * @internal Use {@link DidBtcr2.resolve} to create instances.
128831
129942
  */
128832
129943
  constructor(didComponents, sidecarData, currentDocument, options2) {
128833
129944
  this.#didComponents = didComponents;
@@ -129063,8 +130174,8 @@ ${value2}`;
129063
130174
  }
129064
130175
  /**
129065
130176
  * Advance the state machine. Returns either:
129066
- * - `{ status: 'action-required', needs }` caller must provide data via {@link provide}
129067
- * - `{ status: 'resolved', result }` resolution complete
130177
+ * - `{ status: 'action-required', needs }` - caller must provide data via {@link provide}
130178
+ * - `{ status: 'resolved', result }` - resolution complete
129068
130179
  *
129069
130180
  * Analogous to Rust's `Resolver::resolve()`.
129070
130181
  */