Working with Private Keys
This example program will walk through converting bitcoin private keys with libbitcoin and will help you get an understanding of how the library is used.
If you don’t already have libbitcoin installed follow my tutorial here.
Our starter program is going to take a 256-bit secret key and convert it to a bitcoin private key in wallet import format. I will go over how the libbitcoin API works as well as how this conversion works on a conceptual level.
Bitcoin Private Keys Explained
How to Compile with Libbitcoin
Private Keys
Bitcoin private keys are 256-bit hashes generally expressed in wallet import format(WIF) as a base58 checksum encoded number. What does that mean? Basically, a private key is just some sequence of 256 1’s and 0’s which is then encoded into a format that’s easier to read.
Generally, a random number generator is used to produce a seed from entropy which is then hashed using the SHA256 hashing function which returns our coveted 256-bit number. This product of the hashing function is our valuable password(secret key) on the bitcoin network so it is to be protected and kept secret.
This secret key is then encoded to make it easier to read and transfer between wallet software. Secret keys are generally encoded with certain metadata, such as key type, version info and its checksum, resulting in wallet import format. WIF is structured like so:
Version Prefix + Secret Key + Key Type + Checksum
Where, the version prefix for private keys is 128(0x80 in hex) the secret key is the secret key, the key type is 01 if it is a compressed key or not included if it is an uncompressed key. The checksum is the result of a double SHA256 performed on all the previous parts of the key. The first 4 bytes of this hash are then concatenated to the end of the key and the entire thing is base58 encoded.
Base58 encoding is common encoding used with bitcoin and is similar to an encoding like base16(hex) or base64 it is simply optimized to be easier to read by eliminating ambiguous characters such as I(capital i) and l(lowercase L). This is the same format used for bitcoin addresses.
The checksum is used to ensure that no mistakes have been made in the transcription of any of the characters as wallet software will check that the 4-byte hash matches the hash of the rest of the data thus preventing a costly typo based mistake.
The resulting WIF private key can easily be encoded into a QR code for easy transfer.
How to convert a secret key to WIF in Libbitcoin
Now, the fun part. Fire up your text editor and add your includes.
#include <bitcoin/bitcoin.hpp> #include <string.h> #include <iostream>
The libbitcoin library is included as bitcoin/bitcoin.hpp. After that we’re going to have to use the libbitcoin namespace as shown below. This is because libbitcoin has multiple sub namespaces to accommodate the ambiguity introduced by a lot of the custom types used.
using namespace bc;
Now, inside our main function we are going to declare our hexadecimal secret key as a string and print it out. In order to create this key we will first need to generate it using a pseudo random number generator.
We’re going to work with a general libbitcoin type called a data_chunk which is just a vector of bytes, specifically a vector of unsigned 8 bit integers. Data chunks are used to represent all kinds of raw data in the libbitcoin toolkit.
For our seed we will first need to declare a data_chunk vector of 16-bytes, this will be used as a buffer which we will fill with entropy using the pseudo_random_fill function. After that, we can declare an ec_secret object. Remember a secret key is distinct from a private key; where, secret key refers to the 256-bit value and private key refers to the secret key plus all its metadata(WIF).
We can create the secret key by sha256 hashing the seed twice, both hashes can be accomplished by using the bitcoin_hash_function
data_chunk seed(16); pseudo_random_fill(seed); ec_secret secretKey = bitcoin_hash(seed);
Then, we can use the handy encode_base16 function to encode the vector of raw bytes, AKA our secret_key object, into a base16 formatted string.(Note: these encode/decode function are available for many types including base58, and base64)
std::string hexKey = encode_base16(secretKey); std::cout << "Hex secret: " << std::endl; std::cout << hexKey << std::endl;
Now, to convert the secret key using the libbitcoin API we’re going to use the following code:
ec_secret rawprivateKey; decode_base16(rawprivateKey, hexKey); wallet::ec_private privateKey(rawprivateKey, 0x8000, false); std::cout << "\nPrivate Key Structure: " << std::endl; std::cout << privateKey.encoded() << std::endl; ec_secret keyTest = privateKey.secret(); std::string keyTestString = encode_base16(keyTest); if(keyTestString == hexKey){ std::cout << "Confirmed: " <<keyTestString << std::endl; } else { std::cout << "Error!" <<keyTestString << std::endl; }
The first line here declares a special libbitcoin type ec_secret, which is a special byte array type used for private keys. The next line uses the decode_base16 function to convert our hexKey string from base16 and save it into our ec_secret type variable. The decode_base16 function takes arguments for the output byte array and the input string and returns a bool(true/false) if it worked properly.
ec_secret rawprivateKey; decode_base16(rawprivateKey, hexKey);
Next, we instantiate a libbitcoin private key object by first scoping the namespace wallet and then passing the following arguments to the constructor function: the raw byte array of the secret key, the key version code for a mainnet WIF, 0x8000, and since we want an uncompressed WIF we pass “false” to the compression argument.
wallet::ec_private privateKey(rawprivateKey, 0x8000, false);
Now that we have our private key object, we can print it out using its string serializer function encoded().
std::cout << "\nPrivate Key Structure: " << std::endl; std::cout << privateKey.encoded() << std::endl;
After, we can check that our WIF private key did in fact originate from our initial hex key by using the secret() accessor function and the encode_base16 function to output the the private key in hex form and check that against the original hexKey.
ec_secret keyTest = privateKey.secret(); std::string keyTestString = encode_base16(keyTest); if(keyTestString == hexKey){ std::cout << "Confirmed: " << keyTestString << std::endl; } else { std::cout << "Error!" << keyTestString << std::endl; }
How It Works
Now that we have seen how to convert private keys using the special libbitcoin data structures, we can walk through how the keys are actually formatted by manually using some of libbitcoin’s helper functions.
We’ll start by adding 80, the hex version prefix, to the front of the secret key’s hex string and print that out for reference.
hexKey = "80" + hexKey; std::cout << "\nVersioned Key: " << std::endl; std::cout << hexKey << std::endl;
So, we define 2 variables of this type to store the raw secret key and the WIF key. Additionally, we need to declare a byte array in order to hash the checksum. The size declared here is for an uncompressed WIF.
data_chunk fullKey; data_chunk wifVersion; byte_array<37u> versionedKey;
Once we have our memory allocated, we can use some of the encoding functions we’ve already seen to convert the necessary parts of the key. First, we decode the hex secret key and save the raw bytes into our data chunk variable. Next, we do the same decoding with “0x80”, the hex version prefix, and save that into a data chunk.
After we have the parts of our key converted we need to add the checksum to the end. This is easily accomplished with a libbitcoin helper function which takes the destination byte array and an aggregator of the prefix and secret key as arguments in that order.
decode_base16(fullKey, hexKey); decode_base16(wifVersion, "0x80"); build_checked_array(versionedKey, { wifVersion, fullKey});
After the build_checked_array function executes, we have our full key with it’s prefix and checksum saved into our byte array. This leaves us with just one more step: to encode the key into a bitcoin readable format, base58.
std::string practiceKey = encode_base58(versionedKey); std::cout<< "\nBy Hand:" << std::endl; std::cout<< practiceKey << std::endl;
The output here is the wallet import format for an uncompressed private key.
How to Compile and Run a Libbitcoin Program
In order to run a program with libbitcoin we are going to need to compile our program to link the libbitcoin library and specify standard c++11 syntax using g++.
$ g++ -std=c++11 -o first libbitcoinFirstProgram.cpp $(pkg-config --cflags libbitcoin --libs libbitcoin)
after that we can run it like most c++ programs.
$./first
The output should look similar to this:
This program converts into an uncompressed WIF which is a rarely used format type. For extra practice, go ahead and reimplement this program for compressed keys which simply require the suffix “01” added to the end of the hex secret key.