DEV Community

Sazid Ahmed
Sazid Ahmed

Posted on

I Built a Blockchain Voting System with RSA Encryption — Here's How It Works

Digital voting is one of those problems that sounds simple until you actually think about it.

  • How do you prove a vote was counted?
  • How do you stop someone from voting twice?
  • How do you make results auditable without exposing who voted for whom?

I built a full-stack university voting system that answers all three — using cryptography as the enforcer, not just policy. Here's how it works under the hood.

The Stack
Frontend → React + Vite (port 5173)
Admin Panel → React + Vite (port 5174)
Backend API → Node.js + Express (port 3000)
Institution → Node.js API (port 4000)
Blockchain → 4-node custom network (ports 3001–3004)
Database → MySQL 8 (port 3306)
DB UI → phpMyAdmin (port 8080)

All containerized with Docker Compose. Spin it up with one command.

The Core Problem: Why Not Just Use a Database?
A SQL database is mutable. An admin can UPDATE votes SET candidate_id = 2 WHERE candidate_id = 1 and nobody would know. Even with audit logs, those logs can be edited too.

A blockchain is append-only and distributed. Every vote is a transaction. Every transaction is hashed and linked to the previous one. Tamper with one block, and every subsequent hash breaks — detectable by every other node in the network.

That's the foundation. Now let's look at what happens when someone actually casts a vote.

The Vote Casting Flow

  1. The Page Loads — Keys Are Fetched Silently When a voter navigates to the voting page, the app fetches their cryptographic keypair in the background. Once ready, the UI confirms: ✓ Cryptographic Keys Loaded Your vote will be encrypted and digitally signed.

This isn't just a status message. It's a hard gate — you can't submit without it.

  1. The Voter Selects a Candidate
    Standard radio button UI. Clean, accessible. Nothing unusual here.

  2. Submit — The Crypto Kicks In
    This is where it gets interesting. Before any network request is made, the frontend does three things:

Step 1 — Build the payload

Step 2 — Encrypt it
The payload is encrypted with the election's RSA public key using OAEP padding:

Only the election authority holds the matching private key. The server never sees the plaintext vote.

Step 3 — Sign it

The encrypted payload is signed with the voter's private key:

This proves authenticity — the vote came from this voter — without linking the voter to their candidate choice.

  1. The Receipt After the blockchain records the transaction, the voter gets a receipt:

The nullifier is the key innovation here. It's a cryptographic commitment derived from the voter's identity and the election ID — unique enough to detect double votes, but revealing nothing about the actual vote.

  1. Double-Vote Prevention Try to vote again in the same election:

You have already voted in this election.

Two-layered enforcement:

On-chain — The nullifier is stored on the blockchain. If the same nullifier appears twice, the transaction is rejected at the consensus level.
Application layer — The backend checks the nullifier in the database before even submitting to the chain.
Neither layer alone is sufficient. Together they're airtight.

The Blockchain Network
Four nodes run in Docker containers. Each node:

  • Maintains a full copy of the chain
  • Participates in block consensus
  • Validates incoming transactions (vote receipts)

Nodes discover each other via peer discovery on startup. A vote isn't confirmed until it's accepted by the network — no single node can fabricate or alter a result.

Privacy by Design — Not by Policy
The system is designed so that it is architecturally impossible to correlate a voter's identity with their candidate choice — even by the developers.

What's public

  1. That a vote was cast
  2. The transaction hash
  3. The nullifier
  4. The final tally

What's private

  1. Who the vote was for
  2. The plaintext vote payload
  3. The voter-candidate mapping
  4. Individual choices

This separation isn't achieved through access controls or database permissions. It's enforced by the cryptography itself.

Running It Locally

That's it. All 10 containers start automatically:

What I'd Do Differently
Zero-knowledge proofs instead of nullifiers. A nullifier is good. A ZK-SNARK is better — it lets you prove "I am eligible to vote and haven't voted yet" without revealing any identity information at all. It's significantly more complex to implement, but the privacy gain is substantial.

On-chain tallying. Right now, tallying happens off-chain in the backend. A smart contract approach where tallying is computed on-chain would eliminate the last remaining trust assumption.

Threshold decryption. Currently a single election authority holds the decryption key. Splitting that key across multiple authorities using Shamir's Secret Sharing would prevent any single party from decrypting votes prematurely.

Key Takeaway
The big lesson from this project: don't use access controls where you could use cryptography.

Access controls say "you're not allowed to see this." Cryptography says "you literally cannot see this." One requires trust in the system operator. The other doesn't.

In a voting system — where the entire point is that no one has to trust anyone — that difference is everything.

Have questions about the architecture, the crypto primitives, or the blockchain consensus mechanism? Drop them in the comments — happy to dig in.

Top comments (0)