EOS解读--共识

共识简介

1. DPoS共识算法概要

DPOS算法分为两部分:选择一组区块生产者和调度生产区块。要确保选举过程由股东最终控制,因为当网络连接不畅通时,股东的损失最大。选举方法对实际运行中如何达成共识几乎没有影响,因此,本文将重点介绍如何在区块生产者被选择之后达成共识。

为了帮助解释这个算法,我只假设3个区块生产者A,B和C。因为共识(的达成)需要2⁄3+1(个节点)来解决所有情况,这个简化的模型将假设生产者C是打破僵局的那个节点。在现实世界中,将有21个或更多的区块生产者。像工作量证明(PoW)一样,一般规则是最长链胜出。任何时候当一个诚实的对等节点看到一个有效的更长链,它都会从当前分叉切换到更长的这条链。

我将通过示例展示,在几乎所有可能想得到的网络条件下,DPOS是如何运行。这些例子应该可以帮助你理解为什么DPOS稳健且难以破坏。

2. 正常生产流程

在正常操作模式下,区块生产者们每3秒钟轮流生成一个块。假设没有节点错过自己的轮次,那么这将产生最长链。区块生产者在被调度轮次之外的任何时间段出块都是无效的。

image

正常生产流程示意图

3. 少数节点分叉

不超过节点总数1⁄3的恶意或故障节点可能创建少数分叉。在这种情况下,少数分叉每9秒只能产生1个块,而多数分叉每9秒可以产生2个块。这样,诚实的 2⁄3多数将永远比少数(的链)更长。

image

少数节点分叉示意图

4. 离线少数节点的双重生产

(离线的)少数节点可以试图产生无限数量的分叉,但是他们的所有分叉都将比多数节点的那条链短,因为少数节点在出块速度上注定比多数节点来的更慢。

image

少数节点双重生产示意图

5. 网络分片化(Fragmentation)

网络完全有可能分片,从而导致多个分叉,且所有分叉都不拥有多数区块生成者。在这种情况下,最长的链将倒向最大的那个少数群体。当网络连通性恢复时,较小的少数群体会自然切换到最长的那条链,明确的共识将恢复。

image

网络分片化示意图

有可能存在这样三个分叉,其中两个最长的分叉长度相同。在这种情况下,第3个(较小)分叉的区块生产者重新加入网络时会打破平局。区块生产者总数为奇数,因此不可能长时间保持平局。稍后我们还会讲到“洗牌”生产者,它使得出块顺序随机化,从而确保即使是生产者数目相同的两个分叉也会以不同的步长增长,最终导致一个分叉超过另一个。

6. 在线少数节点的双重生产

在这种场景下,少数节点B在其时间段内产生了两个或更多可供选择的块。下一个计划生产者(C)可以选择基于B产生的任何一种方案继续构建链条。一旦如此,这个选择就成为最长的链,而所有选择B1的节点都将切换分叉。少数不良生产者企图广播再多的替代块也无关紧要,它们作为最长链的一部分永远不会超过一轮。

image

在线少数节点的双重生产示意图

7. 最后不可逆区块(Last Irreversible Block)

在网络分片化的情况下,多个分叉都有可能持续不断增长相当长的时间。长远来看最长的链终将获胜,但观察者需要一种确切的手段来判定一个块是否绝对处于增长最快的那条链。这可以通过观察来自2/3+1多数区块生产者的确认来决定。

在下图中,块B已被C和A所确认,这代表已经被2⁄3+1的多数节点确认了,由此我们可以推断没有其它链会比这个更长 – 如果这2⁄3的生产者是诚实的。

image

最后不可逆区块示意图

请注意,这个“规则”类似于比特币的6块确认“规则”。一些聪明节点也许可以谋划一系列事件使得两个节点(“交易”?)出现在不同的最后不可逆区块上。这种极端例子要求攻击者能完全控制通信延迟,并且在几分钟内两次--而不是一次--使用该控制。即便这真的发生了,那么最长链(胜出)的长期规则仍然适用。我们估计这种攻击的可能性接近0,且经济损失微不足道,因此不足为虑。

8. 法定(Quorum)生产者节点数不足

在缺乏明晰的生产者法定节点数这种不太可能的情况下,少数节点还是可以继续出块。股东可以在这些块里包括更改其投票的交易。这些投票可以选出一组新的生产者,并将出块参与率恢复到100%。一旦如此,少数链将最终超过所有其他以低于100%参与率运行的链。

在此过程中,所有观察者都会知道,在一条参与率超过67%的链形成之前,网络状态一直处于变化之中。那些选择在此条件下进行交易的节点所冒的风险与选择接受少于6次确认的节点相似。他们这样做是因为知道共识最终落在另一个分支上的可能性很小。在实践中,这种情况比接受少于3个比特币交易确认区块要安全多了。

9. 多数生产者舞弊(Corruption)

如果多数生产者变得腐败,那么他们可以产生无限数量的分叉,每个分叉都看起来以2/3多数确认向前推进。这种情况下,最后不可逆块算法蜕回为最长链算法。最长链就是为最大多数所认可的那条链,而这将由少数剩下的诚实节点决定。这种行为不会持续很长时间,因为最终股东会投票替换掉这些生产者。

image

多数生产者舞弊示意图

10. 交易作为权益证明(TaPoS: Transactions as Proof of Stake)

当用户为一个交易签名时,他们这样做是假设当前区块链的状态已经确定了。这个假设是基于他们对最近几个区块的认可。如果最后共识的最长链发生改变,那么它可能会使签署者同意交易时的假设无效。

就TaPoS而言,所有交易都包含最近一个区块的哈希值,如果该区块最终在链历史中不存在,那么这些交易被认为是无效的。任何在孤立分叉上给交易签名的节点,都会发现该交易无效且无法迁移到主分叉。

该过程的另一个作用就是,可以抵御试图产生替代链的长期攻击。每个股东在每次交易时都直接对区块链做出确认。随着时间推移,所有的块都是由所有股东确认过的,而这是无法在伪造链中复制的。

11. 确定性生产者洗牌(Shuffling)

在上面所有例子中,我们展示的都是区块生产者按调度循环出块。实际上,每生产N个区块(N是生产者数量),区块生产者集合都会洗牌一次。这种随机性确保块生成者B不会总是忽略块生成者A,每当形成多个拥有相同数量生产者的分叉时,均势最终都会被打破。

12.EOS的出块流程

Plain Text ........ //启动生产插件 producer_plugin::plugin_startup(); ........ //出块循环 my->schedule_production_loop(); ......... //出块 auto result = start_block(); .......... //签名和提交 auto res = self->maybe_produce_block(); ..........

以上步骤的核心在于:auto result = start_block();
这个函数更跟下去会发现,重点 在于:

chain.start_block(block_time,blocks_to_confirm);
这个函数才是dops的关键词

void start_block( block_timestamp_type when, uint16_t confirm_block_count, controller::block_status s ) {
      FC_ASSERT( !pending );
​
      FC_ASSERT( db.revision() == head->block_num, "",
                ("db.revision()", db.revision())("controller_head_block", head->block_num)("fork_db_head_block", fork_db.head()->block_num) );
​
      auto guard_pending = fc::make_scoped_exit([this](){
         pending.reset();
      });
​
      pending = db.start_undo_session(true);
​
      pending->_block_status = s;
      //由当前区块头创建一个新的block_state ---->>>>> 11111
      pending->_pending_block_state = std::make_shared<block_state>( *head, when ); // promotes pending schedule (if any) to active
      pending->_pending_block_state->in_current_chain = true;
      //确认链上的区块  ----->>>>> 22222
      pending->_pending_block_state->set_confirmed(confirm_block_count);
      //更新激活的生产者 ----->>>> 33333
      auto was_pending_promoted = pending->_pending_block_state->maybe_promote_pending();
       const auto& gpo = db.get<global_property_object>();
      if( gpo.proposed_schedule_block_num.valid() && // if there is a proposed schedule that was proposed in a block ...
          ( *gpo.proposed_schedule_block_num <= pending->_pending_block_state->dpos_irreversible_blocknum ) && // ... that has now become irreversible ...
          pending->_pending_block_state->pending_schedule.producers.size() == 0 && // ... and there is room for a new pending schedule ...
          !was_pending_promoted // ... and not just because it was promoted to active at the start of this block, then:
        )
      {
         // Promote proposed schedule to pending schedule.
         if( !replaying ) {
            ilog( "promoting proposed schedule (set in block ${proposed_num}) to pending; current block: ${n} lib: ${lib} schedule: ${schedule} ",
                  ("proposed_num", *gpo.proposed_schedule_block_num)("n", pending->_pending_block_state->block_num)
                  ("lib", pending->_pending_block_state->dpos_irreversible_blocknum)
                  ("schedule", static_cast<producer_schedule_type>(gpo.proposed_schedule) ) );
         }
         pending->_pending_block_state->set_new_producers( gpo.proposed_schedule );
         db.modify( gpo, [&]( auto& gp ) {
            gp.proposed_schedule_block_num = optional<block_num_type>();
            gp.proposed_schedule.clear();
         });
​
          std::cout<<"\n========555==========\n";
          std::cout<< fc::json::to_string(*pending->_pending_block_state)<<"\n";
          std::cout<<"==========555========\n";
      }

11111,22222,33333这三步涉及到的数据结构是block_state

{ "id": "0000058af1b33d45e4ec927e52b74e8568422f45245dc0399c144bdff6dc16d8", "block_num": 1418, "header": { "timestamp": "2018-08-04T07:31:47.500", "producer": "accountnum33", "confirmed": 0, "previous": "0000058980ac0382075af4216ddbd7be1eae1690a278d7cc04a61570f307b39d", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "eaadc508183ba966551e48dba3a44505a95e6a42d7a53f435db39c3dec3584c9", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_KVyKdpLpEXPQUvRR5jZL4FMYvFDNic6tyvX6icLXFKhoriskCg21esduAceX2cZDv2GwcB12TxdneeKMwfzWB2TckZpZvQ" }, "dpos_proposed_irreversible_blocknum": 1370, "dpos_irreversible_blocknum": 1370, "bft_irreversible_blocknum": 0, "pending_schedule_lib_num": 1369, "pending_schedule_hash": "b4ea72a3e20628a54028e681258cd1e6a46b20ae07210836f29087f9f8f37346", "pending_schedule": { "version": 1, "producers": [ ] }, "active_schedule": { "version": 1, "producers": [ { "producer_name": "accountnum11", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" }, { "producer_name": "accountnum22", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" }, { "producer_name": "accountnum33", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" } ] }, "blockroot_merkle": { "_active_nodes": [ "0000058980ac0382075af4216ddbd7be1eae1690a278d7cc04a61570f307b39d", "ab53f4807abffafbcf15fa6edd6ffe72cf73d18689403e389127015b4fd1e544", "0275481b63dcb217603558835292d4057d39f9a5d41c9d5302230d379a992b18", "af944001e02b80671b6ea50e5f83969fc2db5e56671ff83700a9018336d0fc58", "e1ed7b7a82f5507110497fd41f2098a269ffbe8202de0f57393b4aeb27792434", "5a36ca2325d5f7b2de22b98de7cf70c0a06cf391fc17f1d242b964d16146dee9" ], "_node_count": 1417 }, "producer_to_last_produced": [ [ "accountnum11", 1394 ], [ "accountnum22", 1406 ], [ "accountnum33", 1418 ], [ "eosio", 1370 ] ], "producer_to_last_implied_irb": [ [ "accountnum11", 1370 ], [ "accountnum22", 1370 ], [ "accountnum33", 1370 ] ], "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt", "confirm_count": [ 2,          2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2 ], "confirmations": [ ], "block": { "timestamp": "2018-08-04T07:31:47.500", "producer": "accountnum33", "confirmed": 0, "previous": "0000058980ac0382075af4216ddbd7be1eae1690a278d7cc04a61570f307b39d", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "eaadc508183ba966551e48dba3a44505a95e6a42d7a53f435db39c3dec3584c9", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_KVyKdpLpEXPQUvRR5jZL4FMYvFDNic6tyvX6icLXFKhoriskCg21esduAceX2cZDv2GwcB12TxdneeKMwfzWB2TckZpZvQ", "transactions": [ ], "block_extensions": [ ] }, "validated": true, "in_current_chain": true }

第一步:从当前块头出发,生成一个新的block_state。

........
result.producer_to_last_implied_irb[prokey.producer_name] = result.dpos_proposed_irreversible_blocknum;
result.dpos_irreversible_blocknum                         = result.calc_dpos_last_irreversible(); 
​
/// grow the confirmed count
static_assert(std::numeric_limits<uint8_t>::max() >= (config::max_producers * 2 / 3) + 1, "8bit confirmations may not be able to hold all of the needed confirmations");
​
// This uses the previous block active_schedule because thats the "schedule" that signs and therefore confirms _this_ block
auto num_active_producers = active_schedule.producers.size();
uint32_t required_confs = (uint32_t)(num_active_producers * 2 / 3) + 1;
​
if( confirm_count.size() < config::maximum_tracked_dpos_confirmations ) {
   result.confirm_count.reserve( confirm_count.size() + 1 );
   result.confirm_count  = confirm_count;
   result.confirm_count.resize( confirm_count.size() + 1 );
   result.confirm_count.back() = (uint8_t)required_confs;
} else {
   result.confirm_count.resize( confirm_count.size() );
   memcpy( &result.confirm_count[0], &confirm_count[1], confirm_count.size() - 1 );
   result.confirm_count.back() = (uint8_t)required_confs;
}
.......

当前3个生产节点,required_confs为3,所以11111结束后,confirm_count中会压入3,即当前出产块所需的确认个数。
下面直接给出几个解释:
confirm_count:区块所需确认个数表,从当前出产块往前推,最后的元素为当前出产块所需的确认个数
producer_to_last_implied_irb:由生产者确定的不可逆区块候选名单
dpos_irreversible_blocknum:不可逆区块
dpos_proposed_irreversible_blocknum:候选不可逆区块
active_schedule:已激活生产者的列表
有了对这几个定义的解释,代码变得很清晰

//计算不可逆区块,将不可逆区块候选名单中从小到大排,选出1/3处的区块号作为不可逆区块

uint32_t block_header_state::calc_dpos_last_irreversible()const { vector<uint32_t> blocknums; blocknums.reserve( producer_to_last_implied_irb.size() ); for( auto& i : producer_to_last_implied_irb ) { blocknums.push_back(i.second); } /// 2/3 must be greater, so if I go 1/3 into the list sorted from low to high, then 2/3 are greater ​ if( blocknums.size() == 0 ) return 0; /// TODO: update to nth_element std::sort( blocknums.begin(), blocknums.end() ); return blocknums[ (blocknums.size()-1) / 3 ]; }

上面的程序中出现两次3分之几的算法,第一次是required_confs,没毛病,一个区块要得到2/3个生产者的确认。但是得到2/3的确认与不可逆区块的联系不仅仅是这个,严格来说,不可逆区块是从不可逆区块候选名单中选择出来的。
第11111步之后的JSON

{ "id": "0000000000000000000000000000000000000000000000000000000000000000", "block_num": 1419, "header": { "timestamp": "2018-08-04T07:31:48.000", "producer": "accountnum11", "confirmed": 1, "previous": "0000058af1b33d45e4ec927e52b74e8568422f45245dc0399c144bdff6dc16d8", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne" }, "dpos_proposed_irreversible_blocknum": 1370, "dpos_irreversible_blocknum": 1370, "bft_irreversible_blocknum": 0, "pending_schedule_lib_num": 1369, "pending_schedule_hash": "b4ea72a3e20628a54028e681258cd1e6a46b20ae07210836f29087f9f8f37346", "pending_schedule": { "version": 1, "producers": [ ] }, "active_schedule": { "version": 1, "producers": [ { "producer_name": "accountnum11", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" }, { "producer_name": "accountnum22", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" }, { "producer_name": "accountnum33", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" } ] }, "blockroot_merkle": { "_active_nodes": [ "f8a83438724a996caaa42231c39b757bec3d1b6fb0bd2c982812eb2f7fadc377", "ab53f4807abffafbcf15fa6edd6ffe72cf73d18689403e389127015b4fd1e544", "0275481b63dcb217603558835292d4057d39f9a5d41c9d5302230d379a992b18", "af944001e02b80671b6ea50e5f83969fc2db5e56671ff83700a9018336d0fc58", "e1ed7b7a82f5507110497fd41f2098a269ffbe8202de0f57393b4aeb27792434", "2553b9e4b8225b0f311c2a382fccd027e90f909319616b75933b56f19d20c440" ], "_node_count": 1418 }, "producer_to_last_produced": [ [ "accountnum11", 1419 ], [ "accountnum22", 1406 ], [ "accountnum33", 1418 ], [ "eosio", 1370 ] ], "producer_to_last_implied_irb": [ [ "accountnum11", 1370 ], [ "accountnum22", 1370 ], [ "accountnum33", 1370 ] ], "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt", "confirm_count": [ 2,          2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3 ], "confirmations": [ ], "block": { "timestamp": "2018-08-04T07:31:48.000", "producer": "accountnum11", "confirmed": 1, "previous": "0000058af1b33d45e4ec927e52b74e8568422f45245dc0399c144bdff6dc16d8", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne", "transactions": [ ], "block_extensions": [ ] }, "validated": false, "in_current_chain": false }

到第22222步,这个函数确定了候选不可逆区与确认列表的关系
num_prev_blocks是指当前节点最后一次出块到现在的间隔,本文使用3个节点,故间隔为24
比如出块顺序为accountnum33--“ accountnum11 - “accountnum22 - ”accountnum33
accountnum33会员自己的块确认一次
accountnum11的时候会把33出过的块和自己确认一次....
确认就​​是将确认数组的相应元素减去1,第一步得到的json中:
“confirm_count”:[2,2,2,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2, 2,2,2,3]
此时num_prev_blocks = 24
所以以下程序的工作是先将confirm_count元素从尾到头减去1,最多遍历num_prev_blocks和confirm_count大小中最小的。
“confirm_count”:[2,2,2] ,2,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 ,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,2]
由于出现了“0”的元素,将0元素的区块号(1406)记录在dpos_proposed_irreversible_blocknum中,然后在更新 组,将0之前的元素去掉。
“confirm_count”:[1,1,1,1,1,1,1,1,1,1,1,1,
所以这样看来,dpos_proposed_irreversible_blocknum就是本生产节点在本块确定的候选不可逆区块。而这个参数会在下一次出块时打包进

producer_to_last_implied_irb。
void block_header_state::set_confirmed(
 uint16_t num_prev_blocks ) { /* idump((num_prev_blocks)(confirm_count.size())); ​ for(
 uint32_t i = 0; i < confirm_count.size(); ++i ) { std::cerr << "confirm_count["<<i<<"] = " << int(confirm_count[i]) << "\n"; } */ header.confirmed = num_prev_blocks; ​ int32_t i = (int32_t)(confirm_count.size() - 1); uint32_t blocks_to_confirm = num_prev_blocks + 1; /// confirm the head block too while( i >= 0 && blocks_to_confirm ) { --confirm_count[i]; //idump((confirm_count[i])); if( confirm_count[i] == 0 ) { uint32_t block_num_for_i = block_num - (uint32_t)(confirm_count.size() - 1 - i); dpos_proposed_irreversible_blocknum = block_num_for_i; //idump((dpos2_lib)(block_num)(dpos_irreversible_blocknum)); ​ if (i == confirm_count.size() - 1) { confirm_count.resize(0); } else { memmove( &confirm_count[0], &confirm_count[i + 1], confirm_count.size() - i - 1); confirm_count.resize( confirm_count.size() - i - 1 ); } ​ return; } --i; --blocks_to_confirm; } }

第22222步之后的输出JSON为:

{ "id": "0000000000000000000000000000000000000000000000000000000000000000", "block_num": 1419, "header": { "timestamp": "2018-08-04T07:31:48.000", "producer": "accountnum11", "confirmed": 24, "previous": "0000058af1b33d45e4ec927e52b74e8568422f45245dc0399c144bdff6dc16d8", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne" }, "dpos_proposed_irreversible_blocknum": 1406, "dpos_irreversible_blocknum": 1370, "bft_irreversible_blocknum": 0, "pending_schedule_lib_num": 1369, "pending_schedule_hash": "b4ea72a3e20628a54028e681258cd1e6a46b20ae07210836f29087f9f8f37346", "pending_schedule": { "version": 1, "producers": [ ] }, "active_schedule": { "version": 1, "producers": [ { "producer_name": "accountnum11", "block_signing_key": "EOS8mUftJXepGzdQ2TaCduNuSPAfXJHf22uex4u41ab1EVv9EAhWt" }, { 1, 1, 1, 1, 1, 1, 1, 1, 2 ], "confirmations": [ ], "block": { "timestamp": "2018-08-04T07:31:48.000", "producer": "accountnum11", "confirmed": 1, "previous": "0000058af1b33d45e4ec927e52b74e8568422f45245dc0399c144bdff6dc16d8", "transaction_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "action_mroot": "0000000000000000000000000000000000000000000000000000000000000000", "schedule_version": 1, "header_extensions": [ ], "producer_signature": "SIG_K1_111111111111111111111111111111111111111111111111111111111111111116uk5ne", "transactions": [ ], "block_extensions": [ ] }, "validated": false, "in_current_chain": true }

综上,实际上dpos在代码里面使用是分作两步的.1,2
/ 3共识,符合2/3认可的最高区块可以进入“由生产者确定的不可逆区块候选名单”。由于出块轮转,每个生产者所能见证的高度是不一样的
.1,1 / 3共识,从“由生产者确定的不可逆区块候选名单”中选择1/3高度的地方得到不可逆区块。
仔细一想,2还是加强了不可逆区块的共识。

区块生产

1.一个节点要生成区块,必须满足两个条件
 chain-> _production_enabled==true

_production_enabled=true有几种情况
1.config.ini里

enable-stale-production=true

创世块的时间戳(genesis文件中的initial_timestamp字段)是一个确定的值,节点nodeos第一次启动时当前时间肯定远大于这个创世块的时间戳,因而正常情况下,系统应该已经基于这个创世块生产很多后续区块,因而需要先同步到最新再生产新块的。但是由于这个链是你自己刚建立的,你确定没有其他节点基于你本地的区块(包括创世块)生产了其他区块,因此立即基于当前区块生产的新块是合法的且也是你应该做的。

2.区块同步完成时

          这个很好理解,当我们已经同步下来所有区块时,我们自然可以基于最新的区块生产新的区块
    void on_incoming_block(const signed_block_ptr& block) {

         //如果下一个块的截止时间大于当前时间,意味着同步完成

         if( chain.head_block_state()->header.timestamp.next().to_time_point() >= fc::time_point::now() )

            _production_enabled = true;

    }

节点被投票成了21个代表中的一个,且到了生产区块(21个代表节点是分时生产区块的)
我们知道EOS采用的DPOS+BFT,一个节点要成为真正“生产者”,必须被系统其他节点投票出来成为21个超级节点中的一个。同时,被选择为超级节点后,也是和其他20个节点轮流生产。其实,这里存在一个生产者注册流程,也就说一个节点光配置为producer是不够的,还需要通过eosio.system智能合约注册生产者,这个操作权限只授予给了创世块的initial_key的持有人。
区块生产者

在EOS中,每个用户都可以注册成为区块生产者(BP).注册完成之后需要鼓动其他用户质押自己的EOS给你投票.当满足:
(a)全体用户质押的EOS数量超过总EOS数量的15%.
(B)得票数在前21位 时,就获得了BP的资格.
这个方法接收两个参数,分别时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
在注册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类型组成的的动态数组.
      //  这个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;
      }
      /// 排序
      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重复.否则不会设置.

struct config {
            flat_set<account_name>   actor_whitelist;白名单
            flat_set<account_name>   actor_blacklist;黑名单
            flat_set<account_name>   contract_whitelist;合约白名单
            flat_set<account_name>   contract_blacklist;合约黑名单
            flat_set< pair<account_name, action_name> > action_blacklist;操作黑名单
            flat_set<public_key_type> key_blacklist;key和名单
            path                     blocks_dir             =  chain::config::default_blocks_dir_name;块的路径
            path                     state_dir              =  chain::config::default_state_dir_name;
            uint64_t                 state_size             =  chain::config::default_state_size;
            uint64_t                 reversible_cache_size  =  chain::config::default_reversible_cache_size;
            bool                     read_only              =  false;
            bool                     force_all_checks       =  false;
            bool                     contracts_console      =  false;

            genesis_state            genesis;
            wasm_interface::vm_type  wasm_runtime = chain::config::default_wasm_runtime;
         };

         enum class block_status {
            irreversible = 0, ///< this block has already been applied before by this node and is considered irreversible 
             这个块以前已经被这个节点应用,并且被认为是不可逆的
            validated   = 1, ///< this is a complete block signed by a valid producer and has been previously applied by this node and therefore validated but it is not yet irreversible
             这是一个完整的块,由一个有效的生产者签名,以前已经被这个节点应用,因此验证,但它仍然是不可逆转的。
            complete   = 2, ///< this is a complete block signed by a valid producer but is not yet irreversible nor has it yet been applied by this node
             这是一个完整的块,由一个有效的生产者签名,但尚未不可逆转,也没有被这个节点所接受
            incomplete  = 3, ///< this is an incomplete block (either being produced by a producer or speculatively produced by a node)
             这是一个不完整的块(由生产者产生或由一个节点推测产生)
         };
  ......
 void startup(); 
 void finalize_block(); 完成块
 void sign_block( const std::function<signature_type( const digest_type& )>& signer_callback );签名块
 void commit_block();提交块
 void push_block( const signed_block_ptr& b, block_status s = block_status::complete );
  ......
 signal<void(const block_state_ptr&)>          accepted_block_header; 接受区块头
 signal<void(const block_state_ptr&)>          accepted_block;接受区块
 signal<void(const transaction_metadata_ptr&)> accepted_transaction;接受交易
 signal<void(const transaction_trace_ptr&)>    applied_transaction;
 signal<void(const header_confirmation&)>      accepted_confirmation;接受确认

</pre>

在producer_plugin.cpp

<pre>class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin_impl> {
   public:
      producer_plugin_impl(boost::asio::io_service& io)
      :_timer(io)
      ,_transaction_ack_channel(app().get_channel<compat::channels::transaction_ack>())
      {
      }

      optional<fc::time_point> calculate_next_block_time(const account_name& producer_name) const;计算下一个块的时间
      void schedule_production_loop();//循环计划产生
      void produce_block();//产生块
      bool maybe_produce_block();//可能产生块

插件启动的时候,调用循环计划产生块

{
…
my->schedule_production_loop();
…
}

尝试产生块

   auto reschedule = fc::make_scoped_exit([this]{
      schedule_production_loop();
   });

   try {
      produce_block();
      return true;
   } FC_LOG_AND_DROP();

   fc_dlog(_log, "Aborting block due to produce_block error");
   chain::controller& chain = app().get_plugin<chain_plugin>().chain();
   chain.abort_block();
   return false;
}

产生块并签名,提交

void producer_plugin_impl::produce_block() {
   FC_ASSERT(_pending_block_mode == pending_block_mode::producing, "called produce_block while not actually producing");

   chain::controller& chain = app().get_plugin<chain_plugin>().chain();
   const auto& pbs = chain.pending_block_state();
   const auto& hbs = chain.head_block_state();
   FC_ASSERT(pbs, "pending_block_state does not exist but it should, another plugin may have corrupted it");
   auto signature_provider_itr = _signature_providers.find( pbs->block_signing_key );

   FC_ASSERT(signature_provider_itr != _signature_providers.end(), "Attempting to produce a block for which we don't have the private key");

   //idump( (fc::time_point::now() - chain.pending_block_time()) );
   chain.finalize_block();//完成
   chain.sign_block( [&]( const digest_type& d ) {
      auto debug_logger = maybe_make_debug_time_logger();
      return signature_provider_itr->second(d);
   } );//签名
   chain.commit_block();//提交
   auto hbt = chain.head_block_time();
   //idump((fc::time_point::now() - hbt));

   block_state_ptr new_bs = chain.head_block_state();
   // for newly installed producers we can set their watermarks to the block they became
   if (hbs->active_schedule.version != new_bs->active_schedule.version) {
      flat_set<account_name> new_producers;
      new_producers.reserve(new_bs->active_schedule.producers.size());
      for( const auto& p: new_bs->active_schedule.producers) {
         if (_producers.count(p.producer_name) > 0)
            new_producers.insert(p.producer_name);
      }

      for( const auto& p: hbs->active_schedule.producers) {
         new_producers.erase(p.producer_name);
      }

      for (const auto& new_producer: new_producers) {
         _producer_watermarks[new_producer] = chain.head_block_num();
      }
   }
   _producer_watermarks[new_bs->header.producer] = chain.head_block_num();

   if(new_bs->block_num % 50 == 0) //update by zhoufd
   ilog("Produced block ${id}... #${n} @ ${t} signed by ${p} [trxs: ${count}, lib: ${lib}, confirmed: ${confs}]",
        ("p",new_bs->header.producer)("id",fc::variant(new_bs->id).as_string().substr(0,16))
        ("n",new_bs->block_num)("t",new_bs->header.timestamp)
        ("count",new_bs->block->transactions.size())("lib",chain.last_irreversible_block_num())("confs", new_bs->header.confirmed));

}

image.png

区块产生之后的pending会送到push_transactions中,具体的push_transaction截图如下:

image.png
    我们知道在start_block中产生的区块是未经多节点确认过的,因此这里传入的implicit是为true,即这个块或者说这次交易是未确认的状态,此处我们使用init_for_implicit_trx对该区块进行初始化。而后,将本次交易的回执信息如是否执行成功、CPU的使用情况、net的使用情况等写入到本次交易的回执trace->receipt中。这里需要注意区分trace、trx、trx_context之间的区别与联系。trx则是包含了本次区块产生的交易信息,trx_context则是将trx的信息写入到trx_context类中方便接下来的使用,而trace为trx_context中的一个变量类型为action_trace的值。接下来我们可以看到这三者的区别。
image.png

在图6的标注1中我们可以看到,本次交易的回执信息填充结束之后,调用fc::move_append将trx_context使用move的方式转化为右值引用,即move到区块的action中去。那么这个move_append又是实现了什么功能呢?

image.png

move_append中同样使用了move,在目标容器为空的情况下讲trx_context中的内容全部放心去。当目标容器不为空的情况下,则从目标容器末端开始循环插入trx_context中的信息。而这个目标容器就是pending->_action,就将其打包到区块的_action中去,这个_action为包含有交易回执信息的区块信息。以上操作完成了区块的生产和区块打包的过程,接下来该做些什么呢?当然是把区块信息发布到网络上或者说广播出去,让节点们去验证该区块的存在。

在eos中是如何将区块信息广播出去的呢?我们可以在图6中看到,使用了emit将trx区块内容信息或者将trace区块跟踪信息广播出去。emit的具体实现如下图:

image.png

在函数模板的情况下,完全依照模板的参数类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。这里trx和trace均为左值,因其可以赋值且可取址,而后通过完美转发将参数传递给了Signal。这样Signal中便存在着可以使用的右值。恰如emit( self.accepted_transaction, trx)和emit(self.applied_transaction, trace)。

    熟悉信号槽的人看到emit不免会想,这是不是就是信号槽机制?没错,这正是boost中的signal-slot的机制。信号会在某个特定情况或动作下被触发,槽是等同与接受并处理信号的函数。做过qt开发的人对信号槽机制并不会陌生,拿最简单的on_pushButton_clicked()函数来讲,当某一个特定事件发生时(clicked),一个信号被发送(emit),与信号相关联(connect)的槽(slot)则会响应信号并完成相应的处理。而在boost中也存在类似的机制,我们结合eos源码中关于区块广播来分析下信号槽的实现。在图4中我们知道,通过std::forward将左值trx或trace进行了完美转发变成了信号量Signal,通过跟踪可以找到这些Signal对应的slot,均存在于net_plugin中,如下图
image.png

和大多数信号槽机制一样在net_plugin启动的时候,会去绑定信号和槽之间的关系。通过cc可以获取当前链上的绝大多数信息,而后使用connect的方式绑定了以下信号量,在区块广播出去的过程中并不存在confirm因此通过代码跟踪或者日志打印,一个区块产生、打包、广播出去的过程中只包含了accepted_transaction、applied_transaction、irreversible_block、accepted_block_header、accepted_block,需要注意的是,这里的五个过程是有先后顺序的。

image.png

在on_ irreversible中广播区块的是否可逆信息

image.png

在commit_block中广播区块的相关信息。

image.png

最终在net_plugin里面接收到的消息如下打印:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • 转载声明:本文来自微信公众号:火龙果园长,仅供学习交流,禁止用于商业用途,转载需关注公众号取得文章作者同意。 写在...
    火龙果园长阅读 7,318评论 0 18
  • EOS.IO技术白皮书链接:EOSIO/Documentation 摘要 EOS.IO 软件引入一种新的区块链架构...
    yuyangray阅读 1,667评论 0 4
  • 每天三件事3/100天 1、跑步三公里,拉伸压胫 2、锁定客户当签当回,完成业绩90800,业绩完成还差一单突破战...
    钱程浩瀚阅读 92评论 0 0
  • 只要停下来,就会想起你,想写点东西,脑袋里面也是你,出去买东西,店铺里面的情歌也会让我呆滞。你在的时候,希望自己可...
    九木厘阅读 500评论 0 0
  • 文丨黄铭峰 国内学生职业意识仍不容乐观 笔者在某网络上查到一则信息:国内某所着名高校曾在大一新生中抽出了1400人...
    通识智库阅读 546评论 0 1