Base58编码

84 阅读13分钟

定义

Base58编码是一种主要用于比特币和加密货币系统的二进制数据编码方案。核心字符集是123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz。正常来讲,大小写字母 + 数字 一共是62个字符,但是因为数字0 和 字母O 以及大写I 和小写的l 容易混淆,所以移除了,因此核心字符集是58个。相较于base64编码,不会出现非数字和字母的字符,结果更友好。

原理

将输入数据视为一个大的整数,通过反复除以58并取余数的方式,将其从256进制转换为58进制,余数对应字符集中的字符。本质上就是进制转换。

实现代码

参考比特币的开源代码(github.com/bitcoin/bit… 最新的版本使用到了std::span,是C++20的特性。测试使用编译器版本是4.8.5,版本较旧,因此进行了修改,并且进行精简,只留下编解码的函数。

base.h

/**
 * Why base-58 instead of standard base-64 encoding?
 * - Don't want 0OIl characters that look the same in some fonts and
 *      could be used to create visually identical looking data.
 * - A string with non-alphanumeric characters is not as easily accepted as input.
 * - E-mail usually won't line-break if there's no punctuation to break at.
 * - Double-clicking selects the whole string as one word if it's all alphanumeric.
 */
#ifndef BITCOIN_BASE58_H
#define BITCOIN_BASE58_H

#include <string>
#include <vector>

/**
 * Encode a byte vector as a base58-encoded string
 */
std::string EncodeBase58(const std::vector<unsigned char>& input);

/**
 * Decode a base58-encoded string (str) into a byte vector (vchRet).
 * return true if decoding is successful.
 */
bool DecodeBase58(const char* psz, std::vector<unsigned char>& vchRet, int max_ret_len);

#endif // BITCOIN_BASE58_H

base58.cpp:

#include "base58.h"
#include <cassert>
#include <cstring>
#include <limits>
#include <vector>
#include <cstdint>

/** All alphanumeric characters except for "0", "I", "O", and "l" */
static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static const int8_t mapBase58[256] = {
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1, 0, 1, 2, 3, 4, 5, 6,  7, 8,-1,-1,-1,-1,-1,-1,
    -1, 9,10,11,12,13,14,15, 16,-1,17,18,19,20,21,-1,
    22,23,24,25,26,27,28,29, 30,31,32,-1,-1,-1,-1,-1,
    -1,33,34,35,36,37,38,39, 40,41,42,43,-1,44,45,46,
    47,48,49,50,51,52,53,54, 55,56,57,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
};

bool DecodeBase58(const char* psz, std::vector<unsigned char>& vch, int max_ret_len)
{
    // 去掉对空格字符的判断, 依赖输入的正确性
    // Skip and count leading '1's.
    int zeroes = 0;
    int length = 0;
    while (*psz == '1') {
        zeroes++;
        if (zeroes > max_ret_len) return false;
        psz++;
    }
    // Allocate enough space in big-endian base256 representation.
    int size = strlen(psz) * 733 /1000 + 1; // log(58) / log(256), rounded up.
    std::vector<unsigned char> b256(size);
    // Process the characters.
    static_assert(sizeof(mapBase58) == 256, "mapBase58.size() should be 256"); // guarantee not out of range
    // 简化处理, 这里不过滤空格, 依赖输入保证
    while (*psz) {
        // Decode base58 character
        int carry = mapBase58[(uint8_t)*psz];
        if (carry == -1)  // Invalid b58 character
            return false;
        int i = 0;
        for (std::vector<unsigned char>::reverse_iterator it = b256.rbegin(); (carry != 0 || i < length) && (it != b256.rend()); ++it, ++i) {
            carry += 58 * (*it);
            *it = carry % 256;
            carry /= 256;
        }
        assert(carry == 0);
        length = i;
        if (length + zeroes > max_ret_len) return false;
        psz++;
    }
    // 去掉对结尾空格的处理, 依赖输入的正确性
    if (*psz != 0)
        return false;
    // Skip leading zeroes in b256.
    std::vector<unsigned char>::iterator it = b256.begin() + (size - length);
    // Copy result into output vector.
    vch.reserve(zeroes + (b256.end() - it));
    vch.assign(zeroes, 0x00);
    while (it != b256.end())
        vch.push_back(*(it++));
    return true;
}

std::string EncodeBase58(const std::vector<unsigned char>& input)
{
    // Skip & count leading zeroes.
    int zeroes = 0;
    int length = 0;
    size_t index = 0;
    
    // 替换std::span的subspan操作:使用索引遍历vector
    while (index < input.size() && input[index] == 0) {
        zeroes++;
        index++;
    }
    
    // Allocate enough space in big-endian base58 representation.
    int size = (input.size() - index) * 138 / 100 + 1; // log(256) / log(58), rounded up.
    std::vector<unsigned char> b58(size);
    
    // Process the bytes using index instead of std::span
    while (index < input.size()) {
        int carry = input[index];
        int i = 0;
        // Apply "b58 = b58 * 256 + ch"
        for (std::vector<unsigned char>::reverse_iterator it = b58.rbegin(); 
             (carry != 0 || i < length) && (it != b58.rend()); it++, i++) {
            carry += 256 * (*it);
            *it = carry % 58;
            carry /= 58;
        }

        assert(carry == 0);
        length = i;
        index++;
    }
    
    // Skip leading zeroes in base58 result.
    std::vector<unsigned char>::iterator it = b58.begin() + (size - length);
    while (it != b58.end() && *it == 0)
        it++;
        
    // Translate the result into a string.
    std::string str;
    str.reserve(zeroes + (b58.end() - it));
    str.assign(zeroes, '1');
    while (it != b58.end())
        str += pszBase58[*(it++)];
    return str;
}

一个简单的测试程序,用于测试程序的正确性:

#include <iostream>
#include <vector>
#include <string>
#include <random>
#include <chrono>
#include "base58.h"

// 生成随机字符串
std::vector<unsigned char> generateRandomData(size_t length) {
    std::vector<unsigned char> data;
    data.reserve(length);
    
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, 255);
    
    for (size_t i = 0; i < length; ++i) {
        data.push_back(static_cast<unsigned char>(dis(gen)));
    }
    
    return data;
}

// 比较两个字节向量是否相等
bool compareVectors(const std::vector<unsigned char>& v1, const std::vector<unsigned char>& v2) {
    if (v1.size() != v2.size()) {
        return false;
    }
    
    for (size_t i = 0; i < v1.size(); ++i) {
        if (v1[i] != v2[i]) {
            return false;
        }
    }
    
    return true;
}

// 打印字节向量的十六进制表示
void printHex(const std::vector<unsigned char>& data) {
    for (unsigned char byte : data) {
        printf("%02x", byte);
    }
}

int main() {
    std::cout << "开始Base58编解码测试..." << std::endl;
    std::cout << "将进行1000次随机数据测试" << std::endl;
    
    int totalTests = 1000;
    int successCount = 0;
    int failureCount = 0;
    
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> lengthDis(1, 100); // 生成长度1-100的随机数据
    
    auto startTime = std::chrono::high_resolution_clock::now();
    
    for (int i = 0; i < totalTests; ++i) {
        // 生成随机长度的测试数据
        size_t dataLength = lengthDis(gen);
        std::vector<unsigned char> originalData = generateRandomData(dataLength);
        
        // 编码
        std::string encoded = EncodeBase58(originalData);
        
        // 解码
        std::vector<unsigned char> decodedData;
        bool decodeSuccess = DecodeBase58(encoded.c_str(), decodedData, 1000);
        
        // 验证
        bool testSuccess = decodeSuccess && compareVectors(originalData, decodedData);
        
        if (testSuccess) {
            successCount++;
        } else {
            failureCount++;
            std::cout << "\n❌ 测试失败 #" << i + 1 << std::endl;
            std::cout << "   原始数据长度: " << originalData.size() << std::endl;
            std::cout << "   原始数据: ";
            printHex(originalData);
            std::cout << std::endl;
            std::cout << "   编码结果: " << encoded << std::endl;
            std::cout << "   解码结果长度: " << decodedData.size() << std::endl;
            std::cout << "   解码结果: ";
            printHex(decodedData);
            std::cout << std::endl;
            std::cout << "   解码状态: " << (decodeSuccess ? "成功" : "失败") << std::endl;
            
            if (decodeSuccess && !compareVectors(originalData, decodedData)) {
                std::cout << "   数据不匹配!" << std::endl;
            }
        }
        
        // 显示进度
        if ((i + 1) % 100 == 0) {
            std::cout << "已完成 " << i + 1 << "/" << totalTests << " 次测试..." << std::endl;
        }
    }
    
    auto endTime = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
    
    // 输出统计结果
    std::cout << "\n==========================================" << std::endl;
    std::cout << "测试完成!" << std::endl;
    std::cout << "总测试次数: " << totalTests << std::endl;
    std::cout << "成功次数: " << successCount << std::endl;
    std::cout << "失败次数: " << failureCount << std::endl;
    std::cout << "成功率: " << (successCount * 100.0 / totalTests) << "%" << std::endl;
    std::cout << "总耗时: " << duration.count() << " 毫秒" << std::endl;
    std::cout << "平均每次测试耗时: " << (duration.count() * 1.0 / totalTests) << " 毫秒" << std::endl;
    
    if (failureCount == 0) {
        std::cout << "🎉 所有测试都通过了!Base58实现正确。" << std::endl;
    } else {
        std::cout << "⚠️ 发现 " << failureCount << " 次测试失败,请检查实现。" << std::endl;
    }
    
    // 额外测试:测试一些边界情况
    std::cout << "\n==========================================" << std::endl;
    std::cout << "开始边界情况测试..." << std::endl;
    
    // 测试1:空数据
    std::vector<unsigned char> emptyData;
    std::string emptyEncoded = EncodeBase58(emptyData);
    std::vector<unsigned char> emptyDecoded;
    bool emptyDecodeSuccess = DecodeBase58(emptyEncoded.c_str(), emptyDecoded, 1000);
    std::cout << "空数据测试: " << (emptyDecodeSuccess && compareVectors(emptyData, emptyDecoded) ? "通过" : "失败") << std::endl;
    
    // 测试2:全零数据
    std::vector<unsigned char> zeroData(10, 0x00);
    std::string zeroEncoded = EncodeBase58(zeroData);
    std::vector<unsigned char> zeroDecoded;
    bool zeroDecodeSuccess = DecodeBase58(zeroEncoded.c_str(), zeroDecoded, 1000);
    std::cout << "全零数据测试: " << (zeroDecodeSuccess && compareVectors(zeroData, zeroDecoded) ? "通过" : "失败") << std::endl;
    
    // 测试3:单字节数据
    std::vector<unsigned char> singleByteData = {0x41}; // 'A'
    std::string singleEncoded = EncodeBase58(singleByteData);
    std::vector<unsigned char> singleDecoded;
    bool singleDecodeSuccess = DecodeBase58(singleEncoded.c_str(), singleDecoded, 1000);
    std::cout << "单字节数据测试: " << (singleDecodeSuccess && compareVectors(singleByteData, singleDecoded) ? "通过" : "失败") << std::endl;
    
    return failureCount == 0 ? 0 : 1;
}

/*
编译指令: g++ -std=c++11 main.cpp base58.cpp -o base58
操作系统: centos7.6
gcc版本: 4.8.5
输出:
开始Base58编解码测试...
将进行1000次随机数据测试
已完成 100/1000 次测试...
已完成 200/1000 次测试...
已完成 300/1000 次测试...
已完成 400/1000 次测试...
已完成 500/1000 次测试...
已完成 600/1000 次测试...
已完成 700/1000 次测试...
已完成 800/1000 次测试...
已完成 900/1000 次测试...
已完成 1000/1000 次测试...

==========================================
测试完成!
总测试次数: 1000
成功次数: 1000
失败次数: 0
成功率: 100%
总耗时: 249 毫秒
平均每次测试耗时: 0.249 毫秒
🎉 所有测试都通过了!Base58实现正确。

==========================================
开始边界情况测试...
空数据测试: 通过
全零数据测试: 通过
单字节数据测试: 通过
*/

原理说明

可以发现,不管是编码还是解码,核心都是进行进制转换(编码是256进制转为58进制,解码是58进制转为256进制)。以编码为例,核心代码为:

   	...
	std::vector<unsigned char> b58(size);
    
    // Process the bytes using index instead of std::span
    while (index < input.size()) {
        int carry = input[index];
        int i = 0;
        // Apply "b58 = b58 * 256 + ch"
        for (std::vector<unsigned char>::reverse_iterator it = b58.rbegin(); 
             (carry != 0 || i < length) && (it != b58.rend()); it++, i++) {
            carry += 256 * (*it);
            *it = carry % 58;
            carry /= 58;
        }

        assert(carry == 0);
        length = i;
        index++;
    }
	...

初看这段循环比较难以理解,我们可以先来看它的未优化版本:

std::string EncodeBase58Unoptimized(const std::vector<unsigned char>& input)
{
    // 统计前导零
    int zeroes = 0;
    for (size_t i = 0; i < input.size(); i++) {
        if (input[i] == 0) zeroes++;
        else break;
    }
    
    // 直接复制输入数据作为工作缓冲区
    std::vector<unsigned char> data(input.begin() + zeroes, input.end());
    
    std::vector<unsigned char> result;
    
    // 外层循环:持续除法直到数据全为0
    while (!data.empty()) {
        int remainder = 0;
        
        // 内层循环:模拟大数除法 (data = data / 58)
        for (size_t i = 0; i < data.size(); i++) {
            int value = (remainder * 256) + data[i];
            data[i] = value / 58;        // 商
            remainder = value % 58;      // 余数
        }
        
        // 余数就是Base58的一位
        result.push_back(remainder);
        
        // 移除前导零(在商中)
        while (!data.empty() && data[0] == 0) {
            data.erase(data.begin());
        }
    }
    
    // 反转结果(因为我们是先得到低位)
    std::reverse(result.begin(), result.end());
    
    // 构建输出字符串
    std::string str;
    str.reserve(zeroes + result.size());
    
    // 添加前导'1'(对应输入的前导零)
    for (int i = 0; i < zeroes; i++) {
        str += '1';
    }
    
    // 添加Base58字符
    for (unsigned char digit : result) {
        str += pszBase58[digit];
    }
    
    return str;
}

上面的代码类似于除法竖式,比如常见的10进制除法竖式计算为:

          102
        _____
    12 ) 1234
         12	
         ____
           34
           24
           __
           10

未优化代码进制转换流程如下:

1、使用原始数据作为被除数,58作为除数,得到商和余数。

2、然后使用得到的商作为被除数,58作为除数,得到商和余数

3、重复第二步,直到商为0,最终把所有余数倒序排列。

这也是进制转换的一般流程。将最后的结果转化成58进制的字符中的字符表示,即完成Base58编码。

如0x1234转换成Base58编码
1) 0x1234 / 58 = 4660 / 58 = 8020
2) 80 / 58 = 122
3) 1 / 58 = 01
4) 将所有余数倒序排列:1 22 20, 换成Base58字符表示: 2PM

这种计算方法是显然的,但是性能并不是最优的。相较于优化后的计算方法,未优化的计算方法有以下缺点:

  • 每次迭代都需要进行data.erase, 用于移除商中的前导0
  • 最后的结果要进行reverse
  • 没有提前计算结果大小,push_back过程中可能存在多次内存分配(这一点其实可以优化)

优化后的计算方法可以这样去看:

设原来的字符数组为 an-1an-2...a2a1a0,an-1an-2...a2a1的Base58进制表示为 bm-1bm-2... b2b1b0

则:

an-1an-2...a2a1a0 = an-1an-2...a2a1 * 256 + a0 = bm-1bm-2... b2b1b0 * 256 + a0

剩下的就是将bm-1bm-2... b2b1b0 * 256 + a0转换成58进制表示,假设为 ck-1ck-2... c2c1c0。则容易得到:

c0 = (bm-1bm-2... b2b1b0 * 256 + a0) % 58 = (bm-1bm-2... b2b1 * 58 * 256 + b0 * 256 + a0) % 58

(p + q) % s = (p % s + q % s) % s

可得:

(bm-1bm-2... b2b1 * 58 * 256 + b0 * 256 + a0) % 58

= (bm-1bm-2... b2b1 * 58 * 256 % 58 + (b0 * 256 + a0) % 58 ) % 58

= (0 + (b0 * 256 + a0) % 58 ) % 58

= (b0 * 256 + a0) % 58

接下来继续计算c1

c1 = (bm-1bm-2... b2b1b0 * 256 + a0 - (b0 * 256 + a0) % 58) / 58 % 58

= (bm-1bm-2... b2b1 * 58 * 256 + b0 * 256 + a0 - (b0 * 256 + a0) % 58) / 58 % 58

= (bm-1bm-2... b2b1 * 256 + (b0 * 256 + a0) / 58) % 58

其中 (b0 * 256 + a0) / 58 记为 quotient0,则上式变成: c1 = (bm-1bm-2... b2b1 * 256 + quotient0) % 58

由c0 的计算过程,容易得到

c1 = (b1 * 256 + quotient0) % 58

与c0 形式一致。类似的,可以得到 c2、...、ck-2、 ck-1 的值。

因此,可以通过an-1an-2...a2a1的Base58编码得到an-1an-2...a2a1a0的Base58编码,也就是优化后的算法。

实现细节上,比较难理解的可能是循环条件:

(carry != 0 || i < length) && (it != b256.rend())
  • it != b256.rend() : 这个条件比较容易理解, 是为了避免结果数组越界

  • carry != 0 : 当carry != 0 的时候, 说明还要继续进行计算。carry 其实就是分析过程中的quotient。

  • i < length : 当carry = 0 的时候, 并不一定意味着计算已经结束了, 从之前的计算公式可以看出来:carryr= (br * 256 + carryr-1) / 58,当carry = 0时, 并不意味着计算要结束进行,因为可能更高的位置上还有值,比如br + 1, br + 2...。那么什么时候可以确定更高位置上没有值了呢?当 当前计算过的总长度大于等于上一次计算的Base58编码的长度。i表示的就是当前计算过的总长度;length就是上一次计算的Base58编码的长度。对应到前面的计算过程中,length是 bm-1bm-2... b2b1b0 的长度,也就是m,i是在通过 bm-1bm-2... b2b1b0 计算ck-1ck-2... c2c1c0的过程中不断累加。当carry = 0 且 i >= m 的时候,才说明所有的计算都做完了。比如可以模拟一下 0x3A00 = 58 * 256,就可以发现carry = 0 但是计算还没有做完的情况。

参考

微信公众号: 只做人间不老仙

欢迎关注。