EOS智能合约开发(二十一)从EOS共识机制TaPos分析抛出的异常 Error 3040007

今天通过cleos发送一笔Trx的时候,返回错误,错误如下:

Error 3040007: Invalid Reference Block
Ensure that the reference block exist in the blockchain!

我们就通过源码分析,对这个问题抽丝剥茧。
通过查询EOS错误编码,有如下描述:
3040007 Invalid Reference Block
引用块无效或不匹配,节点间不同有关
https://github.com/EOSIO/eos/issues/4659
我们分析一下,我们要通过cleos发送一笔交易的时候,需要确认两个参数,分别是:
1、ref_block_num ;
2、ref_block_prefix。
这两个参数有什么作用,我们可以查询EOS白皮书描述:

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.通知网络特定用户及其利益在特定分支上。
随着时间的推移,所有用户最终都会直接确认区块链,这使得伪造链很难伪造,因为伪造品无法从合法链中迁移交易。

通过上面描述,我们需要分析TaPos股权证明来分析这样做的目的。
我们简单列举一个例子分析一下:
假设有两个用户A,B,B让A给B10个EOS,A—>B (10 EOS),然后B给A20个USDT,B—>A(20 USDT)。这个时候A已经转了10个EOS给了B在block_a上,那么这个block_a还处于可以回滚的区块中。如果B给A了20USDT,这个时候,block_a区块回滚了怎么办,那么B就白白给了A20个USDT,这时候 ref-block 的作用就体现了,如果区块 block_a 被回滚了,那么 B 转给 A 20 个 USDT 的区块 block_b 也会被丢弃掉。 所以 当区块 block_b ref-block 是 区块 block_a 的时候,只有 区块 block_a 被成功打包了, 区块 block_b 才会被成功打包。
所以很显然, 这两个参数是为了让链更稳固,也让用户交易更安全。
我们先来看看这个两个参数的定义,这两个参数定义在struct transaction_header结构中:

//文件路径 eos\libraries\chain\include\eosio\chain\transaction.hpp
 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.
      uint32_t               ref_block_prefix    = 0UL; ///< specifies the lower 32 bits of the blockid at get_ref_blocknum
      ...

再来看下该参数如何被验证。

 // 在 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!

到这里,我们就分析到这错误的原因。

如果你的私链出现问题,检查你链上有没 2/3 个 BP 在出块,如果没有则是因为没确认块,导致 head_block 和 lib 之间超过了 0xffff 个块而导致该错误。

结论: ref-block 通过白皮书分析,它是为了建立一条难以造假的链,实现TaPos共识机制 ,当一条链分叉后,分叉的链无法从合法链直接迁移交易,只能添加交易。每个 block 都会 ref-block 前面的数据, 你也无法直接 ref-block 的早期的块,因为只能 ref-block 只能是从 head_block_num - 0xffff ~ head_block_num, 并且他告诉用户当前交易是在哪个分叉上, 这样用户可以根据交易需要在哪条分叉上成功来指定分叉, 也就是我们上面举的例子。
2019年2月14日整理于深圳

猜你喜欢

转载自blog.csdn.net/jambeau/article/details/87226733