@did-btcr2/method 0.28.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +13 -5
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/browser.js +34125 -44647
  4. package/dist/browser.mjs +26409 -36931
  5. package/dist/cjs/index.js +2869 -679
  6. package/dist/esm/core/aggregation/beacon-strategy.js +62 -0
  7. package/dist/esm/core/aggregation/beacon-strategy.js.map +1 -0
  8. package/dist/esm/core/aggregation/cohort.js +31 -8
  9. package/dist/esm/core/aggregation/cohort.js.map +1 -1
  10. package/dist/esm/core/aggregation/logger.js +15 -0
  11. package/dist/esm/core/aggregation/logger.js.map +1 -0
  12. package/dist/esm/core/aggregation/messages/base.js +12 -1
  13. package/dist/esm/core/aggregation/messages/base.js.map +1 -1
  14. package/dist/esm/core/aggregation/messages/bodies.js +90 -0
  15. package/dist/esm/core/aggregation/messages/bodies.js.map +1 -0
  16. package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
  17. package/dist/esm/core/aggregation/messages/index.js +1 -0
  18. package/dist/esm/core/aggregation/messages/index.js.map +1 -1
  19. package/dist/esm/core/aggregation/participant.js +39 -46
  20. package/dist/esm/core/aggregation/participant.js.map +1 -1
  21. package/dist/esm/core/aggregation/runner/participant-runner.js +33 -7
  22. package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
  23. package/dist/esm/core/aggregation/runner/service-runner.js +198 -19
  24. package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
  25. package/dist/esm/core/aggregation/service.js +143 -15
  26. package/dist/esm/core/aggregation/service.js.map +1 -1
  27. package/dist/esm/core/aggregation/signing-session.js +44 -5
  28. package/dist/esm/core/aggregation/signing-session.js.map +1 -1
  29. package/dist/esm/core/aggregation/transport/didcomm.js +9 -0
  30. package/dist/esm/core/aggregation/transport/didcomm.js.map +1 -1
  31. package/dist/esm/core/aggregation/transport/factory.js +15 -6
  32. package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
  33. package/dist/esm/core/aggregation/transport/http/client.js +350 -0
  34. package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
  35. package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
  36. package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
  37. package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
  38. package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
  39. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
  40. package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
  41. package/dist/esm/core/aggregation/transport/http/index.js +12 -0
  42. package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
  43. package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
  44. package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
  45. package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
  46. package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
  47. package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
  48. package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
  49. package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
  50. package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
  51. package/dist/esm/core/aggregation/transport/http/server.js +481 -0
  52. package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
  53. package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
  54. package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
  55. package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
  56. package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
  57. package/dist/esm/core/aggregation/transport/index.js +1 -0
  58. package/dist/esm/core/aggregation/transport/index.js.map +1 -1
  59. package/dist/esm/core/aggregation/transport/nostr.js +245 -16
  60. package/dist/esm/core/aggregation/transport/nostr.js.map +1 -1
  61. package/dist/esm/core/beacon/beacon.js +295 -63
  62. package/dist/esm/core/beacon/beacon.js.map +1 -1
  63. package/dist/esm/core/beacon/cas-beacon.js +3 -3
  64. package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
  65. package/dist/esm/core/beacon/singleton-beacon.js +3 -3
  66. package/dist/esm/core/beacon/singleton-beacon.js.map +1 -1
  67. package/dist/esm/core/beacon/smt-beacon.js +3 -3
  68. package/dist/esm/core/beacon/smt-beacon.js.map +1 -1
  69. package/dist/esm/core/beacon/utils.js +14 -9
  70. package/dist/esm/core/beacon/utils.js.map +1 -1
  71. package/dist/esm/core/updater.js +63 -55
  72. package/dist/esm/core/updater.js.map +1 -1
  73. package/dist/esm/did-btcr2.js +0 -4
  74. package/dist/esm/did-btcr2.js.map +1 -1
  75. package/dist/esm/index.js +2 -0
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/utils/did-document.js +2 -2
  78. package/dist/esm/utils/did-document.js.map +1 -1
  79. package/dist/types/core/aggregation/beacon-strategy.d.ts +52 -0
  80. package/dist/types/core/aggregation/beacon-strategy.d.ts.map +1 -0
  81. package/dist/types/core/aggregation/cohort.d.ts +20 -3
  82. package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
  83. package/dist/types/core/aggregation/logger.d.ts +22 -0
  84. package/dist/types/core/aggregation/logger.d.ts.map +1 -0
  85. package/dist/types/core/aggregation/messages/base.d.ts +13 -1
  86. package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
  87. package/dist/types/core/aggregation/messages/bodies.d.ts +130 -0
  88. package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -0
  89. package/dist/types/core/aggregation/messages/factories.d.ts +1 -0
  90. package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
  91. package/dist/types/core/aggregation/messages/index.d.ts +1 -0
  92. package/dist/types/core/aggregation/messages/index.d.ts.map +1 -1
  93. package/dist/types/core/aggregation/participant.d.ts +2 -0
  94. package/dist/types/core/aggregation/participant.d.ts.map +1 -1
  95. package/dist/types/core/aggregation/runner/events.d.ts +32 -6
  96. package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
  97. package/dist/types/core/aggregation/runner/participant-runner.d.ts +7 -5
  98. package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
  99. package/dist/types/core/aggregation/runner/service-runner.d.ts +33 -3
  100. package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
  101. package/dist/types/core/aggregation/service.d.ts +33 -2
  102. package/dist/types/core/aggregation/service.d.ts.map +1 -1
  103. package/dist/types/core/aggregation/signing-session.d.ts +5 -1
  104. package/dist/types/core/aggregation/signing-session.d.ts.map +1 -1
  105. package/dist/types/core/aggregation/transport/didcomm.d.ts +3 -0
  106. package/dist/types/core/aggregation/transport/didcomm.d.ts.map +1 -1
  107. package/dist/types/core/aggregation/transport/factory.d.ts +22 -7
  108. package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
  109. package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
  110. package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
  111. package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
  112. package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
  113. package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
  114. package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
  115. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
  116. package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
  117. package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
  118. package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
  119. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
  120. package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
  121. package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
  122. package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
  123. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
  124. package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
  125. package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
  126. package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
  127. package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
  128. package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
  129. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
  130. package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
  131. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
  132. package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
  133. package/dist/types/core/aggregation/transport/index.d.ts +1 -0
  134. package/dist/types/core/aggregation/transport/index.d.ts.map +1 -1
  135. package/dist/types/core/aggregation/transport/nostr.d.ts +99 -1
  136. package/dist/types/core/aggregation/transport/nostr.d.ts.map +1 -1
  137. package/dist/types/core/aggregation/transport/transport.d.ts +26 -1
  138. package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
  139. package/dist/types/core/beacon/beacon.d.ts +149 -22
  140. package/dist/types/core/beacon/beacon.d.ts.map +1 -1
  141. package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
  142. package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
  143. package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
  144. package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
  145. package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
  146. package/dist/types/core/beacon/smt-beacon.d.ts.map +1 -1
  147. package/dist/types/core/beacon/utils.d.ts +2 -2
  148. package/dist/types/core/beacon/utils.d.ts.map +1 -1
  149. package/dist/types/core/updater.d.ts +27 -12
  150. package/dist/types/core/updater.d.ts.map +1 -1
  151. package/dist/types/did-btcr2.d.ts.map +1 -1
  152. package/dist/types/index.d.ts +2 -0
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/package.json +5 -7
  155. package/src/core/aggregation/beacon-strategy.ts +123 -0
  156. package/src/core/aggregation/cohort.ts +34 -8
  157. package/src/core/aggregation/logger.ts +33 -0
  158. package/src/core/aggregation/messages/base.ts +20 -5
  159. package/src/core/aggregation/messages/bodies.ts +223 -0
  160. package/src/core/aggregation/messages/factories.ts +1 -0
  161. package/src/core/aggregation/messages/index.ts +1 -0
  162. package/src/core/aggregation/participant.ts +40 -46
  163. package/src/core/aggregation/runner/events.ts +27 -3
  164. package/src/core/aggregation/runner/participant-runner.ts +41 -7
  165. package/src/core/aggregation/runner/service-runner.ts +227 -19
  166. package/src/core/aggregation/service.ts +189 -20
  167. package/src/core/aggregation/signing-session.ts +65 -7
  168. package/src/core/aggregation/transport/didcomm.ts +17 -0
  169. package/src/core/aggregation/transport/factory.ts +48 -12
  170. package/src/core/aggregation/transport/http/client.ts +409 -0
  171. package/src/core/aggregation/transport/http/envelope.ts +204 -0
  172. package/src/core/aggregation/transport/http/errors.ts +11 -0
  173. package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
  174. package/src/core/aggregation/transport/http/index.ts +11 -0
  175. package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
  176. package/src/core/aggregation/transport/http/protocol.ts +57 -0
  177. package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
  178. package/src/core/aggregation/transport/http/request-auth.ts +164 -0
  179. package/src/core/aggregation/transport/http/server.ts +615 -0
  180. package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
  181. package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
  182. package/src/core/aggregation/transport/index.ts +1 -0
  183. package/src/core/aggregation/transport/nostr.ts +266 -23
  184. package/src/core/aggregation/transport/transport.ts +34 -1
  185. package/src/core/beacon/beacon.ts +411 -79
  186. package/src/core/beacon/cas-beacon.ts +4 -4
  187. package/src/core/beacon/singleton-beacon.ts +4 -4
  188. package/src/core/beacon/smt-beacon.ts +4 -4
  189. package/src/core/beacon/utils.ts +16 -11
  190. package/src/core/updater.ts +113 -67
  191. package/src/did-btcr2.ts +0 -5
  192. package/src/index.ts +2 -0
  193. package/src/utils/did-document.ts +2 -2
@@ -1,10 +1,14 @@
1
+ import { canonicalize } from '@did-btcr2/common';
1
2
  import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
3
+ import { BIP340Cryptosuite, SchnorrMultikey } from '@did-btcr2/cryptosuite';
2
4
  import type { SchnorrKeyPair } from '@did-btcr2/keypair';
3
5
  import { bytesToHex } from '@noble/hashes/utils';
4
- import type { Transaction } from 'bitcoinjs-lib';
6
+ import type { Transaction } from '@scure/btc-signer';
7
+ import { getBeaconStrategy } from './beacon-strategy.js';
5
8
  import { AggregationCohort } from './cohort.js';
6
9
  import { AggregationServiceError } from './errors.js';
7
10
  import type { BaseMessage } from './messages/base.js';
11
+ import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
8
12
  import {
9
13
  COHORT_OPT_IN,
10
14
  NONCE_CONTRIBUTION,
@@ -13,12 +17,12 @@ import {
13
17
  VALIDATION_ACK,
14
18
  } from './messages/constants.js';
15
19
  import {
20
+ createAggregatedNonceMessage,
16
21
  createAuthorizationRequestMessage,
17
22
  createCohortAdvertMessage,
18
23
  createCohortOptInAcceptMessage,
19
24
  createCohortReadyMessage,
20
25
  createDistributeAggregatedDataMessage,
21
- createAggregatedNonceMessage,
22
26
  } from './messages/factories.js';
23
27
  import type { ServiceCohortPhaseType } from './phases.js';
24
28
  import { ServiceCohortPhase } from './phases.js';
@@ -61,6 +65,23 @@ export interface SigningTxData {
61
65
  prevOutValues: bigint[];
62
66
  }
63
67
 
68
+ /** Reason an incoming message was silently dropped by the state machine. */
69
+ export type RejectionReason =
70
+ | 'WRONG_VERSION'
71
+ | 'UPDATE_TOO_LARGE'
72
+ | 'UPDATE_VERIFICATION_FAILED'
73
+ | 'UPDATE_MALFORMED';
74
+
75
+ /** Record of a silently-dropped inbound message. Drained by the runner to emit events. */
76
+ export interface Rejection {
77
+ /** DID of the sender whose message was rejected. */
78
+ from: string;
79
+ /** Machine-readable code. */
80
+ code: RejectionReason;
81
+ /** Human-readable reason. */
82
+ reason: string;
83
+ }
84
+
64
85
  /** Per-cohort service state — internal. */
65
86
  interface ServiceCohortState {
66
87
  phase: ServiceCohortPhaseType;
@@ -70,11 +91,22 @@ interface ServiceCohortState {
70
91
  acceptedParticipants: Set<string>;
71
92
  signingSession?: BeaconSigningSession;
72
93
  result?: AggregationResult;
94
+ /** Rejections accumulated since last drain. Runner polls via drainRejections(). */
95
+ rejections: Array<Rejection>;
73
96
  }
74
97
 
98
+ /** Default maximum canonicalized byte-length of a submitted BTCR2 update. */
99
+ export const DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
100
+
75
101
  export interface AggregationServiceParams {
76
102
  did: string;
77
103
  keys: SchnorrKeyPair;
104
+ /**
105
+ * Maximum canonicalized byte-length of a signed update body accepted by the
106
+ * service. Submissions above this cap are silently dropped and surfaced as
107
+ * `UPDATE_TOO_LARGE` rejections. Defaults to {@link DEFAULT_MAX_UPDATE_SIZE_BYTES}.
108
+ */
109
+ maxUpdateSizeBytes?: number;
78
110
  }
79
111
 
80
112
  /**
@@ -91,17 +123,36 @@ export interface AggregationServiceParams {
91
123
  export class AggregationService {
92
124
  readonly did: string;
93
125
  readonly keys: SchnorrKeyPair;
126
+ readonly maxUpdateSizeBytes: number;
94
127
 
95
128
  /** Per-cohort state, keyed by cohortId. */
96
129
  #cohortStates: Map<string, ServiceCohortState> = new Map();
97
130
 
98
- constructor({ did, keys }: AggregationServiceParams) {
131
+ constructor({ did, keys, maxUpdateSizeBytes }: AggregationServiceParams) {
99
132
  this.did = did;
100
133
  this.keys = keys;
134
+ this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
101
135
  }
102
136
 
103
137
 
104
138
  receive(message: BaseMessage): void {
139
+ // Reject messages whose wire version doesn't match what this build speaks.
140
+ // Missing version → treat as legacy and drop: bumping the protocol must be
141
+ // coordinated across all participants.
142
+ const version = message.version;
143
+ if(version === undefined || version !== AGGREGATION_WIRE_VERSION) {
144
+ const cohortId = message.body?.cohortId;
145
+ const state = cohortId ? this.#cohortStates.get(cohortId) : undefined;
146
+ if(state) {
147
+ state.rejections.push({
148
+ from : message.from,
149
+ code : 'WRONG_VERSION',
150
+ reason : `Expected wire version ${AGGREGATION_WIRE_VERSION}, got ${String(version)}`,
151
+ });
152
+ }
153
+ return;
154
+ }
155
+
105
156
  const type = message.type;
106
157
  switch(type) {
107
158
  case COHORT_OPT_IN:
@@ -125,6 +176,19 @@ export class AggregationService {
125
176
  }
126
177
  }
127
178
 
179
+ /**
180
+ * Drain the rejection log for a cohort. Used by runners to surface silent
181
+ * drops (bad proof, oversized update, wrong version, etc.) as structured
182
+ * events without breaking the sans-I/O state machine contract.
183
+ */
184
+ drainRejections(cohortId: string): Array<Rejection> {
185
+ const state = this.#cohortStates.get(cohortId);
186
+ if(!state) return [];
187
+ const out = state.rejections;
188
+ state.rejections = [];
189
+ return out;
190
+ }
191
+
128
192
 
129
193
  /**
130
194
  * Create a new cohort with the given config. Returns the cohort ID.
@@ -143,6 +207,7 @@ export class AggregationService {
143
207
  config,
144
208
  pendingOptIns : new Map(),
145
209
  acceptedParticipants : new Set(),
210
+ rejections : [],
146
211
  });
147
212
  return cohort.id;
148
213
  }
@@ -202,6 +267,13 @@ export class AggregationService {
202
267
  const communicationPk = message.body?.communicationPk;
203
268
  if(!participantPk || !communicationPk) return;
204
269
 
270
+ // Reject re-opt-in from already-accepted participants. Without this guard a
271
+ // participant could send a second opt-in with a different key, overwriting
272
+ // pendingOptIns[did] while cohortKeys still holds the original key — opening
273
+ // a desync window where #verifySubmittedUpdate accepts updates signed with
274
+ // a key that is NOT in the MuSig2 cohort.
275
+ if(state.acceptedParticipants.has(participantDid)) return;
276
+
205
277
  state.pendingOptIns.set(participantDid, {
206
278
  cohortId,
207
279
  participantDid,
@@ -235,6 +307,7 @@ export class AggregationService {
235
307
 
236
308
  state.acceptedParticipants.add(participantDid);
237
309
  state.cohort.participants.push(participantDid);
310
+ state.cohort.participantKeys.set(participantDid, optIn.participantPk);
238
311
  state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
239
312
 
240
313
  return [createCohortOptInAcceptMessage({
@@ -290,6 +363,13 @@ export class AggregationService {
290
363
  return state.cohort.pendingUpdates;
291
364
  }
292
365
 
366
+ /**
367
+ * Handle an incoming SUBMIT_UPDATE message from a participant containing their signed update to
368
+ * submit for aggregation.
369
+ * @param {BaseMessage} message - incoming SUBMIT_UPDATE message containing a participant's signed
370
+ * update to submit for aggregation
371
+ * @returns {void} - no return value; updates the service state with the submitted update if valid
372
+ */
293
373
  #handleSubmitUpdate(message: BaseMessage): void {
294
374
  const cohortId = message.body?.cohortId;
295
375
  if(!cohortId) return;
@@ -297,10 +377,43 @@ export class AggregationService {
297
377
  if(!state) return;
298
378
  if(state.phase !== ServiceCohortPhase.CohortSet && state.phase !== ServiceCohortPhase.CollectingUpdates) return;
299
379
 
300
- const signedUpdate = message.body?.signedUpdate;
301
- if(!signedUpdate) return;
380
+ const signedUpdate = message.body?.signedUpdate as SignedBTCR2Update | undefined;
381
+ if(!signedUpdate) {
382
+ state.rejections.push({
383
+ from : message.from,
384
+ code : 'UPDATE_MALFORMED',
385
+ reason : 'SUBMIT_UPDATE missing signedUpdate body',
386
+ });
387
+ return;
388
+ }
389
+
390
+ // Cap the canonicalized update size before doing any heavier verification
391
+ // work. Without this guard, a participant could submit multi-MB payloads
392
+ // that the service would canonicalize, hash, and aggregate — cheap DoS.
393
+ const canonicalSize = canonicalize(signedUpdate as unknown as Record<string, unknown>).length;
394
+ if(canonicalSize > this.maxUpdateSizeBytes) {
395
+ state.rejections.push({
396
+ from : message.from,
397
+ code : 'UPDATE_TOO_LARGE',
398
+ reason : `Canonicalized update is ${canonicalSize} bytes; max allowed is ${this.maxUpdateSizeBytes}`,
399
+ });
400
+ return;
401
+ }
402
+
403
+ // Verify the BIP-340 Data Integrity proof before aggregating. Without this check,
404
+ // a malicious cohort member could submit updates with garbage proofs, which the
405
+ // service would aggregate into the CAS announcement / SMT root and ultimately
406
+ // anchor on-chain with the cohort's MuSig2 signature.
407
+ if(!this.#verifySubmittedUpdate(state, message.from, signedUpdate)) {
408
+ state.rejections.push({
409
+ from : message.from,
410
+ code : 'UPDATE_VERIFICATION_FAILED',
411
+ reason : 'BIP-340 Data Integrity proof verification failed',
412
+ });
413
+ return;
414
+ }
302
415
 
303
- state.cohort.addUpdate(message.from, signedUpdate as unknown as SignedBTCR2Update);
416
+ state.cohort.addUpdate(message.from, signedUpdate);
304
417
 
305
418
  if(state.phase === ServiceCohortPhase.CohortSet) {
306
419
  state.phase = ServiceCohortPhase.CollectingUpdates;
@@ -310,6 +423,46 @@ export class AggregationService {
310
423
  }
311
424
  }
312
425
 
426
+ /**
427
+ * Verify the BIP-340 Schnorr Data Integrity proof on a submitted update using the
428
+ * participant's public key from their cohort opt-in. Returns `false` (and the
429
+ * update is silently dropped) if the proof is missing, the verificationMethod does
430
+ * not name the sender's DID, the participant has no opt-in on record, or the
431
+ * signature fails verification.
432
+ * @param {ServiceCohortState} state - the current state of the cohort to which the update was submitted
433
+ * @param {string} sender - the DID of the participant who submitted the update
434
+ * @param {SignedBTCR2Update} signedUpdate - the signed update containing the proof to verify
435
+ * @returns {boolean} - `true` if the proof is valid and the update can be accepted; `false` otherwise
436
+ */
437
+ #verifySubmittedUpdate(
438
+ state: ServiceCohortState,
439
+ sender: string,
440
+ signedUpdate: SignedBTCR2Update,
441
+ ): boolean {
442
+ const proof = signedUpdate.proof;
443
+ if(!proof?.verificationMethod || !proof.proofValue) return false;
444
+
445
+ // The proof must be signed by the sender's own key. Reject if the
446
+ // verificationMethod references a different DID.
447
+ const vmDid = proof.verificationMethod.split('#')[0];
448
+ if(vmDid !== sender) return false;
449
+
450
+ const optIn = state.pendingOptIns.get(sender);
451
+ if(!optIn) return false;
452
+
453
+ try {
454
+ const multikey = SchnorrMultikey.fromPublicKey({
455
+ id : proof.verificationMethod,
456
+ controller : sender,
457
+ publicKeyBytes : optIn.participantPk,
458
+ }) as SchnorrMultikey;
459
+ const suite = new BIP340Cryptosuite(multikey);
460
+ return suite.verifyProof(signedUpdate).verified === true;
461
+ } catch {
462
+ return false;
463
+ }
464
+ }
465
+
313
466
 
314
467
  /**
315
468
  * Build the aggregated data structure (CAS Announcement or SMT tree) and
@@ -327,30 +480,29 @@ export class AggregationService {
327
480
  );
328
481
  }
329
482
 
330
- if(state.config.beaconType === 'CASBeacon') {
331
- state.cohort.buildCASAnnouncement();
332
- } else if(state.config.beaconType === 'SMTBeacon') {
333
- state.cohort.buildSMTTree();
334
- } else {
483
+ const strategy = getBeaconStrategy(state.config.beaconType);
484
+ if(!strategy) {
335
485
  throw new AggregationServiceError(
336
486
  `Unsupported beacon type: ${state.config.beaconType}`,
337
487
  'UNSUPPORTED_BEACON_TYPE', { cohortId, beaconType: state.config.beaconType }
338
488
  );
339
489
  }
490
+ strategy.buildAggregatedData(state.cohort);
340
491
 
341
492
  const signalBytesHex = bytesToHex(state.cohort.signalBytes!);
342
493
  state.phase = ServiceCohortPhase.DataDistributed;
343
494
 
344
495
  const messages: BaseMessage[] = [];
345
496
  for(const participantDid of state.cohort.participants) {
497
+ const payload = strategy.getDistributePayload(state.cohort, participantDid);
346
498
  messages.push(createDistributeAggregatedDataMessage({
347
499
  from : this.did,
348
500
  to : participantDid,
349
501
  cohortId,
350
502
  beaconType : state.config.beaconType,
351
503
  signalBytesHex,
352
- casAnnouncement : state.config.beaconType === 'CASBeacon' ? state.cohort.casAnnouncement : undefined,
353
- smtProof : state.config.beaconType === 'SMTBeacon' ? state.cohort.smtProofs?.get(participantDid) as unknown as Record<string, unknown> : undefined,
504
+ casAnnouncement : payload.casAnnouncement,
505
+ smtProof : payload.smtProof,
354
506
  }));
355
507
  }
356
508
  return messages;
@@ -422,15 +574,24 @@ export class AggregationService {
422
574
  state.signingSession = session;
423
575
  state.phase = ServiceCohortPhase.SigningStarted;
424
576
 
577
+ const prevOutScript = txData.prevOutScripts[0];
578
+ if(!prevOutScript) {
579
+ throw new AggregationServiceError(
580
+ `Cannot start signing for cohort ${cohortId}: txData.prevOutScripts[0] is missing.`,
581
+ 'MISSING_PREV_OUT_SCRIPT', { cohortId }
582
+ );
583
+ }
584
+
425
585
  const messages: BaseMessage[] = [];
426
586
  for(const participantDid of state.cohort.participants) {
427
587
  messages.push(createAuthorizationRequestMessage({
428
- from : this.did,
429
- to : participantDid,
588
+ from : this.did,
589
+ to : participantDid,
430
590
  cohortId,
431
- sessionId : session.id,
432
- pendingTx : txData.tx.toHex(),
433
- prevOutValue : txData.prevOutValues[0]?.toString() ?? '0',
591
+ sessionId : session.id,
592
+ pendingTx : txData.tx.hex,
593
+ prevOutScriptHex : bytesToHex(prevOutScript),
594
+ prevOutValue : txData.prevOutValues[0]?.toString() ?? '0',
434
595
  }));
435
596
  }
436
597
  return messages;
@@ -507,8 +668,8 @@ export class AggregationService {
507
668
  // All partial sigs received — generate final signature
508
669
  const signature = state.signingSession.generateFinalSignature();
509
670
 
510
- // Set Taproot key-path witness
511
- state.signingSession.pendingTx.setWitness(0, [signature]);
671
+ // Set Taproot key-path witness (finalScriptWitness injects the aggregated MuSig2 sig)
672
+ state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
512
673
 
513
674
  state.result = {
514
675
  cohortId,
@@ -544,4 +705,12 @@ export class AggregationService {
544
705
  get cohorts(): ReadonlyArray<AggregationCohort> {
545
706
  return [...this.#cohortStates.values()].map(s => s.cohort);
546
707
  }
708
+
709
+ /**
710
+ * Remove a cohort from the state map. Used by runners to GC state on cohort
711
+ * completion, failure, or expiry. No-op if the cohort doesn't exist.
712
+ */
713
+ removeCohort(cohortId: string): void {
714
+ this.#cohortStates.delete(cohortId);
715
+ }
547
716
  }
@@ -1,5 +1,6 @@
1
+ import type { Transaction } from '@scure/btc-signer';
2
+ import { SigHash } from '@scure/btc-signer';
1
3
  import * as musig2 from '@scure/btc-signer/musig2';
2
- import { Transaction } from 'bitcoinjs-lib';
3
4
  import type { AggregationCohort } from './cohort.js';
4
5
  import { SigningSessionError } from './errors.js';
5
6
  import type { SigningSessionPhaseType } from './phases.js';
@@ -79,11 +80,11 @@ export class BeaconSigningSession {
79
80
  'SIGHASH_ERROR'
80
81
  );
81
82
  }
82
- return this.pendingTx.hashForWitnessV1(
83
+ return this.pendingTx.preimageWitnessV1(
83
84
  0,
84
85
  this.prevOutScripts,
85
- this.prevOutValues,
86
- Transaction.SIGHASH_DEFAULT
86
+ SigHash.DEFAULT,
87
+ this.prevOutValues
87
88
  );
88
89
  }
89
90
 
@@ -94,6 +95,12 @@ export class BeaconSigningSession {
94
95
  'INVALID_PHASE', { phase: this.phase }
95
96
  );
96
97
  }
98
+ if(!this.cohort.participants.includes(participantDid)) {
99
+ throw new SigningSessionError(
100
+ `Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
101
+ 'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
102
+ );
103
+ }
97
104
  if(nonceContribution.length !== 66) {
98
105
  throw new SigningSessionError(
99
106
  `Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
@@ -132,6 +139,12 @@ export class BeaconSigningSession {
132
139
  'INVALID_PHASE'
133
140
  );
134
141
  }
142
+ if(!this.cohort.participants.includes(participantDid)) {
143
+ throw new SigningSessionError(
144
+ `Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
145
+ 'UNKNOWN_PARTICIPANT', { cohortId: this.cohort.id, participantDid }
146
+ );
147
+ }
135
148
  if(this.partialSignatures.has(participantDid)) {
136
149
  throw new SigningSessionError(
137
150
  `Duplicate partial signature from ${participantDid}.`,
@@ -159,9 +172,47 @@ export class BeaconSigningSession {
159
172
  this.aggregatedNonce,
160
173
  this.cohort.cohortKeys,
161
174
  this.sigHash,
162
- [this.cohort.trMerkleRoot],
175
+ [this.cohort.tapTweak],
163
176
  [true]
164
177
  );
178
+
179
+ // Pre-verify each partial signature against the signer's public key before
180
+ // aggregating (BIP-327 §2.3.5). Delegating verification to partialSigAgg
181
+ // alone makes it impossible to attribute a bad contribution; pinpointing
182
+ // the offending participant lets the service blame and retry without the
183
+ // whole cohort.
184
+ //
185
+ // partialSigVerify(partialSig, pubNonces, i) needs pubNonces ordered to
186
+ // match cohort.cohortKeys, and `i` is the signer's position in that array.
187
+ const pubNoncesByIndex: Uint8Array[] = new Array(this.cohort.cohortKeys.length);
188
+ for(const [did, nonce] of this.nonceContributions) {
189
+ const idx = this.cohort.indexOfParticipant(did);
190
+ if(idx < 0) {
191
+ throw new SigningSessionError(
192
+ `Cannot verify nonce from ${did}: participant key missing from cohort.`,
193
+ 'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
194
+ );
195
+ }
196
+ pubNoncesByIndex[idx] = nonce;
197
+ }
198
+
199
+ for(const [did, partialSig] of this.partialSignatures) {
200
+ const idx = this.cohort.indexOfParticipant(did);
201
+ if(idx < 0) {
202
+ throw new SigningSessionError(
203
+ `Cannot verify partial signature from ${did}: participant key missing from cohort.`,
204
+ 'UNKNOWN_PARTICIPANT_KEY', { participantDid: did }
205
+ );
206
+ }
207
+ const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
208
+ if(!ok) {
209
+ throw new SigningSessionError(
210
+ `Bad partial signature from ${did}.`,
211
+ 'BAD_PARTIAL_SIG', { participantDid: did, index: idx }
212
+ );
213
+ }
214
+ }
215
+
165
216
  this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
166
217
  this.phase = SigningSessionPhase.Complete;
167
218
  return this.signature;
@@ -181,6 +232,10 @@ export class BeaconSigningSession {
181
232
  /**
182
233
  * Generates a partial signature using the participant's secret key + secret nonce.
183
234
  * Requires the aggregated nonce to have been set first (via the service).
235
+ *
236
+ * Zeros the stored `secretNonce` after use. JS cannot truly erase memory (GC
237
+ * and immutable strings), but overwriting the bytes shortens the exposure
238
+ * window and prevents accidental reuse or serialization of a spent nonce.
184
239
  */
185
240
  public generatePartialSignature(participantSecretKey: Uint8Array): Uint8Array {
186
241
  if(!this.aggregatedNonce) {
@@ -193,10 +248,13 @@ export class BeaconSigningSession {
193
248
  this.aggregatedNonce,
194
249
  this.cohort.cohortKeys,
195
250
  this.sigHash,
196
- [this.cohort.trMerkleRoot],
251
+ [this.cohort.tapTweak],
197
252
  [true]
198
253
  );
199
- return session.sign(this.secretNonce, participantSecretKey);
254
+ const partialSig = session.sign(this.secretNonce, participantSecretKey);
255
+ this.secretNonce.fill(0);
256
+ this.secretNonce = undefined;
257
+ return partialSig;
200
258
  }
201
259
 
202
260
  public isComplete(): boolean {
@@ -36,7 +36,24 @@ export class DidCommTransport implements Transport {
36
36
  throw new NotImplementedError('DidCommTransport not implemented.');
37
37
  }
38
38
 
39
+ public unregisterMessageHandler(_actorDid: string, _messageType: string): void {
40
+ throw new NotImplementedError('DidCommTransport not implemented.');
41
+ }
42
+
43
+ public unregisterActor(_did: string): void {
44
+ throw new NotImplementedError('DidCommTransport not implemented.');
45
+ }
46
+
39
47
  public async sendMessage(_message: BaseMessage, _sender: string, _recipient?: string): Promise<void> {
40
48
  throw new NotImplementedError('DidCommTransport not implemented.');
41
49
  }
50
+
51
+ public publishRepeating(
52
+ _message: BaseMessage,
53
+ _sender: string,
54
+ _intervalMs: number,
55
+ _recipient?: string,
56
+ ): () => void {
57
+ throw new NotImplementedError('DidCommTransport not implemented.');
58
+ }
42
59
  }
@@ -1,28 +1,64 @@
1
1
  import { NotImplementedError } from '@did-btcr2/common';
2
- import { NostrTransport } from './nostr.js';
2
+ import type { Logger } from '../logger.js';
3
3
  import { TransportError } from './error.js';
4
- import type { Transport, TransportType } from './transport.js';
4
+ import type { HttpClientTransportConfig } from './http/client.js';
5
+ import { HttpClientTransport } from './http/client.js';
6
+ import type { HttpServerTransportConfig } from './http/server.js';
7
+ import { HttpServerTransport } from './http/server.js';
8
+ import { NostrTransport } from './nostr.js';
9
+ import type { Transport } from './transport.js';
10
+
11
+ /** Discriminated-union config for {@link TransportFactory.establish}. */
12
+ export type TransportConfig =
13
+ | NostrTransportConfigOption
14
+ | DidCommTransportConfigOption
15
+ | HttpClientTransportConfigOption
16
+ | HttpServerTransportConfigOption;
5
17
 
6
- export interface TransportConfig {
7
- type: TransportType;
18
+ export interface NostrTransportConfigOption {
19
+ type: 'nostr';
8
20
  relays?: string[];
21
+ logger?: Logger;
22
+ broadcastLookbackMs?: number;
23
+ }
24
+
25
+ export interface DidCommTransportConfigOption {
26
+ type: 'didcomm';
9
27
  }
10
28
 
11
- /**
12
- * Factory for creating Transport instances.
13
- * @class TransportFactory
14
- */
29
+ export interface HttpClientTransportConfigOption extends HttpClientTransportConfig {
30
+ type: 'http';
31
+ role: 'client';
32
+ }
33
+
34
+ export interface HttpServerTransportConfigOption extends HttpServerTransportConfig {
35
+ type: 'http';
36
+ role: 'server';
37
+ }
38
+
39
+ /** Factory for creating Transport instances. */
15
40
  export class TransportFactory {
16
41
  static establish(config: TransportConfig): Transport {
17
- switch (config.type) {
42
+ switch(config.type) {
18
43
  case 'nostr':
19
- return new NostrTransport({ relays: config.relays });
44
+ return new NostrTransport({
45
+ relays : config.relays,
46
+ logger : config.logger,
47
+ broadcastLookbackMs : config.broadcastLookbackMs,
48
+ });
20
49
  case 'didcomm':
21
50
  throw new NotImplementedError('DIDComm transport not implemented yet.');
51
+ case 'http':
52
+ if(config.role === 'client') return new HttpClientTransport(config);
53
+ if(config.role === 'server') return new HttpServerTransport(config);
54
+ throw new NotImplementedError(
55
+ `HTTP transport role not implemented: ${(config as { role: string }).role}`,
56
+ );
22
57
  default:
23
58
  throw new TransportError(
24
- `Invalid transport type: ${config.type}`,
25
- 'INVALID_TRANSPORT_TYPE', { config }
59
+ `Invalid transport type: ${(config as { type: string }).type}`,
60
+ 'INVALID_TRANSPORT_TYPE',
61
+ { config },
26
62
  );
27
63
  }
28
64
  }