Skip to content

Latest commit

 

History

History
515 lines (496 loc) · 31.3 KB

consensus.md

File metadata and controls

515 lines (496 loc) · 31.3 KB

Consensus

clique 主要涉及POA,用于测试网络; ethash主要涉及POW,用于主网络; misc是用于之前DAO分叉的文件。 下图是consensus文件中各组件的关系图: image Engine接口定义了共识引擎需要实现的所有函数,实际上按功能可以划分为2类:

  • 区块验证类:以Verify开头,当收到新区块时,需要先验证区块的有效性
  • 区块盖章类:包括Prepare/Finalize/Seal等,用于最终生成有效区块(比如添加工作量证明) 与区块验证相关联的还有2个外部接口:Processor用于执行交易,而Validator用于验证区块内容和状态。另外,由于需要访问以前的区块链数据,抽象出了一个ChainReader接口,BlockChain和HeaderChain都实现了该接口以完成对数据的访问。

1.区块验证流程

image Downloader收到新区块后会调用BlockChain的InsertChain()函数插入新区块。在插入之前需要先要验证区块的有效性,基本分为4个步骤:

  • 验证区块头:调用Ethash.VerifyHeaders()
  • 验证区块内容:调用BlockValidator.VerifyBody()(内部还会调用Ethash.VerifyUncles())
  • 执行区块交易:调用BlockProcessor.Process()(基于其父块的世界状态)
  • 验证状态转换:调用BlockValidator.ValidateState()
    如果验证成功,则往数据库中写入区块信息,然后广播ChainHeadEvent事件。

2.区块盖章流程

image 新产生的区块必须经过“盖章(seal)”才能成为有效区块,具体到Ethash来说,就是要执行POW计算以获得低于设定难度的nonce值。这个其实在之前的挖矿流程分析中已经接触过了,主要分为3个步骤:

  • 准备工作:调用Ethash.Prepare()计算难度值
  • 生成区块:调用Ethash.Finalize()打包新区块
  • 盖章:调用Ethash.Seal()进行POW计算,填充nonce值

3.实现分析

3.1 consensus.go

该文件主要是定义整个consensus,chainReader是读取以前的区块数据,Engine是consensus工作的核心模块,POW是目前的一种机制,可以看到他的核心模块是Engine

type PoW interface {
	Engine
	// Hashrate returns the current mining hashrate of a PoW consensus engine.
	Hashrate() float64
}

3.2 ethan/algorithm.go

它涉及到挖矿算法的很多细节。

// cacheSize returns the size of the ethash verification cache that belongs to a certain
// block number.
func cacheSize(block uint64) uint64 {
	epoch := int(block / epochLength)
	if epoch < maxEpoch {
		return cacheSizes[epoch]
	}
	return calcCacheSize(epoch)
}

// calcCacheSize calculates the cache size for epoch. The cache size grows linearly,
// however, we always take the highest prime below the linearly growing threshold in order
// to reduce the risk of accidental regularities leading to cyclic behavior.
func calcCacheSize(epoch int) uint64 {
	size := cacheInitBytes + cacheGrowthBytes*uint64(epoch) - hashBytes
	for !new(big.Int).SetUint64(size / hashBytes).ProbablyPrime(1) { // Always accurate for n < 2^64
		size -= 2 * hashBytes
	}
	return size
}

cache的具体作用涉及到挖矿计算的细节,如下: Ethash 是以太坊使用的 PoW 算法,其原理可以用一个公式来概括:
RAND(h,n)<=M/d
其中 h 是区块头的哈希值(没有 Nonce),n 是 Nonce 值,M 是一个极大的数字,d 指挖矿难度,RAND 是一个根据参数生成随机值的操作,挖矿的过程简单来说就是寻找适合的 nonce,使上述不等式成立。原理和比特币的基本相同,但 Ethash 稍特别一点,因为 geth 的开发者在设计初期就考虑了抵制矿机的问题里
Ethash 的具体步骤为:

  • 对于每个区块,先算出一个种子。种子的计算只依赖当前区块信息。
  • 使用种子生成伪随机数据集,称为 cache。轻客户端需要保存 cache
  • 基于 cache 生成 1GB 大小的数据集,称为 the DAG。这个数据集的每一个元素都依赖于 cache 中的某几个元素,只要有 cache 就可以快速计算出 DAG 中指定位置的元素。完整可挖矿客户端需要保存 DAG。
  • 挖矿可以概括为从 DAG 中随机选择元素,然后暴力枚举选择一个 nonce 值,对其进行哈希计算,使其符合约定的难度,而这个难度其实就是要求哈希值的前缀包括多少个0。验证的时候,基于 cache 计算指定位置 DAG 元素,然后验证这个元素集合的哈希值结果小于某个值,这个过程只需要普通 CPU 和普通内存。
  • cache 和 DAG 每过一个周期更新一次,一个周期长度是 30000 个区块。DAG 只取决于区块数量,大小会随着时间推移线性增长,从 1GB 开始,每年大约增加 7GB。由于 DAG 需要很长时间生成,所以 geth 每次会维护2个 DAG 集合。
// datasetSize returns the size of the ethash mining dataset that belongs to a certain
// block number.
func datasetSize(block uint64) uint64 {
	epoch := int(block / epochLength)
	if epoch < maxEpoch {
		return datasetSizes[epoch]
	}
	return calcDatasetSize(epoch)
}

// calcDatasetSize calculates the dataset size for epoch. The dataset size grows linearly,
// however, we always take the highest prime below the linearly growing threshold in order
// to reduce the risk of accidental regularities leading to cyclic behavior.
func calcDatasetSize(epoch int) uint64 {
	size := datasetInitBytes + datasetGrowthBytes*uint64(epoch) - mixBytes
	for !new(big.Int).SetUint64(size / mixBytes).ProbablyPrime(1) { // Always accurate for n < 2^64
		size -= 2 * mixBytes
	}
	return size
}

dataset就是上文中提到的数据集。datasetsize和cachesetsize都已经硬编码写进了文件当中,

// hasher is a repetitive hasher allowing the same hash data structures to be
// reused between hash runs instead of requiring new ones to be created.
type hasher func(dest []byte, data []byte)

// makeHasher creates a repetitive hasher, allowing the same hash data structures to
// be reused between hash runs instead of requiring new ones to be created. The returned
// function is not thread safe!
func makeHasher(h hash.Hash) hasher {
	// sha3.state supports Read to get the sum, use it to avoid the overhead of Sum.
	// Read alters the state but we reset the hash before every operation.
	type readerHash interface {
		hash.Hash
		Read([]byte) (int, error)
	}
	rh, ok := h.(readerHash)
	if !ok {
		panic("can't find Read method on hash")
	}
	outputLen := rh.Size()
	return func(dest []byte, data []byte) {
		rh.Reset()
		rh.Write(data)
		rh.Read(dest[:outputLen])
	}
}

// seedHash is the seed to use for generating a verification cache and the mining
// dataset.
func seedHash(block uint64) []byte {
	seed := make([]byte, 32)
	if block < epochLength {
		return seed
	}
	keccak256 := makeHasher(sha3.NewLegacyKeccak256())
	for i := 0; i < int(block/epochLength); i++ {
		keccak256(seed, seed)
	}
	return seed
}

seedHash也就是挖矿的第一步生成种子,makeHasher也就是生成种子(hash的过程)

func generateCache(dest []uint32, epoch uint64, seed []byte) 

generateCache是指从之前的种子中根据规则生成cache. The cache production process involves first sequentially filling up 32 MB of memory, then performing two passes of Sergio Demian Lerner's RandMemoHash algorithm from Strict Memory Hard Hashing Functions (2014). The output is a set of 524288 64-byte values.

func generateDatasetItem(cache []uint32, index uint32, keccak512 hasher) []byte
func generateDataset(dest []uint32, epoch uint64, cache []uint32) 

generateDatasetItem combines data from 256 pseudorandomly selected cache nodes, and hashes that to compute a single dataset node. generateDataset generates the entire ethash dataset for mining.

func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte)
func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte)
func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte)
  • hashimoto aggregates data from the full dataset in order to produce our final value for a particular header hash and nonce.
  • hashimotoLight aggregates data from the full dataset (using only a small in-memory cache) in order to produce our final value for a particular header hash and nonce.
  • hashimotoFull aggregates data from the full dataset (using the full in-memory dataset) in order to produce our final value for a particular header hash and nonce. hashimotoLight 的 lookup 函数不需要一个完整的 dataset,只需要一个占存储空间很小的 cache,然后临时生成一个 dataset,而 hashimotoFull 是直接从 dataset 拿到所需数据。因此 hashimotoLight 可以用于轻量级客户端的验证。hashimotoLight 和 hashimotoFull 最终会调用 hashimoto

hashimoto其流程是

  • 首先,将 hash 和 nonce 合并成一个40 bytes长的数组,取它的 SHA-512 哈希值取名 seed,长度为64 bytes。
  • 然后,将 seed[] 转化成以 uint32 为元素的数组 mix[],注意一个 uint32 数等于4 bytes,所以 seed[] 只能转化成16个 uint32 数,而 mix[] 数组长度32,所以此时 mix[] 数组前后各半是等值的。
  • 接着,使用 lookup() 函数。用一个循环,不断调用 lookup() 从外部数据集中取出 uint32 元素类型数组,向 mix[] 数组中混入未知的数据。循环的次数可用参数调节,目前设为64次。每次循环中,变化生成参数 index,从而使得每次调用 lookup() 函数取出的数组都各不相同。这里混入数据的方式是一种类似向量『异或』的操作,来自于 FNV 算法。
  • 待混淆数据完成后,得到一个基本上面目全非的 mix[],长度为32的 uint32 数组。这时,将其折叠(压缩)成一个长度缩小成原长1/4的uint32数组,折叠的操作方法来自于 FNV 算法。
  • 最后,将折叠后的 mix[] 由长度为8的 uint32 型数组直接转化成一个长度32的 byte 数组,这就是返回值 digest;同时将之前的 seed[] 数组与 digest 合并再取一次 SHA-256 哈希值,得到的长度32的 byte 数组,即返回值 result。

经过多次多种哈希运算,hashimoto 返回两个长度均为32的 byte 数组 digest 和 result,前文已提到,在 Ethash 的 mine 方法里,挖矿时需要经过一个死循环,直到找到一个 nonce,使得 hashimoto 返回的 result 和 target 是相等的,这时就表示符合要求,digest 被取 SHA3-256 哈希后也会存到区块头的 MixDigest 字段里,待 Ethash.VerifySeal() 进行验证。 image

3.3 ethan/api.go

the purpose is that API exposes ethash related methods for the RPC interface.

func (api *API) GetWork() ([4]string, error)

GetWork returns a work package for external miner. The work package consists of 3 strings:

  • result[0] - 32 bytes hex encoded current block header pow-hash
  • result[1] - 32 bytes hex encoded seed hash used for DAG
  • result[2] - 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty
  • result[3] - hex encoded block number
func (api *API) SubmitWork(nonce types.BlockNonce, hash, digest common.Hash) bool 

SubmitWork can be used by external miner to submit their POW solution. It returns an indication if the work was accepted. Note either an invalid solution, a stale work a non-existent work will return false.

func (api *API) SubmitHashRate(rate hexutil.Uint64, id common.Hash) bool

SubmitHashrate can be used for remote miners to submit their hash rate. This enables the node to report the combined hash rate of all miners which submit work through this node.what is hash rate?, simply it can be regared as computation.

3.4 ethan/consensus.go

ethan/consensus.go实现的大多函数是对consensus/onsensus.go中Engine中的interface的函数具体实现.具体功能注释都已经写的很详尽,在此不过多赘述。故只挑了一些进行注释。

ethan/consensus.go/VerifyHeaders()

VerifyHeaders和VerifyHeader实现原理都差不多,只不过VerifyHeaders是处理一堆headers

// Spawn as many workers as allowed threads
    workers := runtime.GOMAXPROCS(0)
    if len(headers) < workers {
        workers = len(headers)
    }

首先根据待验证区块的个数确定需要创建的线程数,最大不超过CPU个数。

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++ {
        go func() {
            for index := range inputs {
                errors[index] = ethash.verifyHeaderWorker(chain, headers, seals, index)
                done <- index
            }
        }()
    }

这一步就是创建线程了,每个线程会从inputs信道中获得待验证区块的索引号,然后调用verifyHeaderWorker()函数验证该区块,验证完后向done信道发送区块索引号。

errorsOut := make(chan error, len(headers))
    go func() {
        defer close(inputs)
        var (
            in, out = 0, 0
            checked = make([]bool, len(headers))
            inputs  = inputs
        )
        for {
            select {
            case inputs <- in:
                if in++; in == len(headers) {
                    // Reached end of headers. Stop sending to workers.
                    inputs = nil
                }
            case index := <-done:
                for checked[index] = true; checked[out]; out++ {
                    errorsOut <- errors[out]
                    if out == len(headers)-1 {
                        return
                    }
                }
            case <-abort:
                return
            }
        }
    }()
    return abort, errorsOut

这一步启动一个循环,首先往inputs信道中依次发送区块索引号,然后再从done信道中依次接收子线程处理完成的事件,最后返回验证结果。 接下来我们就分析一下ethash.verifyHeaderWorker()主要做了哪些工作:

func (ethash *Ethash) verifyHeaderWorker(chain consensus.ChainReader, headers []*types.Header, seals []bool, index int) error {
    var parent *types.Header
    if index == 0 {
        parent = chain.GetHeader(headers[0].ParentHash, headers[0].Number.Uint64()-1)
    } else if headers[index-1].Hash() == headers[index].ParentHash {
        parent = headers[index-1]
    }
    if parent == nil {
        return consensus.ErrUnknownAncestor
    }
    if chain.GetHeader(headers[index].Hash(), headers[index].Number.Uint64()) != nil {
        return nil // known block
    }
    return ethash.verifyHeader(chain, headers[index], parent, false, seals[index])
}

首先通过ChainReader拿到父块的header,然后调用ethash.verifyHeader(),这个函数就是真正去验证区块头了,这个函数比较长,大概列一下有哪些检查项:

  • 时间戳超前当前时间不得大于15s
  • 时间戳必须大于父块时间戳
  • 通过父块计算出的难度值必须和区块头难度值相同
  • 消耗的gas必须小于gas limit
  • 当前gas limit和父块gas limit的差值必须在规定范围内
  • 区块高度必须是父块高度+1
  • 调用ethash.VerifySeal()检查工作量证明
  • 验证硬分叉相关的数据
  • ethash.VerifySeal()函数,这个函数主要是用来检查工作量证明,用于校验难度的有效性nonce是否小于目标值(解题成功)
  • 特殊的校验,比如dao分叉后的几个块extra里面写了特殊数据,来判断一下

ethan/consensus.go/VerifyUncles()

这个函数是在BlockValidator.VerifyBody()内部调用的,主要是验证叔块的有效性。

    if len(block.Uncles()) > maxUncles {
        return errTooManyUncles
    }
以太坊规定每个区块打包的叔块不能超过2个。
    uncles, ancestors := set.New(), make(map[common.Hash]*types.Header)
    number, parent := block.NumberU64()-1, block.ParentHash()
    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()
    uncles.Add(block.Hash())

这段代码收集了当前块前7层的祖先块和叔块,用于后面的验证。

    for _, uncle := range block.Uncles() {
        // Make sure every uncle is rewarded only once
        hash := uncle.Hash()
        if uncles.Has(hash) {
            return errDuplicateUncle
        }
        uncles.Add(hash)
        // Make sure the uncle has a valid ancestry
        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
        }
    }

遍历当前块包含的叔块,做以下检查:

  • 如果祖先块中已经包含过了该叔块,返回错误
  • 如果发现该叔块其实是一个祖先块(即在主链上),返回错误
  • 如果叔块的父块不在这7层祖先中,返回错误
  • 如果叔块和当前块拥有共同的父块,返回错误(也就是说不能打包和当前块相同高度的叔块)
  • 最后验证一下叔块头的有效性

ethan/consensus.go/Prepare()


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
}

可以看到,会调用CalcDifficulty()计算难度值,继续跟踪:

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.IsByzantium(next):
        return calcDifficultyByzantium(time, parent)
    case config.IsHomestead(next):
        return calcDifficultyHomestead(time, parent)
    default:
        return calcDifficultyFrontier(time, parent)
    }
}

根据以太坊的Roadmap,会经历Frontier,Homestead,Metropolis,Serenity这几个大的版本,当前处于Metropolis阶段。Metropolis又分为2个小版本:Byzantium和Constantinople,目前的最新代码版本是Byzantium,因此会调用calcDifficultyByzantium()函数。
计算难度的公式如下:
diff = (parent_diff +(parent_diff / 2048 * max((2 if len(parent.uncles) else 1) - ((timestamp - parent.timestamp) / 9), -99))) + 2^(periodCount - 2)

  • parent_diff :上一个区块的难度
  • block_timestamp :当前块的时间戳
  • parent_timestamp:上一个块的时间戳
  • periodCount :区块num/100000
  • block_timestamp - parent_timestamp 差值小于10秒 变难
    block_timestamp - parent_timestamp 差值10-20秒 不变
    block_timestamp - parent_timestamp 差值大于20秒 变容易,并且大的越多,越容易,但是又上限
  • 总体上块的难度是递增的
  • seal 开始做挖矿的事情,“解题”直到成功或者退出.根据挖矿难度计算目标值,选取随机数nonce+区块头(不包含nonce)的hash,再做一次hash,结果小于目标值,则退出,否则循环重试.如果外部退出了(比如已经收到这个块了),则立马放弃当前块的打包.Finalize() 做挖矿成功后最后善后的事情,计算矿工的奖励:区块奖励,叔块奖励,

前面一项是根据父块难度值继续难度调整,而后面一项就是传说中的“难度炸弹”。关于难度炸弹相关的具体细节可以参考下面这篇文章: https://juejin.im/post/59ad6606f265da246f382b88
由于PoS共识机制开发进度延迟,不得不减小难度炸弹从而延迟“冰川时代”的到来,具体做法就是把当前区块高度减小3000000,参见以下代码:

   // calculate a fake block number for the ice-age delay:
    //   https://github.com/ethereum/EIPs/pull/669
    //   fake_block_number = min(0, block.number - 3_000_000
    fakeBlockNumber := new(big.Int)
    if parent.Number.Cmp(big2999999) >= 0 {
        fakeBlockNumber = fakeBlockNumber.Sub(parent.Number, big2999999) // Note, parent is 1 less than the actual block number
    }

ethash/consensus.go/FinalizeAndAssemble()

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) {
    // Accumulate any block and uncle rewards and commit the final state root
    accumulateRewards(chain.Config(), state, header, uncles)
    header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))
    // Header seems complete, assemble into a block and return
    return types.NewBlock(header, txs, uncles, receipts), nil
}

这个挖矿流程是先计算收益,然后生成MPT的Merkle Root,最后创建新区块。

ethash/sealer/seal()

这个函数就是真正执行POW计算的地方了,代码位于consensus/ethash/sealer.go。代码比较长,分段进行分析:

    abort := make(chan struct{})
    found := make(chan *types.Block)

首先创建了两个channel,用于退出和发现nonce时发送事件。

    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 nil, err
        }
        ethash.rand = rand.New(rand.NewSource(seed.Int64()))
    }
    ethash.lock.Unlock()
    if threads == 0 {
        threads = runtime.NumCPU()
    }

接着初始化随机数种子和线程数

    var pend sync.WaitGroup
    for i := 0; i < threads; i++ {
        pend.Add(1)
        go func(id int, nonce uint64) {
            defer pend.Done()
            ethash.mine(block, id, nonce, abort, found)
        }(i, uint64(ethash.rand.Int63()))
    }

然后就是创建线程进行挖矿了,会调用ethash.mine()函数。

    // Wait until sealing is terminated or a nonce is found
    var result *types.Block
    select {
    case <-stop:
        // Outside abort, stop all miner threads
        close(abort)
    case result = <-found:
        // One of the threads found a block, abort all others
        close(abort)
    case <-ethash.update:
        // Thread count was changed on user request, restart
        close(abort)
        pend.Wait()
        return ethash.Seal(chain, block, stop)
    }
    // Wait for all miners to terminate and return the block
    pend.Wait()
    return result, nil

最后就是等待挖矿结果了,有可能找到nonce挖矿成功,也有可能别人先挖出了区块从而需要终止挖矿。
ethash.mine()函数的实现,先看一些变量声明:

    var (
        header  = block.Header()
        hash    = header.HashNoNonce().Bytes()
        target  = new(big.Int).Div(maxUint256, header.Difficulty)
        number  = header.Number.Uint64()
        dataset = ethash.dataset(number)
    )
    // Start generating random nonces until we abort or find a good one
    var (
        attempts = int64(0)
        nonce    = seed
    )

其中hash指的是不带nonce的区块头hash值,nonce是一个随机数种子。target是目标值,等于2^256除以难度值,我们接下来要计算的hash值必须小于这个目标值才算挖矿成功。接下来就是不断修改nonce并计算hash值了:

            digest, result := hashimotoFull(dataset.dataset, hash, nonce)
     if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
     // Correct nonce found, create a new header with it
     header = types.CopyHeader(header)
     header.Nonce = types.EncodeNonce(nonce)
     header.MixDigest = common.BytesToHash(digest)
     // Seal and return a block (if still needed)
     select {
     case found <- block.WithSeal(header):
     logger.Trace("Ethash nonce found and reported", "attempts", nonce-seed, "nonce", nonce)
     case <-abort:
                logger.Trace("Ethash nonce found but discarded", "attempts", nonce-seed, "nonce", nonce)
                }
                break search
            }
     nonce++

hashimotoFull()函数内部会把hash和nonce拼在一起,计算出一个摘要(digest)和一个hash值(result)。如果hash值满足难度要求,挖矿成功,填充区块头的Nonce和MixDigest字段,然后调用block.WithSeal()生成盖过章的区块:

func (b *Block) WithSeal(header *Header) *Block {
    cpy := *header
    return &Block{
        header:       &cpy,
        transactions: b.transactions,
        uncles:       b.uncles,
    }
}

3.5 ethan/ethash.go

将此文件内容分为几个大块进行理解
1)memoryMap块
memoryMapFile tries to memory map an already opened file descriptor, memoryMap tries to memory map a file of uint32s for read only access, memoryMapAndGenerate tries to memory map a temporary file of uint32s for write access, fill it with the data from a generator and then move it into the final path requested.

func memoryMap(path string) (*os.File, mmap.MMap, []uint32, error)
func memoryMapFile(file *os.File, write bool) (mmap.MMap, []uint32, error)
func memoryMapAndGenerate(path string, size uint64, generator func(buffer []uint32)) (*os.File, mmap.MMap, []uint32, error)

2)Lru块
Lru是一个cache的存储策略,此处主要是用于优化dataset和cache中的存储数据方式
3)cache块
具体可以见文件中的注释,具体作用前面也已经说清楚,主要是cache的具体逻辑实现

func (ethash *Ethash) dataset(block uint64) *dataset {
	epoch := block / epochLength
	currentI, futureI := ethash.datasets.get(epoch)
	current := currentI.(*dataset)
	current.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)
	if futureI != nil {
		future := futureI.(*dataset)
		go future.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)
	}
	return current
}

在 consensus/ethash/ethash.go中,dataset 方法对数据集进行了封装。首先尝试从内存中取得,如果不存在则在文件目录中取得,如果还是不存在则通过 func (d *dataset) generate(dir string, limit int, test bool) 生成。具体来说,首先,我们计算 epoch,前面说到,每 30000 个区块就会换 DAG,这里的 30000 也就是 epochLength,也就是说 epoch 不变的的话,DAG 也不需要变。

func (lru *lru) get(epoch uint64) (item, future interface{}) {
	lru.mu.Lock()
	defer lru.mu.Unlock()
	item, ok := lru.cache.Get(epoch)
	if !ok {
		if lru.future > 0 && lru.future == epoch {
			item = lru.futureItem
		} else {
			log.Trace("Requiring new ethash "+lru.what, "epoch", epoch)
			item = lru.new(epoch)
		}
		lru.cache.Add(epoch, item)
	}
	if epoch < maxEpoch-1 && lru.future < epoch+1 {
		log.Trace("Requiring new future ethash "+lru.what, "epoch", epoch+1)
		future = lru.new(epoch + 1)
		lru.future = epoch + 1
		lru.futureItem = future
	}
	return item, future
}

get 方法会返回两个 interface(实际类型为 dataset),第一个是当前 epoch 对应的 dataset,第二个值是未来会用到的 dataset(epoch +1),如果不为空,表明需要重新生成,如果为空,表明之前已经生成过了。在func (d dataset) generate(dir string, limit int, test bool)中,首先通过 csize := cacheSize(d.epochepochLength + 1),dsize := datasetSize(d.epoch*epochLength + 1) 这两个调用得到缓存大小和数据集大小 4)dataset块
具体可以见文件中的注释,具体作用前面也已经说清楚,主要是函数的具体逻辑实现
5)config块

// Config are the configuration parameters of the ethash.
type Config struct {
	CacheDir       string
	CachesInMem    int
	CachesOnDisk   int
	DatasetDir     string
	DatasetsInMem  int
	DatasetsOnDisk int
	PowMode        Mode
}

6)后续代码块
后续代码多为挖矿所需定义的结构体,看注释就可以解决疑惑。

3.6 ethan/sealer.go

sealer主要是用于最终为block打标签,也就是最终的挖矿计算的过程。主要的函数如下:

func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error 

如果是 fake 模式,立即返回 0 nonce,这部分是为了方便单元测试。 如果是共享 pow,转到它的共享对象执行 seal 操作。 接下来通过多个 goroutine 调用 ethash.mine,因此需要上锁,保证缓存的安全。 Seal 的核心还是在 ethash.mine(block, id, nonce, abort, found) 这一行, seal 最后会监听 stop, found, ethash.update 这几个 channel,如果外部意外终止了,停止所有挖矿线程,如果其中有一个线程挖到正确区块,终止其他线程,如果 ethash 对象发生了变化,停止当前所有操作,重新调用 ethash.Seal。

  • Seal implements consensus.Engine, attempting to find a nonce that satisfies the block's difficulty requirements.
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) 

首先是变量的初始化,从区块头部提取一些数据,得到哈希值,目标值等等,注意 target = new(big.Int).Div(maxUint256, header.Difficulty) 这一行, 难度越高,target 也就越小,也就越难得到正确的结果。接下来 nonse 会初始化为 seed 值,然后进入一个死循环,不断增加 nonce 的值,通过调用 hashimotoFull 算法不断尝试,直到找到正确 nonse,写入到 found 这个 chan 里。

  • mine is the actual proof-of-work miner that searches for a nonce starting from seed that results in correct final block difficulty.
func (ethash *Ethash) remote(notify []string, noverify bool)
  • remote is a standalone goroutine to handle remote mining related stuff.