steemd 源码分析3 逻辑主体

2018/06/24 c++ steem blockchain

前面几篇关于steemd的文章,简单描述了steemd中的一些模块和机制,剩下可以值得写的模块可能还剩p2p。再次之前,我们可以先全面了解一下steemd,这样能够使我们全面的了解steemd

本篇文章,就串讲一下steemd逻辑主体,从用户的写入操作到steemd如何处理、传播、出块、同步等等。

transaction的传播

steemd的用户只会有读写2中操作。读操作比较简单,就是通过json_rpc调用各种插件中的get_xxx方法,将用户关心的数据从本地database中读出返回给用户。写操作就比较复杂,因为在区块链中,任何数据的变更都需要让每个节点打成共识,传播出去。在steemd中,所有的变更操作都会成为一个operation,在steemd中有很多种operation

   typedef fc::static_variant<
            vote_operation,
            comment_operation,
            transfer_operation,
            transfer_to_vesting_operation,
            withdraw_vesting_operation,
            limit_order_create_operation,
            limit_order_cancel_operation,
            feed_publish_operation,
            convert_operation,
            account_create_operation,
            account_update_operation,
            witness_update_operation,
            account_witness_vote_operation,
            account_witness_proxy_operation,
            pow_operation,
            custom_operation,
            report_over_production_operation,
            delete_comment_operation,
            custom_json_operation,
            comment_options_operation,
            set_withdraw_vesting_route_operation,
            limit_order_create2_operation,
            placeholder_a_operation,               // A new op can go here
            placeholder_b_operation,               // A new op can go here
            request_account_recovery_operation,
            recover_account_operation,
            change_recovery_account_operation,
            escrow_transfer_operation,
            escrow_dispute_operation,
            escrow_release_operation,
            pow2_operation,
            escrow_approve_operation,
            transfer_to_savings_operation,
            transfer_from_savings_operation,
            cancel_transfer_from_savings_operation,
            custom_binary_operation,
            decline_voting_rights_operation,
            reset_account_operation,
            set_reset_account_operation,
            claim_reward_balance_operation,
            delegate_vesting_shares_operation,
            account_create_with_delegation_operation,
            witness_set_properties_operation,

#ifdef STEEM_ENABLE_SMT
            /// SMT operations
            claim_reward_balance2_operation,
            smt_setup_operation,
            smt_cap_reveal_operation,
            smt_refund_operation,
            smt_setup_emissions_operation,
            smt_set_setup_parameters_operation,
            smt_set_runtime_parameters_operation,
            smt_create_operation,
#endif
            /// virtual operations below this point
            fill_convert_request_operation,
            author_reward_operation,
            curation_reward_operation,
            comment_reward_operation,
            liquidity_reward_operation,
            interest_operation,
            fill_vesting_withdraw_operation,
            fill_order_operation,
            shutdown_witness_operation,
            fill_transfer_from_savings_operation,
            hardfork_operation,
            comment_payout_update_operation,
            return_vesting_delegation_operation,
            comment_benefactor_reward_operation,
            producer_reward_operation
         > operation;

然后用户写操作就是创建一个对应的operation然后将其封装在一个transaction中,然后需要签名的操作,用自己的私钥进行签名,然后发送给steemd

steemd收到这个transaction后,就会运行其逻辑处理,进行一系列的操作:

用户给steemd发送transaction,调用的是插件network_broadcast_api中的broadcast_transaction/broadcast_transaction_synchronous这2个api。

broadcast_transaction是进行的异步逻辑,当steemd收到该交易在本地节点处理完成后即可返回,broadcast_transaction_synchronous是进行同步逻辑,其会阻塞等待用户的这个transaction被打包、出块,然后同步应用到各个节点后再返回。broadcast_transaction_synchronous这个api在最新的代码中已经被删除。

在上图中蓝色线的路线,就是steemd节点通过broadcast_transactionapi收到用户transaction后的主要逻辑。至于什么行为对应什么operation,这些都是客户端完成的。

steemd在收到一个transaction后,先调用插件chain_plugin中的accept_transaction方法。③在accept_transaction,会通过一个队列,将全部transaction发送到一个处理线程中串行处理,在前面讲过json-rpc模块是多线程的。在处理线程中,将队列中的transaction依次取出,然后调用chain::databasepush_transaction方法。

④此处调用push_transaction就是现将用户的变更操作临时应用到本地节点、同时进行验证和校验。

  1. 先创建一个database的写事务,用于时候回滚数据,因为这些变更是临时的。
  2. 调用该transaction中的每个operationvalidate方法,这些validate这里,定义每种operation参数要满足哪些要求。理论上一个transaction中可以封装多个operation
  3. 校验每个transaction的签名和时间。
  4. 将该transaction存储在本地的database中。
  5. 调用通知on_pre_apply_transaction,这里是steemd中使用的一种事件通知机制。这个是在chain::database中使用的比较多的一个机制⑥,可以简单看做一个回调函数链表,其中的回调函数由其他插件⑧在初始化时注册进去。当调用通知时,会依次触发这些回调函数,这里回调函数的参数是transaction
  6. 依次调用通知pre_apply_operation,参数是每个operation
  7. ⑤依次获取每个operation对应的evaluator,然后调用其apply方法。这里是steemd业务逻辑的核心了。这里处理每种operation应该如何变更database中的数据。比如一个转账的operation,该给谁加余额,给谁减余额。evaluator也有一套注册机制,总的来说比较简单,这里就不将了,每个operationevaluator的定义在这里。在这里的这次数据变更在之前创建的写事务中,因此是临时。只有当该transaction被打包、出块、并同步到多数witness节点(75%)上后,其数据变更才是永久的。
  8. 依次调用通知post_apply_operation
  9. 然后将该transaction存储在_pending_tx中。如果轮到本节点打包出块时,该节点就会将_pending_tx中的transaction打包成为一个新的block,这个留在后面讲。

steemdtransaction应用到本地后,然后再调用p2p::p2p_plugin插件的broadcast_transactionapi⑨将其通过p2p网络广播出去⑩,p2p网络的实现是继承自开源项目graphene,这个模块值得将来再开一个专题来讲。

当其他节点通过p2p网络收到一个transaction后⑪,会触发本地节点graphene::net::node的回调函数node_impl::on_message,(在上图中绿色线连接的逻辑都是在p2p网络收到消息后的调用逻辑),在这里,其会根据消息的类型再调用不同的回调函数,当消息是transaction时,其调用node_impl::process_ordinary_message,然后在触发p2p::p2p_plugin中的回调函数impl::handle_transaction⑫。然后再调用插件chain_plugin中的accept_transaction方法⑭。此后运行的逻辑跟③后的都一样的。

此时,用户发送给steemd的一个transaction,在本地节点和其他节点上都会进行一般相同的逻辑处理。我们将这些变更称为临时数据。

打包出块

steemd使用DPOS共识机制,简单来说,只有“超级节点”(witness)才有权收集传播中的transaction,对其进行打包,变成一个block,然后再把这个block传播出去。

当本节点为“超级节点”(witness)时,可以启动witness插件,在配置config.iniplugin项加入witness,例如plugin = witness,同时配置好文件中的witness项和与witness项对应的private-key

witness插件启动后,会启动一个定时器,定时触发调度逻辑maybe_produce_block⑮,该函数会执行witness的调度逻辑(该处可以开个模块另行讲解),如果不该本节点轮次时,就返回等待下一次调度,如果轮到本节点的轮次,则会调用chain::databasegenerate_block⑯,该打包的过程中:

  1. 回滚前面传播transaction时的临时数据变更,因为这些数据可能是不可靠的。因为只有被变成block并传播到各个节点上的数据才是大家公认可靠的数据。
  2. 然后将本节点收集到的被广播的transaction中包含的operation再进行一次校验,该处使用的校验方式是将这些operation在本地应用一次⑰(逻辑跟④中的那些步骤是一样的,可以看上面的描述),这种校验方式时非常重的一个操作,应用完后,还需要再回滚一次数据,这样极大的消耗的性能,极大地限制了该系统的TPS。
  3. 将本地收集的transaction,放入一个block中,然后更新该block的元数据,比如序号、前一个block的id等等,建立该block跟以前block的关系。
  4. 对新生成的block进行签名。
  5. 调用push_block⑱,将新生成的block应用㉒到本地database,这个应用㉒操作会涉及到很多逻辑,留在后面再讲。
  6. 调用p2p插件的broadcast_block⑲方法,将这个签名后的block广播出去。

块数据的传播

当新生成的block被广播出去后,收到该信息的节点会触发其会触发本地节点graphene::net::node的回调函数[node_impl::on_message]⑪,然后根据消息内block的标识,再触发回调函数node_impl::process_block_message,其进行一些简单的处理。然后触发p2p插件的p2p_plugin_impl::handle_block⑬方法。该方法很简单,直接去调用插件chain_plugin中的accept_block方法⑳。在accept_block中将这个新block放入写入队列中㉑,跟③处是同一个队列。然后该队列的消费者,从队列中取出该block数据,然后调用push_blockblock中的数据应用㉒到本地database,此处与⑱处相同,即块生成的节点,和接收块的节点都运行相同的应用㉒逻辑。

根据状态机原理,初始状态的相同的两份数据,经过相同的状态迁移,最终的状态也相同,这样就保证了各个steemd节点的数据最终一致性。

区块链的生长

在每个steemd节点收到新block后㉒,会首先判断是否发生了分叉,如果发生了分叉,则进行分叉修复㉓。

分叉处理

steemd采用的是长链原则,即发生分叉时,无条件信任长链的数据。如果事先本节点应用短链的数据,则会回滚掉短链涉及的全部数据变更,然后再重新应用长链上的数据修改。当一条链被75%的的“超级节点”(witness)应用后,该条链才会变成不可变更的,在此之前本地steemd节点上的区块链数据可能会发生多次回滚、变更。至于一条链需要多久才能被75%的的“超级节点”(witness)应用变得稳定,这个得视各种情况而定,如果“超级节点”(witness)的数量且相互之间延时很低,则发生分叉的概况就低,则达成75%共识则更快,如果从中有人破坏,故意分叉或者网络情况差,无意造成分叉,则达成共识更慢。

目前steemd能够处理1024个块内的分叉,即假设的分叉时长不长于1024*3s(51分钟)。

steemd本地采用了fork_db来缓存每个收到的block,如果新block与本地的链的最后一个block相连,则直接使本地的链生长,应用block中的数据。如果新block与本地的链的最后一个block不相连,则先将其缓存在fork_db中,然后比较哪条链长,如果fork_db缓存中的链比本地链长时,则回滚本地链上的block,使得database恢复到长短2条链共同的分叉点block处,然后将长链的后续block应用到本地database。这段逻辑在这里

生长

当处理完分叉情况后,则会调用apply_block㉔方法,将block中的数据应用到本地database

此时先验证该block的签名、merkle根等信息。感觉校验的逻辑应该移动到分叉处理之前,这样能极大的减少伪造块带来的分叉,从而避免了被攻击的可能,因为分叉回滚会极大的消耗database仅有的处理能力。

当该block被校验完成后,依次调用apply_transaction方法,把block中的transaction应用到本地database,此处的应用逻辑与④处相同,就不再赘述了。

block中的transaction应用完成后,更新本地节点的全局元数据(当前block号、时间、witness状态等),再然后处理一些收尾工作,比如清理过期的transactionorders等,奖励对应block的witness(此处与steemd的激励机制有关)。

总结

至此,就完成了一个用户更新操作的全部流程:从用户的一个变更操作,变成一个transaction,再变成一个block,最后同步到database的变更。

从中可见,区块链并没有什么技术上的创新,使用的都是现成技术方案:

  • 数字签名、hash
  • 批处理,多个transaction打包成一个block
  • p2p网络
  • RSM,从transaction的收集、打包、出块,块的传播,链的生长,就是RSM的逻辑。

Search

    Table of Contents