前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Controller:EOS区块链核心控制器

Controller:EOS区块链核心控制器

作者头像
文彬
发布2018-12-12 15:59:25
1.1K0
发布2018-12-12 15:59:25
举报
文章被收录于专栏:醒者呆

Controller是EOS区块链的核心控制器,其功能丰富、责任重大。

关键字:EOS,区块链,controller,chainbase,db,namespace,using,信号槽,fork_database,snapshot

命名空间namespace

命名空间namespace定义了一个范围,这个范围本身可作为额外的信息,类似于地址,或者位置。如果有两个名字相同的变量或者函数,例如foshan::linshuhao和nba::linshuhao,命名空间可以提供:

  • 区分性或者归类性。不同命名空间下的内容互相孤立,即使内部函数名称相同,也不会产生混淆。
  • 可读性,本例中foshan和nba提供了一层语义。

C++程序架构中,不同的文件可以通过引入相同的命名空间使用或者扩展功能。进一步理解,不同的文件名可以提供一层语义,这些文件可以共同维护一个跨文件的命名空间。

using语法

C++程序设计中,经常会遇到带有using关键字的语句。using正如字面含义,代表了本作用域后续会使用到的内容,这个内容可以是:

  • 其他命名空间,用using声明以后,该命名空间下的公有属性都可被使用。
  • 直接指定其他命名空间下的某个函数,相当于导入功能,可以使用该函数,不过使用时仍旧要带上包含函数命名空间的完整路径。
  • 为某个复杂名字变量起的别名以便于使用。例如using apply_handler = std::function<void(apply_context&)>;

controller依赖功能

通过controller的声明文件,可以看到其整个结构。它声明了两个命名空间:

  • chainbase,这项声明为controller提供了基于chainbase的状态数据库能力。该命名空间是chainbase组件定义的,声明了database类,在chainbase源码中可以找到database类,这个类在前文chainbase的章节已经介绍过。
  • eosio::chain,该命名函数是EOSIO项目中内容最丰富的,在很多其他组件都有定义与使用。Controller引用了其他组件在相同命名空间下定义的功能,包括:
    • authorization_manager,提供权限管理的功能,权限内容有认证信息、依赖密钥、关联权限、许可。管理操作包括增删改查。
    • resource_limits::resource_limits_manager,完全的命名空间为eosio::chain::resource_limits,为controller提供了资源限制管理的功能。此处的资源指的是基于chainbase的数据库的存储资源。例如,增加索引、数据库初始化、快照增加和读取、账户初始化、设置区块参数、更新账户使用等。
    • dynamic_global_property_object,动态维护全局状态信息,继承自chainbase::object。它的值是在正常的链操作期间计算的,以及反映全局区块链属性的当前值。
    • global_property_object,维护全局状态信息,同样继承自chainbase::object。它的的值由委员会成员设置,以调优区块链参数。与上面的区别是一个是动态计算,一个是静态指定。
    • permission_object,同样继承自chainbase::object。增加了属于权限范畴的属性,包括id主键、parent父权限id、权限使用id,账户名、权限名、最后更新时间、权限认证。另外提供了检查传入权限是否等效或大于其他权限。权限是按层次结构组织的,因此父权限严格地比子权限以及孙子权限更强大。
    • account_object,同样继承自chainbase::object。增加了属于账户范畴的属性,包括id主键、账户名、是否拥有超级权限能力、最后code更新时间、code版本、创建时间、code、abi。另外提供了abi设置函数set_abi()和abi查询函数get_abi()。
    • fork_database,分叉数据库。下面会详细介绍。

controller扩展

在controller.hpp中,最重要的部分就是类controller的内容,它是对命名空间eosio::chain内容的扩展。在展开介绍controller类之前,先要说明在eosio::chain命名空间下,有两个枚举类的定义,这也是对命名空间功能的扩展,因为下面介绍controller类的时候会使用:

db_read_mode,db读取模式是一个枚举类,包括:

  • SPECULATIVE,推测模式。内容为两个主体的数据:已完成的头区块,以及还未上链的事务。
  • HEAD,头块模式。内容为当前头区块数据。
  • READ_ONLY,只读模式。内容为同步进来的区块数据,不包括推测状态的事务处理数据。
  • IRREVERSIBLE,不可逆模式。内容为当前不可逆区块的数据。

validation_mode,校验模式也同样是一个枚举类,包括:

  • FULL,完全模式。所有同步进来的区块都将被完整地校验。
  • LIGHT,轻量模式。所有同步进来的区块头都将被完整的校验,通过校验的区块头所在区块的全部事务被认为可信。

下面进入controller类,内容很多,首先包含了一个公有的成员config,它是一个结构体,包含了大量链配置项,可在配置文件或者链启动命令中配置。controller中的config结构体是动态运行时的参数配置,而EOSIO提供了另外一个eosio::chain::config命名空间,这里定义了系统初始化默认的一些配置项的值,controller中的config结构体的某些配置项的初始化会使用到这些默认值。

config的配置项中大量使用到了一个容器:flat_set。这是一个使用键存储对象,且经过排序的容器,同时它是一个去重容器,也就是说容器中不会包含两个相同的元素。

其中被序列化公开的属性有:

代码语言:javascript
复制
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) // 账户集合,为可信生产者。
)

未包含在内的属性有:

代码语言:javascript
复制
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,区块状态枚举类,包括:

  • irreversible = 0,该区块已经被当前节点应用,并且被认为是不可逆的。
  • validated = 1,这是由一个有效生产者签名的完整区块,并且之前已经被当前节点应用,因此该区块已被验证但未成为不可逆。
  • complete = 2,这是一个由有效生产者签名的完整区块,但是还没有成为不可逆,也没有被当前节点应用。
  • incomplete = 3,这是一个未完成的区块,未被生产者签名也没有被某个节点生产。

接下来,查看controller的私有成员:

  • apply_context类对象,处理节点应用区块的上下文环境。其中包含了迭代器缓存、二级索引管理、通用索引管理、构造器等内容。
  • transaction_context类对象,事务上下文环境。包含了构造器,转型,事务的生命周期(包括初始化、执行、完成、刷入磁盘、撤销操作),事务资源管理、分发action、定时事务、资源账单等内容。
  • mutable_db(),返回一个可变db,类型与正常db相同,都是chainbase::database,但这个函数返回的是一个常量引用。
  • controller_impl结构体的实例的唯一指针my。这是整个controller的环境对象,controller_impl结构体包含了众多controller功能的实现。通过my都可以缓存在同一个环境下使用。

controller类的共有成员属性以及私有成员介绍完了,还剩下公有成员函数,这部分内容非常多,几乎包含了整个链运行所涉及到的出块流程相关的一切内容,从区块本地组装、校验签名,到本地节点应用入状态库,经过多节点共识成为不可逆区块等函数。其中每个阶段都有对应的信号,信号功能使用了boost::signals2::signal库。controller维护了这些信号内容:

  • signal<void(const signed_block_ptr&)> pre_accepted_block; // 预承认区块(承认其他节点广播过来的区块是正确的)
  • signal<void(const block_state_ptr&)> accepted_block_header; // 承认区块头(对区块头做过校验)
  • signal<void(const block_state_ptr&)> accepted_block; // 承认区块
  • signal<void(const block_state_ptr&)> irreversible_block; // 不可逆区块
  • signal<void(const transaction_metadata_ptr&)> accepted_transaction; // 承认事务
  • signal<void(const transaction_trace_ptr&)> applied_transaction; // 应用事务(承认其他节点数据要先校验,通过以后可以应用在本地节点)
  • signal<void(const header_confirmation&)> accepted_confirmation; // 承认确认
  • signal<void(const int&)> bad_alloc; // 内存分配错误信号

controller的具体实现

controller函数的具体实现内容,一般是对参数的校验,然后通过my来调用controller_impl结构体的具体函数来处理。所以controller的核心功能实现是在controller_impl结构体中,下面查看其成员属性:

  • self,controller实例的引用。
  • db, chainbase::database的一个实例,用于存储区块全数据,是区块进入不可修改的block_log之前的缓冲地带,包括本地的,同步过来的,未承认的,已承认的等等。
  • reversible_blocks,同样也是chainbase::database的一个实例,但它是用来存储那些已经成功被应用但仍旧是可逆的特殊区块。
  • blog,block_log类实例,是区块链不可逆数据的存储对象。这部分内容在数据存储结构部分已有详细解释,此处不再赘述。
  • pending,处于pending状态的一个区块的包装。
  • head,block_state_ptr结构体是所有区块的统一数据结构,head代表头区块对象。
  • fork_db,fork_database类实例,分叉库。
  • wasmif,wasm_interface类实例,是webassembly虚拟机接口的实例。
  • resource_limits,resource_limits_manager资源限制管理器实例。
  • authorization,authorization_manager认证权限管理器实例。
  • conf,controller::config前文介绍的配置config的实例。
  • chain_id,chain_id_type类型,代表区块链当前id。
  • replaying,是否允许重播,默认初始化为false。
  • replay_head_time,重播的头区块时间。
  • read_mode,数据库读取模式,默认初始话为SPECULATIVE
  • in_trx_requiring_checks,事务中是否需要检查,默认为false。如果为true的话,通常会被跳过的检查不会被跳过。例如身份验证。
  • subjective_cpu_leeway,剩余的cpu资源,以微妙计算。
  • trusted_producer_light_validation,可信的生产者执行轻量级校验,默认为false。
  • snapshot_head_block,快照的头区块号。
  • handler_key,处理者的键,元素为scope和action组成的二元组。
  • apply_handlers,应用操作的处理者,元素为以handler_key为键,std::function<void(apply_context&)>为值的map作为值,账户名作为键的复杂map。
  • unapplied_transactions,未应用的事务map,以sha256加密串作为键,transaction_metadata_ptr为值。pop_block函数或者abort_block函数为执行完毕的事务,如果再次被其他区块应用会从这个列表中移除,生产者在调度新事务打包到区块里时可以查询这个列表。

剩下的内容为controller_impl的众多功能函数的实现了,这些内容都是需要与其他程序组合使用,例如插件程序,或者智能合约,因此在接下来的篇章中,将会重新按照一个功能入口研究完整的使用脉络。而在这些功能中有两个内容需要在此处研究清楚,一个是fork_database,另一个是snapshot。下面逐一展开分析。

fork_database

在fork_database.hpp文件中声明。管理了轻量级状态数据,是由未确认的潜在区块产生的。当本地节点接收receive到新的区块时,它们将被推入fork数据库。fork数据库跟踪最长的链,以及最新不可逆块号。所有大于最新不可逆块号的区块将会在发出“irreversible”不可逆信号以后被释放掉,区块已经成功上链变为不可逆,因此fork库没必要再存储。分叉库提供了很多函数,例如通过区块id获取区块、通过区块号获取区块、插入区块包括set和add各种重载函数、删除区块、获取头区块、通过id获取两个分支、设置区块标志位等。

1. fork_database构造器

在controller_impl的构造函数体中会被调用。

代码语言:javascript
复制
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 )

进入构造器。

代码语言:javascript
复制
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也位于节点数据目录中,是前文介绍唯一没有说到的文件,这里补齐。

2. irreversible信号

上面讲到了,fork_database拥有一个公有成员irreversible信号。这个信号在controller_impl结构体的宏SET_APP_HANDLER中被使用:

代码语言:javascript
复制
fork_db.irreversible.connect( [&]( auto b ) {
                                 on_irreversible(b);
                                 });

这段代码其实是boost的信号槽机制,信号有一个connect操作,其参数是一个slot插槽,可将插槽连接到信号上,最终返回一个connection对象代表这段连接关系,可以灵活控制连接开关。插槽的类型可以是任意对象,这段代码中是一个lambda表达式,调用了on_irreversible函数。 接下来,去fork_database查询该信号的触发位置,出现在prune函数中的一段代码,

代码语言:javascript
复制
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表达式的规则将区块传入。该函数会将入参区块变为不可逆,处理成功以后,下面截取了这部分相关代码:

代码语言:javascript
复制
...
    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四个插件代码中得到了运用,也说明这四个插件是非常关心区块不可逆的状态变化的。至于他们具体是如何运用的,在相关部分会有详细介绍。

3. initialize_fork_db

初始化fork_db,主要工作是从创世块状态设置fork_db的头块。头块的数据结构是区块状态对象,构造头块时,要先构造区块头状态对象,包括:

  • active_schedule,活动的出块安排,默认为初始出块安排。
  • pending_schedule,等待中的出块安排,默认为初始出块安排。
  • pending_schedule_hash,等待中的出块安排的单向哈希值。
  • header.timestamp,等于创世块配置文件genesis中的timestamp值。
  • header.action_mroot,action的Merkel树根,创世块的值为链id值,该值是通过加密算法计算出的。
  • id,块id。
  • block_num,块号。

构建好区块头以后,接着构建区块体,构建完成以后,将完整头块插入到空的fork_db中。

4. commit_block -> add_to_fork_db

提交区块函数,无论提交是否成功,都不再保留活动的pending块。该函数有一个参数add_to_fork_db,是否加入fork_db。在producer_plugin生产者生产区块的逻辑中,提交区块调用controller对象的commit_block函数:

代码语言:javascript
复制
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相关的内容:

代码语言:javascript
复制
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两个插件中,说明这两个插件是要对这个信号感兴趣并捕捉该信号。

5. maybe_switch_forks

或许要切换分叉库到主库。该函数会在controller_impl结构体中的push_block和push_confirmation两个函数中被调用。

代码语言:javascript
复制
if ( read_mode != db_read_mode::IRREVERSIBLE ) { // 在db读取模式不等于IRREVERSIBLE时,要调用maybe_switch_forks函数。
    maybe_switch_forks( s );
}

db读取模式为IRREVERSIBLE时,只关心当前不可逆区块的数据,而fork_db中不存在不可逆区块的数据。而其他三种读取模式都涉及到可逆区块以及未被确认的数据,因此要去maybe_switch_forks函数检查处理一番。

  • 当fork_db头块的上一个块等于当前节点的头块时,说明有新块被接收,先到达fork_db中,执行:
代码语言:javascript
复制
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; // 更新节点主库的头块为当前块。
  • 当fork_db头块的前一个块不等于主库头块且fork_db头块id也不等于当前节点的头块id时,说明fork_db最新的两个块都不等于主库头块。这时候fork_db是更长的一条链,因此要切换主库为fork_db链。切换的过程很复杂,此处不展开。

6. controller析构对fork_db的处理

代码语言:javascript
复制
my->fork_db.close();

在controller析构时将fork_db关掉,因为它会生成irreversible信号到这个controller。如果db读取模式为IRREVERSIBLE,将应用最后一个不可逆区块,my需要成为指向有效controller_impl的指针。

代码语言:javascript
复制
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();
}

7. controller::startup对fork_db的处理

代码语言:javascript
复制
my->head = my->fork_db.head();

controller的startup周期时,会将fork_db的头块设置为主库头块(头块一般不是不可逆的)。

snapshot

快照,顾名思义,可以为区块链提供临时快速备份的功能。

1. abstract_snapshot_row_writer

该结构体位于命名空间eosio::chain::detail。提供了写入snapshot快照的能力,是所有关于快照写入的结构的基类。该结构体是一个抽象类型,包含四个成员函数:

  • write,参数为ostream_wrapper实例(同样在detail命名空间下定义)的引用。
  • write,重载参数为sha256的加密器。
  • to_variant,转型变体。
  • row_type_name,行类型名,字符串类型。

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_writerostream_snapshot_writer都是snapshot_writer的子类,根据不同的数据类型实现了不同的处理逻辑。

2. abstract_snapshot_row_reader

与上面相对的,是读取的部分,所有关于快照读取结构的基类。其包含三个成员虚函数:

  • provide,参数是std::istream的实例引用,说明是对标准库输入流的读取。
  • provide,重载参数是fc::variant的引用,对变体的读取。
  • row_type_name,行类型名,同上,字符串类型。

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_readerostream_snapshot_reader,还有integrity_hash_snapshot_writer(处理的是hash算法sha256的加密串)都是snapshot_writer的子类,根据不同的数据类型实现了不同的处理逻辑。

3. controller::startup对snapshot的处理

代码语言:javascript
复制
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。

代码语言:javascript
复制
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函数:

代码语言:javascript
复制
void read_from_snapshot( const snapshot_reader_ptr& snapshot ) {
  snapshot->read_section<chain_snapshot_header>([this]( auto &section ){
     chain_snapshot_header header;
     section.read_row(header, db);
     header.validate();
  });// 先读取快照头数据。
  snapshot->read_section<block_state>([this]( auto &section ){
     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, &section, &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”的参数,该配置项可以指定读取的快照文件。几个关键校验:

  • 注意不能同时配置“genesis-json”和“genesis-timestamp”两项,因为快照中已经存在这两项的值,会发生冲突。
  • 不能存在已有状态文件data/state/shared_memory.bin,因为快照只能被用来初始化一个空的状态数据库。
  • 校验block_log日志中不可逆区块的创世块是否与快照中的保持一致。

参数设置完毕,在chain_plugin的startup阶段,会检查快照地址,如果存在,则会带上该快照文件启动链。

代码语言:javascript
复制
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>,所以会执行controller的startup函数,这样就与上面的流程挂钩了,形成了一个完整的逻辑闭环。

4. controller::write_snapshot

代码语言:javascript
复制
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函数。

代码语言:javascript
复制
void add_to_snapshot( const snapshot_writer_ptr& snapshot ) const {
  snapshot->write_section<chain_snapshot_header>([this]( auto &section ){
     section.add_row(chain_snapshot_header(), db);// 向快照中写入快照头数据
  });
  snapshot->write_section<genesis_state>([this]( auto &section ){
     section.add_row(conf.genesis, db);// 向快照中写入创世块数据
  });
  snapshot->write_section<block_state>([this]( auto &section ){
     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, &section]( 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);// 向快照中写入资源限制数据
}

5. producer_plugin的create_snapshot()功能

controller::write_snapshot函数在外部由producer_plugin所调用。producer_plugin通过rpc api接口create_snapshot对外提供了创建快照的功能。这个功能无疑是非常实用的,可以为生产者提供快速数据备份的能力,为整个EOS区块链的运维工作增加了健壮性。producer_plugin的具体的实现代码:

代码语言:javascript
复制
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快照进行了详细的研究与分析。其他的众多功能由于他们与插件的紧密交互性,将会在相关插件的部分详细分析。

参考资料

  • EOSIO/eos
  • boost

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-11-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 命名空间namespace
  • using语法
  • controller依赖功能
  • controller扩展
  • controller的具体实现
  • fork_database
    • 1. fork_database构造器
      • 2. irreversible信号
        • 3. initialize_fork_db
          • 4. commit_block -> add_to_fork_db
            • 5. maybe_switch_forks
              • 6. controller析构对fork_db的处理
                • 7. controller::startup对fork_db的处理
                • snapshot
                  • 1. abstract_snapshot_row_writer
                    • 2. abstract_snapshot_row_reader
                      • 3. controller::startup对snapshot的处理
                        • 4. controller::write_snapshot
                          • 5. producer_plugin的create_snapshot()功能
                          • 总结
                          • 参考资料
                          相关产品与服务
                          数据库
                          云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档