← 中文资源

简体中文版本

Base58Check 编码与校验

把数据编码为带双 SHA-256 校验和的 Base58 字符串,保留前导零字节,并在解码时拒绝被篡改的输入。

阅读英文原文

功能说明

Base58 去掉了数字零、大写 O、大写 I 和小写 l 等易混字符,常用于钱包地址和短标识。但单纯换进制没有错误检测能力,抄错一个字符仍可能解出另一个合法字节串。Base58Check 在负载后追加四个字节的校验和,校验和取自负载的 SHA-256 再哈希一次的前四字节,解码时重新计算并比较。

本例同时处理前导零:Base58 编码本质上是把整个字节串当作大整数,前导零会被数值转换吞掉,因此编码时按原始字节的前导零数量补字符一,解码时再把字符一还原为零字节。main 演示带前导零的负载往返,并构造一个被改动的字符串验证校验失败。

关键代码

b58encode 用 int.from_bytes 把字节串转成大整数,循环除以五十八取余映射字符,最后反转。前导零用 lstrip 计算数量并补字母表首字符。b58decode 逐字符查表累加,遇到字母表外字符立即报错,再把数值转回最少字节数,并按字符一首字符数量补回零字节。checksum 做两次 SHA-256 并截取四字节。decode_check 分离负载和校验和,长度不足五字节直接拒绝,比较失败抛出异常。

使用注意

Base58Check 的校验和只能发现随机错误,不能防伪造,需要真实性时应使用签名。校验和比较不是恒定时间,用于秘密数据时应改用 hmac.compare_digest。该实现返回可变长度字节串,协议层应固定负载长度或增加版本字节。字符一首字符的连续数量必须与真实零字节一致,空字符串解码为空字节串,调用方要单独校验。Base58 编码体积比二进制大约三分之一,不适合大文件。

验证方式

运行脚本,带前导零的负载编码后以字符一开头,解码结果应为 b’hello’。篡改最后一个字符后 decode_check 输出 rejected: checksum mismatch,说明校验和生效。可再把输入中的字符零替换成大写 O,观察解码立即报非法字符。

代码示例

import hashlib

ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def b58encode(raw: bytes) -> str:
    number = int.from_bytes(raw, "big")
    digits: list[str] = []
    while number:
        number, remainder = divmod(number, 58)
        digits.append(ALPHABET[remainder])
    leading_zeros = len(raw) - len(raw.lstrip(b"\0"))
    return ALPHABET[0] * leading_zeros + "".join(reversed(digits))


def b58decode(text: str) -> bytes:
    number = 0
    for char in text:
        index = ALPHABET.find(char)
        if index < 0:
            raise ValueError(f"invalid base58 character: {char!r}")
        number = number * 58 + index
    body = number.to_bytes((number.bit_length() + 7) // 8, "big") if number else b""
    leading_ones = len(text) - len(text.lstrip(ALPHABET[0]))
    return b"\0" * leading_ones + body


def checksum(payload: bytes) -> bytes:
    first = hashlib.sha256(payload).digest()
    return hashlib.sha256(first).digest()[:4]


def encode_check(payload: bytes) -> str:
    return b58encode(payload + checksum(payload))


def decode_check(text: str) -> bytes:
    raw = b58decode(text)
    if len(raw) < 5:
        raise ValueError("input too short to contain a checksum")
    payload, supplied = raw[:-4], raw[-4:]
    if checksum(payload) != supplied:
        raise ValueError("checksum mismatch")
    return payload


if __name__ == "__main__":
    token = encode_check(b"\x00hello")
    print(token, decode_check(token))
    corrupted = token[:-1] + ("1" if token[-1] != "1" else "2")
    try:
        decode_check(corrupted)
    except ValueError as error:
        print("rejected:", error)