以太坊源码解读-第6.2讲-pow共识算法实现

前言

这一部分,我们主要是来了解一下pow共识引擎的具体实现。
本文主要是梳理引擎主要方法的流程。

consensus模块->ethash->ethash.go文件中,列出了pow共识具体实现的结构体Ethash。本文将着重介绍该结构体以及其对应实现的方法。其余问题,我们放在后续文章中讲解。

结构体Ethash

先来看看这个结构体,它本身就是pow算法的具体描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//以下几种挖矿模式
const (
ModeNormal Mode = iota
ModeShared //分享模式,避免缓存干扰
ModeTest //测试模式
ModeFake //伪模式?
ModeFullFake //完全伪模式,完全不验证块,提高速度
)

type Config struct {
CacheDir string
CachesInMem int
CachesOnDisk int
DatasetDir string
DatasetsInMem int
DatasetsOnDisk int
PowMode Mode //从此处可以设置不同的挖矿模式
}

type Ethash struct {
config Config //pow的一些基本配置,比如缓存路径、数据库路径等

caches *lru // 在内存中存放的缓存信息
datasets *lru // 在内存中存放的数据库信息

// 挖矿相关
rand *rand.Rand // nonces的随机种子
threads int // 有多少条线程在挖矿
update chan struct{} // 通知更新挖矿参数
hashrate metrics.Meter // 跟踪hash相关的速率,应该是用来监控,保证难度恒定

// Remote sealer related fields,之后的翻译,等我具体了解了细节再来解释
workCh chan *sealTask // Notification channel to push new work and relative result channel to remote sealer
fetchWorkCh chan *sealWork // Channel used for remote sealer to fetch mining work
submitWorkCh chan *mineResult // Channel used for remote sealer to submit their mining result
fetchRateCh chan chan uint64 // Channel used to gather submitted hash rate for local or remote sealer.
submitRateCh chan *hashrate // Channel used for remote sealer to submit their mining hashrate

// 以下都是测试时候使用到的参数
shared *Ethash // Shared PoW verifier to avoid cache regeneration
fakeFail uint64 // Block number which fails PoW check even in fake mode
fakeDelay time.Duration // Time delay to sleep for before returning from verify

lock sync.Mutex // Ensures thread safety for the in-memory caches and mining fields
closeOnce sync.Once // Ensures exit channel will not be closed twice.
exitCh chan chan error // Notification channel to exiting backend threads
}

由于要使用到大量的数据集,所以有两个指向lru的指针,一个指向db,一个指向内存。并且通过threads控制挖矿线程数。并在测试模式或fake模式下,简单快速处理,使之快速得到结果。

共识算法接口实现

上一篇文章以太坊源码解读-第6.1讲-共识模块入口设计我们得知,共识算法是实现Engine接口中的方法。也就是说结构体Ethash要实现其中的每一个接口,又因为它是POW的具体实现,还得实现pow中另一个接口Hashrate()
而具体的实现是在consensus.go文件中。
下面我们一个个来解释。

Author()

获取挖出当前块的矿工地址

1
2
3
func (ethash *Ethash) Author(header *types.Header) (common.Address, error) {
return header.Coinbase, nil
}

VerifyHeader()

用于校验区块头部信息是否符合ethash共识引擎规则,

先总结校验流程

后续涉及到块批量校验,叔块的校验等,都是执行下面类似的步骤。
该方法校验流程(具体细节看下面代码解析,该流程只用作梳理):

  1. 校验是否为ModeFullFake模式,是则返回成功,否则继续。
  2. 根据该块的块号和hash在链中判断,是否能找到该块,能则返回,否则继续
  3. 根据该块的父块的hash和块号,检测能否在链中找到该父块,不能则返回错误,否则继续。(此时说明该块是一个新块)
  4. 获取到父块后,进一步对该新块做更细的校验。
    1. 块头部额外数据(head.extra)的长度是否超过了最大限制长度(据了解extra表示可以让矿工加入有限字符,dao分叉后这个extra被占用了)
    2. 判断该块是否为叔块(true、false)。
      1. 该块若是叔块,则判断时间戳是否超过了256位长度,是则返回异常否则继续
      2. 该块若不是叔块,则判断该块时间戳是否超过当前时间(+15秒,可得知每15秒左右出一个块),若是则返回异常,否则继续
    3. 判断该块的时间戳是否比父块的时间戳大,不是则返回异常,否则继续
    4. 根据该块和它父块难度计算预期的困难度,判断该预期困难度和该块中原先记录的困难度是否一致,不一致则返回异常,否则继续
    5. 判断gaslimit是否大于2^63-1,若大约则返回异常,否则继续(此处可得知gaslimit的上限是2^63-1,相当于9.2个左右的eth)
    6. 判断该块的gasUsed是否 <= gasLimit,不是则返回异常,否则继续
    7. 确保当前块gaslimit相对于父块的是在一个范围内。若不是在该范围内,则返回异常否则继续。(此处可知,gaslimit不得小于5000
    8. 子块number-父块number若不是1,则返回异常
    9. 校验密封?的块是否符合要求(这块暂时不太理解,再埋一个坑,回头填,只知道这块是很重要的,也是调用了共识引擎的一个接口实现)
    10. 如果所有检查通过,则验证硬分叉的特殊字段。

具体代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (ethash *Ethash) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error {
// ModeFullFake模式,则不做任何验证
if ethash.config.PowMode == ModeFullFake {
return nil
}
number := header.Number.Uint64()
// 若根据块号和hash能在链中找到,则说明该块存在
if chain.GetHeader(header.Hash(), number) != nil {
return nil
}
// 此时该块还没有被加入到链中,新生成的块,从其中尝试获取其父块
parent := chain.GetHeader(header.ParentHash, number-1)
if parent == nil { //拿不到父块,则返回异常
return consensus.ErrUnknownAncestor
}
// 拿到父块后,对该块做进一步到验证,用来确保该块的正确性
// false表示不是叔块
return ethash.verifyHeader(chain, header, parent, false, seal)
}

上面代码,小编注释写的很详细了,主要就是验证块头部的正确性(其实就是验证块),前面代码都还好理解,在最后一行代码ethash.verifyHeader(...)(注意verifyHeader是首字母是小写,并不是共识通用引擎中的接口),从逻辑上我们可以看出,它是用来详细校验一个新生成的块,具体来看看它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
func (ethash *Ethash) verifyHeader(chain consensus.ChainReader, header, parent *types.Header, uncle bool, seal bool) error {
// 头部额外的数据段长度合理,这个额外的数据段是什么,姑且放一放,最后再来补坑
if uint64(len(header.Extra)) > params.MaximumExtraDataSize {
return fmt.Errorf("extra-data too long: %d > %d", len(header.Extra), params.MaximumExtraDataSize)
}
// 验证块头部的时间戳
if uncle { //叔块
if header.Time.Cmp(math.MaxBig256) > 0 { //以太坊可以被认为是256位机器,超过后,则异常
return errLargeBlockTime
}
} else {
if header.Time.Cmp(big.NewInt(time.Now().Add(allowedFutureBlockTime).Unix())) > 0 {
//该块的生产时间超过了当前时间(+15秒),说明该块异常
//也说明出一个块15秒
return consensus.ErrFutureBlock
}
}
if header.Time.Cmp(parent.Time) <= 0 {
return errZeroBlockTime //当前块时间戳比它上一个块的时间戳小,则说明块异常
}
// 根据该块的时间戳以及它父块的难度,获取当前块预期的难度值,CalcDifficulty是共识通用引擎的接口实现,我们稍后会专门分析
expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)

// 该块预期难度值和该块head中记录的难度值如果不一样,则返回错误
if expected.Cmp(header.Difficulty) != 0 {
return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
}
// 判断该块的gaslimit上限是否超过2^63-1
cap := uint64(0x7fffffffffffffff)
if header.GasLimit > cap {
return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
}
// 判断该块的gasUsed是否 <= gasLimit
if header.GasUsed > header.GasLimit {
return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
}

// 确保gas在允许范围内
diff := int64(parent.GasLimit) - int64(header.GasLimit)
if diff < 0 { //说明子块的gaslimit比父块的大,
diff *= -1 //保证差异值为正
}
limit := parent.GasLimit / params.GasLimitBoundDivisor //limit=父块gaslimit/1024

//差异值过大,或者gaslimit比5000还要小,则返回
if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
}
// 子块number-父块number若不是1,则返回异常
if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 {
return consensus.ErrInvalidNumber
}
// 校验密封?的块是否符合要求
if seal {
if err := ethash.VerifySeal(chain, header); err != nil {
return err
}
}
// 如果所有检查通过,则验证硬分叉的特殊字段。
// misc上一篇文章小编也提到过,硬分叉相关,暂时不考虑,后续文章再专门分析
if err := misc.VerifyDAOHeaderExtraData(chain.Config(), header); err != nil {
return err
}
if err := misc.VerifyForkHashes(chain.Config(), header, uncle); err != nil {
return err
}
return nil
}

该头部校验的相关内容,到此就讲解完了,开头处也总结了一下整个校验流程,基本可以参考它实现整个校验过程。接着我们看下一个共识引擎接口。

CalcDifficulty()

用于计算下一个块的难度,以太坊分为不同的阶段(可参考:以太坊各阶段说明),因此,这个难度计算针对不同的阶段会有不同的计算方式。
这个方法用于计算下一个块的难度,同时,从上面校验方法我们可知,该方法也用来校验块中的难度值是否一致。
可从以下代码看出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 具体的实现需要一层层的探索
func (ethash *Ethash) CalcDifficulty(chain consensus.ChainReader, time uint64, parent *types.Header) *big.Int {
return CalcDifficulty(chain.Config(), time, parent)
}

// 根据不同阶段来计算下一个块的难度
func CalcDifficulty(config *params.ChainConfig, time uint64, parent *types.Header) *big.Int {
next := new(big.Int).Add(parent.Number, big1)
//可以看到下面是不同阶段的
switch {
case config.IsConstantinople(next): //第三阶段的Metropolis(大都会)的vConstantinople(康斯坦丁堡阶段)
return calcDifficultyConstantinople(time, parent)
case config.IsByzantium(next): //第三阶段的Metropolis(大都会)的vByzantium(拜占庭阶段)
return calcDifficultyByzantium(time, parent)
case config.IsHomestead(next): //第二阶段Homestead(家园)
return calcDifficultyHomestead(time, parent)
default:
return calcDifficultyFrontier(time, parent) //第一阶段Frontier(前沿)
}
}

目前稳定版是在第三阶段的Metropolis(大都会)的vByzantium(拜占庭阶段),使用的纯pow共识算法。因此我们来具体看看这一阶段的难度值是如何计算的。

先总结计算流程

  1. 从第300万个块之后开始按照拜占庭阶段规则计算难度,
  2. 叔块难度介入,使用对应公式实计算,返回计算结果(公式下面给出)。
  3. 难度值始终不小于131072,创世块若不设置难度,默认也是这个大小。

具体代码分析

第三阶段的Metropolis(大都会)的vByzantium(拜占庭阶段)的每个块的下一个块难度计算,进一步是从以下入口开始的:

1
2
3
4
// 这个起源和规则可以参考:https://eips.ethereum.org/EIPS/eip-649
// 3000000表示从以太坊从第300万个块之后都使用这样一个难度计算方式
// 这个被称为难度炸弹。。。
calcDifficultyByzantium = makeDifficultyCalculator(big.NewInt(3000000))

看看具体计算的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 看清楚代码,函数方法
// 这个过程叫做深水炸弹
// makeDifficultyCalculator返回的:是一个带有返回值的方法
// 方法参数bombDelay是块号,表示从哪个块开始
// 返回的方法中,第一个参数是块的时间戳,第二个是父块
func makeDifficultyCalculator(bombDelay *big.Int) func(time uint64, parent *types.Header) *big.Int {
// 要计算下一个块的难度,就需要先知道上一个块的编号
bombDelayFromParent := new(big.Int).Sub(bombDelay, big1)
return func(time uint64, parent *types.Header) *big.Int {
// 参考:https://github.com/ethereum/EIPs/issues/100.
...
...
}
}

计算的具体代码没有列出来,是按照这样一个计算公式实现该难度计算的:
diff = (parent_diff + (parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) / 9), -99)) ) + 2^(periodCount - 2)
公式中可得知,难度的计算加入了叔块。
为了更加清晰,公式整理如下:

1
2
3
4
block_diff = parent_diff + [难度调整] + [难度炸弹]
[难度调整] = parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - (block_timestamp - parent_timestamp) / 9, -99))
[难度炸弹] = INT(2^((periodCount / 100000) - 2))
备注:在300w~310w块之间,[难度炸弹]=0

VerifyHeaders()

该方法和前面提到的VerifyHeader类似,只是这个是批量验证。
中间用到了大量的goroutine,看着代码量挺大的,其实蛮好理解的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func (ethash *Ethash) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) {
... // ModeFullFake,则什么也不处理,直接返回,此处代码略

// 确保每个核都参与工作
workers := runtime.GOMAXPROCS(0) //0,表示使用最大cpu数
if len(headers) < workers {
workers = len(headers)
}
//定义一些必要的变量
var (
inputs = make(chan int)
done = make(chan int, workers)
errors = make([]error, len(headers))
abort = make(chan struct{})
)

for i := 0; i < workers; i++ { //所有worker全马力运转
go func() {
for index := range inputs { //接收到传来的数字(最下面代码注释),然后开始执行
errors[index] = ethash.verifyHeaderWorker(chain, headers, seals, index)
done <- index
}
}()
}

errorsOut := make(chan error, len(headers)) //用于在select接收存储到的错误
... //此处是select,接收到chan信息后的处理逻辑,虽然代码有点长,但不复杂,在此就不列出了
//同时,此处也为input不停的传入数字1,2,3...,直到len(headers)
return abort, errorsOut
}

上面这段代码有空的话,建议好好读读,写的蛮好的,通过goroutine和channel将整个过程做的清晰明了
可以看出,真正验证头部,是在ethash.verifyHeaderWorker()这个方法中执行的,其中主要是调用共识引擎的verifyHeader()接口实现的,这个前面已经讲过,在此就不详述了。

VerifyUncles()

什么是叔块,直接看图:

用来校验当前块的叔块,和校验块头部其实很类似,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
func (ethash *Ethash) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
// ModeFullFake,不执行
if ethash.config.PowMode == ModeFullFake {
return nil
}
// 目前一个块最多可以有两个叔块
if len(block.Uncles()) > maxUncles {
return errTooManyUncles
}
// 汇总叔块和祖先,祖先是指当前块的前8个块
// ancestors 存放祖先块的hash
// uncles 存放每个主块的uncles块的hash
uncles, ancestors := mapset.NewSet(), make(map[common.Hash]*types.Header)

number, parent := block.NumberU64()-1, block.ParentHash() //父块块号和hash
for i := 0; i < 7; i++ { //这一坨,一目了然
ancestor := chain.GetBlock(parent, number)
if ancestor == nil {
break
}
ancestors[ancestor.Hash()] = ancestor.Header()
for _, uncle := range ancestor.Uncles() {
uncles.Add(uncle.Hash())
}
parent, number = ancestor.ParentHash(), number-1
}
ancestors[block.Hash()] = block.Header() //祖先中总共8个
uncles.Add(block.Hash()) //这个貌似有点怪,但不影响,uncles主要就是用来保证hash不重复的

// 把当前块的叔块hash也加入到uncles中做重复过滤
// 使用verifyHeader来进一步校验块
for _, uncle := range block.Uncles() {
//hash重复性检测
hash := uncle.Hash()
if uncles.Contains(hash) {
return errDuplicateUncle
}
uncles.Add(hash)

// 检测有效性
if ancestors[hash] != nil {
return errUncleIsAncestor
}
//叔块应该指向当前块的父块的父块
if ancestors[uncle.ParentHash] == nil || uncle.ParentHash == block.ParentHash() {
return errDanglingUncle
}
// 这个方法前面讲过,详细检测块
if err := ethash.verifyHeader(chain, uncle, ancestors[uncle.ParentHash], true, true); err != nil {
return err
}
}
return nil
}

重要总结

这块我们可以得知以下几个重要信息:

  1. 当前块的父块的父块当前块叔块的父块
  2. 一个块有两个叔块
  3. 将该块的前8个块及其叔块信息分别汇总,是为了更好的确保当前块叔块的正确性,防止有重复等问题,确保叔块和正常块不会交叉混淆,这也是避免有人恶意操作吧。

Prepare()

在pow算法中,这个接口主要是用来填充块头部中的难度值的,目前并没有别的作用。
难度的计算前面已经讲解过了。

1
2
3
4
5
6
7
8
func (ethash *Ethash) Prepare(chain consensus.ChainReader, header *types.Header) error {
parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
if parent == nil {
return consensus.ErrUnknownAncestor
}
header.Difficulty = ethash.CalcDifficulty(chain, header.Time.Uint64(), parent) //计算并填充难度
return nil
}

Finalize()

非常重要的一个方法,对确认的块进行奖励,通俗的讲,就是分配奖励给区块账户叔块账户
需要注意的是,两个叔块是被一个区块引用的,也就是说,该方法最终会把这个区块记录在db中,从而生成一个新的区块。
这里会涉及到很多经济哲理问题,需要自己来意会,来看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) {
// 区块账户和叔块账户的奖励规则
accumulateRewards(chain.Config(), state, header, uncles)
// 根据保存的db state生成该块的root hash,然后记录在db中,这就是该区块的hash
// EIP158硬分叉,参考://https://github.com/ethereum/EIPs/issues/158
header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

// 返回该新块
return types.NewBlock(header, txs, uncles, receipts), nil
}
// 具体的奖励实现
func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
blockReward := FrontierBlockReward // 基本奖励,5个eth奖励
if config.IsByzantium(header.Number) { //拜占庭阶段,3个eth奖励
blockReward = ByzantiumBlockReward
}
if config.IsConstantinople(header.Number) { //康斯坦丁堡阶段,2个eth奖励
blockReward = ConstantinopleBlockReward
}
// 叔块的奖励逻辑
reward := new(big.Int).Set(blockReward)
r := new(big.Int)
for _, uncle := range uncles {
//公式:((uncle.Number+8)-heater.Number)*blockReward/8
//可以看出,叔块块号越大,叔块的奖励越大
r.Add(uncle.Number, big8)
r.Sub(r, header.Number)
r.Mul(r, blockReward)
r.Div(r, big8)
state.AddBalance(uncle.Coinbase, r) //叔块添加到db

//从这里看出,每个区块中,每添加一个叔块,就会增加额外的1/32的奖励
r.Div(blockReward, big32)
reward.Add(reward, r)
}
state.AddBalance(header.Coinbase, reward) //区块账户,奖励加入db
}

总结一下

  1. 当前康斯坦丁堡阶段,一个块默认会给3个eth的奖励,
  2. 该块每添加一个叔块,额外给予默认奖励1/32的奖励(一个块最多两个叔块)
  3. 奖励结束后,就会被永久记录在链中

SealHash()

该方法是返回被seal前的一个块头部的hash值,Seal到底是要干嘛,还不清楚,但不影响看这个方法细节,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) {
hasher := sha3.NewKeccak256()

rlp.Encode(hasher, []interface{}{
header.ParentHash,
header.UncleHash,
header.Coinbase,
header.Root,
header.TxHash,
header.ReceiptHash,
header.Bloom,
header.Difficulty,
header.Number,
header.GasLimit,
header.GasUsed,
header.Time,
header.Extra,
})
hasher.Sum(hash[:0])
return hash
}

这明显是把一个块头部先rlp序列化,然后再生成一个hash,貌似目的是要生成一个块头部的hash,生成以后,这个块也就被确定了

VerifySeal()

VerifySeal()函数基于跟Seal()完全一样的算法原理,通过验证区块的某些属性(Header.Nonce,Header.MixDigest等)是否正确,来确定该区块是否已经经过Seal操作。
会涉及到使用缓存来验证块的合法性或者使用dag来验证,使用dag是为了使得验证更快一些。
看代码:

1
2
3
func (ethash *Ethash) VerifySeal(chain consensus.ChainReader, header *types.Header) error {
return ethash.verifySeal(chain, header, false)
}

直接返回的子方法verifySeal来处理,那就进去看看这个方法,看里面的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//关于fulldag,true表示使用DAG,false使用传统的缓存机制
func (ethash *Ethash) verifySeal(chain consensus.ChainReader, header *types.Header, fulldag bool) error {
// Fake模式,最简化处理,伪验证
if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
time.Sleep(ethash.fakeDelay)
if ethash.fakeFail == header.Number.Uint64() {
return errInvalidPoW
}
return nil
}
// shanred模式的处理,这个暂时不考虑
if ethash.shared != nil {
return ethash.shared.verifySeal(chain, header, fulldag)
}
// 确保难度值大于0
if header.Difficulty.Sign() <= 0 {
return errInvalidDifficulty
}
// 当前块号
number := header.Number.Uint64()

var (
digest []byte
result []byte
)
//
if fulldag { //使用DAG,结合db,生成数据集
dataset := ethash.dataset(number, true)
if dataset.generated() {
digest, result = hashimotoFull(dataset.dataset, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())

// 数据集finalizer中未映射。 确保数据集保持活动状态,直到调用hashimotoFull为止,因此在使用时不会取消映射。
runtime.KeepAlive(dataset)
} else { //使用缓存
fulldag = false
}
}
// 如果使用普通验证或者是DAG还没准备好,则执行其中内容
if !fulldag {
cache := ethash.cache(number)

size := datasetSize(number)
if ethash.config.PowMode == ModeTest {
size = 32 * 1024 //测试环境指定固定大小
}
//调用hash计算算法,这就是hash碰撞的核心地方,参考:https://blog.csdn.net/ddffr/article/details/78773961
digest, result = hashimotoLight(size, cache.cache, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())

//缓存在finalizer中未映射。 确保缓存在调用hashimotoLight之前保持活动状态,因此在使用时不会取消映射。
runtime.KeepAlive(cache)
}
// 验证计算结果是否满足
if !bytes.Equal(header.MixDigest[:], digest) {
return errInvalidMixDigest
}
target := new(big.Int).Div(two256, header.Difficulty)
if new(big.Int).SetBytes(result).Cmp(target) > 0 {
return errInvalidPoW
}
return nil
}

里面会涉及到很复杂的hash计算方式,本文只是大体梳理一下整体的运行情况,只是针对引擎接口的实现来描述,其中涉及到的其余ethash方法后续再描述。

Seal()

它实现了工作量证明,Seal函数尝试找出一个满足区块难度的nonce值。
人们常说的挖矿,其实就是调用的它,找到满足条件的块,它就是整个挖矿过程的核心。
这个方法涉及的比较复杂,是另外在文件sealer.go中实现的。
这个方法看似长,其实就是利用goroutine接受和传递,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
//fack模式则忽略挖矿
...
// shared模式处理方式,暂时没研究这个模式是干嘛的,不影响
...
// 标准模式
abort := make(chan struct{})

ethash.lock.Lock()
threads := ethash.threads //这就是有多少个矿工在挖矿
if ethash.rand == nil {
seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
ethash.lock.Unlock()
return err
}
ethash.rand = rand.New(rand.NewSource(seed.Int64())) //得到一个随机数
}
ethash.lock.Unlock()
if threads == 0 {
threads = runtime.NumCPU() //若没有设置矿工数,则使用cpu数
}
if threads < 0 {
threads = 0 // 本地挖矿将被阻止
}
// 将任务推向远程
if ethash.workCh != nil {
ethash.workCh <- &sealTask{block: block, results: results}
}
var (
pend sync.WaitGroup //类似栅栏模式,线程等待
locals = make(chan *types.Block)
)
for i := 0; i < threads; i++ {
pend.Add(1)
go func(id int, nonce uint64) {
defer pend.Done()
ethash.mine(block, id, nonce, abort, locals) //开始挖矿,进入hash碰撞模式
}(i, uint64(ethash.rand.Int63()))
}
// 获取挖矿返回的结果,select接收,挖矿停止,或者找到合适的nonce
go func() {
var result *types.Block
select {
case <-stop:
close(abort) //关闭所有的channel
case result = <-locals:
// 只要有一个矿工挖到合适的块,则停止其余所有。
select {
case results <- result:
default:
log.Warn("Sealing result is not read by miner", "mode", "local", "sealhash", ethash.SealHash(block.Header()))
}
close(abort)
case <-ethash.update:
// 用户若更新了矿工数(新增或者减少),则停止所有矿工,然后重新挖
close(abort)
if err := ethash.Seal(chain, block, results, stop); err != nil {
log.Error("Failed to restart sealing after update", "err", err)
}
}
// 等待所有矿工结束
pend.Wait()
}()
return nil
}

这段代码解释了挖矿前的操作以及挖完矿后的接收方式,其中我们发现挖矿的细节是在mine()方法中,这个是ethash的方法之一,这个方法细节会在新的文章中讲解。在此只是了解整个流程。多个线程的开启就是多个groutine开启的过程。

Apis()

这个就是对外提供RPC接口服务,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (ethash *Ethash) APIs(chain consensus.ChainReader) []rpc.API {
//为了和旧版本的兼容,加入两个命名空间,都一样
return []rpc.API{
{
Namespace: "eth",
Version: "1.0",
Service: &API{ethash},
Public: true,
},
{
Namespace: "ethash",
Version: "1.0",
Service: &API{ethash},
Public: true,
},
}
}

Hashrate()

这个是pow算法专有的接口,并不是共识算法引擎的,用于获取当前节点的算力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (ethash *Ethash) Hashrate() float64 {
// fake模式使用
if ethash.config.PowMode != ModeNormal && ethash.config.PowMode != ModeTest {
return ethash.hashrate.Rate1()
}
var res = make(chan uint64, 1)

select {
// fetchRateCh本身是一个chan chan uint64类型
//此处说明:本地或远程挖矿的算力会记录到ethash.fetchRateCh,然后它会将结果写入到res中,<-res即可取出结果
case ethash.fetchRateCh <- res: //用来收集本地或者远程的算力
case <-ethash.exitCh:
return ethash.hashrate.Rate1()
}
//返回整个算力
return ethash.hashrate.Rate1() + float64(<-res)
}

具体获取方式还是看上面注释吧,使用了select和channel,若不结合上下文,很难立马想通代码如何实现的。
这个方法只是对蒜粒结果的展示处理,真正的处理过程大体是在sealer.go文件中的mine()方法中,每计算一次,算力+1,具体实现小编后续再专门解释。

Close()

这个相当于是关闭整个挖矿线程,

1
2
3
4
5
6
7
8
9
10
11
12
13
func (ethash *Ethash) Close() error {
var err error
ethash.closeOnce.Do(func() { //只会触发执行一次
if ethash.exitCh == nil {
return
}
errc := make(chan error)
ethash.exitCh <- errc
err = <-errc
close(ethash.exitCh)
})
return err
}

总结

方法调用本身是代码逻辑,我们更多要考虑的是以太坊的设计逻辑,比如为什么要这样做,为什么不那样做,了解设计逻辑,才能真正了解以太坊的精华。
代码中涉及到了大量的channel、goroutine、select的使用,它让我们体验了golang本身的魄力,也体会到了以太坊开发人员强大的实力。
因为时间和精力,今天刚解析完以太坊pow实现共识引擎的整体逻辑,当然Ethash绝不仅仅只靠这几个接口方法就完成了整个共识过程,从上述代码解析中,也发现了Ethash新的一些方法,这些方法是Ethash本身独有的,而很多具体细节的实现也是在这些方法中,小编之后准备研究下Ethash剩余的方法。
尽情期待。

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2017-2023 Jason
  • Visitors: | Views:

谢谢打赏~

微信