Alexis Bruneteau f825a2392c feat: Implement dark theme for frontend with toggle
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>
2025-11-07 16:35:44 +01:00

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:

  1. Stop new submissions:

    • API Server rejects new votes temporarily
  2. Identify issue:

    • Analyze blockchain logs
    • Check validator consensus
    • Verify data integrity
  3. Rollback options:

    • Option A (No data loss): Reset validators to last valid block
    • Option B (Start fresh): Wipe blockchain, restart from genesis
  4. Restart:

    • docker compose down
    • Fix code/configuration
    • docker compose up -d --build
    • Resume voting