前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >EOS行为核心:解析插件chain_plugin

EOS行为核心:解析插件chain_plugin

作者头像
文彬
发布2018-12-14 11:27:20
9050
发布2018-12-14 11:27:20
举报
文章被收录于专栏:醒者呆

EOS提供了大量的rpc接口,其中功能性最强,使用最频繁的一部分接口是EOS的行为核心,由chain_api_plugin提供,具体实现是在chain_plugin。

关键字:EOS,区块链,chain_plugin,chain_api_plugin,rpc,FC_REFLECT,反射,method模板,channel模板

一、接口列表chain_api_plugin

rpc调用逻辑,chainbase数据库底层原理,nodeos启动流程,plugin生命周期在前文都有介绍。本节直接研究chain_plugin的内容,研究入口会从chain_api_plugin中暴漏的rpc接口切入,这些接口是非常熟悉的,因为之前演练cleos相关命令时调用的也是rpc。首先展示一下所有的接口内容:

代码语言:javascript
复制
_http_plugin.add_api({
  CHAIN_RO_CALL(get_info, 200l),
  CHAIN_RO_CALL(get_block, 200),
  CHAIN_RO_CALL(get_block_header_state, 200),
  CHAIN_RO_CALL(get_account, 200),
  CHAIN_RO_CALL(get_code, 200),
  CHAIN_RO_CALL(get_code_hash, 200),
  CHAIN_RO_CALL(get_abi, 200),
  CHAIN_RO_CALL(get_raw_code_and_abi, 200),
  CHAIN_RO_CALL(get_raw_abi, 200),
  CHAIN_RO_CALL(get_table_rows, 200),
  CHAIN_RO_CALL(get_table_by_scope, 200),
  CHAIN_RO_CALL(get_currency_balance, 200),
  CHAIN_RO_CALL(get_currency_stats, 200),
  CHAIN_RO_CALL(get_producers, 200),
  CHAIN_RO_CALL(get_producer_schedule, 200),
  CHAIN_RO_CALL(get_scheduled_transactions, 200),
  CHAIN_RO_CALL(abi_json_to_bin, 200),
  CHAIN_RO_CALL(abi_bin_to_json, 200),
  CHAIN_RO_CALL(get_required_keys, 200),
  CHAIN_RO_CALL(get_transaction_id, 200),
  CHAIN_RW_CALL_ASYNC(push_block, chain_apis::read_write::push_block_results, 202),
  CHAIN_RW_CALL_ASYNC(push_transaction, chain_apis::read_write::push_transaction_results, 202),
  CHAIN_RW_CALL_ASYNC(push_transactions, chain_apis::read_write::push_transactions_results, 202)
});

这些接口可以分为两类,一类是通过宏CHAIN_RO_CALL调用的,另一类是通过宏CHAIN_RW_CALL_ASYNC调用。

(1) CHAIN_RO_CALL

代码语言:javascript
复制
#define CHAIN_RO_CALL(call_name, http_response_code) CALL(chain, ro_api, chain_apis::read_only, call_name, http_response_code)

采用同步只读的方式调用宏CALL。call_name是调用的函数名,http_response_code是响应码。下面进入宏CALL。

代码语言:javascript
复制
/**
 *  @attention 目前调用CALL函数的只有read_only应用。
 *  @param api_name "chain'
 *  @param api_handle app().get_plugin<chain_plugin>().get_read_only_api();
 *  @param api_namespace chain_apis::read_only
 *  @param call_name -INHERIT
 *  @param http_response_code -INHERIT
 */
#define CALL(api_name, api_handle, api_namespace, call_name, http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \ /*拼接接口url:http://ip:port/v1/chain/{call_name}*/ \
    /*
     * @param body:http请求体
     * @param cb:回调函数,用于返回处理结果
     */ \
   [api_handle](string, string body, url_response_callback cb) mutable { \
       api_handle.validate(); \
       try { \
          if (body.empty()) body = "{}"; \
          /*
           * api_handle为chain_plugin中read_only类的实例
           * call_name为函数名,实现体找chain_plugin.cpp文件
           * 函数参数1个:此处规定了一个命名规则,接口名加入后缀_param即为请求参数结构
           */ \
          auto result = api_handle.call_name(fc::json::from_string(body).as<api_namespace::call_name ## _params>()); \
          /*回调函数返回处理结果,此处也规定了一个命名规则,接口名加入后缀_result即为返回结构,转化为json的格式返回。*/ \
          cb(http_response_code, fc::json::to_string(result)); \
       } catch (...) { \
          /*捕捉到异常,调用http_plugin的异常处理函数handle_exception*/ \
          http_plugin::handle_exception(#api_name, #call_name, body, cb); \
       } \
    } \
}
api_handle参数

同步只读的请求传入的api_handle参数值为ro_api变量,该变量是在chain_api_plugin插件启动chain_api_plugin::plugin_startup时(插件的生命周期前文已有介绍)初始化的,

代码语言:javascript
复制
auto ro_api = app().get_plugin<chain_plugin>().get_read_only_api();

app()函数以及与application类相关的内容前文已经介绍过,通过get_plugin<chain_plugin>获取chain_plugin的实例,然后调用其成员函数get_read_only_api(),

代码语言:javascript
复制
chain_apis::read_only get_read_only_api() const { return chain_apis::read_only(chain(), get_abi_serializer_max_time()); } //注意const修饰符,函数体内返回值是不可修改的。

返回的是chain_apis::read_only构造函数返回的read_only实例。类read_only中包含了所有基于只读机制的接口实现,与上面接口列表中声明的保持一致。

代码语言:javascript
复制
read_only(const controller& db, const fc::microseconds& abi_serializer_max_time)
    : db(db), abi_serializer_max_time(abi_serializer_max_time) {}

因此,最后传入CALL宏的api_handle参数值实际就是这个类read_only的实例。之后使用该实例去调用call_name,就是简单的实例调用自身成员函数(一般这个成员函数是声明和实现都有的)的逻辑了。

(2) CHAIN_RW_CALL_ASYNC

代码语言:javascript
复制
#define CHAIN_RW_CALL_ASYNC(call_name, call_result, http_response_code) CALL_ASYNC(chain, rw_api, chain_apis::read_write, call_name, call_result, http_response_code)

采用异步读写的方式调用异步处理宏CALL_ASYNC。call_name是调用的函数名,call_result传入声明的结果接收体(例如chain_apis::read_write::push_transaction_results),http_response_code是响应码。下面进入宏CALL_ASYNC。

代码语言:javascript
复制
/**
 *  @attention 目前调用CALL_ASYNC函数的只有read_write的应用。
 *  @param api_name "chain'
 *  @param api_handle app().get_plugin<chain_plugin>().get_read_write_api();
 *  @param api_namespace chain_apis::read_write
 *  @param call_name -INHERIT
 *  @param call_result -INHERIT
 *  @param http_response_code -INHERIT
 */
#define CALL_ASYNC(api_name, api_handle, api_namespace, call_name, call_result, http_response_code) \
{std::string("/v1/" #api_name "/" #call_name), \ /*同上,拼接接口url:http://ip:port/v1/chain/{call_name}*/ \
    /*
     * http处理请求的函数结构不变,同上。
     * @param body:http请求体
     * @param cb:回调函数,用于返回处理结果
     */ \
   [api_handle](string, string body, url_response_callback cb) mutable { \
      if (body.empty()) body = "{}"; \
      api_handle.validate(); \
      /*
       * api_handle为chain_plugin中read_only类的实例
       * call_name为函数名,实现体找chain_plugin.cpp文件
       * 函数参数2个:
       * @param 此处规定了一个命名规则,接口名加入后缀_param即为请求参数结构
       * @param lambda表达式,将cb和body按值传递进内部函数,该内部函数整体作为异步操作的回调函数,注意与http的回调函数cb区分。
       */ \
      api_handle.call_name(fc::json::from_string(body).as<api_namespace::call_name ## _params>(),\
         [cb, body](const fc::static_variant<fc::exception_ptr, call_result>& result){\
            /*捕获异常,分发异常处理*/ \
            if (result.contains<fc::exception_ptr>()) {\
               try {\
                  result.get<fc::exception_ptr>()->dynamic_rethrow_exception();\
               } catch (...) {\
                  http_plugin::handle_exception(#api_name, #call_name, body, cb);\
               }\
            } else {\
                /*
                 * 异步处理成功,通过http的回调函数cb返回结果。
                 */ \
               cb(http_response_code, result.visit(async_result_visitor()));\
            }\
         });\
   }\
}

其中最后处理结果的语句比较令人好奇result.visit(async_result_visitor()) result的类型是:const fc::static_variant<fc::exception_ptr, call_result>& async_result_visitor()函数:

代码语言:javascript
复制
struct async_result_visitor : public fc::visitor<std::string> {
   template<typename T>
   std::string operator()(const T& v) const {
      return fc::json::to_string(v); //与CALL处理返回结果相同的是,此处做的也是转换json的工作。
   }
};

接着,进入fc库的static_variant.hpp文件中寻找类static_variant,它包含一个模板函数visit:

代码语言:javascript
复制
template<typename visitor>
typename visitor::result_type visit(const visitor& v)const {
    return impl::storage_ops<0, Types...>::apply(_tag, storage, v);
}

异步处理将处理结果转型放置在结果容器中。

api_handle参数

异步读写的请求传入的api_handle参数值为rw_api变量,该变量是在chain_api_plugin插件启动chain_api_plugin::plugin_startup时(插件的生命周期前文已有介绍)初始化的,

代码语言:javascript
复制
auto rw_api = app().get_plugin<chain_plugin>().get_read_write_api();

app()函数以及与application类相关的内容前文已经介绍过,通过get_plugin<chain_plugin>获取chain_plugin的实例,然后调用其成员函数get_read_write_api(),

代码语言:javascript
复制
chain_apis::read_write get_read_write_api() { return chain_apis::read_write(chain(), get_abi_serializer_max_time()); }

返回的是chain_apis::read_write构造函数返回的read_write实例。类read_write中包含了所有基于读写机制的接口实现,与上面接口列表中声明的保持一致。

代码语言:javascript
复制
read_write(controller& db, const fc::microseconds& abi_serializer_max_time)
   : db(db), abi_serializer_max_time(abi_serializer_max_time) {}

因此,最后传入CALL_ASYNC宏的api_handle参数值实际就是这个类read_write的实例。之后使用该实例去调用call_name,就是简单的实例调用自身成员函数(一般这个成员函数是声明和实现都有的)的逻辑了。

chain_api_plugin生命周期
  • set_program_options,空
  • plugin_initialize,空
  • plugin_startup,添加rpc接口,请求chain_plugin功能函数。
  • plugin_shutdown,空

二、结构体成员序列化FC_REFLECT

FC_REFLECT为结构体提供序列化成员的能力。

FC_REFLECT是FC库中提供反射功能的宏。反射的意义在于了解一个未知的对象,反射是不限编程语言的,通过反射能够获取到对象的成员结构。宏#define FC_REFLECT( TYPE, MEMBERS )内部又调用了宏#define FC_REFLECT_DERIVED( TYPE, INHERITS, MEMBERS ),反射功能的具体实现就不深入探究了。下面来看其应用,举个例子:

代码语言:javascript
复制
FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_params, (transaction)(available_keys) )
FC_REFLECT( eosio::chain_apis::read_only::get_required_keys_result, (required_keys) )

两行代码分别包含了关于get_required_keys的两个结构体,

代码语言:javascript
复制
struct get_required_keys_params {
  fc::variant transaction;
  flat_set<public_key_type> available_keys;
};
struct get_required_keys_result {
  flat_set<public_key_type> required_keys;
};

get_required_keys是chain的RPC接口,结构体get_required_keys_params是该接口的请求参数的结构,而另一个get_required_keys_result是接口处理后返回的结构。

回过头继续看FC_REFLECT的两行代码,第一个参数传入的是结构体。第二个参数用圆括号包含,可以有多个,内容与结构体的成员一致。

FC_REFLECT实际上实现了面向对象编程中类成员的getter/setter方法。

三、chain_plugin生命周期

与基类定义的生命周期相同,也包含四个阶段。

chain_plugin::set_program_options

在nodeos程序调试部分有详细介绍。主要是添加chain_plugin相关的配置参数,一组是命令行的,另一组是来自配置文件的,其中命令行的配置项优先级更高。

chain_plugin::plugin_initialize

这个函数也是从nodeos程序入口而来,会传入配置项调用chain_plugin的初始化函数。初始胡函数获取到来自命令行和配置文件的中和配置参数以后,结合创世块配置,逐一处理相关参数逻辑。这些参数对应的处理逻辑如下表(对应controller的成员属性介绍)所示:

param

explanation

detail

action-blacklist

添加action黑名单

每一条数据是有账户和action名组成

key-blacklist

公钥黑名单

公钥集合

blocks-dir

设置数据目录

最终会处理为绝对路径保存到内存

checkpoint

检查点

缓存区块的检查点,用于快速扫描

wasm-runtime

虚拟机类型

可以指定运行时webassembly虚拟机类型

abi-serializer-max-time-ms

abi序列化最大时间

要提高这个数值防止abi序列化失败

chain-state-db-size-mb

链状态库大小

基于chainbase的状态主库的大小

chain-state-db-guard-size-mb

链状态库守卫大小

也是controller中提到的未包含在公开属性中的

reversible-blocks-db-size-mb

链可逆区块库大小

链可逆区块库也是基于chainbase的状态数据库

reversible-blocks-db-guard-size-mb

链可逆区块库守卫大小

也是controller中提到的未包含在公开属性中的

force-all-checks

是否强制执行所有检查

默认为false

disable-replay-opts

是否禁止重播参数

默认为false

contracts-console

是否允许合约输出到控制台

一般为了调试合约使用,默认为false

disable-ram-billing-notify-checks

是否允许内存账单通知

默认为false

extract-genesis-json/print-genesis-json

输出创世块配置

以json格式输出

export-reversible-blocks

导出可逆区块到路径

将可逆区块目录reversible中数据导入到指定路径

delete-all-blocks

删除所有区块数据

重置区块链

truncate-at-blocks

区块截取点

所有生效的指令都要截止到本参数设置的区块号

hard-replay-blockchain

强制重播区块链

清空状态库,通过repair_log获得backup,搭配fix-reversible-blocks从backup中恢复可逆区块到区块目录。

replay-blockchain

重播区块链

清空状态库,搭配fix-reversible-blocks从原区块目录的可逆区块目录自我修复

fix-reversible-blocks

修复可逆区块

调用函数recover_reversible_blocks传入源路径和新路径,可逆缓存大小,以及是否有截取点truncate-at-blocks

import-reversible-blocks

导入可逆区块路径(必须独立使用,没有其他参数命令)

清空可逆区块目录,调用import_reversible_blocks函数导入

snapshot

指定导入的快照路径

在controller的快照部分有详述

genesis-json

指定创世块配置文件

从文件中导出创世块的配置项到内存

genesis-timestamp

指定创世块的时间

同样将该时间配置到内存中对应的变量

read-mode

状态主库的读取模式

controller部分有详述

validation-mode

校验模式

controller部分有详述

chain_plugin参数处理完毕后,设置方法提供者(并没有找到该provider的应用)。接着转播信号到频道,为chain_plugin_impl的唯一指针my的connection属性赋值,创建信号槽。

  • pre_accepted_block_connection,连接信号pre_accepted_block,更新loaded_checkpoints区块检查点位置。
  • accepted_block_header_connection,连接信号accepted_block_header,承认区块头信号。
  • accepted_block_connection,连接信号accepted_block,承认区块信号。
  • irreversible_block_connection,连接信号irreversible_block,区块不可逆。
  • accepted_transaction_connection,连接信号accepted_transaction,承认事务。
  • applied_transaction_connection,连接信号applied_transaction,应用事务。
  • accepted_confirmation_connection,连接信号accepted_confirmation,承认确认。

chain_plugin的插件初始化工作完毕,主要是对chain_plugin的配置参数的处理,以及信号槽的实现。

chain_plugin::plugin_startup

chain_plugin插件的启动,首先是快照的处理,这部分在快照的内容中有介绍,是根据nodeos过来的快照参数,判断是否要加入快照参数调用controller的startup。这期间如有捕捉到异常,则执行controller的reset重置操作。然后根据controller的属性输出链日志信息。

chain_plugin::plugin_shutdown

重置所有的信号槽,重置controller。

四、RPC接口实现

外部rpc调用通过chain_api_plugin插件包裹的接口服务,内部接口的实现是在chain_plugin中,对应关系是在chain_api_plugin的接口列表,通过函数名字匹配。

1. 获取基本信息 get_info

代码语言:javascript
复制
// 返回值为read_only的实体成员get_info_results结构的实例。
read_only::get_info_results read_only::get_info(const read_only::get_info_params&) const {
   const auto& rm = db.get_resource_limits_manager();
   return {
      // 以下字段都与get_info_results结构匹配,最终构造出get_info_results实例返回。
      eosio::utilities::common::itoh(static_cast<uint32_t>(app().version())), // server_version
      db.get_chain_id(), // chain_id
      db.fork_db_head_block_num(), // head_block_num
      db.last_irreversible_block_num(), // last_irreversible_block_num
      db.last_irreversible_block_id(), // last_irreversible_block_id
      db.fork_db_head_block_id(), // head_block_id
      db.fork_db_head_block_time(), // head_block_time
      db.fork_db_head_block_producer(), // head_block_producer
      rm.get_virtual_block_cpu_limit(), // virtual_block_cpu_limit
      rm.get_virtual_block_net_limit(), // virtual_block_net_limit
      rm.get_block_cpu_limit(), // block_cpu_limit
      rm.get_block_net_limit(), // block_net_limit
      //std::bitset<64>(db.get_dynamic_global_properties().recent_slots_filled).to_string(), // recent_slots
      //__builtin_popcountll(db.get_dynamic_global_properties().recent_slots_filled) / 64.0, // participation_rate
      app().version_string(), // server_version_string
   };
}

可以看到get_info_results的部分字段是通过read_only::db对象获取,还有一部分资源相关的内容是通过db的资源限制管理器获得,而关于版本方面的数据是从application实例获得。

2. 获取区块信息 get_block

代码语言:javascript
复制
// 特殊的是,此处并没有创建一个get_block_result的结构体作为返回值的容器,是利用了variant语法将signed_block_ptr转换成可输出的状态。
fc::variant read_only::get_block(const read_only::get_block_params& params) const {
   signed_block_ptr block;
   // 如果参数block_num_or_id为空或者block_num_or_id的长度大于64,属于非法参数不处理,会报错。
   EOS_ASSERT(!params.block_num_or_id.empty() && params.block_num_or_id.size() <= 64, chain::block_id_type_exception, "Invalid Block number or ID, must be greater than 0 and less than 64 characters" );
   try {
      // 通过variant语法将参数block_num_or_id类型擦除然后通过as语法转化为block_id_type类型,
      block = db.fetch_block_by_id(fc::variant(params.block_num_or_id).as<block_id_type>());
      if (!block) {// 如果通过id的方法获得的block为空,则尝试使用区块号的方式获取。
         block = db.fetch_block_by_number(fc::to_uint64(params.block_num_or_id));// 利用to_uint64将参数转型。
      }// 如果获取失败,抛出异常,无效的参数block_num_or_id
   } EOS_RETHROW_EXCEPTIONS(chain::block_id_type_exception, "Invalid block ID: ${block_num_or_id}", ("block_num_or_id", params.block_num_or_id))

   EOS_ASSERT( block, unknown_block_exception, "Could not find block: ${block}", ("block", params.block_num_or_id));
   // 通过校验,开始返回对象。
   fc::variant pretty_output;
   // i将结果block的数据通过resolver解析到pretty_output
   abi_serializer::to_variant(*block, pretty_output, make_resolver(this, abi_serializer_max_time), abi_serializer_max_time);
   // 引用区块的前缀设置
   uint32_t ref_block_prefix = block->id()._hash[1];

   return fc::mutable_variant_object(pretty_output.get_object())
           ("id", block->id())
           ("block_num",block->block_num())
           ("ref_block_prefix", ref_block_prefix);
}

进一步研究区块的id是如何生成的,以及如何通过id获得区块号。是在block_heade.cpp中定义:

代码语言:javascript
复制
namespace eosio { namespace chain {
   digest_type block_header::digest()const
   {
      return digest_type::hash(*this);// hash算法为sha256,然后使用fc::raw::pack打包获取结果
   }
   
   uint32_t block_header::num_from_id(const block_id_type& id)
   {
      return fc::endian_reverse_u32(id._hash[0]);// 实际上是对区块id并入区块号的算法的逆向工程,获得区块号。
   }
   
   // id的类型为block_id_type
   block_id_type block_header::id()const
   {
      // id不包括签名区块头属性,尤其是生产者签名除外。
      block_id_type result = digest();//digest_type::hash(*this),this是id()的调用者。
      result._hash[0] &= 0xffffffff00000000;//对结果进行位操作,并入一个十六进制头。
      result._hash[0] += fc::endian_reverse_u32(block_num()); // 通过上一个区块id找到其区块号然后自增获得当前区块号。并入id数据成为其一部分。
      return result;
   }
} }

get_block拼接好id、block_num、ref_block_prefix最后三个字段以后,返回的数据结构如下图所示:

3. 获取区块头状态 get_block_header_state

注意与上面的get_block的实现区分,get_block_header_state是通过fetch_block_state_by_numberfetch_block_state_by_id函数获取到的是状态库中的区块对象,也就是说是可逆区块数据,而不是get_block通过fetch_block_by_numberfetch_block_by_id函数获取到的不可逆区块。 get_block_header_state获取到可逆区块以后,通过以下代码得到其区块头数据并返回。

代码语言:javascript
复制
fc::variant vo;
fc::to_variant( static_cast<const block_header_state&>(*b), vo );// block_state_ptr b;
return vo;

4. 获取账户信息 get_account

这个功能的实现函数代码较长,但做的工作实际上并不复杂,可以采用从返回的account数据结构来逆向分析该功能的实现方法:

代码语言:javascript
复制
struct get_account_results {
  name                       account_name; // 账户名,入参的值。
  uint32_t                   head_block_num = 0; // 头块号,controller的状态主库db获取
  fc::time_point             head_block_time; // 头块时间,controller的状态主库db获取

  bool                       privileged = false; // 是否超级账户,默认false。controller的状态主库db获取账户的属性之一。
  fc::time_point             last_code_update; // 最后的code修改时间,例如给账户set contract的时间。controller的状态主库db获取账户的属性之一。
  fc::time_point             created; // 账户创建时间。controller的状态主库db获取账户的属性之一。

  optional<asset>            core_liquid_balance; // 主币的余额,在accounts状态表里查到的

  int64_t                    ram_quota  = 0; // 内存限额(资源相关部分有详细介绍),从controller的资源管理器获取
  int64_t                    net_weight = 0; // 网络带宽资源权重,从controller的资源管理器获取
  int64_t                    cpu_weight = 0; // cpu资源权重,从controller的资源管理器获取

  account_resource_limit     net_limit; // 网络带宽资源,包括已使用、剩余可用、总量。从controller的资源管理器获取
  account_resource_limit     cpu_limit; // cpu带宽资源,包括已使用、剩余可用、总量。从controller的资源管理器获取
  int64_t                    ram_usage = 0; // 内存已使用量。从controller的资源管理器获取

  vector<permission>         permissions; // 账户的权限内容(账户多签名部分有详细介绍),在状态主库的表里查到的。

  fc::variant                total_resources; // 总资源量,包括网络、cpu、内存资源总量。在userres状态表里查到的
  fc::variant                self_delegated_bandwidth; // 自我抵押带宽。在delband状态表里查到的
  fc::variant                refund_request; // 退款请求。在refunds状态表里查到的
  fc::variant                voter_info; // 投票相关。在voters状态表里查到的
};

5. 获取账户code信息 get_code

注意该接口修改了源码,不支持返回wast数据了。因此在请求该接口的时候,要使用的参数如下:

代码语言:javascript
复制
{
    "account_name": "eosio.token",
    "code_as_wasm": true
}

返回的数据将包括

  • 该账户的名字。
  • code的hash值,先通过controller状态库查询到账户对象,然后将其code的data和size值做sha256哈希得到的值。
  • wasm的数据,就是完整的原始code的数据。
  • abi数据,通过abi_serializer将账户的abi数据解析出来。
代码语言:javascript
复制
read_only::get_code_results read_only::get_code( const get_code_params& params )const {
   get_code_results result;
   result.account_name = params.account_name;
   const auto& d = db.db();
   const auto& accnt  = d.get<account_object,by_name>( params.account_name );// 从controller状态库中获取账户信息
   // 当前默认不支持返回wast数据
   EOS_ASSERT( params.code_as_wasm, unsupported_feature, "Returning WAST from get_code is no longer supported" );

   if( accnt.code.size() ) {
      if (params.code_as_wasm) {
          // 完整的原始账户的code的数据
         result.wasm = string(accnt.code.begin(), accnt.code.end());
      }
      // 获得code的哈希值:将账户信息下的code的data和size值做sha256哈希得到的值
      result.code_hash = fc::sha256::hash( accnt.code.data(), accnt.code.size() );
   }
   // 获取账户的abi数据:通过abi_serializer将账户的abi数据解析出来。
   abi_def abi;
   if( abi_serializer::to_abi(accnt.abi, abi) ) {
      result.abi = std::move(abi);
   }

   return result;
}

6. 获得账户的code哈希值 get_code_hash

实现方法参照get_code,返回数据只包括code的hash值。

7. 获得账户的abi数据 get_abi

实现方法参照get_code,返回数据只包括账户的abi数据。

8. 获得账户的原始code和abi数据 get_raw_code_and_abi

代码语言:javascript
复制
read_only::get_raw_code_and_abi_results read_only::get_raw_code_and_abi( const get_raw_code_and_abi_params& params)const {
   get_raw_code_and_abi_results result;
   result.account_name = params.account_name;

   const auto& d = db.db();
   const auto& accnt = d.get<account_object,by_name>(params.account_name);
   result.wasm = blob{{accnt.code.begin(), accnt.code.end()}}; // 原始wasm值,完整取出即可。
   result.abi = blob{{accnt.abi.begin(), accnt.abi.end()}}; // 原始abi值,完整取出即可。

   return result;
}

9. 获得账户的原始abi数据 get_raw_abi

实现方法参照get_raw_code_and_abi,返回数据只包括账户的原始abi数据。

10. 获得一条状态库表的值 get_table_rows

首先查看该接口的传入参数的数据结构:

代码语言:javascript
复制
struct get_table_rows_params {
  bool        json = false; // 是否是json的格式
  name        code; // 传入code值,即拥有该table的账户名
  string      scope; // 传入scope值,即查询条件
  name        table; // 传入table的名字
  string      table_key; // table主键
  string      lower_bound; // 设置检索数据的下限,默认是first
  string      upper_bound; // 设置检索数据的上限,默认是last
  uint32_t    limit = 10; // 数据结果的最大条目限制
  string      key_type;  // 通过指定键的数据类型,定位查询依赖的键
  string      index_position; // 通过传入键的位置,,定位查询依赖的键。1 - 主键(first), 2 - 二级索引 (multi_index定义), 3 - 三级索引,等等
  string      encode_type{"dec"}; //加密类型,有十进制还是十六进制,默认是十进制dec。
};

除了code、scope、table以外都是可选的参数,这三个参数是定位检索数据的关键,所以不可省略。下面来看该接口的返回值类型:

代码语言:javascript
复制
struct get_table_rows_result {
  vector<fc::variant> rows; // 数据集合。一行是一条,无论是十六进制加密串还是解析成json对象,都代表一行。
  bool                more = false; // 如果最后显示的元素(受制于limit)并不是数据库中最后一个,则该字段会置为true
};

进入接口实现的函数体,内容较多。首先通过传入参数对象中的index_position字段来确定查询依赖的键,这是通过函数get_table_index_name完成的工作,同时会修改primary原对象的值,返回table名字的同时告知是否是主键(table的键的名字是与table名字相关的)。 接着,如果是主键:

代码语言:javascript
复制
// 对比入参对象的table名字是否与通过index反查的table名字保持一致。
EOS_ASSERT( p.table == table_with_index, chain::contract_table_query_exception, "Invalid table name ${t}", ( "t", p.table ));
auto table_type = get_table_type( abi, p.table );// 获得table类型
if( table_type == KEYi64 || p.key_type == "i64" || p.key_type == "name" ) {//支持这三种table类型
 return get_table_rows_ex<key_value_index>(p,abi);// 具体检索table的函数。
}
// 如果是已支持的三种table类型之外的,则会报错。
EOS_ASSERT( false, chain::contract_table_query_exception,  "Invalid table type ${type}", ("type",table_type)("abi",abi));

具体检索table的函数get_table_rows_ex,这是非常重要的一个函数,需要源码分析:

代码语言:javascript
复制
/**
* 检索table的核心函数
* @tparam IndexType 模板类,支持不同的索引类型
* @param p get_table_rows接口入参对象
* @param abi 通过controller查询入参code对应的程序abi
* @return 查询结果
*/
template <typename IndexType>
read_only::get_table_rows_result get_table_rows_ex( const read_only::get_table_rows_params& p, const abi_def& abi )const {
  read_only::get_table_rows_result result; // 首先定义结果容器
  const auto& d = db.db(); // 状态主库对象
  uint64_t scope = convert_to_type<uint64_t>(p.scope, "scope"); // 获得查询条件。

  abi_serializer abis;
  abis.set_abi(abi, abi_serializer_max_time);// 将abi_def类型的abi通过序列化转到abi_serializer类型的对象abis。

  // 查询状态库表的标准范式,返回的是通过code、scope、table检索到的结果集的数据迭代器,
  const auto* t_id = d.find<chain::table_id_object, chain::by_code_scope_table>(boost::make_tuple(p.code, scope, p.table));

  if (t_id != nullptr) { // 迭代器不为空
     const auto& idx = d.get_index<IndexType, chain::by_scope_primary>(); // 传入查询依赖的键,指定迭代器的索引。
     decltype(t_id->id) next_tid(t_id->id._id + 1);
     auto lower = idx.lower_bound(boost::make_tuple(t_id->id)); // 获取结果集上限
     auto upper = idx.lower_bound(boost::make_tuple(next_tid)); // 获取结果集下限

     if (p.lower_bound.size()) {// 如果入参对象设置了结果集下限
        if (p.key_type == "name") { // 主键类型是账户名字,设置下限对象
           name s(p.lower_bound);
           lower = idx.lower_bound( boost::make_tuple( t_id->id, s.value ));
        } else {// 主键类型是其他类型,设置下限对象
           auto lv = convert_to_type<typename IndexType::value_type::key_type>( p.lower_bound, "lower_bound" );
           lower = idx.lower_bound( boost::make_tuple( t_id->id, lv ));
        }
     }
     if (p.upper_bound.size()) {// 如果入参对象设置了结果集上限
        if (p.key_type == "name") {// 主键类型是账户名字,设置上限对象
           name s(p.upper_bound);
           upper = idx.lower_bound( boost::make_tuple( t_id->id, s.value ));
        } else {// 主键类型是其他类型,设置上限对象
           auto uv = convert_to_type<typename IndexType::value_type::key_type>( p.upper_bound, "upper_bound" );
           upper = idx.lower_bound( boost::make_tuple( t_id->id, uv ));
        }
     }
     // 迭代器启动迭代,开始检索
     vector<char> data;
     auto end = fc::time_point::now() + fc::microseconds(1000 * 10); /// 10ms 是最长时间
     unsigned int count = 0;
     auto itr = lower;
     for (; itr != upper; ++itr) {
        copy_inline_row(*itr, data); // 将迭代器当前指针指向的对象复制到data容器中去。

        if (p.json) { // 处理data为json格式,通过方法binary_to_variant,向result的结果集rows中插入解析后的明文json格式的data
           result.rows.emplace_back( abis.binary_to_variant( abis.get_table_type(p.table), data, abi_serializer_max_time, shorten_abi_errors ) );
        } else { // 未要求json格式,则直接返回data,data不是可读的。
           result.rows.emplace_back(fc::variant(data));
        }
        if (++count == p.limit || fc::time_point::now() > end) { // 两个限制:一是结果集行数limit限制,二是执行时间是否超时
           ++itr;
           break;
        }
     }
     if (itr != upper) { // 如果实际返回的结果集并没有完全输出所有符合要求的数据,则将more字段置为true,提醒用户还有符合要求的数据没显示。
        result.more = true;
     }
  }
  return result;
}

继续回到get_table_rows接口函数体,如果不是主键,则需要按照键类型来区分处理,键类型包括i64, i128, i256, float64, float128, ripemd160, sha256。这里与主键不同的是,检索table的核心函数改为get_table_rows_by_seckey,该函数与主键处理函数大部分逻辑是一致的,只是特殊在键的处理上,由于该函数是处理二级索引的,因此要先通过代码const auto& secidx = d.get_index<IndexType, chain::by_secondary>();获得二级索引。然后对迭代器数据集进行处理,获得结果集的循环起止位,最后循环导出结果集即可。

11. 指定范围获取table数据 get_table_by_scope

此处的scope并不是前面理解的查询条件,而是字面意思,表示一个范围,上面提到了,在表数据中,范围就是上限和下限以及条目限制。因此不难猜出get_table_by_scope接口的入参对象结构:

代码语言:javascript
复制
struct get_table_by_scope_params {
  name        code; // 必须字段,传入账户名
  name        table = 0; // 可选,作为过滤器
  string      lower_bound; // 范围下限,可选
  string      upper_bound; // 范围上限,可选
  uint32_t    limit = 10; // 范围数量,限制条目
};

那么处理结果集就简单了,实际上就是上面函数get_table_rows_ex的一部分,取出相关结果集返回即可。

12. 获取货币余额 get_currency_balance

接口入参结构:

代码语言:javascript
复制
struct get_currency_balance_params {
  name             code; // 账户名,token合约的owner,一般是eosio.token账户
  name             account; // 账户名,检索条件,查询该账户的余额
  optional<string> symbol; // 检索条件,需要的token符号,例如SYS(主币),EOS等。
};

函数的处理逻辑:

代码语言:javascript
复制
vector<asset> read_only::get_currency_balance( const read_only::get_currency_balance_params& p )const {
   const abi_def abi = eosio::chain_apis::get_abi( db, p.code ); // get_abi与前面RPC接口实现函数为同一个。先通过账户code获取eosio.token合约对象abi数据。
   auto table_type = get_table_type( abi, "accounts" ); // 在abi中找到accounts表,返回该表的索引类型。

   vector<asset> results; // 结果容器
   walk_key_value_table(p.code, p.account, N(accounts), [&](const key_value_object& obj){
       // 表数据的value值超过了assert数据类型的大小,说明是无效数据。
      EOS_ASSERT( obj.value.size() >= sizeof(asset), chain::asset_type_exception, "Invalid data on table");

      asset cursor;
      // obj.value.data()是原始数据。
      fc::datastream<const char *> ds(obj.value.data(), obj.value.size());
      fc::raw::unpack(ds, cursor); // 将datastream数据解包到cursor

      EOS_ASSERT( cursor.get_symbol().valid(), chain::asset_type_exception, "Invalid asset");

      if( !p.symbol || boost::iequals(cursor.symbol_name(), *p.symbol) ) { // 对比token符号,一致的添加至结果集。
        results.emplace_back(cursor);
      }
      return !(p.symbol && boost::iequals(cursor.symbol_name(), *p.symbol));
   });

   return results;
}

get_table_type函数:

代码语言:javascript
复制
string get_table_type( const abi_def& abi, const name& table_name ) {
   for( const auto& t : abi.tables ) { //遍历abi下的所有table
      if( t.name == table_name ){ // 找到符合条件的table
         return t.index_type; // 返回该table的索引类型。
      }
   }
   // 如果没查到,报错提示当前ABI中并未找到目标table。
   EOS_ASSERT( false, chain::contract_table_query_exception, "Table ${table} is not specified in the ABI", ("table",table_name) );
}

13. 获取货币状态 get_currency_stats

传入eosio.token合约owner账户以及token符号即可请求到该token的状态信息。

代码语言:javascript
复制
fc::variant read_only::get_currency_stats( const read_only::get_currency_stats_params& p )const {
   fc::mutable_variant_object results; // 结果容器
   const abi_def abi = eosio::chain_apis::get_abi( db, p.code );
   auto table_type = get_table_type( abi, "stat" ); // 在abi的表中找到stat表,返回其索引类型。
   uint64_t scope = ( eosio::chain::string_to_symbol( 0, boost::algorithm::to_upper_copy(p.symbol).c_str() ) >> 8 );

   walk_key_value_table(p.code, scope, N(stat), [&](const key_value_object& obj){
      EOS_ASSERT( obj.value.size() >= sizeof(read_only::get_currency_stats_result), chain::asset_type_exception, "Invalid data on table");
      fc::datastream<const char *> ds(obj.value.data(), obj.value.size());
      read_only::get_currency_stats_result result; // 接口的返回对象
      fc::raw::unpack(ds, result.supply);// 已发行量
      fc::raw::unpack(ds, result.max_supply);// 最大发行量
      fc::raw::unpack(ds, result.issuer); // 发行人
      results[result.supply.symbol_name()] = result; // 数组下标为token符号,内容是token信息。
      return true;
   });
   return results;
}

14. 获取生产者信息 get_producers

入参的结构有是否以json格式输出的布尔类型对象、数据集下限、数据集条目限制,三个都是可选参数。该接口获得的是当前链的生产者信息。该接口的返回值是一个显示所有生产者信息的列表,以及生产者投票总权重信息,最后也有一个more字段用于说明是否有更多未展示的符合条件的数据。

生产者信息是在system合约的producers表中存储。

具体接口的实现函数较长且与前面获取其他状态库表数据的逻辑相似,不在这里重复分析源码。源码中复杂的部分在于对各种二级索引的处理。

15. 获取生产者出块安排 get_producer_schedule

无请求参数,返回参数的结构有三个字段:

  • active,活跃的。直接取自controller的active_producers函数获得,实际上返回的就是controller_impl的属性my->head->active_schedule或者是如果存在pending块时my->pending->_pending_block_state->active_schedule。
  • pending,等待中的。与上面一项相似来自pending_producers()函数,my->head->pending_schedule或者是如果存在pending块时my->pending->_pending_block_state->pending_schedule。
  • proposed,计划中的。来自proposed_producers()函数,返回my->db.get<global_property_object>()获取的全局属性中的proposed_schedule字段。

16. 获取日程安排上链的事务信息 get_scheduled_transactions

请求参数的结构:

代码语言:javascript
复制
struct get_scheduled_transactions_params {
  bool        json = false; 
  string      lower_bound;  // 传入时间戳或者交易id。
  uint32_t    limit = 50;
};

返回值结构:

代码语言:javascript
复制
struct get_scheduled_transactions_result {
  fc::variants  transactions; // 事务数组
  string        more;
};

transactions的一个元素的结构为:

代码语言:javascript
复制
auto row = fc::mutable_variant_object()
          ("trx_id", itr->trx_id)
          ("sender", itr->sender)
          ("sender_id", itr->sender_id)
          ("payer", itr->payer)
          ("delay_until", itr->delay_until)
          ("expiration", itr->expiration)
          ("published", itr->published)
    ;

结果集会根据是否按照json格式输出而做出相应处理,如果不是json格式,要进行事务打包packed,这个之前也分析过。本接口实现函数内容较多,鉴于接口本身使用并不频繁,这里不展开研究。

17. abi数据明文json转二进制 abi_json_to_bin

入参结构:

代码语言:javascript
复制
struct abi_json_to_bin_params {
  name         code; // 合约owner账户
  name         action; // action名字
  fc::variant  args; // action参数,json明文格式
};

返回值就是二进制串集合。实现函数:

代码语言:javascript
复制
read_only::abi_json_to_bin_result read_only::abi_json_to_bin( const read_only::abi_json_to_bin_params& params )const try {
   abi_json_to_bin_result result;
   const auto code_account = db.db().find<account_object,by_name>( params.code ); // 找到合约owner账户
   EOS_ASSERT(code_account != nullptr, contract_query_exception, "Contract can't be found ${contract}", ("contract", params.code));

   abi_def abi;
   if( abi_serializer::to_abi(code_account->abi, abi) ) {// 反序列化解析abi
      abi_serializer abis( abi, abi_serializer_max_time );
      auto action_type = abis.get_action_type(params.action); // 获得action类型,在abi的action中寻找目标action
      EOS_ASSERT(!action_type.empty(), action_validate_exception, "Unknown action ${action} in contract ${contract}", ("action", params.action)("contract", params.code));
      try {
         result.binargs = abis.variant_to_binary( action_type, params.args, abi_serializer_max_time, shorten_abi_errors ); //将入参args由json转为二进制
      } EOS_RETHROW_EXCEPTIONS(chain::invalid_action_args_exception,
                                "'${args}' is invalid args for action '${action}' code '${code}'. expected '${proto}'",
                                ("args", params.args)("action", params.action)("code", params.code)("proto", action_abi_to_variant(abi, action_type)))
   } else {
      EOS_ASSERT(false, abi_not_found_exception, "No ABI found for ${contract}", ("contract", params.code));
   }
   return result;
} FC_RETHROW_EXCEPTIONS( warn, "code: ${code}, action: ${action}, args: ${args}",
                         ("code", params.code)( "action", params.action )( "args", params.args ))

实际上的转换工作是由variant_to_binary函数执行的。

18. abi数据二进制转明文json abi_bin_to_json

功能正好与上一个接口相反。入参结构中唯一不同的字段是json格式的args改为了二进制类型的binargs,实际上这个二进制是字符的集合vector<char>。返回值是json格式。函数实现与上面类似,不再展示。实际的转换工作是由binary_to_variant函数执行的。总结这两个接口实现函数可以发现,binary对应的就是二进制数据格式,而variant变体对应的是json格式。

19. 获取必须密钥 get_required_keys

传入使用密钥的transaction(json格式),以及当前支持的密钥集合。

代码语言:javascript
复制
read_only::get_required_keys_result read_only::get_required_keys( const get_required_keys_params& params )const {
   transaction pretty_input;
   auto resolver = make_resolver(this, abi_serializer_max_time);
   try {
      abi_serializer::from_variant(params.transaction, pretty_input, resolver, abi_serializer_max_time);//根据明文json事务,通过abi序列化器将数据输出到pretty_input,转为transaction对象。
   } EOS_RETHROW_EXCEPTIONS(chain::transaction_type_exception, "Invalid transaction")
   // 通过认证管理器获得必须密钥
   auto required_keys_set = db.get_authorization_manager().get_required_keys( pretty_input, params.available_keys, fc::seconds( pretty_input.delay_sec ));
   get_required_keys_result result;
   result.required_keys = required_keys_set;
   return result;
}

所以核心处理函数为认证管理器authorization_manager的get_required_keys函数:

代码语言:javascript
复制
flat_set<public_key_type> authorization_manager::get_required_keys( const transaction& trx,
                                                                       const flat_set<public_key_type>& candidate_keys,
                                                                       fc::microseconds provided_delay
                                                                     )const
   {
      auto checker = make_auth_checker( [&](const permission_level& p){ return get_permission(p).auth; },// 获取权限内容
                                        _control.get_global_properties().configuration.max_authority_depth, // 当前全局属性的最大权限深度
                                        candidate_keys,
                                        {},
                                        provided_delay,
                                        _noop_checktime
                                      ); // 获取认证检查器

      for (const auto& act : trx.actions ) { // 遍历事务的action
         for (const auto& declared_auth : act.authorization) {
            EOS_ASSERT( checker.satisfied(declared_auth), unsatisfied_authorization,
                        "transaction declares authority '${auth}', but does not have signatures for it.",
                        ("auth", declared_auth) );// 如果在密钥集合中发现没有能满足任意action需要的权限的,即报错提醒。
         }
      }

      return checker.used_keys();
   }

20. 获取事务id get_transaction_id

入参对象会转为transaction结构,返回对象是transaction_id_type,过程就很简单了,因为本身transaction_id_type就是transaction的成员,因此将入参转型后直接返回对象的调用即可。

21. 异步读写操作:推送区块 push_block

入参为chain::signed_block类型:

代码语言:javascript
复制
struct signed_block : public signed_block_header {
  using signed_block_header::signed_block_header; // 签名区块头
  signed_block() = default; // 默认构造器
  signed_block( const signed_block_header& h ):signed_block_header(h){} // 构造器,传入签名区块头
  vector<transaction_receipt>   transactions; // 包含收到事务的集合
  extensions_type               block_extensions; // 区块扩展
};

该接口的返回值push_block_results为空,没有返回值。接口的函数实现:

代码语言:javascript
复制
void read_write::push_block(const read_write::push_block_params& params, next_function<read_write::push_block_results> next) {
   try {
      app().get_method<incoming::methods::block_sync>()(std::make_shared<signed_block>(params));// 命名空间incoming::methods下的成员block_sync
      next(read_write::push_block_results{});// 调用next写入结果,实际上结果为空。
   } catch ( boost::interprocess::bad_alloc& ) {
      chain_plugin::handle_db_exhaustion();
   } CATCH_AND_CALL(next);
}

查看incoming::methods命名空间下的成员block_sync:

代码语言:javascript
复制
namespace incoming {
  namespace methods {
     // 推送block到一个独立的provider
     using block_sync = method_decl<chain_plugin_interface, void(const signed_block_ptr&), first_provider_policy>; 
  }
}

继续看method_decl的定义:

代码语言:javascript
复制
/**
* @tparam Tag - API鉴定器,用于区分相同方法的不同签名
* @tparam FunctionSig - 方法签名
* @tparam DispatchPolicy - 分发策略,规定了provider是如何被访问的
*/
template< typename Tag, typename FunctionSig, template <typename> class DispatchPolicy = first_success_policy>
struct  method_decl {
  using method_type = method<FunctionSig, DispatchPolicy<FunctionSig>>;
  using tag_type = Tag;
};

method_decl中调用了method模板,该特性是由appbase/method提供,它是一个松散的链接应用程序层级的函数。调用者Caller可以抓取一个方法并且调用它,而提供者Providers能够抓取一个方法然后注册它。method模板消除了应用程序中不同插件之间的耦合度,可以在不同插件之间完成松散地函数调用。

method模板的使用方式如下图:

实体A注册了一个函数到method里,使用FunctionSig作为key。实体B传入FunctionSig在method中寻找method并调用。同样的,实体C、实体D都可以来调用,实体A并不关心谁来调用,它不会与调用者发生强关系。 回到:push_block,这一行代码:

app().get_method

block_sync就是key,通过该键能够找到对应的method: app().get_method<incoming::methods::block_sync>()。获取到method以后,可以直接调用,传入参数,通过make_shared将rpc参数转成signed_block对象的(共享)指针: std::make_shared<signed_block>(params)。下面去找到key为block_sync的method的位置,查找其相关的register语句:

代码语言:javascript
复制
my->_incoming_block_sync_provider = app().get_method<incoming::methods::block_sync>().register_provider([this](const signed_block_ptr& block){
  my->on_incoming_block(block);
});

在producer_plugin中找到了method的注册位置,真实调用的函数为生产插件中的on_incoming_block函数,参数在外部处理传入符合signed_block指针类型。

on_incoming_block函数

下面来看on_incoming_block函数。首先打印日志,提醒告知接收到区块的区块号。然后区块时间与本地节点时间对时,超过本地7秒开外的就终止程序,日志提示。接着,获取节点当前链环境:

代码语言:javascript
复制
chain::controller& chain = app().get_plugin<chain_plugin>().chain();

接下来,判断本地节点是否已包含该区块,

代码语言:javascript
复制
signed_block_ptr controller::fetch_block_by_id( block_id_type id )const {// 传入区块id
   auto state = my->fork_db.get_block(id);// 在本地fork_db库中查找,是否之前已接收到分叉库了。
   if( state && state->block ) return state->block; // 如果找到了,则返回区块。
   auto bptr = fetch_block_by_number( block_header::num_from_id(id) ); //将id转为区块号,尝试以区块号来查找。
   if( bptr && bptr->id() == id ) return bptr; // 以区块号来查找并且找到了,则直接返回区块。
   return signed_block_ptr(); // 返回为空的signed_block对象。
}

如果判断存在,则终止程序。不存在可以继续处理。处理接收新区块时,仍旧要丢弃掉pending状态的区块。

pending状态区块的优先级有时候很低,前面讲到在写入快照时,此处又提到接收新区块时,都要将pending区块先丢弃再进行。

总结所有需要先丢弃pending区块的操作还有:

  • producer_plugin_impl::maybe_produce_block
  • producer_plugin_impl::start_block,
  • producer_plugin::get_integrity_hash,获取完整hash
  • producer_plugin::update_runtime_options,更新环境参数
  • producer_plugin::resume
  • producer_plugin::create_snapshot
  • producer_plugin_impl::on_incoming_block

接着设置异常回调,如果发生异常则执行回调函数,回归正常计划的出块循环节奏:

代码语言:javascript
复制
auto ensure = fc::make_scoped_exit([this](){
   schedule_production_loop(); // 正常计划的出块循环节奏。
});

接下来,向链推送目标区块chain.push_block(block);。异常处理,相关标志位处理,日志输出结果。继续回到push_block函数。

push_block函数

首先要判断是否是pending状态,推送区块前要保证没有pending区块。接着校验是否为空区块,区块状态是否为incomplete。通过校验后,发射预承认区块信号,携带区块对象。

代码语言:javascript
复制
emit( self.pre_accepted_block, b ); // 预承认区块信号

接着,如果节点未特殊配置强制检查以及区块状态为不可逆irreversible或者检验过validated,则将区块构建为可信block_state对象加入到fork_db。经历一系列校验,执行auto inserted = my->index.insert(n)添加区块到分叉库创建的多索引库fork_multi_index_type中,返回状态区块对象。回到push_block,检查区块生产者是否在可信生产者列表中,如果在,则将可信的生产者执行轻量级校验的标志位置为true。然后发射承认区块头信号,并携带区块状态数据。

代码语言:javascript
复制
emit( self.accepted_block_header, new_header_state ); // 承认区块头信号

接着判断如果当前数据库读取模式不是IRREVERSIBLE不可逆,则需要调用maybe_switch_forks处理分叉合并的事务。最后判断如果区块状态为irreversible,则发出第三个信号,不可逆区块信号,并携带区块数据。

代码语言:javascript
复制
emit( self.irreversible_block, new_header_state ); // 不可逆区块信号

push_block函数执行完毕,共发射了三个信号,对应的是前文提到的controller维护的信号,通过信号槽机制,找到connection,并执行对应函数操作即可,信号槽机制曾多次分析阐述,此处不展开。 push_block接口是推送本地的区块处理,并未涉及到区块链网络节点的广播。

22. 异步读写操作:推送事务 push_transaction

该接口的函数实现方式以及采用语法特性与push_block相似,本段不重复介绍。该接口的入参类型是一个变体对象variant_object,也就是说它没有像其他接口那样特别声明参数结构,而是在函数实现中,加入了对象的构造过程,参数对象最终通过abi_serializer::from_variant被构造成packed_transaction打包事务类型。返回值结构是有定义的:

代码语言:javascript
复制
struct push_transaction_results {
  chain::transaction_id_type  transaction_id; // 事务id
  fc::variant                 processed; // 加工过的事务对象
};

回到函数体,同样是基于method模板的功能,在producer_plugin中找到transaction_async注册的函数,传入了处理好的打包事务对象,是否存留标志位,用来接收返回值的next函数。实际调用了producer_plugin_impl::on_incoming_transaction_async函数。

on_incoming_transaction_async函数

该函数内容较多。首先,仍及是获取节点当前链环境:

代码语言:javascript
复制
chain::controller& chain = app().get_plugin<chain_plugin>().chain();

接着,判断当前链若是不存在pending块,则增加到pending块。接着推送区块是通过channel模板的机制,这是与method模板想类似的机制。首先来看函数中该机制首次出现的位置:

代码语言:javascript
复制
_transaction_ack_channel.publish(std::pair<fc::exception_ptr, packed_transaction_ptr>(response.get<fc::exception_ptr>(), trx)); // 传入事务对象trx

_transaction_ack_channel是当前实例的成员,找到当前实例的构造函数,发现该成员的初始化信息:

代码语言:javascript
复制
_transaction_ack_channel(app().get_channel<compat::channels::transaction_ack>())

app().get_channel<xxx>的结构与上面介绍method的机制非常相似,查看transaction_ack的声明:

代码语言:javascript
复制
namespace compat {
  namespace channels {
     using transaction_ack       = channel_decl<struct accepted_transaction_tag, std::pair<fc::exception_ptr, packed_transaction_ptr>>;
  }
}

该声明与上面method相关key的声明在同一个文件中,说明设计者的思路是有将他们归为一类的:都属于解耦调用的桥梁。接着查看channel_decl:

代码语言:javascript
复制
/**
* @tparam Tag - API鉴定器,用于区分相同数据类型
* @tparam Data - channel携带的数据类型
* @tparam DispatchPolicy - 当前channel的分发策略。默认是drop_exceptions
*/
template< typename Tag, typename Data, typename DispatchPolicy = drop_exceptions >
struct channel_decl {
  using channel_type = channel<Data, DispatchPolicy>;
  using tag_type = Tag;
};

与method_decl非常相似了。具体channel机制的分析如下图所示。

可以看得出,channel的订阅与发布的模式,对应的是method的注册和调用,主要区别在于主体的角色转换。

channel的订阅是要依赖频道本身的内容发布的,也就是说频道是要先存在的,主体A可以来订阅,主体C、主体D都可以来订阅,而与作为发布方的主体B无关,主体B不用知道有谁订阅了。而method的注册和调用正好是相反的。实际上对于本文研究到的channel和method,主体A都是producer_plugin。本例中,一个区块被广播出来,需要所有的订阅者来执行本地的区块接收操作,因此需要采用channel机制。

下面搜索transaction_ack频道的订阅处:

代码语言:javascript
复制
my->incoming_transaction_ack_subscription = app().get_channel<channels::transaction_ack>().subscribe(boost::bind(&net_plugin_impl::transaction_ack, my.get(), _1));

延伸到实际的调用函数net_plugin_impl::transaction_ack

代码语言:javascript
复制
void net_plugin_impl::transaction_ack(const std::pair<fc::exception_ptr, packed_transaction_ptr>& results) {
  transaction_id_type id = results.second->id();
  if (results.first) { // first位置是用来放异常信息的,如果first不为空则说明有异常。
     fc_ilog(logger,"signaled NACK, trx-id = ${id} : ${why}",("id", id)("why", results.first->to_detail_string()));
     dispatcher->rejected_transaction(id);// 调用rejected_transaction,从received_transactions接收事务集合中将其删除。
  } else {
     fc_ilog(logger,"signaled ACK, trx-id = ${id}",("id", id));
     dispatcher->bcast_transaction(*results.second);
  }
}
bcast_transaction函数

调用广播事务函数dispatch_manager::bcast_transaction。

代码语言:javascript
复制
void dispatch_manager::bcast_transaction (const packed_transaction& trx) {
  std::set<connection_ptr> skips; // 跳过的数据集合
  transaction_id_type id = trx.id();
  auto range = received_transactions.equal_range(id);
  for (auto org = range.first; org != range.second; ++org) {
     skips.insert(org->second); // 在接收事务集合中找到对应id的事务数据遍历放于skips。
  }
  received_transactions.erase(range.first, range.second); // 从received_transactions接收事务集合中将其删除。
  for (auto ref = req_trx.begin(); ref != req_trx.end(); ++ref) {
     if (*ref == id) { // 本地请求事务集合中,找到目标事务删除
        req_trx.erase(ref);
        break;
     }
  }
  if( my_impl->local_txns.get<by_id>().find( id ) != my_impl->local_txns.end( ) ) {
     fc_dlog(logger, "found trxid in local_trxs" );
     return;// 在本地事务集合中找到目标事务了,终止不必重复处理。
  }
  uint32_t packsiz = 0;
  uint32_t bufsiz = 0;
  time_point_sec trx_expiration = trx.expiration();
  net_message msg(trx);
  packsiz = fc::raw::pack_size(msg);
  bufsiz = packsiz + sizeof(packsiz);
  vector<char> buff(bufsiz);
  fc::datastream<char*> ds( buff.data(), bufsiz);
  ds.write( reinterpret_cast<char*>(&packsiz), sizeof(packsiz) );
  fc::raw::pack( ds, msg );// trx转为net_message结构,打包通过数据流ds到缓存buff中。
  node_transaction_state nts = {id,
                                trx_expiration,
                                trx,
                                std::move(buff),
                                0, 0, 0};
  my_impl->local_txns.insert(std::move(nts)); // 插入到本地事务集,net_plugin自定义的多索引库node_transaction_index中。

  if( !large_msg_notify || bufsiz <= just_send_it_max) { // max-implicit-request参数决定just_send_it_max,最大请求数量
     my_impl->send_all( trx, [id, &skips, trx_expiration](connection_ptr c) -> bool {
           if( skips.find(c) != skips.end() || c->syncing ) {// skips中一旦有了当前连接,或者connection正在同步中,则退出。
              return false;
           }
           const auto& bs = c->trx_state.find(id); // 连接中的事务状态多索引库中寻找目标事务,返回事务数据
           bool unknown = bs == c->trx_state.end();
           if( unknown) {// 没找到则插入
              c->trx_state.insert(transaction_state({id,true,true,0,trx_expiration,time_point() }));
              fc_dlog(logger, "sending whole trx to ${n}", ("n",c->peer_name() ) );
           } else { // 找到则更新过期时间、状态库数据
              update_txn_expiry ute(trx_expiration);
              c->trx_state.modify(bs, ute);
           }
           return unknown;
        });
  }else {// 超过最大请求数量以后,不处理trx,而是pending_notify
     notice_message pending_notify;
     pending_notify.known_trx.mode = normal;
     pending_notify.known_trx.ids.push_back( id );
     pending_notify.known_blocks.mode = none;
     my_impl->send_all(pending_notify, [id, &skips, trx_expiration](connection_ptr c) -> bool {
           if (skips.find(c) != skips.end() || c->syncing) {
              return false;
           }
           const auto& bs = c->trx_state.find(id);
           bool unknown = bs == c->trx_state.end();
           if( unknown) {
              fc_dlog(logger, "sending notice to ${n}", ("n",c->peer_name() ) );
              c->trx_state.insert(transaction_state({id,false,true,0,trx_expiration,time_point() }));
           } else {
              update_txn_expiry ute(trx_expiration);
              c->trx_state.modify(bs, ute);
           }
           return unknown;
        });
  }
}

23. 异步读写操作:推送事务数组 push_transactions

这个接口是针对网络情况不理想,一次请求希望携带更多事务的场景而设计的,实现函数是调用一个递归函数push_recurse遍历传入的transactions数组,每个transaction最终仍旧会通过以上push_transaction函数逐一处理。

目前事务数组最多支持1000笔,多了报错。

总结

chain_plugin是EOS的核心,承载了大部分链相关的功能。本文按照rpc访问的脉络分析,从chain_api_plugin的rpc接口列表展开介绍,延伸到chain_plugin的接口实现,深入分析了所有的rpc接口的背后实现逻辑,其中涉及到了FC_REFLECT反射技术,通过method模板关联到了producer_plugin,通过channel模板技术关联到了net_plugin。chain_plugin是核心链处理插件,本文在该范畴下进行了详尽地调研,加深了对于fork_db,多索引库以及各种出现的数据结构的理解。

参考资料

EOSIO\eos

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、接口列表chain_api_plugin
    • (1) CHAIN_RO_CALL
      • api_handle参数
    • (2) CHAIN_RW_CALL_ASYNC
      • api_handle参数
      • chain_api_plugin生命周期
  • 二、结构体成员序列化FC_REFLECT
  • 三、chain_plugin生命周期
    • chain_plugin::set_program_options
      • chain_plugin::plugin_initialize
        • chain_plugin::plugin_startup
          • chain_plugin::plugin_shutdown
          • 四、RPC接口实现
            • 1. 获取基本信息 get_info
              • 2. 获取区块信息 get_block
                • 3. 获取区块头状态 get_block_header_state
                  • 4. 获取账户信息 get_account
                    • 5. 获取账户code信息 get_code
                      • 6. 获得账户的code哈希值 get_code_hash
                        • 7. 获得账户的abi数据 get_abi
                          • 8. 获得账户的原始code和abi数据 get_raw_code_and_abi
                            • 9. 获得账户的原始abi数据 get_raw_abi
                              • 10. 获得一条状态库表的值 get_table_rows
                                • 11. 指定范围获取table数据 get_table_by_scope
                                  • 12. 获取货币余额 get_currency_balance
                                    • 13. 获取货币状态 get_currency_stats
                                      • 14. 获取生产者信息 get_producers
                                        • 15. 获取生产者出块安排 get_producer_schedule
                                          • 16. 获取日程安排上链的事务信息 get_scheduled_transactions
                                            • 17. abi数据明文json转二进制 abi_json_to_bin
                                              • 18. abi数据二进制转明文json abi_bin_to_json
                                                • 19. 获取必须密钥 get_required_keys
                                                  • 20. 获取事务id get_transaction_id
                                                    • 21. 异步读写操作:推送区块 push_block
                                                      • on_incoming_block函数
                                                      • push_block函数
                                                    • 22. 异步读写操作:推送事务 push_transaction
                                                      • on_incoming_transaction_async函数
                                                      • bcast_transaction函数
                                                    • 23. 异步读写操作:推送事务数组 push_transactions
                                                    • 总结
                                                    • 参考资料
                                                    相关产品与服务
                                                    区块链
                                                    云链聚未来,协同无边界。腾讯云区块链作为中国领先的区块链服务平台和技术提供商,致力于构建技术、数据、价值、产业互联互通的区块链基础设施,引领区块链底层技术及行业应用创新,助力传统产业转型升级,推动实体经济与数字经济深度融合。
                                                    领券
                                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档