Files
aitbc/apps/blockchain-node/scripts/setup_production.py
aitbc1 337c68013c 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>
2026-03-16 09:24:07 +00:00

203 lines
6.8 KiB
Python

#!/usr/bin/env python3
"""
Production setup generator for AITBC blockchain.
Creates two wallets:
- aitbc1genesis: Treasury wallet holding all initial supply (1B AIT)
- aitbc1treasury: Spending wallet (for transactions, can receive from genesis)
No admin minting; fixed supply at genesis.
"""
from __future__ import annotations
import argparse
import json
import os
import secrets
import string
from pathlib import Path
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.backends import default_backend
from bech32 import bech32_encode, convertbits
def random_password(length: int = 32) -> str:
"""Generate a strong random password."""
alphabet = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(alphabet) for _ in range(length))
def generate_address(public_key_bytes: bytes) -> str:
"""Bech32m address with HRP 'ait'."""
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(public_key_bytes)
hashed = digest.finalize()
data = convertbits(hashed, 8, 5, True)
return bech32_encode("ait", data)
def encrypt_private_key(private_bytes: bytes, password: str, salt: bytes) -> dict:
"""Web3-style keystore encryption (AES-GCM + PBKDF2)."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100_000,
backend=default_backend()
)
key = kdf.derive(password.encode('utf-8'))
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ciphertext = aesgcm.encrypt(nonce, private_bytes, None)
return {
"crypto": {
"cipher": "aes-256-gcm",
"cipherparams": {"nonce": nonce.hex()},
"ciphertext": ciphertext.hex(),
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"salt": salt.hex(),
"c": 100_000,
"prf": "hmac-sha256"
},
"mac": "TODO" # In production, compute proper MAC
},
"address": None,
"keytype": "ed25519",
"version": 1
}
def generate_wallet(name: str, password: str, keystore_dir: Path) -> dict:
"""Generate ed25519 keypair and return wallet info."""
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
private_bytes = private_key.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
public_bytes = public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
address = generate_address(public_bytes)
salt = os.urandom(32)
keystore = encrypt_private_key(private_bytes, password, salt)
keystore["address"] = address
keystore_file = keystore_dir / f"{name}.json"
with open(keystore_file, 'w') as f:
json.dump(keystore, f, indent=2)
os.chmod(keystore_file, 0o600)
return {
"name": name,
"address": address,
"keystore_file": str(keystore_file),
"public_key_hex": public_bytes.hex()
}
def main():
parser = argparse.ArgumentParser(description="Production blockchain setup")
parser.add_argument("--base-dir", type=Path, default=Path("/opt/aitbc/apps/blockchain-node"),
help="Blockchain node base directory")
parser.add_argument("--chain-id", default="ait-mainnet", help="Chain ID")
parser.add_argument("--total-supply", type=int, default=1_000_000_000,
help="Total token supply (smallest units)")
args = parser.parse_args()
base_dir = args.base_dir
keystore_dir = base_dir / "keystore"
data_dir = base_dir / "data" / args.chain_id
keystore_dir.mkdir(parents=True, exist_ok=True)
data_dir.mkdir(parents=True, exist_ok=True)
# Generate strong random password and save it
password = random_password(32)
password_file = keystore_dir / ".password"
with open(password_file, 'w') as f:
f.write(password + "\n")
os.chmod(password_file, 0o600)
print(f"[setup] Generated keystore password and saved to {password_file}")
# Generate two wallets
wallets = []
for suffix in ["genesis", "treasury"]:
name = f"aitbc1{suffix}"
info = generate_wallet(name, password, keystore_dir)
# Store both the full name and suffix for lookup
info['suffix'] = suffix
wallets.append(info)
print(f"[setup] Created wallet: {name}")
print(f" Address: {info['address']}")
print(f" Keystore: {info['keystore_file']}")
# Create allocations: all supply to genesis wallet, treasury gets 0 (for spending from genesis)
genesis_wallet = next(w for w in wallets if w['suffix'] == 'genesis')
treasury_wallet = next(w for w in wallets if w['suffix'] == 'treasury')
allocations = [
{
"address": genesis_wallet["address"],
"balance": args.total_supply,
"nonce": 0
},
{
"address": treasury_wallet["address"],
"balance": 0,
"nonce": 0
}
]
allocations_file = data_dir / "allocations.json"
with open(allocations_file, 'w') as f:
json.dump(allocations, f, indent=2)
print(f"[setup] Wrote allocations to {allocations_file}")
# Create genesis.json via make_genesis script
import subprocess
genesis_file = data_dir / "genesis.json"
python_exec = base_dir / ".venv" / "bin" / "python"
if not python_exec.exists():
python_exec = "python3" # fallback
result = subprocess.run([
str(python_exec), str(base_dir / "scripts" / "make_genesis.py"),
"--output", str(genesis_file),
"--force",
"--allocations", str(allocations_file),
"--authorities", genesis_wallet["address"],
"--chain-id", args.chain_id
], capture_output=True, text=True, cwd=str(base_dir))
if result.returncode != 0:
print(f"[setup] Genesis generation failed: {result.stderr}")
return 1
print(f"[setup] Created genesis file at {genesis_file}")
print(result.stdout.strip())
print("\n[setup] Production setup complete!")
print(f" Chain ID: {args.chain_id}")
print(f" Total supply: {args.total_supply} (fixed)")
print(f" Genesis wallet: {genesis_wallet['address']}")
print(f" Treasury wallet: {treasury_wallet['address']}")
print(f" Keystore password: stored in {password_file}")
print("\n[IMPORTANT] Keep the keystore files and password secure!")
return 0
if __name__ == "__main__":
exit(main())