一分钟理解:比特币第一次严重漏洞,生产1844亿枚比特币!是如何发生的
1 事件
2010年8月15日(也就是比特币诞生的第二年,创世区块于2009年1月3日诞生),有人发现,在比特币区块链的第74638块上,一笔让人惊愕的交易。这笔交易出现了184 467 440 737.09551616个比特币,其中有922亿个比特币被发送到两个地址,交易如下:
比特币总量是2100万——《比特币知识(3)——比特币上限为什么是2100万》,为什么这两笔交易,会大于总比特币总量?
这恐怕是比特币史上最严重一次漏洞事件了,如果不及时修复这个问题,比特币系统或许直接被毁掉。
2 溢出原理
这个问题是怎么发生的?原理其实也简单:大数溢出引起,算是编码不严谨引起的了。
在小时候,我们学数数时候,用的方法——掰手指。
从1数到10,十个手指最多可以表示到10(不考虑用脚或者半截手指情况),如果数到11,10个手指不够用,手指表示就回到1了,这算是最原始的一个大数溢出的认识吧。
计算机对数的表示也是类似,也有最大限度。
为了方便表达,我们用Go语言的uint8做例子,一个uint8 表示,uint8一个字节空间,表达的范围是:bin(00000000) 到bin(11111111),也就是0-255,如果再大,就会发生溢出,高位被抛弃掉,又会变成0开始了。
代码演示:
理解了这个,比特币的漏洞的理解就方便了。
3 故事例比
用个简朴的例子,父亲去给两个孩子交学费:父亲把钱递给学校财务,搞定,回家睡觉。
嗯~貌似还漏了些什么,得看看两个孩子学费一共要多少,自己带的钱够不够,如果够,就缴费;如果不够还能啥,筹钱去啊!
父亲这次兜里带着10块钱去给两个孩子缴129块钱的学费。
还是用Go的uint8,封装一个函数做演示,:
import (
"fmt"
)
func submit(account, cost1, cost2 uint8) (bool, uint8) {
var total = cost1 + cost2
if total <= account {
fmt.Printf("账户金额:%d, 待缴金额:(%d+%d=)%d\n", account, cost1, cost2, total)
return true, account - total
}
return false, account
}
func main() {
account := uint8(10)
cost1 := uint8(129)
cost2 := uint8(129)
success, account := submit(account, cost1, cost2)
if success {
fmt.Println("缴费成功,余额:", account)
} else {
fmt.Println("金额不够,凑钱去!")
}
}
代码看着没有问题,如果预期正常,应该输出:金额不够,凑钱去!
但实际上却缴费成功了。
(对了,哪的学校兜里10块钱就敢去交学费了?告诉我,我也去霸个学位)
学费为129+129=258(也就是256+2),在一个字节的 uint8存储,就发生了溢出。
回到比特币的那个漏洞,就是和“父亲交学费”的逻辑一样,发生错误的原因:当时的比特币系统仅检测转出的总额是否小于未花费输出,而没有检测每笔转出是否小于未花费输出,以及也没有考虑总额是否有溢出的情况。
当时转出两个922亿个比特币的账户,仅有0.5btc,为什么会转账成功?
因为两个转出额之和发生了溢出(和uint8溢出原理一样,不同是其他的小数类型的溢出):
打包区块奖励
= 输入金额 - 总输出金额
= 0.50btc - (92233720368.54275808 + 92233720368.54275808))
= 0.50 btc- (-0.010btc))
= 0.51btc
这个打包奖励被矿机收囊,更大的影响是无中生有的发行了1844亿个比特币。
4 问题修复
在漏洞被披露后的三小时内,中本聪及其他比特币开发者发布了0.3.1版本的修复程序,此更新引入了逻辑,拒绝具有溢出输出值的交易。
在第74691区块时候,带补丁版本的块链终于追赶上出现漏洞的块链,也就是大约20多小时后,比特币以硬分叉情况有惊无险地解决了这次最为重大的危机事件。
对于大数溢出逻辑代码如何修复,就不用讲述了,应该会coding 的伙伴都懂,可能实际编码中不太严谨而已。
(全文完)
(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn )