《区块链开发指南》一一1.3 挖矿、矿池-阿里云开发者社区

开发者社区> 华章出版社> 正文
登录阅读全文

《区块链开发指南》一一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更为复杂。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享: