Skip to main content

ZKTally

Privacy-preserving, anonymously verifiable e-voting from linkable ring signatures, Paillier homomorphic encryption, and NIZK vote-correctness proofs.

ZKTally is unaudited academic software implementing a protocol from a student research paper. It has not been reviewed by professional cryptographers and must not be used for any binding election.

A ballot is the triple (ciphertexts, proofs, ring signature):

  • Paillier encryption hides the vote. Tallying multiplies ciphertexts and decrypts only the aggregate, so no individual ballot is ever opened.
  • A NIZK OR-proof shows the plaintext is a legal vote without revealing which one.
  • A linkable ring signature proves the caster is some registered voter, and its deterministic key image makes a second ballot from the same key detectable.

Wire format: zktally/1.


Install

pip install zktally                 # no runtime dependencies
pip install "zktally[fast]"         # optional gmpy2 acceleration

Python 3.11+. The pure-stdlib path works everywhere; gmpy2 is roughly 5x faster on Paillier and must produce byte-identical results.

Library

from zktally import *

pk, sk = paillier_keygen(2048)

# Voters generate their own keys. Nothing else may generate them.
voters = [ring_keygen() for _ in range(5)]
ring = canonical_ring([v.public for v in voters])

params = ElectionParams(
    election_id=new_election_id(),
    title="Adopt the proposal?",
    ballot_type="binary", k=1, candidates=("Yes",),
    public_key=pk, trust_model="single-authority", ring=ring,
)

board = Board(params=params)
for choice, voter in zip([1, 0, 1, 1, 0], voters):
    board.append(cast_ballot(choice, params, voter, ring.index(voter.public)))

print(tally(board, sk).totals)      # (3,)

Multi-candidate elections use ballot_type="one-of-k" with k >= 2, which proves each column binary and proves the columns sum to exactly one.

Command line

zktally keygen   --bits 3072 [--threshold 3-of-5] --out keys/
zktally election new --title "…" --type binary|one-of-k --candidates a,b,c \
                     --key keys/public.json --out params.json
zktally voterkey --out voter.json                      # voter-side only
zktally register --params params.json --add voter*.json
zktally vote     --params params.json --key voter.json --choice 1 --out ballot.json
zktally board    --params params.json --add ballot*.json --out board.json
zktally verify   --board board.json [--explain]
zktally tally    --board board.json --shares s1.json,s2.json --out result.json
zktally audit    --board board.json --result result.json

zktally audit takes no secret material. It re-verifies every ballot and every partial decryption from public data alone, which is what makes public verifiability checkable rather than asserted.


Security properties

✅ achieved · ⚠️ conditional · ❌ not provided

Property Conditions
Ballot secrecy ⚠️ Against anyone below quorum. Not against t colluding authorities, nor against a dishonest dealer during v1 setup
Voter anonymity ⚠️ Anonymity set is the ring, or the sub-ring when partitioned. Defeated by traffic analysis
Double-vote prevention ✅ Key image bound into every challenge; first-wins resolution
Ballot-stuffing resistance ✅ Ring fixed by the election parameters, never read from a ballot
Vote well-formedness ✅ Binary / 1-of-k NIZK plus mandatory ciphertext-domain validation
Public verifiability of ballots ✅ Anyone re-verifies every proof and signature from public data
Public verifiability of the tally ✅ Threshold mode only; partial decryptions carry correctness proofs
Non-malleability ✅ Election context bound into every transcript; proofs bound into the signed message
Eligibility ⚠️ Cryptographically enforced given an honest registrar
Receipt-freeness ❌ The voter holds the encryption randomness, which reconstructs their ciphertext. Structural, not a bug
Coercion resistance ❌ Not implemented. Needs fake credentials or an untappable channel
Everlasting privacy ❌ Ciphertexts are public forever; a future break reveals every vote
Side-channel resistance ❌ Not constant-time, and pure-Python int arithmetic cannot be made so
Post-quantum security ❌ Paillier and secp256k1 both fall to Shor

Under trust_model="single-authority" the key holder can decrypt any individual ballot and the published total carries no proof. zktally audit reports that decryption as not proved rather than verified.

v1 uses a trusted dealer for threshold setup: that party transiently knows the factors, so ballot privacy rests on them destroying that material rather than on the threshold itself.

Development

uv venv && uv pip install -e ".[dev]"
pytest                              # full suite
pytest -m "not slow"                # skip real-parameter keygen
ruff check . && ruff format --check .
mypy
zktally vectors generate --seed 00…00 --out tests/vectors/

Test suites: unit/, property/ (Hypothesis), conformance/ (the shared vector corpus, run under both numeric backends), and tamper/ — one test per known attack, each asserting the exact rejection reason.

The corpus in tests/vectors/ is generated here and mirrored into the TypeScript port, which is validated against it. It has exactly one source; hand-editing the mirror is a CI failure.

Relationship to the paper

This implementation is normative where it and the originating paper disagree. The paper states the second OR-proof branch as a discrete-log relation that does not match its own prover, omits R_i and the ring from the ring-signature challenge, leaves the curve, hash, hash-to-curve and point encoding unpinned, and claims receipt-freeness that the construction does not provide.

License

MIT. See LICENSE.

Metadata

Release files for zktally 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for zktally 0.1.0
File Size Uploaded Provenance Actions
zktally-0.1.0.tar.gz 80.7 kB Without provenance Details for zktally-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for zktally 0.1.0
File Interpreter ABI Platform Actions
zktally-0.1.0-py3-none-any.whl Python 3 none any Details for zktally-0.1.0-py3-none-any.whl

Total release size: 140.5 kB

Release files / zktally-0.1.0.tar.gz

Download URL zktally-0.1.0.tar.gz
Size 80.7 kB
Tags Source
SHA-256 checksum
How to use checksums
242e557e16634ded520391b6891accb6e340d29fa56011c73180c0fbbe0531f3
BLAKE2b-256 checksum
How to use checksums
9a290514a399c68aed5cec6549ea9b90aeb9ec8967e9f087fdd34c7e890e2ecb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Kali GNU/Linux","version":"2026.3","id":"kali-rolling","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / zktally-0.1.0-py3-none-any.whl

Download URL zktally-0.1.0-py3-none-any.whl
Size 59.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
16b0e71a398b02a8c1a22f60855bbf379845e5e473b7184ba8ce7a4ba45e19f0
BLAKE2b-256 checksum
How to use checksums
e262384412712a3d58499940f6a4a1c8a4f9c3bf85ced9afbf83d6b58443fce3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Kali GNU/Linux","version":"2026.3","id":"kali-rolling","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for zktally 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page