Pay to public key

Previously, we covered what is a Bitcoin address and what properties it has.

Today, we will start looking into the insanity that is technical details.

To recap, the address is calculated from your public key. We need the address to be uniquely tied to the public key – two public keys must not have the same address. This way we can verify that only the owner of the private key, and nobody else, has rights to the address.

But how do we do that?

First idea: let’s just base58-encode the public key and be done with it!

Nice and simple. And it actually exists, kind of. It is called P2PK, or Pay to Public Key. But it was never really used for text addresses, because…

Problem: the public key is 33 bytes long. Its encoding is 50 characters. That’s not great.

Solution: hash the public key with RIPEMD-160, so the result is 20 bytes, and the address is 33 characters.

In addition to being significantly shorter, it also hides the public key. While we don’t technically need to hide the public key (it is public after all), it is nice to have.

So yeah, let’s just…

Problem: what if someone breaks RIPEMD-160?


Solution: hash the public key with SHA-256, then hash the result with RIPEMD-160, so the address is still 33 characters.


And we are done

This is called P2PKH, or Pay to Public Key Hash. When verifying the transaction, the payer provides the signature and their public key. The network checks that the signature is valid, and that the public key, hashed with SHA-256 and then RIPEMD-160, matches the address.

The choice to chain two hash functions is … not bad or wrong or anything. It is just weird.

Are we done though?

Not by a long shot.

You might have noticed that there are already two different “address formats”: P2PK and P2PKH. We need to somehow distinguish between the two when validating the transaction.

As it turns out, those two are not the only ones. But that is for next time.

What is a Bitcoin address?

An address is a piece of text that encodes some data. This data has a very interesting property: you can prove that you own it.

For that to work, you can’t just choose an address. You need to calculate it in a particular way. First, create a pair of keys:

  • A private key is 32 bytes of (basically) random data. You need to keep it secret.
    32 bytes, or 256 bits, is a lot. Enough that, if you generated your key randomly, it is virtually impossible for someone else to stumble upon the exact same data, ever. The key is the only copy of these particular 32 bytes in the world.
  • A public key is 33 bytes of data, which is calculated from the private key. It is impossible to figure out the private key from the public key, so you can show the public key to anyone. This will be important soon.

This key pair allows you to sign data. You can use the private key to generate a signature over some data, say, a transaction. Anyone who knows the public key can take the signature and verify that it was really created by you, the owner of the private key.

From the public key, you calculate your address. There is no registration step. As soon as you know your address, you can give it out to people and they can send Bitcoins to it.

The only way to create that particular address is to calculate it from your public key – and the only way to create that particular public key is to calculate it from your private key. Because you have the only copy of the private key, it must have been you who created the address.

When you want to spend your Bitcoins, you create a transaction: “send X Bitcoins from address MyAddress (public key MyPubKey) to address SomeOtherAddress“. Then you sign this transaction using your private key.

The network checks that the signature matches the public key in the transaction, and that the public key matches the address from which it is spending. This proves ownership: the person who has the private key to this address really did authorize this transaction.

EOS's creative use of base58check

In cryptocurrency world, the base58 encoding is pretty well known. It is also conceptually very simple: just take a byte array, interpret is as a big-endian number, and convert that number to base 58.

Why 58 in particular? Let’s look at the character set: it’s all upper-case and lower-case latin letters, plus decimal digits. That is 26 lower-case, 26 upper-case, 10 digits, for a total of 64. Now remove l and I and O and 0 because they get confused easily, and you are down to 58.

By the standards of this blog, this is an extremely smart, reasonable and well thought out idea. It does have the curious property that zero is encoded as 1 (because we removed 0), but that’s nothing we actually care about.

It’s also somewhat impractical: given that 256 and 58 are not divisible by each other, you can’t just cut-and-paste parts of the base58 representation as if you were cutting and pasting bytes. base58(x + y) != base58(x) + base58(y) for any x and y. But, again, we are not doing that, so we don’t mind.

With that, enter base58check: take the byte array you want to encode, calculate SHA-256 of it, take first four bytes as a checksum, append to data, encode. Like this:

def base58check_encode(data):
sha = hashlib.sha256(data).digest()
return base58_encode(data + sha[:4])

Now if a user types the thing wrong, the checksum will fail and you can tell them. Nice, simple, very reasonable.

That’s why folks at EOS decided to shake it up.

Firstly, SHA-256 is not good enough for EOS. Instead we’ll use RIPEMD-160. Now, I have nothing against RIPEMD-160, except there is no reason whatsoever to not use SHA-256, but, OK, you do you. What comes next is worse.

EOS signature looks like this: SIG_K1_KirzetKmDKt1Rbf4k(...). The SIG means signature, K1 is one of two supported curves, and the alphabet soup is base58check-encoded data.

So you take the K1, and add it to the checksum calculation.

Not to the data. No. To the checksum calculation only.

So we can’t use the function given above, because the checksum is taken from something other than the encoded data!!

I’m not even talking about how you take the prefix and add it at end. Like this:

def base58check_eos_encode(data, curve_type):
checksum = ripemd160(data + curve_type).digest()
return base58_encode(data + checksum[:4])

I am … not sure what the goal was here. Tie the curve type to the base58 represenation of signature data? So that the user can’t change SIG_K1_ to SIG_R1_ just like that? But then whyyyyy would you not add the curve name at start of the data? Like, base58check_encode(b"K1" + signature)? Then you could even leave it out of the readable prefix and just send SIG_lkasjdfasdoi, right?

For a bit of more fun, there are two formats of public keys: EOSe3LkLKEJSf(...), which is normal base58check, and PUB_K1_e3LkLKEJSf(...), where the K1 goes into the checksum. That’s the only difference, so the base58 string will be mostly the same, except for the checksum at end, so you can’t simply overwrite EOS with PUB_K1_. Because $deity forbid we allow users to simply overwrite the prefix and get, well, the exact same thing in a newer format.

And this is what this blog is about. There are dozens of ways you can go with this combination of formats. You could even choose a fixed prefix for the data so that the Base58 representation starts with ascii string K1 - Bitcoin-like coins do this to get distinct addresses. You could keep base58check as-is and it would be fine.

But no. Out of the countless reasonable options, you had to go and do … well … this.