這篇開始研究以太坊的挖礦流程,基本框架參見下圖:
其中涉及到的組件之間的關係可以參見下面的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