Building a bitcoin HD wallet in go

Theedtron
4 min readApr 19, 2024

From the oxford dictionary, a wallet is simply a pocket sized flat folding case for holding paper money. That definition is almost as old as the english language itself.

Digitization of money or currency led to the digital wallet. In this article we’ll specifically talk about the bitcoin wallet and how you can make one for you and your friends.

Let’s get a bit technical. There are two primary types of bitcoin wallets:

  1. Non-deterministic wallets (aka JBOK wallets, from “Just a Bunch Of Keys”): In this type of wallet, each key is independently generated from a random number, meaning that the keys are not related to each other.
  2. Deterministic wallets: All the keys in this type of wallet are derived from a single master key known as the seed. The keys are all related and can be regenerated if one has the original seed. A common form of deterministic wallets is the hierarchical deterministic wallet, or HD wallet, which uses a tree-like structure for key derivation.

We will be building a HD wallet using go.

First things first we need to setup the dev environment.

You’ll need to install go if you haven’t already. You can download it from here

We are going to use a package called btcd which is a suite of modules which will help us interact with Bitcoin and go-bip39 which helps to create a mnemonic seed phrase.

Enter you command line and create a folder named btcHdWallet and cd into it. Thereafter initialize go project.

mkdir btcHdWallet
cd btcHdWallet
go mod init github.com/yourusername/btcHdwallet

Install dependancies

go get -u github.com/btcsuite/btcd
go get -u github.com/tyler-smith/go-bip39

Next create a file named main.go which will be the entry point of the application. We will generate a new mnemonic seed and derive a master key from. Populate the code below

package main

import (
"fmt"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/tyler-smith/go-bip39"
)

func main() {
// Generate a new mnemonic seed
entropy, err := bip39.NewEntropy(256)
if err != nil {
fmt.Println("Error generating entropy:", err)
return
}

mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
fmt.Println("Error generating mnemonic:", err)
return
}

// Generate a Bip32 HD wallet for the mnemonic and a user supplied password
seed := bip39.NewSeed(mnemonic, "")

masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.TestNet3Params)
if err != nil {
fmt.Println("Error generating master key:", err)
}

fmt.Println("Master key:", masterKey)
}

Next we’ll derive a child private key from the master and then generate a child public key. We will hash the child public key and add bitcoin testnet network params to be able to generate an address from it.

// Derive the first child key
childKey, err := masterKey.Derive(hdkeychain.HardenedKeyStart + 0)
if err != nil {
fmt.Println("Error deriving child key:", err)
return
}

// Convert child key to Bitcoin address
childPubKey, err := childKey.ECPubKey()
if err != nil {
fmt.Println("Error generating address:", err)
return
}

pubKeyHash := btcutil.Hash160(childPubKey.SerializeCompressed())
address, err := btcutil.NewAddressWitnessPubKeyHash(pubKeyHash, &chaincfg.TestNet3Params)
if err != nil {
fmt.Println("Error generating address:", err)
return
}

fmt.Println("Segwit Address:", address.EncodeAddress())

Next we’ll need to interact with bitcoin-core in order to send transactions. For this we will connect to bitcoin-core testnet via rpc client.

// Connect to Bitcoin node
rpcClient, err := rpcclient.New(&rpcclient.ConnConfig{
HTTPPostMode: true,
DisableTLS: true,
Host: "localhost:8334",
User: "btcusername",
Pass: "xxxxxxxxx",
}, nil)
if err != nil {
fmt.Println("Error connecting to node:", err)
return
}

Next we’ll construct a transaction adding it’s inputs and ouptus and signing the transaction.

// Create a new transaction
tx := wire.NewMsgTx(wire.TxVersion)

// Add input to the transaction
prevOutHashStr := "aabbccdd112233445566778899aabbccdd112233445566778899aabbccdd1122"
prevOutHash, err := chainhash.NewHashFromStr(prevOutHashStr)
if err != nil {
fmt.Println("Error parsing previous output hash:", err)
}

prevOut := wire.NewOutPoint(prevOutHash, 0)
txIn := wire.NewTxIn(prevOut, nil, nil)
tx.AddTxIn(txIn)

// Add output to the transaction
outputAddr, err := btcutil.DecodeAddress("tb1q9j37z2ssgvpv3uqz6d0hcg3a5nc8duq0jw6c0l", &chaincfg.TestNet3Params)
if err != nil {
fmt.Println("Error decoding output address:", err)
}

outputScript, err := txscript.PayToAddrScript(outputAddr)
if err != nil {
fmt.Println("Error creating output script:", err)
}

txOut := wire.NewTxOut(100000, outputScript)
tx.AddTxOut(txOut)

// Sign the transaction
hashType := txscript.SigHashAll
privateKey, err := childKey.ECPrivKey() // Here we get the private key from the child key
if err != nil {
fmt.Println("Error obtaining private key:", err)
}

script := outputScript
sigScript, err := txscript.SignatureScript(tx, 0, script, hashType, privateKey, true)
if err != nil {
fmt.Println("Error signing transaction:", err)
}

txIn.SignatureScript = sigScript

fmt.Printf("Signed Transaction: %v\n", tx)

We’ll send the transaction using the rpc client.

// Send the transaction
txHash, err := rpcClient.SendRawTransaction(tx, false)
if err != nil {
fmt.Println("Error sending transaction:", err)
return
}

fmt.Println("Transaction sent! TxHash:", txHash.String())

You can confirm whether the transaction was sent by going to a block explorer such a blockstream. Get the code from my github.

References:

https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05_wallets.adoc

https://github.com/tyler-smith/go-bip39

--

--