Sensible Solidity testing via Go Ethereum
by Dennis StritzkeYou 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.
- Install
geth - Create accounts
- Create the Genesis block configuration and initialise the network
- Start the network
- 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:
- Use the
cliqueconsensus algorithm, which is proof-of-authority and spares us to execute proof-of-work mining. Read more on choosing a consensus algorithm in the documentation. - The
extradataproperty needs to contain the account used to sign blocks. I recommend to use the first account as the signing account. This selection works well with most framework assumptions. Insert the signing account public key without the0xprefix in the padded string. - Add one entry per account in the
allocproperty to assign an initial balance to the respective account. The value is specified in wei. (I find it very useful to executeweb3.toWei(10, "ether")to convert Ether to wei, which outputs10000000000000000000)
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:
- The
--nodiscoverflag makes sure that our node doesn’t search for peers to sync - The
--mineflag enables block creation on our node. In combination with the--fakepowflag we disable proof-of-work mining. - The
--http.*flags enables us to (1) rungeth attachvia the network and (2) access the chain via a Docker based Blockscout instance.
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:
-
Make sure the
gethnetwork is running. -
Clone the Blockscout repository
-
Change to the
dockerdirectory -
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 -
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!