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:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user