threshold-elgamal 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # threshold-elgamal
2
2
 
3
- [![npm downloads](https://img.shields.io/npm/dm/threshold-elgamal?color=5FA04E)](https://www.npmjs.com/package/threshold-elgamal)
3
+ [![npm version](https://img.shields.io/npm/v/threshold-elgamal?color=5FA04E)](https://www.npmjs.com/package/threshold-elgamal) [![npm downloads](https://img.shields.io/npm/dm/threshold-elgamal?color=5FA04E)](https://www.npmjs.com/package/threshold-elgamal)
4
4
 
5
5
  ---
6
6
 
@@ -16,8 +16,9 @@
16
16
 
17
17
  - additive ElGamal on `ristretto255`
18
18
  - honest-majority GJKR DKG
19
- - fixed score voting in `1..10`
20
- - one public manifest shape: `rosterHash` and `optionList`
19
+ - one explicit global contiguous score range per ceremony
20
+ - manifest `scoreRange.max` capped at `100` to keep proofs and tally recovery tractable
21
+ - one public manifest shape: `rosterHash`, `optionList`, and `scoreRange`
21
22
  - organizer-signed `ballot-close` before decryption
22
23
  - full local recomputation and full ceremony verification from the public board
23
24
 
@@ -77,10 +78,10 @@ Older browsers, stale embedded webviews, and runtimes without Web Crypto `X25519
77
78
  The supported boardroom flow is:
78
79
 
79
80
  1. Freeze the roster in the application and hash it with `hashRosterEntries(...)`.
80
- 2. Build the minimal manifest with `createElectionManifest({ rosterHash, optionList })`.
81
+ 2. Build the manifest with `createElectionManifest({ rosterHash, optionList, scoreRange })`.
81
82
  3. Publish the manifest, registrations, and manifest acceptances.
82
83
  4. Run the honest-majority GJKR transcript.
83
- 5. Post ballot payloads for complete `1..10` score ballots.
84
+ 5. Post ballot payloads for complete scores inside the manifest-declared range.
84
85
  6. Post one organizer-signed `ballot-close` payload that freezes which complete ballots are counted.
85
86
  7. Post threshold decryption shares and tally publications for the close-selected ballot set.
86
87
  8. Verify the whole ceremony with `verifyElectionCeremony(...)`.
@@ -93,7 +94,7 @@ The cryptographic threshold is derived internally from the accepted registration
93
94
 
94
95
  There is no supported `n-of-n` mode and no supported public `k-of-n` configuration.
95
96
 
96
- Transcript verification requires key-derivation confirmations from every qualified participant.
97
+ Transcript verification requires `key-derivation-confirmation` payloads from every qualified participant. In the current design those unanimous confirmations are part of verifier soundness: the library does not implement a public post-Feldman complaint/reconstruction phase, so the DKG verifier is participant-confirmed rather than fully public-data-only. Lowering confirmation acceptance to threshold-many is out of scope unless that missing public consistency machinery is added.
97
98
 
98
99
  See [Honest-majority voting flow](https://tenemo.github.io/threshold-elgamal/guides/three-participant-voting-flow/) for the full phase-by-phase transcript.
99
100
 
@@ -129,6 +130,7 @@ const rosterHash = await hashRosterEntries([
129
130
  const manifest = createElectionManifest({
130
131
  rosterHash,
131
132
  optionList: ["Option A", "Option B"],
133
+ scoreRange: { min: 0, max: 5 },
132
134
  });
133
135
 
134
136
  const manifestHash = await hashElectionManifest(manifest);
@@ -143,6 +145,10 @@ console.log(majorityThreshold(3)); // 2
143
145
  console.log(sessionId.length); // 64
144
146
  ```
145
147
 
148
+ The example uses `0..5` only as one concrete score range. The supported rule is
149
+ one manifest-declared contiguous range with non-negative bounds and
150
+ `scoreRange.max <= 100`.
151
+
146
152
  If your application consumes a complete public board, start with [Verifying a public board](https://tenemo.github.io/threshold-elgamal/guides/verifying-a-public-board/) and then move directly to the verifier entry point:
147
153
 
148
154
  ```typescript
@@ -156,7 +162,7 @@ const bundle: VerifyElectionCeremonyInput = {
156
162
  sessionId,
157
163
  dkgTranscript,
158
164
  ballotPayloads,
159
- ballotClosePayload,
165
+ ballotClosePayloads: [ballotClosePayload],
160
166
  decryptionSharePayloads,
161
167
  tallyPublications,
162
168
  };
@@ -171,7 +177,9 @@ if (!result.ok) {
171
177
  }
172
178
  ```
173
179
 
174
- The root package exposes the workflow-facing builders for the signed protocol payloads used across the documented ceremony, including:
180
+ Pass the full published `ballot-close` slot in `ballotClosePayloads`, even when the normal case is one organizer payload. The verifier audits that slot, collapses only exact retransmissions, and requires exactly one accepted close record.
181
+
182
+ The root package exposes the builders and lower-level helpers required for the documented ceremony, including:
175
183
 
176
184
  - manifest publication
177
185
  - registration
@@ -186,14 +194,16 @@ The root package exposes the workflow-facing builders for the signed protocol pa
186
194
  - decryption shares
187
195
  - tally publication
188
196
 
189
- For the reveal path, the workflow-facing API and the advanced public submodules split responsibilities intentionally:
197
+ The reveal path also works from the root package:
198
+
199
+ - prepare the accepted aggregate with `prepareAggregateForDecryption(...)`
200
+ - compute each partial share with `createDecryptionShare(...)`
201
+ - prove it with `createDLEQProof(...)`
202
+ - publish it with `createDecryptionSharePayload(...)`
190
203
 
191
- - prepare the accepted aggregate with `prepareAggregateForDecryption(...)` from `threshold-elgamal/threshold`
192
- - compute each partial share with `createDecryptionShare(...)` from `threshold-elgamal/threshold`
193
- - prove it with `createDLEQProof(...)` from `threshold-elgamal/proofs`
194
- - publish it with `createDecryptionSharePayload(...)` from `threshold-elgamal`
204
+ After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` against the prepared aggregate ciphertext.
195
205
 
196
- After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` from `threshold-elgamal/threshold` against the prepared aggregate ciphertext.
206
+ The grouped public submodules remain available when you prefer narrower imports by subsystem, but the supported full ceremony does not require them.
197
207
 
198
208
  For concrete posted JSON shapes, use [Published payload examples](https://tenemo.github.io/threshold-elgamal/guides/published-payload-examples/).
199
209
 
@@ -204,7 +214,7 @@ The library is designed for an honest-origin, honest-client, static-adversary se
204
214
  What it tries to enforce:
205
215
 
206
216
  - additive-only tallying on `ristretto255`
207
- - fixed `1..10` score ballots
217
+ - one explicit global contiguous manifest score range
208
218
  - grouped per-option ballot verification
209
219
  - mandatory local aggregate recomputation before decryption
210
220
  - organizer-visible and auditable ballot cutoff through `ballot-close`
@@ -7,7 +7,8 @@ export declare const mod: (value: bigint, modulus: bigint) => bigint;
7
7
  /**
8
8
  * Reduces a value into the range `0..q-1`.
9
9
  *
10
- * @throws {@link InvalidScalarError} When `q` is not positive.
10
+ * @throws {@link threshold-elgamal/core!InvalidScalarError | InvalidScalarError}
11
+ * When `q` is not positive.
11
12
  */
12
13
  export declare const modQ: (value: bigint, q: bigint) => bigint;
13
14
  /**
@@ -70,7 +70,8 @@ const modP = (value, p) => mod(value, p);
70
70
  /**
71
71
  * Reduces a value into the range `0..q-1`.
72
72
  *
73
- * @throws {@link InvalidScalarError} When `q` is not positive.
73
+ * @throws {@link threshold-elgamal/core!InvalidScalarError | InvalidScalarError}
74
+ * When `q` is not positive.
74
75
  */
75
76
  export const modQ = (value, q) => mod(value, q);
76
77
  /**
@@ -78,9 +78,16 @@ export declare const deriveJointPublicKey: (feldmanCommitments: readonly {
78
78
  /**
79
79
  * Verifies a DKG transcript, its signatures, Feldman extraction proofs, the
80
80
  * exact claimed threshold degree, accepted complaint outcomes, the DKG
81
- * transcript hash, and the announced joint public key.
81
+ * transcript hash, the announced joint public key, and unanimous qualified
82
+ * participant key confirmations.
82
83
  *
83
84
  * This is the DKG-specific verifier that the full ceremony verifier delegates
84
- * to before it touches ballots or tally material.
85
+ * to before it touches ballots or tally material. It consumes a public signed
86
+ * transcript plus `key-derivation-confirmation` payloads from every qualified
87
+ * participant. It does not implement a public post-Feldman
88
+ * complaint/reconstruction phase, so it is a participant-confirmed transcript
89
+ * verifier rather than a fully public-data-only GJKR verifier. Lowering this
90
+ * requirement to threshold-many confirmations is out of scope unless that
91
+ * missing public consistency machinery is added.
85
92
  */
86
93
  export declare const verifyDKGTranscript: (input: VerifyDKGTranscriptInput) => Promise<VerifiedDKGTranscript>;
@@ -659,10 +659,17 @@ const verifyCheckpointedDKGTranscript = async (input, setup) => {
659
659
  /**
660
660
  * Verifies a DKG transcript, its signatures, Feldman extraction proofs, the
661
661
  * exact claimed threshold degree, accepted complaint outcomes, the DKG
662
- * transcript hash, and the announced joint public key.
662
+ * transcript hash, the announced joint public key, and unanimous qualified
663
+ * participant key confirmations.
663
664
  *
664
665
  * This is the DKG-specific verifier that the full ceremony verifier delegates
665
- * to before it touches ballots or tally material.
666
+ * to before it touches ballots or tally material. It consumes a public signed
667
+ * transcript plus `key-derivation-confirmation` payloads from every qualified
668
+ * participant. It does not implement a public post-Feldman
669
+ * complaint/reconstruction phase, so it is a participant-confirmed transcript
670
+ * verifier rather than a fully public-data-only GJKR verifier. Lowering this
671
+ * requirement to threshold-many confirmations is out of scope unless that
672
+ * missing public consistency machinery is added.
666
673
  */
667
674
  export const verifyDKGTranscript = async (input) => {
668
675
  const auditedTranscript = await auditSignedPayloads(input.transcript);
@@ -6,14 +6,6 @@
6
6
  */
7
7
  import { RISTRETTO_GROUP, assertAdditiveBound, assertInSubgroupOrIdentity, assertPlaintextAdditive, assertValidPublicKey, InvalidScalarError, } from '../core/index.js';
8
8
  import { decodePoint, encodePoint, multiplyBase, pointAdd, pointMultiply, } from '../core/ristretto.js';
9
- /** Validates an additive-mode public key against the built-in suite. */
10
- const assertValidAdditivePublicKey = (publicKey) => {
11
- assertValidPublicKey(publicKey);
12
- };
13
- /** Validates the caller-supplied additive plaintext bound. */
14
- const assertValidAdditiveBound = (bound) => assertAdditiveBound(bound, RISTRETTO_GROUP.q);
15
- /** Validates the plaintext domain and caller-supplied bound for additive mode. */
16
- const assertValidAdditivePlaintext = (value, bound) => assertPlaintextAdditive(value, bound, RISTRETTO_GROUP.q);
17
9
  /**
18
10
  * Validates an additive ciphertext that may already represent an aggregate.
19
11
  *
@@ -24,19 +16,12 @@ export const assertValidAdditiveCiphertext = (ciphertext) => {
24
16
  assertInSubgroupOrIdentity(ciphertext.c1);
25
17
  assertInSubgroupOrIdentity(ciphertext.c2);
26
18
  };
27
- const resolveAdditiveBound = (bound, operation) => {
19
+ const requireAdditiveBound = (bound) => {
28
20
  if (typeof bound !== 'bigint') {
29
- throw new InvalidScalarError(`Additive ${operation} requires an explicit plaintext bound`);
21
+ throw new InvalidScalarError('Additive encryption requires an explicit plaintext bound');
30
22
  }
31
23
  return bound;
32
24
  };
33
- const resolveAdditiveContext = (bound, operation) => {
34
- const resolvedBound = resolveAdditiveBound(bound, operation);
35
- assertValidAdditiveBound(resolvedBound);
36
- return {
37
- bound: resolvedBound,
38
- };
39
- };
40
25
  const assertEncryptionRandomness = (randomness) => {
41
26
  if (randomness <= 0n || randomness >= RISTRETTO_GROUP.q) {
42
27
  throw new InvalidScalarError('Encryption randomness must be in the range 1..q-1');
@@ -75,9 +60,10 @@ export const addEncryptedValues = (left, right) => {
75
60
  * the randomness input and plaintext bound.
76
61
  */
77
62
  export const encryptAdditiveWithRandomness = (message, publicKey, randomness, bound) => {
78
- const context = resolveAdditiveContext(bound, 'encryption');
79
- assertValidAdditivePlaintext(message, context.bound);
80
- assertValidAdditivePublicKey(publicKey);
63
+ const resolvedBound = requireAdditiveBound(bound);
64
+ assertAdditiveBound(resolvedBound, RISTRETTO_GROUP.q);
65
+ assertPlaintextAdditive(message, resolvedBound, RISTRETTO_GROUP.q);
66
+ assertValidPublicKey(publicKey);
81
67
  assertEncryptionRandomness(randomness);
82
68
  return encryptAdditiveWithValidatedInputs(message, publicKey, randomness);
83
69
  };
package/dist/index.d.ts CHANGED
@@ -1,25 +1,41 @@
1
1
  /**
2
- * Workflow-facing root package exports for the public API.
2
+ * Root package exports for the supported public API.
3
3
  *
4
- * Use this entry point for the supported voting workflow: manifest and roster
5
- * setup, transport keys and envelopes, standard signed protocol payload
6
- * builders, and the full ceremony verifier.
4
+ * Use this entry point for the full supported voting workflow: manifest and
5
+ * roster setup, transport keys and envelopes, DKG commitments and share
6
+ * handling, ballot encryption and proofs, decryption-share publication, tally
7
+ * reconstruction, and full ceremony verification.
7
8
  *
8
- * Lower-level math, proof, threshold, DKG, VSS, and protocol primitives are
9
- * available through the public subpath modules such as
10
- * `threshold-elgamal/proofs`, `threshold-elgamal/threshold`, and
11
- * `threshold-elgamal/dkg`.
9
+ * Public subpath modules such as `threshold-elgamal/proofs`,
10
+ * `threshold-elgamal/threshold`, and `threshold-elgamal/dkg` remain available
11
+ * when you prefer grouped imports by subsystem, but the supported ceremony can
12
+ * be implemented from this root package alone.
12
13
  *
13
14
  * @module threshold-elgamal
14
15
  * @packageDocumentation
15
16
  */
17
+ export { RISTRETTO_GROUP, modQ } from './core/public.js';
18
+ export type { EncodedPoint } from './core/public.js';
16
19
  export { majorityThreshold } from './core/validation.js';
20
+ export { encryptAdditiveWithRandomness } from './elgamal/public.js';
21
+ export type { ElGamalCiphertext } from './elgamal/public.js';
22
+ export { deriveJointPublicKey, deriveTranscriptVerificationKey, encodePedersenShareEnvelope, decodePedersenShareEnvelope, verifyDKGTranscript, } from './dkg/public.js';
23
+ export type { VerifyDKGTranscriptInput, VerifiedDKGTranscript, } from './dkg/public.js';
24
+ export { createDisjunctiveProof, createDLEQProof, createSchnorrProof, verifyDisjunctiveProof, verifyDLEQProof, verifySchnorrProof, } from './proofs/public.js';
25
+ export type { DLEQProof, DisjunctiveProof, DLEQStatement, ProofContext, SchnorrProof, } from './proofs/public.js';
17
26
  export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
18
27
  export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
19
28
  export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
20
29
  export type { EncodedAuthPublicKey, EncodedTransportPublicKey, EncryptedEnvelope, EnvelopeContext, TransportKeyPair, } from './transport/types.js';
21
- export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, createTallyPublicationPayload, } from './protocol/builders.js';
30
+ export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, signProtocolPayload, createTallyPublicationPayload, } from './protocol/builders.js';
22
31
  export { canonicalizeElectionManifest, createElectionManifest, deriveSessionId, hashElectionManifest, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './protocol/manifest.js';
23
- export { hashRosterEntries } from './protocol/verification.js';
32
+ export { hashRosterEntries, verifySignedProtocolPayloads, type RosterEntry, type VerifiedProtocolSignatures, } from './protocol/verification.js';
33
+ export { hashProtocolTranscript } from './protocol/transcript.js';
34
+ export { verifyBallotSubmissionPayloadsByOption } from './protocol/voting-ballots.js';
35
+ export { scoreRangeDomain } from './protocol/voting-codecs.js';
24
36
  export { verifyElectionCeremony, tryVerifyElectionCeremony, type ElectionVerificationErrorCode, type ElectionVerificationFailure, type ElectionVerificationResult, type ElectionVerificationStage, type VerifiedElectionCeremony, } from './protocol/voting-verification.js';
25
- export type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, RegistrationPayload, SignedPayload, TallyPublicationPayload, VerifyElectionCeremonyInput, } from './protocol/types.js';
37
+ export type { BallotClosePayload, BallotSubmissionPayload, DecryptionSharePayload, ElectionManifest, EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, EncryptedDualSharePayload, FeldmanCommitmentPayload, KeyDerivationConfirmation, ManifestAcceptancePayload, ManifestPublicationPayload, PedersenCommitmentPayload, PhaseCheckpointPayload, ProtocolMessageType, ProtocolPayload, RegistrationPayload, ScoreRange, SignedPayload, TallyPublicationPayload, VerifyElectionCeremonyInput, } from './protocol/types.js';
38
+ export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/public.js';
39
+ export type { AggregateDecryptionPreparationInput, DecryptionShare, Share, VerifiedAggregateCiphertext, } from './threshold/public.js';
40
+ export { derivePedersenShares, generateFeldmanCommitments, generatePedersenCommitments, verifyFeldmanShare, verifyPedersenShare, } from './vss/public.js';
41
+ export type { FeldmanCommitments, PedersenCommitments, PedersenShare, } from './vss/public.js';
package/dist/index.js CHANGED
@@ -1,23 +1,33 @@
1
1
  /**
2
- * Workflow-facing root package exports for the public API.
2
+ * Root package exports for the supported public API.
3
3
  *
4
- * Use this entry point for the supported voting workflow: manifest and roster
5
- * setup, transport keys and envelopes, standard signed protocol payload
6
- * builders, and the full ceremony verifier.
4
+ * Use this entry point for the full supported voting workflow: manifest and
5
+ * roster setup, transport keys and envelopes, DKG commitments and share
6
+ * handling, ballot encryption and proofs, decryption-share publication, tally
7
+ * reconstruction, and full ceremony verification.
7
8
  *
8
- * Lower-level math, proof, threshold, DKG, VSS, and protocol primitives are
9
- * available through the public subpath modules such as
10
- * `threshold-elgamal/proofs`, `threshold-elgamal/threshold`, and
11
- * `threshold-elgamal/dkg`.
9
+ * Public subpath modules such as `threshold-elgamal/proofs`,
10
+ * `threshold-elgamal/threshold`, and `threshold-elgamal/dkg` remain available
11
+ * when you prefer grouped imports by subsystem, but the supported ceremony can
12
+ * be implemented from this root package alone.
12
13
  *
13
14
  * @module threshold-elgamal
14
15
  * @packageDocumentation
15
16
  */
17
+ export { RISTRETTO_GROUP, modQ } from './core/public.js';
16
18
  export { majorityThreshold } from './core/validation.js';
19
+ export { encryptAdditiveWithRandomness } from './elgamal/public.js';
20
+ export { deriveJointPublicKey, deriveTranscriptVerificationKey, encodePedersenShareEnvelope, decodePedersenShareEnvelope, verifyDKGTranscript, } from './dkg/public.js';
21
+ export { createDisjunctiveProof, createDLEQProof, createSchnorrProof, verifyDisjunctiveProof, verifyDLEQProof, verifySchnorrProof, } from './proofs/public.js';
17
22
  export { decryptEnvelope, encryptEnvelope } from './transport/envelopes.js';
18
23
  export { exportAuthPublicKey, generateAuthKeyPair } from './transport/auth.js';
19
24
  export { exportTransportPublicKey, generateTransportKeyPair, } from './transport/key-agreement.js';
20
- export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, createTallyPublicationPayload, } from './protocol/builders.js';
25
+ export { createBallotClosePayload, createBallotSubmissionPayload, createDecryptionSharePayload, createEncryptedDualSharePayload, createFeldmanCommitmentPayload, createKeyDerivationConfirmationPayload, createManifestAcceptancePayload, createManifestPublicationPayload, createPedersenCommitmentPayload, createPhaseCheckpointPayload, createRegistrationPayload, signProtocolPayload, createTallyPublicationPayload, } from './protocol/builders.js';
21
26
  export { canonicalizeElectionManifest, createElectionManifest, deriveSessionId, hashElectionManifest, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './protocol/manifest.js';
22
- export { hashRosterEntries } from './protocol/verification.js';
27
+ export { hashRosterEntries, verifySignedProtocolPayloads, } from './protocol/verification.js';
28
+ export { hashProtocolTranscript } from './protocol/transcript.js';
29
+ export { verifyBallotSubmissionPayloadsByOption } from './protocol/voting-ballots.js';
30
+ export { scoreRangeDomain } from './protocol/voting-codecs.js';
23
31
  export { verifyElectionCeremony, tryVerifyElectionCeremony, } from './protocol/voting-verification.js';
32
+ export { combineDecryptionShares, createDecryptionShare, prepareAggregateForDecryption, } from './threshold/public.js';
33
+ export { derivePedersenShares, generateFeldmanCommitments, generatePedersenCommitments, verifyFeldmanShare, verifyPedersenShare, } from './vss/public.js';
@@ -1,8 +1,9 @@
1
1
  /**
2
- * CDS94-style disjunctive proofs for additive score ballots.
2
+ * CDS94-style disjunctive proofs for additive ElGamal plaintexts drawn from an
3
+ * explicit finite value set.
3
4
  *
4
5
  * Ballot payloads use this module to prove that a ciphertext encodes one value
5
- * from the supported score domain without revealing which score was chosen.
6
+ * from the manifest-declared domain without revealing which value was chosen.
6
7
  */
7
8
  import { type CryptoGroup, type RandomBytesSource } from '../core/index.js';
8
9
  import type { ElGamalCiphertext } from '../elgamal/types.js';
@@ -18,6 +19,6 @@ export declare const createDisjunctiveProof: (plaintext: bigint, randomness: big
18
19
  * Verifies a CDS94-style disjunctive proof for additive ElGamal plaintexts.
19
20
  *
20
21
  * Ballot verification uses this to reject ciphertexts that do not encode one
21
- * of the allowed score values for the current option slot.
22
+ * of the allowed manifest-domain values for the current option slot.
22
23
  */
23
24
  export declare const verifyDisjunctiveProof: (proof: DisjunctiveProof, ciphertext: ElGamalCiphertext, publicKey: string, validValues: readonly bigint[], group: CryptoGroup, context: ProofContext) => Promise<boolean>;
@@ -1,8 +1,9 @@
1
1
  /**
2
- * CDS94-style disjunctive proofs for additive score ballots.
2
+ * CDS94-style disjunctive proofs for additive ElGamal plaintexts drawn from an
3
+ * explicit finite value set.
3
4
  *
4
5
  * Ballot payloads use this module to prove that a ciphertext encodes one value
5
- * from the supported score domain without revealing which score was chosen.
6
+ * from the manifest-declared domain without revealing which value was chosen.
6
7
  */
7
8
  import { assertInSubgroup, assertInSubgroupOrIdentity, assertScalarInZq, InvalidProofError, modQ, randomScalarBelow, } from '../core/index.js';
8
9
  import { decodePoint, encodePoint, multiplyBase, pointMultiply, pointSubtract, } from '../core/ristretto.js';
@@ -11,7 +12,7 @@ import { assertProofContext, contextElements, fixedPoint, fixedScalar, hashChall
11
12
  import { hedgedNonce } from './nonces.js';
12
13
  const candidateEncoding = (ciphertext, candidateValue, group) => encodePoint(pointSubtract(decodePoint(ciphertext.c2, 'Ciphertext c2'), multiplyBase(modQ(candidateValue, group.q))));
13
14
  const commitmentSequence = (commitments) => encodeSequenceForChallenge(commitments.map((commitment) => concatBytes(encodeForChallenge(fixedPoint(commitment.a1)), encodeForChallenge(fixedPoint(commitment.a2)))));
14
- const challengePayload = (ciphertext, publicKey, validValues, commitments, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(publicKey), fixedPoint(ciphertext.c1), fixedPoint(ciphertext.c2), encodeSequenceForChallenge(validValues.map((value) => fixedScalar(modQ(value, group.q), group))), commitmentSequence(commitments));
15
+ const challengePayload = (ciphertext, publicKey, validValues, commitments, group, context) => encodeForChallenge(...contextElements(context), fixedPoint(group.g), fixedPoint(publicKey), fixedPoint(ciphertext.c1), fixedPoint(ciphertext.c2), encodeSequenceForChallenge(validValues.map((value) => fixedScalar(modQ(value, group.q)))), commitmentSequence(commitments));
15
16
  /**
16
17
  * Creates a CDS94-style disjunctive proof for additive ElGamal plaintexts.
17
18
  *
@@ -67,7 +68,7 @@ export const createDisjunctiveProof = async (plaintext, randomness, ciphertext,
67
68
  * Verifies a CDS94-style disjunctive proof for additive ElGamal plaintexts.
68
69
  *
69
70
  * Ballot verification uses this to reject ciphertexts that do not encode one
70
- * of the allowed score values for the current option slot.
71
+ * of the allowed manifest-domain values for the current option slot.
71
72
  */
72
73
  export const verifyDisjunctiveProof = async (proof, ciphertext, publicKey, validValues, group, context) => {
73
74
  assertProofContext(context, group);
@@ -3,6 +3,6 @@ import type { ProofContext } from './types.js';
3
3
  export declare const assertProofContext: (context: ProofContext, group: CryptoGroup) => void;
4
4
  export declare const contextElements: (context: ProofContext) => (bigint | string | Uint8Array)[];
5
5
  export declare const fixedPoint: (value: string) => Uint8Array;
6
- export declare const fixedScalar: (value: bigint, group: CryptoGroup) => Uint8Array;
6
+ export declare const fixedScalar: (value: bigint) => Uint8Array;
7
7
  export declare const hashChallenge: (payload: Uint8Array, q: bigint) => Promise<bigint>;
8
8
  export declare const sumChallenges: (values: readonly bigint[], q: bigint) => bigint;
@@ -59,9 +59,6 @@ export const contextElements = (context) => {
59
59
  return fields;
60
60
  };
61
61
  export const fixedPoint = (value) => hexToBytes(value);
62
- export const fixedScalar = (value, group) => {
63
- void group;
64
- return hexToBytes(encodeScalar(value));
65
- };
62
+ export const fixedScalar = (value) => hexToBytes(encodeScalar(value));
66
63
  export const hashChallenge = (payload, q) => Promise.resolve(modQ(hashChallengeToScalar(payload), q));
67
64
  export const sumChallenges = (values, q) => values.reduce((sum, value) => modQ(sum + value, q), 0n);
@@ -58,8 +58,8 @@ export type DisjunctiveBranch = {
58
58
  /**
59
59
  * A disjunctive proof over an ordered set of valid plaintext values.
60
60
  *
61
- * Ballot payloads use this to prove that an encrypted score came from the
62
- * allowed score domain without revealing which value was chosen.
61
+ * Ballot payloads use this to prove that an encrypted value came from the
62
+ * allowed manifest domain without revealing which value was chosen.
63
63
  */
64
64
  export type DisjunctiveProof = {
65
65
  readonly branches: readonly DisjunctiveBranch[];
@@ -19,7 +19,7 @@ export declare const signProtocolPayload: <TMessageType extends ProtocolMessageT
19
19
  * Creates a signed `manifest-publication` payload.
20
20
  *
21
21
  * This is the first public payload in the supported ceremony and anchors the
22
- * minimal manifest on the board.
22
+ * explicit manifest on the board.
23
23
  */
24
24
  export declare const createManifestPublicationPayload: (privateKey: CryptoKey, input: {
25
25
  readonly manifest: ElectionManifest;
@@ -30,7 +30,7 @@ export const signProtocolPayload = async (privateKey, payload) => {
30
30
  * Creates a signed `manifest-publication` payload.
31
31
  *
32
32
  * This is the first public payload in the supported ceremony and anchors the
33
- * minimal manifest on the board.
33
+ * explicit manifest on the board.
34
34
  */
35
35
  export const createManifestPublicationPayload = async (privateKey, input) => signProtocolPayload(privateKey, {
36
36
  protocolVersion: input.protocolVersion,
@@ -25,12 +25,12 @@ export declare const assertSupportedProtocolVersion: (protocolVersion: string, l
25
25
  * workflow.
26
26
  *
27
27
  * The manifest is intentionally minimal: it fixes the frozen roster hash and
28
- * option list, while participant count and threshold are derived later from the
29
- * accepted registration roster.
28
+ * option list together with one explicit global score range, while participant
29
+ * count and threshold are derived later from the accepted registration roster.
30
30
  */
31
31
  export declare const validateElectionManifest: (manifest: ElectionManifest) => ElectionManifest;
32
32
  /**
33
- * Creates the minimal election manifest after validating the supported
33
+ * Creates the explicit election manifest after validating the supported
34
34
  * invariants.
35
35
  */
36
36
  export declare const createElectionManifest: (manifest: ElectionManifest) => ElectionManifest;
@@ -8,6 +8,7 @@ import { bytesToHex } from '../core/bytes.js';
8
8
  import { InvalidPayloadError, sha256, utf8ToBytes } from '../core/index.js';
9
9
  import { encodeForChallenge } from '../serialize/encoding.js';
10
10
  import { canonicalizeJson } from './canonical-json.js';
11
+ import { validateSupportedScoreRange } from './score-range.js';
11
12
  /**
12
13
  * Default protocol namespace used by the built-in helpers and verifier.
13
14
  *
@@ -20,6 +21,23 @@ const assertNonEmptyString = (value, label) => {
20
21
  throw new InvalidPayloadError(`${label} must be a non-empty string`);
21
22
  }
22
23
  };
24
+ const validateScoreRange = (scoreRange) => {
25
+ if (typeof scoreRange !== 'object' ||
26
+ scoreRange === null ||
27
+ Array.isArray(scoreRange)) {
28
+ throw new InvalidPayloadError('Election manifest scoreRange must be an object with min and max bounds');
29
+ }
30
+ const scoreRangeRecord = scoreRange;
31
+ if (typeof scoreRangeRecord.min !== 'number' ||
32
+ typeof scoreRangeRecord.max !== 'number') {
33
+ throw new InvalidPayloadError('Election manifest scoreRange requires numeric min and max bounds');
34
+ }
35
+ return validateSupportedScoreRange(scoreRange, {
36
+ comparisonMax: 'scoreRange.max',
37
+ min: 'Election manifest scoreRange.min',
38
+ max: 'Election manifest scoreRange.max',
39
+ });
40
+ };
23
41
  /**
24
42
  * Validates that a protocol version string is present and non-empty.
25
43
  *
@@ -48,12 +66,15 @@ export const assertSupportedProtocolVersion = (protocolVersion, label = 'Protoco
48
66
  * workflow.
49
67
  *
50
68
  * The manifest is intentionally minimal: it fixes the frozen roster hash and
51
- * option list, while participant count and threshold are derived later from the
52
- * accepted registration roster.
69
+ * option list together with one explicit global score range, while participant
70
+ * count and threshold are derived later from the accepted registration roster.
53
71
  */
54
72
  export const validateElectionManifest = (manifest) => {
55
73
  const manifestRecord = manifest;
56
74
  assertNonEmptyString(manifest.rosterHash, 'Roster hash');
75
+ if (!('scoreRange' in manifestRecord)) {
76
+ throw new InvalidPayloadError('Election manifest requires an explicit scoreRange');
77
+ }
57
78
  for (const legacyField of [
58
79
  'participantCount',
59
80
  'reconstructionThreshold',
@@ -71,7 +92,7 @@ export const validateElectionManifest = (manifest) => {
71
92
  'requiresAllOptions',
72
93
  ]) {
73
94
  if (legacyField in manifestRecord) {
74
- throw new InvalidPayloadError(`Legacy manifest field "${legacyField}" is not supported on the Ristretto beta line`);
95
+ throw new InvalidPayloadError(`Legacy manifest field "${legacyField}" is not supported by the public manifest`);
75
96
  }
76
97
  }
77
98
  if (manifest.optionList.length === 0) {
@@ -85,10 +106,13 @@ export const validateElectionManifest = (manifest) => {
85
106
  }
86
107
  seenOptions.add(option);
87
108
  }
88
- return manifest;
109
+ return {
110
+ ...manifest,
111
+ scoreRange: validateScoreRange(manifest.scoreRange),
112
+ };
89
113
  };
90
114
  /**
91
- * Creates the minimal election manifest after validating the supported
115
+ * Creates the explicit election manifest after validating the supported
92
116
  * invariants.
93
117
  */
94
118
  export const createElectionManifest = (manifest) => validateElectionManifest(manifest);
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Low-level protocol helpers for transcript hashing, generic signed payloads,
3
- * ballot proof verification, and protocol payload types.
3
+ * registration-roster hashing, signature verification, ballot proof
4
+ * verification, and protocol payload types.
4
5
  *
5
- * Use this module when you need to work directly with protocol messages rather
6
- * than the workflow-facing builders and verifiers from the root package.
6
+ * Use this module when you want protocol helpers grouped by subsystem instead
7
+ * of importing them from the root package.
7
8
  *
8
9
  * @module threshold-elgamal/protocol
9
10
  * @packageDocumentation
10
11
  */
11
12
  export { signProtocolPayload } from './builders.js';
12
13
  export { hashProtocolTranscript } from './transcript.js';
14
+ export { hashRosterEntries, verifySignedProtocolPayloads, type RosterEntry, type VerifiedProtocolSignatures, } from './verification.js';
13
15
  export { verifyBallotSubmissionPayloadsByOption } from './voting-ballots.js';
14
- export { scoreVotingDomain } from './voting-codecs.js';
15
- export type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ProtocolMessageType, ProtocolPayload, } from './types.js';
16
+ export { scoreRangeDomain } from './voting-codecs.js';
17
+ export type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ProtocolMessageType, ProtocolPayload, ScoreRange, } from './types.js';
@@ -1,14 +1,16 @@
1
1
  /**
2
2
  * Low-level protocol helpers for transcript hashing, generic signed payloads,
3
- * ballot proof verification, and protocol payload types.
3
+ * registration-roster hashing, signature verification, ballot proof
4
+ * verification, and protocol payload types.
4
5
  *
5
- * Use this module when you need to work directly with protocol messages rather
6
- * than the workflow-facing builders and verifiers from the root package.
6
+ * Use this module when you want protocol helpers grouped by subsystem instead
7
+ * of importing them from the root package.
7
8
  *
8
9
  * @module threshold-elgamal/protocol
9
10
  * @packageDocumentation
10
11
  */
11
12
  export { signProtocolPayload } from './builders.js';
12
13
  export { hashProtocolTranscript } from './transcript.js';
14
+ export { hashRosterEntries, verifySignedProtocolPayloads, } from './verification.js';
13
15
  export { verifyBallotSubmissionPayloadsByOption } from './voting-ballots.js';
14
- export { scoreVotingDomain } from './voting-codecs.js';
16
+ export { scoreRangeDomain } from './voting-codecs.js';
@@ -0,0 +1,6 @@
1
+ import type { ScoreRange } from './types.js';
2
+ export declare const validateSupportedScoreRange: (scoreRange: ScoreRange, labels: {
3
+ readonly comparisonMax?: string;
4
+ readonly min: string;
5
+ readonly max: string;
6
+ }) => ScoreRange;
@@ -0,0 +1,24 @@
1
+ import { InvalidPayloadError } from '../core/index.js';
2
+ const MAX_SUPPORTED_SCORE_RANGE_MAX = 100;
3
+ const assertSafeInteger = (value, label) => {
4
+ if (!Number.isSafeInteger(value)) {
5
+ throw new InvalidPayloadError(`${label} must be a safe integer`);
6
+ }
7
+ };
8
+ export const validateSupportedScoreRange = (scoreRange, labels) => {
9
+ assertSafeInteger(scoreRange.min, labels.min);
10
+ assertSafeInteger(scoreRange.max, labels.max);
11
+ if (scoreRange.min < 0) {
12
+ throw new InvalidPayloadError(`${labels.min} must be non-negative`);
13
+ }
14
+ if (scoreRange.max < 0) {
15
+ throw new InvalidPayloadError(`${labels.max} must be non-negative`);
16
+ }
17
+ if (scoreRange.min > scoreRange.max) {
18
+ throw new InvalidPayloadError(`${labels.min} must not exceed ${labels.comparisonMax ?? labels.max}`);
19
+ }
20
+ if (scoreRange.max > MAX_SUPPORTED_SCORE_RANGE_MAX) {
21
+ throw new InvalidPayloadError(`${labels.max} must not exceed ${MAX_SUPPORTED_SCORE_RANGE_MAX}`);
22
+ }
23
+ return scoreRange;
24
+ };
@@ -136,7 +136,7 @@ export type FeldmanCommitmentPayload = BaseProtocolPayload & {
136
136
  }[];
137
137
  };
138
138
  /**
139
- * Final key-derivation confirmation payload for the derived joint key.
139
+ * Final participant confirmation payload for the derived joint key.
140
140
  */
141
141
  export type KeyDerivationConfirmation = BaseProtocolPayload & {
142
142
  readonly messageType: 'key-derivation-confirmation';
@@ -217,12 +217,24 @@ export type SignedPayload<TPayload extends ProtocolPayload = ProtocolPayload> =
217
217
  /**
218
218
  * Canonical election-manifest shape bound into protocol transcripts.
219
219
  *
220
- * The manifest is intentionally minimal and leaves threshold derivation to the
221
- * accepted registration roster.
220
+ * The manifest is intentionally compact: it fixes the frozen roster hash,
221
+ * option list, and one explicit global score range, while participant count
222
+ * and threshold are derived later from the accepted registration roster.
223
+ */
224
+ export type ScoreRange = {
225
+ readonly min: number;
226
+ readonly max: number;
227
+ };
228
+ /**
229
+ * Canonical election-manifest shape bound into protocol transcripts.
230
+ *
231
+ * The score range applies uniformly to every option slot in the supported
232
+ * workflow.
222
233
  */
223
234
  export type ElectionManifest = {
224
235
  readonly rosterHash: string;
225
236
  readonly optionList: readonly string[];
237
+ readonly scoreRange: ScoreRange;
226
238
  };
227
239
  /**
228
240
  * Input bundle for verifying typed ballot payloads.
@@ -267,14 +279,15 @@ export type VerifyDecryptionSharePayloadsByOptionInput = {
267
279
  * Input bundle for full ceremony verification across all published options.
268
280
  *
269
281
  * This is the top-level verifier input that an auditor or bulletin-board
270
- * reader supplies when replaying a full ceremony.
282
+ * reader supplies when replaying a full ceremony from the published board,
283
+ * including the full ballot-close slot instead of a preselected close record.
271
284
  */
272
285
  export type VerifyElectionCeremonyInput = {
273
286
  readonly manifest: ElectionManifest;
274
287
  readonly sessionId: string;
275
288
  readonly dkgTranscript: readonly SignedPayload[];
276
289
  readonly ballotPayloads: readonly SignedPayload<BallotSubmissionPayload>[];
277
- readonly ballotClosePayload: SignedPayload<BallotClosePayload>;
290
+ readonly ballotClosePayloads: readonly SignedPayload<BallotClosePayload>[];
278
291
  readonly decryptionSharePayloads: readonly SignedPayload<DecryptionSharePayload>[];
279
292
  readonly tallyPublications?: readonly SignedPayload<TallyPublicationPayload>[];
280
293
  };
@@ -4,8 +4,9 @@ import { type VerifiedOptionBallotAggregation } from './voting-ballot-aggregatio
4
4
  * Verifies typed ballot-submission payloads and recomputes one aggregate tally
5
5
  * ciphertext per manifest option.
6
6
  *
7
- * This is the public entry point for applications that already have
8
- * signature-checked ballot payloads and want the per-option verified
9
- * ciphertext aggregates that feed threshold decryption.
7
+ * This is the public entry point for applications that already collected
8
+ * signed ballot payloads and want the per-option verified ciphertext
9
+ * aggregates that feed threshold decryption. The helper re-audits the signed
10
+ * payloads before it decodes and aggregates them.
10
11
  */
11
12
  export declare const verifyBallotSubmissionPayloadsByOption: (input: VerifyBallotSubmissionPayloadsByOptionInput) => Promise<readonly VerifiedOptionBallotAggregation[]>;
@@ -40,9 +40,10 @@ const verifyAuditedBallotSubmissionPayloadsByOption = async (input) => {
40
40
  * Verifies typed ballot-submission payloads and recomputes one aggregate tally
41
41
  * ciphertext per manifest option.
42
42
  *
43
- * This is the public entry point for applications that already have
44
- * signature-checked ballot payloads and want the per-option verified
45
- * ciphertext aggregates that feed threshold decryption.
43
+ * This is the public entry point for applications that already collected
44
+ * signed ballot payloads and want the per-option verified ciphertext
45
+ * aggregates that feed threshold decryption. The helper re-audits the signed
46
+ * payloads before it decodes and aggregates them.
46
47
  */
47
48
  export const verifyBallotSubmissionPayloadsByOption = async (input) => {
48
49
  const context = await buildVotingManifestContext(input.manifest, input.sessionId);
@@ -1,6 +1,6 @@
1
1
  import type { ElGamalCiphertext } from '../elgamal/types.js';
2
2
  import type { DLEQProof, DisjunctiveProof } from '../proofs/types.js';
3
- import type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof } from './types.js';
3
+ import type { EncodedCiphertext, EncodedCompactProof, EncodedDisjunctiveProof, ScoreRange } from './types.js';
4
4
  /**
5
5
  * Encodes an additive ciphertext into fixed-width protocol hex.
6
6
  *
@@ -47,8 +47,11 @@ export declare const encodeDisjunctiveProof: (proof: DisjunctiveProof) => Encode
47
47
  */
48
48
  export declare const decodeDisjunctiveProof: (proof: EncodedDisjunctiveProof) => DisjunctiveProof;
49
49
  /**
50
- * Returns the fixed score-voting domain `1..10`.
50
+ * Expands one inclusive contiguous score range into its allowed plaintext
51
+ * domain.
51
52
  *
52
- * The supported voting workflow does not expose a configurable score range.
53
+ * Ballot builders and verifiers pass the resulting values into the
54
+ * disjunctive-proof layer so each encrypted score can be proven to belong to
55
+ * the manifest-declared domain.
53
56
  */
54
- export declare const scoreVotingDomain: () => readonly bigint[];
57
+ export declare const scoreRangeDomain: (scoreRange: ScoreRange) => readonly bigint[];
@@ -3,6 +3,7 @@
3
3
  * their published protocol payload representations.
4
4
  */
5
5
  import { decodePoint, decodeScalar, encodeScalar } from '../core/ristretto.js';
6
+ import { validateSupportedScoreRange } from './score-range.js';
6
7
  /**
7
8
  * Encodes an additive ciphertext into fixed-width protocol hex.
8
9
  *
@@ -66,8 +67,17 @@ export const decodeDisjunctiveProof = (proof) => ({
66
67
  branches: proof.branches.map((branch) => decodeCompactProof(branch)),
67
68
  });
68
69
  /**
69
- * Returns the fixed score-voting domain `1..10`.
70
+ * Expands one inclusive contiguous score range into its allowed plaintext
71
+ * domain.
70
72
  *
71
- * The supported voting workflow does not expose a configurable score range.
73
+ * Ballot builders and verifiers pass the resulting values into the
74
+ * disjunctive-proof layer so each encrypted score can be proven to belong to
75
+ * the manifest-declared domain.
72
76
  */
73
- export const scoreVotingDomain = () => Object.freeze(Array.from({ length: 10 }, (_value, index) => BigInt(index + 1)));
77
+ export const scoreRangeDomain = (scoreRange) => {
78
+ validateSupportedScoreRange(scoreRange, {
79
+ min: 'Score range min',
80
+ max: 'Score range max',
81
+ });
82
+ return Object.freeze(Array.from({ length: scoreRange.max - scoreRange.min + 1 }, (_value, index) => BigInt(scoreRange.min + index)));
83
+ };
@@ -5,6 +5,7 @@ import type { VerifiedOptionDecryptionShares, VerifyDecryptionSharePayloadsByOpt
5
5
  *
6
6
  * This is the public entry point for applications that have already accepted a
7
7
  * DKG transcript and verified ballot aggregates and now need to validate the
8
- * published threshold shares.
8
+ * published threshold shares. The helper re-audits the signed share payloads
9
+ * before it groups and verifies them.
9
10
  */
10
11
  export declare const verifyDecryptionSharePayloadsByOption: (input: VerifyDecryptionSharePayloadsByOptionInput) => Promise<readonly VerifiedOptionDecryptionShares[]>;
@@ -98,7 +98,8 @@ const verifyAuditedDecryptionSharePayloadsByOption = async (input) => {
98
98
  *
99
99
  * This is the public entry point for applications that have already accepted a
100
100
  * DKG transcript and verified ballot aggregates and now need to validate the
101
- * published threshold shares.
101
+ * published threshold shares. The helper re-audits the signed share payloads
102
+ * before it groups and verifies them.
102
103
  */
103
104
  export const verifyDecryptionSharePayloadsByOption = async (input) => {
104
105
  const context = await buildVotingManifestContext(input.manifest, input.sessionId);
@@ -1,5 +1,5 @@
1
1
  import type { ProofContext } from '../proofs/types.js';
2
- import type { DecryptionSharePayload, ElectionManifest, ProtocolPayload, RegistrationPayload, SignedPayload, OptionAggregateInput } from './types.js';
2
+ import type { DecryptionSharePayload, ElectionManifest, RegistrationPayload, ProtocolPayload, SignedPayload, OptionAggregateInput } from './types.js';
3
3
  export declare const BALLOT_SUBMISSION_PHASE = 5;
4
4
  export declare const BALLOT_CLOSE_PHASE = 6;
5
5
  export declare const DECRYPTION_SHARE_PHASE = 7;
@@ -10,6 +10,7 @@ type VotingManifestContext = {
10
10
  readonly optionCount: number;
11
11
  readonly protocolVersion: string;
12
12
  readonly scoreDomainValues: readonly bigint[];
13
+ readonly scoreRangeMax: bigint;
13
14
  readonly sessionId: string;
14
15
  };
15
16
  export declare const assertPhase: (payload: ProtocolPayload, expectedPhase: number, label: string) => void;
@@ -8,7 +8,7 @@ import { assertPositiveParticipantIndex, InvalidPayloadError, RISTRETTO_GROUP, }
8
8
  import { importAuthPublicKey, verifyPayloadSignature } from '../transport/auth.js';
9
9
  import { hashElectionManifest, assertSupportedProtocolVersion, SHIPPED_PROTOCOL_VERSION, validateElectionManifest, } from './manifest.js';
10
10
  import { signedProtocolPayloadBytes } from './payloads.js';
11
- import { scoreVotingDomain } from './voting-codecs.js';
11
+ import { scoreRangeDomain } from './voting-codecs.js';
12
12
  export const BALLOT_SUBMISSION_PHASE = 5;
13
13
  export const BALLOT_CLOSE_PHASE = 6;
14
14
  export const DECRYPTION_SHARE_PHASE = 7;
@@ -53,7 +53,8 @@ export const buildVotingManifestContext = async (manifest, sessionId) => {
53
53
  manifestHash: await hashElectionManifest(validatedManifest),
54
54
  optionCount: validatedManifest.optionList.length,
55
55
  protocolVersion: SHIPPED_PROTOCOL_VERSION,
56
- scoreDomainValues: scoreVotingDomain(),
56
+ scoreDomainValues: scoreRangeDomain(validatedManifest.scoreRange),
57
+ scoreRangeMax: BigInt(validatedManifest.scoreRange.max),
57
58
  sessionId,
58
59
  };
59
60
  };
@@ -66,8 +66,8 @@ export type ElectionVerificationResult = {
66
66
  };
67
67
  /**
68
68
  * Replays the published ceremony from manifest to tally, including board audit,
69
- * DKG verification, ballot verification, decryption-share verification, and
70
- * per-option tally checks.
69
+ * DKG verification, full ballot-close-slot audit, ballot verification,
70
+ * decryption-share verification, and per-option tally checks.
71
71
  *
72
72
  * This is the main verifier entry point for callers that want failures to
73
73
  * abort immediately.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * This module is the shortest path for auditors and bulletin-board readers who
5
5
  * want one call that replays manifest validation, board audit, DKG, ballots,
6
- * decryption shares, and tally checks.
6
+ * the full ballot-close slot, decryption shares, and tally checks.
7
7
  */
8
8
  import { InvalidPayloadError } from '../core/index.js';
9
9
  import { decodeScalar } from '../core/ristretto.js';
@@ -37,7 +37,7 @@ const recomputePublishedTally = (input) => {
37
37
  sessionId: input.sessionId,
38
38
  optionIndex: input.ballots.optionIndex,
39
39
  });
40
- return combineDecryptionShares(preparedAggregate.ciphertext, input.decryptionShares.map((entry) => entry.share), BigInt(input.ballots.aggregate.ballotCount) * 10n);
40
+ return combineDecryptionShares(preparedAggregate.ciphertext, input.decryptionShares.map((entry) => entry.share), BigInt(input.ballots.aggregate.ballotCount) * input.scoreRangeMax);
41
41
  };
42
42
  const verifyPublishedTallyPayload = (payload, optionIndex, ballots, decryptionShares, tally) => {
43
43
  if (payload.transcriptHash !== ballots.aggregate.transcriptHash) {
@@ -127,8 +127,8 @@ const verifyBallotClosePayload = (input) => {
127
127
  };
128
128
  /**
129
129
  * Replays the published ceremony from manifest to tally, including board audit,
130
- * DKG verification, ballot verification, decryption-share verification, and
131
- * per-option tally checks.
130
+ * DKG verification, full ballot-close-slot audit, ballot verification,
131
+ * decryption-share verification, and per-option tally checks.
132
132
  *
133
133
  * This is the main verifier entry point for callers that want failures to
134
134
  * abort immediately.
@@ -150,9 +150,7 @@ export const verifyElectionCeremony = async (input) => {
150
150
  try {
151
151
  dkgAudit = await auditSignedPayloads(input.dkgTranscript);
152
152
  ballotAudit = await auditSignedPayloads(input.ballotPayloads);
153
- ballotCloseAudit = await auditSignedPayloads([
154
- input.ballotClosePayload,
155
- ]);
153
+ ballotCloseAudit = await auditSignedPayloads(input.ballotClosePayloads);
156
154
  decryptionAudit = await auditSignedPayloads(input.decryptionSharePayloads);
157
155
  tallyAudit =
158
156
  input.tallyPublications === undefined ||
@@ -280,6 +278,7 @@ export const verifyElectionCeremony = async (input) => {
280
278
  jointPublicKey: dkg.jointPublicKey,
281
279
  protocolVersion: context.protocolVersion,
282
280
  manifestHash: context.manifestHash,
281
+ scoreRangeMax: context.scoreRangeMax,
283
282
  sessionId: context.sessionId,
284
283
  });
285
284
  const publication = tallyPublicationMap.get(optionIndex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threshold-elgamal",
3
- "version": "1.1.0",
3
+ "version": "2.1.0",
4
4
  "description": "A browser-native TypeScript ElGamal library for Ristretto255-based research prototypes, shipping additive ElGamal, threshold decryption, protocol helpers, board auditing, transport primitives, and log-driven DKG state machines.",
5
5
  "author": "Piotr Piech <piotr@piech.dev>",
6
6
  "license": "MPL-2.0",
@@ -26,7 +26,8 @@
26
26
  "coverage:badge": "pnpm run coverage:node && tsx ./tools/generate-coverage-badge.ts",
27
27
  "smoke:pack": "tsx ./tools/ci/verify-packed-package.ts",
28
28
  "prebuild": "pnpm run check && pnpm run test",
29
- "build": "pnpm exec del-cli dist && tsc --project tsconfig.build.json && tsx ./tools/build/rewrite-dist-relative-imports.ts",
29
+ "build:dist": "pnpm exec del-cli dist && tsc --project tsconfig.build.json && tsx ./tools/build/rewrite-dist-relative-imports.ts",
30
+ "build": "pnpm run build:dist",
30
31
  "vectors:threshold": "tsx ./tools/generate-threshold-vectors.ts",
31
32
  "vectors:protocol": "tsx ./tools/generate-protocol-vectors.ts",
32
33
  "bench:micro": "tsx ./tools/benchmarks/microbench.ts",
@@ -39,8 +40,8 @@
39
40
  "prepare": "husky"
40
41
  },
41
42
  "dependencies": {
42
- "@noble/curves": "^2.0.1",
43
- "@noble/hashes": "^2.0.1"
43
+ "@noble/curves": "^2.2.0",
44
+ "@noble/hashes": "^2.2.0"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@astrojs/mdx": "^5.0.3",
@@ -49,25 +50,25 @@
49
50
  "@eslint/js": "^10.0.1",
50
51
  "@fontsource/ibm-plex-sans": "^5.2.8",
51
52
  "@fontsource/jetbrains-mono": "^5.2.8",
52
- "@types/node": "^25.5.2",
53
- "@typescript-eslint/eslint-plugin": "^8.58.1",
54
- "@typescript-eslint/parser": "^8.58.1",
53
+ "@types/node": "^25.6.0",
54
+ "@typescript-eslint/eslint-plugin": "^8.58.2",
55
+ "@typescript-eslint/parser": "^8.58.2",
55
56
  "@vitest/browser-playwright": "^4.1.4",
56
57
  "@vitest/coverage-v8": "^4.1.4",
57
- "astro": "^6.1.5",
58
+ "astro": "^6.1.7",
58
59
  "del-cli": "^7.0.0",
59
60
  "eslint": "^10.2.0",
60
61
  "eslint-config-prettier": "^10.1.8",
61
62
  "eslint-plugin-import-x": "^4.16.2",
62
63
  "eslint-plugin-only-error": "^1.0.2",
63
64
  "eslint-plugin-prettier": "^5.5.5",
64
- "globals": "^17.4.0",
65
+ "globals": "^17.5.0",
65
66
  "husky": "^9.1.7",
66
- "knip": "^5.67.0",
67
+ "knip": "^6.4.1",
67
68
  "playwright": "^1.59.1",
68
- "prettier": "^3.8.1",
69
+ "prettier": "^3.8.3",
69
70
  "tsx": "^4.21.0",
70
- "typedoc": "^0.28.18",
71
+ "typedoc": "^0.28.19",
71
72
  "typedoc-plugin-markdown": "^4.11.0",
72
73
  "typescript": "^6.0.2",
73
74
  "vite": "^8.0.8",