阅读本文之前,你需要的
- 面向对象编程的能力
- 最好具备中级的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.cpp
的onblock
方法中.
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独自出块了:
可以看到这样减少出块者数目是有效的.
修改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里的其他节点继续出块.