From e5bb677abe0b48456440afb803ec68ced57a8378 Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Wed, 16 Jul 2025 08:35:22 +0200 Subject: [PATCH] Update contract deployment for new RLN contract (#110) * Run deploy scripts for new incentivized contract and mint tokens per account * Add service to manage RLN contract token minting per nwaku service * Add token-mint-service as tool * revert back to sh instead of bash for contract deployer script * Add comments and code cleanup --- deploy_rln_contract.sh | 29 ++- docker-compose.yml | 38 +++- run_nwaku.sh | 4 +- tools/token-mint-service/Dockerfile | 20 ++ tools/token-mint-service/get_account_key.sh | 51 +++++ tools/token-mint-service/init_node_tokens.py | 196 +++++++++++++++++++ tools/token-mint-service/requirements.txt | 1 + wakusim.env | 5 +- 8 files changed, 328 insertions(+), 16 deletions(-) create mode 100644 tools/token-mint-service/Dockerfile create mode 100644 tools/token-mint-service/get_account_key.sh create mode 100644 tools/token-mint-service/init_node_tokens.py create mode 100644 tools/token-mint-service/requirements.txt diff --git a/deploy_rln_contract.sh b/deploy_rln_contract.sh index d199af7..22b6f11 100644 --- a/deploy_rln_contract.sh +++ b/deploy_rln_contract.sh @@ -21,10 +21,7 @@ fi cd /waku-rlnv2-contract git checkout $RLN_CONTRACT_REPO_COMMIT -#3. Replace the hardcoded MAX_MESSAGE_LIMIT -sed -i "s/\b100\b/${MAX_MESSAGE_LIMIT}/g" script/Deploy.s.sol - -# 4. Compile +# 3. Compile Contract Repo echo "forge install..." forge install echo "pnpm install..." @@ -32,7 +29,7 @@ pnpm install echo "forge build..." forge build -# 5. Export environment variables +# 4. Export environment variables export RCL_URL=$RCL_URL export PRIVATE_KEY=$PRIVATE_KEY export ETH_FROM=$ETH_FROM @@ -41,5 +38,23 @@ export API_KEY_ETHERSCAN=123 export API_KEY_CARDONA=123 export API_KEY_LINEASCAN=123 -# 6. Deploy the contract -forge script script/Deploy.s.sol:Deploy --rpc-url $RPC_URL --broadcast -vv --private-key $PRIVATE_KEY --sender $ETH_FROM \ No newline at end of file +# 5. Deploy the TestToken +echo "\nDeploying TestToken (ERC20 Token Contract)...\n" +forge script test/TestToken.sol --broadcast -vv --rpc-url http://foundry:8545 --tc TestTokenFactory --private-key $PRIVATE_KEY +export TOKEN_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 + +echo "\nDeploying LinearPriceCalculator Contract..." +forge script script/Deploy.s.sol --broadcast -vv --rpc-url http://foundry:8545 --tc DeployPriceCalculator --private-key $PRIVATE_KEY + +echo "\nDeploying RLN contract..." +forge script script/Deploy.s.sol --broadcast -vv --rpc-url http://foundry:8545 --tc DeployWakuRlnV2 --private-key $PRIVATE_KEY + +echo "\nDeploying Proxy contract..." +forge script script/Deploy.s.sol --broadcast -vvv --rpc-url http://foundry:8545 --tc DeployProxy --private-key $PRIVATE_KEY +export CONTRACT_ADDRESS=0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 + +# 6. Contract deployment completed +echo "\nContract deployment completed successfully" +echo "TOKEN_ADDRESS: $TOKEN_ADDRESS" +echo "CONTRACT_ADDRESS: $CONTRACT_ADDRESS" +echo "\nEach account registering a membership needs to first mint the token and approve the contract to spend it on their behalf." \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 97040b9..d9476e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,8 @@ services: --allow-origin=* --block-time=12 --chain-id=1234 + --gas-limit=30000000 + --gas-price=1 --silent --config-out=/shared/anvil-config.txt volumes: @@ -38,16 +40,17 @@ services: labels: com.centurylinklabs.watchtower.enable: '${WATCHTOWER_ENABLED:-false}' environment: - - RLN_CONTRACT_REPO_COMMIT=${RLN_CONTRACT_REPO_COMMIT:-64df4593c6a14e43b8b0e9b396d2f4772bb08b34} + - RLN_CONTRACT_REPO_COMMIT=${RLN_CONTRACT_REPO_COMMIT:-ad0dc9a81d892864ac2576d74e628ce93da592ef} - PRIVATE_KEY=${PRIVATE_KEY} - RPC_URL=${RPC_URL:-http://foundry:8545} - ETH_FROM=${ETH_FROM} - - MAX_MESSAGE_LIMIT=${MAX_MESSAGE_LIMIT:-20} + - NUM_NWAKU_NODES=${NUM_NWAKU_NODES:-5} entrypoint: sh command: - '/opt/deploy_rln_contract.sh' volumes: - ./deploy_rln_contract.sh:/opt/deploy_rln_contract.sh + - privatekeys-volume:/shared depends_on: - foundry networks: @@ -82,16 +85,41 @@ services: entrypoint: sh environment: - RPC_URL=${RPC_URL:-http://foundry:8545} - - RLN_CONTRACT_ADDRESS=0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 + - RLN_CONTRACT_ADDRESS=0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 - RLN_CREDENTIAL_PATH=/keystore.json - RLN_CREDENTIAL_PASSWORD=passw123 - - RLN_RELAY_MSG_LIMIT=${RLN_RELAY_MSG_LIMIT:-10} - - RLN_RELAY_EPOCH_SEC=${RLN_RELAY_EPOCH_SEC:-60} + - RLN_RELAY_MSG_LIMIT=${RLN_RELAY_MSG_LIMIT:-100} + - RLN_RELAY_EPOCH_SEC=${RLN_RELAY_EPOCH_SEC:-600} + - TOKEN_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 + - PRIVATE_KEY=${PRIVATE_KEY} command: - '/opt/run_nwaku.sh' volumes: - ./run_nwaku.sh:/opt/run_nwaku.sh:Z - privatekeys-volume:/shared + init: true + depends_on: + contract-repo-deployer: + condition: service_completed_successfully + nwaku-token-init: + condition: service_completed_successfully + networks: + - simulation + + nwaku-token-init: + build: + context: ./tools/token-mint-service + dockerfile: Dockerfile + environment: + - RPC_URL=${RPC_URL:-http://foundry:8545} + - TOKEN_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3 + - CONTRACT_ADDRESS=0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 + - PRIVATE_KEY=${PRIVATE_KEY} + - NUM_NWAKU_NODES=${NUM_NWAKU_NODES:-5} + deploy: + replicas: ${NUM_NWAKU_NODES:-5} + volumes: + - privatekeys-volume:/shared depends_on: contract-repo-deployer: condition: service_completed_successfully diff --git a/run_nwaku.sh b/run_nwaku.sh index b79c771..f957c2f 100755 --- a/run_nwaku.sh +++ b/run_nwaku.sh @@ -1,5 +1,7 @@ #!/bin/sh +ANVIL_CONFIG_PATH=${ANVIL_CONFIG_PATH:-/shared/anvil-config.txt} + # Check Linux Distro Version - it can differ depending on the nwaku image used OS=$(cat /etc/os-release) if echo $OS | grep -q "Debian"; then @@ -49,7 +51,7 @@ fi get_private_key(){ # Read the JSON file - json_content=$(cat /shared/anvil-config.txt) + json_content=$(cat "$ANVIL_CONFIG_PATH") # Check if json_content has a value if [ -z "$json_content" ]; then 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..c093a5a --- /dev/null +++ b/tools/token-mint-service/get_account_key.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Helper script to get the index of the container and use it to retrieve a unique account private key. +# Each node uses a unique Ethereum account to register with the RLN contract. +# The account and private key pairs are stored in anvil-config.txt on a shared volume at anvil startup in the foundry service + +set -e + +ANVIL_CONFIG_PATH=${ANVIL_CONFIG_PATH:-/shared/anvil-config.txt} + +# 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..62cb119 --- /dev/null +++ b/tools/token-mint-service/init_node_tokens.py @@ -0,0 +1,196 @@ +#!/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') + # 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') + + 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") + + # 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 diff --git a/wakusim.env b/wakusim.env index 36c1782..77334dc 100644 --- a/wakusim.env +++ b/wakusim.env @@ -4,7 +4,7 @@ NWAKU_IMAGE=harbor.status.im/wakuorg/nwaku:latest NUM_NWAKU_NODES=50 # Simulation traffic. MSG_SIZE_KBYTES=10 -TRAFFIC_DELAY_SECONDS=30 +TRAFFIC_DELAY_SECONDS=6 # Enable automatic Docker image updates. WATCHTOWER_ENABLED=true # Anvil RPC Node external IP and port @@ -12,8 +12,7 @@ RPC_URL=http://foundry:8545 # Contract-deployment PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ETH_FROM=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 -MAX_MESSAGE_LIMIT=100 # RLNv2 limits RLN_RELAY_MSG_LIMIT=100 RLN_RELAY_EPOCH_SEC=600 -RLN_CONTRACT_REPO_COMMIT=64df4593c6a14e43b8b0e9b396d2f4772bb08b34 +RLN_CONTRACT_REPO_COMMIT=ad0dc9a81d892864ac2576d74e628ce93da592ef