Rust implementation of the Aura private voting protocol (Jivanyan & Feickert, 2022).
Aura is an election protocol that dissociates ballots from voter identity cryptographically, uses verifiable encryption and threshold decryption, and requires no trusted setup.
- Ballot privacy: ElGamal encryption with threshold decryption
- Voter anonymity: One-out-of-N commitment set membership proofs (logarithmic size)
- Coercion resistance: Ballot recasting with encrypted serial numbers
- Trust-free setup: Pedersen DKG with Feldman commitments, no trusted dealer
- Efficient proofs: Batch-verifiable Schnorr-type proving systems
- Universal verifiability: Any observer can verify the election result
src/
proofs/ 6 proving systems (representation, DLEQ, enc validity,
serial validity, bit vector, commitment set)
lib_backed/ Optional library-backed equivalents of the 6 proofs
(feature `lib-proofs`, see "Library-backed proofs" below)
elgamal/ Threshold ElGamal (encrypt, partial decrypt, BSGS dlog)
dkg/ Pedersen distributed key generation
signature.rs Context-bound Schnorr signatures
election/ Protocol orchestration (setup, ballot, tally, bulletin board)
vendor/
one-of-many-proofs/ Vendored fork of cargodog/one-of-many-proofs, used by
`lib_backed::commitment_set`. Patched to drop the
`simd_backend`/`nightly` features (those pulled in
`packed_simd_2`, which no longer compiles on stable
rustc). MIT-licensed; original by cargodog.
- Group: Ristretto255 via
curve25519-dalek(prime-order, DDH-hard, constant-time) - Transcripts: Merlin (Fiat-Shamir transform)
- MSM: Pippenger's algorithm via
curve25519-dalek::vartime_multiscalar_mul
- Rust 1.70+ (install via rustup)
- Optional:
gnuplotfor higher-quality benchmark plots
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Optional: install gnuplot for benchmark HTML plots
sudo apt install gnuplot # Debian/Ubuntu
brew install gnuplot # macOScargo test # hand-rolled (default): 57 unit + 5 integration
cargo test --features lib-proofs # adds 22 lib-backed unit tests → 79 + 5Output: 62 tests by default; 84 with lib-proofs enabled.
cargo test --releasecargo test proofs::commitment_set # all commitment set tests
cargo test proofs::bit_vector # all bit vector tests
cargo test proofs::dleq # DLEQ proof tests
cargo test proofs::representation # representation proof tests
cargo test dkg::protocol # DKG protocol tests
cargo test elgamal::decrypt # ElGamal decryption tests
cargo test signature # signature testscargo test --test full_electionThis runs two end-to-end tests:
full_election_8_voters_3_talliers_4_candidates— complete election flowcoercion_resistance_ballot_recast— voter recasts ballot, old one is discarded
cargo test full_election_8_voters
cargo test coercion_resistancecargo test -- --nocapturecargo clippy -- -D warningsAll benchmarks use Criterion.rs and compile in release mode automatically. Results include statistical analysis with confidence intervals and regression detection.
| Suite | Command | What it measures |
|---|---|---|
| Proof primitives | cargo bench --bench proof_primitives |
All 6 proof types, bit vector scaling, set proof scaling, batch verify (rep, dleq, encval) |
| Proof sizes | cargo bench --bench proof_sizes -- --quick |
Byte sizes of all proofs and ballots |
| Election N=16 | cargo bench --bench election_n16 |
Full election: DKG, voter reg, ballot, batch verify (individual + batched encval), tally |
| Election N=64 | cargo bench --bench election_n64 |
Full election |
| Election N=256 | cargo bench --bench election_n256 |
Full election |
| Election N=1,024 | cargo bench --bench election_n1024 |
Full election |
| Election N=4,096 | cargo bench --bench election_n4096 |
Full election |
| Election N=16,384 | cargo bench --bench election_n16384 |
Full election |
| Election N=65,536 | cargo bench --bench election_n65536 |
Full election |
| Election N=262,144 | cargo bench --bench election_n262144 |
Ballot create/verify + set proof only |
| Election N=1,048,576 | cargo bench --bench election_n1048576 |
Ballot create/verify + set proof only |
| Lib comparison | cargo bench --bench proof_primitives_lib --features lib-proofs |
Hand-rolled vs library-backed prove/verify for all 6 proofs |
Note: For N >= 262,144, only ballot-level operations are benchmarked. For N <= 65,536, the full pipeline is measured including DKG, tally serial decryption, and tally result computation.
cargo benchRunning all suites end-to-end is slow. Prefer individual suites.
cargo bench --bench election_n256cargo bench --bench election_n256 -- "ballot/create"
cargo bench --bench election_n256 -- "ballot/verify"
cargo bench --bench election_n256 -- "tally/full_pipeline"
cargo bench --bench election_n256 -- "tally/serial_decrypt"
cargo bench --bench election_n256 -- "dkg"
cargo bench --bench election_n256 -- "voter_registration"
cargo bench --bench election_n256 -- "verify_batch"cargo bench --bench proof_primitives -- "representation"
cargo bench --bench proof_primitives -- "dleq"
cargo bench --bench proof_primitives -- "enc_validity"
cargo bench --bench proof_primitives -- "serial_validity"
cargo bench --bench proof_primitives -- "bit_vector"
cargo bench --bench proof_primitives -- "commitment_set"Batch verification compresses multiple proof verification equations into a single multi-scalar multiplication (MSM) using random linear combination. This amortizes the per-proof cost as batch size grows — the key efficiency property described in Section 4.1 of the paper.
Proof-level batch verification (individual proof types, batch sizes 1-100):
# Representation proofs: individual vs batch
cargo bench --bench proof_primitives -- "batch_verify_rep"
# DLEQ proofs: individual vs batch
cargo bench --bench proof_primitives -- "batch_verify_dleq"
# Encryption validity proofs: individual vs batch
cargo bench --bench proof_primitives -- "batch_verify_encval"
# All three at once
cargo bench --bench proof_primitives -- "batch_verify"Cross-ballot batch verification (collect all encryption validity proofs across N ballots, batch-verify in one MSM):
# At N=256: compare individual vs batched encval verification for 1/10/50/100 ballots
cargo bench --bench election_n256 -- "verify_batch_encval"
# At N=1024
cargo bench --bench election_n1024 -- "verify_batch_encval"Each batch benchmark reports both individual (verify proofs one by one) and batch (single MSM call) timings at the same batch size, so you can directly compute the speedup factor.
cargo bench --bench election_n256 2>&1 | tee results_n256.txtAfter running any benchmark, Criterion generates HTML reports with plots:
# Open the report index (Linux)
xdg-open target/criterion/report/index.html
# macOS
open target/criterion/report/index.html
# Or browse directly
ls target/criterion/Each benchmark group gets its own subdirectory with:
report/index.html— summary with violin plotsnew/estimates.json— raw timing data- Comparison data if you run benchmarks multiple times
Prints byte sizes for all proof types, full ballots at every scale, and a comparison with ElectionGuard:
cargo bench --bench proof_sizes -- --quickExample output:
INDIVIDUAL PROOF SIZES
Representation (n=2): 96 B
DLEQ: 96 B
Encryption validity: 128 B
Serial validity: 192 B
COMMITMENT SET PROOF SIZES (logarithmic in N)
N=256 : 1,312 B
N=65,536 : 2,592 B
N=1,048,576 : 3,232 B
FULL BALLOT SIZES (k=4, kmin=1, kmax=2)
N=256 : 2.8 KB
N=65,536 : 4.1 KB
N=1,048,576 : 4.7 KB
For a complete performance profile:
# 1. Proof sizes
cargo bench --bench proof_sizes -- --quick
# 2. Individual proof costs
cargo bench --bench proof_primitives
# 3. Small-scale full elections
cargo bench --bench election_n16
cargo bench --bench election_n64
cargo bench --bench election_n256
# 4. Medium-scale full elections
cargo bench --bench election_n1024
cargo bench --bench election_n4096
# 5. Large-scale ballot operations
cargo bench --bench election_n16384
cargo bench --bench election_n65536
cargo bench --bench election_n262144
cargo bench --bench election_n1048576Each of the six proving systems has two parallel implementations in this crate:
- Hand-rolled at src/proofs/ — the default. These reproduce the Aura paper's constructions directly: same statements, same transcript layout, same generators, same MSM strategy. This is what the protocol uses for all elections, ballots, DKG, and tally operations regardless of feature flags.
- Library-backed at src/proofs/lib_backed/ — only compiled when the
lib-proofsCargo feature is enabled. Each proof delegates to an off-the-shelf Rust ZK library on its own native crypto stack, with byte-level bridging at the module boundary.
- Hand-rolled is the paper-faithful reference. Every byte of every transcript and every generator choice matches the Aura paper, so the implementation can be audited line-by-line against the spec, and the protocol's correctness arguments carry over unchanged.
- Library-backed answers the practical question: what would a developer get if they tried to build Aura on top of widely-used Rust ZK crates instead of writing the Sigma protocols by hand? It surfaces the cost of (a) the library's overhead vs. a custom prover, (b) statement-fidelity compromises forced by the library's API, and (c) curve / transcript / RNG bridging when the library targets an older dalek line.
Comparing the two side-by-side gives prove/verify timings, proof sizes, and a concrete sense of which library replacements are clean drop-ins versus which require substantive deviations from the paper's statement.
| # | Proof | Library | Crypto stack | Bridged? |
|---|---|---|---|---|
| 1 | Representation | zkp 0.8 |
curve25519-dalek-ng 3 / merlin 2 / rand 0.7 |
yes |
| 2 | DLEQ | sigma-protocols 0.5 |
curve25519-dalek 4.1 |
no |
| 3 | EncryptionValidity | zkp 0.8 |
curve25519-dalek-ng 3 / merlin 2 / rand 0.7 |
yes |
| 4 | SerialValidity | zkp 0.8 |
curve25519-dalek-ng 3 / merlin 2 / rand 0.7 |
yes |
| 5 | BitVector | bulletproofs 5 (R1CS) |
curve25519-dalek 4.1 / merlin 3 |
no |
| 6 | One-of-N | one-of-many-proofs 0.1 (vendored) |
curve25519-dalek 2 / merlin 2 / rand 0.7 |
yes |
"Bridged" means the library lives on a different curve/transcript stack than Aura's home (curve25519-dalek 4.1 + merlin 3). For those proofs, the lib_backed module converts at the API boundary via the canonical 32-byte Ristretto255 / Scalar encoding (RFC 9496) and re-keys the library's transcript with a 64-byte challenge squeezed from Aura's merlin-3 transcript. The bridge module is at src/proofs/lib_backed/bridge.rs.
Bulletproofs is pinned to git/main (not crates.io 5.0.0) because the released version's R1CS code uses a pre-CtOption Scalar::from_canonical_bytes API that does not compile against curve25519-dalek 4.1.
Two of the six library-backed proofs do not prove the exact statement of the hand-rolled paper construction:
- #5 BitVector (bulletproofs R1CS): certifies the predicate (each
b_i ∈ {0,1},Σ b_i = w) over per-bit Pedersen commitments using bulletproofs' defaultPedersenGens, without binding to Aura's vector commitmentB = r·H + Σ b_i·G_i. The hand-rolled proof additionally binds toB. - #6 One-of-N (
one-of-many-proofs): uses the library's hardcoded basepointG = RISTRETTO_BASEPOINT_POINT(not Aura'sparams.h), only supports set sizes that are exact powers of two, and recomputesc_primeinternally using the library's basepoint — the lib-backedc_primeis stored on the proof, separate fromstatement.c_prime. The hand-rolled proof uses Aura's generators and the Spark-modified Bootle construction.
These are documented in module docstrings (bit_vector.rs, commitment_set.rs) and should be disclosed alongside any timing comparison.
Without the lib-proofs feature, only the hand-rolled proofs are even compiled — there is no possibility of accidentally exercising the library-backed path.
# Tests for every hand-rolled proof + the full protocol (DKG, ballots, tally)
cargo test
# Hand-rolled proof primitives, all six types
cargo bench --bench proof_primitives
# Hand-rolled proof in any election bench
cargo bench --bench election_n256The protocol's call sites in src/election/ballot.rs, src/elgamal/decrypt.rs, src/dkg/protocol.rs, and src/election/setup.rs all import from crate::proofs::* (hand-rolled) directly, so every full-election test and every election bench exercises the hand-rolled path — even when the lib-proofs feature is on.
# Run only the lib-backed unit tests (22 tests, one module per proof)
cargo test --features lib-proofs proofs::lib_backed
# Or one specific lib-backed proof:
cargo test --features lib-proofs proofs::lib_backed::dleq
cargo test --features lib-proofs proofs::lib_backed::commitment_set
# Skip-run any lib-backed test from outside, e.g.:
cargo test --features lib-proofs --lib proofs::lib_backed::bit_vector::tests::large_kcargo bench --bench proof_primitives_lib --features lib-proofsEach Criterion group runs four cases per proof type: prove/handrolled, prove/lib, verify/handrolled, verify/lib. Groups available: rep_compare, dleq_compare, encval_compare, serval_compare, bitvec_compare, set_compare.
Filter to a single proof:
cargo bench --bench proof_primitives_lib --features lib-proofs -- 'dleq_compare'
cargo bench --bench proof_primitives_lib --features lib-proofs -- 'set_compare'- Jivanyan, A., Feickert, A. (2022). Aura: private voting with reduced trust on tallying authorities. ePrint 2022/543
- Bootle, J. et al. (2015). Short Accountable Ring Signatures Based on DDH. ESORICS 2015.
- Jivanyan, A., Feickert, A. (2021). Lelantus Spark. ePrint 2021/1173
BSD-3-Clause