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:
2026-03-16 09:24:07 +00:00
parent f11f277e71
commit 337c68013c
13 changed files with 974 additions and 211 deletions

View File

@@ -61,11 +61,6 @@ class EstimateFeeRequest(BaseModel):
payload: Dict[str, Any] = Field(default_factory=dict)
class MintFaucetRequest(BaseModel):
address: str
amount: int = Field(gt=0)
@router.get("/head", summary="Get current chain head")
async def get_head(chain_id: str = "ait-devnet") -> Dict[str, Any]:
metrics_registry.increment("rpc_get_head_total")
@@ -530,24 +525,6 @@ async def estimate_fee(request: EstimateFeeRequest) -> Dict[str, Any]:
}
@router.post("/admin/mintFaucet", summary="Mint devnet funds to an address")
async def mint_faucet(request: MintFaucetRequest, chain_id: str = "ait-devnet") -> Dict[str, Any]:
metrics_registry.increment("rpc_mint_faucet_total")
start = time.perf_counter()
with session_scope() as session:
account = session.get(Account, (chain_id, request.address))
if account is None:
account = Account(chain_id=chain_id, address=request.address, balance=request.amount)
session.add(account)
else:
account.balance += request.amount
session.commit()
updated_balance = account.balance
metrics_registry.increment("rpc_mint_faucet_success_total")
metrics_registry.observe("rpc_mint_faucet_duration_seconds", time.perf_counter() - start)
return {"address": request.address, "balance": updated_balance}
class ImportBlockRequest(BaseModel):
height: int
hash: str
@@ -663,15 +640,27 @@ async def get_token_supply(chain_id: str = "ait-devnet") -> Dict[str, Any]:
start = time.perf_counter()
with session_scope() as session:
# Simple implementation for now
# Sum balances of all accounts in this chain
result = session.exec(select(func.sum(Account.balance)).where(Account.chain_id == chain_id)).one_or_none()
circulating = int(result) if result is not None else 0
# Total supply is read from genesis (fixed), or fallback to circulating if unavailable
# Try to locate genesis file
genesis_path = Path(f"./data/{chain_id}/genesis.json")
total_supply = circulating # default fallback
if genesis_path.exists():
try:
with open(genesis_path) as f:
g = json.load(f)
total_supply = sum(a["balance"] for a in g.get("allocations", []))
except Exception:
total_supply = circulating
response = {
"chain_id": chain_id,
"total_supply": 1000000000, # 1 billion from genesis
"circulating_supply": 0, # No transactions yet
"faucet_balance": 1000000000, # All tokens in faucet
"faucet_address": "ait1faucet000000000000000000000000000000000",
"total_supply": total_supply,
"circulating_supply": circulating,
"mint_per_unit": cfg.mint_per_unit,
"total_accounts": 0
}
metrics_registry.observe("rpc_supply_duration_seconds", time.perf_counter() - start)
@@ -682,30 +671,35 @@ async def get_token_supply(chain_id: str = "ait-devnet") -> Dict[str, Any]:
async def get_validators(chain_id: str = "ait-devnet") -> Dict[str, Any]:
"""List blockchain validators (authorities)"""
from ..config import settings as cfg
metrics_registry.increment("rpc_validators_total")
start = time.perf_counter()
# For PoA chain, validators are the authorities from genesis
# In a full implementation, this would query the actual validator set
# Build validator set from trusted_proposers config (comma-separated)
trusted = [p.strip() for p in cfg.trusted_proposers.split(",") if p.strip()]
if not trusted:
# Fallback to the node's own proposer_id as the sole validator
trusted = [cfg.proposer_id]
validators = [
{
"address": "ait1devproposer000000000000000000000000000000",
"address": addr,
"weight": 1,
"status": "active",
"last_block_height": None, # Would be populated from actual validator tracking
"last_block_height": None, # Could be populated from metrics
"total_blocks_produced": None
}
for addr in trusted
]
response = {
"chain_id": chain_id,
"validators": validators,
"total_validators": len(validators),
"consensus_type": "PoA", # Proof of Authority
"consensus_type": "PoA",
"proposer_id": cfg.proposer_id
}
metrics_registry.observe("rpc_validators_duration_seconds", time.perf_counter() - start)
return response