EOS 原始碼解析 股權證明的交易(TaPos) 的作用

firesWu發表於2018-11-15

在每個 trx 交易中,我們都會看到 ref_block_num 和 ref_block_prefix, 這2個引數有什麼作用呢。

先講下它的大致作用,再來對程式碼進行分析。

這是白皮書對這2個引數的作用描述

Transaction as Proof of Stake (TaPoS)
The EOS.IO software requires every transaction to include part of the hash of a recent block header. This hash serves two purposes:

1. prevents a replay of a transaction on forks that do not include the referenced block; and
2. signals the network that a particular user and their stake are on a specific fork.
Over time all users end up directly confirming the blockchain which makes it difficult to forge counterfeit chains as the counterfeit would not be able to migrate transactions from the legitimate chain.
1. 他是用來防止有不包含區塊引用的交易被重放到某個分叉上, 這樣能避免不是該分叉的區塊被新增到該分叉。
2. 告訴使用者該塊是在哪個分支上面。
複製程式碼

這樣做有什麼作用呢?

假設現在有2個使用者 A 和 B, B 叫 A 說你轉 2 個 EOS 給我, 我就送你 100 個 LIVE,A 說好啊。 然後 A 就轉 2 個 EOS 給 B 了, 這個時候 A 的區塊 a 還不是不可逆狀態, 如果此時 B 轉給 A 100 個 LIVE, 要是 區塊 a 被回滾掉了怎麼辦,那麼 B 就白白給了 A 100 個 LIVE 了。 這時候 ref-block 的作用就體現了,如果區塊 a 被回滾了,那麼 B 轉給 A 100 個 LIVE 的區塊 b 也會被丟棄掉。 所以 當區塊 b ref-block 是 區塊 a 的時候,只有 區塊 a 被成功打包了, 區塊 b 才會被成功打包。

所以很顯然, 這兩個引數是為了讓鏈更穩固,也讓使用者交易更安全。

先看下 transaction_header 對這兩個欄位的描述。

struct transaction_header {
  // ...
  // 可以指定 head_block_num - 0xffff ~ head_block_num 之間的塊。
  uint16_t               ref_block_num       = 0U; ///< specifies a block num in the last 2^16 blocks.
 // block_id 的按 32 bits分割的第二個部分,也就是 block_id._hash[1];
  uint32_t               ref_block_prefix    = 0UL; ///< specifies the lower 32 bits of the blockid at 
 // ...
};
複製程式碼

再來看下該引數如何被驗證。

 // 在 trx 初始化的時候便回去驗證
void transaction_context::init_for_input_trx( uint64_t packed_trx_unprunable_size,
                                             uint64_t packed_trx_prunable_size,
                                             uint32_t num_signatures,
                                             bool skip_recording )
{
    //...
    if (!control.skip_trx_checks()) {
         control.validate_expiration(trx);
         control.validate_tapos(trx);
         control.validate_referenced_accounts(trx);
      }
    //...
}

void controller::validate_tapos( const transaction& trx )const { try {
   const auto& tapos_block_summary = db().get<block_summary_object>((uint16_t)trx.ref_block_num);

   //Verify TaPoS block summary has correct ID prefix, and that this block's time is not past the expiration
   EOS_ASSERT(trx.verify_reference_block(tapos_block_summary.block_id), invalid_ref_block_exception,
              "Transaction's reference block did not match. Is this transaction from a different fork?",
              ("tapos_summary", tapos_block_summary));
} FC_CAPTURE_AND_RETHROW() }

bool transaction_header::verify_reference_block( const block_id_type& reference_block )const {
   return ref_block_num    == (decltype(ref_block_num))fc::endian_reverse_u32(reference_block._hash[0]) &&
          ref_block_prefix == (decltype(ref_block_prefix))reference_block._hash[1];
}
複製程式碼

從 block_summary_object 獲取的 block 資料拿來跟 ref-block 的, 很奇怪為什麼不直接用 get_block 那種方式取 block 的資訊呢? 這樣不用維護多一個多索引容器,而且還能獲取全部的 block 。 來看看 block_summary_object 是如何建立和維護的。

// libraries/chain/include/eosio/chain/block_summary_object.hpp
class block_summary_object : public chainbase::object<block_summary_object_type, block_summary_object>
   {
         OBJECT_CTOR(block_summary_object)

         id_type        id;
         block_id_type  block_id;
   };

   struct by_block_id;
   using block_summary_multi_index = chainbase::shared_multi_index_container<
      block_summary_object,
      indexed_by<
         ordered_unique<tag<by_id>, BOOST_MULTI_INDEX_MEMBER(block_summary_object, block_summary_object::id_type, id)>
   //      ordered_unique<tag<by_block_id>, BOOST_MULTI_INDEX_MEMBER(block_summary_object, block_id_type, block_id)>
      >
   >;



// 建立了 id 從 0 ~ 65535 的資料
void contoller_impl::initialize_database() {
  // Initialize block summary index
  for (int i = 0; i < 0x10000; i++)
     db.create<block_summary_object>([&](block_summary_object&) {});

  // ...
}

// 每次新增新的區塊的時候都回去更新 block_summary_object 的 索引表
void contoller_impl::finalize_block()
{
  // ...

  auto p = pending->_pending_block_state;
  p->id = p->header.id();

  create_block_summary(p->id);

} FC_CAPTURE_AND_RETHROW() }

void create_block_summary(const block_id_type& id) {
  auto block_num = block_header::num_from_id(id);
  // 從這裡可以看出 block_summary_object 的 id 永遠都是 0 ~ 65535。也就是說它只維護 head_block_num - 0xffff ~ head_block_num 的塊, 你 ref-block 只能是這個區間的塊, 如果 ref 更早的 block 就會驗證出錯。
  auto sid = block_num & 0xffff;
  db.modify( db.get<block_summary_object,by_id>(sid), [&](block_summary_object& bso ) {
      bso.block_id = id;
  });
}
複製程式碼

cleos 在 push transaction 的時候預設的 ref-block 是取 last_irreversible_block ,當head_block_num 跟 lib_num 相差超出 0xffff 個塊的時候就會出現該錯誤:

Error 3040007: Invalid Reference Block Ensure that the reference block exist in the blockchain! Error Details: Transaction's reference block did not match. Is this transaction from a different fork?

如果你的私鏈出現問題,檢查你鏈上有沒 2/3 個 BP 在出塊,如果沒有則是因為沒確認塊,導致 head_block 和 lib 之間超過了 0xffff 個塊而導致該錯誤。

結論: ref-block 的主要作用從白皮書可以看出,它是為了建立一條難以造假的鏈, 因為其他鏈違法從 合法鏈鏈直接遷移交易,只能新增交易。每個 block 都會 ref-block 前面的資料, 你也無法直接 ref-block 的早期的塊,因為只能 ref-block 只能是從 head_block_num - 0xffff ~ head_block_num, 像比特幣,只要你算力足夠,你從第一個塊重新建造一條鏈都可以。 並且他告訴使用者當前交易是在哪個分叉上, 這樣使用者可以根據交易需要在哪條分叉上成功來指定分叉, 也就是我們上面舉的例子。

原文: eos.live/detail/1709…

相關文章