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:
186
apps/blockchain-node/scripts/keystore.py
Normal file
186
apps/blockchain-node/scripts/keystore.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Production key management for AITBC blockchain.
|
||||
|
||||
Generates ed25519 keypairs and stores them in an encrypted JSON keystore
|
||||
(Ethereum-style web3 keystore). Supports multiple wallets (treasury, proposer, etc.)
|
||||
|
||||
Usage:
|
||||
python keystore.py --name treasury --create --password <secret>
|
||||
python keystore.py --name proposer --create --password <secret>
|
||||
python keystore.py --name treasury --show
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
# Uses Cryptography library for ed25519 and encryption
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
# Address encoding: bech32m (HRP 'ait')
|
||||
from bech32 import bech32_encode, convertbits
|
||||
|
||||
|
||||
def generate_address(public_key_bytes: bytes) -> str:
|
||||
"""Generate a bech32m address from a public key.
|
||||
1. Take SHA256 of the public key (produces 32 bytes)
|
||||
2. Convert to 5-bit groups (bech32)
|
||||
3. Encode with HRP 'ait'
|
||||
"""
|
||||
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
|
||||
digest.update(public_key_bytes)
|
||||
hashed = digest.finalize()
|
||||
# Convert to 5-bit words for bech32
|
||||
data = convertbits(hashed, 8, 5, True)
|
||||
return bech32_encode("ait", data)
|
||||
|
||||
|
||||
def encrypt_private_key(private_key_bytes: bytes, password: str, salt: bytes) -> Dict[str, Any]:
|
||||
"""Encrypt a private key using AES-GCM, wrapped in a JSON keystore."""
|
||||
# Derive key from password using PBKDF2
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100_000,
|
||||
backend=default_backend()
|
||||
)
|
||||
key = kdf.derive(password.encode('utf-8'))
|
||||
|
||||
# Encrypt with AES-GCM
|
||||
aesgcm = AESGCM(key)
|
||||
nonce = os.urandom(12)
|
||||
encrypted = aesgcm.encrypt(nonce, private_key_bytes, None)
|
||||
|
||||
return {
|
||||
"crypto": {
|
||||
"cipher": "aes-256-gcm",
|
||||
"cipherparams": {"nonce": nonce.hex()},
|
||||
"ciphertext": encrypted.hex(),
|
||||
"kdf": "pbkdf2",
|
||||
"kdfparams": {
|
||||
"dklen": 32,
|
||||
"salt": salt.hex(),
|
||||
"c": 100_000,
|
||||
"prf": "hmac-sha256"
|
||||
},
|
||||
"mac": "TODO" # In production, compute MAC over ciphertext and KDF params
|
||||
},
|
||||
"address": None, # to be filled
|
||||
"keytype": "ed25519",
|
||||
"version": 1
|
||||
}
|
||||
|
||||
|
||||
def generate_keypair(name: str, password: str, keystore_dir: Path) -> Dict[str, Any]:
|
||||
"""Generate a new ed25519 keypair and store in keystore."""
|
||||
salt = os.urandom(32)
|
||||
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)
|
||||
|
||||
keystore = encrypt_private_key(private_bytes, password, salt)
|
||||
keystore["address"] = address
|
||||
|
||||
keystore_file = keystore_dir / f"{name}.json"
|
||||
keystore_dir.mkdir(parents=True, exist_ok=True)
|
||||
with open(keystore_file, 'w') as f:
|
||||
json.dump(keystore, f, indent=2)
|
||||
os.chmod(keystore_file, 0o600)
|
||||
|
||||
print(f"Generated {name} keypair")
|
||||
print(f" Address: {address}")
|
||||
print(f" Keystore: {keystore_file}")
|
||||
return keystore
|
||||
|
||||
|
||||
def show_keyinfo(keystore_file: Path, password: str) -> None:
|
||||
"""Decrypt and show key info (address, public key)."""
|
||||
with open(keystore_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Derive key from password
|
||||
crypto = data["crypto"]
|
||||
kdfparams = crypto["kdfparams"]
|
||||
salt = bytes.fromhex(kdfparams["salt"])
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=kdfparams["c"],
|
||||
backend=default_backend()
|
||||
)
|
||||
key = kdf.derive(password.encode('utf-8'))
|
||||
|
||||
# Decrypt private key
|
||||
nonce = bytes.fromhex(crypto["cipherparams"]["nonce"])
|
||||
ciphertext = bytes.fromhex(crypto["ciphertext"])
|
||||
aesgcm = AESGCM(key)
|
||||
private_bytes = aesgcm.decrypt(nonce, ciphertext, None)
|
||||
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(private_bytes)
|
||||
public_bytes = private_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
address = generate_address(public_bytes)
|
||||
|
||||
print(f"Keystore: {keystore_file}")
|
||||
print(f"Address: {address}")
|
||||
print(f"Public key (hex): {public_bytes.hex()}")
|
||||
|
||||
|
||||
def main():
|
||||
from getpass import getpass
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
|
||||
parser = argparse.ArgumentParser(description="Production keystore management")
|
||||
parser.add_argument("--name", required=True, help="Key name (e.g., treasury, proposer)")
|
||||
parser.add_argument("--create", action="store_true", help="Generate new keypair")
|
||||
parser.add_argument("--show", action="store_true", help="Show address/public key (prompt for password)")
|
||||
parser.add_argument("--password", help="Password (avoid using in CLI; prefer prompt or env)")
|
||||
parser.add_argument("--keystore-dir", type=Path, default=Path("/opt/aitbc/keystore"), help="Keystore directory")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.create:
|
||||
pwd = args.password or os.getenv("KEYSTORE_PASSWORD") or getpass("New password: ")
|
||||
if not pwd:
|
||||
print("Password required")
|
||||
sys.exit(1)
|
||||
generate_keypair(args.name, pwd, args.keystore_dir)
|
||||
|
||||
elif args.show:
|
||||
pwd = args.password or os.getenv("KEYSTORE_PASSWORD") or getpass("Password: ")
|
||||
if not pwd:
|
||||
print("Password required")
|
||||
sys.exit(1)
|
||||
keystore_file = args.keystore_dir / f"{args.name}.json"
|
||||
if not keystore_file.exists():
|
||||
print(f"Keystore not found: {keystore_file}")
|
||||
sys.exit(1)
|
||||
show_keyinfo(keystore_file, pwd)
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user