@did-btcr2/method 0.35.0 → 0.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +460 -223
- package/dist/browser.mjs +460 -223
- package/dist/cjs/index.js +461 -223
- package/dist/esm/core/aggregation/cohort.js +3 -1
- package/dist/esm/core/aggregation/cohort.js.map +1 -1
- package/dist/esm/core/aggregation/conditions.js +75 -0
- package/dist/esm/core/aggregation/conditions.js.map +1 -0
- package/dist/esm/core/aggregation/messages/base.js.map +1 -1
- package/dist/esm/core/aggregation/messages/bodies.js +16 -2
- package/dist/esm/core/aggregation/messages/bodies.js.map +1 -1
- package/dist/esm/core/aggregation/messages/factories.js.map +1 -1
- package/dist/esm/core/aggregation/participant.js +20 -7
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/participant-runner.js +37 -2
- package/dist/esm/core/aggregation/runner/participant-runner.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +323 -189
- package/dist/esm/core/aggregation/runner/service-runner.js.map +1 -1
- package/dist/esm/core/aggregation/service.js +23 -3
- package/dist/esm/core/aggregation/service.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/types/core/aggregation/cohort.d.ts.map +1 -1
- package/dist/types/core/aggregation/conditions.d.ts +58 -0
- package/dist/types/core/aggregation/conditions.d.ts.map +1 -0
- package/dist/types/core/aggregation/messages/base.d.ts +2 -3
- package/dist/types/core/aggregation/messages/base.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/bodies.d.ts +2 -3
- package/dist/types/core/aggregation/messages/bodies.d.ts.map +1 -1
- package/dist/types/core/aggregation/messages/factories.d.ts +2 -3
- package/dist/types/core/aggregation/messages/factories.d.ts.map +1 -1
- package/dist/types/core/aggregation/participant.d.ts +16 -4
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/events.d.ts +22 -11
- package/dist/types/core/aggregation/runner/events.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/participant-runner.d.ts +21 -12
- package/dist/types/core/aggregation/runner/participant-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/runner/service-runner.d.ts +76 -22
- package/dist/types/core/aggregation/runner/service-runner.d.ts.map +1 -1
- package/dist/types/core/aggregation/service.d.ts +8 -4
- package/dist/types/core/aggregation/service.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/aggregation/cohort.ts +3 -1
- package/src/core/aggregation/conditions.ts +116 -0
- package/src/core/aggregation/messages/base.ts +6 -3
- package/src/core/aggregation/messages/bodies.ts +18 -6
- package/src/core/aggregation/messages/factories.ts +2 -3
- package/src/core/aggregation/participant.ts +28 -11
- package/src/core/aggregation/runner/events.ts +23 -14
- package/src/core/aggregation/runner/participant-runner.ts +43 -13
- package/src/core/aggregation/runner/service-runner.ts +375 -195
- package/src/core/aggregation/service.ts +39 -7
- package/src/index.ts +1 -0
package/dist/cjs/index.js
CHANGED
|
@@ -86,6 +86,7 @@ __export(index_exports, {
|
|
|
86
86
|
InMemoryRateLimitStore: () => InMemoryRateLimitStore,
|
|
87
87
|
InMemoryTransport: () => InMemoryTransport,
|
|
88
88
|
InboxBuffer: () => InboxBuffer,
|
|
89
|
+
KNOWN_BEACON_TYPES: () => KNOWN_BEACON_TYPES,
|
|
89
90
|
KeyPairAggregationSigner: () => KeyPairAggregationSigner,
|
|
90
91
|
NONCE_CONTRIBUTION: () => NONCE_CONTRIBUTION,
|
|
91
92
|
NonceCache: () => NonceCache,
|
|
@@ -158,6 +159,7 @@ __export(index_exports, {
|
|
|
158
159
|
registerBeaconStrategy: () => registerBeaconStrategy,
|
|
159
160
|
reviveFromWire: () => reviveFromWire,
|
|
160
161
|
signEnvelope: () => signEnvelope,
|
|
162
|
+
validateCohortConditions: () => validateCohortConditions,
|
|
161
163
|
verifyEnvelope: () => verifyEnvelope,
|
|
162
164
|
verifyRequestAuth: () => verifyRequestAuth
|
|
163
165
|
});
|
|
@@ -301,7 +303,7 @@ var AggregationCohort = class {
|
|
|
301
303
|
validationRejections = /* @__PURE__ */ new Set();
|
|
302
304
|
constructor({ id, minParticipants, serviceDid, network, beaconType }) {
|
|
303
305
|
this.id = id || crypto.randomUUID();
|
|
304
|
-
this.minParticipants = minParticipants
|
|
306
|
+
this.minParticipants = minParticipants ?? 2;
|
|
305
307
|
this.serviceDid = serviceDid || "";
|
|
306
308
|
this.network = network;
|
|
307
309
|
this.beaconType = beaconType || "CASBeacon";
|
|
@@ -467,6 +469,56 @@ var AggregationCohort = class {
|
|
|
467
469
|
}
|
|
468
470
|
};
|
|
469
471
|
|
|
472
|
+
// src/core/aggregation/conditions.ts
|
|
473
|
+
var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
|
|
474
|
+
function checkPair(problems, label, min, max) {
|
|
475
|
+
if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
|
|
476
|
+
problems.push(`min${label} must be an integer >= 0`);
|
|
477
|
+
}
|
|
478
|
+
if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
|
|
479
|
+
problems.push(`max${label} must be an integer >= 0`);
|
|
480
|
+
}
|
|
481
|
+
if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
|
|
482
|
+
problems.push(`max${label} must be >= min${label}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function checkCost(problems, label, cost) {
|
|
486
|
+
if (cost === void 0) return;
|
|
487
|
+
if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
|
|
488
|
+
problems.push(`${label}.amount must be a finite number >= 0`);
|
|
489
|
+
}
|
|
490
|
+
if (typeof cost.unit !== "string" || cost.unit.length === 0) {
|
|
491
|
+
problems.push(`${label}.unit must be a non-empty string`);
|
|
492
|
+
}
|
|
493
|
+
if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
|
|
494
|
+
problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function validateCohortConditions(c) {
|
|
498
|
+
const problems = [];
|
|
499
|
+
if (!KNOWN_BEACON_TYPES.includes(c.beaconType)) {
|
|
500
|
+
problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
|
|
501
|
+
}
|
|
502
|
+
if (!Number.isInteger(c.minParticipants) || c.minParticipants < 1) {
|
|
503
|
+
problems.push("minParticipants must be an integer >= 1");
|
|
504
|
+
}
|
|
505
|
+
if (c.maxParticipants !== void 0) {
|
|
506
|
+
if (!Number.isInteger(c.maxParticipants) || c.maxParticipants < 1) {
|
|
507
|
+
problems.push("maxParticipants must be an integer >= 1");
|
|
508
|
+
} else if (Number.isInteger(c.minParticipants) && c.maxParticipants < c.minParticipants) {
|
|
509
|
+
problems.push("maxParticipants must be >= minParticipants");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
checkPair(problems, "DidsPerParticipant", c.minDidsPerParticipant, c.maxDidsPerParticipant);
|
|
513
|
+
checkPair(problems, "SecondsBetweenAnnouncements", c.minSecondsBetweenAnnouncements, c.maxSecondsBetweenAnnouncements);
|
|
514
|
+
if (c.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c.pendingUpdateTrigger) || c.pendingUpdateTrigger < 1)) {
|
|
515
|
+
problems.push("pendingUpdateTrigger must be an integer >= 1");
|
|
516
|
+
}
|
|
517
|
+
checkCost(problems, "costOfEnrollment", c.costOfEnrollment);
|
|
518
|
+
checkCost(problems, "costPerAnnouncement", c.costPerAnnouncement);
|
|
519
|
+
return problems;
|
|
520
|
+
}
|
|
521
|
+
|
|
470
522
|
// src/core/aggregation/messages/base.ts
|
|
471
523
|
var AGGREGATION_WIRE_VERSION = 1;
|
|
472
524
|
var BaseMessage = class {
|
|
@@ -899,6 +951,14 @@ var AggregationService = class {
|
|
|
899
951
|
* Cohort starts in `Created` phase — call `advertise()` to broadcast.
|
|
900
952
|
*/
|
|
901
953
|
createCohort(config) {
|
|
954
|
+
const problems = validateCohortConditions(config);
|
|
955
|
+
if (problems.length > 0) {
|
|
956
|
+
throw new AggregationServiceError(
|
|
957
|
+
`Invalid cohort conditions: ${problems.join("; ")}`,
|
|
958
|
+
"INVALID_COHORT_CONDITIONS",
|
|
959
|
+
{ problems }
|
|
960
|
+
);
|
|
961
|
+
}
|
|
902
962
|
const cohort = new AggregationCohort({
|
|
903
963
|
serviceDid: this.did,
|
|
904
964
|
minParticipants: config.minParticipants,
|
|
@@ -931,13 +991,13 @@ var AggregationService = class {
|
|
|
931
991
|
{ cohortId, phase: state.phase }
|
|
932
992
|
);
|
|
933
993
|
}
|
|
994
|
+
const { network, ...conditions } = state.config;
|
|
934
995
|
const message = createCohortAdvertMessage({
|
|
935
996
|
from: this.did,
|
|
936
997
|
cohortId,
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
communicationPk: this.publicKey.compressed
|
|
998
|
+
network,
|
|
999
|
+
communicationPk: this.publicKey.compressed,
|
|
1000
|
+
...conditions
|
|
941
1001
|
});
|
|
942
1002
|
state.phase = "Advertised" /* Advertised */;
|
|
943
1003
|
return [message];
|
|
@@ -996,6 +1056,14 @@ var AggregationService = class {
|
|
|
996
1056
|
{ cohortId, participantDid }
|
|
997
1057
|
);
|
|
998
1058
|
}
|
|
1059
|
+
const maxParticipants = state.config.maxParticipants;
|
|
1060
|
+
if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
|
|
1061
|
+
throw new AggregationServiceError(
|
|
1062
|
+
`Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
|
|
1063
|
+
"COHORT_FULL",
|
|
1064
|
+
{ cohortId, maxParticipants }
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
999
1067
|
state.acceptedParticipants.add(participantDid);
|
|
1000
1068
|
state.cohort.participants.push(participantDid);
|
|
1001
1069
|
state.cohort.participantKeys.set(participantDid, optIn.participantPk);
|
|
@@ -1029,6 +1097,14 @@ var AggregationService = class {
|
|
|
1029
1097
|
{ cohortId }
|
|
1030
1098
|
);
|
|
1031
1099
|
}
|
|
1100
|
+
const maxParticipants = state.config.maxParticipants;
|
|
1101
|
+
if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
|
|
1102
|
+
throw new AggregationServiceError(
|
|
1103
|
+
`Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
|
|
1104
|
+
"TOO_MANY_PARTICIPANTS",
|
|
1105
|
+
{ cohortId, maxParticipants }
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1032
1108
|
const beaconAddress = state.cohort.computeBeaconAddress();
|
|
1033
1109
|
state.phase = "CohortSet" /* CohortSet */;
|
|
1034
1110
|
const messages = [];
|
|
@@ -1347,6 +1423,58 @@ var AggregationService = class {
|
|
|
1347
1423
|
var import_common5 = require("@did-btcr2/common");
|
|
1348
1424
|
var import_utils3 = require("@noble/hashes/utils");
|
|
1349
1425
|
var import_btc_signer3 = require("@scure/btc-signer");
|
|
1426
|
+
|
|
1427
|
+
// src/core/aggregation/messages/bodies.ts
|
|
1428
|
+
var hasStr = (b, k) => !!b && typeof b[k] === "string";
|
|
1429
|
+
var hasIntMin = (b, k, min) => {
|
|
1430
|
+
const v = b ? b[k] : void 0;
|
|
1431
|
+
return typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
1432
|
+
};
|
|
1433
|
+
var optIntMin = (b, k, min) => {
|
|
1434
|
+
const v = b ? b[k] : void 0;
|
|
1435
|
+
return v === void 0 || typeof v === "number" && Number.isInteger(v) && v >= min;
|
|
1436
|
+
};
|
|
1437
|
+
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
1438
|
+
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
1439
|
+
var hasBytesArray = (b, k) => {
|
|
1440
|
+
const v = b ? b[k] : void 0;
|
|
1441
|
+
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
1442
|
+
};
|
|
1443
|
+
function isCohortAdvertMessage(m) {
|
|
1444
|
+
return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasIntMin(m.body, "minParticipants", 1) && optIntMin(m.body, "maxParticipants", 1) && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasBytes(m.body, "communicationPk");
|
|
1445
|
+
}
|
|
1446
|
+
function isCohortOptInMessage(m) {
|
|
1447
|
+
return m.type === COHORT_OPT_IN && hasStr(m.body, "cohortId") && hasBytes(m.body, "participantPk") && hasBytes(m.body, "communicationPk");
|
|
1448
|
+
}
|
|
1449
|
+
function isCohortOptInAcceptMessage(m) {
|
|
1450
|
+
return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, "cohortId");
|
|
1451
|
+
}
|
|
1452
|
+
function isCohortReadyMessage(m) {
|
|
1453
|
+
return m.type === COHORT_READY && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconAddress") && hasBytesArray(m.body, "cohortKeys");
|
|
1454
|
+
}
|
|
1455
|
+
function isSubmitUpdateMessage(m) {
|
|
1456
|
+
return m.type === SUBMIT_UPDATE && hasStr(m.body, "cohortId") && !!m.body && typeof m.body.signedUpdate === "object";
|
|
1457
|
+
}
|
|
1458
|
+
function isDistributeAggregatedDataMessage(m) {
|
|
1459
|
+
return m.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconType") && hasStr(m.body, "signalBytesHex");
|
|
1460
|
+
}
|
|
1461
|
+
function isValidationAckMessage(m) {
|
|
1462
|
+
return m.type === VALIDATION_ACK && hasStr(m.body, "cohortId") && hasBool(m.body, "approved");
|
|
1463
|
+
}
|
|
1464
|
+
function isAuthorizationRequestMessage(m) {
|
|
1465
|
+
return m.type === AUTHORIZATION_REQUEST && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasStr(m.body, "pendingTx") && hasStr(m.body, "prevOutScriptHex") && hasStr(m.body, "prevOutValue");
|
|
1466
|
+
}
|
|
1467
|
+
function isNonceContributionMessage(m) {
|
|
1468
|
+
return m.type === NONCE_CONTRIBUTION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "nonceContribution");
|
|
1469
|
+
}
|
|
1470
|
+
function isAggregatedNonceMessage(m) {
|
|
1471
|
+
return m.type === AGGREGATED_NONCE && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "aggregatedNonce");
|
|
1472
|
+
}
|
|
1473
|
+
function isSignatureAuthorizationMessage(m) {
|
|
1474
|
+
return m.type === SIGNATURE_AUTHORIZATION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "partialSignature");
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/core/aggregation/participant.ts
|
|
1350
1478
|
var AggregationParticipant = class {
|
|
1351
1479
|
did;
|
|
1352
1480
|
/** MuSig2 signing capability. The raw secret never lives as a field here. */
|
|
@@ -1404,16 +1532,15 @@ var AggregationParticipant = class {
|
|
|
1404
1532
|
return map;
|
|
1405
1533
|
}
|
|
1406
1534
|
#handleCohortAdvert(message) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1535
|
+
if (!isCohortAdvertMessage(message)) return;
|
|
1536
|
+
const { cohortId, network, communicationPk, ...conditions } = message.body;
|
|
1409
1537
|
if (this.#cohortStates.has(cohortId)) return;
|
|
1410
1538
|
const advert = {
|
|
1411
1539
|
cohortId,
|
|
1412
1540
|
serviceDid: message.from,
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
serviceCommunicationPk: message.body?.communicationPk ?? new Uint8Array()
|
|
1541
|
+
network,
|
|
1542
|
+
serviceCommunicationPk: communicationPk,
|
|
1543
|
+
...conditions
|
|
1417
1544
|
};
|
|
1418
1545
|
this.#cohortStates.set(cohortId, {
|
|
1419
1546
|
phase: "Discovered" /* Discovered */,
|
|
@@ -1438,7 +1565,7 @@ var AggregationParticipant = class {
|
|
|
1438
1565
|
const cohort = new AggregationCohort({
|
|
1439
1566
|
id: cohortId,
|
|
1440
1567
|
serviceDid: state.serviceDid,
|
|
1441
|
-
minParticipants: state.advert.
|
|
1568
|
+
minParticipants: state.advert.minParticipants,
|
|
1442
1569
|
network: state.advert.network,
|
|
1443
1570
|
beaconType: state.advert.beaconType
|
|
1444
1571
|
});
|
|
@@ -1518,6 +1645,17 @@ var AggregationParticipant = class {
|
|
|
1518
1645
|
}
|
|
1519
1646
|
return map;
|
|
1520
1647
|
}
|
|
1648
|
+
/**
|
|
1649
|
+
* The validated aggregated data retained for a cohort, regardless of phase.
|
|
1650
|
+
* Unlike {@link pendingValidations} (which lists only cohorts still awaiting
|
|
1651
|
+
* the validate decision), this returns the stored validation — including the
|
|
1652
|
+
* participant's sidecar (the CAS Announcement map or its SMT inclusion proof)
|
|
1653
|
+
* — so it is still readable once the cohort reaches Complete. Returns
|
|
1654
|
+
* undefined before aggregated data has been received.
|
|
1655
|
+
*/
|
|
1656
|
+
getValidation(cohortId) {
|
|
1657
|
+
return this.#cohortStates.get(cohortId)?.validation;
|
|
1658
|
+
}
|
|
1521
1659
|
#handleDistributeAggregatedData(message) {
|
|
1522
1660
|
const cohortId = message.body?.cohortId;
|
|
1523
1661
|
if (!cohortId) return;
|
|
@@ -1758,49 +1896,6 @@ var SILENT_LOGGER = {
|
|
|
1758
1896
|
}
|
|
1759
1897
|
};
|
|
1760
1898
|
|
|
1761
|
-
// src/core/aggregation/messages/bodies.ts
|
|
1762
|
-
var hasStr = (b, k) => !!b && typeof b[k] === "string";
|
|
1763
|
-
var hasNum = (b, k) => !!b && typeof b[k] === "number";
|
|
1764
|
-
var hasBool = (b, k) => !!b && typeof b[k] === "boolean";
|
|
1765
|
-
var hasBytes = (b, k) => !!b && b[k] instanceof Uint8Array;
|
|
1766
|
-
var hasBytesArray = (b, k) => {
|
|
1767
|
-
const v = b ? b[k] : void 0;
|
|
1768
|
-
return Array.isArray(v) && v.every((x) => x instanceof Uint8Array);
|
|
1769
|
-
};
|
|
1770
|
-
function isCohortAdvertMessage(m) {
|
|
1771
|
-
return m.type === COHORT_ADVERT && hasStr(m.body, "cohortId") && hasNum(m.body, "cohortSize") && hasStr(m.body, "beaconType") && hasStr(m.body, "network") && hasBytes(m.body, "communicationPk");
|
|
1772
|
-
}
|
|
1773
|
-
function isCohortOptInMessage(m) {
|
|
1774
|
-
return m.type === COHORT_OPT_IN && hasStr(m.body, "cohortId") && hasBytes(m.body, "participantPk") && hasBytes(m.body, "communicationPk");
|
|
1775
|
-
}
|
|
1776
|
-
function isCohortOptInAcceptMessage(m) {
|
|
1777
|
-
return m.type === COHORT_OPT_IN_ACCEPT && hasStr(m.body, "cohortId");
|
|
1778
|
-
}
|
|
1779
|
-
function isCohortReadyMessage(m) {
|
|
1780
|
-
return m.type === COHORT_READY && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconAddress") && hasBytesArray(m.body, "cohortKeys");
|
|
1781
|
-
}
|
|
1782
|
-
function isSubmitUpdateMessage(m) {
|
|
1783
|
-
return m.type === SUBMIT_UPDATE && hasStr(m.body, "cohortId") && !!m.body && typeof m.body.signedUpdate === "object";
|
|
1784
|
-
}
|
|
1785
|
-
function isDistributeAggregatedDataMessage(m) {
|
|
1786
|
-
return m.type === DISTRIBUTE_AGGREGATED_DATA && hasStr(m.body, "cohortId") && hasStr(m.body, "beaconType") && hasStr(m.body, "signalBytesHex");
|
|
1787
|
-
}
|
|
1788
|
-
function isValidationAckMessage(m) {
|
|
1789
|
-
return m.type === VALIDATION_ACK && hasStr(m.body, "cohortId") && hasBool(m.body, "approved");
|
|
1790
|
-
}
|
|
1791
|
-
function isAuthorizationRequestMessage(m) {
|
|
1792
|
-
return m.type === AUTHORIZATION_REQUEST && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasStr(m.body, "pendingTx") && hasStr(m.body, "prevOutScriptHex") && hasStr(m.body, "prevOutValue");
|
|
1793
|
-
}
|
|
1794
|
-
function isNonceContributionMessage(m) {
|
|
1795
|
-
return m.type === NONCE_CONTRIBUTION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "nonceContribution");
|
|
1796
|
-
}
|
|
1797
|
-
function isAggregatedNonceMessage(m) {
|
|
1798
|
-
return m.type === AGGREGATED_NONCE && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "aggregatedNonce");
|
|
1799
|
-
}
|
|
1800
|
-
function isSignatureAuthorizationMessage(m) {
|
|
1801
|
-
return m.type === SIGNATURE_AUTHORIZATION && hasStr(m.body, "cohortId") && hasStr(m.body, "sessionId") && hasBytes(m.body, "partialSignature");
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
1899
|
// src/core/aggregation/messages/guards.ts
|
|
1805
1900
|
var KEYGEN_VALUES = /* @__PURE__ */ new Set([
|
|
1806
1901
|
COHORT_ADVERT,
|
|
@@ -3933,35 +4028,22 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
3933
4028
|
session;
|
|
3934
4029
|
#transport;
|
|
3935
4030
|
#did;
|
|
3936
|
-
#
|
|
4031
|
+
#defaultConfig;
|
|
3937
4032
|
#onOptInReceived;
|
|
3938
4033
|
#onReadyToFinalize;
|
|
3939
4034
|
#onProvideTxData;
|
|
3940
4035
|
#cohortTtlMs;
|
|
3941
4036
|
#phaseTimeoutMs;
|
|
3942
4037
|
#advertRepeatIntervalMs;
|
|
3943
|
-
|
|
4038
|
+
/** Per-cohort run state, keyed by cohortId. */
|
|
4039
|
+
#contexts = /* @__PURE__ */ new Map();
|
|
3944
4040
|
#handlersRegistered = false;
|
|
3945
4041
|
#stopped = false;
|
|
3946
|
-
/**
|
|
3947
|
-
* Guard against the async race where two concurrent #handleOptIn invocations
|
|
3948
|
-
* both pass the `participants.length >= minParticipants` check before either
|
|
3949
|
-
* mutates the cohort phase. Set synchronously before any `await` so subsequent
|
|
3950
|
-
* handlers observe it on their next resumption.
|
|
3951
|
-
*/
|
|
3952
|
-
#finalizing = false;
|
|
3953
|
-
#resolveRun;
|
|
3954
|
-
#rejectRun;
|
|
3955
|
-
#cohortTtlTimer;
|
|
3956
|
-
#phaseTimer;
|
|
3957
|
-
#lastObservedPhase;
|
|
3958
|
-
/** Stop handle for the repeating COHORT_ADVERT publish loop. */
|
|
3959
|
-
#stopAdvertRepeat;
|
|
3960
4042
|
constructor(options) {
|
|
3961
4043
|
super();
|
|
3962
4044
|
this.#transport = options.transport;
|
|
3963
4045
|
this.#did = options.did;
|
|
3964
|
-
this.#
|
|
4046
|
+
this.#defaultConfig = options.config;
|
|
3965
4047
|
this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
|
|
3966
4048
|
this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
|
|
3967
4049
|
finalize: acceptedCount >= minRequired
|
|
@@ -3979,55 +4061,126 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
3979
4061
|
maxUpdateSizeBytes: options.maxUpdateSizeBytes
|
|
3980
4062
|
});
|
|
3981
4063
|
}
|
|
4064
|
+
/** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
|
|
4065
|
+
#contextFor(msg) {
|
|
4066
|
+
const cohortId = msg.body?.cohortId;
|
|
4067
|
+
if (!cohortId) return void 0;
|
|
4068
|
+
return this.#contexts.get(cohortId);
|
|
4069
|
+
}
|
|
3982
4070
|
/**
|
|
3983
|
-
* Drain any silent rejections the state machine recorded
|
|
3984
|
-
* recent receive() and surface them as `message-rejected` events.
|
|
3985
|
-
* call even before a cohortId is assigned.
|
|
4071
|
+
* Drain any silent rejections the state machine recorded for a cohort during
|
|
4072
|
+
* the most recent receive() and surface them as `message-rejected` events.
|
|
3986
4073
|
*/
|
|
3987
|
-
#drainRejections() {
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
this.emit("message-rejected", { cohortId: this.#cohortId, ...r });
|
|
4074
|
+
#drainRejections(ctx) {
|
|
4075
|
+
for (const r of this.session.drainRejections(ctx.cohortId)) {
|
|
4076
|
+
this.emit("message-rejected", { cohortId: ctx.cohortId, ...r });
|
|
3991
4077
|
}
|
|
3992
4078
|
}
|
|
3993
4079
|
/**
|
|
3994
|
-
*
|
|
3995
|
-
*
|
|
4080
|
+
* Advertise a new cohort and begin driving it to completion. Callable many
|
|
4081
|
+
* times on one runner; each cohort runs concurrently and independently.
|
|
4082
|
+
*
|
|
4083
|
+
* @param config Per-cohort conditions + network (see {@link CohortConfig}).
|
|
4084
|
+
* @returns The new cohort's id and a `completion` promise that resolves with
|
|
4085
|
+
* that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
|
|
4086
|
+
* @throws If the runner has been stopped, or the config is invalid
|
|
4087
|
+
* (fail-fast via `createCohort`).
|
|
4088
|
+
*/
|
|
4089
|
+
advertiseCohort(config) {
|
|
4090
|
+
if (this.#stopped) {
|
|
4091
|
+
throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
|
|
4092
|
+
}
|
|
4093
|
+
this.#registerHandlers();
|
|
4094
|
+
const cohortId = this.session.createCohort(config);
|
|
4095
|
+
let resolve;
|
|
4096
|
+
let reject;
|
|
4097
|
+
const completion = new Promise((res, rej) => {
|
|
4098
|
+
resolve = res;
|
|
4099
|
+
reject = rej;
|
|
4100
|
+
});
|
|
4101
|
+
const ctx = {
|
|
4102
|
+
cohortId,
|
|
4103
|
+
config,
|
|
4104
|
+
resolve,
|
|
4105
|
+
reject,
|
|
4106
|
+
completion,
|
|
4107
|
+
finalizing: false,
|
|
4108
|
+
settled: false
|
|
4109
|
+
};
|
|
4110
|
+
this.#contexts.set(cohortId, ctx);
|
|
4111
|
+
try {
|
|
4112
|
+
this.#startTimers(ctx);
|
|
4113
|
+
const advertMsgs = this.session.advertise(cohortId);
|
|
4114
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4115
|
+
this.emit("cohort-advertised", { cohortId });
|
|
4116
|
+
if (this.#advertRepeatIntervalMs > 0) {
|
|
4117
|
+
this.#startAdvertRepeat(ctx, advertMsgs);
|
|
4118
|
+
} else {
|
|
4119
|
+
this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
|
|
4120
|
+
}
|
|
4121
|
+
} catch (err) {
|
|
4122
|
+
this.#failCohort(ctx, err);
|
|
4123
|
+
}
|
|
4124
|
+
return { cohortId, completion };
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* Run a single cohort to completion using the `config` supplied in the
|
|
4128
|
+
* runner options. Thin convenience over {@link advertiseCohort} for the
|
|
4129
|
+
* single-cohort case (and the path {@link AggregationRunner.solo} rides).
|
|
3996
4130
|
*
|
|
3997
4131
|
* @returns {Promise<AggregationResult>} The final result with signature and signed tx.
|
|
3998
4132
|
*/
|
|
3999
4133
|
run() {
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4134
|
+
if (!this.#defaultConfig) {
|
|
4135
|
+
return Promise.reject(new AggregationServiceError(
|
|
4136
|
+
"run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
|
|
4137
|
+
"MISSING_COHORT_CONFIG",
|
|
4138
|
+
{}
|
|
4139
|
+
));
|
|
4140
|
+
}
|
|
4141
|
+
try {
|
|
4142
|
+
return this.advertiseCohort(this.#defaultConfig).completion;
|
|
4143
|
+
} catch (err) {
|
|
4144
|
+
return Promise.reject(err);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Wait for every currently-outstanding cohort to settle and return the
|
|
4149
|
+
* successful results. Dynamic drain: cohorts advertised while this is pending
|
|
4150
|
+
* are included, and it resolves only once no cohorts remain. Failed cohorts
|
|
4151
|
+
* are surfaced via `error` / `cohort-failed` events and their rejected
|
|
4152
|
+
* `completion` promises; they are omitted from the returned array (this
|
|
4153
|
+
* method does not throw). Bound long-running cohorts with `cohortTtlMs` /
|
|
4154
|
+
* `phaseTimeoutMs` or this may never resolve.
|
|
4155
|
+
*
|
|
4156
|
+
* @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
|
|
4157
|
+
*/
|
|
4158
|
+
async runAll() {
|
|
4159
|
+
const collected = /* @__PURE__ */ new Map();
|
|
4160
|
+
const onComplete = (result) => {
|
|
4161
|
+
collected.set(result.cohortId, result);
|
|
4162
|
+
};
|
|
4163
|
+
this.on("signing-complete", onComplete);
|
|
4164
|
+
try {
|
|
4165
|
+
while (this.#contexts.size > 0) {
|
|
4166
|
+
await Promise.allSettled([...this.#contexts.values()].map((c) => c.completion));
|
|
4017
4167
|
}
|
|
4018
|
-
}
|
|
4168
|
+
} finally {
|
|
4169
|
+
this.off("signing-complete", onComplete);
|
|
4170
|
+
}
|
|
4171
|
+
return [...collected.values()];
|
|
4019
4172
|
}
|
|
4020
4173
|
/**
|
|
4021
|
-
* Begin publishing
|
|
4022
|
-
* until
|
|
4023
|
-
*
|
|
4174
|
+
* Begin publishing a cohort's advert immediately and on a repeating interval
|
|
4175
|
+
* until the cohort's advert loop is stopped. Each advert is broadcast (no
|
|
4176
|
+
* recipient) via the transport's `publishRepeating` primitive.
|
|
4024
4177
|
*/
|
|
4025
|
-
#startAdvertRepeat(advertMsgs) {
|
|
4178
|
+
#startAdvertRepeat(ctx, advertMsgs) {
|
|
4026
4179
|
const stops = [];
|
|
4027
4180
|
for (const msg of advertMsgs) {
|
|
4028
4181
|
stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
|
|
4029
4182
|
}
|
|
4030
|
-
|
|
4183
|
+
ctx.stopAdvertRepeat = () => {
|
|
4031
4184
|
for (const stop of stops) {
|
|
4032
4185
|
try {
|
|
4033
4186
|
stop();
|
|
@@ -4036,61 +4189,118 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4036
4189
|
}
|
|
4037
4190
|
};
|
|
4038
4191
|
}
|
|
4039
|
-
/** Stop
|
|
4040
|
-
#stopAdvertRepeating() {
|
|
4041
|
-
if (!
|
|
4042
|
-
const stop =
|
|
4043
|
-
|
|
4192
|
+
/** Stop a cohort's advert republish loop. Idempotent. */
|
|
4193
|
+
#stopAdvertRepeating(ctx) {
|
|
4194
|
+
if (!ctx.stopAdvertRepeat) return;
|
|
4195
|
+
const stop = ctx.stopAdvertRepeat;
|
|
4196
|
+
ctx.stopAdvertRepeat = void 0;
|
|
4044
4197
|
stop();
|
|
4045
4198
|
}
|
|
4046
|
-
/** Schedule cohort TTL + phase timeout
|
|
4047
|
-
#startTimers() {
|
|
4199
|
+
/** Schedule a cohort's TTL + phase timeout when it is advertised. */
|
|
4200
|
+
#startTimers(ctx) {
|
|
4048
4201
|
if (this.#cohortTtlMs !== void 0) {
|
|
4049
|
-
|
|
4050
|
-
const reason = `Cohort ${
|
|
4051
|
-
this.emit("cohort-failed", { cohortId:
|
|
4052
|
-
this.#
|
|
4202
|
+
ctx.cohortTtlTimer = setTimeout(() => {
|
|
4203
|
+
const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
|
|
4204
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
4205
|
+
this.#failCohort(ctx, new Error(reason));
|
|
4053
4206
|
}, this.#cohortTtlMs);
|
|
4054
4207
|
}
|
|
4055
|
-
this.#resetPhaseTimer();
|
|
4208
|
+
this.#resetPhaseTimer(ctx);
|
|
4056
4209
|
}
|
|
4057
|
-
/** Reset
|
|
4058
|
-
#resetPhaseTimer() {
|
|
4059
|
-
if (
|
|
4060
|
-
|
|
4210
|
+
/** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
|
|
4211
|
+
#resetPhaseTimer(ctx) {
|
|
4212
|
+
if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
4213
|
+
ctx.phaseTimer = void 0;
|
|
4061
4214
|
if (this.#phaseTimeoutMs === void 0) return;
|
|
4062
|
-
|
|
4063
|
-
const reason = `Cohort ${
|
|
4064
|
-
this.emit("cohort-failed", { cohortId:
|
|
4065
|
-
this.#
|
|
4215
|
+
ctx.phaseTimer = setTimeout(() => {
|
|
4216
|
+
const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
|
|
4217
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
4218
|
+
this.#failCohort(ctx, new Error(reason));
|
|
4066
4219
|
}, this.#phaseTimeoutMs);
|
|
4067
4220
|
}
|
|
4068
|
-
/** Detect a phase change since the last observation and reset
|
|
4069
|
-
#onPhaseMaybeChanged() {
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
this.#
|
|
4074
|
-
|
|
4075
|
-
|
|
4221
|
+
/** Detect a phase change for a cohort since the last observation and reset its phase timer. */
|
|
4222
|
+
#onPhaseMaybeChanged(ctx) {
|
|
4223
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
4224
|
+
if (phase !== ctx.lastObservedPhase) {
|
|
4225
|
+
ctx.lastObservedPhase = phase;
|
|
4226
|
+
this.#resetPhaseTimer(ctx);
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
/** Clear a cohort's timers. Called on completion, stop, and failure. */
|
|
4230
|
+
#clearTimers(ctx) {
|
|
4231
|
+
if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
|
|
4232
|
+
if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
|
|
4233
|
+
ctx.cohortTtlTimer = void 0;
|
|
4234
|
+
ctx.phaseTimer = void 0;
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
|
|
4238
|
+
* its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
|
|
4239
|
+
* and does NOT detach the shared transport handlers. Leaves the cohort in the
|
|
4240
|
+
* state machine; whether that cohort's `session` state is also removed is the
|
|
4241
|
+
* caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
|
|
4242
|
+
*/
|
|
4243
|
+
#disposeCohort(ctx) {
|
|
4244
|
+
this.#stopAdvertRepeating(ctx);
|
|
4245
|
+
this.#clearTimers(ctx);
|
|
4246
|
+
this.#contexts.delete(ctx.cohortId);
|
|
4247
|
+
}
|
|
4248
|
+
/**
|
|
4249
|
+
* Settle one cohort successfully. Reclaims the runner context but leaves the
|
|
4250
|
+
* completed cohort in `session` so callers can read its beaconAddress / cohort
|
|
4251
|
+
* via `session.getCohort(result.cohortId)`; reclaim it with
|
|
4252
|
+
* `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
|
|
4253
|
+
*/
|
|
4254
|
+
#completeCohort(ctx, result) {
|
|
4255
|
+
if (ctx.settled) return;
|
|
4256
|
+
ctx.settled = true;
|
|
4257
|
+
this.#disposeCohort(ctx);
|
|
4258
|
+
this.emit("signing-complete", result);
|
|
4259
|
+
ctx.resolve(result);
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* Fail one cohort. Reclaims its runner context, drops its now-dead state from
|
|
4263
|
+
* the state machine, and rejects only its completion; siblings keep running
|
|
4264
|
+
* and the shared transport handlers stay registered. Idempotent via
|
|
4265
|
+
* `ctx.settled`.
|
|
4266
|
+
*/
|
|
4267
|
+
#failCohort(ctx, err) {
|
|
4268
|
+
if (ctx.settled) return;
|
|
4269
|
+
ctx.settled = true;
|
|
4270
|
+
this.#disposeCohort(ctx);
|
|
4271
|
+
this.session.removeCohort(ctx.cohortId);
|
|
4272
|
+
this.emit("error", err);
|
|
4273
|
+
ctx.reject(err);
|
|
4076
4274
|
}
|
|
4077
|
-
/**
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4275
|
+
/**
|
|
4276
|
+
* Stop a single cohort early without affecting the rest of the runner. Drops
|
|
4277
|
+
* the cohort's state machine state; its `completion` promise rejects with a
|
|
4278
|
+
* stopped error.
|
|
4279
|
+
*/
|
|
4280
|
+
stopCohort(cohortId) {
|
|
4281
|
+
const ctx = this.#contexts.get(cohortId);
|
|
4282
|
+
if (!ctx || ctx.settled) return;
|
|
4283
|
+
ctx.settled = true;
|
|
4284
|
+
this.#disposeCohort(ctx);
|
|
4285
|
+
this.session.removeCohort(cohortId);
|
|
4286
|
+
ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
|
|
4083
4287
|
}
|
|
4084
4288
|
/**
|
|
4085
|
-
* Stop the runner
|
|
4086
|
-
* handlers so a restart or a new runner doesn't inherit
|
|
4289
|
+
* Stop the whole runner. Fails every outstanding cohort, then detaches the
|
|
4290
|
+
* shared transport handlers so a restart or a new runner doesn't inherit
|
|
4291
|
+
* stale dispatch. Safe to call repeatedly.
|
|
4087
4292
|
*/
|
|
4088
4293
|
stop() {
|
|
4089
4294
|
this.#stopped = true;
|
|
4090
|
-
this.#
|
|
4091
|
-
|
|
4295
|
+
for (const ctx of [...this.#contexts.values()]) {
|
|
4296
|
+
if (ctx.settled) continue;
|
|
4297
|
+
ctx.settled = true;
|
|
4298
|
+
this.#disposeCohort(ctx);
|
|
4299
|
+
this.session.removeCohort(ctx.cohortId);
|
|
4300
|
+
ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
|
|
4301
|
+
}
|
|
4302
|
+
this.#contexts.clear();
|
|
4092
4303
|
this.#unregisterHandlers();
|
|
4093
|
-
if (this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
4094
4304
|
}
|
|
4095
4305
|
/** Message types this runner listens for on the transport. */
|
|
4096
4306
|
static #HANDLED_MESSAGE_TYPES = [
|
|
@@ -4101,7 +4311,10 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4101
4311
|
SIGNATURE_AUTHORIZATION
|
|
4102
4312
|
];
|
|
4103
4313
|
/**
|
|
4104
|
-
* Internal: handler registration with the transport. Idempotent.
|
|
4314
|
+
* Internal: handler registration with the transport. Idempotent. Handlers
|
|
4315
|
+
* are DID-scoped and cohort-agnostic — one registration serves every cohort
|
|
4316
|
+
* this runner drives; demux to the right {@link RunContext} happens in each
|
|
4317
|
+
* handler via the inbound message's cohortId.
|
|
4105
4318
|
*/
|
|
4106
4319
|
#registerHandlers() {
|
|
4107
4320
|
if (this.#handlersRegistered) return;
|
|
@@ -4122,23 +4335,25 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4122
4335
|
}
|
|
4123
4336
|
/**
|
|
4124
4337
|
* Internal: message handlers for each protocol step. Each handler:
|
|
4125
|
-
* 1)
|
|
4126
|
-
* 2)
|
|
4127
|
-
* 3)
|
|
4338
|
+
* 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
|
|
4339
|
+
* 2) feeds the message into the state machine via session.receive()
|
|
4340
|
+
* 3) emits a high-level event (carrying cohortId) for external observers
|
|
4341
|
+
* 4) checks if the new state triggers any automatic next steps, and if so:
|
|
4128
4342
|
* a) calls the appropriate decision callback(s)
|
|
4129
4343
|
* b) sends any resulting messages from the state machine
|
|
4344
|
+
* Errors fail only the owning cohort. A stopped runner ignores messages.
|
|
4130
4345
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
4131
4346
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
4132
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
4133
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
4134
4347
|
*/
|
|
4135
4348
|
async #handleOptIn(msg) {
|
|
4136
4349
|
if (this.#stopped) return;
|
|
4350
|
+
const ctx = this.#contextFor(msg);
|
|
4351
|
+
if (!ctx) return;
|
|
4137
4352
|
try {
|
|
4138
4353
|
this.session.receive(msg);
|
|
4139
|
-
this.#drainRejections();
|
|
4140
|
-
this.#onPhaseMaybeChanged();
|
|
4141
|
-
const optIn = this.session.pendingOptIns(
|
|
4354
|
+
this.#drainRejections(ctx);
|
|
4355
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4356
|
+
const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
|
|
4142
4357
|
if (!optIn) return;
|
|
4143
4358
|
this.emit("opt-in-received", optIn);
|
|
4144
4359
|
if (optIn.communicationPk) {
|
|
@@ -4146,29 +4361,34 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4146
4361
|
}
|
|
4147
4362
|
const decision = await this.#onOptInReceived(optIn);
|
|
4148
4363
|
if (!decision.accepted) return;
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4364
|
+
const maxParticipants = ctx.config.maxParticipants;
|
|
4365
|
+
const cohortNow = this.session.getCohort(ctx.cohortId);
|
|
4366
|
+
if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
|
|
4370
|
+
this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
4371
|
+
const cohort = this.session.getCohort(ctx.cohortId);
|
|
4372
|
+
if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
|
|
4373
|
+
ctx.finalizing = true;
|
|
4154
4374
|
const finalizeDecision = await this.#onReadyToFinalize({
|
|
4155
4375
|
acceptedCount: cohort.participants.length,
|
|
4156
|
-
minRequired:
|
|
4376
|
+
minRequired: ctx.config.minParticipants
|
|
4157
4377
|
});
|
|
4158
4378
|
if (!finalizeDecision.finalize) {
|
|
4159
|
-
|
|
4379
|
+
ctx.finalizing = false;
|
|
4160
4380
|
return;
|
|
4161
4381
|
}
|
|
4162
|
-
const readyMsgs = this.session.finalizeKeygen(
|
|
4163
|
-
this.#stopAdvertRepeating();
|
|
4382
|
+
const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
|
|
4383
|
+
this.#stopAdvertRepeating(ctx);
|
|
4164
4384
|
this.emit("keygen-complete", {
|
|
4165
|
-
cohortId:
|
|
4385
|
+
cohortId: ctx.cohortId,
|
|
4166
4386
|
beaconAddress: cohort.beaconAddress
|
|
4167
4387
|
});
|
|
4168
4388
|
await this.#sendAll(readyMsgs);
|
|
4169
4389
|
}
|
|
4170
4390
|
} catch (err) {
|
|
4171
|
-
this.#
|
|
4391
|
+
this.#failCohort(ctx, err);
|
|
4172
4392
|
}
|
|
4173
4393
|
}
|
|
4174
4394
|
/**
|
|
@@ -4176,23 +4396,23 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4176
4396
|
* and distributes the data for validation.
|
|
4177
4397
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
4178
4398
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
4179
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
4180
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
4181
4399
|
*/
|
|
4182
4400
|
async #handleSubmitUpdate(msg) {
|
|
4183
4401
|
if (this.#stopped) return;
|
|
4402
|
+
const ctx = this.#contextFor(msg);
|
|
4403
|
+
if (!ctx) return;
|
|
4184
4404
|
try {
|
|
4185
4405
|
this.session.receive(msg);
|
|
4186
|
-
this.#drainRejections();
|
|
4187
|
-
this.#onPhaseMaybeChanged();
|
|
4188
|
-
this.emit("update-received", { participantDid: msg.from });
|
|
4189
|
-
if (this.session.getCohortPhase(
|
|
4190
|
-
const distributeMsgs = this.session.buildAndDistribute(
|
|
4191
|
-
this.emit("data-distributed", { cohortId:
|
|
4406
|
+
this.#drainRejections(ctx);
|
|
4407
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4408
|
+
this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
4409
|
+
if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
|
|
4410
|
+
const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
|
|
4411
|
+
this.emit("data-distributed", { cohortId: ctx.cohortId });
|
|
4192
4412
|
await this.#sendAll(distributeMsgs);
|
|
4193
4413
|
}
|
|
4194
4414
|
} catch (err) {
|
|
4195
|
-
this.#
|
|
4415
|
+
this.#failCohort(ctx, err);
|
|
4196
4416
|
}
|
|
4197
4417
|
}
|
|
4198
4418
|
/**
|
|
@@ -4200,111 +4420,95 @@ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEven
|
|
|
4200
4420
|
* automatically requests tx data and starts signing.
|
|
4201
4421
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
4202
4422
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
4203
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
4204
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
4205
4423
|
*/
|
|
4206
4424
|
async #handleValidationAck(msg) {
|
|
4207
4425
|
if (this.#stopped) return;
|
|
4426
|
+
const ctx = this.#contextFor(msg);
|
|
4427
|
+
if (!ctx) return;
|
|
4208
4428
|
try {
|
|
4209
4429
|
this.session.receive(msg);
|
|
4210
|
-
this.#drainRejections();
|
|
4211
|
-
this.#onPhaseMaybeChanged();
|
|
4430
|
+
this.#drainRejections(ctx);
|
|
4431
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4212
4432
|
const approved = !!msg.body?.approved;
|
|
4213
|
-
this.emit("validation-received", { participantDid: msg.from, approved });
|
|
4214
|
-
const phase = this.session.getCohortPhase(
|
|
4433
|
+
this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
|
|
4434
|
+
const phase = this.session.getCohortPhase(ctx.cohortId);
|
|
4215
4435
|
if (phase === "Failed" /* Failed */) {
|
|
4216
4436
|
const reason = `Validation rejected by participant ${msg.from}`;
|
|
4217
|
-
this.emit("cohort-failed", { cohortId:
|
|
4218
|
-
this.#
|
|
4437
|
+
this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
|
|
4438
|
+
this.#failCohort(ctx, new Error(reason));
|
|
4219
4439
|
return;
|
|
4220
4440
|
}
|
|
4221
4441
|
if (phase === "Validated" /* Validated */) {
|
|
4222
|
-
const cohort = this.session.getCohort(
|
|
4442
|
+
const cohort = this.session.getCohort(ctx.cohortId);
|
|
4223
4443
|
const txData = await this.#onProvideTxData({
|
|
4224
|
-
cohortId:
|
|
4444
|
+
cohortId: ctx.cohortId,
|
|
4225
4445
|
beaconAddress: cohort.beaconAddress,
|
|
4226
4446
|
signalBytes: cohort.signalBytes
|
|
4227
4447
|
});
|
|
4228
|
-
const authMsgs = this.session.startSigning(
|
|
4229
|
-
const sessionId = this.session.getSigningSessionId(
|
|
4230
|
-
this.emit("signing-started", { sessionId });
|
|
4448
|
+
const authMsgs = this.session.startSigning(ctx.cohortId, txData);
|
|
4449
|
+
const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
|
|
4450
|
+
this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
|
|
4231
4451
|
await this.#sendAll(authMsgs);
|
|
4232
4452
|
}
|
|
4233
4453
|
} catch (err) {
|
|
4234
|
-
this.#
|
|
4454
|
+
this.#failCohort(ctx, err);
|
|
4235
4455
|
}
|
|
4236
4456
|
}
|
|
4237
4457
|
/**
|
|
4238
|
-
* Handler for receiving nonce contributions
|
|
4239
|
-
*
|
|
4458
|
+
* Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
|
|
4459
|
+
* nonce back to the cohort.
|
|
4240
4460
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
4241
4461
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
4242
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
4243
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
4244
4462
|
*/
|
|
4245
4463
|
async #handleNonceContribution(msg) {
|
|
4246
4464
|
if (this.#stopped) return;
|
|
4465
|
+
const ctx = this.#contextFor(msg);
|
|
4466
|
+
if (!ctx) return;
|
|
4247
4467
|
try {
|
|
4248
4468
|
this.session.receive(msg);
|
|
4249
|
-
this.#drainRejections();
|
|
4250
|
-
this.#onPhaseMaybeChanged();
|
|
4251
|
-
this.emit("nonce-received", { participantDid: msg.from });
|
|
4252
|
-
if (this.session.getCohortPhase(
|
|
4253
|
-
await this.#sendAll(this.session.sendAggregatedNonce(
|
|
4469
|
+
this.#drainRejections(ctx);
|
|
4470
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4471
|
+
this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
|
|
4472
|
+
if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
|
|
4473
|
+
await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
|
|
4254
4474
|
}
|
|
4255
4475
|
} catch (err) {
|
|
4256
|
-
this.#
|
|
4476
|
+
this.#failCohort(ctx, err);
|
|
4257
4477
|
}
|
|
4258
4478
|
}
|
|
4259
4479
|
/**
|
|
4260
4480
|
* Handler for receiving signature authorizations. When all partial signatures are received, the
|
|
4261
|
-
* session automatically completes
|
|
4481
|
+
* session automatically completes; the final result is emitted and the cohort's completion
|
|
4482
|
+
* promise resolves.
|
|
4262
4483
|
* @param {BaseMessage} msg - The incoming message to handle.
|
|
4263
4484
|
* @returns {Promise<void>} Resolves when handling is complete.
|
|
4264
|
-
* @throws {Error} If any step of handling fails, the error is emitted and the run promise is rejected.
|
|
4265
|
-
* Note: if the runner has been stopped, handlers will ignore incoming messages.
|
|
4266
4485
|
*/
|
|
4267
4486
|
async #handleSignatureAuthorization(msg) {
|
|
4268
4487
|
if (this.#stopped) return;
|
|
4488
|
+
const ctx = this.#contextFor(msg);
|
|
4489
|
+
if (!ctx) return;
|
|
4269
4490
|
try {
|
|
4270
4491
|
this.session.receive(msg);
|
|
4271
|
-
this.#drainRejections();
|
|
4272
|
-
this.#onPhaseMaybeChanged();
|
|
4273
|
-
const result = this.session.getResult(
|
|
4492
|
+
this.#drainRejections(ctx);
|
|
4493
|
+
this.#onPhaseMaybeChanged(ctx);
|
|
4494
|
+
const result = this.session.getResult(ctx.cohortId);
|
|
4274
4495
|
if (result) {
|
|
4275
|
-
this.#
|
|
4276
|
-
this.#unregisterHandlers();
|
|
4277
|
-
this.emit("signing-complete", result);
|
|
4278
|
-
this.#resolveRun?.(result);
|
|
4496
|
+
this.#completeCohort(ctx, result);
|
|
4279
4497
|
}
|
|
4280
4498
|
} catch (err) {
|
|
4281
|
-
this.#
|
|
4499
|
+
this.#failCohort(ctx, err);
|
|
4282
4500
|
}
|
|
4283
4501
|
}
|
|
4284
4502
|
/**
|
|
4285
4503
|
* Internal: helper to send all messages sequentially. Catches and propagates errors.
|
|
4286
4504
|
* @param {BaseMessage[]} msgs - The messages to send.
|
|
4287
4505
|
* @returns {Promise<void>} Resolves when all messages have been sent.
|
|
4288
|
-
* @throws {Error} If sending any message fails, the error is emitted and the run promise is
|
|
4289
|
-
* rejected.
|
|
4290
4506
|
*/
|
|
4291
4507
|
async #sendAll(msgs) {
|
|
4292
4508
|
for (const m of msgs) {
|
|
4293
4509
|
await this.#transport.sendMessage(m, this.#did, m.to);
|
|
4294
4510
|
}
|
|
4295
4511
|
}
|
|
4296
|
-
/**
|
|
4297
|
-
* Internal: helper to handle errors. Emits an 'error' event and rejects the run promise.
|
|
4298
|
-
* @param {Error} err - The error to handle.
|
|
4299
|
-
*/
|
|
4300
|
-
#fail(err) {
|
|
4301
|
-
this.#stopAdvertRepeating();
|
|
4302
|
-
this.#clearTimers();
|
|
4303
|
-
this.#unregisterHandlers();
|
|
4304
|
-
if (this.#cohortId) this.session.removeCohort(this.#cohortId);
|
|
4305
|
-
this.emit("error", err);
|
|
4306
|
-
this.#rejectRun?.(err);
|
|
4307
|
-
}
|
|
4308
4512
|
};
|
|
4309
4513
|
|
|
4310
4514
|
// src/core/aggregation/runner/participant-runner.ts
|
|
@@ -4364,7 +4568,8 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
4364
4568
|
}
|
|
4365
4569
|
/**
|
|
4366
4570
|
* Single-shot helper: start, join the first cohort that passes `shouldJoin`,
|
|
4367
|
-
* drive it to completion, and resolve. Convenient for tests and demos.
|
|
4571
|
+
* drive it to completion, and resolve. Convenient for tests and demos. The
|
|
4572
|
+
* single-cohort special case of {@link joinMatching} (count = 1).
|
|
4368
4573
|
*/
|
|
4369
4574
|
static async joinFirst(options) {
|
|
4370
4575
|
return new Promise((resolve, reject) => {
|
|
@@ -4377,6 +4582,37 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
4377
4582
|
runner.start().catch(reject);
|
|
4378
4583
|
});
|
|
4379
4584
|
}
|
|
4585
|
+
/**
|
|
4586
|
+
* Multi-cohort helper: start, join EVERY cohort whose advert passes
|
|
4587
|
+
* `shouldJoin`, drive each to completion in parallel, and resolve once
|
|
4588
|
+
* `count` cohorts have completed (the runner stops at that point). The
|
|
4589
|
+
* N-cohort generalization of {@link joinFirst}, for a participant that joins
|
|
4590
|
+
* several cohorts advertised by one service.
|
|
4591
|
+
*
|
|
4592
|
+
* For an open-ended, long-lived subscriber (no fixed count), construct an
|
|
4593
|
+
* {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
|
|
4594
|
+
* `start()`, and listen for `cohort-complete` — the runner already drives
|
|
4595
|
+
* any number of cohorts concurrently.
|
|
4596
|
+
*
|
|
4597
|
+
* @param options Participant runner options (set `shouldJoin` to select cohorts).
|
|
4598
|
+
* @param count Number of completed cohorts to collect before resolving.
|
|
4599
|
+
* @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
|
|
4600
|
+
*/
|
|
4601
|
+
static async joinMatching(options, count) {
|
|
4602
|
+
return new Promise((resolve, reject) => {
|
|
4603
|
+
const runner = new _AggregationParticipantRunner(options);
|
|
4604
|
+
const completed = [];
|
|
4605
|
+
runner.on("cohort-complete", (info) => {
|
|
4606
|
+
completed.push(info);
|
|
4607
|
+
if (completed.length >= count) {
|
|
4608
|
+
runner.stop();
|
|
4609
|
+
resolve(completed);
|
|
4610
|
+
}
|
|
4611
|
+
});
|
|
4612
|
+
runner.on("error", reject);
|
|
4613
|
+
runner.start().catch(reject);
|
|
4614
|
+
});
|
|
4615
|
+
}
|
|
4380
4616
|
/**
|
|
4381
4617
|
* Internal: handler registration with the transport. Idempotent and safe to call multiple times,
|
|
4382
4618
|
* but only registers handlers once.
|
|
@@ -4525,7 +4761,7 @@ var AggregationParticipantRunner = class _AggregationParticipantRunner extends T
|
|
|
4525
4761
|
if (this.session.getCohortPhase(cohortId) === "Complete" /* Complete */) {
|
|
4526
4762
|
const info = this.session.joinedCohorts.get(cohortId);
|
|
4527
4763
|
if (info) {
|
|
4528
|
-
const validation = this.session.
|
|
4764
|
+
const validation = this.session.getValidation(cohortId);
|
|
4529
4765
|
this.emit("cohort-complete", {
|
|
4530
4766
|
cohortId,
|
|
4531
4767
|
beaconAddress: info.beaconAddress,
|
|
@@ -7826,6 +8062,7 @@ var DidDocumentBuilder = class {
|
|
|
7826
8062
|
InMemoryRateLimitStore,
|
|
7827
8063
|
InMemoryTransport,
|
|
7828
8064
|
InboxBuffer,
|
|
8065
|
+
KNOWN_BEACON_TYPES,
|
|
7829
8066
|
KeyPairAggregationSigner,
|
|
7830
8067
|
NONCE_CONTRIBUTION,
|
|
7831
8068
|
NonceCache,
|
|
@@ -7898,6 +8135,7 @@ var DidDocumentBuilder = class {
|
|
|
7898
8135
|
registerBeaconStrategy,
|
|
7899
8136
|
reviveFromWire,
|
|
7900
8137
|
signEnvelope,
|
|
8138
|
+
validateCohortConditions,
|
|
7901
8139
|
verifyEnvelope,
|
|
7902
8140
|
verifyRequestAuth
|
|
7903
8141
|
});
|