标签归档:graphene

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 源码阅读 – 架构篇 – 见证人配置与启动

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

见证人配置

在 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 源码阅读 – 番外篇 – 出块判断逻辑

见证人节点起来之后, 会周期行的检查是否轮到自己出块了, 判断自己是否能出块的逻辑在 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 啦)