refactor: consolidate environment configuration and add production genesis tooling
- Replace verbose .env.example with concise production-ready template - Add blockchain core, coordinator API, and marketplace web sections - Remove development/production split in favor of single config file - Add create-keystore command to genesis CLI for encrypted key generation - Add init-production command to initialize production chain DB from genesis - Add create_keystore helper function in scripts/keystore.py -
This commit is contained in:
113
.env.example
113
.env.example
@@ -1,63 +1,58 @@
|
|||||||
# AITBC Environment Configuration
|
# AITBC Central Environment Example
|
||||||
# SECURITY NOTICE: Use service-specific environment files
|
# SECURITY NOTICE: Use a secrets manager for production. Do not commit real secrets.
|
||||||
#
|
# Run: python config/security/environment-audit.py --format text
|
||||||
# For development, copy from:
|
|
||||||
# config/environments/development/coordinator.env
|
|
||||||
# config/environments/development/wallet-daemon.env
|
|
||||||
#
|
|
||||||
# For production, use AWS Secrets Manager and Kubernetes secrets
|
|
||||||
# Templates available in config/environments/production/
|
|
||||||
|
|
||||||
# =============================================================================
|
# =========================
|
||||||
# BASIC CONFIGURATION ONLY
|
# Blockchain core
|
||||||
# =============================================================================
|
# =========================
|
||||||
# Application Environment
|
chain_id=ait-mainnet
|
||||||
APP_ENV=development
|
supported_chains=ait-mainnet
|
||||||
DEBUG=false
|
rpc_bind_host=0.0.0.0
|
||||||
LOG_LEVEL=INFO
|
rpc_bind_port=8006
|
||||||
|
p2p_bind_host=0.0.0.0
|
||||||
|
p2p_bind_port=8005
|
||||||
|
proposer_id=aitbc1genesis
|
||||||
|
proposer_key=changeme_hex_private_key
|
||||||
|
keystore_path=/opt/aitbc/keystore
|
||||||
|
keystore_password_file=/opt/aitbc/keystore/.password
|
||||||
|
gossip_backend=broadcast
|
||||||
|
gossip_broadcast_url=redis://127.0.0.1:6379
|
||||||
|
db_path=/opt/aitbc/apps/blockchain-node/data/ait-mainnet/chain.db
|
||||||
|
mint_per_unit=0
|
||||||
|
coordinator_ratio=0.05
|
||||||
|
block_time_seconds=60
|
||||||
|
enable_block_production=true
|
||||||
|
|
||||||
# =============================================================================
|
# =========================
|
||||||
# SECURITY REQUIREMENTS
|
# Coordinator API
|
||||||
# =============================================================================
|
# =========================
|
||||||
# IMPORTANT: Do NOT store actual secrets in this file
|
APP_ENV=production
|
||||||
# Use AWS Secrets Manager for production
|
APP_HOST=127.0.0.1
|
||||||
# Generate secure keys with: openssl rand -hex 32
|
APP_PORT=8011
|
||||||
|
DATABASE__URL=sqlite:///./data/coordinator.db
|
||||||
|
BLOCKCHAIN_RPC_URL=http://127.0.0.1:8026
|
||||||
|
ALLOW_ORIGINS=["http://localhost:8011","http://localhost:8000","http://8026"]
|
||||||
|
JOB_TTL_SECONDS=900
|
||||||
|
HEARTBEAT_INTERVAL_SECONDS=10
|
||||||
|
HEARTBEAT_TIMEOUT_SECONDS=30
|
||||||
|
RATE_LIMIT_REQUESTS=60
|
||||||
|
RATE_LIMIT_WINDOW_SECONDS=60
|
||||||
|
CLIENT_API_KEYS=["client_prod_key_use_real_value"]
|
||||||
|
MINER_API_KEYS=["miner_prod_key_use_real_value"]
|
||||||
|
ADMIN_API_KEYS=["admin_prod_key_use_real_value"]
|
||||||
|
HMAC_SECRET=change_this_to_a_32_byte_random_secret
|
||||||
|
JWT_SECRET=change_this_to_another_32_byte_random_secret
|
||||||
|
|
||||||
# =============================================================================
|
# =========================
|
||||||
# SERVICE CONFIGURATION
|
# Marketplace Web
|
||||||
# =============================================================================
|
# =========================
|
||||||
# Choose your service configuration:
|
VITE_MARKETPLACE_DATA_MODE=live
|
||||||
# 1. Copy service-specific .env file from config/environments/
|
VITE_MARKETPLACE_API=/api
|
||||||
# 2. Fill in actual values (NEVER commit secrets)
|
VITE_MARKETPLACE_ENABLE_BIDS=true
|
||||||
# 3. Run: python config/security/environment-audit.py
|
VITE_MARKETPLACE_REQUIRE_AUTH=false
|
||||||
|
|
||||||
# =============================================================================
|
# =========================
|
||||||
# DEVELOPMENT QUICK START
|
# Notes
|
||||||
# =============================================================================
|
# =========================
|
||||||
# For quick development setup:
|
# For production: move secrets to a secrets manager and reference via secretRef
|
||||||
# cp config/environments/development/coordinator.env .env
|
# Validate config: python config/security/environment-audit.py --format text
|
||||||
# cp config/environments/development/wallet-daemon.env .env.wallet
|
|
||||||
#
|
|
||||||
# Then edit the copied files with your values
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# PRODUCTION DEPLOYMENT
|
|
||||||
# =============================================================================
|
|
||||||
# For production deployment:
|
|
||||||
# 1. Use AWS Secrets Manager for all sensitive values
|
|
||||||
# 2. Reference secrets as: secretRef:secret-name:key
|
|
||||||
# 3. Run security audit before deployment
|
|
||||||
# 4. Use templates in config/environments/production/
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# SECURITY VALIDATION
|
|
||||||
# =============================================================================
|
|
||||||
# Validate your configuration:
|
|
||||||
# python config/security/environment-audit.py --format text
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# FOR MORE INFORMATION
|
|
||||||
# =============================================================================
|
|
||||||
# See: config/security/secret-validation.yaml
|
|
||||||
# See: config/security/environment-audit.py
|
|
||||||
# See: config/environments/ directory
|
|
||||||
|
|||||||
@@ -9,12 +9,57 @@ from ..core.genesis_generator import GenesisGenerator, GenesisValidationError
|
|||||||
from ..core.config import MultiChainConfig, load_multichain_config
|
from ..core.config import MultiChainConfig, load_multichain_config
|
||||||
from ..models.chain import GenesisConfig
|
from ..models.chain import GenesisConfig
|
||||||
from ..utils import output, error, success
|
from ..utils import output, error, success
|
||||||
|
from .keystore import create_keystore_via_script
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def genesis():
|
def genesis():
|
||||||
"""Genesis block generation and management commands"""
|
"""Genesis block generation and management commands"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@genesis.command()
|
||||||
|
@click.option('--address', required=True, help='Wallet address (id) to create')
|
||||||
|
@click.option('--password-file', default='/opt/aitbc/data/keystore/.password', show_default=True, type=click.Path(exists=True, dir_okay=False), help='Path to password file')
|
||||||
|
@click.option('--output-dir', default='/opt/aitbc/data/keystore', show_default=True, help='Directory to write keystore file')
|
||||||
|
@click.option('--force', is_flag=True, help='Overwrite existing keystore file if present')
|
||||||
|
@click.pass_context
|
||||||
|
def create_keystore(ctx, address, password_file, output_dir, force):
|
||||||
|
"""Create an encrypted keystore for a genesis/treasury address."""
|
||||||
|
try:
|
||||||
|
create_keystore_via_script(address=address, password_file=password_file, output_dir=output_dir, force=force)
|
||||||
|
success(f"Created keystore for {address} at {output_dir}")
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Error creating keystore: {e}")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
|
||||||
|
@genesis.command(name="init-production")
|
||||||
|
@click.option('--chain-id', default='ait-mainnet', show_default=True, help='Chain ID to initialize')
|
||||||
|
@click.option('--genesis-file', default='data/genesis_prod.yaml', show_default=True, help='Path to genesis YAML (copy to /opt/aitbc/genesis_prod.yaml if needed)')
|
||||||
|
@click.option('--db', default='/opt/aitbc/data/ait-mainnet/chain.db', show_default=True, help='SQLite DB path')
|
||||||
|
@click.option('--force', is_flag=True, help='Overwrite existing DB (removes file if present)')
|
||||||
|
@click.pass_context
|
||||||
|
def init_production(ctx, chain_id, genesis_file, db, force):
|
||||||
|
"""Initialize production chain DB using genesis allocations."""
|
||||||
|
db_path = Path(db)
|
||||||
|
if db_path.exists() and force:
|
||||||
|
db_path.unlink()
|
||||||
|
python_bin = Path(__file__).resolve().parents[3] / 'apps' / 'blockchain-node' / '.venv' / 'bin' / 'python3'
|
||||||
|
cmd = [
|
||||||
|
str(python_bin),
|
||||||
|
str(Path(__file__).resolve().parents[3] / 'scripts' / 'init_production_genesis.py'),
|
||||||
|
'--chain-id', chain_id,
|
||||||
|
'--db', db,
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
success(f"Initialized production genesis for {chain_id} at {db}")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
error(f"Genesis init failed: {e}")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
@genesis.command()
|
@genesis.command()
|
||||||
@click.argument('config_file', type=click.Path(exists=True))
|
@click.argument('config_file', type=click.Path(exists=True))
|
||||||
@click.option('--output', '-o', 'output_file', help='Output file path')
|
@click.option('--output', '-o', 'output_file', help='Output file path')
|
||||||
|
|||||||
67
cli/aitbc_cli/commands/keystore.py
Normal file
67
cli/aitbc_cli/commands/keystore.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import click
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def _load_keystore_script():
|
||||||
|
"""Dynamically load the top-level scripts/keystore.py module."""
|
||||||
|
root = Path(__file__).resolve().parents[3] # /opt/aitbc
|
||||||
|
ks_path = root / "scripts" / "keystore.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("aitbc_scripts_keystore", ks_path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise ImportError(f"Unable to load keystore script from {ks_path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
def keystore():
|
||||||
|
"""Keystore operations (create wallets/keystores)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@keystore.command()
|
||||||
|
@click.option("--address", required=True, help="Wallet address (id) to create")
|
||||||
|
@click.option(
|
||||||
|
"--password-file",
|
||||||
|
default="/opt/aitbc/data/keystore/.password",
|
||||||
|
show_default=True,
|
||||||
|
type=click.Path(exists=True, dir_okay=False),
|
||||||
|
help="Path to password file",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--output",
|
||||||
|
default="/opt/aitbc/data/keystore",
|
||||||
|
show_default=True,
|
||||||
|
help="Directory to write keystore files",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force",
|
||||||
|
is_flag=True,
|
||||||
|
help="Overwrite existing keystore file if present",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def create(ctx, address: str, password_file: str, output: str, force: bool):
|
||||||
|
"""Create an encrypted keystore for the given address.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
aitbc keystore create --address aitbc1genesis
|
||||||
|
aitbc keystore create --address aitbc1treasury --password-file keystore/.password --output keystore
|
||||||
|
"""
|
||||||
|
pwd_path = Path(password_file)
|
||||||
|
with open(pwd_path, "r", encoding="utf-8") as f:
|
||||||
|
password = f.read().strip()
|
||||||
|
out_dir = Path(output) if output else Path("/opt/aitbc/data/keystore")
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
ks_module = _load_keystore_script()
|
||||||
|
ks_module.create_keystore(address=address, password=password, keystore_dir=out_dir, force=force)
|
||||||
|
click.echo(f"Created keystore for {address} at {out_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
# Helper so other commands (genesis) can reuse the same logic
|
||||||
|
def create_keystore_via_script(address: str, password_file: str = "/opt/aitbc/data/keystore/.password", output_dir: str = "/opt/aitbc/data/keystore", force: bool = False):
|
||||||
|
pwd = Path(password_file).read_text(encoding="utf-8").strip()
|
||||||
|
out_dir = Path(output_dir)
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
ks_module = _load_keystore_script()
|
||||||
|
ks_module.create_keystore(address=address, password=pwd, keystore_dir=out_dir, force=force)
|
||||||
@@ -46,6 +46,7 @@ from .commands.marketplace_advanced import advanced # Re-enabled after fixing r
|
|||||||
from .commands.swarm import swarm
|
from .commands.swarm import swarm
|
||||||
from .commands.chain import chain
|
from .commands.chain import chain
|
||||||
from .commands.genesis import genesis
|
from .commands.genesis import genesis
|
||||||
|
from .commands.keystore import keystore
|
||||||
from .commands.test_cli import test
|
from .commands.test_cli import test
|
||||||
from .commands.node import node
|
from .commands.node import node
|
||||||
from .commands.analytics import analytics
|
from .commands.analytics import analytics
|
||||||
@@ -256,6 +257,7 @@ cli.add_command(ai_group)
|
|||||||
cli.add_command(swarm)
|
cli.add_command(swarm)
|
||||||
cli.add_command(chain)
|
cli.add_command(chain)
|
||||||
cli.add_command(genesis)
|
cli.add_command(genesis)
|
||||||
|
cli.add_command(keystore)
|
||||||
cli.add_command(test)
|
cli.add_command(test)
|
||||||
cli.add_command(node)
|
cli.add_command(node)
|
||||||
cli.add_command(analytics)
|
cli.add_command(analytics)
|
||||||
|
|||||||
@@ -41,6 +41,28 @@ def encrypt_private_key(private_key_hex: str, password: str) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_keystore(address: str, password: str, keystore_dir: Path | str = "/opt/aitbc/keystore", force: bool = False) -> Path:
|
||||||
|
"""Create encrypted keystore file and return its path."""
|
||||||
|
keystore_dir = Path(keystore_dir)
|
||||||
|
keystore_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
out_file = keystore_dir / f"{address}.json"
|
||||||
|
|
||||||
|
if out_file.exists() and not force:
|
||||||
|
raise FileExistsError(f"Keystore file {out_file} exists. Use force=True to overwrite.")
|
||||||
|
|
||||||
|
private_key = secrets.token_hex(32)
|
||||||
|
encrypted = encrypt_private_key(private_key, password)
|
||||||
|
keystore = {
|
||||||
|
"address": address,
|
||||||
|
"crypto": encrypted,
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
out_file.write_text(json.dumps(keystore, indent=2))
|
||||||
|
os.chmod(out_file, 0o600)
|
||||||
|
return out_file
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="Generate encrypted keystore for an account")
|
parser = argparse.ArgumentParser(description="Generate encrypted keystore for an account")
|
||||||
parser.add_argument("address", help="Account address (e.g., aitbc1treasury)")
|
parser.add_argument("address", help="Account address (e.g., aitbc1treasury)")
|
||||||
|
|||||||
Reference in New Issue
Block a user