From 242f8d315792fa58061de0041e0a1f16fab88b16 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Thu, 26 Mar 2026 13:35:56 -0400 Subject: [PATCH 01/12] feat: add Dockerfile with BSC build env --- Dockerfile | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..547b8299 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# Multi-stage Dockerfile for BSC Node +# Stage 1: Build Environment with all required tools +# Using Bookworm as base for newer package support +FROM golang:1.24-bookworm AS build-env + +# Arguments for versions +ARG NODE_VERSION=16.x + +# Install basic dependencies +RUN apt-get update && apt-get install -y \ + curl \ + git \ + make \ + jq \ + build-essential \ + software-properties-common \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js (README mentions v16.15.0) +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - && \ + apt-get install -y nodejs && \ + npm install -g npm@6.14.6 + +# Install Python 3.12 and Poetry +# Since Bookworm (Debian 12) has 3.11, we add the fast track or just use a python base +# To strictly match 3.12.x, we'll use the official python image logic or deadsnakes-like approach +RUN apt-get update && apt-get install -y \ + python3.12 \ + python3.12-dev \ + python3.12-venv \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +# Ensure python3 points to 3.12 +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 + +# Install Poetry using its official installer +RUN curl -sSL https://install.python-poetry.org | python3 - +ENV PATH="/root/.local/bin:$PATH" + +# Install Foundry (Ethereum development toolkit) +# This is needed because BSC's system contracts (in the genesis/ folder) use 'forge' to compile. +RUN curl -L https://foundry.paradigm.xyz | bash && \ + /root/.foundry/bin/foundryup +ENV PATH="/root/.foundry/bin:$PATH" + +WORKDIR /app + +# Copy necessary files for tool verification +COPY requirements.txt . +# Note: Poetry installation of dependencies usually happens here if we have pyproject.toml + +# Tool verification command +RUN go version && \ + node -v && \ + npm -v && \ + python3 --version && \ + poetry --version && \ + forge --version && \ + jq --version From 5aeea830493ef2c58f2e47b2bc5294531a74c817 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Fri, 27 Mar 2026 09:20:52 -0400 Subject: [PATCH 02/12] feat: prepare env for bsc cluster - set useLatestBscClinet to true to compile geth with latest geth on github - xargs -r to ignore error if no previous geth found --- .env | 2 +- Dockerfile | 37 +++++++++++++++++++------------------ bsc_cluster.sh | 3 ++- docker-compose.yml | 23 +++++++++++++++++++++++ 4 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 docker-compose.yml diff --git a/.env b/.env index ebf2b0d7..a5826f58 100644 --- a/.env +++ b/.env @@ -11,7 +11,7 @@ FullImmutabilityThreshold=2048 MinBlocksForBlobRequests=576 DefaultExtraReserveForBlobRequests=32 BreatheBlockInterval=1200 -useLatestBscClient=false +useLatestBscClient=true EnableSentryNode=false EnableFullNode=false RegisterNodeID=false diff --git a/Dockerfile b/Dockerfile index 547b8299..78e2b04b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,17 @@ # Multi-stage Dockerfile for BSC Node # Stage 1: Build Environment with all required tools -# Using Bookworm as base for newer package support -FROM golang:1.24-bookworm AS build-env +# We use Python 3.12 as the base to fulfill the requirement, then add Go 1.24 +FROM python:3.12-bookworm AS build-env # Arguments for versions ARG NODE_VERSION=16.x +ARG GO_VERSION=1.24.0 + +# Install Go 1.24 (Detect architecture to support both x86_64 and arm64) +RUN ARCH=$(uname -m) && \ + if [ "$ARCH" = "x86_64" ]; then GO_ARCH="amd64"; else GO_ARCH="arm64"; fi && \ + curl -fsSL https://go.dev/dl/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz | tar -C /usr/local -xz +ENV PATH="/usr/local/go/bin:$PATH" # Install basic dependencies RUN apt-get update && apt-get install -y \ @@ -21,20 +28,8 @@ RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - && \ apt-get install -y nodejs && \ npm install -g npm@6.14.6 -# Install Python 3.12 and Poetry -# Since Bookworm (Debian 12) has 3.11, we add the fast track or just use a python base -# To strictly match 3.12.x, we'll use the official python image logic or deadsnakes-like approach -RUN apt-get update && apt-get install -y \ - python3.12 \ - python3.12-dev \ - python3.12-venv \ - python3-pip \ - && rm -rf /var/lib/apt/lists/* - -# Ensure python3 points to 3.12 -RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.12 1 - # Install Poetry using its official installer +# (Python 3.12 is already the system default in this image) RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH="/root/.local/bin:$PATH" @@ -44,13 +39,16 @@ RUN curl -L https://foundry.paradigm.xyz | bash && \ /root/.foundry/bin/foundryup ENV PATH="/root/.foundry/bin:$PATH" -WORKDIR /app +# Setting up the workspace +WORKDIR /node_deploy -# Copy necessary files for tool verification +# Copy and install Python requirements +# This avoids installing them manually every time you restart the container. COPY requirements.txt . -# Note: Poetry installation of dependencies usually happens here if we have pyproject.toml +RUN pip3 install --no-cache-dir -r requirements.txt # Tool verification command +# This ensures that when the image is built, all required tools are present and functional. RUN go version && \ node -v && \ npm -v && \ @@ -58,3 +56,6 @@ RUN go version && \ poetry --version && \ forge --version && \ jq --version + +# Default command: show help or just stay open +CMD ["bash"] diff --git a/bsc_cluster.sh b/bsc_cluster.sh index c0c0fe91..4c8e115f 100644 --- a/bsc_cluster.sh +++ b/bsc_cluster.sh @@ -19,7 +19,8 @@ sleepAfterStart=10 # stop geth client function exit_previous() { ValIdx=$1 - ps -ef | grep geth$ValIdx | grep config |awk '{print $2}' | xargs kill + # if no geth exist just skip it + ps -ef | grep geth$ValIdx | grep config |awk '{print $2}' | xargs -r kill sleep ${sleepBeforeStart} } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..976afccf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + bsc-node: + build: + context: . + dockerfile: Dockerfile + image: bsc-toolbox:latest + container_name: bsc-toolbox + volumes: + - .:/node_deploy + # Opening RPC ports for 4 nodes: 8545, 8547, 8549, 8551 + # Opening Metrics ports for 4 nodes: 6060, 6062, 6064, 6066 + ports: + - "8545:8545" + - "8547:8547" + - "8549:8549" + - "8551:8551" + - "6060:6060" + - "6062:6062" + - "6064:6064" + - "6066:6066" + stdin_open: true + tty: true + command: /bin/bash From 89596451538e33e68505c0f040ff64a492800c35 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Fri, 27 Mar 2026 11:52:34 -0400 Subject: [PATCH 03/12] feat: auto build when spin up the container --- Dockerfile | 22 +++++++++++++++++++++- README.md | 43 ++++++++++++++++++++++++++++++++++++++++++- bsc_cluster.sh | 2 +- docker-compose.yml | 3 ++- entrypoint.sh | 27 +++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 78e2b04b..43649e73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,6 +38,7 @@ ENV PATH="/root/.local/bin:$PATH" RUN curl -L https://foundry.paradigm.xyz | bash && \ /root/.foundry/bin/foundryup ENV PATH="/root/.foundry/bin:$PATH" +ENV PATH="/node_deploy/bin:$PATH" # Setting up the workspace WORKDIR /node_deploy @@ -47,6 +48,16 @@ WORKDIR /node_deploy COPY requirements.txt . RUN pip3 install --no-cache-dir -r requirements.txt +# Build and install the validator tool +# By installing to /usr/local/bin, we ensure the binary is available even if the +# /node_deploy directory is shadowed by a volume mount from the host. +COPY create-validator/ ./create-validator/ +RUN cd create-validator && go build -o /usr/local/bin/create-validator + +# Copy entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + # Tool verification command # This ensures that when the image is built, all required tools are present and functional. RUN go version && \ @@ -55,7 +66,16 @@ RUN go version && \ python3 --version && \ poetry --version && \ forge --version && \ - jq --version + jq --version && \ + create-validator --help + +# Add init check to .bashrc to warn users when they login +RUN echo 'if [ -f /tmp/cluster_initializing ]; then' >> /root/.bashrc && \ + echo ' echo "----------------------------------------------------"' >> /root/.bashrc && \ + echo ' echo " WARNING: Cluster is still initializing. Please wait! "' >> /root/.bashrc && \ + echo ' echo "----------------------------------------------------"' >> /root/.bashrc && \ + echo ' echo ""' >> /root/.bashrc && \ + echo 'fi' >> /root/.bashrc # Default command: show help or just stay open CMD ["bash"] diff --git a/README.md b/README.md index cf348fcc..b5463120 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ ## Installation Before proceeding to the next steps, please ensure that the following packages and softwares are well installed in your local machine: + +> [!TIP] +> **Don't want to install these manually?** You can skip to the [Docker Version](#docker-version-recommended) section at the bottom of this file. + - nodejs: v16.15.0 - npm: 6.14.6 - go: 1.24+ @@ -87,4 +91,41 @@ go build cd txblob go build ./txblob -``` \ No newline at end of file +``` + +## Docker Version (Recommended) + +If you don't want to install all dependencies manually on your Mac/PC, you can use the Dockerized toolbox: + +1. **Build and Start Toolbox**: + This command builds the environment, compiles the `create-validator` tool, and **automatically initializes** the BSC cluster. + + ```bash + docker compose up -d --build + ``` + +2. **Enter the Container**: + Once the cluster is initialized (you can check `docker logs -f bsc-toolbox`), enter the container to use the development tools: + + ```bash + docker exec -it bsc-toolbox bash + ``` + +3. **Validator Tool**: + The `create-validator` tool is pre-built and installed at `/usr/local/bin/create-validator`. You can run it from anywhere inside the container. + +4. **Observe from Host (Mac)**: + Access RPC at `http://localhost:8545`. + +### Common Observation Commands (Inside Container) + +Here are some useful commands to monitor the cluster: + +- **Check Block Height**: + `geth --exec "eth.blockNumber" attach .local/node0/geth.ipc` +- **List P2P Peers**: + `geth --exec "admin.peers" attach .local/node0/geth.ipc` +- **Check Node Info**: + `geth --exec "admin.nodeInfo" attach .local/node0/geth.ipc` +- **Tail Logs**: + `tail -f .local/node0/bsc-node.log` \ No newline at end of file diff --git a/bsc_cluster.sh b/bsc_cluster.sh index 4c8e115f..310e80a3 100644 --- a/bsc_cluster.sh +++ b/bsc_cluster.sh @@ -279,7 +279,7 @@ function register_stakehub(){ # wait feynman enable sleep 45 for ((i = 0; i < size; i++));do - ${workspace}/create-validator/create-validator --consensus-key-dir ${workspace}/keys/validator${i} --vote-key-dir ${workspace}/keys/bls${i} \ + create-validator --consensus-key-dir ${workspace}/keys/validator${i} --vote-key-dir ${workspace}/keys/bls${i} \ --password-path ${workspace}/keys/password.txt --amount 20001 --validator-desc Val${i} --rpc-url ${RPC_URL} done } diff --git a/docker-compose.yml b/docker-compose.yml index 976afccf..a806e8f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,4 +20,5 @@ services: - "6066:6066" stdin_open: true tty: true - command: /bin/bash + entrypoint: /usr/local/bin/entrypoint.sh + command: ["/bin/bash"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..59d271ea --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -e + +# Change to the application directory +cd /node_deploy + +# Create a lock file to indicate initialization is in progress +touch /tmp/cluster_initializing + +# If the container is starting for the first time or if we want to ensure +# a fresh start, we run the reset script. +echo "====================================================" +echo "Initializing BSC cluster... Please wait." +echo "use docker logs -f bsc-toolbox check for progress" +echo "====================================================" +bash ./bsc_cluster.sh reset + +# Remove the lock file and signal completion +rm /tmp/cluster_initializing +echo "" +echo "====================================================" +echo ">>> BSC CLUSTER INITIALIZATION COMPLETE! <<<" +echo "====================================================" +echo "" + +# Execute the passed command (e.g., /bin/bash) +exec "$@" From b17343a2a1f73526254dc871a88968f631f2f33e Mon Sep 17 00:00:00 2001 From: Isaac T Date: Sat, 28 Mar 2026 15:40:14 -0400 Subject: [PATCH 04/12] chore: change Pebble path mode to LevelDB hash - cause in local Pebble take too much of my storage space --- bsc_cluster.sh | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/bsc_cluster.sh b/bsc_cluster.sh index 310e80a3..0292c747 100644 --- a/bsc_cluster.sh +++ b/bsc_cluster.sh @@ -16,7 +16,7 @@ gcmode="full" sleepBeforeStart=15 sleepAfterStart=10 -# stop geth client +# 1. Stop any previously running geth instances function exit_previous() { ValIdx=$1 # if no geth exist just skip it @@ -24,6 +24,7 @@ function exit_previous() { sleep ${sleepBeforeStart} } +# 2. Cleanup .local and copy initial keys for validators function create_validator() { rm -rf ${workspace}/.local mkdir -p ${workspace}/.local @@ -34,6 +35,7 @@ function create_validator() { done } +# 3. Build bsc geth client from source if configured function prepare_bsc_client() { if [ ${useLatestBscClient} = true ]; then if [ ! -f "${workspace}/bsc/Makefile" ]; then @@ -43,7 +45,9 @@ function prepare_bsc_client() { cd ${workspace}/bsc && git pull && make geth && mv -f ${workspace}/bsc/build/bin/geth ${workspace}/bin/ fi } -# reset genesis, but keep edited genesis-template.json + +# 4. Reset genesis submodule and install dependencies (Poetry, NPM, Forge) +# This prepares the environment for generating the genesis block function reset_genesis() { if [ ! -f "${workspace}/genesis/genesis-template.json" ]; then cd ${workspace} && git submodule update --init --recursive genesis @@ -67,6 +71,7 @@ function reset_genesis() { git clone https://github.com/dapphub/ds-test } +# 5. Generate validator configurations, hardfork times, and the final genesis.json function prepare_config() { rm -f ${workspace}/genesis/validators.conf @@ -125,6 +130,7 @@ function prepare_config() { cp genesis-dev.json genesis.json } +# 6. Initialize the geth network for each node using the generated genesis.json function initNetwork() { cd ${workspace} for ((i = 0; i < size; i++)); do @@ -168,17 +174,13 @@ function initNetwork() { sed -i -e '/""/d' ${workspace}/.local/node${i}/config.toml # init genesis initLog=${workspace}/.local/node${i}/init.log - if [ $i -eq 0 ] ; then - ${workspace}/bin/geth --datadir ${workspace}/.local/node${i} init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 - else - ${workspace}/bin/geth --datadir ${workspace}/.local/node${i} init --state.scheme path --db.engine pebble ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 - fi + ${workspace}/bin/geth --datadir ${workspace}/.local/node${i} init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 rm -f ${workspace}/.local/node${i}/*bsc.log* if [ ${EnableSentryNode} = true ]; then sed -i -e '/""/d' ${workspace}/.local/sentry${i}/config.toml initLog=${workspace}/.local/sentry${i}/init.log - ${workspace}/bin/geth --datadir ${workspace}/.local/sentry${i} init --state.scheme path --db.engine pebble ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 + ${workspace}/bin/geth --datadir ${workspace}/.local/sentry${i} init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 rm -f ${workspace}/.local/sentry${i}/*bsc.log* fi done @@ -186,7 +188,7 @@ function initNetwork() { sed -i -e '/""/d' ${workspace}/.local/fullnode0/config.toml sed -i -e 's/EnableEVNFeatures = true/EnableEVNFeatures = false/g' ${workspace}/.local/fullnode0/config.toml initLog=${workspace}/.local/fullnode0/init.log - ${workspace}/bin/geth --datadir ${workspace}/.local/fullnode0 init --state.scheme path --db.engine pebble ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 + ${workspace}/bin/geth --datadir ${workspace}/.local/fullnode0 init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 rm -f ${workspace}/.local/fullnode0/*bsc.log* fi } @@ -229,6 +231,7 @@ function start_node() { >> ${datadir}/bsc-node.log 2>&1 & } +# 7. Start the nodes (Validators, Sentry, Full) in the background function native_start() { PassedForkTime=`cat ${workspace}/.local/node0/hardforkTime.txt|grep passedHardforkTime|awk -F" " '{print $NF}'` LastHardforkTime=$(expr ${PassedForkTime} + ${LAST_FORK_MORE_DELAY}) @@ -275,6 +278,7 @@ function native_start() { sleep ${sleepAfterStart} } +# 8. Use create-validator tool to register validator nodes on StakeHub function register_stakehub(){ # wait feynman enable sleep 45 @@ -284,18 +288,20 @@ function register_stakehub(){ done } +# Command dispatcher CMD=$1 ValidatorIdx=$2 case ${CMD} in reset) - exit_previous - create_validator - prepare_bsc_client - reset_genesis - prepare_config - initNetwork - native_start - register_stakehub + # The 'reset' flow perform a clean initialization of the entire local cluster: + exit_previous # Step 1: Kill old processes + create_validator # Step 2: Prepare keys and .local/ + prepare_bsc_client # Step 3: Build geth if necessary + reset_genesis # Step 4: Setup genesis deps (Forge, Poetry, etc) + prepare_config # Step 5: Generate genesis.json and node configs + initNetwork # Step 6: Initialize Geth data directories + native_start # Step 7: Start the cluster nodes + register_stakehub # Step 8: Register validators with the network ;; stop) exit_previous $ValidatorIdx From 30842b939949a42b8f50ec7381d87365620f4e6f Mon Sep 17 00:00:00 2001 From: Isaac T Date: Mon, 30 Mar 2026 13:58:10 -0400 Subject: [PATCH 05/12] feat: Update metrics and pprof bind addresses to 0.0.0.0 - allow monitoring access from the host machine via Docker port forwarding. --- bsc_cluster.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 bsc_cluster.sh diff --git a/bsc_cluster.sh b/bsc_cluster.sh old mode 100644 new mode 100755 index 0292c747..514613a1 --- a/bsc_cluster.sh +++ b/bsc_cluster.sh @@ -212,8 +212,8 @@ function start_node() { --rpc.allow-unprotected-txs --allow-insecure-unlock \ --ws --ws.addr 0.0.0.0 --ws.port ${ws_port} \ --http --http.addr 0.0.0.0 --http.port ${http_port} --http.corsdomain "*" \ - --metrics --metrics.addr localhost --metrics.port ${metrics_port} \ - --pprof --pprof.addr localhost --pprof.port ${pprof_port} \ + --metrics --metrics.addr 0.0.0.0 --metrics.port ${metrics_port} \ + --pprof --pprof.addr 0.0.0.0 --pprof.port ${pprof_port} \ --gcmode ${gcmode} --syncmode full --monitor.maliciousvote \ --rialtohash ${rialtoHash} \ --override.passedforktime ${PassedForkTime} \ From db69ba3c509fee8cd8a2e2cd3dc10b96cc3a7475 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Mon, 30 Mar 2026 14:04:20 -0400 Subject: [PATCH 06/12] feat: forward pprof port --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index a806e8f7..51604ba2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,10 @@ services: - "6062:6062" - "6064:6064" - "6066:6066" + - "7060:7060" + - "7062:7062" + - "7064:7064" + - "7066:7066" stdin_open: true tty: true entrypoint: /usr/local/bin/entrypoint.sh From 87c4ce8a7b876bcb6b29eca1fbde47eb98925099 Mon Sep 17 00:00:00 2001 From: Isaac T Date: Mon, 30 Mar 2026 14:04:40 -0400 Subject: [PATCH 07/12] docs: update readme --- README.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b5463120..3cd5a07e 100644 --- a/README.md +++ b/README.md @@ -128,4 +128,29 @@ Here are some useful commands to monitor the cluster: - **Check Node Info**: `geth --exec "admin.nodeInfo" attach .local/node0/geth.ipc` - **Tail Logs**: - `tail -f .local/node0/bsc-node.log` \ No newline at end of file + `tail -f .local/node0/bsc-node.log` + +### Monitoring and Metrics + +The cluster is configured to expose internal metrics for monitoring. These are accessible from your **(Host)**: + +- **Prometheus Metrics**: `http://localhost:6060/debug/metrics/prometheus` +- **JSON Metrics**: `http://localhost:6060/debug/metrics` +- **Performance Profiling (pprof)**: `http://localhost:7060/debug/pprof/` + +**Port Mapping for Nodes:** + +| Node | RPC (HTTP/WS) | Metrics (Prometheus) | pprof (Debug) | +| :--- | :--- | :--- | :--- | +| **Node 0** | 8545 | 6060 | 7060 | +| **Node 1** | 8547 | 6062 | 7062 | +| **Node 2** | 8549 | 6064 | 7064 | +| **Node 3** | 8551 | 6066 | 7066 | + +### Storage Optimization + +To prevent rapid disk space exhaustion during local testing, this setup uses the following optimizations: + +- **DB Engine**: Forced to `leveldb` (more space-efficient than Pebble for small clusters). +- **State Scheme**: Set to `hash` to significantly reduce state storage size compared to the default path-based scheme. +- **Auto-Reset**: The `docker compose up` command triggers a full reset by default, ensuring you always start with a clean state. \ No newline at end of file From e950c144c7761c72feba2ed010cbc88c470ef121 Mon Sep 17 00:00:00 2001 From: Isaac Tai Date: Thu, 2 Apr 2026 09:18:36 -0400 Subject: [PATCH 08/12] Feat/isolate nodes per container (#3) * fix: prevent OOM kills by geth cache limit - Without this, geth implicitly bumps the default cache to 4096MB when treating the setup as mainnet, which instantly exhausts Docker's memory limit (~8GB) when spinning up a 4-node local cluster. * Add comments to clarify code logic * feat: new script for docker spin up - modify p2p address from localhost to use docker internal DNS name - only handle geth initialization, but not start the node * feat: implement dynamic compose generation - Introduce `NODE_TYPE` and `NODE_INDEX` environment variables to uniquely identify each container and correctly mount its respective data dir (`.local/nodeX`). * feat: add docker entrypoint for bsc cluster - Makefile for lifecycle - docker initialization script "node_entrypoint.sh" for booting geth based on node type (validator, sentry, fullnode) * fix: redirect geth logs to docker stdout forces Geth to output logs to standard output instead of local files, enabling docker's default logging driver to capture * feat: optimize geth build cache - Replaced `mv` with `cp` when extracting the compiled geth binary. prevent redundant rebuilds. - Removed unused `exit_previous` func - Add cluster-restart for fast restart without wiping blockchain data or trigger re-initialization. * docs: update readme with sequence diagram --- .gitignore | 3 + Makefile | 37 +++++ README.md | 92 ++++++----- bsc_cluster.sh | 30 ++++ docker_cluster.sh | 379 +++++++++++++++++++++++++++++++++++++++++++++ node_entrypoint.sh | 67 ++++++++ 6 files changed, 566 insertions(+), 42 deletions(-) create mode 100644 Makefile create mode 100644 docker_cluster.sh create mode 100755 node_entrypoint.sh diff --git a/.gitignore b/.gitignore index ab57b06f..27fe633a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ .idea/ lib/ +bsc/ +docker-compose.cluster.yml +.env.cluster \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..50a1d193 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# Makefile for BSC Local Cluster + +.PHONY: cluster-up cluster-down cluster-logs cluster-clean cluster-restart + +# Default Image to use for bootstrapping +TOOLBOX_IMAGE ?= bsc-toolbox:latest + +# Auto initialize and bring up the cluster +cluster-up: + @echo "[Phase 1] Initializing blockchain data & configs using isolated Toolbox environment..." + docker run --rm -v "$(PWD):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh prepare + @echo "" + @echo "[Phase 2] Data prepared! Starting BSC cluster via Docker Compose..." + docker compose -f docker-compose.cluster.yml up -d + @echo "BSC Local Cluster successfully started in background! Run 'make cluster-logs' to view live logs." + +# Safely stop and remove all containers +cluster-down: + @echo "Stopping and removing all BSC containers..." + if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml down; fi + +# View real-time logs for all cluster nodes +cluster-logs: + if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml logs -f; fi + +# Completely wipe cluster data and auto-generated configs (DANGEROUS) +cluster-clean: cluster-down + @echo "Wiping all local cluster data and temporary configurations (.local/, YAML, .env)..." + rm -rf .local + rm -f .env.cluster docker-compose.cluster.yml + @echo "Workspace is completely clean." + +# Fast restart without wiping data or rebuilding config +cluster-restart: cluster-down + @echo "Restarting all BSC containers with existing config (Phase 2 only)..." + if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml up -d; fi + @echo "BSC Local Cluster successfully restarted." diff --git a/README.md b/README.md index 3cd5a07e..3d8d6bdc 100644 --- a/README.md +++ b/README.md @@ -95,62 +95,70 @@ go build ## Docker Version (Recommended) -If you don't want to install all dependencies manually on your Mac/PC, you can use the Dockerized toolbox: +To run a fully containerized, isolated local BSC cluster without installing dependencies on your host machine, use the provided `Makefile` which handles the 2-phase orchestration automatically. -1. **Build and Start Toolbox**: - This command builds the environment, compiles the `create-validator` tool, and **automatically initializes** the BSC cluster. +### Architecture Workflow - ```bash - docker compose up -d --build - ``` +```mermaid +sequenceDiagram + participant User + participant Makefile + participant Toolbox as Toolbox (Docker) + participant HostFS as Host FileSystem + participant Compose as Docker Compose + participant Docker as Docker Engine -2. **Enter the Container**: - Once the cluster is initialized (you can check `docker logs -f bsc-toolbox`), enter the container to use the development tools: + User->>Makefile: make cluster-up - ```bash - docker exec -it bsc-toolbox bash - ``` + Note over Makefile,Toolbox: Phase 1: Initialization (prepare) + Makefile->>Toolbox: Start disposable 'bsc-toolbox' container and execute script -3. **Validator Tool**: - The `create-validator` tool is pre-built and installed at `/usr/local/bin/create-validator`. You can run it from anywhere inside the container. + Toolbox->>HostFS: Compile and save 'geth' binary + Toolbox->>HostFS: Generate genesis, keystores, config.toml (.local/) + Toolbox->>HostFS: Generate .env.cluster (cluster params, node count) + Toolbox->>HostFS: Generate docker-compose.cluster.yml (based on env) -4. **Observe from Host (Mac)**: - Access RPC at `http://localhost:8545`. + Toolbox-->>Makefile: Exit (container removed) -### Common Observation Commands (Inside Container) + Note over Makefile,Compose: Phase 2: Start Cluster (up) + Makefile->>Compose: docker compose -f docker-compose.cluster.yml up -d -Here are some useful commands to monitor the cluster: + Compose->>HostFS: Read docker-compose.cluster.yml + Compose->>HostFS: Load .env.cluster (env injection) -- **Check Block Height**: - `geth --exec "eth.blockNumber" attach .local/node0/geth.ipc` -- **List P2P Peers**: - `geth --exec "admin.peers" attach .local/node0/geth.ipc` -- **Check Node Info**: - `geth --exec "admin.nodeInfo" attach .local/node0/geth.ipc` -- **Tail Logs**: - `tail -f .local/node0/bsc-node.log` + Compose->>Docker: Create & start N bsc-node-X containers -### Monitoring and Metrics + Docker->>HostFS: Mount volumes (./ -> /node_deploy) -The cluster is configured to expose internal metrics for monitoring. These are accessible from your **(Host)**: + Docker->>Docker: Run node_entrypoint.sh inside each container + Docker->>HostFS: Containers read config.toml / genesis / keystore -- **Prometheus Metrics**: `http://localhost:6060/debug/metrics/prometheus` -- **JSON Metrics**: `http://localhost:6060/debug/metrics` -- **Performance Profiling (pprof)**: `http://localhost:7060/debug/pprof/` + Docker-->>User: Cluster running (N nodes) +``` + +### Quick Commands + +- **`make cluster-up`**: One-click start. It runs the initialization phase (using a disposable toolbox container) and then starts the isolated nodes via Docker Compose. +- **`make cluster-down`**: Safely stop all running nodes. +- **`make cluster-logs`**: Stream aggregated, color-coded logs from all running nodes. +- **`make cluster-restart`**: Fast restart the cluster (nodes only). Use this if you manually modified `.local/nodeX/config.toml` and want to apply changes without wiping the blockchain data. +- **`make cluster-clean`**: **WARNING**. Wipes all generated data (`.local/`), genesis files, and temporary yaml configs. Use this to reset the chain back to block zero. + +### Node Ports Mapping -**Port Mapping for Nodes:** +Each node runs identically on port `8545` internally. Host mapping is structured sequentially: -| Node | RPC (HTTP/WS) | Metrics (Prometheus) | pprof (Debug) | -| :--- | :--- | :--- | :--- | -| **Node 0** | 8545 | 6060 | 7060 | -| **Node 1** | 8547 | 6062 | 7062 | -| **Node 2** | 8549 | 6064 | 7064 | -| **Node 3** | 8551 | 6066 | 7066 | +| Node | RPC (HTTP/WS) | Metrics (Prometheus) | pprof (Debug) | P2P (TCP/UDP) | +| :--- | :--- | :--- | :--- | :--- | +| **Node 0** | 8545 | 6060 | 7060 | 30311 | +| **Node 1** | 8547 | 6062 | 7062 | 30312 | +| **Node 2** | 8549 | 6064 | 7064 | 30313 | +| **Node 3** | 8551 | 6066 | 7066 | 30314 | -### Storage Optimization +For example, to check the block height of Node 1: +`curl -H "Content-Type: application/json" -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://127.0.0.1:8547` -To prevent rapid disk space exhaustion during local testing, this setup uses the following optimizations: +### Logging -- **DB Engine**: Forced to `leveldb` (more space-efficient than Pebble for small clusters). -- **State Scheme**: Set to `hash` to significantly reduce state storage size compared to the default path-based scheme. -- **Auto-Reset**: The `docker compose up` command triggers a full reset by default, ensuring you always start with a clean state. \ No newline at end of file +By default, nodes output all their logs directly to the Docker logging driver (STDOUT). You can view them using: +`docker logs -f bsc-node-0` \ No newline at end of file diff --git a/bsc_cluster.sh b/bsc_cluster.sh index 514613a1..8ccb6eba 100755 --- a/bsc_cluster.sh +++ b/bsc_cluster.sh @@ -54,6 +54,11 @@ function reset_genesis() { cd ${workspace}/genesis && git reset --hard ${GENESIS_COMMIT} fi cd ${workspace}/genesis + + # 1. Update the 'genesis' submodule safely + # Backup user templates, force-update the repository to a specific + # compatible version ($GENESIS_COMMIT) to prevent breaking changes, + # and then restore the user templates. cp genesis-template.json genesis-template.json.bk cp scripts/init_holders.template scripts/init_holders.template.bk git stash @@ -62,8 +67,11 @@ function reset_genesis() { mv genesis-template.json.bk genesis-template.json mv scripts/init_holders.template.bk scripts/init_holders.template + # 2. Install project-specific dependencies poetry install --no-root npm install + + # 3. Clean up and reinstall Foundry framework components (Standard Library & Tests) rm -rf lib/forge-std forge install --no-git foundry-rs/forge-std@v1.7.3 cd lib/forge-std/lib @@ -78,7 +86,10 @@ function prepare_config() { passedHardforkTime=$(expr $(date +%s) + ${PASSED_FORK_DELAY}) echo "passedHardforkTime "${passedHardforkTime} > ${workspace}/.local/hardforkTime.txt initHolders=${INIT_HOLDER} + + # 1. Collect Validator Information & Setup Node Directories for ((i = 0; i < size; i++)); do + # read their randomly generated cryptographic key files (Consensus addresses and BLS vote keys) for f in ${workspace}/.local/validator${i}/keystore/*; do cons_addr="0x$(cat ${f} | jq -r .address)" initHolders=${initHolders}","${cons_addr} @@ -90,11 +101,15 @@ function prepare_config() { cp ${workspace}/keys/password.txt ./ cp ${workspace}/.local/hardforkTime.txt ./ bbcfee_addrs=${fee_addr} + # It assigns a massive, hardcoded amount of "voting power" (0x000001d1a94a2000) so the node has the authority to forge blocks. powers="0x000001d1a94a2000" #2000000000000 mv ${workspace}/.local/bls${i}/bls ./ && rm -rf ${workspace}/.local/bls${i} vote_addr=0x$(cat ./bls/keystore/*json | jq .pubkey | sed 's/"//g') + # it writes all these unique peices: the consensus address, fee collection address, voting power, and BLS vote address echo "${cons_addr},${bbcfee_addrs},${fee_addr},${powers},${vote_addr}" >> ${workspace}/genesis/validators.conf if [ ${EnableSentryNode} = true ]; then + # Sentry nodes act as a protective firewall/proxy for Validators, + # hiding the Validator's real IP address from public P2P network attacks. mkdir -p ${workspace}/.local/sentry${i} fi done @@ -103,11 +118,15 @@ function prepare_config() { fi rm -f ${workspace}/.local/hardforkTime.txt + # 2. Hack / Patch the System Smart Contracts cd ${workspace}/genesis/ git checkout HEAD contracts + # "hack" the source code of the core BSCValidatorSet.sol smart contract right before compiling. + # They lower the turnLength and explicitly tell the system to ignore validator punishments/updates for the first 2,000 blocks to ensure your local network starts smoothly without validators instantly getting jailed. sed -i -e 's/alreadyInit = true;/turnLength = 16;alreadyInit = true;/' ${workspace}/genesis/contracts/BSCValidatorSet.sol sed -i -e 's/public onlyCoinbase onlyZeroGasPrice {/public onlyCoinbase onlyZeroGasPrice {if (block.number < 2000) return;/' ${workspace}/genesis/contracts/BSCValidatorSet.sol + # 3. Generate the Final Genesis Block poetry run python -m scripts.generate generate-validators poetry run python -m scripts.generate generate-init-holders "${initHolders}" poetry run python -m scripts.generate dev \ @@ -133,6 +152,7 @@ function prepare_config() { # 6. Initialize the geth network for each node using the generated genesis.json function initNetwork() { cd ${workspace} + # 1. Assigning P2P Identities for ((i = 0; i < size; i++)); do mkdir ${workspace}/.local/node${i}/geth cp ${workspace}/keys/validator-nodekey${i} ${workspace}/.local/node${i}/geth/nodekey @@ -147,6 +167,7 @@ function initNetwork() { cp ${workspace}/keys/fullnode-nodekey0 ${workspace}/.local/fullnode0/geth/nodekey fi + # 2. Preparing Network Arguments init_extra_args="" if [ ${EnableSentryNode} = true ]; then init_extra_args="--init.sentrynode-size ${size} --init.sentrynode-ports 30411" @@ -168,8 +189,12 @@ function initNetwork() { init_extra_args="${init_extra_args} --init.evn-validator-whitelist" fi fi + + # 3. Generating Network Configs (config.toml) ${workspace}/bin/geth init-network --init.dir ${workspace}/.local --init.size=${size} --config ${workspace}/config.toml ${init_extra_args} ${workspace}/genesis/genesis.json rm -f ${workspace}/*bsc.log* + + # 4. Initializing the Blockchain Database (geth init) for ((i = 0; i < size; i++)); do sed -i -e '/""/d' ${workspace}/.local/node${i}/config.toml # init genesis @@ -209,6 +234,7 @@ function start_node() { nohup ${geth_bin} --config ${datadir}/config.toml \ --datadir ${datadir} \ --nodekey ${datadir}/geth/nodekey \ + --cache 512 \ --rpc.allow-unprotected-txs --allow-insecure-unlock \ --ws --ws.addr 0.0.0.0 --ws.port ${ws_port} \ --http --http.addr 0.0.0.0 --http.port ${http_port} --http.corsdomain "*" \ @@ -237,6 +263,7 @@ function native_start() { LastHardforkTime=$(expr ${PassedForkTime} + ${LAST_FORK_MORE_DELAY}) rialtoHash=`cat ${workspace}/.local/node0/init.log|grep "database=chaindata"|awk -F"=" '{print $NF}'|awk -F'"' '{print $1}'` + # Starting Validator Nodes for ((i=0; i ${workspace}/.local/hardforkTime.txt + initHolders=${INIT_HOLDER} + + # 1. Collect Validator Information & Setup Node Directories + for ((i = 0; i < size; i++)); do + # read their randomly generated cryptographic key files (Consensus addresses and BLS vote keys) + for f in ${workspace}/.local/validator${i}/keystore/*; do + cons_addr="0x$(cat ${f} | jq -r .address)" + initHolders=${initHolders}","${cons_addr} + fee_addr=${cons_addr} + done + + targetDir=${workspace}/.local/node${i} + mkdir -p ${targetDir} && cd ${targetDir} + cp ${workspace}/keys/password.txt ./ + cp ${workspace}/.local/hardforkTime.txt ./ + bbcfee_addrs=${fee_addr} + # It assigns a massive, hardcoded amount of "voting power" (0x000001d1a94a2000) so the node has the authority to forge blocks. + powers="0x000001d1a94a2000" #2000000000000 + mv ${workspace}/.local/bls${i}/bls ./ && rm -rf ${workspace}/.local/bls${i} + vote_addr=0x$(cat ./bls/keystore/*json | jq .pubkey | sed 's/"//g') + # it writes all these unique peices: the consensus address, fee collection address, voting power, and BLS vote address + echo "${cons_addr},${bbcfee_addrs},${fee_addr},${powers},${vote_addr}" >> ${workspace}/genesis/validators.conf + if [ ${EnableSentryNode} = true ]; then + # Sentry nodes act as a protective firewall/proxy for Validators, + # hiding the Validator's real IP address from public P2P network attacks. + mkdir -p ${workspace}/.local/sentry${i} + fi + done + if [ ${EnableFullNode} = true ]; then + mkdir -p ${workspace}/.local/fullnode0 + fi + rm -f ${workspace}/.local/hardforkTime.txt + + # 2. Hack / Patch the System Smart Contracts + cd ${workspace}/genesis/ + git checkout HEAD contracts + # "hack" the source code of the core BSCValidatorSet.sol smart contract right before compiling. + # They lower the turnLength and explicitly tell the system to ignore validator punishments/updates for the first 2,000 blocks to ensure your local network starts smoothly without validators instantly getting jailed. + sed -i -e 's/alreadyInit = true;/turnLength = 16;alreadyInit = true;/' ${workspace}/genesis/contracts/BSCValidatorSet.sol + sed -i -e 's/public onlyCoinbase onlyZeroGasPrice {/public onlyCoinbase onlyZeroGasPrice {if (block.number < 2000) return;/' ${workspace}/genesis/contracts/BSCValidatorSet.sol + + # 3. Generate the Final Genesis Block + poetry run python -m scripts.generate generate-validators + poetry run python -m scripts.generate generate-init-holders "${initHolders}" + poetry run python -m scripts.generate dev \ + --dev-chain-id "${CHAIN_ID}" \ + --init-burn-ratio "1000" \ + --init-felony-slash-scope "60" \ + --breathe-block-interval "10 minutes" \ + --block-interval "3 seconds" \ + --stake-hub-protector "${INIT_HOLDER}" \ + --unbond-period "2 minutes" \ + --downtime-jail-time "2 minutes" \ + --felony-jail-time "3 minutes" \ + --misdemeanor-threshold "50" \ + --felony-threshold "150" \ + --init-voting-period "2 minutes / BLOCK_INTERVAL" \ + --init-min-period-after-quorum "uint64(1 minutes / BLOCK_INTERVAL)" \ + --governor-protector "${INIT_HOLDER}" \ + --init-minimal-delay "1 minutes" \ + --token-recover-portal-protector "${INIT_HOLDER}" + cp genesis-dev.json genesis.json +} + +# 5. Initialize the geth network for each node using the generated genesis.json +function initNetwork() { + cd ${workspace} + # 1. Assigning P2P Identities + for ((i = 0; i < size; i++)); do + mkdir ${workspace}/.local/node${i}/geth + cp ${workspace}/keys/validator-nodekey${i} ${workspace}/.local/node${i}/geth/nodekey + mv ${workspace}/.local/validator${i}/keystore ${workspace}/.local/node${i}/ && rm -rf ${workspace}/.local/validator${i} + if [ ${EnableSentryNode} = true ]; then + mkdir ${workspace}/.local/sentry${i}/geth + cp ${workspace}/keys/sentry-nodekey${i} ${workspace}/.local/sentry${i}/geth/nodekey + fi + done + if [ ${EnableFullNode} = true ]; then + mkdir ${workspace}/.local/fullnode0/geth + cp ${workspace}/keys/fullnode-nodekey0 ${workspace}/.local/fullnode0/geth/nodekey + fi + + # 2. Preparing Network Arguments + init_extra_args="" + if [ ${EnableSentryNode} = true ]; then + init_extra_args="--init.sentrynode-size ${size} --init.sentrynode-ports 30411" + fi + if [ ${EnableFullNode} = true ]; then + init_extra_args="${init_extra_args} --init.fullnode-size 1 --init.fullnode-ports 30511" + fi + if [ "${RegisterNodeID}" = true ]; then + if [ "${EnableSentryNode}" = true ]; then + init_extra_args="${init_extra_args} --init.evn-sentry-register" + else + init_extra_args="${init_extra_args} --init.evn-validator-register" + fi + fi + if [ "${EnableEVNWhitelist}" = true ]; then + if [ "${EnableSentryNode}" = true ]; then + init_extra_args="${init_extra_args} --init.evn-sentry-whitelist" + else + init_extra_args="${init_extra_args} --init.evn-validator-whitelist" + fi + fi + + # 3. Generating Network Configs (config.toml) + ${workspace}/bin/geth init-network --init.dir ${workspace}/.local --init.size=${size} --config ${workspace}/config.toml ${init_extra_args} ${workspace}/genesis/genesis.json + rm -f ${workspace}/*bsc.log* + + # 4. Initializing the Blockchain Database (geth init) + for ((i = 0; i < size; i++)); do + sed -i -e '/""/d' ${workspace}/.local/node${i}/config.toml + # init genesis + initLog=${workspace}/.local/node${i}/init.log + ${workspace}/bin/geth --datadir ${workspace}/.local/node${i} init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 + rm -f ${workspace}/.local/node${i}/*bsc.log* + + if [ ${EnableSentryNode} = true ]; then + sed -i -e '/""/d' ${workspace}/.local/sentry${i}/config.toml + initLog=${workspace}/.local/sentry${i}/init.log + ${workspace}/bin/geth --datadir ${workspace}/.local/sentry${i} init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 + rm -f ${workspace}/.local/sentry${i}/*bsc.log* + fi + done + if [ ${EnableFullNode} = true ]; then + sed -i -e '/""/d' ${workspace}/.local/fullnode0/config.toml + sed -i -e 's/EnableEVNFeatures = true/EnableEVNFeatures = false/g' ${workspace}/.local/fullnode0/config.toml + initLog=${workspace}/.local/fullnode0/init.log + ${workspace}/bin/geth --datadir ${workspace}/.local/fullnode0 init --state.scheme ${stateScheme} --db.engine ${dbEngine} ${workspace}/genesis/genesis.json > "${initLog}" 2>&1 + rm -f ${workspace}/.local/fullnode0/*bsc.log* + fi +} + +# 6. Patch P2P network to use Docker DNS instead of localhost +function patch_p2p_network() { + # Replace 127.0.0.1 IPs in config.toml with docker-compose service names + for ((i=0; i ${workspace}/.env.cluster +RIALTO_HASH=${rialtoHash} +PASSED_FORK_TIME=${PassedForkTime} +LAST_HARDFORK_TIME=${LastHardforkTime} +FULL_IMMUTABILITY_THRESHOLD=${FullImmutabilityThreshold} +BREATHE_BLOCK_INTERVAL=${BreatheBlockInterval} +MIN_FOR_BLOB_REQUESTS=${MinBlocksForBlobRequests} +DEFAULT_EXTRA_RESERVE=${DefaultExtraReserveForBlobRequests} +EOF + + COMPOSE_FILE=${workspace}/docker-compose.cluster.yml + echo "Generating ${COMPOSE_FILE}..." + echo "services:" > $COMPOSE_FILE + + # Validators + for ((i=0; i> $COMPOSE_FILE + bsc-node-${i}: + image: bsc-toolbox:latest + container_name: bsc-node-${i} + env_file: .env.cluster + environment: + - NODE_TYPE=node + - NODE_INDEX=${i} + volumes: + - .:/node_deploy + ports: + - "${base_rpc}:8545" # RPC & WS + - "${base_metrics}:6060" # Metrics + - "${base_pprof}:7060" # Pprof + - "${p2p_port}:${p2p_port}" # P2P TCP + - "${p2p_port}:${p2p_port}/udp" # P2P UDP + command: ["/node_deploy/node_entrypoint.sh"] + +EOF + done + + # Sentry + if [ ${EnableSentryNode} = true ]; then + for ((i=0; i> $COMPOSE_FILE + bsc-sentry-${i}: + image: bsc-toolbox:latest + container_name: bsc-sentry-${i} + env_file: .env.cluster + environment: + - NODE_TYPE=sentry + - NODE_INDEX=${i} + volumes: + - .:/node_deploy + ports: + - "${base_rpc}:8545" + - "${base_metrics}:6060" + - "${base_pprof}:7060" + - "${p2p_port}:${p2p_port}" + - "${p2p_port}:${p2p_port}/udp" + command: ["/node_deploy/node_entrypoint.sh"] + +EOF + done + fi + + # Full Node + if [ ${EnableFullNode} = true ]; then + base_rpc=8645 + base_metrics=6160 + base_pprof=7160 + p2p_port=30511 + cat <> $COMPOSE_FILE + bsc-fullnode-0: + image: bsc-toolbox:latest + container_name: bsc-fullnode-0 + env_file: .env.cluster + environment: + - NODE_TYPE=full + - NODE_INDEX=0 + volumes: + - .:/node_deploy + ports: + - "${base_rpc}:8545" + - "${base_metrics}:6060" + - "${base_pprof}:7060" + - "${p2p_port}:${p2p_port}" + - "${p2p_port}:${p2p_port}/udp" + command: ["/node_deploy/node_entrypoint.sh"] + +EOF + fi + + echo "Generated \${COMPOSE_FILE} successfully!" +} + +# 8. Use create-validator tool to register validator nodes on StakeHub +function register_stakehub(){ + # wait feynman enable + sleep 45 + for ((i = 0; i < size; i++));do + create-validator --consensus-key-dir ${workspace}/keys/validator${i} --vote-key-dir ${workspace}/keys/bls${i} \ + --password-path ${workspace}/keys/password.txt --amount 20001 --validator-desc Val${i} --rpc-url ${RPC_URL} + done +} + +# Command dispatcher +CMD=$1 +case ${CMD} in +prepare) + echo "Preparing Docker cluster configs..." + create_validator # Step 1: Prepare keys and .local/ + prepare_bsc_client # Step 2: Build geth if necessary + reset_genesis # Step 3: Setup genesis deps (Forge, Poetry, etc) + prepare_config # Step 4: Generate genesis.json and node configs + initNetwork # Step 5: Initialize Geth data directories + patch_p2p_network # Step 6: Patch 127.0.0.1 in configs to docker DNS + generate_compose # Step 7: Generate .env.cluster and docker-compose + echo "Preparation complete!" + ;; +register) + register_stakehub + ;; +*) + echo "Usage: docker_cluster.sh | prepare | register" + ;; +esac diff --git a/node_entrypoint.sh b/node_entrypoint.sh new file mode 100755 index 00000000..5c16bad1 --- /dev/null +++ b/node_entrypoint.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -e + +# Node Entrypoint for Docker Compose running BSC geth + +# Ensure environment variables exist +if [ -z "$NODE_TYPE" ] || [ -z "$NODE_INDEX" ]; then + echo "ERROR: NODE_TYPE and NODE_INDEX must be provided" + exit 1 +fi + +if [ -z "$RIALTO_HASH" ]; then + echo "ERROR: RIALTO_HASH not set by env" + exit 1 +fi + +workspace="/node_deploy" +geth_bin="${workspace}/bin/geth" + +echo "Starting BSC $NODE_TYPE node $NODE_INDEX..." + +function start_node() { + local datadir=$1 + local extra_args="" + + # Only Validator nodes require mining and unlocking parameters + if [ "$NODE_TYPE" = "node" ]; then + cons_addr="0x$(jq -r .address ${datadir}/keystore/*)" + extra_args="--mine --vote --unlock ${cons_addr} --miner.etherbase ${cons_addr} --password ${datadir}/password.txt --blspassword ${datadir}/password.txt" + fi + + # Execute geth (replaces the bash shell process with geth) + exec ${geth_bin} --config ${datadir}/config.toml \ + --datadir ${datadir} \ + --nodekey ${datadir}/geth/nodekey \ + --cache 512 \ + --rpc.allow-unprotected-txs --allow-insecure-unlock \ + --ws --ws.addr 0.0.0.0 --ws.port 8545 \ + --http --http.addr 0.0.0.0 --http.port 8545 --http.corsdomain "*" \ + --metrics --metrics.addr 0.0.0.0 --metrics.port 6060 \ + --pprof --pprof.addr 0.0.0.0 --pprof.port 7060 \ + --gcmode full --syncmode full --monitor.maliciousvote \ + --rialtohash ${RIALTO_HASH} \ + --override.passedforktime ${PASSED_FORK_TIME} \ + --override.lorentz ${PASSED_FORK_TIME} \ + --override.maxwell ${PASSED_FORK_TIME} \ + --override.fermi ${LAST_HARDFORK_TIME} \ + --override.osaka ${LAST_HARDFORK_TIME} \ + --override.mendel ${LAST_HARDFORK_TIME} \ + --override.pasteur ${LAST_HARDFORK_TIME} \ + --override.immutabilitythreshold ${FULL_IMMUTABILITY_THRESHOLD} \ + --override.breatheblockinterval ${BREATHE_BLOCK_INTERVAL} \ + --override.minforblobrequest ${MIN_FOR_BLOB_REQUESTS} \ + --override.defaultextrareserve ${DEFAULT_EXTRA_RESERVE} \ + $extra_args +} + +if [ "$NODE_TYPE" = "node" ]; then + start_node "${workspace}/.local/node${NODE_INDEX}" +elif [ "$NODE_TYPE" = "sentry" ]; then + start_node "${workspace}/.local/sentry${NODE_INDEX}" +elif [ "$NODE_TYPE" = "full" ]; then + start_node "${workspace}/.local/fullnode${NODE_INDEX}" +else + echo "Unknown NODE_TYPE: $NODE_TYPE" + exit 1 +fi From a8e32d2ba773315312c50c12b88c429754d6e87d Mon Sep 17 00:00:00 2001 From: Isaac Tai Date: Mon, 6 Apr 2026 17:38:25 -0400 Subject: [PATCH 09/12] feat(load-test): stabilize BSC cluster setup and validator registration flow (#5) Improves the BSC load test environment, including validator registration automation and protocol alignment. It ensures the BSC cluster can work smoothly with the load tester. --- .env | 6 +++--- Makefile | 24 ++++++++++++++++-------- README.md | 11 ++++++++++- docker_cluster.sh | 16 +++++++++++++++- node_entrypoint.sh | 7 ------- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.env b/.env index a5826f58..56a4f57d 100644 --- a/.env +++ b/.env @@ -3,10 +3,10 @@ CHAIN_ID=714 KEYPASS="0123456789" INIT_HOLDER="0x04d63aBCd2b9b1baa327f2Dda0f873F197ccd186" # INIT_HOLDER_PRV="59ba8068eb256d520179e903f43dacf6d8d57d72bd306e1bd603fdb8c8da10e8" -RPC_URL="http://127.0.0.1:8545" +RPC_URL="http://bsc-node-0:8545" GENESIS_COMMIT="34618f607f8356cf147dde6a69fae150bd53d5bf" # fermi commit -PASSED_FORK_DELAY=40 -LAST_FORK_MORE_DELAY=10 +PASSED_FORK_DELAY=-100000000 +LAST_FORK_MORE_DELAY=0 FullImmutabilityThreshold=2048 MinBlocksForBlobRequests=576 DefaultExtraReserveForBlobRequests=32 diff --git a/Makefile b/Makefile index 50a1d193..da906390 100644 --- a/Makefile +++ b/Makefile @@ -4,34 +4,42 @@ # Default Image to use for bootstrapping TOOLBOX_IMAGE ?= bsc-toolbox:latest +COMPOSE_FILE := $(CURDIR)/docker-compose.cluster.yml # Auto initialize and bring up the cluster -cluster-up: +cluster-up: check-deps @echo "[Phase 1] Initializing blockchain data & configs using isolated Toolbox environment..." - docker run --rm -v "$(PWD):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh prepare + docker run --rm -v "$(CURDIR):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh prepare @echo "" @echo "[Phase 2] Data prepared! Starting BSC cluster via Docker Compose..." - docker compose -f docker-compose.cluster.yml up -d + docker compose -f $(COMPOSE_FILE) up -d + @echo "[Phase 3] Waiting for RPC... then Registering Validators" + @bash docker_cluster.sh wait-rpc + @docker run --network bsc_cluster_network --rm -v "$(CURDIR):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh register @echo "BSC Local Cluster successfully started in background! Run 'make cluster-logs' to view live logs." # Safely stop and remove all containers cluster-down: @echo "Stopping and removing all BSC containers..." - if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml down; fi + if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) down; fi # View real-time logs for all cluster nodes cluster-logs: - if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml logs -f; fi + if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) logs -f; fi # Completely wipe cluster data and auto-generated configs (DANGEROUS) cluster-clean: cluster-down @echo "Wiping all local cluster data and temporary configurations (.local/, YAML, .env)..." - rm -rf .local - rm -f .env.cluster docker-compose.cluster.yml + rm -rf $(CURDIR)/.local + rm -f $(CURDIR)/.env.cluster $(COMPOSE_FILE) @echo "Workspace is completely clean." # Fast restart without wiping data or rebuilding config cluster-restart: cluster-down @echo "Restarting all BSC containers with existing config (Phase 2 only)..." - if [ -f docker-compose.cluster.yml ]; then docker compose -f docker-compose.cluster.yml up -d; fi + if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) up -d; fi @echo "BSC Local Cluster successfully restarted." + +# Ensure host has curl for wait-rpc (Phase 3) +check-deps: + @command -v curl >/dev/null 2>&1 || (echo "ERROR: 'curl' not found on host. It is required for Phase 3!" && exit 1) diff --git a/README.md b/README.md index 3d8d6bdc..d136e72c 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,20 @@ sequenceDiagram Docker->>HostFS: Containers read config.toml / genesis / keystore Docker-->>User: Cluster running (N nodes) + + Note over Makefile,Toolbox: Phase 3: Validator Registration (register) + Makefile->>Makefile: Wait for RPC (localhost:8545 ready) + Makefile->>Toolbox: Start new Toolbox container inside 'bsc_cluster_network' + Toolbox->>HostFS: Load .env (get RPC_URL) + Toolbox->>Docker: Send 'Register' Transactions (via RPC to Node 0) + Toolbox-->>Makefile: Exit (registration tasks submitted) + + Note over User,Docker: Local BSC Cluster active with registered validators ``` ### Quick Commands -- **`make cluster-up`**: One-click start. It runs the initialization phase (using a disposable toolbox container) and then starts the isolated nodes via Docker Compose. +- **`make cluster-up`**: One-click start. It runs the initialization phase, starts the isolated nodes via Docker Compose, and then automatically handles validator registration on StakeHub. - **`make cluster-down`**: Safely stop all running nodes. - **`make cluster-logs`**: Stream aggregated, color-coded logs from all running nodes. - **`make cluster-restart`**: Fast restart the cluster (nodes only). Use this if you manually modified `.local/nodeX/config.toml` and want to apply changes without wiping the blockchain data. diff --git a/docker_cluster.sh b/docker_cluster.sh index 2cde2b34..e908c834 100644 --- a/docker_cluster.sh +++ b/docker_cluster.sh @@ -343,13 +343,19 @@ EOF EOF fi + cat <> $COMPOSE_FILE + +networks: + default: + name: bsc_cluster_network +EOF + echo "Generated \${COMPOSE_FILE} successfully!" } # 8. Use create-validator tool to register validator nodes on StakeHub function register_stakehub(){ # wait feynman enable - sleep 45 for ((i = 0; i < size; i++));do create-validator --consensus-key-dir ${workspace}/keys/validator${i} --vote-key-dir ${workspace}/keys/bls${i} \ --password-path ${workspace}/keys/password.txt --amount 20001 --validator-desc Val${i} --rpc-url ${RPC_URL} @@ -373,6 +379,14 @@ prepare) register) register_stakehub ;; +wait-rpc) + echo "Waiting for RPC to be available at http://localhost:8545..." + until curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 > /dev/null; do + printf "." + sleep 2 + done + echo "RPC is ready!" + ;; *) echo "Usage: docker_cluster.sh | prepare | register" ;; diff --git a/node_entrypoint.sh b/node_entrypoint.sh index 5c16bad1..563968f8 100755 --- a/node_entrypoint.sh +++ b/node_entrypoint.sh @@ -41,13 +41,6 @@ function start_node() { --pprof --pprof.addr 0.0.0.0 --pprof.port 7060 \ --gcmode full --syncmode full --monitor.maliciousvote \ --rialtohash ${RIALTO_HASH} \ - --override.passedforktime ${PASSED_FORK_TIME} \ - --override.lorentz ${PASSED_FORK_TIME} \ - --override.maxwell ${PASSED_FORK_TIME} \ - --override.fermi ${LAST_HARDFORK_TIME} \ - --override.osaka ${LAST_HARDFORK_TIME} \ - --override.mendel ${LAST_HARDFORK_TIME} \ - --override.pasteur ${LAST_HARDFORK_TIME} \ --override.immutabilitythreshold ${FULL_IMMUTABILITY_THRESHOLD} \ --override.breatheblockinterval ${BREATHE_BLOCK_INTERVAL} \ --override.minforblobrequest ${MIN_FOR_BLOB_REQUESTS} \ From e5ca839357c9e8dc45dcf795027b34acbcecd165 Mon Sep 17 00:00:00 2001 From: Isaac Tai Date: Mon, 6 Apr 2026 18:02:35 -0400 Subject: [PATCH 10/12] fix: 5 minutes timeout if RPC not responding (#6) --- Makefile | 14 +++++++------- docker_cluster.sh | 24 ++++++++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index da906390..f8314593 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ # Default Image to use for bootstrapping TOOLBOX_IMAGE ?= bsc-toolbox:latest -COMPOSE_FILE := $(CURDIR)/docker-compose.cluster.yml +COMPOSE_FILE := docker-compose.cluster.yml # Auto initialize and bring up the cluster cluster-up: check-deps @@ -12,7 +12,7 @@ cluster-up: check-deps docker run --rm -v "$(CURDIR):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh prepare @echo "" @echo "[Phase 2] Data prepared! Starting BSC cluster via Docker Compose..." - docker compose -f $(COMPOSE_FILE) up -d + docker compose -f "$(COMPOSE_FILE)" up -d @echo "[Phase 3] Waiting for RPC... then Registering Validators" @bash docker_cluster.sh wait-rpc @docker run --network bsc_cluster_network --rm -v "$(CURDIR):/node_deploy" -w /node_deploy $(TOOLBOX_IMAGE) bash docker_cluster.sh register @@ -21,23 +21,23 @@ cluster-up: check-deps # Safely stop and remove all containers cluster-down: @echo "Stopping and removing all BSC containers..." - if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) down; fi + if [ -f "$(COMPOSE_FILE)" ]; then docker compose -f "$(COMPOSE_FILE)" down; fi # View real-time logs for all cluster nodes cluster-logs: - if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) logs -f; fi + if [ -f "$(COMPOSE_FILE)" ]; then docker compose -f "$(COMPOSE_FILE)" logs -f; fi # Completely wipe cluster data and auto-generated configs (DANGEROUS) cluster-clean: cluster-down @echo "Wiping all local cluster data and temporary configurations (.local/, YAML, .env)..." - rm -rf $(CURDIR)/.local - rm -f $(CURDIR)/.env.cluster $(COMPOSE_FILE) + rm -rf "$(CURDIR)/.local" + rm -f "$(CURDIR)/.env.cluster" "$(COMPOSE_FILE)" @echo "Workspace is completely clean." # Fast restart without wiping data or rebuilding config cluster-restart: cluster-down @echo "Restarting all BSC containers with existing config (Phase 2 only)..." - if [ -f $(COMPOSE_FILE) ]; then docker compose -f $(COMPOSE_FILE) up -d; fi + if [ -f "$(COMPOSE_FILE)" ]; then docker compose -f "$(COMPOSE_FILE)" up -d; fi @echo "BSC Local Cluster successfully restarted." # Ensure host has curl for wait-rpc (Phase 3) diff --git a/docker_cluster.sh b/docker_cluster.sh index e908c834..60bd1ee2 100644 --- a/docker_cluster.sh +++ b/docker_cluster.sh @@ -379,14 +379,22 @@ prepare) register) register_stakehub ;; -wait-rpc) - echo "Waiting for RPC to be available at http://localhost:8545..." - until curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 > /dev/null; do - printf "." - sleep 2 - done - echo "RPC is ready!" - ;; + wait-rpc) + echo "Waiting for RPC to be available at http://localhost:8545..." + MAX_WAIT=300 # 5 minutes + ELAPSED=0 + until curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545 > /dev/null 2>&1; do + printf "." + sleep 2 + ELAPSED=$((ELAPSED + 2)) + if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then + echo "" + echo "ERROR: RPC not available after ${MAX_WAIT}s. Aborting." + exit 1 + fi + done + echo "RPC is ready!" + ;; *) echo "Usage: docker_cluster.sh | prepare | register" ;; From b4e50ea282e2e3545afcd39390b53bd349a22e8a Mon Sep 17 00:00:00 2001 From: Taran Date: Mon, 4 May 2026 22:58:59 -0300 Subject: [PATCH 11/12] docker_cluster.sh: support N>4 validators on private chains Add patch_for_private_chain() that pushes Plato/Luban to block 100M and disables post-Plato time-based forks for genesis. Required for private chains with more validators than the default 4-validator setup, where fast-finality (BEP-126) panics during reorg at parlia/snapshot.go:411 when DoubleSign forks occur at block 2 due to multi-validator startup race conditions on a WAN. Also sort validators by consensusAddr ascending in validators.template so genesis extraData matches Parlia's snapshot.validators() ordering (otherwise block 1 sealing fails with "unauthorized validator" because in-turn calculation uses sorted-ascending while extraData was unsorted). Replace forge install --no-git with direct git clone of forge-std, because forge install fails when the parent directory is itself a git repo (submodule path lookup fails for forge-std's ds-test). Toggle: DISABLE_FAST_FINALITY=true (default) applies the patches. Set DISABLE_FAST_FINALITY=false in .env to preserve upstream 4-validator behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker_cluster.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/docker_cluster.sh b/docker_cluster.sh index 60bd1ee2..9aae539d 100644 --- a/docker_cluster.sh +++ b/docker_cluster.sh @@ -62,13 +62,65 @@ function reset_genesis() { npm install # 3. Clean up and reinstall Foundry framework components (Standard Library & Tests) + # Direct git clone instead of `forge install --no-git` because the latter + # fails inside a parent git repo with "not a git repository: ../../.git/modules/lib/ds-test" + # when forge tries to recursively init forge-std's ds-test submodule. rm -rf lib/forge-std - forge install --no-git foundry-rs/forge-std@v1.7.3 + git clone --depth 1 --branch v1.7.3 https://github.com/foundry-rs/forge-std.git lib/forge-std cd lib/forge-std/lib rm -rf ds-test git clone https://github.com/dapphub/ds-test } +# Patches genesis to disable fast-finality (Plato/Luban) for private chains with N>4 validators. +# +# Why: BSC's Plato hardfork (BLS fast finality) activates at block 7 in the default +# genesis-template. With >4 validators across a WAN, race conditions at chain start +# cause DoubleSign forks at block 2, and the fast-finality reorg path +# (consensus/parlia/snapshot.go:411) panics on misshapen attestation state. +# +# Empirically: 4 validators → works. 21 validators across 3 GCP regions → consistently +# panics at block 8-9. See https://github.com/bnb-chain/bsc/blob/master/consensus/parlia/snapshot.go +# Mainnet didn't bootstrap with Plato either; Plato activated years into a running chain. +# +# This function pushes Plato/Luban (and dependent Berlin/London/Hertz) past chain +# lifetime, plus disables time-based forks that depend on Plato. The chain runs +# vanilla Parlia (probabilistic finality, ~12-block depth). +# +# Toggle: set DISABLE_FAST_FINALITY=false in .env to keep upstream behavior (default 4-validator setup). +function patch_for_private_chain() { + if [ "${DISABLE_FAST_FINALITY:-true}" != "true" ]; then + echo "patch_for_private_chain: DISABLE_FAST_FINALITY=false, skipping" + return 0 + fi + + local TPL=${workspace}/genesis/genesis-template.json + sed -i 's/"lubanBlock": 6,/"lubanBlock": 100000000,/' $TPL + sed -i 's/"platoBlock": 7,/"platoBlock": 100000000,/' $TPL + sed -i 's/"berlinBlock": 8,/"berlinBlock": 100000001,/' $TPL + sed -i 's/"londonBlock": 8,/"londonBlock": 100000001,/' $TPL + sed -i 's/"hertzBlock": 8,/"hertzBlock": 100000002,/' $TPL + sed -i 's/"hertzfixBlock": 8,/"hertzfixBlock": 100000002,/' $TPL + sed -i 's/"bohrTime": 0,/"bohrTime": null,/' $TPL + sed -i 's/"pascalTime": 0,/"pascalTime": null,/' $TPL + sed -i 's/"pragueTime": 0,/"pragueTime": null,/' $TPL + sed -i 's/"lorentzTime": 0,/"lorentzTime": null,/' $TPL + sed -i 's/"maxwellTime": 0,/"maxwellTime": null,/' $TPL + sed -i 's/"fermiTime": 0,/"fermiTime": null,/' $TPL + sed -i 's/"haberFixTime": 0,/"haberFixTime": null,/' $TPL + grep -q '"lubanBlock": 100000000' $TPL || { echo "patch_for_private_chain: luban patch failed" >&2; exit 1; } + echo "patch_for_private_chain: pushed Plato/Luban to block 100000000, disabled time-based post-Plato forks" + + # Sort validators ascending by consensusAddr in extraData. + # Parlia's snapshot.validators() returns ascending-sorted; if extraData order + # differs, in-turn calculation goes to wrong validator → "unauthorized validator" at block 1. + local VT=${workspace}/genesis/scripts/validators.template + if ! grep -q "// SORT_PATCHED" $VT; then + sed -i 's|function extraDataSerialize(validators) {|function extraDataSerialize(validators) {\n // SORT_PATCHED\n validators = validators.slice().sort((a,b) => a.consensusAddr.toLowerCase().localeCompare(b.consensusAddr.toLowerCase()));|' $VT + echo "patch_for_private_chain: validators.template now sorts by consensusAddr" + fi +} + # 4. Generate validator configurations, hardfork times, and the final genesis.json function prepare_config() { rm -f ${workspace}/genesis/validators.conf @@ -369,8 +421,9 @@ prepare) echo "Preparing Docker cluster configs..." create_validator # Step 1: Prepare keys and .local/ prepare_bsc_client # Step 2: Build geth if necessary - reset_genesis # Step 3: Setup genesis deps (Forge, Poetry, etc) - prepare_config # Step 4: Generate genesis.json and node configs + reset_genesis # Step 3: Setup genesis deps (Forge, Poetry, etc) + patch_for_private_chain # Step 3b: Disable fast-finality for >4-validator chains (no-op if DISABLE_FAST_FINALITY=false) + prepare_config # Step 4: Generate genesis.json and node configs initNetwork # Step 5: Initialize Geth data directories patch_p2p_network # Step 6: Patch 127.0.0.1 in configs to docker DNS generate_compose # Step 7: Generate .env.cluster and docker-compose From 2c69854255ab61017d4cad024d2d691bcf646e05 Mon Sep 17 00:00:00 2001 From: Taran Date: Wed, 6 May 2026 11:50:56 -0300 Subject: [PATCH 12/12] docker_cluster.sh: support BSC_GETH_BRANCH env var for skip-execution fix The BNB Chain team confirmed the >9-validator fast-finality panic at consensus/parlia/snapshot.go:411 and pushed a fix to the 'skip-execution' branch on bnb-chain/bsc: https://github.com/bnb-chain/bsc/tree/skip-execution Add BSC_GETH_BRANCH env var (default 'master') to control which bsc branch prepare_bsc_client() builds. Use 'BSC_GETH_BRANCH=skip-execution' to pull the fix. Once that branch lands in master, this can be reverted to default. Also handle the case where /workspace/bsc/Makefile already exists (from a previous build) by fetching+checking out the requested branch instead of just running git pull on whatever branch was already checked out. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker_cluster.sh | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docker_cluster.sh b/docker_cluster.sh index 9aae539d..4faff930 100644 --- a/docker_cluster.sh +++ b/docker_cluster.sh @@ -25,14 +25,21 @@ function create_validator() { done } -# 2. Build bsc geth client from source if configured +# 2. Build bsc geth client from source if configured. +# BSC_GETH_BRANCH controls which branch to build (default: master). +# Use "skip-execution" for the BNB-team fix for the >9-validator +# fast-finality panic at consensus/parlia/snapshot.go:411 +# (https://github.com/bnb-chain/bsc/tree/skip-execution). function prepare_bsc_client() { if [ ${useLatestBscClient} = true ]; then + local BRANCH="${BSC_GETH_BRANCH:-master}" if [ ! -f "${workspace}/bsc/Makefile" ]; then cd ${workspace} - git clone https://github.com/bnb-chain/bsc.git + git clone --branch "${BRANCH}" https://github.com/bnb-chain/bsc.git + else + cd ${workspace}/bsc && git fetch origin && git checkout "${BRANCH}" && git pull origin "${BRANCH}" fi - cd ${workspace}/bsc && git pull && make geth && cp -f ${workspace}/bsc/build/bin/geth ${workspace}/bin/ + cd ${workspace}/bsc && make geth && cp -f ${workspace}/bsc/build/bin/geth ${workspace}/bin/ fi }