@did-btcr2/method 0.36.1 → 0.38.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 +1149 -126
- package/dist/browser.mjs +1149 -126
- package/dist/cjs/index.js +1244 -208
- 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 +210 -21
- 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 +12 -3
- package/dist/esm/core/aggregation/runner/aggregation-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +74 -22
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +104 -11
- 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 +116 -20
- 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/fee-estimator.js +8 -0
- package/dist/esm/core/beacon/fee-estimator.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 +41 -7
- 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 +19 -3
- package/dist/types/core/aggregation/runner/aggregation-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +28 -7
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +3 -3
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +47 -5
- 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 +66 -10
- 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/fee-estimator.d.ts +8 -0
- package/dist/types/core/beacon/fee-estimator.d.ts.map +1 -1
- 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 +255 -23
- 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 +34 -5
- package/src/core/aggregation/runner/events.ts +26 -7
- package/src/core/aggregation/runner/participant-runner.ts +75 -23
- package/src/core/aggregation/runner/service-runner.ts +144 -12
- 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 +149 -25
- package/src/core/beacon/cas-beacon.ts +7 -7
- package/src/core/beacon/fee-estimator.ts +9 -0
- 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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
COHORT_OPT_IN_ACCEPT,
|
|
9
9
|
COHORT_READY,
|
|
10
10
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
11
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
11
12
|
} from '../messages/constants.js';
|
|
12
13
|
import type {
|
|
13
14
|
CohortAdvert,
|
|
@@ -29,7 +30,7 @@ export type ShouldJoin = (advert: CohortAdvert) => Promise<boolean>;
|
|
|
29
30
|
export type OnProvideUpdate = (info: {
|
|
30
31
|
cohortId: string;
|
|
31
32
|
beaconAddress: string;
|
|
32
|
-
}) => Promise<SignedBTCR2Update>;
|
|
33
|
+
}) => Promise<SignedBTCR2Update | null>;
|
|
33
34
|
|
|
34
35
|
/** Decision callback: approve or reject aggregated data. */
|
|
35
36
|
export type OnValidateData = (info: PendingValidation) => Promise<{ approved: boolean }>;
|
|
@@ -53,7 +54,7 @@ export interface AggregationParticipantRunnerOptions {
|
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* Provide a signed BTCR2 update for the cohort.
|
|
56
|
-
* REQUIRED
|
|
57
|
+
* REQUIRED - no sensible default.
|
|
57
58
|
*/
|
|
58
59
|
onProvideUpdate: OnProvideUpdate;
|
|
59
60
|
|
|
@@ -161,6 +162,7 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
161
162
|
DISTRIBUTE_AGGREGATED_DATA,
|
|
162
163
|
AUTHORIZATION_REQUEST,
|
|
163
164
|
AGGREGATED_NONCE,
|
|
165
|
+
FALLBACK_AUTHORIZATION_REQUEST,
|
|
164
166
|
];
|
|
165
167
|
|
|
166
168
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
@@ -200,7 +202,7 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
200
202
|
*
|
|
201
203
|
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
202
204
|
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
203
|
-
* `start()`, and listen for `cohort-complete`
|
|
205
|
+
* `start()`, and listen for `cohort-complete` - the runner already drives
|
|
204
206
|
* any number of cohorts concurrently.
|
|
205
207
|
*
|
|
206
208
|
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
@@ -240,6 +242,7 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
240
242
|
this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
|
|
241
243
|
this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
|
|
242
244
|
this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
|
|
245
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
/**
|
|
@@ -302,13 +305,20 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
302
305
|
if (!info) return;
|
|
303
306
|
this.emit('cohort-ready', { cohortId, beaconAddress: info.beaconAddress });
|
|
304
307
|
|
|
305
|
-
// Construct the signed update via caller callback and submit
|
|
308
|
+
// Construct the signed update via caller callback and submit. A null return
|
|
309
|
+
// means the member has no update this round: it declines (cooperative
|
|
310
|
+
// non-inclusion) but stays in the cohort and still signs.
|
|
306
311
|
const signedUpdate = await this.#onProvideUpdate({
|
|
307
312
|
cohortId,
|
|
308
313
|
beaconAddress : info.beaconAddress,
|
|
309
314
|
});
|
|
310
|
-
|
|
311
|
-
|
|
315
|
+
if(signedUpdate === null) {
|
|
316
|
+
await this.#sendAll(this.session.declineUpdate(cohortId));
|
|
317
|
+
this.emit('update-declined', { cohortId });
|
|
318
|
+
} else {
|
|
319
|
+
await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
|
|
320
|
+
this.emit('update-submitted', { cohortId });
|
|
321
|
+
}
|
|
312
322
|
} catch (err) {
|
|
313
323
|
this.emit('error', err as Error);
|
|
314
324
|
}
|
|
@@ -398,29 +408,71 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
398
408
|
await this.#sendAll(this.session.generatePartialSignature(cohortId));
|
|
399
409
|
|
|
400
410
|
// Check if we've reached completion
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
411
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
this.emit('error', err as Error);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Internal: handler for fallback authorization requests. The service abandoned
|
|
419
|
+
* the optimistic key path; the member authorizes the k-of-n script-path spend
|
|
420
|
+
* of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
|
|
421
|
+
* the decision, signs the fallback, and completes once it has contributed (the
|
|
422
|
+
* service needs only k members).
|
|
423
|
+
* @param {BaseMessage} msg - The received fallback authorization request message.
|
|
424
|
+
* @returns {Promise<void>} Resolves when processing is complete.
|
|
425
|
+
*/
|
|
426
|
+
async #handleFallbackAuthorizationRequest(msg: BaseMessage): Promise<void> {
|
|
427
|
+
if (this.#stopped) return;
|
|
428
|
+
try {
|
|
429
|
+
this.session.receive(msg);
|
|
430
|
+
|
|
431
|
+
const cohortId = msg.body?.cohortId;
|
|
432
|
+
if (!cohortId) return;
|
|
433
|
+
|
|
434
|
+
const req = this.session.pendingFallbackRequests.get(cohortId);
|
|
435
|
+
if (!req) return;
|
|
436
|
+
this.emit('fallback-requested', req);
|
|
437
|
+
|
|
438
|
+
const decision = await this.#onApproveSigning(req);
|
|
439
|
+
if (!decision.approved) {
|
|
440
|
+
this.emit('cohort-failed', { cohortId, reason: 'Fallback signing rejected by participant' });
|
|
441
|
+
return;
|
|
418
442
|
}
|
|
443
|
+
|
|
444
|
+
await this.#sendAll(this.session.approveFallback(cohortId));
|
|
445
|
+
// The member has contributed its fallback signature and is done from its
|
|
446
|
+
// own perspective, regardless of whether the service has yet collected k.
|
|
447
|
+
this.#emitCohortCompleteIfDone(cohortId);
|
|
419
448
|
} catch (err) {
|
|
420
449
|
this.emit('error', err as Error);
|
|
421
450
|
}
|
|
422
451
|
}
|
|
423
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Internal: emit `cohort-complete` with the participant's retained sidecar once
|
|
455
|
+
* the cohort has reached the Complete phase. Surfaces the CAS Announcement map
|
|
456
|
+
* (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
|
|
457
|
+
* DID resolution. Read via getValidation (not pendingValidations, which lists
|
|
458
|
+
* only the AwaitingValidation phase) so the sidecar is still available at
|
|
459
|
+
* Complete. Shared by the optimistic and fallback completion paths.
|
|
460
|
+
*/
|
|
461
|
+
#emitCohortCompleteIfDone(cohortId: string): void {
|
|
462
|
+
if (this.session.getCohortPhase(cohortId) !== ParticipantCohortPhase.Complete) return;
|
|
463
|
+
const info = this.session.joinedCohorts.get(cohortId);
|
|
464
|
+
if (!info) return;
|
|
465
|
+
const validation = this.session.getValidation(cohortId);
|
|
466
|
+
this.emit('cohort-complete', {
|
|
467
|
+
cohortId,
|
|
468
|
+
beaconAddress : info.beaconAddress,
|
|
469
|
+
beaconType : validation?.beaconType ?? '',
|
|
470
|
+
included : validation?.included ?? true,
|
|
471
|
+
casAnnouncement : validation?.casAnnouncement,
|
|
472
|
+
smtProof : validation?.smtProof,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
424
476
|
/**
|
|
425
477
|
* Internal: send helper to ensure messages are sent sequentially. This is important for protocol
|
|
426
478
|
* correctness, as some transports may not guarantee message order if sent in parallel.
|
|
@@ -3,12 +3,15 @@ import { AggregationServiceError } from '../errors.js';
|
|
|
3
3
|
import type { BaseMessage } from '../messages/base.js';
|
|
4
4
|
import {
|
|
5
5
|
COHORT_OPT_IN,
|
|
6
|
+
FALLBACK_SIGNATURE,
|
|
6
7
|
NONCE_CONTRIBUTION,
|
|
7
8
|
SIGNATURE_AUTHORIZATION,
|
|
9
|
+
SUBMIT_NONINCLUDED,
|
|
8
10
|
SUBMIT_UPDATE,
|
|
9
11
|
VALIDATION_ACK,
|
|
10
12
|
} from '../messages/constants.js';
|
|
11
13
|
import { ServiceCohortPhase } from '../phases.js';
|
|
14
|
+
import type { ServiceCohortPhaseType } from '../phases.js';
|
|
12
15
|
import type {
|
|
13
16
|
AggregationResult,
|
|
14
17
|
CohortConfig,
|
|
@@ -18,6 +21,8 @@ import {
|
|
|
18
21
|
AggregationService
|
|
19
22
|
} from '../service.js';
|
|
20
23
|
import type { Transport } from '../transport/transport.js';
|
|
24
|
+
import { DEFAULT_FEE_ESTIMATOR } from '../../beacon/fee-estimator.js';
|
|
25
|
+
import type { FeeEstimator } from '../../beacon/fee-estimator.js';
|
|
21
26
|
import type { AggregationServiceEvents } from './events.js';
|
|
22
27
|
import { TypedEventEmitter } from './typed-emitter.js';
|
|
23
28
|
|
|
@@ -35,6 +40,13 @@ export type OnProvideTxData = (info: {
|
|
|
35
40
|
cohortId: string;
|
|
36
41
|
beaconAddress: string;
|
|
37
42
|
signalBytes: Uint8Array;
|
|
43
|
+
/**
|
|
44
|
+
* Fee estimator the runner is configured with (the runner's `feeEstimator`
|
|
45
|
+
* option, or a static 5 sat/vB default). Forward it to the beacon transaction
|
|
46
|
+
* builder so a dynamic rate injected at the runner is honored, rather than
|
|
47
|
+
* hard-coding a rate inside this callback (ADR 045).
|
|
48
|
+
*/
|
|
49
|
+
feeEstimator: FeeEstimator;
|
|
38
50
|
}) => Promise<SigningTxData>;
|
|
39
51
|
|
|
40
52
|
export interface AggregationServiceRunnerOptions {
|
|
@@ -68,10 +80,19 @@ export interface AggregationServiceRunnerOptions {
|
|
|
68
80
|
|
|
69
81
|
/**
|
|
70
82
|
* Provide the Bitcoin transaction data to sign.
|
|
71
|
-
* REQUIRED
|
|
83
|
+
* REQUIRED - no sensible default.
|
|
72
84
|
*/
|
|
73
85
|
onProvideTxData: OnProvideTxData;
|
|
74
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Fee estimator passed to {@link OnProvideTxData} so the beacon transaction the
|
|
89
|
+
* callback builds is sized at a chosen rate. Inject a dynamic estimator (a mempool
|
|
90
|
+
* API or Bitcoin Core `estimatesmartfee`) here as the single standard point for
|
|
91
|
+
* fee-rate selection, instead of hard-coding a rate inside the callback (ADR 045).
|
|
92
|
+
* Defaults to a static 5 sat/vB estimator.
|
|
93
|
+
*/
|
|
94
|
+
feeEstimator?: FeeEstimator;
|
|
95
|
+
|
|
75
96
|
/**
|
|
76
97
|
* Maximum canonicalized byte-length of a signed update body. Submissions
|
|
77
98
|
* above this cap are rejected and surfaced via the `message-rejected` event.
|
|
@@ -98,7 +119,7 @@ export interface AggregationServiceRunnerOptions {
|
|
|
98
119
|
/**
|
|
99
120
|
* Re-publish COHORT_ADVERT on this interval until a cohort's keygen is
|
|
100
121
|
* finalized. Works around relays that don't backfill historical events to
|
|
101
|
-
* late subscribers
|
|
122
|
+
* late subscribers - a republish gives late joiners a window to discover the
|
|
102
123
|
* advert without protocol changes. The first publish is immediate;
|
|
103
124
|
* subsequent publishes fire every `advertRepeatIntervalMs` until that
|
|
104
125
|
* cohort's keygen completes, fails, or is stopped. Defaults to
|
|
@@ -106,6 +127,17 @@ export interface AggregationServiceRunnerOptions {
|
|
|
106
127
|
* once and never retry.
|
|
107
128
|
*/
|
|
108
129
|
advertRepeatIntervalMs?: number;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* When a cohort stalls (phase timeout) while the optimistic n-of-n signing
|
|
133
|
+
* round is in flight, fall back to the k-of-n script path instead of failing
|
|
134
|
+
* the cohort (graceful liveness, ADR 042). Off by default: enabling it trades a
|
|
135
|
+
* cheaper/private key-path spend for a larger script-path spend whenever the
|
|
136
|
+
* optimistic round does not complete in time. A stall outside the signing
|
|
137
|
+
* phases still fails the cohort. Operators can also drive the fallback
|
|
138
|
+
* explicitly via {@link AggregationServiceRunner.triggerFallback}.
|
|
139
|
+
*/
|
|
140
|
+
autoFallbackOnStall?: boolean;
|
|
109
141
|
}
|
|
110
142
|
|
|
111
143
|
/** Default cadence for re-publishing COHORT_ADVERT until keygen completes: 60 seconds. */
|
|
@@ -139,6 +171,14 @@ interface RunContext {
|
|
|
139
171
|
finalizing: boolean;
|
|
140
172
|
/** Once settled (resolved or rejected), late timers/messages must not re-settle. */
|
|
141
173
|
settled: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* The spend path this cohort is committed to once signing reaches a decision:
|
|
176
|
+
* `optimistic` (n-of-n key path) or `fallback` (k-of-n script path). A cohort
|
|
177
|
+
* spends its single beacon UTXO exactly once, so this latch (set synchronously
|
|
178
|
+
* before any await) ensures the optimistic completion and the fallback never
|
|
179
|
+
* both finalize and broadcast the same UTXO (the ADR-042 double-spend hazard).
|
|
180
|
+
*/
|
|
181
|
+
committedPath?: 'optimistic' | 'fallback';
|
|
142
182
|
cohortTtlTimer?: ReturnType<typeof setTimeout>;
|
|
143
183
|
phaseTimer?: ReturnType<typeof setTimeout>;
|
|
144
184
|
lastObservedPhase?: string;
|
|
@@ -155,7 +195,7 @@ interface RunContext {
|
|
|
155
195
|
*
|
|
156
196
|
* A single runner is a long-lived multiplexer: it advertises and drives many
|
|
157
197
|
* cohorts concurrently over one transport. Each advertised cohort owns an
|
|
158
|
-
* independent completion promise and fails in isolation
|
|
198
|
+
* independent completion promise and fails in isolation - a stalled or failed
|
|
159
199
|
* cohort never settles its siblings (see ADR 040). Use
|
|
160
200
|
* {@link AggregationServiceRunner.advertiseCohort} for the multi-cohort path;
|
|
161
201
|
* {@link AggregationServiceRunner.run} is a thin single-cohort convenience over
|
|
@@ -170,8 +210,9 @@ interface RunContext {
|
|
|
170
210
|
* transport,
|
|
171
211
|
* did: serviceDid,
|
|
172
212
|
* keys: serviceKeys,
|
|
173
|
-
* onProvideTxData: async ({
|
|
174
|
-
*
|
|
213
|
+
* onProvideTxData: async ({ beaconAddress, signalBytes, feeEstimator }) => {
|
|
214
|
+
* // Forward feeEstimator so a dynamic rate injected at the runner is honored.
|
|
215
|
+
* return await buildBeaconTransaction(beaconAddress, signalBytes, bitcoin, feeEstimator);
|
|
175
216
|
* },
|
|
176
217
|
* });
|
|
177
218
|
*
|
|
@@ -203,9 +244,18 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
203
244
|
readonly #onOptInReceived: OnOptInReceived;
|
|
204
245
|
readonly #onReadyToFinalize: OnReadyToFinalize;
|
|
205
246
|
readonly #onProvideTxData: OnProvideTxData;
|
|
247
|
+
readonly #feeEstimator: FeeEstimator;
|
|
206
248
|
readonly #cohortTtlMs?: number;
|
|
207
249
|
readonly #phaseTimeoutMs?: number;
|
|
208
250
|
readonly #advertRepeatIntervalMs: number;
|
|
251
|
+
readonly #autoFallbackOnStall: boolean;
|
|
252
|
+
|
|
253
|
+
/** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
|
|
254
|
+
static readonly #SIGNING_PHASES: readonly ServiceCohortPhaseType[] = [
|
|
255
|
+
ServiceCohortPhase.SigningStarted,
|
|
256
|
+
ServiceCohortPhase.NoncesCollected,
|
|
257
|
+
ServiceCohortPhase.AwaitingPartialSigs,
|
|
258
|
+
];
|
|
209
259
|
|
|
210
260
|
/** Per-cohort run state, keyed by cohortId. */
|
|
211
261
|
readonly #contexts: Map<string, RunContext> = new Map();
|
|
@@ -222,9 +272,11 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
222
272
|
finalize : acceptedCount >= minRequired,
|
|
223
273
|
}));
|
|
224
274
|
this.#onProvideTxData = options.onProvideTxData;
|
|
275
|
+
this.#feeEstimator = options.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
|
|
225
276
|
this.#cohortTtlMs = options.cohortTtlMs;
|
|
226
277
|
this.#phaseTimeoutMs = options.phaseTimeoutMs;
|
|
227
278
|
this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
|
|
279
|
+
this.#autoFallbackOnStall = options.autoFallbackOnStall ?? false;
|
|
228
280
|
|
|
229
281
|
this.session = new AggregationService({
|
|
230
282
|
// The coordinator never signs, so the state machine receives only the
|
|
@@ -269,7 +321,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
269
321
|
}
|
|
270
322
|
this.#registerHandlers();
|
|
271
323
|
// createCohort validates the conditions and throws on a bad config before
|
|
272
|
-
// any context exists
|
|
324
|
+
// any context exists - fail-fast, nothing to clean up.
|
|
273
325
|
const cohortId = this.session.createCohort(config);
|
|
274
326
|
|
|
275
327
|
let resolve!: (result: AggregationResult) => void;
|
|
@@ -293,7 +345,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
293
345
|
this.#onPhaseMaybeChanged(ctx);
|
|
294
346
|
this.emit('cohort-advertised', { cohortId });
|
|
295
347
|
// Publish the advert. If advertRepeatIntervalMs > 0 we republish on that
|
|
296
|
-
// cadence until this cohort's keygen-complete / fail / stop
|
|
348
|
+
// cadence until this cohort's keygen-complete / fail / stop - works around
|
|
297
349
|
// relays that don't backfill historical events to late subscribers.
|
|
298
350
|
// Otherwise fall back to a single send.
|
|
299
351
|
if(this.#advertRepeatIntervalMs > 0) {
|
|
@@ -404,12 +456,53 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
404
456
|
ctx.phaseTimer = undefined;
|
|
405
457
|
if(this.#phaseTimeoutMs === undefined) return;
|
|
406
458
|
ctx.phaseTimer = setTimeout(() => {
|
|
459
|
+
// A stall during the optimistic signing round can be salvaged by the k-of-n
|
|
460
|
+
// fallback rather than failing the whole cohort (graceful liveness, ADR
|
|
461
|
+
// 042) - but only if enabled and not already committed to a path.
|
|
462
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
463
|
+
const inSigning = phase !== undefined && AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
|
|
464
|
+
if(this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
|
|
465
|
+
this.triggerFallback(ctx.cohortId).catch(err => this.#failCohort(ctx, err as Error));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
407
468
|
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? '?'} for ${this.#phaseTimeoutMs}ms`;
|
|
408
469
|
this.emit('cohort-failed', { cohortId: ctx.cohortId, reason });
|
|
409
470
|
this.#failCohort(ctx, new Error(reason));
|
|
410
471
|
}, this.#phaseTimeoutMs);
|
|
411
472
|
}
|
|
412
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
|
|
476
|
+
* fallback (script-path) signatures instead (ADR 042). Idempotent and safe
|
|
477
|
+
* against the optimistic completion: it commits the cohort to the fallback
|
|
478
|
+
* path synchronously (the `committedPath` latch) before sending anything, so a
|
|
479
|
+
* late optimistic signature can no longer complete-and-broadcast a competing
|
|
480
|
+
* spend of the same UTXO. No-op if the cohort is unknown, already settled, or
|
|
481
|
+
* already committed to a path.
|
|
482
|
+
*
|
|
483
|
+
* Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
|
|
484
|
+
* set; otherwise call it from an operator decision (a UI "fall back now"
|
|
485
|
+
* action). Throws only if the underlying state machine rejects the transition
|
|
486
|
+
* (e.g. signing has not started).
|
|
487
|
+
*/
|
|
488
|
+
async triggerFallback(cohortId: string): Promise<void> {
|
|
489
|
+
const ctx = this.#contexts.get(cohortId);
|
|
490
|
+
if(!ctx || ctx.settled || ctx.committedPath) return;
|
|
491
|
+
// startFallbackSigning is synchronous and throws if the cohort is not in a
|
|
492
|
+
// signing phase (e.g. a premature operator call). Run it FIRST so a rejected
|
|
493
|
+
// transition cannot poison the latch: only after it commits the state machine
|
|
494
|
+
// to the fallback do we set committedPath. This still happens synchronously
|
|
495
|
+
// before any await, so a concurrent optimistic completion observes the latch
|
|
496
|
+
// and stands down - but a bad-phase call leaves the optimistic path intact.
|
|
497
|
+
const messages = this.session.startFallbackSigning(cohortId);
|
|
498
|
+
ctx.committedPath = 'fallback';
|
|
499
|
+
this.#stopAdvertRepeating(ctx);
|
|
500
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
501
|
+
const sessionId = this.session.getSigningSessionId(cohortId) ?? '';
|
|
502
|
+
this.emit('fallback-started', { cohortId, sessionId });
|
|
503
|
+
await this.#sendAll(messages);
|
|
504
|
+
}
|
|
505
|
+
|
|
413
506
|
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
414
507
|
#onPhaseMaybeChanged(ctx: RunContext): void {
|
|
415
508
|
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
@@ -505,14 +598,16 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
505
598
|
static readonly #HANDLED_MESSAGE_TYPES: readonly string[] = [
|
|
506
599
|
COHORT_OPT_IN,
|
|
507
600
|
SUBMIT_UPDATE,
|
|
601
|
+
SUBMIT_NONINCLUDED,
|
|
508
602
|
VALIDATION_ACK,
|
|
509
603
|
NONCE_CONTRIBUTION,
|
|
510
604
|
SIGNATURE_AUTHORIZATION,
|
|
605
|
+
FALLBACK_SIGNATURE,
|
|
511
606
|
];
|
|
512
607
|
|
|
513
608
|
/**
|
|
514
609
|
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
515
|
-
* are DID-scoped and cohort-agnostic
|
|
610
|
+
* are DID-scoped and cohort-agnostic - one registration serves every cohort
|
|
516
611
|
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
517
612
|
* handler via the inbound message's cohortId.
|
|
518
613
|
*/
|
|
@@ -522,9 +617,14 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
522
617
|
|
|
523
618
|
this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
|
|
524
619
|
this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
|
|
620
|
+
// A non-inclusion (decline) is an update-phase response handled identically:
|
|
621
|
+
// session.receive() routes by type, and the response gate + distribute
|
|
622
|
+
// trigger are shared with SUBMIT_UPDATE.
|
|
623
|
+
this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
|
|
525
624
|
this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
|
|
526
625
|
this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
|
|
527
626
|
this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
|
|
627
|
+
this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
|
|
528
628
|
}
|
|
529
629
|
|
|
530
630
|
/** Internal: detach from the transport. Safe to call repeatedly. */
|
|
@@ -584,7 +684,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
584
684
|
|
|
585
685
|
// Check if it's time to finalize. The per-cohort `finalizing` flag is set
|
|
586
686
|
// synchronously before the first await so concurrent opt-in handlers for
|
|
587
|
-
// the same cohort observe it and skip
|
|
687
|
+
// the same cohort observe it and skip - otherwise two handlers could both
|
|
588
688
|
// pass the minParticipants check and both call finalizeKeygen, the second
|
|
589
689
|
// of which would throw (phase mismatch).
|
|
590
690
|
const cohort = this.session.getCohort(ctx.cohortId)!;
|
|
@@ -595,7 +695,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
595
695
|
minRequired : ctx.config.minParticipants,
|
|
596
696
|
});
|
|
597
697
|
if(!finalizeDecision.finalize) {
|
|
598
|
-
// Operator declined
|
|
698
|
+
// Operator declined - reset the flag so a later opt-in can retry.
|
|
599
699
|
ctx.finalizing = false;
|
|
600
700
|
return;
|
|
601
701
|
}
|
|
@@ -604,7 +704,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
604
704
|
// (which can run all the way to signing-complete) would resolve the
|
|
605
705
|
// cohort's completion promise before this event fires.
|
|
606
706
|
const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
|
|
607
|
-
// Keygen done
|
|
707
|
+
// Keygen done - stop re-advertising the cohort. New participants
|
|
608
708
|
// arriving after this point would be rejected anyway.
|
|
609
709
|
this.#stopAdvertRepeating(ctx);
|
|
610
710
|
this.emit('keygen-complete', {
|
|
@@ -681,6 +781,7 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
681
781
|
cohortId : ctx.cohortId,
|
|
682
782
|
beaconAddress : cohort.beaconAddress,
|
|
683
783
|
signalBytes : cohort.signalBytes!,
|
|
784
|
+
feeEstimator : this.#feeEstimator,
|
|
684
785
|
});
|
|
685
786
|
const authMsgs = this.session.startSigning(ctx.cohortId, txData);
|
|
686
787
|
const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? '';
|
|
@@ -733,7 +834,38 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
733
834
|
this.#drainRejections(ctx);
|
|
734
835
|
this.#onPhaseMaybeChanged(ctx);
|
|
735
836
|
|
|
736
|
-
//
|
|
837
|
+
// If the cohort already committed to the fallback path, ignore a late
|
|
838
|
+
// optimistic completion: only one path may finalize the single beacon UTXO.
|
|
839
|
+
if(ctx.committedPath === 'fallback') return;
|
|
840
|
+
|
|
841
|
+
// The state machine auto-completes when all partial sigs received.
|
|
842
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
843
|
+
if(result) {
|
|
844
|
+
ctx.committedPath = 'optimistic';
|
|
845
|
+
this.#completeCohort(ctx, result);
|
|
846
|
+
}
|
|
847
|
+
} catch(err) {
|
|
848
|
+
this.#failCohort(ctx, err as Error);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Handler for receiving fallback (k-of-n script-path) signatures. The state
|
|
854
|
+
* machine assembles and finalizes the fallback spend once k valid signatures
|
|
855
|
+
* are in; the result is then emitted and the cohort's completion resolves. The
|
|
856
|
+
* cohort is already committed to the fallback path (via {@link triggerFallback}).
|
|
857
|
+
* @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
|
|
858
|
+
* @returns {Promise<void>} Resolves when handling is complete.
|
|
859
|
+
*/
|
|
860
|
+
async #handleFallbackSignature(msg: BaseMessage): Promise<void> {
|
|
861
|
+
if(this.#stopped) return;
|
|
862
|
+
const ctx = this.#contextFor(msg);
|
|
863
|
+
if(!ctx) return;
|
|
864
|
+
try {
|
|
865
|
+
this.session.receive(msg);
|
|
866
|
+
this.#drainRejections(ctx);
|
|
867
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
868
|
+
|
|
737
869
|
const result = this.session.getResult(ctx.cohortId);
|
|
738
870
|
if(result) {
|
|
739
871
|
this.#completeCohort(ctx, result);
|