BETA — mainnet live (chain 482120); features are still being finished. Transparency →
Bitcoin Swap BETA Chain 482120
Developers · Operations

Run a node

Add a full node to chain 482120: files, flags, peers, checks — and why the validator set never has two members.

Operations › Run a node

This is the procedure used to add a machine to the chain as a full node: it downloads and re-executes every block from genesis and serves its own JSON-RPC, but it does not sign blocks. It was written down while the second host was prepared on 2 Oct 2026, so the next node can be built the same way. The compose file is the one that host runs; image, genesis and gas floor are the same on every node.

What you need

Machine2 vCPU, 4 GB RAM, 20 GB disk is enough today (the chain data is under 2 GB). Linux with Docker and the compose plugin.
Clienthyperledger/besu:26.2.0 — pin this exact version; every node of the network runs it.
Genesishttps://btcw.tech/genesis.json · sha256 686cc9ad2df9ba2ce9ca479cb01d551e454bd85f4b3c3707762f66d0a1385fea
Genesis block hash0x4d4c7675b66bff6e0a36a4cc628bd13ada6c83d53bc72cd8692de146604ebc9d — a node started from the right file reports exactly this for block 0.
A peerThe enode URL of a node that is already in sync, and a network path to its P2P port (TCP 30303). The existing nodes are not reachable from the Internet and discovery is off, so a peer is always added by hand — see Peers.

1. Files

mkdir -p /opt/btcw-node/chain && cd /opt/btcw-node
curl -s https://btcw.tech/genesis.json -o chain/genesis.json
sha256sum chain/genesis.json        # must print 686cc9ad…5fea
echo '[]' > chain/static-nodes.json  # peers go here, step 3

2. Compose file

/opt/btcw-node/docker-compose.yml. The node uses the host network and listens on 127.0.0.1 only: Docker's published ports bypass ufw, so nothing is published at all.

services:
  besu-node:
    image: hyperledger/besu:26.2.0
    restart: unless-stopped
    network_mode: host
    environment:
      BESU_OPTS: -Xmx1536m
    command:
      - --data-path=/var/lib/besu
      - --genesis-file=/config/genesis.json
      - --profile=ENTERPRISE
      - --sync-mode=FULL
      - --min-gas-price=1000000
      - --discovery-enabled=false
      - --static-nodes-file=/config/static-nodes.json
      - --p2p-host=127.0.0.1
      - --p2p-interface=127.0.0.1
      - --p2p-port=30303
      - --rpc-http-enabled
      - --rpc-http-host=127.0.0.1
      - --rpc-http-port=28545
      - --rpc-http-api=ETH,NET,WEB3
      - --host-allowlist=localhost,127.0.0.1
    volumes:
      - ./chain/genesis.json:/config/genesis.json:ro
      - ./chain/static-nodes.json:/config/static-nodes.json:ro
      - node-data:/var/lib/besu
    mem_limit: 2560m
    logging: { driver: json-file, options: { max-size: "20m", max-file: "3" } }
volumes:
  node-data:
--sync-mode=FULLRe-executes every block. The chain is small; snapshot sync is not needed and not used by any node.
--min-gas-price=1000000The network's gas floor (0.001 gwei). A node with another value accepts or drops different transactions than its peers.
--profile=ENTERPRISEBesu's profile for private networks: no assumptions about public-network peers.
--rpc-http-api=ETH,NET,WEB3Read and send only. ADMIN, DEBUG, TXPOOL and QBFT stay off on a node others can query.
Storage formatDefault (Bonsai). Only a node that serves debug_* / trace_* to an explorer needs --data-storage-format=FOREST.

3. Peers

On a node that is already in sync, read its enode URL (net_enode is part of the NET API):

curl -s -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"net_enode","params":[]}' http://127.0.0.1:28545

Put it in chain/static-nodes.json on the new node, with the host and port the new node can actually reach:

["enode://<128 hex characters>@<host>:<port>"]

4. Start and check

docker compose up -d besu-node
rpc() { curl -s -H 'content-type: application/json' -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":$2}" http://127.0.0.1:28545; echo; }
rpc eth_chainId '[]'                          # 0x75b48
rpc eth_getBlockByNumber '["0x0",false]'      # "hash":"0x4d4c7675…bc9d"
rpc net_peerCount '[]'                        # 0x1 or more once a peer is configured
rpc eth_blockNumber '[]'                      # climbs until it matches the public RPC, then +1 every 5 seconds

The node is in sync when the hash of a recent block equals the one the public RPC returns for the same number:

cast block 100000 --field hash --rpc-url http://127.0.0.1:28545
cast block 100000 --field hash --rpc-url https://rpc.btcw.tech

With no peer configured the first two checks already pass and the block number stays at 0 — that state proves the genesis file and the flags are right before any connection is made.

Pitfalls met in practice

Full node or validator

A full node needs nobody's approval and changes nothing for the rest of the network. A validator is different: QBFT needs ceil(2N/3) of the N validators online to produce a block.

ValidatorsNeeded onlineCan be offline
1 (today)10
220 — either machine stopping halts the chain, and a halted chain cannot vote a validator out
321
431, and the set also tolerates one validator misbehaving

So the validator set goes from 1 straight to 3 or 4 machines, never to 2. A validator is added by the existing validators voting for its address (qbft_proposeValidatorVote on their own RPC, which is not public); the change takes effect once more than half of them have voted. Read the current set from any node with the QBFT API: qbft_getValidatorsByBlockNumber("latest").