(原题为:1 月挖出的那个块,584942419325) 在一个共识协议中,最简单的错误也会导致灾难。 我准备开一个系列,讲解我在 go-ethereum(Geth 客户端)(以太坊协议的正式 Go 语言实现)中发现的 Bug,本篇是第一篇。虽然阅读这系列文章不需要你对 Geth 有多深的理解,但懂得以太坊协议是怎么运行的,会很有帮助。 这篇文章讲的是 Geth 客户端叔块验证程序中的一个 bug,传入一个专门构造的叔块后,该程序的行动是错误的。如果该漏洞被利用,会导致 Geth 节点和 Parity 节点发生分叉。 区块与叔块 每一条区块链都会在运行中产生一条大家都认可的主链(canonical chain);而主链的辨识方法也是协议定义好的:最长的链,或者总工作量最大的链,等等。不过,网络的延迟意味着,可能会有两个区块在同一时间生成。那么,只有其中一个能最终成为主链的一部分,而另一个则必须被抛弃。 某些区块链协议,比如比特币,会完全无视掉这些被落败的区块,让它们成为主链的 “孤块(orphan)”。另一些区块链协议,比如以太坊,依然会奖励挖出这些落败区块并努力传播它的矿工。在以太坊的语境中,这些仍然成为了主链一部分的孤块叫做 “叔块(uncle block)”。 但一个块要成为一个有效的叔块,还需满足一些条件:(1)该区块本身的所有内容都必须是有效的(根据正常的共识规则);(2)区块与其意图标记的叔块,两者的块高度相差不超过 6(一个叔块挖出后,只有在未来的 6 个区块以内被标记为叔块,才是有效的)。但是,这里有个例外:虽然正常的区块的时间戳间隔不应超过 15 秒,但叔块则无此限制。 关于整数的一个小插曲 大部分的编程语言都有依赖于平台的整数(platform-dependent integer)和定长整数(fixed-width integer)两种概念。依赖于平台的整数可能是 32 位或者 64 位的(等等),取决于程序所在的编译平台。在 C/C++ 和 Go 语言中,你可能会使用uint,而在 Rust 中,你会使用usize。 但是,有些时候程序员想要保证其程序变量可以存储 64 位的数据,即使程序运行在 32 位的平台上。这时候,程序员可以使用定长的整数类型。在 C/C++ 中就是uint64_t,Go 语言是uint64,而在 Rust 中是u64。 使用这些语言自带的整数型的好处是,它们都具备最高优先级(first-class citizen),使用起来都非常简单。来看看这个支持 64 位整数的 Collatz Conjecture 实现: func collatz(n uint64) uint64 { if n % 2 == 0 { return n / 2 } else { return 3 * n + 1 }} 但是,这个实现有个瑕疵:它不支持超过 64 位的整数。因此,我们需要大整数(big integers)。大多数语言都支持大整数,要么是用自带的标准库(比如 Go 的big.Int),要么是通过外部代码库(比如 C/C++ 和 Rust 都是如此)。 难搞的是,使用大整数有一个很大的缺点:很不灵活。我们用支持任意大整数类型的 Collatz Conjecture 把上面的程序再实现一遍: var big0 = big.NewInt(0)var big1 = big.NewInt(1)var big2 = big.NewInt(2)var big3 = big.NewInt(3) 显然,64 位的版本既好写,又好读。所以,不意外的是,程序员都会尽可能使用简单的整数型。 例外 vs. 现实 在以太坊协议中,可以预期大部分数据都不会超过 256 位,虽然某些整数字段的长度是任意的,无法有任何预期。重点是,区块的时间戳Hs也定义为一个 256 位的整数。 Geth 团队尝试通过验证叔块的时间戳是小于2^256 - 1的整数来满足这个定义。再次提醒,叔块的出块时间不受任何限制。 // Verify the header's timestampif uncle { if header.Time.Cmp(math.MaxBig256) > 0 { return errLargeBlockTime }} else { if header.Time.Cmp(big.NewInt(time.Now().Add(allowedFutureBlockTime).Unix())) > 0 { return consensus.ErrFutureBlock }} -来源- 但是,接下来的代码却要将区块时间戳强制调整为一个 64 位的整数,以计算该区块的正确难度。 // Verify the block's difficulty based in it's timestamp and parent's difficultyexpected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent) -来源- 如果 Parity 也是一样的做法,那也不会有什么大问题。但是,Parity 的时间戳在2^64 - 1就已到达上限,不会再溢出了。 let mut blockheader = Header { parent_hash: r.val_at(0)?, uncles_hash: r.val_at(1)?, author: r.val_at(2)?, state_root: r.val_at(3)?, transactions_root: r.val_at(4)?, receipts_root: r.val_at(5)?, log_bloom: r.val_at(6)?, difficulty: r.val_at(7)?, number: r.val_at(8)?, gas_limit: r.val_at(9)?, gas_used: r.val_at(10)?, timestamp: cmp::min(r.val_at::<U256>(11)?, u64::max_value().into()).as_u64(), extra_data: r.val_at(12)?, seal: vec![], hash: keccak(r.as_raw()).into(),}; -来源- 也就是说,如果有个恶意的矿工,在所出的区块里纳入了一个叔块,该叔块的时间戳是584942419325-01-27 07:00:16 UTC,也就是 unix 时间2^64,那么 Geth 会用 Unix 时间0来计算难度,而 Parity 会用 unix 时间2^64 - 1来计算难度。结果会不一样,所以其中一个客户端会在验证叔块后从主链分裂出去。 Geth 团队在PR 19372中修复了这个 Bug,切换到所有时间戳都使用unit64。 结论 每一种参与同一个共识协议的客户端,都必须有同样的行动,因此,一些看起来完全无害的操作可能正是导致一半网络相互隔离的罪魁祸首。 这同样也表明,要发现一个影响巨大的 bug,你并不需要很高的技术水平。如果你对这些东西感兴趣,最好的办法就是立即动手。 下一篇文章,我们会讨论 Geth 客户端如何存储构成以太坊的数据,以及一个手段高明的攻击者可以如何规划一个****,在引爆时导致链硬分叉。 原文链接: https://samczsun.com/the-block-mined-in-january-584942419325/ 作者:samczsun 翻译:阿剑—- 编译者/作者:EthFans 玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。 |
科普 | 叔块验证与网络安全性
2021-04-15 EthFans 来源:区块链网络
LOADING...
相关阅读:
- 区块链神判手:比特币4月15日行情分析斜线向上2021-04-15
- 百枚价值互联网创世者NFT秒光MeshBoxDAO大火2021-04-15
- 预测市场协议PositionalWarfare发布PWT代币经济模型2021-04-15
- 纽约证券交易所通过6支热门科技股的“首次交易”进入NFT狂潮2021-04-15
- 哥伦比亚实施RSK区块链进行石油勘探2021-04-15