EOS区块链核心代码解读-共识机制,定制超级节点数量和产生算法

阅读本文之前,你需要的

  • 面向对象编程的能力
  • 最好具备中级的C++11编程能力。如果不具备,其他面向对象语言的编程能力也将有所帮助。具体包括:
    • 常用的std容器和方法,包括vector,pair等
    • 指针和智能指针,迭代器(iterator)
    • 泛型
  • 理解EOSIO中区块生产者(block producer / BP)的基本概念。本文也将有简单的介绍。
  • 能够运行一个多节点的EOS私链。
  • 本文基于最新版(1.4.2),由于此部分涉及出块节点的选取,是核心部分,不会轻易改变.

本文内容

  • 分析EOS中与DPOS共识机制有关的类和方法,主要是BP的确定.包括onblock , update_elected_producers , set_proposed_producers
  • 实战:如何修改EOS的BP的刷新时间,BP的选取算法.

代码解读

首先对共识机制做一个简述.在EOS中,每个用户都可以注册成为区块生产者(BP).注册完成之后需要鼓动其他用户质押自己的EOS给你投票.当满足:(a)全体用户质押的EOS数量超过总EOS数量的15%.(B)得票数在前21位时,就获得了BP的资格.
BP资格每120个区块刷新一次.新选中的BP会在在BP资格刷新之后进入选中时间表(proposed schedule)中,并且之前的BP按照出块时间表(producer schedule)完成出块之后.选中时间表会代替出块时间表,新选中的BP开始出块.这段时间类似于刚刚被选举的美国总统并不能立即上任,还要等到上一任按时间表卸任.
比如,当我运行了一个私链。此时只有三个节点注册成为了生产者并有票数。那么这三个节点都会轮流出块。每个产生12块。

轮流出块

这一部分的代码在contract/eosio.system/produerpay.cpp文件的onblock方法中.其中,onblock方法意味着每个区块这个方法都会被执行一次.

onblock方法解读

这里只解读与选取出块者有关的代码
完整代码参见:https://github.com/EOSIO/eos/blob/master/contracts/eosio.system/producer_pay.cpp
这个方法接收两个参数,分别是timestamp和producer,也就是当前区块的时间戳和生产者.用于给生产者计算出块奖励.注意,这里的block_timestamp是一个较为复杂的结构体.

void system_contract::onblock( block_timestamp timestamp, account_name producer ) {
      using namespace eosio;

/// ............

      /// 每隔120块(也就是60秒)刷新一次生产者
      if( timestamp.slot - _gstate.last_producer_schedule_update.slot > 120 ) {
         update_elected_producers( timestamp );
/// ............
      }
   }

如果需要修改BP的刷新时间,修改此处120即可,并且可以看到,选定BP的核心方法就是update_elected_producers

update_elected_producers方法解读

需要先了解eosio::producer_key类型.
代码参见: https://github.com/EOSIO/eos/blob/master/contracts/eosiolib/privileged.hpp

   struct producer_key {

      /**
       * Name of the producer
       *
       * @brief Name of the producer
       */
      account_name     producer_name;

      /**
       * Block signing key used by this producer
       *
       * @brief Block signing key used by this producer
       */
      public_key       block_signing_key;

      friend bool operator < ( const producer_key& a, const producer_key& b ) {
         return a.producer_name < b.producer_name;
      }

      EOSLIB_SERIALIZE( producer_key, (producer_name)(block_signing_key) )
   };

这个结构体进行了:

  • 设定两个属性:账户名称和公钥.
  • 重载了<运算符.
  • 增加了一个序列化方法.

update_elected_producers方法是用来更新被选中生产者的.默认情况下120个块执行一次.
代码参见:https://github.com/EOSIO/eos/blob/master/contracts/eosio.system/voting.cpp
在注册bp时,需要给出自己的地区码.注释中称,在给选中的BP排序时会按照地区码相邻的原则排序.其实地区码可以随便设置.
下面的用三个 /// 的注释是原有注释

   void system_contract::update_elected_producers( block_timestamp block_time ) {
      _gstate.last_producer_schedule_update = block_time;

      //  取得一个指向当前已经注册的producer的集合的指针.
      //  _producers 是eosio内置的一个数据库表.此时已经根据其被投票数排序好.
      auto idx = _producers.get_index<N(prototalvote)>();

      //  创建一个由eosio::producer_key类型和uint16_t类型组成的pair的动态数组.
      //  这个vector用来放被选中的producer, 当前是空的.
      //  eosio::produver_key类型定义将在下文解读
      std::vector< std::pair<eosio::producer_key,uint16_t> > top_producers;
      // top_producers 扩容至21,因为EOS默认情况下有21个出块节点
      top_producers.reserve(21);

      // 使用迭代器遍历,for循环条件分别为 :
      //  it = idx.cbegin(); it != idx.cend() 指针指向开始第一个BP,也就是票数最多的,并且还没有到最后一个
      // top_producers.size() < 21 top_producer当前还没有满21个
      //  0 < it->total_votes it是当前迭代到的BP.这里要求它的票数大于0.即便注册BP还没有满21个也不能让0票数的BP出块.
      // it->active() BP可以关闭.这里要求它保持activate状态.
      for ( auto it = idx.cbegin(); it != idx.cend() && top_producers.size() < 21 && 0 < it->total_votes && it->active(); ++it ) {
        // 首先构造一个eosio::producer_key对象,然后和它的地区码一起构造一个pair.再把这个pair加入vector中.
         top_producers.emplace_back( std::pair<eosio::producer_key,uint16_t>({{it->owner, it->producer_key}, it->location}) );
      }
      // 当前的被选中BP数要大于上次的BP数,否则退出.
      if ( top_producers.size() < _gstate.last_producer_schedule_size ) {
         return;
      }
      /// sort by producer name
      std::sort( top_producers.begin(), top_producers.end() );

      std::vector<eosio::producer_key> producers;
      
      producers.reserve(top_producers.size());
      for( const auto& item : top_producers )
         producers.push_back(item.first);
      // 把当前选中的producer打包成datastream数据.
      // datastream 是eos中自定义的一个数据类型.把数据打包(pack)成datastream后,作为参数传递给另一个方法,比直接传递效率要高.
      bytes packed_schedule = pack(producers);
      // 使用set_proposed_producers方法.完成后更新shedule的长度.也就是当前有效的BP的个数.
      if( set_proposed_producers( packed_schedule.data(),  packed_schedule.size() ) >= 0 ) {
         _gstate.last_producer_schedule_size = static_cast<decltype(_gstate.last_producer_schedule_size)>( top_producers.size() );
      }
   }

这个方法的最后调用了set_proposed_producers这个方法,在libraries/chain/wasm_interface.cpp这个文件中.又要进行一波合法性检查.然后又要调用libraries/chain/include/eosio/chain/controller.hpp这个文件中的同名方法.又要进行几个校验.然后才真正的设置.这里就不展开解读这两个方法了.感兴趣者打开这两个文件自行阅读即可.
重要的是,这个过程会校验schedule不与当前的schedule重复.否则不会设置.
这个代码中有一个奇怪的部分: 为何要重复定义两个vector.用第二个vector来传输给后续的set_proposed_producers方法.我分析了一下,认为是:排序时本欲使用location来排序.却因故没有实现,临时改为了使用name来排序.因为实际上对名称排序毫无意义(让出块顺序更整齐有什么意义呢?),后续也没有看到对location排序的实现.

修改相关代码,定制BP产生算法和BP数量.

可以看到,当前的BP是120个块产生一次.这部分代码在producerpay.cpponblock方法中.
if( timestamp.slot - _gstate.last_producer_schedule_update.slot > 120 )
只需要修改这个120即可.本例中我将其改为8.修改这个值过小将会加大节点的负担.

修改BP个数

大多数私链并没有21个这么多的节点.也就是投票数最多的21个BP出块并没有意义.可以把它改小一点.
根据上文的解读,只需要修改update_elected_producers方法中的两个21即可.
同时还需要删除掉下面一句.允许新的BP数目少于之前的BP数目.

      if ( top_producers.size() < _gstate.last_producer_schedule_size ) {
         return;
      }

我实际上修改过.我运行了一个三节点的EOS链.在修改之前有三个出块者.分别是hheos,hmeos,heos.可以看到,它们是有序轮流交替出块的.

轮流交替出块

我把这个方法中的两个21改为1,也就是只让投票数最高的节点出块.
之后编译并上传文件替换掉现有的eosio别忘了这一步.
之后可以看到一个出块者替换提示:

 promoting proposed schedule (set in block 63481) to pending; current block: 63497 lib: 63484 schedule: {"version":1,"producers":[{"producer_name":"hheos","block_signing_key":"EOS7TUVM5bHqC5n4ipaC8H3kKRMifNYcdxCyG4zdq3sr5DnLaKEQv"}]}

便是hheos独自出块了:


hheos独自出块

可以看到这样减少出块者数目是有效的.

修改BP产生算法

永远只是前21个出块多么无趣?不妨来点有意思的.例如,在前21个BP中,随机选取出块者如何?这是一个考验.因为它要求了要能够在区块链中产生不可预测或者难以预测的随机数.我正在探索这一点,所以使用了一个相对较低级的"随机函数",实际上随机函数也不过是一些数学函数的组合罢了.我就随便写了一个复杂的数学表达式.
完整代码:

  void system_contract::update_elected_producers( block_timestamp block_time ) {
      _gstate.last_producer_schedule_update = block_time;

      auto idx = _producers.get_index<N(prototalvote)>();

      std::vector< std::pair<eosio::producer_key,uint16_t> > top_producers;
      top_producers.reserve(21);

      for ( auto it = idx.cbegin(); it != idx.cend() && top_producers.size() < 21 && 0 < it->total_votes && it->active(); ++it ) {
         top_producers.emplace_back( std::pair<eosio::producer_key,uint16_t>({{it->owner, it->producer_key}, it->location}) );
      }

      // if ( top_producers.size() < _gstate.last_producer_schedule_size ) {
      //    return;
      // }

      /// sort by producer name
      std::sort( top_producers.begin(), top_producers.end() );

      std::vector<eosio::producer_key> producers;

      producers.reserve(top_producers.size());
      // for( const auto& item : top_producers )
      //    producers.push_back(item.first);

      uint32_t bpindex = (int32_t)(sin(block_time.slot) * 100 +  (int32_t)pow(2,top_producers.size()) % 16 + block_time.slot ) % top_producers.size();
      producers.push_back( top_producers[bpindex].first );
      bytes packed_schedule = pack(producers);

      if( set_proposed_producers( packed_schedule.data(),  packed_schedule.size() ) >= 0 ) {
         _gstate.last_producer_schedule_size = static_cast<decltype(_gstate.last_producer_schedule_size)>( top_producers.size() );
      }
   }

这个编译运行并提交之后,可以看到不断地有新的schedule产生.也就是不断的随机选取一个节点来出块.
需要注意的是,尽量不要让schedule里只有一个节点.否则一旦这个节点不能出块.整个区块链就停滞了.有多个节点的话当一个节点不能出块时至少还可以让schedule里的其他节点继续出块.

随机出块

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容