Blockchain transparency makes voting dangerous: visible votes enable vote buying (prove your vote, get paid) and coercion. Encrypt the ballots? Whoever holds the decryption key to count them can still see every vote - and potentially sell or leak that data. Traditional encryption just shifts who holds the power.
This example demonstrates tallying votes without decrypting individual ballots. Votes stay encrypted throughout - only aggregate results are revealed.
Transparent blockchain architectures conflict with ballot secrecy requirements:
- Transaction visibility: All blockchain data is publicly accessible by default
- Ballot privacy: People may not want peers, family, or colleagues knowing how they voted on sensitive issues - votes need to stay private to prevent social pressure and judgment
- Vote buying: If you can prove how you voted, someone can pay you to vote a certain way and verify you followed through
- Public tallying: Everyone needs to be able to check that the final count is correct, without seeing how individual people voted
The requirement is computing aggregate vote tallies without revealing individual ballots, while providing accurate and tamper-resistant final counts.
The protocol maintains ballot secrecy while providing accurate results:
- Ballot encryption: Votes are encrypted on the client's computer before submission
- On-chain storage: Encrypted votes are recorded on the blockchain
- Secure distributed tallying: Arcium nodes collaboratively compute aggregate totals
- Result publication: Only aggregate vote counts are revealed, not individual choices
- Security guarantee: Arcium's MPC protocol preserves ballot secrecy even with a dishonest majority—individual votes remain private as long as one node is honest
# Install dependencies
yarn install # or npm install or pnpm install
# Build the program
arcium build
# Run tests
arcium testThe test suite demonstrates poll creation, encrypted ballot submission, secure distributed tallying, and result verification.
Votes are sent as encrypted booleans and stored as encrypted vote counts on-chain (using Enc<Shared, bool> in the code). Arcium's confidential instructions enable aggregate computation over encrypted ballots.
Key properties:
- Ballot secrecy: Individual votes remain encrypted throughout the tallying process
- Distributed computation: Arcium nodes jointly compute aggregate tallies
- Result accuracy: Aggregate totals are computed correctly despite processing only encrypted data
- Double-vote prevention: A
VoterRecordPDA (seeded by poll + voter keys) is initialized via Anchor'sinitconstraint in thevoteinstruction — a second vote from the same voter fails because the account already exists
Conceptual Challenge: How do you count votes without seeing individual ballots?
Traditional approaches all fail:
- Encrypt then decrypt: Someone holds the decryption key and can see votes
- Trusted counter: Requires trusting the tallying authority
The Question: Can we compute "yes_votes + no_votes" on encrypted data without ever decrypting individual votes?
Voting demonstrates storing encrypted counters directly in Anchor accounts:
#[account]
pub struct PollAccount {
pub vote_state: [[u8; 32]; 2], // Two 32-byte ciphertexts
pub nonce: u128, // Cryptographic nonce
pub authority: Pubkey, // Who can reveal results
// ... other fields
}What's stored: Two encrypted u64 counters (yes, no) as raw ciphertexts.
Arx nodes need precise byte locations to read encrypted data from accounts and deserialize it into the proper MPC function arguments.
To specify encrypted account data, provide exact byte offsets:
Argument::Account(
ctx.accounts.poll_acc.key(),
8 + 1, // Skip: Anchor discriminator (8 bytes) + bump (1 byte)
64, // Read: 2 ciphertexts × 32 bytes = 64 bytes
)Memory layout:
Byte 0-7: Anchor discriminator
Byte 8: bump
Byte 9-40: yes ciphertext (Enc<Mxe, u64>)
Byte 41-72: no ciphertext (Enc<Mxe, u64>)
Byte 73+: other fields...
MPC instruction (runs inside encrypted computation):
pub fn vote(
input: Enc<Shared, UserVote>, // Voter's encrypted choice
votes: Enc<Mxe, VoteStats>, // Current encrypted tallies
) -> Enc<Mxe, VoteStats> {
let input = input.to_arcis(); // Decrypt in MPC (never exposed)
let mut votes = votes.to_arcis(); // Decrypt tallies in MPC
if input.vote {
votes.yes += 1; // Increment happens inside MPC
} else {
votes.no += 1;
}
votes.owner.from_arcis(votes) // Re-encrypt updated tallies
}Callback (runs on-chain after MPC completes):
pub fn vote_callback(
ctx: Context<VoteCallback>,
output: SignedComputationOutputs<VoteOutput>,
) -> Result<()> {
let o = match output.verify_output(
&ctx.accounts.cluster_account,
&ctx.accounts.computation_account,
) {
Ok(VoteOutput { field_0 }) => field_0,
Err(_) => return Err(ErrorCode::AbortedComputation.into()),
};
// Save new encrypted tallies + new nonce
ctx.accounts.poll_acc.vote_state = o.ciphertexts;
ctx.accounts.poll_acc.nonce = o.nonce;
Ok(())
}Learn more: Callback Type Generation, Input/Output Patterns
The program restricts result revelation to the poll authority:
pub fn reveal_result(votes: Enc<Mxe, VoteStats>) -> bool {
let votes = votes.to_arcis();
(votes.yes > votes.no).reveal() // Only reveal comparison
}This example shows how to:
- Store encrypted data in Solana accounts: Using raw bytes (
[[u8; 32]; 2]) to persist encrypted values on-chain - Pass encrypted account data to MPC: Using
Argument::Account()with precise byte offsets to read encrypted state - Compute on encrypted state over time: Accumulating encrypted values across multiple transactions (adding new votes to running tallies)
This pattern applies to any scenario requiring private aggregation: voting, surveys, sealed-bid auctions, confidential analytics, and private leaderboards.