fix: evicted transactions could reappear after a node restart#20773
Merged
mrzeszutko merged 2 commits intomerge-train/spartanfrom Feb 24, 2026
Merged
fix: evicted transactions could reappear after a node restart#20773mrzeszutko merged 2 commits intomerge-train/spartanfrom
mrzeszutko merged 2 commits intomerge-train/spartanfrom
Conversation
PhilWindle
approved these changes
Feb 23, 2026
Collaborator
PhilWindle
left a comment
There was a problem hiding this comment.
Looks great, thanks!
AztecBot
pushed a commit
that referenced
this pull request
Feb 24, 2026
## Summary
- Fix a bug in `TxPoolV2.addPendingTxs` where evicted transactions could reappear after a node restart ("come back to life")
- Move all throwable I/O (`buildTxMetaData`, `getMinedBlockId`, `validateMeta`) out of the `transactionAsync` callback into a pre-computation phase, so that if any I/O fails, no in-memory or DB mutations have occurred
## Background
`addPendingTxs` processes a batch of transactions inside a single LMDB `transactionAsync`. When tx N causes nullifier-conflict evictions of earlier pool txs and then tx N+1 triggers a throw (e.g. `getTxEffect` I/O failure or validator crash), the LMDB transaction rolls back — but in-memory mutations from the eviction persist. On restart the pool rehydrates from DB where the soft-delete marker was never committed, and the evicted tx loads back into the pool.
Other pool methods (`prepareForSlot`, `handlePrunedBlocks`, `handleFinalizedBlock`, etc.) are not affected because they either perform throwable I/O *before* any deletions, or wrap throwable operations in try/catch via the EvictionManager.
## Approach (TDD)
The fix was developed test-first. Two failing tests were written that reproduce the inconsistency — one for each throwable path (`getMinedBlockId` and `validateMeta`) — by verifying that `getTxStatus` returns the same value before and after a pool restart. With the bug present, the status diverges (`'deleted'` in memory vs `'pending'` after restart). The implementation was then applied to make both tests pass.
## Implementation
`addPendingTxs` is now split into two phases:
1. **Pre-computation** (outside the transaction): For each tx, compute `buildTxMetaData`, `getMinedBlockId`, and `validateMeta`. If any of these throw, the call fails before any mutations happen.
2. **Transaction** (inside `transactionAsync`): Uses only pre-computed results, in-memory index reads, and buffered DB writes.
Supporting changes:
- `#addTx` accepts an optional `precomputedMeta` parameter (backward-compatible — other call sites unchanged)
- `#tryAddRegularPendingTx` receives pre-computed metadata directly instead of calling `buildTxMetaData`/`validateMeta`; validation rejection is handled by the caller
## Test plan
- [x] Two new persistence consistency tests pass (were failing before the fix)
- [x] All 210 `tx_pool_v2.test.ts` tests pass
- [x] Full p2p test suite passes (1292 tests)
Collaborator
|
✅ Successfully backported to backport-to-v4-staging #20752. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TxPoolV2.addPendingTxswhere evicted transactions could reappear after a node restart ("come back to life")buildTxMetaData,getMinedBlockId,validateMeta) out of thetransactionAsynccallback into a pre-computation phase, so that if any I/O fails, no in-memory or DB mutations have occurredBackground
addPendingTxsprocesses a batch of transactions inside a single LMDBtransactionAsync. When tx N causes nullifier-conflict evictions of earlier pool txs and then tx N+1 triggers a throw (e.g.getTxEffectI/O failure or validator crash), the LMDB transaction rolls back — but in-memory mutations from the eviction persist. On restart the pool rehydrates from DB where the soft-delete marker was never committed, and the evicted tx loads back into the pool.Other pool methods (
prepareForSlot,handlePrunedBlocks,handleFinalizedBlock, etc.) are not affected because they either perform throwable I/O before any deletions, or wrap throwable operations in try/catch via the EvictionManager.Approach (TDD)
The fix was developed test-first. Two failing tests were written that reproduce the inconsistency — one for each throwable path (
getMinedBlockIdandvalidateMeta) — by verifying thatgetTxStatusreturns the same value before and after a pool restart. With the bug present, the status diverges ('deleted'in memory vs'pending'after restart). The implementation was then applied to make both tests pass.Implementation
addPendingTxsis now split into two phases:buildTxMetaData,getMinedBlockId, andvalidateMeta. If any of these throw, the call fails before any mutations happen.transactionAsync): Uses only pre-computed results, in-memory index reads, and buffered DB writes.Supporting changes:
#addTxaccepts an optionalprecomputedMetaparameter (backward-compatible — other call sites unchanged)#tryAddRegularPendingTxreceives pre-computed metadata directly instead of callingbuildTxMetaData/validateMeta; validation rejection is handled by the callerTest plan
tx_pool_v2.test.tstests pass