There are many tutorials online on this topic, but I found most of them not touching on the essence of the problem, and some of them are simply outdated. So while risking to be another outdated post, I want to share my experience of getting a three-node ETH network working (one is my laptop, one is AWS EC2, and the final one is a digital ocean instance).

Our network topology

We will setup a bootnode, which simply maintains the network peer list for us, without actual mining or verifying transactions. And we will have three clients, and one of them is a miner. They will first connect to bootnode, and then connect with each other. Note that one node can be a client node and a bootnode at the same time, as long as the ports don’t collide with each other (so in fact you can setup many nodes in localhost as well, but for testing purpose, I used three remote nodes).

Preparation

Configuration file: we need only a genesis.json template file beforehand. It specifies the genesis block, which determines the parameters of our ETH blockchain. We will fill it with preallocation parameters later. Note that only the initial chain generated from the same genesis.json would be compatible with each other.

Installation: see https://github.com/ethereum/go-ethereum/wiki/Building-Ethereum

Setup the bootnode

The bootnode does NOT depend on the genesis block or whatever. It can be compatible with all ETH clients. The only thing you have to do:

bootnode --genkey=boot.key

# run this in background, note the enode:// address it prints
bootnode --nodekey=boot.key 

Run clients

For each client, first create account locally using geth account new, which is essentially just a passphrase-protected asymmetrical key pair. The public key is the ETH address, and will compose the node address as well, while the private key is stored locally.

Assume that we create three accounts with addresses:

  1. 238dd521ad221b37cc176fa9f4bf88cf19fe39f1
  2. aebc7588345fc7963505dd6de9d12390980fc13d
  3. a5c77bd6319a5eaba9494acd90cac9712f9e15c9

Then, we will create a genesis.json which preallocates some ethers for these accounts for testing purpose, so we can conduct transactions from the very beginning:

{
    "config": {
        "chainId": 1337,
        "homesteadBlock": 0,
        "eip155Block": 0,
        "eip158Block": 0
    },
    "nonce": "0x0000000000000042",
    "timestamp": "0x00",
    "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "extraData": "0x00",
    "gasLimit": "0x8000000",
    "difficulty": "0x01",
    "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "coinbase": "0x238dd521ad221b37cc176fa9f4bf88cf19fe39f1",
    "alloc": {
      "0x238dd521ad221b37cc176fa9f4bf88cf19fe39f1" : {
        "balance" : "200000000000000000000000"
      },
      "0xaebc7588345fc7963505dd6de9d12390980fc13d" : {
        "balance" : "10000000000000000000"
      },
      "0xa5c77bd6319a5eaba9494acd90cac9712f9e15c9" : {
        "balance" : "20000000000000000000"
      }
   }
}

Then we initialize the chain data from the genesis.json. This is done locally and doesn’t depend on the bootnode:

geth --datadir .datadir init genesis.json

Note that we use a customized path for chaindata instead of the default ~/.ethereum, so as to avoid collision with the public net.

After that, we will connect the client with the bootnode, which in turn helps connect with more peer nodes.

geth --datadir .datadir --networkid 1000 --rpc --ipcpath .datadir/geth.ipc --bootnodes <ADDR> console

The <ADDR> is the address printed by bootnode daemon above, with [::] replaced by the node’s IP.

After you entered the console, you can start finding the peers using admin.peers, e.g.

> admin.peers
[{
    caps: ["eth/63"],
    id: "69ef2cbb9d381cb57e7978b17d062577950ed192152314ac17ab0bf7fe8e28c3e7bd13c7d220ff2380845b09fdd738967bf73e0f469408a9e8c6d5f01a4f7e7e",
    name: "Geth/v1.6.7-stable-ab5646c5/linux-amd64/go1.8.1",
    network: {
      localAddress: "45.55.18.182:30303",
      remoteAddress: "52.14.146.67:41032"
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0x5478e39ca8247b0d557254e2c6a84d2b0311370623f716909445d1f39e5045a4",
        version: 63
      }
    }
}, {
    caps: ["eth/63"],
    id: "f0999401f838d9d1b0776dd5781ea3ba5fa64537a527d872667784296585d79a3716ad1ccb8b0eae46b546587fcbef599a1804271ca40c8378527feb70b6b3f1",
    name: "Geth/v1.6.7-stable-ab5646c5/darwin-amd64/go1.9",
    network: {
      localAddress: "45.55.18.182:30303",
      remoteAddress: "60.10.118.236:50466"
    },
    protocols: {
      eth: {
        difficulty: 1,
        head: "0x5478e39ca8247b0d557254e2c6a84d2b0311370623f716909445d1f39e5045a4",
        version: 63
      }
    }
}]

Or querying balance e.g.:

> eth.getBalance("0xc8d11d64b09853c22ad9c917ecc6930164373e97")
2e+23

Or sending transactions e.g.:

> personal.unlockAccount(eth.coinbase, "password")
> eth.sendTransaction({from:eth.coinbase, to:eth.accounts[1], value: web3.toWei(0.05, "ether")})