@did-btcr2/method 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/browser.js +146 -56
- package/dist/browser.mjs +146 -56
- package/dist/cjs/index.js +146 -55
- 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 +9 -7
- package/dist/esm/core/aggregation/participant.js.map +1 -1
- package/dist/esm/core/aggregation/runner/service-runner.js +8 -0
- 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 +7 -4
- package/dist/types/core/aggregation/participant.d.ts.map +1 -1
- 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 +16 -11
- package/src/core/aggregation/runner/service-runner.ts +9 -0
- package/src/core/aggregation/service.ts +39 -7
- package/src/index.ts +1 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort conditions: the constraints an Aggregation Service advertises for a
|
|
3
|
+
* cohort (did:btcr2 spec, "Step 1: Create Aggregation Cohort"). See ADR 039.
|
|
4
|
+
*
|
|
5
|
+
* The spec frames these as an optional menu ("the Aggregation Service can define
|
|
6
|
+
* conditions such as ..."), so only `beaconType` and `minParticipants` are
|
|
7
|
+
* required here; every other condition is optional and, when absent, means
|
|
8
|
+
* unconstrained.
|
|
9
|
+
*
|
|
10
|
+
* Enforcement is staged (ADR 039): `beaconType` and the participant bounds are
|
|
11
|
+
* enforced by the state machine now; DIDs-per-participant, timing/cadence, and
|
|
12
|
+
* the pending-update trigger are modeled and advertised here but enforced when
|
|
13
|
+
* the multi-cohort (AGG-4) and non-inclusion (AGG-5) tracks land. The two cost
|
|
14
|
+
* conditions are advertised metadata only - the protocol performs no payment or
|
|
15
|
+
* settlement (consistent with ADR 008).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Beacon types that support aggregation (singleton is single-party only, per ADR 037). */
|
|
19
|
+
export const KNOWN_BEACON_TYPES = ['CASBeacon', 'SMTBeacon'] as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* An advertised price. `unit` is operator-defined (the spec does not specify a
|
|
23
|
+
* currency); `basis` distinguishes a per-DID from a per-participant charge for
|
|
24
|
+
* "cost per announcement". Advertised only - never settled by the protocol.
|
|
25
|
+
*/
|
|
26
|
+
export interface CohortCost {
|
|
27
|
+
amount: number;
|
|
28
|
+
unit: string;
|
|
29
|
+
basis?: 'per-did' | 'per-participant';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The seven spec cohort conditions. Only beaconType + minParticipants are required. */
|
|
33
|
+
export interface CohortConditions {
|
|
34
|
+
/** 1. Beacon mechanism: 'CASBeacon' or 'SMTBeacon'. Enforced. */
|
|
35
|
+
beaconType: string;
|
|
36
|
+
/** 2. Lower bound on cohort size. Enforced (finalize floor). */
|
|
37
|
+
minParticipants: number;
|
|
38
|
+
/** 2. Upper bound on cohort size. Enforced (accept/finalize ceiling). */
|
|
39
|
+
maxParticipants?: number;
|
|
40
|
+
/** 3. Lower bound on DIDs a participant may register. Advertised; enforcement staged (AGG-5). */
|
|
41
|
+
minDidsPerParticipant?: number;
|
|
42
|
+
/** 3. Upper bound on DIDs a participant may register. Advertised; enforcement staged (AGG-5). */
|
|
43
|
+
maxDidsPerParticipant?: number;
|
|
44
|
+
/** 4. One-time enrollment price. Advertised only - no settlement. */
|
|
45
|
+
costOfEnrollment?: CohortCost;
|
|
46
|
+
/** 5. Recurring per-announcement price. Advertised only - no settlement. */
|
|
47
|
+
costPerAnnouncement?: CohortCost;
|
|
48
|
+
/** 6. Floor on time between announcements (seconds). Advertised; enforcement staged (AGG-4/5). */
|
|
49
|
+
minSecondsBetweenAnnouncements?: number;
|
|
50
|
+
/** 6. Ceiling on time between announcements (seconds). Advertised; enforcement staged - generalizes the ADR 027 Cohort TTL. */
|
|
51
|
+
maxSecondsBetweenAnnouncements?: number;
|
|
52
|
+
/** 7. Pending-update count that triggers an announcement. Advertised; enforcement staged (AGG-5) - generalizes hasAllUpdates(). */
|
|
53
|
+
pendingUpdateTrigger?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Validate an optional [min, max] integer pair. */
|
|
57
|
+
function checkPair(problems: string[], label: string, min?: number, max?: number): void {
|
|
58
|
+
if(min !== undefined && (!Number.isInteger(min) || min < 0)) {
|
|
59
|
+
problems.push(`min${label} must be an integer >= 0`);
|
|
60
|
+
}
|
|
61
|
+
if(max !== undefined && (!Number.isInteger(max) || max < 0)) {
|
|
62
|
+
problems.push(`max${label} must be an integer >= 0`);
|
|
63
|
+
}
|
|
64
|
+
if(min !== undefined && max !== undefined && Number.isInteger(min) && Number.isInteger(max) && max < min) {
|
|
65
|
+
problems.push(`max${label} must be >= min${label}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Validate an optional advertised cost. */
|
|
70
|
+
function checkCost(problems: string[], label: string, cost?: CohortCost): void {
|
|
71
|
+
if(cost === undefined) return;
|
|
72
|
+
if(typeof cost.amount !== 'number' || !Number.isFinite(cost.amount) || cost.amount < 0) {
|
|
73
|
+
problems.push(`${label}.amount must be a finite number >= 0`);
|
|
74
|
+
}
|
|
75
|
+
if(typeof cost.unit !== 'string' || cost.unit.length === 0) {
|
|
76
|
+
problems.push(`${label}.unit must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
if(cost.basis !== undefined && cost.basis !== 'per-did' && cost.basis !== 'per-participant') {
|
|
79
|
+
problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate a set of cohort conditions. Returns a list of human-readable problems
|
|
85
|
+
* (empty when valid) so the caller can decide how to surface them. Used by
|
|
86
|
+
* `createCohort()` to fail fast instead of discovering invalidity at finalize.
|
|
87
|
+
*/
|
|
88
|
+
export function validateCohortConditions(c: CohortConditions): string[] {
|
|
89
|
+
const problems: string[] = [];
|
|
90
|
+
|
|
91
|
+
if(!(KNOWN_BEACON_TYPES as readonly string[]).includes(c.beaconType)) {
|
|
92
|
+
problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
if(!Number.isInteger(c.minParticipants) || c.minParticipants < 1) {
|
|
95
|
+
problems.push('minParticipants must be an integer >= 1');
|
|
96
|
+
}
|
|
97
|
+
if(c.maxParticipants !== undefined) {
|
|
98
|
+
if(!Number.isInteger(c.maxParticipants) || c.maxParticipants < 1) {
|
|
99
|
+
problems.push('maxParticipants must be an integer >= 1');
|
|
100
|
+
} else if(Number.isInteger(c.minParticipants) && c.maxParticipants < c.minParticipants) {
|
|
101
|
+
problems.push('maxParticipants must be >= minParticipants');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
checkPair(problems, 'DidsPerParticipant', c.minDidsPerParticipant, c.maxDidsPerParticipant);
|
|
106
|
+
checkPair(problems, 'SecondsBetweenAnnouncements', c.minSecondsBetweenAnnouncements, c.maxSecondsBetweenAnnouncements);
|
|
107
|
+
|
|
108
|
+
if(c.pendingUpdateTrigger !== undefined && (!Number.isInteger(c.pendingUpdateTrigger) || c.pendingUpdateTrigger < 1)) {
|
|
109
|
+
problems.push('pendingUpdateTrigger must be an integer >= 1');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
checkCost(problems, 'costOfEnrollment', c.costOfEnrollment);
|
|
113
|
+
checkCost(problems, 'costPerAnnouncement', c.costPerAnnouncement);
|
|
114
|
+
|
|
115
|
+
return problems;
|
|
116
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CohortConditions } from '../conditions.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Current on-the-wire protocol version.
|
|
3
5
|
*
|
|
@@ -7,9 +9,11 @@
|
|
|
7
9
|
*/
|
|
8
10
|
export const AGGREGATION_WIRE_VERSION = 1;
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
// Cohort conditions (beaconType, minParticipants, maxParticipants, ...) ride on
|
|
13
|
+
// the wire as flat optional body fields, supplied via `Partial<CohortConditions>`
|
|
14
|
+
// below so the bag stays a single source of truth for the advertised conditions.
|
|
15
|
+
export type BaseBody = Partial<CohortConditions> & {
|
|
11
16
|
cohortId: string;
|
|
12
|
-
cohortSize?: number;
|
|
13
17
|
network?: string;
|
|
14
18
|
participantPk?: Uint8Array;
|
|
15
19
|
beaconAddress?: string;
|
|
@@ -23,7 +27,6 @@ export type BaseBody = {
|
|
|
23
27
|
prevOutScriptHex?: string;
|
|
24
28
|
prevOutValue?: string;
|
|
25
29
|
communicationPk?: Uint8Array;
|
|
26
|
-
beaconType?: string;
|
|
27
30
|
data?: string;
|
|
28
31
|
signedUpdate?: Record<string, unknown>;
|
|
29
32
|
casAnnouncement?: Record<string, string>;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { SerializedSMTProof } from '@did-btcr2/smt';
|
|
15
|
+
import type { CohortConditions } from '../conditions.js';
|
|
15
16
|
import type { BaseMessage } from './base.js';
|
|
16
17
|
import {
|
|
17
18
|
AGGREGATED_NONCE,
|
|
@@ -29,10 +30,8 @@ import {
|
|
|
29
30
|
|
|
30
31
|
// ── Cohort formation (Step 1) ─────────────────────────────────────────────
|
|
31
32
|
|
|
32
|
-
export interface CohortAdvertBody {
|
|
33
|
+
export interface CohortAdvertBody extends CohortConditions {
|
|
33
34
|
cohortId: string;
|
|
34
|
-
cohortSize: number;
|
|
35
|
-
beaconType: string;
|
|
36
35
|
network: string;
|
|
37
36
|
communicationPk: Uint8Array;
|
|
38
37
|
}
|
|
@@ -135,8 +134,16 @@ export type AggregationMessage =
|
|
|
135
134
|
|
|
136
135
|
const hasStr = (b: unknown, k: string): boolean =>
|
|
137
136
|
!!b && typeof (b as Record<string, unknown>)[k] === 'string';
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
/** Present, an integer, and >= min. */
|
|
138
|
+
const hasIntMin = (b: unknown, k: string, min: number): boolean => {
|
|
139
|
+
const v = b ? (b as Record<string, unknown>)[k] : undefined;
|
|
140
|
+
return typeof v === 'number' && Number.isInteger(v) && v >= min;
|
|
141
|
+
};
|
|
142
|
+
/** Absent, or present as an integer >= min. */
|
|
143
|
+
const optIntMin = (b: unknown, k: string, min: number): boolean => {
|
|
144
|
+
const v = b ? (b as Record<string, unknown>)[k] : undefined;
|
|
145
|
+
return v === undefined || (typeof v === 'number' && Number.isInteger(v) && v >= min);
|
|
146
|
+
};
|
|
140
147
|
const hasBool = (b: unknown, k: string): boolean =>
|
|
141
148
|
!!b && typeof (b as Record<string, unknown>)[k] === 'boolean';
|
|
142
149
|
const hasBytes = (b: unknown, k: string): boolean =>
|
|
@@ -147,9 +154,14 @@ const hasBytesArray = (b: unknown, k: string): boolean => {
|
|
|
147
154
|
};
|
|
148
155
|
|
|
149
156
|
export function isCohortAdvertMessage(m: BaseMessage): m is CohortAdvertMessage {
|
|
157
|
+
// Range-check the participant bounds so a malformed advert (missing or
|
|
158
|
+
// zero/negative minParticipants) is rejected rather than silently accepted as
|
|
159
|
+
// a zero-floor cohort. The service does the full cross-field validation at
|
|
160
|
+
// createCohort (see validateCohortConditions); here we guard the wire shape.
|
|
150
161
|
return m.type === COHORT_ADVERT
|
|
151
162
|
&& hasStr(m.body, 'cohortId')
|
|
152
|
-
&&
|
|
163
|
+
&& hasIntMin(m.body, 'minParticipants', 1)
|
|
164
|
+
&& optIntMin(m.body, 'maxParticipants', 1)
|
|
153
165
|
&& hasStr(m.body, 'beaconType')
|
|
154
166
|
&& hasStr(m.body, 'network')
|
|
155
167
|
&& hasBytes(m.body, 'communicationPk');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CohortConditions } from '../conditions.js';
|
|
1
2
|
import { BaseMessage } from './base.js';
|
|
2
3
|
import {
|
|
3
4
|
AGGREGATED_NONCE,
|
|
@@ -18,11 +19,9 @@ import {
|
|
|
18
19
|
* Factory functions for creating messages related to the cohort formation step, where cohorts are
|
|
19
20
|
* formed and participants opt in to join the cohort.
|
|
20
21
|
*/
|
|
21
|
-
type CohortAdvertMessage = {
|
|
22
|
+
type CohortAdvertMessage = CohortConditions & {
|
|
22
23
|
from: string;
|
|
23
24
|
cohortId: string;
|
|
24
|
-
cohortSize: number;
|
|
25
|
-
beaconType: string;
|
|
26
25
|
network: string;
|
|
27
26
|
communicationPk: Uint8Array;
|
|
28
27
|
};
|
|
@@ -5,8 +5,10 @@ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
|
5
5
|
import { Transaction } from '@scure/btc-signer';
|
|
6
6
|
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
7
7
|
import { AggregationCohort } from './cohort.js';
|
|
8
|
+
import type { CohortConditions } from './conditions.js';
|
|
8
9
|
import { AggregationParticipantError } from './errors.js';
|
|
9
10
|
import type { BaseMessage } from './messages/base.js';
|
|
11
|
+
import { isCohortAdvertMessage } from './messages/bodies.js';
|
|
10
12
|
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
11
13
|
import {
|
|
12
14
|
AGGREGATED_NONCE,
|
|
@@ -28,13 +30,15 @@ import { ParticipantCohortPhase } from './phases.js';
|
|
|
28
30
|
import type { AggregationSigner } from './signer.js';
|
|
29
31
|
import { BeaconSigningSession } from './signing-session.js';
|
|
30
32
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Cohort advert as discovered by the participant (UI: list of joinable cohorts).
|
|
35
|
+
* Carries the advertised {@link CohortConditions} (beaconType, minParticipants,
|
|
36
|
+
* maxParticipants, costs, ...) so a `shouldJoin` decision can inspect them.
|
|
37
|
+
*/
|
|
38
|
+
export interface CohortAdvert extends CohortConditions {
|
|
33
39
|
cohortId: string;
|
|
34
40
|
serviceDid: string;
|
|
35
|
-
cohortSize: number;
|
|
36
41
|
network: string;
|
|
37
|
-
beaconType: string;
|
|
38
42
|
serviceCommunicationPk: Uint8Array;
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -168,17 +172,18 @@ export class AggregationParticipant {
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
#handleCohortAdvert(message: BaseMessage): void {
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
// Validate the wire shape (incl. minParticipants range) before trusting it,
|
|
176
|
+
// rather than reading fields with `?? 0` fallbacks (see ADR 039).
|
|
177
|
+
if(!isCohortAdvertMessage(message)) return;
|
|
178
|
+
const { cohortId, network, communicationPk, ...conditions } = message.body;
|
|
173
179
|
if(this.#cohortStates.has(cohortId)) return; // Already known
|
|
174
180
|
|
|
175
181
|
const advert: CohortAdvert = {
|
|
176
182
|
cohortId,
|
|
177
183
|
serviceDid : message.from,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
serviceCommunicationPk : message.body?.communicationPk ?? new Uint8Array(),
|
|
184
|
+
network,
|
|
185
|
+
serviceCommunicationPk : communicationPk,
|
|
186
|
+
...conditions,
|
|
182
187
|
};
|
|
183
188
|
|
|
184
189
|
this.#cohortStates.set(cohortId, {
|
|
@@ -206,7 +211,7 @@ export class AggregationParticipant {
|
|
|
206
211
|
const cohort = new AggregationCohort({
|
|
207
212
|
id : cohortId,
|
|
208
213
|
serviceDid : state.serviceDid,
|
|
209
|
-
minParticipants : state.advert!.
|
|
214
|
+
minParticipants : state.advert!.minParticipants,
|
|
210
215
|
network : state.advert!.network,
|
|
211
216
|
beaconType : state.advert!.beaconType,
|
|
212
217
|
});
|
|
@@ -383,6 +383,15 @@ export class AggregationServiceRunner extends TypedEventEmitter<AggregationServi
|
|
|
383
383
|
const decision = await this.#onOptInReceived(optIn);
|
|
384
384
|
if(!decision.accepted) return;
|
|
385
385
|
|
|
386
|
+
// Don't accept past the advertised maxParticipants: acceptParticipant
|
|
387
|
+
// would throw COHORT_FULL and fail the run. Silently ignore the surplus
|
|
388
|
+
// opt-in (the cohort is full).
|
|
389
|
+
const maxParticipants = this.#config.maxParticipants;
|
|
390
|
+
const cohortNow = this.session.getCohort(this.#cohortId!);
|
|
391
|
+
if(maxParticipants !== undefined && cohortNow && cohortNow.participants.length >= maxParticipants) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
386
395
|
await this.#sendAll(this.session.acceptParticipant(this.#cohortId!, msg.from));
|
|
387
396
|
this.emit('participant-accepted', { participantDid: msg.from });
|
|
388
397
|
|
|
@@ -6,6 +6,7 @@ import { bytesToHex } from '@noble/hashes/utils';
|
|
|
6
6
|
import type { Transaction } from '@scure/btc-signer';
|
|
7
7
|
import { getBeaconStrategy } from './beacon-strategy.js';
|
|
8
8
|
import { AggregationCohort } from './cohort.js';
|
|
9
|
+
import { validateCohortConditions, type CohortConditions } from './conditions.js';
|
|
9
10
|
import { AggregationServiceError } from './errors.js';
|
|
10
11
|
import type { BaseMessage } from './messages/base.js';
|
|
11
12
|
import { AGGREGATION_WIRE_VERSION } from './messages/base.js';
|
|
@@ -28,11 +29,14 @@ import type { ServiceCohortPhaseType } from './phases.js';
|
|
|
28
29
|
import { ServiceCohortPhase } from './phases.js';
|
|
29
30
|
import { BeaconSigningSession } from './signing-session.js';
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Cohort configuration set by the service operator: the advertised cohort
|
|
34
|
+
* {@link CohortConditions} plus the Bitcoin network. `beaconType` and
|
|
35
|
+
* `minParticipants` are required; the other conditions are optional (absent =
|
|
36
|
+
* unconstrained). See ADR 039.
|
|
37
|
+
*/
|
|
38
|
+
export interface CohortConfig extends CohortConditions {
|
|
34
39
|
network: string;
|
|
35
|
-
beaconType: string;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/** Pending opt-in awaiting service operator approval. */
|
|
@@ -201,6 +205,14 @@ export class AggregationService {
|
|
|
201
205
|
* Cohort starts in `Created` phase — call `advertise()` to broadcast.
|
|
202
206
|
*/
|
|
203
207
|
createCohort(config: CohortConfig): string {
|
|
208
|
+
// Fail fast on invalid conditions rather than discovering them at finalize.
|
|
209
|
+
const problems = validateCohortConditions(config);
|
|
210
|
+
if(problems.length > 0) {
|
|
211
|
+
throw new AggregationServiceError(
|
|
212
|
+
`Invalid cohort conditions: ${problems.join('; ')}`,
|
|
213
|
+
'INVALID_COHORT_CONDITIONS', { problems }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
204
216
|
const cohort = new AggregationCohort({
|
|
205
217
|
serviceDid : this.did,
|
|
206
218
|
minParticipants : config.minParticipants,
|
|
@@ -234,13 +246,15 @@ export class AggregationService {
|
|
|
234
246
|
);
|
|
235
247
|
}
|
|
236
248
|
|
|
249
|
+
// Advertise the full condition set (flat fields, per ADR 039). network is
|
|
250
|
+
// a separate cohort parameter; everything else in config is a condition.
|
|
251
|
+
const { network, ...conditions } = state.config;
|
|
237
252
|
const message = createCohortAdvertMessage({
|
|
238
253
|
from : this.did,
|
|
239
254
|
cohortId,
|
|
240
|
-
|
|
241
|
-
beaconType : state.config.beaconType,
|
|
242
|
-
network : state.config.network,
|
|
255
|
+
network,
|
|
243
256
|
communicationPk : this.publicKey.compressed,
|
|
257
|
+
...conditions,
|
|
244
258
|
});
|
|
245
259
|
|
|
246
260
|
state.phase = ServiceCohortPhase.Advertised;
|
|
@@ -310,6 +324,15 @@ export class AggregationService {
|
|
|
310
324
|
'ALREADY_ACCEPTED', { cohortId, participantDid }
|
|
311
325
|
);
|
|
312
326
|
}
|
|
327
|
+
// Enforce the maxParticipants condition: a cohort cannot grow past its
|
|
328
|
+
// advertised ceiling (closes the unbounded-growth path; see ADR 039).
|
|
329
|
+
const maxParticipants = state.config.maxParticipants;
|
|
330
|
+
if(maxParticipants !== undefined && state.acceptedParticipants.size >= maxParticipants) {
|
|
331
|
+
throw new AggregationServiceError(
|
|
332
|
+
`Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
|
|
333
|
+
'COHORT_FULL', { cohortId, maxParticipants }
|
|
334
|
+
);
|
|
335
|
+
}
|
|
313
336
|
|
|
314
337
|
state.acceptedParticipants.add(participantDid);
|
|
315
338
|
state.cohort.participants.push(participantDid);
|
|
@@ -344,6 +367,15 @@ export class AggregationService {
|
|
|
344
367
|
'NOT_ENOUGH_PARTICIPANTS', { cohortId }
|
|
345
368
|
);
|
|
346
369
|
}
|
|
370
|
+
// Ceiling defense: acceptParticipant already rejects past max, so reaching
|
|
371
|
+
// here over max means state was mutated out-of-band.
|
|
372
|
+
const maxParticipants = state.config.maxParticipants;
|
|
373
|
+
if(maxParticipants !== undefined && state.acceptedParticipants.size > maxParticipants) {
|
|
374
|
+
throw new AggregationServiceError(
|
|
375
|
+
`Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
|
|
376
|
+
'TOO_MANY_PARTICIPANTS', { cohortId, maxParticipants }
|
|
377
|
+
);
|
|
378
|
+
}
|
|
347
379
|
|
|
348
380
|
const beaconAddress = state.cohort.computeBeaconAddress();
|
|
349
381
|
state.phase = ServiceCohortPhase.CohortSet;
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './core/aggregation/service.js';
|
|
3
3
|
export * from './core/aggregation/participant.js';
|
|
4
4
|
export * from './core/aggregation/signer.js';
|
|
5
|
+
export * from './core/aggregation/conditions.js';
|
|
5
6
|
export * from './core/aggregation/cohort.js';
|
|
6
7
|
export * from './core/aggregation/signing-session.js';
|
|
7
8
|
export * from './core/aggregation/phases.js';
|