@did-btcr2/method 0.28.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +34125 -44647
- package/dist/browser.mjs +26409 -36931
- package/dist/cjs/index.js +2869 -679
- 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 +33 -7
- 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/factory.js +15 -6
- package/dist/esm/core/aggregation/transport/factory.js.map +1 -1
- package/dist/esm/core/aggregation/transport/http/client.js +350 -0
- package/dist/esm/core/aggregation/transport/http/client.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js +126 -0
- package/dist/esm/core/aggregation/transport/http/envelope.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/errors.js +11 -0
- package/dist/esm/core/aggregation/transport/http/errors.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js +45 -0
- package/dist/esm/core/aggregation/transport/http/inbox-buffer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/index.js +12 -0
- package/dist/esm/core/aggregation/transport/http/index.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js +38 -0
- package/dist/esm/core/aggregation/transport/http/nonce-cache.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js +28 -0
- package/dist/esm/core/aggregation/transport/http/protocol.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js +45 -0
- package/dist/esm/core/aggregation/transport/http/rate-limiter.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js +100 -0
- package/dist/esm/core/aggregation/transport/http/request-auth.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/server.js +481 -0
- package/dist/esm/core/aggregation/transport/http/server.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js +110 -0
- package/dist/esm/core/aggregation/transport/http/sse-stream.js.map +1 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js +25 -0
- package/dist/esm/core/aggregation/transport/http/sse-writer.js.map +1 -0
- package/dist/esm/core/aggregation/transport/index.js +1 -0
- package/dist/esm/core/aggregation/transport/index.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 +295 -63
- package/dist/esm/core/beacon/beacon.js.map +1 -1
- package/dist/esm/core/beacon/cas-beacon.js +3 -3
- package/dist/esm/core/beacon/cas-beacon.js.map +1 -1
- package/dist/esm/core/beacon/singleton-beacon.js +3 -3
- 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/beacon/utils.js +14 -9
- package/dist/esm/core/beacon/utils.js.map +1 -1
- package/dist/esm/core/updater.js +63 -55
- package/dist/esm/core/updater.js.map +1 -1
- package/dist/esm/did-btcr2.js +0 -4
- package/dist/esm/did-btcr2.js.map +1 -1
- package/dist/esm/index.js +2 -0
- 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 +7 -5
- 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/factory.d.ts +22 -7
- package/dist/types/core/aggregation/transport/factory.d.ts.map +1 -1
- package/dist/types/core/aggregation/transport/http/client.d.ts +48 -0
- package/dist/types/core/aggregation/transport/http/client.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts +64 -0
- package/dist/types/core/aggregation/transport/http/envelope.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts +9 -0
- package/dist/types/core/aggregation/transport/http/errors.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts +32 -0
- package/dist/types/core/aggregation/transport/http/inbox-buffer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/index.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts +26 -0
- package/dist/types/core/aggregation/transport/http/nonce-cache.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts +53 -0
- package/dist/types/core/aggregation/transport/http/protocol.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts +41 -0
- package/dist/types/core/aggregation/transport/http/rate-limiter.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts +50 -0
- package/dist/types/core/aggregation/transport/http/request-auth.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts +110 -0
- package/dist/types/core/aggregation/transport/http/server.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts +34 -0
- package/dist/types/core/aggregation/transport/http/sse-stream.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts +12 -0
- package/dist/types/core/aggregation/transport/http/sse-writer.d.ts.map +1 -0
- package/dist/types/core/aggregation/transport/index.d.ts +1 -0
- package/dist/types/core/aggregation/transport/index.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 +26 -1
- package/dist/types/core/aggregation/transport/transport.d.ts.map +1 -1
- package/dist/types/core/beacon/beacon.d.ts +149 -22
- package/dist/types/core/beacon/beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/cas-beacon.d.ts +3 -3
- package/dist/types/core/beacon/cas-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/singleton-beacon.d.ts +3 -3
- package/dist/types/core/beacon/singleton-beacon.d.ts.map +1 -1
- package/dist/types/core/beacon/smt-beacon.d.ts +3 -3
- package/dist/types/core/beacon/smt-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 +27 -12
- package/dist/types/core/updater.d.ts.map +1 -1
- package/dist/types/did-btcr2.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +5 -7
- 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 +41 -7
- 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/factory.ts +48 -12
- package/src/core/aggregation/transport/http/client.ts +409 -0
- package/src/core/aggregation/transport/http/envelope.ts +204 -0
- package/src/core/aggregation/transport/http/errors.ts +11 -0
- package/src/core/aggregation/transport/http/inbox-buffer.ts +53 -0
- package/src/core/aggregation/transport/http/index.ts +11 -0
- package/src/core/aggregation/transport/http/nonce-cache.ts +43 -0
- package/src/core/aggregation/transport/http/protocol.ts +57 -0
- package/src/core/aggregation/transport/http/rate-limiter.ts +75 -0
- package/src/core/aggregation/transport/http/request-auth.ts +164 -0
- package/src/core/aggregation/transport/http/server.ts +615 -0
- package/src/core/aggregation/transport/http/sse-stream.ts +121 -0
- package/src/core/aggregation/transport/http/sse-writer.ts +23 -0
- package/src/core/aggregation/transport/index.ts +1 -0
- package/src/core/aggregation/transport/nostr.ts +266 -23
- package/src/core/aggregation/transport/transport.ts +34 -1
- package/src/core/beacon/beacon.ts +411 -79
- package/src/core/beacon/cas-beacon.ts +4 -4
- package/src/core/beacon/singleton-beacon.ts +4 -4
- package/src/core/beacon/smt-beacon.ts +4 -4
- package/src/core/beacon/utils.ts +16 -11
- package/src/core/updater.ts +113 -67
- package/src/did-btcr2.ts +0 -5
- package/src/index.ts +2 -0
- package/src/utils/did-document.ts +2 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { canonicalHash
|
|
1
|
+
import { canonicalHash } from '@did-btcr2/common';
|
|
2
2
|
import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
|
|
3
3
|
import type { SchnorrKeyPair } from '@did-btcr2/keypair';
|
|
4
4
|
import type { SerializedSMTProof} from '@did-btcr2/smt';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
6
|
+
import { Transaction } from '@scure/btc-signer';
|
|
7
|
+
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
8
8
|
import { AggregationCohort } from './cohort.js';
|
|
9
9
|
import { AggregationParticipantError } from './errors.js';
|
|
10
10
|
import type { BaseMessage } from './messages/base.js';
|
|
11
|
+
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
11
12
|
import {
|
|
12
13
|
AGGREGATED_NONCE,
|
|
13
14
|
AUTHORIZATION_REQUEST,
|
|
@@ -61,6 +62,8 @@ export interface PendingSigningRequest {
|
|
|
61
62
|
cohortId: string;
|
|
62
63
|
sessionId: string;
|
|
63
64
|
pendingTxHex: string;
|
|
65
|
+
/** Hex-encoded scriptPubKey of the UTXO being spent. Required for BIP-341 sighash. */
|
|
66
|
+
prevOutScriptHex: string;
|
|
64
67
|
prevOutValue: string;
|
|
65
68
|
}
|
|
66
69
|
|
|
@@ -110,6 +113,10 @@ export class AggregationParticipant {
|
|
|
110
113
|
* outgoing messages — those come exclusively from action methods.
|
|
111
114
|
*/
|
|
112
115
|
public receive(message: BaseMessage): void {
|
|
116
|
+
// Reject messages whose wire version doesn't match what this build speaks.
|
|
117
|
+
if(message.version === undefined || message.version !== AGGREGATION_WIRE_VERSION) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
113
120
|
const type = message.type;
|
|
114
121
|
switch(type) {
|
|
115
122
|
case COHORT_ADVERT:
|
|
@@ -291,46 +298,28 @@ export class AggregationParticipant {
|
|
|
291
298
|
if(!state.submittedUpdate) return;
|
|
292
299
|
|
|
293
300
|
const beaconType = message.body?.beaconType;
|
|
301
|
+
if(!beaconType) return;
|
|
302
|
+
const strategy = getBeaconStrategy(beaconType);
|
|
303
|
+
if(!strategy) return;
|
|
304
|
+
|
|
294
305
|
const signalBytesHex = message.body?.signalBytesHex ?? '';
|
|
295
306
|
const expectedHash = canonicalHash(state.submittedUpdate);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
state.validation = {
|
|
303
|
-
cohortId,
|
|
304
|
-
beaconType,
|
|
305
|
-
signalBytesHex,
|
|
306
|
-
casAnnouncement,
|
|
307
|
-
expectedHash,
|
|
308
|
-
matches,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
} else if(beaconType === 'SMTBeacon') {
|
|
312
|
-
const smtProof = message.body?.smtProof as unknown as SerializedSMTProof | undefined;
|
|
313
|
-
if(smtProof?.updateId && smtProof?.nonce) {
|
|
314
|
-
// Verify updateId matches the canonicalized update hash
|
|
315
|
-
const canonicalBytes = new TextEncoder().encode(canonicalize(state.submittedUpdate));
|
|
316
|
-
const expectedUpdateId = hashToHex(blockHash(canonicalBytes));
|
|
317
|
-
if(smtProof.updateId === expectedUpdateId) {
|
|
318
|
-
// Verify Merkle inclusion
|
|
319
|
-
const index = didToIndex(this.did);
|
|
320
|
-
const candidateHash = blockHash(blockHash(hexToHash(smtProof.nonce)), hexToHash(smtProof.updateId));
|
|
321
|
-
matches = verifySerializedProof(smtProof, index, candidateHash);
|
|
322
|
-
}
|
|
323
|
-
state.validation = {
|
|
324
|
-
cohortId,
|
|
325
|
-
beaconType,
|
|
326
|
-
signalBytesHex,
|
|
327
|
-
smtProof,
|
|
328
|
-
expectedHash,
|
|
329
|
-
matches,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
}
|
|
307
|
+
const result = strategy.validateParticipantView({
|
|
308
|
+
participantDid : this.did,
|
|
309
|
+
submittedUpdate : state.submittedUpdate,
|
|
310
|
+
expectedHash,
|
|
311
|
+
body : message.body!,
|
|
312
|
+
});
|
|
333
313
|
|
|
314
|
+
state.validation = {
|
|
315
|
+
cohortId,
|
|
316
|
+
beaconType,
|
|
317
|
+
signalBytesHex,
|
|
318
|
+
expectedHash,
|
|
319
|
+
matches : result.matches,
|
|
320
|
+
casAnnouncement : result.casAnnouncement,
|
|
321
|
+
smtProof : result.smtProof,
|
|
322
|
+
};
|
|
334
323
|
state.phase = ParticipantCohortPhase.AwaitingValidation;
|
|
335
324
|
}
|
|
336
325
|
|
|
@@ -395,13 +384,15 @@ export class AggregationParticipant {
|
|
|
395
384
|
|
|
396
385
|
const sessionId = message.body?.sessionId;
|
|
397
386
|
const pendingTxHex = message.body?.pendingTx;
|
|
387
|
+
const prevOutScriptHex = message.body?.prevOutScriptHex;
|
|
398
388
|
const prevOutValue = message.body?.prevOutValue;
|
|
399
|
-
if(!sessionId || !pendingTxHex || !prevOutValue) return;
|
|
389
|
+
if(!sessionId || !pendingTxHex || !prevOutScriptHex || !prevOutValue) return;
|
|
400
390
|
|
|
401
391
|
state.signingRequest = {
|
|
402
392
|
cohortId,
|
|
403
393
|
sessionId,
|
|
404
394
|
pendingTxHex,
|
|
395
|
+
prevOutScriptHex,
|
|
405
396
|
prevOutValue,
|
|
406
397
|
};
|
|
407
398
|
state.phase = ParticipantCohortPhase.AwaitingSigning;
|
|
@@ -425,11 +416,14 @@ export class AggregationParticipant {
|
|
|
425
416
|
);
|
|
426
417
|
}
|
|
427
418
|
|
|
428
|
-
const tx = Transaction.
|
|
419
|
+
const tx = Transaction.fromRaw(hexToBytes(state.signingRequest.pendingTxHex));
|
|
429
420
|
|
|
430
|
-
// Derive UTXO metadata for Taproot sighash (BIP-341).
|
|
431
|
-
//
|
|
432
|
-
|
|
421
|
+
// Derive UTXO metadata for Taproot sighash (BIP-341). Use the script
|
|
422
|
+
// supplied by the service in AUTHORIZATION_REQUEST rather than reading
|
|
423
|
+
// the change output: input and change may use different scripts in future
|
|
424
|
+
// beacon designs, and the prevOutScript must be the UTXO script, not the
|
|
425
|
+
// change script.
|
|
426
|
+
const prevOutScripts = [hexToBytes(state.signingRequest.prevOutScriptHex)];
|
|
433
427
|
const prevOutValues = [BigInt(state.signingRequest.prevOutValue)];
|
|
434
428
|
|
|
435
429
|
const session = new BeaconSigningSession({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { SerializedSMTProof } from '@did-btcr2/smt';
|
|
1
2
|
import type { CohortAdvert, PendingSigningRequest, PendingValidation } from '../participant.js';
|
|
2
|
-
import type { AggregationResult, PendingOptIn } from '../service.js';
|
|
3
|
+
import type { AggregationResult, PendingOptIn, Rejection } from '../service.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* AggregationServiceRunner events are emitted by the AggregationServiceRunner to signal important
|
|
@@ -22,6 +23,13 @@ export type AggregationServiceEvents = {
|
|
|
22
23
|
/** A participant has submitted a signed update. */
|
|
23
24
|
'update-received': [{ participantDid: string }];
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* An inbound message was silently dropped by the state machine (bad proof,
|
|
28
|
+
* oversized payload, wrong wire version, etc.). Fires for *any* rejection,
|
|
29
|
+
* not just SUBMIT_UPDATE.
|
|
30
|
+
*/
|
|
31
|
+
'message-rejected': [Rejection & { cohortId: string }];
|
|
32
|
+
|
|
25
33
|
/** Aggregated data has been distributed to all participants for validation. */
|
|
26
34
|
'data-distributed': [{ cohortId: string }];
|
|
27
35
|
|
|
@@ -37,6 +45,9 @@ export type AggregationServiceEvents = {
|
|
|
37
45
|
/** Signing complete — final aggregated signature is ready to broadcast. */
|
|
38
46
|
'signing-complete': [AggregationResult];
|
|
39
47
|
|
|
48
|
+
/** Cohort transitioned to Failed phase (e.g. a participant rejected validation). */
|
|
49
|
+
'cohort-failed': [{ cohortId: string; reason: string }];
|
|
50
|
+
|
|
40
51
|
/** A non-fatal error occurred. Fatal errors reject the run() promise. */
|
|
41
52
|
'error': [Error];
|
|
42
53
|
};
|
|
@@ -66,8 +77,21 @@ export type AggregationParticipantEvents = {
|
|
|
66
77
|
/** Signing request has arrived. Fires before the sign approval callback. */
|
|
67
78
|
'signing-requested': [PendingSigningRequest];
|
|
68
79
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Cohort signing is complete from this participant's perspective.
|
|
82
|
+
* Includes the aggregated sidecar data the participant needs to keep for
|
|
83
|
+
* future DID resolution: the CAS Announcement map (for CAS beacons) or the
|
|
84
|
+
* SMT inclusion proof (for SMT beacons).
|
|
85
|
+
*/
|
|
86
|
+
'cohort-complete': [{
|
|
87
|
+
cohortId: string;
|
|
88
|
+
beaconAddress: string;
|
|
89
|
+
beaconType: string;
|
|
90
|
+
/** DID → base64url update hash. Populated only for CAS beacons. */
|
|
91
|
+
casAnnouncement?: Record<string, string>;
|
|
92
|
+
/** Merkle inclusion proof for this participant's slot. Populated only for SMT beacons. */
|
|
93
|
+
smtProof?: SerializedSMTProof;
|
|
94
|
+
}];
|
|
71
95
|
|
|
72
96
|
/** Cohort failed (rejected validation, signing error, etc.). */
|
|
73
97
|
'cohort-failed': [{ cohortId: string; reason: string }];
|
|
@@ -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,11 +88,7 @@ export interface AggregationParticipantRunnerOptions {
|
|
|
87
88
|
* keys: myKeys,
|
|
88
89
|
* shouldJoin: async (advert) => advert.beaconType === 'CASBeacon',
|
|
89
90
|
* onProvideUpdate: async ({ beaconAddress }) => {
|
|
90
|
-
<<<<<<< Updated upstream
|
|
91
|
-
* return Update.sign(myDid, unsigned, vm, secretKey);
|
|
92
|
-
=======
|
|
93
91
|
* return Updater.sign(myDid, unsigned, vm, secretKey);
|
|
94
|
-
>>>>>>> Stashed changes
|
|
95
92
|
* },
|
|
96
93
|
* });
|
|
97
94
|
*
|
|
@@ -143,9 +140,29 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
143
140
|
this.#registerHandlers();
|
|
144
141
|
}
|
|
145
142
|
|
|
146
|
-
/** Stop the runner
|
|
143
|
+
/** Stop the runner and detach transport handlers. Safe to call repeatedly. */
|
|
147
144
|
stop(): void {
|
|
148
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
|
+
}
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
/**
|
|
@@ -154,7 +171,15 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
154
171
|
*/
|
|
155
172
|
static async joinFirst(
|
|
156
173
|
options: AggregationParticipantRunnerOptions
|
|
157
|
-
): 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
|
+
}> {
|
|
158
183
|
return new Promise((resolve, reject) => {
|
|
159
184
|
const runner = new AggregationParticipantRunner(options);
|
|
160
185
|
runner.once('cohort-complete', (info) => {
|
|
@@ -341,7 +366,16 @@ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationP
|
|
|
341
366
|
if (this.session.getCohortPhase(cohortId) === ParticipantCohortPhase.Complete) {
|
|
342
367
|
const info = this.session.joinedCohorts.get(cohortId);
|
|
343
368
|
if (info) {
|
|
344
|
-
|
|
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
|
+
});
|
|
345
379
|
}
|
|
346
380
|
}
|
|
347
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
|
}
|