在每个 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, 像比特币,只要你算力足够,你从第一个块重新建造一条链都可以。 并且他告诉用户当前交易是在哪个分叉上, 这样用户可以根据交易需要在哪条分叉上成功来指定分叉, 也就是我们上面举的例子。