diff --git a/deploy_rln_contract.sh b/deploy_rln_contract.sh index 8872e44..70c7a53 100644 --- a/deploy_rln_contract.sh +++ b/deploy_rln_contract.sh @@ -2,9 +2,7 @@ set -e -# 1. Install foundry, pnpm, and required tools -apt update && apt install -y jq - +# 1. Install foundry and pnpm curl -L https://foundry.paradigm.xyz | bash && . /root/.bashrc && foundryup && export PATH=$PATH:$HOME/.foundry/bin echo "installing pnpm..." diff --git a/tools/token-mint-service/Dockerfile b/tools/token-mint-service/Dockerfile new file mode 100644 index 0000000..1f4d17b --- /dev/null +++ b/tools/token-mint-service/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.11-alpine + +WORKDIR /app + +# Install system packages needed by the script +RUN apk add --no-cache bind-tools jq + +# Install requirements +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy scripts +COPY init_node_tokens.py . +COPY get_account_key.sh . + +# Make scripts executable +RUN chmod +x /app/init_node_tokens.py /app/get_account_key.sh + +# Use the account key helper as entrypoint +ENTRYPOINT ["/bin/sh", "/app/get_account_key.sh"] \ No newline at end of file diff --git a/tools/token-mint-service/get_account_key.sh b/tools/token-mint-service/get_account_key.sh new file mode 100644 index 0000000..6a8ec36 --- /dev/null +++ b/tools/token-mint-service/get_account_key.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# Helper script to get the index of the container and use it to retrieve a unique account private key per nwaku node to be used to generate the keystore + +set -e + +# Tools already installed in Dockerfile + +ANVIL_CONFIG_PATH=${ANVIL_CONFIG_PATH:-/shared/anvil-config.txt} + +# Wait for anvil config to be available +echo "Waiting for anvil config at $ANVIL_CONFIG_PATH..." +while [ ! -f "$ANVIL_CONFIG_PATH" ]; do + sleep 2 +done + +# Get container IP and determine index (same method as run_nwaku.sh) +IP=$(ip a | grep "inet " | grep -Fv 127.0.0.1 | sed 's/.*inet \([^/]*\).*/\1/') +echo "Container IP: $IP" + +# Extract container name from reverse DNS lookup and get index +CNTR=$(dig -x $IP +short | cut -d'.' -f1) +INDEX=$(echo $CNTR | sed 's/.*[-_]\([0-9]*\)/\1/') + +if [ $? -ne 0 ] || [ -z "$INDEX" ]; then + echo "Error: Failed to determine the replica index from IP." >&2 + exit 1 +fi + +echo "Determined container index: $INDEX" + +# Read anvil config +json_content=$(cat "$ANVIL_CONFIG_PATH") +if [ -z "$json_content" ]; then + echo "Error: Failed to read the JSON file or the file is empty." >&2 + exit 1 +fi + +# Get private key and address for this index +ARRAY_INDEX=$((INDEX - 1)) + +ACCOUNT_PRIVATE_KEY=$(echo "$json_content" | jq -r ".private_keys[$ARRAY_INDEX]") +ACCOUNT_ADDRESS=$(echo "$json_content" | jq -r ".available_accounts[$ARRAY_INDEX]") + +if [ "$ACCOUNT_PRIVATE_KEY" = "null" ] || [ "$ACCOUNT_ADDRESS" = "null" ]; then + echo "Failed to get account private key or address for index $INDEX (array index $ARRAY_INDEX)" >&2 + exit 1 +fi + +# Export for the Python script +export NODE_PRIVATE_KEY="$ACCOUNT_PRIVATE_KEY" +export NODE_ADDRESS="$ACCOUNT_ADDRESS" +export NODE_INDEX="$INDEX" + +echo "Node $INDEX using Ethereum account: $ACCOUNT_ADDRESS" + +# Run the Python initialization script +exec python3 /app/init_node_tokens.py \ No newline at end of file diff --git a/tools/token-mint-service/init_node_tokens.py b/tools/token-mint-service/init_node_tokens.py new file mode 100644 index 0000000..30c643d --- /dev/null +++ b/tools/token-mint-service/init_node_tokens.py @@ -0,0 +1,194 @@ +#!/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', '0x5FbDB2315678afecb367f032d93F642f64180aa3') + self.contract_address = os.getenv('CONTRACT_ADDRESS', '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707') + self.private_key = os.getenv('NODE_PRIVATE_KEY') # Ethereum account private key (not nwaku node-key) + self.node_address = os.getenv('NODE_ADDRESS') # Ethereum account address + self.node_index = os.getenv('NODE_INDEX', '0') + self.mint_amount = int(os.getenv('MINT_AMOUNT', '5000000000000000000')) # 5 tokens + + 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") + + # 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}") + + # 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) + + 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 mint_tokens(self) -> bool: + """Mint tokens to this node's address using the node's own private key.""" + try: + logger.info(f"Minting {self.mint_amount} tokens to {self.node_address}") + + # Use the node's own private key since mint() is public + nonce = self.w3.eth.get_transaction_count(self.node_address) + + # 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 + + transaction = { + 'to': self.token_address, + 'value': 0, + 'gas': 200000, + 'gasPrice': self.w3.eth.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}") + return False + + except Exception as e: + logger.error(f"✗ Mint failed for node {self.node_index}: {str(e)}") + return False + + def approve_tokens(self) -> bool: + """Approve RLN contract to spend tokens.""" + try: + logger.info(f"Approving {self.mint_amount} tokens for contract {self.contract_address}") + + nonce = self.w3.eth.get_transaction_count(self.node_address) + + # 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 + + transaction = { + 'to': self.token_address, + 'value': 0, + 'gas': 200000, + 'gasPrice': self.w3.eth.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}") + return False + + except Exception as e: + logger.error(f"✗ Approval failed for node {self.node_index}: {str(e)}") + return False + + def run(self) -> bool: + """Run the token initialization process.""" + try: + logger.info(f"Starting token initialization for node {self.node_index}") + + # Step 1: Mint tokens + if not self.mint_tokens(): + return False + + # Step 2: 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() \ No newline at end of file diff --git a/tools/token-mint-service/requirements.txt b/tools/token-mint-service/requirements.txt new file mode 100644 index 0000000..c053f47 --- /dev/null +++ b/tools/token-mint-service/requirements.txt @@ -0,0 +1 @@ +web3==6.15.1 \ No newline at end of file