Living a Simple Life is a Happy Life

有饭吃,自由自在,就非常开心

比特币POW难度调节分析

| Comments

比特币白皮书在工作量证明章节中解释了工作量证明(PoW)的方式:

我们在区块中补增一个随机数(Nonce),这个随机数要使得该给定区块的随机散列值出现了所需的那么多个0。我们通过反复尝试来找到这个随机数,直到找到为止,这样我们就构建了一个工作量证明机制。只要该CPU耗费的工作量能够满足该工作量证明机制,那么除非重新完成相当的工作量,该区块的信息就不可更改。由于之后的区块是链接在该区块之后的,所以想要更改该区块中的信息,就还需要重新完成之后所有区块的全部工作量。

那这个随机数难度值是怎么产生的呢?

原理是简单的,但是细节总是需要穷根究底。

比特币难度值Difficulty

难度值在区块中并不记录,仅仅是为了人类直观感受解题难度而演变出的一个浮点数。难度每2016个区块改变一次,公式如下:

1
diffculty = difficulty_1_target / currentTarget

此处的 difficulty_1_target 为一个常数,非常大的一个数字。表示矿池挖矿最大难度。目标值越小,区块生成难度越大。区块中存储的是这个名为target的值。

难度值如何存储在区块中的

在区块中存储的是Target,但是将Target经类似于浮点数的一种压缩表示法,字段为nbits。例如,如果区块bits记录为0x1b0404cb,那么他表示的十六进制的Target值为:

1
0x0404cb * 2**(8*(0x1b - 3)) = 0x00000000000404CB000000000000000000000000000000000000000000000000

在计算时,后面3个字节0x0404cb作为底,前面1字节0x1b表示次方数。具体压缩过程如下:

  • 将数字转换为256进制数

  • 如果第一位数字大于127(0x7f),则前面添加0

  • 压缩结果中的第一位存放该256进制数的位数

  • 后面三个数存放该256进制数的前三位,如果不足三位,则后面补零

例如,将数字1000压缩,先转换为256进制数

1
1000 = 0x03 * 256 + 0xe8 * 1

那么是由两个数字构成:

1
03   e8

第一个数未超过0x7f,则不需填0,但长度两位低于三位,在后面补零,最终表示为:0x0203e800

等等,我有点晕了,为什么要采取这种绕弯的存储方式呢?

  • 比特币的工作量证明本质是计算一个256bits的hash值,并保证这个值小于target,表示为公式如下:
1
SHA256(SHA256(区块头)) < Target
  • 初始Target,即difficulty_1_target设置为0x00000000FFFF0000000000000000000000000000000000000000000000000000,此时难度为1

  • Target是一个256位的很大的数,对这个数进行乘除运算需要特殊的库来处理,所以中本聪考虑用一个32位的数来近似表示Target

  • 256 / 32 = 8, 2^8 = 256,因此我们需要用256进制来表示Target,256进制的运算规则如上所述

  • 那么初始Target其实可以表示为0x1D00FFFFFF,解压验证一下:

1
    0x00ffff *256** (0x1d - 3)  = ff ff 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  • 0x1D00FFFFFF 这个值可以称为nbits,就是存储在区块中的原始值,通过nbits可以推算当前Target,通过当前Target及初始Target可以推算当前难度

难度如何调节

目标值计算公式如下,但在实际计算时有些特别处理,将目标值控制在一定范围内。

1
新目标值= 当前目标值 * 实际2016个区块出块时间 / 理论2016个区块出块时间(2周)。
  1. 判断是否需要更新目标值(2016的整数倍),如果不是则继续使用最后一个区块的目标值

  2. 计算前2016个区块出块用时

  3. 如果用时低于半周,则按半周计算。防止难度增加4倍以上。

  4. 如果用时高于8周,则按8周计算。防止难度降低到4倍以下。

  5. 用时乘以当前难度, 再除以2周

  6. 如果超过最大难度限制,则按最大难度处理

代码参考这里:

https://github.com/brain-zhang/bitcoin/blob/a1c3d8f14dca6a86fa103d86ef125e95372f860c/src/main.cpp#L857

知道nbits,如何推算全网算力

  • nbits为0x1b0404cb时,难度为:
1
0x00000000FFFF0000000000000000000000000000000000000000000000000000 / 0x00000000000404CB000000000000000000000000000000000000000000000000 = 16307.420938523983
  • 为了找到新区块,该区块的target值必须小于目标target值,实际上是一个在0到2^256-1之间的随机数,难度1的偏移量是:
1
2
   0xffff * 2^208
   
  • 难度D的偏移量是
1
2
   (0xffff * 2^208)/D
   
  • 在难度D下,为了找到新区块,我们预期要计算的HASH数量是
1
2
   D * 2^256 / (0xffff * 2^208)
   
  • 难度的设定,是为了以每10分钟一个区块的产生速度产生2016个区块,因而我们在600秒内计算 (D * 2^48 / 0xffff) 个HASH,这就意味着产生2016个区块的网络HASH速率(算力)是
1
2
   D * 2^48 / 0xffff / 600
   

可以进一步简化为:

1
2
   D * 2^32 / 600
   
  • 2018-02-12 21:00:00(UTC+8), 难度值D为2,874,674,234,415; 此时全网算力为20.75EH/S

  • 如果我有一台蚂蚁S9,算力13T/S,那么一个区块周期(10分钟)的期望BTC收益为12.5 * 13T / 20.75EH

一点小TIPS

  • 难度为1时,目标target在比特币客户端中表示为
1
0x00000000FFFF0000000000000000000000000000000000000000000000000000

但是在绝大部分矿池里面表示为

1
0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF

这样挖矿的时候,挖矿软件显示的难度和比特币客户端api调用算出来的难度有微小差别,可以忽略。这个其实时早期矿池实现的时候找方便造成的不统一,因为比特币客户端判断HASH合法性的时候用的是nbits来判断,所以不影响最终计算结果

  • 现有的算法中,难度值每2016个区块调整一次,但新的难度值不需要与难度“1”进行比较运算,而是根据前2015个块的出块时间来计算,所以严谨的计算公式为:
1
difficulty = [prev_target] * 【前2015个区块生成所用的时间】 / 1209600 (按标准每10分钟出一个块,2016个块所需要的秒数)

为啥?就是中本聪早期的代码比较糙,他在循环的时候因为还有一个genius block要处理,可能为了代码干净起见就不去特殊处理了,其实也没啥影响

Comments