threshold-elgamal 1.0.0-beta.2 → 1.0.0-beta.3

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 CHANGED
@@ -29,6 +29,14 @@ self-contained library.
29
29
 
30
30
  This library is a hardened research prototype. It has not been audited.
31
31
 
32
+ The current `1.x` scope is additive score voting with public rosters, private
33
+ ballots, strict-majority threshold decryption, and locally verifiable per-option
34
+ sum tallies that callers can interpret as arithmetic means. The current
35
+ checkpointed DKG path is intended for small thesis-scale all-equal ceremonies.
36
+ Treat `10` participants as the recommended default size for regression testing
37
+ and mobile-first deployments today. Treat `50` all-equal participants as an
38
+ experimental upper target, not the default operating point.
39
+
32
40
  Start with these guides:
33
41
 
34
42
  - [Get started](https://tenemo.github.io/threshold-elgamal/guides/getting-started/)
@@ -47,8 +55,8 @@ Start with these guides:
47
55
 
48
56
  - [Threshold sharing and decryption helpers](https://tenemo.github.io/threshold-elgamal/api/reference/threshold/) provide dealer-based Shamir sharing, verified decryption shares, and aggregate decryption support.
49
57
  - [Feldman and Pedersen VSS helpers](https://tenemo.github.io/threshold-elgamal/api/reference/vss/) cover verifiable secret sharing commitments and share checks.
50
- - [Typed protocol payloads, manifest handling, transcript hashing, and published tally verification](https://tenemo.github.io/threshold-elgamal/api/reference/protocol/) cover the library's signed ceremony and tally surface.
51
- - [Log-driven Joint-Feldman and GJKR reducers](https://tenemo.github.io/threshold-elgamal/api/reference/dkg/) provide the distributed key-generation state machines behind the threshold workflow.
58
+ - [Typed protocol payloads, manifest handling, transcript hashing, and per-option published tally verification](https://tenemo.github.io/threshold-elgamal/api/reference/protocol/) cover the library's signed ceremony and tally surface.
59
+ - [Log-driven Joint-Feldman and GJKR reducers](https://tenemo.github.io/threshold-elgamal/api/reference/dkg/) provide the distributed key-generation state machines behind the threshold workflow, including checkpointed phase closure and verifier-side `QUAL` reduction when setup participants drop out.
52
60
 
53
61
  ### Proofs, transport, and runtime
54
62
 
@@ -69,6 +77,12 @@ pnpm add threshold-elgamal
69
77
  - Browsers need native `bigint` together with Web Crypto (`crypto.subtle` and `crypto.getRandomValues`).
70
78
  - Node requires version `24.14.1` or newer with `globalThis.crypto`.
71
79
 
80
+ ## Performance model
81
+
82
+ - Keep worker orchestration in the application. `threshold-elgamal` stays pure and importable inside a Web Worker, while the app decides pool size, chunking, and lifecycle.
83
+ - The library now exposes a pluggable bigint backend through `setBigintMathBackend()` in `threshold-elgamal/core`. JavaScript remains the default backend. Optional WASM acceleration should be installed explicitly by the caller.
84
+ - `minimumPublicationThreshold` is a publication privacy floor for tally release. It is not the DKG reconstruction threshold.
85
+
72
86
  ## Quickstart
73
87
 
74
88
  ```typescript
@@ -135,10 +149,10 @@ pnpm run ci
135
149
 
136
150
  ## DKG benchmark
137
151
 
138
- For the DKG benchmark sweep, run:
152
+ For the recommended default regression benchmark, run:
139
153
 
140
154
  ```bash
141
- pnpm run bench:dkg -- --group=ffdhe3072 --transport=X25519 3,11,21,31,41,51
155
+ pnpm run bench:dkg -- --group=ffdhe3072 --transport=X25519 --options=3 10
142
156
  ```
143
157
 
144
158
  Measurements were collected on this machine:
@@ -151,20 +165,19 @@ Results:
151
165
 
152
166
  - Group: `ffdhe3072`
153
167
  - Transport: `X25519`
154
- - Total elapsed time: `2 h 25 min 49.572 s`
155
-
156
- | Participants (`n`) | Threshold (`k`) | Transcript messages | Full voting flow | Transcript verification | Total |
157
- | ------------------ | --------------- | ------------------- | ------------------ | ----------------------- | ------------------ |
158
- | 3 | 2 | 22 | 21.322 s | 729.839 ms | 22.052 s |
159
- | 11 | 6 | 166 | 2 min 6.564 s | 8.222 s | 2 min 14.786 s |
160
- | 21 | 11 | 526 | 8 min 2.140 s | 29.292 s | 8 min 31.432 s |
161
- | 31 | 16 | 1086 | 19 min 55.017 s | 1 min 5.193 s | 21 min 0.210 s |
162
- | 41 | 21 | 1846 | 39 min 54.015 s | 1 min 46.184 s | 41 min 40.199 s |
163
- | 51 | 26 | 2806 | 1 h 9 min 19.986 s | 2 min 40.902 s | 1 h 12 min 0.888 s |
164
-
165
- The sweep scales superlinearly in both transcript volume and runtime, with the
166
- `n=51`, `k=26` case producing `2806` transcript messages and taking just over
167
- `72` minutes for a full run plus verification on this hardware.
168
+ - Options: `3`
169
+ - Participants: `10`
170
+ - Threshold: `6`
171
+ - Total elapsed time: `2 min 36.369 s`
172
+
173
+ | Participants (`n`) | Threshold (`k`) | Options | Transcript messages | Full voting flow | Transcript verification | Total |
174
+ | ------------------ | --------------- | ------- | ------------------- | ---------------- | ----------------------- | -------------- |
175
+ | 10 | 6 | 3 | 155 | 2 min 31.076 s | 5.294 s | 2 min 36.369 s |
176
+
177
+ The current DKG path is still all-to-all in the setup phase, so this benchmark
178
+ is a readiness spot check for a recommended default size of `10` participants,
179
+ not evidence that the symmetric all-equal flow is suitable for thousands of
180
+ participants.
168
181
 
169
182
  ## License
170
183
 
@@ -1,3 +1,44 @@
1
+ /** One base-exponent pair used by multi-exponentiation helpers. */
2
+ export type MultiExponentiationTerm = {
3
+ readonly base: bigint;
4
+ readonly exponent: bigint;
5
+ };
6
+ /** Optional pluggable bigint backend used for modular exponentiation. */
7
+ export type BigintMathBackend = {
8
+ readonly modPow?: (base: bigint, exponent: bigint, modulus: bigint) => bigint;
9
+ };
10
+ /**
11
+ * Installs an optional bigint backend for modular exponentiation.
12
+ *
13
+ * Passing `null` or `undefined` restores the built-in JavaScript backend.
14
+ *
15
+ * @param backend Optional custom bigint backend.
16
+ */
17
+ export declare const setBigintMathBackend: (backend?: BigintMathBackend | null) => void;
18
+ /** Returns the currently installed bigint backend. */
19
+ export declare const getBigintMathBackend: () => BigintMathBackend;
20
+ /** Restores the built-in JavaScript bigint backend. */
21
+ export declare const resetBigintMathBackend: () => void;
22
+ /**
23
+ * Computes `base^exponent mod modulus` using a fixed-base cache when the
24
+ * built-in JavaScript backend is active.
25
+ *
26
+ * The cache is bounded and intended for genuinely reused bases such as frozen
27
+ * generators. Variable bases should prefer `modPowP()`.
28
+ *
29
+ * @throws {@link InvalidScalarError} When `modulus` is not positive or
30
+ * `exponent` is negative.
31
+ */
32
+ export declare const fixedBaseModPow: (base: bigint, exponent: bigint, modulus: bigint) => bigint;
33
+ /**
34
+ * Computes `Π(base_i^exponent_i) mod modulus` with a shared JS bit walk.
35
+ *
36
+ * Custom backends fall back to repeated backend-backed exponentiations.
37
+ *
38
+ * @throws {@link InvalidScalarError} When `modulus` is not positive or any
39
+ * exponent is negative.
40
+ */
41
+ export declare const multiExponentiate: (terms: readonly MultiExponentiationTerm[], modulus: bigint) => bigint;
1
42
  /**
2
43
  * Reduces a value into the canonical range `0..modulus-1`.
3
44
  *
@@ -1,4 +1,7 @@
1
1
  import { InvalidScalarError } from './errors.js';
2
+ const fixedBasePowerCache = new Map();
3
+ const maxFixedBaseCacheEntries = 16;
4
+ let currentBackend = Object.freeze({});
2
5
  const assertPositiveModulus = (modulus) => {
3
6
  if (modulus <= 0n) {
4
7
  throw new InvalidScalarError('Modulus must be positive');
@@ -30,7 +33,8 @@ const modInv = (value, modulus) => {
30
33
  }
31
34
  return normalize(x, modulus);
32
35
  };
33
- const modPow = (base, exponent, modulus) => {
36
+ const bitLength = (value) => value === 0n ? 1 : value.toString(2).length;
37
+ const jsModPow = (base, exponent, modulus) => {
34
38
  if (modulus === 1n) {
35
39
  return 0n;
36
40
  }
@@ -48,6 +52,129 @@ const modPow = (base, exponent, modulus) => {
48
52
  }
49
53
  return result;
50
54
  };
55
+ const fixedBaseCacheKey = (base, modulus) => `${modulus.toString(16)}:${base.toString(16)}`;
56
+ const ensureFixedBasePowers = (base, modulus, exponent) => {
57
+ const key = fixedBaseCacheKey(base, modulus);
58
+ const requiredLength = bitLength(exponent);
59
+ const existing = fixedBasePowerCache.get(key) ?? [base];
60
+ while (existing.length < requiredLength) {
61
+ const previous = existing[existing.length - 1];
62
+ existing.push(normalize(previous * previous, modulus));
63
+ }
64
+ fixedBasePowerCache.set(key, existing);
65
+ if (fixedBasePowerCache.size > maxFixedBaseCacheEntries) {
66
+ const oldestKey = fixedBasePowerCache.keys().next().value;
67
+ if (typeof oldestKey === 'string') {
68
+ fixedBasePowerCache.delete(oldestKey);
69
+ }
70
+ }
71
+ return existing;
72
+ };
73
+ const modPowWithBackend = (base, exponent, modulus) => normalize(currentBackend.modPow?.(base, exponent, modulus) ??
74
+ jsModPow(base, exponent, modulus), modulus);
75
+ /**
76
+ * Installs an optional bigint backend for modular exponentiation.
77
+ *
78
+ * Passing `null` or `undefined` restores the built-in JavaScript backend.
79
+ *
80
+ * @param backend Optional custom bigint backend.
81
+ */
82
+ export const setBigintMathBackend = (backend) => {
83
+ currentBackend = Object.freeze({
84
+ modPow: backend?.modPow,
85
+ });
86
+ fixedBasePowerCache.clear();
87
+ };
88
+ /** Returns the currently installed bigint backend. */
89
+ export const getBigintMathBackend = () => currentBackend;
90
+ /** Restores the built-in JavaScript bigint backend. */
91
+ export const resetBigintMathBackend = () => {
92
+ setBigintMathBackend();
93
+ };
94
+ /**
95
+ * Computes `base^exponent mod modulus` using a fixed-base cache when the
96
+ * built-in JavaScript backend is active.
97
+ *
98
+ * The cache is bounded and intended for genuinely reused bases such as frozen
99
+ * generators. Variable bases should prefer `modPowP()`.
100
+ *
101
+ * @throws {@link InvalidScalarError} When `modulus` is not positive or
102
+ * `exponent` is negative.
103
+ */
104
+ export const fixedBaseModPow = (base, exponent, modulus) => {
105
+ assertPositiveModulus(modulus);
106
+ if (exponent < 0n) {
107
+ throw new InvalidScalarError('Exponent must be non-negative');
108
+ }
109
+ if (currentBackend.modPow !== undefined) {
110
+ return modPowWithBackend(normalize(base, modulus), exponent, modulus);
111
+ }
112
+ if (modulus === 1n) {
113
+ return 0n;
114
+ }
115
+ if (exponent === 0n) {
116
+ return 1n;
117
+ }
118
+ const normalizedBase = normalize(base, modulus);
119
+ const powers = ensureFixedBasePowers(normalizedBase, modulus, exponent);
120
+ let result = 1n;
121
+ let currentExponent = exponent;
122
+ let bitIndex = 0;
123
+ while (currentExponent > 0n) {
124
+ if ((currentExponent & 1n) === 1n) {
125
+ result = normalize(result * powers[bitIndex], modulus);
126
+ }
127
+ currentExponent >>= 1n;
128
+ bitIndex += 1;
129
+ }
130
+ return result;
131
+ };
132
+ /**
133
+ * Computes `Π(base_i^exponent_i) mod modulus` with a shared JS bit walk.
134
+ *
135
+ * Custom backends fall back to repeated backend-backed exponentiations.
136
+ *
137
+ * @throws {@link InvalidScalarError} When `modulus` is not positive or any
138
+ * exponent is negative.
139
+ */
140
+ export const multiExponentiate = (terms, modulus) => {
141
+ assertPositiveModulus(modulus);
142
+ const normalizedTerms = terms
143
+ .filter((term) => term.exponent !== 0n)
144
+ .map((term) => {
145
+ if (term.exponent < 0n) {
146
+ throw new InvalidScalarError('Exponent must be non-negative');
147
+ }
148
+ return {
149
+ base: normalize(term.base, modulus),
150
+ exponent: term.exponent,
151
+ };
152
+ });
153
+ if (normalizedTerms.length === 0) {
154
+ return 1n;
155
+ }
156
+ if (normalizedTerms.length === 1) {
157
+ return modPowWithBackend(normalizedTerms[0].base, normalizedTerms[0].exponent, modulus);
158
+ }
159
+ if (currentBackend.modPow !== undefined) {
160
+ return normalizedTerms.reduce((product, term) => normalize(product *
161
+ modPowWithBackend(term.base, term.exponent, modulus), modulus), 1n);
162
+ }
163
+ let result = 1n;
164
+ const maxBits = Math.max(...normalizedTerms.map((term) => bitLength(term.exponent)));
165
+ for (let bitIndex = maxBits - 1; bitIndex >= 0; bitIndex -= 1) {
166
+ result = normalize(result * result, modulus);
167
+ let factor = 1n;
168
+ const bit = BigInt(bitIndex);
169
+ for (const term of normalizedTerms) {
170
+ if (((term.exponent >> bit) & 1n) === 1n) {
171
+ factor = normalize(factor * term.base, modulus);
172
+ }
173
+ }
174
+ result = normalize(result * factor, modulus);
175
+ }
176
+ return result;
177
+ };
51
178
  /**
52
179
  * Reduces a value into the canonical range `0..modulus-1`.
53
180
  *
@@ -100,5 +227,5 @@ export const modPowP = (base, exponent, p) => {
100
227
  if (exponent < 0n) {
101
228
  throw new InvalidScalarError('Exponent must be non-negative');
102
229
  }
103
- return modPow(modP(base, p), exponent, p);
230
+ return modPowWithBackend(modP(base, p), exponent, p);
104
231
  };
@@ -30,25 +30,25 @@ export declare const assertPlaintextAdditive: (value: bigint, bound: bigint, q:
30
30
  */
31
31
  export declare const assertThreshold: (threshold: number, participantCount: number) => void;
32
32
  /**
33
- * Derives the supported honest-majority threshold `ceil(n / 2)`.
33
+ * Derives the minimum strict-majority threshold `floor(n / 2) + 1`.
34
34
  *
35
35
  * @param participantCount Total participant count `n`.
36
- * @returns Supported reconstruction threshold `k`.
36
+ * @returns Minimum supported reconstruction threshold `k`.
37
37
  *
38
38
  * @throws {@link ThresholdViolationError} When `participantCount` is not a
39
39
  * positive integer.
40
40
  */
41
41
  export declare const majorityThreshold: (participantCount: number) => number;
42
42
  /**
43
- * Validates that the supplied threshold matches the supported honest-majority
44
- * threshold `ceil(n / 2)`.
43
+ * Validates that the supplied threshold satisfies the shipped strict-majority
44
+ * policy.
45
45
  *
46
46
  * @param threshold Claimed reconstruction threshold.
47
47
  * @param participantCount Total participant count `n`.
48
- * @returns The validated majority threshold.
48
+ * @returns The validated reconstruction threshold.
49
49
  *
50
- * @throws {@link ThresholdViolationError} When the threshold does not match the
51
- * supported honest-majority policy.
50
+ * @throws {@link ThresholdViolationError} When the threshold falls outside the
51
+ * supported strict-majority policy.
52
52
  */
53
53
  export declare const assertMajorityThreshold: (threshold: number, participantCount: number) => number;
54
54
  /**
@@ -47,10 +47,10 @@ export const assertThreshold = (threshold, participantCount) => {
47
47
  }
48
48
  };
49
49
  /**
50
- * Derives the supported honest-majority threshold `ceil(n / 2)`.
50
+ * Derives the minimum strict-majority threshold `floor(n / 2) + 1`.
51
51
  *
52
52
  * @param participantCount Total participant count `n`.
53
- * @returns Supported reconstruction threshold `k`.
53
+ * @returns Minimum supported reconstruction threshold `k`.
54
54
  *
55
55
  * @throws {@link ThresholdViolationError} When `participantCount` is not a
56
56
  * positive integer.
@@ -59,26 +59,30 @@ export const majorityThreshold = (participantCount) => {
59
59
  if (!Number.isInteger(participantCount) || participantCount < 1) {
60
60
  throw new ThresholdViolationError('Participant count must be a positive integer');
61
61
  }
62
- return Math.ceil(participantCount / 2);
62
+ return Math.floor(participantCount / 2) + 1;
63
63
  };
64
64
  /**
65
- * Validates that the supplied threshold matches the supported honest-majority
66
- * threshold `ceil(n / 2)`.
65
+ * Validates that the supplied threshold satisfies the shipped strict-majority
66
+ * policy.
67
67
  *
68
68
  * @param threshold Claimed reconstruction threshold.
69
69
  * @param participantCount Total participant count `n`.
70
- * @returns The validated majority threshold.
70
+ * @returns The validated reconstruction threshold.
71
71
  *
72
- * @throws {@link ThresholdViolationError} When the threshold does not match the
73
- * supported honest-majority policy.
72
+ * @throws {@link ThresholdViolationError} When the threshold falls outside the
73
+ * supported strict-majority policy.
74
74
  */
75
75
  export const assertMajorityThreshold = (threshold, participantCount) => {
76
76
  assertThreshold(threshold, participantCount);
77
- const expectedThreshold = majorityThreshold(participantCount);
78
- if (threshold !== expectedThreshold) {
79
- throw new ThresholdViolationError(`Supported distributed threshold must equal ceil(n / 2) = ${expectedThreshold} for n = ${participantCount}`);
77
+ if (participantCount < 3) {
78
+ throw new ThresholdViolationError('Distributed threshold workflows require at least three participants');
80
79
  }
81
- return expectedThreshold;
80
+ const minimumThreshold = majorityThreshold(participantCount);
81
+ const maximumThreshold = participantCount - 1;
82
+ if (threshold < minimumThreshold || threshold > maximumThreshold) {
83
+ throw new ThresholdViolationError(`Supported distributed threshold must satisfy floor(n / 2) + 1 <= k <= n - 1 (minimum ${minimumThreshold}, maximum ${maximumThreshold} for n = ${participantCount})`);
84
+ }
85
+ return threshold;
82
86
  };
83
87
  /**
84
88
  * Validates a 1-based participant index without assuming a fixed participant
@@ -0,0 +1,21 @@
1
+ import type { PhaseCheckpointPayload, SignedPayload } from '../protocol/types.js';
2
+ import type { DKGProtocol } from './types.js';
3
+ /** Finalized threshold-supported checkpoint for one DKG phase. */
4
+ export type FinalizedPhaseCheckpoint = {
5
+ readonly payload: PhaseCheckpointPayload;
6
+ readonly signatures: readonly SignedPayload<PhaseCheckpointPayload>[];
7
+ readonly signers: readonly number[];
8
+ };
9
+ /** Returns `true` when the signed payload is a phase checkpoint. */
10
+ export declare const isPhaseCheckpointPayload: (payload: SignedPayload) => payload is SignedPayload<PhaseCheckpointPayload>;
11
+ /** Returns the required checkpoint phases for the selected DKG protocol. */
12
+ export declare const requiredCheckpointPhases: (protocol: DKGProtocol) => readonly number[];
13
+ /** Returns the last required checkpoint phase for the selected DKG protocol. */
14
+ export declare const finalCheckpointPhase: (protocol: DKGProtocol) => number;
15
+ /** Groups all checkpoint variants observed for one closed DKG phase. */
16
+ export declare const collectCheckpointVariants: (transcript: readonly SignedPayload[], checkpointPhase: number) => readonly FinalizedPhaseCheckpoint[];
17
+ /**
18
+ * Returns the unique threshold-supported checkpoint variant for one phase, or
19
+ * `null` when no unique threshold-supported checkpoint exists yet.
20
+ */
21
+ export declare const resolveFinalizedPhaseCheckpoint: (transcript: readonly SignedPayload[], checkpointPhase: number, threshold: number) => FinalizedPhaseCheckpoint | null;
@@ -0,0 +1,49 @@
1
+ const checkpointKey = (payload) => JSON.stringify({
2
+ sessionId: payload.sessionId,
3
+ manifestHash: payload.manifestHash,
4
+ phase: payload.phase,
5
+ messageType: payload.messageType,
6
+ checkpointPhase: payload.checkpointPhase,
7
+ checkpointTranscriptHash: payload.checkpointTranscriptHash,
8
+ qualParticipantIndices: payload.qualParticipantIndices,
9
+ });
10
+ const compareNumbers = (left, right) => left - right;
11
+ /** Returns `true` when the signed payload is a phase checkpoint. */
12
+ export const isPhaseCheckpointPayload = (payload) => payload.payload.messageType === 'phase-checkpoint';
13
+ /** Returns the required checkpoint phases for the selected DKG protocol. */
14
+ export const requiredCheckpointPhases = (protocol) => (protocol === 'gjkr' ? [0, 1, 2, 3] : [0, 1, 2]);
15
+ /** Returns the last required checkpoint phase for the selected DKG protocol. */
16
+ export const finalCheckpointPhase = (protocol) => requiredCheckpointPhases(protocol)[requiredCheckpointPhases(protocol).length - 1] ?? 0;
17
+ /** Groups all checkpoint variants observed for one closed DKG phase. */
18
+ export const collectCheckpointVariants = (transcript, checkpointPhase) => {
19
+ const grouped = new Map();
20
+ for (const signedPayload of transcript) {
21
+ if (!isPhaseCheckpointPayload(signedPayload) ||
22
+ signedPayload.payload.checkpointPhase !== checkpointPhase) {
23
+ continue;
24
+ }
25
+ const key = checkpointKey(signedPayload.payload);
26
+ const existing = grouped.get(key) ??
27
+ new Map();
28
+ existing.set(signedPayload.payload.participantIndex, signedPayload);
29
+ grouped.set(key, existing);
30
+ }
31
+ return [...grouped.values()].map((signatureMap) => {
32
+ const signatures = [...signatureMap.values()];
33
+ return {
34
+ payload: signatures[0].payload,
35
+ signatures,
36
+ signers: signatures
37
+ .map((entry) => entry.payload.participantIndex)
38
+ .sort(compareNumbers),
39
+ };
40
+ });
41
+ };
42
+ /**
43
+ * Returns the unique threshold-supported checkpoint variant for one phase, or
44
+ * `null` when no unique threshold-supported checkpoint exists yet.
45
+ */
46
+ export const resolveFinalizedPhaseCheckpoint = (transcript, checkpointPhase, threshold) => {
47
+ const supported = collectCheckpointVariants(transcript, checkpointPhase).filter((entry) => entry.signatures.length >= threshold);
48
+ return supported.length === 1 ? supported[0] : null;
49
+ };
@@ -1,5 +1,7 @@
1
1
  import type { ComplaintPayload, ComplaintResolutionPayload, SignedPayload } from '../protocol/types.js';
2
2
  import type { DKGConfig, DKGError, DKGState, DKGTransition } from './types.js';
3
+ /** Removes dealers targeted by unresolved complaints from an existing QUAL set. */
4
+ export declare const reduceQualByComplaints: (qual: readonly number[], complaints: readonly ComplaintPayload[], complaintResolutions?: readonly ComplaintResolutionPayload[]) => readonly number[];
3
5
  /**
4
6
  * Computes QUAL from the frozen participant roster and accepted complaint set.
5
7
  *
@@ -2,6 +2,22 @@ import { classifySlotConflict } from '../protocol/payloads.js';
2
2
  const allParticipants = (participantCount) => Array.from({ length: participantCount }, (_value, index) => index + 1);
3
3
  const complaintKey = (complainantIndex, dealerIndex, envelopeId) => `${complainantIndex}:${dealerIndex}:${envelopeId}`;
4
4
  const isParticipantIndexInRange = (index, participantCount) => Number.isInteger(index) && index >= 1 && index <= participantCount;
5
+ /** Removes dealers targeted by unresolved complaints from an existing QUAL set. */
6
+ export const reduceQualByComplaints = (qual, complaints, complaintResolutions = []) => {
7
+ const complaintKeys = new Set(complaints.map((complaint) => complaintKey(complaint.participantIndex, complaint.dealerIndex, complaint.envelopeId)));
8
+ const resolvedComplaints = new Set(complaintResolutions
9
+ .filter((resolution) => qual.includes(resolution.dealerIndex) &&
10
+ qual.includes(resolution.complainantIndex) &&
11
+ resolution.participantIndex === resolution.dealerIndex &&
12
+ complaintKeys.has(complaintKey(resolution.complainantIndex, resolution.dealerIndex, resolution.envelopeId)))
13
+ .map((resolution) => complaintKey(resolution.complainantIndex, resolution.dealerIndex, resolution.envelopeId)));
14
+ const disqualifiedDealers = new Set(complaints
15
+ .filter((complaint) => qual.includes(complaint.participantIndex) &&
16
+ qual.includes(complaint.dealerIndex) &&
17
+ !resolvedComplaints.has(complaintKey(complaint.participantIndex, complaint.dealerIndex, complaint.envelopeId)))
18
+ .map((complaint) => complaint.dealerIndex));
19
+ return qual.filter((index) => !disqualifiedDealers.has(index));
20
+ };
5
21
  /**
6
22
  * Computes QUAL from the frozen participant roster and accepted complaint set.
7
23
  *
@@ -16,17 +32,7 @@ const isParticipantIndexInRange = (index, participantCount) => Number.isInteger(
16
32
  * @returns Sorted QUAL participant indices.
17
33
  */
18
34
  export const computeQual = (participantCount, complaints, complaintResolutions = []) => {
19
- const complaintKeys = new Set(complaints.map((complaint) => complaintKey(complaint.participantIndex, complaint.dealerIndex, complaint.envelopeId)));
20
- const resolvedComplaints = new Set(complaintResolutions
21
- .filter((resolution) => resolution.participantIndex === resolution.dealerIndex &&
22
- isParticipantIndexInRange(resolution.dealerIndex, participantCount) &&
23
- isParticipantIndexInRange(resolution.complainantIndex, participantCount) &&
24
- complaintKeys.has(complaintKey(resolution.complainantIndex, resolution.dealerIndex, resolution.envelopeId)))
25
- .map((resolution) => complaintKey(resolution.complainantIndex, resolution.dealerIndex, resolution.envelopeId)));
26
- const disqualifiedDealers = new Set(complaints
27
- .filter((complaint) => !resolvedComplaints.has(complaintKey(complaint.participantIndex, complaint.dealerIndex, complaint.envelopeId)))
28
- .map((complaint) => complaint.dealerIndex));
29
- return allParticipants(participantCount).filter((index) => !disqualifiedDealers.has(index));
35
+ return reduceQualByComplaints(allParticipants(participantCount).filter((index) => isParticipantIndexInRange(index, participantCount)), complaints, complaintResolutions);
30
36
  };
31
37
  /**
32
38
  * Creates the initial empty DKG reducer state.
@@ -1,12 +1,12 @@
1
1
  import type { SignedPayload } from '../protocol/types.js';
2
- import type { DKGState, DKGTransition, MajorityDKGConfigInput } from './types.js';
2
+ import type { DKGConfigInput, DKGState, DKGTransition } from './types.js';
3
3
  /**
4
4
  * Creates an empty GJKR state.
5
5
  *
6
6
  * @param config DKG configuration.
7
7
  * @returns Initial GJKR state.
8
8
  */
9
- export declare const createGjkrState: (config: MajorityDKGConfigInput) => DKGState;
9
+ export declare const createGjkrState: (config: DKGConfigInput) => DKGState;
10
10
  /**
11
11
  * Processes one signed payload through the GJKR log reducer.
12
12
  *
@@ -22,4 +22,4 @@ export declare const processGjkrPayload: (state: DKGState, signedPayload: Signed
22
22
  * @param transcript Signed transcript payloads.
23
23
  * @returns Final GJKR state after replay.
24
24
  */
25
- export declare const replayGjkrTranscript: (config: MajorityDKGConfigInput, transcript: readonly SignedPayload[]) => DKGState;
25
+ export declare const replayGjkrTranscript: (config: DKGConfigInput, transcript: readonly SignedPayload[]) => DKGState;
@@ -1,4 +1,5 @@
1
1
  /** Public log-driven DKG reducer exports. */
2
+ export * from './checkpoints.js';
2
3
  export * from './complaints.js';
3
4
  export * from './gjkr.js';
4
5
  export * from './joint-feldman.js';
package/dist/dkg/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Public log-driven DKG reducer exports. */
2
+ export * from './checkpoints.js';
2
3
  export * from './complaints.js';
3
4
  export * from './gjkr.js';
4
5
  export * from './joint-feldman.js';
@@ -1,12 +1,12 @@
1
1
  import type { SignedPayload } from '../protocol/types.js';
2
- import type { DKGState, DKGTransition, MajorityDKGConfigInput } from './types.js';
2
+ import type { DKGConfigInput, DKGState, DKGTransition } from './types.js';
3
3
  /**
4
4
  * Creates an empty Joint-Feldman state.
5
5
  *
6
6
  * @param config DKG configuration.
7
7
  * @returns Initial Joint-Feldman state.
8
8
  */
9
- export declare const createJointFeldmanState: (config: MajorityDKGConfigInput) => DKGState;
9
+ export declare const createJointFeldmanState: (config: DKGConfigInput) => DKGState;
10
10
  /**
11
11
  * Processes one signed payload through the Joint-Feldman log reducer.
12
12
  *
@@ -22,4 +22,4 @@ export declare const processJointFeldmanPayload: (state: DKGState, signedPayload
22
22
  * @param transcript Signed transcript payloads.
23
23
  * @returns Final Joint-Feldman state after replay.
24
24
  */
25
- export declare const replayJointFeldmanTranscript: (config: MajorityDKGConfigInput, transcript: readonly SignedPayload[]) => DKGState;
25
+ export declare const replayJointFeldmanTranscript: (config: DKGConfigInput, transcript: readonly SignedPayload[]) => DKGState;
@@ -1,4 +1,4 @@
1
1
  import type { SignedPayload } from '../protocol/types.js';
2
- import type { DKGProtocol, DKGState, DKGTransition, MajorityDKGConfigInput } from './types.js';
3
- export declare const createMajorityDkgState: (config: MajorityDKGConfigInput, protocol: DKGProtocol) => DKGState;
2
+ import type { DKGProtocol, DKGState, DKGTransition, DKGConfigInput } from './types.js';
3
+ export declare const createMajorityDkgState: (config: DKGConfigInput, protocol: DKGProtocol) => DKGState;
4
4
  export declare const processMajorityDkgPayload: (state: DKGState, signedPayload: SignedPayload) => DKGTransition;