922亿个BTC的惊天bug|区块链安全经典案例

这个案例的经典程度,让人惊掉下巴。那一天,所有看到它的人都惊呆了,天哪,还有这样的事?怎么能发生这种事?

此事发生的时候,中本聪还活着(我高度怀疑他早已不在人世了),至少是还活跃着,他和他的伙伴们(至少有Gavin Andresen)不得不紧急做出响应,赶紧发布新的补丁,作废有bug的区块,以一种非常规方式解决了问题。

当然,这是没有面子的,这就好比报纸都印出来几天了,突然说这几天的报纸全部作废,重新印制发行一样。大家知道,区块链之所以牛,是因为它的不可篡改性,现在可好,官方完全可以回滚重来嘛!

只不过,换了谁当官方,都会别无选择吧,因为不改就是死路一条。比特币宣称永远只有2100万个,出事以后,系统内出现1845亿个比特币,这还怎么玩。

现在我们看一下到底发生了什么,又是怎么发生的。

一个奇怪的发现

我们知道,比特币于2009年1月3日(UTC)产生了第一个区块,之后,就按着平均每10分钟一个区块的速度持续输出。

然而,当比特币输出第74638区块时(2010年8月15日,UTC),有人发现了明显的不对劲,此人是比特币前核心开发人员Jeff Garzik,当时,他已经创业了一家区块链公司叫Bloq。

他在比特币社区论坛(bitcointalk.org)里公布了这个“奇怪”的问题:

922亿个BTC的惊天bug|区块链安全经典案例_第1张图片

帖子标题是: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)

922亿个BTC的惊天bug|区块链安全经典案例_第2张图片

这里再继续科普一下:

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,如下图所示:

922亿个BTC的惊天bug|区块链安全经典案例_第3张图片

这个奇怪交易的输入,正是前一天晚上那个交易的第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,和前面看到的正好一致。

922亿个BTC的惊天bug|区块链安全经典案例_第4张图片

赶紧修补!

正常的第74637块,产出于8月15日11点34分;那个“奇怪”的74638块,产于12点05分;Jeff发帖子说“奇怪”的时候,已经是下午1点08分了。

下午3:39,Gavin Andresen给出了第一个补丁。3

922亿个BTC的惊天bug|区块链安全经典案例_第5张图片

他说,“我做了极少的测试,这个修补看来可以工作,当然还会有更好的补丁……你需要重新下载坏块之前的部分区块,删除blkindex.dat和blk0001.dat文件”。

922亿个BTC的惊天bug|区块链安全经典案例_第6张图片Gavin Andresen:BTC前核心开发人员

下午3:59,中本聪做出了响应4

922亿个BTC的惊天bug|区块链安全经典案例_第7张图片

他说,“这是初步的修改,大家看看对吗?这不是全部,我还要做更多的改动,很快会上传SVN。”

注:那时,比特币的源码在SourceForge提供的SVN上维护。

下午4:40,中本聪往SVN代码库上传了第一个官方补丁5

922亿个BTC的惊天bug|区块链安全经典案例_第8张图片

最终,在下午6:48,中本聪发布了0.3.10补丁完美修复了该问题6,不再需要删除数据库文件,而是在程序中自动识别和拒绝错误的第74638块。

922亿个BTC的惊天bug|区块链安全经典案例_第9张图片

而矿工们,在下午6:53,产生了正确的74638块7。所以,在现在的比特币区块链上,已经看不到那个奇怪的块了。

922亿个BTC的惊天bug|区块链安全经典案例_第10张图片正确的74638块

自此,这场危机解除。

中本聪改了什么

比特币的源码一开始是在SourceForge上维护的。但自2011年9月13日,比特币的开发库就迁移到了https://github.com/bitcoin/bitcoin/。

我们可以在github上查看v0.3.10的改动:

922亿个BTC的惊天bug|区块链安全经典案例_第11张图片

这次改动中最核心的部分位于main.h:

922亿个BTC的惊天bug|区块链安全经典案例_第12张图片

注意第19行定义了一个币有多少聪,第21行定义了整个系统有多少个比特币。在交易检查函数(CheckTransaction函数)的定义中,遍历交易序列时,加入了两个检查:每个输出的值不能大于比特币总数;该交易所有输出值的和不能大于比特币总数。

虽然,这种改法,并没有直接处理溢出这种可能,但由于每个输出的值不能大于比特币总数,事实上也就不可能溢出了。

故事的启示

我们常说,区块链是建立在共识之上的。

搞区块链的人又常说,代码即法律(code is law)。

还有人说:一切都由代码说了算(everything was done by the code)。

在我看来,代码是共识,但最终,人心是共识。

如果代码出现问题,人们肯定不会袖手旁观,必然是修改代码。

所以,现在的比特币,未必是未来的比特币,正如现在的以太坊早已不是以前的以太坊。

比特币的总量是2100万个,但我们已经看到,这只不过是一行代码的事。

如果官方都认为2100万个不够用,他们就会更改那行代码。

这会发生吗?

我想不会。

漂亮的一击

不得不说,这名黑客真是酷毙了,他干了一件载入史册的事。

他发现了bug,但他不屑于不痛不痒地提出。

他要让比特币开发团队大惊失色。

他知道自己并不会因此获得什么,因为这个块肯定会被回滚。

他就是要让人们知道,看,你们弄的这个东西,有bug!

快改吧!

他是编程高手,他认真研读了比特币代码,并亲手制作了交易。

他有至少235.5个比特币,现在仍然静静躺在他的地址上。

翻一翻区块链,你就会知道,他有更多。

文|卫剑钒

参考文献:

  1. https://bitcointalk.org/index.php?topic=822.msg9481#msg9481 

  2. https://bitcointalk.org/index.php?topic=822.msg9503#msg9503 

  3. http://bitcointalk.org/index.php?topic=823.msg9524#msg9524 

  4. https://bitcointalk.org/index.php?topic=823.msg9530#msg9530 

  5. http://bitcointalk.org/index.php?topic=823.msg9548#msg9548 

  6. https://bitcointalk.org/index.php?topic=827.0 

  7. https://bitcoinblockexplorers.com/block/000000000069e1affe7161ab4bcbeacebb4ddf155b50e807f42de971b688a09b 

你可能感兴趣的:(922亿个BTC的惊天bug|区块链安全经典案例)