以太坊挖礦源碼分析

這篇開始研究以太坊的挖礦流程,基本框架參見下圖:

這裏寫圖片描述

其中涉及到的組件之間的關係可以參見下面的UML圖:

這裏寫圖片描述

1. Miner啓動打包

在eth Service初始化的時候,會創建一個Miner實例:

eth.miner = miner.New(eth, eth.chainConfig, eth.EventMux(), eth.engine)

我們看一下這個New()函數,代碼位於miner/miner.go:

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
}

代碼分爲3個部分:創建Miner實例、註冊Agent、等待區塊同步完成,下面分別進行分析。

1.1 創建Miner實例

這一步最主要的工作是調用newWorker創建一個worker實例,Miner只是一個發起人,真正幹活的是worker。看一下newWorker()函數,代碼位於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,
        txCh:           make(chan core.TxPreEvent, txChanSize),
        chainHeadCh:    make(chan core.ChainHeadEvent, chainHeadChanSize),
        chainSideCh:    make(chan core.ChainSideEvent, chainSideChanSize),
        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),
    }
    // Subscribe TxPreEvent for tx pool
    worker.txSub = eth.TxPool().SubscribeTxPreEvent(worker.txCh)
    // Subscribe events for 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
}

這裏一個比較重要的字段是recv,是一個channel類型,用於接收從Agent那邊傳過來的Result。
同時還啓動一個goroutine運行worker.wait(),這個函數主要就是監聽recv,把新區塊寫入數據庫從而更新世界狀態。

1.2 註冊Agent

Agent是一個接口,定義位於miner/worker.go:

type Agent interface {
    Work() chan<- *Work
    SetReturnCh(chan<- *Result)
    Stop()
    Start()
    GetHashRate() int64
}

其中Work()函數用於獲取一個channel,當worker產生新的Work時會通過這個接口發送給Agent。
同時SetReturnCh()函數用於註冊一個channel,當Agent這邊完成POW計算後,會通過這個channel把Result發送給worker。
CpuAgent是Agent的具體實現類,可以通過NewCpuAgent()創建一個CpuAgent實例。

我們是通過調用Miner的Register()函數完成Agent註冊的,參數是一個CpuAgent實例。看一下這個函數:

func (self *Miner) Register(agent Agent) {
    if self.Mining() {
        agent.Start()
    }
    self.worker.register(agent)
}

繼續跟蹤worker的register()函數:

func (self *worker) register(agent Agent) {
    self.mu.Lock()
    defer self.mu.Unlock()
    self.agents[agent] = struct{}{}
    agent.SetReturnCh(self.recv)
}

可以看到,這裏首先在一個map中記錄了這個Agent,然後調用SetReturnCh()函數註冊了一個接收channel。

1.3 等待區塊同步完成

在開始挖礦之前,首先需要等待和其他結點之間完成區塊同步,這樣才能在最新的狀態挖礦。因此這裏啓動了一個goroutine調用Miner.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() {
                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)
            }
            // unsubscribe. we're only interested in this event once
            events.Unsubscribe()
            // stop immediately and ignore all further pending events
            break out
        }
    }
}

可以看到,訂閱了downloader的StartEvent、DoneEvent、FailedEvent事件:

  • 當收到StartEvent時,會把canStart置爲0,這樣即使你調用Miner的Start()函數也不會真正啓動
  • 當收到DoneEvent或者FailedEvent時,將canStart置爲1,然後調用Miner的Start()函數啓動挖礦

值得注意的是,收到downloader的消息後會立即停止訂閱這些消息並退出,也就是說這個函數只會運行一次。

接着看一下Miner的Start()函數:

func (self *Miner) Start(coinbase common.Address) {
    atomic.StoreInt32(&self.shouldStart, 1)
    self.SetEtherbase(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()
}

可以看到這裏會判斷canStart標誌,如果同步沒有完成的話是不會真正啓動的。
緊接着調用了worker的start()函數,以及最關鍵的commitNewWork()函數。我們先看一下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()
    }
}

這裏會遍歷所有的Agent,調用它們的Start()函數。我們可以看一下CpuAgent的Start()函數:

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

啓動了一個goroutine調用update()函數,這個函數主要作用就是接收worker發送過來的Work並進行處理了,具體留待第3節分析。

2. worker打包區塊,發送Work給Agent

剛剛提到了commitNewWork()是一個關鍵函數,完成主要的區塊打包工作。這個函數比較長,分成幾段來分析。

2.1 創建新區塊頭

    parent := self.chain.CurrentBlock()
    ……

    num := parent.Number()
    header := &types.Header{
        ParentHash: parent.Hash(),
        Number:     num.Add(num, common.Big1),
        GasLimit:   core.CalcGasLimit(parent),
        Extra:      self.extra,
        Time:       big.NewInt(tstamp),
    }

    if atomic.LoadInt32(&self.mining) == 1 {
        header.Coinbase = self.coinbase
    }

首先獲取當前區塊,然後創建一個Header結構,填充父塊hash、區塊高度、GasLimit、礦工地址(Coinbase)等信息。
其中GasLimit是區塊中打包的交易消耗的總gas的上限,通過CalcGasLimit()函數計算出來。這個值是每生成一個區塊都會動態調整的:如果上一個區塊消耗的總gas < gas limit的2/3,則增大gas limit,否則減小gas limit。通過這種方式可以動態調整區塊的大小。
有興趣可以到core/block_validator.go中查閱具體代碼。

2.2 初始化共識引擎

    if err := self.engine.Prepare(self.chain, header); err != nil {
        log.Error("Failed to prepare header for mining", "err", err)
        return
    }

也就是調用共識引擎的Prepare()函數。默認使用基於POW算法的Ethash共識引擎,可以看一下Ethash的Prepare()函數:

func (ethash *Ethash) Prepare(chain consensus.ChainReader, header *types.Header) error {
    parent := chain.GetHeader(header.ParentHash, header.Number.Uint64()-1)
    if parent == nil {
        return consensus.ErrUnknownAncestor
    }
    header.Difficulty = ethash.CalcDifficulty(chain, header.Time.Uint64(), parent)
    return nil
}

首先獲取父塊的Header,然後根據其中的信息計算新的挖礦難度值。具體邏輯留待分析共識引擎的時候再分析。

2.3 創建新Work

    err := self.makeCurrent(parent, header)

看一下makeCurrent()函數:

func (self *worker) makeCurrent(parent *types.Block, header *types.Header) error {
    state, err := self.chain.StateAt(parent.Root())
    if err != nil {
        return err
    }
    work := &Work{
        config:    self.config,
        signer:    types.NewEIP155Signer(self.config.ChainId),
        state:     state,
        ancestors: set.New(),
        family:    set.New(),
        uncles:    set.New(),
        header:    header,
        createdAt: time.Now(),
    }

    // when 08 is processed ancestors contain 07 (quick block)
    for _, ancestor := range self.chain.GetBlocksFromHash(parent.Hash(), 7) {
        for _, uncle := range ancestor.Uncles() {
            work.family.Add(uncle.Hash())
        }
        work.family.Add(ancestor.Hash())
        work.ancestors.Add(ancestor.Hash())
    }

    // Keep track of transactions which return errors so they can be removed
    work.tcount = 0
    self.current = work
    return nil
}

首先根據父塊狀態創建了一個新的StateDB實例。
然後創建Work實例,主要初始化了它的state和header字段。
接下來還要更新Work中和叔塊(Uncle Block)相關的字段,關於叔塊可以參見下面這篇文章的介紹:
https://blog.csdn.net/superswords/article/details/76445278

最後把新創建的Work實例賦值給self.current字段。

2.4 執行交易

    work := self.current
    ……
    pending, err := self.eth.TxPool().Pending()
    if err != nil {
        log.Error("Failed to fetch pending transactions", "err", err)
        return
    }
    txs := types.NewTransactionsByPriceAndNonce(self.current.signer, pending)
    work.commitTransactions(self.mux, txs, self.chain, self.coinbase)

首先獲取txpool的待處理交易列表的一個拷貝,然後封裝進一個TransactionsByPriceAndNonce類型的結構中。這個結構中包含一個heads字段,把交易按照gas price進行排序,類型定義參見以下代碼:

type TransactionsByPriceAndNonce struct {
    txs    map[common.Address]Transactions // Per account nonce-sorted list of transactions
    heads  TxByPrice                       // Next transaction for each unique account (price heap)
    signer Signer                          // Signer for the set of transactions
}

接下來就是調用commitTransactions()把交易提交到EVM去執行了:

func (env *Work) commitTransactions(mux *event.TypeMux, txs *types.TransactionsByPriceAndNonce, bc *core.BlockChain, coinbase common.Address) {
    gp := new(core.GasPool).AddGas(env.header.GasLimit)
    ……

    for {
        // If we don't have enough gas for any further transactions then we're done
        if gp.Gas() < params.TxGas {
            log.Trace("Not enough gas for further transactions", "gp", gp)
            break
        }
        // Retrieve the next transaction and abort if all done
        tx := txs.Peek()
        if tx == nil {
            break
        }
        ……

        from, _ := types.Sender(env.signer, tx)
        ……

        // Start executing the transaction
        env.state.Prepare(tx.Hash(), common.Hash{}, env.tcount)
        err, logs := env.commitTransaction(tx, bc, coinbase, gp)
        switch err {
        ……

        case nil:
            // Everything ok, collect the logs and shift in the next transaction from the same account
            coalescedLogs = append(coalescedLogs, logs...)
            env.tcount++
            txs.Shift()
        ……
}

GasPool其實就是uint64類型,初始值爲GasLimit,後面每執行一筆交易都會遞減。

接下來進入循環,首先判斷當前剩餘的gas是否還夠執行一筆交易,如果不夠的話就退出循環。
然後從交易列表中取出一個,計算出發送方地址,進而提交給EVM執行。

先看一下Prepare()函數,代碼位於core/state/statedb.go:

func (self *StateDB) Prepare(thash, bhash common.Hash, ti int) {
    self.thash = thash
    self.bhash = bhash
    self.txIndex = ti
}

僅僅是幾個賦值操作,記錄了交易的hash,塊hash目前爲空,txIndex表明這是正在執行的第幾筆交易。

接着看commitTransaction()函數:

func (env *Work) commitTransaction(tx *types.Transaction, bc *core.BlockChain, coinbase common.Address, gp *core.GasPool) (error, []*types.Log) {
    snap := env.state.Snapshot()

    receipt, _, err := core.ApplyTransaction(env.config, bc, &coinbase, gp, env.state, env.header, tx, &env.header.GasUsed, vm.Config{})
    if err != nil {
        env.state.RevertToSnapshot(snap)
        return err, nil
    }
    env.txs = append(env.txs, tx)
    env.receipts = append(env.receipts, receipt)

    return nil, receipt.Logs
}

這裏首先獲取當前狀態的快照,然後調用ApplyTransaction()執行交易:

  • 如果交易執行失敗,則回滾到之前的快照狀態並返回錯誤,該賬戶的所有後續交易都將被跳過(txs.Pop())
  • 如果交易執行成功,則記錄該交易以及交易執行的回執(receipt)並返回,然後移動到下一筆交易(txs.Shift())

2.5 處理叔塊

var (
        uncles    []*types.Header
        badUncles []common.Hash
    )
    for hash, uncle := range self.possibleUncles {
        if len(uncles) == 2 {
            break
        }
        if err := self.commitUncle(work, uncle.Header()); err != nil {
            log.Trace("Bad uncle found and will be removed", "hash", hash)
            log.Trace(fmt.Sprint(uncle))

            badUncles = append(badUncles, hash)
        } else {
            log.Debug("Committing new uncle to block", "hash", hash)
            uncles = append(uncles, uncle.Header())
        }
    }
    for _, hash := range badUncles {
        delete(self.possibleUncles, hash)
    }

遍歷所有叔塊,然後調用commitUncle()把叔塊header的hash添加進Work.uncles集合中。以太坊規定每個區塊最多打包2個叔塊的header,每打包一個叔塊可以獲得挖礦報酬的1/32。看一下commitUncle()函數:

func (self *worker) commitUncle(work *Work, uncle *types.Header) error {
    hash := uncle.Hash()
    if work.uncles.Has(hash) {
        return fmt.Errorf("uncle not unique")
    }
    if !work.ancestors.Has(uncle.ParentHash) {
        return fmt.Errorf("uncle's parent unknown (%x)", uncle.ParentHash[0:4])
    }
    if work.family.Has(hash) {
        return fmt.Errorf("uncle already in family (%x)", hash)
    }
    work.uncles.Add(uncle.Hash())
    return nil
}

這裏會用之前初始化的幾個集合來驗證叔塊的有效性,以太坊規定叔塊必須是之前2~7層的祖先的直接子塊。如果發現叔塊無效,會從集合中剔除。

2.6 打包新區塊

    if work.Block, err = self.engine.Finalize(self.chain, header, work.state, work.txs, uncles, work.receipts); err != nil {
        log.Error("Failed to finalize block for sealing", "err", err)
        return
    }

萬事俱備,現在需要把header、txs、uncles、receipts送到共識引擎的Finalize()函數中生成新區塊。
看一下Ethash的Finalize()函數:

func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) {
    // Accumulate any block and uncle rewards and commit the final state root
    accumulateRewards(chain.Config(), state, header, uncles)
    header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number))

    // Header seems complete, assemble into a block and return
    return types.NewBlock(header, txs, uncles, receipts), nil
}

這裏主要做了3件事,依次開始分析。

2.6.1 計算報酬

根據以太坊的規則:
每挖出一個新區塊可以獲得5個以太的報酬
每包含一個叔塊可以獲得該塊報酬的1/32
被包含的叔塊對應的礦工也可以收到報酬,根據其祖先所在的層數依次遞減:

  • 間隔1層,可以收到報酬的7/8
  • 間隔2層,可以收到報酬的6/8
  • 間隔3層,可以收到報酬的5/8
  • 間隔4層,可以收到報酬的4/8
  • 間隔5層,可以收到報酬的3/8
  • 間隔6層,可以收到報酬的2/8

看一下accumulateRewards(),就是按照上面的規則來實現的:

func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header *types.Header, uncles []*types.Header) {
    // Select the correct block reward based on chain progression
    blockReward := FrontierBlockReward
    if config.IsByzantium(header.Number) {
        blockReward = ByzantiumBlockReward
    }
    // Accumulate the rewards for the miner and any included uncles
    reward := new(big.Int).Set(blockReward)
    r := new(big.Int)
    for _, uncle := range uncles {
        r.Add(uncle.Number, big8)
        r.Sub(r, header.Number)
        r.Mul(r, blockReward)
        r.Div(r, big8)
        state.AddBalance(uncle.Coinbase, r)

        r.Div(blockReward, big32)
        reward.Add(reward, r)
    }
    state.AddBalance(header.Coinbase, reward)
}

這個FrontierBlockReward定義爲5e+18 wei,也就是5個以太:

FrontierBlockReward    *big.Int = big.NewInt(5e+18)

2.6.2 生成MPT根

MPT全稱Merkle Patricia Trie,是以太坊用來存儲狀態信息的一種數據結構,這棵樹的根需要存儲到區塊頭中。看一下這個IntermediateRoot()函數:

func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash {
    s.Finalise(deleteEmptyObjects)
    return s.trie.Hash()
}

調用了Trie接口的Hash()方法來獲取MPT的根hash,關於MPT實現的具體細節後面會專門寫一篇文章分析。

2.6.3 生成新區塊

最後看一下這個NewBlock()函數:

func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt) *Block {
    b := &Block{header: CopyHeader(header), td: new(big.Int)}

    // TODO: panic if len(txs) != len(receipts)
    if len(txs) == 0 {
        b.header.TxHash = EmptyRootHash
    } else {
        b.header.TxHash = DeriveSha(Transactions(txs))
        b.transactions = make(Transactions, len(txs))
        copy(b.transactions, txs)
    }

    if len(receipts) == 0 {
        b.header.ReceiptHash = EmptyRootHash
    } else {
        b.header.ReceiptHash = DeriveSha(Receipts(receipts))
        b.header.Bloom = CreateBloom(receipts)
    }

    if len(uncles) == 0 {
        b.header.UncleHash = EmptyUncleHash
    } else {
        b.header.UncleHash = CalcUncleHash(uncles)
        b.uncles = make([]*Header, len(uncles))
        for i := range uncles {
            b.uncles[i] = CopyHeader(uncles[i])
        }
    }

    return b
}

這裏主要分爲3個部分:

  • 第1部分把所有交易組織成一個MPT,並計算它的根hash
  • 第2部分把所有回執組織成一個MPT,並計算它的根hash,另外還會創建一個bloom filter,主要是爲了加快查詢速度
  • 第3部分計算叔塊頭的hash,同時把叔塊頭拷貝進區塊頭中

2.7 向Agent推送Work

    if atomic.LoadInt32(&self.mining) == 1 {
        log.Info("Commit new mining work", "number", work.Block.Number(), "txs", work.tcount, "uncles", len(uncles), "elapsed", common.PrettyDuration(time.Since(tstart)))
        self.unconfirmed.Shift(work.Block.NumberU64() - 1)
    }
    self.push(work)

上一步已經生成新區塊了,這裏會先把它放進未經確認的區塊列表unconfirmed中,然後調用push()把Work推送給Agent去做POW計算:

func (self *worker) push(work *Work) {
    if atomic.LoadInt32(&self.mining) != 1 {
        return
    }
    for agent := range self.agents {
        atomic.AddInt32(&self.atWork, 1)
        if ch := agent.Work(); ch != nil {
            ch <- work
        }
    }
}

可以看到,會調用Agent的Work()函數獲取channel,然後把Work推送到channel中。

2.8 更新快照

self.updateSnapshot()

看一下這個updateSnapshot()函數的實現:

func (self *worker) updateSnapshot() {
    self.snapshotMu.Lock()
    defer self.snapshotMu.Unlock()

    self.snapshotBlock = types.NewBlock(
        self.current.header,
        self.current.txs,
        nil,
        self.current.receipts,
    )
    self.snapshotState = self.current.state.Copy()
}

可以看到,用同樣的數據創建了一個新區塊,但是沒有傳叔塊列表。創建的區塊賦值給snapshotBlock字段,同時把當前的state也複製了一份作爲快照。

3. Agent完成POW計算後返回Result

前面1.3節提到過,調用CpuAgent的Start()函數時,會啓動一個goroutine執行update()函數,用於監聽推送過來的Work。我們先看一下這個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
        }
    }
}

可以看到,在接收到Work後,會起一個goroutine調用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
    }
}

先調用共識引擎的Seal()函數,實際上就是進行POW計算,不斷修改nonce值直到找到一個小於難度值的hash。如果計算完成,就說明成功挖出了一個新塊,我們獲得的返回值就是一個有效的Block。
把Work和Block組織成一個Result結構,發送給之前註冊返回channel的調用者,也就是worker。

4. worker存儲新區塊,啓動下一次打包

還記得1.1節中提到的recv字段和worker.wait()函數嗎?它們就是用來接收Agent發過來的Result的。
先看一下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
    ……

    }
}

是一個無限循環,從recv這個channel中讀取Result,獲得Work和Block。
接下來我們分段解讀其他部分的代碼。

4.1 修改Log中的區塊hash值

            for _, r := range work.receipts {
                for _, l := range r.Logs {
                    l.BlockHash = block.Hash()
                }
            }
            for _, log := range work.state.Logs() {
                log.BlockHash = block.Hash()
            }

這個Log是用來記錄智能合約執行過程中產生的event的。由於之前區塊尚未生成,所以無法計算區塊的hash值,現在已經生成了,因此需要更新每個Log的BlockHash字段。

4.2 將區塊和狀態信息寫入數據庫

            stat, err := self.chain.WriteBlockWithState(block, work.receipts, work.state)

這個WriteBlockWithState()函數的代碼非常長,可以分段進行解讀:

    ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1) 
    ……

    currentBlock := bc.CurrentBlock()
    localTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64())
    externTd := new(big.Int).Add(block.Difficulty(), ptd)

    // Irrelevant of the canonical status, write the block itself to the database
    if err := bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd); err != nil {
        return NonStatTy, err
    }

Td即總難度(Total Difficulty),由於以太坊要求總是選擇最長(總難度最大)的鏈作爲主鏈,通過比較這兩個值就可以知道自己挖出來的塊是有效塊還是叔塊。這裏計算出了鏈上當前的總難度localTd和新挖出來的區塊所對應的總難度externTd。

    batch := bc.db.NewBatch()
    rawdb.WriteBlock(batch, block)

這段代碼將新挖出的block寫入數據庫。

    root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number()))
    if err != nil {
        return NonStatTy, err
    }

這段代碼將新的世界狀態更新到MPT中(緩存)。

    triedb := bc.stateCache.TrieDB()

    // If we're running an archive node, always flush
    if bc.cacheConfig.Disabled {
        if err := triedb.Commit(root, false); err != nil {
            return NonStatTy, err
        }
    } else {
        ……
    }

這段代碼將新的世界狀態寫入數據庫。

    rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts)

這段代碼將所有交易執行的回執寫入數據庫。

    reorg := externTd.Cmp(localTd) > 0
    currentBlock = bc.CurrentBlock()
    if !reorg && externTd.Cmp(localTd) == 0 {
        // Split same-difficulty blocks by number, then at random
        reorg = block.NumberU64() < currentBlock.NumberU64() || (block.NumberU64() == currentBlock.NumberU64() && mrand.Float64() < 0.5)
    }
    if reorg {
        // Reorganise the chain if the parent is not the head block
        if block.ParentHash() != currentBlock.Hash() {
            if err := bc.reorg(currentBlock, block); err != nil {
                return NonStatTy, err
            }
        }
        // Write the positional metadata for transaction/receipt lookups and preimages
        rawdb.WriteTxLookupEntries(batch, block)
        rawdb.WritePreimages(batch, block.NumberU64(), state.Preimages())

        status = CanonStatTy
    } else {
        status = SideStatTy
    }

這裏首先判斷externTd和localTd的大小,會出現3種情況:

  • externTd > localTd:說明新挖出的區塊是有效塊,有資格作爲鏈頭
  • externTd < localTd:說明已經有人在你之前挖出了新區塊,且總難度更高,你挖出的是叔塊
  • externTd = localTd:說明已經有人在你之前挖出了新區塊,且總難度和你相同。這種情況應該極少,如果出現的話,通過一個隨機數來決策是否需要接受新挖出來的塊作爲鏈頭

如果決定接受新挖出的區塊作爲鏈頭,則需要判斷當前鏈頭是否是新區塊的父塊,如果不是的話需要進行重組,同時把狀態設置爲CanonStatTy。否則的話把狀態設置爲SideStatTy。

    if status == CanonStatTy {
        bc.insert(block)
    }
    bc.futureBlocks.Remove(block.Hash())

最後如果發現狀態是CanonStatTy,說明新挖出的區塊是有效塊,插入新區塊作爲鏈頭。

4.3 發送NewMinedBlockEvent事件

            self.mux.Post(core.NewMinedBlockEvent{Block: block})

發送這個事件是爲了把新挖出的區塊廣播給其他結點,事件處理代碼位於eth/handler.go:

func (pm *ProtocolManager) minedBroadcastLoop() {
    // automatically stops if unsubscribe
    for obj := range pm.minedBlockSub.Chan() {
        switch ev := obj.Data.(type) {
        case core.NewMinedBlockEvent:
            pm.BroadcastBlock(ev.Block, true)  // First propagate block to peers
            pm.BroadcastBlock(ev.Block, false) // Only then announce to the rest
        }
    }
}

4.4 發送ChainEvent事件

            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)

搜了一下似乎只有filter訂閱了這個事件,等以後遇到了再分析。

4.5 啓動下一次打包

            self.unconfirmed.Insert(block.NumberU64(), block.Hash())

            if mustCommitNewWork {
                self.commitNewWork()
            }

這個比較簡單,就是更新unconfirm列表,然後再次調用commitNewWork()啓動下一次打包。
這樣就完成了一次完整的挖礦流程,回到了原點。以Miner調用commitNewWork()開始,到最後worker再次調用commitNewWork()啓動下一次打包。

至此,以太坊挖礦相關的源代碼就分析完畢了。

更多文章歡迎關注“鑫鑫點燈”專欄:https://blog.csdn.net/turkeycock/article/category/7669858

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