How To Setup A Custom Bitcoin Testnet?
Working with Bitcoin, we quickly realized that testnet does not meet our needs. Transactions can take a long time to be mined and since supply is limited, faucets can’t really give us the amount of coins that we want. These issues slow us down and limit the scenarios that we can run on our testing environment. Not only our testers need to obtain the required amount of coins from the faucets themselves, which takes time, but they would just sit there and wait for their transfers to go through. And since time is such a valuable resource, we decided to take the other route, which is hosting our custom chain. In this post I’ll go into details about how we did this and explain the reasons behind our decisions.
Right tools for the job
Let’s first think about the objectives. In our case, we don’t need to simulate the way production blockchain works, we just want to send funds back and forth. These transactions should be able to complete as quickly as possible. Since we’re going to be the only users on the chain, we’d like to control all the coins, be able to generate them on demand and do whatever we want with them. Blocks should be mined with a fixed rate with an option to speed it up whenever we want to. Ideally, we’d like to have a graphical wallet app and a block explorer so it’s more accessible to users of our testnet.
Our setup consists of three services:
- Single Bitcoin node running in regtest mode,
- ElectrumX server,
- BTC RPC Explorer
We chose the regtest mode because it allows us to generate blocks on demand. For the sake of simplicity, I’d like to stick to a single node. While you could have more, this just adds unnecessary complication to the setup. From our experience, there doesn’t seem to be much benefit from doing that. If your application is RPC heavy, you might run into stability issues such as timeouts and adding another node might help a bit with that, but in the long run you should just take a step back and rework your code, making it rely on the RPC as little as possible. Trust me, I’ve been there. Bitcoin daemon tends to use locks quite aggressively making things painfully slow, no matter how big your queue size is nor how many RPC threads are running. We’ll be running a patched version of Bitcoin. We opted for that, because the halving interval on a regtest chain is quite low, 150. This means the reward for mining will be decreased by half every 150 blocks, starting with 50 BTC per block, which effectively limits the total supply of coins to measly 15 000 BTC. What am I even going to do with that? Can’t even afford toilet paper these days. That’s why we bumped it up to 210000, increasing token supply to 21 million, same as on Bitcoin mainnet and testnet. Also, we’ll have a script running in the background which mines a block every minute. We chose that rate because storage requirements increase with the amount of blocks, but we also don’t want to wait too long for the transaction to go through. This also gives us a steady stream of tokens which we can use at any time. Docker image is available here.
ElectrumX will enable us to connect to our testnet via the Electrum Bitcoin wallet app. It’s really fast, lightweight and should be able to serve all our users. The app itself is also simple and user-friendly. Sure, it doesn’t have cool graphs and other bells and whistles but it does the job.
Explorer provides a nice way to track transactions and address balances on the blockchain through a web interface. We went with the BTC RPC Explorer app, since it’s simple to set up and doesn’t rely on any additional services. We can configure it to connect with our ElectrumX server to get all the features we need, such as transaction history for addresses. As the name suggests, it makes use of RPC for most things, which frankly isn’t ideal. It tends to get a little slow every now and then, but we should be fine.
Setting it up
To run the testnet on our machine, we’re going to use a Docker Compose file.
version: '3' services: node: image: ulamlabs/bitcoind-custom-regtest:latest electrumx: image: lukechilds/electrumx:latest links: - node ports: - "51001:50001" - "51002:50002" environment: - NET=regtest - COIN=BitcoinSegwit - DAEMON_URL=http://test:test@node:19001 explorer: image: ulamlabs/btc-rpc-explorer:latest links: - node - electrumx ports: - "3002:3002" environment: - BTCEXP_HOST=0.0.0.0 - BTCEXP_BITCOIND_URI=http://test:test@node:19001 - BTCEXP_ELECTRUMX_SERVERS=tls://electrumx:50002 - BTCEXP_ADDRESS_API=electrumx
Save this file as docker-compose.yml in your working directory, open the shell and run the following command:
$ docker-compose up -d
After a couple of seconds, our testnet should be up and running. We can verify that by heading to http://localhost:3002.
If you scroll down, you should see a list of the latest blocks. Clicking on any link in the “Height” column will show you the block and all the transactions that were incorporated into it. Notice the 50 BTC being sent to our wallet. Clicking on an address will reveal our current balance and all the transactions. As you can see, fresh coins are being mined every minute.
Let’s open a shell in the Bitcoin node container. When using Docker Compose, every container name will be prefixed with your working directory, in my case it’s bitcoin-testnet.
$ docker exec -it bitcoin-testnet_node_1 /bin/bash
We can check the balance on our wallet using the bitcoin-cli tool.
bash-5.0$ bitcoin-cli getbalance
You might be surprised that it returns 0. Shouldn’t we have a couple hundred bitcoins already? Well yes, but actually no. Bitcoin network enforces a 100-block maturation time for each reward in case blocks become orphaned. This is a mechanism built into the protocol itself and allows multiple users to participate in the mining process. What if two miners completed the same block at the same time? The miners will essentially get divided between ones that consider the first block as next one and the others. The longest chain (the one more users agree on) wins, while all the other blocks are orphaned and the reward for them gets lost. It’s only safe to use the freshly minted coins after 100 new blocks were mined. getbalances command provides a breakdown of our balance into mature and immature funds. Let’s make ourselves some new coins.
$ bitcoin-cli generatetoaddress 200 `bitcoin-cli getnewaddress`
This will advance our testnet by 200 blocks. As an output we’ll get a list of hashes for all the blocks. Now we should have at least 5000 BTC plus whatever was mined before ready to be spent.
Using Electrum wallet
One of the requirements was a working wallet app, let’s confirm that it indeed works. To be able to connect to a regtest node using Electrum, we need to run the app in a special mode by passing a --regtest argument. This might be different on your platform. On Mac we run it like this:
$ open /Applications/Electrum.app --args --regtest
Even if you used Electrum before to send transactions on a mainnet or testnet, Electrum will ask you to create a new wallet. In this mode, Electrum is configured to connect to a server running on localhost, so you can leave “Auto connect” on. Let’s create a standard wallet with a new seed. Since this is just a test wallet, you can ignore the password step as it’s not necessary. Once you’re done, you will be warned that testnet coins are worthless.
Dot in the bottom right corner indicates whether we’re connected to the network. It’s green, which means that everything went smoothly. You can now send yourself some funds by invoking a CLI command inside a container.
$ docker exec bitcoin-testnet_node_1 bitcoin-cli sendtoaddress [address from “Receive” tab] 10
In a couple of seconds, an incoming transaction for 10 BTC should show up on our wallet. Icon next to it indicates the amount of confirmations it has. This essentially tells you how possible it is that it will be reverted. The more blocks were mined on top of the one that includes it, the harder it is to revert. After reaching 6 confirmations, the icon will change to a checkmark.
Running the testnet for a longer period of time, we ran into an issue with Electrum suddenly crashing. This is unfortunately a known bug and it’s well documented in this ticket. Essentially, there is a counter that overflows after some time and brings the whole thing down. It is recommended that a script called
electrumx_compact_history is executed on a regular basis to avoid it. It’s available in
/electrumx directory on the container.