Multi-Node Private Ethereum Network using Docker


Bhaskar S *UPDATED*10/21/2022


Overview

In Introduction to Ethereum - Part 1, we demonstrated the simple vehicle buying use-case (involving the buyer, the dealer, and the dmv) using a single node on a private Ethereum network.

In reality, if one were to setup a private Ethereum network in any Enterprise, it would involve more than one node in the network.

In this article, we will lay out the steps to setup a multi-node private Ethereum network (one for each of the entities - the bank, the buyer, the dealer, and the dmv).

Installation

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

Ensure Docker is installed and setup. Else, refer to the article Introduction to Docker.

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

$ mkdir -p Ethereum/conf

$ mkdir -p Ethereum/data/bank

$ mkdir -p Ethereum/data/buyer

$ mkdir -p Ethereum/data/dealer

$ mkdir -p Ethereum/data/dmv

Now, change the current working directory to the directory /home/polarsparc/Ethereum.

For our exploration, we will be downloading and using the official docker image ethereum/client-go:alltools-v1.10.25 for Ethereum Go Client.

To pull and download the docker image for Ethereum Go Client, execute the following command:

docker pull ethereum/client-go:alltools-v1.10.25

The following should be the typical output:

Output.1

alltools-v1.10.25: Pulling from ethereum/client-go
213ec9aee27d: Pull complete 
2963a9ae9217: Pull complete 
811f758345ec: Pull complete 
Digest: sha256:22a9874c0cf02914693023ec5fe5fb50a9861d13fd213ffa6492302454484554
Status: Downloaded newer image for ethereum/client-go:alltools-v1.10.25
docker.io/ethereum/client-go:alltools-v1.10.25

To check everything was ok with the Ethereum Go Client docker image, execute the following command:

$ docker run --rm --name geth ethereum/client-go:alltools-v1.10.25 geth version

The following would be the typical output:

Output.2

Geth
Version: 1.10.25-stable
Git Commit: 69568c554880b3567bace64f8848ff1be27d084d
Architecture: amd64
Go Version: go1.18.6
Operating System: linux
GOPATH=
GOROOT=go

Multi-Node Setup

For this multi-node setup, we will create 4 accounts, one for each of the entities - the bank, the buyer, the dealer, and the dmv. Before we proceed further, we will create text files (.txt) for each of the entities (bank, buyer, dealer, and dmv), in their respective directories, containing their account password. To create the password text files, execute the following commands:

$ echo 'bank' data/bank/bank.txt

$ echo 'buyer' data/buyer/buyer.txt

$ echo 'dealer' data/dealer/dealer.txt

$ echo 'dmv' data/dmv/dmv.txt

The next step in a multi-node private network setup, is to create accounts corresponding to each of the peers, namely, the bank, the buyer, the dealer, and the dmv.

To create an account for the bank entity, execute the following command:

$ docker run -it --rm --name bank -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --password /root/data/bank/bank.txt --datadir /root/data/bank account new

The following would be the typical output:

Output.3

INFO [10-20|22:23:14.050] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:23:14.050] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"

Your new key was generated

Public address of the key:   0xc95eD0386316E2A985B8ad11178eb83757999D3b
Path of the secret key file: /root/data/bank/keystore/UTC--2022-10-20T22-23-14.050914931Z--c95ed0386316e2a985b8ad11178eb83757999d3b

- You can share your public address with anyone. Others need it to interact with you.
- You must NEVER share the secret key with anyone! The key controls access to your funds!
- You must BACKUP your key file! Without the key, it's impossible to access account funds!
- You must REMEMBER your password! Without the password, it's impossible to decrypt the key!

The following is the repetition of the above command to create the remaining 3 accounts for our setup.

Account buyer:

$ docker run -it --rm --name buyer -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --password /root/data/buyer/buyer.txt --datadir /root/data/buyer account new

Account buyer output:

Output.4

INFO [10-20|22:23:39.943] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:23:39.944] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"

Your new key was generated

Public address of the key:   0x694D784017852fd6e84a8EbD27cA2162966C40c4
Path of the secret key file: /root/data/buyer/keystore/UTC--2022-10-20T22-23-39.944789223Z--694d784017852fd6e84a8ebd27ca2162966c40c4

- You can share your public address with anyone. Others need it to interact with you.
- You must NEVER share the secret key with anyone! The key controls access to your funds!
- You must BACKUP your key file! Without the key, it's impossible to access account funds!
- You must REMEMBER your password! Without the password, it's impossible to decrypt the key!

Account dealer:

$ docker run -it --rm --name dealer -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --password /root/data/dealer/dealer.txt --datadir /root/data/dealer account new

Account dealer output:

Output.5

INFO [10-20|22:24:07.580] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:24:07.581] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"

Your new key was generated

Public address of the key:   0xad7430C568AF808D785B7E0B6BAB057cf7EE3e24
Path of the secret key file: /root/data/dealer/keystore/UTC--2022-10-20T22-24-07.581815941Z--ad7430c568af808d785b7e0b6bab057cf7ee3e24

- You can share your public address with anyone. Others need it to interact with you.
- You must NEVER share the secret key with anyone! The key controls access to your funds!
- You must BACKUP your key file! Without the key, it's impossible to access account funds!
- You must REMEMBER your password! Without the password, it's impossible to decrypt the key!

Account dmv:

$ docker run -it --rm --name dmv -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --password /root/data/dmv/dmv.txt --datadir /root/data/dmv account new

Account dmv output:

Output.6

INFO [10-20|22:24:28.986] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:24:28.986] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"

Your new key was generated

Public address of the key:   0x1cF2Da205D16f1d9cf485e46ADEd945E841946fa
Path of the secret key file: /root/data/dmv/keystore/UTC--2022-10-20T22-24-28.986842822Z--1cf2da205d16f1d9cf485e46aded945e841946fa

- You can share your public address with anyone. Others need it to interact with you.
- You must NEVER share the secret key with anyone! The key controls access to your funds!
- You must BACKUP your key file! Without the key, it's impossible to access account funds!
- You must REMEMBER your password! Without the password, it's impossible to decrypt the key!

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 create an initialization file called multi-genesis.json that will be located in the directory conf, with the following contents:


multi-genesis.json
{
  "config": {
    "chainId": 21,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0,
    "berlinBlock": 0,
    "clique": {
      "period": 10,
      "epoch": 30000
    }
  },
  "difficulty": "0x1",
  "gasLimit": "0xa00000",
  "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000c95ed0386316e2a985b8ad11178eb83757999d3b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "alloc": {
    "c95ed0386316e2a985b8ad11178eb83757999d3b": { "balance": "20000000000000000000" },
    "694d784017852fd6e84a8ebd27ca2162966c40c4": { "balance": "10000000000000000000" },
    "ad7430c568af808d785b7e0b6bab057cf7ee3e24": { "balance": "5000000000000000000" },
    "1cf2da205d16f1d9cf485e46aded945e841946fa": { "balance": "3000000000000000000" }
  }
}

To initialize our private Ethereum network with the Genesis block, execute the following command for the bank node:

$ docker run -it --rm --name bank -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --datadir /root/data/bank init /root/conf/multi-genesis.json

The following would be the typical output:

Output.7

INFO [10-20|22:26:09.709] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:26:09.710] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [10-20|22:26:09.713] Set global gas cap                       cap=50,000,000
INFO [10-20|22:26:09.715] Allocated cache and file handles         database=/root/data/bank/geth/chaindata cache=16.00MiB handles=16
INFO [10-20|22:26:09.732] Opened ancient database                  database=/root/data/bank/geth/chaindata/ancient/chain readonly=false
INFO [10-20|22:26:09.732] Writing custom genesis block 
INFO [10-20|22:26:09.732] Persisted trie from memory database      nodes=5 size=764.00B time="52.399µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:09.733] Successfully wrote genesis state         database=chaindata                      hash=0ba7a6..9957ed
INFO [10-20|22:26:09.733] Allocated cache and file handles         database=/root/data/bank/geth/lightchaindata cache=16.00MiB handles=16
INFO [10-20|22:26:09.749] Opened ancient database                  database=/root/data/bank/geth/lightchaindata/ancient/chain readonly=false
INFO [10-20|22:26:09.749] Writing custom genesis block 
INFO [10-20|22:26:09.749] Persisted trie from memory database      nodes=5 size=764.00B time="46.037µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:09.750] Successfully wrote genesis state         database=lightchaindata                      hash=0ba7a6..9957ed

Repeat the genesis block initialization for each of the remaining nodes buyer, dealer, and dmv as shown below:

For account buyer:

$ docker run -it --rm --name buyer -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --datadir /root/data/buyer init /root/conf/multi-genesis.json

The following would be the typical output:

Output.8

INFO [10-20|22:26:31.418] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:26:31.421] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [10-20|22:26:31.428] Set global gas cap                       cap=50,000,000
INFO [10-20|22:26:31.431] Allocated cache and file handles         database=/root/data/buyer/geth/chaindata cache=16.00MiB handles=16
INFO [10-20|22:26:31.448] Opened ancient database                  database=/root/data/buyer/geth/chaindata/ancient/chain readonly=false
INFO [10-20|22:26:31.449] Writing custom genesis block 
INFO [10-20|22:26:31.449] Persisted trie from memory database      nodes=5 size=764.00B time="52.008µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:31.450] Successfully wrote genesis state         database=chaindata                       hash=0ba7a6..9957ed
INFO [10-20|22:26:31.450] Allocated cache and file handles         database=/root/data/buyer/geth/lightchaindata cache=16.00MiB handles=16
INFO [10-20|22:26:31.473] Opened ancient database                  database=/root/data/buyer/geth/lightchaindata/ancient/chain readonly=false
INFO [10-20|22:26:31.473] Writing custom genesis block 
INFO [10-20|22:26:31.474] Persisted trie from memory database      nodes=5 size=764.00B time="81.414µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:31.474] Successfully wrote genesis state         database=lightchaindata                       hash=0ba7a6..9957ed

For account dealer:

$ docker run -it --rm --name dealer -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --datadir /root/data/dealer init /root/conf/multi-genesis.json

The following would be the typical output:

Output.9

INFO [10-20|22:26:51.111] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:26:51.114] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [10-20|22:26:51.121] Set global gas cap                       cap=50,000,000
INFO [10-20|22:26:51.125] Allocated cache and file handles         database=/root/data/dealer/geth/chaindata cache=16.00MiB handles=16
INFO [10-20|22:26:51.143] Opened ancient database                  database=/root/data/dealer/geth/chaindata/ancient/chain readonly=false
INFO [10-20|22:26:51.143] Writing custom genesis block 
INFO [10-20|22:26:51.144] Persisted trie from memory database      nodes=5 size=764.00B time="46.557µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:51.144] Successfully wrote genesis state         database=chaindata                        hash=0ba7a6..9957ed
INFO [10-20|22:26:51.144] Allocated cache and file handles         database=/root/data/dealer/geth/lightchaindata cache=16.00MiB handles=16
INFO [10-20|22:26:51.159] Opened ancient database                  database=/root/data/dealer/geth/lightchaindata/ancient/chain readonly=false
INFO [10-20|22:26:51.159] Writing custom genesis block 
INFO [10-20|22:26:51.160] Persisted trie from memory database      nodes=5 size=764.00B time="425.713µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:26:51.161] Successfully wrote genesis state         database=lightchaindata                        hash=0ba7a6..9957ed

For account dmv:

$ docker run -it --rm --name dmv -u $(id -u $USER):$(id -g $USER) -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --datadir /root/data/dmv init /root/conf/multi-genesis.json

The following would be the typical output:

Output.10

INFO [10-20|22:27:14.874] Maximum peer count                       ETH=50 LES=0 total=50
INFO [10-20|22:27:14.876] Smartcard socket not found, disabling    err="stat /run/pcscd/pcscd.comm: no such file or directory"
INFO [10-20|22:27:14.880] Set global gas cap                       cap=50,000,000
INFO [10-20|22:27:14.881] Allocated cache and file handles         database=/root/data/dmv/geth/chaindata cache=16.00MiB handles=16
INFO [10-20|22:27:14.900] Opened ancient database                  database=/root/data/dmv/geth/chaindata/ancient/chain readonly=false
INFO [10-20|22:27:14.900] Writing custom genesis block 
INFO [10-20|22:27:14.901] Persisted trie from memory database      nodes=5 size=764.00B time="54.593µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:27:14.901] Successfully wrote genesis state         database=chaindata                     hash=0ba7a6..9957ed
INFO [10-20|22:27:14.901] Allocated cache and file handles         database=/root/data/dmv/geth/lightchaindata cache=16.00MiB handles=16
INFO [10-20|22:27:14.916] Opened ancient database                  database=/root/data/dmv/geth/lightchaindata/ancient/chain readonly=false
INFO [10-20|22:27:14.917] Writing custom genesis block 
INFO [10-20|22:27:14.918] Persisted trie from memory database      nodes=5 size=764.00B time="311.758µs" gcnodes=0 gcsize=0.00B gctime=0s livenodes=1 livesize=0.00B
INFO [10-20|22:27:14.919] Successfully wrote genesis state         database=lightchaindata                     hash=0ba7a6..9957ed

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 terminal 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 terminal windows, change the working directory to /home/polarsparc/Ethereum.

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

$ docker run -it --rm --name bank -u $(id -u $USER):$(id -g $USER) -p 8081:8081 -p 30001:30001 -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --networkid "21" --identity "bank" --datadir /root/data/bank --syncmode "full" --ipcdisable --nodiscover --allow-insecure-unlock --unlock 0x2929cc22c9203107509392ab77da1bc17f652c3c --password /root/data/bank/bank.txt --http --http.addr "0.0.0.0" --http.port 8081 --http.corsdomain "*" --port 30001 --verbosity 2 console

Notice the use of the the option password pointing to the text file containing the account password and the option unlock specifying the index of the account.

The --http* related options enable API access via the bank node.

The following would be the typical output:

Output.11

WARN [10-20|22:30:51.895] Failed to load snapshot, regenerating    err="missing or corrupted snapshot"
WARN [10-20|22:30:51.896] Error reading unclean shutdown markers   error="leveldb: not found"
WARN [10-20|22:30:51.896] Engine API enabled                       protocol=eth
WARN [10-20|22:30:51.896] Engine API started but chain not configured for merge yet 
Welcome to the Geth JavaScript console!

instance: Geth/bank/v1.10.25-stable-69568c55/linux-amd64/go1.18.6
coinbase: 0xc95ed0386316e2a985b8ad11178eb83757999d3b
at block: 0 (Thu Jan 01 1970 00:00:00 GMT+0000 (UTC))
  datadir: /root/data/bank
  modules: admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

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

$ docker run -it --rm --name buyer -u $(id -u $USER):$(id -g $USER) -p 30002:30002 -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --networkid "21" --identity "buyer" --datadir /root/data/buyer --syncmode "full" --ipcdisable --nodiscover --unlock 0x9a4f8e15ec721adbdf8af75be091279dff159523 --password /root/data/buyer/buyer.txt --port 30002 --verbosity 2 console

The following would be the typical output:

Output.12

WARN [10-20|22:31:35.288] Failed to load snapshot, regenerating    err="missing or corrupted snapshot"
WARN [10-20|22:31:35.289] Error reading unclean shutdown markers   error="leveldb: not found"
WARN [10-20|22:31:35.289] Engine API enabled                       protocol=eth
WARN [10-20|22:31:35.289] Engine API started but chain not configured for merge yet 
Welcome to the Geth JavaScript console!

instance: Geth/buyer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6
coinbase: 0x694d784017852fd6e84a8ebd27ca2162966c40c4
at block: 0 (Thu Jan 01 1970 00:00:00 GMT+0000 (UTC))
  datadir: /root/data/buyer
  modules: admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

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

$ docker run -it --rm --name dealer -u $(id -u $USER):$(id -g $USER) -p 30003:30003 -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --networkid "21" --identity "dealer" --datadir /root/data/dealer --syncmode "full" --ipcdisable --nodiscover --unlock 0xfd23c931300c331343696fc1df24cd99f4cf3191 --password /root/data/dealer/dealer.txt --port 30003 --verbosity 2 console

The following would be the typical output:

Output.13

WARN [10-20|22:33:06.202] Failed to load snapshot, regenerating    err="missing or corrupted snapshot"
WARN [10-20|22:33:06.203] Error reading unclean shutdown markers   error="leveldb: not found"
WARN [10-20|22:33:06.203] Engine API enabled                       protocol=eth
WARN [10-20|22:33:06.203] Engine API started but chain not configured for merge yet 
Welcome to the Geth JavaScript console!

instance: Geth/dealer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6
coinbase: 0xad7430c568af808d785b7e0b6bab057cf7ee3e24
at block: 0 (Thu Jan 01 1970 00:00:00 GMT+0000 (UTC))
  datadir: /root/data/dealer
  modules: admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

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

$ docker run -it --rm --name dmv -u $(id -u $USER):$(id -g $USER) -p 30004:30004 -v $HOME/Ethereum:/root ethereum/client-go:alltools-v1.10.25 geth --networkid "21" --identity "dmv" --datadir /root/data/dmv --syncmode "full" --ipcdisable --nodiscover --unlock 0x8703b39acc4cad0cb9d0f7a5157ed85499a97486 --password /root/data/dmv/dmv.txt --port 30004 --verbosity 2 console

The following would be the typical output:

Output.14

WARN [10-20|22:33:32.364] Failed to load snapshot, regenerating    err="missing or corrupted snapshot"
WARN [10-20|22:33:32.365] Error reading unclean shutdown markers   error="leveldb: not found"
WARN [10-20|22:33:32.365] Engine API enabled                       protocol=eth
WARN [10-20|22:33:32.365] Engine API started but chain not configured for merge yet 
Welcome to the Geth JavaScript console!

instance: Geth/dmv/v1.10.25-stable-69568c55/linux-amd64/go1.18.6
coinbase: 0x1cf2da205d16f1d9cf485e46aded945e841946fa
at block: 0 (Thu Jan 01 1970 00:00:00 GMT+0000 (UTC))
  datadir: /root/data/dmv
  modules: admin:1.0 clique:1.0 debug:1.0 engine:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d or type exit
>

!!! ATTENTION !!!

The values for the port has to be unique for every Ethereum node in the private network.

Every node in the private Ethereum network has a unique endpoint address. Note that we have started each of the 4 nodes with the nodiscover option. This means the 4 nodes in our private network have no knowledge of each other. We need to manually connect the nodes to each other so they are aware of their peers in our private network. In order to achieve this, we need the endpoint address for the nodes bank, buyer, and dealer.

For our private network, we will add the endpoint address of the bank as a peer to the 3 nodes buyer, dealer, and dmv.

Next, we will add the endpoint address of the buyer as a peer to the nodes dealer and dmv.

Finally, we will add the endpoint address of the dealer as a peer to the node dmv .

The end result is a private network with nodes as shown in the illustration below:

Private Network
Our Private Network

Note that the node bank is also the miner (the transaction authorizer).

To identify the endpoint address of the bank, execute the following command in the Javascript prompt of the bank terminal:

> admin.nodeInfo

The following would be the typical output:

Output.15

{
  enode: "enode://0fad6ce97abfb06faaec281068316c1cfe5301fe617a36e63c217700c8335a88003a8d5e4bc28245b12ab28d0a9898c68b8f66dd7d9644d2b04063021b885428@127.0.0.1:30001?discport=0",
  enr: "enr:-Jy4QI6Hghb5cPy_-AhIt_8gwYl1sv0VQWkwImp_kOmAYZnNQjTZCksluJR9G84la9cq7PaSS8ZjfWgZNFBOnCRTgqmGAYP3hKtqg2V0aMfGhEvaIuaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQIPrWzper-wb6rsKBBoMWwc_lMB_mF6NuY8IXcAyDNaiIRzbmFwwIN0Y3CCdTE",
  id: "1c3aa3c637a4af6b9cfaf0cf0903f54295e8b4a6ddd0ffdba7f317deb2c09f41",
  ip: "127.0.0.1",
  listenAddr: "[::]:30001",
  name: "Geth/bank/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
  ports: {
    discovery: 0,
    listener: 30001
  },
  protocols: {
    eth: {
      config: {
        berlinBlock: 0,
        byzantiumBlock: 0,
        chainId: 21,
        clique: {...},
        constantinopleBlock: 0,
        eip150Block: 0,
        eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        eip155Block: 0,
        eip158Block: 0,
        homesteadBlock: 0,
        istanbulBlock: 0,
        petersburgBlock: 0
      },
      difficulty: 1,
      genesis: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      network: 21
    },
    snap: {}
  }
}

The trimmed string value enode://0fad6ce...@127.0.0.1:30001?discport=0 is the endpoint address for the node bank.

Next, to identify the endpoint address of the buyer, execute the following command in the Javascript promt of the buyer terminal:

> admin.nodeInfo

The following would be the typical output:

Output.16

{
  enode: "enode://13c5e8704608292825030e7d9fb4f0ef283fd533c7177a8d43d3d2415f93ecb7d350ea59998618adb76d1732682218b4b5226cf1875a95ebfdc287ef1eb83b98@127.0.0.1:30002?discport=0",
  enr: "enr:-Jy4QNE096uFK8lTZk4If-9a1bX9Hw6Oen4eihAi_127LFxzCoPWgryv-dfyDQxZJXnLl5Pw_HQdmILj-zNoL-krTliGAYP3hfaBg2V0aMfGhEvaIuaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQITxehwRggpKCUDDn2ftPDvKD_VM8cXeo1D09JBX5Pst4RzbmFwwIN0Y3CCdTI",
  id: "8512ec46f1a6d302cc9af09e7117235909d3f8d25d5d830915ade41aa2899a16",
  ip: "127.0.0.1",
  listenAddr: "[::]:30002",
  name: "Geth/buyer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
  ports: {
    discovery: 0,
    listener: 30002
  },
  protocols: {
    eth: {
      config: {
        berlinBlock: 0,
        byzantiumBlock: 0,
        chainId: 21,
        clique: {...},
        constantinopleBlock: 0,
        eip150Block: 0,
        eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        eip155Block: 0,
        eip158Block: 0,
        homesteadBlock: 0,
        istanbulBlock: 0,
        petersburgBlock: 0
      },
      difficulty: 1,
      genesis: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      network: 21
    },
    snap: {}
  }
}

The trimmed string value enode://13c5e87...@127.0.0.1:30002?discport=0 is the endpoint address for the node buyer.

Finally, to identify the endpoint address of the dealer, execute the following command in the Javascript promt of the dealer terminal:

> admin.nodeInfo

The following would be the typical output:

Output.17

{
  enode: "enode://8cd44ef8f159ce3d267220476b376194fcec9b5adcdcbf4d5001c553ab7ddd4f73218da6494754f054ed6ccf21c20858b6b87bc1fb47726349e24fa7ede1ff52@127.0.0.1:30003?discport=0",
  enr: "enr:-Jy4QKjw0Fv2jTEfuGgBsSCPkU-oXI1dhEqQoYrN62RT5wj2EDggY0ZSGBFnTTahq_UqbGs3nb15qoH7iCFYzSR_X0aGAYP3h1mjg2V0aMfGhEvaIuaAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKM1E748VnOPSZyIEdrN2GU_OybWtzcv01QAcVTq33dT4RzbmFwwIN0Y3CCdTM",
  id: "fa0c135282b8b1b30c65863b98db67c98a1473d02ac262712a7627a3db9d07e9",
  ip: "127.0.0.1",
  listenAddr: "[::]:30003",
  name: "Geth/dealer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
  ports: {
    discovery: 0,
    listener: 30003
  },
  protocols: {
    eth: {
      config: {
        berlinBlock: 0,
        byzantiumBlock: 0,
        chainId: 21,
        clique: {...},
        constantinopleBlock: 0,
        eip150Block: 0,
        eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
        eip155Block: 0,
        eip158Block: 0,
        homesteadBlock: 0,
        istanbulBlock: 0,
        petersburgBlock: 0
      },
      difficulty: 1,
      genesis: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
      network: 21
    },
    snap: {}
  }
}

The trimmed string value enode://8cd44e...@127.0.0.1:30003?discport=0 is the endpoint address for the node dealer.

We also need the IP address of the Docker containers running the bank, buyer, and dealer nodes.

Open a new terminal window and execute the following command:

$ docker ps -a

The following would be the typical output:

Output.18

CONTAINER ID   IMAGE                                  COMMAND                  CREATED              STATUS              PORTS                                                                                                                           NAMES
4c9c4a78c564   ethereum/client-go:alltools-v1.10.25   "geth --networkid 21…"   About a minute ago   Up About a minute   8545-8546/tcp, 30303/tcp, 0.0.0.0:30004->30004/tcp, :::30004->30004/tcp, 30303/udp                                              dmv
54a2aaf5950a   ethereum/client-go:alltools-v1.10.25   "geth --networkid 21…"   About a minute ago   Up About a minute   8545-8546/tcp, 30303/tcp, 0.0.0.0:30003->30003/tcp, :::30003->30003/tcp, 30303/udp                                              dealer
14b3eb39ccba   ethereum/client-go:alltools-v1.10.25   "geth --networkid 21…"   3 minutes ago        Up 3 minutes        8545-8546/tcp, 30303/tcp, 0.0.0.0:30002->30002/tcp, :::30002->30002/tcp, 30303/udp                                              buyer
ea96fe103568   ethereum/client-go:alltools-v1.10.25   "geth --networkid 21…"   4 minutes ago        Up 4 minutes        0.0.0.0:8081->8081/tcp, :::8081->8081/tcp, 8545-8546/tcp, 0.0.0.0:30001->30001/tcp, :::30001->30001/tcp, 30303/tcp, 30303/udp   bank

From the above Output.18, the Docker instances for the bank, the buyer, and the dealer nodes are ea96fe103568, 14b3eb39ccba, and 54a2aaf5950a respectively.

To extract the IP address of the bank node, execute the following command:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ea96fe103568

The following would be the typical output:

Output.19

172.17.0.2

Next, to extract the IP address of the buyer node, execute the following command:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 14b3eb39ccba

The following would be the typical output:

Output.20

172.17.0.3

Finally, to extract the IP address of the dealer node, execute the following command:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 54a2aaf5950a

The following would be the typical output:

Output.21

172.17.0.4

From the above Output.19, Output.20, and Output.21, the IP address for the bank, the buyer, and the dealer nodes are 172.17.0.2, 172.17.0.3, and 172.17.0.4 respectively.

To add the endpoint address of the bank as a peer to the nodes buyer, dealer, and dmv, execute the following command in the Javascript prompt of the corresponding terminals buyer, dealer, and dmv:

> admin.addPeer('enode://0fad6ce97abfb06faaec281068316c1cfe5301fe617a36e63c217700c8335a88003a8d5e4bc28245b12ab28d0a9898c68b8f66dd7d9644d2b0 4063021b885428@172.17.0.2:30001')

The following would be the typical output:

Output.22

true

Next, add the endpoint address of the buyer as a peer to the nodes dealer and dmv by executing the following command in the Javascript prompt of the corresponding terminals dealer and dmv:

> admin.addPeer('enode://13c5e8704608292825030e7d9fb4f0ef283fd533c7177a8d43d3d2415f93ecb7d350ea59998618adb76d1732682218b4b5226cf1875a95ebfd c287ef1eb83b98@172.17.0.3:30002')

Finally, add the endpoint address of the dealer as a peer to the node dmv by executing the following command in the Javascript prompt of the corresponding terminal dmv:

> admin.addPeer('enode://8cd44ef8f159ce3d267220476b376194fcec9b5adcdcbf4d5001c553ab7ddd4f73218da6494754f054ed6ccf21c20858b6b87bc1fb47726349 e24fa7ede1ff52@172.17.0.4:30003')

To verify the peers connected to any of the nodes, execute the following command in the Javascript promt of the corresponding terminal:

> admin.peers

The following would be the typical output:

Output.23

[{
    caps: ["eth/66", "eth/67", "snap/1"],
    enode: "enode://0fad6ce97abfb06faaec281068316c1cfe5301fe617a36e63c217700c8335a88003a8d5e4bc28245b12ab28d0a9898c68b8f66dd7d9644d2b04063021b885428@172.17.0.2:30001",
    id: "1c3aa3c637a4af6b9cfaf0cf0903f54295e8b4a6ddd0ffdba7f317deb2c09f41",
    name: "Geth/bank/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
    network: {
      inbound: false,
      localAddress: "172.17.0.5:56192",
      remoteAddress: "172.17.0.2:30001",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
        version: 67
      },
      snap: {
        version: 1
      }
    }
}, {
    caps: ["eth/66", "eth/67", "snap/1"],
    enode: "enode://13c5e8704608292825030e7d9fb4f0ef283fd533c7177a8d43d3d2415f93ecb7d350ea59998618adb76d1732682218b4b5226cf1875a95ebfdc287ef1eb83b98@172.17.0.3:30002",
    id: "8512ec46f1a6d302cc9af09e7117235909d3f8d25d5d830915ade41aa2899a16",
    name: "Geth/buyer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
    network: {
      inbound: false,
      localAddress: "172.17.0.5:49414",
      remoteAddress: "172.17.0.3:30002",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
        version: 67
      },
      snap: {
        version: 1
      }
    }
}, {
    caps: ["eth/66", "eth/67", "snap/1"],
    enode: "enode://8cd44ef8f159ce3d267220476b376194fcec9b5adcdcbf4d5001c553ab7ddd4f73218da6494754f054ed6ccf21c20858b6b87bc1fb47726349e24fa7ede1ff52@172.17.0.4:30003",
    id: "fa0c135282b8b1b30c65863b98db67c98a1473d02ac262712a7627a3db9d07e9",
    name: "Geth/dealer/v1.10.25-stable-69568c55/linux-amd64/go1.18.6",
    network: {
      inbound: false,
      localAddress: "172.17.0.5:43204",
      remoteAddress: "172.17.0.4:30003",
      static: true,
      trusted: false
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0x0ba7a661645bed437de13cc78f2c7e1dddd0af6240a9f1f0e5dcc839469957ed",
        version: 67
      },
      snap: {
        version: 1
      }
    }
}]

The following is a custom Javascript utility function to display account balances (in ethers):


showBalances
function showBalances() {
  var i = 0;
  eth.accounts.forEach(function(e) {
     console.log("---> eth.accounts["+i+"]: " + e + " \tbalance: " + web3.fromWei(eth.getBalance(e), "ether") + " ether");
     i++;
  })
};

Paste the above code in the Javascript shell prompt of all the 4 terminals.

To display account balances using the Javascript utility function showBalances, execute the following command in the Javascript shell prompt of each of the 4 terminals:

> showBalances()

In the bank terminal, the following would be the typical output:

Output.24

---> eth.accounts[0]: 0xc95ed0386316e2a985b8ad11178eb83757999d3b 	balance: 20 ether
undefined

In the buyer terminal, the following would be the typical output:

Output.25

---> eth.accounts[0]: 0x694d784017852fd6e84a8ebd27ca2162966c40c4 	balance: 10 ether
undefined

In the dealer terminal, the following would be the typical output:

Output.26

---> eth.accounts[0]: 0xad7430c568af808d785b7e0b6bab057cf7ee3e24 	balance: 5 ether
undefined

Finally, in the dmv terminal, the following would be the typical output:

Output.27

---> eth.accounts[0]: 0x1cf2da205d16f1d9cf485e46aded945e841946fa 	balance: 3 ether
undefined

!!! ATTENTION !!!

It is *EXTREMELY IMPORTANT* that the miner is running before sending any transactions. Else, the transactions will be stuck in the pending state forever.

Given that the bank is the authorized signer (miner), start the miner (with one mining thread) by executing the following command in the Javascript shell prompt of the bank terminal:

> miner.start(1)

In the buyer terminal, assign the variables buyer and dealer with their corresponding account address using the Javascript shell as shown below:

> buyer = "0x694d784017852fd6e84a8ebd27ca2162966c40c4"

> dealer = "0xad7430c568af808d785b7e0b6bab057cf7ee3e24"

To send a transaction to transfer 1 ether from the buyer to the dealer, execute the following command in the Javascript shell prompt of the buyer terminal:

> eth.sendTransaction({from: buyer, to: dealer, value: web3.toWei(1, "ether")})

The following would be the typical output:

Output.28

"0xadb276931f378f2dfd21b5006e45a2e5b5f2ca99c964cc0bb9cd07bc7e261452"

To display all the pending transactions on this private network, execute the following command in the Javascript shell prompt of the buyer terminal:

> eth.pendingTransactions

The following would be the typical output:

Output.29

[{
    blockHash: null,
    blockNumber: null,
    chainId: "0x15",
    from: "0x694d784017852fd6e84a8ebd27ca2162966c40c4",
    gas: 21000,
    gasPrice: 1000000000,
    hash: "0xadb276931f378f2dfd21b5006e45a2e5b5f2ca99c964cc0bb9cd07bc7e261452",
    input: "0x",
    nonce: 0,
    r: "0x24c10bbb6453bd36be2b291d3b54c0128b9c232b8edbdf4bc5d49f8810ca14b3",
    s: "0x125bb21059c3dd271f7e6580105f8246a2dcb974d79041247c489683736b8570",
    to: "0xad7430c568af808d785b7e0b6bab057cf7ee3e24",
    transactionIndex: null,
    type: "0x0",
    v: "0x4d",
    value: 1000000000000000000
}]

Wait a few seconds and check the status of the sent transaction by executing the following command in the Javascript shell prompt of the buyer terminal:

> eth.pendingTransactions

The following would be the typical output:

Output.30

[]

To stop the miner, execute the following command in the Javascript shell prompt of the bank terminal:

> miner.stop()

Now, we check the account balances using the Javascript utility function showBalances, execute the following command in the Javascript shell prompt of each of the 4 terminals:

> showBalances()

In the bank terminal, the following would be the typical output:

Output.31

---> eth.accounts[0]: 0xc95ed0386316e2a985b8ad11178eb83757999d3b 	balance: 20.000021 ether
undefined

In the buyer terminal, the following would be the typical output:

Output.32

---> eth.accounts[0]: 0x694d784017852fd6e84a8ebd27ca2162966c40c4 	balance: 8.999979 ether
undefined

In the dealer terminal, the following would be the typical output:

Output.33

---> eth.accounts[0]: 0xad7430c568af808d785b7e0b6bab057cf7ee3e24 	balance: 6 ether
undefined

Finally, in the dmv terminal, the following would be the typical output:

Output.34

---> eth.accounts[0]: 0x1cf2da205d16f1d9cf485e46aded945e841946fa 	balance: 3 ether
undefined

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

Now, we will make a few API requests via the JSON-RPC HTTP interface using the curl command. Note that the bank exposes the JSON-RPC HTTP interface at the network endpoint localhost:8081.

In order to execute the curl commands, open a new terminal window.

To get the endpoint version, execute the following command:

$ curl --location --request POST 'localhost:8081/' \ --header 'Content-Type: application/json' \ --data-raw '{ "jsonrpc":"2.0", "method":"web3_clientVersion", "params":[], "id":1 }'

The following would be the typical output:

Output.35

{"jsonrpc":"2.0","id":1,"result":"Geth/bank/v1.10.25-stable-69568c55/linux-amd64/go1.18.6"}

To get the list of accounts, execute the following command:

$ curl --location --request POST 'localhost:8081/' \ --header 'Content-Type: application/json' \ --data-raw '{ "jsonrpc":"2.0", "method":"eth_accounts", "params":[], "id":1 }'

The following would be the typical output:

Output.36

{"jsonrpc":"2.0","id":1,"result":["0xc95ed0386316e2a985b8ad11178eb83757999d3b"]}

To get the balance of a specified account, execute the following command:

$ curl --location --request POST 'localhost:8081/' \ --header 'Content-Type: application/json' \ --data-raw '{ "jsonrpc":"2.0", "method":"eth_getBalance", "params":[ "0xc95ed0386316e2a985b8ad11178eb83757999d3b", "latest" ], "id":1 }'

The following would be the typical output:

Output.37

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

HOORAY !!! At this point we have successfully demonstrated a 4-node private Ethereum network using Docker.


References

Introduction to Ethereum - Part 1

Official Ethereum Developer Documentation

Ethereum Private Network

Command-line Options for the Ethereum Go (geth) client

Ethereum JSON-RPC Server

Introduction to Blockchain

Introduction to Docker