diff --git a/.gitignore b/.gitignore index 6292b6c..0bc410c 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,8 @@ data/ node_output.log server_output*.log qbtc.log +/validator-rocks-db/ +/liboqs/ # Docker docker-compose.override.yml diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index d88d28a..13c5bd5 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -40,6 +40,7 @@ This creates a `wallet.json` file with your ML-DSA keypair. Keep it safe! ### Scenario 1: Local Test Network (3 Nodes) Perfect for development and testing. Creates a complete network with: + - 1 Bootstrap node - 2 Validator nodes - Full monitoring stack (Prometheus + Grafana) @@ -138,12 +139,14 @@ docker compose -f docker-compose.validator.yml logs -f validator ### Environment Variables #### Common Variables + - `WALLET_PASSWORD` - Password for the wallet file (required) - `WALLET_FILE` - Wallet filename (default: varies by node type) - `ADMIN_ADDRESS` - Admin address for security features - `USE_REDIS` - Enable Redis caching (default: true) #### Security Configuration + - `RATE_LIMIT_ENABLED` - Enable rate limiting (default: false) - `DDOS_PROTECTION_ENABLED` - Enable DDoS protection (default: true for production) - `ATTACK_PATTERN_DETECTION` - Detect attack patterns (default: true for production) @@ -152,6 +155,7 @@ docker compose -f docker-compose.validator.yml logs -f validator - `SECURITY_LOGGING_ENABLED` - Enable security logging (default: true) #### Monitoring Configuration + - `GRAFANA_ADMIN_USER` - Grafana admin username (default: admin) - `GRAFANA_ADMIN_PASSWORD` - Grafana admin password (required for production) - `GRAFANA_DOMAIN` - Domain for Grafana (required for production) @@ -180,6 +184,7 @@ python3 main.py --external-ip YOUR_PUBLIC_IP ### Grafana Dashboards All deployments include pre-configured Grafana dashboards: + - **Network Overview** - Peer connections, blockchain height, sync status - **Performance Metrics** - CPU, memory, disk usage - **Transaction Flow** - Mempool size, transaction throughput @@ -188,6 +193,7 @@ All deployments include pre-configured Grafana dashboards: ### Prometheus Metrics Key metrics exposed: + - `qbtc_connected_peers_total` - Number of connected peers - `qbtc_blockchain_height` - Current blockchain height - `qbtc_pending_transactions` - Mempool size @@ -245,16 +251,19 @@ docker compose -f docker-compose.SCENARIO.yml logs -f SERVICE_NAME ### Connection Issues 1. Check if bootstrap is reachable: + ```bash curl http://api.bitcoinqs.org:8080/health ``` 2. Verify ports are open: + ```bash netstat -tuln | grep -E "8001|8002|8080|8332" ``` 3. Check Docker network: + ```bash docker network ls docker network inspect qbtc-core_qbtc-network @@ -263,11 +272,13 @@ docker network inspect qbtc-core_qbtc-network ### Wallet Issues 1. Verify wallet file exists: + ```bash ls -la wallet.json ``` 2. Check wallet password in environment: + ```bash docker compose -f docker-compose.SCENARIO.yml config | grep WALLET ``` @@ -275,22 +286,26 @@ docker compose -f docker-compose.SCENARIO.yml config | grep WALLET ### Performance Issues 1. Check resource usage: + ```bash docker stats ``` 2. View metrics in Grafana: + - http://localhost:3000 (test network) - https://your-domain.com/grafana/ (production) ## Security Best Practices 1. **Wallet Security** + - Use strong passwords - Backup wallet files securely - Never commit wallets to git 2. **Network Security** + - Use firewalls to limit port access - Enable SSL for production - Regularly update Docker images @@ -305,6 +320,7 @@ docker stats ### Custom Network Configuration Create a `.env` file: + ```bash # Network settings BOOTSTRAP_SERVER=api.bitcoinqs.org @@ -324,6 +340,7 @@ MEMORY_LIMIT=2G ### Multi-Region Deployment For geo-distributed networks: + 1. Deploy bootstrap servers in multiple regions 2. Use external IPs for NAT traversal 3. Configure monitoring aggregation @@ -332,4 +349,4 @@ For geo-distributed networks: - GitHub Issues: https://github.com/q-btc/qBTC-core/issues - Documentation: https://qb.tc/docs -- Community: Discord/Telegram links \ No newline at end of file +- Community: Discord/Telegram links diff --git a/Dockerfile b/Dockerfile index 4e3782b..5e7cc2e 100755 --- a/Dockerfile +++ b/Dockerfile @@ -64,6 +64,9 @@ COPY . . # ──────────────────────────────────────────────────────────────────────────────── ENV WALLET_PASSWORD=your_wallet_password ENV ROCKSDB_PATH=/app/db +ENV RPC_PORT=8332 +ENV GRAFANA=true +ENV GRAFANA_PORT=443 EXPOSE 8080/tcp 8332/tcp 8001/udp 8002/tcp @@ -74,4 +77,4 @@ EXPOSE 8080/tcp 8332/tcp 8001/udp 8002/tcp COPY docker-entrypoint.sh /app/docker-entrypoint.sh RUN chmod +x /app/docker-entrypoint.sh -ENTRYPOINT ["/app/docker-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/README.md b/README.md index 7f675d4..78b7d4a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Built from the ground up in Python to demonstrate a proof-of-concept, qBTC introduces key innovations for the future of Bitcoin: -- **Post-Quantum Security** using the ML-DSA signature scheme -- **Decentralized validator discovery** via a Kademlia DHT -- **Fast, scalable propagation** through an asynchronous gossip network +- **Post-Quantum Security** using the ML-DSA signature scheme +- **Decentralized validator discovery** via a Kademlia DHT +- **Fast, scalable propagation** through an asynchronous gossip network The cryptographic layer is modular, allowing ML-DSA to be replaced with other post-quantum algorithms as standards evolve. @@ -109,6 +109,7 @@ usage: main.py [-h] [--bootstrap] [--bootstrap_server BOOTSTRAP_SERVER] ``` Optional arguments: + - `--bootstrap`: Run as bootstrap server - `--bootstrap_server`: Bootstrap server host (default: api.bitcoinqs.org) - `--bootstrap_port`: Bootstrap server port (default: 8001) @@ -259,6 +260,7 @@ docker compose -f docker-compose.validator.yml logs -f validator ### Key Docker Features All configurations include: + - **Automatic wallet generation** with secure passwords - **Redis** for caching and rate limiting - **Prometheus** metrics collection @@ -270,6 +272,7 @@ All configurations include: - **Security hardening** (no-new-privileges, read-only filesystems where possible) Production configurations additionally include: + - **SSL/TLS encryption** via nginx - **DDoS protection** and attack detection - **Rate limiting** on API endpoints @@ -285,6 +288,7 @@ You can simulate multiple validators by launching separate containers or Python ### Docker Multi-Node Network The test network (`docker-compose.test.yml`) creates: + - 1 Bootstrap node (internal port 8080, accessed via nginx on 8080) - 2 Validator nodes (ports 8081, 8082) - Prometheus monitoring (port 9090) @@ -298,21 +302,21 @@ All nodes automatically discover each other and maintain peer connections. ## 📜 Core Components -| Component | Description | -|---------------------|--------------------------------------------------| -| `main.py` | Entry point - starts web/RPC servers | -| `blockchain/` | Block, transaction, UTXO, Merkle logic | -| `chain_manager.py` | Manages blockchain state and fork resolution | -| `dht/` | Kademlia-based peer discovery | -| `gossip/` | Gossip protocol for block/tx propagation | -| `web/` | FastAPI web server with API endpoints | -| `rpc/` | Bitcoin-compatible RPC for mining | -| `wallet/` | Post-quantum key management (ML-DSA) | -| `database/` | RocksDB storage layer | -| `monitoring/` | Health checks and Prometheus metrics | -| `events/` | Event bus for internal communication | -| `security/` | Rate limiting and DDoS protection | -| `mempool/` | Transaction pool with conflict detection | +| Component | Description | +| ------------------ | -------------------------------------------- | +| `main.py` | Entry point - starts web/RPC servers | +| `blockchain/` | Block, transaction, UTXO, Merkle logic | +| `chain_manager.py` | Manages blockchain state and fork resolution | +| `dht/` | Kademlia-based peer discovery | +| `gossip/` | Gossip protocol for block/tx propagation | +| `web/` | FastAPI web server with API endpoints | +| `rpc/` | Bitcoin-compatible RPC for mining | +| `wallet/` | Post-quantum key management (ML-DSA) | +| `database/` | RocksDB storage layer | +| `monitoring/` | Health checks and Prometheus metrics | +| `events/` | Event bus for internal communication | +| `security/` | Rate limiting and DDoS protection | +| `mempool/` | Transaction pool with conflict detection | --- @@ -331,6 +335,7 @@ python3 broadcast_tx_test_harness.py \ ``` This sends 500 qBTC to the specified address using your signed wallet. The transaction includes: + - **Chain ID** for replay protection - **Timestamp** for transaction expiration - **ML-DSA signature** for post-quantum security @@ -348,6 +353,7 @@ docker run --rm -it cpuminer-opt \ ``` The RPC server automatically: + - Includes pending transactions from the mempool - Creates proper coinbase transactions with fees - Broadcasts mined blocks to all peers via gossip @@ -368,6 +374,7 @@ The RPC server automatically: ### Prometheus Metrics (http://localhost:9090) Key metrics include: + - `qbtc_connected_peers_total` - Number of connected peers - `qbtc_blockchain_height` - Current blockchain height - `qbtc_pending_transactions` - Mempool size @@ -377,6 +384,7 @@ Key metrics include: ### Grafana Dashboards (http://localhost:3000) Pre-configured dashboards show: + - Network topology and peer connections - Blockchain growth and sync status - Transaction throughput @@ -405,6 +413,7 @@ Internal and external audits can be found in the `audits/` folder. We are active ### Peer Discovery Nodes use Kademlia DHT for decentralized peer discovery: + 1. Bootstrap nodes maintain the DHT network 2. New nodes query the DHT for active validators 3. Validators announce their presence with gossip endpoints @@ -413,6 +422,7 @@ Nodes use Kademlia DHT for decentralized peer discovery: ### Block & Transaction Propagation The gossip protocol ensures fast network-wide propagation: + 1. Transactions are broadcast to all connected peers 2. Blocks are propagated immediately upon mining 3. Nodes sync missing blocks automatically @@ -430,6 +440,7 @@ The gossip protocol ensures fast network-wide propagation: ## 📈 Roadmap ### Completed ✅ + - Merkle Root validation - Gossip protocol implementation - Kademlia DHT integration @@ -444,12 +455,14 @@ The gossip protocol ensures fast network-wide propagation: - Event-driven architecture ### In Progress 🚧 + - TLS encryption for all connections - Peer authentication with ML-DSA - Advanced fork choice rules - State pruning optimizations ### Planned 📋 + - Fee market implementation - Smart contract support - Light client protocol @@ -468,11 +481,11 @@ MIT License. See [LICENSE](./LICENSE) for more information. PRs and issues welcome! To contribute: -1. Fork the repo -2. Create your feature branch (`git checkout -b feature/foo`) -3. Commit your changes -4. Push to the branch -5. Open a PR +1. Fork the repo +2. Create your feature branch (`git checkout -b feature/foo`) +3. Commit your changes +4. Push to the branch +5. Open a PR ### Development Tips diff --git a/blockchain/event_integration.py b/blockchain/event_integration.py index a3405a4..44ff437 100644 --- a/blockchain/event_integration.py +++ b/blockchain/event_integration.py @@ -5,7 +5,7 @@ import json import logging import asyncio -from typing import Dict, Any +from typing import Dict, Any, Optional from database.database import get_db from events.event_bus import event_bus, EventTypes @@ -13,35 +13,41 @@ logger = logging.getLogger(__name__) -async def emit_transaction_event(txid: str, transaction: Dict[str, Any], confirmed: bool = False): +async def emit_transaction_event(txid: str, transaction: Dict[str, Any], confirmed: bool = False, block_height: Optional[int] = None): """Emit transaction-related events""" try: # Emit transaction event event_type = EventTypes.TRANSACTION_CONFIRMED if confirmed else EventTypes.TRANSACTION_PENDING - - await event_bus.emit(event_type, { + + event_data = { 'txid': txid, 'inputs': transaction.get('inputs', []), 'outputs': transaction.get('outputs', []), 'timestamp': transaction.get('timestamp'), 'body': transaction.get('body', {}) - }, source='blockchain') - + } + + # Include block height if available and transaction is confirmed + if confirmed and block_height is not None: + event_data['blockHeight'] = block_height + + await event_bus.emit(event_type, event_data, source='blockchain') + # Collect affected wallets affected_wallets = set() - + # From inputs (spending wallets) for inp in transaction.get('inputs', []): if inp.get('receiver'): # The receiver of the UTXO is spending it affected_wallets.add(inp['receiver']) - + # From outputs (receiving wallets) for output in transaction.get('outputs', []): if output.get('receiver'): affected_wallets.add(output['receiver']) if output.get('sender'): affected_wallets.add(output['sender']) - + # Emit wallet balance change events for wallet in affected_wallets: await event_bus.emit(EventTypes.WALLET_BALANCE_CHANGED, { @@ -49,9 +55,9 @@ async def emit_transaction_event(txid: str, transaction: Dict[str, Any], confirm 'txid': txid, 'reason': 'transaction' }, source='blockchain') - + logger.info(f"Emitted events for transaction {txid}, affected wallets: {affected_wallets}") - + except Exception as e: logger.error(f"Error emitting transaction event: {e}") @@ -66,9 +72,9 @@ async def emit_block_event(block_height: int, block_data: Dict[str, Any]): 'tx_ids': block_data.get('tx_ids', []), 'miner': block_data.get('miner') }, source='blockchain') - + logger.info(f"Emitted block event for height {block_height}") - + except Exception as e: logger.error(f"Error emitting block event: {e}") @@ -77,7 +83,7 @@ async def emit_utxo_event(utxo_key: str, utxo_data: Dict[str, Any], spent: bool """Emit UTXO-related events""" try: event_type = EventTypes.UTXO_SPENT if spent else EventTypes.UTXO_CREATED - + await event_bus.emit(event_type, { 'utxo_key': utxo_key, 'txid': utxo_data.get('txid'), @@ -87,7 +93,7 @@ async def emit_utxo_event(utxo_key: str, utxo_data: Dict[str, Any], spent: bool 'amount': utxo_data.get('amount'), 'spent': spent }, source='blockchain') - + # Emit wallet balance change wallet = utxo_data.get('receiver') if wallet: @@ -96,31 +102,31 @@ async def emit_utxo_event(utxo_key: str, utxo_data: Dict[str, Any], spent: bool 'utxo_key': utxo_key, 'reason': 'utxo_spent' if spent else 'utxo_created' }, source='blockchain') - + logger.debug(f"Emitted UTXO event for {utxo_key}, spent={spent}") - + except Exception as e: logger.error(f"Error emitting UTXO event: {e}") class EventEmittingDatabase: """Wrapper for database that emits events on write operations""" - + def __init__(self, db): self._db = db - + def __getattr__(self, name): """Delegate all other attributes to the underlying database""" return getattr(self._db, name) - + def put(self, key: bytes, value: bytes): """Wrapped put operation that emits events""" # Call original put self._db.put(key, value) - + # Decode key to determine type key_str = key.decode('utf-8') - + # Handle different key types if key_str.startswith('tx:'): # Transaction was stored @@ -131,7 +137,7 @@ def put(self, key: bytes, value: bytes): asyncio.create_task(emit_transaction_event(txid, tx_data, confirmed=True)) except Exception as e: logger.error(f"Error emitting event for transaction {txid}: {e}") - + elif key_str.startswith('block:'): # Block was stored - key format is block:{hash} block_hash = key_str[6:] @@ -145,7 +151,7 @@ def put(self, key: bytes, value: bytes): logger.error(f"Block {block_hash} missing height field") except Exception as e: logger.error(f"Error emitting event for block {block_hash}: {e}") - + elif key_str.startswith('utxo:'): # UTXO was stored/updated utxo_key = key_str[5:] @@ -161,14 +167,19 @@ def put(self, key: bytes, value: bytes): _event_db_wrapper = None -def emit_database_event(key: bytes, value: bytes): +def emit_database_event(key: bytes, value: bytes, block_height: Optional[int] = None): """ Emit events based on database operations. This should be called after any database put operation. + + Args: + key: Database key + value: Database value + block_height: Optional block height for confirmed transactions """ # Decode key to determine type key_str = key.decode('utf-8') - + # Handle different key types if key_str.startswith('tx:'): # Transaction was stored @@ -176,10 +187,10 @@ def emit_database_event(key: bytes, value: bytes): try: tx_data = json.loads(value.decode()) # Use asyncio to run the async emit function - asyncio.create_task(emit_transaction_event(txid, tx_data, confirmed=True)) + asyncio.create_task(emit_transaction_event(txid, tx_data, confirmed=True, block_height=block_height)) except Exception as e: logger.error(f"Error emitting event for transaction {txid}: {e}") - + elif key_str.startswith('block:'): # Block was stored - key format is block:{hash} block_hash = key_str[6:] @@ -193,7 +204,7 @@ def emit_database_event(key: bytes, value: bytes): logger.error(f"Block {block_hash} missing height field") except Exception as e: logger.error(f"Error emitting event for block {block_hash}: {e}") - + elif key_str.startswith('utxo:'): # UTXO was stored/updated utxo_key = key_str[5:] @@ -210,4 +221,4 @@ def wrap_database_operations(): This function is now a no-op since we can't wrap RocksDB. Instead, we'll need to manually call emit_database_event after put operations. """ - logger.info("Event integration initialized - manual emission required") \ No newline at end of file + logger.info("Event integration initialized - manual emission required") diff --git a/config/config.py b/config/config.py index 21baaa9..aad2592 100755 --- a/config/config.py +++ b/config/config.py @@ -3,7 +3,7 @@ import os VALIDATOR_ID = str(uuid.uuid4())[:8] -ROCKSDB_PATH = os.environ.get("ROCKSDB_PATH", "ledger.rocksdb") +ROCKSDB_PATH = os.environ.get("ROCKSDB_PATH", "ledger.rocksdb") DEFAULT_GOSSIP_PORT = 7002 DHT_PORT = 8001 HEARTBEAT_INTERVAL = 30 @@ -22,3 +22,5 @@ CHAIN_ID = int(os.getenv("CHAIN_ID", "1")) # Transaction expiration time in seconds (default: 1 hour) TX_EXPIRATION_TIME = int(os.getenv("TX_EXPIRATION_TIME", "3600")) +# RPC server port (default: 8332 for Bitcoin compatibility) +RPC_PORT = int(os.getenv("RPC_PORT", "8332")) diff --git a/docker-compose.bootstrap.yml b/docker-compose.bootstrap.yml index e9ed8af..8420022 100644 --- a/docker-compose.bootstrap.yml +++ b/docker-compose.bootstrap.yml @@ -1,5 +1,3 @@ - version: '3.8' - services: # Production Bootstrap node bootstrap: @@ -28,10 +26,15 @@ RATE_LIMIT_HEALTH: "100" RATE_LIMIT_UTXOS: "100" RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8332} + # Monitoring configuration + GRAFANA_ENABLED: ${GRAFANA:-true} + GRAFANA_PORT: ${GRAFANA_PORT:-443} command: ["--bootstrap", "--dht-port", "8001", "--gossip-port", "8002"] ports: # Remove 8080 from here since nginx will handle it - - "8332:8332" # RPC + - "${RPC_PORT:-8332}:8332" # RPC (configurable) - "8001:8001/udp" # DHT UDP - "8002:8002" # Gossip TCP volumes: @@ -94,11 +97,22 @@ depends_on: - bootstrap - # Grafana for visualization - PUBLIC ACCESS + # Grafana for visualization - PUBLIC ACCESS (conditional) grafana: image: grafana/grafana:latest container_name: qbtc-grafana-prod + command: > + sh -c ' + if [ "$${GRAFANA:-true}" = "false" ]; then + echo "Grafana disabled via GRAFANA=false"; + exit 0; + else + exec /run.sh; + fi' environment: + # Grafana control + - GRAFANA=${GRAFANA:-true} + # Admin settings (for backend access only) - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} @@ -136,7 +150,7 @@ - GF_USERS_DEFAULT_PERMISSIONS=Viewer - GF_USERS_VIEWERS_CAN_EDIT=false - GF_EXPLORE_ENABLED=false - + # Performance and chunk loading fixes - GF_DATAPROXY_TIMEOUT=300 - GF_DATAPROXY_KEEP_ALIVE_SECONDS=300 @@ -148,26 +162,27 @@ - ./monitoring/grafana/custom.ini:/etc/grafana/grafana.ini:ro networks: - qbtc-network - restart: unless-stopped + restart: "no" depends_on: - prometheus - # Nginx reverse proxy - Serves API on 8080 and Grafana on 443 + # Nginx reverse proxy - Serves API on 8080 and optionally Grafana nginx: image: nginx:alpine container_name: qbtc-nginx-prod ports: - - "443:443" # Grafana (public dashboard) + - "${GRAFANA_PORT:-443}:443" # Grafana (configurable port) - "80:80" # HTTP redirect to HTTPS - "8080:8080" # API/WebSocket with SSL volumes: - ./monitoring/nginx/nginx-prod-split.conf:/etc/nginx/nginx.conf:ro - ./monitoring/nginx/ssl:/etc/nginx/ssl:ro + environment: + - GRAFANA_ENABLED=${GRAFANA:-true} networks: - qbtc-network restart: unless-stopped depends_on: - - grafana - bootstrap security_opt: - no-new-privileges:true diff --git a/docker-compose.test.yml b/docker-compose.test.yml index d05b688..6f27b9f 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # Bootstrap node - initial network node bootstrap: @@ -28,10 +26,15 @@ services: RATE_LIMIT_HEALTH: "100" RATE_LIMIT_UTXOS: "100" RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8332} + # Monitoring configuration + GRAFANA_ENABLED: ${GRAFANA:-true} + GRAFANA_PORT: ${GRAFANA_PORT:-3000} command: ["--bootstrap", "--dht-port", "8001", "--gossip-port", "8002"] ports: # Remove 8080 from here since nginx will handle it in production mode - - "8332:8332" # RPC + - "${RPC_PORT:-8332}:8332" # RPC (configurable) - "8001:8001/udp" # DHT UDP - "8002:8002" # Gossip TCP volumes: @@ -85,10 +88,15 @@ services: RATE_LIMIT_HEALTH: "100" RATE_LIMIT_UTXOS: "100" RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8333} + # Monitoring configuration + GRAFANA_ENABLED: ${GRAFANA:-true} + GRAFANA_PORT: ${GRAFANA_PORT:-3000} command: ["--bootstrap_server", "bootstrap", "--bootstrap_port", "8001", "--dht-port", "8003", "--gossip-port", "8004"] ports: - "8081:8080" - - "8333:8332" + - "${RPC_PORT:-8333}:8332" # RPC (configurable) - "8003:8003/udp" - "8004:8004" volumes: @@ -143,10 +151,15 @@ services: RATE_LIMIT_HEALTH: "100" RATE_LIMIT_UTXOS: "100" RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8334} + # Monitoring configuration + GRAFANA_ENABLED: ${GRAFANA:-true} + GRAFANA_PORT: ${GRAFANA_PORT:-3000} command: ["--bootstrap_server", "bootstrap", "--bootstrap_port", "8001", "--dht-port", "8005", "--gossip-port", "8006"] ports: - "8082:8080" - - "8334:8332" + - "${RPC_PORT:-8334}:8332" # RPC (configurable) - "8005:8005/udp" - "8006:8006" volumes: @@ -214,36 +227,47 @@ services: - validator1 - validator2 - # Grafana for visualization - TEST MODE (full access) + # Grafana for visualization - TEST MODE (full access) - Conditional grafana: image: grafana/grafana:latest container_name: qbtc-grafana + command: > + sh -c ' + if [ "$${GRAFANA:-true}" = "false" ]; then + echo "Grafana disabled via GRAFANA=false"; + exit 0; + else + exec /run.sh; + fi' ports: - - "3000:3000" + - "${GRAFANA_PORT:-3000}:3000" # Grafana (configurable port) environment: + # Grafana control + - GRAFANA=${GRAFANA:-true} + # Admin settings - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin123} - + # Test mode - full access - GF_AUTH_ANONYMOUS_ENABLED=false - GF_USERS_ALLOW_SIGN_UP=false - GF_USERS_ALLOW_ORG_CREATE=false - GF_AUTH_DISABLE_LOGIN_FORM=false - + # Security settings - GF_SECURITY_DISABLE_GRAVATAR=true - GF_SECURITY_COOKIE_SECURE=false # false for test mode - GF_SECURITY_STRICT_TRANSPORT_SECURITY=false # false for test mode - + # Dashboard settings - GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH=/var/lib/grafana/dashboards/qbtc-overview.json - + # Enable editing for test mode - GF_USERS_DEFAULT_PERMISSIONS=Editor - GF_USERS_VIEWERS_CAN_EDIT=true - GF_EXPLORE_ENABLED=true - + # Performance and chunk loading fixes - GF_DATAPROXY_TIMEOUT=300 - GF_DATAPROXY_KEEP_ALIVE_SECONDS=300 @@ -255,7 +279,7 @@ services: - ./monitoring/grafana/custom.ini:/etc/grafana/grafana.ini:ro networks: - qbtc-network - restart: unless-stopped + restart: "no" depends_on: - prometheus @@ -290,4 +314,4 @@ networks: driver: bridge ipam: config: - - subnet: 172.20.0.0/16 \ No newline at end of file + - subnet: 172.20.0.0/16 diff --git a/docker-compose.validator-testing.yml b/docker-compose.validator-testing.yml new file mode 100644 index 0000000..a20fc0a --- /dev/null +++ b/docker-compose.validator-testing.yml @@ -0,0 +1,160 @@ +services: + # Validator node connecting to mainnet + validator: + build: . + container_name: qbtc-validator + environment: + WALLET_PASSWORD: ${VALIDATOR_WALLET_PASSWORD} + WALLET_FILE: ${VALIDATOR_WALLET_FILE:-validator.json} + DISABLE_NAT_TRAVERSAL: "false" + ROCKSDB_PATH: "/app/db" + # Production security configuration + RATE_LIMIT_ENABLED: "false" + DDOS_PROTECTION_ENABLED: "true" + ADMIN_ADDRESS: ${ADMIN_ADDRESS} + ATTACK_PATTERN_DETECTION: "true" + BOT_DETECTION_ENABLED: "true" + PEER_REPUTATION_ENABLED: "true" + SECURITY_LOGGING_ENABLED: "true" + # Redis configuration + USE_REDIS: "true" + REDIS_URL: "redis://redis:6379/0" + # Rate limits (requests per minute) + RATE_LIMIT_WORKER: "10" + RATE_LIMIT_BALANCE: "100" + RATE_LIMIT_TRANSACTIONS: "100" + RATE_LIMIT_HEALTH: "100" + RATE_LIMIT_UTXOS: "100" + RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8332} + command: ["--bootstrap_server", "${BOOTSTRAP_SERVER:-api.bitcoinqs.org}", "--bootstrap_port", "${BOOTSTRAP_PORT:-8001}", "--dht-port", "8001", "--gossip-port", "8002"] + ports: + # Remove 8080 from here since nginx will handle it + - "${RPC_PORT:-8332}:8332" # RPC (configurable) + - "8001:8001/udp" # DHT UDP + - "8002:8002" # Gossip TCP + volumes: + - validator-data:/app/db + - ./logs:/var/log/qbtc + depends_on: + - redis + networks: + - qbtc-network + restart: unless-stopped + security_opt: + - no-new-privileges:true + deploy: + resources: + limits: + cpus: '1.0' + memory: 2G + reservations: + cpus: '0.5' + memory: 512M + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + + # Redis for rate limiting and caching + redis: + image: redis:7-alpine + container_name: qbtc-redis-validator + volumes: + - redis-data:/data + - ./config/redis.conf:/usr/local/etc/redis/redis.conf:ro + command: redis-server /usr/local/etc/redis/redis.conf + networks: + - qbtc-network + restart: unless-stopped + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp:noexec,nosuid,size=50m + + # Prometheus for metrics collection + prometheus: + image: prom/prometheus:latest + container_name: qbtc-prometheus-validator + volumes: + - ./monitoring/prometheus-validator.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=720h' # 30 days retention + - '--web.enable-lifecycle' + - '--web.listen-address=:9090' + networks: + - qbtc-network + restart: unless-stopped + depends_on: + - validator + + # Nginx reverse proxy - Serves both API on 8080 and Grafana on 8443 (when GRAFANA=true) + nginx: + image: nginx:alpine + container_name: qbtc-nginx-validator + ports: + - "8080:8080" # API/WebSocket (HTTP) + - "8443:80" # Grafana (HTTP) + volumes: + - ./monitoring/nginx/nginx-validator-nossl.conf:/etc/nginx/nginx.conf:ro + networks: + - qbtc-network + restart: unless-stopped + depends_on: + - validator + security_opt: + - no-new-privileges:true + + # Grafana for visualization (only started when GRAFANA=true) + grafana: + image: grafana/grafana:latest + container_name: qbtc-grafana-validator + environment: + # Grafana configuration + - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin} + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin} + - GF_SERVER_ROOT_URL=http://${GRAFANA_DOMAIN:-localhost}:8443 + - GF_SERVER_SERVE_FROM_SUB_PATH=false + - GF_ANALYTICS_REPORTING_ENABLED=false + - GF_INSTALL_PLUGINS= + # Security settings + - GF_SECURITY_COOKIE_SECURE=false # No SSL + - GF_SECURITY_STRICT_TRANSPORT_SECURITY=false # No HSTS + - GF_USERS_ALLOW_SIGN_UP=false + - GF_AUTH_ANONYMOUS_ENABLED=false + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + - GF_AUTH_BASIC_ENABLED=true + - GF_SECURITY_DISABLE_GRAVATAR=true + - GF_SECURITY_COOKIE_SAMESITE=lax + - GF_SECURITY_CONTENT_SECURITY_POLICY=false + volumes: + - grafana-data:/var/lib/grafana + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + - ./monitoring/grafana/custom.ini:/etc/grafana/grafana.ini:ro + networks: + - qbtc-network + restart: unless-stopped + depends_on: + - prometheus + security_opt: + - no-new-privileges:true + +volumes: + validator-data: + redis-data: + prometheus-data: + grafana-data: + +networks: + qbtc-network: + driver: bridge + ipam: + config: + - subnet: 172.22.0.0/16 diff --git a/docker-compose.validator.yml b/docker-compose.validator.yml index 1fe7638..8a03297 100644 --- a/docker-compose.validator.yml +++ b/docker-compose.validator.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: # Validator node connecting to mainnet validator: @@ -28,10 +26,15 @@ services: RATE_LIMIT_HEALTH: "100" RATE_LIMIT_UTXOS: "100" RATE_LIMIT_DEFAULT: "60" + # RPC configuration + RPC_PORT: ${RPC_PORT:-8332} + # Monitoring configuration + GRAFANA_ENABLED: ${GRAFANA:-true} + GRAFANA_PORT: ${GRAFANA_PORT:-443} command: ["--bootstrap_server", "${BOOTSTRAP_SERVER:-api.bitcoinqs.org}", "--bootstrap_port", "${BOOTSTRAP_PORT:-8001}", "--dht-port", "8001", "--gossip-port", "8002"] ports: # Remove 8080 from here since nginx will handle it - - "8332:8332" # RPC + - "${RPC_PORT:-8332}:8332" # RPC (configurable) - "8001:8001/udp" # DHT UDP - "8002:8002" # Gossip TCP volumes: @@ -94,11 +97,43 @@ services: depends_on: - validator + # Nginx reverse proxy - Serves API on 8080 and Grafana on 8443 + nginx: + image: nginx:alpine + container_name: qbtc-nginx-validator + ports: + - "${GRAFANA_PORT:-443}:443" # Grafana (configurable port) + - "80:80" # HTTP redirect to HTTPS + - "8080:8080" # API/WebSocket with SSL + environment: + - GRAFANA_ENABLED=${GRAFANA:-true} + volumes: + - ./monitoring/nginx/nginx-validator.conf:/etc/nginx/nginx.conf:ro + - ./monitoring/nginx/ssl:/etc/nginx/ssl:ro + networks: + - qbtc-network + restart: unless-stopped + depends_on: + - validator + security_opt: + - no-new-privileges:true + # Grafana for visualization - PUBLIC ACCESS grafana: image: grafana/grafana:latest container_name: qbtc-grafana-validator + command: > + sh -c ' + if [ "$${GRAFANA:-true}" = "false" ]; then + echo "Grafana disabled via GRAFANA=false"; + exit 0; + else + exec /run.sh; + fi' environment: + # Grafana control + - GRAFANA=${GRAFANA:-true} + # Admin settings (for backend access only) - GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} @@ -136,7 +171,7 @@ services: - GF_USERS_DEFAULT_PERMISSIONS=Viewer - GF_USERS_VIEWERS_CAN_EDIT=false - GF_EXPLORE_ENABLED=false - + # Performance and chunk loading fixes - GF_DATAPROXY_TIMEOUT=300 - GF_DATAPROXY_KEEP_ALIVE_SECONDS=300 @@ -148,30 +183,10 @@ services: - ./monitoring/grafana/custom.ini:/etc/grafana/grafana.ini:ro networks: - qbtc-network - restart: unless-stopped + restart: "no" depends_on: - prometheus - # Nginx reverse proxy - Serves API on 8080 and Grafana on 443 - nginx: - image: nginx:alpine - container_name: qbtc-nginx-validator - ports: - - "443:443" # Grafana (public dashboard) - - "80:80" # HTTP redirect to HTTPS - - "8080:8080" # API/WebSocket with SSL - volumes: - - ./monitoring/nginx/nginx-validator.conf:/etc/nginx/nginx.conf:ro - - ./monitoring/nginx/ssl:/etc/nginx/ssl:ro - networks: - - qbtc-network - restart: unless-stopped - depends_on: - - grafana - - validator - security_opt: - - no-new-privileges:true - volumes: validator-data: redis-data: diff --git a/main.py b/main.py index 03ea755..caeda4d 100755 --- a/main.py +++ b/main.py @@ -7,6 +7,7 @@ from rpc.rpc import rpc_app from node.startup import startup, shutdown from log_utils import setup_logging +from config.config import RPC_PORT # Setup structured logging logger = setup_logging( @@ -21,7 +22,7 @@ async def main(args): logger.info(f"Mode: {'bootstrap' if args.bootstrap else 'peer'}") logger.info(f"Args: bootstrap={args.bootstrap}, dht_port={args.dht_port}, gossip_port={args.gossip_port}") logger.info(f"Bootstrap server: {args.bootstrap_server}:{args.bootstrap_port}") - + try: await startup(args) logger.info("Node startup completed successfully") @@ -42,12 +43,12 @@ async def main(args): config_rpc = uvicorn.Config( rpc_app, host="0.0.0.0", - port=8332, + port=RPC_PORT, log_level="info", access_log=True ) server_rpc = uvicorn.Server(config_rpc) - logger.info("RPC server configured on port 8332") + logger.info(f"RPC server configured on port {RPC_PORT}") try: logger.info("Starting web and RPC servers") @@ -64,7 +65,7 @@ async def main(args): if __name__ == "__main__": parser = argparse.ArgumentParser(description='qBTC Node') - parser.add_argument('--bootstrap', action='store_true', + parser.add_argument('--bootstrap', action='store_true', help='Run as bootstrap server') parser.add_argument('--bootstrap_server', type=str, default='api.bitcoinqs.org', help='Bootstrap server host (default: api.bitcoinqs.org)') @@ -76,9 +77,9 @@ async def main(args): help='Gossip port (default: 8002)') parser.add_argument('--external-ip', type=str, default=None, help='External IP address for NAT traversal') - + args = parser.parse_args() - + try: asyncio.run(main(args)) except KeyboardInterrupt: diff --git a/monitoring/README.md b/monitoring/README.md index ee470db..890fdab 100644 --- a/monitoring/README.md +++ b/monitoring/README.md @@ -5,6 +5,7 @@ This directory contains the monitoring configuration for qBTC nodes using Promet ## Architecture Each qBTC node exposes a `/health` endpoint that provides Prometheus-compatible metrics including: + - Node health status (database, blockchain, network, mempool) - Blockchain height - Connected and synced peer counts @@ -15,6 +16,7 @@ Each qBTC node exposes a `/health` endpoint that provides Prometheus-compatible ## Docker Compose Configurations ### 1. Test Environment (docker-compose.test.yml) + - 3-node test network (1 bootstrap + 2 validators) - Prometheus and Grafana included - Nginx load balancer for API access @@ -23,11 +25,13 @@ Each qBTC node exposes a `/health` endpoint that provides Prometheus-compatible - Suitable for development and testing **Start with:** + ```bash docker compose -f docker-compose.test.yml up -d ``` **Access:** + - Bootstrap API: http://localhost:8080 (via nginx) - Validator 1 API: http://localhost:8081 - Validator 2 API: http://localhost:8082 @@ -36,6 +40,7 @@ docker compose -f docker-compose.test.yml up -d - RPC: localhost:8332, 8333, 8334 ### 2. Production Bootstrap (docker-compose.bootstrap.yml) + - Single bootstrap node with secure monitoring - Nginx reverse proxy with SSL/TLS - Public read-only Grafana dashboards @@ -43,10 +48,12 @@ docker compose -f docker-compose.test.yml up -d - DDoS protection enabled **Requirements:** + - SSL certificates in `monitoring/nginx/ssl/` - Environment variables set **Start with:** + ```bash # Set required environment variables export BOOTSTRAP_WALLET_PASSWORD="your-secure-password" @@ -67,6 +74,7 @@ docker compose -f docker-compose.bootstrap.yml up -d ``` **Access:** + - API: https://localhost:8080 (SSL) - Grafana: https://localhost:443 (public read-only) - Admin Grafana: https://localhost:443 (login as admin) @@ -75,6 +83,7 @@ docker compose -f docker-compose.bootstrap.yml up -d - Gossip: localhost:8002/tcp ### 3. Production Validator (docker-compose.validator.yml) + - Connects to mainnet via api.bitcoinqs.org:8001 - Local monitoring stack - SSL/TLS enabled @@ -82,6 +91,7 @@ docker compose -f docker-compose.bootstrap.yml up -d - DDoS protection enabled **Start with:** + ```bash # Set required environment variables export VALIDATOR_WALLET_PASSWORD="your-secure-password" @@ -106,6 +116,7 @@ BOOTSTRAP_SERVER=your.bootstrap.server BOOTSTRAP_PORT=8001 \ ``` **Access:** + - API: https://localhost:8080 (SSL) - Grafana: https://localhost:443 (public read-only) - Admin Grafana: https://localhost:443 (login as admin) @@ -114,6 +125,7 @@ BOOTSTRAP_SERVER=your.bootstrap.server BOOTSTRAP_PORT=8001 \ ## Grafana Dashboard A pre-configured dashboard (`qbtc-overview.json`) is automatically loaded showing: + - Node health status - Blockchain height over time - Network peer statistics @@ -124,6 +136,7 @@ A pre-configured dashboard (`qbtc-overview.json`) is automatically loaded showin ## Production Security Features Both production configurations (bootstrap and validator) include: + - **Anonymous Access**: Public read-only dashboards - **Admin Access**: Secured admin login for configuration - **SSL/TLS**: All traffic encrypted @@ -134,6 +147,7 @@ Both production configurations (bootstrap and validator) include: ## Prometheus Metrics Available metrics include: + - `qbtc_node_info` - Node information - `qbtc_uptime_seconds` - Node uptime - `qbtc_blockchain_height` - Current blockchain height @@ -149,6 +163,7 @@ Available metrics include: ## Nginx Configuration Production deployments use nginx for: + - SSL/TLS termination - Reverse proxy to API and Grafana - Load balancing (test environment) @@ -156,27 +171,32 @@ Production deployments use nginx for: - Rate limiting (when configured) Configuration files: + - `nginx-test.conf` - Simple load balancer for test environment - `nginx-prod-split.conf` - Production config with SSL and split ports ## Troubleshooting ### Grafana not showing data + 1. Check Prometheus targets at http://localhost:9090/targets 2. Verify nodes are running and `/health` endpoint is accessible 3. Check container logs: `docker compose logs prometheus grafana` ### SSL Certificate Issues + 1. Ensure `server.crt` and `server.key` are in `monitoring/nginx/ssl/` 2. Verify certificate validity and domain match 3. Check nginx logs: `docker compose -f docker-compose.bootstrap.yml logs nginx` ### Authentication Issues + 1. Verify GRAFANA_ADMIN_PASSWORD is set correctly 2. For public dashboards, ensure anonymous access is working 3. Admin login at /login with configured credentials ### Connection Issues + 1. Check if bootstrap is reachable: `curl http://api.bitcoinqs.org:8080/health` 2. Verify Docker network: `docker network inspect qbtc-core_qbtc-network` 3. Check firewall rules for required ports @@ -187,4 +207,4 @@ Configuration files: 2. **Alert Configuration**: Set up alerts in Grafana for critical metrics 3. **Log Aggregation**: Consider adding log aggregation for production 4. **Backup**: Regularly backup Prometheus data and Grafana configurations -5. **Updates**: Keep Prometheus and Grafana images updated for security \ No newline at end of file +5. **Updates**: Keep Prometheus and Grafana images updated for security diff --git a/monitoring/nginx/nginx-prod-split.conf b/monitoring/nginx/nginx-prod-split.conf index f69fb6e..d2d74f3 100644 --- a/monitoring/nginx/nginx-prod-split.conf +++ b/monitoring/nginx/nginx-prod-split.conf @@ -21,7 +21,7 @@ http { limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s; limit_req_zone $binary_remote_addr zone=grafana_limit:10m rate=100r/s; limit_req_status 429; - + # Whitelist for Grafana internal requests map $http_x_forwarded_for $limit_grafana { default 1; @@ -39,6 +39,12 @@ http { ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; + # Upstream for Grafana with fallback handling + upstream grafana_backend { + server grafana:3000 max_fails=1 fail_timeout=5s; + # If grafana is down, return 503 + } + # Port 8080 - API and WebSocket with SSL server { listen 8080 ssl; @@ -62,12 +68,12 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # WebSocket specific timeouts proxy_connect_timeout 7d; proxy_send_timeout 7d; proxy_read_timeout 7d; - + # Disable buffering for WebSocket proxy_buffering off; } @@ -79,10 +85,10 @@ http { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # DON'T add CORS headers here - let the backend handle them # This prevents duplicate headers - + # Handle preflight requests if ($request_method = OPTIONS) { # Pass OPTIONS requests to backend @@ -91,7 +97,7 @@ http { } } - # Port 443 - Grafana Dashboard (public, view-only) + # Port 443 - Grafana Dashboard (public, view-only) - Conditional server { listen 443 ssl default_server; http2 on; @@ -100,29 +106,77 @@ http { ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; - # Grafana static assets with no rate limiting + # Health check for Grafana availability + location = /grafana-health { + internal; + proxy_pass http://grafana_backend/api/health; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + } + + # Error page for when Grafana is disabled + location = /grafana-disabled { + internal; + return 503 '{"error": "Grafana monitoring is disabled", "status": 503, "message": "Set GRAFANA=true to enable monitoring dashboard"}'; + add_header Content-Type application/json; + } + + # Try Grafana first, fallback to disabled message + location / { + # Try to reach Grafana health endpoint + auth_request /grafana-health; + + # If Grafana is available, proxy to it + proxy_pass http://grafana_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support for live updates + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Increased timeouts + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + # Increased buffer sizes + proxy_buffer_size 64k; + proxy_buffers 8 128k; + proxy_busy_buffers_size 256k; + + # Fallback to disabled page on any error + error_page 502 503 504 = @grafana_unavailable; + } + + # Grafana static assets with no rate limiting (when available) location ~* ^/(public|api/frontend|api/datasources|api/plugins)/.+\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map|json)$ { - # Disable rate limiting for static assets - no limit_req directive means no rate limiting - - proxy_pass http://grafana:3000; + # Try Grafana health first + auth_request /grafana-health; + + proxy_pass http://grafana_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # Aggressive caching for static assets proxy_cache_valid 200 302 1h; proxy_cache_valid 404 1m; - + # Cache headers add_header Cache-Control "public, max-age=3600, immutable"; - + # CORS headers for chunk loading add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Methods "GET, OPTIONS" always; add_header Access-Control-Allow-Headers "*" always; add_header Access-Control-Max-Age "3600" always; - + # Handle preflight if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Origin "*" always; @@ -133,73 +187,64 @@ http { add_header Content-Type text/plain; return 204; } - + # Increased buffer sizes for large chunks proxy_buffer_size 256k; proxy_buffers 32 512k; proxy_busy_buffers_size 1m; proxy_temp_file_write_size 1m; - + # Timeouts proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; + + # Fallback to disabled page on any error + error_page 502 503 504 = @grafana_unavailable; } - - # API endpoints with rate limiting + + # API endpoints with rate limiting (when available) location ~* ^/api/(dashboards|annotations|search|query|tsdb|ds/query|alerts|alert-notifications|user|org|users|orgs|teams|admin|playlists|dashboard|live) { + # Try Grafana health first + auth_request /grafana-health; + # Apply rate limiting to API endpoints limit_req zone=grafana_limit burst=50 nodelay; limit_req_status 429; - - proxy_pass http://grafana:3000; + + proxy_pass http://grafana_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; - + # Increased buffer sizes proxy_buffer_size 128k; proxy_buffers 16 256k; proxy_busy_buffers_size 512k; - + # Timeouts proxy_connect_timeout 300s; proxy_send_timeout 300s; proxy_read_timeout 300s; - } - - # Allow all Grafana paths - location / { - proxy_pass http://grafana:3000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # WebSocket support for live updates - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - - # Increased timeouts - proxy_connect_timeout 300s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - - # Increased buffer sizes - proxy_buffer_size 64k; - proxy_buffers 8 128k; - proxy_busy_buffers_size 256k; + + # Fallback to disabled page on any error + error_page 502 503 504 = @grafana_unavailable; } - # Only block specific admin endpoints + # Block specific admin endpoints location ~ ^/(admin|profile/password) { return 403; } + + # Fallback handler for when Grafana is unavailable + location @grafana_unavailable { + return 503 '{"error": "Grafana monitoring is disabled or unavailable", "status": 503, "message": "Set GRAFANA=true and ensure Grafana service is running to access monitoring dashboard", "api_url": "https://$host:8080/health"}'; + add_header Content-Type application/json; + } } - # Redirect port 80 to HTTPS Grafana + # Redirect port 80 to HTTPS Grafana (or show disabled message) server { listen 80; server_name _; diff --git a/monitoring/nginx/nginx-validator-nossl.conf b/monitoring/nginx/nginx-validator-nossl.conf new file mode 100644 index 0000000..f6950af --- /dev/null +++ b/monitoring/nginx/nginx-validator-nossl.conf @@ -0,0 +1,119 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Security headers (removed HSTS since we're not using SSL) + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s; + limit_req_zone $binary_remote_addr zone=grafana_limit:10m rate=10r/s; + limit_req_status 429; + + # Hide nginx version + server_tokens off; + + # Upstream for qBTC API + upstream qbtc_api { + server validator:8332; + } + + # Upstream for Grafana (with fallback) + upstream grafana { + server grafana:3000 max_fails=3 fail_timeout=30s; + } + + # Port 80 - Grafana (HTTP only, no redirect) + server { + listen 80; + server_name _; + + # Rate limiting for Grafana + limit_req zone=grafana_limit burst=20 nodelay; + + location / { + proxy_pass http://grafana; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket support for Grafana Live + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Error handling + error_page 502 503 504 @grafana_error; + } + + location @grafana_error { + default_type application/json; + return 503 '{"error": "Grafana service unavailable"}'; + } + } + + # Port 8080 - API and WebSocket (HTTP only) + server { + listen 8080; + server_name _; + + # Rate limiting for API + limit_req zone=api_limit burst=40 nodelay; + + # API endpoints + location /api/ { + proxy_pass http://qbtc_api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + } + + # WebSocket endpoint + location /ws { + proxy_pass http://qbtc_api/ws; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } + + # Health check + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Default handler + location / { + return 404; + } + } + + # Logging + access_log /var/log/nginx/access.log; + + # Compression + gzip on; + gzip_vary on; + gzip_types text/plain text/css text/xml text/javascript application/json application/javascript application/xml+rss; + gzip_min_length 1000; +} diff --git a/sync/sync.py b/sync/sync.py index 95eca49..ac80e75 100755 --- a/sync/sync.py +++ b/sync/sync.py @@ -23,20 +23,20 @@ def _cleanup_mempool_after_sync(blocks: list[dict]): tx_ids = block.get("tx_ids", []) if len(tx_ids) > 1: # Skip coinbase all_tx_ids.update(tx_ids[1:]) - + if not all_tx_ids: return - + removed_count = 0 for txid in all_tx_ids: if mempool_manager.get_transaction(txid) is not None: mempool_manager.remove_transaction(txid) removed_count += 1 logging.debug(f"[SYNC] Removed synced transaction {txid} from mempool") - + if removed_count > 0: logging.info(f"[SYNC] Post-sync cleanup: removed {removed_count} mined transactions from mempool") - + except Exception as e: logging.error(f"Error during mempool cleanup: {e}", exc_info=True) @@ -48,10 +48,10 @@ def process_blocks_from_peer(blocks: list[dict]): global _processing_count _processing_count += 1 call_id = _processing_count - + logging.info(f"***** IN GOSSIP MSG RECEIVE BLOCKS RESPONSE (call #{call_id})") logging.info(f"Call #{call_id}: Processing {len(blocks)} blocks") - + # Wrap entire function to catch any error try: result = _process_blocks_from_peer_impl(blocks) @@ -64,7 +64,7 @@ def process_blocks_from_peer(blocks: list[dict]): def _process_blocks_from_peer_impl(blocks: list[dict]): """Actual implementation of process_blocks_from_peer""" - + # Debug logging to understand block structure logging.info(f"_process_blocks_from_peer_impl called with {len(blocks) if isinstance(blocks, list) else 'non-list'} blocks") if blocks and isinstance(blocks, list): @@ -81,23 +81,23 @@ def _process_blocks_from_peer_impl(blocks: list[dict]): logging.error(f" {k}: {v}") else: logging.error(f" {k}: {type(v)}") - + try: db = get_db() cm = get_chain_manager() raw_blocks = blocks logging.debug(f"[SYNC] Processing {len(raw_blocks)} blocks from peer") - + # Enable sync mode for bulk block processing cm.set_sync_mode(True) - + # Log the type and structure for debugging logging.info(f"Received blocks type: {type(blocks)}") if blocks and len(blocks) > 0: logging.info(f"First block type: {type(blocks[0])}") logging.info(f"First block keys: {list(blocks[0].keys()) if isinstance(blocks[0], dict) else 'Not a dict'}") - + if isinstance(raw_blocks, dict): raw_blocks = [raw_blocks] @@ -115,32 +115,32 @@ def get_height(block): return -1 # Use -1 to sort invalid blocks first else: return 0 - + # Filter out malformed blocks before processing valid_blocks = [] for block in raw_blocks: if not isinstance(block, dict): logging.error(f"Skipping non-dict block: {type(block)}") continue - + height = block.get("height") block_hash = block.get("block_hash") - + # Check if height looks like a block hash if isinstance(height, str) and len(height) == 64 and all(c in '0123456789abcdefABCDEF' for c in height): logging.error(f"Skipping malformed block with hash in height field: height={height}, hash={block_hash}") continue - + # Check if block_hash looks valid if not isinstance(block_hash, str) or len(block_hash) != 64: logging.error(f"Skipping block with invalid hash: {block_hash}") continue - + valid_blocks.append(block) - + blocks = sorted(valid_blocks, key=get_height) logging.info("Received %d blocks, %d valid after filtering", len(raw_blocks), len(blocks)) - + # Check for duplicate blocks seen_hashes = set() seen_heights = set() @@ -162,34 +162,34 @@ def get_height(block): accepted_count = 0 rejected_count = 0 - + try: for block in blocks: try: height = block.get("height") block_hash = block.get("block_hash") prev_hash = block.get("previous_hash") - + # Validate height before processing if isinstance(height, str) and len(height) == 64 and all(c in '0123456789abcdefABCDEF' for c in height): logging.error(f"Skipping block with hash in height field: height={height}, block_hash={block_hash}") logging.error(f"Block keys: {list(block.keys())}") rejected_count += 1 continue - + # Log block structure for debugging logging.info(f"Processing block at height {height} (type: {type(height)}) with hash {block_hash}") logging.info(f"Block has bits field: {'bits' in block}") logging.debug(f"Full block structure: {json.dumps(block, indent=2)}") - + # Add full_transactions to block if not present if "full_transactions" not in block: block["full_transactions"] = block.get("full_transactions", []) - + # Let ChainManager handle consensus logging.debug(f"Calling add_block with height={block.get('height')} (type: {type(block.get('height'))})") success, error = cm.add_block(block) - + if success: accepted_count += 1 # Only process if block is in main chain @@ -202,8 +202,8 @@ def get_height(block): else: rejected_count += 1 logging.warning("Block %s rejected: %s", block_hash, error) - - # Even if block is rejected, we should still remove any transactions + + # Even if block is rejected, we should still remove any transactions # from mempool that are in this block (they might be invalid) if "tx_ids" in block and len(block.get("tx_ids", [])) > 1: # Skip coinbase (first transaction) @@ -214,33 +214,33 @@ def get_height(block): mempool_manager.remove_transaction(txid) removed_txids.append(txid) logging.info(f"[SYNC] Removed transaction {txid} from mempool (block rejected)") - + if removed_txids: logging.info(f"[SYNC] Removed {len(removed_txids)} transactions from mempool after rejected block {block_hash}") - + continue - + except Exception as e: logging.error("Error processing block %s: %s", block.get("block_hash", "unknown"), e) - logging.error("Block data at error: height=%s (type: %s), hash=%s", + logging.error("Block data at error: height=%s (type: %s), hash=%s", block.get("height"), type(block.get("height")), block.get("block_hash")) logging.error("Exception type: %s", type(e).__name__) logging.error("Full traceback:", exc_info=True) rejected_count += 1 logging.info("Block processing complete: %d accepted, %d rejected", accepted_count, rejected_count) - + # After initial sync, do a final mempool cleanup # This ensures any transactions that were mined in blocks we just synced are removed if accepted_count > 0: _cleanup_mempool_after_sync(blocks) - + # Check if we need to request more blocks best_tip, best_height = cm.get_best_chain_tip() logging.info("Current best chain height: %d", best_height) - + return accepted_count > 0 - + finally: # Always disable sync mode after processing cm.set_sync_mode(False) @@ -250,10 +250,10 @@ def _process_block_in_chain(block: dict): # Validate input type if not isinstance(block, dict): raise TypeError(f"Expected dict for block, got {type(block)}: {block}") - + db = get_db() batch = WriteBatch() - + height = block.get("height") block_hash = block.get("block_hash") prev_hash = block.get("previous_hash") @@ -265,7 +265,7 @@ def _process_block_in_chain(block: dict): block_merkle_root = block.get("merkle_root") version = block.get("version") bits = block.get("bits") - + # Validate height is a proper integer if isinstance(height, str): try: @@ -280,19 +280,19 @@ def _process_block_in_chain(block: dict): raise ValueError(f"Missing height in block {block_hash}") elif not isinstance(height, int): raise ValueError(f"Height must be an integer, got {type(height)} for block {block_hash}") - + logging.info("[SYNC] Processing confirmed block height %s with hash %s", height, block_hash) logging.info("[SYNC] Block has %d full transactions", len(full_transactions)) - + # Create transaction validator with sync mode enabled (skip time validation) validator = TransactionValidator(db) validator.skip_time_validation = True # CRITICAL: Set this for historical blocks during sync - + # First validate all transactions in the block is_valid, error_msg, total_fees = validator.validate_block_transactions(block) if not is_valid: raise ValueError(f"Block {block_hash} contains invalid transactions: {error_msg}") - + # Track spent UTXOs within this block to prevent double-spending spent_in_block = set() # Store coinbase data for validation after fee calculation @@ -305,11 +305,11 @@ def _process_block_in_chain(block: dict): if "txid" not in tx: raise ValueError(f"Coinbase transaction missing txid in block {height}") coinbase_tx_id = tx["txid"] - + is_valid, error_msg = validator.validate_coinbase_transaction(tx, height, total_fees) if not is_valid: raise ValueError(f"Invalid coinbase transaction: {error_msg}") - + # Store coinbase outputs for later processing coinbase_outputs = [] for idx, output in enumerate(tx.get("outputs", [])): @@ -324,14 +324,14 @@ def _process_block_in_chain(block: dict): "spent": False, } coinbase_outputs.append((output_key, utxo)) - + coinbase_data = { "tx": tx, "tx_id": coinbase_tx_id, "outputs": coinbase_outputs } break - + # Now process all transactions (they've already been validated) for raw in full_transactions: if raw is None: @@ -349,9 +349,9 @@ def _process_block_in_chain(block: dict): if "txid" not in tx: logging.warning(f"[SYNC] Skipping transaction without txid in block {height}") continue - + txid = tx["txid"] - + inputs = tx.get("inputs", []) outputs = tx.get("outputs", []) @@ -364,7 +364,7 @@ def _process_block_in_chain(block: dict): # Handle both formats inp_txid = inp.get('txid') or inp.get('prev_txid') inp_index = inp.get('utxo_index', inp.get('prev_index', 0)) - + spent_key = f"utxo:{inp_txid}:{inp_index}".encode() if spent_key in db: utxo_rec = json.loads(db.get(spent_key).decode()) @@ -390,7 +390,7 @@ def _process_block_in_chain(block: dict): batch.put(f"tx:{coinbase_data['tx_id']}".encode(), json.dumps(coinbase_data['tx']).encode()) for output_key, utxo in coinbase_data['outputs']: batch.put(output_key, json.dumps(utxo).encode()) - + calculated_root = calculate_merkle_root(tx_ids) if calculated_root != block_merkle_root: raise ValueError( @@ -409,27 +409,27 @@ def _process_block_in_chain(block: dict): "version": version, "bits": bits, } - + # Store the block (ChainManager already validated it) block_key = f"block:{block_hash}".encode() batch.put(block_key, json.dumps(block_record).encode()) - + db.write(batch) logging.info("[SYNC] Stored block %s (height %s) successfully", block_hash, height) - + # Update the height index height_index = get_height_index() height_index.add_block_to_index(height, block_hash) - + # Invalidate height cache since we added a new block invalidate_height_cache() - + # Remove transactions from mempool # Skip the first tx_id as it's the coinbase transaction - + # Remove confirmed transactions using mempool manager logging.info(f"[SYNC] Block has tx_ids: {tx_ids}") - + # If tx_ids is empty but we have full_transactions, extract tx_ids from them if not tx_ids and full_transactions: tx_ids = [] @@ -440,9 +440,9 @@ def _process_block_in_chain(block: dict): # Coinbase should have txid, if not it's an error logging.error(f"[SYNC] Coinbase transaction missing txid in block {height}") logging.info(f"[SYNC] Extracted tx_ids from full_transactions: {tx_ids}") - + confirmed_txids = tx_ids[1:] if len(tx_ids) > 1 else [] # Skip coinbase (first transaction) - + # Track which transactions were actually in our mempool before removal confirmed_from_mempool = [] for txid in confirmed_txids: @@ -451,13 +451,13 @@ def _process_block_in_chain(block: dict): logging.debug(f"[SYNC] Transaction {txid} was in our mempool") else: logging.debug(f"[SYNC] Transaction {txid} not in mempool (might be from another node)") - + # Now remove them mempool_manager.remove_confirmed_transactions(confirmed_txids) - + if confirmed_from_mempool: logging.info(f"[SYNC] Removed {len(confirmed_from_mempool)} transactions from mempool after block {block_hash}") - + # Emit confirmation events for transactions that were in mempool for txid in confirmed_from_mempool: # Get transaction data @@ -472,7 +472,7 @@ def _process_block_in_chain(block: dict): sender = output["sender"] if output.get("receiver"): receiver = output["receiver"] - + # Emit transaction confirmed event asyncio.create_task(event_bus.emit(EventTypes.TRANSACTION_CONFIRMED, { 'txid': txid, @@ -486,16 +486,16 @@ def _process_block_in_chain(block: dict): 'blockHeight': height, 'confirmed_from_mempool': True }, source='sync')) - + logging.info(f"[SYNC] Emitted TRANSACTION_CONFIRMED event for {txid}") - + # Emit events for all database operations # Emit transaction events for txid in tx_ids: tx_key = f"tx:{txid}".encode() if tx_key in db: - emit_database_event(tx_key, db.get(tx_key)) - + emit_database_event(tx_key, db.get(tx_key), block_height=height) + # Emit UTXO events - use full_transactions instead of undefined block_transactions for tx in full_transactions: if tx and "txid" in tx: @@ -504,7 +504,7 @@ def _process_block_in_chain(block: dict): out_key = f"utxo:{txid}:{out.get('utxo_index', 0)}".encode() if out_key in db: emit_database_event(out_key, db.get(out_key)) - + # Emit block event emit_database_event(block_key, db.get(block_key)) @@ -512,11 +512,11 @@ def get_blockchain_info() -> Dict: """Get current blockchain information""" cm = get_chain_manager() best_hash, best_height = cm.get_best_chain_tip() - + return { "best_block_hash": best_hash, "height": best_height, "chain_tips": list(cm.chain_tips), "orphan_count": len(cm.orphan_blocks), "index_size": len(cm.block_index) - } \ No newline at end of file + }