原文连接:醒者呆的博客园,www.cnblogs.com/Evsward/p/c…html
Controller是EOS区块链的核心控制器,其功能丰富、责任重大。 关键字:EOS,区块链,controller,chainbase,db,namespace,using,信号槽,fork_database,snapshotjava
命名空间namespace定义了一个范围,这个范围自己可做为额外的信息,相似于地址,或者位置。若是有两个名字相同的变量或者函数,例如foshan::linshuhao和nba::linshuhao,命名空间能够提供:ios
区分性或者归类性。不一样命名空间下的内容互相孤立,即便内部函数名称相同,也不会产生混淆。c++
可读性,本例中foshan和nba提供了一层语义。web
C++程序架构中,不一样的文件能够经过引入相同的命名空间使用或者扩展功能。进一步理解,不一样的文件名能够提供一层语义,这些文件能够共同维护一个跨文件的命名空间。
复制代码
C++程序设计中,常常会遇到带有using关键字的语句。using正如字面含义,表明了本做用域后续会使用到的内容,这个内容能够是:算法
using apply_handler = std::function<void(apply_context&)>;
经过controller的声明文件,能够看到其整个结构。它声明了两个命名空间:mongodb
在controller.hpp中,最重要的部分就是类controller的内容,它是对命名空间eosio::chain内容的扩展。在展开介绍controller类以前,先要说明在eosio::chain命名空间下,有两个枚举类的定义,这也是对命名空间功能的扩展,由于下面介绍controller类的时候会使用:数据库
db_read_mode,db读取模式是一个枚举类,包括:json
validation_mode,校验模式也一样是一个枚举类,包括:api
下面进入controller类,内容不少,首先包含了一个公有的成员config,它是一个结构体,包含了大量链配置项,可在配置文件或者链启动命令中配置。controller中的config结构体是动态运行时的参数配置,而EOSIO提供了另一个eosio::chain::config命名空间,这里定义了系统初始化默认的一些配置项的值,controller中的config结构体的某些配置项的初始化会使用到这些默认值。
config的配置项中大量使用到了一个容器:flat_set。这是一个使用键存储对象,且通过排序的容器,同时它是一个去重容器,也就是说容器中不会包含两个相同的元素。
复制代码
其中被序列化公开的属性有:
FC_REFLECT( eosio::chain::controller::config,
(actor_whitelist) // 帐户集合,做为actor白名单
(actor_blacklist) // 帐户集合,做为actor黑名单
(contract_whitelist) // 帐户集合,做为合约白名单
(contract_blacklist) // 帐户集合,做为合约黑名单
(blocks_dir) // 存储区块数据的目录名字,有默认值为"blocks"
(state_dir) // 存储状态数据的目录名字,有默认值为"state"
(state_size) // 状态数据的大小,有默认值为1GB
(reversible_cache_size) // 可逆去快数据的缓存大小,有默认值为340MB
(read_only) // 是否只读,默认为false。
(force_all_checks) // 是否强制执行全部检查,默认为false。
(disable_replay_opts) // 是否禁止重播参数,默认为false。
(contracts_console) // 是否容许合约输出到控制台,通常为了调试合约使用,默认为false。
(genesis) // eosio::chain::genesis_state结构体的实例,包含了创世块的初始化配置内容。
(wasm_runtime) // 运行时webassembly虚拟机的类型,默认值为eosio::chain::wasm_interface::vm_type::wabt
(resource_greylist) // 帐户集合,是资源灰名单。
(trusted_producers) // 帐户集合,为可信生产者。
)
复制代码
未包含在内的属性有:
flat_set< pair<account_name, action_name> > action_blacklist; // 帐户和action组成一个二元组做为元素的集合,储存了action的黑名单
flat_set<public_key_type> key_blacklist; // 公钥集合,公钥黑名单
uint64_t state_guard_size = chain::config::default_state_guard_size; // 状态守卫大小,默认为128MB
uint64_t reversible_guard_size = chain::config::default_reversible_guard_size; // 可逆区块守卫大小,默认为2MB
bool allow_ram_billing_in_notify = false; // 是否容许内存帐单通知,默认为false。
db_read_mode read_mode = db_read_mode::SPECULATIVE; // db只读模式,默认为SPECULATIVE
validation_mode block_validation_mode = validation_mode::FULL; // 区块校验模式,默认为FULL
复制代码
controller::block_status,区块状态枚举类,包括:
接下来,查看controller的私有成员:
controller类的共有成员属性以及私有成员介绍完了,还剩下公有成员函数,这部份内容很是多,几乎包含了整个链运行所涉及到的出块流程相关的一切内容,从区块本地组装、校验签名,到本地节点应用入状态库,通过多节点共识成为不可逆区块等函数。其中每一个阶段都有对应的信号,信号功能使用了boost::signals2::signal
库。controller维护了这些信号内容,共8个:
全部信号的发射时机都是在controller中。
发射时机: push_block函数,会对已签区块校验,包括不能有pending块,不能push空块,区块状态不能是incomplete。经过校验后,会发射该信号,携带该区块。 插件捕捉处理: chain_plugin链接该信号,由信号槽转播到channel,pre_accepted_block_channel发布该区块。可是该channel没有订阅者。
发射时机①: commit_block函数,若是该函数的参数add_to_fork_db为true,须要添加至fork_db,首先将pending状态区块的状态置为已校验,在fork_db中添加pending状态区块,而后发射该信号并携带pending状态区块。 发射时机②: push_block函数,pre_accepted_block发射完之后,获取区块的可信状态并添加至fork_db,而后发射该信号,携带fork_db添加成功后返回的状态区块。 插件捕捉处理①: net_plugin链接该信号,绑定处理函数,函数体实现了日志打印。 插件捕捉处理②: chain_plugin链接该信号,由信号槽转播到channel,accepted_block_header_channel发布该区块。bnet_plugin订阅该channel,绑定bnet_plugin_impl的on_accepted_block_header函数,该函数涉及到线程池等概念,将会在bnet_plugin插件的部分详细分析。遍历线程池,转到session会话下的on_accepted_block_header函数执行。若是传入区块与本地时间相差6秒之内则接收,以外不处理。接收处理时先从本地多索引库表block_status中查找是否已存在,不存在则插入block_status结构对象,若是不是远程不可逆请求以及不存在该区块,或者该区块不是来自其余节点的状况,要在区块头通知集合中插入该区块id。
发射时机: commit_block函数,fork_db以及重播的处理结束后,发射认可区块的信号,携带pending状态区块数据。 插件捕捉处理①: net_plugin链接该信号,绑定处理函数,打印日志的同时调用dispatch_manager::bcast_block,传入区块数据。send_all向全部链接发送广播,这部份内容会在net_plugin部分详细研究。 插件捕捉处理②: chain_plugin链接该信号,由信号槽转播到channel,accepted_block_channel发布该区块。bnet_plugin订阅该channel,依然有线程池的处理,会话遍历,执行单个会话的on_accepted_block函数,删除缓存中的全部事务,遍历接收到的区块的事务receipt,得到事务的打包对象,事务id,在多索引表_transaction_status中查找该id,若是找到了则删除。接下来若是在空闲状态下,尝试发送下一条pingpong心跳链接信息。 插件捕捉处理③: mongo_db_plugin链接该信号,绑定其mongo_db_plugin_impl::accepted_block函数,传入区块内容。该函数首先校验是否达到了mongo配置中的开始处理的区块号,这项配置是经过参数start_block_num设置的。若是传入区块号大于该参数设置的值(默认是0),则将标志位start_block_reached置为true。接着根据另外一个配置项mongodb-store-blocks(是否储存区块数据)以及mongodb-store-block-states(是否储存状态区块数据)来判断是否要储存区块数据。储存区块的方式是调用队列block_state_\queue,传入区块数据等待被消费,等待的过程又涉及到一个速度平衡的机制,关于mongo插件的内容请查阅相关篇章。 插件捕捉处理④: producer_plugin链接该信号,执行其on_block函数,传入区块数据。函数首先作了校验,包括时间是否大于最后签名区块的时间以及大于当前时间,还有区块号是否大于最后签名区块号。校验经过之后,活跃生产者帐户集合active_producers开辟新空间,插入计划出块生产者。 接下来利用set_intersection取本地生产者与集合active_producers的交集(若是结果为空,说明本地生产者没有出块权利不属于活跃生产者的一份子)。将结果存入一个迭代器,迭代执行内部函数,若是交集生产者不等于接收区块的生产者,说明是校验别人生产的区块,若是是相等的没必要作特殊处理。校验别人生产的区块,首先要在活跃生产者的key中找到匹配的key(本地生产者帐户公钥),不然说明该区块不是合法生产者签名抛弃不处理。接下来,获取本地生产者私钥,组装生产确认数据字段,包括区块id,区块摘要,生产者,签名。更新producer插件本地标志位_last_signed_block_time和_last_signed_block_num。最后发射信号confirmed_block,携带以上组装好的数据。但通过搜索,项目中目前没有对该信号设置槽connection。 在区块建立以前要为该区块的生产者设置水印用来标示该区块的生产者是谁。
发射时机①: push_block函数,当推送的区块状态为irreversible不可逆时,发射该信号,携带状态区块数据。 发射时机②: on_irreversible函数,更改区块状态为irreversible的函数,操做成功最后发射该信号。 插件捕捉处理①: net_plugin链接该信号,绑定函数irreversible_block,打印日志。 插件捕捉处理②: chain_plugin链接该信号,由信号槽转播到channel,irreversible_block_channel发布该区块。 bnet_plugin订阅该channel,依然线程池遍历会话,执行on_new_lib函数,当本地库领先时能够清除历史直到知足当前库,或者直到最后一个被远端节点所知道的区块。最后若是空闲,尝试发送下一条pingpong心跳链接信息。 插件捕捉处理③: mongo_db_plugin链接该信号,执行applied_irreversible_block函数,仍旧参照mongo配置项的值决定是否储存区块、状态区块以及事务数据,而后将区块数据塞入队列等待消费。同上不赘述。 插件捕捉处理④: producer_plugin链接该信号,绑定执行函数on_irreversible_block,设置producer成员_irreversible_block_time的值为区块的时间。
发射时机①: push_scheduled_transaction函数,推送计划事务时,将事务体通过一系列转型以及校验,当事务超时时间小于pending区块时间时的处理,接着发射该信号,认可事务。当事务超时时间大于等于pending区块时间时的处理,最后发射该信号,认可事务。当事务的sender发送者不是空且没有主观失败的处理,最后发射该信号,认可事务。基于生产和校验的主观修改,主观时的处理以后发射该信号,认可事务。当不是主观问题而是硬逻辑错误时的处理,接着发射该信号,认可事务。 发射时机②: push_transaction函数,新事务到大状态区块,要通过身份认证以及决定是否如今执行仍是延期执行,最后要插入到pending区块的receipt接收事务中去。当检查事务未被认可时,发射一次该信号。最后所有函数处理完毕,再次发射该信号。 插件捕捉处理①: net_plugin链接该信号,绑定函数accepted_transaction,打印日志。 插件捕捉处理②: chain_plugin链接该信号,由信号槽转播到channel,accepted_transaction_channel发布该事务。bnet_plugin订阅该channel,线程池遍历会话,执行函数on_accepted_transaction。在多是多个的投机块中一个事务被认可,当一个区块包含该认可事务或者切换分叉时,该事务状态变为“receive now”,被添加至数据库表中,做为发送给其余节点的证据。当该事务被发送给其余节点时,根据这个状态能够保证以后不会重复发送。每一次事务被“accepted”,都会延时5秒钟。每次一个区块被应用,全部超过5秒未被应用的但被认可的事务都将被清除。 插件捕捉处理③: mongo_db_plugin链接该信号,执行函数accepted_transaction,校验加入队列待消费。
发射时机①: push_scheduled_transaction函数,事务过时时间小于pending区块时间处理后发射该信号。反之大于等于处理后发射该信号。当事务的sender发送者不为空且没有主观失败的处理后发射该信号。基于生产和校验的主观修改,主观处理后发射该信号,非主观处理发射该信号。 发射时机② :push_transaction函数,发射两次该信号,逻辑较多,这段包括以上那个函数的可读性不好,注释几乎没有。 插件捕捉处理①: net_plugin链接该信号,绑定函数applied_transaction,打印日志。 插件捕捉处理② : chain_plugin链接该信号,由信号槽转播到channel,原理基本同上,再也不重复。 插件捕捉处理③ : mongo_db_plugin同上。
发射时机 : push_confirmation函数,推送确认信息,在此阶段不容许有pending区块存在,接着fork_db添加确认信息,发射该信号。 插件捕捉处理① : net_plugin链接该信号,绑定函数accepted_confirmation,打印日志。 插件捕捉处理② : chain_plugin链接该信号,由信号槽转播到channel,基本同上。
发射时机 : 与前面七种不一样,该信号没有发射,是属于boost::interprocess::bad_alloc,用于捕捉内存分配错误的异常。 插件捕捉处理 : 无connect。
controller函数的具体实现内容,通常是对参数的校验,而后经过my来调用controller_impl结构体的具体函数来处理。因此controller的核心功能实现是在controller_impl结构体中,下面查看其成员属性:
std::function<void(apply_context&)>
为值的map做为值,帐户名做为键的复杂map。剩下的内容为controller_impl的众多功能函数的实现了,这些内容都是须要与其余程序组合使用,例如插件程序,或者智能合约,所以在接下来的篇章中,将会从新按照一个功能入口研究完整的使用脉络。而在这些功能中有两个内容须要在此处研究清楚,一个是fork_database,另外一个是snapshot。下面逐一展开分析。
在fork_database.hpp文件中声明。管理了轻量级状态数据,是由未确认的潜在区块产生的。当本地节点接收receive到新的区块时,它们将被推入fork数据库。fork数据库跟踪最长的链,以及最新不可逆块号。全部大于最新不可逆块号的区块将会在发出“irreversible”不可逆信号之后被释放掉,区块已经成功上链变为不可逆,所以fork库不必再存储。分叉库提供了不少函数,例如经过区块id获取区块、经过区块号获取区块、插入区块包括set和add各类重载函数、删除区块、获取头区块、经过id获取两个分支、设置区块标志位等。
在controller_impl的构造函数体中会被调用。
controller_impl( const controller::config& cfg, controller& s )
:self(s),
db( cfg.state_dir,
cfg.read_only ? database::read_only : database::read_write,
cfg.state_size ),
reversible_blocks( cfg.blocks_dir/config::reversible_blocks_dir_name,
cfg.read_only ? database::read_only : database::read_write,
cfg.reversible_cache_size ),
blog( cfg.blocks_dir ),
fork_db( cfg.state_dir ), // 调用fork_db构造器,传入一个文件路径。
wasmif( cfg.wasm_runtime ),
resource_limits( db ),
authorization( s, db ),
conf( cfg ),
chain_id( cfg.genesis.compute_chain_id() ),
read_mode( cfg.read_mode )
复制代码
进入构造器。
fork_database::fork_database( const fc::path& data_dir ):my( new fork_database_impl() ) {
my->datadir = data_dir;
if (!fc::is_directory(my->datadir))
fc::create_directories(my->datadir);
auto fork_db_dat = my->datadir / config::forkdb_filename; // 在该目录下建立一个文件forkdb.dat
if( fc::exists( fork_db_dat ) ) { // 若是该文件已存在
string content;
fc::read_file_contents( fork_db_dat, content ); // 将其读到内存中
fc::datastream<const char*> ds( content.data(), content.size() );
unsigned_int size; fc::raw::unpack( ds, size ); // 按照区块结构解析
for( uint32_t i = 0, n = size.value; i < n; ++i ) { // 遍历全部区块
block_state s;
fc::raw::unpack( ds, s );
set( std::make_shared<block_state>( move( s ) ) ); // 逐一插入到数据库fork_database中
}
block_id_type head_id;
fc::raw::unpack( ds, head_id );
my->head = get_block( head_id ); // 处理fork_database的头区块数据
fc::remove( fork_db_dat ); // 删除持久化文件forkdb.dat。
}
}
复制代码
文件forkdb.dat也位于节点数据目录中,是前文介绍惟一没有说到的文件,这里补齐。
上面讲到了,fork_database拥有一个公有成员irreversible信号。这个信号在controller_impl结构体的宏SET_APP_HANDLER中被使用:
fork_db.irreversible.connect( [&]( auto b ) {
on_irreversible(b);
});
复制代码
这段代码实际上是boost的信号槽机制,信号有一个connect操做,其参数是一个slot插槽,可将插槽链接到信号上,最终返回一个connection对象表明这段链接关系,能够灵活控制链接开关。插槽的类型能够是任意对象,这段代码中是一个lambda表达式,调用了on_irreversible函数。 接下来,去fork_database查询该信号的触发位置,出如今prune函数中的一段代码,
auto itr = my->index.find( h->id ); // h是prune入参,const block_state_ptr& h
if( itr != my->index.end() ) {
irreversible(*itr);
my->index.erase(itr);
}
复制代码
在table中查询入参区块,查找到之后,会触发信号irreversible并携带区块源数据发射。而后执行fork_database的删除操做将目标区块从分叉库中删除。 irreversible信号携带区块被发射后,因为上面宏的做用,会调用controller_impl的on_irreversible函数,并按照lambda表达式的规则将区块传入。该函数会将入参区块变为不可逆,处理成功之后,下面截取了这部分相关代码:
...
fork_db.mark_in_current_chain(head, true);
fork_db.set_validity(head, true);
}
emit(self.irreversible_block, s);
复制代码
这两行是该函数对fork_db的所有操做,将fork_db的属性in_current_chain和validated置为true。在on_irreversible函数的最后,它也发射了一个本身的信号,注意发射方式采用了关键字emit,也携带了操做的区块数据。
信号触发能够有两种方式,使用关键字emit(signal,param)和直接调用signal(param)。
复制代码
这个信号原本是与这一小节的内容不相干,但既然分析到这了,仍是但愿能有个闭环,那么来看一下该信号的链接槽位置,如图所示。
能够看到,区块不可逆的信号在net_plugin,chain_plugin,mongo_db_plugin,producer_plugin四个插件代码中获得了运用,也说明这四个插件是很是关心区块不可逆的状态变化的。至于他们具体是如何运用的,在相关部分会有详细介绍。
初始化fork_db,主要工做是从创世块状态设置fork_db的头块。头块的数据结构是区块状态对象,构造头块时,要先构造区块头状态对象,包括:
构建好区块头之后,接着构建区块体,构建完成之后,将完整头块插入到空的fork_db中。
提交区块函数,不管提交是否成功,都再也不保留活动的pending块。该函数有一个参数add_to_fork_db,是否加入fork_db。在producer_plugin生产者生产区块的逻辑中,提交区块调用controller对象的commit_block函数:
void controller::commit_block() {
validate_db_available_size(); // 校验db数据库的大小
validate_reversible_available_size(); // 校验reversible数据库的大小
my->commit_block(true); // 调用controller_impl结构体中的的commit_block函数,而且传入true
}
复制代码
从这条逻辑过来的提交区块,会执行add_to_fork_db,而commit_block函数的另外一处调用是在应用区块部分,没有触发add_to_fork_db。至于commit_block函数的内容不在此处展开,只看fork_db相关的内容:
if (add_to_fork_db) {
pending->_pending_block_state->validated = true; // 将pending区块对象的状态属性validated置为true,标记已校验。
auto new_bsp = fork_db.add(pending->_pending_block_state); // 将pending区块添加至fork_db。
emit(self.accepted_block_header, pending->_pending_block_state); // 发射controller的accepted_block_header信号,携带pending区块状态对象。
head = fork_db.head(); // 将当前节点的头块设置为fork_db的头块。
// 校验pending区块是否最终成功同时变为fork_db以及主节点的头块。
EOS_ASSERT(new_bsp == head, fork_database_exception, "committed block did not become the new head in fork database");
}
复制代码
以上代码中又发射一个信号accepted_block_header,仍旧查看一下该信号的链接槽在哪里,通过查找,发现是在net_plugin和chain_plugin两个插件中,说明这两个插件是要对这个信号感兴趣并捕捉该信号。
或许要切换分叉库到主库。该函数会在controller_impl结构体中的push_block和push_confirmation两个函数中被调用。
if ( read_mode != db_read_mode::IRREVERSIBLE ) { // 在db读取模式不等于IRREVERSIBLE时,要调用maybe_switch_forks函数。
maybe_switch_forks( s );
}
复制代码
db读取模式为IRREVERSIBLE时,只关心当前不可逆区块的数据,而fork_db中不存在不可逆区块的数据。而其余三种读取模式都涉及到可逆区块以及未被确认的数据,所以要去maybe_switch_forks函数检查处理一番。
apply_block( new_head->block, s ); // 将新块应用到主库中去。
fork_db.mark_in_current_chain( new_head, true ); // 在fork_db中将新块的属性in_current_chain标记为true。
fork_db.set_validity( new_head, true ); // 在fork_db中将新块的属性validity标记为true。
head = new_head; // 更新节点主库的头块为当前块。
复制代码
my->fork_db.close();
复制代码
在controller析构时将fork_db关掉,由于它会生成irreversible信号到这个controller。若是db读取模式为IRREVERSIBLE,将应用最后一个不可逆区块,my须要成为指向有效controller_impl的指针。
void fork_database::close() {
if( my->index.size() == 0 ) return;
auto fork_db_dat = my->datadir / config::forkdb_filename;
// 获取文件输出流。
std::ofstream out( fork_db_dat.generic_string().c_str(), std::ios::out | std::ios::binary | std::ofstream::trunc );
uint32_t num_blocks_in_fork_db = my->index.size();
// 将当前fork_db的区块数据打包到输出流,持久化到fork_db.dat文件中。
fc::raw::pack( out, unsigned_int{num_blocks_in_fork_db} );
for( const auto& s : my->index ) {
fc::raw::pack( out, *s );
}
if( my->head )
fc::raw::pack( out, my->head->id );
else
fc::raw::pack( out, block_id_type() );
// 一般头块不是不可逆的。若是fork_db中只剩一个块就是头块,通常不会将它删除由于下一个区块须要从头块创建。不过能够在退出以前将这个区块做为不可逆区块从fork_db中删除。
auto lib = my->head->dpos_irreversible_blocknum;
auto oldest = *my->index.get<by_block_num>().begin();
if( oldest->block_num <= lib ) {
prune( oldest );
}
my->index.clear();
}
复制代码
my->head = my->fork_db.head();
复制代码
controller的startup周期时,会将fork_db的头块设置为主库头块(头块通常不是不可逆的)。
快照,顾名思义,能够为区块链提供临时快速备份的功能。
该结构体位于命名空间eosio::chain::detail。提供了写入snapshot快照的能力,是全部关于快照写入的结构的基类。该结构体是一个抽象类型,包含四个成员函数:
snapshot_row_writer继承了abstract_snapshot_row_writer,在构造该结构体实例时,要传入data数据被缓存在函数体。接着,实际上,write向两种数据类型的输出流中写入的时候,对象就是data,写入方法都是fc::raw::pack(out, data);,最终将内存中的data数据写入到输出流。to_variant函数也被实现了,转型的目标是data,返回转型后的variant对象。data类型是模板类型,row_type_name实现了经过boost::core::demangle
库得到data的具体类型名。最后,对外提供了make_row_writer函数,接收任何类型的数据,初始化以上快照行写入的功能。 snapshot_writer进一步封装了写入功能,对外提供了write_row写入接口以及其余辅助功能接口。该类使用到了detail的内容,包括make_row_writer函数的类。 接着,定义了snapshot_writer_ptr是snapshot_writer实例的共享指针。 variant_snapshot_writer和ostream_snapshot_writer都是snapshot_writer的子类,根据不一样的数据类型实现了不一样的处理逻辑。
与上面相对的,是读取的部分,全部关于快照读取结构的基类。其包含三个成员虚函数:
snapshot_row_reader继承了abstract_snapshot_row_reader,在构造该结构体实例时,要传入data数据被缓存在函数体。接着,分别对应不一样输入流的处理不一样,最终会将不一样输入流的数据读取到内存的data实例中。row_type_name的实现同上。make_row_reader的意义同上。 snapshot_reader进一步封装了读取功能,对外提供了read_row读取接口以及其余辅助功能接口。该类使用到了detail的内容,包括make_row_reader函数的类。 接着,定义了snapshot_reader_ptr是snapshot_reader实例的共享指针。 variant_snapshot_reader和ostream_snapshot_reader,还有integrity_hash_snapshot_writer(处理的是hash算法sha256的加密串)都是snapshot_writer的子类,根据不一样的数据类型实现了不一样的处理逻辑。
void controller::startup( const snapshot_reader_ptr& snapshot ) {
my->head = my->fork_db.head(); // 将fork_db的头块设置为状态主库头块
if( !my->head ) { // 若是状态主库头块为空,则说明fork_db没有数据,可能须要重播block_log生成这些数据。
elog( "No head block in fork db, perhaps we need to replay" );
}
my->init(snapshot); // 根据startup的入参snapshot调用controller_impl的初始化函数init。
}
复制代码
进入controller_impl的初始化函数init。
void init(const snapshot_reader_ptr& snapshot) {
if (snapshot) { // 若是入参snapshot不为空
EOS_ASSERT(!head, fork_database_exception, "");//快照存在而状态主库头块不存在是个异常状态。
snapshot->validate();// 校验快照
read_from_snapshot(snapshot);// 执行read_from_snapshot函数
auto end = blog.read_head();// 从日志文件中获取不可逆区块头块。
if( !end ) {// 若是不可逆区块头块为空,重置日志文件,清除全部数据,从新初始化block_log状态。
blog.reset(conf.genesis, signed_block_ptr(), head->block_num + 1);
} else if ( end->block_num() > head->block_num) {// 若是不可逆区块头块号大于状态主库头块号。
replay();// 状态库的数据与真实数据不一样步,版本过旧,须要重播修复状态主库数据。
} else {
// 校验提示报错:区块日志提供了快照,但不包含主库头块号
EOS_ASSERT(end->block_num() == head->block_num, fork_database_exception,
"Block log is provided with snapshot but does not contain the head block from the snapshot");
}
} else if( !head ) {若是入参snapshot为空且状态主库的头块也不存在,说明状态库彻底是空的。
initialize_fork_db(); // 从新初始化fork_db
auto end = blog.read_head();// 读取区块日志中的不可逆区块头块。
if( end && end->block_num() > 1 ) {// 若是头块存在且头块号大于1
replay();// 重播生成状态库。
} else if( !end ) {// 若是头块不存在
blog.reset( conf.genesis, head->block );// 重置日志文件,清除全部数据,从新初始化block_log状态。
}
}
...
if( snapshot ) {//快照存在,计算完整hash值。经过sha256算法计算,将结果写入快照,同时将结果打印到控制台。
const auto hash = calculate_integrity_hash();
ilog( "database initialized with hash: ${hash}", ("hash", hash) );
}
}
复制代码
EOS为snapshot定义了一个chain_snapshot_header结构体,用来储存快照版本信息。
复制代码
执行read_from_snapshot函数:
void read_from_snapshot( const snapshot_reader_ptr& snapshot ) {
snapshot->read_section<chain_snapshot_header>([this]( auto §ion ){
chain_snapshot_header header;
section.read_row(header, db);
header.validate();
});// 先读取快照头数据。
snapshot->read_section<block_state>([this]( auto §ion ){
block_header_state head_header_state;
section.read_row(head_header_state, db);// 读取区块头状态数据
auto head_state = std::make_shared<block_state>(head_header_state);
// 对fork_db的设置。
fork_db.set(head_state);
fork_db.set_validity(head_state, true);
fork_db.mark_in_current_chain(head_state, true);
head = head_state;
snapshot_head_block = head->block_num;// 设置快照的头块号为主库头块号
});
controller_index_set::walk_indices([this, &snapshot]( auto utils ){
using value_t = typename decltype(utils)::index_t::value_type;
// 跳过table_id_object(内联的合同表格部分)
if (std::is_same<value_t, table_id_object>::value) {
return;
}
snapshot->read_section<value_t>([this]( auto& section ) {//按照value_t类型读取快照到section
bool more = !section.empty();
while(more) {// 循环读取section内容,知道所有读取完毕。
decltype(utils)::create(db, [this, §ion, &more]( auto &row ) {
more = section.read_row(row, db);// 按行读取数据,回调逐行写入主库。
});
}
});
});
read_contract_tables_from_snapshot(snapshot);//从快照中同步合约数据
authorization.read_from_snapshot(snapshot);//从快照中同步认证数据
resource_limits.read_from_snapshot(snapshot);//从快照中同步资源限制数据
db.set_revision( head->block_num );// 更新头块
}
复制代码
同步快照数据的操做是在controller的startup周期中执行的,根据传入的snapshot,会调整区块链的基于block_log的不可逆日志数据,基于chainbase的状态主库数据。在controller的startup完毕后,能够保证三者数据的健康同步。
在chain_plugin的插件配置项中有一个“snapshot”的参数,该配置项能够指定读取的快照文件。几个关键校验:
参数设置完毕,在chain_plugin的startup阶段,会检查快照地址,若是存在,则会带上该快照文件启动链。
if (my->snapshot_path) {
auto infile = std::ifstream(my->snapshot_path->generic_string(), (std::ios::in | std::ios::binary));
auto reader = std::make_shared<istream_snapshot_reader>(infile);
my->chain->startup(reader);// 带上该快照文件启动链。
infile.close();
}
复制代码
my->chain的类型是fc::optional,因此会执行controller的startup函数,这样就与上面的流程挂钩了,造成了一个完整的逻辑闭环。
void controller::write_snapshot( const snapshot_writer_ptr& snapshot ) const {
// 写入快照时,不容许存在pending区块。
EOS_ASSERT( !my->pending, block_validate_exception, "cannot take a consistent snapshot with a pending block" );
return my->add_to_snapshot(snapshot);
}
复制代码
调用add_to_snapshot函数。
void add_to_snapshot( const snapshot_writer_ptr& snapshot ) const {
snapshot->write_section<chain_snapshot_header>([this]( auto §ion ){
section.add_row(chain_snapshot_header(), db);// 向快照中写入快照头数据
});
snapshot->write_section<genesis_state>([this]( auto §ion ){
section.add_row(conf.genesis, db);// 向快照中写入创世块数据
});
snapshot->write_section<block_state>([this]( auto §ion ){
section.template add_row<block_header_state>(*fork_db.head(), db);// 向快照中写入头块区块头数据。
});
controller_index_set::walk_indices([this, &snapshot]( auto utils ){
using value_t = typename decltype(utils)::index_t::value_type;
if (std::is_same<value_t, table_id_object>::value) {// 跳过table_id_object(内联的合同表格部分)
return;
}
snapshot->write_section<value_t>([this]( auto& section ){ // 遍历主库db区块。
decltype(utils)::walk(db, [this, §ion]( const auto &row ) {
section.add_row(row, db); // 向快照中逐行写入快照
});
});
});
add_contract_tables_to_snapshot(snapshot);// 向快照中写入合约数据
authorization.add_to_snapshot(snapshot);// 向快照中写入认证数据
resource_limits.add_to_snapshot(snapshot);// 向快照中写入资源限制数据
}
复制代码
controller::write_snapshot函数在外部由producer_plugin所调用。producer_plugin经过rpc api接口create_snapshot对外提供了建立快照的功能。这个功能无疑是很是实用的,能够为生产者提供快速数据备份的能力,为整个EOS区块链的运维工做增长了健壮性。producer_plugin的具体的实现代码:
producer_plugin::snapshot_information producer_plugin::create_snapshot() const {
chain::controller& chain = app().get_plugin<chain_plugin>().chain();// 获取chain_plugin的插件实例
auto reschedule = fc::make_scoped_exit([this](){// 获取生产者出块计划
my->schedule_production_loop();
});
if (chain.pending_block_state()) {// 快照大忌:若是有pending块,不可生成快照。
// abort the pending block
chain.abort_block();// 将pending块干掉
} else {
reschedule.cancel();// 无pending块,则取消出块计划。
}
// 开始写快照。
auto head_id = chain.head_block_id();
// 快照目录:可经过配置producer_plugin的snapshots-dir项来指定快照目录,会在节点数据目录下生成该快照目录,若是未特殊指定,默认目录名字为“snapshots”
// 在快照目录下生成格式为“snapshot-${id}.bin”的快照文件。id是当前链的头块id
std::string snapshot_path = (my->_snapshots_dir / fc::format_string("snapshot-${id}.bin", fc::mutable_variant_object()("id", head_id))).generic_string();
EOS_ASSERT( !fc::is_regular_file(snapshot_path), snapshot_exists_exception,
"snapshot named ${name} already exists", ("name", snapshot_path));
auto snap_out = std::ofstream(snapshot_path, (std::ios::out | std::ios::binary));// 构造快照文件输出流
auto writer = std::make_shared<ostream_snapshot_writer>(snap_out);// 构造快照写入器
chain.write_snapshot(writer);// 备份当前链写入快照
// 资源释放。
writer->finalize();
snap_out.flush();
snap_out.close();
return {head_id, snapshot_path};// 返回快照文件路径
}
复制代码
快照的部分就介绍完毕了,区块生产者能够根据须要调用producer_plugin的rpc接口create_snapshot为当前链建立快照。通过以上研究能够得出,EOS的快照是对状态数据库的备份,而不是block_log日志文件的备份,不可逆区块在全网有不少节点做为备份,没必要本地备份,而状态数据库极可能是本地惟一的,与其余节点都不一样,若是有损坏会形成不少未上到不可逆区块日志的事务丢失。 当须要使用快照恢复时,能够从新启动链,同时设置chain_plugin的参数“snapshot”,传入快照文件路径,经过快照恢状态数据库。
本节重点介绍了EOS中的核心控制器controller的功能和使用。controller的功能是很是多的,贯穿整个链生命周期的大部分行为,深刻研究会发现controller其实是对数据的控制,正如java中的mvc模式,控制器的功能就是对持久化数据的操做。本节首先介绍了两个c++的语法使用,一个是命名空间另外一个是using关键字,另外文中也提到了boost的信号槽机制。接着浏览了controller的声明和实现的代码结构,最后,在众多功能中挑选了fork_database分叉库和snapshot快照进行了详细的研究与分析。其余的众多功能因为他们与插件的紧密交互性,将会在相关插件的部分详细分析。
圆方圆学院聚集大批区块链名师,打造精品的区块链技术课程。 在各大平台都长期有优质免费公开课,欢迎报名收看。
公开课地址:ke.qq.com/course/3451…