EOS共識總結

1 基本概念

1 eos 每500毫秒出一個塊,每個生產節點連續出12個塊,然後切換到下一節點生產。

2 eos需要兩輪共識

  新生產/接收的塊會放入內存塊分叉數據庫fork_db中,等待共識。

單節點不可逆塊:我們把完成第一輪共識的塊,叫做單節點不可逆塊。
全網不可逆塊: 完成第二輪共識的塊,叫全網不可逆塊。

  完成兩輪共識的全網不可逆塊,纔是我們常說的真正意義上的不可逆塊,會從fork_db中移出寫入block_log,實現落塊。

3 共識相關關鍵變量

​ 下面是eos中定義的block_header_state數據結構,共識用到的大多數變量值都保存在這裏:


 struct block_header_state {
       uint32_t    block_num = 0;//塊號

       // 1 第一輪  , 記錄每個塊的待確認數,
      vector<uint8_t>    confirm_count;    // [1,1,1,...2,2,2,...3,3,3] 

      // 2 第二輪,   經過首輪確認,得到的候選不可逆,單節點不可逆最大塊號
      uint32_t  dpos_proposed_irreversible_blocknum = 0;
      // 記錄每個生產者的最後不可逆塊序號
      flat_map<account_name,uint32_t>   producer_to_last_implied_irb;

      // 3 全網不可逆塊號
      uint32_t  dpos_irreversible_blocknum = 0;
}

  //1 第一輪   ,水印值,記錄每個生產者生產的最後一個塊的塊號。
  std::map<chain::account_name, uint32_t>   _producer_watermarks;
  • confirm_count:vector數組,第一輪共識使用,裏面保存着未完成第一輪共識的塊的待確認數。

  • dpos_proposed_irreversible_blocknum :單節點不可逆塊號,第二輪共識使用,記錄經過第一輪共識得到的單節點不可逆塊號。

  • producer_to_last_implied_irb:map對象,存放生產者及單節點不可逆塊號的鍵值對,第二輪共識用。

  • dpos_irreversible_blocknum:完成第二輪共識的最新全網不可逆塊塊號,根據該塊號實現落塊。

    另外生產者水印值也是一個用於共識的關鍵變量值,保存在生產者插件中:

    //1 第一輪   生產者水印值,記錄每個生產者生產的最後一個塊的塊號。
    std::map<chain::account_name, uint32_t>   _producer_watermarks;
  • _producer_watermarks: map對象,生產節點水印值,第一輪共識使用,保存每個生產節點最後生產的塊號。

2 共識原理舉例

第一輪共識

  • 生產節點會對它未確認的塊進行確認共識,未確認塊數=當前鏈頭塊號-它最後出塊的塊號。

  • 節點出塊時,會按拜占庭將軍算法(當前生產節點數*2/3+1)計算該塊需要幾個節點共識,並把該值放入待共識數組confirm_count[]的最後。
  • 生產節點出塊時,除了會共識之前未確認過的塊,也會對本次出的塊進行本節點確認共識。

  • 只有生產節點出塊時,纔會對之前節點出的塊進行確認共識,節點收到塊時,並不做對收到的塊進行確認共識。

    以3個生產節點每輪出2個塊爲例,假設此時輪到節點1出第7塊:

EOS共識總結

1 計算節點1未確認塊數:目前鏈頭塊塊號6 - 節點水印值2(節點1生產的最後一個塊)=4;

​ 即生產7號塊時,前面有4個塊待節點1確認。

2 計算7號塊需要被幾個節點確認: 3(節點數)* 2/3 +1 = 3,將3放入confirm_count[]最後;

3 第一輪共識計算:實際就是對confirm_count[]數組中保存的待確認數進行減1計算。

​ 塊待確認數組confirm_count[] 在第一輪共識中的變動情況展示如下:
EOS共識總結

  1. 生產7塊前,裏面有4個待確認塊。
  2. 先將7塊的待確認數3插confirm_count的最後。
  3. 循環從confirm_count[]尾部對其中的值執行減1操作。當有confirm_count[i]==0成立,停止循環。i位置對應的塊號,即此次共識得到的單節點不可逆塊號,此時4號塊成爲單節點不可逆塊,它之前的塊不用計算,都自動成爲單節點不可逆塊。
  4. 移動confirm_count[],清除4號塊之前所有塊。

第一輪共識完成,得到dpos_proposed_irreversible_blocknum=4。

第二輪共識

​ 這裏主要用到producer_to_last_implied_irb,它是一個map容器,裏面放的是生產節點的節點名及該節點對應的單點不可逆塊號,此處的單點不可逆塊號就是該節點在執行第一輪共識計算時得到的值。只有出塊節點的單節單節點不可逆值纔會更新。

剛纔生產7號塊時計算得到節點1的當前不可逆塊號是4,可推得此時節點3對應的值應該是2,節點2對應值0。當節點2生產第9個塊時,節點2第一輪共識得到的單節點不可逆塊號6, 其它節點不變,如下圖所示:
EOS共識總結

eos的第二輪共識算法,會將producer_to_last_implied_irb中的塊號排序,從小到大取1/3位置的塊,成爲最終不可逆塊。

  • 7 號塊生產時,取最小1/3的塊,塊號爲0,此時沒有塊成爲全網不可逆塊。

  • 9號塊生產時,取最小1/3的塊,塊號爲2,此時2號之前的塊都成爲全網不可逆塊。

    第二輪共識結束,7號塊生產時,全網不可逆塊dpos_irreversible_blocknum = 0;

    9號塊生產時,全網不可逆塊dpos_irreversible_blocknum = 2;

    3 eos塊共識流程

EOS共識總結

​ 下圖爲根據eos塊生產流程畫出的塊共識流程,下面分別從生產節點作爲生產者出塊和作爲驗證節點收到兩種情況,結合代碼分別對兩輪共識流程展開進行分析。

​ 節點作爲驗證節點收到塊,會調用conntroller_impl::apply_block()函數,處理塊中數據,最終落塊。圖中紅色底框的內容是第一輪共識用到的函數或變量,橙色底框則是第二輪共識用到的。

3.1 出塊節點共識

3.1.1 第一輪共識

​ 節點循環調用schedule_production_loop()函數進行生產循環,函數中會調用start_block()判斷是否該本節點出塊。

  1. 計算本塊需要確認的塊數:blocks_to_confirm = 鏈頭塊號 - 本節點水印值;
producer_plugin_impl::start_block_result producer_plugin_impl::start_block() {
    //計算出塊者
   const auto& scheduled_producer = hbs->get_scheduled_producer(block_time);
    //獲取該出塊者已出最後一個塊的塊號
    auto currrent_watermark_itr = 
             _producer_watermarks.find(scheduled_producer.producer_name);
  if (currrent_watermark_itr != _producer_watermarks.end()) {
     //水印值 節點生產的最後塊號 
      auto watermark = currrent_watermark_itr->second;
      if (watermark < hbs->block_num) {
          //待確認塊數=鏈頭塊號-本節點最後生產的塊號
      blocks_to_confirm = hbs->block_num - watermark;
   }
  }
     ...

   chain.start_block(block_time, blocks_to_confirm);
}

  1. 輪到本節點出塊,將出塊時間和blocks_to_confirm作爲參數傳入controller_impl::start_block()函數,啓動出塊:
void start_block(block_timestamp_type when, uint16_t confirm_block_count,.. )
{
   ...
    //構造block_state類型
    pending->_pending_block_state = std::make_shared<block_state>( *head, when );
   ...
    // 執行第一輪共識 
    pending->_pending_block_state->set_confirmed(confirm_block_count);

    //生產者切換
    auto was_pending_promoted = 
                    pending->_pending_block_state->maybe_promote_pending();
    ...
}
  • 首先構造block_state數據,存塊的數據信息,block_state是block_header_state的子類,構造時會先調用block_header_state::generate_next()函數,構造block_header_state:

    block_header_state::generate_next( block_timestamp_type when )const {
    
    block_header_state result; 
    //根據生產者數量計算生產塊的待確認數required_confs
    auto num_active_producers = active_schedule.producers.size();
    uint32_t required_confs = (uint32_t)(num_active_producers * 2 / 3) + 1; 
     ...  
    //required_confs插入confirm_count[]最後
    result.confirm_count.back() = (uint8_t)required_confs;
    
    //producer_to_last_implied_irb數組構建
    result.dpos_proposed_irreversible_blocknum   = 
                               dpos_proposed_irreversible_blocknum;
    result.producer_to_last_implied_irb[prokey.producer_name] = 
                               result.dpos_proposed_irreversible_blocknum;
    
    //第二輪確認,從建議不可逆塊數組計算最終不可逆塊號。
    result.dpos_irreversible_blocknum = result.calc_dpos_last_irreversible(); 
    }
  • 計算本次出塊的待確認數,放入待確認數組confirm_count[]的最後。
  • 更新producer_to_last_implied_irb中本節點的單節點不可逆塊號。
  • 根據producer_to_last_implied_irb的值進行第二輪共識計算,具體算法後面分析。

generate_next()中已將本次生產塊的待確認數加入了confirm_count[]中,下面開始第一輪共識計算:

 void block_header_state::set_confirmed( uint16_t num_prev_blocks ) {
     //confirm_count[]   [1,1,1,...2,2,2,...3,3,3]    記錄所有待確認塊
     int32_t i = (int32_t)(confirm_count.size() - 1);

     // 本次確認塊數,傳入確認數+1 把本次出的塊也確認了
     uint32_t blocks_to_confirm = num_prev_blocks + 1; 

     //從confirm_count最後放入的最新塊開始確認,逐個減一
     while( i >= 0 && blocks_to_confirm ) {
           --confirm_count[i];
           //減完後==0,該塊被確認次數滿足該塊設定的待確認數
           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;

              //該塊就是confirm_count最後一個,confirm_count重置
              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;
        }
   }
  • 參數num_prev_blocks是鏈頭塊號減水印值得到的,因爲對本次生產的塊也要進行確認,所以,實際確認塊數會加1。
  • 從後到前對confirm_count[]減1,相當於節點確認了該塊。
  • 第一個使confirm_count[i]==0成立處的塊號,就是經過第一輪共識得到的單節點不可逆塊號。

出塊時間到,節點會調用produce_block()函數進行一些收尾工作,比如計算默克爾根,塊簽名等,生產者水印值也是在這裏跟新的:

void producer_plugin_impl::produce_block() { 
 ...
   chain.finalize_block();
   chain.commit_block(); 
  ...
   //記錄生產者水印值,記錄當前生產者生產的最新塊塊號
   _producer_watermarks[new_bs->header.producer] = chain.head_block_num();
}
  • chain.commit_block()函數中會將此次生產的塊加入fork_db庫中,且將鏈頭塊跟新爲此次生產的塊。
  • chain.head_block_num()函數會得到當前鏈頭塊塊號。
3.1.2 第二輪共識

前面已經介紹了,在generate_next()中會調用calc_dpos_last_irreversible()函數進行第二輪共識計算,下面我來看一下具體的計算過程:

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);
      }
      if( blocknums.size() == 0 ) return 0;
      std::sort( blocknums.begin(), blocknums.end() );
      return blocknums[ (blocknums.size()-1) / 3 ];
   }

*  new_producer_to_last_implied_irb 是一個map容器結構,裏面放的是<生產者,節點最後單節點不可逆號> 對,容器的到小等於active_schedule裏生產節點的數量。
* 構造了blocknums[]數組,將new_producer_to_last_implied_irb中的單節點不可逆號複製到裏面,所以這裏blocknums的大小也等於實際生產節點的大小。
* 將blocknums按從小到大的順序排序,取其(blocknums.size()-1) / 3位置的塊號,成爲全網不可逆塊號。
 
 以21個生產節點爲例,(blocknums.size()-1) / 3 =6,blocknums[6]位置的塊號,索引從0開始,即從塊號從大到小排第15個節點的塊號,由拜占庭共識算法21×2/ 3+1=15,可以看出,此處計算雖然是取的1/ 3處的塊號,但它是逆序取的,所以第二輪共識實際也是一次拜占庭共識。

這裏還有一個注意點,在進行生產節點切換有新節點加入時,producer_to_last_implied_irb中新加入節點的單節點不可逆塊號會使用當前鏈頭號進行初始化。

flat_map<account_name,uint32_t> new_producer_to_last_implied_irb;
  for( const auto& pro : active_schedule.producers ) {
    auto existing = producer_to_last_implied_irb.find( pro.producer_name );
     if( existing != producer_to_last_implied_irb.end() ) {
      new_producer_to_last_implied_irb[pro.producer_name] = existing->second;
     } else {
        new_producer_to_last_implied_irb[pro.producer_name] = 
            dpos_irreversible_blocknum;
 }

3.2 驗證節點共識

  驗證節點收到塊,會根據塊頭中的confirmed變量,即該塊已確認塊數,在本節點重做該塊的共識過程,以保證節點的狀態一直性。生產節點收到塊,將塊放入fork_db時,會調用apply_block()函數,進行塊驗證,驗證過程會調用controller_impl::start_block()函數,構造塊的驗證環境。

    void apply_block( const signed_block_ptr& b, controller::block_status s ) 
    {
         ......
        auto producer_block_id = b->id();
       //驗證塊前,需構造pendding塊環境
       start_block( b->timestamp, b->confirmed, s , producer_block_id);
       //驗證交易
       ......
    }

這個start_block()和出塊時調用的start_block()是同一函數,所以前面進行的兩輪共識計算,這裏也會做一遍。只是這次做是爲了和出塊節點保持狀態一直,做的是實際出塊者的共識過程,並不代表本節點共識了相應塊。更新的不可逆塊號也是實際出塊者的值。

4 eos塊落差336

eos中是21個節點出塊,每個節點連續出12個塊,切換下個生產者。每個塊需要:21*2/3 +1 =15個節點共識。

​ 下圖爲eos節點出塊序號表展示,節點1第一輪出1-12塊,接着節點2出11-24塊,圖中pirb爲單節點不可逆塊號,irb爲全網不可逆塊號。
EOS共識總結

  • 節點1出1 - 12塊,需要包括自己在內的15個節點確認,則到節點15出第169塊時,1-12塊變成單節點不可逆塊,節點15的pirb=12;
  • 以此類推,16號節點生產時,24號塊成爲單節點不可逆,... ,節點8第二輪生產337塊時,170塊成爲單節點不可逆。按第二輪共識,把21個節點的單節點不可逆號放入blocknums[21]數組排序,blocknums裏面的數據就是圖中pirb桔色底色的值。取blocknums[(21-1)/3] 的塊號成爲單節點不可逆號。 i=6處的塊號12成爲全網不可逆塊。
  • 337塊生產前,系統中已經生產了336個塊,但沒有一個塊成爲不可逆落塊,直到337塊生產開始,系統中才首次出現全網不可逆塊落塊。得到eos系統最大塊落差336。

5 鏈接

星河公鏈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章