eth pow 源码分析
区块头的基本数据结构
废话不多说,直接看代码:
// 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"` }
挖矿难度控制
|
PoW参数
接下来的两个参数就和无人不知无人不晓的工作量证明息息相关了,以太坊的工作量证明最终拼的就是谁最先得到这两个参数:MixDigest
和Nonce
.
目前以太坊线上使用的共识算法是基于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
startCh
:接收startCh
信号,开始挖矿chainHeadCh
:表示接收到新区块,需要终止当前的挖矿工作,开始新的挖矿。timer.C
:默认每三秒检查一次是否有新交易需要处理。如果有则需要重新开始挖矿。以便将加高的交易优先打包到区块中。
在 newWorkLoop
中还有一个辅助信号,resubmitAdjustCh
和 resubmitIntervalCh
。运行外部修改timer计时器的时钟。resubmitAdjustCh
是根据历史情况重新计算一个合理的间隔时间。而resubmitIntervalCh
则允许外部,实时通过 Miner
实例方法 SetRecommitInterval
修改间隔时间。
mainLoop
newWorkCh
:接收生成新的挖矿任务信号chainSideCh
:接收区块链中加入了一个新区块作为当前链头的旁支的信号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