定义
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 = 80 余 20
2) 80 / 58 = 1 余 22
3) 1 / 58 = 0 余 1
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 但是计算还没有做完的情况。
参考
- 腾讯元宝-deepseek(yuanbao.tencent.com/)和deepseek官…
- 比特币的开源代码(github.com/bitcoin/bit…)
微信公众号: 只做人间不老仙
欢迎关注。