以太坊源码解读(17)ethash挖矿原理
POW的本质是基于算力解决一个数学上困难的问题,解决问题关键点是除了暴力枚举,没有任何办法可以找到我们所需要的nonce值,但对于验证输出的结果是非常简单容易的。
经典的比特币POW的算法原理是对block的header加上循环更新的nonce去进行hash运算,运算的target是hash值的前n位为0。这个计算只能通过暴力枚举来进行,验证也很容易,只要使用最终的nonce代入header按照之前的算法验证即可。
以太坊采用的ethash算法与比特币不同,但基本过程类似,都是找到一个nonce值输入到算法中,得到的结果低于一个基于特定困难值的阈值。
RAND(h, n) <= M/d RAND是一系列复杂的运算 h: header 不包含nonce n: nonce M: 2^256 d: 难度值,该难度值基于父区块的时间戳和难度而得到
上面的公式意思是我们用header和nonce经过复杂的计算,如果得到的结果小于或等于M/d,该nonce就是可用的,意味着挖矿成功。
上图是ethash算法在以太坊源码中的实现,我们一个个看。
一、Seal方法
Seal主要的任务是: 1、获得种子seed; 2、基于seed获得Rand对象,rand值将作为初始化nonce进行挖矿; 3、启动mine函数,执行挖矿。
ethash.lock.Lock() // 挖矿的线程 threads := ethash.threads if ethash.rand == nil { // 获得种子seed seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64)) if err != nil { ethash.lock.Unlock() return err } // 执行成功,生成rand对象,并赋值 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 } 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) // Seal核心工作 }(i, uint64(ethash.rand.Int63())) }
二、mine函数
这个函数的主要任务是: 1、取出block的header; 2、取出没有nonce时的区块hash; 3、设置目标target,即上面公式中的M/td; 4、获得dataset,即数据集 5、开启无限循环,计算每一轮nonce值的POW结果,知道获得正确解。
补充:DAG和epoch
1、上面的dataset就来自内存中的一组数据或者硬盘里的DAG。 2、DAG是有向无环图,以太坊的DAG是基于区块高度生成的。 3、以太坊中每3万个块会生成一代DAG,这一代就成称为一个epoch。 4、挖矿的时候需要从DAG中随机选取dataset,所以挖矿工作只能在现世DAG创建以后才能开始。
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 ) 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: ... // 计算当前nonce的POW值 digest, result := hashimotoFull(dataset.dataset, hash, nonce) // 真正执行POW计算的函数 if new(big.Int).SetBytes(result).Cmp(target) <= 0 { // 找到nonce后重构header,更新nonce和hash 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和hashimoto
这两个函数是ethash算法的核心,hashimotoFull中定义了lookup函数,用于从dataset中找到数据子集:
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) }
hashimoto则是真正对nonce的运算:
func hashimoto(hash []byte, nonce uint64, size uint64, lookup func(index uint32) []uint32) ([]byte, []byte) { // 计算数据集的理论行数 rows := uint32(size / mixBytes) // 将header+nonce合并成一个40字节的seed seed := make([]byte, 40) copy(seed, hash) binary.LittleEndian.PutUint64(seed[32:], nonce) // 经历一遍Keccak512加密,生成64字节的seed seed = crypto.Keccak512(seed) // 从seed中获取seedHead用于后面数据集的lookup seedHead := binary.LittleEndian.Uint32(seed) // 开始重复混合seed mix := make([]uint32, mixBytes/4) // mixBytes常量= 128,mix的长度为32,元素为uint32,是32位,对应为4字节大小。所以mix总共大小为4*32=128字节大小 for i := 0; i < len(mix); i++ { mix[i] = binary.LittleEndian.Uint32(seed[i%16*4:]) // 共循环32次,前16和后16位的元素值相同 } // 再与随机的dataset做混合 temp := make([]uint32, len(mix)) // 新建一个temp,结构与mix相同 for i := 0; i < loopAccesses; i++ { // loopAccesses是常量 = 64 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)) // 通过seed生成parent,再基于parent去源数据中查找数据子集,最后复制到temp中 } fnvHash(mix, temp) // 对mix和temp进行fnvHash处理 } // 压缩mix,先对mix进行一次大混淆,然后去最前面8个有效位置的数据 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,并基于seed+digest得到我们想要的值,如果这个值小于target,就结束循环 // 这里的digest就是我们要的区块最终的hash digest := make([]byte, common.HashLength) for i, val := range mix { binary.LittleEndian.PutUint32(digest[i*4:], val) } return digest, crypto.Keccak256(append(seed, digest...)) }
FNV hash 算法
func fnv(a, b uint32) uint32 { return a*0x01000193 ^ b } func fnvHash(mix []uint32, data []uint32) { for i := 0; i < len(mix); i++ { mix[i] = mix[i]*0x01000193 ^ data[i] } }
0x01000193是FNV hash算法的一个hash质数(Prime number,又叫素数,只能被1和其本身整除),哈希算法会基于一个常数来做散列操作。0x01000193是FNV针对32 bit数据的散列质数。
下一章:以太坊源码解读(18)以太坊交易执行流程
之前分析挖矿模块,miner从TxPool拿来的交易,交给worker对象。后者要调用commitTransaction在本地执行交易,生成receipt,更改世界状态,打包成挖矿的block最后递 ...