go-ethereum中ethash源码分析

本文代码大部分集中在consensus目录下

Engine

Engine就是以太坊中共识引擎,在构建Ethereum时被创建使用CreateConsensusEngine方法

CreateConsensusEngine(ctx, chainConfig, &config.Ethash, config.MinerNotify, config.MinerNoverify, chainDb),

func CreateConsensusEngine(ctx *node.ServiceContext, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database) consensus.Engine {
	if chainConfig.Clique != nil {
		return clique.New(chainConfig.Clique, db)
	}
	switch config.PowMode {
	case ethash.ModeFake:
		log.Warn("Ethash used in fake mode")
		return ethash.NewFaker()
	case ethash.ModeTest:
		log.Warn("Ethash used in test mode")
		return ethash.NewTester(nil, noverify)
	case ethash.ModeShared:
		log.Warn("Ethash used in shared mode")
		return ethash.NewShared()
	default:
		engine := ethash.New(ethash.Config{
			CacheDir:       ctx.ResolvePath(config.CacheDir),
			CachesInMem:    config.CachesInMem,
			CachesOnDisk:   config.CachesOnDisk,
			DatasetDir:     config.DatasetDir,
			DatasetsInMem:  config.DatasetsInMem,
			DatasetsOnDisk: config.DatasetsOnDisk,
		}, notify, noverify)
		engine.SetThreads(-1) // Disable CPU mining
		return engine
	}
}

首先如果需要PoA共识的话,创建clique对象。对于PoW共识的话根据不同模式创建不同的ethash对象。

对于CreateConsensusEngine是返回的一个Engine类型对象,它是一个接口,定义如下

// go-ethereum\consensus\consensus.go
type Engine interface {
	Author(header *types.Header) (common.Address, error)

	VerifyHeader(chain ChainReader, header *types.Header, seal bool) error

	VerifyHeaders(chain ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error)

	VerifyUncles(chain ChainReader, block *types.Block) error

	VerifySeal(chain ChainReader, header *types.Header) error

	Prepare(chain ChainReader, header *types.Header) error

	Finalize(chain ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction,
		uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error)

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

	SealHash(header *types.Header) common.Hash

	CalcDifficulty(chain ChainReader, time uint64, parent *types.Header) *big.Int

	APIs(chain ChainReader) []rpc.API

	Close() error
}

Author是返回挖出该区块的地址,接下来几个Verify开头的方法是用来验证对错的。Prepare用于填充区块头的难度字段。Finalize是发放奖励和组装区块。Seal用来打包区块。SealHash返回打包区块的hash。CalcDifficulty计算新区快的应有难度。

与Engine一起定义的还有一个PoW接口,它除了继承了Engine接口内容,还多一个Hashrate方法用于返回当前挖矿的hash率。

New

本文主要介绍Ethash源码,先看其New方法:

func New(config Config, notify []string, noverify bool) *Ethash {
	if config.CachesInMem <= 0 {
		log.Warn("One ethash cache must always be in memory", "requested", config.CachesInMem)
		config.CachesInMem = 1
	}
	if config.CacheDir != "" && config.CachesOnDisk > 0 {
		log.Info("Disk storage enabled for ethash caches", "dir", config.CacheDir, "count", config.CachesOnDisk)
	}
	if config.DatasetDir != "" && config.DatasetsOnDisk > 0 {
		log.Info("Disk storage enabled for ethash DAGs", "dir", config.DatasetDir, "count", config.DatasetsOnDisk)
	}
	ethash := &Ethash{
		config:       config,
		caches:       newlru("cache", config.CachesInMem, newCache),
		datasets:     newlru("dataset", config.DatasetsInMem, newDataset),
		update:       make(chan struct{}),
		hashrate:     metrics.NewMeterForced(),
		workCh:       make(chan *sealTask),
		fetchWorkCh:  make(chan *sealWork),
		submitWorkCh: make(chan *mineResult),
		fetchRateCh:  make(chan chan uint64),
		submitRateCh: make(chan *hashrate),
		exitCh:       make(chan chan error),
	}
	go ethash.remote(notify, noverify)
	return ethash
}

主要就是根据config信息构建一个ethash对象,我们简单回顾一下ethash的配置,首先在eth.DefaultConfig中加载了一部分默认配置信息

// go-ethereum\eth\config.go
var DefaultConfig = Config{
	SyncMode: downloader.FastSync,
	Ethash: ethash.Config{
		CacheDir:       "ethash",
		CachesInMem:    2,
		CachesOnDisk:   3,
		DatasetsInMem:  1,
		DatasetsOnDisk: 2,
	},
	NetworkId:      1,
	LightPeers:     100,
	DatabaseCache:  512,
	TrieCleanCache: 256,
	TrieDirtyCache: 256,
	TrieTimeout:    60 * time.Minute,
	MinerGasFloor:  8000000,
	MinerGasCeil:   8000000,
	MinerGasPrice:  big.NewInt(params.GWei),
	MinerRecommit:  3 * time.Second,

	TxPool: core.DefaultTxPoolConfig,
	GPO: gasprice.Config{
		Blocks:     20,
		Percentile: 60,
	},
}

这里给出了构建ethash的几个关键参数,还有一个config.DatasetDir是在go-ethereum\eth\config.go的init中初始化,默认是在/home/.ethash。除此之外还有一些列用户可以设定的参数,需要在启动以太坊节点后根据固定参数设置,在go-ethereum\cmd\utils\flags.go的SetEthConfig中加载参数,一般我们都没有指定特殊参数。new方法最后启动了一个goroutine运行remote方法,我们稍后再讲

Author

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

这个很简单就是返回区块头的Coinbase字段

VerifyHeader

func (ethash *Ethash) VerifyHeader(chain consensus.ChainReader, header *types.Header, seal bool) error {
	if ethash.config.PowMode == ModeFullFake {
		return nil
	}

	number := header.Number.Uint64()
	if chain.GetHeader(header.Hash(), number) != nil {
		return nil
	}
	parent := chain.GetHeader(header.ParentHash, number-1)
	if parent == nil {
		return consensus.ErrUnknownAncestor
	}

	return ethash.verifyHeader(chain, header, parent, false, seal)
}

首先对于ModeFullFake模式下的共识对任何区块头都予以放行,不进行检测。对于需要检测的区块头,首先获取可区块的高度,接着从本地尝试获取对应高度和hash的区块头,如果能获取到,说明该区块正确。之后在本地检查了父块,如果找不到返回ErrUnknownAncestor错误。

接下来如果能找到父块,则调用verifyHeader方法进行检测

verifyHeader

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 {
			return errLargeBlockTime
		}
	} else {
		if header.Time.Cmp(big.NewInt(time.Now().Add(allowedFutureBlockTime).Unix())) > 0 {
			return consensus.ErrFutureBlock
		}
	}
	if header.Time.Cmp(parent.Time) <= 0 {
		return errZeroBlockTime
	}

	expected := ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)

	if expected.Cmp(header.Difficulty) != 0 {
		return fmt.Errorf("invalid difficulty: have %v, want %v", header.Difficulty, expected)
	}

	cap := uint64(0x7fffffffffffffff)
	if header.GasLimit > cap {
		return fmt.Errorf("invalid gasLimit: have %v, max %v", header.GasLimit, cap)
	}

	if header.GasUsed > header.GasLimit {
		return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit)
	}

	diff := int64(parent.GasLimit) - int64(header.GasLimit)
	if diff < 0 {
		diff *= -1
	}
	limit := parent.GasLimit / params.GasLimitBoundDivisor

	if uint64(diff) >= limit || header.GasLimit < params.MinGasLimit {
		return fmt.Errorf("invalid gas limit: have %d, want %d += %d", header.GasLimit, parent.GasLimit, limit)
	}

	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
		}
	}

	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
}

这个是区块头验证的主要地方,基本遵循黄皮书4.3.4的所述。分以下几步:

  1. 验证额外数据长度度:在区块头有一个额外数据的字段Extra,它的长度要小于32字节
  2. 验证时间戳:时间戳最基本的要求是要大于父块的时间戳。其次对于如果是一个叔块,时间戳不能太大,代码中要求不大于2的256次方。对于普通区块,不能为未来15秒之外的区块。
  3. 验证总难度:首先调用CalcDifficulty计算应该难度,然后判断是否相等,关于难度的计算稍后介绍。
  4. 验证gas限制:首先要求不大于2^63-1,但不能小于5000。其次gaslimit还和父块中的gaslimit相关,先关公式如下:
  5. 验证gas用量:要求小于gasLimit即可
  6. 验证区块高度:要求等于父块高度加一
  7. 其他验证:如果方法参数seal为true,执行VerifySeal方法,稍后介绍
  8. 验证分叉

VerifyHeaders

func (ethash *Ethash) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) {
	if ethash.config.PowMode == ModeFullFake || len(headers) == 0 {
		abort, results := make(chan struct{}), make(chan error, len(headers))
		for i := 0; i < len(headers); i++ {
			results <- nil
		}
		return abort, results
	}

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

	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) {
					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
}

这个是批量验证。首先还是对ModeFullFake模式不进行实质的验证,直接放行。接着利用一个循环,并发的执行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])
}

这里逻辑很简单,根据index验证相应的区块头,首先获取父区块,然后要验证的区块在本地是否存在,之后调用verifyHeader方法。

回到VerifyHeaders,前面的那个并发程序后河面这个匿名方法是组合使用。手下前面的for循环启动了workers个goroutine,workers表示目前可用的cpu核心数,这样做是为了尽量满负荷运行,加速验证过程。但是每个线程都在从inputs这个channel读数据的时候阻塞,这时第二个匿名函数开始执行,首先他向inputs中写入数据,写入的是要验证的区块头的index,这样前面的验证线程可以结束阻塞执行verifyHeaderWorker方法。在验证完成后,将结果写入errors中,并向done赋值,这样又触发匿名方法中select中的逻辑,这里讲错误放入errorsOut,并由errorsOut最终返回。

VerifyUncles

func (ethash *Ethash) VerifyUncles(chain consensus.ChainReader, block *types.Block) error {
	if ethash.config.PowMode == ModeFullFake {
		return nil
	}

	if len(block.Uncles()) > maxUncles {
		return errTooManyUncles
	}

	uncles, ancestors := mapset.NewSet(), 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())

	for _, uncle := range block.Uncles() {
		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
}

这是验证叔块的。首先也是对于ModeFullFake模式直接放行。然后检查该块的叔块数量,如果大于2报错。接着寻找了该块的父块高度和hash。之后收集了该块的祖先,最高7代。ancestors是一个map,key是hash类型,value是header类型,它存储着从该块的父块开始连续寻找7个祖先块的区块头,最后将自己也放入在内。uncles存储着每一代祖先的所有叔块hash以及该块自己的hash。

之后遍历该块的所有叔块,对于每个叔块,首先验证是否已经在uncles内,uncles存的是该块祖先的叔块,所以他的叔块不应包含在内。在之后验证是否在ancestors,因为ancestors是其直接祖先块,所以也不能成为其叔块。在之后叔块的父块时候包含在ancestors内,或者叔块的父块等于该块的父块,这都是不和逻辑的。最后校验每个叔块是否正确,调用verifyHeader方法

VerifySeal

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

func (ethash *Ethash) verifySeal(chain consensus.ChainReader, header *types.Header, fulldag bool) error {
	if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
		time.Sleep(ethash.fakeDelay)
		if ethash.fakeFail == header.Number.Uint64() {
			return errInvalidPoW
		}
		return nil
	}

	if ethash.shared != nil {
		return ethash.shared.verifySeal(chain, header, fulldag)
	}

	if header.Difficulty.Sign() <= 0 {
		return errInvalidDifficulty
	}

	number := header.Number.Uint64()

	var (
		digest []byte
		result []byte
	)

	if fulldag {
		dataset := ethash.dataset(number, true)
		if dataset.generated() {
			digest, result = hashimotoFull(dataset.dataset, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())

			runtime.KeepAlive(dataset)
		} else {
			// Dataset not yet generated, don't hang, use a cache instead
			fulldag = false
		}
	}

	if !fulldag {
		cache := ethash.cache(number)

		size := datasetSize(number)
		if ethash.config.PowMode == ModeTest {
			size = 32 * 1024
		}
		digest, result = hashimotoLight(size, cache.cache, ethash.SealHash(header).Bytes(), header.Nonce.Uint64())

		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
}

这个方法用来检测区块是否满足PoW的难度要求,主要是验证nonce。首先对于ModeFake和ModeFullFake不进行检测。另外如果是shared模式,则采用对应的verifySeal方法。接下来检测区块头的难度值必须大于0.接下来根据fulldag的具体情况执行不同逻辑。

如果fullgdag为true,则先加载对应数据集,如果数据集已经产生则使用hashimotoFull进行验证,否则fulldag置为false。如果fullgad为false,则加载对应的随机数集cache,然后计算数据集大小,之后用hashimotoLight进行验证。这两处都用了KeepAlive方法,这是告诉编译器不要回收给定代码的内存。

验证之后返回两个结果digest和result,按照下面公式进行验证:

其中的n就是result,m就是digest。Hd表示该区块的难度;Hm表示该区块头的mixHash字段,是一个256位的哈希值,用来与nonce一起证明当前区块已经承载了足够的计算量。上图中的PoW函数的第一个参数表示不包含nonce和mixHash的区块头,也就是SealHash方法所产生的。

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
}

这个方法首先查找了给定区块头的父区块,然后计算了难度。

Finalize

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)
	header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

	return types.NewBlock(header, txs, uncles, receipts), nil
}

首先调用accumulateRewards发放奖励:

func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
	blockReward := FrontierBlockReward
	if config.IsByzantium(header.Number) {
		blockReward = ByzantiumBlockReward
	}
	if config.IsConstantinople(header.Number) {
		blockReward = ConstantinopleBlockReward
	}

	reward := new(big.Int).Set(blockReward)
	r := new(big.Int)
	for _, uncle := range uncles {
		r.Add(uncle.Number, big8)
		r.Sub(r, header.Number)
		r.Mul(r, blockReward)
		r.Div(r, big8)
		state.AddBalance(uncle.Coinbase, r)

		r.Div(blockReward, big32)
		reward.Add(reward, r)
	}
	state.AddBalance(header.Coinbase, reward)
}

先根据区块高度决定基本的区块奖励,详细奖励发放的细节见黄皮书11.3节描述。如果是拜占庭版本也就是区块高度大于4370000,每个区块基本奖励为3以太币。如果是君士坦丁堡版本,也就是区块高度大于7280000,每个区块基本奖励为2以太币。如果区块高度低于4370000,则是5以太币。之后遍历所有叔块,每多一个叔块,奖励提高三十二分之一的基本区块奖励。对于每个叔块也有其相应奖励,公式如下:

上面公式用Ui表示叔块高度,BHi表示当前区块高度,Rblock表示表示基本区块奖励。每计算一个叔块用state的AddBalance修改对应地址的余额。最后修改该区块头的coinbase余额,奖励发放完毕。

回到Finalize,计算新状态的状态树根hash,然后染回一个新的区块。

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

这里首先计算该区块多对应的版本号,简单说一下不同版本的划分,高度在7280000以上的为君士坦丁堡版本;高度在4370000以上的为拜占庭版本;高度在1150000以上的为家园(Homestead)版本;最后高度在1150000一下的为前沿(Frontier)版本。其中君士坦丁堡版本和拜占庭版本都调用了makeDifficultyCalculator方法,区别是其中的参数不同,这个参数为了推迟难度炸弹的来临,关于难度计算是根据黄皮书中的公式计算的,这里讲一下公式,公式如下:

公式(41)描述了难度的计算,首先创世区块为131072,转为16进制表示就是0x‭20000,这就是官网给的创世区块json示例中的难度值。对于非创世区块则在两个值中取一个最大值。第一个值就是创世区块的难度。第二个值由三部分组成:

  1. P(H)Hd表示父区块难度
  2. x表示父区块的难度除以2048并向下取整。与x相乘的有一个系数,由公式(44)计算。这个公式中y有父区块的叔块数量决定,如果叔块数量为0则取1,否则取2.公式(44)中的Hs表示当前区块的时间戳,P(H)Hs表示父区块的时间戳。
  3. 第三部分由公式(45)(46)联合计算,这里在以太坊初始版本中没有公式(46),直接是用区块高度除以100000,由于是做指数运算,会使第三部分的值每十万个区块指数增长一次,难度值会越来越夸张,所以从拜占庭版本开始对区块高度减去一个值,拜占庭版本是3000000,君士坦丁堡版本是5000000。公式中的Hi就是区块高度。

区块的难度就由上面的公式计算的,代码也都是一些数学运算,这里不再叙述了。

关于难度计算通过公式可以看到也是动态调整的,首先它是基于父块的难度,然后难度的第二部分,当两个区块时间间隔过近时,x的系数为正,加大难度;当两个区块时间间隔过远时,x的系数为负数,减小难度值。最后一部分则会根据区块高度稳定增加一定的难度。

Seal

这个方法就是尝试找到一个nonce去满足PoW要求,然后封装一个区块

func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
	if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
		header := block.Header()
		header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}
		select {
		case results <- block.WithSeal(header):
		default:
			log.Warn("Sealing result is not read by miner", "mode", "fake", "sealhash", ethash.SealHash(block.Header()))
		}
		return nil
	}

	if ethash.shared != nil {
		return ethash.shared.Seal(chain, block, results, stop)
	}

	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()
	}
	if threads < 0 {
		threads = 0 // Allows disabling local mining without extra logic around local/remote
	}

	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)
		}(i, uint64(ethash.rand.Int63()))
	}

	go func() {
		var result *types.Block
		select {
		case <-stop:
			close(abort)
		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
}

首先对于ModeFake或ModeFullFake模式,直接打包一个区块,nonce为0。另外如果为shared模式则调用对应的Seal方法。

接下来ethash.threads获取挖矿线程数,如果为0的话表示线程数等于cpu核数,如果小于0则禁用。接着如果随机数种子为空,则生成一个。接着启动threads个goroutine去执行mine方法。另外在外面还有一个独立的goroutine执行一个匿名方法,用于接受信号执行响应逻辑。stop读到信号表示终止操作;locals读到信号表示有线程找到了合法值;update收到信号表示进行重启。最后这个方法会等待前面执行mine的goroutine都结束才会退出。

mine

func (ethash *Ethash) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {
	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)
	)

	var (
		attempts = int64(0)
		nonce    = seed
	)
	logger := log.New("miner", id)
	logger.Trace("Started ethash search for new nonces", "seed", seed)
search:
	for {
		select {
		case <-abort:
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			break search

		default:
			attempts++
			if (attempts % (1 << 15)) == 0 {
				ethash.hashrate.Mark(attempts)
				attempts = 0
			}
			digest, result := hashimotoFull(dataset.dataset, hash, nonce)
			if new(big.Int).SetBytes(result).Cmp(target) <= 0 {
				header = types.CopyHeader(header)
				header.Nonce = types.EncodeNonce(nonce)
				header.MixDigest = common.BytesToHash(digest)

				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++
		}
	}
	runtime.KeepAlive(dataset)
}

这个方法才是实际上寻找nonce的方法,基本逻辑从后面的for循环开始,从给定的seed开始,每次加一,利用hashimotoFull计算digest和result,就和前面的VerifySeal方法一样,只要result小于target即可。target等于2^256除以区块的难度。

如果找到的话给区块头补充nonce和misdigest字段,并通过WithSeal产生一个区块赋值给found,found就是前面Seal中的locals。

Ethash算法

官方文档有详细的叙述,这里简单描述一下。

Ethash是以太坊中PoW的主要算法,基本流程如下:

  1. 根据区块高度计算出一个seed
  2. 根据seed产生一个16MB伪随机数据集cache
  3. 根据cache产生一个1GB大小的数据集DAG,一般完全节点需要DAG,轻客户端只需要cache
  4. 从DAG中随机选出元素,对其进行hash运算,看是否满足要求
  5. 数据集30000个区块更新一次,约100小时,并且会线性增长,DAG每次增长8MB,cache每次128KB。(之所以是100小时,目的是为了在18月左右增长一倍,符合摩尔定律)

数据结构

type cache struct {
	epoch uint64    // Epoch for which this cache is relevant
	dump  *os.File  // File descriptor of the memory mapped cache
	mmap  mmap.MMap // Memory map itself to unmap before releasing
	cache []uint32  // The actual cache data content (may be memory mapped)
	once  sync.Once // Ensures the cache is generated only once
}

type dataset struct {
	epoch   uint64    // Epoch for which this cache is relevant
	dump    *os.File  // File descriptor of the memory mapped cache
	mmap    mmap.MMap // Memory map itself to unmap before releasing
	dataset []uint32  // The actual cache data content
	once    sync.Once // Ensures the cache is generated only once
	done    uint32    // Atomic flag to determine generation status
}

cache生成

前面在verifySeal方法中就通过cache获取了对应的cache

func (ethash *Ethash) cache(block uint64) *cache {
	epoch := block / epochLength
	currentI, futureI := ethash.caches.get(epoch)
	current := currentI.(*cache)

	current.generate(ethash.config.CacheDir, ethash.config.CachesOnDisk, ethash.config.PowMode == ModeTest)


	if futureI != nil {
		future := futureI.(*cache)
		go future.generate(ethash.config.CacheDir, ethash.config.CachesOnDisk, ethash.config.PowMode == ModeTest)
	}
	return current
}

第一步计算了代数,就是区块高度除以30000,然后从caches从尝试获取cache,caches的get获取所需代数及下一代的cache。

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
}

lru.cache是一个lru类型的缓存,先从中获取指定代数的,如果没有尝试从futureItem中获取,futureItem永远存储着下一代的cache,当然最开始为空。如果都没有新建一个。不管从哪里取到的话,如果代数低于最大代数,而且future需要更新的话,就更新future与futureItem。

我们再看新建方法:

func newCache(epoch uint64) interface{} {
	return &cache{epoch: epoch}
}

就是新建一个cache对象。回到cache方法,调用了generate方法

func (c *cache) generate(dir string, limit int, test bool) {
	c.once.Do(func() {
		size := cacheSize(c.epoch*epochLength + 1)
		seed := seedHash(c.epoch*epochLength + 1)
		if test {
			size = 1024
		}

		if dir == "" {
			c.cache = make([]uint32, size/4)
			generateCache(c.cache, c.epoch, seed)
			return
		}

		var endian string
		if !isLittleEndian() {
			endian = ".be"
		}
		path := filepath.Join(dir, fmt.Sprintf("cache-R%d-%x%s", algorithmRevision, seed[:8], endian))
		logger := log.New("epoch", c.epoch)


		runtime.SetFinalizer(c, (*cache).finalizer)


		var err error
		c.dump, c.mmap, c.cache, err = memoryMap(path)
		if err == nil {
			logger.Debug("Loaded old ethash cache from disk")
			return
		}
		logger.Debug("Failed to load old ethash cache", "err", err)


		c.dump, c.mmap, c.cache, err = memoryMapAndGenerate(path, size, func(buffer []uint32) { generateCache(buffer, c.epoch, seed) })
		if err != nil {
			logger.Error("Failed to generate mapped ethash cache", "err", err)

			c.cache = make([]uint32, size/4)
			generateCache(c.cache, c.epoch, seed)
		}

		for ep := int(c.epoch) - limit; ep >= 0; ep-- {
			seed := seedHash(uint64(ep)*epochLength + 1)
			path := filepath.Join(dir, fmt.Sprintf("cache-R%d-%x%s", algorithmRevision, seed[:8], endian))
			os.Remove(path)
		}
	})
}

首先计算了cache大小,使用cacheSize方法,如果代数小于2048(按30000块一代,2048代有61440000个区块,需要二十几年才能生成)则根据预先计算好的表查找,否则先计算size = 初始值(128MB) + 128KB * 代数 - 64B,然后验证size/64取整后是否为质数,否则size减去128在验证,循环递减直到size/64取整后为质数。对于测试模式直接设为1024.

第二步计算了seed,如果是第零代区块,直接返回一个空的长度为32的字节数组。否则对那个空字节数组循环做n次keccak256哈希运算,n就是区块所在代数。总之seed是一个256位的数据

接着组装了本地的存储文件,接下来的runtime.SetFinalizer方法是设置cache被回收时执行的方法。接着调用memoryMap将加载文件并在内存中映射,这个方法如果不报错说明之前在磁盘有旧文件,可以直接用。否则需要生成cache文件。

func memoryMapAndGenerate(path string, size uint64, generator func(buffer []uint32)) (*os.File, mmap.MMap, []uint32, error) {
	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
		return nil, nil, nil, err
	}
	temp := path + "." + strconv.Itoa(rand.Int())

	dump, err := os.Create(temp)
	if err != nil {
		return nil, nil, nil, err
	}
	if err = dump.Truncate(int64(len(dumpMagic))*4 + int64(size)); err != nil {
		return nil, nil, nil, err
	}
	mem, buffer, err := memoryMapFile(dump, true)
	if err != nil {
		dump.Close()
		return nil, nil, nil, err
	}
	copy(buffer, dumpMagic)

	data := buffer[len(dumpMagic):]
	generator(data)

	if err := mem.Unmap(); err != nil {
		return nil, nil, nil, err
	}
	if err := dump.Close(); err != nil {
		return nil, nil, nil, err
	}
	if err := os.Rename(temp, path); err != nil {
		return nil, nil, nil, err
	}
	return memoryMap(path)
}

首先创建目录,然后创建一个临时文件,接着调用Truncate方法修改临时文件大小,大小为8+size。接着映射该临时文件到内存,用的是memoryMapFile方法,我们具体看一下:

func memoryMapFile(file *os.File, write bool) (mmap.MMap, []uint32, error) {

	flag := mmap.RDONLY
	if write {
		flag = mmap.RDWR
	}
	mem, err := mmap.Map(file, flag, 0)
	if err != nil {
		return nil, nil, err
	}

	header := *(*reflect.SliceHeader)(unsafe.Pointer(&mem))
	header.Len /= 4
	header.Cap /= 4

	return mem, *(*[]uint32)(unsafe.Pointer(&header)), nil
}

首先设置模式,只读或是可读写。接着调用mmap.Map方法,这是”github.com/edsrzf/mmap-go”的库。mmap将一个文件或者其它对象映射进内存,让用户程序直接访问设备内存,这种机制,相比较在用户空间和内核空间互相拷贝数据,效率更高。Map方法返回一个MMap对象,翻阅源码实际就是字节数组。接下来将MMap转为一个切片(切片实际就是SliceHeader类型),这里将切片的长度和容器都缩减到原来四分之一,这是因为原来是字节数组,后面要改为uin32数组,之后有将修改过的切片转为32位无符号整形数组连同MMap一起返回。

回到memoryMapAndGenerate中,这里将dumpMagic拷贝到刚才返回的那个uint32数组中,dumpMagic是预置的一个含有两个魔法数的数组。接着调用generator生成数据,实际上是generateCache方法

func generateCache(dest []uint32, epoch uint64, seed []byte) {
	logger := log.New("epoch", epoch)

	start := time.Now()
	defer func() {
		elapsed := time.Since(start)

		logFn := logger.Debug
		if elapsed > 3*time.Second {
			logFn = logger.Info
		}
		logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
	}()

	header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
	header.Len *= 4
	header.Cap *= 4
	cache := *(*[]byte)(unsafe.Pointer(&header))


	size := uint64(len(cache))
	rows := int(size) / hashBytes


	var progress uint32

	done := make(chan struct{})
	defer close(done)

	go func() {
		for {
			select {
			case <-done:
				return
			case <-time.After(3 * time.Second):
				logger.Info("Generating ethash verification cache", "percentage", atomic.LoadUint32(&progress)*100/uint32(rows)/4, "elapsed", common.PrettyDuration(time.Since(start)))
			}
		}
	}()

	keccak512 := makeHasher(sha3.NewLegacyKeccak512())

	keccak512(cache, seed)
	for offset := uint64(hashBytes); offset < size; offset += hashBytes {
		keccak512(cache[offset:], cache[offset-hashBytes:offset])
		atomic.AddUint32(&progress, 1)
	}

	temp := make([]byte, hashBytes)

	for i := 0; i < cacheRounds; i++ {
		for j := 0; j < rows; j++ {
			var (
				srcOff = ((j - 1 + rows) % rows) * hashBytes
				dstOff = j * hashBytes
				xorOff = (binary.LittleEndian.Uint32(cache[dstOff:]) % uint32(rows)) * hashBytes
			)
			bitutil.XORBytes(temp, cache[srcOff:srcOff+hashBytes], cache[xorOff:xorOff+hashBytes])
			keccak512(cache[dstOff:], temp)

			atomic.AddUint32(&progress, 1)
		}
	}

	if !isLittleEndian() {
		swap(cache)
	}
}

传入的参数有三个。第一个就是生成的数据要填充的数组,也就是前面映射文件时返回的一个数组,第二个是区块所在代数,第三个是开始计算的种子。cache生成流程如下:

  1. 第一步将传进来的数组转为切片,然后长度和容量都乘以4。这里有几个大小需要注意,首先创建临时文件时大小是8+size,映射完返回的buffer容量是2+cap/4,之后前两个字节被写入固定值,之后剩余cap/4容量被传过来,这里又乘以4,进行了恢复,目的是为了将uint32数组转为字节数组,一个uint32等于4个字节长度。之后再将切片转为字节数组。然后计算大小size和轮数rows。接着启动一个goroutine,里面有一个for-select结构,每3秒打印一个log,显示cache生成进度。

  2. 第二步,准备一个hash函数–keccak512,keccak512(cache, seed)方法表示对seed计算hash值并写入cache中。此时cache的前512位也就是前64个字节

  3. 第三步开始一个循环,对cache进行预填充,每512位一组即64字节,第一组是对seed进行hash后的结果,之后每一组都是对前一组的hash。

  4. 第四步开始主循环,外循环有3轮,内循环轮数为第一步计算的rows。这一步连同上一步合起来是一种叫RandMemoHash的函数,是一种内存困难型的计算,详细内容参见原文或者译文。这里不再叙述,只是说一下几个方法的作用,XORBytes接受三个参数,作用是将第二个和第三个参数异或后的结果放入第一个参数中。keccak512是对第二个参数做hash运算,结果放入第一个参数。

经过上面第四步的循环后,cache被填满,最后还要保证cache中的数据时小端模式。注意cache是由header转换类型而来的,header则是的dest转换类型而来的,所以最后dest中也填满了数据。

最后cache中是若干组数据,每组有64个字节,也就是512位数据。

再回到memoryMapAndGenerate中,经过generator方法后,data填充了数据,data是buffer从2开始的切片,buffer前两个索引被放入的魔法数字,此时buffer也是有数据的。接着解除映射,这一步会将刚才内存中的数据写入文件,但注意此时还是那个temp临时文件,这里对其重命名,然后调用memoryMap

func memoryMap(path string) (*os.File, mmap.MMap, []uint32, error) {
	file, err := os.OpenFile(path, os.O_RDONLY, 0644)
	if err != nil {
		return nil, nil, nil, err
	}
	mem, buffer, err := memoryMapFile(file, false)
	if err != nil {
		file.Close()
		return nil, nil, nil, err
	}
	for i, magic := range dumpMagic {
		if buffer[i] != magic {
			mem.Unmap()
			file.Close()
			return nil, nil, nil, ErrInvalidDumpMagic
		}
	}
	return file, mem, buffer[len(dumpMagic):], err
}

这里再次对文件进行映射,只不过这次时只读模式,然后检查开头两个魔法数字来判断格式是否正确,然后返回文件句柄,映射对象,实际数据。

再往前回到cache的generate方法,memoryMapAndGenerate结束后,返回的几个值分别赋给了cache的几个成员,其中cache就是实际数据。接着判断前一步是否有错,有错的话则不映射文件直接生成数据。最后删除之前一些旧的文件。

最后回到Ethash的cache方法,此时我们只是生成的当前代的cache,接下来,为了方便,我们在生成下一代的cache,不过是在一个独立的goroutine中生成,方法一样,最后返回当前代的cache。

dataset生成

同样在verifySeal方法中也有获取dateset的方法,用的是ethash的dataset方法

func (ethash *Ethash) dataset(block uint64, async bool) *dataset {
	epoch := block / epochLength
	currentI, futureI := ethash.datasets.get(epoch)
	current := currentI.(*dataset)

	if async && !current.generated() {
		go func() {
			current.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)

			if futureI != nil {
				future := futureI.(*dataset)
				future.generate(ethash.config.DatasetDir, ethash.config.DatasetsOnDisk, ethash.config.PowMode == ModeTest)
			}
		}()
	} else {
		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
}

前三行和cache一样,都尝试获取当前和下一代的数据集,接着判断当前代的dataset是否已经生成,没有的话调用generate

func (d *dataset) generate(dir string, limit int, test bool) {
	d.once.Do(func() {
		defer atomic.StoreUint32(&d.done, 1)

		csize := cacheSize(d.epoch*epochLength + 1)
		dsize := datasetSize(d.epoch*epochLength + 1)
		seed := seedHash(d.epoch*epochLength + 1)
		if test {
			csize = 1024
			dsize = 32 * 1024
		}

		if dir == "" {
			cache := make([]uint32, csize/4)
			generateCache(cache, d.epoch, seed)

			d.dataset = make([]uint32, dsize/4)
			generateDataset(d.dataset, d.epoch, cache)

			return
		}

		var endian string
		if !isLittleEndian() {
			endian = ".be"
		}
		path := filepath.Join(dir, fmt.Sprintf("full-R%d-%x%s", algorithmRevision, seed[:8], endian))
		logger := log.New("epoch", d.epoch)

		runtime.SetFinalizer(d, (*dataset).finalizer)

		d.dump, d.mmap, d.dataset, err = memoryMap(path)
		if err == nil {
			logger.Debug("Loaded old ethash dataset from disk")
			return
		}
		logger.Debug("Failed to load old ethash dataset", "err", err)


		cache := make([]uint32, csize/4)
		generateCache(cache, d.epoch, seed)

		d.dump, d.mmap, d.dataset, err = memoryMapAndGenerate(path, dsize, func(buffer []uint32) { generateDataset(buffer, d.epoch, cache) })
		if err != nil {
			logger.Error("Failed to generate mapped ethash dataset", "err", err)

			d.dataset = make([]uint32, dsize/2)
			generateDataset(d.dataset, d.epoch, cache)
		}

		for ep := int(d.epoch) - limit; ep >= 0; ep-- {
			seed := seedHash(uint64(ep)*epochLength + 1)
			path := filepath.Join(dir, fmt.Sprintf("full-R%d-%x%s", algorithmRevision, seed[:8], endian))
			os.Remove(path)
		}
	})
}

这里首先计算了对应代的cache大小和种子seed,和前面cache中一样,不在叙述。除此之外还计算了dateset大小:

func datasetSize(block uint64) uint64 {
	epoch := int(block / epochLength)
	if epoch < maxEpoch {
		return datasetSizes[epoch]
	}
	return calcDatasetSize(epoch)
}

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
}

和计算cachesize一样,对于2048代以内是查表,超过2048代是size=1GB+8MB*代数-128B,同样还是要求size除以128为质数,否则减去256在判断,如此循环直到为质数为止。

回到generate中,如果是测试模式,则设置cache的大小为1024,dateset大小为32*1024。接着如果没有指定目录的话,则不用文件映射,直接先生成一个cache,之后再生成dateset

func generateDataset(dest []uint32, epoch uint64, cache []uint32) {
	logger := log.New("epoch", epoch)

	start := time.Now()
	defer func() {
		elapsed := time.Since(start)

		logFn := logger.Debug
		if elapsed > 3*time.Second {
			logFn = logger.Info
		}
		logFn("Generated ethash verification cache", "elapsed", common.PrettyDuration(elapsed))
	}()

	swapped := !isLittleEndian()

	header := *(*reflect.SliceHeader)(unsafe.Pointer(&dest))
	header.Len *= 4
	header.Cap *= 4
	dataset := *(*[]byte)(unsafe.Pointer(&header))

	threads := runtime.NumCPU()
	size := uint64(len(dataset))

	var pend sync.WaitGroup
	pend.Add(threads)

	var progress uint32
	for i := 0; i < threads; i++ {
		go func(id int) {
			defer pend.Done()

			keccak512 := makeHasher(sha3.NewLegacyKeccak512())

			batch := uint32((size + hashBytes*uint64(threads) - 1) / (hashBytes * uint64(threads)))
			first := uint32(id) * batch
			limit := first + batch
			if limit > uint32(size/hashBytes) {
				limit = uint32(size / hashBytes)
			}

			percent := uint32(size / hashBytes / 100)
			for index := first; index < limit; index++ {
				item := generateDatasetItem(cache, index, keccak512)
				if swapped {
					swap(item)
				}
				copy(dataset[index*hashBytes:], item)

				if status := atomic.AddUint32(&progress, 1); status%percent == 0 {
					logger.Info("Generating DAG in progress", "percentage", uint64(status*100)/(size/hashBytes), "elapsed", common.PrettyDuration(time.Since(start)))
				}
			}
		}(i)
	}
	pend.Wait()
}

这里首先对传入的dest也就是要存放数据的数组进行处理,先转为切片,修改其长度和容量,都是扩大4倍,之后再转为字节数组类型。接着获取cpu核数记为threads,获取数据集大小,然后启动threads个独立的goroutine。

首先dataset的整体数据结构和cache类似,都是按64字节也就是512位为一组,这里为了实现多线程并行计算,在每个goroutine中计算了需要计算的组数,然后循环调用generateDatasetItem生成每一组数据:

func generateDatasetItem(cache []uint32, index uint32, keccak512 hasher) []byte {
	rows := uint32(len(cache) / hashWords)

	mix := make([]byte, hashBytes)

	binary.LittleEndian.PutUint32(mix, cache[(index%rows)*hashWords]^index)
	for i := 1; i < hashWords; i++ {
		binary.LittleEndian.PutUint32(mix[i*4:], cache[(index%rows)*hashWords+uint32(i)])
	}
	keccak512(mix, mix)

	intMix := make([]uint32, hashWords)
	for i := 0; i < len(intMix); i++ {
		intMix[i] = binary.LittleEndian.Uint32(mix[i*4:])
	}

	for i := uint32(0); i < datasetParents; i++ {
		parent := fnv(index^i, intMix[i%16]) % rows
		fnvHash(intMix, cache[parent*hashWords:])
	}

	for i, val := range intMix {
		binary.LittleEndian.PutUint32(mix[i*4:], val)
	}
	keccak512(mix, mix)
	return mix
}

首先把cache中的数据每16个视作一行,计算cache的总行数,由于cache每个元素时32位,16个也即是512位,也就是cache中的一组。这里初始化了一个含有64字节的数组mix,总长度也就是512位。接着按照一定规则计算mix,基本原则是充分利用cache中足够多的数据。

接着把mix中的数据放入一个长度为16的32位整型数组intmix,总长度也是512位。然后有一个循环,每次循环都利用了所谓的fnv和fnvhash函数,总循环为256次,目的还是尽可能多的使用cache中数据。每次循环的结果还是放在intmix中。(fnv全称Fowler–Noll–Vo,也是一种hash函数,他计算速度非常快,冲突率较低,也就是对于相似的数据也能产生大量的差别,其分散性较好,之所以用它是为了能较为随机的从数据集中取数据,避免数据间的使用率差别较大)

最后将intmix的数据写入mix中,再对mix做一次哈希运算,最后得到一个512位的数据,就是dataset中一组数据。

数据集的生成算法借鉴了Hashimoto算法,要想了解更多请参考Hashimoto算法,译文见这里

回到generateDataset中,调用generateDatasetItem之后,获取了一组数据,将其放到dateset对应位置,经过若干次上面过程后一个完整的dataset得以生成。

回到dataset的generate方法中,刚才我们分析的是没有指定数据存储路径的流程,接下来我们在看一下有数据路径的流程。

首先构建文件的最终路径,然后调用memoryMap进行映射,这一步和cache相同,如果没有错误说明原本就有,直接返回,读取的数据放入dataset相应字段中。如果读不到数据或有错,则先用generateCache生成cache数据,在使用memoryMapAndGenerate生成空文件,并进行映射然后生成dataset数据进行填充,流程和cache一样,不在叙述。只不过和前面直接生成数据不同的是,这里在数据开头写入了64位的魔法数字,和cache一样。最后尝试旧的数据文件,和cache一样的流程。

最后回到ethash的dataset方法,在这里主要是分了同步还是异步,逻辑都还是一样,只不过异步的是在一个单独的goroutine中执行的。最后生成完当代的dateset后,又根据具体需要生成了下一代数据。

hashimotoFull

func hashimotoFull(dataset []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
	lookup := func(index uint32) []uint32 {
		offset := index * hashWords
		return dataset[offset : offset+hashWords]
	}
	return hashimoto(hash, nonce, uint64(len(dataset))*4, lookup)
}

这是在全节点中节点有完整dateset数据集时的验证方法,直接调用了hashimoto方法

hashimotoLight

func hashimotoLight(size uint64, cache []uint32, hash []byte, nonce uint64) ([]byte, []byte) {
	keccak512 := makeHasher(sha3.NewLegacyKeccak512())

	lookup := func(index uint32) []uint32 {
		rawData := generateDatasetItem(cache, index, keccak512)

		data := make([]uint32, len(rawData)/4)
		for i := 0; i < len(data); i++ {
			data[i] = binary.LittleEndian.Uint32(rawData[i*4:])
		}
		return data
	}
	return hashimoto(hash, nonce, size, lookup)
}

这是没有完整dataset之后cache的轻节点的验证方法,也是直接调用了hashimoto方法,和hashimotoFull的区别是lookup的差别

hashimoto

func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) {
	rows := uint32(size / mixBytes)


	seed := make([]byte, 40)
	copy(seed, hash)
	binary.LittleEndian.PutUint64(seed[32:], nonce)

	seed = crypto.Keccak512(seed)
	seedHead := binary.LittleEndian.Uint32(seed)


	mix := make([]uint32, mixBytes/4)
	for i := 0; i < len(mix); i++ {
		mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:])
	}

	temp := make([]uint32, len(mix))

	for i := 0; i < loopAccesses; i++ {
		parent := fnv(uint32(i)^seedHead, mix[i%len(mix)]) % rows
		for j := uint32(0); j < mixBytes/hashBytes; j++ {
			copy(temp[j*hashWords:], lookup(2*parent+j))
		}
		fnvHash(mix, temp)
	}

	for i := 0; i < len(mix); i += 4 {
		mix[i/4] = fnv(fnv(fnv(mix[i], mix[i+1]), mix[i+2]), mix[i+3])
	}
	mix = mix[:len(mix)/4]

	digest := make([]byte, common.HashLength)
	for i, val := range mix {
		binary.LittleEndian.PutUint32(digest[i*4:], val)
	}
	return digest, crypto.Keccak256(append(seed, digest...))
}

首先根据给定的头部hash和nonce计算seed,头部hash是32字节,nonce是8字节,将其拼接后做一次keccak512哈希运算,得到一个长度为64的字节数组。接着创建了一个无符号32位整型数组,长度是32。然后将seed中的数据数据放入mix中,由于mix总位数是seed的两倍,所以mix先后放置了两个seed的数据内容。

继续向下,设置一个和mix大小一样的临时数组。开始了64次循环,每次循环还是fnv和fnvhash进行运算,其中循环中fnv即其后的那个循环是为了对temp进行填充,fnvhash是为了利用temp对mix进行运算,最后结果放入mix中。关键一点是在lookup中。对于节点有完整dataset时,lookup实现如下

lookup := func(index uint32) []uint32 {
		offset := index * hashWords
		return dataset[offset : offset+hashWords]
	}

很简单就是输入一个索引,然后从dateset中取一组数据(16个uint32数据,共512位)。对于没有完整dateset时,实现如下:

lookup := func(index uint32) []uint32 {
		rawData := generateDatasetItem(cache, index, keccak512)

		data := make([]uint32, len(rawData)/4)
		for i := 0; i < len(data); i++ {
			data[i] = binary.LittleEndian.Uint32(rawData[i*4:])
		}
		return data
	}

由于没有完整的数据,我们要根据索引去临时计算一个数据,调用的是generateDatasetItem方法,这个计算量以及内存占用都是比较少的。

回到hshimoto中,之所以用fnv以及lookup只是为了能随机的从dateset中取一个数据,这样对于挖矿节点,最优的方法是在内存中完整保留dataset数据副本,这就是内存困难型工作证明函数。经过64轮的fnv、lookup及fnvhash运算,最后得到的还是mix数组,接着对mix进行压缩,压缩为之前的四分之一,也就是128位,这就是区块头的MixDigest字段。最后将seed(512位)和digest拼接进行一次keccak256运算得到一个256位的值,挖矿也就是将这个值和难度值比较。

这里我们简要分析一下,从dataset中取数据的索引由parent和j构成,parent由seedhead(seed的前四个字节按小端序组成)和mix(mix由seed经过简单的操作生成),j则是0或1。而seed是由头结点hash和nonce组成。所以对于验证节点,只需一个完整的cache数据,然后在那64轮循环中,每次循环根据索引从cache中计算0dateset的某组数据。而对于挖矿节点,虽然也可以和验证节点一样每次都计算一组,但是由于每次是从整个dataset中随机选一组数据,虽然是伪随机的,但是也难以预测,所以最好的方法是能是保存整个数据集副本,这样就实现了内存困难型计算。

最后在验证过程中既要小于难度值,又要等于digest,所以也就强迫挖矿节点按照刚才流程来进行计算。因为最后和难度值比较的数只是seed和某个128位的值拼接后hash的结果,所以如果不比较digest,挖矿节点只用不断尝试nonce,然后计算出seed即可,digest就是使用dateset的一个证明。

geth生成数据集

为了方便我们测试,geth提供了生成数据集的命令,只用指定区块号即可

geth makecache 36000 ethash/
geth makedag 36000 ethash/

关于大小端

无论是cache还是dateset中的数据都是按照小端模式存储的。所谓小端模式就是在低地址中存放低位,相反低地址存放高位就是大端模式。对于0x01020304这个数,04就是低位,01就是高位。源码中也给出了测试我们的机器是大端还是小端的代码:

func isLittleEndian() bool {
	n := uint32(0x01020304)
	return *(*byte)(unsafe.Pointer(&n)) == 0x04
}

很简单,就是存一个数,然后取低地址看是否是低位即可,这里的取低地址就是通过指针转为字节类型,由于32位整型转为8位字节,只保留的低位数据。

关于为什么要使用小端模式,实际上无论大小端都可以,但是在计算数据集的时候存在多次uin32与[]byte的转换,如果不统一一下格式,在不同机器上就会有不同结果。同样源码也给出了转为小端模式的代码

func swap(buffer []byte) {
	for i := 0; i < len(buffer); i += 4 {
		binary.BigEndian.PutUint32(buffer[i:], binary.LittleEndian.Uint32(buffer[i:]))
	}
}

func (littleEndian) Uint32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}

实际就是利用了littleEndian中的方法翻一下顺序即可。

题图来自unsplash:https://unsplash.com/photos/kDJ4i77UPe0