协议演进 / BIP/比特币改进提案 / BIP-50
BIP-50
复盘 2013 年 3 月因旧版客户端 Berkeley DB 锁配置不足引发的意外链分叉:发生了什么、根因是什么、当时如何恢复,以及事后定下的行动项。本 BIP 目前处于已部署状态。
一、档案卡
| 编号 | BIP-50 |
|---|---|
| 标题 | March 2013 Chain Fork Post-Mortem |
| 作者 | Gavin Andresen <[email protected]> |
| 指定日期 | 2013-03-20 |
| 类型 | Informational(信息类) |
| 状态 | Deployed(已部署) |
| 阅读版 | https://bips.dev/50 |
| 官方原文 | https://github.com/bitcoin/bips/blob/master/bip-0050.mediawiki |
二、放在哪一族
信息类事后分析(post-mortem),不提出新规则,只记录一次真实事故。它与一般规范类 BIP 不同:读它是为了理解早期比特币在实现层面出过什么问题、社区当时如何处置。
它记录的事件牵涉两个版本世代的差异:0.8 之前的客户端使用 Berkeley DB,0.8 改用了 LevelDB。也因此,它常被放在“实现差异如何变成共识风险”这一主题下阅读。
三、它要解决什么
3.1 出了什么事
一个“交易输入总数比以往见过的都多”的区块被挖出并广播。0.8 节点能处理它,但部分 0.8 之前的节点拒绝了它,于是区块链出现了意料之外的分叉。
当时 0.8 一侧的链约占 60% 算力,分叉因此不会自动消解——若 0.8 之前的链在总工作量上反超,0.8 节点本会重组过去,但算力分布让这件事没有自然发生。
3.2 当时如何恢复
为了尽快恢复一条公认的链,BTCGuild 与 Slush 两家矿池把自家 0.8 节点降级回 0.7,让它们的矿池也拒绝那个大区块。这样,多数算力回到了不含该区块的链上,最终 0.8 节点重组到了 0.8 之前的链上。
期间至少发生了一笔大额双花。原文同时说明:实施者只是试验双花是否可行,并无恶意盗取的意图。
3.3 哪些地方做对了
原文列出的“做对了”包括:
- 分叉被非常快地检测到;
- 关键人物当时在线,能在 IRC 上联系到,或能直接联系上;
- Marek Palatinus(Slush)与 BTCGuild 的 Michael Marsee(Eleuthria)迅速降级节点、恢复以旧链为准,尽管这让他们牺牲了可观的收入;
- 主要交易所的充值与 BitPay 支付被迅速暂停,事后又迅速恢复;
- 唯一一次针对商家的攻击,出手者并无真正盗钱的意图。
四、规则本身
本 BIP 不定新规则,以下是原文给出的根因与行动项,属于事后结论。
4.1 根因
0.8 之前的版本为 Berkeley DB 配置的锁数量不足,处理不了较大但本身有效的区块。Berkeley DB 的锁必须由使用其 API 的一方按预期负载手工配置;手册建议在压力条件下测出实际用到的锁、locker 与锁对象数量,再翻倍留作安全余量。
锁配置不足带来一个隐蔽后果:它事实上变成了一条决定区块有效性的网络共识规则,而且是一条不一致、不安全的规则——因为每个节点实际需要多少锁,取决于其本地数据库的具体状态。
几个叠加因素让问题此前没有暴露:
- 最大尺寸的区块曾在测试网上成功处理过,没人想到更小的区块也可能需要更多锁;
- 0.7 之前,未修改的挖矿节点自我限制区块最大为 500,000 字节,进一步避免触发此情形;
- 0.7 把目标大小改为可配置,且事发前一周矿工正被鼓励调高这一目标。
0.8 不再使用 Berkeley DB,改用 LevelDB,没有同样的锁限制,因此能成功处理那个引发分叉的区块。
原文还指出:BDB 锁在处理链重组时同样需要,0.8 之前的版本可能也处理不了某些有效的重组。而且即便全网都跑 0.7.2,问题依然存在:各节点 blkindex.dat 内容不完全相同、锁按页获取,一台 0.7.2 节点造出的区块,另一台可能验证不了。
4.2 行动项(原文记录,多项标注为已完成)
立即项:
- 发布 0.8.1(直接从 0.8.0 分出),在接下来两个月内执行新规:拒绝可能需要超过 10,000 个锁的区块;把造块的最大尺寸限制为 500,000 字节;
- 为旧版本发布补丁执行同样规则,同时把最大锁数提高到 537,000;
- 在 bitcoin.org 建页面敦促升级到 0.8.1,并说明实在不能升级时如何把 DB_CONFIG 设为 537,000 个锁;
- 在两个月内向旧版本用户发送一系列警报指向该页面。
警报系统:复查警报系统密钥的持有者名单、确保彼此有联系方式且时区覆盖良好;实现 -alertnotify=<command>,让服务在收到适用于自己的警报时运行命令;预先生成 52 条测试警报,每周在测试网上定时广播以便演练。
安全模式设想:检测到足够长的侧链时触发警报(哪怕它不是主链),矿池可据此自动暂停 payouts。
测试:在测试网上运行机器人,从水龙头取币、生成大量随机拆分/合并交易再发回,以随机化在线测试更早暴露这类病态区块。
双花问题:此次双花能成功,很可能是矿池节点降级时内存池被清空所致;原文提出的解法方向是节点启动时彼此同步内存池,但这需要先有内存池过期策略。
4.3 结局
原文 Resolution 一节记录:2013 年 8 月 16 日,区块 252,451 被主网接受,未打补丁的节点由此被分叉出网络。
五、谁必须动
普通持币人:本文本身不要求任何操作。它记录的是历史事件与当时的处置。
钱包与商家服务:原文肯定了事发时暂停充值与支付、事后迅速恢复的做法,并提出让商家/交易所(乃至矿池)在跟随网络多数版本的 bitcoind 后面运行新代码的想法(来自 Michael Gronager)。
全节点与矿工:当时的行动项直接指向他们:升级或打补丁、按页面说明调整锁配置、关注警报。
开发者与实现维护者:教训在实现层——数据库这类底层组件的配置上限,可能在事实上变成共识规则;多实现、多版本共存时,这类隐性上限最危险。
六、现在算不算数,以及一个常见误读
状态为 Deployed,但它是信息类文档:价值在于留档,不在于现行效力。今天的客户端早已不使用当年那套 Berkeley DB 配置,文中具体的锁数、版本与补丁只对理解这段历史有意义。
常见误读:
“2013 年分叉是协议规则本身写错了、或是有人蓄意攻击造成的。”
不对。原文的根因结论很明确:区块本身有效,问题出在旧实现里 Berkeley DB 锁配置不足这一实现细节,它在事实上冒充了一条不一致的共识规则。那笔双花也被原文定性为试验性质、并无盗取意图。真正的教训是:实现细节可能悄悄变成共识的一部分。
