以太坊源碼之--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 loop
	mux    *event.TypeMux 
	events *event.TypeMuxSubscription
	wg     sync.WaitGroup

	agents map[Agent]struct{} //agent 是挖礦代理,實際執行挖礦的代理,目前以太坊默認註冊cpuagent,礦池應該是自己實現了自己的agent註冊到這裏
	recv   chan *Result //這是一個結果通道,挖礦完成以後將結果推送到此通道

	eth     Backend //以太坊定義
	chain   *core.BlockChain
	proc    core.Validator
	chainDb ethdb.Database

	coinbase common.Address //基礎帳戶地址
	extra    []byte

	currentMu sync.Mutex
	current   *Work  //實際將每一個區塊作爲一個工作work推給agent進行挖礦

	uncleMu        sync.Mutex
	possibleUncles map[common.Hash]*types.Block //可能的數塊

	txQueueMu sync.Mutex
	txQueue   map[common.Hash]*types.Transaction

	unconfirmed *unconfirmedBlocks // set of locally mined blocks pending canonicalness confirmations

	// atomic status counters
	mining int32
	atWork int32

	fullValidation bool
}
//agent接口如下,實現以下接口的 就可作爲一個agent
type Agent interface {
	Work() chan<- *Work
	SetReturnCh(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 content
		switch ev := event.Data.(type) {
		//如果是新區塊加入事件,那麼工人開始挖下一個區塊
		case core.ChainHeadEvent:
			self.commitNewWork()
		//如果是區塊旁支事件(俗稱的叔塊)
		case core.ChainSideEvent:
			self.uncleMu.Lock()
			//在map結構裏添加可能的叔塊
			self.possibleUncles[ev.Block.Hash()] = ev.Block
			self.uncleMu.Unlock()
		case core.TxPreEvent:
			// Apply transaction to the pending state if we're not mining
			if 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.Block
			work := 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 transactions
				if stat == core.CanonStatTy {
					// This puts transactions in a extra db for rpc
					core.WriteTransactions(self.chainDb, block)
					// store the receipts
					core.WriteReceipts(self.chainDb, work.receipts)
					// Write map map bloom filters
					core.WriteMipmapBloom(self.chainDb, block.NumberU64(), work.receipts)
					// implicit by posting ChainHeadEvent
					mustCommitNewWork = false
				}
				//廣播相關事件出去
				// broadcast before waiting for validation
				go 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 confirmations
			self.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的CAS
	if !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 immediately
	if 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 it
	if 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.hash
		ethash.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 found
	var result *types.Block
	select {
	case <-stop:
		// Outside abort, stop all miner threads
		close(abort)
	case result = <-found:
		// One of the threads found a block, abort all others
		close(abort)
	case <-ethash.update:
		// Thread count was changed on user request, restart
		close(abort)
		pend.Wait()
		return ethash.Seal(chain, block, stop)
	}
	// Wait for all miners to terminate and return the block
	pend.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 header
	var (
		header = block.Header() 
		hash   = header.HashNoNonce().Bytes() //獲取commitNewWork提交來的區塊頭無nonce的hash
		target = new(big.Int).Div(maxUint256, header.Difficulty) //target

		number  = header.Number.Uint64()
		dataset = ethash.dataset(number) //根據區塊號獲取數據集,數據集又是另一個話題
	)
	// Start generating random nonces until we abort or find a good one
	var (
		//嘗試次數
		attempts = int64(0)
		//nonce
		nonce    = 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 abort
			logger.Trace("Ethash nonce search aborted", "attempts", nonce-seed)
			ethash.hashrate.Mark(attempts)
			return

		default:
			// We don't have to update hash rate on every nonce, so update after after 2^X nonces
			attempts++
			//當嘗試測試達到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) == 1

			atomic.StoreInt32(&self.canStart, 1)
			atomic.StoreInt32(&self.shouldStart, 0)
			if shouldStart {
				self.Start(self.coinbase)
			}
			// unsubscribe. we're only interested in this event once
			events.Unsubscribe()
			// stop immediately and ignore all further pending events
			break 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 隨後迭代修改

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章