diff --git a/.gitignore b/.gitignore index 5d1225da..67b1a6a1 100644 --- a/.gitignore +++ b/.gitignore @@ -150,6 +150,7 @@ out/ secrets/ credentials/ .secrets +.gitea_token.sh # =================== # Backup Files (organized) diff --git a/SETUP_PRODUCTION.md b/SETUP_PRODUCTION.md new file mode 100644 index 00000000..0ae5d73d --- /dev/null +++ b/SETUP_PRODUCTION.md @@ -0,0 +1,138 @@ +# Production Blockchain Setup Guide + +## Overview + +This guide sets up the AITBC blockchain in production mode with: +- Proper cryptographic key management (encrypted keystore) +- Fixed supply with predefined allocations (no admin minting) +- Secure configuration (localhost-only RPC, removed admin endpoints) +- Multi-chain support (devnet preserved) + +## Steps + +### 1. Generate Keystore for `aitbc1genesis` + +Run as `aitbc` user: + +```bash +sudo -u aitbc /opt/aitbc/apps/blockchain-node/.venv/bin/python /opt/aitbc/scripts/keystore.py aitbc1genesis --output-dir /opt/aitbc/keystore +``` + +- Enter a strong encryption password (store in password manager). +- **COPY** the printed private key (hex). Save it securely; you'll need it for `.env`. +- File: `/opt/aitbc/keystore/aitbc1genesis.json` (600) + +### 2. Generate Keystore for `aitbc1treasury` + +```bash +sudo -u aitbc /opt/aitbc/apps/blockchain-node/.venv/bin/python /opt/aitbc/scripts/keystore.py aitbc1treasury --output-dir /opt/aitbc/keystore +``` + +- Choose another strong password. +- **COPY** the printed private key. +- File: `/opt/aitbc/keystore/aitbc1treasury.json` (600) + +### 3. Initialize Production Database + +```bash +# Create data directory +sudo mkdir -p /opt/aitbc/data/ait-mainnet +sudo chown -R aitbc:aitbc /opt/aitbc/data/ait-mainnet + +# Run init script +export DB_PATH=/opt/aitbc/data/ait-mainnet/chain.db +export CHAIN_ID=ait-mainnet +sudo -E -u aitbc /opt/aitbc/apps/blockchain-node/.venv/bin/python /opt/aitbc/scripts/init_production_genesis.py --chain-id ait-mainnet --db-path "$DB_PATH" +``` + +Verify: + +```bash +sqlite3 /opt/aitbc/data/ait-mainnet/chain.db "SELECT address, balance FROM account ORDER BY balance DESC;" +``` + +Expected: 13 rows with balances from `ALLOCATIONS`. + +### 4. Configure `.env` for Production + +Edit `/opt/aitbc/apps/blockchain-node/.env`: + +```ini +CHAIN_ID=ait-mainnet +SUPPORTED_CHAINS=ait-mainnet +DB_PATH=./data/ait-mainnet/chain.db +PROPOSER_ID=aitbc1genesis +PROPOSER_KEY=0x +PROPOSER_INTERVAL_SECONDS=5 +BLOCK_TIME_SECONDS=2 + +RPC_BIND_HOST=127.0.0.1 +RPC_BIND_PORT=8006 +P2P_BIND_HOST=127.0.0.2 +P2P_BIND_PORT=8005 + +MEMPOOL_BACKEND=database +MIN_FEE=0 +GOSSIP_BACKEND=memory +``` + +Replace `` with the actual hex string (include `0x` prefix if present). + +### 5. Restart Services + +```bash +sudo systemctl daemon-reload +sudo systemctl restart aitbc-blockchain-node aitbc-blockchain-rpc +``` + +Check status: + +```bash +sudo systemctl status aitbc-blockchain-node +sudo journalctl -u aitbc-blockchain-node -f +``` + +### 6. Verify RPC + +Query the head: + +```bash +curl "http://127.0.0.1:8006/head?chain_id=ait-mainnet" | jq +``` + +Expected output: + +```json +{ + "height": 0, + "hash": "0x...", + "timestamp": "2025-01-01T00:00:00", + "tx_count": 0 +} +``` + +## Optional: Add Balance Query Endpoint + +If you need to check account balances via RPC, I can add a simple endpoint `/account/{address}`. Request it if needed. + +## Clean Up Devnet (Optional) + +To free resources, you can archive the old devnet DB: + +```bash +sudo mv /opt/aitbc/apps/blockchain-node/data/devnet /opt/aitbc/apps/blockchain-node/data/devnet.bak +``` + +## Notes + +- Admin minting (`/admin/mintFaucet`) has been removed. +- RPC is bound to localhost only; external access should go through a reverse proxy with TLS and API key. +- The `aitbc1treasury` account exists but cannot spend until wallet daemon integration is complete. +- All other service accounts are watch-only. Generate additional keystores if they need to sign. +- Back up the keystore files and encryption passwords immediately. + +## Troubleshooting + +- **Proposer not starting**: Check `PROPOSER_KEY` format (hex, with 0x prefix sometimes required). Ensure DB is initialized. +- **DB initialization error**: Verify `DB_PATH` points to a writable location and that the directory exists. +- **RPC unreachable**: Confirm RPC bound to 127.0.0.1:8006 and firewall allows local access. diff --git a/ai-memory/README.md b/ai-memory/README.md new file mode 100644 index 00000000..029d763f --- /dev/null +++ b/ai-memory/README.md @@ -0,0 +1,26 @@ +# AI Memory — Structured Knowledge for Autonomous Agents + +This directory implements a hierarchical memory architecture to improve agent coordination and recall. + +## Layers + +- **daily/** – chronological activity logs (append-only) +- **architecture/** – system design documents +- **decisions/** – recorded decisions (architectural, protocol) +- **failures/** – known failure patterns and debugging notes +- **knowledge/** – persistent technical knowledge (coding standards, dependencies, environment) +- **agents/** – agent-specific behavior and responsibilities + +## Usage Protocol + +Before starting work: +1. Read `architecture/system-overview.md` and relevant `knowledge/*` +2. Check `failures/` for known issues +3. Read latest `daily/YYYY-MM-DD.md` + +After completing work: +4. Append a summary to `daily/YYYY-MM-DD.md` +5. If new failure discovered, add to `failures/` +6. If architectural decision made, add to `decisions/` + +This structure prevents context loss and repeated mistakes across sessions. \ No newline at end of file diff --git a/ai-memory/agents/README.md b/ai-memory/agents/README.md new file mode 100644 index 00000000..ce3f3b92 --- /dev/null +++ b/ai-memory/agents/README.md @@ -0,0 +1,8 @@ +# Agent Memory + +Define behavior and specialization for each agent. + +Files: +- `agent-dev.md` – development agent +- `agent-review.md` – review agent +- `agent-ops.md` – operations agent \ No newline at end of file diff --git a/ai-memory/agents/agent-dev.md b/ai-memory/agents/agent-dev.md new file mode 100644 index 00000000..ce0274f0 --- /dev/null +++ b/ai-memory/agents/agent-dev.md @@ -0,0 +1,54 @@ +# Agent Observations Log + +Structured notes from agent activities, decisions, and outcomes. Used to build collective memory. + +## 2026-03-15 + +### Agent: aitbc1 + +**Claim System Implemented** (`scripts/claim-task.py`) +- Uses atomic Git branch creation (`claim/`) to lock tasks. +- Integrates with Gitea API to find unassigned issues with labels `task,bug,feature,good-first-task-for-agent`. +- Creates work branches with pattern `aitbc1/-`. +- State persisted in `/opt/aitbc/.claim-state.json`. + +**Monitoring System Enhanced** (`scripts/monitor-prs.py`) +- Auto-requests review from sibling (`@aitbc`) on my PRs. +- For sibling PRs: clones branch, runs `py_compile` on Python files, auto-approves if syntax passes; else requests changes. +- Releases claim branches when associated PRs merge or close. +- Checks CI statuses and reports failures. + +**Issues Created via API** +- Issue #3: "Add test suite for aitbc-core package" (task, good-first-task-for-agent) +- Issue #4: "Create README.md for aitbc-agent-sdk package" (task, good-first-task-for-agent) + +**PRs Opened** +- PR #5: `aitbc1/3-add-tests-for-aitbc-core` — comprehensive pytest suite for `aitbc.logging`. +- PR #6: `aitbc1/4-create-readme-for-agent-sdk` — enhanced README with usage examples. +- PR #10: `aitbc1/fix-imports-docs` — CLI import fixes and blockchain documentation. + +**Observations** +- Gitea API token must have `repository` scope; read-only limited. +- Pull requests show `requested_reviewers` as `null` unless explicitly set; agents should proactively request review to avoid ambiguity. +- Auto-approval based on syntax checks is a minimal validation; real safety requires CI passing. +- Claim branches must be deleted after PR merge to allow re-claiming if needed. +- Sibling agent (`aitbc`) also opened PR #11 for issue #7, indicating autonomous work. + +**Learnings** +- The `needs-design` label should be used for architectural changes before implementation. +- Brotherhood between agents benefits from explicit review requests and deterministic claim mechanism. +- Confidence scoring and task economy are next-level improvements to prioritize work. + +--- + +### Template for future entries + +``` +**Date**: YYYY-MM-DD +**Agent**: +**Action**: +**Outcome**: +**Issues Encountered**: +**Resolution**: +**Notes for other agents**: +``` diff --git a/ai-memory/agents/agent-ops.md b/ai-memory/agents/agent-ops.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/agents/agent-review.md b/ai-memory/agents/agent-review.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/architecture/README.md b/ai-memory/architecture/README.md new file mode 100644 index 00000000..42000b12 --- /dev/null +++ b/ai-memory/architecture/README.md @@ -0,0 +1,8 @@ +# Architecture Memory + +This layer documents the system's structure. + +Files: +- `system-overview.md` – high-level architecture +- `agent-roles.md` – responsibilities of each agent +- `infrastructure.md` – deployment layout, services, networks \ No newline at end of file diff --git a/ai-memory/architecture/system-overview.md b/ai-memory/architecture/system-overview.md new file mode 100644 index 00000000..e2b01d9b --- /dev/null +++ b/ai-memory/architecture/system-overview.md @@ -0,0 +1,49 @@ +# Architecture Overview + +This document describes the high-level structure of the AITBC project for agents implementing changes. + +## Rings of Stability + +The codebase is divided into layers with different change rules: + +- **Ring 0 (Core)**: `packages/py/aitbc-core/`, `packages/py/aitbc-sdk/` + - Spec required, high confidence threshold (>0.9), two approvals +- **Ring 1 (Platform)**: `apps/coordinator-api/`, `apps/blockchain-node/` + - Spec recommended, confidence >0.8 +- **Ring 2 (Application)**: `cli/`, `apps/analytics/` + - Normal PR, confidence >0.7 +- **Ring 3 (Experimental)**: `experiments/`, `playground/` + - Fast iteration allowed, confidence >0.5 + +## Key Subsystems + +### Coordinator API (`apps/coordinator-api/`) +- Central orchestrator for AI agents and compute marketplace +- Exposes REST API and manages provider registry, job dispatch +- Services live in `src/app/services/` and are imported via `app.services.*` +- Import pattern: add `apps/coordinator-api/src` to `sys.path`, then `from app.services import X` + +### CLI (`cli/aitbc_cli/`) +- User-facing command interface built with Click +- Bridges to coordinator-api services using proper package imports (no hardcoded paths) +- Located under `commands/` as separate modules: surveillance, ai_trading, ai_surveillance, advanced_analytics, regulatory, enterprise_integration + +### Blockchain Node (Brother Chain) (`apps/blockchain-node/`) +- Minimal asset-backed blockchain for compute receipts +- PoA consensus, transaction processing, RPC API +- Devnet: RPC on 8026, health on `/health`, gossip backend memory +- Configuration in `.env`; genesis generated by `scripts/make_genesis.py` + +### Packages +- `aitbc-core`: logging utilities, base classes (Ring 0) +- `aitbc-sdk`: Python SDK for interacting with Coordinator API (Ring 0) +- `aitbc-agent-sdk`: agent framework; `Agent.create()`, `ComputeProvider`, `ComputeConsumer` (Ring 0) +- `aitbc-crypto`: cryptographic primitives (Ring 0) + +## Conventions + +- Branches: `/-` +- Claim locks: `claim/` (short-lived) +- PR titles: imperative mood, reference issue with `Closes #` +- Tests: use pytest; aim for >80% coverage in modified modules +- CI: runs on Python 3.11, 3.12; goal is to support 3.13 diff --git a/ai-memory/daily/README.md b/ai-memory/daily/README.md new file mode 100644 index 00000000..4cdf0dad --- /dev/null +++ b/ai-memory/daily/README.md @@ -0,0 +1,21 @@ +# Daily Memory Directory + +This directory stores append-only daily logs of agent activities. + +Files are named `YYYY-MM-DD.md`. Each entry should include: +- date +- agent working (aitbc or aitbc1) +- tasks performed +- decisions made +- issues encountered + +Example: +``` +date: 2026-03-15 +agent: aitbc1 +event: deep code review +actions: + - scanned for bare excepts and print statements + - created issues #20, #23 + - replaced print with logging in services +``` \ No newline at end of file diff --git a/ai-memory/decisions/README.md b/ai-memory/decisions/README.md new file mode 100644 index 00000000..a3882712 --- /dev/null +++ b/ai-memory/decisions/README.md @@ -0,0 +1,12 @@ +# Decision Memory + +Records architectural and process decisions to avoid re-debating. + +Format: +``` +Decision: +Date: YYYY-MM-DD +Context: ... +Rationale: ... +Impact: ... +``` \ No newline at end of file diff --git a/ai-memory/decisions/architectural-decisions.md b/ai-memory/decisions/architectural-decisions.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/decisions/protocol-decisions.md b/ai-memory/decisions/protocol-decisions.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/failures/README.md b/ai-memory/failures/README.md new file mode 100644 index 00000000..d5c7abed --- /dev/null +++ b/ai-memory/failures/README.md @@ -0,0 +1,12 @@ +# Failure Memory + +Capture known failure patterns and resolutions. + +Structure: +``` +Failure: +Cause: ... +Resolution: ... +Detected: YYYY-MM-DD +``` +Agents should consult this before debugging. \ No newline at end of file diff --git a/ai-memory/failures/ci-failures.md b/ai-memory/failures/ci-failures.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/failures/debugging-notes.md b/ai-memory/failures/debugging-notes.md new file mode 100644 index 00000000..0b5a704d --- /dev/null +++ b/ai-memory/failures/debugging-notes.md @@ -0,0 +1,57 @@ +# Debugging Playbook + +Structured checklists for diagnosing common subsystem failures. + +## CLI Command Fails with ImportError + +1. Confirm service module exists: `ls apps/coordinator-api/src/app/services/` +2. Check `services/__init__.py` exists. +3. Verify command module adds `apps/coordinator-api/src` to `sys.path`. +4. Test import manually: + ```bash + python3 -c "import sys; sys.path.insert(0, 'apps/coordinator-api/src'); from app.services.trading_surveillance import start_surveillance" + ``` +5. If missing dependencies, install coordinator-api requirements. + +## Blockchain Node Not Starting + +1. Check virtualenv: `source apps/blockchain-node/.venv/bin/activate` +2. Verify database file exists: `apps/blockchain-node/data/chain.db` + - If missing, run genesis generation: `python scripts/make_genesis.py` +3. Check `.env` configuration (ports, keys). +4. Test RPC health: `curl http://localhost:8026/health` +5. Review logs: `tail -f apps/blockchain-node/logs/*.log` (if configured) + +## Package Installation Fails (pip) + +1. Ensure `README.md` exists in package root. +2. Check `pyproject.toml` for required fields: `name`, `version`, `description`. +3. Install dependencies first: `pip install -r requirements.txt` if present. +4. Try editable install: `pip install -e .` with verbose: `pip install -v -e .` + +## Git Push Permission Denied + +1. Verify SSH key added to Gitea account. +2. Confirm remote URL is SSH, not HTTPS. +3. Test connection: `ssh -T git@gitea.bubuit.net`. +4. Ensure token has `push` permission if using HTTPS. + +## CI Pipeline Not Running + +1. Check `.github/workflows/` exists and YAML syntax is valid. +2. Confirm branch protection allows CI. +3. Check Gitea Actions enabled (repository settings). +4. Ensure Python version matrix includes active versions (3.11, 3.12, 3.13). + +## Tests Fail with ImportError in aitbc-core + +1. Confirm package installed: `pip list | grep aitbc-core`. +2. If not installed: `pip install -e ./packages/py/aitbc-core`. +3. Ensure tests can import `aitbc.logging`: `python3 -c "from aitbc.logging import get_logger"`. + +## PR Cannot Be Merged (stuck) + +1. Check if all required approvals present. +2. Verify CI status is `success` on the PR head commit. +3. Ensure no merge conflicts (Gitea shows `mergeable: true`). +4. If outdated, rebase onto latest main and push. diff --git a/ai-memory/knowledge/README.md b/ai-memory/knowledge/README.md new file mode 100644 index 00000000..7c15fbf6 --- /dev/null +++ b/ai-memory/knowledge/README.md @@ -0,0 +1,9 @@ +# Knowledge Memory + +Persistent technical knowledge about the project. + +Files: +- `coding-standards.md` +- `dependencies.md` +- `environment.md` +- `repository-layout.md` \ No newline at end of file diff --git a/ai-memory/knowledge/dependencies.md b/ai-memory/knowledge/dependencies.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/knowledge/environment.md b/ai-memory/knowledge/environment.md new file mode 100644 index 00000000..e69de29b diff --git a/ai-memory/knowledge/repository-layout.md b/ai-memory/knowledge/repository-layout.md new file mode 100644 index 00000000..e69de29b diff --git a/apps/blockchain-node/.env.example b/apps/blockchain-node/.env.example index d4277c6f..64958516 100644 --- a/apps/blockchain-node/.env.example +++ b/apps/blockchain-node/.env.example @@ -1,4 +1,6 @@ # Blockchain Node Configuration +# For development use: chain_id=ait-devnet (includes faucet) +# For production-like testing: chain_id=ait-mainnet (no faucet) chain_id=ait-devnet supported_chains=ait-devnet diff --git a/apps/blockchain-node/src/aitbc_chain/rpc/router.py b/apps/blockchain-node/src/aitbc_chain/rpc/router.py index 9a0c2291..4fbd7250 100755 --- a/apps/blockchain-node/src/aitbc_chain/rpc/router.py +++ b/apps/blockchain-node/src/aitbc_chain/rpc/router.py @@ -61,10 +61,6 @@ class EstimateFeeRequest(BaseModel): 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(chain_id: str = "ait-devnet") -> Dict[str, Any]: @@ -530,23 +526,6 @@ async def estimate_fee(request: EstimateFeeRequest) -> Dict[str, Any]: } -@router.post("/admin/mintFaucet", summary="Mint devnet funds to an address") -async def mint_faucet(request: MintFaucetRequest, chain_id: str = "ait-devnet") -> Dict[str, Any]: - metrics_registry.increment("rpc_mint_faucet_total") - start = time.perf_counter() - with session_scope() as session: - account = session.get(Account, (chain_id, request.address)) - if account is None: - account = Account(chain_id=chain_id, 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 ImportBlockRequest(BaseModel): height: int diff --git a/apps/coordinator-api/src/app/main.py b/apps/coordinator-api/src/app/main.py index 73b8dea7..aa423f7e 100755 --- a/apps/coordinator-api/src/app/main.py +++ b/apps/coordinator-api/src/app/main.py @@ -1,3 +1,19 @@ +"""Coordinator API main entry point.""" +import sys +import os + +# Security: Lock sys.path to trusted locations to prevent malicious package shadowing +# Keep: site-packages under /opt/aitbc (venv), stdlib paths, and our app directory +_LOCKED_PATH = [] +for p in sys.path: + if 'site-packages' in p and '/opt/aitbc' in p: + _LOCKED_PATH.append(p) + elif 'site-packages' not in p and ('/usr/lib/python' in p or '/usr/local/lib/python' in p): + _LOCKED_PATH.append(p) + elif p.startswith('/opt/aitbc/apps/coordinator-api'): # our app code + _LOCKED_PATH.append(p) +sys.path = _LOCKED_PATH + from sqlalchemy.orm import Session from typing import Annotated from slowapi import Limiter, _rate_limit_exceeded_handler @@ -203,7 +219,6 @@ def create_app() -> FastAPI: docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, - # Custom OpenAPI config to handle Annotated[Session, Depends(get_session)] issues openapi_components={ "securitySchemes": { "ApiKeyAuth": { @@ -225,6 +240,22 @@ def create_app() -> FastAPI: ] ) + # API Key middleware (if configured) + required_key = os.getenv("COORDINATOR_API_KEY") + if required_key: + @app.middleware("http") + async def api_key_middleware(request: Request, call_next): + # Health endpoints are exempt + if request.url.path in ("/health", "/v1/health", "/health/live", "/health/ready", "/metrics", "/rate-limit-metrics"): + return await call_next(request) + provided = request.headers.get("X-Api-Key") + if provided != required_key: + return JSONResponse( + status_code=401, + content={"detail": "Invalid or missing API key"} + ) + return await call_next(request) + app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) diff --git a/apps/coordinator-api/src/app/services/secure_pickle.py b/apps/coordinator-api/src/app/services/secure_pickle.py index 2bbcc81e..bf65461a 100644 --- a/apps/coordinator-api/src/app/services/secure_pickle.py +++ b/apps/coordinator-api/src/app/services/secure_pickle.py @@ -4,6 +4,8 @@ Secure pickle deserialization utilities to prevent arbitrary code execution. import pickle import io +import importlib.util +import os from typing import Any # Safe classes whitelist: builtins and common types @@ -15,19 +17,76 @@ SAFE_MODULES = { 'datetime': {'datetime', 'date', 'time', 'timedelta', 'timezone'}, 'collections': {'OrderedDict', 'defaultdict', 'Counter', 'namedtuple'}, 'dataclasses': {'dataclass'}, - 'typing': {'Any', 'List', 'Dict', 'Tuple', 'Set', 'Optional', 'Union', 'TypeVar', 'Generic', 'NamedTuple', 'TypedDict'}, } +# Compute trusted origins: site-packages inside the venv and stdlib paths +_ALLOWED_ORIGINS = set() + +def _initialize_allowed_origins(): + """Build set of allowed module file origins (trusted locations).""" + # 1. All site-packages directories that are under the application venv + for entry in os.sys.path: + if 'site-packages' in entry and os.path.isdir(entry): + # Only include if it's inside /opt/aitbc/apps/coordinator-api/.venv or similar + if '/opt/aitbc' in entry: # restrict to our app directory + _ALLOWED_ORIGINS.add(os.path.realpath(entry)) + # 2. Standard library paths (typically without site-packages) + # We'll allow any origin that resolves to a .py file outside site-packages and not in user dirs + # But simpler: allow stdlib modules by checking they come from a path that doesn't contain 'site-packages' and is under /usr/lib/python3.13 + # We'll compute on the fly in find_class for simplicity. + +_initialize_allowed_origins() + class RestrictedUnpickler(pickle.Unpickler): """ Unpickler that restricts which classes can be instantiated. - Only allows classes from SAFE_MODULES whitelist. + Only allows classes from SAFE_MODULES whitelist and verifies module origin + to prevent shadowing by malicious packages. """ def find_class(self, module: str, name: str) -> Any: if module in SAFE_MODULES and name in SAFE_MODULES[module]: - return super().find_class(module, name) + # Verify module origin to prevent shadowing attacks + spec = importlib.util.find_spec(module) + if spec and spec.origin: + origin = os.path.realpath(spec.origin) + # Allow if it's from a trusted site-packages (our venv) + for allowed in _ALLOWED_ORIGINS: + if origin.startswith(allowed + os.sep) or origin == allowed: + return super().find_class(module, name) + # Allow standard library modules (outside site-packages and not in user/local dirs) + if 'site-packages' not in origin and ('/usr/lib/python' in origin or '/usr/local/lib/python' in origin): + return super().find_class(module, name) + # Reject if origin is unexpected (e.g., current working directory, /tmp, /home) + raise pickle.UnpicklingError( + f"Class {module}.{name} originates from untrusted location: {origin}" + ) + else: + # If we can't determine origin, deny (fail-safe) + raise pickle.UnpicklingError(f"Cannot verify origin for module {module}") raise pickle.UnpicklingError(f"Class {module}.{name} is not allowed for unpickling (security risk).") def safe_loads(data: bytes) -> Any: """Safely deserialize a pickle byte stream.""" return RestrictedUnpickler(io.BytesIO(data)).load() + +# ... existing code ... + +def _lock_sys_path(): + """Replace sys.path with a safe subset to prevent shadowing attacks.""" + import sys + if isinstance(sys.path, list): + trusted = [] + for p in sys.path: + # Keep site-packages under /opt/aitbc (our venv) + if 'site-packages' in p and '/opt/aitbc' in p: + trusted.append(p) + # Keep stdlib paths (no site-packages, under /usr/lib/python) + elif 'site-packages' not in p and ('/usr/lib/python' in p or '/usr/local/lib/python' in p): + trusted.append(p) + # Keep our application directory + elif p.startswith('/opt/aitbc/apps/coordinator-api'): + trusted.append(p) + sys.path = trusted + +# Lock sys.path immediately upon import to prevent later modifications +_lock_sys_path() diff --git a/apps/coordinator-api/src/app/services/translation_cache.py b/apps/coordinator-api/src/app/services/translation_cache.py new file mode 100644 index 00000000..406264d8 --- /dev/null +++ b/apps/coordinator-api/src/app/services/translation_cache.py @@ -0,0 +1,71 @@ +""" +Translation cache service with optional HMAC integrity protection. +""" + +import json +import hmac +import hashlib +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict, Any, Optional + +class TranslationCache: + def __init__(self, cache_file: str = "translation_cache.json", hmac_key: Optional[str] = None): + self.cache_file = Path(cache_file) + self.cache: Dict[str, Dict[str, Any]] = {} + self.last_updated: Optional[datetime] = None + self.hmac_key = hmac_key.encode() if hmac_key else None + self._load() + + def _load(self) -> None: + if not self.cache_file.exists(): + return + data = self.cache_file.read_bytes() + if self.hmac_key: + # Verify HMAC-SHA256(key || data) + stored = json.loads(data) + mac = bytes.fromhex(stored.pop("mac", "")) + expected = hmac.new(self.hmac_key, json.dumps(stored, separators=(",", ":")).encode(), hashlib.sha256).digest() + if not hmac.compare_digest(mac, expected): + raise ValueError("Translation cache HMAC verification failed") + data = json.dumps(stored).encode() + payload = json.loads(data) + self.cache = payload.get("cache", {}) + last_iso = payload.get("last_updated") + self.last_updated = datetime.fromisoformat(last_iso) if last_iso else None + + def _save(self) -> None: + payload = { + "cache": self.cache, + "last_updated": (self.last_updated or datetime.now(timezone.utc)).isoformat() + } + if self.hmac_key: + raw = json.dumps(payload, separators=(",", ":")).encode() + mac = hmac.new(self.hmac_key, raw, hashlib.sha256).digest() + payload["mac"] = mac.hex() + self.cache_file.write_text(json.dumps(payload, indent=2)) + + def get(self, source_text: str, source_lang: str, target_lang: str) -> Optional[str]: + key = f"{source_lang}:{target_lang}:{source_text}" + entry = self.cache.get(key) + if not entry: + return None + return entry["translation"] + + def set(self, source_text: str, source_lang: str, target_lang: str, translation: str) -> None: + key = f"{source_lang}:{target_lang}:{source_text}" + self.cache[key] = { + "translation": translation, + "timestamp": datetime.now(timezone.utc).isoformat() + } + self._save() + + def clear(self) -> None: + self.cache.clear() + self.last_updated = None + if self.cache_file.exists(): + self.cache_file.unlink() + + def size(self) -> int: + return len(self.cache) \ No newline at end of file diff --git a/dev/scripts/dev_heartbeat.py b/dev/scripts/dev_heartbeat.py new file mode 100644 index 00000000..d8738e8c --- /dev/null +++ b/dev/scripts/dev_heartbeat.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +""" +Dev Heartbeat: Periodic checks for /opt/aitbc development environment. +Outputs concise markdown summary. Exit 0 if clean, 1 if issues detected. +""" +import os +import json +import subprocess +import sys +from datetime import datetime, timedelta +from pathlib import Path + +REPO_ROOT = Path("/opt/aitbc") +LOGS_DIR = REPO_ROOT / "logs" + +def sh(cmd, cwd=REPO_ROOT): + """Run shell command, return (returncode, stdout).""" + result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True) + return result.returncode, result.stdout.strip() + +def check_git_status(): + """Return summary of uncommitted changes.""" + rc, out = sh("git status --porcelain") + if rc != 0 or not out: + return None + lines = out.splitlines() + changed = len(lines) + # categorize simply + modified = sum(1 for l in lines if l.startswith(' M') or l.startswith('M ')) + added = sum(1 for l in lines if l.startswith('A ')) + deleted = sum(1 for l in lines if l.startswith(' D') or l.startswith('D ')) + return {"changed": changed, "modified": modified, "added": added, "deleted": deleted, "preview": lines[:10]} + +def check_build_tests(): + """Quick build and test health check.""" + checks = [] + # 1) Poetry check (dependency resolution) + rc, out = sh("poetry check") + checks.append(("poetry check", rc == 0, out)) + # 2) Fast syntax check of CLI package + rc, out = sh("python -m py_compile cli/aitbc_cli/__main__.py") + checks.append(("cli syntax", rc == 0, out if rc != 0 else "OK")) + # 3) Minimal test run (dry-run or 1 quick test) + rc, out = sh("python -m pytest tests/ -v --collect-only 2>&1 | head -20") + tests_ok = rc == 0 + checks.append(("test discovery", tests_ok, out if not tests_ok else f"Collected {out.count('test') if 'test' in out else '?'} tests")) + all_ok = all(ok for _, ok, _ in checks) + return {"all_ok": all_ok, "details": checks} + +def check_logs_errors(hours=1): + """Scan logs for ERROR/WARNING in last N hours.""" + if not LOGS_DIR.exists(): + return None + errors = [] + warnings = [] + cutoff = datetime.now() - timedelta(hours=hours) + for logfile in LOGS_DIR.glob("*.log"): + try: + mtime = datetime.fromtimestamp(logfile.stat().st_mtime) + if mtime < cutoff: + continue + with open(logfile) as f: + for line in f: + if "ERROR" in line or "FATAL" in line: + errors.append(f"{logfile.name}: {line.strip()[:120]}") + elif "WARN" in line: + warnings.append(f"{logfile.name}: {line.strip()[:120]}") + except Exception: + continue + return {"errors": errors[:20], "warnings": warnings[:20], "total_errors": len(errors), "total_warnings": len(warnings)} + +def check_dependencies(): + """Check outdated packages via poetry.""" + rc, out = sh("poetry show --outdated --no-interaction") + if rc != 0 or not out: + return [] + # parse package lines + packages = [] + for line in out.splitlines()[2:]: # skip headers + parts = line.split() + if len(parts) >= 3: + packages.append({"name": parts[0], "current": parts[1], "latest": parts[2]}) + return packages + +def check_vulnerabilities(): + """Run security audits for Python and Node dependencies.""" + issues = [] + # Python: pip-audit (if available) + rc, out = sh("pip-audit --requirement <(poetry export --without-hashes) 2>&1", shell=True) + if rc == 0: + # No vulnerabilities + pass + else: + # pip-audit returns non-zero when vulns found; parse output for count + # Usually output contains lines with "Found X vulnerabilities" + if "vulnerabilities" in out.lower(): + issues.append(f"Python dependencies: vulnerabilities detected\n```\n{out[:2000]}\n```") + else: + # Command failed for another reason (maybe not installed) + pass + # Node: npm audit (if package.json exists) + if (REPO_ROOT / "package.json").exists(): + rc, out = sh("npm audit --json") + if rc != 0: + try: + audit = json.loads(out) + count = audit.get("metadata", {}).get("vulnerabilities", {}).get("total", 0) + if count > 0: + issues.append(f"Node dependencies: {count} vulnerabilities (npm audit)") + except: + issues.append("Node dependencies: npm audit failed to parse") + return issues + +def main(): + report = [] + issues = 0 + + # Git + git = check_git_status() + if git and git["changed"] > 0: + issues += 1 + report.append(f"### Git: {git['changed']} uncommitted changes\n") + if git["preview"]: + report.append("```\n" + "\n".join(git["preview"]) + "\n```") + else: + report.append("### Git: clean") + + # Build/Tests + bt = check_build_tests() + if not bt["all_ok"]: + issues += 1 + report.append("### Build/Tests: problems detected\n") + for label, ok, msg in bt["details"]: + status = "OK" if ok else "FAIL" + report.append(f"- **{label}**: {status}") + if not ok and msg: + report.append(f" ```\n{msg}\n```") + else: + report.append("### Build/Tests: OK") + + # Logs + logs = check_logs_errors() + if logs and logs["total_errors"] > 0: + issues += 1 + report.append(f"### Logs: {logs['total_errors']} recent errors (last hour)\n") + for e in logs["errors"][:10]: + report.append(f"- `{e}`") + if logs["total_errors"] > 10: + report.append(f"... and {logs['total_errors']-10} more") + elif logs and logs["total_warnings"] > 0: + # warnings non-blocking but included in report + report.append(f"### Logs: {logs['total_warnings']} recent warnings (last hour)") + else: + report.append("### Logs: no recent errors") + + # Dependencies + outdated = check_dependencies() + if outdated: + issues += 1 + report.append(f"### Dependencies: {len(outdated)} outdated packages\n") + for pkg in outdated[:10]: + report.append(f"- {pkg['name']}: {pkg['current']} → {pkg['latest']}") + if len(outdated) > 10: + report.append(f"... and {len(outdated)-10} more") + else: + report.append("### Dependencies: up to date") + + # Vulnerabilities + vulns = check_vulnerabilities() + if vulns: + issues += 1 + report.append("### Security: vulnerabilities detected\n") + for v in vulns: + report.append(f"- {v}") + else: + report.append("### Security: no known vulnerabilities (audit clean)") + + # Final output + header = f"# Dev Heartbeat — {datetime.now().strftime('%Y-%m-%d %H:%M UTC')}\n\n" + summary = f"**Issues:** {issues}\n\n" if issues > 0 else "**Status:** All checks passed.\n\n" + full_report = header + summary + "\n".join(report) + + print(full_report) + + # Exit code signals issues presence + sys.exit(1 if issues > 0 else 0) + +if __name__ == "__main__": + main() + diff --git a/genesis_prod.yaml b/genesis_prod.yaml new file mode 100644 index 00000000..adb7f6c8 --- /dev/null +++ b/genesis_prod.yaml @@ -0,0 +1,296 @@ +genesis: + chain_id: ait-mainnet + chain_type: enhanced + purpose: development-with-new-features + name: AITBC Mainnet + description: Enhanced development network with AI trading, surveillance, analytics, + and multi-chain features + timestamp: '2026-03-07T11:00:00Z' + parent_hash: '0x0000000000000000000000000000000000000000000000000000000000000000' + gas_limit: 15000000 + gas_price: 1000000000 + consensus: + algorithm: poa + validators: + - ait1devproposer000000000000000000000000000000 + - ait1aivalidator00000000000000000000000000000 + - ait1surveillance0000000000000000000000000000 + accounts: + - address: aitbc1genesis + balance: '10000000' + type: genesis + metadata: + purpose: Genesis account with initial supply + features: + - governance + - staking + - validation + - address: aitbc1treasury + balance: '5000000' + type: treasury + metadata: + purpose: Treasury for ecosystem rewards + features: + - rewards + - staking + - governance + - address: aitbc1aiengine + balance: '2000000' + type: service + metadata: + purpose: AI Trading Engine operational account + service_type: ai_trading_engine + features: + - trading + - analytics + - prediction + - address: aitbc1surveillance + balance: '1500000' + type: service + metadata: + purpose: AI Surveillance service account + service_type: ai_surveillance + features: + - monitoring + - risk_assessment + - compliance + - address: aitbc1analytics + balance: '1000000' + type: service + metadata: + purpose: Advanced Analytics service account + service_type: advanced_analytics + features: + - real_time_analytics + - reporting + - metrics + - address: aitbc1marketplace + balance: '2000000' + type: service + metadata: + purpose: Global Marketplace service account + service_type: global_marketplace + features: + - trading + - liquidity + - cross_chain + - address: aitbc1enterprise + balance: '3000000' + type: service + metadata: + purpose: Enterprise Integration service account + service_type: enterprise_api_gateway + features: + - api_gateway + - multi_tenant + - security + - address: aitbc1multimodal + balance: '1500000' + type: service + metadata: + purpose: Multi-modal AI service account + service_type: multimodal_agent + features: + - gpu_acceleration + - modality_optimization + - fusion + - address: aitbc1zkproofs + balance: '1000000' + type: service + metadata: + purpose: Zero-Knowledge Proofs service account + service_type: zk_proofs + features: + - zk_circuits + - verification + - privacy + - address: aitbc1crosschain + balance: '2000000' + type: service + metadata: + purpose: Cross-chain bridge service account + service_type: cross_chain_bridge + features: + - bridge + - atomic_swap + - reputation + - address: aitbc1developer1 + balance: '500000' + type: developer + metadata: + purpose: Primary developer testing account + permissions: + - full_access + - service_deployment + - address: aitbc1developer2 + balance: '300000' + type: developer + metadata: + purpose: Secondary developer testing account + permissions: + - testing + - debugging + - address: aitbc1tester + balance: '200000' + type: tester + metadata: + purpose: Automated testing account + permissions: + - testing_only + contracts: + - name: AITBCToken + address: '0x0000000000000000000000000000000000001000' + type: ERC20 + metadata: + symbol: AITBC-E + decimals: 18 + initial_supply: '21000000000000000000000000' + purpose: Enhanced network token with chain-specific isolation + - name: AISurveillanceRegistry + address: '0x0000000000000000000000000000000000001001' + type: Registry + metadata: + purpose: Registry for AI surveillance patterns and alerts + features: + - pattern_registration + - alert_management + - risk_scoring + - name: AnalyticsOracle + address: '0x0000000000000000000000000000000000001002' + type: Oracle + metadata: + purpose: Oracle for advanced analytics data feeds + features: + - price_feeds + - market_data + - performance_metrics + - name: CrossChainBridge + address: '0x0000000000000000000000000000000000001003' + type: Bridge + metadata: + purpose: Cross-chain bridge for asset transfers + features: + - atomic_swaps + - reputation_system + - chain_isolation + - name: EnterpriseGateway + address: '0x0000000000000000000000000000000000001004' + type: Gateway + metadata: + purpose: Enterprise API gateway with multi-tenant support + features: + - api_management + - tenant_isolation + - security + parameters: + block_time: 3 + max_block_size: 2097152 + min_stake: 1000 + max_validators: 100 + block_reward: '2000000000000000000' + stake_reward_rate: '0.05' + governance_threshold: '0.51' + surveillance_threshold: '0.75' + analytics_retention: 86400 + cross_chain_fee: '10000000000000000' + enterprise_min_stake: 10000 + privacy: + access_control: permissioned + require_invitation: false + visibility: public + encryption: enabled + zk_proofs: enabled + audit_logging: enabled + features: + ai_trading_engine: true + ai_surveillance: true + advanced_analytics: true + enterprise_integration: true + multi_modal_ai: true + zk_proofs: true + cross_chain_bridge: true + global_marketplace: true + adaptive_learning: true + performance_monitoring: true + services: + ai_trading_engine: + port: 8010 + enabled: true + config: + models: + - mean_reversion + - momentum + - arbitrage + risk_threshold: 0.02 + max_positions: 100 + ai_surveillance: + port: 8011 + enabled: true + config: + risk_models: + - isolation_forest + - neural_network + alert_threshold: 0.85 + retention_days: 30 + advanced_analytics: + port: 8012 + enabled: true + config: + indicators: + - rsi + - macd + - bollinger + - volume + update_interval: 60 + history_retention: 86400 + enterprise_gateway: + port: 8013 + enabled: true + config: + max_tenants: 1000 + rate_limit: 1000 + auth_required: true + multimodal_ai: + port: 8014 + enabled: true + config: + gpu_acceleration: true + modalities: + - text + - image + - audio + fusion_model: transformer_based + zk_proofs: + port: 8015 + enabled: true + config: + circuit_types: + - receipt + - identity + - compliance + verification_speed: fast + memory_optimization: true + network: + max_peers: 50 + min_peers: 5 + boot_nodes: + - ait1bootnode0000000000000000000000000000000:8008 + - ait1bootnode0000000000000000000000000000001:8008 + propagation_timeout: 30 + sync_mode: fast + governance: + voting_period: 604800 + execution_delay: 86400 + proposal_threshold: '1000000000000000000000000' + quorum_rate: '0.40' + emergency_pause: true + multi_signature: true + economics: + total_supply: '21000000000000000000000000' + inflation_rate: '0.02' + burn_rate: '0.01' + treasury_allocation: '0.20' + staking_allocation: '0.30' + ecosystem_allocation: '0.25' + team_allocation: '0.15' + community_allocation: '0.10' diff --git a/scripts/claim-task.py b/scripts/claim-task.py index 21097ea8..12804650 100755 --- a/scripts/claim-task.py +++ b/scripts/claim-task.py @@ -6,7 +6,7 @@ Uses Git branch atomic creation as a distributed lock to prevent duplicate work. import os import json import subprocess -from datetime import datetime +from datetime import datetime, timedelta REPO_DIR = '/opt/aitbc' STATE_FILE = '/opt/aitbc/.claim-state.json' @@ -16,6 +16,7 @@ MY_AGENT = os.getenv('AGENT_NAME', 'aitbc1') ISSUE_LABELS = ['security', 'bug', 'feature', 'refactor', 'task'] # priority order BONUS_LABELS = ['good-first-task-for-agent'] AVOID_LABELS = ['needs-design', 'blocked', 'needs-reproduction'] +CLAIM_TTL = timedelta(hours=2) # Stale claim timeout def query_api(path, method='GET', data=None): url = f"{API_BASE}/{path}" @@ -105,15 +106,36 @@ def create_work_branch(issue_number, title): return branch_name def main(): - now = datetime.utcnow().isoformat() + 'Z' - print(f"[{now}] Claim task cycle starting...") + now = datetime.utcnow() + print(f"[{now.isoformat()}Z] Claim task cycle starting...") state = load_state() current_claim = state.get('current_claim') + if current_claim: + claimed_at_str = state.get('claimed_at') + if claimed_at_str: + try: + # Convert 'Z' suffix to offset for fromisoformat + if claimed_at_str.endswith('Z'): + claimed_at_str = claimed_at_str[:-1] + '+00:00' + claimed_at = datetime.fromisoformat(claimed_at_str) + age = now - claimed_at + if age > CLAIM_TTL: + print(f"Claim for issue #{current_claim} is stale (age {age}). Releasing.") + # Try to delete remote claim branch + claim_branch = state.get('claim_branch', f'claim/{current_claim}') + subprocess.run(['git', 'push', 'origin', '--delete', claim_branch], + capture_output=True, cwd=REPO_DIR) + # Clear state + state = {'current_claim': None, 'claimed_at': None, 'work_branch': None} + save_state(state) + current_claim = None + except Exception as e: + print(f"Error checking claim age: {e}. Will attempt to proceed.") + if current_claim: print(f"Already working on issue #{current_claim} (branch {state.get('work_branch')})") - # Optional: could check if that PR has been merged/closed and release claim here return issues = get_open_unassigned_issues() diff --git a/scripts/init_production_genesis.py b/scripts/init_production_genesis.py new file mode 100644 index 00000000..d722cc9d --- /dev/null +++ b/scripts/init_production_genesis.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +""" +Initialize the production chain (ait-mainnet) with genesis allocations. +This script: +- Ensures the blockchain database is initialized +- Creates the genesis block (if missing) +- Populates account balances according to the production allocation +- Outputs the addresses and their balances +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import yaml +from datetime import datetime +from pathlib import Path + +# Add the blockchain node src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "apps/blockchain-node/src")) + +from aitbc_chain.config import settings as cfg +from aitbc_chain.database import init_db, session_scope +from aitbc_chain.models import Block, Account +from aitbc_chain.consensus.poa import PoAProposer, ProposerConfig +from aitbc_chain.mempool import init_mempool +import hashlib +from sqlmodel import select + +# Production allocations (loaded from genesis_prod.yaml if available, else fallback) +ALLOCATIONS = {} + + +def load_allocations() -> dict[str, int]: + yaml_path = Path("/opt/aitbc/genesis_prod.yaml") + if yaml_path.exists(): + import yaml + with yaml_path.open() as f: + data = yaml.safe_load(f) + allocations = {} + for acc in data.get("genesis", {}).get("accounts", []): + addr = acc["address"] + balance = int(acc["balance"]) + allocations[addr] = balance + return allocations + else: + # Fallback hardcoded + return { + "aitbc1genesis": 10_000_000, + "aitbc1treasury": 5_000_000, + "aitbc1aiengine": 2_000_000, + "aitbc1surveillance": 1_500_000, + "aitbc1analytics": 1_000_000, + "aitbc1marketplace": 2_000_000, + "aitbc1enterprise": 3_000_000, + "aitbc1multimodal": 1_500_000, + "aitbc1zkproofs": 1_000_000, + "aitbc1crosschain": 2_000_000, + "aitbc1developer1": 500_000, + "aitbc1developer2": 300_000, + "aitbc1tester": 200_000, + } + +ALLOCATIONS = load_allocations() + +# Authorities (proposers) for PoA +AUTHORITIES = ["aitbc1genesis"] + + +def compute_genesis_hash(chain_id: str, timestamp: datetime) -> str: + payload = f"{chain_id}|0|0x00|{timestamp.isoformat()}".encode() + return "0x" + hashlib.sha256(payload).hexdigest() + + +def ensure_genesis_block(chain_id: str) -> Block: + with session_scope() as session: + # Check if any block exists for this chain + head = session.exec(select(Block).where(Block.chain_id == chain_id).order_by(Block.height.desc()).limit(1)).first() + if head is not None: + print(f"[*] Chain already has block at height {head.height}") + return head + + # Create deterministic genesis timestamp + timestamp = datetime(2025, 1, 1, 0, 0, 0) + block_hash = compute_genesis_hash(chain_id, timestamp) + genesis = Block( + chain_id=chain_id, + height=0, + hash=block_hash, + parent_hash="0x00", + proposer="genesis", + timestamp=timestamp, + tx_count=0, + state_root=None, + ) + session.add(genesis) + session.commit() + print(f"[+] Created genesis block: height=0, hash={block_hash}") + return genesis + + +def seed_accounts(chain_id: str) -> None: + with session_scope() as session: + for address, balance in ALLOCATIONS.items(): + account = session.get(Account, (chain_id, address)) + if account is None: + account = Account(chain_id=chain_id, address=address, balance=balance, nonce=0) + session.add(account) + print(f"[+] Created account {address} with balance {balance}") + else: + # Already exists; ensure balance matches if we want to enforce + if account.balance != balance: + account.balance = balance + print(f"[~] Updated account {address} balance to {balance}") + session.commit() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--chain-id", default="ait-mainnet", help="Chain ID to initialize") + parser.add_argument("--db-path", type=Path, help="Path to SQLite database (overrides config)") + args = parser.parse_args() + + # Override environment for config + os.environ["CHAIN_ID"] = args.chain_id + if args.db_path: + os.environ["DB_PATH"] = str(args.db_path) + + from aitbc_chain.config import Settings + settings = Settings() + + print(f"[*] Initializing database at {settings.db_path}") + init_db() + print("[*] Database initialized") + + # Ensure mempool DB exists (though not needed for genesis) + mempool_path = settings.db_path.parent / "mempool.db" + init_mempool(backend="database", db_path=str(mempool_path), max_size=10000, min_fee=0) + print(f"[*] Mempool initialized at {mempool_path}") + + # Create genesis block + ensure_genesis_block(args.chain_id) + + # Seed accounts + seed_accounts(args.chain_id) + + print("\n[+] Production genesis initialization complete.") + print(f"[!] Next steps:") + print(f" 1) Generate keystore for aitbc1genesis and aitbc1treasury using scripts/keystore.py") + print(f" 2) Update .env with CHAIN_ID={args.chain_id} and PROPOSER_KEY=") + print(f" 3) Restart the blockchain node.") + + +if __name__ == "__main__": + main() diff --git a/scripts/keystore.py b/scripts/keystore.py new file mode 100644 index 00000000..b37d622a --- /dev/null +++ b/scripts/keystore.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Keystore management for AITBC production keys. +Generates a random private key and encrypts it with a password using Fernet (AES-128). +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import os +import secrets +from datetime import datetime +from pathlib import Path + +from cryptography.fernet import Fernet + + +def derive_key(password: str, salt: bytes = b"") -> bytes: + """Derive a 32-byte key from the password using SHA-256.""" + if not salt: + salt = secrets.token_bytes(16) + # Simple KDF: hash(password + salt) + dk = hashlib.sha256(password.encode() + salt).digest() + return base64.urlsafe_b64encode(dk), salt + + +def encrypt_private_key(private_key_hex: str, password: str) -> dict: + """Encrypt a hex-encoded private key with Fernet, returning a keystore dict.""" + key, salt = derive_key(password) + f = Fernet(key) + token = f.encrypt(private_key_hex.encode()) + return { + "cipher": "fernet", + "cipherparams": {"salt": base64.b64encode(salt).decode()}, + "ciphertext": base64.b64encode(token).decode(), + "kdf": "sha256", + "kdfparams": {"dklen": 32, "salt": base64.b64encode(salt).decode()}, + } + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate encrypted keystore for an account") + parser.add_argument("address", help="Account address (e.g., aitbc1treasury)") + parser.add_argument("--output-dir", type=Path, default=Path("/opt/aitbc/keystore"), help="Keystore directory") + parser.add_argument("--force", action="store_true", help="Overwrite existing keystore file") + parser.add_argument("--password", help="Encryption password (or read from KEYSTORE_PASSWORD / keystore/.password)") + args = parser.parse_args() + + out_dir = args.output_dir + out_dir.mkdir(parents=True, exist_ok=True) + out_file = out_dir / f"{args.address}.json" + + if out_file.exists() and not args.force: + print(f"Keystore file {out_file} exists. Use --force to overwrite.") + return + + # Determine password: CLI > env var > password file + password = args.password + if not password: + password = os.getenv("KEYSTORE_PASSWORD") + if not password: + pw_file = Path("/opt/aitbc/keystore/.password") + if pw_file.exists(): + password = pw_file.read_text().strip() + if not password: + print("No password provided. Set KEYSTORE_PASSWORD, pass --password, or create /opt/aitbc/keystore/.password") + sys.exit(1) + + print(f"Generating keystore for {args.address}...") + private_key = secrets.token_hex(32) + print(f"Private key (hex): {private_key}") + print("** SAVE THIS KEY SECURELY ** (It cannot be recovered from the encrypted file without the password)") + + encrypted = encrypt_private_key(private_key, password) + keystore = { + "address": args.address, + "crypto": encrypted, + "created_at": datetime.utcnow().isoformat() + "Z", + } + + out_file.write_text(json.dumps(keystore, indent=2)) + os.chmod(out_file, 0o600) + print(f"[+] Keystore written to {out_file}") + print(f"[!] Keep the password safe. Without it, the private key cannot be recovered.") + + +if __name__ == "__main__": + main() diff --git a/scripts/run_production_node.py b/scripts/run_production_node.py new file mode 100644 index 00000000..2c68cfa9 --- /dev/null +++ b/scripts/run_production_node.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" +Production launcher for AITBC blockchain node. +Sets up environment, initializes genesis if needed, and starts the node. +""" + +from __future__ import annotations + +import os +import sys +import subprocess +from pathlib import Path + +# Configuration +CHAIN_ID = "ait-mainnet" +DATA_DIR = Path("/opt/aitbc/data/ait-mainnet") +DB_PATH = DATA_DIR / "chain.db" +KEYS_DIR = Path("/opt/aitbc/keystore") + +# Check for proposer key in keystore +PROPOSER_KEY_FILE = KEYS_DIR / "aitbc1genesis.json" +if not PROPOSER_KEY_FILE.exists(): + print(f"[!] Proposer keystore not found at {PROPOSER_KEY_FILE}") + print(" Run scripts/keystore.py to generate it first.") + sys.exit(1) + +# Set environment variables +os.environ["CHAIN_ID"] = CHAIN_ID +os.environ["SUPPORTED_CHAINS"] = CHAIN_ID +os.environ["DB_PATH"] = str(DB_PATH) +os.environ["PROPOSER_ID"] = "aitbc1genesis" +# PROPOSER_KEY will be read from keystore by the node? Currently .env expects hex directly. +# We can read the keystore, decrypt, and set PROPOSER_KEY, but the node doesn't support that out of box. +# So we require that PROPOSER_KEY is set in .env file manually after key generation. +# This script will check for PROPOSER_KEY env var or fail with instructions. +if not os.getenv("PROPOSER_KEY"): + print("[!] PROPOSER_KEY environment variable not set.") + print(" Please edit /opt/aitbc/apps/blockchain-node/.env and set PROPOSER_KEY to the hex private key of aitbc1genesis.") + sys.exit(1) + +# Ensure data directory +DATA_DIR.mkdir(parents=True, exist_ok=True) + +# Optionally initialize genesis if DB doesn't exist +if not DB_PATH.exists(): + print("[*] Database not found. Initializing production genesis...") + result = subprocess.run([ + sys.executable, + "/opt/aitbc/scripts/init_production_genesis.py", + "--chain-id", CHAIN_ID, + "--db-path", str(DB_PATH) + ], check=False) + if result.returncode != 0: + print("[!] Genesis initialization failed. Aborting.") + sys.exit(1) + +# Start the node +print(f"[*] Starting blockchain node for chain {CHAIN_ID}...") +# Change to the blockchain-node directory (since .env and uvicorn expect relative paths) +os.chdir("/opt/aitbc/apps/blockchain-node") +# Use the virtualenv Python +venv_python = Path("/opt/aitbc/apps/blockchain-node/.venv/bin/python") +if not venv_python.exists(): + print(f"[!] Virtualenv not found at {venv_python}") + sys.exit(1) + +# Exec uvicorn +os.execv(str(venv_python), [str(venv_python), "-m", "uvicorn", "aitbc_chain.app:app", "--host", "127.0.0.1", "--port", "8006"]) diff --git a/scripts/setup_production.py b/scripts/setup_production.py new file mode 100644 index 00000000..6eaeb2ee --- /dev/null +++ b/scripts/setup_production.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +""" +Full production setup: +- Generate keystore password file +- Generate encrypted keystores for aitbc1genesis and aitbc1treasury +- Initialize production database with allocations +- Configure blockchain node .env for ait-mainnet +- Restart services +""" + +import os +import subprocess +import sys +from pathlib import Path + +# Configuration +CHAIN_ID = "ait-mainnet" +DATA_DIR = Path("/opt/aitbc/data/ait-mainnet") +DB_PATH = DATA_DIR / "chain.db" +KEYS_DIR = Path("/opt/aitbc/keystore") +PASSWORD_FILE = KEYS_DIR / ".password" +NODE_VENV = Path("/opt/aitbc/apps/blockchain-node/.venv/bin/python") +NODE_ENV = Path("/opt/aitbc/apps/blockchain-node/.env") +SERVICE_NODE = "aitbc-blockchain-node" +SERVICE_RPC = "aitbc-blockchain-rpc" + +def run(cmd, check=True, capture_output=False): + print(f"+ {cmd}") + if capture_output: + result = subprocess.run(cmd, shell=True, check=check, capture_output=True, text=True) + else: + result = subprocess.run(cmd, shell=True, check=check) + return result + +def main(): + if os.geteuid() != 0: + print("Run as root (sudo)") + sys.exit(1) + + # 1. Keystore directory and password + run(f"mkdir -p {KEYS_DIR}") + run(f"chown -R aitbc:aitbc {KEYS_DIR}") + if not PASSWORD_FILE.exists(): + run(f"openssl rand -hex 32 > {PASSWORD_FILE}") + run(f"chmod 600 {PASSWORD_FILE}") + os.environ["KEYSTORE_PASSWORD"] = PASSWORD_FILE.read_text().strip() + + # 2. Generate keystores + print("\n=== Generating keystore for aitbc1genesis ===") + result = run( + f"sudo -u aitbc {NODE_VENV} /opt/aitbc/scripts/keystore.py aitbc1genesis --output-dir {KEYS_DIR} --force", + capture_output=True + ) + print(result.stdout) + genesis_priv = None + for line in result.stdout.splitlines(): + if "Private key (hex):" in line: + genesis_priv = line.split(":",1)[1].strip() + break + if not genesis_priv: + print("ERROR: Could not extract genesis private key") + sys.exit(1) + (KEYS_DIR / "genesis_private_key.txt").write_text(genesis_priv) + os.chmod(KEYS_DIR / "genesis_private_key.txt", 0o600) + + print("\n=== Generating keystore for aitbc1treasury ===") + result = run( + f"sudo -u aitbc {NODE_VENV} /opt/aitbc/scripts/keystore.py aitbc1treasury --output-dir {KEYS_DIR} --force", + capture_output=True + ) + print(result.stdout) + treasury_priv = None + for line in result.stdout.splitlines(): + if "Private key (hex):" in line: + treasury_priv = line.split(":",1)[1].strip() + break + if not treasury_priv: + print("ERROR: Could not extract treasury private key") + sys.exit(1) + (KEYS_DIR / "treasury_private_key.txt").write_text(treasury_priv) + os.chmod(KEYS_DIR / "treasury_private_key.txt", 0o600) + + # 3. Data directory + run(f"mkdir -p {DATA_DIR}") + run(f"chown -R aitbc:aitbc {DATA_DIR}") + + # 4. Initialize DB + os.environ["DB_PATH"] = str(DB_PATH) + os.environ["CHAIN_ID"] = CHAIN_ID + run(f"sudo -E -u aitbc {NODE_VENV} /opt/aitbc/scripts/init_production_genesis.py --chain-id {CHAIN_ID} --db-path {DB_PATH}") + + # 5. Write .env for blockchain node + env_content = f"""CHAIN_ID={CHAIN_ID} +SUPPORTED_CHAINS={CHAIN_ID} +DB_PATH=./data/ait-mainnet/chain.db +PROPOSER_ID=aitbc1genesis +PROPOSER_KEY=0x{genesis_priv} +PROPOSER_INTERVAL_SECONDS=5 +BLOCK_TIME_SECONDS=2 + +RPC_BIND_HOST=127.0.0.1 +RPC_BIND_PORT=8006 +P2P_BIND_HOST=127.0.0.2 +P2P_BIND_PORT=8005 + +MEMPOOL_BACKEND=database +MIN_FEE=0 +GOSSIP_BACKEND=memory +""" + NODE_ENV.write_text(env_content) + os.chmod(NODE_ENV, 0o644) + print(f"[+] Updated {NODE_ENV}") + + # 6. Restart services + run("systemctl daemon-reload") + run(f"systemctl restart {SERVICE_NODE} {SERVICE_RPC}") + + print("\n[+] Production setup complete!") + print(f"[+] Verify with: curl 'http://127.0.0.1:8006/head?chain_id={CHAIN_ID}' | jq") + print(f"[+] Keystore files in {KEYS_DIR} (encrypted, 600)") + print(f"[+] Private keys saved in {KEYS_DIR}/genesis_private_key.txt and treasury_private_key.txt (keep secure!)") + +if __name__ == "__main__": + main() diff --git a/systemd/aitbc-blockchain-rpc.service b/systemd/aitbc-blockchain-rpc.service index b25177bf..ab14d77a 100644 --- a/systemd/aitbc-blockchain-rpc.service +++ b/systemd/aitbc-blockchain-rpc.service @@ -7,7 +7,7 @@ Type=simple User=aitbc WorkingDirectory=/opt/aitbc/apps/blockchain-node Environment=PYTHONPATH=/opt/aitbc/apps/blockchain-node/src:/opt/aitbc/apps/blockchain-node/scripts -ExecStart=/opt/aitbc/apps/blockchain-node/.venv/bin/python -m uvicorn aitbc_chain.app:app --host 0.0.0.0 --port 8006 --log-level info +ExecStart=/opt/aitbc/apps/blockchain-node/.venv/bin/python -m uvicorn aitbc_chain.app:app --host 127.0.0.1 --port 8006 --log-level info Restart=always RestartSec=5 StandardOutput=journal