这个案例的经典程度,让人惊掉下巴。那一天,所有看到它的人都惊呆了,天哪,还有这样的事?怎么能发生这种事?
此事发生的时候,中本聪还活着(我高度怀疑他早已不在人世了)
,至少是还活跃着,他和他的伙伴们(至少有Gavin Andresen)
不得不紧急做出响应,赶紧发布新的补丁,作废有bug的区块,以一种非常规方式解决了问题。
当然,这是没有面子的,这就好比报纸都印出来几天了,突然说这几天的报纸全部作废,重新印制发行一样。大家知道,区块链之所以牛,是因为它的不可篡改性,现在可好,官方完全可以回滚重来嘛!
只不过,换了谁当官方,都会别无选择吧,因为不改就是死路一条。比特币宣称永远只有2100万个,出事以后,系统内出现1845亿个比特币,这还怎么玩。
现在我们看一下到底发生了什么,又是怎么发生的。
我们知道,比特币于2009年1月3日(UTC)产生了第一个区块,之后,就按着平均每10分钟一个区块的速度持续输出。
然而,当比特币输出第74638区块时(2010年8月15日,UTC),有人发现了明显的不对劲,此人是比特币前核心开发人员Jeff Garzik,当时,他已经创业了一家区块链公司叫Bloq。
他在比特币社区论坛(bitcointalk.org)里公布了这个“奇怪”的问题:
帖子标题是:Strange block 74638 (奇怪的第74638块),时间是2010年8月15日下午06:08:49 UTC。CDT时间为下午01:08,北京时间为8月16日上午02:08。
本文中截图的时间都是UTC时间。北美中部夏令时(CDT)要比UTC晚5个小时(-5),而北京时间(CST)要提早8个小时(+8)。为了有画面感,下面我们的解说都用CDT时间,因为那几个处理问题的人,大约都在北美。
他在帖子里说,“第74638块的输出值很奇怪啊,居然是92233720368.54277039个BTC!我怀疑是UINT64_MAX?”
为了让更多人看懂这个故事,这里略微科普一下(值得小白读若干遍)
:
1. 比特币的区块里面,除了区块头,就是交易列表。(所谓交易,就是转账。)
2. 每个交易,有输入和输出两部分,输入就是转账来源(钱从哪来),输出就是转账目的地(钱到哪去)。
3. 每个交易的输入和输出可以是多个,输入金额之和减去输出金额之和就是给矿工的手续费。
4. 每个区块的第一个交易为coinbase交易,用来奖励挖出区块的矿工。金额为:固定奖励+所有的交易手续费,早期每挖出一个区块的固定奖励50个比特币(现在是6.25个)。
5. 现在的比特币区块链,每个区块里有两三千个交易, 早期的时候,玩的人很少,只有几个交易。
Garzik在帖子里附上了这个区块的主要信息,我用省略号略去了不太相关的内容,并增加了中文注释。
{
//区块头:本区块的hash值
"hash" : "0000000000790ab3f22ec756ad43b6ab569abf0bddeb97c67a6f7b1470a7ec1c",
//区块头:上个区块的hash值,位于本区块头中
"prev_block" : "0000000000606865e679308edf079991764d88e8122ca9250aef5386962b6e84",
//区块头:时间戳,为北京时间2010-08-16 01:05:57 AM
"time" : 1281891957,
……
//区块包含的交易列表:本区块一共有两个交易。
"tx" : [
{
//交易1的hash值。注:本交易是coinbase交易,也即奖励矿工的交易
"hash" : "012cd8f8910355da9dd214627a31acfeb61ac66e13560255bfd87d3e9c50e1ca",
……
//交易1的输入信息
"in" : [
{
//交易1的UTXO引用,因为是Coinbase交易,所以引用交易的hash为0.
"prev_out" : {
"hash" : "0000000000000000000000000000000000000000000000000000000000000000",
"n" : 4294967295
},
"coinbase" : "040e80001c028f00"
}
],
//交易1的输出信息
"out" : [
{
//交易1的输出值:50.51,也即矿工得到奖励为50.51个比特币,50个是系统奖励,0.51是交易费
"value" : 50.51000000,
"scriptPubKey" : ……
}
]
},
{
//交易2 的hash。注:该交易有1个输入,两个输出。
"hash" : "1d5e512a9723cbef373b970eb52f1e9598ad67e7408077a82fdac194b65333c9",
……
//交易2的输入信息
"in" : [
{
//交易2引用的UTXO
"prev_out" : {
"hash" : "237fe8348fc77ace11049931058abb034c99698c7fe99b1cc022b1365a705d39",
"n" : 0
},
"scriptSig" : ……
}
],
//交易2的输出信息
"out" : [
{
//交易2的输出1,*这就是那个太离谱的值!!*
"value" : 92233720368.54277039,
"scriptPubKey" : "OP_DUP OP_HASH160 0xB7A73EB128D7EA3D388DB12418302A1CBAD5E890 OP_EQUALVERIFY OP_CHECKSIG"
},
{
//交易2的输出2,*又是一个太离谱的值!!*
"value" : 92233720368.54277039,
"scriptPubKey" : "OP_DUP OP_HASH160 0x151275508C66F89DEC2C5F43B6F9CBE0B5C4722C OP_EQUALVERIFY OP_CHECKSIG"
}
]
}
],
……
}
很明显,交易2的两个输出太离谱了,转账金额居然都是92233720368.54277039个BTC!加起来就是1845亿个!(四舍五入)
所有人都知道,比特币一共才2100万个,这算怎么回事?
20分钟后,ID为theymos的用户证实了这一问题1。注意此人给出的输出值更为准确,即:92233720368.54275808。至于Jeff一开始贴出来的那个值为什么不准,这里就不细究了(姑且认为是Jeff所用软件的小bug)
。
这里再继续科普一下:
1. 很多人以为比特币是不可分割的,1个就是1个,其实不然,比特币是可以再细分的,比如0.1个,0.01个,0.00078个,都可以,最多可以细分到小数点后8位。正如人民币最小的单位是“分”(小数点后2位),比特币最小单位是“聪”(小数点后8位),一个比特币就是一亿个聪。
前面讲过:对于某个交易,发送金额(即输入之和)减去接收金额(即输出之和)就是矿工可得的手续费,比如输入为10个比特币,输出为9.9个比特币,那么就是发送者自愿交0.1比特币的手续费。对的,手续费是用户自己设置的,手续费越高,矿工就会越快让交易上区块链。
2. 在比特币系统中,每笔交易的每个输入都指明了比特币的来源,这个来源必然是以前某个交易的某个输出,而且是还未花费的输出,此即UTXO,一旦被花费了,就花光其中所有的金额,这个UTXO就不复存在。
3. 输入具体是历史上哪个交易的第几个输出呢,输入信息会标明的,用hash值来标明是哪个交易,用n标明是第几个输出。
那这个“奇怪”交易的输入金额是多少呢,查一下这个输入的hash就可以发现,输入所指向的历史交易,是在2010-08-14 23:05:17 UTC发生的(事件发生前一天晚上),在第74421个区块中,这个交易的输出是两个:一个是0.5BTC,一个是235BTC,如下图所示:
这个奇怪交易的输入,正是前一天晚上那个交易的第1个输出(n=0),也即0.5BTC。
至此可以断定,比特币程序肯定出问题了,因为程序必然会做这样的校验:
输入总和 > 输出总和
根据上一节的信息:输入金额为0.5,输出金额为1845亿。
难道 0.5 > 92233720368.54 + 92233720368.54 ?
Jeff在帖子里面问,922亿是UINT64_MAX吗,底下很快有答复者说:
这是2^63/10^8, 所以这更像是INT64_MAX, 而不是UINT64_MAX
学过C语言编程的大都知道,约定俗成,INT64指的就是64位整数(就是long long int),UINT64就是无符号整数64位(unsigned long long int),MAX就是最大值的意思。
对于一个有符号的整数,首位为1的话,就是负数,首位为0,就是正数。
在比特币程序中,是用INT64来表示比特币金额的,单位是聪,从最小值到最大值,就是从-9223372036854775808聪到9223372036854775807聪。
这样,事情就比较明朗了,大概率是程序员写程序的时候,忘了溢出的事!忘了两个很大的整数加起来,会溢出成一个负数!
当时的比特币代码,检查了每个交易的输出是不是负数,但是却忘了检查输出的总和是不是负数。
下午2:34,ID为Ifm的用户给出了准确的分析2:
输出1的值:9223372036854275808 (7ffffffffff85ee0)
输出2的值:9223372036854275808 (7ffffffffff85ee0)
这两个值加起来,对于有符号数,就是-1000000。
关于这一点,我写了个C程序验证了一下:
#include
typedef long long INT64;
int main()
{
INT64 i1, i2;
i1 = i2 = 9223372036854275808;
printf( "%lld + %lld = %lld \n", i1 , i2, i1 + i2);
return 0;
}
程序输出结果为:
9223372036854275808 + 9223372036854275808 = -1000000
这就是100万个聪,就是-0.01个比特币。
这样就完全对得上了。
输入为0.5比特币,输出是-0.01比特币。
由于手续费 = 输入 - 输出,所以手续费为:
0.5-(-0.01)= 0.51
该块挖出来的奖励是50比特币,加上手续费,总值为50.51,和前面看到的正好一致。
正常的第74637块,产出于8月15日11点34分;那个“奇怪”的74638块,产于12点05分;Jeff发帖子说“奇怪”的时候,已经是下午1点08分了。
下午3:39,Gavin Andresen给出了第一个补丁。3
他说,“我做了极少的测试,这个修补看来可以工作,当然还会有更好的补丁……你需要重新下载坏块之前的部分区块,删除blkindex.dat和blk0001.dat文件”。
下午3:59,中本聪做出了响应4:
他说,“这是初步的修改,大家看看对吗?这不是全部,我还要做更多的改动,很快会上传SVN。”
注:那时,比特币的源码在SourceForge提供的SVN上维护。
下午4:40,中本聪往SVN代码库上传了第一个官方补丁5。
最终,在下午6:48,中本聪发布了0.3.10补丁完美修复了该问题6,不再需要删除数据库文件,而是在程序中自动识别和拒绝错误的第74638块。
而矿工们,在下午6:53,产生了正确的74638块7。所以,在现在的比特币区块链上,已经看不到那个奇怪的块了。
自此,这场危机解除。
比特币的源码一开始是在SourceForge上维护的。但自2011年9月13日,比特币的开发库就迁移到了https://github.com/bitcoin/bitcoin/。
我们可以在github上查看v0.3.10的改动:
这次改动中最核心的部分位于main.h:
注意第19行定义了一个币有多少聪,第21行定义了整个系统有多少个比特币。在交易检查函数(CheckTransaction函数)的定义中,遍历交易序列时,加入了两个检查:每个输出的值不能大于比特币总数;该交易所有输出值的和不能大于比特币总数。
虽然,这种改法,并没有直接处理溢出这种可能,但由于每个输出的值不能大于比特币总数,事实上也就不可能溢出了。
我们常说,区块链是建立在共识之上的。
搞区块链的人又常说,代码即法律(code is law)。
还有人说:一切都由代码说了算(everything was done by the code)。
在我看来,代码是共识,但最终,人心是共识。
如果代码出现问题,人们肯定不会袖手旁观,必然是修改代码。
所以,现在的比特币,未必是未来的比特币,正如现在的以太坊早已不是以前的以太坊。
比特币的总量是2100万个,但我们已经看到,这只不过是一行代码的事。
如果官方都认为2100万个不够用,他们就会更改那行代码。
这会发生吗?
我想不会。
不得不说,这名黑客真是酷毙了,他干了一件载入史册的事。
他发现了bug,但他不屑于不痛不痒地提出。
他要让比特币开发团队大惊失色。
他知道自己并不会因此获得什么,因为这个块肯定会被回滚。
他就是要让人们知道,看,你们弄的这个东西,有bug!
快改吧!
他是编程高手,他认真研读了比特币代码,并亲手制作了交易。
他有至少235.5个比特币,现在仍然静静躺在他的地址上。
翻一翻区块链,你就会知道,他有更多。
文|卫剑钒
参考文献:
https://bitcointalk.org/index.php?topic=822.msg9481#msg9481
https://bitcointalk.org/index.php?topic=822.msg9503#msg9503
http://bitcointalk.org/index.php?topic=823.msg9524#msg9524
https://bitcointalk.org/index.php?topic=823.msg9530#msg9530
http://bitcointalk.org/index.php?topic=823.msg9548#msg9548
https://bitcointalk.org/index.php?topic=827.0
https://bitcoinblockexplorers.com/block/000000000069e1affe7161ab4bcbeacebb4ddf155b50e807f42de971b688a09b