#挖矿难度精讲 ##挖矿难度
比特币主链上,平均每十分钟会出一个块。随着数字货币的发展,参与的miner数量与日俱增,挖矿技术日新月异,全网的算力也是以惊人的速度增长。BTC中为了保证主链平均的高度增加速度依然维持最初设定,进而设置了挖矿难度调整的功能。深入理解挖矿难度的概念,以及挖矿难度调整的方案,对开发人员以及miner都很重要,因为挖矿难度设置不合理可能会导致全网出块速度极不稳定。本文将详细介绍BTC&BCH挖矿难度及调整方案,我们先从PoW算法讲起。
####1.PoW算法
PoW(Proof-of-Work)算法工作量证明(Proof-of-Work,PoW)是一种对应服务与资源滥用、或是阻断服务攻击的经济对策。一般是要求用户进行一些耗时适当的复杂运算,并且答案能被服务方快速验算,以此耗用的时间、设备与能源做为担保成本,以确保服务与资源是被真正的需求所使用。
PoW算法具有:去中心化,单向,随机性,目标难度易调整等特点,所以现在包括BTC,BCH在内的很多币种都采用了PoW共识机制。
从实现上来说,PoW算法的输入为任意长度,输出为固定长度,比如通常使用SHA256算法对应输出256-bit。在挖矿过程中miner用PoW算法计算整个块头的hash值,由于SHA256的特性:块头任意一位发生变化,得到的hash值会变得完全不一样,而且大小变化方向不确定。于是,我们比较hash值是否小于某个值(实际上这个值是保存在块头中的nBit“解压后”的current_target值)来判断是否满足要求;如果小于,则广播这个区块;如果不小于,则按照当前挖矿节点的规则改变块头中可以改变的值,然后再次计算块头hash值,以此往复,直到结果小于目标值。
由此可知,current_target值越小,满足挖矿要求的概率就越小,挖矿难度就越大。
####2. 块头 & Coinbase交易 块头的生成: 当miner开始新一轮打包之后,首先会创建一个空的块,块结构分为块头以及块信息两部分。先打包块信息,再根据块信息填充块头。
首先看一下已经成功打包的块。下图是写本文稿时截取最新的BTC block详情。
块信息 存放的是从mempool里取出来的一系列交易信息,miner并以此创建了一个Merkle Tree,交易信息作为leaf,最终生成的Merkle Root将填到块头里。值得注意的是,交易列表中的第一个是一个非常独特的交易:Coinbase Transaction。Coinbase Transaction与普通交易主要的区别有: 1)Coinbase Transaction不消耗UTXO 2)input只有一个,叫做Coinbase 3)output的addresss为miner的btc/bch地址 4)value由挖矿奖励和交易费组成 5)更值得注意的是input中没有Unlocking-script,取而代之的是Coinbase Data(这部分数据包含Extra Nonce,在挖矿难度非常高时,将起非常重要的作用) Coinbase 交易input的结构如下:
Coinbase data,该字段数据长度范围为2-byte~100-byte:
- block height 起初Coinbase是不包含块高度信息,由于重复交易的问题出现,诞⽣了BIP30,随后第二套解决方案BIP34)。BIP34规定Coinbase data最高字节表示用于表示块高度的数据段的字节数,接下来的字节以⼩端法表示具体的块高度,创世块的高度为0。
例如:2013-12-28 BTC 的一个块的Coinbase解析中coinbase data为0x03443b04...,则块高度用16进制表示为0x043b44,十进制为277316;
-
extra nonce作为中间字段,将会在后续提及的Extra Nonce Solution详细说明作用;
-
**上图Coinbase data中用以结尾的“/P2SH/”**是12年miner进行投票支持BTC是采用BIP16还是BIP17的产物,现已弃用。(众所周知,BIP16 P2SH 获得了更多票数,被BTC采用)
在交易信息聚合完毕得到了MerkleRoot之后,接下来填充区块头。块头结构如下(其中nBit就是PoW小节提到的current-target的压缩版):
区块头80-byte,一共6个字段:
- 版本号,允许改变但不推荐
- 前一个块的hash值,不允许改变
- MerkleRoot的hash值,用于存块信息里的交易的Merkle Tree的root节点的值,允许改变(改变coinbase中input中的值)
- 时间戳,允许基于MTP11进行调整改变
- nonce,用于PoW算法的随机值,允许改变
- nBit,PoW算法结果必须小于这个数对应的current_target才能算块打包成功。这个值是在每一个块开始打包之前就确定了,不允许改变
块头中80 bytes任意一个值发生改变,PoW的Hash结果就会发生改变。
####3.挖矿难度及难度调整 理解了PoW运算以及运算结果可能受哪些个因素影响,接下来了解一下我们要满足的难度要求是什么。 挖矿难度的描述可以认为有三种形式,difficulty(难度值,浮点数),current_target(当前目标值,256-bit),nbits(32-bit);形式不同其实实质是表达的同一个难度要求,而且这个难度要求在每个块打包前就确定了。
difficulty 不写在区块中,而是以浮点数的形式展现,给人直观的感受难度程度。
difficulty = difficulty_1 / current_target;
difficulty_1为常数:
0x00000000ffff0000000000000000000000000000000000000000000000000000
创世块的 current_target = difficulty_1,所以创世块的difficulty = 1.0。
nbits 就是区块头 nBits 字段的值,用长度为32-bit的数值表示256-bit的数值,是需要牺牲一定精度的,可以理解它为“压缩”后的current_target。
在计算current_target时,我们先转换为二进制然后用公式(a)来计算256-bit的current_target。(值得注意的是:current_target 是一个无符号256-bit的值,之所以设置一个Sign字段,是为了与bitcoind代码保持一致,保留符号位参考的是IEEE浮点型表示法,其实是无用的)
-------------------------------------------------
| Exponent | Sign | Mantissa |
-------------------------------------------------
| 8 bits [31-24] | 1 bit [23] | 23 bits [22-00] |
-------------------------------------------------
This compact form is only used in bitcoin to encode unsigned 256-bit numbers which represent difficulty targets, thus there really is not a need for a sign bit, but it is implemented here to stay consistent with bitcoind.
计算current_target的公式为:�
$ currenttarget = Mantissa * 2 ^ {(8 * (Exponent - 3))}$ (a)
例如 nBits = 0x180192d4,
current_target = 0x192d4 * 2 ^ {(8 * (0x18 - 3))}
= 0x00000000000000000192d4000000000000000000000000000000000000000000
(最高16位为零)
相较于创世块,current_target减小了大约 1/$2^{36}$ 倍,difficulty增加了大约 7184404942701倍。
为了维持平均每十分钟生成一个块的频率,BTC中current_target设计为了一个动态值,current_target根据全网算力的改变而做一些相应的调整,这就是挖矿难度调整。
在BTC中,挖矿难度调整idea为:以2016个块(两周)为一个周期,每个周期根据前一个周期的实际耗时与理论耗时之间的差别进行调整。
新目标值= 当前目标值 * 实际2016个区块出块时间 / 理论2016个区块出块时间(2周)。
方案具体逻辑是:
- 判断是否需要更新目标值( 2016的整数倍),如果不是则继续使用最后一个区块的目标值
- 计算2016个块实际使用时长:如果用时低于半周,则按半周计算,防止难度增加4倍以上;如果用时高于8周,则按8周计算。防止难度降低到4倍以下。
- 实际使用时长乘以当前难度,再除以2周
- 如果超过最大难度限制,则按最大难度处理
计算过程,Go代码如下:
func CalculateNextWorkTarget(prev2016block, lastBlock Block) *big.Int {
// 如果新区块(+1)不是2016的整数倍,则不需要更新,仍然是最后一个区块的 bits
if (lastBlock.Head.Height+1)%2016 != 0 {
return CompactToBig(lastBlock.Head.Bits)
}
// 计算 2016个区块出块时间
actualTimespan :=
lastBlock.Head.Timestamp.Sub(prev2016block.Head.Timestamp)
if actualTimespan < powTargetTimespan/4 {
actualTimespan = powTargetTimespan / 4
} else if actualTimespan > powTargetTimespan*4 {
// 如果超过8周,则按8周计算
actualTimespan = powTargetTimespan * 4
}
lastTarget := CompactToBig(lastBlock.Head.Bits)
// 计算公式: target = lastTarget * actualTime / expectTime
newTarget :=
new(big.Int).Mul(lastTarget, big.NewInt(int64(actualTimespan.Seconds())))
newTarget.Div(newTarget, big.NewInt(int64(powTargetTimespan.Seconds())))
//超过最多难度,则重置
if newTarget.Cmp(mainPowLimit) > 0 {
newTarget.Set(mainPowLimit)
}
return newTarget
}
####4. BCH难度调整
BCH 诞生于区块高度 478558,两条链都采用相同PoW共识算法(平均10分钟生成一个块),所以miner可以任意在BTC与BCH间切换,但由于通常BCH全网算力只占有BTC的7%左右,当BCH获利大于BTC的时候,大量原BTC miner(尤其是大的矿场)会切入BCH,一段时间后随着算力提升,难度值也会提升,miner会纷纷离开切回BTC,算力降低,难度居高不下将导致接下来出块十分困难。倘若继续沿用BTC的难度调整方案,BCH将无法保证出块速率稳定在平均10mins/block,事实上BCH的难度值调整算法已经先后经历了两种,第一种是紧急难度调整规则(EDA),目前使用的是难度调整规则(DAA)。
紧急难度调整规则(EDA)
EDA是在沿用BTC难度调整算法的基础上,增加了一个Emergency Difficulty Adjustment处理方案,主要是针对于出块缓慢情况及时降低难度。算法具体逻辑是:对于高度为2016倍数的就拿到此高度前2016块的blocktime,沿用BTC难度调整方案;对于高度非2016倍数的块,则计算生成前六块的块总共耗时是否超过12h,如果超过则降低挖矿难度20%。 具体实现代码如下:
func (b *BlockChain) getNextEDAWorkRequired(prevBlock *blockNode,
header *wire.BlockHeader) (uint32, error) {
// 如果块高度为2016整数倍,拿到此高度前2016块的blocktime,做BTC难度值调整;
curHeight := prevBlock.height + 1
if int64(curHeight)%b.chainParams.DifficultyAdjustmentInterval() == 0 &&
int64(curHeight) >= b.chainParams.DifficultyAdjustmentInterval() {
// Go back by what we want to be 14 days worth of blocks
firstHeight := curHeight -
int32(b.chainParams.DifficultyAdjustmentInterval())
firstNode := b.bestChain.NodeByHeight(firstHeight)
return b.calculateNextWorkRequired(prevBlock, firstNode.timestamp)
}
proofOfWorkLimit := b.chainParams.PowLimitBits
if b.chainParams.ReduceMinDifficulty {
// 测试网络中,如果当前块和上一个块blocktime超过20min,则将难度调整为最小1;
if int64(header.Timestamp.Second()) >
prevBlock.timestamp+
2*int64(b.chainParams.TargetTimePerBlock.Seconds()) {
return proofOfWorkLimit, nil
}
// Return the last non-special-min-difficulty-rules-block
node := prevBlock
for node.parent != nil &&
int64(node.height)%b.chainParams.DifficultyAdjustmentInterval()
!= 0 && node.bits == proofOfWorkLimit {
node = node.parent
}
return node.bits, nil
}
// We can't go bellow the minimum, so early bail.
bits := prevBlock.bits
if bits == proofOfWorkLimit {
return proofOfWorkLimit, nil
}
// If producing the last 6 block took less than 12h, we keep the same
// difficulty
node6 := b.bestChain.NodeByHeight(curHeight - 7)
if node6 == nil {
panic("the block Index should not equal nil")
}
mtp6Blocks := prevBlock.CalcPastMedianTime().Unix()
- node6.CalcPastMedianTime().Unix()
if mtp6Blocks < 12*3600 {
return bits, nil
}
/* 如果当前区块父块的MTP11时间和第(父块-6)区块的MTP11时间相差12个小时,
则将当前区块难度降低20%;为了保证HashRat�e不会急剧下降。
MTP11时间是11个块按照blocktime排序后的中间时间;
实际上发生这种情况时,难度并不会马上调整,而是等后面检测到时,
也就是一般会推迟几个块,才开始紧急调整难度
*/
pow := CompactToBig(bits)
pow.Add(pow, new(big.Int).Div(pow, big.NewInt(4)))
// Make sure we do not go bellow allowed values.
powLimit := CompactToBig(b.chainParams.PowLimitBits)
if pow.Cmp(powLimit) > 0 {
pow = powLimit
}
return BigToCompact(pow), nil
}
难度调整规则(DAA)
然而在运行三个多月中,EDA表现不尽如人意(非常差),由于其应对出块速率过高并没有做相应及时调整而依赖于BTC原有的2016块一次的调整机制,所以在面对算力波动时表现并不是很好。
本人分别截取了三段“典型”数据,为了对上述观点进行说明:
- 2017.8.26号的算力攻击,2017.8.27号大算力切走之后出块困难,23小时只出13个块,导致连续调整(降低)挖矿难度。
- 2017.10.2由于矿工趋利性导致BCH网络算力突然增加,仅仅30mins就出了20多区块。
- 用一个更直观的数据,BTC和BCH的起跑线都是一样的,都是2017年8月1日,高度同为478,558,而截止到17年11月12日晚BTC挖到了494,079高度,而BCH挖到了503,815高度,多了将近10000个块。
所以优化BCH的难度调整方案的刻不容缓,在得到几大矿池的稳定算力支持后,17年11月13日,BCH再次升级,就是为了优化EDA。BCH开发团队(并非社区)收到几份DAA,最终采用了BTCABC开发团队Amaury Sechet的DAA提案。 这份proposal的ieda可以用一句俗语来形容“魔高一尺,道高一尺,魔有天花板”,根据前一天的算力为基准从而预测需要设置多少工作量才能耗掉十分钟。 其实现逻辑如下:
- 新算法将在高度504031开始生效
- 假设需要得到target_height的目标难度
- (prevBlock - 1)至 (prevBlock - 1 - 2 )这三块的ntime,排序,取ntime在中间的那块为lastNode
- 取(prevBlock - 1 - 144)至 (prevBlock - 1 - 144 - 2)这三块的ntime,排序,取ntime在中间的那块为firstNode 备注:bch的目标是10分钟产生一块,一天产生144块
- 根据最近的144个区块的链上累计工作量(ChainWork)可以推算出满足当前算力的所需工作量work :work = 10 * 60 * ( indexLast.ChainWork — indexFirst.ChainWork) / actualTimeSpan可以得出当前10分钟的算力值work
- 再通过算力值得出目标难度。 �
具体实现代码如下:
func (b *BlockChain) getNextCashWorkRequired(prevBlock *blockNode,
header *wire.BlockHeader) (uint32, error) {
// 测试网络中,如果当前块和上一个块blocktime超过20min,则将难度调整为最小1;
if b.chainParams.ReduceMinDifficulty &&
(header.Timestamp.Unix() >
(prevBlock.timestamp +
int64(2*b.chainParams.TargetTimePerBlock.Seconds()))) {
return b.chainParams.PowLimitBits, nil
}
// (prevBlock - 1)至 (prevBlock - 1 - 2 )
//这三块的ntime,排序,取ntime在中间的那块为lastNode
lastNode := b.getSuitableBlock(prevBlock)
// 取(prevBlock - 1 - 144)至 (prevBlock - 1 - 144 - 2)
//这三块的ntime,排序,取ntime在中间的那块为firstNode
firstHeight := prevBlock.height - 144
firstNode := b.getSuitableBlock(b.bestChain.NodeByHeight(firstHeight))
if firstNode == nil {
panic("the firstNode should not equal nil")
}
/*通过:work = 10 * 60 *
( lastNode.ChainWork — firstNode.ChainWork) / actualTimeSpan
可以得出当前10分钟的算力值work
*/
nextTarget := b.computeTarget(firstNode, lastNode)
if nextTarget.Cmp(b.chainParams.PowLimit) > 0 {
return b.chainParams.PowLimitBits, nil
}
return BigToCompact(nextTarget), nil
}
计算新Target方法如下:
func (b *BlockChain) computeTarget(indexFirst, indexLast *blockNode) *big.Int {
if indexLast.height <= indexFirst.height {
panic("indexLast height should be greater than indexFirst height ")
}
/**
* From the total work done and the time it took to produce that much work,
* we can deduce how much work we expect to be produced in the targeted time
* between blocks.
*/
work := new(big.Int).Sub(indexLast.workSum, indexFirst.workSum)
work.Mul(work, big.NewInt(int64(b.chainParams.TargetTimePerBlock.Seconds())))
// In order to avoid difficulty cliffs, we bound the amplitude of the
// adjustment we are going to do.
if indexLast.timestamp <= indexFirst.timestamp {
panic("indexLast time should greater than indexFirst time ")
}
actualTimeSpan := indexLast.timestamp - indexFirst.timestamp
interval := int64(b.chainParams.TargetTimePerBlock.Seconds())
if actualTimeSpan > 288*interval {
actualTimeSpan = 288 * interval
} else if actualTimeSpan < 72*interval {
actualTimeSpan = 72 * interval
}
work.Div(work, big.NewInt(actualTimeSpan))
/**
* We need to compute T = (2^256 / W) - 1 but 2^256 doesn't fit in 256 bits.
* By expressing 1 as W / W, we get (2^256 - W) / W, and we can compute
* 2^256 - W as the complement of W.
*/
return new(big.Int).Sub(new(big.Int).Div(oneLsh256, work), big.NewInt(1))
}
总结下来,DAA算法具有以下特性:
- 基于前144个块的算力来逐块设置挖矿难度;
- 算力按指数规律变化时,网络将快速调整难度,保证公平性;
- 避免当前算力与目标难度的不匹配导致的反馈振荡。
- 可以一定程度上减少timestamp manipulation等攻击的影响。
DAA应对算力攻击的效果如何呢?
以下是两个算力变化极端场景:算力陡增两倍,算力陡然减半。 poc代码如下:
interval = [600 for i in range(144)]
difficulty = [1.0 for i in range(144)]
next_diff = 600 * sum(difficulty) / sum(interval)
next_time = 600 / 2
# next_time = 600 * 2
index = 1
while next_diff < 2.0:
print (index, next_diff, next_time)
if index >=10 :
interval.append(next_time)
interval = interval[1:]
difficulty.append(next_diff)
difficulty = difficulty[1:]
next_diff = 600 * sum(difficulty) / sum(interval)
next_time = interval[143] * (next_diff / difficulty[143])
index += 1
####5. extra nonce解决方案 目前,BTC挖矿难度设置到了7184404942701.792( 0x17272d92对应的current_target 为0x000000000000000000272d920000000000000000000000000000000000000000)。 也就是说随机选取的数满足target的小于概率是$1/(2^{72})$,但是块头的nonce字段只有4bytes,也就是32位,有可能�$2^{32}$ 个随机数都试完来仍然找不到满足target的result。所以允许块头内部分其他的字段改变,用来生成新的result。允许改变的字段在第二小节块头部分已指明。
试想一下,如果频繁更改Coinbase Data里的Extra Nonce,来改变块头的Merkle Root会怎么样?很明显效率会很低,所以实际挖矿中策略是:尽可能减少块头中Version,TimeStamp,Merkle Root(绿色区域数据)值的改变,而“疯狂”遍历Nonce(红色区域)的值用于PoW;当遍历完没找到满足target的result,再改变绿色区域的值,然后继续“疯狂”遍历Nonce。如此往复直至找到满足target的result或者这一轮PoW竞赛中失败开始新一轮打包。
本文由哥白尼团队何思羽创作,转载无需授权。
Reference: