An educational blockchain-based voting prototype written in Python. Votes are submitted to a pending pool, validators create blocks using a simple Proof of Authority consensus mechanism, and P2P nodes share blocks and votes with each other.
The project demonstrates the core ideas behind blockchain voting:
- one-vote-per-voter protection;
- confirmed votes stored in a blockchain;
- chain integrity checks through block hashes;
- Proof of Authority with round-robin validator rotation;
- Merkle roots stored in blocks and Merkle proofs for compact vote inclusion verification;
- a local CLI client and Flask API for node interaction.
.
├── app.py # starts a Flask P2P node
├── demo.py # local demo without HTTP servers
├── run_nodes_test.sh # smoke test with three local nodes
├── core/
│ ├── block.py # block model
│ ├── blockchain.py # blockchain storage and integrity checks
│ └── merkle.py # Merkle tree and proof verification
├── consensus/
│ └── poa.py # Proof of Authority logic
├── voting/
│ ├── vote.py # vote model
│ ├── vote_pool.py # pending vote pool
│ └── ledger.py # vote tallying and ledger audit
├── network/
│ ├── node.py # P2P node behavior
│ └── routes.py # HTTP API routes
└── client/
├── cli.py # command-line client
├── client.py # HTTP client wrapper
└── verification.py # receipts and audit helpers
Python 3.10+ is recommended.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtDependencies:
- Flask
- requests
Run the local demo without starting any servers:
python demo.pyThe demo:
- Creates a blockchain, vote pool, and PoA validator.
- Submits several votes.
- Tests double-vote prevention.
- Creates a block containing the votes.
- Shows the block hash and stored Merkle root.
- Verifies the PoA signature.
- Verifies a compact Merkle proof for one vote receipt.
- Prints election results and a ledger audit.
Use the smoke-test script:
./run_nodes_test.shThe script starts three nodes:
node1onlocalhost:5001node2onlocalhost:5002node3onlocalhost:5003
It then submits two votes, waits for a block to be produced, prints node status, shows election results, verifies the first vote receipt, and runs an audit.
Run each command in a separate terminal:
python app.py --node-id node1 --port 5001 \
--validators node1,node2,node3 \
--peers localhost:5002,localhost:5003python app.py --node-id node2 --port 5002 \
--validators node1,node2,node3 \
--peers localhost:5001,localhost:5003python app.py --node-id node3 --port 5003 \
--validators node1,node2,node3 \
--peers localhost:5001,localhost:5002The default shared PoA secret is:
shared_poa_secret_2024
You can override it with the --secret argument.
Submit a vote:
python client/cli.py --node http://localhost:5001 vote \
--voter-id voter001 \
--candidate Alice \
--salt 2024The CLI returns a vote_id. Save it so you can later verify that the vote was included in a block.
Verify a vote:
python client/cli.py --node http://localhost:5001 verify \
--vote-id <vote_id>The client asks the node for a compact receipt and then verifies the Merkle proof locally. It does not need to download the full chain for this verification step.
Show results:
python client/cli.py --node http://localhost:5001 resultsShow node status:
python client/cli.py --node http://localhost:5001 statusRun an audit:
python client/cli.py --node http://localhost:5001 auditMain node endpoints:
| Method | Endpoint | Description |
|---|---|---|
GET |
/ping |
Check whether the node is alive |
GET |
/status |
Node status, chain height, peers, and next validator |
GET |
/peers |
List known peers |
POST |
/peers/register |
Register a peer node |
GET |
/chain |
Return the current blockchain |
POST |
/votes/submit |
Submit a new vote |
POST |
/votes/receive |
Receive a vote from another node |
GET |
/votes/pending |
Return pending votes |
POST |
/blocks/receive |
Receive a block from another node |
GET |
/verify/<vote_id> |
Return a receipt and Merkle proof |
GET |
/results |
Return tally results and audit data |
GET |
/ledger/validate |
Validate the ledger |
GET |
/log |
Return the vote pool audit log |
Example vote submission through HTTP:
curl -X POST http://localhost:5001/votes/submit \
-H "Content-Type: application/json" \
-d '{
"vote_id": "example-vote-id",
"voter_id": "example-voter-hash",
"voter_hash": "example-voter-salted-hash",
"candidate": "Alice",
"timestamp": 1710000000
}'In normal use, the CLI is easier because it creates the vote payload automatically.
The vote pool audit log records every submission attempt with:
- timestamp;
- event status, such as
accept,reject, orconfirmed; vote_id;- hashed
voter_id; - salted
voter_hash; - selected candidate;
vote_hash, a SHA-256 hash of the submitted vote payload.
- The client creates a vote with
Vote.create(...). - The real voter id is hashed and is not stored in plain text.
- The node checks that the candidate is valid and that the voter has not already voted.
- The vote is added to
VotePool. - The node broadcasts the vote to its peers.
- When a validator's turn arrives, it collects pending votes into a block.
- The block stores a Merkle root computed from the included
vote_idvalues. - The block hash commits to the Merkle root, and the PoA signature signs that block hash.
- The node broadcasts the block to its peers.
- Other nodes validate the PoA signature, block index, previous hash, Merkle root, and block hash.
- Once confirmed, the vote can be verified by its
vote_id.
Validators are configured with a comma-separated list:
node1,node2,node3
Blocks are produced in round-robin order:
block #1 -> node1
block #2 -> node2
block #3 -> node3
block #4 -> node1
...
This prototype signs blocks with HMAC-SHA256 and a shared secret. In a production system, this should be replaced with real public/private key signatures such as ECDSA or Ed25519.
After a vote is submitted, it receives a vote_id. Once the vote is included in a block, this endpoint:
/verify/<vote_id>
returns a compact receipt:
- block index;
- block hash;
- validator id;
- Merkle root;
- Merkle proof.
Example receipt shape:
{
"success": true,
"verified": true,
"vote_id": "saved-vote-id",
"block_index": 1,
"block_hash": "confirmed-block-hash",
"validator": "node1",
"merkle_root": "root-stored-in-the-block",
"proof": [
["sibling-hash", "right"]
],
"proof_length": 1
}Each block stores a merkle_root that is computed from the vote_id values included in that block. The Merkle root is part of the block hash, and the PoA signature signs that block hash. This means the receipt is tied to the confirmed block rather than being an arbitrary proof generated after the fact.
The local Merkle proof check verifies that:
hash(vote_id) + proof path -> merkle_root stored in the block
If the recomputed root matches the receipt root, the vote_id is included in the block committed by that Merkle root.
This is a demonstration project, not a production-ready voting system.
Important limitations:
- no real cryptographic voter identity;
- all validators share the same PoA secret;
- no database or persistent storage;
- restarting a node resets its in-memory blockchain;
- no Sybil resistance or advanced network conflict handling;
- synchronization uses the longest valid chain rule;
- Flask's development server is not suitable for production.
Useful commands:
python demo.py
python -m compileall app.py demo.py core consensus voting network client
./run_nodes_test.shIf everything works, the demo ends with Demo complete!, and the smoke test prints election results, a successful Merkle verification, and Audit: PASSED.

