diff --git a/lez/sequencer/core/src/lib.rs b/lez/sequencer/core/src/lib.rs index 4b15b8f2..34ca24a5 100644 --- a/lez/sequencer/core/src/lib.rs +++ b/lez/sequencer/core/src/lib.rs @@ -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 = 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 = 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"); diff --git a/lez/sequencer/core/src/tests.rs b/lez/sequencer/core/src/tests.rs index d5a18ac6..44778a22 100644 --- a/lez/sequencer/core/src/tests.rs +++ b/lez/sequencer/core/src/tests.rs @@ -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();