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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.