feat(blockchain): production genesis with encrypted keystore, remove admin minting

- Introduce production setup script: scripts/setup_production.py
  - Generates aitbc1genesis (treasury) and aitbc1treasury (spending) wallets
  - Encrypts keys via AES-GCM, stores password in keystore/.password (600)
  - Creates allocations.json and genesis.json with fixed total supply
  - Sets mint_per_unit=0 (no inflation)

- Update make_genesis.py:
  - Accept allocations file instead of single faucet
  - Use 'allocations' key in genesis (renamed from 'accounts')
  - Enforce mint_per_unit=0 in default params

- Remove admin mint endpoint:
  - Deleting MintFaucetRequest and /rpc/admin/mintFaucet from router.py
  - Removes faucet CLI command from cli/aitbc_cli/commands/blockchain.py

- RPC supply endpoint now computes total supply from genesis file (fixed)
- Validators endpoint derives list from trusted_proposers config

- Config enhancements (config.py):
  - Add keystore_path and keystore_password_file
  - Change mint_per_unit default to 0
  - main.py: Auto-load proposer private key from keystore into settings.proposer_key (hex) for future use

- Launcher scripts:
  - scripts/mainnet_up.sh: Loads .env.production, derives proposer_id from keystore if needed, starts node + RPC
  - scripts/devnet_up.sh: Updated to use new allocations-based genesis and proper proposer address

- Documentation:
  - Rewrite blockchain-node/README.md for production model (no faucet, keystore management, multi-chain)
  - Update MEMORY.md with production blockchain section

- Database: Multi-chain support already present via chain_id foreign keys.

This change makes the blockchain production‑ready: immutable supply, secure key storage, and removal of dev‑only admin functions.

Co-authored-by: Andreas Michael Fleckl <andreas@example.com>
This commit is contained in:
2026-03-16 09:24:07 +00:00
parent f11f277e71
commit 337c68013c
13 changed files with 974 additions and 211 deletions

View File

@@ -1,5 +1,10 @@
#!/usr/bin/env python3
"""Generate a deterministic devnet genesis file for the blockchain node."""
"""Generate a production-ready genesis file with fixed allocations.
This replaces the old devnet faucet model. Genesis now defines a fixed
initial coin supply allocated to specific addresses. No admin minting
is allowed; the total supply is immutable after genesis.
"""
from __future__ import annotations
@@ -7,75 +12,79 @@ import argparse
import json
import time
from pathlib import Path
from typing import List, Dict, Any
DEFAULT_GENESIS = {
"chain_id": "ait-devnet",
"timestamp": None, # populated at runtime
"params": {
"mint_per_unit": 1000,
"coordinator_ratio": 0.05,
"base_fee": 10,
"fee_per_byte": 1,
},
"accounts": [
{
"address": "ait1faucet000000000000000000000000000000000",
"balance": 1_000_000_000,
"nonce": 0,
}
],
"authorities": [
{
"address": "ait1devproposer000000000000000000000000000000",
"weight": 1,
}
],
# Chain parameters - these are on-chain economic settings
CHAIN_PARAMS = {
"mint_per_unit": 0, # No new minting after genesis
"coordinator_ratio": 0.05,
"base_fee": 10,
"fee_per_byte": 1,
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate devnet genesis data")
parser = argparse.ArgumentParser(description="Generate production genesis data")
parser.add_argument(
"--output",
type=Path,
default=Path("data/devnet/genesis.json"),
help="Path to write the generated genesis file (default: data/devnet/genesis.json)",
help="Path to write the genesis file",
)
parser.add_argument(
"--force",
action="store_true",
help="Overwrite the genesis file if it already exists.",
help="Overwrite existing genesis file",
)
parser.add_argument(
"--faucet-address",
default="ait1faucet000000000000000000000000000000000",
help="Address seeded with devnet funds.",
)
parser.add_argument(
"--faucet-balance",
type=int,
default=1_000_000_000,
help="Faucet balance in smallest units.",
"--allocations",
type=Path,
required=True,
help="JSON file mapping addresses to initial balances (smallest units)",
)
parser.add_argument(
"--authorities",
nargs="*",
default=["ait1devproposer000000000000000000000000000000"],
help="Authority addresses included in the genesis file.",
required=True,
help="List of PoA authority addresses (proposer/validators)",
)
parser.add_argument(
"--chain-id",
default="ait-devnet",
help="Chain ID (default: ait-devnet)",
)
return parser.parse_args()
def build_genesis(args: argparse.Namespace) -> dict:
genesis = json.loads(json.dumps(DEFAULT_GENESIS)) # deep copy via JSON
genesis["timestamp"] = int(time.time())
genesis["accounts"][0]["address"] = args.faucet_address
genesis["accounts"][0]["balance"] = args.faucet_balance
genesis["authorities"] = [
{"address": address, "weight": 1}
for address in args.authorities
def load_allocations(path: Path) -> List[Dict[str, Any]]:
"""Load address allocations from a JSON file.
Expected format:
[
{"address": "ait1...", "balance": 1000000000, "nonce": 0}
]
return genesis
"""
with open(path) as f:
data = json.load(f)
if not isinstance(data, list):
raise ValueError("allocations must be a list of objects")
# Validate required fields
for item in data:
if "address" not in item or "balance" not in item:
raise ValueError(f"Allocation missing required fields: {item}")
return data
def build_genesis(chain_id: str, allocations: List[Dict[str, Any]], authorities: List[str]) -> dict:
"""Construct the genesis block specification."""
timestamp = int(time.time())
return {
"chain_id": chain_id,
"timestamp": timestamp,
"params": CHAIN_PARAMS.copy(),
"allocations": allocations, # Renamed from 'accounts' to avoid confusion
"authorities": [
{"address": addr, "weight": 1} for addr in authorities
],
}
def write_genesis(path: Path, data: dict, force: bool) -> None:
@@ -88,8 +97,12 @@ def write_genesis(path: Path, data: dict, force: bool) -> None:
def main() -> None:
args = parse_args()
genesis = build_genesis(args)
allocations = load_allocations(args.allocations)
genesis = build_genesis(args.chain_id, allocations, args.authorities)
write_genesis(args.output, genesis, args.force)
total = sum(a["balance"] for a in allocations)
print(f"[genesis] Total supply: {total} (fixed, no future minting)")
print("[genesis] IMPORTANT: Keep the private keys for these addresses secure!")
if __name__ == "__main__":