- Created `/frontend/app/api/votes/check/route.ts` to handle GET requests for checking if a user has voted in a specific election. - Added error handling for unauthorized access and missing election ID. - Forwarded requests to the backend API and returned appropriate responses. - Updated `/frontend/app/api/votes/history/route.ts` to fetch user's voting history with error handling. - Ensured both endpoints utilize the authorization token for secure access.
15 KiB
Blockchain Voting Flow - Complete Documentation
Overview
When a user votes through the web interface, the entire flow is:
User selects candidate
↓
Vote encrypted (ElGamal) on client
↓
Vote submitted to API (/api/votes/submit)
↓
Backend validates voter & election
↓
Vote recorded in database
↓
Vote added to blockchain (immutable)
↓
User sees success with transaction ID
↓
Vote visible in blockchain viewer
Detailed Flow
1. Frontend: Vote Selection & Encryption
File: frontend/components/voting-interface.tsx
// Step 1: Get voter profile and public keys
const voterResponse = await fetch("/api/auth/profile")
const voterId = voterData.id
// Step 2: Get election's public keys for encryption
const keysResponse = await fetch(`/api/votes/public-keys?election_id=${electionId}`)
const publicKeys = keysResponse.data
// Step 3: Create signed ballot with client-side encryption
const ballot = createSignedBallot(
voteValue, // 1 for selected candidate
voterId, // Voter ID
publicKeys.elgamal_pubkey, // ElGamal public key
"" // Private key for signing
)
// Result includes:
// - encrypted_vote: Base64 encoded ElGamal ciphertext
// - zkp_proof: Zero-knowledge proof of validity
// - signature: RSA-PSS signature
2. Frontend: Vote Submission
File: frontend/components/voting-interface.tsx (lines 116-130)
const submitResponse = await fetch("/api/votes/submit", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({
election_id: electionId,
candidate_id: selectedCandidate,
encrypted_vote: ballot.encrypted_vote,
zkp_proof: ballot.zkp_proof,
signature: ballot.signature,
timestamp: ballot.timestamp
})
})
3. Backend: Vote Validation & Recording
File: backend/routes/votes.py (lines 99-210)
@router.post("/submit")
async def submit_vote(
vote_bulletin: schemas.VoteBulletin,
current_voter: Voter = Depends(get_current_voter),
db: Session = Depends(get_db),
request: Request = None
):
# 1. Verify voter hasn't already voted
if services.VoteService.has_voter_voted(db, current_voter.id, election_id):
raise HTTPException(detail="Already voted")
# 2. Verify election exists
election = services.ElectionService.get_election(db, election_id)
# 3. Verify candidate exists
candidate = db.query(Candidate).filter(...).first()
# 4. Decode encrypted vote (from base64)
encrypted_vote_bytes = base64.b64decode(vote_bulletin.encrypted_vote)
# 5. Generate ballot hash (immutable record)
ballot_hash = SecureHash.hash_bulletin(
vote_id=current_voter.id,
candidate_id=candidate_id,
timestamp=int(time.time())
)
# 6. Record vote in database
vote = services.VoteService.record_vote(
db=db,
voter_id=current_voter.id,
election_id=election_id,
candidate_id=candidate_id,
encrypted_vote=encrypted_vote_bytes,
ballot_hash=ballot_hash,
ip_address=request.client.host
)
4. Backend: Blockchain Recording
File: backend/routes/votes.py (lines 181-210)
# Generate unique transaction ID (anonymized)
transaction_id = f"tx-{uuid.uuid4().hex[:12]}"
# Add vote to blockchain
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
block = blockchain.add_block(
encrypted_vote=vote_bulletin.encrypted_vote,
transaction_id=transaction_id
)
# Mark voter as voted
services.VoterService.mark_as_voted(db, current_voter.id)
# Return success with blockchain info
return {
"id": vote.id,
"transaction_id": transaction_id,
"block_index": block.index,
"ballot_hash": ballot_hash,
"timestamp": vote.timestamp
}
5. Backend: Blockchain Structure
File: backend/blockchain.py
class Block:
"""Immutable voting block"""
index: int
prev_hash: str # Hash of previous block (chain integrity)
timestamp: int
encrypted_vote: str # Base64 encrypted vote
transaction_id: str # Unique identifier for this vote
block_hash: str # SHA-256 hash of this block
signature: str # Digital signature
class Blockchain:
"""Election blockchain"""
blocks: List[Block] # All blocks for election
def add_block(self, encrypted_vote: str, transaction_id: str) -> Block:
"""Add new vote block and compute hash chain"""
new_block = Block(
index=len(self.blocks),
prev_hash=self.blocks[-1].block_hash if self.blocks else "0"*64,
timestamp=int(time.time()),
encrypted_vote=encrypted_vote,
transaction_id=transaction_id
)
# Compute SHA-256 hash of block
new_block.block_hash = sha256(json.dumps({...}).encode()).hexdigest()
self.blocks.append(new_block)
return new_block
def verify_chain_integrity(self) -> bool:
"""Verify no blocks have been tampered with"""
for i, block in enumerate(self.blocks):
# Check hash chain continuity
if i > 0 and block.prev_hash != self.blocks[i-1].block_hash:
return False
# Recompute hash and verify
if block.block_hash != compute_block_hash(block):
return False
return True
6. Frontend: Blockchain Viewer
File: frontend/app/dashboard/blockchain/page.tsx
The blockchain page fetches and displays the blockchain:
// Fetch blockchain for selected election
const response = await fetch(
`/api/votes/blockchain?election_id=${selectedElection}`,
{ headers: { Authorization: `Bearer ${token}` } }
)
const data = await response.json()
// Returns:
// {
// blocks: [
// {
// index: 0,
// prev_hash: "0000...",
// timestamp: 1699328400,
// encrypted_vote: "aGVsbG8...", // Base64
// transaction_id: "tx-abc123...",
// block_hash: "e3b0c4...",
// signature: "d2d2d2..."
// },
// ...
// ],
// verification: {
// chain_valid: true,
// total_blocks: 3,
// total_votes: 2
// }
// }
File: frontend/components/blockchain-visualizer.tsx
Displays blockchain with:
- Statistics dashboard (blocks, votes, status, security)
- Expandable block cards showing all fields
- Copy-to-clipboard for hashes
- Visual integrity check
- Staggered animations
7. Backend: Blockchain Retrieval
File: backend/routes/votes.py (lines 351-370)
@router.get("/blockchain")
async def get_blockchain(
election_id: int = Query(...),
db: Session = Depends(get_db)
):
"""Retrieve complete blockchain for election"""
blockchain = blockchain_manager.get_or_create_blockchain(election_id)
return blockchain.get_blockchain_data()
# Returns: {blocks: [...], verification: {...}}
Data Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ │
│ 1. User votes on /dashboard/votes/active/1 │
│ 2. VotingInterface encrypts vote (ElGamal) │
│ 3. Submits to POST /api/votes/submit │
│ 4. Shows success with transaction_id │
│ 5. User views blockchain on /dashboard/blockchain │
│ 6. BlockchainVisualizer displays all blocks │
└─────────────────────────────────────────────────────────────┘
↑↓ HTTP API
┌─────────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ │
│ POST /api/votes/submit: │
│ - Validate voter & election │
│ - Verify candidate exists │
│ - Record vote in database │
│ - ADD TO BLOCKCHAIN ← New block created │
│ - Return transaction_id & block_index │
│ │
│ GET /api/votes/blockchain: │
│ - Retrieve all blocks for election │
│ - Calculate chain verification status │
│ - Return blocks + verification data │
│ │
│ POST /api/votes/verify-blockchain: │
│ - Verify chain integrity (no tampering) │
│ - Check all hashes are correct │
│ - Return verification result │
└─────────────────────────────────────────────────────────────┘
↑↓ Database
┌─────────────────────────────────────────────────────────────┐
│ MariaDB Database │
│ │
│ votes table: │
│ - voter_id, election_id, candidate_id │
│ - encrypted_vote (stored encrypted) │
│ - ballot_hash, timestamp │
│ - ip_address │
│ │
│ blockchain (in-memory BlockchainManager): │
│ - All blocks for each election │
│ - Chain integrity verified on each access │
└─────────────────────────────────────────────────────────────┘
Security Features
1. Encryption
- ElGamal Homomorphic: Vote encrypted client-side
- Cannot decrypt individual votes (only aggregate counts)
- Public key available at
/api/votes/public-keys
2. Signatures
- RSA-PSS: Each ballot digitally signed
- Ballot Hash: SHA-256 hash of vote metadata
- Transaction ID: Anonymized identifier (not tied to voter)
3. Blockchain Integrity
- Hash Chain: Each block references previous hash
- Tamper Detection: Any change breaks chain verification
- Immutable: Blocks cannot be modified (verified on GET)
- Voter Anonymity: Transaction ID is anonymous UUID
4. Vote Validation
- One vote per person:
has_voter_voted()check - Valid election: Election must exist and be active
- Valid candidate: Candidate must be in election
- Authentication: Vote requires valid JWT token
Testing the Flow
1. Via Web Interface
1. Go to http://localhost:3000/dashboard/votes/active
2. Click "Participer" on election
3. Select candidate and confirm
4. See "Vote enregistré avec succès"
5. Click "Voir la blockchain"
6. See your vote block added to chain
2. Via API (cURL)
# 1. Register user
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "voter@test.fr",
"password": "Test@12345",
"first_name": "Jean",
"last_name": "Dupont",
"citizen_id": "12345ABC"
}'
# Response: {"access_token": "eyJ0eXA..."}
# 2. Get public keys
curl http://localhost:8000/api/votes/public-keys?election_id=1
# 3. Submit vote (encrypted client-side in real scenario)
curl -X POST http://localhost:8000/api/votes/submit \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXA..." \
-d '{
"election_id": 1,
"candidate_id": 2,
"encrypted_vote": "aGVsbG8gd29ybGQ=",
"zero_knowledge_proof": "...",
"signature": "..."
}'
# Response:
# {
# "id": 1,
# "transaction_id": "tx-abc123def456",
# "block_index": 1,
# "ballot_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
# }
# 4. Fetch blockchain
curl http://localhost:8000/api/votes/blockchain?election_id=1
# 5. Verify blockchain integrity
curl -X POST http://localhost:8000/api/votes/verify-blockchain \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXA..." \
-d '{"election_id": 1}'
# Response: {"chain_valid": true}
Key Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/api/votes/submit |
POST | Submit encrypted vote (adds to blockchain) |
/api/votes/blockchain |
GET | Retrieve all blocks for election |
/api/votes/verify-blockchain |
POST | Verify chain integrity |
/api/votes/results |
GET | Get election results with verification |
/api/votes/public-keys |
GET | Get public keys for encryption |
/api/elections/active |
GET | List active elections |
/api/elections/{id} |
GET | Get election details with candidates |
Files Involved
Frontend
frontend/components/voting-interface.tsx- Vote submission formfrontend/app/dashboard/votes/active/page.tsx- Elections listfrontend/app/dashboard/votes/active/[id]/page.tsx- Vote detail pagefrontend/app/dashboard/blockchain/page.tsx- Blockchain viewerfrontend/components/blockchain-visualizer.tsx- Blockchain visualizationfrontend/lib/crypto-client.ts- Encryption & signing
Backend
backend/routes/votes.py- Vote endpointsbackend/blockchain.py- Blockchain implementationbackend/crypto/elgamal.py- ElGamal encryptionbackend/crypto/signatures.py- Digital signaturesbackend/crypto/hashing.py- SHA-256 hashingbackend/services/vote_service.py- Vote business logic
Success Indicators
✓ User can select candidate and vote ✓ Vote encrypted before transmission ✓ Vote recorded in database ✓ Vote added to blockchain ✓ Transaction ID returned to user ✓ Blockchain viewer shows new block ✓ All hashes verify correctly ✓ Chain integrity: valid ✓ ✓ One vote per person enforced ✓ Only active elections can be voted on