前面几篇关于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_transaction
api收到用户transaction
后的主要逻辑。至于什么行为对应什么operation
,这些都是客户端完成的。
②steemd
在收到一个transaction
后,先调用插件chain_plugin
中的accept_transaction
方法。③在accept_transaction
,会通过一个队列,将全部transaction
发送到一个处理线程中串行处理,在前面讲过json-rpc
模块是多线程的。在处理线程中,将队列中的transaction
依次取出,然后调用chain::database
的push_transaction
方法。
④此处调用push_transaction
就是现将用户的变更操作临时应用到本地节点、同时进行验证和校验。
- 先创建一个
database
的写事务,用于时候回滚数据,因为这些变更是临时的。 - 调用该
transaction
中的每个operation
的validate
方法,这些validate
在这里,定义每种operation
参数要满足哪些要求。理论上一个transaction
中可以封装多个operation
。 - 校验每个
transaction
的签名和时间。 - 将该
transaction
存储在本地的database
中。 - 调用通知
on_pre_apply_transaction
,这里是steemd
中使用的一种事件通知机制。这个是在chain::database
中使用的比较多的一个机制⑥,可以简单看做一个回调函数链表,其中的回调函数由其他插件⑧在初始化时注册进去。当调用通知时,会依次触发这些回调函数,这里回调函数的参数是transaction
。 - 依次调用通知
pre_apply_operation
,参数是每个operation
。 - ⑤依次获取每个
operation
对应的evaluator
,然后调用其apply
方法。这里是steemd
业务逻辑的核心了。这里处理每种operation
应该如何变更database
中的数据。比如一个转账的operation
,该给谁加余额,给谁减余额。evaluator
也有一套注册机制,总的来说比较简单,这里就不将了,每个operation
的evaluator
的定义在这里。在这里的这次数据变更在之前创建的写事务中,因此是临时。只有当该transaction
被打包、出块、并同步到多数witness节点(75%)上后,其数据变更才是永久的。 - 依次调用通知
post_apply_operation
。 - 然后将该
transaction
存储在_pending_tx
中。如果轮到本节点打包出块时,该节点就会将_pending_tx
中的transaction
打包成为一个新的block
,这个留在后面讲。
在steemd
将transaction
应用到本地后,然后再调用p2p::p2p_plugin
插件的broadcast_transaction
api⑨将其通过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.ini
的plugin
项加入witness
,例如plugin = witness
,同时配置好文件中的witness
项和与witness
项对应的private-key
。
在witness
插件启动后,会启动一个定时器,定时触发调度逻辑maybe_produce_block
⑮,该函数会执行witness
的调度逻辑(该处可以开个模块另行讲解),如果不该本节点轮次时,就返回等待下一次调度,如果轮到本节点的轮次,则会调用chain::database
的generate_block
⑯,该打包的过程中:
- 回滚前面传播
transaction
时的临时数据变更,因为这些数据可能是不可靠的。因为只有被变成block
并传播到各个节点上的数据才是大家公认可靠的数据。 - 然后将本节点收集到的被广播的
transaction
中包含的operation
再进行一次校验,该处使用的校验方式是将这些operation
在本地应用一次⑰(逻辑跟④中的那些步骤是一样的,可以看上面的描述),这种校验方式时非常重的一个操作,应用完后,还需要再回滚一次数据,这样极大的消耗的性能,极大地限制了该系统的TPS。 - 将本地收集的
transaction
,放入一个block
中,然后更新该block
的元数据,比如序号、前一个block
的id等等,建立该block
跟以前block
的关系。 - 对新生成的
block
进行签名。 - 调用
push_block
⑱,将新生成的block
应用㉒到本地database
,这个应用㉒操作会涉及到很多逻辑,留在后面再讲。 - 调用
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_block
将block
中的数据应用㉒到本地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状态等),再然后处理一些收尾工作,比如清理过期的transaction
、orders
等,奖励对应block
的witness(此处与steemd
的激励机制有关)。
总结
至此,就完成了一个用户更新操作的全部流程:从用户的一个变更操作,变成一个transaction
,再变成一个block
,最后同步到database
的变更。
从中可见,区块链并没有什么技术上的创新,使用的都是现成技术方案:
- 数字签名、hash
- 批处理,多个
transaction
打包成一个block
p2p
网络RSM
,从transaction
的收集、打包、出块,块的传播,链的生长,就是RSM
的逻辑。