以太坊源码之--Pow挖矿源码剖析

PoW挖矿

代码基于在学习以太坊挖矿以前先来了解几个相关的数据结构作为铺垫:

数据结构1:
type Miner struct {mux *event.TypeMux // 事件锁,已被feed.mu.lock替代worker *worker // 干活的人coinbase common.Address // 结点地址mining   int32 // 代表挖矿进行中的状态eth      Backend // Backend对象,Backend是一个自定义接口封装了所有挖矿所需方法。engine   consensus.Engine // 共识引擎canStart    int32 // 是否能够开始挖矿操作shouldStart int32 // 同步以后是否应该开始挖矿
}
//实际的工人
type worker struct {config *params.ChainConfig //链配置engine consensus.Engine //一致性引擎,ethash或者clique poa(这个目前只在测试网测试)mu sync.Mutex //锁// update loopmux    *event.TypeMux events *event.TypeMuxSubscriptionwg     sync.WaitGroupagents map[Agent]struct{} //agent 是挖矿代理,实际执行挖矿的代理,目前以太坊默认注册cpuagent,矿池应该是自己实现了自己的agent注册到这里recv   chan *Result //这是一个结果通道,挖矿完成以后将结果推送到此通道eth     Backend //以太坊定义chain   *core.BlockChainproc    core.ValidatorchainDb ethdb.Databasecoinbase common.Address //基础帐户地址extra    []bytecurrentMu sync.Mutexcurrent   *Work  //实际将每一个区块作为一个工作work推给agent进行挖矿uncleMu        sync.MutexpossibleUncles map[common.Hash]*types.Block //可能的数块txQueueMu sync.MutextxQueue   map[common.Hash]*types.Transactionunconfirmed *unconfirmedBlocks // set of locally mined blocks pending canonicalness confirmations// atomic status countersmining int32atWork int32fullValidation bool
}
//agent接口如下,实现以下接口的 就可作为一个agent
type Agent interface {Work() chan<- *WorkSetReturnCh(chan<- *Result)Stop()Start()GetHashRate() int64
}

上面记录了要开始学习挖矿的基础结构,其实还有block的header数据结构需要很熟悉,方便后续分析

在backend.go里面New一个ethereum时候,调用了如下语句:

//先单独看如下两句:engine: CreateConsensusEngine(ctx, config, chainConfig, chainDb),engine := ethash.New(ctx.ResolvePath(config.EthashCacheDir), config.EthashCachesInMem, config.EthashCachesOnDisk,config.EthashDatasetDir, config.EthashDatasetsInMem, config.EthashDatasetsOnDisk)//从上面可以看出来geth启动时候默认的共识引擎为ethash//下面语句开始New一个miner了
eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)# go-ethereum/miner/miner.go
func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *Miner {//开始创建miner结构体miner := &Miner{eth:      eth,mux:      mux,engine:   engine,worker:   newWorker(config, engine, common.Address{}, eth, mux), //创建了一个工人canStart: 1,}//注册代理miner.Register(NewCpuAgent(eth.BlockChain(), engine))go miner.update()return miner
}
# go-ethereum/miner/worker.go
func newWorker(config *params.ChainConfig, engine consensus.Engine, coinbase common.Address, eth Backend, mux *event.TypeMux) *worker {worker := &worker{config:         config,engine:         engine,eth:            eth,mux:            mux,chainDb:        eth.ChainDb(),recv:           make(chan *Result, resultQueueSize), //结果通道chain:          eth.BlockChain(),proc:           eth.BlockChain().Validator(),possibleUncles: make(map[common.Hash]*types.Block),coinbase:       coinbase,txQueue:        make(map[common.Hash]*types.Transaction),agents:         make(map[Agent]struct{}),unconfirmed:    newUnconfirmedBlocks(eth.BlockChain(), 5),fullValidation: false,}//worker开始订阅相关三个事件worker.events = worker.mux.Subscribe(core.ChainHeadEvent{}, core.ChainSideEvent{}, core.TxPreEvent{})//先来分析一下update()函数go worker.update()go worker.wait()worker.commitNewWork()return worker
}func (self *worker) update() {//遍历自己的事件通道for event := range self.events.Chan() {// A real event arrived, process interesting contentswitch ev := event.Data.(type) {//如果是新区块加入事件,那么工人开始挖下一个区块case core.ChainHeadEvent:self.commitNewWork()//如果是区块旁支事件(俗称的叔块)case core.ChainSideEvent:self.uncleMu.Lock()//在map结构里添加可能的叔块self.possibleUncles[ev.Block.Hash()] = ev.Blockself.uncleMu.Unlock()case core.TxPreEvent:// Apply transaction to the pending state if we're not miningif atomic.LoadInt32(&self.mining) == 0 {self.currentMu.Lock()acc, _ := types.Sender(self.current.signer, ev.Tx)txs := map[common.Address]types.Transactions{acc: {ev.Tx}}txset := types.NewTransactionsByPriceAndNonce(txs)self.current.commitTransactions(self.mux, txset, self.chain, self.coinbase)self.currentMu.Unlock()}}}
}

​ 介绍下ChainHeadEvent,ChainSideEvent,TxPreEvent几个事件,每个事件会触发worker不同的反应。ChainHeadEvent是指区块链中已经加入了一个新的区块作为整个链的链头,这时worker的回应是立即开始准备挖掘下一个新区块(也是够忙的);ChainSideEvent指区块链中加入了一个新区块作为当前链头的旁支,worker会把这个区块收纳进possibleUncles[]数组,作为下一个挖掘新区块可能的Uncle之一;TxPreEvent是TxPool对象发出的,指的是一个新的交易tx被加入了TxPool,这时如果worker没有处于挖掘中,那么就去执行这个tx,并把它收纳进Work.txs数组,为下次挖掘新区块备用。

​ 需要稍稍注意的是,ChainHeadEvent并不一定是外部源发出。由于worker对象有个成员变量chain(eth.BlockChain),所以当worker自己完成挖掘一个新区块,并把它写入数据库,加进区块链里成为新的链头时,worker自己也可以调用chain发出一个ChainHeadEvent,从而被worker.update()函数监听到,进入下一次区块挖掘

commitNewWork()在另外一篇文章中已经单独分析,接下来主要分析worker.wait()
func (self *worker) wait() {for {mustCommitNewWork := true//worker.wait会一直阻塞在这里,等待有新的区块经过seal后被推送到recv通道for result := range self.recv {atomic.AddInt32(&self.atWork, -1)if result == nil {continue}block := result.Blockwork := result.Work//是否是全验证模式if self.fullValidation {//将新区块插入到主链if _, err := self.chain.InsertChain(types.Blocks{block}); err != nil {log.Error("Mined invalid block", "err", err)continue}//发送新挖出区块事件,会通知当前的miner和protocolManager和其他订阅者go self.mux.Post(core.NewMinedBlockEvent{Block: block})} else {work.state.CommitTo(self.chainDb, self.config.IsEIP158(block.Number()))stat, err := self.chain.WriteBlock(block)if err != nil {log.Error("Failed writing block to chain", "err", err)continue}// update block hash since it is now available and not when the receipt/log of individual transactions were created//遍历当前所挖区块的所有txreceipts,给log的blockhash字段填充值for _, r := range work.receipts {for _, l := range r.Logs {l.BlockHash = block.Hash()}}for _, log := range work.state.Logs() {log.BlockHash = block.Hash()}// check if canon block and write transactionsif stat == core.CanonStatTy {// This puts transactions in a extra db for rpccore.WriteTransactions(self.chainDb, block)// store the receiptscore.WriteReceipts(self.chainDb, work.receipts)// Write map map bloom filterscore.WriteMipmapBloom(self.chainDb, block.NumberU64(), work.receipts)// implicit by posting ChainHeadEventmustCommitNewWork = false}//广播相关事件出去// broadcast before waiting for validationgo func(block *types.Block, logs []*types.Log, receipts []*types.Receipt) {self.mux.Post(core.NewMinedBlockEvent{Block: block})self.mux.Post(core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})if stat == core.CanonStatTy {self.mux.Post(core.ChainHeadEvent{Block: block})self.mux.Post(logs)}if err := core.WriteBlockReceipts(self.chainDb, block.Hash(), block.NumberU64(), receipts); err != nil {log.Warn("Failed writing block receipts", "err", err)}}(block, work.state.Logs(), work.receipts)}//将区块号和区块hash插入未确认表// Insert the block into the set of pending ones to wait for confirmationsself.unconfirmed.Insert(block.NumberU64(), block.Hash())//如果再挖出一个新块必须开启下一次挖掘工作,那么执行新的挖矿工作if mustCommitNewWork {self.commitNewWork()}}}
}
接下来回到Miner.New下面继续看miner.Register(NewCpuAgent(eth.BlockChain(), engine))func (self *Miner) Register(agent Agent) {//如果自己开启了挖矿if self.Mining() {//那么启动代理agent.Start()}//在工人处注册此代理self.worker.register(agent)
}
# go-ethereum/miner/agent.go
func (self *CpuAgent) Start() {//类似java的CASif !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {return // agent already started}//自己开启一个携程进行工作go self.update()
}func (self *CpuAgent) update() {
out://死循环工作,以太坊常常做的事情,哈哈for {select {//遍历workCh,查看是否有work提交,前面分析commitNewWork()时候讲解到会将一个区块信息填充执行Finalize后提交到此通道case work := <-self.workCh:self.mu.Lock()if self.quitCurrentOp != nil {close(self.quitCurrentOp)}self.quitCurrentOp = make(chan struct{})//开启协程执行挖矿操作,核心操作go self.mine(work, self.quitCurrentOp)self.mu.Unlock()case <-self.stop:self.mu.Lock()if self.quitCurrentOp != nil {close(self.quitCurrentOp)self.quitCurrentOp = nil}self.mu.Unlock()break out}}............................
}func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) {//实际调用ethash.Seal进行挖矿if result, err := self.engine.Seal(self.chain, work.Block, stop); result != nil {log.Info("Successfully sealed new block", "number", result.Number(), "hash", result.Hash())//如果挖矿有结果则推送到returnCh,交给worker.wait()处理self.returnCh <- &Result{work, result}} else {if err != nil {log.Warn("Block sealing failed", "err", err)}self.returnCh <- nil}
}
# go-ethereum/consensus/ethhash/sealer.go
// Seal implements consensus.Engine, attempting to find a nonce that satisfies
// the block's difficulty requirements.
func (ethash *Ethash) Seal(chain consensus.ChainReader, block *types.Block, stop <-chan struct{}) (*types.Block, error) {//一种测试模式// If we're running a fake PoW, simply return a 0 nonce immediatelyif ethash.fakeMode {header := block.Header()header.Nonce, header.MixDigest = types.BlockNonce{}, common.Hash{}return block.WithSeal(header), nil}//一种测试模式// If we're running a shared PoW, delegate sealing to itif ethash.shared != nil {return ethash.shared.Seal(chain, block, stop)}// Create a runner and the multiple search threads it directs//中断通道abort := make(chan struct{})//结果通道found := make(chan *types.Block)ethash.lock.Lock()threads := ethash.threads//开始为区块中的nonce做准备if ethash.rand == nil {//使用"crypto/rand"下的函数生成随机数种子seed, err := crand.Int(crand.Reader, big.NewInt(math.MaxInt64))if err != nil {ethash.lock.Unlock()return nil, err}//使用随机数种子生成随机数赋值给ethash.hashethash.rand = rand.New(rand.NewSource(seed.Int64()))}ethash.lock.Unlock()//如果挖矿线程数为0则将线程数赋值为cpu个数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//开启数个线程同时执行挖矿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) //调用ethash进行实际挖矿}(i, uint64(ethash.rand.Int63())) //将上面生成的随机数赋值给nonce做初始值}//一直在此等着上面有如下几种结果之一出现// Wait until sealing is terminated or a nonce is foundvar result *types.Blockselect {case <-stop:// Outside abort, stop all miner threadsclose(abort)case result = <-found:// One of the threads found a block, abort all othersclose(abort)case <-ethash.update:// Thread count was changed on user request, restartclose(abort)pend.Wait()return ethash.Seal(chain, block, stop)}// Wait for all miners to terminate and return the blockpend.Wait()return result, nil
}
//实际的挖矿函数
// 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) mine(block *types.Block, id int, seed uint64, abort chan struct{}, found chan *types.Block) {// Extract some data from the headervar (header = block.Header() hash   = header.HashNoNonce().Bytes() //获取commitNewWork提交来的区块头无nonce的hashtarget = new(big.Int).Div(maxUint256, header.Difficulty) //targetnumber  = header.Number.Uint64()dataset = ethash.dataset(number) //根据区块号获取数据集,数据集又是另一个话题)// Start generating random nonces until we abort or find a good onevar (//尝试次数attempts = int64(0)//noncenonce    = seed)logger := log.New("miner", id)logger.Trace("Started ethash search for new nonces", "seed", seed)for {select {case <-abort:// Mining terminated, update stats and abortlogger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)ethash.hashrate.Mark(attempts)returndefault:// We don't have to update hash rate on every nonce, so update after after 2^X noncesattempts++//当尝试测试达到2的15次方时候,做一次标记,并从头开始if (attempts % (1 << 15)) == 0 {ethash.hashrate.Mark(attempts)attempts = 0}// Compute the PoW value of this nonce//下面就是主要计算符合挖矿条件的函数digest, result := hashimotoFull(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)//给区块头填充nonce值header.Nonce = types.EncodeNonce(nonce)//给区块mixHash字段填充值,为了验证做准备header.MixDigest = common.BytesToHash(digest)// Seal and return a block (if still needed)select {//将组装的block推送到found通道,其实最终交由worker.wait()处理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)}return}//nonce在初始化以后每次都会自增一后重新尝试nonce++}}
}
//最后分析一下Miner.New的最后一个方法
// update keeps track of the downloader events. Please be aware that this is a one shot type of update loop.
// It's entered once and as soon as `Done` or `Failed` has been broadcasted the events are unregistered and
// the loop is exited. This to prevent a major security vuln where external parties can DOS you with blocks
// and halt your mining operation for as long as the DOS continues.
func (self *Miner) update() {//订阅download下的事件events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
out:for ev := range events.Chan() {switch ev.Data.(type) {//如果downloader开始事件case downloader.StartEvent://一个downloader开始,意味着需要去别的节点主动下载一些数据,那么理论上跟本地挖矿是冲突的,所以当一个downloader开始时候  将停止自己的挖矿atomic.StoreInt32(&self.canStart, 0)if self.Mining() {self.Stop()atomic.StoreInt32(&self.shouldStart, 1)log.Info("Mining aborted due to sync")}//如果downloader 完成事件,失败事件  都会在此开启挖矿case downloader.DoneEvent, downloader.FailedEvent:shouldStart := atomic.LoadInt32(&self.shouldStart) == 1atomic.StoreInt32(&self.canStart, 1)atomic.StoreInt32(&self.shouldStart, 0)if shouldStart {self.Start(self.coinbase)}// unsubscribe. we're only interested in this event onceevents.Unsubscribe()// stop immediately and ignore all further pending eventsbreak out}}
}

简单总结一下:

挖矿简单来讲就是找到符合如下公式的一个nonce

​ rand(n,h)=M/D (n:nonce , h:headerHashNoNonce, M:uint256Max,D:Diffculty)

​ 首先挖矿结构体(Miner)组合了一个worker ,在New Miner时候会先去NewWorker,NewWorker的时候会订阅相关事件,并且开启两个主要线程 go worker.update()go worker.wait() ,前一个主要遍历事件做出相应动作 ,commitNewWork()将一个新的区块的header填充好,交易执行完成,奖励发送到相应矿工和挖出叔块的地址后将结果封装为一个work提交给注册的实际的“矿工”,例如CpuAgent进行“挖矿”操作,CpuAgent拿到work以后开始调用共识引擎(ethash)的Seal(对外,mine对内)进行共识计算,计算找到一个合适的nonce 时候将结果提交到一个结果通道,此时worker.wait()开始拿到结果进行实际插入到blockchain,并进行事件广播。

​ 以上就是挖矿部分的流程分析,具体的计算函数hashimotoFull()本次不做展开,待下次记录。

此文档初稿:2019.06.03 17:00 随后迭代修改


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部