Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@

.idea/
lib/
bsc/
docker-compose.cluster.yml
.env.cluster
37 changes: 37 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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."
92 changes: 50 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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`
30 changes: 30 additions & 0 deletions bsc_cluster.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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 \
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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 '/"<nil>"/d' ${workspace}/.local/node${i}/config.toml
# init genesis
Expand Down Expand Up @@ -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 "*" \
Expand Down Expand Up @@ -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<size; i++)); do
datadir="${workspace}/.local/node${i}"

Expand All @@ -248,13 +275,16 @@ function native_start() {
# get validator address
cons_addr="0x$(jq -r .address ${datadir}/keystore/*)"

# Copying Geth binaries to unique names (geth0, geth1...)
# for easier node-specific monitoring in htop.
cp ${workspace}/bin/geth ${datadir}/geth${i}

base=$((8545 + i*2))
start_node "node" $i $datadir "${datadir}/geth${i}" "${cons_addr}" \
$base $base $((6060+i*2)) $((7060+i*2))
done

# Starting Sentry Nodes
if [ ${EnableSentryNode} = true ]; then
sleep 10
for ((i=0; i<size; i++)); do
Expand Down
Loading