Version: 1.0.0
Status: Draft
AnonVote is a privacy-preserving voting protocol for organizations on the Stellar blockchain. It provides cryptographic voter anonymity and tamper-proof result integrity without relying on trust in the platform operator. Voter identity is structurally separated from ballot choice — not by policy, but at the schema and cryptographic layer.
Digital voting systems face a core tension: preventing fraud (one person, one vote) requires knowing who voted, but protecting voters requires not recording who voted for what. Most tools resolve this by trusting the operator — a policy guarantee that breaks the moment the operator is compromised, coerced, or dishonest.
AnonVote resolves this structurally. Even with full database access, it is computationally infeasible to link a vote back to a voter.
identifierHash = SHA-256(trim(lowercase(voterIdentifier)))
The raw identifier (email, employee ID, etc.) is never written to the database. Only the hash is stored, used solely to check eligibility and prevent duplicate token issuance.
rawToken = CSPRNG(32 bytes) → 64-char hex ← given to voter, never stored
tokenHash = SHA-256(rawToken) ← stored in DB
256 bits of entropy. Brute-force enumeration is computationally infeasible.
key = BALLOT_ENCRYPTION_KEY (32 bytes from env, never in DB)
iv = CSPRNG(12 bytes), unique per vote
(ct, authTag) = AES-256-GCM(key, iv, optionId)
encryptedPayload = base64(iv) + ":" + base64(authTag) + ":" + base64(ct)
AES-256-GCM provides authenticated encryption — tampering with a stored payload is detected and rejected at tally time.
resultHash = SHA-256(JSON.stringify(tally))
Written immutably to the Soroban contract and as a Stellar manageData operation. Anyone can verify the hash independently.
The three tables that matter:
EligibilityEntry VoterToken Vote
──────────────── ────────── ────
identifierHash tokenHash encryptedPayload
tokenIssued ballotId ballotId
weight used weight
usedAt rank (optional)
No foreign key, no shared column, no log entry links these tables. The only relationship is that all records belong to the same ballot — temporal, not relational.
An attacker with full DB access has: an identifier hash, a token hash, and an encrypted payload. Recovering the plaintext requires reversing SHA-256 or breaking AES-256-GCM. Both are computationally infeasible.
Every TOKEN_ISSUED, VOTE_CAST, and RESULT_PUBLISHED event is written to Stellar as a manageData operation. The transaction hash is stored in the DB and shown on the public result page. Anyone can verify via Stellar Explorer.
Provides on-chain queryable state:
- Token and vote counts per ballot
- Immutable result hash storage
- Public
is_consistent()check (tokens_issued == votes_cast)
No voter data, token values, or vote content is ever written to the contract.
Weighted voting — Each eligibility entry carries a weight. The tally sums vote.weight rather than counting rows. Consistency check: SUM(weights) == COUNT(used tokens).
Vote delegation — A token holder can delegate to another. The delegator's token is marked used with a delegatedTo pointer; the delegate's token carries delegatedFrom. The privacy engine resolves the chain at submission time.
Ranked-choice — Votes carry an optional rank field. Ballots configure allowRankedChoice and maxRankings.
- TOKEN_FLOW.md — step-by-step identity → token → vote flow
- SECURITY.md — threat model and what AnonVote does and doesn't protect against
- API.md — full REST API surface
- AnonVote/js —
@anonvote/cryptoimplementation - AnonVote/contracts — Soroban contract implementation
- NIST SP 800-38D (AES-GCM), FIPS PUB 180-4 (SHA-256)