在主网上玩耍的小伙伴们肯定遇到过区块回滚导致自己的交易没有上链。这种情况让有些人误以为区块回滚会丢弃交易。 其实区块回滚并不是导致交易没上链的主要原因, 主要原因是交易过期了才导致交易被丢弃。
流程描述:
每个交易是会被广播到全网每个节点上面的( ps: 当然传播过程中过期的话,当我没说哈 ),假如出块节点 A 打包了 trx a, 但此时出块节点 B 没接受到 A 的打包块,他也开始打包了,那么他也包含了该 trx a 并会将他打包( ps: 当然也有例外情况,那就是 出块节点 B 接收到 trx a 时,他就过期了,所以还没打包就丢弃他,或者还没传递到出块节点 B, 但会抵达下个节点 C D E, 但情况相似,就不另外说明了 )。但如果 a 的过期时间设置过短,导致出块节点 B 打包时发现他过期了,就会丢弃他。 这便是交易没上链的原因。
源码解析:
我们来看看区块生产时是如何丢弃过期交易的。区块生产的流程 区块的分叉处理可以看下我之前的文章。这里生产区块以及回滚的细节就不赘述了。
区块打包时,会将 pending block 里已经执行成功了的 trx 另外存起来, 并初始化 pending block。 等到打包的时候会再去执行一次这些 trx , 咦,为什么要重新执行一遍浪费资源,因为这些 trx 都是在区块打包之前执行的,鬼知道过期了没有 =,=。
以下是交易被残忍丢弃的过程:
// controller.cpp
// 将 pending block 的 trx 保存到 unapplied_transaction
void abort_block() {
if( pending ) {
if ( read_mode == db_read_mode::SPECULATIVE ) {
for( const auto& t : pending->_pending_block_state->trxs )
unapplied_transactions[t->signed_id] = t;
}
pending.reset();
}
}
// producer_plugin.cpp
producer_plugin_impl::start_block_result producer_plugin_impl::start_block(bool &last_block) {
// …
try {
// ...
// 将 pending block 里面的 trx 保存了 unapplied_transaction
chain.abort_block();
// 初始化 pending block
chain.start_block(block_time, blocks_to_confirm);
} FC_LOG_AND_DROP();
const auto& pbs = chain.pending_block_state();
if (pbs) {
// ...
// _persistent_transactions 是指通过该节点的 http 端口 push_transaction 推送过来的 trx
// remove all persisted transactions that have now expired
auto& persisted_by_id = _persistent_transactions.get<by_id>();
auto& persisted_by_expiry = _persistent_transactions.get<by_expiry>();
if (!persisted_by_expiry.empty()) {
int num_expired_persistent = 0;
int orig_count = _persistent_transactions.size();
// 丢弃过期交易
while(!persisted_by_expiry.empty() && persisted_by_expiry.begin()->expiry <= pbs->header.timestamp.to_time_point()) {
auto const& txid = persisted_by_expiry.begin()->trx_id;
if (_pending_block_mode == pending_block_mode::producing) {
fc_dlog(_trx_trace_log, "[TRX_TRACE] Block ${block_num} for producer ${prod} is EXPIRING PERSISTED tx: ${txid}",
("block_num", chain.head_block_num() + 1)
("prod", chain.pending_block_state()->header.producer)
("txid", txid));
} else {
fc_dlog(_trx_trace_log, "[TRX_TRACE] Speculative execution is EXPIRING PERSISTED tx: ${txid}",
("txid", txid));
}
persisted_by_expiry.erase(persisted_by_expiry.begin());
num_expired_persistent++;
}
fc_dlog(_log, "Processed ${n} persisted transactions, Expired ${expired}",
("n", orig_count)
("expired", num_expired_persistent));
}
try {
size_t orig_pending_txn_size = _pending_incoming_transactions.size();
// Processing unapplied transactions...
// 当节点不是 出块节点并且 也没有通过该节点推送的 trx, 则 unapplied_transaction 无意义
// 因为你不需要打包区块, 也没有 trx 需要广播出去。
if (_producers.empty() && persisted_by_id.empty()) {
// if this node can never produce and has no persisted transactions,
// there is no need for unapplied transactions they can be dropped
chain.drop_all_unapplied_transactions();
} else {
std::vector<transaction_metadata_ptr> apply_trxs;
{ // derive appliable transactions from unapplied_transactions and drop droppable transactions
auto unapplied_trxs = chain.get_unapplied_transactions();
apply_trxs.reserve(unapplied_trxs.size());
auto calculate_transaction_category = [&](const transaction_metadata_ptr& trx) {
if (trx->packed_trx.expiration() < pbs->header.timestamp.to_time_point()) {
return tx_category::EXPIRED;
} else if (persisted_by_id.find(trx->id) != persisted_by_id.end()) {
return tx_category::PERSISTED;
} else {
return tx_category::UNEXPIRED_UNPERSISTED;
}
};
// 将没过期的放进 apply_trxs, 过期的丢弃掉。
for (auto& trx: unapplied_trxs) {
auto category = calculate_transaction_category(trx);
if (category == tx_category::EXPIRED || (category == tx_category::UNEXPIRED_UNPERSISTED && _producers.empty())) {
if (!_producers.empty()) {
fc_dlog(_trx_trace_log, "[TRX_TRACE] Node with producers configured is dropping an EXPIRED transaction that was PREVIOUSLY ACCEPTED : ${txid}",
("txid", trx->id));
}
chain.drop_unapplied_transaction(trx);
} else if (category == tx_category::PERSISTED || (category == tx_category::UNEXPIRED_UNPERSISTED && _pending_block_mode == pending_block_mode::producing)) {
apply_trxs.emplace_back(std::move(trx));
}
}
}
if (!apply_trxs.empty()) {
// 执行 trx, 成功的 emplace_back 进 pending block
}
}
// ...
// 执行 deffered transaction
// 和执行 初始化 pending block 时推送进来的 transcation ( 因为初始化时,pending block 不能存 trx, 所以先另外存起来)
}
return start_block_result::failed;
}
总结:
回滚并不会丢弃 trx, 只会导致 trx 延后打包,以致于 trx 可能过期被丢弃。
设置过期时间时,时间跨度应该足够 2 个 BP 出完块,这样即使 B 没接收到 A 的区块,但 trx 不会因为过期而被 B 丢弃,当然还要大致估算你的 trx 广播到出块节点的时间。
有任何疑问或者想交流的朋友可以加 EOS LIVE 小助手,备注 eos开发者拉您进 EOS LIVE DAPP 开发者社区微信群哦。
图片描述