现在以太坊公链使用geth挖矿周期大概是15秒出一个新块。在自己基于go-ethereum源码来建立私有链的时候,如果想加快挖矿出块周期,应该修改哪些地方呢?许多人的第一想法是修改创世配置文件genesis.json文件中的difficulty,把它改成一个较小的值,比如0x10000。在电脑上跑起来后,发现挖矿基本上是1秒出几个块,这有点太快了。于是改大,改成0x20000,发现出块还是1s几个块。这种是凑的方法不太靠谱。
以太坊采用PoW工作量证明机制来进行挖矿,最简单的理解就是寻找一个整数nonce,使得哈希值sha3(data+nonce)<M/difficulty。difficulty越大,挖矿难度越大,反之挖矿越容易。查看以太坊中计算难度值的函数,在go-ethereum/consensus/ethash/consensus.go中:
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
//return big.NewInt(0x100000);
next := new(big.Int).Add(parent.Number, big1)
switch {
case config.IsByzantium(next):
return calcDifficultyByzantium(time, parent)
case config.IsHomestead(next):
return calcDifficultyHomestead(time, parent)
default:
return calcDifficultyFrontier(time, parent)
}
}
对于1.8.1版本,使用的calcDifficultyHomestead函数:
func calcDifficultyHomestead(time uint64, parent *types.Header) *big.Int {
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.md
// 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
}
看函数注释,计算difficulty的算法是:
diff = (parent_diff +
(parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99))
) + 2^(periodCount - 2)
分为3部分,第一项是parent_diff,父区块的难度值;第二项是难度调整值parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99),第三项是难度炸弹,难度炸弹=2^(blockNumber/100000-2),每过10万个区块难度增加2。
关键是第二项难度调整,Y=parent_diff / 2048 * max(1 - (block_timestamp - parent_timestamp) // 10, -99),这个表达式看起来这么长,乍一看不知所云。认真分析一下,deltaTime=block_timestamp - parent_timestamp,及当前区块和前一个区块的产生时间差。//操作符是先除再向下去整的意思。当deltaTime<10s时,Y=parrent_diff/2048;当10<deltaTime<20时,Y=0;当Y>=20时,Y=-parrent_diff/2048。这样一看就很明了了。这是采用自控里面的负反馈调节的思路。当时间差小于10s时,增大难度让时间变长。当时间差介于10s到20s时,难度不变。当时间差大于20s时,减小难度来让挖矿更容易。平均挖矿时间就是10~20之间,最终平均时间大概15s。
决定周期的就是这个除数10,如果将除数10改成2,则挖矿周期将在2~4秒之间。只需要这样修改:
x.Div(x, big10) ==> x.Div(x,big.NewInt(2))
看起来好简单,关键是理清这个逻辑。