剝開比原看代碼14:比原的挖礦流程是什麼樣的?

來源:互聯網
上載者:User

作者:freewind

比原項目倉庫:

Github地址:https://github.com/Bytom/bytom

Gitee地址:https://gitee.com/BytomBlockc...

當我們以bytom init --chain_id=solonet建立比原單機節點用於本地測試時,很快會發現自己將面臨一個尷尬的問題:餘額為0。就算我們使用bytom node --mining開啟挖礦,理論上由於我們是單機狀態,本機算力就是全網算力,應該每次都能夠挖到,但是不知道為什麼,在我嘗試的時候發現總是挖不到,所以打算簡單研究一下比原的挖礦流程,看看有沒有辦法能改點什麼,給自己單機多挖點BTM以方便後面的測試。

所以在今天我打算通過原始碼分析一下比原的挖礦流程,但是考慮到它肯定會涉及到比原的核心,所以太複雜的地方我就會先跳過,那些地方時機成熟的時候會徹底研究一下。

如果我們快速搜尋一下,就能發現在比原代碼中有一個類型叫CPUMiner,我們圍繞著它應該就可以了。

首先還是從比原啟動開始,看看CPUMiner是如何被啟動的。

下面是bytom node --mining對應的入口函數:

cmd/bytomd/main.go#L54-L57

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對象:

node/node.go#L59-L142

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會監聽它,並按要求增減worker
  • queryHashesPerSec:這個沒用上,忽略吧。我發現比原的開發人員很喜歡預先設計,有很多這樣沒用上的代碼
  • 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 #961
  • wgworkerWg:都是跟控制goroutine流程相關的
  • speedMonitorQuit:也沒什麼用,忽略
  • quit:外界可以給這個通道發訊息來通知CPUMiner退出

再回到n.Start看看cpuMiner是何時啟動的:

node/node.go#L169-L180

func (n *Node) OnStart() error {    if n.miningEnable {        n.cpuMiner.Start()    }    // ...}

由於我們傳入了參數--mining,所以n.miningEnabletrue,於是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. 第1處代碼是按指定的worker數量啟動挖礦常式
  2. 第2處是監聽應該保持的worker數量並增減
  3. 第3處在被知關閉的時候安全關閉

代碼比較清楚,應該不需要多講。

可以看第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()}

方法裡省略了一些不太重要的代碼,我們可以從標註的幾處看一下在做什麼:

  1. 第1處通過mining.NewBlockTemplate根據模板產生了一個block
  2. 第2處是以暴力方式(從0開始挨個計算)來爭奪對該區塊的記帳權
  3. 第3處是通過chain.ProcessBlock(block)嘗試把它加到本機持有的區塊鏈上
  4. 第4處是向newBlockCh通道發出訊息,通知外界自己挖到了新的塊

mining.NewBlockTemplate

我們先看一下第1處中的mining.NewBlockTemplate

mining/mining.go#L67-L154

func NewBlockTemplate(c *protocol.Chain, txPool *protocol.TxPool, accountManager *account.Manager) (b *types.Block, err error) {    // ...    return b, err}

這個方法很長,但是內容都被我忽略了,原因是它的內容過於細節,並且已經觸及到了比原的核心,所以現在大概瞭解一下就可以了。

比原在一個Block區塊裡,有一些基本資料,比如在其頭部有前一塊的hash值、挖礦難度值、時間戳記等等,主體部有各種交易記錄,以及多次層的hash摘要。在這個方法中,主要的邏輯就是去找到這些資訊然後把它們封裝成一個Block對象,然後交由後面處理。我覺得在我們還沒有深刻理解比原的區塊鏈結構和規則的情況下,看這些太細節的東西沒有太大用處,所以先忽略,等以後合適的時候再回過頭來看就簡單了。

m.solveBlock

我們繼續向下,當由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塊,依次簡單講解:

  1. 第1處是從本地區塊鏈中找到新產生的區塊指定的父區塊,並由它計算出來seed,它是如何計算出來的我們暫時不關心(比較複雜),此時只要知道它是用來檢查工作量的就可以了
  2. 第2處是使用暴力方式來計算目標值,用於爭奪記帳權。為什麼說是暴力方式?因為挖礦的演算法保證了想解開難題,沒有比從0開始一個個計算更快的辦法,所以這裡從0開始依次嘗試,直到maxNonce結束。maxNonce是一個非常大的數^uint64(0)(即2^64 - 1),基本上是不可能在一個區塊時間內遍曆完的。
  3. 第3處是在每次迴圈中進行計算之前,都看一看是否需要退出。在兩種情況下應該退出,一是quit通道裡有新訊息,被人提醒退出(可能是時間到了);另一種是本地的區塊鏈中已經收到了新的塊,且高度比較自己高,說明已經有別人搶到了。
  4. 第4處是把當前迴圈的數字當作Nonce,計算出Hash值
  5. 第5處是調用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/byto...,有興趣的同學可以去看看。由於這個演算法的難度肯定超出了本文的預期,所以就不研究它了。在以後,如果有機會有條件的話,也許我會試著理解一下(不要期待~)

從這個方法裡可以看出,它是調用了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就太快了,反而使檢查通過不了。不過我感覺這裡是有一點問題的,留待以後再研究。

這樣修改完以後,再重新編譯並啟動比原節點,每個塊都能挖到了,差不多一秒一個塊(一下子變成大富豪了:)

m.chain.ProcessBlock

我們此時該回到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把剛才成功拿到記帳權的塊向本地區塊鏈上添加:

protocol/block.go#L191-L196

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是由外面傳入的,很多地方都會用到。當它裡面有新的資料時,就說明本機挖到了新塊(並且已經添加到了原生區塊鏈上),其它的地方就可以使用它進行別的操作(比如廣播出去)

那麼到這裡,我們今天的問題就算解決了,留下了很多坑,以後專門填。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.