以太坊原始碼解讀 BlockChain的初始化

weixin_33861800發表於2018-12-19

一、前言

ETHereum服務初始化的時候會呼叫core.SetupGenesisBlock來載入創始區塊,然後呼叫core.NewBlockChain來載入BlockChain模組,這章節主要介紹BlockChain的NewBlockChain函式,BlockChain是以太坊的全節點模式下[區塊鏈鏈式管理的模組,主要負責區塊鏈的追加、回滾、分叉處理等,另外還為前端提供區塊資訊查詢等功能。

二、 BlockChain中的一些概念

在分析原始碼之前,我們先介紹一些基本概念,這些概念在原始碼分析的時候會頻繁提到,理解了這些概念可以幫助大家更好的理解BlockChain的管理機制。

2.1 TD

TD的意思是總難度,一個區塊的TD等於本區塊的所有祖先區塊的難度和加上自己難度的總和。

Block.TD =[ Block.diff Block.parent.diff. ...... Genesis.diff]

以太坊在獲取一個區塊的總難度時,不需要遍歷整個區塊鏈然後將每個區塊的難度值相加,因為BlockChain在插入每個區塊的時候,同時會將這個區塊的TD寫入資料庫,寫入的key是"h" num hash "t", value是td的編碼後的值,也就是說只要知道一個區塊的hash和區塊號就能獲取這個區塊的TD。

2.2 規範鏈

在區塊的建立過程中,可能在短時間內產生一些分叉, 在我們的資料庫裡面記錄的其實是一顆區塊樹。我們會認為其中總難度最高的一條路徑認為是我們的規範的區塊鏈。 有很多區塊雖然也能形成區塊鏈, 但是不是規範的區塊鏈。

2.3 區塊在資料庫的儲存方式

一個區塊在資料庫中並不是整體儲存的,而是分為多個部分單獨儲存,主要分為如下幾個部分:

區塊頭、區塊體、總難度、收據、區塊號、狀態

他們在資料庫中是以單獨的鍵值對儲存,儲存方式如下:

Key ------------Value

‘h’ num ‘n’------------規範鏈上區塊號對應的hash值

‘h’ num hash ‘t’------------區塊的總難度

‘H’ hash---------------區塊號

‘h’ num hash-------------Header的RLP編碼值

‘b’ num hash--------------Body的RLP編碼值

‘r’ num hash--------------Receipts的RPL編碼值

從上面這個表可以看出,已知一個區塊的hash可以從資料庫中獲取一個區塊的區塊號,已知一個區塊的hash和區塊號可以獲取一個區塊的總難度、區塊頭和區塊體以及收據。另外僅僅知道一個區塊的區塊號只能獲取規範鏈上對應區塊號的區塊hash。

區塊的狀態不是簡單的用key-value鍵值對儲存,以太坊中有一個專門的類stateDB來管理區塊的狀態, stateDB可以通過區塊的StateRoot從資料庫中構建整個世界狀態。

三、BlockChain資料結構

BlockChain模組定義在core/blockchain.go中。我們首先來看它的資料結構。

  type BlockChain struct {
   //鏈相關的配置
   chainConfig *params.ChainConfig // Chain & network configuration
   cacheConfig *CacheConfig        // Cache configuration for pruning
   //區塊儲存的資料庫
   db     ethdb.Database // Low level persistent database to [STO](http://www.btb8.com/s/sto/)re final c[ONT](http://www.btb8.com/ont/)ent in
   triegc *prque.Prque   // Priority queue mapping block numbers to tries to gc
   gcproc time.Duration  // Accumulates canonical block processing for trie dumping

   hc            *HeaderChain
   rmLogsFeed    e[VEN](http://www.btb8.com/ven/)t.Feed
   chainFeed     event.Feed
   chainSideFeed event.Feed
   chainHeadFeed event.Feed
   logsFeed      event.Feed
   scope         event.SubscriptionScope
   genesisBlock  *types.Block

   mu      sync.RWMutex // global mutex for locking chain operations
   chainmu sync.RWMutex // blockchain insertion lock
   procmu  sync.RWMutex // block processor lock

   checkpoint       int          // checkpoint counts towards the new checkpoint
   //當前規範鏈的頭區塊
   currentBlock     atomic.Value // Current head of the block chain
   currentFastBlock atomic.Value // Current head of the fast-sync chain (may be above the block chain!)

   stateCache    state.Database // State database to reuse between imports (contains state cache)
   bodyCache     *lru.Cache     // Cache for the most recent block bodies
   bodyRLPCache  *lru.Cache     // Cache for the most recent block bodies in RLP encoded format
   receiptsCache *lru.Cache     // Cache for the most recent receipts per block
   blockCache    *lru.Cache     // Cache for the most recent entire blocks
   //未來區塊,大於當前時間15秒但小於30秒的區塊,暫時不能處理,但是後面可以處理
   futureBlocks  *lru.Cache     // future blocks are blocks added for later processing

   quit    chan struct{} // blockchain quit channel
   running int32         // running must be called atomically
   // procInterrupt must be atomically called
   procInterrupt int32          // interrupt signaler for block processing
   wg            sync.WaitGroup // chain processing wait group for shutting down

   engine    consensus.Engine
   processor Processor // block processor interface
   validator Validator // block and state validator interface
   vmConfig  vm.Config

   badBlocks      *lru.Cache              // Bad block cache
   shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block.
}

我們在BlockChain這個結構中並沒有看到類似連結串列一樣的結構來表示區塊鏈,因為區塊鏈經過一段時間的延伸後體積會比較大,如果將所有的區塊鏈都載入到BlockChain這個結構中的話,整個結構的記憶體消耗會越來越大,所以BlockChain只儲存了一個頭區塊currentBlock,其他的區塊都在資料庫中,通過頭區塊中的父區塊hash可以很方便的找到它的父區塊,以此類推可以把所有的區塊從資料庫中都取出來。

四、NewBlockChain函式

  func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(block *types.Block) bool) (*BlockChain, error) {
   if cacheConfig == nil {
      cacheConfig = &CacheConfig{
         TrieNodeLimit: 256 * 1024 * 1024,
         TrieTimeLimit: 5 * time.Minute,
      }
   }
   bodyCache, _ := lru.New(bodyCacheLimit)
   bodyRLPCache, _ := lru.New(bodyCacheLimit)
   receiptsCache, _ := lru.New(receiptsCacheLimit)
   blockCache, _ := lru.New(blockCacheLimit)
   futureBlocks, _ := lru.New(maxFutureBlocks)
   badBlocks, _ := lru.New(badBlockLimit)
   //1.初始化BlockChain物件
   bc := &BlockChain{
      chainConfig:    chainConfig,
      cacheConfig:    cacheConfig,
      db:             db,
      triegc:         prque.New(nil),
      stateCache:     state.NewDatabase(db),
      quit:           make(chan struct{}),
      shouldPreserve: shouldPreserve,
      bodyCache:      bodyCache,
      bodyRLPCache:   bodyRLPCache,
      receiptsCache:  receiptsCache,
      blockCache:     blockCache,
      futureBlocks:   futureBlocks,
      engine:         engine,
      vmConfig:       vmConfig,
      badBlocks:      badBlocks,
   }
   bc.SetValidator(NewBlockValidator(chainConfig, bc, engine))
   bc.SetProcessor(NewStateProcessor(chainConfig, bc, engine))

   var err error
   bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.getProcInterrupt)
   if err != nil {
      return nil, err
   }
   bc.genesisBlock = bc.GetBlockByNumber(0)
   if bc.genesisBlock == nil {
      return nil, ErrNoGenesis
   }
   //2.從資料庫中讀取儲存的最新頭區塊賦值給blockchain.currentBlock
   if err := bc.loadLastState(); err != nil {
      return nil, err
   }
   //3.檢查本地的規範鏈中有沒有壞區塊,如果由壞區塊,就將當前規範鏈回滾到壞區塊的前一個區塊
   //遍歷壞區塊列表
   for hash := range BadHashes {
      //使用hash從資料庫中獲取一個區塊,如果能獲取到說明資料資料庫中存在這個壞區塊,但還不能確定這個壞區塊在不在規範鏈上
      if header := bc.GetHeaderByHash(hash); header != nil {
         //通過區塊號到規範鏈上查詢存不存在這個區塊,如果存在則回滾規範鏈
         headerByNumber := bc.GetHeaderByNumber(header.Number.Uint64())
         // make sure the headerByNumber (if present) is in our current canonical chain
         if headerByNumber != nil && headerByNumber.Hash() == header.Hash() {
            log.Error("Found bad hash, rewinding chain", "number", header.Number, "hash", header.ParentHash)
            bc.SetHead(header.Number.Uint64() - 1)
            log.Error("Chain rewind was successful, resuming normal operation")
         }
      }
   }
   // 4.啟動處理未來區塊的go程
   go bc.update()
   return bc, nil
}

通過前面的分析我們知道,BlockChain中的僅僅只儲存了區塊鏈的頭區塊,通過這個頭區塊和區塊在資料庫中的key-value儲存方式,BlockChain可以管理一整條鏈,假如在執行的過程中計算機斷電了,下次再啟動時BlockChain該如何找回頭區塊呢?為了解決這個問題,BlockChain在資料庫中專門使用一個鍵值對來儲存這個頭區塊的hash,這個鍵值對的key是”LastBlock”,value是頭區塊的hash。BlockChain每追加一個新區塊都會更新資料庫中“LastBlock”的值,這樣即使斷電或者關機,下次啟動以太坊時BlockChain也能從資料庫中將上一次儲存的頭區塊取出。

上面第2步loadLastSate就是以“LastBlock”為key從資料庫中載入之前儲存的頭區塊。

  func (bc *BlockChain) loadLastState() error {
   // 1.讀取上次儲存的區塊頭賦值給Blockchain的currentBlock
   head := rawdb.ReadHeadBlockHash(bc.db)
   if head == (common.Hash{}) {
      //如果從資料庫中取出來的區塊的hash是空hash,則回滾到創世區塊,Reset方法是刪除創世區塊後的所有區塊,從新構建區塊鏈
      log.Warn("Empty database, resetting chain")
      return bc.Reset()
   }
   // Make sure the entire head block is available
   // 2.通過這個hash從資料庫中取Block,如果不能取出,則回滾到創世區塊。
   currentBlock := bc.GetBlockByHash(head)
   if currentBlock == nil {
      // Corrupt or empty database, init from scratch
      log.Warn("Head block missing, resetting chain", "hash", head)
      return bc.Reset()
   }
   // 3.確保這個區塊的狀態從資料庫中是可獲取的
   // Make sure the state as[SOC](http://www.btb8.com/soc/)iated with the block is available
   if _, err := state.New(currentBlock.Root(), bc.stateCache); err != nil {
      // Dangling block without a state associated, init from scratch
      log.Warn("Head state missing, re[PAI](http://www.btb8.com/pai/)ring chain", "number", currentBlock.Number(), "hash", currentBlock.Hash())
      if err := bc.repair(¤tBlock); err != nil {
         return err
      }
   }
   //上面的驗證都通過,說明這個區塊沒有問題, 可以賦值給BlockChain的currentBlock
   bc.currentBlock.Store(currentBlock)

   //4.更新headchain的頭區塊頭,,如果資料庫中沒有儲存頭鏈的頭區塊,則用BlockChain的currentBlock的區塊頭替代
   currentHeader := currentBlock.Header()
   if head := rawdb.ReadHeadHeaderHash(bc.db); head != (common.Hash{}) {
      if header := bc.GetHeaderByHash(head); header != nil {
         currentHeader = header
      }
   }
   bc.hc.S[ETC](http://www.btb8.com/etc/)urrentHeader(currentHeader)
   //5.更新fast模式下的的頭區塊,如果資料庫中沒有儲存fast的頭區塊,則用BlockChain的currentBlock來替代

   // Restore the last known head fast block
   bc.currentFastBlock.Store(currentBlock)
   if head := rawdb.ReadHeadFastBlockHash(bc.db); head != (common.Hash{}) {
      if block := bc.GetBlockByHash(head); block != nil {
         bc.currentFastBlock.Store(block)
      }
   }

   // Issue a status log for the user
   currentFastBlock := bc.CurrentFastBlock()

   headerTd := bc.GetTd(currentHeader.Hash(), currentHeader.Number.Uint64())
   blockTd := bc.GetTd(currentBlock.Hash(), currentBlock.NumberU64())
   fastTd := bc.GetTd(currentFastBlock.Hash(), currentFastBlock.NumberU64())

   log.Info("Loaded most recent local header", "number", currentHeader.Number, "hash", currentHeader.Hash(), "td", headerTd, "age", common.PrettyAge(time.Unix(currentHeader.Time.Int64(), 0)))
   log.Info("Loaded most recent local full block", "number", currentBlock.Number(), "hash", currentBlock.Hash(), "td", blockTd, "age", common.PrettyAge(time.Unix(currentBlock.Time().Int64(), 0)))
   log.Info("Loaded most recent local fast block", "number", currentFastBlock.Number(), "hash", currentFastBlock.Hash(), "td", fastTd, "age", common.PrettyAge(time.Unix(currentFastBlock.Time().Int64(), 0)))

   return nil
}

上面的程式碼還涉及到兩個概念,一個是HeaderChain,另一個是fastBlock,HeaderChain也是一條鏈,它和BlockChain的區別是HeaderChain中的區塊只有區塊頭沒有區塊體,BlockChain中的區塊既包含區塊頭也包含區塊體,在fast同步模式下,從其他節點會有優先同步一條只包含區塊頭的區塊鏈,也就是HeaderChain,資料庫中會有一個專門的key-value鍵值對來儲存HeaderChain的頭,這個鍵是“LastHeader”。上面第4步就是從資料庫中讀取這個頭,然後將它賦值給HeadChain結構的currentHeader,如果資料庫中不存在,則直接用currentBlock來替代。

fastBlock是fast同步模式下,完整構建一個區塊鏈時的頭區塊,前面我們提到說fast模式下會優先從其他節點同步一個條只包含區塊頭的鏈,但是在隨後會慢慢同步區塊體和收據,實現完整的區塊鏈的同步,而fastBlock就是這個完整的鏈的頭區塊,區塊體和區塊頭同步會比較慢,所以一般會滯後於頭鏈的更新,所以fastBlock的區塊高度一般會小於currentHeader的區塊高度。資料庫中會有一個專門的key-value來儲存這個頭區塊,這個鍵是“LastFast”。上面的第5步就是從資料庫中讀取這個頭區塊賦值給BlockChain的currentFastBlock,如果資料庫中不存在,則直接用currentBlock來替代。

NewBlockChain程式碼塊的第4步會開啟一個處理未來區塊的go程,什麼是未來區塊呢?如果從網路上收到一個區塊,它的時間戳大於當前時間15s但是小於30s,BlockChain會把這個區塊放入到自己的一個快取中,然後啟動一個定時器,每隔5s會去檢查一下哪些區塊可以插入的區塊鏈中,如果滿足條件則將區塊插入到區塊鏈中。如果從網路上收到一個區塊,它的時間戳大於當前時間30s,則會丟棄這個區塊。

五、總結

BlockChain的主要功能是管理規範鏈,而管理一條規範鏈只要獲取到這條鏈的頭區塊即可,所以NewBlockChain的主要功能就是從資料庫中載入規範鏈的頭區塊到BlockChain中,另外它還檢查了規範鏈中有沒有壞區塊、啟動一個處理未來區塊的go協程。

相關文章