Generating Bitcoin Private Keys and Public Addresses with Elixir

Written by Pete Corey on Jan 22, 2018.

Lately I’ve been working my way through Mastering Bitcoin, implementing as many of the examples in the book in Elixir as I can.

I’ve been amazed at how well Elixir has fared with implementing the algorithms involved in working with Bitcoin keys and addresses. Elixir ships with all the tools required to generate a cryptographically secure private key and transform it into a public address string.

Let’s walk through the process step by step and build our our own Elixir module to generate private keys and public addresses.

What are Private Keys and Public Addresses?

A Bitcoin private key is really just a random two hundred fifty six bit number. As the name implies, this number is intended to be kept private.

From each private key, a public-facing Bitcoin address can be generated. Bitcoin can be sent to this public address by anyone in the world. However, only the keeper of the private key can produce a signature that allows them to access the Bitcoin stored there.

Let’s use Elixir to generate a cryptographically secure private key and then generate its most basic corresponding public address so we can receive some Bitcoin!

Pulling a Private Key Out of Thin Air

As I mentioned earlier, a Bitcoin private key is really just a random two hundred and fifty six bit number. In other words, a private key can be any number between 0 and 2^256.

However, not all random numbers are created equally. We need to be sure that we’re generating our random number from a cryptographically secure source of entropy. Thankfully, Elixir exposes Erlang’s :crypto.strong_rand_bytes/1 function which lets us easily generate a list of truly random bytes.

Let’s use :crypto.strong_rand_bytes/1 as the basis for our private key generator. We’ll start by creating a new PrivateKey module and a generate/0 function that takes no arguments:

defmodule PrivateKey do
  def generate

Inside our generate/0 function, we’ll request 32 random bytes (or 256 bits) from :crypto.strong_rand_bytes/1:

def generate do

This gives us a random set of 32 bytes that, when viewed as an unsigned integer, ranges between 0 and 2^256 - 1.

Unfortunately, we’re not quite done.

Validating our Private Key

To ensure that our private key is difficult to guess, the Standards for Efficient Cryptography Group recommends that we pick a private key between the number 1 and a number slightly smaller than 1.158e77:

An excerpt of the SECG guidelines.

We can add this validation check fairly easily by adding the SECG-provided upper bound as an attribute to our PrivateKey module:

@n :binary.decode_unsigned(<<
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
  0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
  0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41

Next, we’ll add a valid?/1 function to our module that returns true if the provided secret key falls within this range, and false if it does not:

defp valid?(key) when key > 1 and key < @n, do: true
defp valid?(_), do: false

Before we pass our private key into our valid?/1 function, we’ll need to convert it from a thirty two byte binary into an unsigned integer. Let’s add a third valid?/1 function head that does just that:

defp valid?(key) when is_binary(key) do
  |> :binary.decode_unsigned
  |> valid?

We’ll finish off our validation by passing our generated private key into our new valid?/1 function. If the key is valid, we’ll return it. Otherwise, we’ll generate a new private key and try again:

def generate do
  private_key = :crypto.strong_rand_bytes(32)
  case valid?(private_key) do
    true  -> private_key
    false -> generate

Now we can call PrivateKey.generate to generate a new Bitcoin private key!

From Private Key to Public Key …

The most basic process for turning a Bitcoin private key into a sharable public address involves three basic steps. The first step is to transform our private key into a public key with the help of elliptic curve cryptography.

We’ll start by adding a new to_public_key/1 function to our PrivateKey module:

def to_public_key(private_key)

In our to_public_key/1 function, we’ll use Erlang’s :crypto.generate_key function to sign our private_key using an elliptic curve. We’ll specifically use the :secp256k1 curve:

:crypto.generate_key(:ecdh, :crypto.ec_curve(:secp256k1), private_key)

We’re using the elliptic curve key generation as a trapdoor function to ensure our private key’s secrecy. It’s easy for us to generate our public key from our private key, but reversing the computation and generating our private key from our public key is nearly impossible.

The :crypto.generate_key function returns a two-element tuple. The first element in this tuple is our Bitcoin public key. We’ll pull it out using Elixir’s elem/1 function:

:crypto.generate_key(:ecdh, :crypto.ec_curve(:secp256k1), private_key)
|> elem(0)

The returned value is a sixty five byte binary representing our public key!

… Public Key to Public Hash …

Once we have our public key in memory, our next step in transforming it into a public address is to hash it. This gives us what’s called the “public hash” of our public key.

Let’s make a new function, to_public_hash/1 that takes our private_key as an argument:

def to_public_hash(private_key)

We’ll start the hashing process by turning our private_key into a public key with a call to to_public_key:

|> to_public_key

Next, we pipe our public key through two hashing functions: SHA-256, followed by RIPEMD-160:

|> to_public_key
|> hash(:sha256)
|> hash(:ripemd160)

Bitcoin uses the RIPEMD-160 hashing algorithm because it produces a short hash. The intermediate SHA-256 hashing is used to prevent insecurities through unexpected interactions between our elliptic curve signing algorithm and the RIPEMD algorithm.

In this example, hash/1 is a helper function that wraps Erlang’s :crypto.hash.

defp hash(data, algorithm), do: :crypto.hash(algorithm, data)

Flipping the arguments to :crypto.hash in this way lets us easily pipe our data through the hash/1 helper.

… And Public Hash to Public Address

Lastly, we can convert our public hash into a full-fledged Bitcoin address by Base58Check encoding the hash with a version byte corresponding to the network where we’re using the address.

Let’s add a to_public_address/2 function to our PrivateKey module:

def to_public_address(private_key, version \\ <<0x00>>)

The to_public_address/2 function takes a private_key and a version byte as its arguments. The version defaults to <<0x00>>, indicating that this address will be used on the live Bitcoin network.

To create a Bitcoin address, we start by converting our private_key into a public hash with a call to to_public_hash/1:

|> to_public_hash

All that’s left to do is Base58Check encode the resulting hash with the provided version byte:

|> to_public_hash
|> Base58Check.encode(version)

After laying the groundwork, the final pieces of the puzzle effortlessly fall into place.

Putting Our Creation to Use

Now that we can generate cryptographically secure private keys and transform them into publishable public addresses, we’re in business.


Let’s generate a new private key, transform it into its corresponding public address, and try out on the Bitcoin testnet. We’ll start by generating our private key:

private_key = PrivateKey.generate

This gives us a thirty two byte binary. If we wanted, we could Base58Check encode this with a testnet version byte of 0xEF. This is known as the “Wallet Import Format”, or WIF, of our Bitcoin private key:

Base58Check.encode(private_key, <<0xEF>>)

As its name suggests, converting our private key into a WIF allows us to easily import it into most Bitcoin wallet software:

Importing our test private key.

Next, let’s convert our private key into a testnet public address using a version byte of 0x6F:

PrivateKey.to_public_address(private_key, <<0x6F>>)

Now that we have our public address, let’s find a testnet faucet and send a few tBTC to our newly generated address! After initiating the transaction with our faucet, we should see our Bitcoin arrive at our address on either a blockchain explorer, or within our wallet software.

Our tBTC has arrived.


Final Thoughts

Elixir, thanks to its Erlang heritage, ships with a wealth of tools that make this kind of hashing, signing, and byte mashing a walk in the park.

I encourage you to check our the PrivateKey module on Github to get a better feel for the simplicity of the code we wrote today. Overall, I’m very happy with the result.

If you enjoyed this article, I highly recommend you check out the Mastering Bitcoin book. If you really enjoyed this article, feel free to send a few Bitcoin to this address I generated using our new PrivateKey module:


Stay tuned for more Bitcoin-related content as I work my way through Mastering Bitcoin!

Secure Meteor

Written by Pete Corey on Jan 15, 2018.

For the past three years, I’ve been writing and speaking about Meteor security, building and deploying secure Meteor applications, working with amazing teams to better secure their applications, and building security-focused packages and tools for the Meteor ecosystem.

Needless to say, I’ve learned a lot over that time.

I’m excited to announce that I’ve started work on a new project called Secure Meteor in an attempt to capture and distill everything I know about Meteor security into an easily understandable, actionable guide to building secure Meteor applications.

Secure Meteor is still very much in its early days, so there’s not much to share yet. That said, as a teaser and a token of thanks for showing interest in the project, I want to give you the most detailed Meteor security checklist available anywhere, for free!

Be sure to sign up to receive your free security checklist, and let me know what you’d like to see in the project.

Bitcoin's Base58Check in Pure Elixir

Written by Pete Corey on Jan 8, 2018.

An important piece of the process of transforming a Bitcoin private key into a public address, as outlined in the fantastic Mastering Bitcoin book, is the Base58Check encoding algorithm.

The Bitcoin wiki has a great article on Base58Check encoding, and even gives an example implementation of the underlying Base58 encoding algorithm in C.

This algorithm seems especially well-suited to Elixir, so I thought it’d be a fun and useful exercise to build out Base58 and Base58Check modules to use in future Bitcoin and Elixir experiments.

Like Base64, but Less Confusing

Base58 is a binary-to-text encoding algorithm that’s designed to encode a blob of arbitrary binary data into human readable text, much like the more well known Base64 algorithm.

Unlike Base64 encoding, Bitcoin’s Base58 encoding algorithm omits characters that can be potentially confusing or ambiguous to a human reader. For example, the characters O and 0, or I and l can look similar or identical to some readers or users of certain fonts.

To avoid that ambiguity, Base58 simply removes those characters from its alphabet.

Shrinking the length of the alphabet we map our binary data onto from sixty four characters down to fifty eight characters means that we can’t simply group our binary into six-bit chunks and map each chunk onto its corresponding letter in our alphabet.

Instead, our Base58 encoding algorithm works by treating our binary as a single large number. We repeatedly divide that number by the size of our alphabet (fifty eight), and use the remainder of that division to map onto a character in our alphabet.

Implementing Base58 in Elixir

This kind of algorithm can neatly be expressed in Elixir. We’ll start by creating a Base58 module and adding our alphabet as a module attribute:

defmodule Base58 do
  @alphabet '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

Inside our Base58 module, we’ll define an encode/2 function. If we pass encode a binary, we want to convert it into a number using Erlang’s :binary.decode_unsigned:

def encode(data, hash \\ "")
def encode(data, hash) when is_binary(data) do
  encode(:binary.decode_unsigned(data), hash)

Once converted, we pass our binary-come-number into a recursive call to encode/2 along with the beginning of our hash, an empty string.

For each recursive call to encode/2, we use div and rem to divide our number by 58 and find the reminder. We use that remainder to map into our @alphabet, and prepend the resulting character onto our hash:

def encode(data, hash) do
  character = <<, rem(data, 58))>>
  encode(div(data, 58), hash <> character)

We’ll continue recursing until we’ve divided our data down to 0. In that case, we’ll return the hash string we’ve built up:

def encode(0, hash), do: hash

This implementation of our Base58 encoded mostly works. We can encode any text string and receive correct results:

iex(1)> Base58.encode("hello")

Encoding Leading Zeros

However when we try to encode binaries with leading zero bytes, those bytes vanish from our resulting hash:

iex(1)> Base58.encode(<<0x00>> <> "hello")

That zero should become a leading "1" in our resulting hash, but our process of converting the initial binary into a number is truncating those leading bytes. We’ll need to count those leading zeros, encode them manually, and prepend them to our final hash.

Let’s start by writing a function that counts the number of leading zeros in our initial binary:

defp leading_zeros(data) do
  |> Enum.find_index(&(&1 != 0))

We use Erlang’s :binary.bin_to_list to convert our binary into a list of bytes, and Enum.find_index to find the first byte in our list that isn’t zero. This index value is equivalent to the number of leading zero bytes in our binary.

Next, we’ll write a function to manually encode those leading zeros:

defp encode_zeros(data) do
  <<, 0)>>
  |> String.duplicate(leading_zeros(data)) 

We simply grab the character in our alphabet that maps to a zero byte ("1"), and duplicate it as many times as we need.

Finally, we’ll update our initial encode/2 function to prepend these leading zeros onto our resulting hash:

def encode(data, hash) when is_binary(data) do
  encode_zeros(data) <> encode(:binary.decode_unsigned(data), hash)

Now we should be able to encode binaries with leading zero bytes and see their resulting "1" values in our final hash:

iex(1)> Base58.encode(<<0x00>> <> "hello")


Base58 + Checksum = Base58Check

Now that we have a working implementation of the Base58 encoding algorithm, we can implement our Base58Check algorithm!

Base58Check encoding is really just Base58 with an added checksum. This checksum is important to in the Bitcoin world to ensure that public addresses aren’t mistyped or corrupted before funds are exchanged.

At a high level, the process of Base58Check encoding a blob of binary data involves hashing that data, taking the first four bytes of the resulting hash and appending them to the end of the binary, and Base58 encoding the result.

We can implement Base58Check fairly easily using our newly written Base58 module. We’ll start by creating a new Base58Check module:

defmodule Base58Check do

In our module, we’ll define a new encode/2 function that takes a version byte and the binary we want to encode:

def encode(version, data)

Bitcoin uses the version byte to specify the type of address being encoded. A version byte of 0x00 means that we’re encoding a regular Bitcoin address to be used on the live Bitcoin network.

The first thing we’ll need to do is generate our checksum from our version and our data. We’ll do that in a new function:

defp checksum(version, data) do
  version <> data
  |> sha256
  |> sha256
  |> split

We concatenate our version and data binaries together, hash them twice using a sha256/1 helper function, and then returning the first four bytes of the resulting hash with a call to split/1.

split/1 is a helper function that pulls the first four bytes out of the resulting hash using binary pattern matching:

defp split(<< hash :: bytes-size(4), _ :: bits >>), do: hash

Our sha256/1 helper function uses Erlang’s :crypto.hash function to SHA-256 hash its argument:

defp sha256(data), do: :crypto.hash(:sha256, data)

We’ve wrapped this in a helper function to facilitate Elixir-style piping.

Now that we have our four-byte checksum, we can flesh out our original encode/2 function:

def encode(version, data) do
  version <> data <> checksum(version, data)
  |> Base58.encode

We concatenate our version, data, and the result of our checksum function together, and Base58 encode the result. That’s it!

Base58Check encoding our "hello" string with a version of <<0x00>> should give us a result of "12L5B5yqsf7vwb". We can go further and verify our implementation with an example pulled from the Bitcoin wiki:

iex(1)> Base58Check.encode(<<0x00>>, 
    <<0x01, 0x09, 0x66, 0x77, 0x60,
      0x06, 0x95, 0x3D, 0x55, 0x67,
      0x43, 0x9E, 0x5E, 0x39, 0xF8, 
      0x6A, 0x0D, 0x27, 0x3B, 0xEE>>)


Wrapping Up

If you’d like to see both modules in their full glory, I’ve included them in my hello_bitcoin repository on Github. Here’s a direct link to the Base58 module, and the Base58Check module, along with a simple unit test. If that repository looks familiar, it’s because it was used in a previous article on controlling a Bitcoin node with Elixir.

I highly suggest you read through Andreas Antonopoulos’ Mastering Bitcoin book if you’re at all interested in how the Bitcoin blockchain works, or Bitcoin development in general. His book has been my primary source of inspiration and information for every Bitcoin article I’ve written to date.