科普 | 叔块验证与网络安全性

2024-02-09 14:20

本文主要是介绍科普 | 叔块验证与网络安全性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

来源 | 以太坊爱好者

责编 | 晋兆雨 

头图 | 付费下载于视觉中国 

在一个共识协议中,最简单的错误也会导致灾难。

我准备开一个系列,讲解我在 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)
func collatzBig(n *big.Int) *big.Int {  if new(big.Int).Mod(n, big2).Cmp(big0) == 0 {    return new(big.Int).Div(n, big2)  } else {    v := new(big.Int).Mul(big3, n)    v.Add(v, big1)    return v  }}显然,64 位的版本既好写,又好读。所以,不意外的是,程序员都会尽可能使用简单的整数型。


例外 vs. 现实

在以太坊协议中,可以预期大部分数据都不会超过 256 位,虽然某些整数字段的长度是任意的,无法有任何预期。重点是,区块的时间戳 Hs 也定义为一个 256 位的整数。

- 以太坊黄皮书,P6 -

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 客户端如何存储构成以太坊的数据,以及一个手段高明的攻击者可以如何规划一个定时炸弹,在引爆时导致链硬分叉。

更多阅读推荐

  • C罗捧回欧洲杯首个区块链奖杯,但区块链+体育不止于此

  • 从历次升级看以太坊协议的演化

  • V神设计理念公布,什么是以太坊的初心?

  • 绝密邮件曝光!看乔布斯如何拯救濒危的苹果?

  • 超视频化到来,你能看到什么?

这篇关于科普 | 叔块验证与网络安全性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/694450

相关文章

MySQL 主从复制部署及验证(示例详解)

《MySQL主从复制部署及验证(示例详解)》本文介绍MySQL主从复制部署步骤及学校管理数据库创建脚本,包含表结构设计、示例数据插入和查询语句,用于验证主从同步功能,感兴趣的朋友一起看看吧... 目录mysql 主从复制部署指南部署步骤1.环境准备2. 主服务器配置3. 创建复制用户4. 获取主服务器状态5

Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式

《Java通过驱动包(jar包)连接MySQL数据库的步骤总结及验证方式》本文详细介绍如何使用Java通过JDBC连接MySQL数据库,包括下载驱动、配置Eclipse环境、检测数据库连接等关键步骤,... 目录一、下载驱动包二、放jar包三、检测数据库连接JavaJava 如何使用 JDBC 连接 mys

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

Spring Security中用户名和密码的验证完整流程

《SpringSecurity中用户名和密码的验证完整流程》本文给大家介绍SpringSecurity中用户名和密码的验证完整流程,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定... 首先创建了一个UsernamePasswordAuthenticationTChina编程oken对象,这是S

Linux网络配置之网桥和虚拟网络的配置指南

《Linux网络配置之网桥和虚拟网络的配置指南》这篇文章主要为大家详细介绍了Linux中配置网桥和虚拟网络的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 一、网桥的配置在linux系统中配置一个新的网桥主要涉及以下几个步骤:1.为yum仓库做准备,安装组件epel-re

python如何下载网络文件到本地指定文件夹

《python如何下载网络文件到本地指定文件夹》这篇文章主要为大家详细介绍了python如何实现下载网络文件到本地指定文件夹,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下...  在python中下载文件到本地指定文件夹可以通过以下步骤实现,使用requests库处理HTTP请求,并结合o

Linux高并发场景下的网络参数调优实战指南

《Linux高并发场景下的网络参数调优实战指南》在高并发网络服务场景中,Linux内核的默认网络参数往往无法满足需求,导致性能瓶颈、连接超时甚至服务崩溃,本文基于真实案例分析,从参数解读、问题诊断到优... 目录一、问题背景:当并发连接遇上性能瓶颈1.1 案例环境1.2 初始参数分析二、深度诊断:连接状态与

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

Linux系统配置NAT网络模式的详细步骤(附图文)

《Linux系统配置NAT网络模式的详细步骤(附图文)》本文详细指导如何在VMware环境下配置NAT网络模式,包括设置主机和虚拟机的IP地址、网关,以及针对Linux和Windows系统的具体步骤,... 目录一、配置NAT网络模式二、设置虚拟机交换机网关2.1 打开虚拟机2.2 管理员授权2.3 设置子