做者:freewindnode
比原項目倉庫:git
Github地址:https://github.com/Bytom/bytomgithub
Gitee地址:https://gitee.com/BytomBlockchain/bytom算法
當咱們以bytom init --chain_id=solonet
創建比原單機節點用於本地測試時,很快會發現本身將面臨一個尷尬的問題:餘額爲0。就算咱們使用bytom node --mining
開啓挖礦,理論上因爲咱們是單機狀態,本機算力就是全網算力,應該每次都可以挖到,可是不知道爲何,在我嘗試的時候發現老是挖不到,因此打算簡單研究一下比原的挖礦流程,看看有沒有辦法能改點什麼,給本身單機多挖點BTM以方便後面的測試。安全
因此在今天我打算經過源代碼分析一下比原的挖礦流程,可是考慮到它確定會涉及到比原的核心,因此太複雜的地方我就會先跳過,那些地方時機成熟的時候會完全研究一下。app
若是咱們快速搜索一下,就能發如今比原代碼中有一個類型叫CPUMiner
,咱們圍繞着它應該就能夠了。函數
首先仍是從比原啓動開始,看看CPUMiner
是如何被啓動的。區塊鏈
下面是bytom node --mining
對應的入口函數:測試
func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() }
因爲傳入了參數node
,因此建立Node並啓動:
cmd/bytomd/commands/run_node.go#L41-L54
func runNode(cmd *cobra.Command, args []string) error { // Create & start node n := node.NewNode(config) if _, err := n.Start(); err != nil { // ... }
在建立一個Node對象的時候,也會建立CPUMiner
對象:
func NewNode(config *cfg.Config) *Node { // ... node.cpuMiner = cpuminer.NewCPUMiner(chain, accounts, txPool, newBlockCh) node.miningPool = miningpool.NewMiningPool(chain, accounts, txPool, newBlockCh) // ... return node }
這裏能夠看到建立了兩個與挖礦相關的東西,一個是NewCPUMiner
,另外一個是miningPool
。咱們先看NewCPUMiner
對應的代碼:
mining/cpuminer/cpuminer.go#L282-L293
func NewCPUMiner(c *protocol.Chain, accountManager *account.Manager, txPool *protocol.TxPool, newBlockCh chan *bc.Hash) *CPUMiner { return &CPUMiner{ chain: c, accountManager: accountManager, txPool: txPool, numWorkers: defaultNumWorkers, updateNumWorkers: make(chan struct{}), queryHashesPerSec: make(chan float64), updateHashes: make(chan uint64), newBlockCh: newBlockCh, } }
從這裏的字段能夠看到,CPUMiner在工做的時候:
chain
(表明本機持有的區塊鏈),accountManager
(管理賬戶),txPool
(交易池)numWorkers
:應該保持幾個worker在挖礦,默認值defaultNumWorkers
爲常量1
,也就是說默認只有一個worker。這對於多核cpu來講有點虧,真要挖礦的話能夠把它改大點,跟核心數相同(不過用普通電腦不太可能挖到了)updateNumWorkers
:外界若是想改變worker的數量,能夠經過向這個通道發消息實現。CPUMiner會監聽它,並按要求增減workerqueryHashesPerSec
:這個沒用上,忽略吧。我發現比原的開發人員很喜歡預先設計,有不少這樣沒用上的代碼updateHashes
: 這個沒用上,忽略newBlockCh
: 一個來自外部的通道,用來告訴外面本身成功挖到了塊,而且已經放進了本地區塊鏈,其它地方就能夠用它了(好比廣播出去)然而這裏出現的並非CPUMiner
所有的字段,僅僅是須要特地初始化的幾個。完整的在這裏:
mining/cpuminer/cpuminer.go#L29-L45
type CPUMiner struct { sync.Mutex chain *protocol.Chain accountManager *account.Manager txPool *protocol.TxPool numWorkers uint64 started bool discreteMining bool wg sync.WaitGroup workerWg sync.WaitGroup updateNumWorkers chan struct{} queryHashesPerSec chan float64 updateHashes chan uint64 speedMonitorQuit chan struct{} quit chan struct{} newBlockCh chan *bc.Hash }
能夠看到還多出了幾個:
sync.Mutex
:爲CPUMiner提供了鎖,方便在不一樣的goroutine代碼中進行同步started
:記錄miner是否啓動了discreteMining
:這個在當前代碼中沒有賦過值,永遠是false
,我以爲應該刪除。已提issue #961wg
和workerWg
:都是跟控制goroutine流程相關的speedMonitorQuit
:也沒什麼用,忽略quit
:外界能夠給這個通道發消息來通知CPUMiner退出再回到n.Start
看看cpuMiner
是什麼時候啓動的:
func (n *Node) OnStart() error { if n.miningEnable { n.cpuMiner.Start() } // ... }
因爲咱們傳入了參數--mining
,因此n.miningEnable
是true
,因而n.cpuMiner.Start
會運行:
mining/cpuminer/cpuminer.go#L188-L205
func (m *CPUMiner) Start() { m.Lock() defer m.Unlock() if m.started || m.discreteMining { return } m.quit = make(chan struct{}) m.speedMonitorQuit = make(chan struct{}) m.wg.Add(1) go m.miningWorkerController() m.started = true log.Infof("CPU miner started") }
這段代碼沒太多須要說的,主要是經過判斷m.started
保證不會重複啓動,而後把真正的工做放在了m.miningWorkerController()
中:
mining/cpuminer/cpuminer.go#L126-L125
func (m *CPUMiner) miningWorkerController() { // 1. var runningWorkers []chan struct{} launchWorkers := func(numWorkers uint64) { for i := uint64(0); i < numWorkers; i++ { quit := make(chan struct{}) runningWorkers = append(runningWorkers, quit) m.workerWg.Add(1) go m.generateBlocks(quit) } } runningWorkers = make([]chan struct{}, 0, m.numWorkers) launchWorkers(m.numWorkers) out: for { select { // 2. case <-m.updateNumWorkers: numRunning := uint64(len(runningWorkers)) if m.numWorkers == numRunning { continue } if m.numWorkers > numRunning { launchWorkers(m.numWorkers - numRunning) continue } for i := numRunning - 1; i >= m.numWorkers; i-- { close(runningWorkers[i]) runningWorkers[i] = nil runningWorkers = runningWorkers[:i] } // 3. case <-m.quit: for _, quit := range runningWorkers { close(quit) } break out } } m.workerWg.Wait() close(m.speedMonitorQuit) m.wg.Done() }
這個方法看起來代碼挺多的,可是實際上作的事情仍是比較好理清的,主要是作了三件事:
代碼比較清楚,應該不須要多講。
能夠看第1處代碼中,真正挖礦的工做是放在generateBlocks
裏的:
mining/cpuminer/cpuminer.go#L84-L119
func (m *CPUMiner) generateBlocks(quit chan struct{}) { ticker := time.NewTicker(time.Second * hashUpdateSecs) defer ticker.Stop() out: for { select { case <-quit: break out default: } // 1. block, err := mining.NewBlockTemplate(m.chain, m.txPool, m.accountManager) // ... // 2. if m.solveBlock(block, ticker, quit) { // 3. if isOrphan, err := m.chain.ProcessBlock(block); err == nil { // ... // 4. blockHash := block.Hash() m.newBlockCh <- &blockHash // ... } } } m.workerWg.Done() }
方法裏省略了一些不過重要的代碼,咱們能夠從標註的幾處看一下在作什麼:
mining.NewBlockTemplate
根據模板生成了一個block0
開始挨個計算)來爭奪對該區塊的記賬權chain.ProcessBlock(block)
嘗試把它加到本機持有的區塊鏈上newBlockCh
通道發出消息,通知外界本身挖到了新的塊咱們先看一下第1處中的mining.NewBlockTemplate
:
func NewBlockTemplate(c *protocol.Chain, txPool *protocol.TxPool, accountManager *account.Manager) (b *types.Block, err error) { // ... return b, err }
這個方法很長,可是內容都被我忽略了,緣由是它的內容過於細節,而且已經觸及到了比原的核心,因此如今大概瞭解一下就能夠了。
比原在一個Block區塊裏,有一些基本信息,好比在其頭部有前一塊的hash值、挖礦難度值、時間戳等等,主體部有各類交易記錄,以及屢次層的hash摘要。在這個方法中,主要的邏輯就是去找到這些信息而後把它們包裝成一個Block對象,而後交由後面處理。我以爲在咱們尚未深入理解比原的區塊鏈結構和規則的狀況下,看這些太細節的東西沒有太大用處,因此先忽略,等之後合適的時候再回過頭來看就簡單了。
咱們繼續向下,當由NewBlockTemplate
生成好了一個Block對象後,它會交給solveBlock
方法處理:
mining/cpuminer/cpuminer.go#L50-L75
func (m *CPUMiner) solveBlock(block *types.Block, ticker *time.Ticker, quit chan struct{}) bool { // 1. header := &block.BlockHeader seed, err := m.chain.CalcNextSeed(&header.PreviousBlockHash) // ... // 2. for i := uint64(0); i <= maxNonce; i++ { // 3. select { case <-quit: return false case <-ticker.C: if m.chain.BestBlockHeight() >= header.Height { return false } default: } // 4. header.Nonce = i headerHash := header.Hash() // 5. if difficulty.CheckProofOfWork(&headerHash, seed, header.Bits) { return true } } return false }
這個方法就是挖礦中咱們最關心的部分了:爭奪記賬權。
我把代碼分紅了4塊,依次簡單講解:
seed
,它是如何計算出來的咱們暫時不關心(比較複雜),此時只要知道它是用來檢查工做量的就能夠了maxNonce
結束。maxNonce
是一個很是大的數^uint64(0)
(即2^64 - 1
),基本上是不可能在一個區塊時間內遍歷完的。quit
通道里有新消息,被人提醒退出(多是時間到了);另外一種是本地的區塊鏈中已經收到了新的塊,且高度比較本身高,說明已經有別人搶到了。Nonce
,計算出Hash值difficulty.CheckProofOfWork
來檢查當前算出來的hash值是否知足了當前難度。若是知足就說明本身擁有了記賬權,這個塊是有效的;不然就繼續計算而後咱們再看一下第5處的difficulty.CheckProofOfWork
:
consensus/difficulty/difficulty.go#L120-L123
func CheckProofOfWork(hash, seed *bc.Hash, bits uint64) bool { compareHash := tensority.AIHash.Hash(hash, seed) return HashToBig(compareHash).Cmp(CompactToBig(bits)) <= 0 }
在這個方法裏,能夠看到出現了一個tensority.AIHash
,這是比原獨有的人工智能友好的工做量算法,相關論文的下載地址:https://github.com/Bytom/bytom/wiki/download/tensority-v1.2.pdf,有興趣的同窗能夠去看看。因爲這個算法的難度確定超出了本文的預期,因此就不研究它了。在之後,若是有機會有條件的話,也許我會試着理解一下(不要期待~)
從這個方法裏能夠看出,它是調用了tensority.AIHash
中的相關方法進判斷當前計算出來的hash是否知足難度要求。
在本文的開始,咱們說過但願能找到一種方法修改比原的代碼,讓咱們在solonet
模式下,能夠正常挖礦,獲得BTM用於測試。看到這個方法的時候,我以爲已經找到了,咱們只須要修改一下讓它永遠返回true
便可:
func CheckProofOfWork(hash, seed *bc.Hash, bits uint64) bool { compareHash := tensority.AIHash.Hash(hash, seed) return HashToBig(compareHash).Cmp(CompactToBig(bits)) <= 0 || true }
這裏也許會讓人以爲有點奇怪,爲何要在最後的地方加上|| true
,而不是在前面直接返回true
呢?這是由於,若是直接返回true
,可能使得程序中關於時間戳檢查的地方出現問題,出現以下的錯誤:
time="2018-05-17T12:10:14+08:00" level=error msg="Miner fail on ProcessBlock block, timestamp is not in the valid range: invalid block" height=32
緣由還未深究,多是由於本來的代碼是須要消耗一些時間的,正好使得檢查經過。若是直接返回true
就太快了,反而使檢查經過不了。不過我感受這裏是有一點問題的,留待之後再研究。
這樣修改完之後,再從新編譯並啓動比原節點,每一個塊都能挖到了,差很少一秒一個塊(一會兒變成大富豪了:)
咱們此時該回到generateBlocks
方法中的第3處,即:
mining/cpuminer/cpuminer.go#L84-L119
func (m *CPUMiner) generateBlocks(quit chan struct{}) { //... if m.solveBlock(block, ticker, quit) { // 3. if isOrphan, err := m.chain.ProcessBlock(block); err == nil { // ... // 4. blockHash := block.Hash() m.newBlockCh <- &blockHash // ... } } } m.workerWg.Done() }
m.chain.ProcessBlock
把剛纔成功拿到記賬權的塊向本地區塊鏈上添加:
func (c *Chain) ProcessBlock(block *types.Block) (bool, error) { reply := make(chan processBlockResponse, 1) c.processBlockCh <- &processBlockMsg{block: block, reply: reply} response := <-reply return response.isOrphan, response.err }
能夠看到這裏其實是把這個工做甩出去了,由於它把要處理的塊放進了Chain.processBlockCh
這個通道里,同時傳過去的還有一個用於對方回覆的通道reply
。而後監聽reply
等消息就能夠了。
那麼誰將會處理c.processBlockCh
裏的內容呢?固然是由Chain
,只不過這裏就屬於比原核心了,咱們留等之後再詳細研究,今天就先跳過。
若是處理完沒有出錯,就進入到了第4塊,把這個block的hash放在newBlockCh
通道里。這個newBlockCh
是由外面傳入的,不少地方都會用到。當它裏面有新的數據時,就說明本機挖到了新塊(而且已經添加到了本機的區塊鏈上),其它的地方就可使用它進行別的操做(好比廣播出去)
那麼到這裏,咱們今天的問題就算解決了,留下了不少坑,之後專門填。