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 +31 -18
- package/dist/core/bigint.d.ts +41 -0
- package/dist/core/bigint.js +129 -2
- package/dist/core/validation.d.ts +7 -7
- package/dist/core/validation.js +16 -12
- package/dist/dkg/checkpoints.d.ts +21 -0
- package/dist/dkg/checkpoints.js +49 -0
- package/dist/dkg/complaints.d.ts +2 -0
- package/dist/dkg/complaints.js +17 -11
- package/dist/dkg/gjkr.d.ts +3 -3
- package/dist/dkg/index.d.ts +1 -0
- package/dist/dkg/index.js +1 -0
- package/dist/dkg/joint-feldman.d.ts +3 -3
- package/dist/dkg/majority-reducer.d.ts +2 -2
- package/dist/dkg/majority-reducer.js +62 -13
- package/dist/dkg/phase-plan.d.ts +2 -2
- package/dist/dkg/phase-plan.js +7 -3
- package/dist/dkg/types.d.ts +4 -5
- package/dist/dkg/verification.d.ts +3 -0
- package/dist/dkg/verification.js +222 -51
- package/dist/proofs/disjunctive.js +31 -11
- package/dist/proofs/dleq.js +16 -6
- package/dist/proofs/schnorr.js +9 -4
- package/dist/protocol/ballots.d.ts +16 -0
- package/dist/protocol/ballots.js +46 -0
- package/dist/protocol/manifest.d.ts +1 -1
- package/dist/protocol/manifest.js +1 -1
- package/dist/protocol/payloads.js +3 -1
- package/dist/protocol/transcript.d.ts +20 -0
- package/dist/protocol/transcript.js +22 -0
- package/dist/protocol/types.d.ts +14 -3
- package/dist/protocol/voting.d.ts +81 -11
- package/dist/protocol/voting.js +256 -83
- package/dist/vss/commitment-product.js +7 -4
- package/dist/vss/pedersen.js +9 -5
- package/package.json +1 -1
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
|
|
152
|
+
For the recommended default regression benchmark, run:
|
|
139
153
|
|
|
140
154
|
```bash
|
|
141
|
-
pnpm run bench:dkg -- --group=ffdhe3072 --transport=X25519 3
|
|
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
|
-
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
package/dist/core/bigint.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/core/bigint.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
33
|
+
* Derives the minimum strict-majority threshold `floor(n / 2) + 1`.
|
|
34
34
|
*
|
|
35
35
|
* @param participantCount Total participant count `n`.
|
|
36
|
-
* @returns
|
|
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
|
|
44
|
-
*
|
|
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
|
|
48
|
+
* @returns The validated reconstruction threshold.
|
|
49
49
|
*
|
|
50
|
-
* @throws {@link ThresholdViolationError} When the threshold
|
|
51
|
-
* supported
|
|
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
|
/**
|
package/dist/core/validation.js
CHANGED
|
@@ -47,10 +47,10 @@ export const assertThreshold = (threshold, participantCount) => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
|
-
* Derives the
|
|
50
|
+
* Derives the minimum strict-majority threshold `floor(n / 2) + 1`.
|
|
51
51
|
*
|
|
52
52
|
* @param participantCount Total participant count `n`.
|
|
53
|
-
* @returns
|
|
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.
|
|
62
|
+
return Math.floor(participantCount / 2) + 1;
|
|
63
63
|
};
|
|
64
64
|
/**
|
|
65
|
-
* Validates that the supplied threshold
|
|
66
|
-
*
|
|
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
|
|
70
|
+
* @returns The validated reconstruction threshold.
|
|
71
71
|
*
|
|
72
|
-
* @throws {@link ThresholdViolationError} When the threshold
|
|
73
|
-
* supported
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/dkg/complaints.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/dkg/complaints.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/dkg/gjkr.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { SignedPayload } from '../protocol/types.js';
|
|
2
|
-
import type { DKGState, DKGTransition
|
|
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:
|
|
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:
|
|
25
|
+
export declare const replayGjkrTranscript: (config: DKGConfigInput, transcript: readonly SignedPayload[]) => DKGState;
|
package/dist/dkg/index.d.ts
CHANGED
package/dist/dkg/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { SignedPayload } from '../protocol/types.js';
|
|
2
|
-
import type { DKGState, DKGTransition
|
|
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:
|
|
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:
|
|
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,
|
|
3
|
-
export declare const createMajorityDkgState: (config:
|
|
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;
|