BTC#: Base 58 Encoded Addresses

Series: BTC# – Learning to Program Bitcoin in C#

« Previous: DER Serialisation

Next: Transaction Objects »


Base 58 Encoding

Base 58 encoding is a compromise between hexadecimal, i.e. base 16 encoding, which can store 4 bits per character, and base 64 encoding, which can store 6 bits per character but can be confusing for humans to read. Base 58 tries to remove the confusion by eliminating characters that get mixed up, like O and 0 and 1 and l.

Base 58 seems slightly unnatural because we’ve worked in base 10 since we were toddlers and in powers to two – base 2, base 16, and base 64 – since we learned about computers. Despite feeling odd, the principle is the same. We cycle through a 58-character alphabet and when that loops over we move to the next column. Instead of ones, tens, and hundreds, we have ones, 58s, and 58-squareds, etc.

The Base 58 alphabet is held as a constant on the Serialisation class.

public const string BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";

That class also contains the method to encode.

public static string EncodeAsBase58(byte[] buffer)
    var base58 = new StringBuilder();
    var leadingZeroesCount = buffer.TakeWhile(b => b == 0).Count();
    base58.Append('1', leadingZeroesCount);

    var number = buffer.ToBigInteger(ByteArrayFormat.BigEndianUnsigned);
    var base58CharBuffer = new StringBuilder(); //Least significant character first
    while (number > 0)
        number = number / 58;

    return base58.ToString();

The algorithm involves converting the byte array to a BigInteger and then repeatedly dividing by 58 and using the remainders to look up the character to go into each column. In contrast to the code in the book, the base 58 string here is built up in the opposite order, so needs to be reversed once we have all the characters.

Base 58 with Checksum

Part of the motivation for base 58 encoding was to allow addresses to be copied by hand, from printed text, or read out loud. To help prevent mistakes, the Base58Check format is used. This hashes the data prior to encoding and adds the first four bytes of the hash at the end. If there’s an error in copying, the hash won’t match and an address with a type in it won’t be accepted as valid.

The method for Base58Check is also on the serialisation class. The Base58CheckType enum indicates which leading bytes should be added based on what’s being encoded.

public static string EncodeAsBase58Check(byte[] addressBytes, Base58CheckType type)
    var prefix = GetBase58CheckPrefix(type);
    var prefixedLength = prefix.Length + addressBytes.Length;
    var addressBuffer = new byte[prefixedLength + 4];

    prefix.CopyTo(addressBuffer, 0);
    addressBytes.CopyTo(addressBuffer, prefix.Length);

    var checkBytes = Hash.DoubleSha256(addressBuffer.Segment(0, prefixedLength)).Segment(0, 4);
    checkBytes.CopyTo(addressBuffer, prefixedLength);

    return EncodeAsBase58(addressBuffer);

The method generates the checksum hash, concatenates all the pieces and then base-58 encodes the result.

Addresses and Hash 160

Bitcoin payments could be made to public keys but generally are not. The more common payment destination is an address derived from the public key.

Addresses are shorter than SEC format public keys because they take the SEC formatted public key and hash it using SHA-256 followed by another hash algorithm called RIPEMD-160, which results in 20 bytes rather than 33. This combination is known as Hash160.

I’ve created a Hash160 implementation on the static Hash class using C#’s implementation of RIPEMD-160.

public static byte[] Hash160(byte[] buffer)
    var sha256 = SHA256.Create();
    var ripemd160 = RIPEMD160.Create();
    return ripemd160.ComputeHash(sha256.ComputeHash(buffer));

The code to create a Bitcoin address is on the PublicKey class. It’s very short as it simply composes pieces we’ve already looked at.

public string ToBase58Address(SerialisationFormat format)
    var type = (format & SerialisationFormat.TestNet) > 0 ?
        Base58CheckType.TESTNET_ADDRESS : Base58CheckType.MAINNET_ADDRESS;
    return Serialisation.EncodeAsBase58Check(Hash.Hash160(ToSecFormat(format)), type);

There’s a header byte to indicate which network the address belongs to and then a Base58Check encoded hash of the SEC formatted public key.

Wallet Import Formatted Private Keys

Wallet Import Format (WIF) is used to serialise private keys. Generally you don’t want to do this because the keys should remain secret and they shouldn’t be left lying around. There are times, though, when you may need to transfer them from one place to another.

WIF is very similar to address format we saw above with the header bytes and the Base58Check encoding.

Prior to now, the private key was stored as a raw number inside a KeyPair object but, since were adding behaviour, I’ve broken it out into its own class.

public PrivateKey(BigInteger secret)
    Secret = secret;

public string ToWifFormat(SerialisationFormat format)
    var type = (format & SerialisationFormat.TestNet) > 0 ?
        Base58CheckType.PRIVATE_KEY_WIF_TESTNET : Base58CheckType.PRIVATE_KEY_WIF;
    var wifBuffer = Secret.ToByteArray(ByteArrayFormat.BigEndianUnsigned, 32);
    if ((format & SerialisationFormat.Compressed) > 0)
        wifBuffer = wifBuffer.Concat(new byte[] { 1 }).ToArray();
    return Serialisation.EncodeAsBase58Check(wifBuffer, type);

That’s the end of the scaffolding work. We have all the maths and the serialisation code in place. The next chapters get into Bitcoin transactions and scripting.

« Previous: DER Serialisation

Next: Transaction Objects »

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s