feat: add ConfidentialVoting amendment (encrypted-tally ballots) - #36
feat: add ConfidentialVoting amendment (encrypted-tally ballots)#36dangell7 wants to merge 2 commits into
Conversation
|
No failures yet, but checks are still running: |
| Slice const& proof, | ||
| uint256 const& contextHash); | ||
|
|
||
| } // namespace xrpl |
There was a problem hiding this comment.
The aggregated Bulletproof proof slice is passed directly to mpt_verify_aggregated_bulletproof with no size validation — not even an upper bound. BallotCastVote preflight only rejects an empty blob; a multi-megabyte crafted proof passes preflight, reaches preclaim, and forces the Bulletproof verifier to process an arbitrarily large input. Every other verifier in this file (verifySchnorrProof, verifyClawbackProof, verifySendProof) enforces an exact expected byte length before the C call — this one is the only exception. Add an upper-bound check in preflight tied to the maximum option count (8 options), and add a corresponding lower-bound check in verifyBallotRangeProof itself, matching the pattern of every sibling function.
Suggested fix
In BallotCastVote::preflight, replace the bare !ctx.tx[sfZKProof].empty() check with a range check: proof size must be within [minBulletproofSize(n), maxBulletproofSize(n)] where n comes from the ballot's sfOptionCount. In verifyBallotRangeProof itself, add if (proof.size() < kMinAggregatedBulletproofLength || proof.size() > kMaxAggregatedBulletproofLength) return tecBAD_PROOF; before the mpt_verify_aggregated_bulletproof call, consistent with all other verifiers in this file.
|
No failures yet, but checks are still running: |
1 similar comment
|
No failures yet, but checks are still running: |
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
Ballotledger object holds one ElGamal ciphertext per option under a tally key.BallotCastVotesubmits 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,BallotFinalizepublishes the plaintext counts with a per-option decryption proof that validators check against the on-ledger ciphertexts.Two eligibility modes, one per ballot:
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
Ballot,BallotVoteBallotCreate,BallotCastVote,BallotFinalize,BallotDeleteConfidentialVotingA 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-cryptolow-level API already exposes what that takes:secp256k1_compact_standard_verifygives the ciphertext-to-commitment linkage (its balance-linkage terms are neutralized with a canonical witness that constrains nothing about the vote), andsecp256k1_elgamal_verify_encryptiongives 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)
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.sum(results)provably equals total cast weight; an explicit invariant restating that bound is optional hardening.