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
This commit is contained in:
Tanya S 2025-07-16 08:35:22 +02:00 committed by GitHub
parent 5f0dfb50ea
commit e5bb677abe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 328 additions and 16 deletions

View File

@ -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
# 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."

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1 @@
web3==6.15.1

View File

@ -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