Skip to content

Conversation

@mattac21
Copy link
Contributor

@mattac21 mattac21 commented Dec 16, 2025

Description

This adds the ability for proposals to be created out of txs in the pending pool while the pool is resetting via a new block. Previously, this resetting would hold the lock on the pool for the entire duration of the reset, which would need to call recheck on every tx in the pending pool. This may take multiple seconds, which will block a call to Pending and therefore block PrepareProposal.

This PR addresses this by pushing txs into a txCollector as they are validated via demoteUnexecutables. Creating a 'snapshot' of the pending txs that have been validated for a height at any given time. This snapshot no longer shares the same global legacypool lock, thus unblocking PrepareProposal's call to Pending.

This PR also adds functionality where the reorg loop will be cancelled if it is still processing a reset (new block seen), and a new block arrives. This is to prevent the case where demoteUnexecutables takes such a long time to process txs that processing the previous height, always takes longer than the timeout the caller is willing to wait in Pending, thus preventing any txs from ever making it into a proposal.

Closes: STACK-2008


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • tackled an existing issue or discussed with a team member
  • left instructions on how to review the changes
  • targeted the main branch

@linear
Copy link

linear bot commented Dec 19, 2025

@mattac21 mattac21 marked this pull request as ready for review December 19, 2025 04:52
Comment on lines +221 to +251
for addr, txs := range t.txs {
sort.Sort(types.TxByNonce(txs))

// Filter by minimum tip if configured
if minTipBig != nil {
for i, tx := range txs {
if tx.EffectiveGasTipIntCmp(minTipBig, baseFeeBig) < 0 {
txs = txs[:i]
break
}
}
}

// Convert to lazy transactions
if len(txs) > 0 {
lazies := make([]*txpool.LazyTransaction, len(txs))
for i, tx := range txs {
lazies[i] = &txpool.LazyTransaction{
Hash: tx.Hash(),
Tx: tx,
Time: tx.Time(),
GasFeeCap: uint256.MustFromBig(tx.GasFeeCap()),
GasTipCap: uint256.MustFromBig(tx.GasTipCap()),
Gas: tx.Gas(),
BlobGas: tx.BlobGas(),
}
}
numSelected += len(lazies)
pending[addr] = lazies
}
}

Check warning

Code scanning / CodeQL

Iteration over map Warning

Iteration over map may be a possible source of non-determinism
}

evmPendingTxs := m.txPool.Pending(txpool.PendingFilter{
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Can we fetch consensus params from the context and replace ms with something that correlates with the configured propose timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good idea, still trying to figure out what a good value for this is


// runReorg runs reset and promoteExecutables on behalf of scheduleReorgLoop.
func (pool *LegacyPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirtyAccounts *accountSet, events map[common.Address]*SortedMap) {
func (pool *LegacyPool) runReorg(done chan struct{}, cancelReset chan struct{}, reset *txpoolResetRequest, dirtyAccounts *accountSet, events map[common.Address]*SortedMap) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we replace these args with reorgInput{}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there is a lot i can consolidate to a struct


// isResetCancelled returns true if the pool is resetting and it has been
// signaled to cancel the reset.
func (pool *LegacyPool) isReorgCancelled(reset *txpoolResetRequest, cancelled chan struct{}) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be moved to a standalone function as it doesn't use the pool

}

// add adds txs to the current set.
func (t *txs) Add(addr common.Address, txs types.Transactions) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to write a duplicate here. How does the caller ensure that no duplicates are written?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm you're right. I think a scenario like:

  • new block arrives
  • new tx arrives that replaces a tx in the pending queue (this will get added to txs struct via markTxPromoted)
  • reorg loop runs with reset != nil
  • tx that just replaced the tx in pending is validated again during demote unexecutables and added to txs structure again
  • ahhh

I can add in protection to make sure no duplicates are added.

@mattac21 mattac21 force-pushed the ma/incremental-recheck branch from 3c991da to 392848e Compare December 19, 2025 21:51
@mattac21 mattac21 force-pushed the ma/incremental-recheck branch from b8e1b2b to 9134baf Compare December 19, 2025 22:19
// pending limit. The algorithm tries to reduce transaction counts by an approximately
// equal number for all for accounts with many pending transactions.
func (pool *LegacyPool) truncatePending() {
defer func(t0 time.Time) { pendingTruncateTimer.UpdateSince(t0) }(time.Now())

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism
func (pool *LegacyPool) markTxRemoved(tx *types.Transaction, p PoolType) {
func (pool *LegacyPool) markTxRemoved(addr common.Address, tx *types.Transaction, p PoolType) {
if p == Pending {
defer func(t0 time.Time) { pendingRemoveCBTimer.UpdateSince(t0) }(time.Now())

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism

// RemoveTx removes a tx from the collector.
func (c *txCollector) RemoveTx(addr common.Address, tx *types.Transaction) {
defer func(t0 time.Time) { collectorRemoveDuraiton.UpdateSince(t0) }(time.Now())

Check warning

Code scanning / CodeQL

Calling the system time Warning

Calling the system time may be a possible source of non-determinism
@mattac21 mattac21 force-pushed the ma/incremental-recheck branch 2 times, most recently from e87dacf to 9134baf Compare December 20, 2025 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants