From 5773156ce17f5b71d3465988b7398fc30ec3813d Mon Sep 17 00:00:00 2001 From: oib Date: Wed, 11 Feb 2026 21:14:56 +0100 Subject: [PATCH] docs: add structure.md, update files.md, rewrite README for GitHub, add favicon, replace debug prints with logging, remove stale src/ copy and empty dirs --- .gitignore | 11 + README.md | 157 +++++- apps/.service_pids | 1 - .../src/app/routers/exchange.py | 5 +- .../src/app/routers/governance.py | 11 +- .../src/app/services/bitcoin_wallet.py | 19 +- .../src/app/services/blockchain.py | 5 +- .../src/app/services/receipts.py | 5 +- apps/coordinator-api/src/app/storage/db_pg.py | 5 +- docs/files.md | 82 +++- docs/structure.md | 265 ++++++++++ src/aitbc_chain/rpc/router.py | 457 ------------------ src/aitbc_chain/sync/__init__.py | 7 - src/aitbc_chain/sync/cross_site.py | 222 --------- website/favicon.svg | 10 + 15 files changed, 523 insertions(+), 739 deletions(-) delete mode 100644 apps/.service_pids create mode 100644 docs/structure.md delete mode 100644 src/aitbc_chain/rpc/router.py delete mode 100644 src/aitbc_chain/sync/__init__.py delete mode 100644 src/aitbc_chain/sync/cross_site.py create mode 100644 website/favicon.svg diff --git a/.gitignore b/.gitignore index d6b08f96..7cd3ad3e 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,12 @@ pids/ .classpath .settings/ +# =================== +# Runtime / PID files +# =================== +*.pid +apps/.service_pids + # =================== # OS Files # =================== @@ -143,6 +149,11 @@ home/client/client_wallet.json home/genesis_wallet.json home/miner/miner_wallet.json +# =================== +# Stale source copies +# =================== +src/aitbc_chain/ + # =================== # Project Specific # =================== diff --git a/README.md b/README.md index f94f1fbc..2d6e55d9 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,143 @@ -# AITBC Monorepo +# AITBC — AI Token Blockchain -This repository houses all components of the Artificial Intelligence Token Blockchain (AITBC) stack, including coordinator services, blockchain node, miner daemon, client-facing web apps, SDKs, and documentation. +Decentralized GPU compute marketplace with blockchain-based job coordination, Ollama inference, ZK receipt verification, and token payments. + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + +## Overview + +AITBC is a full-stack blockchain platform that connects GPU compute providers (miners) with AI workload consumers (clients) through a decentralized marketplace. The system handles job submission, miner matching, GPU inference execution, cryptographic receipt generation, and on-chain payment settlement. + +**Key capabilities:** +- **Blockchain nodes** — PoA consensus, gossip relay, WebSocket RPC +- **Coordinator API** — Job lifecycle, miner registry, marketplace, multi-tenancy +- **GPU mining** — Ollama-based LLM inference with host GPU passthrough +- **Wallet daemon** — Balance tracking, receipt verification, ledger management +- **Trade exchange** — Bitcoin/AITBC trading with order book and price ticker +- **ZK circuits** — Zero-knowledge receipt verification (Circom/snarkjs) +- **Browser wallet** — Firefox extension for AITBC transactions +- **Explorer** — Real-time blockchain explorer (blocks, transactions, addresses, receipts) + +## Architecture + +``` +Client ──► Coordinator API ──► Pool Hub ──► Miner (GPU/Ollama) + │ │ + ▼ ▼ + Marketplace ZK Receipt + │ │ + ▼ ▼ + Wallet Daemon ◄──── Blockchain Nodes ◄─── Settlement + │ + ▼ + Trade Exchange +``` ## Directory Structure ``` aitbc/ -├── apps/ # Main applications (coordinator-api, blockchain-node, etc.) -├── packages/ # Shared packages and SDKs -├── scripts/ # Utility scripts -│ └── testing/ # Test scripts and utilities -├── tests/ # Pytest test suites -├── docs/ # Documentation -│ ├── guides/ # Development guides -│ └── reports/ # Generated reports and summaries -├── infra/ # Infrastructure and deployment configs -├── dev-utils/ # Development utilities -└── .windsurf/ # Windsurf IDE configuration +├── apps/ # Core microservices +│ ├── blockchain-node/ # PoA blockchain node (FastAPI + gossip) +│ ├── coordinator-api/ # Job coordination API (FastAPI) +│ ├── explorer-web/ # Blockchain explorer (TypeScript + Vite) +│ ├── marketplace-web/ # GPU marketplace frontend (TypeScript + Vite) +│ ├── pool-hub/ # Mining pool management (FastAPI + Redis) +│ ├── trade-exchange/ # BTC/AITBC exchange (FastAPI + WebSocket) +│ ├── wallet-daemon/ # Wallet service (FastAPI) +│ └── zk-circuits/ # ZK proof circuits (Circom) +├── cli/ # CLI tools (client, miner, wallet) +├── contracts/ # Solidity smart contracts +├── docs/ # Documentation (structure, guides, reference, reports) +├── extensions/ # Browser extensions (Firefox wallet) +├── home/ # Local simulation scripts +├── infra/ # Infrastructure (nginx, k8s, helm, terraform) +├── packages/ # Shared libraries +│ ├── py/aitbc-crypto/ # Cryptographic primitives +│ ├── py/aitbc-sdk/ # Python SDK +│ └── solidity/aitbc-token/# ERC-20 token contract +├── plugins/ollama/ # Ollama LLM integration +├── scripts/ # Deployment, GPU, service, and test scripts +├── systemd/ # Systemd service units +├── tests/ # Test suites (unit, integration, e2e, security, load) +└── website/ # Public website and HTML documentation ``` -## Getting Started +See [docs/structure.md](docs/structure.md) for detailed file-level documentation. -1. Review the bootstrap documents under `docs/bootstrap/` to understand stage-specific goals. -2. Fill in service-specific READMEs located under `apps/` and `packages/` as the implementations progress. -3. Use the provided directory scaffold as the starting point for coding each subsystem. -4. Explore the new Python receipt SDK under `packages/py/aitbc-sdk/` for helpers to fetch and verify coordinator receipts (see `docs/run.md` for examples). -5. Run `scripts/ci/run_python_tests.sh` (via Poetry) to execute coordinator, SDK, miner-node, and wallet-daemon test suites before submitting changes. -6. GitHub Actions (`.github/workflows/python-tests.yml`) automatically runs the same script on pushes and pull requests targeting `main`. +## Quick Start -## Testing +### Prerequisites -- Test scripts are located in `scripts/testing/` -- Run individual tests: `python3 scripts/testing/test_block_import.py` -- Run pytest suite: `pytest tests/` -- See `docs/guides/WINDSURF_TESTING_GUIDE.md` for detailed testing instructions +- Python 3.11+ +- Node.js 18+ (for web apps and ZK circuits) +- PostgreSQL (optional, SQLite for development) +- Ollama (for GPU inference) + +### Run Services Locally + +```bash +# Start all services +./scripts/dev_services.sh + +# Or start individually +cd apps/coordinator-api && uvicorn app.main:app --port 8000 +cd apps/blockchain-node && python -m aitbc_chain.main +cd apps/wallet-daemon && uvicorn app.main:app --port 8002 +``` + +### Run Tests + +```bash +# Full test suite +pytest tests/ + +# Unit tests only +pytest tests/unit/ + +# Integration tests +pytest tests/integration/ + +# CI script (all apps) +./scripts/ci/run_python_tests.sh +``` + +### CLI Usage + +```bash +# Submit a job as a client +python cli/client.py submit --model llama3 --prompt "Hello world" + +# Start mining +python cli/miner.py start --gpu 0 + +# Check wallet balance +python cli/wallet.py balance +``` + +## Deployment + +Services run in an Incus container with systemd units. See `systemd/` for service definitions and `scripts/deploy/` for deployment automation. + +```bash +# Deploy to container +./scripts/deploy/deploy-to-container.sh + +# Deploy blockchain nodes +./scripts/deploy/deploy-blockchain.sh +``` + +## Documentation + +| Document | Description | +|----------|-------------| +| [docs/structure.md](docs/structure.md) | Codebase structure and file layout | +| [docs/files.md](docs/files.md) | File audit and status tracking | +| [docs/roadmap.md](docs/roadmap.md) | Development roadmap | +| [docs/components.md](docs/components.md) | Component overview | +| [docs/infrastructure.md](docs/infrastructure.md) | Infrastructure guide | +| [docs/full-documentation.md](docs/full-documentation.md) | Complete technical documentation | + +## License + +[MIT](LICENSE) — Copyright (c) 2025 AITBC diff --git a/apps/.service_pids b/apps/.service_pids deleted file mode 100644 index 4a56572f..00000000 --- a/apps/.service_pids +++ /dev/null @@ -1 +0,0 @@ -1529925 1529926 1529927 1529928 diff --git a/apps/coordinator-api/src/app/routers/exchange.py b/apps/coordinator-api/src/app/routers/exchange.py index 86946211..43740a28 100644 --- a/apps/coordinator-api/src/app/routers/exchange.py +++ b/apps/coordinator-api/src/app/routers/exchange.py @@ -8,6 +8,9 @@ import uuid import time import json import os +import logging + +logger = logging.getLogger(__name__) from ..schemas import ExchangePaymentRequest, ExchangePaymentResponse from ..services.bitcoin_wallet import get_wallet_balance, get_wallet_info @@ -109,7 +112,7 @@ async def confirm_payment( from ..services.blockchain import mint_tokens mint_tokens(payment['user_id'], payment['aitbc_amount']) except Exception as e: - print(f"Error minting tokens: {e}") + logger.error("Error minting tokens: %s", e) # In production, handle this error properly return { diff --git a/apps/coordinator-api/src/app/routers/governance.py b/apps/coordinator-api/src/app/routers/governance.py index 777b6234..1265185e 100644 --- a/apps/coordinator-api/src/app/routers/governance.py +++ b/apps/coordinator-api/src/app/routers/governance.py @@ -7,6 +7,9 @@ from pydantic import BaseModel, Field from typing import Optional, Dict, Any, List from datetime import datetime, timedelta import json +import logging + +logger = logging.getLogger(__name__) from ..schemas import UserProfile from ..storage import SessionDep @@ -349,7 +352,7 @@ async def execute_parameter_change(target: Dict[str, Any], background_tasks): """Execute a parameter change proposal""" # This would update system parameters - print(f"Executing parameter change: {target}") + logger.info("Executing parameter change: %s", target) # Implementation would depend on the specific parameters @@ -357,7 +360,7 @@ async def execute_protocol_upgrade(target: Dict[str, Any], background_tasks): """Execute a protocol upgrade proposal""" # This would trigger a protocol upgrade - print(f"Executing protocol upgrade: {target}") + logger.info("Executing protocol upgrade: %s", target) # Implementation would involve coordinating with nodes @@ -365,7 +368,7 @@ async def execute_fund_allocation(target: Dict[str, Any], background_tasks): """Execute a fund allocation proposal""" # This would transfer funds from treasury - print(f"Executing fund allocation: {target}") + logger.info("Executing fund allocation: %s", target) # Implementation would involve treasury management @@ -373,7 +376,7 @@ async def execute_policy_change(target: Dict[str, Any], background_tasks): """Execute a policy change proposal""" # This would update system policies - print(f"Executing policy change: {target}") + logger.info("Executing policy change: %s", target) # Implementation would depend on the specific policy diff --git a/apps/coordinator-api/src/app/services/bitcoin_wallet.py b/apps/coordinator-api/src/app/services/bitcoin_wallet.py index d582aff1..617b1c9b 100644 --- a/apps/coordinator-api/src/app/services/bitcoin_wallet.py +++ b/apps/coordinator-api/src/app/services/bitcoin_wallet.py @@ -6,9 +6,12 @@ Uses RPC to connect to Bitcoin Core (or alternative like Block.io) import os import json +import logging import requests from typing import Dict, Optional +logger = logging.getLogger(__name__) + # Bitcoin wallet configuration WALLET_CONFIG = { # For development, we'll use testnet @@ -31,11 +34,11 @@ class BitcoinWallet: try: result = self._rpc_call('getbalance', ["*", 0, False]) if result.get('error') is not None: - print(f"Bitcoin RPC error: {result['error']}") + logger.error("Bitcoin RPC error: %s", result['error']) return 0.0 return result.get('result', 0.0) except Exception as e: - print(f"Failed to get balance: {e}") + logger.error("Failed to get balance: %s", e) return 0.0 def get_new_address(self) -> str: @@ -43,11 +46,11 @@ class BitcoinWallet: try: result = self._rpc_call('getnewaddress', ["", "bech32"]) if result.get('error') is not None: - print(f"Bitcoin RPC error: {result['error']}") + logger.error("Bitcoin RPC error: %s", result['error']) return self.config['fallback_address'] return result.get('result', self.config['fallback_address']) except Exception as e: - print(f"Failed to get new address: {e}") + logger.error("Failed to get new address: %s", e) return self.config['fallback_address'] def list_transactions(self, count: int = 10) -> list: @@ -55,11 +58,11 @@ class BitcoinWallet: try: result = self._rpc_call('listtransactions', ["*", count, 0, True]) if result.get('error') is not None: - print(f"Bitcoin RPC error: {result['error']}") + logger.error("Bitcoin RPC error: %s", result['error']) return [] return result.get('result', []) except Exception as e: - print(f"Failed to list transactions: {e}") + logger.error("Failed to list transactions: %s", e) return [] def _rpc_call(self, method: str, params: list = None) -> Dict: @@ -83,7 +86,7 @@ class BitcoinWallet: response.raise_for_status() return response.json() except Exception as e: - print(f"RPC call failed: {e}") + logger.error("RPC call failed: %s", e) return {"error": str(e)} # Create a wallet instance @@ -117,7 +120,7 @@ def get_wallet_info() -> Dict[str, any]: "blocks": blockchain_info.get('result', {}).get('blocks', 0) if is_connected else 0 } except Exception as e: - print(f"Error getting wallet info: {e}") + logger.error("Error getting wallet info: %s", e) return { "balance": 0.0, "address": "tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", diff --git a/apps/coordinator-api/src/app/services/blockchain.py b/apps/coordinator-api/src/app/services/blockchain.py index 8ba7c780..f38ad5a5 100644 --- a/apps/coordinator-api/src/app/services/blockchain.py +++ b/apps/coordinator-api/src/app/services/blockchain.py @@ -4,10 +4,13 @@ Blockchain service for AITBC token operations import httpx import asyncio +import logging from typing import Optional from ..config import settings +logger = logging.getLogger(__name__) + BLOCKCHAIN_RPC = f"http://127.0.0.1:9080/rpc" async def mint_tokens(address: str, amount: float) -> dict: @@ -44,6 +47,6 @@ def get_balance(address: str) -> Optional[float]: return float(data.get("balance", 0)) except Exception as e: - print(f"Error getting balance: {e}") + logger.error("Error getting balance: %s", e) return None diff --git a/apps/coordinator-api/src/app/services/receipts.py b/apps/coordinator-api/src/app/services/receipts.py index 145e1cba..037ac8bb 100644 --- a/apps/coordinator-api/src/app/services/receipts.py +++ b/apps/coordinator-api/src/app/services/receipts.py @@ -1,9 +1,12 @@ from __future__ import annotations +import logging from typing import Any, Dict, Optional from secrets import token_hex from datetime import datetime +logger = logging.getLogger(__name__) + from aitbc_crypto.signing import ReceiptSigner from sqlmodel import Session @@ -129,7 +132,7 @@ class ReceiptService: except Exception as e: # Log error but don't fail receipt creation - print(f"Failed to generate ZK proof: {e}") + logger.warning("Failed to generate ZK proof: %s", e) receipt_row = JobReceipt(job_id=job.id, receipt_id=payload["receipt_id"], payload=payload) self.session.add(receipt_row) diff --git a/apps/coordinator-api/src/app/storage/db_pg.py b/apps/coordinator-api/src/app/storage/db_pg.py index d6f00fa8..7377722b 100644 --- a/apps/coordinator-api/src/app/storage/db_pg.py +++ b/apps/coordinator-api/src/app/storage/db_pg.py @@ -8,9 +8,12 @@ import psycopg2 from psycopg2.extras import RealDictCursor from typing import Generator, Optional, Dict, Any, List import json +import logging from datetime import datetime from decimal import Decimal +logger = logging.getLogger(__name__) + from .config_pg import settings # SQLAlchemy setup for complex queries @@ -203,7 +206,7 @@ def init_db(): # Create all tables Base.metadata.create_all(bind=engine) - print("✅ PostgreSQL database initialized successfully!") + logger.info("PostgreSQL database initialized successfully") # Health check def check_db_health() -> Dict[str, Any]: diff --git a/docs/files.md b/docs/files.md index 51c90f62..a780eb3e 100644 --- a/docs/files.md +++ b/docs/files.md @@ -5,7 +5,7 @@ This document categorizes all files and folders in the repository by their statu - **Greylist (⚠️)**: Uncertain status, may need review - **Blacklist (❌)**: Legacy, unused, outdated, candidates for removal -Last updated: 2026-01-29 +Last updated: 2026-02-11 --- @@ -65,14 +65,6 @@ Last updated: 2026-01-29 | `docs/guides/` | ✅ Active | Development guides (moved from root) | | `docs/reports/` | ✅ Active | Generated reports (moved from root) | -### Cascade Skills (`.windsurf/`) - -| Path | Status | Notes | -|------|--------|-------| -| `.windsurf/skills/blockchain-operations/` | ✅ Active | Node management skill | -| `.windsurf/skills/deploy-production/` | ✅ Active | Deployment skill | -| `.windsurf/workflows/` | ✅ Active | Workflow definitions | - ### CLI Tools (`cli/`) | Path | Status | Notes | @@ -264,14 +256,82 @@ These empty folders are intentional scaffolding for planned future work per the --- +## Issues Found (2026-02-11) + +### Empty Directories (Delete) + +| Path | Action | +|------|--------| +| `apps/blockchain-node/src/aitbc_chain/ledger/` | Delete — empty placeholder, never implemented | +| `apps/blockchain-node/src/aitbc_chain/mempool/` | Delete — empty dir, mempool logic is in `mempool.py` | +| `apps/coordinator-api/src/app/ws/` | Delete — empty WebSocket placeholder, never implemented | +| `apps/explorer-web/public/js/components/` | Delete — empty, TS components are in `src/components/` | +| `apps/explorer-web/public/js/pages/` | Delete — empty, TS pages are in `src/pages/` | +| `apps/explorer-web/public/js/vendors/` | Delete — empty vendor dir | +| `apps/explorer-web/public/assets/` | Delete — empty assets dir | +| `packages/py/aitbc-crypto/build/bdist.linux-x86_64/` | Delete — build artifact | + +### Files in Wrong Location (Move) + +| Current Path | Correct Path | Reason | +|-------------|-------------|--------| +| `apps/coordinator-api/coordinator.db` | gitignored / `data/` | SQLite database should not be in git | +| `apps/coordinator-api/.env` | gitignored | Environment file with secrets, should not be in git | +| `apps/.service_pids` | gitignored | Runtime PID file, should not be in git | +| `src/aitbc_chain/` | `apps/blockchain-node/src/aitbc_chain/` | Duplicate/stale copy of blockchain node source | +| `website/docs-clients.html` | `website/docs/docs-clients.html` | Inconsistent location, duplicate of file in `docs/` | +| `website/docs-developers.html` | `website/docs/docs-developers.html` | Inconsistent location, duplicate of file in `docs/` | +| `website/docs-miners.html` | `website/docs/docs-miners.html` | Inconsistent location, duplicate of file in `docs/` | +| `website/docs-index.html` | `website/docs/index.html` | Inconsistent location, duplicate of file in `docs/` | + +### Legacy Files (Delete) + +| Path | Reason | +|------|--------| +| `SECURITY_CLEANUP_GUIDE.md` | One-time cleanup guide, already completed | +| `apps/trade-exchange/index_working.html` | Backup copy of `index.html` | +| `apps/trade-exchange/index.prod.html` | Superseded by `build.py` production build | +| `apps/trade-exchange/index.real.html` | Superseded by `build.py` production build | +| `tests/conftest_fixtures.py` | Unused alternate conftest | +| `tests/conftest_full.py` | Unused alternate conftest | +| `tests/conftest_path.py` | Unused alternate conftest | +| `tests/pytest_simple.ini` | Duplicate of root `pytest.ini` | +| `tests/test_blockchain_simple.py` | Superseded by `test_blockchain_nodes.py` | +| `tests/test_blockchain_final.py` | Superseded by `test_blockchain_nodes.py` | +| `tests/test_discovery.py` | One-time discovery script | +| `tests/test_windsurf_integration.py` | IDE-specific test, not for GitHub | +| `scripts/exchange-router-fixed.py` | One-time fix script | +| `scripts/start_mock_blockchain.sh` | Superseded by `tests/mock_blockchain_node.py` | +| `apps/marketplace-web/src/counter.ts` | Vite template boilerplate, unused | +| `apps/marketplace-web/src/typescript.svg` | Vite template boilerplate, unused | +| `apps/marketplace-web/public/vite.svg` | Vite template boilerplate, unused | +| `.vscode/` | IDE-specific, should be gitignored | + +### Debug Print Statements (Replace with logging) + +| File | Lines | Statement | +|------|-------|-----------| +| `apps/coordinator-api/src/app/routers/exchange.py` | 112 | `print(f"Error minting tokens: {e}")` | +| `apps/coordinator-api/src/app/routers/governance.py` | 352-376 | 4x `print(f"Executing ...")` | +| `apps/coordinator-api/src/app/services/receipts.py` | 132 | `print(f"Failed to generate ZK proof: {e}")` | +| `apps/coordinator-api/src/app/services/blockchain.py` | 47 | `print(f"Error getting balance: {e}")` | +| `apps/coordinator-api/src/app/services/bitcoin_wallet.py` | 34-134 | 8x `print(...)` debug statements | +| `apps/coordinator-api/src/app/storage/db_pg.py` | 206 | `print("✅ PostgreSQL database initialized successfully!")` | + +--- + ## Summary Statistics | Category | Count | Action | |----------|-------|--------| | **Whitelist ✅** | ~60 items | Keep and maintain | -| **Greylist ⚠️** | 0 items | All resolved! | -| **Placeholders 📋** | 12 folders | Fill per roadmap Stage 19 | +| **Greylist ⚠️** | 0 items | All resolved | +| **Placeholders 📋** | 12 folders | Fill per roadmap | | **Removed ❌** | 35 items | Cleaned up 2026-01-24 | +| **Empty dirs** | 8 dirs | Delete | +| **Misplaced files** | 8 files | Move or gitignore | +| **Legacy files** | 18 files | Delete | +| **Debug prints** | 17 statements | Replace with logger | ### Completed Actions (2026-01-24) diff --git a/docs/structure.md b/docs/structure.md new file mode 100644 index 00000000..a648346e --- /dev/null +++ b/docs/structure.md @@ -0,0 +1,265 @@ +# AITBC Codebase Structure + +> Monorepo layout for the AI Token Blockchain platform. + +## Top-Level Overview + +``` +aitbc/ +├── apps/ # Core microservices and web applications +├── assets/ # Shared frontend assets (CSS, JS, fonts) +├── cli/ # Command-line interface tools +├── contracts/ # Solidity smart contracts (standalone) +├── dev-utils/ # Developer utilities and path configs +├── docs/ # Markdown documentation +├── examples/ # Usage examples and demos +├── extensions/ # Browser extensions (Firefox wallet) +├── home/ # Local workflow scripts (client/miner simulation) +├── infra/ # Infrastructure configs (nginx, k8s, terraform, helm) +├── packages/ # Shared libraries and SDKs +├── plugins/ # Plugin integrations (Ollama) +├── protocols/ # Protocol specs and sample data +├── scripts/ # Operational and deployment scripts +├── src/ # Shared Python source (cross-site sync, RPC) +├── systemd/ # Systemd service unit files +├── tests/ # Pytest test suites (unit, integration, e2e, security, load) +├── website/ # Public-facing website and HTML documentation +├── .gitignore +├── .editorconfig +├── LICENSE # MIT License +├── pyproject.toml # Python project configuration +├── pytest.ini # Pytest settings and markers +└── README.md +``` + +--- + +## apps/ — Core Applications + +### blockchain-node +Full blockchain node implementation with PoA consensus, gossip relay, mempool, RPC API, WebSocket support, and observability dashboards. + +``` +apps/blockchain-node/ +├── src/aitbc_chain/ +│ ├── app.py # FastAPI application +│ ├── main.py # Entry point +│ ├── config.py # Node configuration +│ ├── database.py # Chain storage +│ ├── models.py # Block/Transaction models +│ ├── mempool.py # Transaction mempool +│ ├── metrics.py # Prometheus metrics +│ ├── logger.py # Structured logging +│ ├── consensus/poa.py # Proof-of-Authority consensus +│ ├── gossip/ # P2P gossip protocol (broker, relay) +│ ├── observability/ # Dashboards and exporters +│ └── rpc/ # JSON-RPC router and WebSocket +├── scripts/ # Genesis creation, key generation, benchmarks +├── tests/ # Unit tests (models, gossip, WebSocket, observability) +└── pyproject.toml +``` + +### coordinator-api +Central job coordination API with marketplace, payments, ZK proofs, multi-tenancy, and governance. + +``` +apps/coordinator-api/ +├── src/app/ +│ ├── main.py # FastAPI entry point +│ ├── config.py # Configuration +│ ├── database.py # Database setup +│ ├── deps.py # Dependency injection +│ ├── exceptions.py # Custom exceptions +│ ├── logging.py # Logging config +│ ├── metrics.py # Prometheus metrics +│ ├── domain/ # Domain models (job, miner, payment, user, marketplace) +│ ├── models/ # DB models (registry, confidential, multitenant, services) +│ ├── routers/ # API endpoints (admin, client, miner, marketplace, payments, governance, exchange, explorer, ZK) +│ ├── services/ # Business logic (jobs, miners, payments, receipts, ZK proofs, encryption, HSM, blockchain, bitcoin wallet) +│ ├── storage/ # Database adapters (SQLite, PostgreSQL) +│ ├── middleware/ # Tenant context middleware +│ ├── repositories/ # Data access layer +│ └── schemas/ # Pydantic schemas +├── aitbc/settlement/ # Cross-chain settlement (LayerZero bridge) +├── migrations/ # SQL migrations (schema, indexes, data, payments) +├── scripts/ # PostgreSQL migration scripts +├── tests/ # API tests (jobs, marketplace, ZK, receipts, miners) +└── pyproject.toml +``` + +### explorer-web +Blockchain explorer SPA built with TypeScript and Vite. + +``` +apps/explorer-web/ +├── src/ +│ ├── main.ts # Application entry +│ ├── config.ts # API configuration +│ ├── components/ # UI components (header, footer, data mode toggle, notifications) +│ ├── lib/ # Data models and mock data +│ └── pages/ # Page views (overview, blocks, transactions, addresses, receipts) +├── public/ # Static assets (CSS themes, mock JSON data) +├── tests/e2e/ # Playwright end-to-end tests +├── vite.config.ts +└── tsconfig.json +``` + +### marketplace-web +GPU compute marketplace frontend built with TypeScript and Vite. + +``` +apps/marketplace-web/ +├── src/ +│ ├── main.ts # Application entry +│ ├── lib/ # API client and auth +│ └── style.css # Styles +├── public/ # Mock data (offers, stats) +├── vite.config.ts +└── tsconfig.json +``` + +### wallet-daemon +Wallet service with receipt verification and ledger management. + +``` +apps/wallet-daemon/ +├── src/app/ +│ ├── main.py # FastAPI entry point +│ ├── settings.py # Configuration +│ ├── ledger_mock/ # Mock ledger with PostgreSQL adapter +│ └── receipts/ # Receipt verification service +├── scripts/ # PostgreSQL migration +├── tests/ # Wallet API and receipt tests +└── pyproject.toml +``` + +### trade-exchange +Bitcoin/AITBC trading exchange with order book, price ticker, and admin panel. + +``` +apps/trade-exchange/ +├── server.py # WebSocket price server +├── simple_exchange_api.py # Exchange REST API (SQLite) +├── simple_exchange_api_pg.py # Exchange REST API (PostgreSQL) +├── exchange_api.py # Full exchange API +├── bitcoin-wallet.py # Bitcoin wallet integration +├── database.py # Database layer +├── build.py # Production build script +├── index.html # Exchange frontend +├── admin.html # Admin panel +└── scripts/ # PostgreSQL migration +``` + +### pool-hub +Mining pool management with job matching, miner scoring, and Redis caching. + +``` +apps/pool-hub/ +├── src/ +│ ├── app/ # Legacy app structure (routers, registry, scoring) +│ └── poolhub/ # Current app (routers, models, repositories, services, Redis) +├── migrations/ # Alembic migrations +└── tests/ # API and repository tests +``` + +### zk-circuits +Zero-knowledge proof circuits for receipt verification. + +``` +apps/zk-circuits/ +├── circuits/receipt.circom # Circom circuit definition +├── generate_proof.js # Proof generation +├── test.js # Circuit tests +└── benchmark.js # Performance benchmarks +``` + +--- + +## packages/ — Shared Libraries + +``` +packages/ +├── py/ +│ ├── aitbc-crypto/ # Cryptographic primitives (signing, hashing, key derivation) +│ └── aitbc-sdk/ # Python SDK for coordinator API (receipt fetching/verification) +└── solidity/ + └── aitbc-token/ # ERC-20 AITBC token contract with Hardhat tooling +``` + +--- + +## scripts/ — Operations + +``` +scripts/ +├── aitbc-cli.sh # Main CLI entry point +├── deploy/ # Deployment scripts (container, remote, blockchain, explorer, exchange, nginx) +├── gpu/ # GPU miner management (host miner, registry, exchange integration) +├── service/ # Service lifecycle (start, stop, diagnose, fix) +├── testing/ # Test runners and verification scripts +├── test/ # Individual test scripts (coordinator, GPU, explorer) +├── ci/ # CI pipeline scripts +├── ops/ # Operational scripts (systemd install) +└── dev/ # Development tools (WebSocket load test) +``` + +--- + +## infra/ — Infrastructure + +``` +infra/ +├── nginx/ # Nginx configs (reverse proxy, local, production) +├── k8s/ # Kubernetes manifests (backup, cert-manager, network policies, sealed secrets) +├── helm/ # Helm charts (coordinator deployment, values per environment) +├── terraform/ # Terraform modules (Kubernetes cluster, environments: dev/staging/prod) +└── scripts/ # Infra scripts (backup, restore, chaos testing) +``` + +--- + +## tests/ — Test Suites + +``` +tests/ +├── unit/ # Unit tests (blockchain node, coordinator API, wallet daemon) +├── integration/ # Integration tests (blockchain node, full workflow) +├── e2e/ # End-to-end tests (user scenarios, wallet daemon) +├── security/ # Security tests (confidential transactions, comprehensive audit) +├── load/ # Load tests (Locust) +├── conftest.py # Shared pytest fixtures +└── test_blockchain_nodes.py # Live node connectivity tests +``` + +--- + +## website/ — Public Website + +``` +website/ +├── index.html # Landing page +├── 404.html # Error page +├── docs/ # HTML documentation (per-component pages, CSS, JS) +├── dashboards/ # Admin and miner dashboards +├── BrowserWallet/ # Browser wallet interface +├── extensions/ # Packaged browser extensions (.zip, .xpi) +└── aitbc-proxy.conf # Nginx proxy config for website +``` + +--- + +## Other Directories + +| Directory | Purpose | +|-----------|---------| +| `cli/` | CLI tools for client, miner, wallet operations and GPU testing | +| `plugins/ollama/` | Ollama LLM integration (client plugin, miner plugin, service layer) | +| `home/` | Local simulation scripts for client/miner workflows | +| `extensions/` | Firefox wallet extension source code | +| `contracts/` | Standalone Solidity contracts (ZKReceiptVerifier) | +| `protocols/` | Protocol sample data (signed receipts) | +| `src/` | Shared Python modules (cross-site sync, RPC router) | +| `systemd/` | Systemd unit files for all AITBC services | +| `dev-utils/` | Developer utilities (Python path config) | +| `docs/` | Markdown documentation (guides, reports, reference, tutorials, operator docs) | +| `assets/` | Shared frontend assets (Tailwind CSS, FontAwesome, Lucide icons, Axios) | diff --git a/src/aitbc_chain/rpc/router.py b/src/aitbc_chain/rpc/router.py deleted file mode 100644 index 106662ec..00000000 --- a/src/aitbc_chain/rpc/router.py +++ /dev/null @@ -1,457 +0,0 @@ -from __future__ import annotations - -import asyncio -import hashlib -import json -import time -from datetime import datetime -from typing import Any, Dict, Optional, List - -from fastapi import APIRouter, HTTPException, status -from pydantic import BaseModel, Field, model_validator -from sqlmodel import select - -from ..config import settings -from ..database import session_scope -from ..gossip import gossip_broker -from ..mempool import get_mempool -from ..metrics import metrics_registry -from ..models import Account, Block, Receipt, Transaction - -router = APIRouter() - - -def _serialize_receipt(receipt: Receipt) -> Dict[str, Any]: - return { - "receipt_id": receipt.receipt_id, - "job_id": receipt.job_id, - "payload": receipt.payload, - "miner_signature": receipt.miner_signature, - "coordinator_attestations": receipt.coordinator_attestations, - "minted_amount": receipt.minted_amount, - "recorded_at": receipt.recorded_at.isoformat(), - } - - -class TransactionRequest(BaseModel): - type: str = Field(description="Transaction type, e.g. TRANSFER or RECEIPT_CLAIM") - sender: str - nonce: int - fee: int = Field(ge=0) - payload: Dict[str, Any] - sig: Optional[str] = Field(default=None, description="Signature payload") - - @model_validator(mode="after") - def normalize_type(self) -> "TransactionRequest": # type: ignore[override] - normalized = self.type.upper() - if normalized not in {"TRANSFER", "RECEIPT_CLAIM"}: - raise ValueError(f"unsupported transaction type: {self.type}") - self.type = normalized - return self - - -class ReceiptSubmissionRequest(BaseModel): - sender: str - nonce: int - fee: int = Field(ge=0) - payload: Dict[str, Any] - sig: Optional[str] = None - - -class EstimateFeeRequest(BaseModel): - type: Optional[str] = None - payload: Dict[str, Any] = Field(default_factory=dict) - - -class MintFaucetRequest(BaseModel): - address: str - amount: int = Field(gt=0) - - -@router.get("/head", summary="Get current chain head") -async def get_head() -> Dict[str, Any]: - metrics_registry.increment("rpc_get_head_total") - start = time.perf_counter() - with session_scope() as session: - result = session.exec(select(Block).order_by(Block.height.desc()).limit(1)).first() - if result is None: - metrics_registry.increment("rpc_get_head_not_found_total") - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="no blocks yet") - metrics_registry.increment("rpc_get_head_success_total") - metrics_registry.observe("rpc_get_head_duration_seconds", time.perf_counter() - start) - return { - "height": result.height, - "hash": result.hash, - "timestamp": result.timestamp.isoformat(), - "tx_count": result.tx_count, - } - - -@router.get("/blocks/{height}", summary="Get block by height") -async def get_block(height: int) -> Dict[str, Any]: - metrics_registry.increment("rpc_get_block_total") - start = time.perf_counter() - with session_scope() as session: - block = session.exec(select(Block).where(Block.height == height)).first() - if block is None: - metrics_registry.increment("rpc_get_block_not_found_total") - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="block not found") - metrics_registry.increment("rpc_get_block_success_total") - metrics_registry.observe("rpc_get_block_duration_seconds", time.perf_counter() - start) - return { - "proposer": block.proposer, - "proposer": block.proposer, - "height": block.height, - "hash": block.hash, - "parent_hash": block.parent_hash, - "timestamp": block.timestamp.isoformat(), - "tx_count": block.tx_count, - "state_root": block.state_root, - } - - -@router.get("/blocks-range", summary="Get blocks in range") -async def get_blocks_range(start_height: int = 0, end_height: int = 100, limit: int = 1000) -> List[Dict[str, Any]]: - metrics_registry.increment("rpc_get_blocks_range_total") - start = time.perf_counter() - - # Validate parameters - if limit > 10000: - limit = 10000 - if end_height - start_height > limit: - end_height = start_height + limit - - with session_scope() as session: - stmt = ( - select(Block) - .where(Block.height >= start_height) - .where(Block.height <= end_height) - .order_by(Block.height) - ) - blocks = session.exec(stmt).all() - - metrics_registry.observe("rpc_get_blocks_range_duration_seconds", time.perf_counter() - start) - return [ - { - "proposer": block.proposer, - "proposer": block.proposer, - "height": block.height, - "hash": block.hash, - "parent_hash": block.parent_hash, - "timestamp": block.timestamp.isoformat(), - "tx_count": block.tx_count, - "state_root": block.state_root, - } - for block in blocks - ] - -@router.get("/tx/{tx_hash}", summary="Get transaction by hash") -async def get_transaction(tx_hash: str) -> Dict[str, Any]: - metrics_registry.increment("rpc_get_transaction_total") - start = time.perf_counter() - with session_scope() as session: - tx = session.exec(select(Transaction).where(Transaction.tx_hash == tx_hash)).first() - if tx is None: - metrics_registry.increment("rpc_get_transaction_not_found_total") - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="transaction not found") - metrics_registry.increment("rpc_get_transaction_success_total") - metrics_registry.observe("rpc_get_transaction_duration_seconds", time.perf_counter() - start) - return { - "tx_hash": tx.tx_hash, - "block_height": tx.block_height, - "sender": tx.sender, - "recipient": tx.recipient, - "payload": tx.payload, - "created_at": tx.created_at.isoformat(), - } - - -@router.get("/receipts/{receipt_id}", summary="Get receipt by ID") -async def get_receipt(receipt_id: str) -> Dict[str, Any]: - metrics_registry.increment("rpc_get_receipt_total") - start = time.perf_counter() - with session_scope() as session: - receipt = session.exec(select(Receipt).where(Receipt.receipt_id == receipt_id)).first() - if receipt is None: - metrics_registry.increment("rpc_get_receipt_not_found_total") - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="receipt not found") - metrics_registry.increment("rpc_get_receipt_success_total") - metrics_registry.observe("rpc_get_receipt_duration_seconds", time.perf_counter() - start) - return _serialize_receipt(receipt) - - -@router.get("/getBalance/{address}", summary="Get account balance") -async def get_balance(address: str) -> Dict[str, Any]: - metrics_registry.increment("rpc_get_balance_total") - start = time.perf_counter() - with session_scope() as session: - account = session.get(Account, address) - if account is None: - metrics_registry.increment("rpc_get_balance_empty_total") - metrics_registry.observe("rpc_get_balance_duration_seconds", time.perf_counter() - start) - return {"address": address, "balance": 0, "nonce": 0} - metrics_registry.increment("rpc_get_balance_success_total") - metrics_registry.observe("rpc_get_balance_duration_seconds", time.perf_counter() - start) - return { - "address": account.address, - "balance": account.balance, - "nonce": account.nonce, - "updated_at": account.updated_at.isoformat(), - } - - -@router.post("/sendTx", summary="Submit a new transaction") -async def send_transaction(request: TransactionRequest) -> Dict[str, Any]: - metrics_registry.increment("rpc_send_tx_total") - start = time.perf_counter() - mempool = get_mempool() - tx_dict = request.model_dump() - tx_hash = mempool.add(tx_dict) - try: - asyncio.create_task( - gossip_broker.publish( - "transactions", - { - "tx_hash": tx_hash, - "sender": request.sender, - "payload": request.payload, - "nonce": request.nonce, - "fee": request.fee, - "type": request.type, - }, - ) - ) - metrics_registry.increment("rpc_send_tx_success_total") - return {"tx_hash": tx_hash} - except Exception: - metrics_registry.increment("rpc_send_tx_failed_total") - raise - finally: - metrics_registry.observe("rpc_send_tx_duration_seconds", time.perf_counter() - start) - - -@router.post("/submitReceipt", summary="Submit receipt claim transaction") -async def submit_receipt(request: ReceiptSubmissionRequest) -> Dict[str, Any]: - metrics_registry.increment("rpc_submit_receipt_total") - start = time.perf_counter() - tx_payload = { - "type": "RECEIPT_CLAIM", - "sender": request.sender, - "nonce": request.nonce, - "fee": request.fee, - "payload": request.payload, - "sig": request.sig, - } - tx_request = TransactionRequest.model_validate(tx_payload) - try: - response = await send_transaction(tx_request) - metrics_registry.increment("rpc_submit_receipt_success_total") - return response - except HTTPException: - metrics_registry.increment("rpc_submit_receipt_failed_total") - raise - except Exception: - metrics_registry.increment("rpc_submit_receipt_failed_total") - raise - finally: - metrics_registry.observe("rpc_submit_receipt_duration_seconds", time.perf_counter() - start) - - -@router.post("/estimateFee", summary="Estimate transaction fee") -async def estimate_fee(request: EstimateFeeRequest) -> Dict[str, Any]: - metrics_registry.increment("rpc_estimate_fee_total") - start = time.perf_counter() - base_fee = 10 - per_byte = 1 - payload_bytes = len(json.dumps(request.payload, sort_keys=True, separators=(",", ":")).encode()) - estimated_fee = base_fee + per_byte * payload_bytes - tx_type = (request.type or "TRANSFER").upper() - metrics_registry.increment("rpc_estimate_fee_success_total") - metrics_registry.observe("rpc_estimate_fee_duration_seconds", time.perf_counter() - start) - return { - "type": tx_type, - "base_fee": base_fee, - "payload_bytes": payload_bytes, - "estimated_fee": estimated_fee, - } - - -@router.post("/admin/mintFaucet", summary="Mint devnet funds to an address") -async def mint_faucet(request: MintFaucetRequest) -> Dict[str, Any]: - metrics_registry.increment("rpc_mint_faucet_total") - start = time.perf_counter() - with session_scope() as session: - account = session.get(Account, request.address) - if account is None: - account = Account(address=request.address, balance=request.amount) - session.add(account) - else: - account.balance += request.amount - session.commit() - updated_balance = account.balance - metrics_registry.increment("rpc_mint_faucet_success_total") - metrics_registry.observe("rpc_mint_faucet_duration_seconds", time.perf_counter() - start) - return {"address": request.address, "balance": updated_balance} - - -class TransactionData(BaseModel): - tx_hash: str - sender: str - recipient: str - payload: Dict[str, Any] = Field(default_factory=dict) - -class ReceiptData(BaseModel): - receipt_id: str - job_id: str - payload: Dict[str, Any] = Field(default_factory=dict) - miner_signature: Optional[str] = None - coordinator_attestations: List[str] = Field(default_factory=list) - minted_amount: int = 0 - recorded_at: str - -class BlockImportRequest(BaseModel): - height: int = Field(gt=0) - hash: str - parent_hash: str - proposer: str - timestamp: str - tx_count: int = Field(ge=0) - state_root: Optional[str] = None - transactions: List[TransactionData] = Field(default_factory=list) - receipts: List[ReceiptData] = Field(default_factory=list) - - -@router.get("/test-endpoint", summary="Test endpoint") -async def test_endpoint() -> Dict[str, str]: - """Test if new code is deployed""" - return {"status": "updated_code_running"} - -@router.post("/blocks/import", summary="Import block from remote node") -async def import_block(request: BlockImportRequest) -> Dict[str, Any]: - """Import a block from a remote node after validation.""" - import logging - logger = logging.getLogger(__name__) - - metrics_registry.increment("rpc_import_block_total") - start = time.perf_counter() - - try: - logger.info(f"Received block import request: height={request.height}, hash={request.hash}") - logger.info(f"Transactions count: {len(request.transactions)}") - if request.transactions: - logger.info(f"First transaction: {request.transactions[0]}") - - with session_scope() as session: - # Check if block already exists - existing = session.exec(select(Block).where(Block.height == request.height)).first() - if existing: - if existing.hash == request.hash: - metrics_registry.increment("rpc_import_block_exists_total") - return {"status": "exists", "height": request.height, "hash": request.hash} - else: - metrics_registry.increment("rpc_import_block_conflict_total") - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"Block at height {request.height} already exists with different hash" - ) - - # Check if parent block exists - if request.height > 0: - parent = session.exec(select(Block).where(Block.hash == request.parent_hash)).first() - if not parent: - metrics_registry.increment("rpc_import_block_orphan_total") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Parent block not found" - ) - - # Validate block hash using the same algorithm as PoA proposer - payload = f"{settings.chain_id}|{request.height}|{request.parent_hash}|{request.timestamp}".encode() - expected_hash = "0x" + hashlib.sha256(payload).hexdigest() - - if request.hash != expected_hash: - metrics_registry.increment("rpc_import_block_invalid_hash_total") - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Invalid block hash. Expected: {expected_hash}, Got: {request.hash}" - ) - - # Create and save block - block_timestamp = datetime.fromisoformat(request.timestamp.replace('Z', '+00:00')) - - block = Block( - height=request.height, - hash=request.hash, - parent_hash=request.parent_hash, - proposer=request.proposer, - timestamp=block_timestamp, - tx_count=request.tx_count, - state_root=request.state_root - ) - - session.add(block) - session.flush() # Get block ID - - # Add transactions if provided - for tx_data in request.transactions: - # Create transaction using constructor with all fields - tx = Transaction( - tx_hash=str(tx_data.tx_hash), - block_height=block.id, # Use block.id instead of block.height for foreign key - sender=str(tx_data.sender), - recipient=str(tx_data.recipient), - payload=tx_data.payload if tx_data.payload else {}, - created_at=datetime.utcnow() - ) - - session.add(tx) - - # Add receipts if provided - for receipt_data in request.receipts: - receipt = Receipt( - block_height=block.id, # Use block.id instead of block.height for foreign key - receipt_id=receipt_data.receipt_id, - job_id=receipt_data.job_id, - payload=receipt_data.payload, - miner_signature=receipt_data.miner_signature, - coordinator_attestations=receipt_data.coordinator_attestations, - minted_amount=receipt_data.minted_amount, - recorded_at=datetime.fromisoformat(receipt_data.recorded_at.replace('Z', '+00:00')) - ) - session.add(receipt) - - session.commit() - - # Broadcast block via gossip - try: - gossip_broker.broadcast("blocks", { - "type": "block_imported", - "height": block.height, - "hash": block.hash, - "proposer": block.proposer - }) - except Exception: - pass # Gossip broadcast is optional - - metrics_registry.increment("rpc_import_block_success_total") - metrics_registry.observe("rpc_import_block_duration_seconds", time.perf_counter() - start) - - logger.info(f"Successfully imported block {request.height}") - - return { - "status": "imported", - "height": block.height, - "hash": block.hash, - "tx_count": block.tx_count - } - - except HTTPException: - # Re-raise HTTP exceptions as-is - raise - except Exception as e: - logger.error(f"Failed to import block {request.height}: {str(e)}", exc_info=True) - metrics_registry.increment("rpc_import_block_failed_total") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Internal server error: {str(e)}" - ) diff --git a/src/aitbc_chain/sync/__init__.py b/src/aitbc_chain/sync/__init__.py deleted file mode 100644 index 495401ed..00000000 --- a/src/aitbc_chain/sync/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Cross-site synchronization module for AITBC blockchain. -""" - -from .cross_site import CrossSiteSync - -__all__ = ['CrossSiteSync'] diff --git a/src/aitbc_chain/sync/cross_site.py b/src/aitbc_chain/sync/cross_site.py deleted file mode 100644 index 89e864d3..00000000 --- a/src/aitbc_chain/sync/cross_site.py +++ /dev/null @@ -1,222 +0,0 @@ -""" -Cross-site RPC synchronization module for AITBC blockchain nodes. -Enables block and transaction synchronization across different sites via HTTP RPC endpoints. -""" - -import asyncio -import logging -from typing import List, Dict, Optional, Any -import httpx -from datetime import datetime, timedelta - -logger = logging.getLogger(__name__) - - -class CrossSiteSync: - """Handles synchronization with remote blockchain nodes via RPC.""" - - def __init__(self, local_rpc_url: str, remote_endpoints: List[str], poll_interval: int = 10): - """ - Initialize cross-site synchronization. - - Args: - local_rpc_url: URL of local RPC endpoint (e.g., "http://localhost:8082") - remote_endpoints: List of remote RPC URLs to sync with - poll_interval: Seconds between sync checks - """ - self.local_rpc_url = local_rpc_url.rstrip('/') - self.remote_endpoints = remote_endpoints - self.poll_interval = poll_interval - self.last_sync = {} - self.sync_task = None - self.client = httpx.AsyncClient(timeout=5.0) - - async def get_remote_head(self, endpoint: str) -> Optional[Dict[str, Any]]: - """Get the head block from a remote node.""" - try: - response = await self.client.get(f"{endpoint.rstrip('/')}/head") - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Failed to get head from {endpoint}: {e}") - return None - - async def get_remote_block(self, endpoint: str, height: int) -> Optional[Dict[str, Any]]: - """Get a specific block from a remote node.""" - try: - response = await self.client.get(f"{endpoint.rstrip('/')}/blocks/{height}") - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Failed to get block {height} from {endpoint}: {e}") - return None - - async def get_local_head(self) -> Optional[Dict[str, Any]]: - """Get the local head block.""" - try: - response = await self.client.get(f"{self.local_rpc_url}/rpc/head") - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Failed to get local head: {e}") - return None - - async def import_block(self, block_data: Dict[str, Any]) -> bool: - """Import a block from a remote node.""" - try: - response = await self.client.post( - f"{self.local_rpc_url}/rpc/blocks/import", - json=block_data - ) - if response.status_code == 200: - result = response.json() - if result.get("status") in ["imported", "exists"]: - logger.info(f"Successfully imported block {block_data.get('height')}") - return True - else: - logger.error(f"Block import failed: {result}") - return False - else: - logger.error(f"Block import request failed: {response.status_code} {response.text}") - return False - except Exception as e: - logger.error(f"Failed to import block: {e}") - return False - - async def submit_block(self, block_data: Dict[str, Any]) -> bool: - """Submit a block to the local node.""" - try: - response = await self.client.post( - f"{self.local_rpc_url}/rpc/block", - json=block_data - ) - return response.status_code == 200 - except Exception as e: - logger.error(f"Failed to submit block: {e}") - return False - - async def sync_with_remotes(self) -> None: - """Check and sync with all remote endpoints.""" - local_head = await self.get_local_head() - if not local_head: - return - - local_height = local_head.get('height', 0) - - for endpoint in self.remote_endpoints: - remote_head = await self.get_remote_head(endpoint) - if not remote_head: - continue - - remote_height = remote_head.get('height', 0) - - # If remote is ahead, fetch missing blocks - if remote_height > local_height: - logger.info(f"Remote {endpoint} is ahead (height {remote_height} vs {local_height})") - - # Fetch missing blocks one by one - for height in range(local_height + 1, remote_height + 1): - block = await self.get_remote_block(endpoint, height) - if block: - # Format block data for import - import_data = { - "height": block.get("height"), - "hash": block.get("hash"), - "parent_hash": block.get("parent_hash"), - "proposer": block.get("proposer"), - "timestamp": block.get("timestamp"), - "tx_count": block.get("tx_count", 0), - "state_root": block.get("state_root"), - "transactions": block.get("transactions", []), - "receipts": block.get("receipts", []) - } - success = await self.import_block(import_data) - if success: - logger.info(f"Imported block {height} from {endpoint}") - local_height = height - else: - logger.error(f"Failed to import block {height}") - break - else: - logger.error(f"Failed to fetch block {height} from {endpoint}") - break - - async def get_remote_mempool(self, endpoint: str) -> List[Dict[str, Any]]: - """Get mempool transactions from a remote node.""" - try: - response = await self.client.get(f"{endpoint.rstrip('/')}/mempool") - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Failed to get mempool from {endpoint}: {e}") - return [] - - async def get_local_mempool(self) -> List[Dict[str, Any]]: - """Get local mempool transactions.""" - try: - response = await self.client.get(f"{self.local_rpc_url}/rpc/mempool") - if response.status_code == 200: - return response.json() - except Exception as e: - logger.error(f"Failed to get local mempool: {e}") - return [] - - async def submit_transaction(self, tx_data: Dict[str, Any]) -> bool: - """Submit a transaction to the local node.""" - try: - response = await self.client.post( - f"{self.local_rpc_url}/rpc/transaction", - json=tx_data - ) - return response.status_code == 200 - except Exception as e: - logger.error(f"Failed to submit transaction: {e}") - return False - - async def sync_transactions(self) -> None: - """Sync transactions from remote mempools.""" - local_mempool = await self.get_local_mempool() - local_tx_hashes = {tx.get('hash') for tx in local_mempool} - - for endpoint in self.remote_endpoints: - remote_mempool = await self.get_remote_mempool(endpoint) - for tx in remote_mempool: - tx_hash = tx.get('hash') - if tx_hash and tx_hash not in local_tx_hashes: - success = await self.submit_transaction(tx) - if success: - logger.info(f"Imported transaction {tx_hash[:8]}... from {endpoint}") - - async def sync_loop(self) -> None: - """Main synchronization loop.""" - logger.info("Starting cross-site sync loop") - - while True: - try: - # Sync blocks - await self.sync_with_remotes() - - # Sync transactions - await self.sync_transactions() - - except Exception as e: - logger.error(f"Error in sync loop: {e}") - - await asyncio.sleep(self.poll_interval) - - async def start(self) -> None: - """Start the synchronization task.""" - if self.sync_task is None: - self.sync_task = asyncio.create_task(self.sync_loop()) - - async def stop(self) -> None: - """Stop the synchronization task.""" - if self.sync_task: - self.sync_task.cancel() - try: - await self.sync_task - except asyncio.CancelledError: - pass - self.sync_task = None - - await self.client.aclose() diff --git a/website/favicon.svg b/website/favicon.svg new file mode 100644 index 00000000..82791e7c --- /dev/null +++ b/website/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + AI +