433 lines
15 KiB
Markdown
433 lines
15 KiB
Markdown
# 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`
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```python
|
|
@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)
|
|
|
|
```python
|
|
# 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`
|
|
|
|
```python
|
|
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:
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
```python
|
|
@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)
|
|
|
|
```bash
|
|
# 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 form
|
|
- `frontend/app/dashboard/votes/active/page.tsx` - Elections list
|
|
- `frontend/app/dashboard/votes/active/[id]/page.tsx` - Vote detail page
|
|
- `frontend/app/dashboard/blockchain/page.tsx` - Blockchain viewer
|
|
- `frontend/components/blockchain-visualizer.tsx` - Blockchain visualization
|
|
- `frontend/lib/crypto-client.ts` - Encryption & signing
|
|
|
|
### Backend
|
|
- `backend/routes/votes.py` - Vote endpoints
|
|
- `backend/blockchain.py` - Blockchain implementation
|
|
- `backend/crypto/elgamal.py` - ElGamal encryption
|
|
- `backend/crypto/signatures.py` - Digital signatures
|
|
- `backend/crypto/hashing.py` - SHA-256 hashing
|
|
- `backend/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
|