Skip to content

feat: add ConfidentialVoting amendment (encrypted-tally ballots)#36

Open
dangell7 wants to merge 1 commit into
developfrom
dangell7/confidential-voting
Open

feat: add ConfidentialVoting amendment (encrypted-tally ballots)#36
dangell7 wants to merge 1 commit into
developfrom
dangell7/confidential-voting

Conversation

@dangell7

Copy link
Copy Markdown
Member

ConfidentialVoting

Adds a native ballot primitive with an encrypted, homomorphic tally. Individual choices and the running count stay hidden while a ballot is open; results are revealed at close with a decryption-correctness proof, so the tally authority can see results early but cannot lie about them.

This reuses the confidential-MPT crypto stack (mpt-crypto, EC-ElGamal + bulletproofs + compact sigma proofs) that already ships in develop — no new dependency, no Rust changes.

What it does

A Ballot ledger object holds one ElGamal ciphertext per option under a tally key. BallotCastVote submits a vector of encrypted votes (chosen option gets the weight, the rest get zero — every counter updates each cast, so which one changed leaks nothing) and the transactor homomorphically adds them into the tally. At close, BallotFinalize publishes the plaintext counts with a per-option decryption proof that validators check against the on-ledger ciphertexts.

Two eligibility modes, one per ballot:

  • Token-weighted — references an MPT issuance; weight = the holder's balance, locked until the ballot closes (lock-to-vote, since we have no historical state proofs for a record-date snapshot). This is the equity story: common stock is the issuance a ballot references.
  • Credential-gated (1p1v) — references an XLS-80 permissioned domain; any account holding an accepted credential that satisfies the domain gets exactly one vote. This is the committee / assembly story, and it plugs straight into existing XLS-70 credential infrastructure.

Each cast is verified for real: an aggregated bulletproof proves every option value is non-negative (no subtracting weight from an option you dislike), a sum check pins the vector to the voter's weight (no inflation), and a per-option compact-sigma proof links each ciphertext to its range-proven commitment so the tally can't be corrupted. Double-voting is blocked by a one-BallotVote-per-account rule (plus the token-mode lock).

New objects and transactions

  • Ledger entries: Ballot, BallotVote
  • Transactions: BallotCreate, BallotCastVote, BallotFinalize, BallotDelete
  • Amendment: ConfidentialVoting

A note on the crypto

Confidential transfer never needs to prove a value encrypted under a key you don't own is well-formed — the sender always owns their balance key. A vote encrypts under the tally key, which the voter does not own, so it needs verifiable encryption. The mpt-crypto low-level API already exposes what that takes: secp256k1_compact_standard_verify gives the ciphertext-to-commitment linkage (its balance-linkage terms are neutralized with a canonical witness that constrains nothing about the vote), and secp256k1_elgamal_verify_encryption gives the sum-to-weight check. So both modes are sound today with the merged library.

Trust model (v1)

The tally authority holds the decryption key — it can see individual votes, same trust as a transfer agent or registrar. It cannot lie about the result. This is confidential to the market, transparent to the registrar; it is not a secret ballot against the authority. A threshold-key committee (secret against the authority too) and recast-for-coercion-resistance are future work — see the spec.

Tests

New suite ConfidentialVoting (6 cases, 274 tests): amendment gating, token-mode and credential-mode happy paths end to end (create → cast → finalize with real proofs, tally decrypts to the exact counts), double-vote / window / eligibility / malformed-proof rejection, lock enforcement, and a low-level round-trip check of the linkage primitive incl. a tamper case. All proofs are generated live against the real library — no fixtures. Related suites (ConfidentialTransfer, MPToken, Invariants) still pass.

Known follow-ups (not blocking)

  • Voter-recoverable casts (lsfVoterRecoverable) accept a voter key + mirror vector and the linkage proof already pins that mirror; a dedicated Schnorr PoK on the voter key is not yet enforced.
  • The finalize decryption proof already binds results to the tally, so sum(results) provably equals total cast weight; an explicit invariant restating that bound is optional hardening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant