- 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>
203 lines
6.8 KiB
Python
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())
|