功能说明
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)