Conversation
Update no longer rebuilds byHash/byEvmHash/byNonce and re-sorts the whole mempool when the store fits in softLimit; it re-fetches account state and re-derives readiness in O(m). Reap(remove=true) removes the reaped txs incrementally and only refreshes readiness of the affected accounts, so the inclusion order is computed once instead of twice. The RPC snapshot is recomputed lazily when it went stale.
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4240 +/- ##
==========================================
- Coverage 66.91% 65.78% -1.14%
==========================================
Files 2176 2055 -121
Lines 167167 155277 -11890
==========================================
- Hits 111858 102142 -9716
+ Misses 55168 52994 -2174
Partials 141 141
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
PR SummaryMedium Risk Overview
Readiness/nonce logic is refactored into Reviewed by Cursor Bugbot for commit 00db340. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Replacing the per-block compact with incremental remove/refresh/refreshReady looks correct: readiness is still derived by resetting nextNonce to firstNonce and walking byNonce, counter bookkeeping and the RemovedTxs/EvictedTxs/recordPendingNonce* metrics match the old re-insertion path, and the reap-demotion semantics (successors of a reaped head become pending until the next Update) are identical to what compact(inner, false) produced. No blockers; the main gap is test coverage for the new partial-reap path.
Findings: 0 blocking | 3 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- [suggestion] The new
refreshReadydemotion path has no direct test. The only existingReap(remove=true)coverage isTestTxMempool_DescendingNonceDrain, which submits nonces in descending order so exactly one tx is ready per block —refreshReadynever actually demotes a successor there. Add a txStore-level test that inserts a contiguous ready chain for one account, reaps only its head (MaxTxs< chain length), and asserts the successors moved to pending with correctready/totalcount and byte totals, thatNextNoncefalls back tofirstNonce, and that the followingUpdatepromotes them again. Same forrefresh: a case where the app nonce advances past some mempool txs and another where the balance drops below a mid-chain tx'srequiredBalance, asserting the drop/demotion and thatbyEvmHash/byNonceno longer hold the dropped entries. These are the two functions the PR description itself flags as the ones to scrutinise, and they are the only pathsUpdate/Reapnow take in normal operation. - [suggestion]
benchTxStoreraisesSize,PendingSize,MaxTxsBytesandMaxPendingTxsBytesbut leavesTestConfig'sCacheSizeat 1000 while inserting 10k–50k txs. Sincerefreshpushes metadata for every surviving tx, each iteration evicts and reallocates ~m LRU nodes that a right-sized cache would only relink, so the reportedUpdatetimings include allocation churn production (CacheSizedefault 10000) would not see at the 10k point. Settingcfg.CacheSize = numAccounts * txsPerAccount(asrecheck_drain_test.godoes) would make the headline numbers reflect the O(m) refresh work itself. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| return inner.snapshot | ||
| } | ||
| } | ||
| for inner := range s.inner.Lock() { |
There was a problem hiding this comment.
[suggestion] RecentSnapshot can now escalate from RLock to the exclusive Lock and run the O(m log m) inInclusionOrder sort inline. Since refresh/Reap set snapshotStale, the first /unconfirmed_txs request after each block pays that sort while holding the write lock, blocking Insert/CheckTx/Reap and the consensus Update for roughly the 9–20 ms the PR description measures at 10k–20k txs. Total work is not higher than before (consensus used to pay it unconditionally), but it is now triggerable at an arbitrary moment by an RPC caller rather than at a point consensus controls.
Two cheaper options: keep the snapshot a byproduct of the inInclusionOrder that Reap already runs under the lock, or compute the ordering into a local slice and publish it with a short critical section. Worth at least a comment recording the intended trade-off.
Separately, this makes RecentSnapshot unsafe to call from any context already holding inner.RLock() (sync.RWMutex is not reentrant). No current caller does, but the previous version was safe there.
Every block,
txStore.Updatecalledcompact, which recomputesinInclusionOrder(two O(m log m) sorts over the whole mempool) and then throws away and rebuildsbyHash/byEvmHash/byNonceby re-inserting every tx.Reap(remove=true)was worse: it computed the inclusion order to pick txs, then calledcompact, which computed it a second time. At the ~10k tx mempools seen in production this is tens of milliseconds of serialized work on the consensus path per block.Updatenow removes executed/invalid/expired txs in place through a newremovehelper and, when the store still fits insoftLimit, callsrefreshinstead ofcompact: it clears and re-fetches account state, drops txs whose nonce fell below the account nonce, re-caches their metadata and re-derives readiness per account withadvanceReady(the loop extracted frominsert), all in O(m) with no sort and no index rebuild.nextNonceis still reset tofirstNonceand rebuilt by walkingbyNonce, so readiness semantics are unchanged.Reap(remove=true)removes the reaped txs incrementally and re-derives readiness only for the affected accounts (refreshReady); it only falls back tocompactwhen the store is still abovesoftLimit, where eviction genuinely needs the full order. I did not reuse the pre-removal order for that fallback because reaping the head of an account demotes its successors from ready to pending, so the post-removal order is not a filter of the pre-removal one. Since the RPC snapshot is no longer a byproduct ofcompact,RecentSnapshotrecomputes it lazily whensnapshotStaleis set.compactTotal/compactDurationSecondsnow only fire whencompactactually runs (insert overflow, or when a store abovesoftLimitneeds eviction), so those series will drop in normal operation. Reviewers should look most closely atrefreshandrefreshReadyagainst the previouscompactre-insertion path. Existing mempool tests pass unchanged under-race; the newtx_bench_test.goshowsUpdategoing from ~20ms to ~9ms at 10k txs (32→9ms at 20k txs over 5k accounts, 121→49ms at 50k) andReap(remove=true)from ~21ms to ~7ms (50→15ms at 20k), the remainder being the unavoidable per-account state re-fetch and the single inclusion-order sort.