深入剖析实现比特币减半的十五行代码

简介: 北京时间 5 月 12 日 3 时 23 分左右,比特币在区块高度 630000 处完成诞生以来第三次减半,比特币区块奖励由 12.5 枚 BTC 减至 6.25 枚 BTC,剩余待开采比特币数量仅剩约 262 万。那么,什么是比特币减半,该事件背后的代码工作原理是什么呢?

云栖号资讯:【点击查看更多行业资讯
在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来!

北京时间 5 月 12 日 3 时 23 分左右,比特币在区块高度 630000 处完成诞生以来第三次减半,比特币区块奖励由 12.5 枚 BTC 减至 6.25 枚 BTC,剩余待开采比特币数量仅剩约 262 万。
那么,什么是比特币减半,该事件背后的代码工作原理是什么呢?
让我们一起深入了解其中有趣的细节吧。

区块补贴和奖励减半

先来简单回顾一下一个基础知识点,所谓“矿工”,是指此时此刻分布于世界各地,正在运行硬件和软件计算下一个比特币区块哈希值的人。
如果矿工们及时解决了比特币区块链网络中的数学难题,他们就可以获得区块奖励。
以上描述里出现了不少时髦的流行语,是不是?这就来让我们对它们进行逐个解释。

什么是哈希值?

比特币整个采矿的概念设计得十分巧妙。实际上,采矿并不是一个冥思苦想解题的过程,它更多的是一种尝试猜出一个神奇数字的蛮力尝试。
比特币网络几乎在所有地方都采用了 SHA256 哈希算法。世界各地的矿工都在尝试运行的采矿功能,可以用下面这个简化版本的函数来表示:

SHA256(
    $previousBlockHash,
    $newTransactionsToBeIncluded,
    $magicNumber
);

其中 $magicNumber 也被称为随机数(nonce),在密码学中是指一个只被使用一次的数字。通过在新区块的哈希计算中包含以前区块的哈希值,实际上就形成了一个链式结构,将每个以前的区块逐个链接,一直链接到新的区块为止。因此,区块链本质上就是个高端版本的链表。
矿工们所做的就是一直不断地猜测 magicNumber 的值。他们一遍又一遍地运行相同的计算,用递增(或随机)的穷举法来试 magicNumber 的有效值。通过这样的计算方式,矿工每次都会改变上述函数的哈希值结果。
那他们什么时候算“赢”呢?
一旦他们找到一个开头 0 的数量足够多的哈希值。
就是这样:开头有足够多的 0。
这个答案就是如此简单而粗暴,看起来并不高大上。而采矿计算的难度就取决于哈希值的开头需要多少个 0。当第一个区块被开采出来时,它的哈希值开头只有 8 个 0:

00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048

而在编写本文时,该区块链计算出来的第 629828 个区块的哈希值,它开头有 19 个 0 :

0000000000000000000133e7bffe43530e508183ec48a89bad23a370692b16e8

而开头需要的 0 数量越多,就越难猜出这个随机数。
这里多说一句,比特币这个算法的精巧之处在于,它每隔 2016 个区块会自动重新计算其难度目标(即开头需要多少个 0),但本文就不再为此赘述了。
采矿奖励
如果你猜对了这个随机数,你就会得到奖励。这种奖励以新铸造出来的比特币的形式发放。
本质上,这些比特币是凭空产生的。只不过它既不便宜也不是信手拈来。比特币网络中采矿是要花费大量计算能力和电量的。付出这些辛勤劳动后,你得到了比特币。为了维护巩固这个比特币网络,你作为一名矿工会得到理所应当的奖励。
这些新铸造的比特币是在所谓的生成交易(Coinbase Transaction)中创建的。这是一种独特的交易,它包括在每个区块之中,其作用是支付一定数量的比特币奖励给猜中了正确哈希值的矿工。
是的,热门的“Coinbase 加密货币交换所”的名字就来源于这个词汇,它原本是指每个区块中对矿工提供奖励的特殊交易(生成交易)的引用。
每一个被正确计算出来的区块,会自动计算出矿工奖励,并随着比特币网络的发展而对奖励金额进行自动调整。它的工作原理也设计得十分巧妙,请让我来带你了解一下背后的代码。
GetBlockSubsidy() 函数
矿工奖励减半的魔法发生在函数 GetBlockSubsidy() 中,该函数包含在源代码的 src/validt.cpp 文件中。

CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams)
{
    int halvings = nHeight / consensusParams.nSubsidyHalvingInterval;
    // Force block reward to zero when right shift is undefined.

if (halvings >= 64)
plain
        return 0;

CAmount nSubsidy = 50 * COIN;

// Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years.
nSubsidy >>= halvings;
return nSubsidy;
}

那么,这个函数包含什么功能?让我们来剖析一下代码。虽然我已经有一段时间没碰 C 语言了,但幸运的是,这段代码相当容易读懂。
已经发生过多少次比特币减半了?

让我们从顶部开始看:

int halvings = nHeight / consensusParams.nSubsidyHalvingInterval;

上面最后一个共识参数 consensusParams.nSubsidyHalvingInterval,在 src/chainparms.cpp 文件中被定义为共识规则的一部分。这些是比特币网络上每个人都必须遵守的一套公共规则。
这里需要说明的是,对这些共识规则的任何更改都将创建一个“硬分叉(Hard fork)”,即一组不再遵循原始区块链规则的新规则。

consensus.nSubsidyHalvingInterval = 210000;

这个常量表示,每产生 210,000 个区块,就会出现一次比特币减半。其中变量 nHeight 指的是当前的区块高度(Height)。或者换句话说,已经开采出来的区块的数量。
而 5 月 12 日比特币开采数量就达到 630,000 个区块了。所以此时,这个等式就变为:

int halvings = 630000 / 210000;

这将使减半次数值变为 3。这也是比特币网络第三次将其区块奖励减半。
但是如果结果是一个浮点值,显然这个结果并不符合该变量所声明的 int 类型,那会发生什么事情呢?比如:

int halvings = 620000 / 210000;

这将得到 2.952380952 的计算结果。但是,由于变量被定义为整数类型,因此会通过直接抹去小数点后面的值,而取整为该结果范围内最小的整数值。所以,这时的减半次数是 2。
区块奖励结束
让我们来看看这个代码片段:

// Force block reward to zero when right shift is undefined.
if (halvings >= 64)
    return 0;

因为 nSubsidy 是个 64 位的有符号整数,在执行右移操作时可能出现未定义行为,这意味着 x >> 65 操作会变成 x >> 1,就会导致数值环回,所以这里需要保留对减半次数大于等于 64 的检查。这对于上面所贴的后续代码很重要。
最初的比特币核心代码并没有包含这个 bug 的修复,直到 2014 年才合并了这个 pull request 。
这部分经常被误解为“将会有 64 次比特币减半”。这并不正确。其实只会有 33 次比特币减半,后面我们会看到关于这一点的解释。

你能得到多少比特币作为奖励?

“减半”一词其实指的是对矿工可以获得的比特币数量进行限制。每采出 210,000 个区块,这个奖励金额就会减为一半。

CAmount nSubsidy = 50 * COIN;
nSubsidy >>= halvings;
return nSubsidy;

一开始,在 10 多年前,在网络上每开采出一个区块,就会奖励矿工 50 个比特币。
然后,在 210,000 个区块被开采之后,这个奖励金额被减半为 25 个比特币。又采出 210,000 个区块之后,变成了 12.5 个比特币。这就是截止到这次减半之前的情况。
而这次减半之后,比特币采矿奖励又会被减为一半,也即是说矿工只能得到 6.25 个比特币的奖励。之后再采出 210,000 个区块,奖励就会变成 3.125。以此类推。
这段代码中有一个巧妙的位运算操作,我想在这里强调一下:

nSubsidy >>= halvings;

我打赌你并不是每天都能在自己的代码中看到这样的 >>= 运算符。
让我们为每个变量填上实际的值,重写代码如下:

CAmount nSubsidy = 50 * 100000000;
nSubsidy >>= 3;
return nSubsidy;

区块奖励有一个固定的初始值 50。而按照 src/amount.h 文件中的定义,每枚比特币又可分为 1 亿个更小的基本单位(Satoshi,即“聪”)。如果想要说得更准确的话,矿工不会得到 1 整个“比特币”作为奖励,而是将 1 个比特币均分为 1 亿份,然后得到“1 亿”份这样的基本单位作为奖励。
而这样 1 亿份基本单位加在一起等于 1 个比特币。
这给出了 nSubsidy 的初始值为 50 亿基本单位。如果用二进制来表示,可以得到如下数字:

100101010000001011111001000000000

表示将位右移 3 位。这就得到了:

100101010000001011111001000000

当你将该二进制值转换为其十进制表示形式时,会得到 625,000,000。换句话说,这就是 625,000,000 个基本单位或 6.25 个比特币。
在下一次减半发生时,会随着右移一位让上面的二进制数值末尾的 0 又减少一个,从而有效地再次将奖励金额减半。
因为初始值 50 亿总共只有 33 比特位,我们将在第 33 比特减半后让区块奖励变为 0,这大约会发生在 2140 年。
且这个假设的前提是,到那个时候,交易所支付的采矿奖励应该还足以补偿矿工的电力消耗和硬件成本。

结论

比特币的货币供应量通过硬编码进行了限制,每个人都可以看到和审查这段代码。
矿工数量庞大,即使不是百万数量级,至少也是数以千计,能让这么多矿工能以和谐的方式一起工作的想法真的令人感到不可思议。如果你曾经尝试设置过任意一个包含 5 个节点的集群,你就会明白要在多个节点间达成共识或多数仲裁是多么困难的一件事情,因为你需要像比特币网络一样考虑到:

  • 如何让所有节点达成共识?
  • 如何防止结果发生偏差?
  • 如何防止网络连接中断造成的影响?
  • 如何防止数据损失造成的影响?
  • 如果一个节点在执行写操作的过程中崩溃了怎么办?

而在比特币网络上这一切都正常运转,而且是在硬件、网速、节点版本和软件均为未知的条件下运行的——这都是因为设计时所包含的数学原理和社会共识,让每个人在该网络上都遵循相同的规则。
这真是令人着迷的技术啊。

作者介绍:
Mattias Geniar,独立开发人员,Linux 系统管理员和通用问题解决者。

【云栖号在线课堂】每天都有产品技术专家分享!
课程地址:https://yqh.aliyun.com/zhibo

立即加入社群,与专家面对面,及时了解课程最新动态!
【云栖号在线课堂 社群】https://c.tb.cn/F3.Z8gvnK

原文发布时间:2020-05-19
本文作者:Mattias Geniar
本文来自:“InfoQ 微信公众号”,了解相关信息可以关注“InfoQ

相关文章
|
9月前
|
SQL 存储 安全
IPP swap螺旋上涨模式系统开发详细丨IPPswap螺旋上涨项目系统开发方案逻辑/案例介绍/源码说明
  什么是智能合约DApp   智能合约DApp是计算机程序,通过它,交易双方可以共同完成交易。如果在执行过程中发生了一系列变化,那么智能合约将会记录这些变化并自动执行这些变化。
|
存储 编解码 算法
压缩的必要性和可能性(下)| 学习笔记
快速学习压缩的必要性和可能性(下),介绍了压缩的必要性和可能性(下)系统机制, 以及在实际应用过程中如何使用。
492 0
压缩的必要性和可能性(下)| 学习笔记
|
算法 安全 区块链
区块哈希游戏竞猜系统开发(成熟代码)
区块链的四大核心技术是密码学、分布式账本、共识机制和智能合约。密码学作为其中最重要的部分,可以说是区块链的基石,其他技术都是基于密码学构建区块链的高层建筑。区块链中主要使用的密码算法有两部分: 一是哈希算法, 二是非对称加密。
|
算法 程序员
弄懂“三门问题”,成功概率翻倍,来用代码验证一下
弄懂“三门问题”,成功概率翻倍,来用代码验证一下
217 0
弄懂“三门问题”,成功概率翻倍,来用代码验证一下
|
IDE Java 开发工具
推荐一款代码神器,代码量至少省一半!
在我们 Java 项目里面,有很多 Java Bean 需要为每个属性生成 get/ set 方法,增删改属性都需要维护这些 get/ set 方法甚是麻烦。 今天给大家介绍一款能帮助我们简化这些代码的神器:Lombok!有了这个神器,你的 Java Bean 类的代码量至少可以省一半。
132 0
推荐一款代码神器,代码量至少省一半!
|
算法 Go 区块链
剥开比原看代码14:比原的挖矿流程是什么样的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 当我们以bytom init --chain_id=solonet建立比原单机节点用于本地测试时,很快会发现自己将面临一个尴尬的问题:余额为0。
1342 0
|
SQL 前端开发 .NET
剥开比原看代码15:比原是如何转帐的
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在前面几篇中,我们做了足够了准备,现在终于可以试一试转帐功能了! 这里的转帐最好使用solonet,再按前一篇文章的办法修改代码后产生单机测试币,然后再试。
1287 0
|
安全 算法 Go
剥开比原看代码04:如何连上一个比原
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在上一篇我们已经知道了比原是如何监听节点的p2p端口,本篇就要继续在上篇中提到的问题:我们如何成功的连接上比原的节点,并且通过身份验证,以便后续继续交换数据? 在上一篇中,我们的比原节点是以solonet这个chain_id启动的,它监听的是46658端口。
1136 0
|
前端开发 JavaScript
剥开比原看代码17:比原是如何显示交易的详细信息的?
作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockchain/bytom 在上上篇文章里,我们还剩下一个小问题没有解决,即前端是如何显示一个交易的详细信息的。
1265 0