Visitar URL original
fix(evm): treat nonce too high as a final client error by kasrakhosravi · Pull Request #1431 · erpc/erpc · GitHub
Skip to content

fix(evm): treat nonce too high as a final client error - #1431

Open
kasrakhosravi wants to merge 4 commits into
mainfrom
fix/nonce-too-high-client-error
Open

kasrakhosravi wants to merge 4 commits into
mainfrom
fix/nonce-too-high-client-error

Conversation

@kasrakhosravi

@kasrakhosravi kasrakhosravi commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator
  • nonce too high had no normalizer rule
  • Fell to server-side fallback: retried, tripped breaker
  • On main: 1 refusal → 6 upstream calls, both breakers open
  • Now execution exception: never counts toward the breaker
  • Every method keeps failover: a lagging upstream misreads the nonce
  • Failover runs in the upstream sweep: no network retry policy needed
  • Refusing upstream not asked again; ≤3 calls per send
  • Broadcast consensus waits for outstanding participants after a node-local refusal (12b, 12c)
  • Behavior change: other retryable execution exceptions (12b, 13 tracing, 14 sends, send reverts) also sweep without a network retry
  • Variants: geth/reth/Nethermind, Besu (-32006, too distant), Erigon (nonce gap too large)
  • Docs: errors reference rule 12c, gotchas 30-31
  • Tests: NonceTooHighEverywhereKeepsBreakersClosed, fails on main
  • Tests: NonceTooHighFromLaggingUpstream* (no network retry, retry, consensus broadcast, consensus only)
  • Lab 843: breakers never reopen after the outage (main: 4)
  • Context: OpenZeppelin Relayer fix: honor WithRetryableTowardNetwork(false) at network scope #843 failover lab

- Fell to the server-side fallback: retried on every upstream, counted toward the breaker.
- A relayer's nonce gap opened the breakers and refused its reads too.
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor
File Lines Key changes Risk
🟠 error_normalizer.go +32/-0 — ⚠ ErrEndpointExecutionException, ErrJsonRpcExceptionInternal
🟠 errors.go +16/-0 IsRetryableExecutionException ⚠ ErrCodeEndpointExecutionException
🟠 networks.go +6/-2 — ⚠ ErrCodeEndpointExecutionException
1 test files +232

xray — see through AI slop with deterministic architecture PR diff reviews

@graphify-labs

graphify-labs Bot commented Nov 9, 2026

Copy link
Copy Markdown

Graphify reviews are paused for this workspace because nobody has used it in 7 days. A workspace owner can resume reviews from the Repositories page: https://app.graphify.com/erpc/repositories

@blacksmith-sh

This comment has been minimized.

@kasrakhosravi

Copy link
Copy Markdown
Collaborator Author

Lab check: relayer-failure-lab scenario 843 (OpenZeppelin Relayer v1.9.0), eRPC in front of 2 strict-nonce upstreams, 60 s HTTP 503 on both.

build breaker nonce too high upstream replies breaker opens after outage refused eth_getTransactionCount
main a788b61 none 2600 – 0
this PR none 1300 – 0
main a788b61 20/80, 5m 649 4 (+286…+590 s) 7423
this PR 20/80, 5m 1575 0 0

@aramalipoor

Copy link
Copy Markdown
Member

@kasrakhosravi we need to ensure this doesn't conflict with how we do safe nonce via consensus and sendTx broadcasting, basically no degradation in the write/send flows via erpc when there are multiple upstreams with differing views

- A lagging upstream reads the correct next nonce as a gap; a final verdict ended the send there
@kasrakhosravi

Copy link
Copy Markdown
Collaborator Author

@aramalipoor good call, it did conflict. The first commit (70ec9a6) made nonce too high final for every method. An upstream that has not applied the sender's last mined tx reads the correct next nonce as a gap, so a send stopped at that one stale upstream and never reached a fresh one.

Fixed in 6dce2b8:

  • eth_sendRawTransaction: nonce too high keeps failover to other upstreams; the refusing upstream is not asked again
  • consensus broadcast (4 participants, agreement 1): a lagging upstream's refusal no longer ends the round; a fresh upstream's tx hash wins
  • all other methods: still final, 1 upstream
  • every case: execution exception, so it never counts toward the breaker; the eth_getTransactionCount (pending) reads a relayer needs stay served
  • safe-nonce reads via consensus: untouched, they never hit this path

Tests, lagging upstream + fresh upstreams:

test 70ec9a6 main 6dce2b8
NonceTooHighFromLaggingUpstreamFailsOverToFreshUpstream fail pass pass
NonceTooHighFromLaggingUpstreamDoesNotEndBroadcast fail pass pass
NonceTooHighEverywhereKeepsBreakersClosed pass (as NonceTooHighIsFinal…) fail pass

A real gap on every upstream: ≤3 upstream calls per send (main: 6, breakers open). Not re-run: relayer lab 843 on 6dce2b8.

@aramalipoor
aramalipoor self-requested a review November 13, 2026 10:05
…errors

- 6dce2b8 failed over only through a network retry round; with no network failsafe the lagging upstream's nonce too high reached the client (20 of 20 sends).
@kasrakhosravi

Copy link
Copy Markdown
Collaborator Author

Lab re-run: relayer-failure-lab 843, Relayer v1.9.0, 2 strict-nonce upstreams, 60 s HTTP 503 on both.

Found on 6dce2b8: failover needed a network retry policy.

  • Sweep returned on any execution exception
  • No network failsafe: 20 of 20 sends got the lagging upstream's refusal (main: 0)
  • e88bc15: sweep continues on retryable execution exceptions
  • NonceTooHighFromLaggingUpstreamFailsOverToFreshUpstream/NoNetworkRetry: fails on 6dce2b8, passes now

Breaker 20/80, halfOpenAfter: 5m:

build breaker opens (s after outage start) reopens after outage refused eth_getTransactionCount transfers landed /360
main a788b61 +44, +349, +351, +655, +715 4 7663 160
6dce2b8 +35, +38 0 1232 119
e88bc15 +41 0 0 360

No breaker:

build runs nonce too high upstream replies drift, last 300 s
main a788b61 2 2922, 2602 33–39, 32–33
70ec9a6 1 1351 33–35
6dce2b8 2 4383, 1384 65–102, 33–38
e88bc15 1 2600 33
  • Opens inside the 503 window are real outages; 5 m halfOpenAfter then refuses reads
  • 6dce2b8's 1232 refusals: both breakers opened in the window
  • Drift varies run to run: 6dce2b8 gave 65–102, then 33–38
  • e88bc15 asks both upstreams per refusal, like main, but no breaker count
  • Relayer drift (32+) unchanged: Relayer-side, fix: honor WithRetryableTowardNetwork(false) at network scope #843
  • Local: ForwardLlamaRPCEndpointRateLimitResponseSingle fails on main too

@blacksmith-sh

This comment has been minimized.

…ls over on every method

- consensus-only broadcast (no retry/hedge) returned a lagging node's nonce too high though 3 upstreams accepted; main returned the hash

- e88bc15 made nonce too high final for eth_call/eth_estimateGas/eth_sendRawTransactionSync; main failed over
@kasrakhosravi

Copy link
Copy Markdown
Collaborator Author

Adversarial pass on e88bc15 (fresh agent, change + claims only, runs vs main a788b61). 2 regressions found, fixed in 455f97f.

finding on e88bc15 main 455f97f
Consensus broadcast, no retry/hedge, lagging node answers first: client gets nonce too high accepted hash accepted hash
eth_call / eth_estimateGas with a nonce, lagging node: final at 1 upstream fails over fails over
  • Consensus: consensus_error_threshold short-circuit treated the refusal (threshold 1) as agreement
  • Fix: tx broadcasts skip that short-circuit for retryable execution exceptions while participants remain
  • Test: NonceTooHighFromLaggingUpstreamDoesNotEndBroadcast/ConsensusOnly, fails on e88bc15
  • Nonce rule now retryable toward network for every method, not only eth_sendRawTransaction
  • Behavior change kept, documented (gotcha 31): 12b/13/14 retryable execution exceptions sweep without network retry; main reached them only via network retry
  • Not changed: upstream-level retry still re-asks the refusing upstream (same on main; default MaxAttempts: 1)
  • Lab not re-run on 455f97f: lab has no consensus, sends already failed over

This branch was successfully deployed

1 active deployment
xray — 455f97ff Deployed Nov 13, 2026 by kasrakhosravi via xray #2676
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.

2 participants