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. 这是一个使用键存储对象, 且经过排序的容器, 同时它是一个去重容器, 也就是说容器中不会包含两个相同的元素.
其中被序列化公开的属性有:
- 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, 区块状态枚举类, 包括:
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 的构造函数体中会被调用.
- 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 也位于节点数据目录中, 是前文介绍唯一没有说到的文件, 这里补齐.
2. irreversible 信号
上面讲到了, 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 四个插件代码中得到了运用, 也说明这四个插件是非常关心区块不可逆的状态变化的. 至于他们具体是如何运用的, 在相关部分会有详细介绍.
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 函数:
- 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 两个插件中, 说明这两个插件是要对这个信号感兴趣并捕捉该信号.
5. maybe_switch_forks
或许要切换分叉库到主库. 该函数会在 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 函数检查处理一番.
当 fork_db 头块的上一个块等于当前节点的头块时, 说明有新块被接收, 先到达 fork_db 中, 执行:
- 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 的处理
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();
- }
7. controller::startup 对 fork_db 的处理
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_writer 和 ostream_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_reader 和 ostream_snapshot_reader, 还有 integrity_hash_snapshot_writer(处理的是 hash 算法 sha256 的加密串)都是 snapshot_writer 的子类, 根据不同的数据类型实现了不同的处理逻辑.
3. controller::startup 对 snapshot 的处理
- 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" 的参数, 该配置项可以指定读取的快照文件. 几个关键校验:
注意不能同时配置 "genesis-json" 和 "genesis-timestamp" 两项, 因为快照中已经存在这两项的值, 会发生冲突.
不能存在已有状态文件 data/state/shared_memory.bin, 因为快照只能被用来初始化一个空的状态数据库.
校验 block_log 日志中不可逆区块的创世块是否与快照中的保持一致.
参数设置完毕, 在 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>, 所以会执行 controller 的 startup 函数, 这样就与上面的流程挂钩了, 形成了一个完整的逻辑闭环.
- 4. controller::write_snapshot
- 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);// 向快照中写入资源限制数据
- }
5. producer_plugin 的 create_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 快照进行了详细的研究与分析. 其他的众多功能由于他们与插件的紧密交互性, 将会在相关插件的部分详细分析.
参考资料
EOSIO/eos
boost
更多文章请转到醒者呆的博客园.
来源: https://www.cnblogs.com/Evsward/p/controller.html