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