以太坊C++系列(06)-以太坊出块流程

概述

描述一下基于 C++ 版本的以太坊代码,从交易到出块上链的整个流程做个备忘。以太坊公链采用的共识算法为Pow,联盟链采用的算法是PBFT。

交易

交易接收

交易的接收有两大类,一类是通过RPC接口eth_sendRawTransaction()eth_sendTransaction()接收到的交易。eth_sendRawTransaction()接口接收到的是已经使用私钥签名后的交易,交易接收处接口为ClientBase::injectTransaction()。如果是eth_sendTransaction()接口发送过来的交易,那么需要将账号解锁,后台会自动用解锁过后的账号的私钥给该交易签名。交易接收处接口为JuClient::submitTransaction()
另外一类是其他节点通过p2p广播收到的交易。在EthereumPeerObserver::onPeerTransactions()接收到的交易放入未验证交易队列待验证。

交易入交易队列

通过RPC接收到交易,会调用TransactionQueue::import()导入交易队列,在导入交易队列过程中,会对交易进行一系列检查。比如,在交易队列中是否已存在该交易,在已丢弃的队列中是否存在该交易,该交易在链上是否已存在等等。而通过p2p收到的交易,则唤醒专门验证的线程,调用TransactionQueue::verifierBody()从未验证交易队列中取出交易进行验证。如果检查通过,则将该交易存入当前交易队列。同时,触发回调m_onReady()通知打包线程交易队列中已有交易触发JuClient::syncTransactionQueue()开始对交易进行打包。

交易的广播

上面说到了,有一类交易是其他节点通过p2p广播过来的。交易进入交易队列之后执行JuClient::syncTransactionQueue()开始对交易进行打包。在打包的过程中,会调用EthereumHost::noteNewTransactions()通知p2p将交易队列中的交易广播给其他节点。

交易打包

当有交易进入交易队列时,会不断触发区块调用Block::sync()在交易队列里面获取交易进行打包交易。在打包的过程中,会对交易进行预执行,预执行会丢弃掉一些不符合要求的交易。比如:超过区块最大gas的交易,已上链的交易(防止重放攻击)等。这些交易会放入交易队列的丢弃列表里面。

区块

区块共识启动

由于使用的是PBFT共识机制,每隔一定的时间就需要出块。所以在交易打包的过程中,如果到了规定出块的时间。那么就算交易队列中还有待打包的交易,也会跳出交易打包流程。不管是待出块节点还是非出块节点都会执行交易的打包,如果检测到待出的区块中有交易,那么就会调用PBFT::notify()启动共识流程。如果是待出块节点,那么状态由Initial状态进入WaitingBlock状态。而对于非出块节点,则从状态Initial状态进入WaitingPrePrepare状态。

区块共识状态转换

以正常流程出块,共识状态转换如下所示:
对于待出块节点:Initial --> WaitingBlock --> Waiting_2fp1_Prepare --> Waiting_2fp1_Commit --> ConsensusFinished --> WaitingBlockImportFinish --> Initial
对于非出块节点:Initial --> WaitingPrePrepare --> Waiting_2fp1_Prepare --> Waiting_2fp1_Commit --> ConsensusFinished --> WaitingBlockImportFinish --> Initial
对各个状态解释如下:

  • Initial:初始化状态
  • WaitingBlock:等待最终区块数据(待出块节点)
  • WaitingPrePrepare:等待最终区块数据(非出块节点)
  • Waiting_2fp1_Prepare:等待2f+1个节点已经收到最终的区块数据
  • Waiting_2fp1_Commit:等待2f+1个节点提交签名
  • ConsensusFinished:共识结束
  • WaitingBlockImportFinish:等待区块入链

当然,还有很多异常状态,比如等待的过程中出现超时转到那种状态,为了简化问题,暂不考虑。
稍微解释一下,为什么待出块节点,在已经将交易打包好的情况下面,进入的是WaitingBlock 而不是直接将打包好的区块广播出去直接进入WaitingPrePrepare
这是因为在以太坊公链中,启用的是Pow共识算法。也就是说已经将交易打好包的块,此时还缺nonce,需要经过挖矿找到合适的nonce才能确定最终的块。所以此时的状态还是WaitingBlock。我们的联盟链还保留了这个"挖矿"的流程。但是挖矿的流程没有去找块的nonce,所以整个挖矿的流程会非常非常快。走完这个挖矿流程,就确定了最终的快,调用PBFT::start()WaitingBlock状态进入Waiting_2fp1_Prepare状态。同时广播当前出块的一些条件,比如节点列表,当前最大块高,当前块的哈希,当前块的父哈希等等。同时广播该区块的区块数据。这样非出块节点收到出块节点广播过来的区块数据就有了块的数据,从WaitingPrePrepare状态进入Waiting_2fp1_Prepare状态。

区块入区块队列

当区块的共识进入ConsensusFinished状态之后,会通过回调函数PBFT::setConsensusFinishedFunc()调用JuClient::submitBlock()将区块进行基础检查。主要基础检查区块的交易树根节点,共识签名,区块队列中是否已存在该区块,链上已存在该区块等等。如果检查通过,则将区块放入区块的未验证列表,同时触发m_moreToVerify信号,启动线程对区块进行更细致的检查。如果此细致的检查通过,则将区块从未验证列表中取出放入已验证列表,同时触发信号m_onReady()回调到JuClient::onBlockQueueReady(),触发Client::syncBlockQueue()执行区块的入链写入数据库的流程。
当然,除了共识结束会有区块入区块队列,还有另外一个p2p同步过来的区块也会入区块队列,后续处理流程是一致的。

区块入链写入数据库

触发入链流程之后,执行BlockChain::import()进行入链。主要对入链区块调用BlockChain::verifyBlock()的更进一步检查(在入区块队列有调用此函数进行块的初步检查)。如果检查通过,则调用区块Block::enactOn() --> Block::sync()进行交易的执行,以及将区块,交易以及其他数据写入关系数据库。写入成功之后,调用JuClient::onChainChanged()触发EthereumHost::maintainBlocks()对最新交易进行官博,清理交易队列中上链的交易,更新PBFT共识算法的视图view,通知PBFT共识算法区块已入量,将状态由WaitingBlockImportFinish状态进入Initial状态。

自此,出块整个流程完毕。

流程图

这是我之前为了理解sendTransaction()画的一个交易时序图,放上来方便理解部分流程。

您的支持将鼓励我继续创作!
0%