《区块链开发指南》一一1.3 挖矿、矿池

简介:

本节书摘来自华章计算机《区块链开发指南》一书中的第1章,第1.3节,作者:申屠青春 主编 宋 波 张 鹏 汪晓明 季宙栋 左川民 编著更多章节内容可以访问云栖社区“华章计算机”公众号查看。

1.3 挖矿、矿池

1.3.1 挖矿原理与区块的产生
比特币的挖矿和节点软件是基于对等网络、数字签名来发起和验证交易的。节点向网络广播交易,这些广播出来的交易需要经过矿工的验证,矿工们会用自己的工作证明结果来表达确认,确认后的交易会被打包到数据块中,数据块会串起来形成连续的数据块链。中本聪本人设计了第一版的比特币挖矿程序,这一程序随后被开发为广泛使用的第一代挖矿软件bitcoind,这一代软件在2009年到2010年期间都比较流行。
每一个比特币的节点都会收集所有尚未确认的交易,并且会将其归集到一个数据块中,这个数据块将和前面一个数据块集成在一起。矿工节点会附加一个随机调整数,并计算前一个数据块的SHA-256 Hash运算值。挖矿节点不断进行重复尝试,直到它找到的随机调整数使得产生的Hash值低于某个特定的目标为止。由于Hash运算是不可逆的,因此寻找到符合要求的随机调整数将会非常困难,因此需要一个可以预计总次数的不断试错的过程。这时,工作量证明机制就发挥作用了。当一个节点找到了符合要求的解,那么它就可以向全网广播自己的结果。其他节点就可以接收这个新解出来的数据块,并检验其是否符合规则。只要其他节点通过计算Hash值发现其确实满足要求(比特币要求的运算目标),那么该数据块就是有效的,其他的节点就会接受该数据块,并将其附加在自己已有的链条之后。
当挖矿时,你会经常对区块头进行散列,你正在挖的区块也会时常进行更新,一个区块头如上文所述的表1-3所示。
表1-3中的大部分数据项对所有用户都是一致的,不过,在时间戳上可能会有些区别。如果当前区块的时间戳大于前11个区块的平均时间戳,并且小于“网络调整时间(Network-Adjusted Time)”+ 2小时,则认为该时间戳是有效的。其中的“网络调整时间”是指与你相连接的所有节点的平均时间。当节点A连接到节点B时,A将从B处得到一个UTC的时间戳,A先将其转换成本地UTC并保存起来,网络调整时间等于所有节点的本地UTC时间+所有相连节点的偏移量平均值,然而,该网络时间永远不会调整到超过本地系统时间70分钟以上。
Nonce随机数通常都不会相同,但是它以严格的线性方式在增长,从0开始,每次执行散列时都会增长,当Nonce溢出时(此事经常发生),挖矿交易的extraNonce项就会增长,其将改变Merkle树的根节点。
假定针对这些数据项,人们经常会独自产生同样序列号的Hash值,那么,最快的矿机通常会赢。然而,两人产生同样的Merkle根节点基本上(或几乎)是不可能的,因为区块中的第一个交易是挖矿交易并且会“发送”到你独一无二的比特币地址上。而你的区块与其他人的区块是有区别的,因此,产生的Hash值肯定也会(几乎可以肯定)不同,你计算的每个Hash值和网络中的其他人一样,都有同样的获胜机会。
比特币使用SHA256(SHA256(区块头))计算Hash值,但要注意字节序。例如,以下的Python代码用于计算某一区块的Hash值,使用的是2011年6月区块号为125552的最小Hash值。该区块头建立在表1-3所示的6个数据项之上,并且以十六进制的小端结尾方式连接在一起。

>>> import hashlib
>>> header_hex = ("01000000" +
    "81cd02ab7e569e8bcd9317e2fe99f2de44d49ab2b8851ba4a308000000000000" +
    "e320b6c2fffc8d750423db8b1eb942ae710e951ed797f7affc8892b0f1fc122b" +
    "c7f5d74d" +
    "f2b9441a" +
     "42a14695")
>>> header_bin = header_hex.decode('hex')
>>> hash = hashlib.sha256(hashlib.sha256(header_bin).digest()).digest()
>>> hash.encode('hex_codec')
  '1dbd981fe6985776b644b173a4d0385ddc1aa2a829688d1e0000000000000000'
>>> hash[::-1].encode('hex_codec')
  '00000000000000001e8d6829a8a21adc5d38d0a473b144b6765798e61f98bd1d'

实际的Hash值是一串256位的数值,首部有许多零。当以大头端十六进制常数方式打印或存储时,它的首部有许多零;如果它以小头端打印或存储时,零就会变换到尾部。例如,如果表示成字节串-最低(或者开头)的字节串地址显示最小位的数,那么这就是小头端表示。blockexplorer的输出把Hash值显示为大头端表示的数值,因为数字的表示通常是首部数字是最大的数字(从左向右读)。
1.3.2 挖矿难度
挖矿难度是对挖矿困难程度的度量,即指计算符合给定目标的一个Hash值的困难程度。比特币网络有一个全局的区块难度,有效的区域必须有一个Hash值,该Hash值必须小于给定的目标Hash值。矿池也会有一个自定义的共享难度,用来设定产生股份的最低难度限制。
难度每过2016块就会改变一次,计算公式为:
difficulty = difficulty_1_target/current_target
其中,目标(target)是一个256位长的数值。
测量难度有许多不同的方法,通过这些方法得到的diff?iculty_1_target有可能也会不同。传统情况下,它表示一个Hash值,前32位为0,后续部分为1(称之为矿池难度或pdiff),比特币协议把目标Hash值表示成一个固定精度的自定义浮点类型,因而,比特币客户端用该值来估计难度(称之为bdiff)。
难度经常被存储在区块中,每个块存储一个十六制的目标Hash值的压缩表达式(称之为Bits),目标Hash值可以通过预先定义的公式计算出来。例如:如果区块中压缩的目标Hash值为0x1b0404cb,那么十六进制的目标Hash值就为如下形式:

0x0404cb×28×(0x1b - 3) = 0x00000000000404CB000000000000000000000
  ???  000000000000000000000000000

因而目标Hash值为0x1b0404cb时,难度为:

0x00000000FFFF0000000000000000000000000000000000000000000000000000/
0x00000000000404CB000000000000000000000000000000000000000000000000
= 16307.420938523983 (bdiff)

或者:

0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF/
0x00000000000404CB000000000000000000000000000000000000000000000000
= 16307.669773817162 (pdiff)

其中:0x00000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF是挖矿机使用的最大目标Hash值。而0x00000000FFFF00000000000000000000000000000
00000000000000000000000则是比特币网络使用的浮点编码类型,后面的位数被缩短了。
下面是一个快速计算比特币难度的方法,这里使用的算法是修改的泰勒序列(读者可以参考wikipedia上的教程),并且依赖记录来转换难度计算。

#include <iostream>
#include <cmath>
inline float fast_log(float val)
{
  int * const exp_ptr = reinterpret_cast <int *>(&val);
  int x = *exp_ptr;
  const int log_2 = ((x >> 23) & 255) - 128;
  x &= ~(255 << 23);
  x += 127 << 23;
  *exp_ptr = x;

  val = ((-1.0f/3) * val + 2) * val - 2.0f/3;
  return ((val + log_2) * 0.69314718f);
} 

float difficulty(unsigned int bits)
{
    static double max_body = fast_log(0x00ffff), scaland = fast_log(256);
    return exp(max_body - fast_log(bits & 0x00ffffff) + scaland * (0x1d - ((bits & 0xff000000) >> 24)));
}

int main()
{
    std::cout << difficulty(0x1b0404cb) << std::endl;
    return 0;
}

如果想要了解难度计算的数学原理,那么可以看看如下的Python代码:

import decimal, math
l = math.log
e = math.e
print 0x00ffff * 2**(8*(0x1d - 3)) / float(0x0404cb * 2**(8*(0x1b - 3)))
print l(0x00ffff * 2**(8*(0x1d - 3)) / float(0x0404cb * 2**(8*(0x1b - 3))))
print l(0x00ffff * 2**(8*(0x1d - 3))) - l(0x0404cb * 2**(8*(0x1b - 3)))
print l(0x00ffff) + l(2**(8*(0x1d - 3))) - l(0x0404cb) - l(2**(8*(0x1b - 3)))
print l(0x00ffff) + (8*(0x1d - 3))*l(2) - l(0x0404cb) - (8*(0x1b - 3))*l(2)
print l(0x00ffff / float(0x0404cb)) + (8*(0x1d - 3))*l(2) - (8*(0x1b - 3))*l(2)
print l(0x00ffff / float(0x0404cb)) + (0x1d - 0x1b)*l(2**8)

前难度可以通过http://blockexplorer.com/q/getdiff?iculty来获得,下一个难度可以通过http://blockexplorer.com/q/estimate来获得。难度的变化情况可以查看http://bitcoin.sipa.be/
最大难度大约等于maximum_target/1(因为0会导致无穷大),这是一个非常大的数值,大约为2224;当maximum_target为最小值1时,则最小难度值也为1。
难度可根据以前2016个区块的产生时间而定,每过2016块就会改变一次。预计每隔10分钟产生一个区块,因而产生2016个区块要花费2周时间。如果前2016个区块的产生时间多于两周,则难度会降低;否则难度就会增加。
为了找到新区块,该区块的Hash值必须小于目标Hash值,实际上它是一个在0到2256-1之间的随机数,难度1的偏移量是:
0xffff×2208
难度D的偏移量是:
(0xffff×2208)/D
在难度D下,为了找到新区块,预期要计算的Hash数量是:
D×2256/(0xffff×2208)
或者只是:
D×248/0xffff
难度的设定,是为了以每10分钟一个区块的速度产生2016个区块,因而,在600秒内计算(D×248/0xffff)个Hash,这就意味着产生2016个区块的网络Hash速率(算力)是:
D×248/0xffff/600
可以进一步简化为:
D×232/600
以上公式有较好的精度。
在难度为1时,算力是7Mhashes/秒,难度是5?006?860?589,这就意味着以前2016个区块被找到,其平均算力是:35.840PHash/s。
5?006?860?589×232/600≈35.840 PHash/s
发现一个区块的平均时间,可以用以下公式估计:
时间=难度×232/算力
其中,难度是当前的难度,算力是指矿机的计算能力,以hashes/s为单位,时间是你找到两个区块的平均时间。举例来说,使用Python计算,算力为1Ghashes/s的矿机,难度在20?000时,产生一个新区块的时间,计算式如下:
$ python -c "print 20000 * 232 / 109 / 60 / 60.0"
23.85
其中**表示指数,该语句运算的结果就是:找到一个新区块要花费近1天的时间。
1.3.3 矿池原理与商业模式
随着生成区块的难度逐步增加,挖矿变成了一个碰运气的事情,单一节点要生成一个区块需要花费数年的时间(除非这个单一节点拥有大量的计算力)。为了激励计算力较低的用户继续参与挖矿,矿池就出现了。在一个矿池里,许多不同的人贡献出自己的计算力来生成一个区块,然后再根据每个人的贡献比例来分发奖励。通过这种方式,要得到那个50个比特币的奖励就不必等待数年的时间了,小矿工能定期得到属于他们那部分的比特币奖励。一个share(贡献/股份)为一个矿池给客户端的一个合法的工作证明,同时这也是用来生成区块的工作证明,但是没有这么复杂,只需要很少的时间就能达到一个share。
矿池是比特币(Bitcoin)等P2P密码学虚拟货币开采所必需的基础设施,一般是对外开放的团队开采服务器。其存在的意义是提升比特币开采的稳定性,使矿工薪酬趋于稳定,目前国内较为著名的比特币商业矿池有F2Pool、BTCC Pool、BW Pool、BTC.com等。
关于矿池挖矿的方式,目前存在如下几种不同的方式。
Slush方式:Slush矿池基于积分制,较老的shares将比新的shares拥有更低的权重,以减少一轮中切换矿池的投机分子。
Pay-Per-Share方式:该方式为立即为每一个share支付报酬。该支出来源于矿池现有的比特币资金,因此可以立即取现,而不用等待区块生成完毕或确认之后。这样可以避免矿池运营者幕后操纵。这种方法减少了矿工的风险,但将风险转移给了矿池的运营者。运营者可以收取手续费来弥补这些风险可能造成的损失。
Luke-Jr方式:该方式借用了其他方式的长处,如Slush方式一样,矿工需要提供工作证明来获得shares,如Puddinpop方式一样,当区块生成时马上进行支付。但是不像之前的方式,一个区块的shares,会被再次利用来生成下一个区块。为了区分参与矿工的交易传输费用,只有当矿工的余额超过1BTC时才进行支付。如果没有达到1BTC,那么将在下一个区块生成时进行累计。如果矿工在一周内没有提供一个share,那么矿池会将剩下的余额进行支付,不管余额是多少。
Triplemining方式:该方式是将一些中等大小矿池的计算力合并起来,然后将获得奖励的1%按照各个矿池计算力的比例分发给矿池运营者。
P2Pool方式:P2Pool的挖矿节点工作在类似于比特币区块链的一种shares链上。由于没有中心,所以也不会受到DoS攻击。和其他现有的矿池技术都不一样,在该方式下,每个节点工作的区块,都包括支付给前期shares的所有者及该节点自己的比特币。99%的奖励(50BTC +交易费用)会平均分配给矿工,另外0.5%会奖励给生成区块的人。
Puddinpop方式:一种使用“元哈希”技术的方式,使用特定的Puddinpop挖矿软件,现在已经没有矿池使用这种方式了。
目前使用较多的方式为Pay-Per-Share(PPS),矿工使用起来也比较方便。
但从去中心化的角度来说,还是推荐P2Pool,在避免DoS攻击的同时,也能防止个别矿池拥有超大的计算力而对比特币网络造成威胁。不过P2Pool的使用方式较PPS更为复杂。

相关文章
|
存储 算法 安全
数字货币区块链合约系统开发指南与方案
区块链的工作原理是通过共识算法来解决网络中的节点之间的信任问题
|
供应链 算法 测试技术
Golang 区块链开发指南
Golang 区块链开发指南
|
算法 区块链 数据安全/隐私保护
区块链去中心化交易所系统开发成熟技术|开发指南与流程
Web3算法革命将会在多个领域产生深远的影响。首先,它将会对数据安全和隐私保护产生积极的作用
|
区块链 开发者
《EOS区块链应用开发指南》| 每日读本书
本书结合实战经验,从基础的概念和原理,到一线的执行与案例,对EOS技术进行了系统且深入的阐述。
1126 0
|
区块链
区块链每日一问 | 矿场和矿池分别是什么?它们有什么关系?
比特币也是“挖”出来的,只不过它是由计算机在虚拟网络世界中开采出来的。
1340 0