Changes:
- Add next-themes dependency for theme management
- Create ThemeProvider wrapper for app root layout
- Set dark mode as default theme
- Create ThemeToggle component with Sun/Moon icons
- Add theme toggle to home page navigation
- Add theme toggle to dashboard header
- App now starts in dark mode with ability to switch to light mode
Styling uses existing Tailwind dark mode variables configured in
tailwind.config.ts and globals.css. All existing components automatically
support dark theme.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
21 KiB
21 KiB
Technical Design: Proof-of-Authority Blockchain Architecture
Architecture Overview
Network Topology
Private Docker Network (172.25.0.0/16)
│
├─ bootnode:8546 (Peer Discovery)
│ └─ POST /register_peer - Register validator
│ └─ GET /discover - List active peers
│
├─ validator-1:30303 (PoA Validator + JSON-RPC)
│ ├─ p2p_listen_addr: 30303
│ ├─ rpc_listen_addr: 8001
│ └─ Authorized signer: validator_1_private_key
│
├─ validator-2:30304 (PoA Validator + JSON-RPC)
│ ├─ p2p_listen_addr: 30304
│ ├─ rpc_listen_addr: 8002
│ └─ Authorized signer: validator_2_private_key
│
├─ validator-3:30305 (PoA Validator + JSON-RPC)
│ ├─ p2p_listen_addr: 30305
│ ├─ rpc_listen_addr: 8003
│ └─ Authorized signer: validator_3_private_key
│
├─ api-server:8000 (FastAPI)
│ └─ Submits votes to validators via JSON-RPC
│
├─ mariadb:3306 (Database)
│ └─ Stores voter data and election metadata
│
└─ frontend:3000 (Next.js)
└─ User interface
Component Details
1. Bootnode Service
Purpose: Enable peer discovery and network bootstrap
Interface:
# HTTP Interface
GET /health
Returns: {"status": "healthy"}
POST /register_peer
Request: {
"node_id": "validator-1",
"ip": "validator-1",
"p2p_port": 30303,
"rpc_port": 8001,
"public_key": "0x..."
}
Returns: {"registered": true, "peers": [...]}
GET /discover
Query params: node_id=validator-1
Returns: {
"peers": [
{"node_id": "validator-2", "ip": "validator-2", "p2p_port": 30304, ...},
{"node_id": "validator-3", "ip": "validator-3", "p2p_port": 30305, ...}
]
}
GET /peers
Returns: {"peers": [...], "count": 3}
Implementation (bootnode/bootnode.py):
- FastAPI service
- In-memory peer registry (dict)
- Register endpoint (POST /register_peer)
- Discover endpoint (GET /discover)
- Health check endpoint
- Periodic cleanup of stale peers
Docker Service:
bootnode:
build:
context: .
dockerfile: docker/Dockerfile.bootnode
container_name: evoting_bootnode
ports:
- "8546:8546"
networks:
- evoting_network
environment:
BOOTNODE_PORT: 8546
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8546/health"]
2. Validator Node Service
Purpose: Run PoA consensus and maintain blockchain state
Key Data Structures:
# Genesis Block (hardcoded in each validator)
genesis_block = {
"index": 0,
"prev_hash": "0x" + "0" * 64,
"timestamp": 1699360000,
"transactions": [],
"block_hash": "0x...",
"validator": "genesis",
"signature": "genesis",
"authorized_validators": [
"0x1234567890abcdef...", # validator-1 public key
"0x0987654321fedcba...", # validator-2 public key
"0xabcdefabcdefabcd...", # validator-3 public key
]
}
# Block Structure
block = {
"index": 1,
"prev_hash": "0x...", # Hash of previous block
"timestamp": 1699360010,
"transactions": [
{
"voter_id": "anon-tx-abc123",
"election_id": 1,
"encrypted_vote": "0x7f3a...",
"ballot_hash": "0x9f2b...",
"proof": "0xabcd...",
"timestamp": 1699360001
}
],
"block_hash": "0xdeadbeef...",
"validator": "0x1234567...", # Address of validator who created block
"signature": "0x5678..." # Signature of block_hash
}
# Pending Transaction Pool
pending_transactions = [
# Votes waiting to be included in a block
]
Consensus Algorithm (PoA - Proof of Authority):
Round-robin block creation:
1. Validator #1 creates block 1, signs it
2. Validator #2 creates block 2, signs it
3. Validator #3 creates block 3, signs it
4. Validator #1 creates block 4, signs it
... (repeat)
Block validation by other validators:
1. Receive block from peer
2. Verify block_hash is correct
3. Verify signature is from authorized validator
4. Verify block extends previous block (prev_hash matches)
5. Verify all transactions in block are valid
6. If valid, add to chain and broadcast to other peers
7. If invalid, reject and optionally report to watchdog
JSON-RPC Interface (for API Server communication):
# Standard Ethereum-like JSON-RPC methods
# Send vote (transaction)
POST /rpc
{
"jsonrpc": "2.0",
"method": "eth_sendTransaction",
"params": [{
"to": null,
"data": "0x..." + base64(encrypted_vote_json)
}],
"id": 1
}
Response: {"jsonrpc": "2.0", "result": "0x...", "id": 1}
# result = transaction hash
# Get transaction receipt
POST /rpc
{
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": ["0x..."],
"id": 2
}
Response: {"jsonrpc": "2.0", "result": {...}, "id": 2}
# Get block number
POST /rpc
{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": [],
"id": 3
}
Response: {"jsonrpc": "2.0", "result": "0x64", "id": 3} # Block 100
# Get block by number
POST /rpc
{
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": ["0x64", true],
"id": 4
}
Response: {"jsonrpc": "2.0", "result": {...block...}, "id": 4}
P2P Network Protocol (Simple custom protocol):
# Message Types
{
"type": "peer_hello",
"node_id": "validator-1",
"genesis_hash": "0x...",
"chain_length": 42,
"best_hash": "0x..."
}
{
"type": "sync_request",
"from_index": 0,
"to_index": 42
}
{
"type": "sync_response",
"blocks": [{...}, {...}, ...]
}
{
"type": "new_block",
"block": {...}
}
{
"type": "new_transaction",
"transaction": {...}
}
Implementation (validator/validator.py):
class PoAValidator:
def __init__(self, node_id, private_key, bootnode_url):
self.node_id = node_id
self.private_key = private_key
self.chain = [genesis_block]
self.pending_transactions = []
self.peer_connections = {}
self.bootnode_url = bootnode_url
async def startup(self):
# 1. Register with bootnode
await self.register_with_bootnode()
# 2. Discover other peers
await self.discover_peers()
# 3. Connect to peers
await self.connect_to_peers()
# 4. Sync blockchain
await self.sync_blockchain()
# 5. Start block creation task
asyncio.create_task(self.block_creation_loop())
# 6. Start P2P listen
asyncio.create_task(self.p2p_listen())
async def block_creation_loop(self):
while True:
if self.should_create_block():
block = self.create_block()
self.add_block_to_chain(block)
await self.broadcast_block(block)
await asyncio.sleep(5) # Create block every 5 seconds
def should_create_block(self):
# Determine if this validator's turn to create block
# Based on round-robin: block_index % num_validators
next_index = len(self.chain)
validator_index = get_validator_index(self.node_id)
num_validators = 3
return next_index % num_validators == validator_index
def create_block(self):
# Take up to N pending transactions
txs = self.pending_transactions[:32]
self.pending_transactions = self.pending_transactions[32:]
block = {
"index": len(self.chain),
"prev_hash": self.chain[-1]["block_hash"],
"timestamp": time.time(),
"transactions": txs,
"validator": self.public_key,
"block_hash": None, # Calculated below
"signature": None # Calculated below
}
# Calculate block hash
block_hash = sha256(json.dumps(block, sort_keys=True))
block["block_hash"] = block_hash
# Sign block hash
signature = sign(block_hash, self.private_key)
block["signature"] = signature
return block
def validate_block(self, block):
# Verify block is valid
checks = [
self.verify_block_hash(block),
self.verify_signature(block),
self.verify_prev_hash(block),
self.verify_transactions(block)
]
return all(checks)
async def handle_block(self, block):
if self.validate_block(block):
self.add_block_to_chain(block)
await self.broadcast_block(block) # Gossip to peers
else:
logger.error(f"Invalid block: {block['index']}")
async def handle_transaction(self, transaction):
self.pending_transactions.append(transaction)
await self.broadcast_transaction(transaction)
Docker Service:
validator-1:
build:
context: .
dockerfile: docker/Dockerfile.validator
container_name: evoting_validator_1
ports:
- "8001:8001" # JSON-RPC port
- "30303:30303" # P2P port
environment:
NODE_ID: validator-1
PRIVATE_KEY: ${VALIDATOR_1_PRIVATE_KEY}
BOOTNODE_URL: http://bootnode:8546
RPC_PORT: 8001
P2P_PORT: 30303
networks:
- evoting_network
depends_on:
- bootnode
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8001/rpc"]
3. API Server Integration
Changes to existing FastAPI backend:
# In backend/main.py or new backend/blockchain_client.py
class BlockchainClient:
def __init__(self, validator_urls: List[str]):
self.validator_urls = validator_urls
self.current_validator_index = 0
async def submit_vote(self, encrypted_vote: bytes, ballot_hash: str) -> str:
"""
Submit vote to blockchain via validator JSON-RPC
Returns: transaction hash
"""
# Format vote as transaction
transaction = {
"voter_id": f"anon-tx-{uuid.uuid4().hex[:12]}",
"election_id": election_id,
"encrypted_vote": encrypted_vote.hex(),
"ballot_hash": ballot_hash,
"proof": proof,
"timestamp": int(time.time())
}
# Send via JSON-RPC
tx_hash = await self.send_transaction(transaction)
return tx_hash
async def send_transaction(self, transaction: dict) -> str:
"""Send transaction to blockchain"""
payload = {
"jsonrpc": "2.0",
"method": "eth_sendTransaction",
"params": [{
"data": "0x" + json.dumps(transaction).encode().hex()
}],
"id": 1
}
response = await self._rpc_call(payload)
return response["result"]
async def get_transaction_receipt(self, tx_hash: str) -> dict:
"""Get confirmation that vote was included in block"""
payload = {
"jsonrpc": "2.0",
"method": "eth_getTransactionReceipt",
"params": [tx_hash],
"id": 2
}
response = await self._rpc_call(payload)
return response["result"]
async def _rpc_call(self, payload: dict) -> dict:
"""Make JSON-RPC call with failover to other validators"""
for i in range(len(self.validator_urls)):
validator_url = self.validator_urls[self.current_validator_index]
self.current_validator_index = (self.current_validator_index + 1) % len(self.validator_urls)
try:
async with aiohttp.ClientSession() as session:
async with session.post(f"{validator_url}/rpc", json=payload) as resp:
if resp.status == 200:
return await resp.json()
except Exception as e:
logger.error(f"Failed to reach {validator_url}: {e}")
continue
raise Exception("All validators unreachable")
# Initialize blockchain client
blockchain_client = BlockchainClient([
"http://validator-1:8001",
"http://validator-2:8002",
"http://validator-3:8003"
])
# In routes/votes.py
@router.post("/api/votes/submit")
async def submit_vote(vote_bulletin: VoteBulletin, current_voter: Voter = Depends(get_current_voter)):
"""
Submit encrypted vote to blockchain
"""
# ... existing validation ...
# Submit to blockchain
tx_hash = await blockchain_client.submit_vote(
encrypted_vote=encrypted_vote_bytes,
ballot_hash=ballot_hash
)
# Optional: wait for confirmation
# receipt = await blockchain_client.get_transaction_receipt(tx_hash)
# Mark voter as voted
services.VoterService.mark_as_voted(db, current_voter.id)
return {
"id": vote.id,
"transaction_id": tx_hash,
"ballot_hash": ballot_hash,
"timestamp": vote.timestamp
}
Database Schema Updates
New Tables (optional - for vote indexing):
-- Store vote transaction hashes for quick lookup
CREATE TABLE vote_transactions (
id INT PRIMARY KEY AUTO_INCREMENT,
voter_id INT NOT NULL,
election_id INT NOT NULL,
tx_hash VARCHAR(66) NOT NULL UNIQUE, -- Transaction hash on blockchain
block_number INT, -- Block containing this vote
ballot_hash VARCHAR(64) NOT NULL,
submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (voter_id) REFERENCES voter(id),
FOREIGN KEY (election_id) REFERENCES election(id)
);
-- Store validator list for audit trail
CREATE TABLE validator_nodes (
id INT PRIMARY KEY AUTO_INCREMENT,
node_id VARCHAR(64) NOT NULL UNIQUE,
public_key VARCHAR(256) NOT NULL,
endpoint VARCHAR(256) NOT NULL,
active BOOLEAN DEFAULT TRUE,
registered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_heartbeat TIMESTAMP
);
Security Considerations
Validator Key Management
# Private keys generated once during setup
# Stored in environment variables
validator-1:
VALIDATOR_1_PRIVATE_KEY=0x...
validator-2:
VALIDATOR_2_PRIVATE_KEY=0x...
validator-3:
VALIDATOR_3_PRIVATE_KEY=0x...
# In production:
# - Store in HashiCorp Vault
# - Use AWS KMS for key management
# - Implement HSM (Hardware Security Module) support
Byzantine Fault Tolerance
With 3 validators:
- Can tolerate 1 validator failure (f = 1)
- Need 2/3 majority for consensus (2 validators)
Formula: Can tolerate (n - 1) / 3 Byzantine nodes
- 3 validators: tolerate 0 (actually 1 if majority rule)
- 5 validators: tolerate 1
- 7 validators: tolerate 2
For true BFT, would need different consensus (PBFT)
Vote Confidentiality
Vote submission path:
1. Voter -> Frontend (HTTPS)
2. Frontend encrypts vote with ElGamal public key
3. Frontend submits encrypted vote to API Server (HTTPS)
4. API Server -> Validator (encrypted over HTTPS)
5. Validator broadcasts encrypted vote via P2P
6. Encrypted vote stored on blockchain
7. Only authorized party can decrypt with private key
Docker Compose Updates
version: '3.8'
services:
# Bootnode for peer discovery
bootnode:
build:
context: .
dockerfile: docker/Dockerfile.bootnode
container_name: evoting_bootnode
ports:
- "8546:8546"
networks:
- evoting_network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8546/health"]
depends_on: []
# Validators (PoA blockchain nodes)
validator-1:
build:
context: .
dockerfile: docker/Dockerfile.validator
container_name: evoting_validator_1
ports:
- "8001:8001"
- "30303:30303"
depends_on:
- bootnode
environment:
NODE_ID: validator-1
PRIVATE_KEY: ${VALIDATOR_1_PRIVATE_KEY:-0x...}
BOOTNODE_URL: http://bootnode:8546
networks:
- evoting_network
validator-2:
# ... similar to validator-1
ports:
- "8002:8002"
- "30304:30304"
environment:
NODE_ID: validator-2
PRIVATE_KEY: ${VALIDATOR_2_PRIVATE_KEY:-0x...}
validator-3:
# ... similar to validator-1
ports:
- "8003:8003"
- "30305:30305"
environment:
NODE_ID: validator-3
PRIVATE_KEY: ${VALIDATOR_3_PRIVATE_KEY:-0x...}
# API Server (existing backend with blockchain integration)
api-server:
build:
context: .
dockerfile: docker/Dockerfile.backend
container_name: evoting_api
ports:
- "8000:8000"
depends_on:
- mariadb
- validator-1
- validator-2
- validator-3
environment:
DB_HOST: mariadb
VALIDATOR_URLS: http://validator-1:8001,http://validator-2:8002,http://validator-3:8003
networks:
- evoting_network
mariadb:
image: mariadb:latest
# ... existing config ...
frontend:
# ... existing config ...
Testing Strategy
Unit Tests
# tests/test_validator.py
def test_block_creation():
validator = PoAValidator("test-validator", private_key)
block = validator.create_block()
assert block["index"] == 1
assert block["validator"] == validator.public_key
assert verify_signature(block["signature"], block["block_hash"])
def test_block_validation():
validator = PoAValidator("test-validator", private_key)
valid_block = validator.create_block()
assert validator.validate_block(valid_block) == True
# Tamper with block
valid_block["transactions"][0]["encrypted_vote"] = "0x00"
assert validator.validate_block(valid_block) == False
def test_consensus():
# Three validators create blocks in sequence
validators = [
PoAValidator("validator-1", key1),
PoAValidator("validator-2", key2),
PoAValidator("validator-3", key3)
]
# Simulate block creation round
for i, validator in enumerate(validators):
if validator.should_create_block():
block = validator.create_block()
for other in validators:
if other != validator:
assert other.validate_block(block) == True
Integration Tests
# tests/integration/test_voting_workflow.py
async def test_complete_voting_workflow():
# 1. Start Docker containers
# 2. Register voter
# 3. Login
# 4. Submit vote
# 5. Verify vote on blockchain
# 6. Check blockchain integrity
response = await register_voter(email="test@example.com")
assert response["success"] == True
token = await login(email="test@example.com", password="password")
assert token is not None
tx_hash = await submit_vote(token, election_id=1, candidate_id=1)
assert tx_hash is not None
receipt = await get_transaction_receipt(tx_hash)
assert receipt["blockNumber"] > 0
block = await get_block(receipt["blockNumber"])
assert len(block["transactions"]) > 0
End-to-End Tests
#!/bin/bash
# tests/e2e.sh
# Start system
docker compose up -d --build
# Wait for services to be ready
sleep 30
# Test bootnode
curl http://localhost:8546/health
assert_success
# Test validators
for i in {1..3}; do
curl http://localhost:$((8000 + i))/rpc
assert_success
done
# Test voting workflow
# ... (Python test script)
# Check blockchain integrity
curl http://localhost:8000/api/blockchain/verify
assert_success
# Cleanup
docker compose down
Deployment Notes
Initial Setup
# 1. Generate validator keys
python scripts/generate_keys.py
-> Creates VALIDATOR_1_PRIVATE_KEY, VALIDATOR_2_PRIVATE_KEY, etc.
# 2. Update .env file
echo "VALIDATOR_1_PRIVATE_KEY=0x..." >> .env
echo "VALIDATOR_2_PRIVATE_KEY=0x..." >> .env
echo "VALIDATOR_3_PRIVATE_KEY=0x..." >> .env
# 3. Start system
docker compose up -d --build
# 4. Verify all services are healthy
docker compose ps
docker compose logs bootnode | grep "healthy"
Monitoring
# Monitor blockchain growth
curl http://localhost:8000/api/blockchain/stats
-> {"total_blocks": 42, "total_votes": 39, "chain_valid": true}
# Monitor validator health
curl http://localhost:8001/health
curl http://localhost:8002/health
curl http://localhost:8003/health
# Check peer connections
curl http://localhost:8546/peers
-> {"peers": [{"node_id": "validator-1", ...}, ...]}
Performance Characteristics
Throughput
- Block creation: 1 block per 5 seconds (configurable)
- Transactions per block: ~32 (configurable)
- Overall throughput: ~6.4 votes per second
- Can increase by:
- Reducing block time
- Increasing transactions per block
- Adding more validators (sharding)
Latency
- Vote submission to blockchain: ~5-10 seconds
- 1-2 seconds: submit to validator
- 3-5 seconds: wait for block creation
- 1-2 seconds: broadcast to peers
- Confirmation: 1-2 additional blocks (10-20 seconds)
Storage
- Per vote: ~1 KB (encrypted vote + metadata)
- Per block: ~32 KB (32 votes + signatures)
- Annual storage: ~1-5 GB for 100K votes
Rollback Plan
If critical issues discovered:
-
Stop new submissions:
- API Server rejects new votes temporarily
-
Identify issue:
- Analyze blockchain logs
- Check validator consensus
- Verify data integrity
-
Rollback options:
- Option A (No data loss): Reset validators to last valid block
- Option B (Start fresh): Wipe blockchain, restart from genesis
-
Restart:
docker compose down- Fix code/configuration
docker compose up -d --build- Resume voting