Repository: paradigmxyz/reth
Pull request #15931: fix: park descendant when delete a pending tx
Author: login: int88
Labels: name: C-bug
description: An unexpected or incorrect behavior
name: A-tx-pool
description: Related to the transaction mempool
Created: 2025-04-25T14:34:46Z | Merged: 2025-04-26T06:54:32Z

fixes: #15911



Diff:
diff --git a/crates/transaction-pool/Cargo.toml b/crates/transaction-pool/Cargo.toml
index 71424696928..4b24a347e5e 100644
--- a/crates/transaction-pool/Cargo.toml
+++ b/crates/transaction-pool/Cargo.toml
@@ -34,7 +34,7 @@ alloy-consensus = { workspace = true, features = ["kzg"] }
 # async/futures
 futures-util.workspace = true
 parking_lot.workspace = true
-tokio = { workspace = true, default-features = false, features = ["sync"] }
+tokio = { workspace = true, features = ["sync"] }
 tokio-stream.workspace = true
 
 # metrics
@@ -70,6 +70,7 @@ criterion.workspace = true
 assert_matches.workspace = true
 tempfile.workspace = true
 serde_json.workspace = true
+tokio = { workspace = true, features = ["rt-multi-thread"] }
 
 [features]
 serde = [
diff --git a/crates/transaction-pool/src/pool/mod.rs b/crates/transaction-pool/src/pool/mod.rs
index 5f5763ce7ab..aee6badb5d4 100644
--- a/crates/transaction-pool/src/pool/mod.rs
+++ b/crates/transaction-pool/src/pool/mod.rs
@@ -726,6 +726,9 @@ where
     }
 
     /// Removes and returns all matching transactions from the pool.
+    ///
+    /// This behaves as if the transactions got discarded (_not_ mined), effectively introducing a
+    /// nonce gap for the given transactions.
     pub fn remove_transactions(
         &self,
         hashes: Vec<TxHash>,
diff --git a/crates/transaction-pool/src/pool/txpool.rs b/crates/transaction-pool/src/pool/txpool.rs
index 6ffa022cf0b..f3ac4111baf 100644
--- a/crates/transaction-pool/src/pool/txpool.rs
+++ b/crates/transaction-pool/src/pool/txpool.rs
@@ -819,14 +819,20 @@ impl<T: TransactionOrdering> TxPool<T> {
         self.remove_from_subpool(pool, tx.id())
     }
 
-    /// Remove the transaction from the entire pool via its hash.
+    /// Remove the transaction from the entire pool via its hash. This includes the total set of
+    /// transactions and the subpool it currently resides in.
     ///
-    /// This includes the total set of transactions and the subpool it currently resides in.
+    /// This treats the descendants as if this transaction is discarded and removing the transaction
+    /// reduces a nonce gap.
     fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
     ) -> Option<Arc<ValidPoolTransaction<T::Transaction>>> {
         let (tx, pool) = self.all_transactions.remove_transaction_by_hash(tx_hash)?;
+
+        // After a tx is removed, its descendants must become parked due to the nonce gap
+        let updates = self.all_transactions.park_descendant_transactions(tx.id());
+        self.process_updates(updates);
         self.remove_from_subpool(pool, tx.id())
     }
 
@@ -834,7 +840,8 @@ impl<T: TransactionOrdering> TxPool<T> {
     /// subpool.
     ///
     /// This is intended to be used when a transaction is included in a block,
-    /// [`Self::on_canonical_state_change`]
+    /// [`Self::on_canonical_state_change`]. So its descendants will not change from pending to
+    /// parked, just like what we do in `remove_transaction_by_hash`.
     fn prune_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
@@ -1464,6 +1471,35 @@ impl<T: PoolTransaction> AllTransactions<T> {
         Some((tx, internal.subpool))
     }
 
+    /// If a tx is removed (_not_ mined), all descendants are set to parked due to the nonce gap
+    pub(crate) fn park_descendant_transactions(
+        &mut self,
+        tx_id: &TransactionId,
+    ) -> Vec<PoolUpdate> {
+        let mut updates = Vec::new();
+
+        for (id, tx) in self.descendant_txs_mut(tx_id) {
+            let current_pool = tx.subpool;
+
+            tx.state.remove(TxState::NO_NONCE_GAPS);
+
+            // update the pool based on the state.
+            tx.subpool = tx.state.into();
+
+            // check if anything changed.
+            if current_pool != tx.subpool {
+                updates.push(PoolUpdate {
+                    id: *id,
+                    hash: *tx.transaction.hash(),
+                    current: current_pool,
+                    destination: tx.subpool.into(),
+                })
+            }
+        }
+
+        updates
+    }
+
     /// Removes a transaction from the set.
     ///
     /// This will _not_ trigger additional updates, because descendants without nonce gaps are
@@ -3077,15 +3113,15 @@ mod tests {
         assert!(pool.queued_transactions().is_empty());
         assert_eq!(2, pool.pending_transactions().len());
 
-        // Remove first (nonce 0) - simulating that it was taken to be a part of the block.
-        pool.prune_transaction_by_hash(v0.hash());
+        // Remove first (nonce 0)
+        pool.remove_transaction_by_hash(v0.hash());
 
         // Now add transaction with nonce 2
         let _res = pool.add_transaction(v2, on_chain_balance, on_chain_nonce).unwrap();
 
-        // v2 is in the queue now. v1 is still in 'pending'.
-        assert_eq!(1, pool.queued_transactions().len());
-        assert_eq!(1, pool.pending_transactions().len());
+        // v1 and v2 should both be in the queue now.
+        assert_eq!(2, pool.queued_transactions().len());
+        assert!(pool.pending_transactions().is_empty());
 
         // Simulate new block arrival - and chain nonce increasing.
         let mut updated_accounts = HashMap::default();
@@ -3156,12 +3192,53 @@ mod tests {
 
         pool.remove_transactions(vec![*v0.hash(), *v2.hash()]);
 
-        assert_eq!(0, pool.queued_transactions().len());
-        assert_eq!(2, pool.pending_transactions().len());
+        assert_eq!(2, pool.queued_transactions().len());
+        assert!(pool.pending_transactions().is_empty());
         assert!(pool.contains(v1.hash()));
         assert!(pool.contains(v3.hash()));
     }
 
+    #[test]
+    fn test_remove_transactions_middle_pending_hash() {
+        let on_chain_balance = U256::from(10_000);
+        let on_chain_nonce = 0;
+        let mut f = MockTransactionFactory::default();
+        let mut pool = TxPool::new(MockOrdering::default(), Default::default());
+
+        let tx_0 = MockTransaction::eip1559().set_gas_price(100).inc_limit();
+        let tx_1 = tx_0.next();
+        let tx_2 = tx_1.next();
+        let tx_3 = tx_2.next();
+
+        // Create 4 transactions
+        let v0 = f.validated(tx_0);
+        let v1 = f.validated(tx_1);
+        let v2 = f.validated(tx_2);
+        let v3 = f.validated(tx_3);
+
+        // Add them to the pool
+        let _res = pool.add_transaction(v0, on_chain_balance, on_chain_nonce).unwrap();
+        let _res = pool.add_transaction(v1.clone(), on_chain_balance, on_chain_nonce).unwrap();
+        let _res = pool.add_transaction(v2, on_chain_balance, on_chain_nonce).unwrap();
+        let _res = pool.add_transaction(v3, on_chain_balance, on_chain_nonce).unwrap();
+
+        assert_eq!(0, pool.queued_transactions().len());
+        assert_eq!(4, pool.pending_transactions().len());
+
+        let mut removed_txs = pool.remove_transactions(vec![*v1.hash()]);
+        assert_eq!(1, removed_txs.len());
+
+        assert_eq!(2, pool.queued_transactions().len());
+        assert_eq!(1, pool.pending_transactions().len());
+
+        // reinsert
+        let removed_tx = removed_txs.pop().unwrap();
+        let v1 = f.validated(removed_tx.transaction.clone());
+        let _res = pool.add_transaction(v1, on_chain_balance, on_chain_nonce).unwrap();
+        assert_eq!(0, pool.queued_transactions().len());
+        assert_eq!(4, pool.pending_transactions().len());
+    }
+
     #[test]
     fn test_remove_transactions_and_descendants() {
         let on_chain_balance = U256::from(10_000);
diff --git a/crates/transaction-pool/src/traits.rs b/crates/transaction-pool/src/traits.rs
index 002988a2c44..795bbd33696 100644
--- a/crates/transaction-pool/src/traits.rs
+++ b/crates/transaction-pool/src/traits.rs
@@ -315,6 +315,8 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
 
     /// Removes all transactions corresponding to the given hashes.
     ///
+    /// Note: This removes the transactions as if they got discarded (_not_ mined).
+    ///
     /// Consumer: Utility
     fn remove_transactions(
         &self,


Reviews:
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:37:00Z
user: login: mattsse
body: nice, only have a style request,

perhaps we add one more test case?
and document this behaviour on `fn remove_transaction`

state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T14:49:57Z
user: login: int88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:55:04Z
user: login: int88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:56:30Z
user: login: int88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T14:57:59Z
user: login: int88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T16:20:22Z
user: login: 0xtinkle
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T17:02:36Z
user: login: 0xtinkle
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T17:04:16Z
user: login: mattsse
body: this looks good, I pushed some docs and test feature fixes.

I only have one more pedantic nitpick
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T18:54:06Z
user: login: int88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T00:26:38Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T01:21:52Z
user: login: mattsse
body: lgtm, ty
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T06:22:23Z
user: login: graphite-app[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T06:23:03Z

Review comments:
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
         let tx = self.by_hash.remove(tx_hash)?;
         let internal = self.txs.remove(&tx.transaction_id)?;
         // decrement the counter for the sender.
         self.tx_decr(tx.sender_id());
         self.update_size_metrics();
-        Some((tx, internal.subpool))
+
+        if !update_descendant {
+            return Some((tx, internal.subpool, vec![]));
+        }
+
+        let mut updates = Vec::new();
+
+        for (id, tx) in self.descendant_txs_mut(tx.id()) {
+            let current_pool = tx.subpool;
+
+            tx.state.remove(TxState::NO_NONCE_GAPS);
+
+            // udpate the pool based on the state.
path: crates/transaction-pool/src/pool/txpool.rs
user: login: graphite-app[bot]
body: There's a small typo in the comment: `udpate` should be `update`.
```suggestion
            // update the pool based on the state.
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=paradigmxyz&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-25T14:36:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3077,15 +3105,15 @@ mod tests {
         assert!(pool.queued_transactions().is_empty());
         assert_eq!(2, pool.pending_transactions().len());
 
-        // Remove first (nonce 0) - simulating that it was taken to be a part of the block.
-        pool.prune_transaction_by_hash(v0.hash());
+        // Remove first (nonce 0)
+        pool.remove_transaction_by_hash(v0.hash());
 
         // Now add transaction with nonce 2
         let _res = pool.add_transaction(v2, on_chain_balance, on_chain_nonce).unwrap();
 
-        // v2 is in the queue now. v1 is still in 'pending'.
-        assert_eq!(1, pool.queued_transactions().len());
-        assert_eq!(1, pool.pending_transactions().len());
+        // v1 and v2 should both be in the queue now.
+        assert_eq!(2, pool.queued_transactions().len());
+        assert!(pool.pending_transactions().is_empty());
path: crates/transaction-pool/src/pool/txpool.rs
user: login: mattsse
body: this is actually the bug you spotted earlier, right?
created_at: 2025-04-25T14:45:19Z
author_association: COLLABORATOR
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
path: crates/transaction-pool/src/pool/txpool.rs
user: login: mattsse
body: instead of making this conditional I'd prefer having a new dedicated function for this instead, that we can call after `remove_transaction_by_hash` in `fn remove_transaction(`
created_at: 2025-04-25T14:46:37Z
author_association: COLLABORATOR
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
         let tx = self.by_hash.remove(tx_hash)?;
         let internal = self.txs.remove(&tx.transaction_id)?;
         // decrement the counter for the sender.
         self.tx_decr(tx.sender_id());
         self.update_size_metrics();
-        Some((tx, internal.subpool))
+
+        if !update_descendant {
+            return Some((tx, internal.subpool, vec![]));
+        }
+
+        let mut updates = Vec::new();
+
+        for (id, tx) in self.descendant_txs_mut(tx.id()) {
+            let current_pool = tx.subpool;
+
+            tx.state.remove(TxState::NO_NONCE_GAPS);
path: crates/transaction-pool/src/pool/txpool.rs
user: login: mattsse
body: cool, this should actually do it, this is basically equivalent to reinserting them
created_at: 2025-04-25T14:47:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
         let tx = self.by_hash.remove(tx_hash)?;
         let internal = self.txs.remove(&tx.transaction_id)?;
         // decrement the counter for the sender.
         self.tx_decr(tx.sender_id());
         self.update_size_metrics();
-        Some((tx, internal.subpool))
+
+        if !update_descendant {
+            return Some((tx, internal.subpool, vec![]));
+        }
+
+        let mut updates = Vec::new();
+
+        for (id, tx) in self.descendant_txs_mut(tx.id()) {
+            let current_pool = tx.subpool;
+
+            tx.state.remove(TxState::NO_NONCE_GAPS);
path: crates/transaction-pool/src/pool/txpool.rs
user: login: int88
body: yes, similar to the operations in `insert_tx`
created_at: 2025-04-25T14:55:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3077,15 +3105,15 @@ mod tests {
         assert!(pool.queued_transactions().is_empty());
         assert_eq!(2, pool.pending_transactions().len());
 
-        // Remove first (nonce 0) - simulating that it was taken to be a part of the block.
-        pool.prune_transaction_by_hash(v0.hash());
+        // Remove first (nonce 0)
+        pool.remove_transaction_by_hash(v0.hash());
 
         // Now add transaction with nonce 2
         let _res = pool.add_transaction(v2, on_chain_balance, on_chain_nonce).unwrap();
 
-        // v2 is in the queue now. v1 is still in 'pending'.
-        assert_eq!(1, pool.queued_transactions().len());
-        assert_eq!(1, pool.pending_transactions().len());
+        // v1 and v2 should both be in the queue now.
+        assert_eq!(2, pool.queued_transactions().len());
+        assert!(pool.pending_transactions().is_empty());
path: crates/transaction-pool/src/pool/txpool.rs
user: login: int88
body: Yes! The status of these two txs should be same, as they are continuous, and the balance is sufficient
created_at: 2025-04-25T14:56:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
path: crates/transaction-pool/src/pool/txpool.rs
user: login: int88
body: 👌 
created_at: 2025-04-25T14:57:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -827,14 +827,19 @@ impl<T: TransactionOrdering> TxPool<T> {
         tx_hash: &B256,
     ) -> Option<Arc<ValidPoolTransaction<T::Transaction>>> {
         let (tx, pool) = self.all_transactions.remove_transaction_by_hash(tx_hash)?;
+
+        // After tx is removed, its descendants must become parked due to the nonce gap
+        let updates = self.all_transactions.park_descendant_pending_txs(tx.id());
+        self.process_updates(updates);
         self.remove_from_subpool(pool, tx.id())
     }
 
     /// This removes the transaction from the pool and advances any descendant state inside the
     /// subpool.
     ///
     /// This is intended to be used when a transaction is included in a block,
-    /// [`Self::on_canonical_state_change`]
+    /// [`Self::on_canonical_state_change`]. So its descendants will not change from pending to
path: crates/transaction-pool/src/pool/txpool.rs
user: login: int88
body: @SozinM this one will be used when tx was mined.
created_at: 2025-04-25T16:20:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1455,13 +1457,39 @@ impl<T: PoolTransaction> AllTransactions<T> {
     pub(crate) fn remove_transaction_by_hash(
         &mut self,
         tx_hash: &B256,
-    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool)> {
+        update_descendant: bool,
+    ) -> Option<(Arc<ValidPoolTransaction<T>>, SubPool, Vec<PoolUpdate>)> {
path: crates/transaction-pool/src/pool/txpool.rs
user: login: 0xtinkle
body: maybe need to add a unit test that mimics a mined tx being removed and then verify descendant state explicitly stays queued/pending as expected?
created_at: 2025-04-25T17:02:35Z
author_association: NONE
diff_hunk: @@ -827,14 +827,19 @@ impl<T: TransactionOrdering> TxPool<T> {
         tx_hash: &B256,
     ) -> Option<Arc<ValidPoolTransaction<T::Transaction>>> {
         let (tx, pool) = self.all_transactions.remove_transaction_by_hash(tx_hash)?;
+
+        // After tx is removed, its descendants must become parked due to the nonce gap
+        let updates = self.all_transactions.park_descendant_pending_txs(tx.id());
path: crates/transaction-pool/src/pool/txpool.rs
user: login: 0xtinkle
body: This change makes sense for mined txs but does it also need to consider whether this path could be called for re-org scenarios? If a mined tx is reverted due to a chain re-org, would its descendants be requeued correctly, or could it end up with inconsistent subpool state?
created_at: 2025-04-25T17:04:15Z
author_association: NONE
diff_hunk: @@ -1464,6 +1469,31 @@ impl<T: PoolTransaction> AllTransactions<T> {
         Some((tx, internal.subpool))
     }
 
+    pub(crate) fn park_descendant_pending_txs(&mut self, tx_id: &TransactionId) -> Vec<PoolUpdate> {
path: crates/transaction-pool/src/pool/txpool.rs
user: login: mattsse
body: this needs some docs
created_at: 2025-04-25T18:50:26Z
author_association: COLLABORATOR
diff_hunk: @@ -827,14 +827,19 @@ impl<T: TransactionOrdering> TxPool<T> {
         tx_hash: &B256,
     ) -> Option<Arc<ValidPoolTransaction<T::Transaction>>> {
         let (tx, pool) = self.all_transactions.remove_transaction_by_hash(tx_hash)?;
+
+        // After a tx is removed, its descendants must become parked due to the nonce gap
+        let updates = self.all_transactions.park_descendant_pending_txs(tx.id());
path: crates/transaction-pool/src/pool/txpool.rs
user: login: mattsse
body: I'd like this function to be a separate function because is remove_transaction_by_hash is called in two places:

https://github.com/paradigmxyz/reth/blob/main/crates/transaction-pool/src/pool/txpool.rs#L779-L786

and processing updates for this one is redundant.
created_at: 2025-04-25T18:53:33Z
author_association: COLLABORATOR
diff_hunk: @@ -827,14 +827,19 @@ impl<T: TransactionOrdering> TxPool<T> {
         tx_hash: &B256,
     ) -> Option<Arc<ValidPoolTransaction<T::Transaction>>> {
         let (tx, pool) = self.all_transactions.remove_transaction_by_hash(tx_hash)?;
+
+        // After a tx is removed, its descendants must become parked due to the nonce gap
+        let updates = self.all_transactions.park_descendant_pending_txs(tx.id());
path: crates/transaction-pool/src/pool/txpool.rs
user: login: int88
body: 👍 
created_at: 2025-04-26T00:26:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -315,6 +315,8 @@ pub trait TransactionPool: Clone + Debug + Send + Sync {
 
     /// Removes all transactions corresponding to the given hashes.
     ///
+    /// Note: This removes the transactions as if the got discarded (_not_ mined).
path: crates/transaction-pool/src/traits.rs
user: login: graphite-app[bot]
body: There's a small typo in the documentation comment: "as if the got discarded" should be "as if they got discarded".
```suggestion
    /// Note: This removes the transactions as if they got discarded (_not_ mined).
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=paradigmxyz&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-26T01:21:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -726,6 +726,9 @@ where
     }
 
     /// Removes and returns all matching transactions from the pool.
+    ///
+    /// This behaves as if the transactions got discarded (_not_ mined), effecitvely introducing a
path: crates/transaction-pool/src/pool/mod.rs
user: login: graphite-app[bot]
body: There's a typo in the word `effecitvely` which should be spelled `effectively`.
```suggestion
    /// This behaves as if the transactions got discarded (_not_ mined), effectively introducing a
```
  

*Spotted by [Diamond](https://app.graphite.dev/diamond/?org=paradigmxyz&ref=ai-review-comment)*<i class='graphite__hidden'><br /><br />Is this helpful? React 👍 or 👎 to let us know.</i>
created_at: 2025-04-26T06:23:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: codspeed-hq[bot]
created_at: 2025-04-25T14:59:05Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/paradigmxyz/reth/branches/int88%3Aremove-tx)

### Merging #15931 will **not alter performance**

<sub>Comparing <code>int88:remove-tx</code> (c8916d6) with <code>main</code> (59b717e)</sub>



### Summary

`✅ 77` untouched benchmarks  




author_association: NONE
user: login: int88
created_at: 2025-04-25T16:16:14Z
body: @mattsse updated!

> perhaps we add one more test case?

what test? perhaps we can do in the next PR
author_association: CONTRIBUTOR
user: login: SozinM
created_at: 2025-04-25T16:16:35Z
body: This remove also invoked when tx is mined -> won't cause nonce gap
author_association: CONTRIBUTOR
user: login: SozinM
created_at: 2025-04-28T13:51:30Z
body: @mattsse @int88
Seems there is little corner case:
When i use this function inner pool gets updated, tx is places in queue, but txpool_content will return incorrect data (with tx in pending and not in queue) 
author_association: CONTRIBUTOR
user: login: mattsse
created_at: 2025-04-28T14:00:16Z
body: can you open a new issue with the scenario, not obvious to me how this could happen now
author_association: COLLABORATOR