How to Set Up a Private Ethereum Blockchain using Geth

Ethereum is run by different clients on different peoples’ computers. Whether you’re using a C++, Rust, Golang, or some other implementation doesn’t matter so long as your client implements the protocol properly.

In this tutorial, we’ll be using Geth, the Golang implementation of Ethereum, to create a private blockchain on our computer. This means we’ll be creating a new blockchain from scratch that we can play with however we like, and our private blockchain has nothing to do with the mainnet Ethereum blockchain to which everyone connects.

If you find this post useful, I encourage you to follow my Twitter account, where I post more tutorials and low-level explanations.

Installing Geth

brew update
brew upgrade
brew tap ethereum/ethereum
brew install ethereum

Now you have Geth available to you on the command-line.

Creating an Account

You need to create a private/public keypair first so you can write transactions to the blockchain. You do this in Geth with the following command (note: do not forget the passphrase you choose).

arvanaghi> geth account new
Your new account is locked with a password. Please give a password. Do not forget this password.
Repeat Passphrase:
Address: {<xxxxxx>}

What you see next to Address is your wallet address. Your wallet and encrypted private key are stored in a file in the following locations based on operating system:

  • OSX: ~/Library/Ethereum
  • Linux: ~/.ethereum
  • Windows: %APPDATA%\Ethereum.

If you look inside that file, you will only see your encrypted key, never the unencrypted key, and some other metadata about the key. Geth does not support storing these private keys unencrypted.

When you want to use that key to create a transaction using Geth, you will need to enter the passphrase you created so Geth can decrypt your private key in that file. We’ll get to that shortly.

You can see your account by running:

arvanaghi> geth account list

The genesis.json file

The genesis file determines two things: what will take place in the genesis block, or the first block of your blockchain, and also the configuration rules your blockchain will follow. I wrote a detailed writeup about how it works and every field in the genesis.json file: Deep Diving into the Ethereum Genesis State File.

Store the following into a genesis.json file in whatever directory you are running your command-line commands. We will use this for our private blockchain:

// genesis.json
 "config": {
   "chainID": 1234,
   "homesteadBlock": 0,
   "eip155Block": 0,
   "eip158Block": 0
 "alloc": {
      "balance": "100000000000000000000000000000"
 "difficulty": "0x4000",
 "gasLimit": "0xffffffff",
 "nonce": "0x0000000000000000",
 "coinbase": "0x0000000000000000000000000000000000000000",
 "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
 "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
 "extraData": "0x123458db4e347b1234537c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
 "timestamp": "0x00"

Note: Remember the address that creating the new account gave you? Be sure to substitute that address in under "alloc" where it says <your wallet's address>. This is us telling Geth we want the first block in our blockchain to give 100 billion ether to that address.

We can do this because it’s the first block in the entire chain and we are the creators of this chain. We have the power to decide who starts out with what. That’s what the first block of any cryptocurrency does, including the main Ethereum blockchain.

Read my other post on the genesis state file to understand all these fields. If you’d rather just proceed, know that difficulty refers to how easy it is to mine a block. In our genesis.json file, it is set to hexadecimal 0x4000. This is 16384 in decimal, which means there is a 1/16384 chance you mine a block successfully in your first attempt. This will be quick for us.

Creating and storing the blockchain

Just as the mainnet Ethereum blockchain resides on multiple computers, we will simulate having multiple computers storing our blockchain.

We will create two nodes, which act like two different computers hosting and interacting with the same blockchain. Any node that wants to interact with a blockchain will want to store the blockchain on their computer somewhere (it can be too large to keep in memory). Using the –datadir flag, we specifiy where we want to store the blockchain data as we create it and it continues to grow.

Since we’re going to create two different nodes, we will run Geth from two different Terminal windows to simulate two different computers. We will also use two different datadir directories so each node has a separate place to store their local copy of our blockchain.

As mentioned, the three major operating systems by default store their blockchains in the following locations:

  • OSX: ~/Library/Ethereum
  • Linux: ~/.ethereum
  • Windows %APPDATA%\Ethereum.

We’re creating a new, completely private blockchain, so let’s store it elsewhere.

Run the following to store Node #1’s copy of our private blockchain in a “LocalNode1” folder (it will get created with this command):

geth --datadir "~/Library/LocalNode1" init genesis.json

(This assumes the genesis.json file you created is in the same directory from which you’re running these commands.)

Run the following to store Node #2’s copy of our private blockchain in a “LocalNode2” folder:

geth --datadir "~/Library/LocalNode2" init genesis.json

You can look inside the directories Geth just created at ~/Library/LocalNode1 and ~/Library/LocalNode2. You’ll soon notice how the contents of the “geth” directory grows as we add to our chain.

Interacting with the blockchain

Now that we have the first block written and our configuration variables set, we can launch the geth console to interact with the blockchain from our first node.

geth --datadir "~/Library/LocalNode1" --networkid 1234 --port 11111 --nodiscover console

The networkid here has to match the chainID we set in the genesis.json file. --nodiscover means that even though we’re running an Ethereum client, we don’t want other people in the world trying to connect to our chain. Finally, port is the port number our node will communicate with other peers over.

Once you run that command, you should see a message Welcome to the Geth JavaScript console! appear towards the bottom of your terminal.

We have just opened our console to interact with our first node’s copy of our blockchain. Now, open another tab in Terminal. We’ll now set up a console for our second Node to interact with the same blockchain.

geth --datadir "~/Library/LocalNode2" --networkid 1234 --port 11112 --nodiscover console

Notice that the datadir corresponds to the directory where we are storing the blockchain for our second node (LocalNode2) and that we changed port to 11112 so our two nodes are not colliding on the same port.

List wallets

From node 1, run personal.listWallets. This will list everything inside the _keystore_ file in the datadir directory for that node.

> personal.listWallets

Right now, this yields an empty set [].

We created a wallet at the beginning of this tutorial, and then specified in the genesis.json file that we want to allocate an initial balance of ether to that wallet in the genesis block. When we created that wallet, Geth by default places the encrypted file in ~/Library/Ethereum/keystore/.

We are using a custom datadir since we’re not interacting with the mainnet blockchain, so we need to copy that file into the keystore directory we’re using. Copying that file over will give our node access to that wallet file:

arvanaghi> cp ~/Library/Ethereum/keystore/UTC--<rest of wallet file's name> ~/Library/LocalNode1/keystore/

Alternatively, when we launched the Geth console, we could have explicitly used the --keystore flag to tell Geth where our wallet files were.

After copying over the file, go back to Node 1 and run personal.listWallets.

> personal.listWallets
    accounts: [{
        address: "0x<your wallet address>",
        url: "keystore:///Users/<Your username>Library/LocalNode1/keystore/UTC--<datecreated>Z--<your wallet address>"
    status: "Locked",
    url: "keystore:///Users/<Your username>/Library/LocalNode1/keystore/UTC--<datecreated>Z--<your wallet address>"

To see just the wallet address, you can also run:

> personal.listAccounts
["<your wallet address>"]

As mentioned, we allocated some ether to this wallet in the genesis block via our genesis.json file. Let’s see if it worked:

> web3.fromWei(eth.getBalance(eth.accounts[0]), "ether");

This genesis block we created recognizes the wallet we control as the owner of 100 billion ether on this private chain. If you want to be rich, it’s your job to convince the world to use your chain instead of the one of the mainnet. Good luck!

Connecting to Peers

Nodes 1 and 2 have the same genesis state files and configurations, so if they could communicate with one-another, they would build on the same blockchain.

We set the --nodiscover flag earlier to ensure these nodes don’t automatically interact with other nodes. We’ll have to manually tell one of the nodes about the other’s existence. Currently, entering net.peerCount or admin.peers produces 0 and the empty set respectively.

Tab over to Node 2 and enter admin.nodeInfo.enode. What gets returned is the following:

> admin.nodeInfo.enode
"enode://<enode ID>@<ip>:<TCP discovery port>?discport=<UDP discovery port>"

The enode ID is a unique hexadecimal value. Immediately after it is the “@” sign and then the IP address of the node. In our case, since everything is running locally, this will return [::].

After the IP address is a colon and the TCP discovery port number that the enode is listening on. If you recall, we set Node 2 to listen on port 1112, so that’s what you will see here.

Finally, after “?”, we have discport=. This tells us the UDP discovery port for that node. Since we did not set this, it returns 0.

Copy this entire NodeUrl and tab back over to Node 1. Copy it all into the admin.addPeer function like so:


Now, running admin.peers from either node will give you detailed information about the other node.

If you want to automate this process in the future, you can use set up static nodes, which allows nodes to automatically connect with known enodes.


The coinbase or etherbase we set for a node is the address that will collect the rewards of any mining that takes place on that node.

Right now, checking the coinbase on either of our nodes yields the following error:

> eth.coinbase
Error: etherbase address must be explicitly specified
    at web3.js:3143:20
    at web3.js:6347:15
    at get (web3.js:6247:38)
    at <unknown>

We need to explicitly set it.

If we wanted to set our pre-existing account as the coinbase, we can do so with the following:

> miner.setEtherbase(eth.accounts[0])

But, let’s create a separate address for our mining rewards. We can create an account through the geth console like so:

> personal.newAccount()
Repeat passphrase: 
["<new wallet address"]

You will see this new encrypted key appear in the keystore directory in the datadir for this node.

Now, entering eth.accounts produces an array of two addresses: the original we had at index 0, and the new one we created at index 1. We can set the new wallet address as the coinbase like so:


We can verify this value by then entering eth.coinbase, which should return the wallet address we just set.


We will now send a transaction from a wallet address on node 1 to a wallet address on node 2. We have yet to create a wallet on node 2, so go to that geth tab and do the same process from earlier:

// On node 2
> personal.newAccount()
Repeat passphrase: 
["<new wallet address on node 2"]

This encrypted private key will be in the keystore file in the datadir for node 2.

Now, tab back to Node 1. We will send 20 ether to the wallet address on node 2 like so:

> eth.sendTransaction({from: eth.accounts[0], to: "<wallet address on node 2>", value: 20})

This will yield an error, like so:

Error: authentication needed: password or unlock
    at web3.js:3143:20
    at web3.js:6347:15
    at web3.js:5081:36
    at <anonymous>:1:1

The reason we get this error is because, at this point, our private key file is still encrypted for our first wallet address. This protects an untrusted third party with access to the file in the keystore directory from being able to issue transactions without knowing the passphrase.

First, we unlock the account (decrypt the private key for geth’s use) by running personal.unlockAccount(eth.accounts[0]). After entering the passphrase, the key will be decrypted in memory and ready to sign transactions for geth.

Now, try the same sendTransaction command from earlier, and you will get a response that the transaction has been submitted.

> eth.sendTransaction({from: eth.accounts[0], to: "<wallet address on node 2>", value: 20})
INFO [MM-DDD|HH:MM:SS] Submitted transaction
fullhash=<transaction hash> recipient=<wallet address on node 2>
"<transaction hash>"

The transaction has been submitted. But, if we go to Node 2 and check the balance of the address to which we sent the ether, we see the following:

// Checking the balance of the destination address on node 2
> web3.fromWei(eth.getBalance(eth.accounts[0]), "ether")

Why? Because though we have submitted the transaction, it has yet to be _mined_ and therefore it has not been added to the blockchain.

Going back to node 1, we simply run:

> miner.start()

Because our difficulty is so low, you’ll see blocks mined very quickly. After a few seconds, enter miner.stop().

Now, go back to Node 2 and enter the same command from above to check accounts[0]’s balance:

> web3.fromWei(eth.getBalance(eth.accounts[0]), "ether");

The transaction has successfully been written to our private blockchain, and the wallet owned by node 2 now has 20 ether!

You’ll see that the coinbase account we created on Node 1 will also have some ether now:

> eth.getBalance(eth.coinbase)
<amount in Wei>

In another post, we will go over how to publish smart contracts on our private blockchain so we can test them locally before deploying them to the mainnet.

For more tutorials and content about Ethereum, you can follow me on Twitter.