以太坊源码分析(四):以太坊服务初始化

一、前言

上一节中我们看到Ethereum服务初始化的时候会调用core.SetupGenesisBlock来加载创始区块,然后调用core.NewBlockChain来加载BlockChain模块,这章节主要介绍BlockChain的NewBlockChain函数,BlockChain是以太坊的全节点模式下区块链链式管理的模块,主要负责区块链的追加、回滚、分叉处理等,另外还为前端提供区块信息查询等功能。

二、 BlockChain中的一些概念

在分析源码之前,我们先介绍一些基本概念,这些概念在源码分析的时候会频繁提到,理解了这些概念可以帮助大家更好的理解BlockChain的管理机制。

2.1 TD

TD的意思是总难度,一个区块的TD等于本区块的所有祖先区块的难度和加上自己难度的总和。

以太坊在获取一个区块的总难度时,不需要遍历整个区块链然后将每个区块的难度值相加,因为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 store final content 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 event.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 associated 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, repairing 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.SetCurrentHeader(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程,下一章节我们将介绍BlockChain的inertChain方法。

-END-

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181123G1PYS300?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券