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