@did-btcr2/method 0.36.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +316 -169
- package/dist/browser.mjs +316 -169
- package/dist/cjs/index.js +317 -170
- package/dist/esm/core/aggregation/participant.js +11 -0
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +37 -2
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +318 -192
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +9 -0
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +22 -11
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +21 -12
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +76 -22
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/participant.ts +12 -0
- package/src/core/aggregation/runner/events.ts +23 -14
- package/src/core/aggregation/runner/participant-runner.ts +43 -13
- package/src/core/aggregation/runner/service-runner.ts +370 -199
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
2
|
+
import { AggregationServiceError } from '../errors.js';
|
|
2
3
|
import type { BaseMessage } from '../messages/base.js';
|
|
3
4
|
import {
|
|
4
5
|
COHORT_OPT_IN,
|
|
@@ -44,8 +45,13 @@ export interface AggregationServiceRunnerOptions {
|
|
|
44
45
|
did: string;
|
|
45
46
|
keys: SchnorrKeyPair;
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Default cohort configuration for the {@link AggregationServiceRunner.run}
|
|
50
|
+
* convenience path. Optional: omit it when driving the runner with
|
|
51
|
+
* {@link AggregationServiceRunner.advertiseCohort}, which takes a per-cohort
|
|
52
|
+
* config and can be called many times on one runner.
|
|
53
|
+
*/
|
|
54
|
+
config?: CohortConfig;
|
|
49
55
|
|
|
50
56
|
/**
|
|
51
57
|
* Decide whether to accept a participant's opt-in.
|
|
@@ -74,26 +80,28 @@ export interface AggregationServiceRunnerOptions {
|
|
|
74
80
|
maxUpdateSizeBytes?: number;
|
|
75
81
|
|
|
76
82
|
/**
|
|
77
|
-
* Overall wall-clock budget for
|
|
78
|
-
* On expiry the cohort is dropped, `cohort-failed` is
|
|
79
|
-
* rejects with a timeout error.
|
|
83
|
+
* Overall wall-clock budget for each cohort, from advertise to
|
|
84
|
+
* signing-complete. On expiry the cohort is dropped, `cohort-failed` is
|
|
85
|
+
* emitted, and that cohort's completion rejects with a timeout error. Other
|
|
86
|
+
* cohorts on the same runner are unaffected. Leave undefined to disable.
|
|
80
87
|
*/
|
|
81
88
|
cohortTtlMs?: number;
|
|
82
89
|
|
|
83
90
|
/**
|
|
84
|
-
* Maximum time allowed between phase transitions. Protects
|
|
85
|
-
* cohorts (e.g. a participant vanishing mid-protocol). Reset
|
|
86
|
-
* on every observed phase change.
|
|
91
|
+
* Maximum time allowed between phase transitions for a cohort. Protects
|
|
92
|
+
* against stalled cohorts (e.g. a participant vanishing mid-protocol). Reset
|
|
93
|
+
* automatically on every observed phase change. Applied per cohort. Leave
|
|
94
|
+
* undefined to disable.
|
|
87
95
|
*/
|
|
88
96
|
phaseTimeoutMs?: number;
|
|
89
97
|
|
|
90
98
|
/**
|
|
91
|
-
* Re-publish COHORT_ADVERT on this interval until keygen is
|
|
92
|
-
* Works around relays that don't backfill historical events to
|
|
93
|
-
* subscribers — a republish gives late joiners a window to discover the
|
|
99
|
+
* Re-publish COHORT_ADVERT on this interval until a cohort's keygen is
|
|
100
|
+
* finalized. Works around relays that don't backfill historical events to
|
|
101
|
+
* late subscribers — a republish gives late joiners a window to discover the
|
|
94
102
|
* advert without protocol changes. The first publish is immediate;
|
|
95
|
-
* subsequent publishes fire every `advertRepeatIntervalMs` until
|
|
96
|
-
* keygen
|
|
103
|
+
* subsequent publishes fire every `advertRepeatIntervalMs` until that
|
|
104
|
+
* cohort's keygen completes, fails, or is stopped. Defaults to
|
|
97
105
|
* {@link DEFAULT_ADVERT_REPEAT_INTERVAL_MS} (60 s). Set to 0 to publish
|
|
98
106
|
* once and never retry.
|
|
99
107
|
*/
|
|
@@ -103,6 +111,41 @@ export interface AggregationServiceRunnerOptions {
|
|
|
103
111
|
/** Default cadence for re-publishing COHORT_ADVERT until keygen completes: 60 seconds. */
|
|
104
112
|
export const DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 60_000;
|
|
105
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Per-cohort runtime bookkeeping the runner keeps for each advertised cohort.
|
|
116
|
+
* One {@link RunContext} per cohortId lives in the runner's `#contexts` map so
|
|
117
|
+
* many cohorts run concurrently on a single runner, each with its own
|
|
118
|
+
* completion promise, finalize guard, timers, and advert-republish loop. The
|
|
119
|
+
* underlying {@link AggregationService} state machine is already keyed by
|
|
120
|
+
* cohortId; this struct is the runner-layer counterpart (see ADR 040).
|
|
121
|
+
*/
|
|
122
|
+
interface RunContext {
|
|
123
|
+
/** The cohort this context drives. */
|
|
124
|
+
cohortId: string;
|
|
125
|
+
/** The conditions this cohort was advertised with. */
|
|
126
|
+
config: CohortConfig;
|
|
127
|
+
/** Resolve this cohort's completion with its aggregation result. */
|
|
128
|
+
resolve: (result: AggregationResult) => void;
|
|
129
|
+
/** Reject this cohort's completion. */
|
|
130
|
+
reject: (err: Error) => void;
|
|
131
|
+
/** The promise handed back from {@link AggregationServiceRunner.advertiseCohort}. */
|
|
132
|
+
completion: Promise<AggregationResult>;
|
|
133
|
+
/**
|
|
134
|
+
* Guard against the async race where two concurrent #handleOptIn invocations
|
|
135
|
+
* for THIS cohort both pass the `participants.length >= minParticipants`
|
|
136
|
+
* check before either mutates the cohort phase. Set synchronously before any
|
|
137
|
+
* `await` so subsequent handlers observe it on their next resumption.
|
|
138
|
+
*/
|
|
139
|
+
finalizing: boolean;
|
|
140
|
+
/** Once settled (resolved or rejected), late timers/messages must not re-settle. */
|
|
141
|
+
settled: boolean;
|
|
142
|
+
cohortTtlTimer?: ReturnType<typeof setTimeout>;
|
|
143
|
+
phaseTimer?: ReturnType<typeof setTimeout>;
|
|
144
|
+
lastObservedPhase?: string;
|
|
145
|
+
/** Stop handle for THIS cohort's repeating COHORT_ADVERT publish loop. */
|
|
146
|
+
stopAdvertRepeat?: () => void;
|
|
147
|
+
}
|
|
148
|
+
|
|
106
149
|
/**
|
|
107
150
|
* High-level facade for running an Aggregation Service over a Transport.
|
|
108
151
|
*
|
|
@@ -110,6 +153,14 @@ export const DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 60_000;
|
|
|
110
153
|
* encapsulating message handler registration, outgoing message dispatch,
|
|
111
154
|
* and decision callback orchestration.
|
|
112
155
|
*
|
|
156
|
+
* A single runner is a long-lived multiplexer: it advertises and drives many
|
|
157
|
+
* cohorts concurrently over one transport. Each advertised cohort owns an
|
|
158
|
+
* independent completion promise and fails in isolation — a stalled or failed
|
|
159
|
+
* cohort never settles its siblings (see ADR 040). Use
|
|
160
|
+
* {@link AggregationServiceRunner.advertiseCohort} for the multi-cohort path;
|
|
161
|
+
* {@link AggregationServiceRunner.run} is a thin single-cohort convenience over
|
|
162
|
+
* it.
|
|
163
|
+
*
|
|
113
164
|
* @example
|
|
114
165
|
* ```typescript
|
|
115
166
|
* const transport = new NostrTransport({ relays: [RELAY] });
|
|
@@ -119,16 +170,21 @@ export const DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 60_000;
|
|
|
119
170
|
* transport,
|
|
120
171
|
* did: serviceDid,
|
|
121
172
|
* keys: serviceKeys,
|
|
122
|
-
*
|
|
123
|
-
* onProvideTxData: async ({ beaconAddress, signalBytes }) => {
|
|
173
|
+
* onProvideTxData: async ({ cohortId, beaconAddress, signalBytes }) => {
|
|
124
174
|
* return await buildBeaconTransaction(beaconAddress, signalBytes, bitcoin);
|
|
125
175
|
* },
|
|
126
176
|
* });
|
|
127
177
|
*
|
|
128
|
-
* runner.on('keygen-complete', ({ beaconAddress }) => console.log(beaconAddress));
|
|
129
|
-
* runner.on('signing-complete', ({ signature }) => console.log('done'));
|
|
178
|
+
* runner.on('keygen-complete', ({ cohortId, beaconAddress }) => console.log(beaconAddress));
|
|
179
|
+
* runner.on('signing-complete', ({ cohortId, signature }) => console.log('done', cohortId));
|
|
180
|
+
*
|
|
181
|
+
* // Multi-cohort: advertise several cohorts; each completion resolves independently.
|
|
182
|
+
* const a = runner.advertiseCohort({ minParticipants: 2, network: 'mutinynet', beaconType: 'CASBeacon' });
|
|
183
|
+
* const b = runner.advertiseCohort({ minParticipants: 3, network: 'mutinynet', beaconType: 'SMTBeacon' });
|
|
184
|
+
* const [ra, rb] = await Promise.all([a.completion, b.completion]);
|
|
130
185
|
*
|
|
131
|
-
*
|
|
186
|
+
* // Single-cohort convenience (requires `config` in the options):
|
|
187
|
+
* // const result = await runner.run();
|
|
132
188
|
* ```
|
|
133
189
|
*
|
|
134
190
|
* For full manual control, drop down to the underlying state machine via
|
|
@@ -143,7 +199,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
143
199
|
|
|
144
200
|
readonly #transport: Transport;
|
|
145
201
|
readonly #did: string;
|
|
146
|
-
readonly #
|
|
202
|
+
readonly #defaultConfig?: CohortConfig;
|
|
147
203
|
readonly #onOptInReceived: OnOptInReceived;
|
|
148
204
|
readonly #onReadyToFinalize: OnReadyToFinalize;
|
|
149
205
|
readonly #onProvideTxData: OnProvideTxData;
|
|
@@ -151,29 +207,16 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
151
207
|
readonly #phaseTimeoutMs?: number;
|
|
152
208
|
readonly #advertRepeatIntervalMs: number;
|
|
153
209
|
|
|
154
|
-
|
|
210
|
+
/** Per-cohort run state, keyed by cohortId. */
|
|
211
|
+
readonly #contexts: Map<string, RunContext> = new Map();
|
|
155
212
|
#handlersRegistered = false;
|
|
156
213
|
#stopped = false;
|
|
157
|
-
/**
|
|
158
|
-
* Guard against the async race where two concurrent #handleOptIn invocations
|
|
159
|
-
* both pass the `participants.length >= minParticipants` check before either
|
|
160
|
-
* mutates the cohort phase. Set synchronously before any `await` so subsequent
|
|
161
|
-
* handlers observe it on their next resumption.
|
|
162
|
-
*/
|
|
163
|
-
#finalizing = false;
|
|
164
|
-
#resolveRun?: (result: AggregationResult) => void;
|
|
165
|
-
#rejectRun?: (err: Error) => void;
|
|
166
|
-
#cohortTtlTimer?: ReturnType<typeof setTimeout>;
|
|
167
|
-
#phaseTimer?: ReturnType<typeof setTimeout>;
|
|
168
|
-
#lastObservedPhase?: string;
|
|
169
|
-
/** Stop handle for the repeating COHORT_ADVERT publish loop. */
|
|
170
|
-
#stopAdvertRepeat?: () => void;
|
|
171
214
|
|
|
172
215
|
constructor(options: AggregationServiceRunnerOptions) {
|
|
173
216
|
super();
|
|
174
217
|
this.#transport = options.transport;
|
|
175
218
|
this.#did = options.did;
|
|
176
|
-
this.#
|
|
219
|
+
this.#defaultConfig = options.config;
|
|
177
220
|
this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
|
|
178
221
|
this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
|
|
179
222
|
finalize : acceptedCount >= minRequired,
|
|
@@ -193,131 +236,269 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
193
236
|
});
|
|
194
237
|
}
|
|
195
238
|
|
|
239
|
+
/** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
|
|
240
|
+
#contextFor(msg: BaseMessage): RunContext | undefined {
|
|
241
|
+
const cohortId = msg.body?.cohortId;
|
|
242
|
+
if(!cohortId) return undefined;
|
|
243
|
+
return this.#contexts.get(cohortId);
|
|
244
|
+
}
|
|
245
|
+
|
|
196
246
|
/**
|
|
197
|
-
* Drain any silent rejections the state machine recorded
|
|
198
|
-
* recent receive() and surface them as `message-rejected` events.
|
|
199
|
-
* call even before a cohortId is assigned.
|
|
247
|
+
* Drain any silent rejections the state machine recorded for a cohort during
|
|
248
|
+
* the most recent receive() and surface them as `message-rejected` events.
|
|
200
249
|
*/
|
|
201
|
-
#drainRejections(): void {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.emit('message-rejected', { cohortId: this.#cohortId, ...r });
|
|
250
|
+
#drainRejections(ctx: RunContext): void {
|
|
251
|
+
for(const r of this.session.drainRejections(ctx.cohortId)) {
|
|
252
|
+
this.emit('message-rejected', { cohortId: ctx.cohortId, ...r });
|
|
205
253
|
}
|
|
206
254
|
}
|
|
207
255
|
|
|
208
256
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
257
|
+
* Advertise a new cohort and begin driving it to completion. Callable many
|
|
258
|
+
* times on one runner; each cohort runs concurrently and independently.
|
|
259
|
+
*
|
|
260
|
+
* @param config Per-cohort conditions + network (see {@link CohortConfig}).
|
|
261
|
+
* @returns The new cohort's id and a `completion` promise that resolves with
|
|
262
|
+
* that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
|
|
263
|
+
* @throws If the runner has been stopped, or the config is invalid
|
|
264
|
+
* (fail-fast via `createCohort`).
|
|
265
|
+
*/
|
|
266
|
+
advertiseCohort(config: CohortConfig): { cohortId: string; completion: Promise<AggregationResult> } {
|
|
267
|
+
if(this.#stopped) {
|
|
268
|
+
throw new AggregationServiceError('Cannot advertise on a stopped runner.', 'RUNNER_STOPPED', {});
|
|
269
|
+
}
|
|
270
|
+
this.#registerHandlers();
|
|
271
|
+
// createCohort validates the conditions and throws on a bad config before
|
|
272
|
+
// any context exists — fail-fast, nothing to clean up.
|
|
273
|
+
const cohortId = this.session.createCohort(config);
|
|
274
|
+
|
|
275
|
+
let resolve!: (result: AggregationResult) => void;
|
|
276
|
+
let reject!: (err: Error) => void;
|
|
277
|
+
const completion = new Promise<AggregationResult>((res, rej) => { resolve = res; reject = rej; });
|
|
278
|
+
const ctx: RunContext = {
|
|
279
|
+
cohortId,
|
|
280
|
+
config,
|
|
281
|
+
resolve,
|
|
282
|
+
reject,
|
|
283
|
+
completion,
|
|
284
|
+
finalizing : false,
|
|
285
|
+
settled : false,
|
|
286
|
+
};
|
|
287
|
+
this.#contexts.set(cohortId, ctx);
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
this.#startTimers(ctx);
|
|
291
|
+
// Emit cohort-advertised BEFORE the send so the event fires before any downstream cascade.
|
|
292
|
+
const advertMsgs = this.session.advertise(cohortId);
|
|
293
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
294
|
+
this.emit('cohort-advertised', { cohortId });
|
|
295
|
+
// Publish the advert. If advertRepeatIntervalMs > 0 we republish on that
|
|
296
|
+
// cadence until this cohort's keygen-complete / fail / stop — works around
|
|
297
|
+
// relays that don't backfill historical events to late subscribers.
|
|
298
|
+
// Otherwise fall back to a single send.
|
|
299
|
+
if(this.#advertRepeatIntervalMs > 0) {
|
|
300
|
+
this.#startAdvertRepeat(ctx, advertMsgs);
|
|
301
|
+
} else {
|
|
302
|
+
this.#sendAll(advertMsgs).catch(err => this.#failCohort(ctx, err as Error));
|
|
303
|
+
}
|
|
304
|
+
} catch(err) {
|
|
305
|
+
this.#failCohort(ctx, err as Error);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { cohortId, completion };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Run a single cohort to completion using the `config` supplied in the
|
|
313
|
+
* runner options. Thin convenience over {@link advertiseCohort} for the
|
|
314
|
+
* single-cohort case (and the path {@link AggregationRunner.solo} rides).
|
|
211
315
|
*
|
|
212
316
|
* @returns {Promise<AggregationResult>} The final result with signature and signed tx.
|
|
213
317
|
*/
|
|
214
318
|
run(): Promise<AggregationResult> {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
319
|
+
if(!this.#defaultConfig) {
|
|
320
|
+
return Promise.reject(new AggregationServiceError(
|
|
321
|
+
'run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.',
|
|
322
|
+
'MISSING_COHORT_CONFIG', {}
|
|
323
|
+
));
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
return this.advertiseCohort(this.#defaultConfig).completion;
|
|
327
|
+
} catch(err) {
|
|
328
|
+
return Promise.reject(err as Error);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Wait for every currently-outstanding cohort to settle and return the
|
|
334
|
+
* successful results. Dynamic drain: cohorts advertised while this is pending
|
|
335
|
+
* are included, and it resolves only once no cohorts remain. Failed cohorts
|
|
336
|
+
* are surfaced via `error` / `cohort-failed` events and their rejected
|
|
337
|
+
* `completion` promises; they are omitted from the returned array (this
|
|
338
|
+
* method does not throw). Bound long-running cohorts with `cohortTtlMs` /
|
|
339
|
+
* `phaseTimeoutMs` or this may never resolve.
|
|
340
|
+
*
|
|
341
|
+
* @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
|
|
342
|
+
*/
|
|
343
|
+
async runAll(): Promise<AggregationResult[]> {
|
|
344
|
+
const collected = new Map<string, AggregationResult>();
|
|
345
|
+
// Capture every completion, including a cohort that is advertised and
|
|
346
|
+
// finishes entirely within one drain round (so it never appears in a
|
|
347
|
+
// snapshot below).
|
|
348
|
+
const onComplete = (result: AggregationResult): void => { collected.set(result.cohortId, result); };
|
|
349
|
+
this.on('signing-complete', onComplete);
|
|
350
|
+
try {
|
|
351
|
+
// Block until the live set empties; re-snapshot each round to pick up
|
|
352
|
+
// cohorts advertised mid-drain.
|
|
353
|
+
while(this.#contexts.size > 0) {
|
|
354
|
+
await Promise.allSettled([ ...this.#contexts.values() ].map(c => c.completion));
|
|
238
355
|
}
|
|
239
|
-
}
|
|
356
|
+
} finally {
|
|
357
|
+
this.off('signing-complete', onComplete);
|
|
358
|
+
}
|
|
359
|
+
return [ ...collected.values() ];
|
|
240
360
|
}
|
|
241
361
|
|
|
242
362
|
/**
|
|
243
|
-
* Begin publishing
|
|
244
|
-
* until
|
|
245
|
-
*
|
|
363
|
+
* Begin publishing a cohort's advert immediately and on a repeating interval
|
|
364
|
+
* until the cohort's advert loop is stopped. Each advert is broadcast (no
|
|
365
|
+
* recipient) via the transport's `publishRepeating` primitive.
|
|
246
366
|
*/
|
|
247
|
-
#startAdvertRepeat(advertMsgs: BaseMessage[]): void {
|
|
367
|
+
#startAdvertRepeat(ctx: RunContext, advertMsgs: BaseMessage[]): void {
|
|
248
368
|
// COHORT_ADVERT is always a single broadcast message in the current
|
|
249
369
|
// protocol, but iterate for generality.
|
|
250
370
|
const stops: Array<() => void> = [];
|
|
251
371
|
for(const msg of advertMsgs) {
|
|
252
372
|
stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
|
|
253
373
|
}
|
|
254
|
-
|
|
374
|
+
ctx.stopAdvertRepeat = () => {
|
|
255
375
|
for(const stop of stops) {
|
|
256
376
|
try { stop(); } catch { /* ignore */ }
|
|
257
377
|
}
|
|
258
378
|
};
|
|
259
379
|
}
|
|
260
380
|
|
|
261
|
-
/** Stop
|
|
262
|
-
#stopAdvertRepeating(): void {
|
|
263
|
-
if(!
|
|
264
|
-
const stop =
|
|
265
|
-
|
|
381
|
+
/** Stop a cohort's advert republish loop. Idempotent. */
|
|
382
|
+
#stopAdvertRepeating(ctx: RunContext): void {
|
|
383
|
+
if(!ctx.stopAdvertRepeat) return;
|
|
384
|
+
const stop = ctx.stopAdvertRepeat;
|
|
385
|
+
ctx.stopAdvertRepeat = undefined;
|
|
266
386
|
stop();
|
|
267
387
|
}
|
|
268
388
|
|
|
269
|
-
/** Schedule cohort TTL + phase timeout
|
|
270
|
-
#startTimers(): void {
|
|
389
|
+
/** Schedule a cohort's TTL + phase timeout when it is advertised. */
|
|
390
|
+
#startTimers(ctx: RunContext): void {
|
|
271
391
|
if(this.#cohortTtlMs !== undefined) {
|
|
272
|
-
|
|
273
|
-
const reason = `Cohort ${
|
|
274
|
-
this.emit('cohort-failed', { cohortId:
|
|
275
|
-
this.#
|
|
392
|
+
ctx.cohortTtlTimer = setTimeout(() => {
|
|
393
|
+
const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
|
|
394
|
+
this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
|
|
395
|
+
this.#failCohort(ctx, new Error(reason));
|
|
276
396
|
}, this.#cohortTtlMs);
|
|
277
397
|
}
|
|
278
|
-
this.#resetPhaseTimer();
|
|
398
|
+
this.#resetPhaseTimer(ctx);
|
|
279
399
|
}
|
|
280
400
|
|
|
281
|
-
/** Reset
|
|
282
|
-
#resetPhaseTimer(): void {
|
|
283
|
-
if(
|
|
284
|
-
|
|
401
|
+
/** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
|
|
402
|
+
#resetPhaseTimer(ctx: RunContext): void {
|
|
403
|
+
if(ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
404
|
+
ctx.phaseTimer = undefined;
|
|
285
405
|
if(this.#phaseTimeoutMs === undefined) return;
|
|
286
|
-
|
|
287
|
-
const reason = `Cohort ${
|
|
288
|
-
this.emit('cohort-failed', { cohortId:
|
|
289
|
-
this.#
|
|
406
|
+
ctx.phaseTimer = setTimeout(() => {
|
|
407
|
+
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? '?'} for ${this.#phaseTimeoutMs}ms`;
|
|
408
|
+
this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
|
|
409
|
+
this.#failCohort(ctx, new Error(reason));
|
|
290
410
|
}, this.#phaseTimeoutMs);
|
|
291
411
|
}
|
|
292
412
|
|
|
293
|
-
/** Detect a phase change since the last observation and reset
|
|
294
|
-
#onPhaseMaybeChanged(): void {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.#
|
|
299
|
-
this.#resetPhaseTimer();
|
|
413
|
+
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
414
|
+
#onPhaseMaybeChanged(ctx: RunContext): void {
|
|
415
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
416
|
+
if(phase !== ctx.lastObservedPhase) {
|
|
417
|
+
ctx.lastObservedPhase = phase;
|
|
418
|
+
this.#resetPhaseTimer(ctx);
|
|
300
419
|
}
|
|
301
420
|
}
|
|
302
421
|
|
|
303
|
-
/** Clear
|
|
304
|
-
#clearTimers(): void {
|
|
305
|
-
if(
|
|
306
|
-
if(
|
|
307
|
-
|
|
308
|
-
|
|
422
|
+
/** Clear a cohort's timers. Called on completion, stop, and failure. */
|
|
423
|
+
#clearTimers(ctx: RunContext): void {
|
|
424
|
+
if(ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
|
|
425
|
+
if(ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
426
|
+
ctx.cohortTtlTimer = undefined;
|
|
427
|
+
ctx.phaseTimer = undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
|
|
432
|
+
* its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
|
|
433
|
+
* and does NOT detach the shared transport handlers. Leaves the cohort in the
|
|
434
|
+
* state machine; whether that cohort's `session` state is also removed is the
|
|
435
|
+
* caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
|
|
436
|
+
*/
|
|
437
|
+
#disposeCohort(ctx: RunContext): void {
|
|
438
|
+
this.#stopAdvertRepeating(ctx);
|
|
439
|
+
this.#clearTimers(ctx);
|
|
440
|
+
this.#contexts.delete(ctx.cohortId);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Settle one cohort successfully. Reclaims the runner context but leaves the
|
|
445
|
+
* completed cohort in `session` so callers can read its beaconAddress / cohort
|
|
446
|
+
* via `session.getCohort(result.cohortId)`; reclaim it with
|
|
447
|
+
* `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
|
|
448
|
+
*/
|
|
449
|
+
#completeCohort(ctx: RunContext, result: AggregationResult): void {
|
|
450
|
+
if(ctx.settled) return;
|
|
451
|
+
ctx.settled = true;
|
|
452
|
+
this.#disposeCohort(ctx);
|
|
453
|
+
this.emit('signing-complete', result);
|
|
454
|
+
ctx.resolve(result);
|
|
309
455
|
}
|
|
310
456
|
|
|
311
457
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
458
|
+
* Fail one cohort. Reclaims its runner context, drops its now-dead state from
|
|
459
|
+
* the state machine, and rejects only its completion; siblings keep running
|
|
460
|
+
* and the shared transport handlers stay registered. Idempotent via
|
|
461
|
+
* `ctx.settled`.
|
|
462
|
+
*/
|
|
463
|
+
#failCohort(ctx: RunContext, err: Error): void {
|
|
464
|
+
if(ctx.settled) return;
|
|
465
|
+
ctx.settled = true;
|
|
466
|
+
this.#disposeCohort(ctx);
|
|
467
|
+
this.session.removeCohort(ctx.cohortId);
|
|
468
|
+
this.emit('error', err);
|
|
469
|
+
ctx.reject(err);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Stop a single cohort early without affecting the rest of the runner. Drops
|
|
474
|
+
* the cohort's state machine state; its `completion` promise rejects with a
|
|
475
|
+
* stopped error.
|
|
476
|
+
*/
|
|
477
|
+
stopCohort(cohortId: string): void {
|
|
478
|
+
const ctx = this.#contexts.get(cohortId);
|
|
479
|
+
if(!ctx || ctx.settled) return;
|
|
480
|
+
ctx.settled = true;
|
|
481
|
+
this.#disposeCohort(ctx);
|
|
482
|
+
this.session.removeCohort(cohortId);
|
|
483
|
+
ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, 'COHORT_STOPPED', { cohortId }));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Stop the whole runner. Fails every outstanding cohort, then detaches the
|
|
488
|
+
* shared transport handlers so a restart or a new runner doesn't inherit
|
|
489
|
+
* stale dispatch. Safe to call repeatedly.
|
|
314
490
|
*/
|
|
315
491
|
stop(): void {
|
|
316
492
|
this.#stopped = true;
|
|
317
|
-
this.#
|
|
318
|
-
|
|
493
|
+
for(const ctx of [ ...this.#contexts.values() ]) {
|
|
494
|
+
if(ctx.settled) continue;
|
|
495
|
+
ctx.settled = true;
|
|
496
|
+
this.#disposeCohort(ctx);
|
|
497
|
+
this.session.removeCohort(ctx.cohortId);
|
|
498
|
+
ctx.reject(new AggregationServiceError('Service runner stopped.', 'RUNNER_STOPPED', { cohortId: ctx.cohortId }));
|
|
499
|
+
}
|
|
500
|
+
this.#contexts.clear();
|
|
319
501
|
this.#unregisterHandlers();
|
|
320
|
-
if(this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
321
502
|
}
|
|
322
503
|
|
|
323
504
|
/** Message types this runner listens for on the transport. */
|
|
@@ -330,7 +511,10 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
330
511
|
];
|
|
331
512
|
|
|
332
513
|
/**
|
|
333
|
-
* Internal: handler registration with the transport. Idempotent.
|
|
514
|
+
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
515
|
+
* are DID-scoped and cohort-agnostic — one registration serves every cohort
|
|
516
|
+
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
517
|
+
* handler via the inbound message's cohortId.
|
|
334
518
|
*/
|
|
335
519
|
#registerHandlers(): void {
|
|
336
520
|
if(this.#handlersRegistered) return;
|
|
@@ -354,25 +538,28 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
354
538
|
|
|
355
539
|
/**
|
|
356
540
|
* Internal: message handlers for each protocol step. Each handler:
|
|
357
|
-
* 1)
|
|
358
|
-
* 2)
|
|
359
|
-
* 3)
|
|
541
|
+
* 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
|
|
542
|
+
* 2) feeds the message into the state machine via session.receive()
|
|
543
|
+
* 3) emits a high-level event (carrying cohortId) for external observers
|
|
544
|
+
* 4) checks if the new state triggers any automatic next steps, and if so:
|
|
360
545
|
* a) calls the appropriate decision callback(s)
|
|
361
546
|
* b) sends any resulting messages from the state machine
|
|
547
|
+
* Errors fail only the owning cohort. A stopped runner ignores messages.
|
|
362
548
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
363
549
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
364
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
365
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
366
550
|
*/
|
|
367
551
|
async #handleOptIn(msg: BaseMessage): Promise<void> {
|
|
368
552
|
if(this.#stopped) return;
|
|
553
|
+
const ctx = this.#contextFor(msg);
|
|
554
|
+
if(!ctx) return;
|
|
369
555
|
try {
|
|
370
556
|
this.session.receive(msg);
|
|
371
|
-
this.#drainRejections();
|
|
372
|
-
this.#onPhaseMaybeChanged();
|
|
557
|
+
this.#drainRejections(ctx);
|
|
558
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
373
559
|
|
|
374
|
-
const optIn = this.session.pendingOptIns(
|
|
560
|
+
const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
|
|
375
561
|
if(!optIn) return;
|
|
562
|
+
// PendingOptIn already carries cohortId, so this event is cohort-identified.
|
|
376
563
|
this.emit('opt-in-received', optIn);
|
|
377
564
|
|
|
378
565
|
// Register peer key for encrypted messaging
|
|
@@ -384,49 +571,50 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
384
571
|
if(!decision.accepted) return;
|
|
385
572
|
|
|
386
573
|
// Don't accept past the advertised maxParticipants: acceptParticipant
|
|
387
|
-
// would throw COHORT_FULL and fail the
|
|
574
|
+
// would throw COHORT_FULL and fail the cohort. Silently ignore the surplus
|
|
388
575
|
// opt-in (the cohort is full).
|
|
389
|
-
const maxParticipants =
|
|
390
|
-
const cohortNow = this.session.getCohort(
|
|
576
|
+
const maxParticipants = ctx.config.maxParticipants;
|
|
577
|
+
const cohortNow = this.session.getCohort(ctx.cohortId);
|
|
391
578
|
if(maxParticipants !== undefined && cohortNow && cohortNow.participants.length >= maxParticipants) {
|
|
392
579
|
return;
|
|
393
580
|
}
|
|
394
581
|
|
|
395
|
-
await this.#sendAll(this.session.acceptParticipant(
|
|
396
|
-
this.emit('participant-accepted', { participantDid: msg.from });
|
|
397
|
-
|
|
398
|
-
// Check if it's time to finalize. The
|
|
399
|
-
// before the first await so concurrent opt-in handlers
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
582
|
+
await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
|
|
583
|
+
this.emit('participant-accepted', { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
584
|
+
|
|
585
|
+
// Check if it's time to finalize. The per-cohort `finalizing` flag is set
|
|
586
|
+
// synchronously before the first await so concurrent opt-in handlers for
|
|
587
|
+
// the same cohort observe it and skip — otherwise two handlers could both
|
|
588
|
+
// pass the minParticipants check and both call finalizeKeygen, the second
|
|
589
|
+
// of which would throw (phase mismatch).
|
|
590
|
+
const cohort = this.session.getCohort(ctx.cohortId)!;
|
|
591
|
+
if(cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
|
|
592
|
+
ctx.finalizing = true;
|
|
405
593
|
const finalizeDecision = await this.#onReadyToFinalize({
|
|
406
594
|
acceptedCount : cohort.participants.length,
|
|
407
|
-
minRequired :
|
|
595
|
+
minRequired : ctx.config.minParticipants,
|
|
408
596
|
});
|
|
409
597
|
if(!finalizeDecision.finalize) {
|
|
410
598
|
// Operator declined — reset the flag so a later opt-in can retry.
|
|
411
|
-
|
|
599
|
+
ctx.finalizing = false;
|
|
412
600
|
return;
|
|
413
601
|
}
|
|
414
602
|
// finalizeKeygen() computes the beacon address synchronously
|
|
415
603
|
// emit BEFORE awaiting sendAll. Otherwise the downstream cascade
|
|
416
604
|
// (which can run all the way to signing-complete) would resolve the
|
|
417
|
-
//
|
|
418
|
-
const readyMsgs = this.session.finalizeKeygen(
|
|
605
|
+
// cohort's completion promise before this event fires.
|
|
606
|
+
const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
|
|
419
607
|
// Keygen done — stop re-advertising the cohort. New participants
|
|
420
608
|
// arriving after this point would be rejected anyway.
|
|
421
|
-
this.#stopAdvertRepeating();
|
|
609
|
+
this.#stopAdvertRepeating(ctx);
|
|
422
610
|
this.emit('keygen-complete', {
|
|
423
|
-
cohortId :
|
|
611
|
+
cohortId : ctx.cohortId,
|
|
424
612
|
beaconAddress : cohort.beaconAddress,
|
|
425
613
|
});
|
|
426
614
|
await this.#sendAll(readyMsgs);
|
|
427
615
|
}
|
|
428
616
|
} catch(err) {
|
|
429
|
-
this.#
|
|
617
|
+
this.#failCohort(ctx, err as Error);
|
|
430
618
|
}
|
|
431
619
|
}
|
|
432
620
|
|
|
@@ -435,25 +623,25 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
435
623
|
* and distributes the data for validation.
|
|
436
624
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
437
625
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
438
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
439
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
440
626
|
*/
|
|
441
627
|
async #handleSubmitUpdate(msg: BaseMessage): Promise<void> {
|
|
442
628
|
if(this.#stopped) return;
|
|
629
|
+
const ctx = this.#contextFor(msg);
|
|
630
|
+
if(!ctx) return;
|
|
443
631
|
try {
|
|
444
632
|
this.session.receive(msg);
|
|
445
|
-
this.#drainRejections();
|
|
446
|
-
this.#onPhaseMaybeChanged();
|
|
447
|
-
this.emit('update-received', { participantDid: msg.from });
|
|
633
|
+
this.#drainRejections(ctx);
|
|
634
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
635
|
+
this.emit('update-received', { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
448
636
|
|
|
449
637
|
// When all updates collected, build and distribute
|
|
450
|
-
if(this.session.getCohortPhase(
|
|
451
|
-
const distributeMsgs = this.session.buildAndDistribute(
|
|
452
|
-
this.emit('data-distributed', { cohortId:
|
|
638
|
+
if(this.session.getCohortPhase(ctx.cohortId) === ServiceCohortPhase.UpdatesCollected) {
|
|
639
|
+
const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
|
|
640
|
+
this.emit('data-distributed', { cohortId: ctx.cohortId });
|
|
453
641
|
await this.#sendAll(distributeMsgs);
|
|
454
642
|
}
|
|
455
643
|
} catch(err) {
|
|
456
|
-
this.#
|
|
644
|
+
this.#failCohort(ctx, err as Error);
|
|
457
645
|
}
|
|
458
646
|
}
|
|
459
647
|
|
|
@@ -462,98 +650,96 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
462
650
|
* automatically requests tx data and starts signing.
|
|
463
651
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
464
652
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
465
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
466
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
467
653
|
*/
|
|
468
654
|
async #handleValidationAck(msg: BaseMessage): Promise<void> {
|
|
469
655
|
if(this.#stopped) return;
|
|
656
|
+
const ctx = this.#contextFor(msg);
|
|
657
|
+
if(!ctx) return;
|
|
470
658
|
try {
|
|
471
659
|
this.session.receive(msg);
|
|
472
|
-
this.#drainRejections();
|
|
473
|
-
this.#onPhaseMaybeChanged();
|
|
660
|
+
this.#drainRejections(ctx);
|
|
661
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
474
662
|
const approved = !!msg.body?.approved;
|
|
475
|
-
this.emit('validation-received', { participantDid: msg.from, approved });
|
|
663
|
+
this.emit('validation-received', { cohortId: ctx.cohortId, participantDid: msg.from, approved });
|
|
476
664
|
|
|
477
|
-
const phase = this.session.getCohortPhase(
|
|
665
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
478
666
|
|
|
479
667
|
// A participant rejection flips the cohort to Failed. Emit a structured
|
|
480
668
|
// event so the runner/caller sees the failure instead of the cohort
|
|
481
669
|
// silently stalling.
|
|
482
670
|
if(phase === ServiceCohortPhase.Failed) {
|
|
483
671
|
const reason = `Validation rejected by participant ${msg.from}`;
|
|
484
|
-
this.emit('cohort-failed', { cohortId:
|
|
485
|
-
this.#
|
|
672
|
+
this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
|
|
673
|
+
this.#failCohort(ctx, new Error(reason));
|
|
486
674
|
return;
|
|
487
675
|
}
|
|
488
676
|
|
|
489
677
|
// When all validations received, request tx data and start signing
|
|
490
678
|
if(phase === ServiceCohortPhase.Validated) {
|
|
491
|
-
const cohort = this.session.getCohort(
|
|
679
|
+
const cohort = this.session.getCohort(ctx.cohortId)!;
|
|
492
680
|
const txData = await this.#onProvideTxData({
|
|
493
|
-
cohortId :
|
|
681
|
+
cohortId : ctx.cohortId,
|
|
494
682
|
beaconAddress : cohort.beaconAddress,
|
|
495
683
|
signalBytes : cohort.signalBytes!,
|
|
496
684
|
});
|
|
497
|
-
const authMsgs = this.session.startSigning(
|
|
498
|
-
const sessionId = this.session.getSigningSessionId(
|
|
499
|
-
this.emit('signing-started', { sessionId });
|
|
685
|
+
const authMsgs = this.session.startSigning(ctx.cohortId, txData);
|
|
686
|
+
const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? '';
|
|
687
|
+
this.emit('signing-started', { cohortId: ctx.cohortId, sessionId });
|
|
500
688
|
await this.#sendAll(authMsgs);
|
|
501
689
|
}
|
|
502
690
|
} catch(err) {
|
|
503
|
-
this.#
|
|
691
|
+
this.#failCohort(ctx, err as Error);
|
|
504
692
|
}
|
|
505
693
|
}
|
|
506
694
|
|
|
507
695
|
/**
|
|
508
|
-
* Handler for receiving nonce contributions
|
|
509
|
-
*
|
|
696
|
+
* Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
|
|
697
|
+
* nonce back to the cohort.
|
|
510
698
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
511
699
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
512
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
513
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
514
700
|
*/
|
|
515
701
|
async #handleNonceContribution(msg: BaseMessage): Promise<void> {
|
|
516
702
|
if(this.#stopped) return;
|
|
703
|
+
const ctx = this.#contextFor(msg);
|
|
704
|
+
if(!ctx) return;
|
|
517
705
|
try {
|
|
518
706
|
this.session.receive(msg);
|
|
519
|
-
this.#drainRejections();
|
|
520
|
-
this.#onPhaseMaybeChanged();
|
|
521
|
-
this.emit('nonce-received', { participantDid: msg.from });
|
|
707
|
+
this.#drainRejections(ctx);
|
|
708
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
709
|
+
this.emit('nonce-received', { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
522
710
|
|
|
523
711
|
// When all nonces collected, send aggregated nonce
|
|
524
|
-
if(this.session.getCohortPhase(
|
|
525
|
-
await this.#sendAll(this.session.sendAggregatedNonce(
|
|
712
|
+
if(this.session.getCohortPhase(ctx.cohortId) === ServiceCohortPhase.NoncesCollected) {
|
|
713
|
+
await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
|
|
526
714
|
}
|
|
527
715
|
} catch(err) {
|
|
528
|
-
this.#
|
|
716
|
+
this.#failCohort(ctx, err as Error);
|
|
529
717
|
}
|
|
530
718
|
}
|
|
531
719
|
|
|
532
720
|
/**
|
|
533
721
|
* Handler for receiving signature authorizations. When all partial signatures are received, the
|
|
534
|
-
* session automatically completes
|
|
722
|
+
* session automatically completes; the final result is emitted and the cohort's completion
|
|
723
|
+
* promise resolves.
|
|
535
724
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
536
725
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
537
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
538
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
539
726
|
*/
|
|
540
727
|
async #handleSignatureAuthorization(msg: BaseMessage): Promise<void> {
|
|
541
728
|
if(this.#stopped) return;
|
|
729
|
+
const ctx = this.#contextFor(msg);
|
|
730
|
+
if(!ctx) return;
|
|
542
731
|
try {
|
|
543
732
|
this.session.receive(msg);
|
|
544
|
-
this.#drainRejections();
|
|
545
|
-
this.#onPhaseMaybeChanged();
|
|
733
|
+
this.#drainRejections(ctx);
|
|
734
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
546
735
|
|
|
547
736
|
// The state machine auto-completes when all partial sigs received
|
|
548
|
-
const result = this.session.getResult(
|
|
737
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
549
738
|
if(result) {
|
|
550
|
-
this.#
|
|
551
|
-
this.#unregisterHandlers();
|
|
552
|
-
this.emit('signing-complete', result);
|
|
553
|
-
this.#resolveRun?.(result);
|
|
739
|
+
this.#completeCohort(ctx, result);
|
|
554
740
|
}
|
|
555
741
|
} catch(err) {
|
|
556
|
-
this.#
|
|
742
|
+
this.#failCohort(ctx, err as Error);
|
|
557
743
|
}
|
|
558
744
|
}
|
|
559
745
|
|
|
@@ -561,25 +747,10 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
561
747
|
* Internal: helper to send all messages sequentially. Catches and propagates errors.
|
|
562
748
|
* @param {BaseMessage[]} msgs - The messages to send.
|
|
563
749
|
* @returns {Promise<void>} Resolves when all messages have been sent.
|
|
564
|
-
* @throws {Error} If sending any message fails, the error is emitted and the run promise is
|
|
565
|
-
* rejected.
|
|
566
750
|
*/
|
|
567
751
|
async #sendAll(msgs: BaseMessage[]): Promise<void> {
|
|
568
752
|
for(const m of msgs) {
|
|
569
753
|
await this.#transport.sendMessage(m, this.#did, m.to);
|
|
570
754
|
}
|
|
571
755
|
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
|
|
575
|
-
* @param {Error} err - The error to handle.
|
|
576
|
-
*/
|
|
577
|
-
#fail(err: Error): void {
|
|
578
|
-
this.#stopAdvertRepeating();
|
|
579
|
-
this.#clearTimers();
|
|
580
|
-
this.#unregisterHandlers();
|
|
581
|
-
if(this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
582
|
-
this.emit('error', err);
|
|
583
|
-
this.#rejectRun?.(err);
|
|
584
|
-
}
|
|
585
756
|
}
|