threshold-elgamal 1.1.0 → 2.0.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(...)`.
@@ -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: 1, max: 10 },
132
134
  });
133
135
 
134
136
  const manifestHash = await hashElectionManifest(manifest);
@@ -171,7 +173,7 @@ if (!result.ok) {
171
173
  }
172
174
  ```
173
175
 
174
- The root package exposes the workflow-facing builders for the signed protocol payloads used across the documented ceremony, including:
176
+ The root package exposes the builders and lower-level helpers required for the documented ceremony, including:
175
177
 
176
178
  - manifest publication
177
179
  - registration
@@ -186,14 +188,16 @@ The root package exposes the workflow-facing builders for the signed protocol pa
186
188
  - decryption shares
187
189
  - tally publication
188
190
 
189
- For the reveal path, the workflow-facing API and the advanced public submodules split responsibilities intentionally:
191
+ The reveal path also works from the root package:
190
192
 
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`
193
+ - prepare the accepted aggregate with `prepareAggregateForDecryption(...)`
194
+ - compute each partial share with `createDecryptionShare(...)`
195
+ - prove it with `createDLEQProof(...)`
196
+ - publish it with `createDecryptionSharePayload(...)`
195
197
 
196
- After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` from `threshold-elgamal/threshold` against the prepared aggregate ciphertext.
198
+ After collecting a threshold subset, recover the tally with `combineDecryptionShares(...)` against the prepared aggregate ciphertext.
199
+
200
+ The grouped public submodules remain available when you prefer narrower imports by subsystem, but the supported full ceremony does not require them.
197
201
 
198
202
  For concrete posted JSON shapes, use [Published payload examples](https://tenemo.github.io/threshold-elgamal/guides/published-payload-examples/).
199
203
 
@@ -204,7 +208,7 @@ The library is designed for an honest-origin, honest-client, static-adversary se
204
208
  What it tries to enforce:
205
209
 
206
210
  - additive-only tallying on `ristretto255`
207
- - fixed `1..10` score ballots
211
+ - one explicit global contiguous manifest score range
208
212
  - grouped per-option ballot verification
209
213
  - mandatory local aggregate recomputation before decryption
210
214
  - 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
  /**
@@ -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
+ };
@@ -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.
@@ -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
+ };
@@ -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
  };
@@ -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) {
@@ -280,6 +280,7 @@ export const verifyElectionCeremony = async (input) => {
280
280
  jointPublicKey: dkg.jointPublicKey,
281
281
  protocolVersion: context.protocolVersion,
282
282
  manifestHash: context.manifestHash,
283
+ scoreRangeMax: context.scoreRangeMax,
283
284
  sessionId: context.sessionId,
284
285
  });
285
286
  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.0.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",