Sensible Solidity testing via Go Ethereum

2021-10-13

You have built a smart contract, tested it via unit tests, explored it via Metamask and a block explorer and now want to deploy to the Mainnet. Didn’t you forget something?

Until now you most likely tested your Solidity code via a local network like hardhat node, ganache-cli or the Ganache UI. Those are very convenient and fast, but are not the same thing as the real network. In this post we will explore how to test your code using the “official Go implementation of the Ethereum protocol” called geth, which enables us to be much closer to the Mainnet.

I will be using Hardhat on macOS within zsh in my descriptions, but similar workflows exist for other toolchains.

Start a local network via Geth

If you are looking for a very quick local setup and are fine with having only one account available, geth --dev is a solid option. The chain does not persist, if the command is terminated.

In most cases you will need more though: multiple accounts, account locking and unlocking, mining controls, timing changes, etc. All of these are possible through geth. Follow these steps to create your geth based local network.

  1. Install geth
  2. Create accounts
  3. Create the Genesis block configuration and initialise the network
  4. Start the network
  5. Cleanup

Install geth

Follow the extensive Installing geth instructions in the documentation.

Create accounts

geth handles encrypted private keys only. Within the local development environment we (oftentimes) don’t really care about key security. To handle passwords create a file named passwords containing the same password on each line. Create as many lines as you want to create accounts.

# passwords
secret
...
secret

Execute the geth account new command once for every account you need.

geth account new --datadir data --password passwords

Genesis Block configuration

Next we have to instruct geth how our chain should behave. This is done via the genesis block.

{
  "config": {
    "chainId": 15,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "clique": {
      "period": 0,
      "epoch": 30000
    }
  },
  "difficulty": "1",
  "gasLimit": "8000000",
  "extradata": "0x0000000000000000000000000000000000000000000000000000000000000000<insert signer key without 0x prefix>0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "alloc": {
    "c736af4e3dd4e0b463c3cbfecc9ab84705b69d8b": { "balance": "40000000000000000000" },
    ...
    "ba4b47ee3abdc81531304bb09d45a085fb59c6ce": { "balance": "40000000000000000000" }
  }
}

Notable properties:

Initialise the network by running geth init --datadir data genesis.json.

Start the network

Execute the following command to start the private network. Replace and insert all public keys of the accounts you want to unlock in the --unlock flag. Remember that your passwords file needs to have at least as many lines has you want to unlock accounts.

geth --datadir data --nodiscover --mine --fakepow \
  --http --http.addr 0.0.0.0 --http.vhosts "*" --http.api "eth,net,web3,txpool,debug"
  --allow-insecure-unlock --password data/passwords \
  --unlock 0xc736af4e3dd4e0b463c3cbfecc9ab84705b69d8b,...

Notable properties:

Cleanup

In the following commands we used a directory named data as the storage location. Just remove it once you are done.

Run your test suite

Now that the local geth network is running, the actual testing is straight forward. First, within the hardhat.config.js add a new network.

module.exports = {
  ...
  networks: {
    ...
    geth: {
      url: "http://127.0.0.1:8545"
    }
  }
};

Having done that, execute your test suite via npx hardhat test --network geth. Do your tests also work on the official Go Ethereum implementation? Yes, that is great. No? Good you caught that before deploying to Mainnet.

$ npx hardhat test --network geth

  Distribution Wallet
    deployment
      ✓ should set the correct owner

  1 passing (194ms)

If you need to analyse some issues or want to dig deeper, start the Geth JavaScript console via geth attach http://127.0.0.1:8545/, which allows you to execute the web3, eth and more commands.

$ geth attach http://127.0.0.1:8545/
Welcome to the Geth JavaScript console!

instance: Geth/v1.10.8-stable/darwin-amd64/go1.16.6
coinbase: 0xc736af4e3dd4e0b463c3cbfecc9ab84705b69d8b
at block: 9 (Tue Oct 12 2021 09:53:25 GMT+0200 (CEST))
 modules: debug:1.0 eth:1.0 net:1.0 rpc:1.0 txpool:1.0 web3:1.0

To exit, press ctrl-d
> web3.toWei(10, "ether")
"10000000000000000000"
> eth.accounts
["0xc736af4e3dd4e0b463c3cbfecc9ab84705b69d8b", "0xd8508d13c889728420103d6025761dfd98f43c36", "0x1a30d4f3db1d4781daf11398b55c62001cbef590", "0x3057d5b4f3ee1a6a56965af6708736fce858bc59", "0xba4b47ee3abdc81531304bb09d45a085fb59c6ce"]
> eth.getBalance(eth.accounts[1])
45999979000000000000
> web3.fromWei(eth.getBalance(eth.accounts[1]))
45.999979

Bonus: Block Explorer

In the post Using Block Explorers with Hardhat we tried different approaches to visually explore the local network. That worked reasonably well.

As our local network is now run through geth, compatibility is of no issue anymore. Fancy some Blockscout? Follow these steps:

  1. Make sure the geth network is running.

  2. Clone the Blockscout repository

  3. Change to the docker directory

  4. Execute the Makefile, which will build Blockscout on its first execution. (This took about 15 minutes on my machine)

    COIN=ETH \
    ETHEREUM_JSONRPC_VARIANT=geth \
    ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:8545 \
    make start
    
  5. Visit http://localhost:4000

Going forward

Looking at this post, I am quite surprised how lengthy it got. Maybe it is time to implement a script geth-dev that provides the start, stop, cleanup and accounts commands? Maybe something to integrate this easily into Mocha based tests? Send an email to me and tell me what you think!