Using Base58
Base58 is unlike the other encodings in this library: its radix (58) is not a power of two, so it cannot use
the bit-stream technique that Base16, Base32, and Base64 share. Instead the implementation treats the input as a
big-integer and repeatedly divides by 58 to extract digits.
Base58's defining feature is that its alphabet excludes the four visually ambiguous characters 0, O, I, and
l. This makes it suitable for any context where a user might transcribe the encoded form by hand - Bitcoin
addresses, IPFS CIDs, Solana / Stellar identifiers, Flickr short URLs.
input bytes : 00 EB 15 23 1D FC EB 60 92 58 86 B6 7D 06 52 99 92 59 15 AE B1 72 C0 66 47
(Bitcoin payload)
┌── leading 0x00 → leading '1' character ──┐
│ remaining 24 bytes → big-integer encode │
encoded text : 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L (Bitcoin/Flickr)
Quick reference
using Bodu.Text.Encoding;
byte[] payload = Convert.FromHexString("00EB15231DFCEB60925886B67D065299925915AEB172C06647");
// Bitcoin/Flickr alphabet (default)
string address = Base58.Encode(payload);
// "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"
// Decode back
byte[] back = Base58.Decode(address);
// Ripple alphabet (XRP ledger)
string rippled = Base58.Encode(payload, Base58Variant.Ripple);
Variant comparison
| Variant | Alphabet ordering | Used by |
|---|---|---|
BitcoinFlickr (default) |
Digits ascending, then upper-case letters (minus O, I), then lower-case letters (minus l) |
Bitcoin, IPFS, Solana, NEAR, Stellar, Flickr |
Ripple |
Permuted ordering specific to the XRP ledger | Ripple / XRP |
Both variants use the same 58 characters - only the order differs, so a string encoded under one variant decodes to different bytes under the other.
Leading zeros
Because Base58 uses big-integer arithmetic, leading zero bytes would normally be lost. The Bitcoin convention
preserves them as leading 1 characters in the encoded form (where 1 is alphabet[0]):
| Input bytes | Encoded |
|---|---|
0x00 |
1 |
0x00 0x00 |
11 |
0x00 0x01 |
12 |
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 |
1111111111 |
This is the rule that makes Bitcoin P2PKH addresses (which always start with the mainnet version byte 0x00)
always start with 1 in their encoded form.
// Round trip preserves leading zero bytes exactly:
byte[] withZeros = new byte[] { 0x00, 0x00, 0xDE, 0xAD };
string encoded = Base58.Encode(withZeros); // "115Q"
byte[] back = Base58.Decode(encoded); // back.SequenceEqual(withZeros)
Excluded characters
The decoder rejects the four visually ambiguous characters that the alphabet intentionally omits:
Base58.Decode("0Ajdvzr"); // FormatException - '0' (zero) excluded
Base58.Decode("OAjdvzr"); // FormatException - 'O' (capital O) excluded
Base58.Decode("IAjdvzr"); // FormatException - 'I' (capital I) excluded
Base58.Decode("lAjdvzr"); // FormatException - 'l' (lower L) excluded
This is intentional - a user transcribing 1NS17 might write lNS17 or INS17, and the decoder rejecting those
inputs catches the typo at the input boundary rather than silently producing different bytes.
Lenient parsing
| Flag | Effect |
|---|---|
BaseFormatStyles.IgnoreWhitespace |
Strip ASCII space / tab / CR / LF anywhere |
BaseFormatStyles.AllowPrefix |
No-op for Base58 |
BaseFormatStyles.AllowMissingPadding |
No-op for Base58 (no padding character) |
// User-pasted address with stray spaces - strip and decode
byte[] payload = Base58.Decode(
" 1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L ",
Base58Variant.BitcoinFlickr,
BaseFormatStyles.IgnoreWhitespace);
Span path
// Predict an upper bound (exact length is data-dependent for Base58)
int maxChars = Base58.GetMaxEncodedLength(payload.Length);
char[] buffer = new char[maxChars];
int written = Base58.Encode(payload, buffer);
ReadOnlySpan<char> result = buffer.AsSpan(0, written);
Base58 uses big-integer arithmetic, so there is no OperationStatus streaming path: each operation requires the
entire input to be available. The EncodeToUtf8 / DecodeFromUtf8 methods exist for API consistency with the
other encodings but behave as a single-shot transform.
Validation and sizing
Base58.IsValid("1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"); // true
Base58.IsValid("0NS17iag"); // false ('0' excluded)
Base58.IsBase58Digit('1'); // true
Base58.IsBase58Digit('0'); // false
Base58.GetMaxEncodedLength(25); // ≈ 35 (upper bound)
Base58.GetMaxDecodedLength(34); // ≈ 25 (upper bound)
Exact encoded length depends on the actual data (specifically the leading-zero count and the magnitude of the
non-zero portion), so the library only exposes GetMaxEncodedLength / GetMaxDecodedLength. Code that needs the
exact length can call Encode / Decode and use the result's length.
Common patterns
Bitcoin P2PKH address payload extraction
// payload = [version byte][20-byte HASH160][4-byte checksum]
byte[] payload = Base58.Decode(address);
byte version = payload[0];
ReadOnlySpan<byte> hash160 = payload.AsSpan(1, 20);
ReadOnlySpan<byte> checksum = payload.AsSpan(21, 4);
(The example above decodes the raw payload and leaves checksum verification to the caller. For checksum-protected payloads, prefer Base58Check below, which appends and verifies the 4-byte double-hash checksum for you.)
Round-trip short ID
byte[] randomBytes = RandomNumberGenerator.GetBytes(8);
string shortId = Base58.Encode(randomBytes); // ≈ 11 characters, no ambiguity
Encode a GUID as an ambiguity-free token
Base58 encodes a Guid directly - a ~22-character token with none of the visually confusable
0/O/I/l characters, ideal for an identifier a human might read aloud or transcribe:
Guid id = Guid.NewGuid();
string token = Base58.Encode(id); // no encode-side options - Base58 has no formatting flags
Guid back = Base58.DecodeGuid(token); // FormatException unless it decodes to 16 bytes
bool ok = Base58.TryDecodeGuid(token, out Guid parsed);
The 16 bytes are the GUID's native mixed-endian layout (matching Guid.TryWriteBytes), so the round trip is exact.
Because Base58 preserves leading-zero bytes as leading 1 characters, a GUID whose first bytes are zero still
round-trips losslessly.
Base58Check - checksum-protected payloads
Base58Check wraps Base58 with the Bitcoin-style 4-byte checksum: Encode appends a
truncated double-hash of the payload, and Decode verifies it (throwing on a corrupted string) before returning the
original bytes. This is the right entry point for address- and key-style payloads where a single mistyped character
must be rejected rather than silently decoded.
using Bodu.Text.Encoding;
string encoded = Base58Check.Encode(payload); // payload + checksum, Base58
byte[] decoded = Base58Check.Decode(encoded); // verifies, then strips the checksum
bool valid = Base58Check.IsValid(encoded); // non-throwing validity probe
Like the core type, it offers a span path and explicit sizing, and accepts a Base58Variant
(default BitcoinFlickr):
Span<byte> destination = stackalloc byte[Base58Check.GetMaxDecodedLength(encoded.Length)];
if (Base58Check.TryDecode(encoded, destination, out int written))
Use(destination[..written]);
Where to go next
- Base32 guide - when you want human-friendly encoding but power-of-two radix.
- Base85 guide - when you need the densest possible ASCII-safe encoding.
IBinaryEncodinginterface - runtime-selected encoding choice.- Text & Serialization guides - every guide in this topic, across Bodu.Text.Encoding, Bodu.Text.Formats, and the Bencode / TOML serializers.