死磕以太坊源码分析之区块上链入库
配合以下代码进行阅读:https://github.com/blockchainGuide/
写文不易,给个小关注,有什么问题可以指出,便于大家交流学习。
引言
不管是矿工挖矿还是Fetcher
同步,Downloader
同步,或者是导入本地文件等等,最中都是将区块上链入库。接下来我们就详细分析这部分的动作。
几处可能调用的地方
①:在Downloader同步最后会将区块插入到区块链中
1 | func (d *Downloader) importBlockResults(results []*fetchResult) error { |
②:创建一个新的以太坊协议管理器,也会将区块插入到链中
1 | func NewProtocolManager(...) (*ProtocolManager, error) { |
③:插入侧链数据
1 | func (bc *BlockChain) insertSideChain(block *types.Block, it *insertIterator) (int, error) { |
④:从本地文件导入链
1 | func (api *PrivateAdminAPI) ImportChain(file string) (bool, error) { |
⑤:fetcher同步导入块
1 | func (f *Fetcher) insert(peer string, block *types.Block) { |
以上就是比较常见的需要将区块上链的动作。调用的核心方法就是:
1 | func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, error) {} |
获取区块链所有相关文章以及资料,请参阅:https://github.com/blockchainGuide/
插入数据到blockchain中
①:如果链正在中断,直接返回
1 | if atomic.LoadInt32(&bc.procInterrupt) == 1 { |
②:开启并行的签名恢复
1 | senderCacher.recoverFromBlocks(types.MakeSigner(bc.chainConfig, chain[0].Number()), chain) |
③:开启并行校验header
1 | abort, results := bc.engine.VerifyHeaders(bc, headers, seals) |
校验header
是共识引擎所要做的事情,我们这里只分析ethash
它的实现。
1 | func (ethash *Ethash) VerifyHeaders(chain consensus.ChainReader, headers []*types.Header, seals []bool) (chan<- struct{}, <-chan error) { |
1 | func (ethash *Ethash) verifyHeaderWorker(chain consensus.ChainReader, headers []*types.Header, seals []bool, index int) error { |
首先会调用verifyHeaderWorker
进行校验,主要检验块的祖先是否已知以及块是否已知,接着会调用verifyHeader
进行更深的校验,也是最核心的校验,大概做了以下几件事:
- header.Extra不可超过32字节
- header.Time不能超过15秒,15秒以后的就被认定为未来的块
- 当前header的时间戳不可以等于父块的时间戳
- 根据难度计算算法得出的expected必须和header.Difficulty 一致。
- Gas limit 要 <= 2 ^ 63-1
- gasUsed<= gasLimit
- Gas limit 要在允许范围内
- 块号必须是父块加1
- 根据 ethash.VerifySeal去验证块是否满足POW难度要求
到此验证header的事情就做完了。
④:循环校验body
1 | block, err := it.next() |
包括以下错误:
- block已知
- uncle太多
- 重复的uncle
- uncle是祖先块
- uncle哈希不匹配
- 交易哈希不匹配
- 未知祖先
- 祖先块的状态无法获取
4.1 如果block
存在,且是已知块,则写入已知块。
1 | bc.writeKnownBlock(block) |
4.2 如果是祖先块的状态无法获取的错误,则作为侧链插入:
1 | bc.insertSideChain(block, it) |
4.3 如果是未来块或者未知祖先,则添加未来块:
1 | bc.addFutureBlock(block); |
注意这里的添加 futureBlock,会被扔进futureBlocks里面去,在NewBlockChain的时候会开启新的goroutine:
1 | go bc.update() |
1 | func (bc *BlockChain) update() { |
1 | func (bc *BlockChain) procFutureBlocks() { |
会开启一个计时器,每5秒就会去执行插入这些未来的块。
4.4 如果是其他错误,直接中断,并且报告坏块。
1 | bc.futureBlocks.Remove(block.Hash()) |
⑤:没有校验错误
5.1 如果是坏块,则报告;
1 | if BadHashes[block.Hash()] { |
5.2 如果是未知块,则写入未知块;
1 | if err == ErrKnownBlock { |
5.3 根据给定trie,创建状态;
1 | parent := it.previous() |
5.4执行块中的交易: (稍后会在下节对此进行详细分析)
1 | receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) |
5.5 使用默认的validator校验状态:
1 | bc.validator.ValidateState(block, statedb, receipts, usedGas); |
5.6 将块写入到区块链中并获取状态: (稍后会在下节对此进行详细分析)
1 | status, err := bc.writeBlockWithState(block, receipts, logs, statedb, false) |
⑥:校验写入区块的状态
CanonStatTy
: 插入成功新的blockSideStatTy
:插入成功新的分叉区块Default
:插入未知状态的block
⑦:如果还有块,并且是未来块的话,那么将块添加到未来块的缓存中去
1 | bc.addFutureBlock(block) |
至此insertChain
大概介绍清楚。
执行块中交易
在我们将区块上链,有一个关键步骤就是执行区块交易:
1 | receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) |
进入函数,具体分析:
①:准备要用的字段,循环执行交易
关键函数:ApplyTransaction
,根据此函数返回收据。
1.1 将交易结构转成Message
结构
1 | msg, err := tx.AsMessage(types.MakeSigner(config, header.Number)) |
1.2 创建要在EVM环境中使用的新上下文
1 | context := NewEVMContext(msg, header, bc, author) |
1.3 创建一个新环境,其中包含有关事务和调用机制的所有相关信息。
1 | vmenv := vm.NewEVM(context, statedb, config, cfg) |
1.4 将交易应用到当前状态(包含在env中)
1 | _, gas, failed, err := ApplyMessage(vmenv, msg, gp) |
这部分代码继续跟进:
1 | func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) ([]byte, uint64, bool, error) { |
NewStateTransition
是一个状态转换对象,TransitionDb()
负责转换交易状态,继续跟进:
先进行preCheck
,用来校验nonce
是否正确
1 | st.preCheck() |
计算所需gas
:
1 | gas, err := IntrinsicGas(st.data, contractCreation, homestead, istanbul) |
扣除gas
:
1 | if err = st.useGas(gas); err != nil { |
1 | func (st *StateTransition) useGas(amount uint64) error { |
如果是合约交易,则新建一个合约
1 | ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value) |
如果不是合约交易,则增加nonce
1 | st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1) |
重点关注evm.call
方法:
检查账户是否有足够的气体进行转账
1 | if !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { |
如果stateDb不存在此账户,则新建账户
1 | if !evm.StateDB.Exist(addr) { |
执行转账操作
1 | evm.Transfer(evm.StateDB, caller.Address(), to.Address(), value) |
创建合约
1 | contract := NewContract(caller, to, value, gas) |
执行合约
1 | ret, err = run(evm, contract, input, false) |
添加余额
1 | st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice)) |
回到ApplyTransaction
1.5 调用IntermediateRoot
计算状态trie
的当前根哈希值。
最终确定所有肮脏的存储状态,并把它们写进trie
1 | s.Finalise(deleteEmptyObjects) |
将trie根设置为当前的根哈希并将给定的object
写入到trie
中
1 | obj.updateRoot(s.db) |
1.6 创建收据
1 | receipt := types.NewReceipt(root, failed, *usedGas) |
②:最后完成区块,应用任何共识引擎特定的额外功能(例如区块奖励)
1 | p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) |
1 | func (ethash *Ethash) Finalize(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) { |
到此为止bc.processor.Process
执行完毕,返回receipts
.
校验状态
大致包括4部分的校验:
①:校验使用的gas
是否相等
1 | if block.GasUsed() != usedGas { |
②:校验bloom是否相等
1 | rbloom := types.CreateBloom(receipts) |
③:校验收据哈希是否相等
1 | receiptSha := types.DeriveSha(receipts) |
④:校验merkleroot 是否相等
1 | if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { |
将块和关联状态写入到数据库
函数:WriteBlockWithState
①:计算块的total td
1 | ptd := bc.GetTd(block.ParentHash(), block.NumberU64()-1) |
②:添加待插入块本身的td
,并将此时最新的total td
存储到数据库中。
1 | bc.hc.WriteTd(block.Hash(), block.NumberU64(), externTd) |
③:将块的header
和body
分别序列化到数据库
1 | rawdb.WriteBlock(bc.db, block) |
④:将状态写入底层内存Trie
数据库
1 | state.Commit(bc.chainConfig.IsEIP158(block.Number())) |
⑤:遍历节点数据写入到磁盘
1 | triedb.Commit(header.Root, true) |
⑥:存储一个块的所有交易数据
1 | rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts) |
⑦:将新的head
块注入到当前链中
1 | if status == CanonStatTy { |
- 存储分配给规范块的哈希
- 存储头块的哈希
- 存储最新的快
- 更新
currentFastBlock
⑧:发送chainEvent
事件或者ChainSideEvent
事件或者ChainHeadEvent
事件
1 | if status == CanonStatTy { |
到此writeBlockWithState 结束,从上面可以知道,insertChain的最终还是调用了writeBlockWithState
的insert方法完成了最终的上链入库动作。
最后整个insertChain
函数,如果已经完成了插入,就发送chain head
事件
1 | defer func() { |
比较常见的有这么几处会进行订阅chain head
事件:
在tx_pool.go中,收到此事件会进行换head的操作
1
pool.chainHeadSub = pool.chain.SubscribeChainHeadEvent(pool.chainHeadCh)
在worker.go中,其他节点的矿工收到此事件就会停止当前的挖矿,继续下一个挖矿任务
1
worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh)
到此整个区块上链入库就完成了,最后再送上一张总结的图: