eth pow 源码分析

区块头的基本数据结构

version ethereum 1.8

废话不多说,直接看代码:

 
// Header represents a block header in the Ethereum blockchain.
type Header struct {
    // 1.结构信息
    ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
    UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
    Number      *big.Int       `json:"number"           gencodec:"required"`

    // 2.挖矿基础信息
    Coinbase    common.Address `json:"miner"            gencodec:"required"`
    GasLimit    uint64         `json:"gasLimit"         gencodec:"required"`
    GasUsed     uint64         `json:"gasUsed"          gencodec:"required"`

    // 3.状态信息
    Time        *big.Int       `json:"timestamp"        gencodec:"required"`
    Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
    TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
    ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
    Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`

    // 4.挖矿难度控制
    Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`

    // 5.PoW参数
    MixDigest   common.Hash    `json:"mixHash"          gencodec:"required"`
    Nonce       BlockNonce     `json:"nonce"            gencodec:"required"`

    // 6.其他
    Extra       []byte         `json:"extraData"        gencodec:"required"`
}

挖矿难度控制

 Difficulty以太坊的挖矿难度是动态调整的,它的难度调整仅和父区块和本区块挖矿时间有关。 而该函数实现里根据启动参数目前有三种难度调整方案:
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)
    }
}
 
本区块难度 = 父区块难度 + 难度调整值 + 难度炸弹
难度调整值 = f(父区块难度,父区块产生时间,本区块产生时间)
难度炸弹 = 2^(区块号/100000 - 2)

 

 

PoW参数

 

接下来的两个参数就和无人不知无人不晓的工作量证明息息相关了,以太坊的工作量证明最终拼的就是谁最先得到这两个参数:MixDigestNonce.

目前以太坊线上使用的共识算法是基于PoW的ethash算法,主要实现位于github.com/ethereum/go-ethereum/consensus/ethash包中。

PoW算法的思路都大致是相似的,通过暴力枚举猜测一个nonce值,使得根据这个nonce种子计算出的hash值符合约定的难度,这个难度其实就是要求hash值前缀包含多少个0.

目前以太坊使用的hash是256位,所以将难度折算成前缀0的位数就是:bits0 = (2^256)/difficulty,那么我们的代码不停枚举nonce然后将计算得到的hash值前缀0位数和这个做比较就行了,主逻辑代码如下:

func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
    // Extract some data from the header
    var (
        header  = block.Header()
        hash    = header.HashNoNonce().Bytes()
        // 将难度转换得出前缀0的位数
        target  = new(big.Int).Div(maxUint256, header.Difficulty)
        number  = header.Number.Uint64()
        dataset = ethash.dataset(number)
    )
    ...
search:
    for {
        ...
            // Compute the PoW value of this nonce
            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)
                ...
            }
            nonce++
         ...
    }
}

  

该函数首先计算出区块难度对应的前缀0位数target,然后生成PoW依赖的计算数据集dataset = ethash.dataset(number),最终开始死循环尝试计算digest, result := hashimotoFull(dataset.dataset, hash, nonce),得到结果后将这两个随机数据赋值到区块头对应字段去。

当这个区块成功挖出后,别的区块很容易验证这个区块的PoW是否有效,就使用同样方法产生计算数据集dataset,然后调用hashimotoLight(和hashimotoFull基本一致)计算出digest和区块头的MixDigest做比较就可以了。

这里我们跳过了两个重要的步骤:

a.依赖数据集dataset的生成实现 b.hashimotoFull/hashimotoLight的具体实现

依赖数据集的生成就要说到以太坊的DAG

 

DAG

ethash将DAG(有向非循环图)用于工作量证明算法,这是为每个epoch(epoch := block / epochLength)生成,例如,每3000个区块(125个小时,大约5.2天)。DAG要花很长时间生成。如果客户端只是按需要生成它,那么在找到新epoch第一个区块之前,每个epoch过渡都要等待很长时间。然而,DAG只取决于区块数量,所以可以预先计算来避免在每个epoch过渡过长的等待时间。Geth和ethminer执行自动的DAG生成,每次维持2个DAG以便epoch过渡流畅。挖矿从控制台操控的时候,自动DAG生成会被打开和关闭。

 

hashimoto

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()函数中,对于hashimotoFull()的两个返回值,会直接以big.int整型数形式比较result和target;如果符合要求,则将digest取SHA3-256的哈希值(256 bits),并存于Header.MixDigest中,待以后Ethash.VerifySeal()可以加以验证。

源码
func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
	// Extract some data from the header
	var (
		header  = block.Header()
		hash    = ethash.SealHash(header).Bytes()
		target  = new(big.Int).Div(two256, header.Difficulty)
		number  = header.Number.Uint64()
		dataset = ethash.dataset(number, false)
	)
	// Start generating random nonces until we abort or find a good one
	var (
		attempts  = int64(0)
		nonce     = seed
		powBuffer = new(big.Int)
	)
	logger := ethash.config.Log.New("miner", id)
	logger.Trace("Started ethash search for new nonces", "seed", seed)
search:
	for {
		select {
		case <-abort:
			// Mining terminated, update stats and abort
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			break search

		default:
			// We don't have to update hash rate on every nonce, so update after after 2^X nonces
			attempts++
			if (attempts % (1 << 15)) == 0 {
				ethash.hashrate.Mark(attempts)
				attempts = 0
			}
			// Compute the PoW value of this nonce
			digest, result := hashimotoFull(dataset.dataset, hash, nonce)
			//	cmp
			//	-1 if x <  y
			//	 0 if x == y
			//	+1 if x >  y
			if powBuffer.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++
		}
	}
	// Datasets are unmapped in a finalizer. Ensure that the dataset stays live
	// during sealing so it's not unmapped while being read.
	runtime.KeepAlive(dataset)
}

  

 

 

 

 

miner 架构

 

 

首先外部是通过miner对象进行了操作,miner里面则是实用worker对象来实现挖矿的整体功能。miner决定着是否停止挖矿或者是否可以开始挖矿,同时还可以设置矿工的地址来获取奖励。

真正调度处理挖矿相关细节的则是在worker.go里面,我们先来看一张总体的图。

 

 

 

有四个循环,分别通过几个channel负责不同的事:

newWorkLoop

  1. startCh:接收startCh信号,开始挖矿
  2. chainHeadCh:表示接收到新区块,需要终止当前的挖矿工作,开始新的挖矿。
  3. timer.C:默认每三秒检查一次是否有新交易需要处理。如果有则需要重新开始挖矿。以便将加高的交易优先打包到区块中。

在 newWorkLoop 中还有一个辅助信号,resubmitAdjustCh 和 resubmitIntervalCh。运行外部修改timer计时器的时钟。resubmitAdjustCh是根据历史情况重新计算一个合理的间隔时间。而resubmitIntervalCh则允许外部,实时通过 Miner 实例方法 SetRecommitInterval 修改间隔时间。

mainLoop

  1. newWorkCh:接收生成新的挖矿任务信号
  2. chainSideCh:接收区块链中加入了一个新区块作为当前链头的旁支的信号
  3. txsCh:接收交易池的Pending中新加入了交易事件的信号

TaskLoop则是提交新的挖矿任务,而resultLoop则是成功出块之后做的一些处理

参考资料

 

https://qjpcpu.github.io/blog/2018/02/24/shen-ru-ethereumyuan-ma-cong-qu-kuai-tou-kan-gong-shi-wa-kuang/

https://segmentfault.com/a/1190000038460904

 

posted @ 2022-11-25 11:06  熵殇  阅读(251)  评论(0编辑  收藏  举报