标签归档:bitshares

Graphene 源码阅读 – 交易篇 – 交易费用

交易费用

操作类型不同, 所需费用也不同. 各项操作的费用记录在 global_property_object::chain_parameters::fee_schedule 中.

石墨烯代码将创世信息中的 inital_parameters::current_fees, global_property_object::chain_parameters::fee_schdule, 以及各项操作中的 struct fee_parameters_type {} 结构关联了起来.

节点启动之前, 一般我们会使用 —create-genesis-json 选项创建创世文件, 创世文件中的 inital_parameters::current_fees 信息会使用各个操作的 struct fee_parameters_type {} 结构写入, 参见:

// 代码 1.1
//  libraries/app/application.cpp

 79 namespace detail {
  80
  81    graphene::chain::genesis_state_type create_example_genesis() {
  82       auto nathan_key = fc::ecc::private_key::regenerate(fc::sha256::hash(string("nathan")));
  83       dlog("Allocating all stake to ${key}", ("key", utilities::key_to_wif(nathan_key)));
  84       graphene::chain::genesis_state_type initial_state;
  85       initial_state.initial_parameters.current_fees = fee_schedule::get_default();//->set_all_fees(GRAPHENE_BLOCKCHAIN_PRECISION);
  86       initial_state.initial_active_witnesses = GRAPHENE_DEFAULT_MIN_WITNESS_COUNT;
  87       initial_state.initial_timestamp = time_point_sec(time_point::now().sec_since_epoch() /
  88             initial_state.initial_parameters.block_interval *
  89             initial_state.initial_parameters.block_interval);
  90       for( uint64_t i = 0; i < initial_state.initial_active_witnesses; ++i )
  91       {
  92          auto name = "init"+fc::to_string(i);
  93          initial_state.initial_accounts.emplace_back(name,
  94                                                      nathan_key.get_public_key(),
  95                                                      nathan_key.get_public_key(),
  96                                                      true);
  97          initial_state.initial_committee_candidates.push_back({name});
  98          initial_state.initial_witness_candidates.push_back({name, nathan_key.get_public_key()});
  99       }
 100
 101       initial_state.initial_accounts.emplace_back("nathan", nathan_key.get_public_key());
 102       initial_state.initial_balances.push_back({nathan_key.get_public_key(),
 103                                                 GRAPHENE_SYMBOL,
 104                                                 GRAPHENE_MAX_SHARE_SUPPLY});
 105       initial_state.initial_chain_id = fc::sha256::hash( "BOGUS" );
 106
 107       return initial_state;
 108    }

然后在启动时, global_property_object::chain_parameters::fee_schdule 会用创世信息中的 inital_parameters::current_fees 初始化自己; 后续创建打包交易使用的费用信息都是从 global_property_object::chain_parameters::fee_schdule 获得, 各个操作自己的 struct fee_parameters_type {} 不再被使用.

交易费用的设置

设置交易费用一般发生在交易签名之前, 如果交易中包含多个操作, 每个操作的费用都会被计算并设置:

// 代码 1.2
// libraries/wallet/wallet.cpp

 501    void set_operation_fees( signed_transaction& tx, const fee_schedule& s  )
 502    {
 503       for( auto& op : tx.operations )
 504          s.set_fee(op);
 505    }

fee_schedule::set_fee(op) 方法以操作为参数, 负责设置每个操作的费用. set_fee() 首先调用 calculate_fee() 设置计算操作的费用, calculate_fee() 这里用到了一个 calc_fee_visitor, 这个 visitor 以 fee_scheduleop 为参数, 就是用 op 的计费方法以及 fee_schedule 的计费参数计算费用. calc_fee_visitor 里有一个 try … catch (代码 1.4) 可能不好理解, 这里的 try … catch 是因为 fee_schedule 这块代码有点问题, 除了 opaccount_create_operation 之外, 其它情况下 param.get<OpType>() 都会抛异常, 这点感兴趣可以看一下 fee_schedule 的源码便知原因.

// 代码 1.3
// libraries/chain/protocol/fee_schedule.cpp

133    asset fee_schedule::calculate_fee( const operation& op, const price& core_exchange_rate )const
134    {
135       auto base_value = op.visit( calc_fee_visitor( *this, op ) );
136       auto scaled = fc::uint128(base_value) * scale;
137       scaled /= GRAPHENE_100_PERCENT;
138       FC_ASSERT( scaled <= GRAPHENE_MAX_SHARE_SUPPLY );
139       //idump( (base_value)(scaled)(core_exchange_rate) );
140       auto result = asset( scaled.to_uint64(), asset_id_type(0) ) * core_exchange_rate;
141       //FC_ASSERT( result * core_exchange_rate >= asset( scaled.to_uint64()) );
142
143       while( result * core_exchange_rate < asset( scaled.to_uint64()) )
144         result.amount++;
145
146       FC_ASSERT( result.amount <= GRAPHENE_MAX_SHARE_SUPPLY );
147       return result;
148    }

150    asset fee_schedule::set_fee( operation& op, const price& core_exchange_rate )const
151    {
152       auto f = calculate_fee( op, core_exchange_rate );
153       auto f_max = f;
154       for( int i=0; i<MAX_FEE_STABILIZATION_ITERATION; i++ )
155       {
156          op.visit( set_fee_visitor( f_max ) );
157          auto f2 = calculate_fee( op, core_exchange_rate );
158          if( f == f2 )
159             break;
160          f_max = std::max( f_max, f2 );
161          f = f2;
162          if( i == 0 )
163          {
164             // no need for warnings on later iterations
165             wlog( "set_fee requires multiple iterations to stabilize with core_exchange_rate ${p} on operation ${op}",
166                ("p", core_exchange_rate) ("op", op) );
167          }
168       }
169       return f_max;
170    }
// 代码 1.4
libraries/chain/protocol/fee_schedule.cpp

 78    struct calc_fee_visitor
 79    {
 80       typedef uint64_t result_type;
 81
 82       const fee_schedule& param;
 83       const int current_op;
 84       calc_fee_visitor( const fee_schedule& p, const operation& op ):param(p),current_op(op.which()){}
 85
 86       template<typename OpType>
 87       result_type operator()( const OpType& op )const
 88       {
 89          try {
 90             return op.calculate_fee( param.get<OpType>() ).value;
 91          } catch (fc::assert_exception e) {
 92              fee_parameters params; params.set_which(current_op);
 93              auto itr = param.parameters.find(params);
 94              if( itr != param.parameters.end() ) params = *itr;
 95              return op.calculate_fee( params.get<typename OpType::fee_parameters_type>() ).value;
 96          }
 97       }
 98    };

calculate_fee 算出费用后, 便会调用 op.visit(set_fee_visitor(f_max)) 将具体费用设置到操作中, set_fee_visitor() 很简单, 就是将 f_max 赋值给操作的 fee 成员, 是的, 每个操作都有一个 fee 成员.

另外在 fee_schedule::set_fee 代码中还考虑到 core_exchange_rate 的变动而多循环执行了几次费用计算, 以达到费用更精确的目的.

// 代码 1.5
// libraries/chain/protocol/fee_schedule.cpp

100    struct set_fee_visitor
101    {
102       typedef void result_type;
103       asset _fee;
104
105       set_fee_visitor( asset f ):_fee(f){}
106
107       template<typename OpType>
108       void operator()( OpType& op )const
109       {
110          op.fee = _fee;
111       }
112    };

至此, 这笔操作的交易费用就被计算并设置到了操作的成员变量中.

Graphene 源码阅读 – RPC 篇 – API 注册机制

API 注册这部分感觉又是 BM 炫技的部分, 之前数据库索引篇的各种泛型, 奇异模板, 递归模板已经让我抓狂了一次, 没想到 api 注册本以为很直观的东西, 竟然也搞的那么复杂, 真是累觉不爱, 本篇不做详解, 只陈述一下概念和流程.

Graphene 的 api 被分为 login_api, database_api, network_broadcast_api, history_api, asset_api 等几类, 除了 database_api 之外都定义在 <app>/api.hpp 中, database_api 可能会因为 api 数太多所以单独放在 <app>/database_api.hpp 中.

下面自顶向下, 看一看 api 注册经过了哪些模块, 是如何被注册的.

websocket_server 与 websocket_connection

websocket_server 的作用显而易见, 它负责监听在 RPC 服务端口上, 接受客户端连接并响应客户端请求. 每当新的连接到来就会创建一个 websocket_connection 实例, 这个实例用来后续与对应的客户端通信, 这和我们所了解的原生 socket 编程中 accept 返回与客户端通信的 socket 是一样的.

websocket_server
             |
             |
             | on_connection (创建 websocket_connection 用于和客户端通信)
             |
             | websocket_connection 包含 websocket_api_connection
            V
websocket_api_connection (register login_api, database_api) 

websocket_api_connection

上面的 websocket_server, websocket_connection 是对 websocketapp 的直接封装, 而 websocket_api_connection 则是 fc 嫌 websocket_connection 的 “状态” 表现不丰富而定义的一个类, 它作为 websocket_connection 的数据成员, 扩充了每个连接的 “状态” 信息, 在 bitshares 的 RPC 通信过程中客户端后面所能够调用的 api 可能随前面所调的 api 影响, 这些状态当然是业务相关的, 而不是 websocket 协议相关的, 所以需要 websocket_api_connection 这个扩展类来做.

一个比较典型的例子就是, 连接建立后客户端首先调用 login_apidatabase 方法来开启 database_api 的访问 (注意这里说的不是 api 访问权限, 那是另一个问题), 其次才能调用 database_api 下的各个方法.

>>> {"id":1,"method":"call","params":[2,"get_chain_id",[]]}
{"id":1, "result":"error!"}

>>> {"id":1,"method":"call","params":[1,"database",[]]}
{"id":1, "result":2}

>>> {"id":1,"method":"call","params":[2,"get_chain_id",[]]}
{"id":1, "result":"correct chain id"}

websocket_api_connection 的核心成员如下:

fc::http::websocket_connection&          _connection;                                  // 指向包含它的 websocket_connection 对象
fc::rpc::state                                        _rpc_state;                                    // 通信状态, 包括 request, response, 递增请求 id

/// 下面这俩继承自父类 api_connection
std::vector< std::unique_ptr<generic_api> >                      _local_apis;       // 存储所有注册进来的 api 们
std::map< uint64_t, api_id_type >                                _handle_to_id;       // api 的 handle 实际上是 api 实例的指针, api 的 id 就是它注册进 _local_apis 的序号. 目前只在 register_api 时查重用, 不必做太多关注

fc::rpc::state

websocket_api_connection 的成员 _rpc_state 维护了与客户端通信的 request/response 队列, 以及消息 id 的自增. websocket_api_connection 在构建时就会调用 _rpc_stateadd_method 方法, 添加三个方法, 分别是 “call”, “notice”, “callback”. 这三个字段就是我们在抓包时看到的 {"id":1,"method":"call","params":[2,"get_chain_id",[]]} 中的 method 字段的值.

这三个方法的 handlers 分别是三个 lambda 定义的回调函数, 在 “call” 的回调函数中, 会解析 rpc json 消息中的 params 字段, 取出 api_id, 方法名和参数去掉用实际的 api, 这是一个复杂的反射过程, 后面会介绍.

fc::api 与 generic_api

websocket_api_connection 除了调用 _rpc_state 添加那三个方法外, 还会负责注册一下 login_api — 因为总得让客户端有最初可调用的 api 不是嘛!

注册由 websocket_api_connection::register_api 方法负责, 但是在注册之前, login_api, database_api 等 api 需要用 fc::api 包装一下, fc::api 中定义了一些宏, 为每个 api 定义了 vtable 类型, vtable 里定义了每个 api 的 visit 函数, visit 函数会将 api 中的方法们用传入的 visitor 问候个遍. fc::api 还重载了 -> 操作符, 使得对 fc::api 的调用都会变成对对应的 vtable 的调用. 被包装过的 login_api 记做 fc::api<login_api>.

register_api 会将 fc::api<login_api> push 到上面说的 _local_apis 字段中, 但是, 你也看到了 _local_apis 是个向量, 成员类型是 generic_api. 是的, 这里还有一层转换, 就是 fc::api<login_api>generic_api 的转换.

我们先来看 generic_api 的核心成员们:

fc::any                                                 _api;                                   // 指向实际的 api, 比如 login_api
std::vector< std::function<variant(const variants&)> >  _methods;    // api 中的方法们
std::map< std::string, uint32_t >                       _by_name;              // 记录方法名 => 方法 id 的映射, 方法 id 实际上就是

generic_api::api_visitor 子类型                                                        // 子类中包含反指向 generic_api 的成员

然后再来看这步转换, 转换在 generic_api 的构造函数中可以窥知一二, 在这里 fc::api<login_api> 的 visitor 接口被调用, 传入的 visitor 是 generic_api::api_visitor 这个访问者, 这个访问者会将 fc::api<login_api> 中的方法们塞入 _methods 字段, 但是, 你又看到了, _methods 的元素类型是 std::function<variant(const variants&)>, 这里又涉及到一步转换, 就是讲 fc::api<login_api> 的方法们通过 to_generic 转换成 “通用方法”, 而 to_generic 是个模板函数, 其模板参数也很复杂, 看代码时要特别留意 api 下的各个方法对应哪个 to_generic.

比如说 login_api::database() 这个方法, 这个方法的签名是:

fc::api<database_api, Transform = identity_member> login_api::database()const

而在 fc::api 的宏作用下, 这个方法的签名会变成:

std::function<fc::api<database_api, Transform = identity_member>(Args...)> login_api::database()const

以这个函数签名做参数调用 to_generic 的话, 匹配的会是如下这个变种. 这里特别注意一下这个方法的最后一句是一个 register_api 调用, 这一句不是每个 to_generic 变种都有的, 只有 login_api 下那些返回 fc::api<xxxx> 的方法才会匹配到下面这个 to_generic 方法. 这一点很重要, 这体现了客户端通过调用 login_api 的各个方法来打开对其它 api 访问通道 (再提醒一句这里指的不是 api 的访问权限, api 访问权限由另外的逻辑保证).

393    template<typename Interface, typename Adaptor, typename ... Args>
394    std::function<variant(const fc::variants&)> generic_api::api_visitor::to_generic(
395                                                const std::function<fc::api<Interface,Adaptor>(Args...)>& f )const
396    {
397       auto api_con = _api_con;
398       auto gapi = &_api;
399       return [=]( const variants& args ) {
400          auto con = api_con.lock();
401          FC_ASSERT( con, "not connected" );
402
403          auto api_result = gapi->call_generic( f, args.begin(), args.end(), con->_max_conversion_depth );
404          return con->register_api( api_result );
405       };
406    }

结语

至此, 我们看到了, api 的注册实际上就是注册进了 websocket_api_connection 的 _local_apis 字段, 进而每个方法注册进了 generic_api 的 _methods 字段.

本文到此为止, 下文将介绍 websocket_server 是如何将收到的调用请求翻译成相应 api 的方法的.

Graphene 源码阅读 – 架构篇 – 见证人配置与启动

欢迎大家继续阅读, 看完了上一节 节点实例 这个公共组件, 这节我们就来介绍一下比特股系统中最重要的部分 — 见证人, 的配置与启动过程.

见证人配置

在 bitshares 中, 见证人功能是在 witness 插件里实现的, 类似的还有 cli_wallet, delayed_node 都是通过独立的插件提供, 这些插件的公共部分就是上一节我们介绍的 节点实例.

见证人在启动时依赖 config.ini 这个配置文件, 如果 config.ini 不存在会使用代码中的默认配置创建它, 实际上 config.ini 也是所有插件的配置文件, 但本节我们只关注见证人相关的配置, 配置和解释如下:

  • enable-stale-production
    正常情况下, 当见证人节点启动时, 会去其它节点同步区块, 当区块没有同步到全网最新状态时, 是不允许见证人生产区块的, 而此配置项置为 true 的话, 会无视这一规则.
  • required-participation
    DPoS 协议中有一个参与度的概念, 如果一条链参与度太低的话说明这条链已经不被很多见证人认可, 理应不该再在此链上出块. 此配置项就是用来设定参与度阈值.
  • witness-id
    此项配置的是当前节点实例要跑的是哪个见证人
  • private-key
    此项配置的是见证人的签名密钥

运行出块

见证人的运行就是作为一个调度单元, 每隔 1 秒就被调度起来生产区块的过程. 这个过程的执行流如下:

app::startup_plugin() => witness_plugin::plugin_startup() => schedule_production_loop() => block_production_loop() => maybe_produce_block()
                                                                                                               ^                                                 |
                                                                                                               |____________________________|

这里面很重要的一个工作就是 maybe_produce_block(), 这个方法的返回结果决定了要不要出块, 我们重点看下这个方法.

首先会检查区块是否同步到最新, enable-stale-production 配置项会影响这个判断, 如果 enable-stale-production 设为 true 了, 那这里的 _production_enabled 就会是 true.

217    // If the next block production opportunity is in the present or future, we're synced.
218    if( !_production_enabled )
219    {
220       if( db.get_slot_time(1) >= now )
221          _production_enabled = true;
222       else
223          return block_production_condition::not_synced;
224    }

接下来这段代码会判断是否到达出块时间, 关于这段代码看似简短其实背后也有文章, 我们在 出块判断逻辑 一文中有过一次稍微详细的介绍, 感兴趣的可以去看看. 这里仅补充一下为什么会没达到出块时间.

实际上原因也很简单, 因为见证人的调度是每秒都会调度的, 然而出块时间却可能是 3s, 10s 甚至是 20s, 所以见证人被调度执行时, 自然是可能还没有到达出块时间的.

226    // is anyone scheduled to produce now or one second in the future?
227    uint32_t slot = db.get_slot_at_time( now );
228    if( slot == 0 )
229    {
230       capture("next_time", db.get_slot_time(1));
231       return block_production_condition::not_time_yet;
232    }

如果出块时间也满足的话, 会继续判断这轮是不是轮到自己出块, 这部分涉及到 DPoS 见证人洗牌相关问题, 我们留到共识篇再详细讨论.

244    graphene::chain::witness_id_type scheduled_witness = db.get_scheduled_witness( slot );
245    // we must control the witness scheduled to produce the next block.
246    if( _witnesses.find( scheduled_witness ) == _witnesses.end() )
247    {
248       capture("scheduled_witness", scheduled_witness);
249       return block_production_condition::not_my_turn;
250    }

再继续会相继检查是否有配置签名私钥以及当前主链的参与率是否达到要求, 这就是上面说的 private-key 以及 required-participation 配置项的作用.

252    fc::time_point_sec scheduled_time = db.get_slot_time( slot );
253    graphene::chain::public_key_type scheduled_key = scheduled_witness( db ).signing_key;
254    auto private_key_itr = _private_keys.find( scheduled_key );
255
256    if( private_key_itr == _private_keys.end() )
257    {
258       capture("scheduled_key", scheduled_key);
259       return block_production_condition::no_private_key;
260    }
261
262    uint32_t prate = db.witness_participation_rate();
263    if( prate < _required_witness_participation )
264    {
265       capture("pct", uint32_t(100*uint64_t(prate) / GRAPHENE_1_PERCENT));
266       return block_production_condition::low_participation;
267    }

最后一项要检查的是当前要出的块的理论出块时间和当前实际时间相差多大, 如果相差了 500ms 以上, 那也不会允许出块, 如果你运行过见证人, 也许有看到过

Not producing block because node didn't wake up within 500ms of the slot time.

这样的错误, 这就是我们所说的 miss 了.

当上述所有检查都没有问题就会执行出块了. 出块部分我们留作后续讨论.

Graphene 源码阅读 – 架构篇 – 节点实例启动流

大家好, 本文正式开启架构篇.

如果你还有印象的话, 我们之前在 源码结构 中曾简要的介绍过 bitshares 源码中的每个模块, 这个篇章将会和大家探讨各个模块之间的调用关系, 节点运行时模块之前如何交互, 逐步为大家呈现出一条清晰地脉络.

与整个节点的运行最相关的要数 app 模块了, 所以 app 模块就作为架构篇的第一个模块在本文介绍. 下文中 app 模块我们将其称之为节点实例模块或者直接成为节点实例, 这个名字将更能够表达此模块的含义.

节点实例

节点实例模块在整个系统中无疑是位于顶端的, 系统中各个组件都是由它触发激活并监控他们的状态. 节点在启动时会调用 app::initialize() 以及 app::startup() 方法, app::initialize() 中主要的工作就是注册插件到自己的静态变量中, app::startup() 才包含了节点启动的主要流程.

下面我们就来详细看看节点启动流程.

节点实例启动

genesis 加载进内存

第一步就是加载创世信息 genesis.json, 如果你没指定 —genesis-json (如果你要跑主链的话也不需要指定), egenesis 组件会负责加载默认创世信息 (参见 默认的 Genesis 创世信息), 创世信息加载进内存后会被存储在 genesis_state_type 类对象中, genesis_state_type 类的定义和 genesis.json 中的字段是一一对应的, 只有一个例外是 genesis_state_type 中包含一个 initial_chain_id 字段, 这个字段是在创世信息全加载进内存后对这块内存求哈希算出来的, 所以在 genesis.json 中没有对应.

initial_chain_id 是很重要的字段, 它被用来标识不同节点所运行的是不是同一条链. 所以现在我们知道了, 所以只要不同节点使用相同的创世信息, 它们跑的就一同一条链; 而你如果修改哪怕只是创世信息中的一个微不足道的字段, 其 hash 值都会改变, 你也将不能与其它节点通信 – 实际上这已经创建了一条新链.

数据库打开

如果看过之前的数据库篇, 想必对这个过程会亲切一些, 没错, 这个过程做的就是从磁盘加载对象索引, 打开区块数据文件流, 必要的话还会重建部分对象索引.

另外要注意的是, 如果是节点的第一次启动, 数据库打开这个过程的末尾还会对上面加载的创世信息进行 apply, 这个工作很重要, 创新信息的很多内容和正常链上内容无二, 所以它也要上链, 总不能每次启动节点都将它是放在内存里的. init_genesis() 方法就是做这个工作的.

init_genesis

init_genesis() 在节点第一次启动时被调用, 它读取前面加载到内存中的创世信息并 apply, 这其中包含了大量的工作, 我们选一些主要的说明一下:

创建特殊账户

这个过程会创建区块链上的一些特殊账户对象, 并且是不用走检验直接在索引上创建, 用的是数据库篇说过的 object_database::create() 方法

特殊账户的个数由创世信息的 immutable_parameters.num_special_accounts 字段指定, 对目前的 bitshares 主链来说这个值是 100.

首先创建的是 committee-account, witness_account, relaxed-committee-account, null-account, temp-account, proxy-to-self 这 6 个特殊账户, 这六个账户的 id 依次为 1.2.0 ~ 1.2.5.

实际上目前特殊账户也就这 6 个, 创世信息中指定的 100 个是为了预留以备不时之需. 紧接着要为剩下的 94 个特殊账户预留位置, 预留位置的方式很简单粗暴, 就是创建 94 个账户然后再立马把它们删除, 这样索引结构中 6 ~ 99 号 id 就也被占用了, 新创建的账户将只能从 100 号开始, 如果你用 bitshares 的区块浏览器查看, 就会发现从 1.2.6 ~ 1.2.99 这 94 个账户至今还是空的.

数据库篇 ~ 索引模型 中我们说到过创建对象时有个 set_next_id() 方法可以显式指定要创建的对象的 id, 所以将来要创建 6 ~ 99 号账户的话, 就可以发起一个账户创建交易, 其中指定账户的 id.

创建核心资产与特殊资产

核心资产也就是 BTS 资产, 特殊资产是一些名字以 “SPECIAL_” 打头的资产, 估计也是留作备用. 它们的创建套路和创建初始账户一致, 不再敖述.

创建全局对象

包括全局属性对象 (global_property_object), 动态全局属性对象 (dynamic_global_property_object), 链属性对象 (chain_property_object), 以及区块总结对象 (block_summary_object).

创建初始账户

上面创建的特殊账户是不属于个人的账户, 这里创建的 “初始账户” 实际上都是一些个人账户了, 这些账户可以说是这条链上的 “创世账户”.

不过这些账户毕竟是个人账户, 这些账户都是当初社区最早的那批参与者们, 账户信息也都是他们自己提供的. 既然是人提供的信息, 那就不是 100% 可信的, 所以这些账户的创建不能像特殊账户那样直接创建加索引, 而是要走操作创建, 检验上链的过程, 所以创建这些账户时会看到用的是 apply_operation 方法.

初始账户们由创世信息的 initial_accounts 字段指定, 总共有 90653 个账户.

创建初始资产

和初始账户一样, 初始资产也是一些 “个人” 资产. 创建过程和初始账户一样, 不再敖述.

其它

其它的还有创建初始余额, 初始见证人, 委员会, 提案, 套路和上述一致.

顺便说一句, 初始账户, 初始资产, 初始余额这几个信息几乎是占据了默认创世信息的全部, 默认创世文件 genesis.json 总共 35M 之大, 而其中 99.99% 都是这级部分信息贡献的.

p2p 网络与 rpc 服务初始化

数据库搞好之后, 下一步就是 p2p 网络的建立以及 rpc 服务的启动.

篇幅关系, 这两部分留到后面继续介绍吧, 敬请期待.

Graphene 源码阅读 – RPC 篇 – 通信协议与服务端实现

从现在开始我们进入一个新的篇章: RPC 篇, 这个篇章会包含客户端 (钱包, UI 等) 与节点间的通信细则, API 分类, 服务端的实现等内容, 最后会挑几个主要的 API 讲一下.

本文是 RPC 篇的第一章, 我们就先来介绍一下整体的通信机制和服务端 (节点) 实现.

通信协议

Bitshares 提供两种通信方式: http 和 websocket. 这俩最大的区别就是 websocket 是双向通信, 客户端和服务段都能主动向对方发送消息; 而 http 则只能由客户端主动发送消息. Websocket 的双向通信特性能够满足一些对实时性需求较高的应用.

Websocket 和 http 如此不同, 但却又难解难分, websocket 是在 http 之后出现的, 它复用了 http 的传输层以及协议端口, 并且握手过程也是使用 http 消息格式实现的, 只不过在 http 头部添加了几个新的 headers, 当服务端检测到 websocket 的 headers 时, 就会知道这是个 websocket 连接, 从而与传统的 http 请求过程区分开来.

刚说了 websocket 复用了 http 的传输层, http 的传输层可以是未加密的 tcp, 也可以是加密过的 tls, 那么 websocket 自然也可以用这两种传输层协议.

\ http websocket
tcp http:// ws://
tls https:// wss://

关于 websocket 协议的细则可以自行 google 一下, 这里就不再敖述了.

消息格式

不管是 websocket 还是 http, 客户端与节点通信时的消息体都是 json 格式, 一个典型的请求体内容如下:

{
 "jsonrpc": "2.0",
 "id": 1
 "method": "get_accounts",
 "params": [["1.2.0", "1.2.1"]],
}

其中 id 是自增的, jsonrpc 目前固定是 2.0. methodparams 不用过多解释一看便知其意. 返回体会因请求不同而不同, 但当然也是标准的 json 格式, 而且一般会包含 errno, errmsg 这样的通用字段, 不再贴出.

服务端实现

服务端的实现借助了 websocketapp 库, 这个库能够帮助我们方便的开发 websocket 服务端程序, 不但如此, 它也支持对普通 http 消息的处理, 因为前面说了 websocket 和 http 使用共同的传输层和端口, websocket 协议也只是在握手阶段使用 http 的消息格式, 所以 websocketapp 很容易区分客户端发来的是 websocket 消息还是普通的 http 消息, 相应的做不同的处理, 为此 websocketapp 提供了两个回调接口: on_message 和 on_http, 应用程序可以注册这两个回调方法. 当收到 websocket 消息时, on_message 会被调用; 而收到普通 http 消息时, on_http 会被调用.

除了 on_message 和 on_http 之外还有一个重要的回调是 on_connection, 它代表着有新的客户端连接过来.

Bitshares 代码中当然是实现了这三个回调的, 下面我们就来看一下服务端启动的主要流程以及对上面三个回调的实现.

服务流程

在节点启动时, 会调用 application::startup() 方法, 而这个方法的最后一个工作就是启动 RPC server, 这在 reset_websocket_server() 方法中去做:

// 代码 1.1

 277 void application_impl::reset_websocket_server()
 278 { try {
 279    if( !_options->count("rpc-endpoint") )
 280       return;
 281
 282    _websocket_server = std::make_shared<fc::http::websocket_server>();
 283    _websocket_server->on_connection( std::bind(&application_impl::new_connection, this, std::placeholders::_1) );
 284
 285    ilog("Configured websocket rpc to listen on ${ip}", ("ip",_options->at("rpc-endpoint").as<string>()));
 286    _websocket_server->listen( fc::ip::endpoint::from_string(_options->at("rpc-endpoint").as<string>()) );
 287    _websocket_server->start_accept();
 288 } FC_CAPTURE_AND_RETHROW() }

这个方法很简单, 首先直接实例化了 _websocket_server 对象, 这个对象的类型是 fc::http::websocket_server, 它又是属于 fc 库的一部分, 然而这不重要, 这里不需要再了解 fc 库中对应的代码了. 实际上 fc::http::websocket_server 就是对前面我们说的 websocketapp 库的封装, 我们可以把 fc::http::websocket_server 就看做是 websocketapp.

那么可以看到紧接着就是注册了 on_connection 回调, 然后就是 listen, accept, 多么熟悉的套接字编程套路, websocket 服务端就这么愉快的启起来了~

我知道你要问什么, 怎么没看见注册 on_message 和 on_http 回调呢? 对了, 看到注册 on_connection 回调用的 application_impl::new_connection 方法了吗, on_message 和 on_http 实际上就是在这个方法里注册的:

// 代码 1.2

 245 void application_impl::new_connection( const fc::http::websocket_connection_ptr& c )
 246 {
 247    auto wsc = std::make_shared<fc::rpc::websocket_api_connection>(*c, GRAPHENE_NET_MAX_NESTED_OBJECTS);
 248    auto login = std::make_shared<graphene::app::login_api>( std::ref(*_self) );
 249    login->enable_api("database_api");
 250
 251    wsc->register_api(login->database());
 252    wsc->register_api(fc::api<graphene::app::login_api>(login));
 253    c->set_session_data( wsc );
….
….

// 代码 1.3

 10 websocket_api_connection::websocket_api_connection( fc::http::websocket_connection& c, uint32_t max_depth )
 11    : api_connection(max_depth),_connection(c)
 12 {
 13    _rpc_state.add_method( "call", [this]( const variants& args ) -> variant
 14    {
 15       FC_ASSERT( args.size() == 3 && args[2].is_array() );
…
…
…
 48    } );
 49
 50    _connection.on_message_handler( [&]( const std::string& msg ){ on_message(msg,true); } );
 51    _connection.on_http_handler( [&]( const std::string& msg ){ return on_message(msg,false); } );
 52    _connection.closed.connect( [this](){ closed(); } );
 53 }

application_impl::new_connection 的参数 fc::http::websocket_connection_ptr 这个类型又是对 websocketapp 的封装, 不难理解, 我们直接认为它就是 websocketapp 传过来的对这个新连接的上下文描述就好, 紧接着实例化了一个 fc::rpc::websocket_api_connection 对象并且把这个上下文传了进去, fc::rpc::websocket_api_connection 的构造函数在代码 1.3, 可以看到在构造函数最后它注册了 on_message 和 on_http 的 handler, 而这两个 handlers 实际上是调用的同一个方法: on_message, 注意这里这个 on_message 可是 websocket_api_connection::on_message.

到这里为止, 我们就知道该如何 track 各种请求在服务期短的处理了, 新连接的处理就看 application_impl::new_connection, 来了请求怎么处理就看 websocket_api_connection::on_message, 当然对 websocket 来说还有一个过程就是服务器端主动发消息给客户端的过程, 这部分感兴趣可以自己研究一下, 提示一下: fc::http::websocket_connection::send_message 方法.

后记

本文最后引出了 on_connection 和 on_message 这两个重要的回调, 下篇文章将会简单介绍 on_connection 实现, 然后从 on_message 展开介绍一下各类 api 们, 以及从请求体到这些 api 们的映射机制.

Graphene 源码阅读 – 番外篇 – 出块判断逻辑

见证人节点起来之后, 会周期行的检查是否轮到自己出块了, 判断自己是否能出块的逻辑在 maybe_produce_block()方法中, 这里面的第一步就是判断自己是不是和网络同步了, 同步了的话就继续下面的判断, 没有同步的话就直接返回 not_synced 错误告诉外面不要出块.

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( !_production_enabled )
219    {
220       if( db.get_slot_time(1) >= now )
221          _production_enabled = true;
222       else
223          return block_production_condition::not_synced;
224    }

正常情况下, 第一次启动时 _production_enabled 的值是 false, 因此这段逻辑确保了在同步到最新块之前本节点肯定不会出块的, 否则就分叉了; 而一旦达到过一次同步状态, _production_enabled 就会置为 true, 我搜索了全部代码, 程序运行周期中没有其它地方会将 _production_enabled 再置为 false 了, 而且 maybe_produce_block() 方法中后面也没有对节点同步状态进行检查的逻辑了, 这就意味着只要见证人节点曾经同步到最新过, 并且始终没有宕机没有重启, 那么就算后来节点不再处于同步状态, 照样也能出块?

这个行为我诈一看感觉不太合理, 按理说节点如果没同步的话那肯定也得直接返回 not_synced 不能让出块呀! 所以 maybe_produce_block() 这段逻辑得改改, 于是我计划写个 issue 然后提个 patch, 修改方案已经想好了, head_block_time() + block_interval()now 比较, 如果前者大就说明节点已经同步到最新状态了, 否则就说明节点显然已经落后了 (这也是上述代码中 db.get_slot_time() 方法的逻辑). 这样一来上面这段代码可以改成类似如下这样就可以了:

211 block_production_condition::block_production_condition_enum witness_plugin::maybe_produce_block( fc::limited_mutable_variant_object& capture )
212 {
213    chain::database& db = database();
214    fc::time_point now_fine = fc::time_point::now();
215    fc::time_point_sec now = now_fine + fc::microseconds( 500000 );
216
217    // If the next block production opportunity is in the present or future, we're synced.
218    if( db.get_slot_time(1) < now )
219       return block_production_condition::not_synced;

本来 issue 都已经写完了, 然而在写完的时候我才意识到自己错了, 如果按照上面的说法做的话, 可能会导致从某个时间起所有见证人都不再出块, 用一个简单的例子说明. 假设有 A, B, C 三个见证人, 他们先按照 A -> B -> C 的顺序每人出了一个块, 然后顺序变成 B -> A -> C, 所以这时轮到 B 出块了, 然而不幸的是 B miss 了, 于是到 A, A 这时判断最新块的时间加上出块间隔发现小于当前时间, 按照上面的策略, 这个认为自己 out of sync 了, 所以 A 也不会出块, 到 C 时也是一样的情况, C 也认为自己 out of sync 于是 C 也不出块. 于是.. 就达到了一个没有节点出块的局面…

所以说 _production_enabled 实际上正是避免了出现这个局面, 只是它的名字可能有点让人困惑, 实际上它就是一个标识, 标识着当节点第一次达到了与全网同步的状态时, 它就初步具备了出块资格.

也许改叫 once-synced 之类的名字会好些 😛

(PS: 这篇的内容貌似已经有点涉及 DPoS 啦)