Tanya S 479b908c42
chore: make token initialisation more robust (#114)
* Make token initialisation more robust

* revert default tokens to mint to 5
2025-10-13 07:52:41 +02:00

322 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Per-node token initialization service.
This script runs as an init container for each nwaku node to:
1. Mint ERC20 tokens to the node's address
2. Approve the RLN contract to spend those tokens
Each node gets its own private key and handles its own token setup.
"""
import os
import sys
import time
import logging
from web3 import Web3
from web3.exceptions import TransactionNotFound
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - [Node Init] %(message)s'
)
logger = logging.getLogger(__name__)
class NodeTokenInitializer:
def __init__(self):
"""Initialize the node token service."""
# Required environment variables
self.rpc_url = os.getenv('RPC_URL', 'http://foundry:8545')
self.token_address = os.getenv('TOKEN_ADDRESS', '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512')
self.contract_address = os.getenv('RLN_CONTRACT_ADDRESS', '0x0165878A594ca255338adfa4d48449f69242Eb8F')
# The values for NODE_PRIVATE_KEY, NODE_ADDRESS, and NODE_INDEX are set by the get_account_key.sh script
self.private_key = os.getenv('NODE_PRIVATE_KEY')
self.node_address = os.getenv('NODE_ADDRESS')
self.node_index = os.getenv('NODE_INDEX', '0')
# Approver private key for adding accounts to approved minters list
self.approver_private_key = os.getenv('PRIVATE_KEY')
self.mint_amount = int(os.getenv('MINT_AMOUNT', '5000000000000000000')) # at least 5 tokens required for membership with RLN_RELAY_MSG_LIMIT=100
if not self.private_key:
raise ValueError("NODE_PRIVATE_KEY (Ethereum account private key) environment variable is required")
if not self.node_address:
raise ValueError("NODE_ADDRESS (Ethereum account address) environment variable is required")
if not self.approver_private_key:
logger.error("PRIVATE_KEY environment variable is missing!")
raise ValueError("PRIVATE_KEY (Token contract owner private key) environment variable is required")
else:
logger.info(f"PRIVATE_KEY loaded successfully")
# Initialize Web3
self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
if not self.w3.is_connected():
raise Exception(f"Failed to connect to Ethereum node at {self.rpc_url}")
# Get approver address from private key
try:
self.approver_address = self.w3.eth.account.from_key(self.approver_private_key).address
logger.info(f"Approver address derived: {self.approver_address}")
except Exception as e:
logger.error(f"Failed to derive approver address from PRIVATE_KEY: {str(e)}")
raise
# Convert addresses to proper checksum format
self.node_address = self.w3.to_checksum_address(self.node_address)
self.token_address = self.w3.to_checksum_address(self.token_address)
self.contract_address = self.w3.to_checksum_address(self.contract_address)
self.approver_address = self.w3.to_checksum_address(self.approver_address)
logger.info(f"Node {self.node_index} initializing tokens")
logger.info(f"Address: {self.node_address}")
logger.info(f"Token: {self.token_address}")
logger.info(f"Contract: {self.contract_address}")
def wait_for_transaction(self, tx_hash: str, timeout: int = 120) -> bool:
"""Wait for transaction to be mined."""
start_time = time.time()
while time.time() - start_time < timeout:
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
if receipt.status == 1:
logger.info(f"Transaction {tx_hash} confirmed")
return True
else:
logger.error(f"Transaction {tx_hash} failed with status {receipt.status}")
return False
except TransactionNotFound:
time.sleep(2)
continue
logger.error(f"Transaction {tx_hash} timed out after {timeout} seconds")
return False
def approve_account_for_minting(self) -> bool:
"""Add this node's address to the approved minters list."""
logger.info(f"=== STARTING APPROVAL PROCESS ===")
logger.info(f"Node address to approve: {self.node_address}")
logger.info(f"Approver address: {self.approver_address}")
logger.info(f"Token contract: {self.token_address}")
for attempt in range(3):
try:
logger.info(f"Adding {self.node_address} to approved minters list (attempt {attempt + 1}/3)")
# Use the approver's private key (contract owner)
nonce = self.w3.eth.get_transaction_count(self.approver_address, 'pending')
# Build addApprovedAccount transaction
function_signature = self.w3.keccak(text="addMinter(address)")[:4]
encoded_address = self.node_address[2:].lower().zfill(64)
data = function_signature.hex() + encoded_address
# Increase gas price for retries to avoid underpriced transactions
gas_price = self.w3.eth.gas_price
if attempt > 0:
gas_price = int(gas_price * (1.1 ** attempt)) # 10% increase per retry
transaction = {
'to': self.token_address,
'value': 0,
'gas': 200000,
'gasPrice': gas_price,
'nonce': nonce,
'data': data,
}
# Sign and send with approver's key
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.approver_private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
logger.info(f"Approve account transaction sent: {tx_hash.hex()}")
if self.wait_for_transaction(tx_hash.hex()):
logger.info(f"✓ Account approval successful for node {self.node_index}")
return True
else:
logger.error(f"✗ Account approval failed for node {self.node_index} (attempt {attempt + 1})")
if attempt < 2:
logger.info(f"Retrying account approval in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
logger.error(f"✗ Account approval failed for node {self.node_index} (attempt {attempt + 1}): {str(e)}")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Exception details: {repr(e)}")
if attempt < 2:
logger.info(f"Retrying account approval in 5 seconds...")
time.sleep(5)
continue
logger.error(f"✗ Account approval failed for node {self.node_index} after 3 attempts")
logger.error(f"=== APPROVAL PROCESS FAILED ===")
return False
def mint_tokens(self) -> bool:
"""Mint tokens to this node's address using the node's own private key."""
for attempt in range(3):
try:
logger.info(f"Minting {self.mint_amount} tokens to {self.node_address} (attempt {attempt + 1}/3)")
# Use the node's own private key since mint() is public
nonce = self.w3.eth.get_transaction_count(self.node_address, 'pending')
# Build mint transaction
function_signature = self.w3.keccak(text="mint(address,uint256)")[:4]
encoded_address = self.node_address[2:].lower().zfill(64)
encoded_amount = hex(self.mint_amount)[2:].zfill(64)
data = function_signature.hex() + encoded_address + encoded_amount
# Increase gas price for retries to avoid underpriced transactions
gas_price = self.w3.eth.gas_price
if attempt > 0:
gas_price = int(gas_price * (1.1 ** attempt)) # 10% increase per retry
transaction = {
'to': self.token_address,
'value': 0,
'gas': 200000,
'gasPrice': gas_price,
'nonce': nonce,
'data': data,
}
# Sign and send with node's own key
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
logger.info(f"Mint transaction sent: {tx_hash.hex()}")
if self.wait_for_transaction(tx_hash.hex()):
logger.info(f"✓ Mint successful for node {self.node_index}")
return True
else:
logger.error(f"✗ Mint failed for node {self.node_index} (attempt {attempt + 1})")
if attempt < 2:
logger.info(f"Retrying mint in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
logger.error(f"✗ Mint failed for node {self.node_index} (attempt {attempt + 1}): {str(e)}")
if attempt < 2:
logger.info(f"Retrying mint in 5 seconds...")
time.sleep(5)
continue
logger.error(f"✗ Mint failed for node {self.node_index} after 3 attempts")
return False
def approve_tokens(self) -> bool:
"""Approve RLN contract to spend tokens."""
for attempt in range(3):
try:
logger.info(f"Approving {self.mint_amount} tokens for contract {self.contract_address} (attempt {attempt + 1}/3)")
nonce = self.w3.eth.get_transaction_count(self.node_address, 'pending')
# Build approve transaction
function_signature = self.w3.keccak(text="approve(address,uint256)")[:4]
encoded_contract = self.contract_address[2:].lower().zfill(64)
encoded_amount = hex(self.mint_amount)[2:].zfill(64)
data = function_signature.hex() + encoded_contract + encoded_amount
# Increase gas price for retries to avoid underpriced transactions
gas_price = self.w3.eth.gas_price
if attempt > 0:
gas_price = int(gas_price * (1.1 ** attempt)) # 10% increase per retry
transaction = {
'to': self.token_address,
'value': 0,
'gas': 200000,
'gasPrice': gas_price,
'nonce': nonce,
'data': data,
}
# Sign and send with node's own key
signed_txn = self.w3.eth.account.sign_transaction(transaction, self.private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
logger.info(f"Approve transaction sent: {tx_hash.hex()}")
if self.wait_for_transaction(tx_hash.hex()):
logger.info(f"✓ Approval successful for node {self.node_index}")
return True
else:
logger.error(f"✗ Approval failed for node {self.node_index} (attempt {attempt + 1})")
if attempt < 2:
logger.info(f"Retrying approval in 5 seconds...")
time.sleep(5)
continue
except Exception as e:
logger.error(f"✗ Approval failed for node {self.node_index} (attempt {attempt + 1}): {str(e)}")
if attempt < 2:
logger.info(f"Retrying approval in 5 seconds...")
time.sleep(5)
continue
logger.error(f"✗ Approval failed for node {self.node_index} after 3 attempts")
return False
def run(self) -> bool:
"""Run the token initialization process."""
try:
logger.info(f"Starting token initialization for node {self.node_index}")
logger.info(f"=== DEBUG INFO ===")
logger.info(f"Node address: {self.node_address}")
logger.info(f"Approver address: {self.approver_address}")
logger.info(f"Token address: {self.token_address}")
logger.info(f"PRIVATE_KEY present: {'Yes' if self.approver_private_key else 'No'}")
logger.info(f"==================")
# Step 1: Add node address to approved minters list
logger.info(f"STEP 1: Starting approval process...")
try:
if not self.approve_account_for_minting():
logger.error(f"STEP 1 FAILED: Could not approve account for minting")
return False
logger.info(f"STEP 1 SUCCESS: Account approved for minting")
except Exception as e:
logger.error(f"STEP 1 EXCEPTION: {str(e)}")
logger.error(f"Exception type: {type(e).__name__}")
return False
# Step 2: Mint tokens
if not self.mint_tokens():
return False
# Step 3: Approve contract
if not self.approve_tokens():
return False
logger.info(f"✓ Node {self.node_index} token initialization completed successfully")
return True
except Exception as e:
logger.error(f"✗ Node {self.node_index} initialization failed: {str(e)}")
return False
def main():
"""Main entry point."""
try:
initializer = NodeTokenInitializer()
success = initializer.run()
if success:
logger.info("Node ready to start")
sys.exit(0)
else:
logger.error("Node initialization failed")
sys.exit(1)
except Exception as e:
logger.error(f"Failed to initialize: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()