fix(sequencer): resubmit orphaned tx which comes only after final tip

This commit is contained in:
Daniil Polyakov 2026-07-31 19:54:58 +03:00
parent 9d9b75d42f
commit 3d009acb9c
2 changed files with 78 additions and 6 deletions

View File

@ -1162,12 +1162,6 @@ fn apply_follow_update(
let (resubmit_txs, outcome) = {
let mut chain = chain.lock().expect("chain state mutex poisoned");
// User txs of orphaned blocks, returned to the mempool below.
let resubmit_txs: Vec<LeeTransaction> = orphaned
.iter()
.flat_map(|(_, block)| resubmittable_txs(block))
.collect();
// Outcomes align with `adopted`.
let outcomes = chain.apply_channel_update(&orphaned, &adopted);
let mut to_persist: Vec<(&Block, bool)> = adopted
@ -1201,6 +1195,22 @@ fn apply_follow_update(
AcceptOutcome::Parked(_) | AcceptOutcome::RetryableFailure(_) => {}
}
}
// User txs of orphaned blocks, returned to the mempool below.
//
// Computed after the finalized tier has advanced, and only for blocks
// above it: the zone-sdk reports a block as orphaned once LIB pruning
// drops its inscription from the channel lineage, so every block of
// ours is orphaned a poll or two after it finalizes. Those transactions
// are irreversibly included, and returning them to the mempool puts
// them back in every block we produce from then on.
let final_height = chain.final_tip().map(|tip| tip.block_id);
let resubmit_txs: Vec<LeeTransaction> = orphaned
.iter()
.filter(|(_, block)| final_height.is_none_or(|id| block.header.block_id > id))
.flat_map(|(_, block)| resubmittable_txs(block))
.collect();
// Snapshot the advanced final tier so a restart re-anchors on it.
let final_meta = final_advanced.then(|| {
let tip = chain.final_tip().expect("advanced final tier has a tip");

View File

@ -2172,6 +2172,68 @@ async fn follow_orphan_reverts_head_and_requeues_user_txs() {
);
}
#[tokio::test]
async fn follow_orphan_of_a_finalized_block_requeues_nothing() {
// The zone-sdk reports a block as orphaned once LIB pruning drops its
// inscription from the channel lineage, which happens a poll or two after
// every block of ours finalizes. Its transactions are irreversibly
// included, so requeueing them would put them back in every block we
// produce from then on.
let config = setup_sequencer_config();
let (mut sequencer, mempool_handle) =
SequencerCoreWithMockClients::start_from_config(config).await;
let acc1 = initial_public_user_accounts()[0].account_id;
let acc2 = initial_public_user_accounts()[1].account_id;
let tx = common::test_utils::create_transaction_native_token_transfer(
acc1,
0,
acc2,
10,
&create_signing_key_for_account1(),
);
mempool_handle
.push((TransactionOrigin::User, tx))
.await
.unwrap();
sequencer.produce_new_block().await.unwrap();
let block2 = sequencer.store.get_block_at_id(2).unwrap().unwrap();
apply_follow_update(
&sequencer.store.dbio(),
&sequencer.chain(),
&mempool_handle,
FollowUpdate {
finalized: vec![(MsgId::from(block2.header.hash.0), block2.clone())],
..empty_follow_update()
},
);
apply_follow_update(
&sequencer.store.dbio(),
&sequencer.chain(),
&mempool_handle,
FollowUpdate {
orphaned: vec![(MsgId::from(block2.header.hash.0), block2)],
..empty_follow_update()
},
);
assert_eq!(
sequencer.chain_height(),
2,
"an irreversible block cannot be reverted"
);
assert_eq!(
sequencer.with_state(|s| s.get_account_by_id(acc2).balance),
20010,
"the finalized transfer stands"
);
assert!(
sequencer.mempool.pop().is_none(),
"a transaction that is already irreversible must not be requeued"
);
}
#[tokio::test]
async fn follow_finalized_own_block_moves_final_tier_and_marks_store() {
let config = setup_sequencer_config();