2.2 哈希函数:给每个铸件打钢印


2.2 哈希函数:给每个铸件打钢印

哈希函数是把任意长度输入映射为固定长度摘要的单向函数,好的密码学哈希同时满足确定性、单向性、抗碰撞性与雪崩效应。区块链用它给区块盖钢印、给地址做派生、给交易做批量质检,还把它变成矿工的抽奖机。本节用 Python 复算全部关键性质,并算一笔挖矿难度的账。

为什么这枚钢印能让账本"改不动"

如果没有钢印,怎么知道一份文件被动过?对比文件本身,前提是你留着原件——那只是把"信任原件"的问题换成"信任保管"。哈希函数提供了一种不需要保管原件的办法:只记下它的摘要。摘要固定长度、来者不拒、校验飞快;更重要的是,攻击者找不到别的文件能撞出同一枚摘要。于是"保管原件"退化为"保管一小串指纹",而指纹可以抄在任何地方——区块头里、报纸上、法院卷宗里。

反常识之处在后面:指纹到原件是单行道。从摘要推不回输入,不是因为算法藏起来,而是因为映射空间大到"倒着走"只能穷举。这枚钢印还有一个暴脾气:输入动一个比特,输出像被重新摇匀的骰子,一半左右的比特翻转。这三条性质合起来,才有"改一字、全变样"的戏剧效果。

图 2-2 雪崩效应:一比特输入,摇匀整个输出

图 2-2 雪崩效应:一比特输入,摇匀整个输出

性质复算:四条军规逐条过秤

四条军规——确定性、单向性、抗碰撞性、雪崩效应——全部可以在本地复算。第一个实验验证确定性与雪崩:

import hashlib def h(s: str) -> str: return hashlib.sha256(s.encode()).hexdigest() a = "转账 老王 100 元" print(h(a)) # 每次运行都相同:确定性 print(h(a) == h(a)) # True def flip_ratio(s1: str, s2: str) -> float: """两个十六进制摘要的比特翻转比例""" n1, n2 = int(h(s1), 16), int(h(s2), 16) diff_bits = bin(n1 ^ n2).count("1") return diff_bits / 256 # SHA-256 输出 256 比特 print(f"翻转比例: {flip_ratio(a, '转账 老王 101 元'):.2%}") # 约 50% print(f"翻转比例: {flip_ratio(a, '转账 老王 200 元'):.2%}") # 约 50%

无论改动多小,输出翻转比例都稳定在半数附近——这不是巧合,而是好哈希的硬指标:输出对输入不残留任何可用的线性关系。第二个实验看"单向性"在实践中的样子,同时预热挖矿逻辑——找一个指定前缀的摘要需要多少次尝试:

import hashlib import time def mine(prefix_zeros: int, data: str) -> tuple[int, str]: """暴力寻找 nonce,使摘要十六进制表示以指定个 0 开头""" target = "0" * prefix_zeros nonce = 0 t0 = time.time() while True: digest = hashlib.sha256(f"{data}:{nonce}".encode()).hexdigest() if digest.startswith(target): return nonce, digest, time.time() - t0 nonce += 1 for zeros in (3, 4, 5): nonce, digest, cost = mine(zeros, "blockdata") print(f"{zeros} 个前导 0 -> nonce={nonce:<8} 用时 {cost*1000:6.1f} ms {digest[:16]}...")

每加一个前导零,期望耗时大约翻十五倍上下(十六进制的一个位)。真实比特币把目标定在远多于这个量级的难度上,全网每秒尝试的天文数字次哈希,本质与这段循环无异——所谓挖矿,就是"谁先抽中满足难度前缀的彩票"。第 4 章会看到难度如何动态调节出块节奏,本节先把"抽奖机"的机械原理装进脑子。

密码学细节:抗碰撞的账怎么算

抗碰撞性说的是找不到任何两个不同输入共享摘要。直觉常低估这件事的难度,认为空间大就慢慢找——但碰撞搜索的加速来自生日悖论:不找"指定靶子的碰撞",而是让任意两者撞车,尝试次数从空间规模的量级骤降为平方根量级。对 SHA-256 的输出空间而言,平方根量级仍然是天文数字,这就是它至今稳坐钢印位置的原因。用模拟感受一下量级关系:

import hashlib, random def birthday_crash(space_bits: int, trials: int) -> float: """在缩小版空间里模拟生日式碰撞(取摘要前 n 比特)""" seen = set() for i in range(trials): d = hashlib.sha256(str(i).encode()).digest() v = int.from_bytes(d[: max(1, space_bits // 16)], "big") % (2 ** min(space_bits, 32)) if v in seen: return i / trials # 出现碰撞时的进度 seen.add(v) return 1.0 for bits in (8, 12, 16): print(f"空间 {bits:>2} 比特 -> 碰撞出现于约 {birthday_crash(bits, 5000):.0%} 处")

空间稍增,碰撞位置明显后移。SHA-256 的真实空间下,生日攻击的代价也远超任何现存算力的合理预算,因此比特币区块钢印至今沿用。⚠️ 但哈希不是可以随便替换的耗材:算法的输出长度、内部结构与已知的弱点族决定了它的岗位资格。MD 系与初代 SHA 因结构弱点已被逐出钢印岗位;新链若采用新哈希,工程上必须先过公开密码分析的长期检验,这属于 2.5 节的换料风险评估。

工程现实:一枚钢印的多处打卡

哈希在链上至少有四个工位。区块头自身摘要(挖矿目标与链式焊缝);交易列表打包成 Merkle 根(下一节展开);公钥经过哈希压缩成地址(先哈希后编码,防直接暴露公钥、缩短长度);地址与区块头再哈希成"区块标识"供检索。同一个函数被反复用在防篡改、防碰瓷与寻址上——材料少而岗位多,是这台机器设计上的克制。

💡 一个值得记住的直觉:钢印验证的是"此刻的内容",链式焊缝验证的是"全部的历史"。单块合法只说明这块自洽;沿 prev-hash 一路验到创世块,才确认了历史的连续。轻钱包的快速验证、区块浏览器的重组告警,都建立在这条单行道上。

攻防边界:钢印不设防的地方

哈希不提供机密性——摘要本身不藏输入,低熵输入可被字典反查(上节的手机号教训);哈希也不提供真实性——任何人都能给任意内容盖章,"是谁的内容"要靠下一节的签名材料。另外要区分"哈希碰撞"与"长度扩展攻击"两类不同的疼:前者针对函数本身,后者针对"摘要=哈希(秘密‖消息)"这种错误用法。规范做法是把密钥与消息换用专为认证设计的构造,而不是裸拼。钢印的岗位说明书到此完整:内容指纹,仅此而已。

材料库里第一味主料验收完毕。下一节领取第二味——公钥密码与数字签名,它回答哈希答不了的问题:这枚铸件,是谁授权出厂的?


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U