Computing a Bitcoin Address, Part 2: Public Key to (Hex) Address
In a previous post, we derived a Bitcoin public key from a private key. This post explores how to convert that public key into a (hexadecimal) Bitcoin address. I’ll be using the Racket language to help me.
This is the second post in a four-part series titled “Computing a Bitcoin Address”. Here are all the articles in the series:
- Part 1: Private to Public Key
- Part 2: Public Key to (Hex) Address (this post)
- Part 3: Base58Check Encoding
- Part 4: Wallet Import Format (WIF)
To convert from a public key to a Bitcoin address, we need an implementation of the SHA–256 and RIPEMD–160 hash functions. Racket doesn’t come with these functions but we can easily call to OpenSSL’s implementation of these functions via Racket’s C FFI.
Conveniently, the standard Racket distribution already defines a hook into the libcrypto
library, also named libcrypto
. Racket comes with wrapper functions for some libcrypto
C functions, but not SHA256
or RIPEMD160
so let’s create those.
Here’s the header for the SHA256
C function:
1 |
unsigned char *SHA256( const unsigned char *d, size_t n, unsigned char *md ); |
We use the Racket get-ffi-obj
function to create a Racket wrapper for SHA256
. Here’s a Racket sha256
function that calls the C SHA256
function:
1 2 3 4 5 6 7 8 |
The first argument to get-ffi-obj
is the name of the C function and the second argument is the hook into the appropriate library. The third argument is the type, which specifies how to mediate between Racket and C values. _fun
is the function type and in this case the function has three arguments (each delimited with brackets by convention).
Examining the types of the arguments:
-
The first argument to the
SHA256
C function is an array of input bytes. Accordingly, we giveget-ffi-obj
a_bytes
type for this argument. -
The second argument is the length of the input byte array. The
=
and the expression following it describe how to calculate this argument automatically. Thus a caller ofsha256
does not provide this argument. -
The third argument is the output byte array. The
o
indicates a return pointer and is followed by the expected length of the output array, which should be 32 bytes here. We define a constantSHA256-DIGEST-LEN
which is analogous to theSHA256_DIGEST_LENGTH
constant in the C library.
Similarly, here’s a definition for a Racket ripemd160
function:
1 2 3 4 5 6 7 8 9 10 |
(define RIPEMD160-DIGEST-LEN 20) ; bytes ; from crypto/ripemd/ripemd.h: ; unsigned char *RIPEMD160(const unsigned char *d, size_t n, unsigned char *md); (define ripemd160 (get-ffi-obj 'RIPEMD160 libcrypto (_fun [input : _bytes] [input-len : _ulong = (bytes-length input)] [output : (_bytes o RIPEMD160-DIGEST-LEN)] -> (_bytes o RIPEMD160-DIGEST-LEN)))) |
Testing
To test our wrapper functions, let’s see if we can duplicate this example from the Bitcoin wiki, which shows how to convert a Bitcoin private key into a public address. We covered how to derive a public key from a private key in a previous post, so we start with the public key here.
For ease of comparison, here’s the sequence of expected hashes, copied from the Bitcoin wiki example:
- public key:
0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6
- SHA–256:
600FFE422B4E00731A59557A5CCA46CC183944191006324A447BDB2D98D4B408
- RIPEMD–160:
010966776006953D5567439E5E39F86A0D273BEE
- prepend
0x00
:00010966776006953D5567439E5E39F86A0D273BEE
- SHA–256:
445C7A8007A93D8733188288BB320A8FE2DEBD2AE1B47F0F50BC10BAE845C094
- SHA–256 (checksum is first 4 bytes):
D61967F63C7DD183914A4AE452C9F6AD5D462CE3D277798075B107615C1A8A30
- step 4 result + checksum = (hex) address:
00010966776006953D5567439E5E39F86A0D273BEED61967F6
The hashes are all in hexdecimal form so we extend our hash functions to convert to and from hex strings (bytes->hex-string
and hex-string->bytes
are built-in Racket functions):
1 2 3 4 5 |
Now we can duplicate the sequence of hashes from the example, using the code from this post (saved to a file crypto.rkt
) and the Racket (extended) REPL (the ^
token in the REPL represents the last printed result):
$ racket
Welcome to Racket v6.0.0.3.
-> (require "crypto.rkt")
-> (define pub-key "0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6")
-> (sha256/hex pub-key)
"600ffe422b4e00731a59557a5cca46cc183944191006324a447bdb2d98d4b408"
-> (ripemd160/hex ^)
"010966776006953d5567439e5e39f86a0d273bee"
-> (string-append "00" ^)
"00010966776006953d5567439e5e39f86a0d273bee"
-> (define version0+hash160 ^)
-> (sha256/hex ^)
"445c7a8007a93d8733188288bb320a8fe2debd2ae1b47f0f50bc10bae845c094"
-> (sha256/hex ^)
"d61967f63c7dd183914a4ae452c9f6ad5d462ce3d277798075b107615c1a8a30"
-> (substring ^ 0 8) ; checksum
"d61967f6"
-> (string-append version0+hash160 ^)
"00010966776006953d5567439e5e39f86a0d273beed61967f6"
The final result is the Bitcoin address from the example, in hexadecimal format. The Bitcoin wiki article performs one more step to convert to Base58Check encoding, which is the standard representation for Bitcoin addresses. We’ll look at Base58Check encoding in the next post!
Software
All the code from this post is available here. In this post, I’m using OpenSSL 1.0.1e with Racket 6.0.0.3, running in Debian 7.0.