Cicada is a private on-chain voting protocol based on homomorphic time-lock puzzles.
Requires Foundry.
Install: forge install
Build: forge build
Differential tests: forge test --match-test testRef --ffi
Other tests: forge test --no-match-test testRef
At a high-level, our implementation adapts the linearly-homomorphic time-lock puzzle scheme described in (Malavolta and Thyagarajan, 2019), using exponential ElGamal instead of Paillier encryption.
Throughout the following descriptions, we take LibUint1024.normalize
. We use
A time-lock puzzle is a cryptographic puzzle that encapsulates some secret which can be recovered by performing
Time-lock puzzles are useful for voting schemes because they allow users to post their votes as a puzzle, ensuring it can eventually be revealed while keeping it secret during the election, a property called running-tally privacy. The goal is that users can cast votes without being influenced by other votes already cast. Time-lock puzzles are rather unique in the field of private voting schemes in that they achieve running-tally privacy without relying on tallying authorities, threshold encryption or any other trusted parties: anybody can solve a time-lock puzzle to ensure votes are revealed after the election.
A homomorphic time-lock puzzle is one which can be homomorphically manipulated without knowing the solution or backdoor. In particular, a linearly homomorphic time-lock puzzle allows one to add two puzzles, producing a new puzzle which encapsulates the sum of the the original two puzzles, or perform scalar multiplications on puzzles.
As the authors of the paper note, linearly homomorphic time-lock puzzles are particularly suitable primitive for private voting: ballots can be encoded as puzzles, and they can be homomorphically combined to obtain a puzzle encoding the final tally. This allows a single computation to recover the final tally, rather than solving a unique puzzle for every vote.
In the original HTLP paper, the linearly-homomorphic scheme is presented using Paillier encryption for the time-lock puzzle:
The structure of the Paillier cryptosystem provides additive homomorphism and fast decoding of the secret
Instead, we use exponential ElGamal, as follows:
Exponential ElGamal provides additive homomorphism, but decoding the final tally requires brute-forcing the discrete log of
To instantiate an election, the following parameters are required:
- An RSA modulus
$N$ (e.g. 1024-4096 bits). This could be a standard modulus, or one generated via multi-party computation. If someone knows the factorization of$N$ , they would be able to quickly decrypt all ballots, undermining running-tally privacy. Future implementations could utilize class groups, removing the risk of this trapdoor. Our default implementation uses a 1024-bit modulus for efficiency. Note that, while this is well above the largest RSA modulus ever publicly factored (which was 829 bits) it is below the normally recommended size of 2048 bits for use with RSA encryption or signatures. However, we don't need long-term security in our application. Once an election is finished there is no risk if$N$ is factored in the future. Thus it is reasonable to use a relatively small modulus; this can be easily updated in the future if factoring algorithms improve. - A "time" parameter
$\mathcal{T}$ , the number of sequential squarings required to reveal the contents of the time-lock puzzle. This parameter must be carefully chosen to ensure an adversary (potentially with hardware acceleration) can't decrypt votes during the ballot-casting period. - Two randomly chosen generators
$g$ and$y$ of$\mathbb{Z}_N^*$ with Jacobi symbol 1, and the precomputed value$h := g^{2^\mathcal{T}} \mod{N}$ . It is possible to efficiently prove that$h$ was generated correctly using a Wesolowski proof or Pietrzak proof.
In the _createVote
function, the time-lock puzzle associated with the vote's running tally is initialized to a value encoding 0. This could be done by setting both
Together, pp
in the smart contract. Note that
The _createVote
function takes the following parameters:
string description
(A human-readable description of the vote)uint64 startTime
(When the voting period starts, as a Unix timestamp)uint64 votingPeriod
(The length of the voting period, in seconds)
To cast a ballot, the voter provides a homomorphic time-lock puzzle encoding their choice (0 representing "no", 1 representing "yes"), and a zero-knowledge proof that the provided puzzle is a valid ballot.
Proving that the ballot is a valid is equivalent to the disjunction of two
where the former case represents a "no" ballot and the
latter case represents a "yes" ballot. Protocol 1 describes the
A proof of ballot validity consists of eight values:
Note that the protocol is made non-interactive using the Fiat-Shamir heuristic (the challenge
If the proof of validity is successfully verified, the running tally is updated, leveraging the additive homomorphism of the time-lock puzzles:
To finalize the result of a vote, someone needs to
- perform the sequential squarings:
$w := u^{2^T}$ , - brute-force the discrete-log to recover the tally value:
$\text{tally}_\text{pt} := \text{dlog}_y(v \cdot w^{-1})$ , and - call the
_finalizeVote
function with the solution and associated proof of exponentiation.
For the proof of exponentiation, we will need a 256-bit prime. To make finalization non-interactive, we use the Fiat-Shamir heuristic. –– the prime must be a valid hash-to-prime output, as follows:
The primality test is instantiated using Baillie-PSW, but can be replaced with another probabilistic primality test (or e.g. Pocklington primality certificates).
The contract then verifies the Wesolowski proof of exponentiation as follows:
Finally, the contract checks that the relation between
If all the checks pass, the vote is marked as finalized.
The construction described above provides tally privacy –– the time-lock puzzle property keeps the tally private for the time paramter
For some elections this may not be desirable; while we are satisfied with temporary tally privacy, we may want indefinite ballot privacy. To accomplish this, we can combine the homomorphic time-lock puzzle scheme with an anonymous voter eligibility protocol, instantiated by zero-knowledge set membership proofs. This way, even if a ballot is decrypted, all it would reveal is that someone voted that way –– which is already known from the tally.
We provide an example contract using Semaphore for anonymity, but you can plug in your ZK set membership solution of choice.
This contract contains the core logic of the homomorphic time-lock puzzle voting protocol. It is meant to be inherited and extended with application-specific logic, e.g. access control: who can create a vote, who can cast a ballot, etc.
The three main functions follow the intuitive lifecycle of a vote: _createVote
, _castBallot
, and _finalizeVote
.
The SemaphoreCicada
contract is an example of how one can extend CicadaVote
with a ZK set membership protocol to achieve ballot privacy. Note that Semaphore could be replaced with some other ZK set membership module (e.g. Semacaulk) to the same effect.
The modular 1024-bit arithmetic needed for RSA group operations is implemented in LibUint1024.sol
. We represent the numbers using uint256[4]
types.
Most functions in the library can be generated for an arbitrary number size (uint256[N]
for some N
) using the LibUint.sol.jinja
template and the corresponding script generate_lib_uint.py
. The mulMod
is a notable exception; the approach implemented LibUint1024.sol
doesn't scale past uint256[4]
due to stack size limitations.
This library implements various primality tests: Miller-Rabin, Lucas, and Baillie-PSW. In addition, it implements Pocklington primality certificate verification, which guarantees (deterministically) that a number is prime.
The checkHashToPrime
function (used in CicadaVote.sol
) uses the Baillie-PSW primality test because it offers a good balance between gas efficiency and security.
LibUint1024.t.sol
contains fuzz tests for the arithmetic functions in LibUint1024
. These include differential tests, where the reference functions are written in Python (see big_math_reference.py
).
The numbers used in LibPrime.t.sol
were generated using https://bigprimes.org/.
VerifyBallot.t.sol
and VerifySolution.t.sol
contain tests for _verifyBallotValidity
and _verifySolutionCorrectness
, respectively. The tests were generated using generate_verify_ballot_test.py
and generate_verify_solution_test.py
. These scripts also give a sketch of how a user might generate these proofs in a production setting.
CicadaVote.t.sol
contains an end-to-end test, and can be used to benchmark the gas cost of casting a ballot (using the --gas-report
flag).
These smart contracts are being provided as is. No guarantee, representation or warranty is being made, express or implied, as to the safety or correctness of the user interface or the smart contracts. They have not been audited and as such there can be no assurance they will work as intended, and users may experience delays, failures, errors, omissions or loss of transmitted information. THE SMART CONTRACTS CONTAINED HEREIN ARE FURNISHED AS IS, WHERE IS, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING ANY WARRANTY OF MERCHANTABILITY, NON- INFRINGEMENT OR FITNESS FOR ANY PARTICULAR PURPOSE. Further, use of any of these smart contracts may be restricted or prohibited under applicable law, including securities laws, and it is therefore strongly advised for you to contact a reputable attorney in any jurisdiction where these smart contracts may be accessible for any questions or concerns with respect thereto. Further, no information provided in this repo should be construed as investment advice or legal advice for any particular facts or circumstances, and is not meant to replace competent counsel. a16z is not liable for any use of the foregoing, and users should proceed with caution and use at their own risk. See a16z.com/disclosures for more info.