摘要:所以在今天我打算通過源代碼分析一下比原的挖礦流程,但是考慮到它肯定會涉及到比原的核心,所以太復(fù)雜的地方我就會先跳過,那些地方時機(jī)成熟的時候會徹底研究一下。
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
當(dāng)我們以bytom init --chain_id=solonet建立比原單機(jī)節(jié)點用于本地測試時,很快會發(fā)現(xiàn)自己將面臨一個尷尬的問題:余額為0。就算我們使用bytom node --mining開啟挖礦,理論上由于我們是單機(jī)狀態(tài),本機(jī)算力就是全網(wǎng)算力,應(yīng)該每次都能夠挖到,但是不知道為什么,在我嘗試的時候發(fā)現(xiàn)總是挖不到,所以打算簡單研究一下比原的挖礦流程,看看有沒有辦法能改點什么,給自己單機(jī)多挖點BTM以方便后面的測試。
所以在今天我打算通過源代碼分析一下比原的挖礦流程,但是考慮到它肯定會涉及到比原的核心,所以太復(fù)雜的地方我就會先跳過,那些地方時機(jī)成熟的時候會徹底研究一下。
如果我們快速搜索一下,就能發(fā)現(xiàn)在比原代碼中有一個類型叫CPUMiner,我們圍繞著它應(yīng)該就可以了。
首先還是從比原啟動開始,看看CPUMiner是如何被啟動的。
下面是bytom node --mining對應(yīng)的入口函數(shù):
cmd/bytomd/main.go#L54-L57
func main() { cmd := cli.PrepareBaseCmd(commands.RootCmd, "TM", os.ExpandEnv(config.DefaultDataDir())) cmd.Execute() }
由于傳入了參數(shù)node,所以創(chuàng)建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 { // ... }
在創(chuàng)建一個Node對象的時候,也會創(chuàng)建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 }
這里可以看到創(chuàng)建了兩個與挖礦相關(guān)的東西,一個是NewCPUMiner,另一個是miningPool。我們先看NewCPUMiner對應(yīng)的代碼:
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(代表本機(jī)持有的區(qū)塊鏈),accountManager(管理帳戶),txPool(交易池)
numWorkers:應(yīng)該保持幾個worker在挖礦,默認(rèn)值defaultNumWorkers為常量1,也就是說默認(rèn)只有一個worker。這對于多核cpu來說有點虧,真要挖礦的話可以把它改大點,跟核心數(shù)相同(不過用普通電腦不太可能挖到了)
updateNumWorkers:外界如果想改變worker的數(shù)量,可以通過向這個通道發(fā)消息實現(xiàn)。CPUMiner會監(jiān)聽它,并按要求增減worker
queryHashesPerSec:這個沒用上,忽略吧。我發(fā)現(xiàn)比原的開發(fā)人員很喜歡預(yù)先設(shè)計,有很多這樣沒用上的代碼
updateHashes: 這個沒用上,忽略
newBlockCh: 一個來自外部的通道,用來告訴外面自己成功挖到了塊,并且已經(jīng)放進(jìn)了本地區(qū)塊鏈,其它地方就可以用它了(比如廣播出去)
然而這里出現(xiàn)的并不是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代碼中進(jìn)行同步
started:記錄miner是否啟動了
discreteMining:這個在當(dāng)前代碼中沒有賦過值,永遠(yuǎn)是false,我覺得應(yīng)該刪除。已提issue #961
wg和workerWg:都是跟控制goroutine流程相關(guān)的
speedMonitorQuit:也沒什么用,忽略
quit:外界可以給這個通道發(fā)消息來通知CPUMiner退出
再回到n.Start看看cpuMiner是何時啟動的:
node/node.go#L169-L180
func (n *Node) OnStart() error { if n.miningEnable { n.cpuMiner.Start() } // ... }
由于我們傳入了參數(shù)--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保證不會重復(fù)啟動,然后把真正的工作放在了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處代碼是按指定的worker數(shù)量啟動挖礦例程
第2處是監(jiān)聽?wèi)?yīng)該保持的worker數(shù)量并增減
第3處在被知關(guān)閉的時候安全關(guān)閉
代碼比較清楚,應(yīng)該不需要多講。
可以看第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() }
方法里省略了一些不太重要的代碼,我們可以從標(biāo)注的幾處看一下在做什么:
第1處通過mining.NewBlockTemplate根據(jù)模板生成了一個block
第2處是以暴力方式(從0開始挨個計算)來爭奪對該區(qū)塊的記帳權(quán)
第3處是通過chain.ProcessBlock(block)嘗試把它加到本機(jī)持有的區(qū)塊鏈上
第4處是向newBlockCh通道發(fā)出消息,通知外界自己挖到了新的塊
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 }
這個方法很長,但是內(nèi)容都被我忽略了,原因是它的內(nèi)容過于細(xì)節(jié),并且已經(jīng)觸及到了比原的核心,所以現(xiàn)在大概了解一下就可以了。
比原在一個Block區(qū)塊里,有一些基本信息,比如在其頭部有前一塊的hash值、挖礦難度值、時間戳等等,主體部有各種交易記錄,以及多次層的hash摘要。在這個方法中,主要的邏輯就是去找到這些信息然后把它們包裝成一個Block對象,然后交由后面處理。我覺得在我們還沒有深刻理解比原的區(qū)塊鏈結(jié)構(gòu)和規(guī)則的情況下,看這些太細(xì)節(jié)的東西沒有太大用處,所以先忽略,等以后合適的時候再回過頭來看就簡單了。
m.solveBlock我們繼續(xù)向下,當(dāng)由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 }
這個方法就是挖礦中我們最關(guān)心的部分了:爭奪記帳權(quán)。
我把代碼分成了4塊,依次簡單講解:
第1處是從本地區(qū)塊鏈中找到新生成的區(qū)塊指定的父區(qū)塊,并由它計算出來seed,它是如何計算出來的我們暫時不關(guān)心(比較復(fù)雜),此時只要知道它是用來檢查工作量的就可以了
第2處是使用暴力方式來計算目標(biāo)值,用于爭奪記帳權(quán)。為什么說是暴力方式?因為挖礦的算法保證了想解開難題,沒有比從0開始一個個計算更快的辦法,所以這里從0開始依次嘗試,直到maxNonce結(jié)束。maxNonce是一個非常大的數(shù)^uint64(0)(即2^64 - 1),基本上是不可能在一個區(qū)塊時間內(nèi)遍歷完的。
第3處是在每次循環(huán)中進(jìn)行計算之前,都看一看是否需要退出。在兩種情況下應(yīng)該退出,一是quit通道里有新消息,被人提醒退出(可能是時間到了);另一種是本地的區(qū)塊鏈中已經(jīng)收到了新的塊,且高度比較自己高,說明已經(jīng)有別人搶到了。
第4處是把當(dāng)前循環(huán)的數(shù)字當(dāng)作Nonce,計算出Hash值
第5處是調(diào)用difficulty.CheckProofOfWork來檢查當(dāng)前算出來的hash值是否滿足了當(dāng)前難度。如果滿足就說明自己擁有了記帳權(quán),這個塊是有效的;否則就繼續(xù)計算
然后我們再看一下第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 }
在這個方法里,可以看到出現(xiàn)了一個tensority.AIHash,這是比原獨有的人工智能友好的工作量算法,相關(guān)論文的下載地址:https://github.com/Bytom/byto...,有興趣的同學(xué)可以去看看。由于這個算法的難度肯定超出了本文的預(yù)期,所以就不研究它了。在以后,如果有機(jī)會有條件的話,也許我會試著理解一下(不要期待~)
從這個方法里可以看出,它是調(diào)用了tensority.AIHash中的相關(guān)方法進(jìn)判斷當(dāng)前計算出來的hash是否滿足難度要求。
在本文的開始,我們說過希望能找到一種方法修改比原的代碼,讓我們在solonet模式下,可以正常挖礦,得到BTM用于測試??吹竭@個方法的時候,我覺得已經(jīng)找到了,我們只需要修改一下讓它永遠(yuǎn)返回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,可能使得程序中關(guān)于時間戳檢查的地方出現(xiàn)問題,出現(xiàn)如下的錯誤:
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就太快了,反而使檢查通過不了。不過我感覺這里是有一點問題的,留待以后再研究。
這樣修改完以后,再重新編譯并啟動比原節(jié)點,每個塊都能挖到了,差不多一秒一個塊(一下子變成大富豪了:)
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把剛才成功拿到記帳權(quán)的塊向本地區(qū)塊鏈上添加:
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 }
可以看到這里實際上是把這個工作甩出去了,因為它把要處理的塊放進(jìn)了Chain.processBlockCh這個通道里,同時傳過去的還有一個用于對方回復(fù)的通道reply。然后監(jiān)聽reply等消息就可以了。
那么誰將會處理c.processBlockCh里的內(nèi)容呢?當(dāng)然是由Chain,只不過這里就屬于比原核心了,我們留等以后再詳細(xì)研究,今天就先跳過。
如果處理完沒有出錯,就進(jìn)入到了第4塊,把這個block的hash放在newBlockCh通道里。這個newBlockCh是由外面?zhèn)魅氲?,很多地方都會用到。?dāng)它里面有新的數(shù)據(jù)時,就說明本機(jī)挖到了新塊(并且已經(jīng)添加到了本機(jī)的區(qū)塊鏈上),其它的地方就可以使用它進(jìn)行別的操作(比如廣播出去)
那么到這里,我們今天的問題就算解決了,留下了很多坑,以后專門填。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/24178.html
摘要:所以這個文章系列叫作剝開比原看代碼。所以我的問題是比原初始化時,產(chǎn)生了什么樣的配置文件,放在了哪個目錄下下面我將結(jié)合源代碼,來回答這個問題。將用來確認(rèn)數(shù)據(jù)目錄是有效的,并且將根據(jù)傳入的不同,來生成不同的內(nèi)容寫入到配置文件中。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee...
摘要:所以本文本來是想去研究一下,當(dāng)別的節(jié)點把區(qū)塊數(shù)據(jù)發(fā)給我們之后,我們應(yīng)該怎么處理,現(xiàn)在換成研究比原的是怎么做出來的。進(jìn)去后會看到大量的與相關(guān)的配置。它的功能主要是為了在訪問與的函數(shù)之間增加了一層轉(zhuǎn)換。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlo...
摘要:下一步,將進(jìn)入比原的節(jié)點也就是后端。它具體是怎么創(chuàng)建密鑰的,這在以后的文章中將詳細(xì)討論。當(dāng)我們清楚了在本文中,前后端數(shù)據(jù)是如何交互的,就很容易推廣到更多的情景。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前面一篇文章,我們粗略...
摘要:作者比原項目倉庫地址地址在上上篇文章里,我們還剩下一個小問題沒有解決,即前端是如何顯示一個交易的詳細(xì)信息的。那我們在本文看一下,比原是如何顯示這個交易的詳細(xì)信息的。到今天為止,我們終于把比原是如何創(chuàng)建一個交易的這件事的基本流程弄清楚了。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https:/...
摘要:繼續(xù)看生成地址的方法由于這個方法里傳過來的是而不是對象,所以還需要再用查一遍,然后,再調(diào)用這個私有方法創(chuàng)建地址該方法可以分成部分在第塊中主要關(guān)注的是返回值。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在比原的dashboard中...
閱讀 2432·2023-04-26 00:46
閱讀 593·2023-04-25 21:36
閱讀 737·2021-11-24 10:19
閱讀 2282·2021-11-23 09:51
閱讀 1028·2021-10-21 09:39
閱讀 841·2021-09-22 10:02
閱讀 1677·2021-09-03 10:29
閱讀 2708·2019-08-30 15:53