intermediate 45 min

Base58Check encoding

How XRPL encodes account IDs, keys and seeds as human-readable strings with a checksum and type byte.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Explain the Base58Check format and its checksum.
  • Map TokenType values to address prefixes (r, s, n…).
  • Encode and decode account IDs with `encodeBase58Token`.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈45 min · Intermediate · builds on Secure memory handling

That familiar r... address is more than a random string, it's a carefully encoded value with a built-in typo detector. In this module you'll learn the Base58Check format, how its checksum catches errors, and how the type byte decides whether you see an r, s or n prefix. Encode and decode an address yourself and it'll never look opaque again.

The Problem with Raw Binary

In brief: raw bytes are error-prone to type and copy, so humans need a safer encoding.

Consider an account ID in different formats:

Binary (20 bytes):
10001011 10001010 01101100 01010011 00111111 ...

Hexadecimal:
8B8A6C533F09CA0E5E00E7C32AA7EC323485ED3F

Base58Check:
rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C

Problems with hex:

  • Easy to mistype: 8B8A vs 8B8B
  • Visually similar characters: 0 (zero) vs O (letter O)
  • No error detection: One wrong character, wrong address
  • Not compact: 40 characters for 20 bytes

Base58Check solutions:

  • Excludes confusing characters
  • Includes checksum (detects errors)
  • More compact: 34 characters for 20 bytes + checksum
  • URL-safe (no special characters)

The Base58 Alphabet

In brief: 58 characters chosen to drop look-alikes such as 0/O and I/l.

// From src/libxrpl/protocol/tokens.cpp (kAlphabetForward)
static const char* BASE58_ALPHABET =
    "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";

Watch out. Base58 is a family of encodings: the algorithm (divide by 58, map each remainder to a character) is universal, but the alphabet (which character represents which value) is chosen per project. XRPL's alphabet above is not the Bitcoin alphabet (123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz), it is a different permutation of the same 58 characters. Encode the same bytes with the wrong alphabet and you get a string that looks plausible but decodes to nothing meaningful on the real network.

Excluded characters (true of both Bitcoin's and XRPL's alphabet, only the order differs):

0 (zero)        - Looks like O (letter O)
O (letter O)    - Looks like 0 (zero)
I (letter I)    - Looks like l (lowercase L) or 1
l (lowercase L) - Looks like I (letter I) or 1

These exclusions prevent human transcription errors.

Included: 58 characters, XRPL's order

9 digits:      3 9 4 7 2 6 5 8 1
24 uppercase:  B U D N E G H J K L M P Q R S T V W X Y Z C F A
25 lowercase:  r p s h n a f w b c d e g j k m o q i t u v x y z
Total:         58 characters

Base58 Encoding Algorithm

Base58 is like converting a number to a different base (like hexadecimal is base 16):

Decimal:    255 = 2×100 + 5×10 + 5×1
Hex:        FF  = 15×16 + 15×1
Base58:     4k  = 4×58 + 45×1

The Mathematics

Handling Leading Zeros

// Special case: preserve leading zero bytes as ALPHABET[0] characters
// (that's 'r' for XRPL's alphabet, not '1' as in Bitcoin's)
for (uint8_t byte : input) {
    if (byte == 0)
        result = BASE58_ALPHABET[0] + result;
    else
        break;
}

This ensures the encoding is one-to-one: every distinct byte sequence produces a distinct string.

Implementation

Base58Check: Adding Error Detection

In brief: append a double-SHA-256 checksum so typos are caught before use.

Base58Check end to end: the 20-byte payload gains a version prefix, then a 4-byte double-SHA-256 checksum, then becomes the r-address; decoding walks back up and recomputes the checksum, so one typo is a hard error.

Base58 alone doesn't detect errors. Base58Check adds a checksum:

Structure:
[Type Byte] [Payload] [Checksum (4 bytes)]
     ↓          ↓           ↓
   0x00     20 bytes    SHA256(SHA256(prefix + payload))

Encoding Process

Token Types

enum class TokenType : std::uint8_t {
    None            = 1,
    NodePublic      = 28,   // Node public keys:  starts with 'n'
    NodePrivate     = 32,   // Node private keys
    AccountID       = 0,    // Account addresses: starts with 'r'
    AccountPublic   = 35,   // Account public keys: starts with 'a'
    AccountSecret   = 34,   // Account secret keys (deprecated)
    FamilySeed      = 33,   // Seeds: starts with 's'
};

The type byte determines the first character of the encoded result:

Type 0  (AccountID)     → starts with 'r'
Type 33 (FamilySeed)    → starts with 's'
Type 28 (NodePublic)    → starts with 'n'
Type 35 (AccountPublic) → starts with 'a'

This provides visual identification of what kind of data you're looking at.

Key idea. The checksum is what makes an address self-verifying: mistype one character and decoding fails, instead of silently pointing at the wrong account.

Decoding and Validation

In brief: reverse the process and reject anything whose checksum does not match.

Error Detection

The 4-byte (32-bit) checksum provides strong error detection:

Probability of random error passing checksum:
1 / 2^32 = 1 / 4,294,967,296

Approximately: 1 in 4.3 billion

Types of errors detected:

  • Single character typos: 100%
  • Transpositions: 100%
  • Missing characters: 100%
  • Extra characters: 100%
  • Random corruption: 99.9999999767%

Complete Example: Account Address

Seeds and Human Readability

Seeds can be encoded in two formats:

Base58Check Format

Seed seed = generateRandomSeed();
std::string b58 = toBase58(seed);
// Example: sp5fghtJtpUorTwvof1NpDXAzNwf5

Properties:

  • Compact (25-28 characters)
  • Checksum for error detection
  • Safe to copy-paste

RFC 1751 Word Format

std::string words = seedAs1751(seed);
// Example: "MAD WARM EVEN SHOW BALK FELT TOY STIR OBOE COST HOPE VAIN"

Properties:

  • 12 words from a dictionary
  • Easier to write down by hand
  • Easier to read aloud (for backup)
  • Checksum built into last word

Practical Usage

Creating an Account

// Generate key pair
auto [publicKey, secretKey] = randomKeyPair(KeyType::ed25519);

// Derive account ID
AccountID accountID = calcAccountID(publicKey);

// Encode as address
std::string address = toBase58(accountID);

std::cout << "Your XRPL address: " << address << "\n";
// Your XRPL address: rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4C

Validating User Input

Parsing Different Token Types

Comparison with Other Encodings

Encoding Characters Case-Sensitive Checksum Compact URL-Safe
Hex 16 No No No (2×) Yes
Base64 64 Yes No Yes (1.33×) No (+, /)
Base58 58 Yes No Yes (1.37×) Yes
Base58Check 58 Yes Yes (4 bytes) Yes (1.37×) Yes

Base58Check wins for:

  • Human readability (no confusing characters)
  • Error detection (checksum)
  • URL safety (no special characters)
  • Blockchain addresses

Common Pitfalls

Typos Without Validation

// User types address wrong
std::string userAddress = "rN7n7otQDd6FczFgLdlqtyMVrn3LNU8B4D";  // Last char wrong

// Send funds without validation
sendPayment(userAddress, amount);  // WRONG ADDRESS!

Solution:

if (!isValidAddress(userAddress)) {
    throw std::runtime_error("Invalid address - check for typos");
}

Assuming All Addresses Start with 'r'

// ❌ WRONG
bool isAddress(std::string const& s) {
    return s[0] == 'r';  // Too simplistic
}

Solution:

// ✅ CORRECT
bool isAddress(std::string const& s) {
    return !decodeBase58Token(s, TokenType::AccountID).empty();
}

Manual Base58 Implementation

// ❌ WRONG - Don't implement yourself
std::string myBase58Encode(/* ... */) {
    // Custom implementation - likely has bugs
}

Solution:

// ✅ CORRECT - Use library functions
std::string encoded = encodeBase58Token(type, data, size);

Performance Considerations

// Base58 encoding is relatively slow compared to hex:
// Hex encoding:     ~1 microsecond
// Base58 encoding:  ~10 microseconds

// But this doesn't matter for user-facing operations:
// - Displaying addresses: once per UI render
// - Parsing user input: once per input
// - Not a bottleneck in practice

When performance matters:

// For internal storage and processing, use binary:
AccountID accountID;  // 20 bytes, fast comparisons

// Only encode to Base58 when presenting to users:
std::string address = toBase58(accountID);  // For display only

Summary

This module explained how XRPL turns raw bytes into the human-readable strings you see. Base58 drops look-alike characters (0/O, I/l), and Base58Check adds a double-SHA-256 checksum so a mistyped address fails to decode instead of pointing at the wrong account. A type byte sets the visible prefix (r for accounts, s for seeds, n for node keys), so you can tell at a glance what an encoded value is.

To remember:

  • The Base58 algorithm is universal, but the alphabet is not: XRPL's (rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz) is a different permutation of the same 58 characters than Bitcoin's, never assume the two are interchangeable
  • The Base58 alphabet drops the look-alikes 0, O, I, l
  • Layout: type byte + payload + 4-byte checksum (double SHA-256), all Base58-encoded
  • The type byte sets the visible prefix: r account, s seed, n node public key
  • A typo makes decoding fail; it can never silently resolve to another account
  • Code: include/xrpl/protocol/tokens.h (encodeBase58Token / decodeBase58Token)
  • Seeds also exist in RFC 1751 word form
  • Validate input by decoding it, not with a regex: the checksum is the real check
  • Watch out: an address that LOOKS valid is not valid until the checksum passes; length and prefix prove nothing

Next up. Addresses identify accounts; signatures prove them. Next: transaction signing and verification, end to end, canonicality included.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026