什么是难度
难度(Difficulty)一词来源于区块链技术的先驱比特币,用来度量挖出一个区块平均需要的运算次数。挖矿本质上就是在求解一个谜题,不同的电子币设置了不同的谜题。比如比特币使用SHA-256、莱特币使用Scrypt、以太坊使用Ethash。一个谜题的解的所有可能取值被称为解的空间,挖矿就是在这些可能的取值中寻找一个解。
这些谜题都有如下共同的特点:
- 没有比穷举法更有效的求解方法
- 解在空间中均匀分布,从而使每一次穷举尝试找到一个解的概率基本一致
- 解的空间足够大,保证一定能够找到解
假设现在有一种电子币,解所在的空间为0-99共100个数字,谜题为x<100。这个谜题非常简单,空间中的任何一个数字都能满足。如果想让谜题更难以求解该怎么做呢?把谜题改成x<50,现在空间中只有一半的数字能满足了,也就是说,现在的难度比原来大了。并且我们还能知道难度大了多少,原来求解平均要尝试1次,现在求解平均要尝试2次了,也就是说,x<50的难度是x<100的2/1=2倍。同理,如果谜题变成x<10,难度就是x<100的100/10=10倍。
现在谜题多了个参数Difficulty,谜题变成了x<Difficulty,上面的100、50、10,都是Difficulty的取值,这个参数Difficulty就是我们常说的难度(Difficulty)。
难度(Difficulty)通过控制合格的解在空间中的数量来控制平均求解所需要尝试的次数,也就可以间接的控制产生一个区块需要的时间,这样就可以使区块以一个合理而稳定的速度产生。
当挖矿的人很多,单位时间能够尝试更多次时,难度就会增大,当挖矿的人减少,单位时间能够尝试的次数变少时,难度就降低。这样产生一个区块需要的时间就可以做到稳定。
有了难度,我们还需要一种算法,用来确定和调整合理的难度值是多少。
以太坊是如何计算难度的
以太坊的代码是完全开源的,官方使用Go语言实现了一个钱包,被称为Geth。
Geth可以从这里下载:Go Ethereum Downloads
源代码可以在这里找到:ethereum/go-ethereum
Geth的开发者对“共识”的理解相当深入,代码也很容易阅读。关于难度的代码在consensus/ethash/consensus.go中。
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int { next := new(big.Int).Add(parent.Number, big1) switch { case config.IsMetropolis(next): return calcDifficultyMetropolis(time, parent) case config.IsHomestead(next): return calcDifficultyHomestead(time, parent) default: return calcDifficultyFrontier(time, parent) } }
有三种计算难度的规则,分别对应以太坊的三个主要版本:已经成为历史的Frontier、正在使用的Homestead和将要发布的Metropolis。
现在正在使用的Homestead的难度计算规则在calcDifficultyHomestead中实现。
func calcDifficultyHomestead(time uint64, parent *types.Header) *big.Int { // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.mediawiki // algorithm: // diff = (parent_diff + // (parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99)) // ) + 2^(periodCount - 2) bigTime := new(big.Int).SetUint64(time) bigParentTime := new(big.Int).Set(parent.Time) // holds intermediate values to make the algo easier to read & audit x := new(big.Int) y := new(big.Int) // 1 - (block_timestamp - parent_timestamp) // 10 x.Sub(bigTime, bigParentTime) x.Div(x, big10) x.Sub(big1, x) // max(1 - (block_timestamp - parent_timestamp) // 10, -99) if x.Cmp(bigMinus99) < 0 { x.Set(bigMinus99) } // (parent_diff + parent_diff // 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99)) y.Div(parent.Difficulty, params.DifficultyBoundDivisor) x.Mul(y, x) x.Add(parent.Difficulty, x) // minimum difficulty can ever be (before exponential factor) if x.Cmp(params.MinimumDifficulty) < 0 { x.Set(params.MinimumDifficulty) } // for the exponential factor periodCount := new(big.Int).Add(parent.Number, big1) periodCount.Div(periodCount, expDiffPeriod) // the exponential factor, commonly referred to as "the bomb" // diff = diff + 2^(periodCount - 2) if periodCount.Cmp(big1) > 0 { y.Sub(periodCount, big2) y.Exp(big2, y, nil) x.Add(x, y) } return x }
不过我不是来搬运代码的,我们说人话。
在解释具体计算过程之前,先来介绍一下,用到的几种运算符。
- 整数除法,符号//
计算a//b时,先计算a/b,然后取不大于a/b的最大整数。
例如:
-11.3 // 5 = -3
11.3 // 5 = 2
- 取整,符号INT
计算INT(a)时,仅仅取整数部分,丢弃小数。
例如:
INT(3.7) = 3
INT(-3.7) = -3
- 最大值,符号MAX
计算MAX(a,b)时,结果为a和b中较大的那一个。
例如:
MAX(-1,0) = 0
MAX(7,10) = 10
计算一个区块的难度时,需要以下输入:
parent_timestamp:上一个区块产生的时间
parent_diff:上一个区块的难度
block_timestamp:当前区块产生的时间
block_number:当前区块的序号
block_diff = parent_diff + 难度调整 + 难度炸弹
难度调整 = parent_diff // 2048 * MAX(1 - (block_timestamp - parent_timestamp) // 10, -99)
难度炸弹 = INT(2**((block_number // 100000) - 2))
另外,区块难度不能低于以太坊的创世区块,创世区块的难度为131072,这是以太坊难度的下限。
实践一下
让我们来计算一个区块的难度。
以太坊的区块链是公开的,可以在这里查看:The ethereum blockchain explorer
现在我们将根据4212371区块的难度来计算4212372区块的难度。
先查看下4212371区块的难度和时间戳,难度为2,117,963,098,883,076,时间戳是2017-08-28 11:24:23。
再来看下4212372区块的时间戳,时间戳是2017-08-28 11:25:43。
现在需要的信息都有了。
parent_timestamp = 2017-08-28 11:24:23
parent_diff = 2,117,963,098,883,076
block_timestamp = 2017-08-28 11:25:43
block_number = 4212372
先算难度炸弹:
INT(2**((4212372 // 100000) - 2)) =
INT(2**(42 - 2)) = INT(2**40) = 2**40 = 1099511627776
再算难度调整:
2117963098883076 // 2048 * MAX(1 - (2017-08-28 11:25:43 - 2017-08-28 11:24:23) // 10, -99) =
2117963098883076 // 2048 * MAX(1 - (2017-08-28 11:25:43 - 2017-08-28 11:24:23) // 10, -99) =
2117963098883076 // 2048 * MAX(1 - 80 // 10, -99) =
2117963098883076 // 2048 * MAX(1 - 8, -99) =
2117963098883076 // 2048 * MAX(-7, -99) =
2117963098883076 // 2048 * -7 =
1034161669376 * -7 = -7239131685632
最终的难度为:
block_diff = 2117963098883076 - 7239131685632 + 1099511627776 = 2,111,823,478,825,220
检查下是不是大于创世区块的难度131072,显然满足条件。
是不是和查到的一模一样?4212372区块的难度为2,111,823,478,825,220。
以太坊难度的特点
以太坊的区块难度以单个区块为单位进行调整,可以非常迅速的适应算力的变化,正是这种机制,使以太坊在硬分叉出以太坊经典(ETC)以后没有出现比特币分叉出比特币现金(BCC)后的算力“暴击”问题。同时,以太坊的新区块难度在老区块的基础上有限调整的机制也使区块难度不会出现非常大的跳变。
下面是BTC和BCC的难度变化:
这是以太坊的难度变化:
可以看出以太坊难度的递增比较平滑,出现的几个跳变是由于难度炸弹造成的。
关于难度炸弹
其实大家谈之色变的难度炸弹只是难度计算公式中的一部分而已,也就是下面的这部分。
INT(2**((block_number // 100000) - 2))
难度炸弹每100000个区块就会翻倍,目前只有1T左右。但是由于是指数级递增,还记得用铜钱铺满棋盘的故事么,指数递增是很迅速的,用炸弹来形容倒是非常贴切。
可以看到,到5400000区块时,难度将达到4500T,会使挖矿变得极为困难,甚至不能收回电费成本。
为什么要这样设置呢?
以太坊的开发者认为,现在的版本都不是最终的状态,未来将会使用PoS方式分配新产生的以太坊而不是PoW。PoS又被称为虚拟挖矿,可以理解为根据目前拥有的以太坊的多少来分配新产生的以太坊,届时将不再需要矿工消耗大量电力挖矿,这将会使矿工们“失业”。
比特币社区出现的矿工和用户对立的情况警示了以太坊的开发者,为了避免矿工拥有过多的话语权,阻碍新技术的应用,于是设立了难度炸弹,到一定的时间点之后,矿工自然会因为无利可图自动退场。不得不说,这个设定是非常英明的。
因为每个新区块的难度包含上一区块的难度,而上一区块的难度又包含了一个难度炸弹,也就是说难度炸弹的难度会在每个区块的难度中逐渐累积起来。这将使难度炸弹除了指数跳变的影响之外,又增加了较为平缓的长期影响。