✅ Complete Data Path Migration - Updated CLI commands to use /var/lib/aitbc/data - Updated production config to use /var/lib/aitbc/data - Updated systemd services to use /var/lib/aitbc/data - Removed all hardcoded /opt/aitbc/data references - Removed all hardcoded /opt/aitbc/production/data references ✅ System Compliance - All data paths now point to /var/lib/aitbc/data - No repository data paths remaining - FHS compliant data storage - Production-ready data architecture ✅ Git Repository Clean - No dynamic data in repository - All data paths updated to system location - .gitignore prevents future tracking - Clean git history 🚀 AITBC data architecture is now fully compliant with system standards!
476 lines
18 KiB
Python
Executable File
476 lines
18 KiB
Python
Executable File
"""Genesis block generation commands for AITBC CLI"""
|
|
|
|
import click
|
|
import json
|
|
import yaml
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from core.genesis_generator import GenesisGenerator, GenesisValidationError
|
|
from core.config import MultiChainConfig, load_multichain_config
|
|
from models.chain import GenesisConfig
|
|
from utils import output, error, success
|
|
from commands.keystore import create_keystore_via_script
|
|
import subprocess
|
|
import sys
|
|
|
|
@click.group()
|
|
def genesis():
|
|
"""Genesis block generation and management commands"""
|
|
pass
|
|
|
|
|
|
@genesis.command()
|
|
@click.option('--address', required=True, help='Wallet address (id) to create')
|
|
@click.option('--password-file', default='/var/lib/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='/var/lib/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='/var/lib/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()
|
|
@click.argument('config_file', type=click.Path(exists=True))
|
|
@click.option('--output', '-o', 'output_file', help='Output file path')
|
|
@click.option('--template', help='Use predefined template')
|
|
@click.option('--format', type=click.Choice(['json', 'yaml']), default='json', help='Output format')
|
|
@click.pass_context
|
|
def create(ctx, config_file, output_file, template, format):
|
|
"""Create genesis block from configuration"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
if template:
|
|
# Create from template
|
|
genesis_block = generator.create_from_template(template, config_file)
|
|
else:
|
|
# Create from configuration file
|
|
with open(config_file, 'r') as f:
|
|
config_data = yaml.safe_load(f)
|
|
|
|
genesis_config = GenesisConfig(**config_data['genesis'])
|
|
genesis_block = generator.create_genesis(genesis_config)
|
|
|
|
# Determine output file
|
|
if output_file is None:
|
|
chain_id = genesis_block.chain_id
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
output_file = f"genesis_{chain_id}_{timestamp}.{format}"
|
|
|
|
# Save genesis block
|
|
output_path = Path(output_file)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if format == 'yaml':
|
|
with open(output_path, 'w') as f:
|
|
yaml.dump(genesis_block.dict(), f, default_flow_style=False, indent=2)
|
|
else:
|
|
with open(output_path, 'w') as f:
|
|
json.dump(genesis_block.dict(), f, indent=2)
|
|
|
|
success("Genesis block created successfully!")
|
|
result = {
|
|
"Chain ID": genesis_block.chain_id,
|
|
"Chain Type": genesis_block.chain_type.value,
|
|
"Purpose": genesis_block.purpose,
|
|
"Name": genesis_block.name,
|
|
"Genesis Hash": genesis_block.hash,
|
|
"Output File": output_file,
|
|
"Format": format
|
|
}
|
|
|
|
output(result, ctx.obj.get('output_format', 'table'))
|
|
|
|
if genesis_block.privacy.visibility == "private":
|
|
success("Private chain genesis created! Use access codes to invite participants.")
|
|
|
|
except GenesisValidationError as e:
|
|
error(f"Genesis validation error: {str(e)}")
|
|
raise click.Abort()
|
|
except Exception as e:
|
|
error(f"Error creating genesis block: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('genesis_file', type=click.Path(exists=True))
|
|
@click.pass_context
|
|
def validate(ctx, genesis_file):
|
|
"""Validate genesis block integrity"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
# Load genesis block
|
|
genesis_path = Path(genesis_file)
|
|
if genesis_path.suffix.lower() in ['.yaml', '.yml']:
|
|
with open(genesis_path, 'r') as f:
|
|
genesis_data = yaml.safe_load(f)
|
|
else:
|
|
with open(genesis_path, 'r') as f:
|
|
genesis_data = json.load(f)
|
|
|
|
from models.chain import GenesisBlock
|
|
genesis_block = GenesisBlock(**genesis_data)
|
|
|
|
# Validate genesis block
|
|
validation_result = generator.validate_genesis(genesis_block)
|
|
|
|
if validation_result.is_valid:
|
|
success("Genesis block is valid!")
|
|
|
|
# Show validation details
|
|
checks_data = [
|
|
{
|
|
"Check": check,
|
|
"Status": "✓ Pass" if passed else "✗ Fail"
|
|
}
|
|
for check, passed in validation_result.checks.items()
|
|
]
|
|
|
|
output(checks_data, ctx.obj.get('output_format', 'table'), title="Validation Results")
|
|
else:
|
|
error("Genesis block validation failed!")
|
|
|
|
# Show errors
|
|
errors_data = [
|
|
{
|
|
"Error": error_msg
|
|
}
|
|
for error_msg in validation_result.errors
|
|
]
|
|
|
|
output(errors_data, ctx.obj.get('output_format', 'table'), title="Validation Errors")
|
|
|
|
# Show failed checks
|
|
failed_checks = [
|
|
{
|
|
"Check": check,
|
|
"Status": "✗ Fail"
|
|
}
|
|
for check, passed in validation_result.checks.items()
|
|
if not passed
|
|
]
|
|
|
|
if failed_checks:
|
|
output(failed_checks, ctx.obj.get('output_format', 'table'), title="Failed Checks")
|
|
|
|
raise click.Abort()
|
|
|
|
except Exception as e:
|
|
error(f"Error validating genesis block: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('genesis_file', type=click.Path(exists=True))
|
|
@click.pass_context
|
|
def info(ctx, genesis_file):
|
|
"""Show genesis block information"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
genesis_info = generator.get_genesis_info(genesis_file)
|
|
|
|
# Basic information
|
|
basic_info = {
|
|
"Chain ID": genesis_info["chain_id"],
|
|
"Chain Type": genesis_info["chain_type"],
|
|
"Purpose": genesis_info["purpose"],
|
|
"Name": genesis_info["name"],
|
|
"Description": genesis_info.get("description", "No description"),
|
|
"Created": genesis_info["created"],
|
|
"Genesis Hash": genesis_info["genesis_hash"],
|
|
"State Root": genesis_info["state_root"]
|
|
}
|
|
|
|
output(basic_info, ctx.obj.get('output_format', 'table'), title="Genesis Block Information")
|
|
|
|
# Configuration details
|
|
config_info = {
|
|
"Consensus Algorithm": genesis_info["consensus_algorithm"],
|
|
"Block Time": f"{genesis_info['block_time']}s",
|
|
"Gas Limit": f"{genesis_info['gas_limit']:,}",
|
|
"Gas Price": f"{genesis_info['gas_price'] / 1e9:.1f} gwei",
|
|
"Accounts Count": genesis_info["accounts_count"],
|
|
"Contracts Count": genesis_info["contracts_count"]
|
|
}
|
|
|
|
output(config_info, ctx.obj.get('output_format', 'table'), title="Configuration Details")
|
|
|
|
# Privacy settings
|
|
privacy_info = {
|
|
"Visibility": genesis_info["privacy_visibility"],
|
|
"Access Control": genesis_info["access_control"]
|
|
}
|
|
|
|
output(privacy_info, ctx.obj.get('output_format', 'table'), title="Privacy Settings")
|
|
|
|
# File information
|
|
file_info = {
|
|
"File Size": f"{genesis_info['file_size']:,} bytes",
|
|
"File Format": genesis_info["file_format"]
|
|
}
|
|
|
|
output(file_info, ctx.obj.get('output_format', 'table'), title="File Information")
|
|
|
|
except Exception as e:
|
|
error(f"Error getting genesis info: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('genesis_file', type=click.Path(exists=True))
|
|
@click.pass_context
|
|
def hash(ctx, genesis_file):
|
|
"""Calculate genesis hash"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
genesis_hash = generator.calculate_genesis_hash(genesis_file)
|
|
|
|
result = {
|
|
"Genesis File": genesis_file,
|
|
"Genesis Hash": genesis_hash
|
|
}
|
|
|
|
output(result, ctx.obj.get('output_format', 'table'))
|
|
|
|
except Exception as e:
|
|
error(f"Error calculating genesis hash: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
|
|
@click.pass_context
|
|
def templates(ctx, format):
|
|
"""List available genesis templates"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
templates = generator.list_templates()
|
|
|
|
if not templates:
|
|
output("No templates found", ctx.obj.get('output_format', 'table'))
|
|
return
|
|
|
|
if format == 'json':
|
|
output(templates, ctx.obj.get('output_format', 'table'))
|
|
else:
|
|
templates_data = [
|
|
{
|
|
"Template": template_name,
|
|
"Description": template_info["description"],
|
|
"Chain Type": template_info["chain_type"],
|
|
"Purpose": template_info["purpose"]
|
|
}
|
|
for template_name, template_info in templates.items()
|
|
]
|
|
|
|
output(templates_data, ctx.obj.get('output_format', 'table'), title="Available Templates")
|
|
|
|
except Exception as e:
|
|
error(f"Error listing templates: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('template_name')
|
|
@click.option('--output', '-o', help='Output file path')
|
|
@click.pass_context
|
|
def template_info(ctx, template_name, output):
|
|
"""Show detailed information about a template"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
templates = generator.list_templates()
|
|
|
|
if template_name not in templates:
|
|
error(f"Template {template_name} not found")
|
|
raise click.Abort()
|
|
|
|
template_info = templates[template_name]
|
|
|
|
info_data = {
|
|
"Template Name": template_name,
|
|
"Description": template_info["description"],
|
|
"Chain Type": template_info["chain_type"],
|
|
"Purpose": template_info["purpose"],
|
|
"File Path": template_info["file_path"]
|
|
}
|
|
|
|
output(info_data, ctx.obj.get('output_format', 'table'), title=f"Template Information: {template_name}")
|
|
|
|
# Show template content if requested
|
|
if output:
|
|
template_path = Path(template_info["file_path"])
|
|
if template_path.exists():
|
|
with open(template_path, 'r') as f:
|
|
template_content = f.read()
|
|
|
|
output_path = Path(output)
|
|
output_path.write_text(template_content)
|
|
success(f"Template content saved to {output}")
|
|
|
|
except Exception as e:
|
|
error(f"Error getting template info: {str(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('--force', is_flag=True, help='Overwrite existing DB (removes file if present)')
|
|
@click.pass_context
|
|
def init_production(ctx, chain_id, genesis_file, force):
|
|
"""Initialize production chain DB using genesis allocations."""
|
|
db_path = Path("/var/lib/aitbc/data") / chain_id / "chain.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,
|
|
]
|
|
try:
|
|
subprocess.run(cmd, check=True)
|
|
success(f"Initialized production genesis for {chain_id} at {db_path}")
|
|
except subprocess.CalledProcessError as e:
|
|
error(f"Genesis init failed: {e}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('chain_id')
|
|
@click.option('--format', type=click.Choice(['json', 'yaml']), default='json', help='Export format')
|
|
@click.option('--output', '-o', help='Output file path')
|
|
@click.pass_context
|
|
def export(ctx, chain_id, format, output):
|
|
"""Export genesis block for a chain"""
|
|
try:
|
|
config = load_multichain_config()
|
|
generator = GenesisGenerator(config)
|
|
|
|
genesis_data = generator.export_genesis(chain_id, format)
|
|
|
|
if output:
|
|
output_path = Path(output)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if format == 'yaml':
|
|
# Parse JSON and convert to YAML
|
|
parsed_data = json.loads(genesis_data)
|
|
with open(output_path, 'w') as f:
|
|
yaml.dump(parsed_data, f, default_flow_style=False, indent=2)
|
|
else:
|
|
output_path.write_text(genesis_data)
|
|
|
|
success(f"Genesis block exported to {output}")
|
|
else:
|
|
# Print to stdout
|
|
if format == 'yaml':
|
|
parsed_data = json.loads(genesis_data)
|
|
output(yaml.dump(parsed_data, default_flow_style=False, indent=2),
|
|
ctx.obj.get('output_format', 'table'))
|
|
else:
|
|
output(genesis_data, ctx.obj.get('output_format', 'table'))
|
|
|
|
except Exception as e:
|
|
error(f"Error exporting genesis block: {str(e)}")
|
|
raise click.Abort()
|
|
|
|
@genesis.command()
|
|
@click.argument('template_name')
|
|
@click.argument('output_file')
|
|
@click.option('--format', type=click.Choice(['json', 'yaml']), default='yaml', help='Output format')
|
|
@click.pass_context
|
|
def create_template(ctx, template_name, output_file, format):
|
|
"""Create a new genesis template"""
|
|
try:
|
|
# Basic template structure
|
|
template_data = {
|
|
"description": f"Genesis template for {template_name}",
|
|
"genesis": {
|
|
"chain_type": "topic",
|
|
"purpose": template_name,
|
|
"name": f"{template_name.title()} Chain",
|
|
"description": f"A {template_name} chain for AITBC",
|
|
"consensus": {
|
|
"algorithm": "pos",
|
|
"block_time": 5,
|
|
"max_validators": 100,
|
|
"authorities": []
|
|
},
|
|
"privacy": {
|
|
"visibility": "public",
|
|
"access_control": "open",
|
|
"require_invitation": False
|
|
},
|
|
"parameters": {
|
|
"max_block_size": 1048576,
|
|
"max_gas_per_block": 10000000,
|
|
"min_gas_price": 1000000000,
|
|
"block_reward": "2000000000000000000"
|
|
},
|
|
"accounts": [],
|
|
"contracts": []
|
|
}
|
|
}
|
|
|
|
output_path = Path(output_file)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if format == 'yaml':
|
|
with open(output_path, 'w') as f:
|
|
yaml.dump(template_data, f, default_flow_style=False, indent=2)
|
|
else:
|
|
with open(output_path, 'w') as f:
|
|
json.dump(template_data, f, indent=2)
|
|
|
|
success(f"Template created: {output_file}")
|
|
|
|
result = {
|
|
"Template Name": template_name,
|
|
"Output File": output_file,
|
|
"Format": format,
|
|
"Chain Type": template_data["genesis"]["chain_type"],
|
|
"Purpose": template_data["genesis"]["purpose"]
|
|
}
|
|
|
|
output(result, ctx.obj.get('output_format', 'table'))
|
|
|
|
except Exception as e:
|
|
error(f"Error creating template: {str(e)}")
|
|
raise click.Abort()
|