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.
- Add a new account
- Import, and paste the private key from the
genesis.json
file - 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
- Look for section
[chains.mainnet]
- Set
chainId = "1337"
- 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:
- Read the current value
- Fetch all past events
- Set a new value
- Read the new value
Credits
This guide is based on Create a private network using QBFT