How to Run a Private (Local) Ethereum Network with Go Ethereum

How to Run a Private (Local) Ethereum Network with Go Ethereum
Photo by DrawKit Illustrations / Unsplash

I've been learning more and more about Ethereum lately and I wanted to solidify some of my understandings by running an Ethereum node locally. I ran into several issues along the way due to a combination of less-than-helpful documentation and super outdated Stack Exchange and Medium posts. In this post I'll give an up-to-date (as of May 2022) tutorial on how to get started running a local, private Ethereum network using geth. This post will take a number of liberties and is not intended for any form of production use.

Special thanks to Pradeep for his article which helped me get a baseline understanding of many of the necessary steps to achieve this.

Getting Started

In order to simulate a more robust Ethereum network, we are going to spin up 1 miner node, 1 boot node, and 2 non-mining (peer) nodes.

This post will assume that geth is already installed locally. Install instructions for geth can be found here.

The first step is to create the necessary directory structure for the mining and peer nodes. Each node must use its own directory to store its data.

mkdir data/
mkdir data/node1
mkdir data/node2
mkdir data/miner

The next step is to create an Ethereum account on each node. The bootnode (discussed in the next section) does not require an account. For simplicity's sake, all of our accounts will share the same password: password.

echo "password" > password.txt
geth --datadir data/node1/ account new --password password.txt
geth --datadir data/node2/ account new --password password.txt
geth --datadir data/miner/ account new --password password.txt

Calling geth account new will output a bunch of information, including the public address of the account's key:

Your new key was generated

Public address of the key:   0xb509C72D257F316804dA59cfD1b33293Caf02B01

Make note of the public key generated for the node1 account, as it will be referenced in the genesis block configuration below.

The Genesis Block

The first block of every Ethereum-based blockchain is known as the genesis block. In the case of Ethereum mainnet, the genesis block is hard-coded into the geth source code. In the case of a private/custom geth network, the genesis block is configurable. Create a new file called genesis.json and insert the following:

  "config": {
    "chainId": 666,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "ethash": {}
  "difficulty": "1",
  "gasLimit": "8000000",
  "alloc": {
    "XXXXXXXXXXXXXXXXXX": { "balance": "100000000000000000000" }

The chainId parameter is important and should be set to a number that is not already in use by another blockchain. Chainlist can be referenced to see existing chains and their IDs. This example will use id 666.

The alloc parameter is used to pre-fill specific wallet addresses with an amount of Ether (set in wei). Use eth-converter to convert between Ether and wei, if necessary. This blockchain will start with 100 Ether in the node1 wallet generated earlier. Replace XXXXXXXXXXXXXXXXXX with your wallet address.

Save the changes to genesis.json. Now that the genesis block is configued, each of the nodes must be configured to use it:

geth --datadir data/node1/ init genesis.json
geth --datadir data/node2/ init genesis.json
geth --datadir data/miner/ init genesis.json

Every node in a blockchain must have the same gensis block in order to participate.

The Bootnode

An Ethereum bootnode is used to facilitate the connection of peers in an Ethereum network. It does not store any blockchain data and does not have any awareness of the state of the chain.

Bootnodes are identified by an enode. Enodes are derived from private key, so that key must first be generated before the bootnode can start. Open a terminal and run the following command which will generate the private key and start the bootnode on local port 30300:

$ bootnode -genkey boot.key
$ bootnode -nodekey boot.key -addr :30300

A long enode address will be output. Copy or make a note of this address, as it will be needed in the next section to configure the peer and miner nodes. The bootnode must remain running in the next sections in order for nodes to connect with each other.

Starting The Miner and Peers

The commands to start the miner and peer nodes for a private network are pretty long, as they require a lot of configuration.

Starting the First Peer

Open a separate terminal window and run the following in the same directory as earlier commands:

geth --datadir data/node1 --ipcdisable --port 30301 --bootnodes 'enode://XXXXXXXXXX@' --networkid 666 --http --http.addr 'localhost' --http.port 8101 --http.api 'admin,debug,eth,miner,net,personal,txpool,web3' --allow-insecure-unlock

Let's cover everything going on in this command:

  • --datadir indicates the data directory for the databases and keystore. Each node is required to have a unique data directory.
  • --ipcdisable disables the IPC-RPC server, which is not needed since all the nodes are running locally and geth does not need to communicate with any external processes.
  • --port 30301 specifies the network listening port. Each node is required to run on a unique port.
  • --bootnodes 'enode://XXXXXXXXXX@' configures a list of bootnodes. Replace the enode in this command with the one generated from the bootnode section above.
  • networkid 666 sets the networkid and should match the network id configured in the genesis block, otherwise the node will default to network 1.
  • --http --http.addr 'localhost' --http.port 8101 --http.api 'admin,debug,eth,miner,net,personal,txpool,web3' are all flags used to enable and configure the HTTP-RPC server, which is necessary to attach to this node in the next section.
  • --allow-insecure-unlock allows for insecure account unlocking when account-related RPCs are exposed by HTTP. This is necessary for sending a transaction in the next section, but should never be used in a production environment.

This command will output a lot of information and then start to hang as it awaits updates to the network and the blockchain. Leave this node running throughout the rest of the sections.

Starting the Second Peer

The second peer requires a similar command to the first, but does not require as many arguments since you will not need to attach to it later on. Open another separate terminal window and run the following:

geth --datadir data/node2 --ipcdisable --port 30302 --bootnodes 'enode://XXXXXXXXXX@' --networkid 666

Remember to replace the enode again with the enode of your bootnode, and to increment the --port argument by one.

After running the command above, the node1 and node2 peers should starting showing some Looking for peers logs:

INFO [05-16|15:38:13.076] Looking for peers                        peercount=0 tried=0 static=0
INFO [05-16|15:38:48.077] Looking for peers                        peercount=1 tried=1 static=0

This shows that the bootnode is working properly and connecting the nodes to eachother. The peercount here shows how many nodes that the current node is connected to.

Starting the Miner

Now that the two peer nodes are running, it's time to spin up the miner node and start mining some new blocks. Open another separate terminal window and run the following:

geth --datadir data/miner --ipcdisable --port 30303 --networkid 666 --bootnodes 'enode://af20e504f8b4c57c7572fe182d55868cc3fd6c4905d1fcb8607cd5c149ce6c21fbf863d4655ad4c6b168b38105a75556efbc601cbe1641cf89d7ee9301f3bd98@' --mine --miner.threads=1

This command is just like the commands used to start the peer nodes, but uses the --mine --miner.threads=1 flags to enable and configure mining.

Again, this command will produce a lot of output, and then it will begin quickly mining blocks. The mining output will look as such:

INFO [05-16|15:48:40.869] Successfully sealed new block            number=1485 sealhash=896e95..e90aa2 hash=3da9ce..a64b24 elapsed=4.822s
INFO [05-16|15:48:40.870] 🔗 block reached canonical chain          number=1478 hash=4c6419..071906
INFO [05-16|15:48:40.870] 🔨 mined potential block                  number=1485 hash=3da9ce..a64b24
INFO [05-16|15:48:40.870] Commit new sealing work                  number=1486 sealhash=1fbab6..1fc7e3 uncles=0 txs=0 gas=0 fees=0 elapsed="83.25µs"
INFO [05-16|15:48:40.870] Commit new sealing work                  number=1486 sealhash=1fbab6..1fc7e3 uncles=0 txs=0 gas=0 fees=0 elapsed="138.291µs"

Switch back to either of the peer node terminals and notice the new logs:

INFO [05-16|12:37:31.196] Imported new chain segment               blocks=1 txs=0 mgas=0.000 elapsed=3.21ms    mgasps=0.000 number=10 hash=28093f..6040a7 dirty=4.66KiB

These logs show the peer successfully syncing with the blockchain as new blocks are being mined. The txs=0 shows that new block did not contain any transactions.

Let's fix this by sending a transaction.

Sending a Transaction

Now that all the nodes are running and connected, it's time to test out one of the most important features of the blockchain: transactions. geth includes an interactive console which uses web3.js to allow for real-time interaction with the blockchain. In another new terminal window, run the following:

geth attach http://localhost:8101

This command will connect to a JavaScript REPL running on node1. You can then use various web3.js APIs to interact with the node and blockchain, as seen below:

// assign node1's only wallet to a convenience variable
> acc1 = personal.listAccounts[0]

// assign node2's only wallet to another variable
> acc2 = "0x209787cdd6f27ba51517cd87ec7f42370926782c"

// check the wallet's starting balance
> eth.getBalance(acc1)

// send a small amount of Ether from acc1 to acc2
> eth.sendTransaction({from: acc1, to: acc2, value: '5000000'})
Error: authentication needed: password or unlock
	at web3.js:6365:37(47)
	at send (web3.js:5099:62(35))
	at <eval>:1:20(10)

// unlock the wallet
> personal.unlockAccount(acc1, "password")

// send Ether again after unlocking the account
> tx1 = eth.sendTransaction({from: acc1, to: acc2, value: '5000000'})

// get details about the transaction
> eth.getTransaction(tx1)
  blockHash: "0xaff02931234dbad170535aafacd44ba43b361b40ce5e097a66b28bab237891a2",
  blockNumber: 118,
  from: "0xd7a4077d0cf40673cffe9afe871a270f07df457c",
  gas: 21000,
  gasPrice: 1000000000,
  hash: "0xbb0f034b69b3456cc514fae266f5d3a01ada59f5b9901c76a415f0c24d85c6a1",
  input: "0x",
  nonce: 0,
  r: "0x4580fc207b0c3b0d9a2441275ca0ba59065c3fabff525cb8da6d7c275bd819fa",
  s: "0x30ce54642cc23d25d7bc5b9c20fd725b82b09c2fcf35178f735532f9f333df46",
  to: "0x209787cdd6f27ba51517cd87ec7f42370926782c",
  transactionIndex: 0,
  type: "0x0",
  v: "0x558",
  value: 5000000

// observe that the node1 wallet balance has decreased
> eth.getBalance(acc1)

// observe that the node2 wallet balance has increased
> eth.getBalance(acc2)

After running the eth.sendTransaction command, switch to the terminal tab for the miner node and look for a log similar to this that shows the new block with the transaction from above in it (as indicated by txs=1):

INFO [05-16|17:06:37.390] Commit new sealing work                  number=118 sealhash=c25d39..bf70e9 uncles=0 txs=1 gas=21000 fees=2.1e-05 elapsed="268.667µs"

Switch back to the JavaScript REPL and observe more details about this particular block:

// get details about the specific block with the transaction
> eth.getBlockByNumber(118)
  difficulty: "0x21d9d",
  extraData: "0xd783010a11846765746886676f312e31388664617277696e",
  gasLimit: "0x7a1200",
  gasUsed: "0x5208",
  hash: "0xaff02931234dbad170535aafacd44ba43b361b40ce5e097a66b28bab237891a2",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x2a42b361caa4e4b1a3a979d988838deecbb03916",
  mixHash: "0xa9ee2a914536ad6761ff09253880163cd4d1ddb72beaf8e67331a5ef75879420",
  nonce: "0x382db97a79b732d6",
  number: "0x76",
  parentHash: "0xea93bce69e236c3714c3be473834c7806dc740fb83ae3c469dd6398d1d20511a",
  receiptsRoot: "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: "0x284",
  stateRoot: "0xc7d112d470a0e2a04030d26f4628ef62b13a42c7350d96bc986f49f290dedd3f",
  timestamp: "0x6282bcda",
  totalDifficulty: "0xf2bf46",
  transactions: ["0xbb0f034b69b3456cc514fae266f5d3a01ada59f5b9901c76a415f0c24d85c6a1"],
  transactionsRoot: "0xc9d70dd259cb7f47dc29191b15761b2a7ca1cc9bb320b92ef031983e17dd8837",
  uncles: []

This is just a small preview of the power of the JavaScript REPL provided by geth.

Remote Connections

This section is coming soon. The goal is to allow for a node from an external network to connect to a locally running node and be able to send/receive transactions within the private network. Stay tuned for the completion of this section.

Wrapping Up

This post has covered the initialization and startup of a private blockchain network comprised of a bootnode, a mining node, and two peer nodes. There is a lot more to explore here and the JavaScript console is a great way to do that. Check out the latest web3.js API docs to see all of the APIs that can be explored.

Hopefully this post contained some new or useful information. Thanks for reading!