区块链区块链,即区块组成的链,不妨先从区块谈起。这一篇我们将着眼于区块链的一些基本操作。在区块链中,区块存储有效信息,在阅读源代码之前,我们应该对区块头,区块体,区块链这些基本的数据结构有所了解。
数据结构
Block
, Header
, BlockChain
的数据结构请查阅 go-ethereum 源码笔记(概览)
区块链基本操作
创世区块
在 go-ethereum 源码笔记(cmd 模块-geth 命令) 这一篇,我们提到有一个 geth init
命令,它可以用来创建创世区块。如果我们将本地的 geth 节点连接测试网络或主网的话,我们不会再进行创世区块的创建,因为区块链已经存在了,这时候应该是从其他节点进行同步。而如果我们需要运行一个私有链的话,这时候就需要一个创建一个创世区块。这部分代码在 core/genesis.go
中。
数据结构
genesis.go
会定义创世区块的数据结构,提供创建,查询创世区块的方法。
首先看 Genesis 结构体,它定义了创世区块应包含的数据。
1 | type Genesis struct { |
伴随创世区块的还有创世账户。
1 | type GenesisAlloc map[common.Address]GenesisAccount |
创建创世区块
SetupGenesisBlock
函数用来在数据库中写入创世区块。
1 | func SetupGenesisBlock(db ethdb.Database, genesis *Genesis) (*params.ChainConfig, common.Hash, error) { |
SetupGenesisBlock
会根据创世区块返回一个区块链的配置。从 db 参数中拿到的区块里如果没有创世区块的话,首先提交一个新区块。接着通过调用 genesis.configOrDefault(stored)
拿到当前链的配置,测试兼容性后将配置写回 DB 中。最后返回区块链的配置信息。
Genesis
有一个 ToBlock
方法,它会根据 Genesis
的数据,使用基于内存的数据库,创建一个区块并返回(通过 types.NewBlock
)。
1 | func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { |
Commit 方法将给定的 genesis
的区块和 state
写入数据库。
1 | func (g *Genesis) Commit(db ethdb.Database) (*types.Block, error) { |
使用 NewBlockChain
初始化区块链
1 | func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config) (*BlockChain, error) { |
BlockChain
的初始化需要 ethdb.Database
, *CacheConfig
, params.ChainConfig
, consensus.Engine
,vm.Config
参数。它们分别表示 db 对象;缓存配置(在 core/blockchain.go
中定义);区块链配置(可通过 core/genesis.go
中的 SetupGenesisBlock
拿到);一致性引擎(可通过 core/blockchain.go
中的 CreateConsensusEngine
得到);虚拟机配置(通过 core/vm
定义)这些实参需要提前定义,以 eth 的 backend.go
为例,你可以在初始化 Ethereum 对象时看到这些参数是怎么初始化的,当然你也可以查看对应的测试代码学习 NewBlockChain
如何使用。
回到 NewBlockChain
的具体代码,首先判断是否有默认 cacheConfig
,如果没有根据默认配置创建 cacheConfig
,再通过 hashicorp 公司的 lru 模块创建 bodyCache
, bodyRLPCache
等缓存对象(lru 是 last recently used 的缩写,常见数据结构,不了解的朋友请自行查阅相关资料),根据这些信息创建 BlockChain
对象,然后通过调用 BlockChain
的 SetValidator
和 SetProcessor
方法创建验证器和处理器,接下来通过 NewHeaderChain
获得区块头,尝试判断创始区块是否存在,bc.loadLastState()
加载区块最新状态,最后检查当前状态,确保本地运行的区块链上没有非法的区块。接下来我们深入到 loadLastState
方法。
1 | func (bc *BlockChain) loadLastState() error { |
loadLastState
会从数据库中加载区块链状态,首先通过 GetHeadBlockHash
从数据库中取得当前区块头,如果当前区块不存在,即数据库为空的话,通过 Reset
将创始区块写入数据库以达到重置目的。如果当前区块不存在,同样通过 Reset
重置。接下来确认当前区块的世界状态是否正确,世界状态这是一个稍特别的概念,这个过程我们将在之后的文章中描述。如果有问题,则通过 repair
进行修复,repair
中是一个死循环,它会一直回溯当前区块,直到找到对应的世界状态。然后通过 bc.hc.SetCurrentHeader
设置当前区块头,并恢复快速同步区块。
在 NewBlockChain
调用 loadLastState
之后,会判断是否需要硬分叉,BadHashes
是手工配置的区块 hash 值,根据这些值我们可以决定是否以及如何进行硬分叉。最后以 goroutine 的方式调用 bc.update()
。
1 | func (bc *BlockChain) update() { |
update()
的作用是定时处理 Future 区块,简单地来说就是定时调用 procFutureBlocks
。
1 | func (bc *BlockChain) procFutureBlocks() { |
procFutureBlocks
可以从 futureBlocks
拿到需要插入的区块,最终会调用 InsertChain
将区块插入到区块链中。
插入区块
1 | func (bc *BlockChain) InsertChain(chain types.Blocks) (int, error) { |
InsertChain
将尝试将给定的区块插入到规范的区块链中,或者创建一个分支,插入后,会通过 PostChainEvents
触发所有事件。下面我们看看 insertChain
的实现。
1 | func (bc *BlockChain) insertChain(chain types.Blocks) (int, []interface{}, []*types.Log, error) { |
首先做一个健康检查,确保要插入的链是有序且相互连接的。接下来通过 bc.engine.VerifyHeaders
调用一致性引擎来验证区块头是有效的。进入 for i, block := range chain
循环后,接收 results
这个 chan,可以获得一致性引擎获得区块头的结果,如果是已经插入的区块,跳过;如果是未来的区块,时间距离不是很长,加入到 futureBlocks
中,否则返回一条错误信息;如果没能找到该区块祖先,但在 futureBlocks
能找到,也加入到 futureBlocks
中。
加入 futureBlocks
的过程结束后,通过 core/state_processor.go
中的 Process 改变世界状态(关于世界状态的管理,可以阅读后续的文章 go-ethereum 源码笔记(core 模块-状态管理))。在返回收据,日志,使用的 Gas 后。通过 bc.Validator().ValidateState
再次验证,通过后,通过 WriteBlockAndState
写入区块以及相关状态到区块链,WriteBlockAndState
我们接下来会详谈。最后,如果我们生成了一个新的区块头,最新的区块头等于 lastCanon
的哈希值,发布一个 ChainHeadEvent
的事件。
现在我们来看看 WriteBlockAndState
是如何写入区块及相关状态到区块链的。
1 | func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, state *state.StateDB) (status WriteStatus, err error) { |
WriteBlockWithState
将区块以及相关所有的状态写入数据库。首先通过 bc.GetTd(block.ParentHash(), block.NumberU64()-1)
获取待插入区块的总难度,bc.GetTd(bc.currentBlock.Hash(), bc.currentBlock.NumberU64())
计算当前区块的区块链的总难度,externTd := new(big.Int).Add(block.Difficulty(), ptd)
获得新的区块链的总难度。通过 bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd)
写入区块 hash,高度,对应总难度。然后使用 batch
的方式写入区块的其他数据。插入数据后,判断这个区块的父区块是否为当前区块,如果不是,说明存在分叉,调用 reorg
重新组织区块链。插入成功后,调用 bc.futureBlocks.Remove(block.Hash())
从 futureBlocks
中移除区块。
下面我们来看看 reorg
方法。
1 | func (bc *BlockChain) reorg(oldBlock, newBlock *types.Block) error { |
上面提到,reorg
方法用来将新区块链替换本地区块链为规范链。对于老链比新链高的情况,减少老链,让它和新链一样高;否则的话减少新链,待后续插入。潜在的会丢失的交易会被当做事件发布。接着进入一个 for 循环,找到两条链共同的祖先。再将上述减少新链阶段保存的 newChain
一块块插入到链中,更新规范区块链的 key,并且写入交易的查询信息。最后是清理工作,删除交易查询信息,删除日志,并通过 bc.rmLogsFeed.Send
发送消息通知,删除了哪些旧链则通过 bc.chainSideFeed.Send
进行消息通知。
至此,插入区块的操作就完成了。