PolarSPARC

Hyperledger Besu Private Network using Docker


Bhaskar S *UPDATED*07/04/2025


Overview


Hyperledger Besu is a Java-based, open source Ethereum client that is designed for the Enterprise use-cases and can be run in both the public (permissionless) and the private (permissioned) scenarios.

Hyperledger Besu includes the following capabilities:

The following diagram illustrates the high-level architecture of Hyperledger Besu:


Besu Architecture
Hyperledger Besu Architecture

In the article Introduction to Ethereum - Part 1, we introduced some basic concepts of Ethereum, got our hands dirty in setting up a private network with 4 accounts (one for each of the entities - the bank, the buyer, the dealer, & the dmv), and demonstrated sending a sample transaction between two of the accounts.

In this article, we will demonstrate the same transaction behavior, but with a different setup using the official Docker image for Hyperledger Besu.

Installation


The setup will be on a Ubuntu 24.04 LTS based Linux desktop.

Ensure Docker is installed and setup (Reference).

Also, ensure the Python programming language (version 3.1x) is installed and setup.

Assuming that we are logged in as polarsparc and the current working directory is the home directory /home/polarsparc, we will setup a directory structure by executing the following commands in a terminal window:


$ mkdir -p HyperledgerBesu/conf

$ mkdir -p HyperledgerBesu/data{/bank/keystore,/buyer/keystore,/dealer/keystore,/dmv/keystore}


Now, change the current working directory to the directory /home/polarsparc/HyperledgerBesu. In the following paragraphs we will refer to this location as $BESU_HOME.

Visit the link Hyperledger Besu to determine the current version of the docker image. At the time of this article, the current stable version was 25.7.0.

To pull and download the docker image for Hyperledger Besu, execute the following command:


docker pull hyperledger/besu:25.7.0


The following should be the typical output:


Output.1

25.7.0: Pulling from hyperledger/besu
b08e2ff4391e: Pull complete 
65b4faee11fb: Pull complete 
4f4fb700ef54: Pull complete 
8c5b0b547769: Pull complete 
dcc6ce8db6bf: Pull complete 
bc0fb53ff1a9: Pull complete 
Digest: sha256:09317071e2742ca0530b504ad40f3efeba3a0ef85a0038917bc056865fce7c50
Status: Downloaded newer image for hyperledger/besu:25.7.0
docker.io/hyperledger/besu:25.7.0

Once the download is complete, execute the following command to check everything was ok with the image for Hyperledger Besu:


$ docker run --rm --name besu hyperledger/besu:25.7.0 --version


The following would be the typical output:


Output.2

besu/v25.7.0/linux-x86_64/openjdk-java-21

Next step is to download and install the Python module for Web3. Execute the following command:


python -m pip install web3


This completes the installation part. Moving on to setup the private network.

Private Network Setup


For this demonstration, we will need 4 accounts, one for each of the entities - the bank, the buyer, the dealer, and the dmv.

We will create the 4 accounts using MyEtherWallet.

First, we will create an account for the entity bank. Click on Create a new wallet as shown in the illustration below:


Create New Wallet
Create a New Wallet

Next, click on Other Methods as shown in the illustration below:


Wallet Method
Wallet Method

Next, click on Create a software wallet as shown in the illustration below:


Software Wallet
Software Wallet

Next, click on Keystore File as shown in the illustration below:


Keystore File
Keystore File

Next, enter a secure password in the text box Password, re-enter the same password in the text box Confirm Password, and then click on Create Wallet as shown in the illustration below:


Create Wallet
Create Wallet

Next, click on Acknowledge & Download as shown in the illustration below:


Acknowledge & Download
Acknowledge & Download

This action will take a few seconds and the keystore file UTC--{timestamp}--{address} will be downloaded to the directory $HOME/Downloads.

Move the keystore file we just created for the bank to the appropriate directory by executing the following command:


$ mv $HOME/Downloads/UTC--* $BESU_HOME/data/bank/keystore


Perform the above steps for creating a new keystore for each of the remaining entities - the buyer, the dealer, and the dmv.

Once the 4 keystore files are created and moved to the appropriate location, execute the following command at the location $BESU_HOME:


$ tree ./data


The following would be the typical output:


Output.3

./data
|-- bank
|   |-- keystore
|       |-- UTC--2025-07-04T19-35-44.627Z--4ed5d88f55c4715298d577d17d3ffc57bf3dc981
|-- buyer
|   |-- keystore
|       |-- UTC--2025-07-04T19-39-12.796Z--5a4240acd295d673c13e197b99d0fa70bbd64ca6
|-- dealer
|   |-- keystore
|       |-- UTC--2025-07-04T19-41-36.877Z--3b66024245bc6b4c4f183d203170b6fa78754948
|-- dmv
    |-- keystore
        |-- UTC--2025-07-04T19-42-07.826Z--6a571605bf60218e3d155eca6d6f507e83e2ca14

9 directories, 4 files

From the Output.3 above, we have the 4 accounts and their respective addresses as follows:

In order to setup a private Ethereum blockchain network, we need to initialize and create the Genesis block. The Genesis block is the first block of the blockchain that has no previous block. We will use the following template to create the genesis file:


genesis-template.json
{
  "config":{
    "chainId": 21,
    "berlinBlock": 0,
    "clique":{
      "blockperiodseconds": 10,
      "epochlength": 30000
    }
  },
  "coinbase":"0x0000000000000000000000000000000000000000",
  "difficulty":"0x1",
  "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000bank-address0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "gasLimit":"0xa00000",
  "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
  "nonce":"0x0",
  "timestamp":"0x5c51a607",
  "alloc": {
      "bank-address": {
        "balance": "20000000000000000000"
      },
      "buyer-address": {
        "balance": "10000000000000000000"
      },
      "dealer-address": {
        "balance": "5000000000000000000"
      },
      "dmv-address": {
        "balance": "3000000000000000000"
      }
   },
  "gasUsed": "0x0",
  "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"
}

Create a file called genesis.json with above contents in the directory $BESU_HOME/conf.

For this demonstration, we will be using the Clique algorithm, which is one of the supported Proof of Authority (PoA) consensus algorithm.

The PoA is most suitable for Enterprise scenarios where the participants know and trust each other. As a result, the PoA consensus algorithms have faster block creation times and higher transaction throughput.

Clique is a reputation-based algorithm in which only approved and trusted accounts (known as the Signers) can validate transactions and create blocks. If there are more than one Signers, they take turns to create the next block.

We will use the bank as the trusted Signers in our private Ethereum network. The address of the Signers is specified in the extraData field.

The following illustration shows the template for the extraData field:


Signer Template
Signer Template

Using the bank's address as the Signer (which is a 40 character hex string) f1da86e0f5288f0537865e7b51edd302089ae06f, the following illustration shows the final extraData field:


Bank as Signer
Bank as Signer

After modifying the contents of the file genesis.json (located in the directory $BESU_HOME/conf) with the Signer and the 4 account addresses, the following would be the contents of the genesis file:


genesis.json
{
  "config":{
    "chainId": 21,
    "berlinBlock": 0,
    "clique":{
      "blockperiodseconds": 10,
      "epochlength": 30000
    }
  },
  "coinbase":"0x0000000000000000000000000000000000000000",
  "difficulty":"0x1",
  "extraData":"0x00000000000000000000000000000000000000000000000000000000000000004ed5d88f55c4715298d577d17d3ffc57bf3dc9810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "gasLimit":"0xa00000",
  "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000",
  "nonce":"0x0",
  "timestamp":"0x5c51a607",
  "alloc": {
    "4ed5d88f55c4715298d577d17d3ffc57bf3dc981": {
      "balance": "20000000000000000000"
    },
    "5a4240acd295d673c13e197b99d0fa70bbd64ca6": {
      "balance": "10000000000000000000"
    },
    "3b66024245bc6b4c4f183d203170b6fa78754948": {
      "balance": "5000000000000000000"
    },
    "6a571605bf60218e3d155eca6d6f507e83e2ca14": {
      "balance": "3000000000000000000"
    }
  },
  "gasUsed": "0x0",
  "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000"
}

In the following section, we will explain some of the important parameters used in the genysis file shown above:

Moving along, the next step is to extract the private key for the 4 accounts. For this, we will leverage the web3 module we installed for Python.

Open a new terminal window, change the current directory to $BESU_HOME, and launch the Python interpreter.

To extract the private key for bank, execute the following commands at the Python interpreter prompt:


>>> from web3.auto import w3

>>> with open(''./data/bank/keystore/UTC--2025-07-04T19-35-44.627Z--4ed5d88f55c4715298d577d17d3ffc57bf3dc981') as key_file:

... encrypted_key = key_file.read()

... bank_priv_key = w3.eth.account.decrypt(encrypted_key, 'Bank_Secret')

>>> bank_priv_key


The following would be the typical output:


Output.4

HexBytes('0x712c1cc999b24753768bad4b3e1371d530abd9693803b627a974009b64b0dbf1')

Copy the private key 712c1cc999b24753768bad4b3e1371d530abd9693803b627a974009b64b0dbf1 (without the 0x) and save it to a file called key in the directory $BESU_HOME/data/bank.

We will repeat the above steps for each of the other accounts buyer, dealer , and dmv.

To extract the private key for buyer, execute the following commands at the Python interpreter prompt:


>>> with open('./data/buyer/keystore/UTC--2025-07-04T19-39-12.796Z--5a4240acd295d673c13e197b99d0fa70bbd64ca6') as key_file:

... encrypted_key = key_file.read()

... buyer_priv_key = w3.eth.account.decrypt(encrypted_key, 'Buyer_Secret')

>>> buyer_priv_key


The following would be the typical output:


Output.5

HexBytes('0x1680ff9e0ad288d507d04b569ea2d1aa9e5e3b58458e8a5dea7df12d2c41a1b9')

Copy the private key 1680ff9e0ad288d507d04b569ea2d1aa9e5e3b58458e8a5dea7df12d2c41a1b9 (without the 0x) and save it to a file called key in the directory $BESU_HOME/data/buyer.

To extract the private key for dealer, execute the following commands at the Python interpreter prompt:


>>> with open('./data/dealer/keystore/UTC--2025-07-04T19-41-36.877Z--3b66024245bc6b4c4f183d203170b6fa78754948') as key_file:

... encrypted_key = key_file.read()

... dealer_priv_key = w3.eth.account.decrypt(encrypted_key, 'Dealer_Secret')

>>> dealer_priv_key


The following would be the typical output:


Output.6

HexBytes('0xdb35217c01ade41b401fb2616a83af102ce8c0178fbca298f8af42100bc5aab5')

Copy the private key db35217c01ade41b401fb2616a83af102ce8c0178fbca298f8af42100bc5aab5 (without the 0x) and save it to a file called key in the directory $BESU_HOME/data/dealer.

Finally, to extract the private key for dmv, execute the following commands at the Python interpreter prompt:


>>> with open('./data/dmv/keystore/UTC--2025-07-04T19-42-07.826Z--6a571605bf60218e3d155eca6d6f507e83e2ca14') as key_file:

... encrypted_key = key_file.read()

... dmv_priv_key = w3.eth.account.decrypt(encrypted_key, 'Dmv_Secret')

>>> dmv_priv_key


The following would be the typical output:


Output.7

HexBytes('0x308c9f3ab1124c392c5977b073597313f6500ddf55cec824bff72aad0abd6999')

Copy the private key 308c9f3ab1124c392c5977b073597313f6500ddf55cec824bff72aad0abd6999 (without the 0x) and save it to a file called key in the directory $BESU_HOME/data/dmv.

The next step is to extract the public address of the bootnode and store it in a file named bootnode_pubkey that will be located at $BESU_HOME/data/bank. A bootnode is used for initial discovery of the nodes (peers) in the network. We will designate the node running the bank as the bootnode.

To extract the public address for bank, execute the following command in the terminal prompt:


$ docker run --rm --name bank -u $(id -u $USER):$(id -g $USER) -v $BESU_HOME:/root hyperledger/besu:25.7.0 --data-path=/root/data/bank --node-private-key-file=/root/data/bank/key public-key export-address --to=/root/data/bank/bootnode_pubkey


The following would be the typical output:


Output.8

2025-07-05 00:22:12.764+00:00 | main | INFO  | KeyPairUtil | Attempting to load public key from /root/data/bank/key
2025-07-05 00:22:12.789+00:00 | main | INFO  | KeyPairUtil | Loaded public key 0x02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77 from /root/data/bank/key

For simplicity, the nodes of our Hyperledger Besu based private Ethereum network will on localhost 127.0.0.1 on different ports.

The next step is to create a configuration file with various options for the bank node. The configuration file will be used by Hyperledger Besu to start the Ethereum node.

The following will be the contents of the configuration file bank-config.toml (located in the directory $BESU_HOME/conf):


bank-config.toml
logging="INFO"
identity="bank"
data-path="/opt/besu/data"
genesis-file="/config/genesis.json"
node-private-key-file="/opt/besu/keys/key"
nat-method="DOCKER"
host-allowlist=["*"]

# P2P related

p2p-host="127.0.0.1"
p2p-port=30303

# RPC related

rpc-http-enabled=true
rpc-http-host="127.0.0.1"
rpc-http-port=8545
rpc-http-api=["ADMIN", "CLIQUE", "ETH", "NET", "TXPOOL", "WEB3"]
rpc-http-cors-origins=["*"]

In the following section, we will explain some of the configuration options used above:

Open 4 terminal windows, each representing the 4 entities, namely, the bank, the buyer, the dealer, and the dmv. We will refer to these 4 windows corresponding to the entity they refer. For example, the first terminal will be referred to as the bank , the second terminal as the buyer, the third terminal as the dealer and the fourth terminal as the dmv.

In each of the 4 windows, change the current working directory to $BESU_HOME.

In the bank terminal, start the Ethereum node by executing the following command:


$ docker run --rm --name bank --network host -e BESU_OPTS="--add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true" -u $(id -u $USER):$(id -g $USER) -v $BESU_HOME/data/bank:/opt/besu/data -v $BESU_HOME/data/bank:/opt/besu/keys -v $BESU_HOME/data/bank:/opt/besu/public-keys -v $BESU_HOME/conf/bank-config.toml:/config/config.toml -v $BESU_HOME/conf/genesis.json:/config/genesis.json hyperledger/besu:25.7.0 --config-file=/config/config.toml


The following would be the typical trimmed output:


Output.9

Setting logging level to INFO
2025-07-05 00:35:37.080+00:00 | main | INFO  | Besu | Starting Besu
2025-07-05 00:35:37.191+00:00 | main | WARN  | Besu | --rpc-ws-host has been ignored because --rpc-ws-enabled was not defined on the command line.
2025-07-05 00:35:37.194+00:00 | main | WARN  | Besu | --graphql-http-host has been ignored because --graphql-http-enabled was not defined on the command line.
2025-07-05 00:35:37.280+00:00 | main | WARN  | Besu | Forcing --bonsai-limit-trie-logs-enabled=false, since it cannot be enabled with --sync-mode=FULL and --data-storage-format=BONSAI.
2025-07-05 00:35:37.280+00:00 | main | INFO  | Besu | Connecting to 0 static nodes.
.....[TRIM].....
2025-07-05 00:35:37.337+00:00 | main | INFO  | Besu | Security Module: localfile
2025-07-05 00:35:37.337+00:00 | main | INFO  | VersionMetadata | Lookup version metadata file in data directory: /opt/besu/data
2025-07-05 00:35:37.338+00:00 | main | INFO  | VersionMetadata | No version data detected. Writing Besu version 25.7.0 to metadata file
2025-07-05 00:35:37.369+00:00 | main | INFO  | Besu | Using the native implementation of alt bn128
2025-07-05 00:35:37.369+00:00 | main | INFO  | Besu | Using the Java implementation of modexp
2025-07-05 00:35:37.369+00:00 | main | INFO  | Besu | Using the native implementation of the signature algorithm
2025-07-05 00:35:37.370+00:00 | main | INFO  | Besu | Using the native implementation of the blake2bf algorithm
2025-07-05 00:35:37.425+00:00 | main | INFO  | KeyPairUtil | Attempting to load public key from /opt/besu/keys/key
2025-07-05 00:35:37.438+00:00 | main | INFO  | KeyPairUtil | Loaded public key 0x02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77 from /opt/besu/keys/key
2025-07-05 00:35:37.507+00:00 | main | INFO  | ProtocolScheduleBuilder | Protocol schedule created with milestones: [Berlin:0]
2025-07-05 00:35:37.510+00:00 | main | INFO  | RocksDBKeyValueStorageFactory | No existing database at /opt/besu/data. Using default metadata for new db versionedStorageFormat=BaseVersionedStorageFormat{format=BONSAI, version=3}
2025-07-05 00:35:37.673+00:00 | main | INFO  | FlatDbStrategyProvider | Flat db mode found FULL
2025-07-05 00:35:37.674+00:00 | main | INFO  | FlatDbStrategyProvider | DB mode with code stored using code hash enabled = true
2025-07-05 00:35:37.680+00:00 | main | INFO  | FlatDbStrategyProvider | Flat db mode found FULL
2025-07-05 00:35:37.681+00:00 | main | INFO  | FlatDbStrategyProvider | DB mode with code stored using code hash enabled = true
2025-07-05 00:35:37.735+00:00 | main | INFO  | TransactionPoolFactory | Enabling transaction pool
2025-07-05 00:35:37.744+00:00 | main | INFO  | BesuControllerBuilder | TTD difficulty is not present, creating initial sync phase for PoW
2025-07-05 00:35:37.761+00:00 | main | INFO  | RunnerBuilder | Resolved 0 bootnodes.
2025-07-05 00:35:37.766+00:00 | main | INFO  | RunnerBuilder | Detecting NAT service.
2025-07-05 00:35:37.830+00:00 | main | INFO  | Runner | Starting external services ... 
2025-07-05 00:35:37.830+00:00 | main | INFO  | JsonRpcHttpService | Starting JSON-RPC service on 0.0.0.0:8545
2025-07-05 00:35:37.888+00:00 | vert.x-eventloop-thread-1 | INFO  | JsonRpcHttpService | JSON-RPC service started and listening on 0.0.0.0:8545
2025-07-05 00:35:37.890+00:00 | main | INFO  | AutoTransactionLogBloomCachingService | Starting auto transaction log bloom caching service.
2025-07-05 00:35:37.891+00:00 | main | INFO  | LogBloomCacheMetadata | Lookup cache metadata file in data directory: /opt/besu/data/caches
2025-07-05 00:35:37.896+00:00 | main | INFO  | Runner | Starting Ethereum main loop ... 
2025-07-05 00:35:37.896+00:00 | main | INFO  | DockerNatManager | Starting docker NAT manager.
2025-07-05 00:35:37.898+00:00 | main | INFO  | NetworkRunner | Starting Network.
2025-07-05 00:35:37.904+00:00 | nioEventLoopGroup-2-1 | INFO  | RlpxAgent | P2P RLPx agent started and listening on /[0:0:0:0:0:0:0:0]:30303.
2025-07-05 00:35:37.904+00:00 | main | INFO  | PeerDiscoveryAgent | Starting peer discovery agent on host=0.0.0.0, port=30303. IPv6 available.
2025-07-05 00:35:37.911+00:00 | vert.x-eventloop-thread-1 | INFO  | VertxPeerDiscoveryAgent | Started peer discovery agent successfully, on effective host=0:0:0:0:0:0:0:0 and port=30303
2025-07-05 00:35:37.913+00:00 | vert.x-eventloop-thread-1 | INFO  | PeerDiscoveryAgent | P2P peer discovery agent started and listening on /[0:0:0:0:0:0:0:0]:30303
2025-07-05 00:35:37.938+00:00 | vert.x-eventloop-thread-1 | INFO  | PeerDiscoveryAgent | Writing node record to disk. NodeRecord{seq=1, publicKey=0x0302746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8, udpAddress=Optional[/127.0.0.1:30303], tcpAddress=Optional[/127.0.0.1:30303], udp6Address=Optional.empty, tcp6Address=Optional.empty, asBase64=-Je4QGDSNn261CGC4kWGeQv3gQ726TJkQRC6BZhq6VfPk29zcn7DVzdATLO2flH0qhTDA0NjfpkczY2kH3lBa2mueKcBg2V0aMfGhFgzXyCAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQMCdGhQrrEkNcvMlpj20S0uu6wQTqY1zwnq0nZ2-veYyIN0Y3CCdl-DdWRwgnZf, nodeId=0x487968a8aa662889f99da14e4ed5d88f55c4715298d577d17d3ffc57bf3dc981, customFields={tcp=30303, udp=30303, eth=[[0x58335f20, 0x]], id=V4, ip=0x7f000001, secp256k1=0x0302746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8}}
2025-07-05 00:35:37.945+00:00 | main | INFO  | DefaultP2PNetwork | Enode URL enode://02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77@127.0.0.1:30303
2025-07-05 00:35:37.946+00:00 | main | INFO  | DefaultP2PNetwork | Node address 0x4ed5d88f55c4715298d577d17d3ffc57bf3dc981
2025-07-05 00:35:37.950+00:00 | main | INFO  | NetworkRunner | Supported capabilities: [eth/66, eth/67, eth/68, eth/69], [snap/1]
2025-07-05 00:35:37.950+00:00 | main | INFO  | DefaultSynchronizer | Starting synchronizer.
2025-07-05 00:35:37.951+00:00 | main | INFO  | TransactionPoolFactory | Enabling transaction handling following initial sync
2025-07-05 00:35:37.951+00:00 | main | INFO  | FullSyncDownloader | Starting full sync.
2025-07-05 00:35:37.951+00:00 | main | INFO  | FullSyncTargetManager | Unable to find sync target. Waiting for 5 peers minimum. Currently checking 0 peers for usefulness
2025-07-05 00:35:37.955+00:00 | main | INFO  | Runner | Ethereum main loop is up.

From the Output.8 above, we need to make a note of the bootnode address from the line that contains the sub-string Enode URL.

We will now create the configuration files (with various options including the address of the bootnode) for the remaining 3 entities.


* WARNING *

The docker option --network host was the *ONLY* way to get the Ethereum private network work, with all the nodes discovered and connected.

The following will be the contents of the configuration file buyer-config.toml (located in the directory $BESU_HOME/conf):


buyer-config.toml
logging="INFO"
identity="buyer"
data-path="/opt/besu/data"
genesis-file="/config/genesis.json"
node-private-key-file="/opt/besu/keys/key"
nat-method="DOCKER"
host-allowlist=["*"]
bootnodes=["enode://02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77@127.0.0.1:30303"]

# P2P related

p2p-host="127.0.0.1"
p2p-port=30304

# RPC related

rpc-http-enabled=false
rpc-http-host="127.0.0.1"
rpc-http-port=8546
rpc-http-api=["ADMIN", "CLIQUE", "ETH", "NET", "TXPOOL", "WEB3"]
rpc-http-cors-origins=["*"]

The following will be the contents of the configuration file dealer-config.toml (located in the directory $BESU_HOME/conf):


dealer-config.toml
logging="INFO"
identity="dealer"
data-path="/opt/besu/data"
genesis-file="/config/genesis.json"
node-private-key-file="/opt/besu/keys/key"
nat-method="DOCKER"
host-allowlist=["*"]
bootnodes=["enode://02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77@127.0.0.1:30303"]

# P2P related

p2p-host="127.0.0.1"
p2p-port=30305

# RPC related

rpc-http-enabled=false
rpc-http-host="127.0.0.1"
rpc-http-port=8547
rpc-http-api=["ADMIN", "CLIQUE", "ETH", "NET", "TXPOOL", "WEB3"]
rpc-http-cors-origins=["*"]

The following will be the contents of the configuration file dmv-config.toml (located in the directory $BESU_HOME/conf):


dmv-config.toml
logging="INFO"
identity="dmv"
data-path="/opt/besu/data"
genesis-file="/config/genesis.json"
node-private-key-file="/opt/besu/keys/key"
nat-method="DOCKER"
host-allowlist=["*"]
bootnodes=["enode://02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77@127.0.0.1:30303"]

# P2P related

p2p-host="127.0.0.1"
p2p-port=30306

# RPC related

rpc-http-enabled=false
rpc-http-host="127.0.0.1"
rpc-http-port=8548
rpc-http-api=["ADMIN", "CLIQUE", "ETH", "NET", "TXPOOL", "WEB3"]
rpc-http-cors-origins=["*"]

CAUTION:

The values for the configuration options p2p-port and the rpc-http-port have to be unique for every Ethereum node in the private network.

In the buyer terminal, start the Ethereum node by executing the following command:


$ docker run --rm --name buyer --network host -e BESU_OPTS="--add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true" -u $(id -u $USER):$(id -g $USER) -v $BESU_HOME/data/buyer:/opt/besu/data -v $BESU_HOME/data/buyer:/opt/besu/keys -v $BESU_HOME/conf/buyer-config.toml:/config/config.toml -v $BESU_HOME/conf/genesis.json:/config/genesis.json hyperledger/besu:25.7.0 --config-file=/config/config.toml


The following would be the typical trimmed output:


Output.10

Setting logging level to INFO
2025-07-05 00:44:23.235+00:00 | main | INFO  | Besu | Starting Besu
2025-07-05 00:44:23.347+00:00 | main | WARN  | Besu | --rpc-http-host, --rpc-http-port, --rpc-http-cors-origins and --rpc-http-api has been ignored because --rpc-http-enabled was not defined on the command line.
2025-07-05 00:44:23.348+00:00 | main | WARN  | Besu | --rpc-ws-host has been ignored because --rpc-ws-enabled was not defined on the command line.
2025-07-05 00:44:23.351+00:00 | main | WARN  | Besu | --graphql-http-host has been ignored because --graphql-http-enabled was not defined on the command line.
2025-07-05 00:44:23.421+00:00 | main | WARN  | Besu | Forcing --bonsai-limit-trie-logs-enabled=false, since it cannot be enabled with --sync-mode=FULL and --data-storage-format=BONSAI.
2025-07-05 00:44:23.422+00:00 | main | INFO  | Besu | Connecting to 0 static nodes.
.....[TRIM].....
2025-07-05 00:44:23.477+00:00 | main | INFO  | Besu | Security Module: localfile
2025-07-05 00:44:23.477+00:00 | main | INFO  | VersionMetadata | Lookup version metadata file in data directory: /opt/besu/data
2025-07-05 00:44:23.478+00:00 | main | INFO  | VersionMetadata | No version data detected. Writing Besu version 25.7.0 to metadata file
2025-07-05 00:44:23.509+00:00 | main | INFO  | Besu | Using the native implementation of alt bn128
2025-07-05 00:44:23.510+00:00 | main | INFO  | Besu | Using the Java implementation of modexp
2025-07-05 00:44:23.510+00:00 | main | INFO  | Besu | Using the native implementation of the signature algorithm
2025-07-05 00:44:23.511+00:00 | main | INFO  | Besu | Using the native implementation of the blake2bf algorithm
2025-07-05 00:44:23.569+00:00 | main | INFO  | KeyPairUtil | Attempting to load public key from /opt/besu/keys/key
2025-07-05 00:44:23.582+00:00 | main | INFO  | KeyPairUtil | Loaded public key 0x16ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da442fdcc829163ccf521ae661d6ba74e63f4369318cbf69201a8152e82f8b57495e from /opt/besu/keys/key
2025-07-05 00:44:23.652+00:00 | main | INFO  | ProtocolScheduleBuilder | Protocol schedule created with milestones: [Berlin:0]
2025-07-05 00:44:23.657+00:00 | main | INFO  | RocksDBKeyValueStorageFactory | No existing database at /opt/besu/data. Using default metadata for new db versionedStorageFormat=BaseVersionedStorageFormat{format=BONSAI, version=3}
2025-07-05 00:44:23.819+00:00 | main | INFO  | FlatDbStrategyProvider | Flat db mode found FULL
2025-07-05 00:44:23.820+00:00 | main | INFO  | FlatDbStrategyProvider | DB mode with code stored using code hash enabled = true
2025-07-05 00:44:23.827+00:00 | main | INFO  | FlatDbStrategyProvider | Flat db mode found FULL
2025-07-05 00:44:23.827+00:00 | main | INFO  | FlatDbStrategyProvider | DB mode with code stored using code hash enabled = true
2025-07-05 00:44:23.884+00:00 | main | INFO  | TransactionPoolFactory | Enabling transaction pool
2025-07-05 00:44:23.894+00:00 | main | INFO  | BesuControllerBuilder | TTD difficulty is not present, creating initial sync phase for PoW
2025-07-05 00:44:23.910+00:00 | main | INFO  | RunnerBuilder | Resolved 1 bootnodes.
2025-07-05 00:44:23.915+00:00 | main | INFO  | RunnerBuilder | Detecting NAT service.
2025-07-05 00:44:23.954+00:00 | main | INFO  | Runner | Starting external services ... 
2025-07-05 00:44:23.956+00:00 | main | INFO  | AutoTransactionLogBloomCachingService | Starting auto transaction log bloom caching service.
2025-07-05 00:44:23.957+00:00 | main | INFO  | LogBloomCacheMetadata | Lookup cache metadata file in data directory: /opt/besu/data/caches
2025-07-05 00:44:23.963+00:00 | main | INFO  | Runner | Starting Ethereum main loop ... 
2025-07-05 00:44:23.964+00:00 | main | INFO  | DockerNatManager | Starting docker NAT manager.
2025-07-05 00:44:23.965+00:00 | main | INFO  | NetworkRunner | Starting Network.
2025-07-05 00:44:23.996+00:00 | nioEventLoopGroup-2-1 | INFO  | RlpxAgent | P2P RLPx agent started and listening on /[0:0:0:0:0:0:0:0]:30304.
2025-07-05 00:44:23.997+00:00 | main | INFO  | PeerDiscoveryAgent | Starting peer discovery agent on host=0.0.0.0, port=30304. IPv6 available.
2025-07-05 00:44:24.006+00:00 | vert.x-eventloop-thread-1 | INFO  | VertxPeerDiscoveryAgent | Started peer discovery agent successfully, on effective host=0:0:0:0:0:0:0:0 and port=30304
2025-07-05 00:44:24.007+00:00 | vert.x-eventloop-thread-1 | INFO  | PeerDiscoveryAgent | P2P peer discovery agent started and listening on /[0:0:0:0:0:0:0:0]:30304
2025-07-05 00:44:24.030+00:00 | vert.x-eventloop-thread-1 | INFO  | PeerDiscoveryAgent | Writing node record to disk. NodeRecord{seq=1, publicKey=0x0216ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da44, udpAddress=Optional[/127.0.0.1:30304], tcpAddress=Optional[/127.0.0.1:30304], udp6Address=Optional.empty, tcp6Address=Optional.empty, asBase64=-Je4QM5XF1fURPVPoZfw30eHppag21fnGLv_UWcpNAw-3IcROPrbHvMAX51WORaQjrBVaH3wLgrB_rSMjc6mlBURvFsBg2V0aMfGhFgzXyCAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIW6gM2WayW7AixYgLEmUyZGjPn2rarpsSr-AGoGoTaRIN0Y3CCdmCDdWRwgnZg, nodeId=0x8aff6771f979f0a9fc9e36d65a4240acd295d673c13e197b99d0fa70bbd64ca6, customFields={tcp=30304, udp=30304, eth=[[0x58335f20, 0x]], id=V4, ip=0x7f000001, secp256k1=0x0216ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da44}}
2025-07-05 00:44:24.042+00:00 | main | INFO  | DefaultP2PNetwork | Enode URL enode://16ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da442fdcc829163ccf521ae661d6ba74e63f4369318cbf69201a8152e82f8b57495e@127.0.0.1:30304
2025-07-05 00:44:24.042+00:00 | main | INFO  | DefaultP2PNetwork | Node address 0x5a4240acd295d673c13e197b99d0fa70bbd64ca6
2025-07-05 00:44:24.047+00:00 | main | INFO  | NetworkRunner | Supported capabilities: [eth/66, eth/67, eth/68, eth/69], [snap/1]
2025-07-05 00:44:24.047+00:00 | main | INFO  | DefaultSynchronizer | Starting synchronizer.
2025-07-05 00:44:24.048+00:00 | main | INFO  | TransactionPoolFactory | Enabling transaction handling following initial sync
2025-07-05 00:44:24.048+00:00 | main | INFO  | FullSyncDownloader | Starting full sync.
2025-07-05 00:44:24.048+00:00 | main | INFO  | FullSyncTargetManager | Unable to find sync target. Waiting for 5 peers minimum. Currently checking 0 peers for usefulness
2025-07-05 00:44:24.053+00:00 | main | INFO  | Runner | Ethereum main loop is up.
2025-07-05 00:44:24.246+00:00 | nioEventLoopGroup-3-1 | INFO  | TransactionPoolFactory | Node out of sync, disabling transaction handling
2025-07-05 00:44:29.141+00:00 | EthScheduler-Services-5 (importBlock) | INFO  | TransactionPoolFactory | Node is in sync, enabling transaction handling

In the dealer terminal, start the Ethereum node by executing the following command:


$ docker run --rm --name dealer --network host -e BESU_OPTS="--add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true" -u $(id -u $USER):$(id -g $USER) -v $BESU_HOME/data/dealer:/opt/besu/data -v $BESU_HOME/data/dealer:/opt/besu/keys -v $BESU_HOME/conf/dealer-config.toml:/config/config.toml -v $BESU_HOME/conf/genesis.json:/config/genesis.json hyperledger/besu:25.7.0 --config-file=/config/config.toml


In the dmv terminal, start the Ethereum node by executing the following command:


$ docker run --rm --name dmv --network host -e BESU_OPTS="--add-opens java.base/jdk.internal.misc=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true" -u $(id -u $USER):$(id -g $USER) -v $BESU_HOME/data/dmv:/opt/besu/data -v $BESU_HOME/data/dmv:/opt/besu/keys -v $BESU_HOME/conf/dmv-config.toml:/config/config.toml -v $BESU_HOME/conf/genesis.json:/config/genesis.json hyperledger/besu:25.7.0 --config-file=/config/config.toml


To make API requests on our private blockchain network, we will issue the following commands at the Python interpreter prompt:


>>> import web3

>>> from web3 import Web3

>>> from web3.middleware import ExtraDataToPOAMiddleware

>>> provider = Web3.HTTPProvider('http://127.0.0.1:8545')

>>> w3 = Web3(provider)

>>> w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)


To check if we have successfully connected to the bank node, execute the following command at the Python interpreter prompt:


>>> w3.is_connected()


The following would be the typical output:


Output.11

True

To get information about the bank node, execute the following command at the Python interpreter prompt:


>>> w3.geth.admin.node_info()


The following would be the typical output:


Output.12

AttributeDict({'enode': 'enode://02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77@127.0.0.1:30303', 'listenAddr': '127.0.0.1:30303', 'activeFork': 'Berlin', 'ip': '127.0.0.1', 'name': 'besu/bank/v25.7.0/linux-x86_64/openjdk-java-21', 'id': '02746850aeb12435cbcc9698f6d12d2ebbac104ea635cf09ead27676faf798c8c6735139b74165157f9e027e89fcc777a9875d43e49d436c9da823a0e3003c77', 'ports': AttributeDict({'discovery': 30303, 'listener': 30303}), 'protocols': AttributeDict({'eth': AttributeDict({'config': AttributeDict({'chainId': 21, 'berlinBlock': 0, 'clique': AttributeDict({'epochLength': 30000, 'blockPeriodSeconds': 10, 'createemptyblocks': True})}), 'difficulty': 393, 'genesis': '0x9fe2eac5405ea5af672e3d6843c40f2a925280e08dabc90099559d9e42c2dc56', 'head': '0xca43c0a4ff43972c94e98342c924a01a1844be3ef8eaa22ac63ed2f4da9d7141', 'network': 21})})})

The trimmed string value enode://0274685...@127.0.0.1:30303 is the endpoint address for the bank node.

To verify the peers connected to the bank node, execute the following command at the Python interpreter prompt:


>>> w3.geth.admin.peers()


The following would be the typical output:


Output.13

[AttributeDict({'version': '0x5', 'name': 'besu/dmv/v25.7.0/linux-x86_64/openjdk-java-21', 'caps': ['eth/66', 'eth/67', 'eth/68', 'eth/69', 'snap/1'], 'network': AttributeDict({'localAddress': '127.0.0.1:40760', 'remoteAddress': '127.0.0.1:30306', 'inbound': False}), 'port': '0x7662', 'id': '0xfa14a71f42599c6574640977b80e0fc1116e5044b8a86cd92b0b193b1c1cc66ded1c10c988db83ae01941c3ada28d5effec9f99c470290017aea848b0cd25610', 'protocols': AttributeDict({'eth': AttributeDict({'difficulty': '0x187', 'head': '0xd080a3426bd1f62a17bb758a599eee5275a13b5fc3d8aba874c75f284a8a5406', 'version': 69})}), 'enode': 'enode://fa14a71f42599c6574640977b80e0fc1116e5044b8a86cd92b0b193b1c1cc66ded1c10c988db83ae01941c3ada28d5effec9f99c470290017aea848b0cd25610@127.0.0.1:30306'}), AttributeDict({'version': '0x5', 'name': 'besu/dealer/v25.7.0/linux-x86_64/openjdk-java-21', 'caps': ['eth/66', 'eth/67', 'eth/68', 'eth/69', 'snap/1'], 'network': AttributeDict({'localAddress': '127.0.0.1:56606', 'remoteAddress': '127.0.0.1:30305', 'inbound': False}), 'port': '0x7661', 'id': '0x4f320374381cf02087afa4b52b1d8f98f01d696814b4ce26c027bfc56247f09c57d25e5924f4189071e002732938db11f8341edcdda1ec7c0536bf7bb3c8f813', 'protocols': AttributeDict({'eth': AttributeDict({'difficulty': '0x187', 'head': '0xd080a3426bd1f62a17bb758a599eee5275a13b5fc3d8aba874c75f284a8a5406', 'version': 69})}), 'enode': 'enode://4f320374381cf02087afa4b52b1d8f98f01d696814b4ce26c027bfc56247f09c57d25e5924f4189071e002732938db11f8341edcdda1ec7c0536bf7bb3c8f813@127.0.0.1:30305'}), AttributeDict({'version': '0x5', 'name': 'besu/buyer/v25.7.0/linux-x86_64/openjdk-java-21', 'caps': ['eth/66', 'eth/67', 'eth/68', 'eth/69', 'snap/1'], 'network': AttributeDict({'localAddress': '127.0.0.1:30303', 'remoteAddress': '127.0.0.1:51698', 'inbound': True}), 'port': '0x7660', 'id': '0x16ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da442fdcc829163ccf521ae661d6ba74e63f4369318cbf69201a8152e82f8b57495e', 'protocols': AttributeDict({'eth': AttributeDict({'difficulty': '0x187', 'head': '0xd080a3426bd1f62a17bb758a599eee5275a13b5fc3d8aba874c75f284a8a5406', 'version': 69})}), 'enode': 'enode://16ea033659ac96ec08b16202c4994c991a33e7dab6aba6c4abf801a81a84da442fdcc829163ccf521ae661d6ba74e63f4369318cbf69201a8152e82f8b57495e@127.0.0.1:30304?discport=0'})]

To retrieve the details of a block by the block number 0 (zero), execute the following command at the Python interpreter prompt:


>>> json.dumps(json.loads(w3.to_json(w3.eth.get_block(0))), indent=2)


The following would be the typical output:


Output.14

{
  "number": 0,
  "hash": "0x9fe2eac5405ea5af672e3d6843c40f2a925280e08dabc90099559d9e42c2dc56",
  "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "nonce": "0x0000000000000000",
  "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  "stateRoot": "0x550dcc5cabd0941cfcf95ba88e9e36eea4fbd08b33dfac815fd995ab6c4dc971",
  "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  "miner": "0x0000000000000000000000000000000000000000",
  "difficulty": 1,
  "totalDifficulty": 1,
  "proofOfAuthorityData": "0x00000000000000000000000000000000000000000000000000000000000000004ed5d88f55c4715298d577d17d3ffc57bf3dc9810000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "size": 626,
  "gasLimit": 10485760,
  "gasUsed": 0,
  "timestamp": 1548854791,
  "uncles": [],
  "transactions": []
}

To display account balance for the buyer account, execute the following command at the Python interpreter prompt:


>>> w3.eth.getBalance(Web3.to_checksum_address('0x5a4240acd295d673c13e197b99d0fa70bbd64ca6'))


The following would be the typical output:


Output.15

10000000000000000000

*** WARNING ***

If we do not use Web3.to_checksum_address on the address to get the account balance, we will encounter an error

Similarly, to display account balance for the dealer account, execute the following command at the Python interpreter prompt:


>>> w3.eth.getBalance(Web3.to_checksum_address('0x3b66024245bc6b4c4f183d203170b6fa78754948'))


The following would be the typical output:


Output.16

5000000000000000000

We will now send a transaction to transfer 1 ether from the buyer to the dealer. For this we will need to create a send transaction message. Enter the following data at the Python interpreter prompt to create the message structure called buyer_dealer_txn:


>>> buyer_dealer_txn = {

... 'from': Web3.to_checksum_address('0x5a4240acd295d673c13e197b99d0fa70bbd64ca6'),

... 'to': Web3.to_checksum_address('0x3b66024245bc6b4c4f183d203170b6fa78754948'),

... 'value': w3.to_wei(1, 'ether'),

... 'gas': 90000,

... 'gasPrice': 18000000000,

... 'nonce': 0,

... 'chainId': 21

...}


Before sending the transaction message buyer_dealer_txn, it needs to be digitally signed using the private key of the buyer. To do that, execute the following commands at the Python interpreter prompt:

>>> buyer_priv_key = '0x1680ff9e0ad288d507d04b569ea2d1aa9e5e3b58458e8a5dea7df12d2c41a1b9'

>>> signed_txn = w3.eth.account.sign_transaction(buyer_dealer_txn, buyer_priv_key)

To send the signed transaction to transfer 1 ether from the buyer to the dealer, execute the following command at the Python interpreter prompt:


>>> w3.eth.send_raw_transaction(signed_txn.raw_transaction)


The following would be the typical output:


Output.17

HexBytes('0x0be38c14e1e23665970c115517d1c318ed3ab5b0395992ada7d656d1e5485544')

The w3.eth.send_raw_transaction method will return a transaction hash as a hex string (similar to a transaction id).

To display all the details of a transaction on this private network, execute the following command at the Python interpreter prompt:


>>> json.dumps(json.loads(w3.to_json(w3.eth.get_transaction('0x0be38c14e1e23665970c115517d1c318ed3ab5b0395992ada7d656d1e5485544'))), indent=2)


The following would be the typical output:


Output.18

{
  "blockHash": "0x4a8d86a720be419cfa2be08c4fdd7a299c0c85f1e18d239d8d7a2202f4f101aa",
  "blockNumber": 224,
  "chainId": 21,
  "from": "0x5A4240ACD295D673C13E197B99D0FA70bBd64ca6",
  "gas": 90000,
  "gasPrice": 18000000000,
  "hash": "0x7141a3260cf678c3e6c2d723cbaf163b31a6a141d346c88dc8f61a74e54f3afa",
  "input": "0x",
  "nonce": 0,
  "to": "0x3B66024245bC6b4C4f183d203170B6Fa78754948",
  "transactionIndex": 0,
  "type": 0,
  "value": 1000000000000000000,
  "v": 78,
  "r": "0x63d9c66ae4a2a97a88cbdc846c0f3f18ac56ab4fe4dd45556f246639a623dd71",
  "s": "0x314290da53e71a8d6ad118067f25e33f1c6d6a821cfaa2ac83e2395e018f7b24"
}

From the Output.18 above, we see that transaction is processed in block number 224.

To display account balance for the buyer account, execute the following command at the Python interpreter prompt:


>>> w3.from_wei(w3.eth.get_balance(Web3.to_checksum_address('0x5a4240acd295d673c13e197b99d0fa70bbd64ca6')), 'ether')


The following would be the typical output:


Output.19

8.999622

Similarly, to display account balance for the dealer account, execute the following command at the Python interpreter prompt:


>>> w3.from_wei(w3.eth.get_balance(Web3.to_checksum_address('0x3b66024245bc6b4c4f183d203170b6fa78754948')), 'ether')


The following would be the typical output:


Output.20

6

As is evident from the Output.19 and Output.20 above, the account balance for the buyer has decreased, while the account balance for the dealer has increased.

BINGO !!! We have successfully demonstrated a 4-node Hyperledger Besu based private Ethereum network using Docker.


References

Hyperledger Besu Enterprise Ethereum Client

Hyperledger Besu Command Line Options

Web3.py Documentation

Introduction to Ethereum - Part 1



© PolarSPARC