diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index d61ad07..474552e 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -17,6 +17,7 @@ on: - story-docker - kava-docker - evmos-docker + - canto-docker - mezo-docker - kii-docker - tac-docker @@ -99,6 +100,15 @@ jobs: elif [ "${CHAIN_ENV}" = "evmos-docker" ]; then echo "Pulling Evmos Docker image..." DOCKER_DEFAULT_PLATFORM=linux/amd64 docker pull tharsishq/evmos:v20.0.0 + elif [ "${CHAIN_ENV}" = "canto-docker" ]; then + echo "Building Canto Docker image from source..." + git clone --depth 1 https://github.com/Canto-Network/Canto.git /tmp/canto-src + cd /tmp/canto-src + # Upstream Dockerfile uses deprecated golang:stretch; patch to a maintained base image + sed -i 's/^FROM golang:stretch AS build-env/FROM golang:1.21-bookworm AS build-env/' Dockerfile + sed -i 's/^FROM golang:stretch$/FROM golang:1.21-bookworm/' Dockerfile + DOCKER_DEFAULT_PLATFORM=linux/amd64 docker build -t canto-network/canto:local . + echo "canto-network/canto:local built successfully" elif [ "${CHAIN_ENV}" = "kii-docker" ]; then echo "Building kiichain/kiichaind Docker image from source..." git clone --depth 1 --branch v7.0.1 https://github.com/KiiChain/kiichain.git /tmp/kiichain-src @@ -147,6 +157,9 @@ jobs: elif [ "${CHAIN_ENV}" = "evmos-docker" ]; then chmod +x init-multinode.sh ./init-multinode.sh + elif [ "${CHAIN_ENV}" = "canto-docker" ]; then + chmod +x init-multinode.sh + ./init-multinode.sh elif [ "${CHAIN_ENV}" = "kii-docker" ]; then chmod +x init-multinode.sh ./init-multinode.sh @@ -157,6 +170,8 @@ jobs: chmod +x init-multinode.sh ./init-multinode.sh elif [ "${CHAIN_ENV}" = "sei-docker" ]; then + chmod +x init-multinode.sh + ./init-multinode.sh elif [ "${CHAIN_ENV}" = "injective-docker" ]; then chmod +x init-multinode.sh ./init-multinode.sh @@ -333,6 +348,9 @@ jobs: elif [ "${CHAIN_ENV}" = "evmos-docker" ]; then echo "=== Evmos validator1 logs (last 100 lines) ===" docker compose --env-file .env.multinode -f docker-compose.yml logs evmos-validator1 --tail=100 2>/dev/null || true + elif [ "${CHAIN_ENV}" = "canto-docker" ]; then + echo "=== Canto validator1 logs (last 100 lines) ===" + docker compose --env-file .env.multinode -f docker-compose.yml logs canto-validator1 --tail=100 2>/dev/null || true elif [ "${CHAIN_ENV}" = "kii-docker" ]; then echo "=== Kii validator1 logs (last 100 lines) ===" docker compose --env-file .env.multinode -f docker-compose.yml logs kii-validator1 --tail=100 2>/dev/null || true diff --git a/chains/canto-docker/.env.multinode b/chains/canto-docker/.env.multinode new file mode 100644 index 0000000..98a4fad --- /dev/null +++ b/chains/canto-docker/.env.multinode @@ -0,0 +1,11 @@ +# Canto Multi-Validator Localnet Environment Variables +# Copy to .env.multinode and adjust as needed + +# Canto Docker image tag +CANTO_TAG=local + +# CometBFT persistent peers (auto-populated by init-multinode.sh) +PERSISTENT_PEERS=4db90966006cfeabeb3942da3b05622ed4927d87@canto-validator1:26656,7322977d89d5ca6e25f6d134bda08e8f35825363@canto-validator2:26656,9724e8b1b3c6a3b256996e23a8d9333a285727bb@canto-validator3:26656,974355153a8ec9e4a7e73267ef7d90e7faf9f156@canto-validator4:26656 + +# Logging +LOG_LEVEL=info diff --git a/chains/canto-docker/.env.multinode.sample b/chains/canto-docker/.env.multinode.sample new file mode 100644 index 0000000..b27c4fe --- /dev/null +++ b/chains/canto-docker/.env.multinode.sample @@ -0,0 +1,11 @@ +# Canto Multi-Validator Localnet Environment Variables +# Copy to .env.multinode and adjust as needed + +# Canto Docker image tag +CANTO_TAG=local + +# CometBFT persistent peers (auto-populated by init-multinode.sh) +PERSISTENT_PEERS="" + +# Logging +LOG_LEVEL=info diff --git a/chains/canto-docker/docker-compose.yml b/chains/canto-docker/docker-compose.yml new file mode 100644 index 0000000..05bdf14 --- /dev/null +++ b/chains/canto-docker/docker-compose.yml @@ -0,0 +1,174 @@ +# docker-compose.yml +# Canto 4-Validator Localnet for ChainSmith Testing +# +# Architecture: Each validator = 1 cantod container (CometBFT + EVM in same process) +# +# Usage: +# 1. Init: ./init-multinode.sh +# 2. Start: ./start-multinode.sh +# 3. Check: docker compose --env-file .env.multinode -f docker-compose.yml ps +# 4. Logs: docker compose --env-file .env.multinode -f docker-compose.yml logs -f canto-validator1 +# 5. Stop: ./stop-multinode.sh + +services: + # ============ Validator 1 ============ + canto-validator1: + image: canto-network/canto:${CANTO_TAG:-local} + entrypoint: ['cantod'] + restart: unless-stopped + user: root + networks: + canto-multi: + env_file: + - .env.multinode + logging: + options: + max-size: '12m' + max-file: '5' + command: > + start + --home /root/.cantod + --chain-id canto_7700-1 + --p2p.laddr "tcp://0.0.0.0:26656" + --p2p.persistent_peers "${PERSISTENT_PEERS}" + --rpc.laddr "tcp://0.0.0.0:26657" + --minimum-gas-prices "0.0001acanto" + --json-rpc.api "eth,txpool,personal,net,debug,web3" + --pruning nothing + --log_level ${LOG_LEVEL:-info} + volumes: + - type: volume + source: canto_validator1_home + target: /root/.cantod + ports: + - '26657:26657' # CometBFT RPC + - '1317:1317' # Cosmos REST API + - '8545:8545' # EVM JSON-RPC + - '8546:8546' # EVM WebSocket + - '9090:9090' # gRPC + - '26656:26656' # P2P + + # ============ Validator 2 ============ + canto-validator2: + image: canto-network/canto:${CANTO_TAG:-local} + entrypoint: ['cantod'] + restart: unless-stopped + user: root + networks: + canto-multi: + env_file: + - .env.multinode + logging: + options: + max-size: '12m' + max-file: '5' + command: > + start + --home /root/.cantod + --chain-id canto_7700-1 + --p2p.laddr "tcp://0.0.0.0:26656" + --p2p.persistent_peers "${PERSISTENT_PEERS}" + --rpc.laddr "tcp://0.0.0.0:26657" + --minimum-gas-prices "0.0001acanto" + --json-rpc.api "eth,txpool,personal,net,debug,web3" + --pruning nothing + --log_level ${LOG_LEVEL:-info} + volumes: + - type: volume + source: canto_validator2_home + target: /root/.cantod + ports: + - '36657:26657' # CometBFT RPC + - '21317:1317' # Cosmos REST API + - '28545:8545' # EVM JSON-RPC + - '28546:8546' # EVM WebSocket + - '36656:26656' # P2P + + # ============ Validator 3 ============ + canto-validator3: + image: canto-network/canto:${CANTO_TAG:-local} + entrypoint: ['cantod'] + restart: unless-stopped + user: root + networks: + canto-multi: + env_file: + - .env.multinode + logging: + options: + max-size: '12m' + max-file: '5' + command: > + start + --home /root/.cantod + --chain-id canto_7700-1 + --p2p.laddr "tcp://0.0.0.0:26656" + --p2p.persistent_peers "${PERSISTENT_PEERS}" + --rpc.laddr "tcp://0.0.0.0:26657" + --minimum-gas-prices "0.0001acanto" + --json-rpc.api "eth,txpool,personal,net,debug,web3" + --pruning nothing + --log_level ${LOG_LEVEL:-info} + volumes: + - type: volume + source: canto_validator3_home + target: /root/.cantod + ports: + - '46657:26657' # CometBFT RPC + - '31317:1317' # Cosmos REST API + - '38545:8545' # EVM JSON-RPC + - '38546:8546' # EVM WebSocket + - '46656:26656' # P2P + + # ============ Validator 4 ============ + canto-validator4: + image: canto-network/canto:${CANTO_TAG:-local} + entrypoint: ['cantod'] + restart: unless-stopped + user: root + networks: + canto-multi: + env_file: + - .env.multinode + logging: + options: + max-size: '12m' + max-file: '5' + command: > + start + --home /root/.cantod + --chain-id canto_7700-1 + --p2p.laddr "tcp://0.0.0.0:26656" + --p2p.persistent_peers "${PERSISTENT_PEERS}" + --rpc.laddr "tcp://0.0.0.0:26657" + --minimum-gas-prices "0.0001acanto" + --json-rpc.api "eth,txpool,personal,net,debug,web3" + --pruning nothing + --log_level ${LOG_LEVEL:-info} + volumes: + - type: volume + source: canto_validator4_home + target: /root/.cantod + ports: + - '56657:26657' # CometBFT RPC + - '41317:1317' # Cosmos REST API + - '48545:8545' # EVM JSON-RPC + - '48546:8546' # EVM WebSocket + - '56656:26656' # P2P + +volumes: + canto_validator1_home: + external: true + name: canto_validator1_home + canto_validator2_home: + external: true + name: canto_validator2_home + canto_validator3_home: + external: true + name: canto_validator3_home + canto_validator4_home: + external: true + name: canto_validator4_home + +networks: + canto-multi: diff --git a/chains/canto-docker/init-multinode.sh b/chains/canto-docker/init-multinode.sh new file mode 100755 index 0000000..26e6ec5 --- /dev/null +++ b/chains/canto-docker/init-multinode.sh @@ -0,0 +1,328 @@ +#!/bin/bash +# init-multinode.sh — Initialize 4-validator Canto localnet +# +# Architecture: Cosmos SDK + CometBFT + integrated EVM (single process) +# Each validator = 1 container (consensus + EVM in the same cantod process) +# +# Usage: +# chmod +x init-multinode.sh +# ./init-multinode.sh + +set -e + +IMAGE="canto-network/canto:${CANTO_TAG:-local}" +CHAIN_ID="canto_7700-1" +CANTO_HOME="/root/.cantod" +DENOM="acanto" +BASE_FEE="1000000000" + +# Amounts (18 decimals) +VALIDATOR_BALANCE="1000000000000000000000000${DENOM}" # 1,000,000 CANTO +VALIDATOR_STAKE="100000000000000000000000${DENOM}" # 100,000 CANTO +FOUNDER_BALANCE="10000000000000000000000000${DENOM}" # 10,000,000 CANTO + +if [ -z "$TEST_WALLET_PRIVATE_KEY" ]; then + echo "Error: TEST_WALLET_PRIVATE_KEY environment variable is not set." + echo "Please set it before running this script (e.g., export TEST_WALLET_PRIVATE_KEY=0x...)" + exit 1 +fi + +# Strip 0x prefix if present +FOUNDER_ETH_PRIVKEY="${TEST_WALLET_PRIVATE_KEY#0x}" + +NUM_VALIDATORS=4 +TOTAL_STEPS=11 + +run_canto() { + local vol="$1" + shift + docker run --rm --user root --entrypoint cantod \ + -v "${vol}:${CANTO_HOME}" "$IMAGE" "$@" --home "${CANTO_HOME}" +} + +run_canto_quiet() { + local vol="$1" + shift + docker run --rm --user root --entrypoint cantod \ + -v "${vol}:${CANTO_HOME}" "$IMAGE" "$@" --home "${CANTO_HOME}" >/dev/null 2>&1 +} + +run_genesis_cmd() { + local vol="$1" + shift + docker run --rm --user root --entrypoint cantod \ + -v "${vol}:${CANTO_HOME}" "$IMAGE" genesis "$@" --home "${CANTO_HOME}" >/dev/null 2>&1 || \ + docker run --rm --user root --entrypoint cantod \ + -v "${vol}:${CANTO_HOME}" "$IMAGE" "$@" --home "${CANTO_HOME}" >/dev/null 2>&1 +} + +echo "============================================" +echo " Canto Multi-Validator Localnet Init" +echo " Architecture: CometBFT + Integrated EVM" +echo " Validators: ${NUM_VALIDATORS}" +echo " Image: ${IMAGE}" +echo " Chain ID: ${CHAIN_ID}" +echo "============================================" +echo "" + +# ---------------------------------------------------------- +# Step 1: Ensure Docker image +# ---------------------------------------------------------- +echo "Step 1/${TOTAL_STEPS}: Ensuring Docker image..." +if ! docker image inspect "${IMAGE}" >/dev/null 2>&1; then + if [ "${CANTO_TAG:-local}" = "local" ]; then + echo "Error: ${IMAGE} not found." + echo "Build it first (e.g., in CI this is done automatically)." + exit 1 + fi + echo "Image not found locally, pulling ${IMAGE}..." + docker pull "${IMAGE}" >/dev/null +fi +echo "Image ready" +echo "" + +# ---------------------------------------------------------- +# Step 2: Clean old Docker volumes +# ---------------------------------------------------------- +echo "Step 2/${TOTAL_STEPS}: Cleaning old volumes..." +for i in $(seq 1 $NUM_VALIDATORS); do + docker volume rm -f canto_validator${i}_home 2>/dev/null || true +done +echo "Cleaned" +echo "" + +# ---------------------------------------------------------- +# Step 3: Initialize validator homes +# ---------------------------------------------------------- +echo "Step 3/${TOTAL_STEPS}: Initializing nodes..." +for i in $(seq 1 $NUM_VALIDATORS); do + run_canto_quiet "canto_validator${i}_home" init validator${i} --chain-id ${CHAIN_ID} + echo "Validator $i: initialized" +done +echo "" + +# ---------------------------------------------------------- +# Step 4: Create validator keys +# ---------------------------------------------------------- +echo "Step 4/${TOTAL_STEPS}: Creating validator keys..." +declare -a VALIDATOR_ADDRS +for i in $(seq 1 $NUM_VALIDATORS); do + run_canto_quiet "canto_validator${i}_home" keys add validator${i} --keyring-backend test --algo eth_secp256k1 || true + + ADDR=$(docker run --rm --user root --entrypoint cantod \ + -v canto_validator${i}_home:${CANTO_HOME} \ + "$IMAGE" keys show validator${i} \ + --keyring-backend test --home ${CANTO_HOME} -a 2>/dev/null | tr -d '\n\r') + + if [ -z "$ADDR" ]; then + echo "Error: failed to resolve validator${i} address." + exit 1 + fi + + VALIDATOR_ADDRS+=("$ADDR") + echo "Validator $i: ${ADDR}" +done +echo "" + +# ---------------------------------------------------------- +# Step 5: Import founder wallet key on validator1 +# ---------------------------------------------------------- +echo "Step 5/${TOTAL_STEPS}: Importing founder test wallet (Hardhat Account #0)..." +echo -e "password123\npassword123" | docker run -i --rm --user root --entrypoint cantod \ + -v canto_validator1_home:${CANTO_HOME} \ + "$IMAGE" keys unsafe-import-eth-key founder ${FOUNDER_ETH_PRIVKEY} \ + --keyring-backend test --home ${CANTO_HOME} >/dev/null 2>&1 || true + +FOUNDER_ADDR=$(docker run --rm --user root --entrypoint cantod \ + -v canto_validator1_home:${CANTO_HOME} \ + "$IMAGE" keys show founder \ + --keyring-backend test --home ${CANTO_HOME} -a 2>/dev/null | tr -d '\n\r') + +if [ -z "$FOUNDER_ADDR" ]; then + echo "Error: founder key import failed. Please verify TEST_WALLET_PRIVATE_KEY." + exit 1 +fi + +echo "Founder Cosmos address: ${FOUNDER_ADDR}" +echo "Founder EVM address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +echo "" + +# ---------------------------------------------------------- +# Step 6: Add genesis accounts and patch genesis on validator1 +# ---------------------------------------------------------- +echo "Step 6/${TOTAL_STEPS}: Adding genesis accounts and patching genesis..." + +run_genesis_cmd "canto_validator1_home" add-genesis-account "${FOUNDER_ADDR}" "${FOUNDER_BALANCE}" --keyring-backend test +echo "Founder account added" + +for i in $(seq 1 $NUM_VALIDATORS); do + idx=$((i-1)) + ADDR="${VALIDATOR_ADDRS[$idx]}" + if [ $i -eq 1 ]; then + run_genesis_cmd "canto_validator1_home" add-genesis-account validator1 "${VALIDATOR_BALANCE}" --keyring-backend test + else + run_genesis_cmd "canto_validator1_home" add-genesis-account "${ADDR}" "${VALIDATOR_BALANCE}" --keyring-backend test + fi + echo "Validator $i account added" +done + +docker run --rm --user root --entrypoint sh \ + -v canto_validator1_home:${CANTO_HOME} \ + "$IMAGE" -c ' + GENESIS='"${CANTO_HOME}"'/config/genesis.json + jq '"'"' + .app_state.staking.params.bond_denom = "acanto" | + (if .app_state.gov.deposit_params? then .app_state.gov.deposit_params.min_deposit[0].denom = "acanto" else . end) | + (if .app_state.gov.params? then .app_state.gov.params.min_deposit[0].denom = "acanto" else . end) | + (if .app_state.inflation?.params? then .app_state.inflation.params.mint_denom = "acanto" else . end) | + (if .app_state.mint?.params? then .app_state.mint.params.mint_denom = "acanto" else . end) | + (if .app_state.evm?.params? then .app_state.evm.params.evm_denom = "acanto" else . end) | + (if .app_state.feemarket?.params? then .app_state.feemarket.params.base_fee = "'"${BASE_FEE}"'" else . end) | + .consensus.params.block.max_gas = "10000000" + '"'"' "$GENESIS" > "${GENESIS}.tmp" && mv "${GENESIS}.tmp" "$GENESIS" + ' >/dev/null 2>&1 + +echo "Genesis patched (denom/base_fee/max_gas)" +echo "" + +# ---------------------------------------------------------- +# Step 7: Distribute genesis (accounts) to all nodes +# ---------------------------------------------------------- +echo "Step 7/${TOTAL_STEPS}: Distributing genesis with accounts..." +for i in $(seq 2 $NUM_VALIDATORS); do + docker run --rm --user root \ + -v canto_validator1_home:/src:ro \ + -v canto_validator${i}_home:/dst \ + alpine sh -c "cp /src/config/genesis.json /dst/config/genesis.json" 2>/dev/null + echo "Genesis -> Validator $i" +done +echo "" + +# ---------------------------------------------------------- +# Step 8: Create gentx for each validator +# ---------------------------------------------------------- +echo "Step 8/${TOTAL_STEPS}: Creating gentx..." +for i in $(seq 1 $NUM_VALIDATORS); do + run_genesis_cmd "canto_validator${i}_home" gentx validator${i} ${VALIDATOR_STAKE} \ + --chain-id ${CHAIN_ID} --keyring-backend test + echo "Validator $i: gentx created" +done +echo "" + +# ---------------------------------------------------------- +# Step 9: Collect gentxs and configure node settings +# ---------------------------------------------------------- +echo "Step 9/${TOTAL_STEPS}: Collecting gentxs and patching configs..." +for i in $(seq 2 $NUM_VALIDATORS); do + docker run --rm --user root \ + -v canto_validator${i}_home:/src:ro \ + -v canto_validator1_home:/dst \ + alpine sh -c "cp /src/config/gentx/* /dst/config/gentx/" 2>/dev/null +done + +run_genesis_cmd "canto_validator1_home" collect-gentxs +run_genesis_cmd "canto_validator1_home" validate-genesis || true + +for i in $(seq 1 $NUM_VALIDATORS); do + docker run --rm --user root --entrypoint sh \ + -v canto_validator${i}_home:${CANTO_HOME} \ + "$IMAGE" -c ' + CFG='"${CANTO_HOME}"'/config/config.toml + APP='"${CANTO_HOME}"'/config/app.toml + + sed -i "s|laddr = \"tcp://127.0.0.1:26657\"|laddr = \"tcp://0.0.0.0:26657\"|" "$CFG" + sed -i "s|laddr = \"tcp://localhost:26657\"|laddr = \"tcp://0.0.0.0:26657\"|" "$CFG" + sed -i "s|laddr = \"tcp://127.0.0.1:26656\"|laddr = \"tcp://0.0.0.0:26656\"|" "$CFG" + sed -i "s|laddr = \"tcp://localhost:26656\"|laddr = \"tcp://0.0.0.0:26656\"|" "$CFG" + sed -i "s|cors_allowed_origins = \\[\\]|cors_allowed_origins = [\"*\"]|" "$CFG" + + sed -i "/\\[api\\]/,/\\[/{s|enable = false|enable = true|}" "$APP" + sed -i "s|address = \"tcp://localhost:1317\"|address = \"tcp://0.0.0.0:1317\"|" "$APP" + sed -i "s|address = \"tcp://127.0.0.1:1317\"|address = \"tcp://0.0.0.0:1317\"|" "$APP" + sed -i "s|enabled-unsafe-cors = false|enabled-unsafe-cors = true|" "$APP" + + sed -i "s|address = \"localhost:9090\"|address = \"0.0.0.0:9090\"|" "$APP" + sed -i "s|address = \"127.0.0.1:9090\"|address = \"0.0.0.0:9090\"|" "$APP" + + sed -i "/\\[json-rpc\\]/,/\\[/{s|enable = false|enable = true|}" "$APP" + sed -i "s|address = \"127.0.0.1:8545\"|address = \"0.0.0.0:8545\"|" "$APP" + sed -i "s|address = \"localhost:8545\"|address = \"0.0.0.0:8545\"|" "$APP" + sed -i "s|ws-address = \"127.0.0.1:8546\"|ws-address = \"0.0.0.0:8546\"|" "$APP" + sed -i "s|ws-address = \"localhost:8546\"|ws-address = \"0.0.0.0:8546\"|" "$APP" + sed -i "s|api = \"eth,net,web3\"|api = \"eth,txpool,personal,net,debug,web3\"|" "$APP" + sed -i "s|api = \"eth,web3,net,txpool,debug\"|api = \"eth,txpool,personal,net,debug,web3\"|" "$APP" + + sed -i "s|minimum-gas-prices = \"\"|minimum-gas-prices = \"0.0001acanto\"|" "$APP" + sed -i "s|minimum-gas-prices = \"0stake\"|minimum-gas-prices = \"0.0001acanto\"|" "$APP" + sed -i "s|minimum-gas-prices = \"0acanto\"|minimum-gas-prices = \"0.0001acanto\"|" "$APP" + ' >/dev/null 2>&1 + + echo "Validator $i: configured" +done +echo "" + +# ---------------------------------------------------------- +# Step 10: Distribute final genesis to all nodes +# ---------------------------------------------------------- +echo "Step 10/${TOTAL_STEPS}: Distributing final genesis.json..." +for i in $(seq 2 $NUM_VALIDATORS); do + docker run --rm --user root \ + -v canto_validator1_home:/src:ro \ + -v canto_validator${i}_home:/dst \ + alpine sh -c "cp /src/config/genesis.json /dst/config/genesis.json" 2>/dev/null + echo "Genesis -> Validator $i" +done +echo "" + +# ---------------------------------------------------------- +# Step 11: Get node IDs and write PERSISTENT_PEERS +# ---------------------------------------------------------- +echo "Step 11/${TOTAL_STEPS}: Getting CometBFT Node IDs..." +PEERS="" +for i in $(seq 1 $NUM_VALIDATORS); do + NODE_ID=$(docker run --rm --user root --entrypoint cantod \ + -v canto_validator${i}_home:${CANTO_HOME} \ + "$IMAGE" comet show-node-id --home ${CANTO_HOME} 2>/dev/null || \ + docker run --rm --user root --entrypoint cantod \ + -v canto_validator${i}_home:${CANTO_HOME} \ + "$IMAGE" tendermint show-node-id --home ${CANTO_HOME} 2>/dev/null) + + NODE_ID=$(echo "$NODE_ID" | tr -d '\n\r') + PEER="${NODE_ID}@canto-validator${i}:26656" + echo "Validator $i: ${PEER}" + + if [ -z "$PEERS" ]; then + PEERS="$PEER" + else + PEERS="${PEERS},${PEER}" + fi +done +echo "" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cp "${SCRIPT_DIR}/.env.multinode.sample" "${SCRIPT_DIR}/.env.multinode" 2>/dev/null || true +ENV_FILE="${SCRIPT_DIR}/.env.multinode" + +if [ -f "$ENV_FILE" ] && grep -q "^PERSISTENT_PEERS=" "$ENV_FILE"; then + sed -i.bak "s|^PERSISTENT_PEERS=.*|PERSISTENT_PEERS=${PEERS}|" "$ENV_FILE" + rm -f "${ENV_FILE}.bak" +else + echo "PERSISTENT_PEERS=${PEERS}" >> "$ENV_FILE" +fi +echo "Updated PERSISTENT_PEERS in .env.multinode" + +echo "" +echo "============================================" +echo "Initialization complete!" +echo "${NUM_VALIDATORS} validators configured" +echo "============================================" +echo "" +echo "Founder Wallet (Hardhat Account #0):" +echo "Cosmos: ${FOUNDER_ADDR}" +echo "EVM: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +echo "" +echo "Next steps:" +echo "1. Start network: ./start-multinode.sh" +echo "2. Check status: docker compose --env-file .env.multinode -f docker-compose.yml ps" +echo "3. Stop network: ./stop-multinode.sh" +echo "" diff --git a/chains/canto-docker/start-multinode.sh b/chains/canto-docker/start-multinode.sh new file mode 100755 index 0000000..38c93b6 --- /dev/null +++ b/chains/canto-docker/start-multinode.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# start-multinode.sh — Start Canto 4-validator localnet +# +# Prerequisites: +# Run ./init-multinode.sh first to initialize all validators +# +# Usage: +# chmod +x start-multinode.sh +# ./start-multinode.sh + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" + +if [ ! -f "${SCRIPT_DIR}/.env.multinode" ]; then + echo "Error: .env.multinode not found. Run ./init-multinode.sh first." + exit 1 +fi + +echo "Starting Canto multi-validator localnet..." +docker compose --env-file "${SCRIPT_DIR}/.env.multinode" -f "$COMPOSE_FILE" up -d + +echo "" +echo "Waiting for network to produce blocks..." +MAX_WAIT=120 +WAITED=0 +HEIGHT="0" +while [ $WAITED -lt $MAX_WAIT ]; do + HEIGHT=$(curl -s http://localhost:26657/status 2>/dev/null | \ + grep -o '"latest_block_height":"[0-9]*"' | \ + grep -o '[0-9]*' || echo "0") + + if [ "$HEIGHT" != "0" ] && [ -n "$HEIGHT" ]; then + echo "Block height: ${HEIGHT}" + break + fi + + echo "Waiting... (${WAITED}s)" + sleep 5 + WAITED=$((WAITED + 5)) +done + +if [ $WAITED -ge $MAX_WAIT ]; then + echo "Timeout waiting for blocks. Check logs:" + echo "1. docker compose --env-file .env.multinode -f docker-compose.yml logs canto-validator1" + echo "2. docker compose --env-file .env.multinode -f docker-compose.yml ps" + exit 1 +fi + +VALIDATOR_COUNT=$(curl -s http://localhost:26657/validators 2>/dev/null | \ + grep -o '"total":"[0-9]*"' | \ + grep -o '[0-9]*' | head -1 || echo "unknown") + +EVM_OK=$(curl -s -X POST http://localhost:8545 \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' 2>/dev/null | \ + grep -c "result" 2>/dev/null || true) +EVM_OK=${EVM_OK:-0} +EVM_OK=$((EVM_OK + 0)) + +if [ "$EVM_OK" -gt 0 ] 2>/dev/null; then + EVM_STATUS="Ready" +else + EVM_STATUS="Not ready yet" +fi + +echo "" +echo "Canto multi-validator localnet started!" +echo "" +echo "Network Status:" +echo "Block Height: ${HEIGHT}" +echo "Validators: ${VALIDATOR_COUNT}" +echo "EVM JSON-RPC: ${EVM_STATUS}" +echo "" +echo "Validator 1 Endpoints (primary):" +echo "CometBFT RPC: http://localhost:26657" +echo "Cosmos REST: http://localhost:1317" +echo "EVM JSON-RPC: http://localhost:8545" +echo "EVM WebSocket: ws://localhost:8546" +echo "gRPC: localhost:9090" +echo "" +echo "Other Validators:" +echo "Validator 2: RPC=:36657 REST=:21317 EVM=:28545" +echo "Validator 3: RPC=:46657 REST=:31317 EVM=:38545" +echo "Validator 4: RPC=:56657 REST=:41317 EVM=:48545" +echo "" +echo "To stop: ./stop-multinode.sh" +echo "" diff --git a/chains/canto-docker/stop-multinode.sh b/chains/canto-docker/stop-multinode.sh new file mode 100755 index 0000000..fff919c --- /dev/null +++ b/chains/canto-docker/stop-multinode.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# stop-multinode.sh — Stop Canto multi-validator localnet +# +# Usage: +# chmod +x stop-multinode.sh +# ./stop-multinode.sh [--clean] +# +# Options: +# --clean Also remove Docker volumes (full reset, requires re-init) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.yml" + +echo "Stopping Canto multi-validator localnet..." +docker compose --env-file "${SCRIPT_DIR}/.env.multinode" -f "$COMPOSE_FILE" down 2>/dev/null || true +echo "Containers stopped" + +if [ "$1" = "--clean" ]; then + echo "" + echo "Cleaning Docker volumes..." + for i in 1 2 3 4; do + docker volume rm -f canto_validator${i}_home 2>/dev/null || true + done + rm -f "${SCRIPT_DIR}/.env.multinode" 2>/dev/null || true + echo "Volumes and config cleaned" + echo "Run ./init-multinode.sh to re-initialize" +fi + +echo "" +echo "Canto multi-validator localnet stopped." diff --git a/tests/config.ci.json b/tests/config.ci.json index 6ef6b1b..cb43de0 100644 --- a/tests/config.ci.json +++ b/tests/config.ci.json @@ -321,6 +321,78 @@ "privateKeySource": "local" } }, + "canto-docker": { + "description": "Canto Local Multi-Validator Devnet (4 validators)", + "supportedCosmosModules": ["staking", "staking_delegation", "slashing"], + "consensusLayer": "cosmos", + "executeLayer": "evm", + "chainId": 7700, + "executeLayerHttpRpcUrl": "http://localhost:8545", + "consensusLayerRpcUrl": "http://localhost:26657", + "consensusLayerHttpRestApiUrl": "http://localhost:1317", + "consensusRestApiPathPrefix": "/cosmos", + "consensusRestApiVersion": "v1beta1", + "executionMethod": "docker", + "docker": { + "containerPatterns": { + "executionLayer": "canto-docker-canto-validator{index}-1", + "consensusLayer": "canto-docker-canto-validator{index}-1" + }, + "timeout": 30000 + }, + "nodes": [ + { + "index": 1, + "url": "http://localhost", + "type": "validator", + "votingPower": 1, + "active": true, + "executeLayerHttpRpcPort": 8545, + "consensusLayerRpcPort": 26657, + "consensusLayerHttpRestApiPort": 1317, + "consensusLayerP2pCommPort": 26656 + }, + { + "index": 2, + "url": "http://localhost", + "type": "validator", + "votingPower": 1, + "active": true, + "executeLayerHttpRpcPort": 28545, + "consensusLayerRpcPort": 36657, + "consensusLayerHttpRestApiPort": 21317, + "consensusLayerP2pCommPort": 36656 + }, + { + "index": 3, + "url": "http://localhost", + "type": "validator", + "votingPower": 1, + "active": true, + "executeLayerHttpRpcPort": 38545, + "consensusLayerRpcPort": 46657, + "consensusLayerHttpRestApiPort": 31317, + "consensusLayerP2pCommPort": 46656 + }, + { + "index": 4, + "url": "http://localhost", + "type": "validator", + "votingPower": 1, + "active": true, + "executeLayerHttpRpcPort": 48545, + "consensusLayerRpcPort": 56657, + "consensusLayerHttpRestApiPort": 41317, + "consensusLayerP2pCommPort": 56656 + } + ], + "founderWallet": { + "name": "founder", + "address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "privateKey": "", + "privateKeySource": "local" + } + }, "mezo-docker": { "description": "Mezo Local Multi-Validator Devnet (4 validators, Evmos fork)", "supportedCosmosModules": [],