1. 程式人生 > >以太坊源碼機制:挖礦

以太坊源碼機制:挖礦

date 問題 pem 廣播 tty hand 同時 就會 upd

狗年吉祥,開工利是,我們繼續研究以太坊源碼。從本篇文章開始,我們會深入到以太坊核心源碼中去,進而分析與研究以太坊的核心技術。

關鍵字:拜占庭,挖礦,礦工,分叉,源碼分析,uncle叔塊,agent,worker,事件監聽

本文基於go-ethereum 1.7.3-stable源碼版本。源碼範圍主要在miner pkg。

miner.start()

miner即礦工的意思,礦工要做的工作就是“挖礦”,挖礦就是將一系列最新未封裝到塊中的交易封裝到一個新的區塊的過程。學習以太坊挖礦之前,我們要先搞清楚幾個概念:

拜占庭將軍問題

分布式系統的狀態同步問題。

拜占庭帝國繁榮富饒,周邊的幾個小國家的將軍對其垂涎已久但又各自心懷鬼胎。他們必須有超過一半以上的將軍同意進攻拜占庭並且不能在戰場上做出背叛的動作(達成共識),否則就會進攻失敗,引火燒身。而將軍的領土有可能被其他幾個將軍瓜分。基於這種情況,將軍們之間的溝通很成問題,有的人口是心非,有的人是忠誠為了組織的利益。如何能最終達成共識,這是個問題。

分布式系統中的每個結點就是將軍,這些結點要同步狀態時,就會面臨拜占庭將軍問題。如何有效避免結點發出的錯誤信息對結果造成的影響?

POW(Proof Of Work)

工作量證明,顧名思義,就是證明你做了多少工作。POW是目前最流行的解決上面拜占庭將軍問題的共識算法,比特幣、以太坊等主流區塊鏈數字貨幣都是基POW。

要想解決拜占庭將軍問題,需要先確定一個方法:就是在這些平等的將軍中間選拔出來一個忠誠的“大將軍”,其他將軍聽他的決定即可。

這似乎違反了去中心化的思想,但我們仔細分析可得,這些將軍在做這次決定之前都是去中心化的平等的結點,而選拔出來的大將軍只是針對這一次決定,下一次決定還會再次選拔。而不是中心化的固定了一位大將軍,以後幾十年的朝堂內外都要聽他一人的命令。

搞清楚了這個問題以後,下面要解決的就是如何選拔大將軍的問題。決定之前,這些將軍都是平等結點,所以要通過他們在本次決定時的發言來判斷。將軍們通過已知戰場情報,各自估量當前戰場形勢,進行計算,有了結論(出塊)以後廣播給其他將軍。同時該將軍還要始終監聽來自其他將軍的廣播內容,一旦接收到來自其他將軍的結論廣播,將軍們會立即停止手中的計算,來驗證廣播的內容,如果所有將軍都驗證通過,那麽第一個發出這個可驗證通過結果廣播的將軍就被選拔為大將軍,本次決定聽他的結論(已經包含在廣播中的結果內容)。

所以這個過程中比較重要的因素有兩點:

  1. 首先是速度,第一個通過驗證的才能被選拔為大將軍,慢一步的第二個沒有任何機會。
  2. 然後是正確性,即將軍發出的結論是否正確,可被其他將軍驗證成功。

速度問題是計算能力的問題,例如我的電腦是8核32G配置,計算能力肯定比你單核1G內存的快,這個與POW算法關系不大。而POW算法是用來解決正確性的方案,POW提供了一種難於計算易於驗證的方式,這是基於哈希函數的特性實現的,前面文章也有介紹過,哈希函數是

  • 免碰撞的
  • 逆向困難的
  • 如果想求得特定範圍的加密值,只能通過窮舉法

通過這些特性,工作量證明可以發布給各結點一個哈希加密函數,各結點將待封印區塊信息通過該函數再加上一個nonce值計算得到一個加密哈希,這個加密哈希需要滿足一定的規則(例如前四位必須是1111),各結點只能通過窮舉法來不斷嘗試,直到滿足條件以後,即得出結論,也即出塊成功,這時候再將區塊哈希廣播出去,其他結點一驗證,的確滿足預定規則(前四位的確是1111),則完成共識,該塊由剛才廣播的結點出塊。這個工作量指的就是結點在不斷嘗試計算的工作量,得到符合條件的區塊哈希以後,經過廣播,其他結點手頭正在進行的以及已經完成的工作量作廢(其實這也是一種算力浪費),該塊被證明為由你出塊。

問題1:篩選塊

就是當你廣播時,其他某個結點也算出來符合條件的哈希值了,即該結點也出了編號相同的一個區塊,此時就要對比兩個塊的時間戳,時間較早的會被確認,保留到鏈上,而另一個較晚的則會被拋棄。

問題2:分叉鏈

當某個結點發布了新的共識規則,其他結點並未同步該共識規則,一般來講新的共識規則是向前兼容的,即鏈上前面的數據仍然是有效的被承認的,但未同步新規則的結點會繼續挖礦,他們挖出的塊不會被更新新規則的結點共識或者承認。這個時候,鏈就分叉了,分為1.0(舊共識規則)和2.0(新共識規則)兩條鏈。此時有更多群眾(礦工)基礎的鏈會留下來。

例如這個公鏈是我們公司發布的,我們會去拉動更多客戶進來,但其實作為發布者,我和這些客戶的結點都是平等的,此時某個陌生結點只要加入進來,它也可以發布新規則,而作為發布者的我們也要更新我們的軟件,所以社區就非常重要了,通過社區,我們可以維護著我們客戶的支持與信任,當我們發布新規則時會得到他們的支持。由於區塊鏈本身的開源、去中心化的屬性,我們的公鏈一旦發布,就不屬於我們了,而是屬於每一個參與進來的結點,而我們只能通過確切地辦實事,解決問題,才會得到客戶礦工們的認可才能保障這條鏈的優秀競爭力(當然了,作為發布者的我們擁有了大量的預購幣,所以為了鏈的發展,影響力的擴大,幣的升值,我們的動力更加強勁)。

不過也有一種情況,就是原來1.0的鏈還有一部分人再使用,但我要說的是,他們那條鏈的生命力肯定不行了,因為沒有利益相關者,大家不會免費付出。區塊鏈技術是平等公平的,沒有得到大家也就不會付出。

miner源碼分析

下面采用代碼調試的順序,來分析以太坊miner.go文件源碼內容。整個以太坊挖礦相關的操作都是通過Miner結構體暴露出來的方法:

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 // 同步以後是否應該開始挖礦
}
  • 事件鎖,事件發生時,會有一個TypeMux將時間分派給註冊的接收者。接收者可以註冊以處理特定類型的事件。在mux結束後調用的任何操作將返回ErrMuxClosed。
  • 共識引擎,獲得共識算法的工具對象,以提供後續共識相關操作使用。我會在這篇文章之後單獨寫兩篇文章仔細分析以太坊的兩種共識算法ethash和clique。

worker

Miner結構體中其他的都介紹完畢,唯獨worker對象需要深入研究,因為外部有一個單獨的worker.go文件,Miner包含了這個worker對象。上面註釋給出的是“幹活的人”,每個miner都會有一個worker成員對象,可以理解為工人,他負責全部具體的挖礦工作流程。

type worker struct {
    config *params.ChainConfig
    engine consensus.Engine

    mu sync.Mutex

    // update loop
    mux          *event.TypeMux
    txCh         chan core.TxPreEvent
    txSub        event.Subscription
    chainHeadCh  chan core.ChainHeadEvent
    chainHeadSub event.Subscription
    chainSideCh  chan core.ChainSideEvent
    chainSideSub event.Subscription
    wg           sync.WaitGroup

    agents map[Agent]struct{} // worker擁有一個Agent的map集合
    recv   chan *Result

    eth     Backend
    chain   *core.BlockChain
    proc    core.Validator
    chainDb ethdb.Database

    coinbase common.Address
    extra    []byte

    currentMu sync.Mutex
    current   *Work

    uncleMu        sync.Mutex
    possibleUncles map[common.Hash]*types.Block

    unconfirmed *unconfirmedBlocks // 本地挖出的待確認的塊

    mining int32
    atWork int32
}

worker的屬性非常多而具體了,都是挖礦具體操作相關的,其中包括鏈本身的屬性以及區塊數據結構的屬性。首先來看ChainConfig:

type ChainConfig struct {
    ChainId *big.Int `json:"chainId"` // 鏈id標識了當前鏈,主鍵唯一id,也用於replay protection重發保護(用來防止replay attack重發攻擊:惡意重復或拖延正確數據傳輸的一種網絡攻擊手段)

    HomesteadBlock *big.Int `json:"homesteadBlock,omitempty"` // 當前鏈Homestead,置為0

    DAOForkBlock   *big.Int `json:"daoForkBlock,omitempty"`   // TheDAO硬分叉切換。
    DAOForkSupport bool     `json:"daoForkSupport,omitempty"` // 結點是否支持或者反對DAO硬分叉。

    // EIP150 implements the Gas price changes (https://github.com/ethereum/EIPs/issues/150)
    EIP150Block *big.Int    `json:"eip150Block,omitempty"` // EIP150 HF block (nil = no fork)
    EIP150Hash  common.Hash `json:"eip150Hash,omitempty"`  // EIP150 HF hash (needed for header only clients as only gas pricing changed)

    EIP155Block *big.Int `json:"eip155Block,omitempty"` // EIP155 HF block,沒有硬分叉置為0
    EIP158Block *big.Int `json:"eip158Block,omitempty"` // EIP158 HF block,沒有硬分叉置為0

    ByzantiumBlock *big.Int `json:"byzantiumBlock,omitempty"` // Byzantium switch block (nil = no fork, 0 = already on byzantium)

    // Various consensus engines
    Ethash *EthashConfig `json:"ethash,omitempty"`
    Clique *CliqueConfig `json:"clique,omitempty"`
}

ChainConfig顧名思義就是鏈的配置屬性。

go語法補充:結構體中的標簽。我想對於上面ChainId屬性後面的``內容,我們都有疑惑,這是結構體中的標簽。它是可選的,是對變量的附加內容,通過reflect包可以讀取到這些內容,通過對ChainConfig結構體中的屬性標簽的觀察,我們能看出來這些標簽是用來聲明變量在結構體轉化為json結構以後的id值,這個值是可以與當前變量名字不同的。

書歸正傳,ChainConfig中包含了ChainID等屬性,其中有很多都是針對以太坊歷史發生的問題進行的專門配置。

  • ChainId可以預防replay攻擊。
  • Homestead是以太坊發展藍圖中的一個階段。第一階段是以太坊區塊鏈面世,代號為frontier,第二個階段即為當前階段,代號為Homestead(家園),第三階段為大都會Metropolis,大都會又細分為兩個小階段,第一個是拜占庭Byzantium硬分叉(引入新型零知識證明算法以及pos權益證明共識算法),第二個是君士坦丁堡Constantinople硬分叉(以太坊正式應用pow和pos混合鏈,解決拜占庭引發的問題)。最後一個階段代號Serenity,最終版本的以太坊穩定運行。
  • 2017年6月18日,以太坊上DAO(去中心自治組織)的一次大危機做出的相應調整。感興趣的可以自行谷百。
  • 2017年10月16日,以太坊的一次Byzantium拜占庭硬分叉。
  • EIPs(Ethereum Improvement Proposals),是以太坊更新改善的一些方案,對應後面的數字就是以太坊github源碼issue的編號。

agent

一個miner擁有一個worker,一個worker擁有多個agent。Agent接口是定義在Worker.go文件中:

// Agent 可以註冊到worker
type Agent interface {
    Work() chan<- *Work
    SetReturnCh(chan<- *Result)
    Stop()
    Start()
    GetHashRate() int64
}

這個接口有兩個實現:CpuAgent和RemoteAgent。這裏使用的是CpuAgent,該Agent會完成一個塊的出塊工作,同級的多個Agent是競爭關系,最終通過共識算法完成出一個塊的工作。

type CpuAgent struct {
    mu sync.Mutex // 鎖

    workCh        chan *Work // Work通道對象
    stop          chan struct{} // 結構體通道對象
    quitCurrentOp chan struct{} // 結構體通道對象
    returnCh      chan<- *Result // Result指針通道

    chain  consensus.ChainReader
    engine consensus.Engine

    isMining int32 // agent是否正在挖礦的標誌位
}

挖礦start()整個生命周期

開始挖礦,首先要初始化一個miner實例,

func New(eth Backend, config *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine) *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
}

在創建miner實例時,會根據Miner結構體成員屬性依次賦值,其咋紅worker對象需要調用newWorker構造函數,

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,
        txCh:           make(chan core.TxPreEvent, txChanSize),// TxPreEvent事件是TxPool發出的事件,代表一個新交易tx加入到了交易池中,這時候如果work空閑會將該筆交易收進work.txs,準備下一次打包進塊。
        chainHeadCh:    make(chan core.ChainHeadEvent, chainHeadChanSize),// ChainHeadEvent事件,代表已經有一個塊作為鏈頭,此時work.update函數會監聽到這個事件,則會繼續挖新的區塊。
        chainSideCh:    make(chan core.ChainSideEvent, chainSideChanSize),// ChainSideEvent事件,代表有一個新塊作為鏈的旁支,會被放到possibleUncles數組中,可能稱為叔塊。
        chainDb:        eth.ChainDb(),// 區塊鏈數據庫
        recv:           make(chan *Result, resultQueueSize),
        chain:          eth.BlockChain(), // 鏈
        proc:           eth.BlockChain().Validator(),
        possibleUncles: make(map[common.Hash]*types.Block),// 存放可能稱為下一個塊的叔塊數組
        coinbase:       coinbase,
        agents:         make(map[Agent]struct{}),
        unconfirmed:    newUnconfirmedBlocks(eth.BlockChain(), miningLogAtDepth),// 返回一個數據結構,包括追蹤當前未被確認的區塊。
    }
    // 註冊TxPreEvent事件到tx pool交易池
    worker.txSub = eth.TxPool().SubscribeTxPreEvent(worker.txCh)
    // 註冊事件到blockchain
    worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
    worker.chainSideSub = eth.BlockChain().SubscribeChainSideEvent(worker.chainSideCh)
    go worker.update()

    go worker.wait()
    worker.commitNewWork()

    return worker
}

在創建work實例的時候,會有幾個比較重要的事件,包括TxPreEvent、ChainHeadEvent、ChainSideEvent,我在上面代碼註釋中都有了標識。下面看一下開啟新線程執行的worker.update(),

        case <-self.chainHeadCh:
            self.commitNewWork()

        // Handle ChainSideEvent
        case ev := <-self.chainSideCh:
            self.uncleMu.Lock()
            self.possibleUncles[ev.Block.Hash()] = ev.Block
            self.uncleMu.Unlock()

        // Handle TxPreEvent
        case ev := <-self.txCh:

由於源碼較長,我只展示了一部分,我們知道update方法是用來監聽處理上面談到的三個事件即可。下面再看一下worker.wait()方法,

func (self *worker) wait() {
    for {
        mustCommitNewWork := true
        for result := range self.recv {
            atomic.AddInt32(&self.atWork, -1)

            if result == nil {
                continue
            }
            block := result.Block
            work := result.Work

            // Update the block hash in all logs since it is now available and not when the
            // receipt/log of individual transactions were created.
            for _, r := range work.receipts {
                for _, l := range r.Logs {
                    l.BlockHash = block.Hash()
                }
            }
            for _, log := range work.state.Logs() {
                log.BlockHash = block.Hash()
            }
            stat, err := self.chain.WriteBlockAndState(block, work.receipts, work.state)
            if err != nil {
                log.Error("Failed writing block to chain", "err", err)
                continue
            }
            // 檢查是否是標準塊,寫入交易數據。
            if stat == core.CanonStatTy {
                // 受ChainHeadEvent事件的影響。
                mustCommitNewWork = false
            }
            // 廣播一個塊聲明插入鏈事件NewMinedBlockEvent
            self.mux.Post(core.NewMinedBlockEvent{Block: block})
            var (
                events []interface{}
                logs   = work.state.Logs()
            )
            events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
            if stat == core.CanonStatTy {
                events = append(events, core.ChainHeadEvent{Block: block})
            }
            self.chain.PostChainEvents(events, logs)

            // 將處理中的數據插入到區塊中,等待確認
            self.unconfirmed.Insert(block.NumberU64(), block.Hash())

            if mustCommitNewWork {
                self.commitNewWork() // 多次見到,顧名思義,就是提交新的work
            }
        }
    }
}

wait方法比較長,但必須展示出來的原因是它包含了重要的具體寫入塊的操作,具體內容請看上面代碼中的註釋。

通過New方法來初始化創建一個miner實例,入參包括Backend對象,ChainConfig對象屬性集合,事件鎖,以及指定共識算法引擎,返回一個Miner指針。方法體中對miner對象進行了組裝賦值,並且調用了方法NewCpuAgent創建agent的一個實例然後註冊到該miner上來,啟動一個單獨線程執行miner.update(),我們先來看NewCpuAgent方法:

func NewCpuAgent(chain consensus.ChainReader, engine consensus.Engine) *CpuAgent {
    miner := &CpuAgent{
        chain:  chain,
        engine: engine,
        stop:   make(chan struct{}, 1),
        workCh: make(chan *Work, 1),
    }
    return miner
}

通過NewCpuAgent方法,先組裝一個CpuAgent,賦值ChainReader,共識引擎,stop結構體,work通道,然後將這個CpuAgent實例賦值給miner並返回miner。然後讓我們回到miner.update()方法:

// update方法可以保持對下載事件的監聽,請了解這是一段短型的update循環。
func (self *Miner) update() {
    // 註冊下載開始事件,下載結束事件,下載失敗事件。
    events := self.mux.Subscribe(downloader.StartEvent{}, downloader.DoneEvent{}, downloader.FailedEvent{})
out:
    for ev := range events.Chan() {
        switch ev.Data.(type) {
        case downloader.StartEvent:
            atomic.StoreInt32(&self.canStart, 0)
            if self.Mining() {// 開始下載對應Miner操作Mining。
                self.Stop()
                atomic.StoreInt32(&self.shouldStart, 1)
                log.Info("Mining aborted due to sync")
            }
        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) // 執行Miner的start方法。
            }
            // 處理完以後要取消訂閱
            events.Unsubscribe()
            // 跳出循環,不再監聽
            break out
        }
    }
}

然後我們來看miner的Mining方法,

// 如果miner的mining屬性大於1即返回ture,說明正在挖礦中。
func (self *Miner) Mining() bool {
    return atomic.LoadInt32(&self.mining) > 0
}

再來看Miner的start方法,它是屬於Miner指針實例的方法,首字母大寫代表可以被外部所訪問,傳入一個地址。

func (self *Miner) Start(coinbase common.Address) {
    atomic.StoreInt32(&self.shouldStart, 1)
    self.worker.setEtherbase(coinbase)
    self.coinbase = coinbase

    if atomic.LoadInt32(&self.canStart) == 0 {
        log.Info("Network syncing, will start miner afterwards")
        return
    }
    atomic.StoreInt32(&self.mining, 1)

    log.Info("Starting mining operation")
    self.worker.start()
    self.worker.commitNewWork()
}

關鍵代碼在self.worker.start()和self.worker.commitNewWork()。先來說worker.start()方法。

func (self *worker) start() {
    self.mu.Lock()
    defer self.mu.Unlock()

    atomic.StoreInt32(&self.mining, 1)

    // spin up agents
    for agent := range self.agents {
        agent.Start()
    }
}

worker.start()實際上遍歷啟動了它所有的agent。上面提到了,這裏走的是CpuAgent的實現。

func (self *CpuAgent) Start() {
    if !atomic.CompareAndSwapInt32(&self.isMining, 0, 1) {
        return // agent already started
    }
    go self.update()
}

啟用一個單獨線程去執行CpuAgent的update()方法。update方法與上面miner.update十分相似。

func (self *CpuAgent) update() {
out:
    for {
        select {
        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
        }
    }
}

out: break out , 跳出for循環,for循環不斷監聽self信號,當監測到self停止時,則調用關閉操作代碼,並直接挑出循環監聽,函數退出。

通過監測CpuAgent的workCh通道,是否有work工作信號進入,如果有agent則開始挖礦,期間要上鎖,啟用一個單獨線程去執行CpuAgent的mine方法。

func (self *CpuAgent) mine(work *Work, stop <-chan struct{}) {
    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())
        self.returnCh <- &Result{work, result}
    } else {
        if err != nil {
            log.Warn("Block sealing failed", "err", err)
        }
        self.returnCh <- nil
    }
}

執行到這裏,可以看到是調用的CpuAgent的共識引擎的塊封裝函數Seal來執行具體的挖礦操作。

** 前面提到了以太坊共識算法有ethash和clique兩種,所以對應著有兩種Seal方法的實現,這裏賣個關子,我們在之後的博文會抽絲剝繭,仔細介紹。**

這裏收起一個口子,回到上面繼續分析另一個比較重要的方法self.worker.commitNewWork()。commitNewWork方法源碼比較長,這裏就不粘貼出來了,這個方法主要的工作是為新塊準備基本數據,包括header,txs,uncles等。

uncles叔塊的概念

區塊鏈中會存在一種可能,就是由於網絡的原因造成一個塊並未存在於最長的鏈上,這個塊就叫做孤塊。一般來講區塊鏈崇尚最長即正義,會毫不猶豫地淘汰掉孤塊,但叔塊的挖掘也有很大的能耗,並且它是合法的,只是不在最長鏈上。在以太坊中,孤塊被稱作叔塊,不會被認為是無價值的,而是也會給予獎勵。

我們換一種方式來解釋叔塊,當要出塊的時候,可能有兩個結點同時出塊了,這時候,區塊鏈會保留這兩個塊,然後看哪個塊先有後繼塊就采用誰,而淘汰另一個塊(誰先生兒子,誰就是老大,淘汰另一個)。在以太坊中,生兒子的老大會稱為正式塊,但叔塊的礦工也會得到1/32的獎勵,同時,老大的兒子如果記錄了叔塊,也會得到額外的獎勵,但是叔塊本身封裝的交易會被退回交易池,重新等待打包。這樣一來,以太坊顯得很有人情味,相當於對挖叔塊的工作量的一種認可,是一種以公平為出發點的角度設計的。

總結

關於以太坊挖礦的源碼粗略的分析就到這,粗略的意思是對於我自己的標準來講,我並未逐一介紹每個流程控制,還有每行代碼的具體意思,只是大致提供了一種看源碼的路線,一條一條的進入,再收緊退回,最終完成了一個閉環,讓我們對以太坊挖礦的一些具體操作有了了解。這部分源碼主要工作是對交易數據池的維護,塊數據組織,各種事件的監聽處理,miner-worker-agent的分工關系。最後是留下了唯一的出塊共識的問題,就是決定到底誰出塊的算法,我們在下一篇文章中繼續介紹。

參考資料

go-ethereum源碼,網上資料

更多文章請轉到醒者呆的博客園。

以太坊源碼機制:挖礦