1. 程式人生 > >死磕以太坊原始碼分析之區塊上鍊入庫

死磕以太坊原始碼分析之區塊上鍊入庫

> 死磕以太坊原始碼分析之區塊上鍊入庫 > > 配合以下程式碼進行閱讀:https://github.com/blockchainGuide/ > > 寫文不易,給個小關注,有什麼問題可以指出,便於大家交流學習。 ## 引言 不管是礦工挖礦還是`Fetcher`同步,`Downloader`同步,或者是匯入本地檔案等等,最中都是將區塊上鍊入庫。接下來我們就詳細分析這部分的動作。 ## 幾處可能呼叫的地方 ①:在Downloader同步最後會將區塊插入到區塊鏈中 ```go func (d *Downloader) importBlockResults(results []*fetchResult) error { ... if index, err := d.blockchain.InsertChain(blocks); err != nil { .... } } ``` ②:建立一個新的以太坊協議管理器,也會將區塊插入到鏈中 ```go func NewProtocolManager(...) (*ProtocolManager, error) { ... n, err := manager.blockchain.InsertChain(blocks) } ``` ③:插入側鏈資料 ```go func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator) (int, error) { ... if _, err := bc.insertChain(blocks, false); err != nil { .... } } ``` ④:從本地檔案匯入鏈 ```go func (api *PrivateAdminAPI) ImportChain(file string) (bool, error) { if _, err := api.eth.BlockChain().InsertChain(blocks); err != nil { .... } } ``` ⑤:fetcher同步匯入塊 ```go func (f *Fetcher) insert(peer string, block *types.Block) { ... if _, err := f.insertChain(types.Blocks{block}); err != nil { ... } } ``` 以上就是比較常見的需要將區塊上鍊的動作。呼叫的核心方法就是: ```go func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, error) {} ``` ------ > 獲取區塊鏈所有相關文章以及資料,請參閱:https://github.com/blockchainGuide/ ## 插入資料到blockchain中 ①:如果鏈正在中斷,直接返回 ```GO if atomic.LoadInt32(&bc.procInterrupt) == 1 { return 0, nil } ``` ②:開啟並行的簽名恢復 ```GO senderCacher.recoverFromBlocks(types.MakeSigner(bc.chainConfig, chain[0].Number()), chain) ``` ③:開啟並行校驗header ```go abort, results := bc.engine.VerifyHeaders(bc, headers, seals) ``` 校驗`header`是共識引擎所要做的事情,我們這裡只分析`ethash`它的實現。 ```go func (ethash *Ethash) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { .... errors[index] = ethash.verifyHeaderWorker(chain, headers, seals, index) } ``` ```go func (ethash *Ethash) verifyHeaderWorker(chain consensus.ChainReader, headers []*types.Header, seals []bool, index int) error { var parent *types.Header if index == 0 { parent = chain.GetHeader(headers[0].ParentHash, headers[0].Number.Uint64()-1) } else if headers[index-1].Hash() == headers[index].ParentHash { parent = headers[index-1] } if parent == nil { return consensus.ErrUnknownAncestor } if chain.GetHeader(headers[index].Hash(), headers[index].Number.Uint64()) != nil { return nil // known block } return ethash.verifyHeader(chain, headers[index], parent, false, seals[index]) } ``` 首先會呼叫`verifyHeaderWorker`進行校驗,主要檢驗塊的祖先是否已知以及塊是否已知,接著會呼叫`verifyHeader`進行更深的校驗,也是最核心的校驗,大概做了以下幾件事: 1. header.Extra*不可超過32位元組* 2. header.Time*不能超過15秒,15秒以後的就被認定為未來的塊* 3. *當前header的時間戳不可以等於父塊的時間戳* 4. *根據難度計算演算法得出的expected必須和header.Difficulty 一致。* 5. *Gas limit 要 <= 2 ^ 63-1* 6. *gasUsed<= gasLimit* 7. *Gas limit 要在允許範圍內* 8. *塊號必須是父塊加1* 9. *根據 ethash.VerifySeal去驗證塊是否滿足POW難度要求* 到此驗證header的事情就做完了。 ④:迴圈校驗body ```go block, err := it.next() -> ValidateBody -> VerifyUncles ``` 包括以下錯誤: - **block**已知 - **uncle**太多 - 重複的**uncle** - **uncle**是祖先塊 - **uncle**雜湊不匹配 - 交易雜湊不匹配 - 未知祖先 - 祖先塊的狀態無法獲取 4.1 如果`block`存在,且是已知塊,則寫入已知塊。 ```go bc.writeKnownBlock(block) ``` 4.2 如果是祖先塊的狀態無法獲取的錯誤,則作為側鏈插入: ```go bc.insertSideChain(block, it) ``` 4.3 如果是未來塊或者未知祖先,則新增未來塊: ```go bc.addFutureBlock(block); ``` 注意這裡的新增 futureBlock,會被扔進futureBlocks裡面去,在NewBlockChain的時候會開啟新的goroutine: ```go go bc.update() ``` ```go func (bc *BlockChain) update() { futureTimer := time.NewTicker(5 * time.Second) for{ select{ case <-futureTimer.C: bc.procFutureBlocks() } } } ``` ```go func (bc *BlockChain) procFutureBlocks() { ... for _, hash := range bc.futureBlocks.Keys() { if block, exist := bc.futureBlocks.Peek(hash); exist { blocks = append(blocks, block.(*types.Block)) } } ... for i := range blocks { bc.InsertChain(blocks[i : i+1]) } } } ``` 會開啟一個計時器,每5秒就會去執行插入這些未來的塊。 4.4 如果是其他錯誤,直接中斷,並且報告壞塊。 ```go bc.futureBlocks.Remove(block.Hash()) ... bc.reportBlock(block, nil, err) ``` ⑤:沒有校驗錯誤 5.1 如果是壞塊,則報告; ```go if BadHashes[block.Hash()] { bc.reportBlock(block, nil, ErrBlacklistedHash) return it.index, ErrBlacklistedHash } ``` 5.2 如果是未知塊,則寫入未知塊; ```go if err == ErrKnownBlock { logger := log.Debug if bc.chainConfig.Clique == nil { logger = log.Warn } ... if err := bc.writeKnownBlock(block); err != nil { return it.index, err } stats.processed++ lastCanon = block continue } ``` 5.3 根據給定trie,建立狀態; ```go parent := it.previous() if parent == nil { parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } statedb, err := state.New(parent.Root, bc.stateCache) ``` 5.4執行塊中的交易: (**稍後會在下節對此進行詳細分析**) ```go receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) ``` 5.5 使用預設的validator校驗狀態: ```go bc.validator.ValidateState(block, statedb, receipts, usedGas); ``` 5.6 將塊寫入到區塊鏈中並獲取狀態: (**稍後會在下節對此進行詳細分析**) ```go status, err := bc.writeBlockWithState(block, receipts, logs, statedb, false) ``` ⑥:校驗寫入區塊的狀態 - `CanonStatTy` : 插入成功新的block - `SideStatTy`:插入成功新的分叉區塊 - `Default`:插入未知狀態的block ⑦:如果還有塊,並且是未來塊的話,那麼將塊新增到未來塊的快取中去 ```go bc.addFutureBlock(block) ``` 至此`insertChain` 大概介紹清楚。 ----- ### 執行塊中交易 在我們將區塊上鍊,有一個關鍵步驟就是執行區塊交易: ```go receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) ``` 進入函式,具體分析: ①:準備要用的欄位,迴圈執行交易 關鍵函式:`ApplyTransaction`,根據此函式返回收據。 1.1 將交易結構轉成`Message`結構 ```go msg, err := tx.AsMessage(types.MakeSigner(config, header.Number)) ``` 1.2 建立要在EVM環境中使用的新上下文 ```go context := NewEVMContext(msg, header, bc, author) ``` 1.3 建立一個新環境,其中包含有關事務和呼叫機制的所有相關資訊。 ```GO vmenv := vm.NewEVM(context, statedb, config, cfg) ``` 1.4 將交易應用到當前狀態(包含在env中) ```go _, gas, failed, err := ApplyMessage(vmenv, msg, gp) ``` 這部分程式碼繼續跟進: ```go func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) { return NewStateTransition(evm, msg, gp).TransitionDb() } ``` `NewStateTransition` 是一個狀態轉換物件,`TransitionDb()` 負責轉換交易狀態,繼續跟進: 先進行`preCheck`,用來校驗`nonce`是否正確 ```go st.preCheck() if st.msg.CheckNonce() { nonce := st.state.GetNonce(st.msg.From()) if nonce < st.msg.Nonce() { return ErrNonceTooHigh } else if nonce > st.msg.Nonce() { return ErrNonceTooLow } } ``` 計算所需`gas`: ```go gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul) ``` 扣除`gas`: ```go if err = st.useGas(gas); err != nil { return nil, 0, false, err } ``` ```go func (st *StateTransition) useGas(amount uint64) error { if st.gas < amount { return vm.ErrOutOfGas } st.gas -= amount return nil } ``` 如果是合約交易,則新建一個合約 ```go ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value) ``` 如果不是合約交易,則增加`nonce` ```go st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value) ``` 重點關注`evm.call`方法: *檢查賬戶是否有足夠的氣體進行轉賬* ```go if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { return nil, gas, ErrInsufficientBalance } ``` *如果stateDb不存在此賬戶,則新建賬戶* ```go if !evm.StateDB.Exist(addr) { evm.StateDB.CreateAccount(addr) } ``` *執行轉賬操作* ```go evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) ``` *建立合約* ```go contract := NewContract(caller, to, value, gas) ``` *執行合約* ```go ret, err = run(evm, contract, input, false) ``` 新增餘額 ```go st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice)) ``` 回到`ApplyTransaction` 1.5 呼叫`IntermediateRoot`計算狀態`trie`的當前根雜湊值。 最終確定所有骯髒的儲存狀態,並把它們寫進`trie` ```go s.Finalise(deleteEmptyObjects) ``` 將trie根設定為當前的根雜湊並將給定的`object`寫入到`trie`中 ```go obj.updateRoot(s.db) s.updateStateObject(obj) ``` 1.6 建立收據 ```go receipt := types.NewReceipt(root, failed, *usedGas) receipt.TxHash = tx.Hash() receipt.GasUsed = gas if msg.To() == nil { receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce()) } // Set the receipt logs and create a bloom for filtering receipt.Logs = statedb.GetLogs(tx.Hash()) receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) receipt.BlockHash = statedb.BlockHash() receipt.BlockNumber = header.Number receipt.TransactionIndex = uint(statedb.TxIndex()) ``` ②:最後完成區塊,應用任何共識引擎特定的額外功能(例如區塊獎勵) ```go p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) ``` ```go func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { // 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)) } ``` 到此為止`bc.processor.Process`執行完畢,返回`receipts`. ------ ### 校驗狀態 大致包括4部分的校驗: ①:校驗使用的`gas`是否相等 ```go if block.GasUsed() != usedGas { return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas) } ``` ②:校驗bloom是否相等 ```go rbloom := types.CreateBloom(receipts) if rbloom != header.Bloom { return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) } ``` ③:校驗收據雜湊是否相等 ```go receiptSha := types.DeriveSha(receipts) if receiptSha != header.ReceiptHash { return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha) } ``` ④:校驗merkleroot 是否相等 ```go if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { return fmt.Errorf("invalid merkle root (remote: %x local: %x)", header.Root, root) } ``` ----- ### 將塊和關聯狀態寫入到資料庫 函式:**WriteBlockWithState** ①:計算塊的`total td` ```go ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1) ``` ②:新增待插入塊本身的`td` ,並將此時最新的`total td` 儲存到資料庫中。 ```GO bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd) ``` ③:將塊的`header`和`body`分別序列化到資料庫 ```go rawdb.WriteBlock(bc.db, block) ->WriteBody(db, block.Hash(), block.NumberU64(), block.Body()) ->WriteHeader(db, block.Header()) ``` ④:將狀態寫入底層記憶體`Trie`資料庫 ```go state.Commit(bc.chainConfig.IsEIP158(block.Number())) ``` ⑤:遍歷節點資料寫入到磁碟 ```go triedb.Commit(header.Root, true) ``` ⑥:儲存一個塊的所有交易資料 ```go rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts) ``` ⑦:將新的`head`塊注入到當前鏈中 ```go if status == CanonStatTy { bc.insert(block) } ``` - 儲存分配給規範塊的雜湊 - 儲存頭塊的雜湊 - 儲存最新的快 - 更新`currentFastBlock` ⑧:傳送`chainEvent`事件或者`ChainSideEvent`事件或者`ChainHeadEvent`事件 ```go if status == CanonStatTy { bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs}) if len(logs) > 0 { bc.logsFeed.Send(logs) } if emitHeadEvent { bc.chainHeadFeed.Send(ChainHeadEvent{Block: block}) } } else { bc.chainSideFeed.Send(ChainSideEvent{Block: block}) } ``` 到此writeBlockWithState 結束,從上面可以知道,insertChain的最終還是呼叫了`writeBlockWithState`的insert方法完成了最終的上鍊入庫動作。 最後整個`insertChain` *函式,如果已經完成了插入,就傳送`chain head`事件* ```go defer func() { if lastCanon != nil && bc.CurrentBlock().Hash() == lastCanon.Hash() { bc.chainHeadFeed.Send(ChainHeadEvent{lastCanon}) } }() ``` 比較常見的有這麼幾處會進行訂閱`chain head` 事件: 1. 在tx_pool.go中,收到此事件會進行換head的操作 ```go pool.chainHeadSub = pool.chain.SubscribeChainHeadEvent(pool.chainHeadCh) ``` 2. 在worker.go中,其他節點的礦工收到此事件就會停止當前的挖礦,繼續下一個挖礦任務 ```go worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh) ``` 到此整個區塊上鍊入庫就完成了,最後再送上一張總結的圖: ![image-20201224104046731](https://tva1.sinaimg.cn/large/0081Kckwgy1glyqxrr9p0j31530u0jz0.jpg) ----- ## 參考 > https://mindcarver.cn > > https://github.com/blockch