Warning: This article is not for beginners. It will explore HD wallets, how they’re built, and how to use them in Bitcoin.
This article will be relevant across Bitcoin, Bitcoin Cash, and all other forks of Bitcoin that still hold “Bitcoin” in the name.
What exactly is an HD wallet?
How to Build a Bitcoin HD Wallet in node.js
Now you should know what an HD wallet is and that it’s really a tree of nodes, each with a private and public key. First what we’ll do is set up the wallet: create a mnemonic, create an address and then we’ll create a raw transaction and broadcast it across the Bitcoin Testnet!
- bip39 (for mnemonic generation)
- hdkey (implementation of BIP 32)
- create-hash (for.. you guessed it creating hashes)
- bs58check (for base58 checking)
Master Private Key Generation
How do you start a tree? By beginning at the node. Now, HD Wallets are created by a random bit of data called a seed. We will create a mnemonic from random data and convert that mnemonic to a seed. For those unaware as to what a mnemonic is, here is a quote from the bitcoin wiki.
A simplified explanation of how mnemonic phrases work is that the wallet software has a wordlist taken from a dictionary, with each word assigned to a number. The mnemonic phrase can be converted to a number which is used as the seed to a deterministic wallet that generates all the key pairs used in the wallet.
const mnemonic = bip39.generateMnemonic(); //generates string
const seed = bip39.mnemonicToSeed(mnemonic); //creates seed buffer
Now that we have our entropy in the format we need it in, it’s time to generate the root of the node tree. We can also get our masterPrivateKey and hold onto it from here as well.
const root = hdkey.fromMasterSeed(seed);
const masterPrivateKey = root.privateKey.toString('hex');
Note: Treat your
root.publicKey as securely as you would treat your
masterPrivateKey as you can still generate the addresses nodes with it.
We have our root node. It’s time to generate the rest of the tree. Recall from HD Wallets Explained, the derivation path BIP 44 set in place.
m / purpose' / coin_type' / account' / change / address_index
We’ll use the path
purpose = 44 for BIP 44,
coin_type = 0 for bitcoin (satoshi labs used to host the coin_types on their main page, but have since taken it down for some reason. I am unable to find another source as complete as this). The rest of the variables are 0 because this is the first address we’re creating in the first account and 0 is the external address code.
Again, if any of this is unfamiliar to you I highly suggest reading HD Wallets Explained.
const addrnode = root.derive("m/44'/60'/0'/0/0");
Addresses in Bitcoin have 34 characters, but can have as few as 26 and still be valid. From the Wiki:
Several of the characters inside a Bitcoin address are used as a checksum so that typographical errors can be automatically found and rejected. The checksum also allows Bitcoin software to confirm that a 33-character (or shorter) address is in fact valid and isn’t simply an address with a missing character.
- Get your public key
- Perform SHA-256 hashing on the public key
- Perform RIPEMD-160 hashing on the result of SHA-256
const step1 = addrnode._publicKey;
const step2 = createHash('sha256').update(step0).digest();
const step3 = createHash('rmd160').update(step1).digest();
4. Add version byte in front of RIPEMD-160 hash (0x00 for mainnet, 0x6f for testnet)
var step4 = Buffer.allocUnsafe(21);
step3.copy(step4, 1) //step3 now holds the extended RIPEMD160 result
The next steps are the Base58Check encoding. According to Pieter Wuille
The “Base58” refers to the fact that it is a base 58 format (it uses 58 different characters to encode the data), and the “Check” refers to the fact that a checksum is added to the encoded data.
The second part of the name is "Check". This refers to a checksum. The SHA256 double of the data that we want to encode is calculated, and the first four bytes of that hash are used as a checksum. A checksum is a small bit of data derived from a larger block of data for the purpose of detecting errors. This detection method helps ensure that the data is correct and not corrupted. Those four bytes are appended to the data that we are encoding before we perform a base conversion to generate the base 58 string.
To simplify: Step 3 is the base address. We’re going to derive the checksum for the base address by hashing the base twice with SHA-256 and we’ll take the first 4 bytes and add it to the end of the base address. This way we’ll know if the derivation process for generating the address was accurate. (others can perform steps 4 to 7 for themselves and verify if the hashing we performed is accurate without giving up the public key used to get the base address).
5. Perform SHA-256 hash on the extended RIPEMD-160 result
6. Perform SHA-256 hash on the result of the previous SHA-256 hash
7. Take the first 4 bytes of the second SHA-256 hash. This is the address checksum
8. Add the 4 checksum bytes from stage 7 at the end of extended RIPEMD-160 hash from stage 4. This is the 25-byte binary Bitcoin Address.
9. Convert the result from a byte string into a base58 string using Base58Check encoding. This is the most commonly used Bitcoin Address format.
var step9 = bs58check.encode(step4); //Steps 5-9
For a visualization of these 9 steps, take a look at gobittest. Here is a gist of all the steps involved in address generation.
How’d you like this article? If you liked it or learned something, please leave a clap! BitCraft is a crypto development group and we’re always taking on new clients. Also — WE’RE HIRING!! If you’re a great developer and you’re familiar with remote work, we’re interested in you! Reach out to us at firstname.lastname@example.org or visit our website at bitcraft.io!
Some other articles and resources of note: