石墨烯架构分析三-交易,RPC,WS,创世快,数据库,出块速度,共识机制,服务流程,P2P网络,节点实例,交易,知识要活学活用。
发布于 1 个月前 作者 stvenyin 625 次浏览 来自 EOS

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 image.png 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

image.png 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_schedule 和 op 为参数, 就是用 op 的计费方法以及 fee_schedule 的计费参数计算费用. calc_fee_visitor 里有一个 try … catch (代码 1.4) 可能不好理解, 这里的 try … catch 是因为 fee_schedule 这块代码有点问题, 除了 op 是 account_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§,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 }; 至此, 这笔操作的交易费用就被计算并设置到了操作的成员变量中. 发布于 五月 29, 2018 分类 Graphene 源码系列 标签 bitshares、graphene 于Graphene 源码阅读 – 交易篇 – 交易费用 留下评论 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_api 的 database 方法来开启 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_state 的 add_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 的方法的. 发布于 五月 25, 2018 分类 Graphene 源码系列 标签 bitshares 于Graphene 源码阅读 – RPC 篇 – 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 了. 当上述所有检查都没有问题就会执行出块了. 出块部分我们留作后续讨论. 发布于 五月 3, 2018 分类 Graphene 源码系列 标签 bitshares、graphene 于Graphene 源码阅读 – 架构篇 – 见证人配置与启动 留下评论 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 服务的启动. 篇幅关系, 这两部分留到后面继续介绍吧, 敬请期待. 发布于 五月 1, 2018 分类 Graphene 源码系列 标签 bitshares、cn、cn-programming、graphene Graphene 源码阅读 – 架构篇 – 节点实例启动流 有1条评论 Daniel Larmer 列传 2013 年, 全职挺进区块链行业. 在 bitcointalk 上找到了 Adam Levine 和 Charles Hoskinson 一起开发了 bitshares 2014 年发布 Bitshares 第一版, 沿用了比特币的数据库技术和 UTXO 思想, 导致性能不好, 不能跑什么实际应用 2015, 首次提出, 从 “So how does Delegated Proof of Stake work?” 这段开始介绍了 DPoS 的机制 2015 年 1 月, 发表对 Nothing at Stake 问题的看法: http://bytemaster.github.io/article/2015/01/08/Nothing-at-Stake-Nothing-to-Fear/ 2015 年 6 月, 发布了石墨烯工具组, 在此基础上建立 Bitshares 2.0, 这个架构的一个特点是东西全 load 进内存, 出块时间 3s 2016 以前, 在 http://bytemaster.github.io/ 上发文, 后来在 steemit 上, 再后来在 medium 上. 2017 年, dan 发表文章称 PoW 没有解决拜占庭将军问题 (实际也确实如此), https://steemit.com/blockchain/@dantheman/the-problem-with-byzantine-generals 2018 年 1 月, dan 录了两段视频介绍 DPoS/PBFT: https://twitter.com/go_eos/status/956607047449137153, 网上有文字翻译. 2018 年 2 月, Ivan 对 BM 做了一次采访, 内容很全, 讲述了自己的生平, bitshares/steem 的创建, 还讨论了以太坊和 EOS, 还有 DPoS 的起源: https://twitter.com/IvanOnTech/status/960908134628896768. 网上能找到中文文字版. 2018 年 4 月, 巨蟹在巴比特直播, 表示如果时光倒流, 希望 BM 不要离开 http://www.8btc.com/20180412 2018 年 5 月 5 日, 有媒体对巨蟹做了一次独家专访, 谈到了巨蟹的公开市场操作 https://mp.weixin.qq.com/s/h3twkhoOSmGRv1aBJZ0SfQ 以上信息来源于互联网 发布于 四月 15, 2018 分类 Graphene 源码系列 于Daniel Larmer 列传 留下评论 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. method 和 params 不用过多解释一看便知其意. 返回体会因请求不同而不同, 但当然也是标准的 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_sharedfc::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_sharedfc::rpc::websocket_api_connection(c, GRAPHENE_NET_MAX_NESTED_OBJECTS); 248 auto login = std::make_sharedgraphene::app::login_api( std::ref(_self) ); 249 login->enable_api(“database_api”); 250 251 wsc->register_api(login->database()); 252 wsc->register_api(fc::apigraphene::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© 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 们的映射机制. 发布于 四月 14, 2018 分类 Graphene 源码系列 标签 bitshares 于Graphene 源码阅读 – RPC 篇 – 通信协议与服务端实现 留下评论 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 啦) 发布于 四月 11, 2018 分类 Graphene 源码系列 标签 bitshares、graphene 于Graphene 源码阅读 – 番外篇 – 出块判断逻辑 留下评论 Graphene 源码阅读 – 数据库篇 – 区块数据管理 在 bitshares 中除了对象数据需要落盘之外, 网络上接收到的区块数据也是需要落盘的, 区块数据的管理依赖的就是 chain::block_database 模块. chain::block_database 这个模块听起来好像很复杂 – 毕竟管理着区块数据的落盘与加载, 但实际上非常简单. 它维护了两个文件流: _blocks 存储具体区块数据, _block_num_to_pos 则是索引数据. 当我们知道了区块 id 或者区块号时, 就可以从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 进而从 _blocks 中取出整个块数据. 区块数据的内存模型 区块数据在内存中由 chain::signed_block 类型表示, 但是与对象在内存中会挂到红黑树上不同, 区块数据从其他节点收到并 apply 完后就会序列化到磁盘并在内存中释放. 区块数据与区块索引的序列化与反序列化 读写 _blocks 文件流时会涉及到区块数据的序列化与反序列化, 这里依赖的也是 对象序列化 中介绍的 fc::raw::pack/fc::raw::unpack, 不再敖述. 而读写 _block_num_to_pos 流时关于区块索引的序列化反序列化就简单了, 它没有使用 fc::raw::pack/fc::raw::unpack, 而是简单粗暴的强转成字符序列: // 代码 6.1

78 void block_database::store( const block_id_type& _id, const signed_block& b ) 79 { 80 block_id_type id = _id; 81 if( id == block_id_type() ) 82 { 83 id = b.id(); 84 elog( “id argument of block_database::store() was not initialized for block ${id}”, (“id”, id) ); 85 } 86 _block_num_to_pos.seekp( sizeof( index_entry ) * int64_t(block_header::num_from_id(id)) ); 87 index_entry e; 88 _blocks.seekp( 0, _blocks.end ); 89 auto vec = fc::raw::pack( b ); 90 e.block_pos = _blocks.tellp(); 91 e.block_size = vec.size(); 92 e.block_id = id; 93 _blocks.write( vec.data(), vec.size() ); 94 _block_num_to_pos.write( (char*)&e, sizeof(e) ); 95 } 反序列过程实际上面说过了, 就是拿区块 id 或者区块号从 _block_num_to_pos 查询出此区块在 _blocks 中的偏移和大小, 然后从 _blocks 中取出整个块数据: // 代码 6.2

184 index_entry e; 185 int64_t index_pos = sizeof(e) * int64_t(block_num); 186 _block_num_to_pos.seekg( 0, _block_num_to_pos.end ); 187 if ( _block_num_to_pos.tellg() <= index_pos ) 188 return {}; 189 190 _block_num_to_pos.seekg( index_pos, _block_num_to_pos.beg ); 191 _block_num_to_pos.read( (char*)&e, sizeof(e) ); 192 193 vector<char> data( e.block_size ); 194 _blocks.seekg( e.block_pos ); 195 _blocks.read( data.data(), e.block_size ); 196 auto result = fc::raw::unpack<signed_block>(data); 197 FC_ASSERT( result.id() == e.block_id ); 198 return result; chain::database 初始化 如果说 db::object_database 是管理对象数据, chain::block_database 管理区块数据, 那么 chain::database 就是管理 db::object_database 和 chain::block_database 了. 从结构上来讲 chain::database 的内容应该单独放在一篇文章里, 但是由于 block_database 模块比较简单, 篇幅太少, 而 chain::database 的内容有比较多, 估计下一篇一篇介绍不完, 所以这里就暂且先把 chain::database 的初始化部分挪到这里来. 也能让我们先对 chain::database 有个大概的认识. chain::database 的初始化涉及到两个过程, 一个是节点实例被构造时, 同时也会构造 chain::database 实例, 而在 chain::database 构造时就会执行我们前面章节提到过的 initialize_indexes(), add_index<>() 等过程. 对应下面这条链路: node = new app::application() => new detail::application_impl(this) => _chain_db(std::make_sharedchain::database()

chain::database() => initialize_indexes() => add_index<>() 其次就是在节点 startup 时, 会调用 chain::database::open() 方法, 这里面包含了 chain::database 初始化阶段的主要工作内容. 首先, 读取 witness_node_data_dir/blockchain/db_version 文件, 比较一下数据库版本和当前运行的版本的程序的数据库版本是否一致, 如果版本不一致或者这个文件不存在, 就会先清空对象库, 然后写入当前的版本号. // 代码 6.3 143 bool wipe_object_db = false; 144 if( !fc::exists( data_dir / “db_version” ) ) 145 wipe_object_db = true; 146 else 147 { 148 std::string version_string; 149 fc::read_file_contents( data_dir / “db_version”, version_string ); 150 wipe_object_db = ( version_string != db_version ); 151 } 152 if( wipe_object_db ) { 153 ilog(“Wiping object_database due to missing or wrong version”); 154 object_database::wipe( data_dir ); 155 std::ofstream version_file( (data_dir / “db_version”).generic_string().c_str(), 156 std::ios::out | std::ios::binary | std::ios::trunc ); 157 version_file.write( db_version.c_str(), db_version.size() ); 158 version_file.close(); 159 } 然后就是调用 object_database 和 block_database 的 open() 方法, 到这里我们看到了 chain::database 确实是操控 object_database 和 block_database 的 “上游” 模块. // 代码 6.4

161 object_database::open(data_dir); 162 163 _block_id_to_block.open(data_dir / “database” / “block_num_to_block”); 接下来, 如果这是节点的第一次启动 (说明从 object_database 中加载的对象中没有找到 global_property_object) 的话, 就要初始化创世状态, 而创世信息从里来呢? 请参考 Genesis 创世信息生成. // 代码 6.5

165 if( !find(global_property_id_type()) ) 166 init_genesis(genesis_loader()); 创世过程的主要工作包括创建一些特殊和初始账户, 以及一些核心资产, 这些信息不用经过广播直接在本地出块, 因为所有其他节点也要执行要全同样的过程. 指的一提的是, 创世信息中的账户我本以为都是链上的公共账户或者委员会特殊账户之类的, 但没想到里面还有 9w 多的正常会员账户, 这些会员账户可谓是 bitshares 共链的 “创世居民”. 后记 区块中保存了整条链的原始记录, 只要区块数据正确完好, 我们就能从这些数据中 replay 出一条一模一样的链. 好了, 本文就到此为止. 感谢阅读. 发布于 四月 6, 2018 分类 Graphene 源码系列 于Graphene 源码阅读 – 数据库篇 – 区块数据管理 留下评论 Graphene 源码 – 番外篇 – 默认的 Genesis 创世信息 Genesis 生成 egenesis 最前面的 e 代表 embedded, 意思是说创世信息嵌入代码中, 它有两个 full 和 brief 两个版本, full 版本包含所有创世信息, brief 只包含这些创世信息的 hash (创世信息的 hash 也是 chain_id). full 和 brief 的生成靠的都是下面这段 make 脚本. // libraries/egenesis/CMakeLists.txt

22 MESSAGE( STATUS "egenesis: " ${GRAPHENE_EGENESIS_JSON} ) 23 24 if( GRAPHENE_EGENESIS_JSON ) 25 list( APPEND embed_genesis_args --genesis-json “${GRAPHENE_EGENESIS_JSON}” ) 26 endif( GRAPHENE_EGENESIS_JSON ) 27 28 MESSAGE( STATUS "embed_genesis_args: " ${embed_genesis_args} ) 29 30 add_custom_command( 31 OUTPUT 32 "${CMAKE_CURRENT_BINARY_DIR}/egenesis_brief.cpp" 33 "${CMAKE_CURRENT_BINARY_DIR}/egenesis_full.cpp" 34 WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} 35 COMMAND embed_genesis ${embed_genesis_args} 36 DEPENDS 37 "${GRAPHENE_EGENESIS_JSON}" 38 "${CMAKE_CURRENT_SOURCE_DIR}/egenesis_brief.cpp.tmpl" 39 "${CMAKE_CURRENT_SOURCE_DIR}/egenesis_full.cpp.tmpl" 40 embed_genesis ) 其中 GRAPHENE_EGENESIS_JSON 在 bitshares-core 根目录被设置为 genesis.json: // ./CMakeLists.txt

26:set(GRAPHENE_EGENESIS_JSON “${CMAKE_CURRENT_SOURCE_DIR}/genesis.json” ) 没错, 就是 bitshares-core 源码根目录的那个 genesis.json 文件. egenesis 相关的代码都位于 libraries/egenesis/ 目录. 上面 libraries/egenesis/CMakeLists.txt 里的 embed_genesis 这个命令的源码实际就是 libraries/egenesis/embed_genesis.cpp; embed_genesis_args 参数展开就是 —genesis-json genesis.json; embed_genesis 程序会用 egenesis_full.cpp.tmpl, egenesis_brief.cpp.tmpl 这两个模板 egenesis_full.cpp, egenesis_brief.cpp 两个文件. 引用 上面说了 full 和 brief 两个 genesis 源码, 他们在构建时会分别生成 libgraphene_egenesis_full.a 和 libgraphene_egenesis_brief.a, 这俩库里面实现的方法都是 <egenesis/egenesis.hpp> 中定义的方法, 如果同时链接这两个库势必会造成符号重定义错误. 所以这俩只会链接一个. 仍然以 witness 为例, 在 witness_node/CMakeLists.txt 中我们可以看到, 链接时实际上链的是 full 版本: // witness_node/CMakeLists.txt

15 PRIVATE graphene_app graphene_delayed_node graphene_account_history graphene_elasticsearch graphene_market_history graphene_witness graphene_chain graphene_debug_witness graphen e_egenesis_full graphene_snapshot fc ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS} ) 参考 https://github.com/cryptonomex/graphene/wiki/egenesis

Graphene 源码系列

回到顶部