现在以太坊公链使用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))
看起来好简单,关键是理清这个逻辑。