How the encoders work
Pick an encoding, type something, and watch the bytes turn into characters. The four mechanisms below are the whole library — every one of the thirteen builds on one of them.
Try it
Four mechanisms
An encoding answers one question: how do I write this byte, or this run of bits, using only characters from a small alphabet? moonbase answers it in exactly four ways, and which one an encoding uses decides everything about how it handles a leading zero, a short tail, and a string you can stream.
Bit packing
Read the bytes as one long bit string, then cut it into fixed-width groups — 1, 3, 4, 5 or 6 bits. Each group is an index into the alphabet. The trailing group is shifted up and zero-filled.
Repeated division
Read the bytes as one big integer, then divide by the base again and again. Each remainder is a digit, read off from the last division to the first.
One number a group
Two or four bytes at a time become one number, written in base 45 or 85 with a fixed number of digits. Short, local, and cheap to decode on a small device.
Substitution and checksum
base256emoji is a 256-entry table, one character a byte, with no arithmetic at all. bech32 reads five bits a group, then appends a BCH checksum over the prefix and the data.
Bit packing
base16 takes 4 bits at a time, so one byte is exactly two characters and a leading
0x00 stays "00". base32 takes 5 bits, base64 takes 6, base2 takes
one and base8 takes three. The leftover bits of the last group are shifted up and padded
with zeros; base32 and base64 then pad the output with = to a full group, and
base2 and base8 cannot, because their groups divide a byte exactly.
This is why a base64 string of b"moonbase" is bW9vbmJhc2U= —
eleven characters of payload, one = of padding. The live
demo shows the finished strings; this page shows the groups.
Because the group never crosses a byte boundary in base2, base8 and base16, those three are the ones a stream can encode as it reads: nothing has to wait for the next chunk.
Repeated division
base10, base36, base58 and base62 read the whole byte string as one big-endian integer. To write it in base N, divide by N and keep the remainder; repeat on the quotient until it reaches zero. The remainders, reversed, are the digits.
Leading zero bytes are special: an integer has no leading zeros, so the encoder counts
them first and writes that many zero digits up front. That is why
b"\x00\x00\x01" in base58 is 112 — two leading 1s
from the zeros, then the 1 itself.
This is also why these four have no streaming face: the first digit depends on the last
byte, so there is nothing to hand back until the input ends. The
encode_int functions take the same path on a UInt64, and
encode_big on a BigInt, skipping the byte step entirely.
One number a group
base45 and base85 sit between the two: they do not read the whole payload as one integer, and they do not cut it into bits. They take a fixed number of bytes at a time and write that group's value in a larger base.
base45 takes two bytes — a number below 65536 — and writes three base-45 digits, least significant first. Three digits of base 45 are more than a QR code's alphanumeric mode needs, which is the point: the characters are all ones that mode can hold, so the payload fits in a smaller symbol. There is no padding; an odd final byte is two digits.
base85 takes four bytes and writes five base-85 digits, most significant first. Adobe's
Ascii85 also lets a zero group be written as a single z, and shortens the last
group to n + 1 characters; ZeroMQ's Z85 refuses a tail that is not a whole
group, because its frame is four bytes or nothing.
A checksum
bech32 is the one encoding here that carries its own integrity check. The payload is regrouped from eight bits to five, the human-readable prefix is expanded into its high bits and low bits, and a BCH code over both produces six more characters. A single typo anywhere — including in the prefix, which says which network an address belongs to — fails that code and is reported at the index where the reader stopped believing.
BIP 173 and BIP 350 differ only in one constant of that code: witness version 0 uses
Bech32, every later version uses Bech32m, and no string is valid
under both. That is a rule about addresses, not about bech32 — the library will happily
write either, which is why the choice is a named argument.