简介
EOS出块机制是DPOS(Delegate Proof of Stake),EOS中的区块生产者通过投票选举产生,每过一分钟取得票最高的前21个区块生产节点作为新的区块生产者,投票选举时刻都在进行,这21个区块生产者记录在一张名单上然后按照名单排名顺序依次生产区块,这里我们来讨论这张名单是怎么来的以及怎么更新的
区块生产者的选举
- 用户投票
EOS中用户想投票给某一节点,必须先抵押他们的EOS形成投票权重
1)用户通过delegatebw命令把EOS抵押来换取cpu跟net资源,同时抵押的EOS多少会记录在该用户的voter_info表中的staked字段,该字段表示用户一共抵押了多少EOS,后面会把它换算成投票权重,抵押的越多权重越大,voter_info结构如下:
struct voter_info {
account_name owner = 0; // 用户
account_name proxy = 0; // 用户设置的代理节点名称
std::vector<account_name> producers; // 用户投票的节点,如果没有设置代理
int64_t staked = 0; // 用户抵押的EOS数
double last_vote_weight = 0; // 用户的投票权重
double proxied_vote_weight= 0; // 如果本用户是代理 表示该代理持有的全部权重(其他用户选择本用户作为代理后的投票权重相加)
bool is_proxy = 0; // 本用户是否为代理
uint32_t reserved1 = 0;
time reserved2 = 0;
eosio::asset reserved3;
uint64_t primary_key()const { return owner; }
};
eos系统合约中跟用户抵押资源相关的还有一张表delegated_bandwidth,该表具体记录了谁给谁抵押了多少cpu/net
struct delegated_bandwidth {
account_name from;
account_name to;
asset net_weight;
asset cpu_weight;
uint64_t primary_key()const { return to; }
};
用户的net_weight+cpu_weight等于该voter_info中的staked ,例如:
创建用户eosiotest111
./cleos system newaccount --stake-net '10.0000 SYS' --stake-cpu '10.0000 SYS' --buy-ram-kbytes 30 eosio eosiotest111 EOS6jDzj7MWmdJVvJJ7ikNyq8BHfJhA3B7KbJsugJxsHhbf3sobP2
转入100 SYS
./cleos push action eosio.token transfer '[ "eosio", "eosiotest111", "100.0000 SYS", "m" ]' -p eosio@active
给自己抵押50 SYS
./cleos system delegatebw eosiotest111 eosiotest111 '25.0000 SYS' '25.0000 SYS'
给hellohello22抵押50 SYS
./cleos system delegatebw eosiotest111 hellohello22 '25.0000 SYS' '25.0000 SYS'
然后获取eosiotest111 voter_info跟delegated_bandwidth信息
./cleos get table eosio eosio voters
{
"rows": [
...
,{
"owner": "eosiotest111",
"proxy": "",
"producers": [],
"staked": 1000000,
"last_vote_weight": "0.00000000000000000",
"proxied_vote_weight": "0.00000000000000000",
"is_proxy": 0
},
...
],
"more": false
}
./cleos get table eosio eosiotest111 delband
{
"rows": [{
"from": "eosiotest111",
"to": "eosiotest111",
"net_weight": "25.0000 SYS",
"cpu_weight": "25.0000 SYS"
},{
"from": "eosiotest111",
"to": "hellohello22",
"net_weight": "25.0000 SYS",
"cpu_weight": "25.0000 SYS"
}
],
"more": false
}
可以看到staked为1000000,这里因为staked保存的是asset的amount所以包含了4位小数精度,其实这个staked为 100.0000 SYS,而用户抵押的net_weight+cpu_weight所有加起来也等于 100.0000 SYS。
2)用户通过voteproducer命令对区块候选生产节点进行投票或者投票给代理,简单的说就是把上述voter_info表中的staked换算成投票权重投给指定的区块生产者,这里我们来看看抵押的EOS到投票权重的换算
double stake2vote( int64_t staked ) {
/// TODO subtract 2080 brings the large numbers closer to this decade
double weight = int64_t( (now() - (block_timestamp::block_timestamp_epoch / 1000)) / (seconds_per_day * 7) ) / double( 52 );
return double(staked) * std::pow( 2, weight );
}
上述weight表示当前距离2000年1月1日有多少年过去了,可以看到离2000年越远权重越大,例如假设当前时间为2018年1月1日,那么weight = 18,最终权重=staked * 2 ** 18,假设当前时间为2019年1月1日,那么weight = 19,最终权重=staked * 2 ** 19,可以看到权重在时间上会衰减,这个半衰期为1年,意思是你今天投票了得到的权重是 w,明年这个时候同样的EOS可以得到权重2w,这么设计的初衷我想是EOS鼓励用户要经常投票,如果你不投你已有的权重就会衰减。
3)最终系统合约会把所有的候选区块生产者节点得到投票权重记录在producer_info表中的total_votes 字段,然后按照得票权重的高低排序选取前21个节点作为区块生产者
struct producer_info {
account_name owner;
double total_votes = 0; // 本节点得到的投票权重
eosio::public_key producer_key; /// a packed public key object
bool is_active = true;
std::string url;
uint32_t unpaid_blocks = 0;
uint64_t last_claim_time = 0;
uint16_t location = 0;
uint64_t primary_key()const { return owner; }
double by_votes()const { return is_active ? -total_votes : total_votes; }
bool active()const { return is_active; }
void deactivate() { producer_key = public_key(); is_active = false; }
};
区块生产者的更新
- 区块生产者名单
block_header_state 结构体中的active_schedule就是当前区块生产者名单,既然说到了 block_header_state ,就必须说一下block_state,struct block_state : public block_header_state,block_state继承block_header_state ,block_state它包含了当前要生产的块的所有需要的状态上下文信息
,要完全明白它还必须说明controller,forkdb,producer_plugin这几者联系在一起的块生产过程,有机会放在下一篇再讲。
struct block_header_state {
...
producer_schedule_type pending_schedule; // 待更新的区块生产者候选名单
producer_schedule_type active_schedule; // 当前的区块生产者名单
...
}
我们来看看如何从当前区块生产者名单中根据当前块时间获取相应生产者get_scheduled_producer
producer_key block_header_state::get_scheduled_producer( block_timestamp_type t )const {
// config::producer_repetitions = 12
auto index = t.slot % (active_schedule.producers.size() * config::producer_repetitions);
index /= config::producer_repetitions;
return active_schedule.producers[index];
}
上面t.slot表示多少个块时间过去了,active_schedule.producers.size() * config::producer_repetitions表示一轮块生产循环为多少个块,index表示目前位于第几轮,index /= config::producer_repetitions表示目前轮到哪个生产者,这里config::producer_repetitions 从配置文件中可以发现是12,表示每一轮块生产过程中每个生产者最大能生产12个区块
- 区块生产者名单更新
1)特殊事务onblock,该事务用来执行系统合约中的onblock action,该事务在controller中的start_block中调用,start_block用来创建一个pending block,即上面的block_state,即开始为生产下一个块提供状态上下文,onblock携带的参数为当前head块的头
void start_block( block_timestamp_type when, uint16_t confirm_block_count, controller::block_status s ) {
...
// 获取onblock事务
auto onbtrx = std::make_shared<transaction_metadata>( get_on_block_transaction() );
auto reset_in_trx_requiring_checks = fc::make_scoped_exit([old_value=in_trx_requiring_checks,this](){
in_trx_requiring_checks = old_value;
});
in_trx_requiring_checks = true;
// 执行onblock事务
push_transaction( onbtrx, fc::time_point::maximum(), true, self.get_global_properties().configuration.min_transaction_cpu_usage, true );
...
}
注意上述push_transaction调用中的第三个参数为true,表示该事务是隐式的,不会检查权限也不会进入区块
2)系统合约中的onblock,该函数主要做的2件事 1)记录该块是谁生产的(后面区块生产者能拿奖励) 2)判断距离上一次更新区块生产者是否已经过去60秒 如果是则更新区块生产者
void system_contract::onblock( block_timestamp timestamp, account_name producer ) {
using namespace eosio;
require_auth(N(eosio));
if( _gstate.total_activated_stake < min_activated_stake )
return;
if( _gstate.last_pervote_bucket_fill == 0 ) /// start the presses
_gstate.last_pervote_bucket_fill = current_time();
auto prod = _producers.find(producer);
if ( prod != _producers.end() ) {
_gstate.total_unpaid_blocks++;
// 记录该块是谁生产的
_producers.modify( prod, 0, [&](auto& p ) {
p.unpaid_blocks++;
});
}
// 判断距离上一次更新区块生产者是否已经过去60秒 如果是则更新区块生产者
if( timestamp.slot - _gstate.last_producer_schedule_update.slot > 120 ) {
update_elected_producers( timestamp );
}
}
3)set_proposed_producers函数按投票数从高到低排列选取得票最高的前21个作为区块生产者最终调用wasm接口 set_proposed_producers
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);
// 按投票数从高到低排列选取得票最高的前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}) );
}
4)controller::set_proposed_producers函数,该函数先判断一下新的区块生产者名单是否跟当前区块生产者名单相同,如果相同则返回,否则最终记录在global_property_multi_index表中
int64_t controller::set_proposed_producers( vector<producer_key> producers ) {
const auto& gpo = get_global_properties();
...
// 把当前块号跟区块生产者名单记录在表中
my->db.modify( gpo, [&]( auto& gp ) {
gp.proposed_schedule_block_num = cur_block_num;
gp.proposed_schedule = std::move(sch);
});
return version;
}
5)最终记录在global_property_multi_index中的最新区块生产者会在controller中的start_block中的set_new_producers更新到区块生产者候选名单,最终由maybe_promote_pending把区块生产者候选名单更新到当前的区块生产者名单
// 更新到区块生产者候选名单
pending->_pending_block_state->set_new_producers( gpo.proposed_schedule );
// 把区块生产者候选名单更新到当前的区块生产者名单
auto was_pending_promoted = pending->_pending_block_state->maybe_promote_pending();
至此完成了区块生产者名单的更新
网友评论