Private Ethereum Network with Besu

Welcome, my name is Franz Geffke. I help organizations such as succeed online: Increase reach and revenue, sharpen customer focus and lower aquisition cost and integrate with AI - If you think it's time to grow, contact me.

You are here because you clicked on the referral link on my clients website: .

TLDR This post goes over how to use Besu to create a private Ethereum network (on your guix machine)

Instructions on how-to setup (and interact with) a private Ethereum network with proof of authority (QBFT) consensus algorithm using Besu.

Setup your working directory:

I’ve setup a repository with a bunch of helpers to make the process easier:

git clone https://github.com/franzos/private-ethereum-network-besu.git
cd private-ethereum-network-besu

The step below assume that you’re working in the repository directory.

Setup Besu

wget https://github.com/hyperledger/besu/releases/download/25.2.2/besu-25.2.2.zip
unzip besu-25.2.2.zip && mv besu-25.2.2 besu

Setup a guix environment, and check if besu is working:

guix shell openjdk
./besu/bin/besu --version

Configure the network

Create a genesis config qbftConfigFile.json. This file is already provided in the repository; Adjust it to your liking:

{
  "genesis": {
    "config": {
      "chainId": 1337,
      "grayGlacierBlock": 0,
      "qbft": {
        "blockperiodseconds": 2,
        "epochlength": 30000,
        "requesttimeoutseconds": 4
      }
    },
    "nonce": "0x0",
    "timestamp": "0x58ee40ba",
    "gasLimit": "0x47b760",
    "difficulty": "0x1",
    "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365",
    "coinbase": "0x0000000000000000000000000000000000000000",
    "alloc": {
      "fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
        "privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63",
        "comment": "private key and this comment are ignored.  In a real chain, the private key should NOT be stored",
        "balance": "0xad78ebc5ac6200000"
      },
      "627306090abaB3A6e1400e9345bC60c78a8BEf57": {
        "privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3",
        "comment": "private key and this comment are ignored.  In a real chain, the private key should NOT be stored",
        "balance": "90000000000000000000000"
      },
      "f17f52151EbEF6C7334FAD080c5704D77216b732": {
        "privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f",
        "comment": "private key and this comment are ignored.  In a real chain, the private key should NOT be stored",
        "balance": "90000000000000000000000"
      }
    }
  },
  "blockchain": {
    "nodes": {
      "generate": true,
      "count": 6
    }
  }
}

There’s a couple of things to be aware:

  • The grayGlacierBlock is the latest Ethereum milestone block. Replace it with whatever milestone to start with; Usually it’s best to stick to the latest from here.
  • Blockhain nodes count: The minimum is currently 5 + bootnode

Create the file structure for nodes:

mkdir -p network/node-1/data network/node-2/data network/node-3/data network/node-4/data network/node-5/data network/node-6/data

Generate the keys for the nodes:

$ ./besu/bin/besu operator generate-blockchain-config --config-file=qbftConfigFile.json --to=networkFiles --private-key-file-name=key

# Result
$ tree networkFiles/
networkFiles/
├── genesis.json
└── keys
    ├── 0x3630f48b632d998fa54fd2e08a77a627aabdbd77
    │   ├── key
    │   └── key.pub
    ├── 0x422ef4e30554229ee8bd7bd576bc28b95ba9a733
    │   ├── key
    │   └── key.pub
    ├── 0x628da40ed943b3f57813058808d35f08b9125402
    │   ├── key
    │   └── key.pub
    ├── 0x992fd1dad418738a6a4a83cc80b3cacc85963822
    │   ├── key
    │   └── key.pub
    ├── 0xa8f66aa0e453ba1ecd9a722a7cb69c2369305517
    │   ├── key
    │   └── key.pub
    └── 0xf7b6f5857b179f705277bb489164d0921b94eb7e
        ├── key
        └── key.pub

Move the keys and genesis.json to the respective node directories:

$ bash move-generated-files.sh

# Result
$ tree network/
network/
├── node-1
│   ├── data
│   │   ├── key
│   │   └── key.pub
│   ├── genesis.json
│   ├── http-port
│   └── p2p-port
├── node-2
│   ├── data
│   │   ├── key
│   │   └── key.pub
│   ├── genesis.json
│   ├── http-port
│   └── p2p-port
├── node-3
│   ├── data
│   │   ├── key
│   │   └── key.pub
│   ├── genesis.json
│   ├── http-port
│   └── p2p-port
├── node-4
│   ├── data
│   │   ├── key
│   │   └── key.pub
│   ├── genesis.json
│   ├── http-port
│   └── p2p-port
├── node-5
│   ├── data
│   │   ├── key
│   │   └── key.pub
│   ├── genesis.json
│   ├── http-port
│   └── p2p-port
└── node-6
    ├── data
    │   ├── key
    │   └── key.pub
    ├── genesis.json
    ├── http-port
    └── p2p-port

13 directories, 30 files

Start the network

Bootnode

Now you can start the first node, in a new terminal:

cd network/node-1
guix shell openjdk -- ../../besu/bin/besu --data-path=data --genesis-file=genesis.json --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-http-cors-origins="all" --p2p-port=30303

Optionally, from the output, record the encode URL; For ex.:

2025-03-09 09:52:05.176+00:00 | main | INFO  | DefaultP2PNetwork | Enode URL enode://3b88135adbbeb081ec04c1ba403d6675af6200f031dfbc72c725adb0c3f5021cfbaca654798da6e59b0cfdb8c94b55b0fd83d0c895616dbb3de23d89f4597c5a@127.0.0.1:30303

Nodes

Now to start the second node, in a new terminal:

cd network/node-2
guix shell openjdk -- ../../besu/bin/besu --data-path=data --genesis-file=genesis.json --rpc-http-enabled --rpc-http-api=ETH,NET,QBFT --host-allowlist="*" --rpc-http-cors-origins="all" --bootnodes=enode://5b5a2ac557e8cb769afa6ca4bb33ba5218fe41c57ec1163db8efe757e74ccd37cdc02563b8073086a38dd31689b8a8c57f44dd252c69a044005af5ade7b389aa@127.0.0.1:30303 --p2p-port=30304 --rpc-http-port=8546

or use the provided script, each line in a new terminal:

bash launch-node.sh node-2 127.0.0.1 ETH,NET,QBFT,WEB3,TRACE,DEBUG,ADMIN
bash launch-node.sh node-3
bash launch-node.sh node-4
bash launch-node.sh node-5
bash launch-node.sh node-6

You can verify all peers are online:

curl -X POST --data '{"jsonrpc":"2.0","method":"qbft_getValidatorsByBlockNumber","params":["latest"], "id":1}' localhost:8545

Result

{
    "jsonrpc":"2.0",
    "id":1,
    "result": [
        "0x3630f48b632d998fa54fd2e08a77a627aabdbd77",
        "0x422ef4e30554229ee8bd7bd576bc28b95ba9a733",
        "0x628da40ed943b3f57813058808d35f08b9125402",
        "0x992fd1dad418738a6a4a83cc80b3cacc85963822",
        "0xa8f66aa0e453ba1ecd9a722a7cb69c2369305517",
        "0xf7b6f5857b179f705277bb489164d0921b94eb7e"
    ]
}

Within a couple of minutes, you should see the block number increasing:

2025-03-09 10:15:20.859+00:00 | EthScheduler-Timer-0 | INFO  | FullSyncTargetManager | Unable to find sync target. Waiting for 5 peers minimum. Currently checking 5 peers for usefulness
2025-03-09 10:15:21.664+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | BFT round summary (quorum = 4)
2025-03-09 10:15:21.664+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0x992fd1dad418738a6a4a83cc80b3cacc85963822  Round: 5 (Local node)
2025-03-09 10:15:21.665+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0xf7b6f5857b179f705277bb489164d0921b94eb7e  Round: 5
2025-03-09 10:15:21.665+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0xa8f66aa0e453ba1ecd9a722a7cb69c2369305517  Round: 5
2025-03-09 10:15:21.665+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0x3630f48b632d998fa54fd2e08a77a627aabdbd77  Round: 6
2025-03-09 10:15:21.665+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0x422ef4e30554229ee8bd7bd576bc28b95ba9a733  Round: 6
2025-03-09 10:15:21.665+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | RoundChangeManager | Address: 0x628da40ed943b3f57813058808d35f08b9125402  Round: 5
2025-03-09 10:15:21.729+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | QbftRound | Importing proposed block to chain. round=ConsensusRoundIdentifier{Sequence=1, Round=5}, hash=0xc0871cb0bd986548a35f12aadb0174522e17c6d3ee28245ee13ac1b070bddc51
2025-03-09 10:15:21.789+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | QbftBesuControllerBuilder | Imported empty block #1 / 0 tx / 0 pending / 0 (0.0%) gas / (0xc0871cb0bd986548a35f12aadb0174522e17c6d3ee28245ee13ac1b070bddc51)
2025-03-09 10:15:23.068+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | QbftBesuControllerBuilder | Imported empty block #2 / 0 tx / 0 pending / 0 (0.0%) gas / (0x834df30d707f27f287f3c5f181546448da901bc2e384d3071c0cc482a16358c7)
2025-03-09 10:15:23.078+00:00 | EthScheduler-Workers-0 | INFO  | PersistBlockTask | Block 2 (0x834df30d707f27f287f3c5f181546448da901bc2e384d3071c0cc482a16358c7) is already imported
2025-03-09 10:15:25.074+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | QbftBesuControllerBuilder | Imported empty block #3 / 0 tx / 0 pending / 0 (0.0%) gas / (0xfb409380276bcfff61d3513ca41e76fc231586b3575276ac8333fda47979121c)

You can also check the block number:

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[], "id":1}' localhost:8545

Result

{"jsonrpc":"2.0","id":1,"result":"0x57"}

Wallet

To play with the network on Metamask, add it as “Custom Network” with the following details:

  • Name: Besu DEV
  • New RPC URL: http://localhost:8545
  • Chain ID: 1337
  • Currency Symbol: BESU

Once you added the network, you can import the accounts using the private keys from the genesis.json file.

  1. Add a new account
  2. Import, and paste the private key from the genesis.json file
  3. You should see the balance of the account (200 BESU)

Repeat this for the second account, and make a transfer between them; It should show-up in the logs:

2025-03-09 10:34:21.019+00:00 | BftProcessorExecutor-QBFT-0 | INFO  | QbftBesuControllerBuilder | Imported empty block #571 / 0 tx / 1 pending / 0 (0.0%) gas / (0xcde78d11808b554e55d83f843f3a160b0f413f2ad382ee6a5c9730956bacde88)

Indexing with TrueBlocks

Do this in a seperate directory ref:

guix shell git cmake make gcc-toolchain ninja curl python jq go
git clone --depth 1 --no-single-branch --recurse-submodules --branch v4.2.0 https://github.com/TrueBlocks/trueblocks-core
cd trueblocks-core
mkdir build && cd build
../scripts/go-work-sync.sh
cmake ../src
make

I don’t like to pollute my paths, so I just set it for the current shell:

export PATH="/home/franz/git/trueblocks-core/bin:$PATH"

Run the application once, to generate the config:

chifra config --paths

Next, edit the config

$ nvim ~/.config/trueBlocks.toml
  1. Look for section [chains.mainnet]
  2. Set chainId = "1337"
  3. Set rpcProvider = "http://localhost:8546"

Usage

Note: The TB_NO_PROVIDER_CHECK=true is necessary, because of a failing check. I discuss this in the issue here.

Query a block:

$ TB_NO_PROVIDER_CHECK=true chifra blocks 23021
INFO[10-03|15:00:33.965] Skipping rpcProvider check
WARN[10-03|15:00:33.973] the --calldata value provided (manifestHashMap(0x0, "mainnet-ts")) was not found: abi not found for manifestHashMap(0x0, "mainnet-ts"): abi not found
{
  "data": [
    {
      "baseFeePerGas": 0,
      "blockNumber": 23021,
      "date": "2025-03-10 11:47:03 UTC",
      "difficulty": 1,
      "gasLimit": 4700000,
      "gasUsed": 25009,
      "hash": "0x8daeb989f85f23faf4dd605cfb50fcb1adb7e569074713f0b3c2a3e6945796fb",
      "miner": "0xf7b6f5857b179f705277bb489164d0921b94eb7e",
      "parentHash": "0x7a79fda98accab9876ebfd424b5603f6933b57944a9a32bc086e3605c50a6899",
      "timestamp": 1741607223,
      "transactions": [
        {
          "blockHash": "0x8daeb989f85f23faf4dd605cfb50fcb1adb7e569074713f0b3c2a3e6945796fb",
          "blockNumber": 23021,
          "date": "2025-03-10 11:47:03 UTC",
          "ether": "0",
          "from": "0xf17f52151ebef6c7334fad080c5704d77216b732",
          "gas": 25096,
          "gasCost": 30010800,
          "gasPrice": 1200,
          "gasUsed": 25009,
          "hash": "0xf5bb5c29a62e39f09911f81eb7e93097f5ac8d304c995908736e4b17777bd8c7",
          "input": "0x60fe47b10000000000000000000000000000000000000000000000000000000000000064",
          "nonce": 3,
          "receipt": {
            "contractAddress": "0x0",
            "effectiveGasPrice": 1200,
            "gasUsed": 25009,
            "logs": [
              {
                "address": "0x4d2d24899c0b115a1fce8637fca610fe02f1909e",
                "data": "0x000000000000000000000000f17f52151ebef6c7334fad080c5704d77216b7320000000000000000000000000000000000000000000000000000000000000064",
                "date": "2025-03-10 11:47:03 UTC",
                "logIndex": 0,
                "timestamp": 1741607223,
                "topics": [
                  "0xc9db20adedc6cf2b5d25252b101ab03e124902a73fcb12b753f3d1aaa2d8f9f5"
                ]
              }
            ],
            "status": null
          },
          "timestamp": 1741607223,
          "to": "0x4d2d24899c0b115a1fce8637fca610fe02f1909e",
          "traces": [],
          "transactionIndex": 0,
          "value": "0"
        }
      ],
      "uncles": [],
      "withdrawals": []
    }
  ]
}

Indexing with Chainlens

git clone https://github.com/web3labs/chainlens-free
cd chainlens-free/docker-compose

Because of how I have this setup, I made some changes to the docker-compose.yml, to run with host networking:

version: "3.6"
services:
  api:
    image: web3labs/epirus-free-api:latest
    environment:
      - NODE_ENDPOINT=${NODE_ENDPOINT}
      - MONGO_CLIENT_URI=mongodb://127.0.0.1:27017
      - REINDEX_ENDPOINT=http://ingestion/reindex/
      - MONGO_DB_NAME=epirus
      - MONGO_CREATE_INDICES=true
      - REDIS_HOST=127.0.0.1
      - REDIS_PORT=6379
    depends_on:
      - redis 
      - mongodb
    network_mode: "host"
  
  mongodb:
    image: mongo:5.0.8
    environment:
      - COMPOSE_HTTP_TIMEOUT=900
      - DOCKER_CLIENT_TIMEOUT=900
    entrypoint: mongod --bind_ip "127.0.0.1"
    network_mode: "host"

  redis:
    image: redis
    restart: unless-stopped
    container_name: redis
    network_mode: "host"

  web:
    image: web3labs/epirus-free-web:latest
    environment:
      - API_URL=/api
      - WS_API_URL=ws://localhost:8090
      - DISPLAY_NETWORK_TAB=disabled
    depends_on:
      - api
    network_mode: "host"
  
  ingestion:
    image: web3labs/epirus-free-ingestion:latest
    environment:
      - NODE_ENDPOINT=${NODE_ENDPOINT}
      - MONGO_CLIENT_URI=mongodb://127.0.0.1:27017
      - MONGO_DB_NAME=epirus
      - LIST_OF_METRICS_TO_CALCULATE_PER_MINUTE=hourly,daily,monthly,yearly
    depends_on:
      - mongodb
      - redis
    network_mode: "host"
      
  nginx:
    image: nginx:latest
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./5xx.html:/www/error_pages/5xx.html
    depends_on:
      - api
      - web
    network_mode: "host"

as well as the nginx.conf config:

events { }

http {
  server {
    listen 80;
    charset utf-8;

    location /api/ {
        proxy_pass http://127.0.0.1:8090/;
    }

    location / {
      proxy_pass http://127.0.0.1:3000/;
    }

    error_page 500 502 503 504 /5xx.html;
    location /5xx.html {
      root /www/error_pages/;
    } 
  }
}

Start Docker:

NODE_ENDPOINT=http://127.0.0.1:8546 docker-compose -f docker-compose.yml -f chainlens-extensions/docker-compose-quorum-dev-quickstart.yml up

You can access the Chainlens UI at http://localhost:80.

Smart Contract

Next, let’s deploy a smart contract; We’re following an example from the besu documentation, with minor changes.

Deploy

Start a new environment:

# You may use npm instead of pnpm
guix node pnpm
cd smart-contract
pnpm install

Compile the contract:

node compile.js

Get the bytecode:

pnpm exec solcjs StorageExample.sol --bin --abi

Submit the contract:

$ node public_tx.js
Creating transaction...
Signing transaction...
Sending transaction...
tx transactionHash: 0x77d1c0fcbb77bf8212d83ea7061330227f9aaeac8cec276379cc701bae87fd63
tx contractAddress: 0x4d2d24899c0b115a1fce8637fca610fe02f1909e

If you setup Chainlens previously, you should be able to see the contract now.

Interact

Now, let’s interact with the contract:

node query_contract.js

This will:

  1. Read the current value
  2. Fetch all past events
  3. Set a new value
  4. Read the new value

Credits

This guide is based on Create a private network using QBFT