fix(nonce_manager): reuse freed/gap nonce instead of skipping it#93
Merged
abhicris merged 1 commit intoJun 4, 2026
Merged
Conversation
acquire_nonce returned max(pending_nonces) + 1 whenever any nonce was
pending. If a lower nonce was freed by release_nonce (a tx that failed
locally before broadcast) while a higher nonce stayed pending, the freed
nonce was never reissued — acquire_nonce jumped past it. The same applies
to a gap left by an out-of-order confirmation.
Ethereum requires gapless nonces, so a skipped nonce stalls every
higher-nonce pending transaction in the mempool until the gap is filled.
This also contradicted release_nonce's own contract ("making it available
again").
Fix: acquire_nonce now returns the lowest nonce at or above confirmed_nonce
that is not already pending, walking the SortedSet from confirmed_nonce via
irange. In the gapless common case the result is identical to the old
max+1 behaviour, so existing callers are unaffected.
Added a regression test (test_release_lower_nonce_is_reused) covering
release-of-lower-nonce followed by reuse. Full suite: 171 passed, 56
skipped.
Contributor
|
🤖 Audit verdict: Legitimate logic fix for Ethereum nonce gap-filling with correct algorithm and comprehensive regression test; no malicious intent or security issues detected. Audited by the kcolbchain PR pipeline. See pipeline docs. |
Contributor
|
Merged — thanks, @kite-builds. That's 3 merged PRs to kcolbchain now. At 5 merged PRs you become a Fellow and enter the invited-work pool. Next-up issues across the org: https://kcolbchain.com/invitations/ |
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.
What
NonceManager.acquire_noncereturnedmax(pending_nonces) + 1whenever any nonce was pending. When a lower nonce was freed byrelease_nonce(e.g. a tx that failed locally before broadcast) while a higher nonce was still pending, the freed nonce was never reissued — the nextacquire_noncejumped past it. The same hole appears after an out-of-order confirmation.This change makes
acquire_noncereturn the lowest nonce at or aboveconfirmed_noncethat is not already pending, walking theSortedSetfromconfirmed_nonceviairange.Why
Ethereum requires gapless nonces. A skipped nonce leaves a permanent hole, and every higher-nonce pending transaction sits in the mempool unmined until that hole is filled — a liveness bug for any agent that ever releases a nonce. It also contradicted
release_nonce's own docstring, which states it frees the nonce "making it available again."Repro on
main:After the fix the final call returns
0.In the gapless common case the result is identical to the old
max + 1behaviour, so existing callers are unaffected. All existingnonce_managertests pass unchanged.Tests
Added
test_release_lower_nonce_is_reused, which releases a lower nonce while a higher one stays pending and asserts the freed nonce is reused, then that the sequence extends normally afterward.pytest tests/test_nonce_manager.py -v→ 10 passedpytest -q(full suite) → 171 passed, 56 skippedruff check switchboard/nonce_manager.py→ cleanNo related open issue; this is a standalone correctness fix to the nonce manager (introduced in PR #11) that hardens the same component issue #79 plans to extend.