Skip to content

eth, core/state: self-heal missing contract code on stateless import - #2402

Draft
lucca30 wants to merge 3 commits into
lmartins/stateless-incomplete-state-guardfrom
lmartins/stateless-code-self-heal
Draft

eth, core/state: self-heal missing contract code on stateless import#2402
lucca30 wants to merge 3 commits into
lmartins/stateless-incomplete-state-guardfrom
lmartins/stateless-code-self-heal

Conversation

@lucca30

@lucca30 lucca30 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2401 — base branch is lmartins/stateless-incomplete-state-guard,
not develop. Review/merge #2401 first; the diff here is only the three
commits on top of it. It builds directly on the typed error #2401 introduces.

Problem

#2401 makes a stateless node detect a missing contract code and reject the
block instead of masking it. But rejection alone means the node stalls on that
block until the code happens to arrive by some other path. A WIT2 witness never
carries code, so there is no in-band way to recover — the node needs to fetch
exactly the missing blob.

Fix

Three layers, bottom-up:

  1. core/stateMissingCodeError{Addr, Hash} types the two "code is not
    found" paths in stateObject.Code/CodeSize (message unchanged, so
    witness-regen fixtures still match) and is wrapped into ExecuteStateless's
    result. A caller can now errors.As the missing hash, and tell a missing
    code (fetchable, content-addressed) from a missing trie node (an
    incomplete witness, not fetchable).

  2. eth/protocols/snapSyncer.FetchByteCodes fetches specific bytecodes
    from a connected snap peer outside the bulk sync loop, verifying each blob
    against its hash and failing over across peers. Responses route by a top-bit
    reqid (loop reqids are uint64(rand.Int63()), top bit clear, so the id
    spaces never collide). Peers are usable because Register/Unregister track
    them independently of a running Sync cycle — the stateless case, where the
    initial heal has already completed.

  3. eth/downloader — on a *state.MissingCodeError from a stateless batch
    import, fetch that content-addressed blob, persist it to the chain db, and
    retry the batch. Bounded by maxStatelessCodeHeals so a blob no peer can
    serve degrades to the existing safe failure instead of looping; a batch
    referencing several missing codes heals them across successive attempts. Any
    non-code failure is left to the caller's existing path untouched.

Test

  • core/state — the guard test (core: reject stateless blocks on incomplete state or code #2401) is extended to assert the hash
    round-trips via errors.As.
  • eth/protocols/snapTestFetchByteCodesOnDemand (well-behaving peer;
    unknown hash never fabricated) and TestFetchByteCodesFailsOverAndVerifies
    (fails over past a peer returning bytes that don't match the requested hash).
  • eth/downloaderTestRecoverMissingStatelessCode (a servable miss is
    fetched, verified and persisted; a non-code error and an unservable blob are
    both left for the safe failure path, writing nothing).

Scope / risk

Every fetched blob is verified content-addressed (keccak(code) == hash) before
it is persisted, so a malicious peer cannot inject wrong code — it simply fails
over. Recovery is bounded and safe-stops on an unservable code. Together with
#2401 this recovers any missing code at execution time regardless of how the
gap arose (heal incompleteness, crash window, or fast-forward), which is why it
is deployed ahead of any mechanism-specific prevention.

Branch 3 groundwork. Introduce state.MissingCodeError{Addr,Hash}, set on the
"code is not found" paths in stateObject.Code/CodeSize (message unchanged, so
witness-regen fixtures still match), and wrap it into ExecuteStateless's result
with multi-%w. A stateless caller can now errors.As the missing code hash and
fetch exactly that blob, and tell a missing code (fetchable) from a missing
state trie node (an incomplete witness, not fetchable). Extends the branch-1
test to assert the hash round-trips via errors.As.
Branch 3 groundwork. Syncer.FetchByteCodes fetches specific contract bytecodes
from a connected snap peer outside the bulk sync loop, verifying each blob
against its hash and failing over across peers. Responses are routed by a
top-bit reqid (loop reqids are uint64(rand.Int63()), top bit clear, so the id
spaces never collide) intercepted at the head of OnByteCodes. Peers are usable
because Register/Unregister track them by lifecycle, independent of a running
Sync cycle — which is the stateless self-heal case (heal already completed).

Tests cover a well-behaving peer, an unknown hash (never fabricated), and
failover past a peer that returns bytes not matching the requested hash.
Branch 3. When a stateless batch import fails with a *state.MissingCodeError
(a called contract's bytecode absent from local disk — WIT2 witnesses carry
no code), fetch exactly that content-addressed blob from a snap peer via
Syncer.FetchByteCodes, persist it to the chain db, and retry the batch. Bounded
by maxStatelessCodeHeals so a blob no peer can serve degrades to the existing
safe failure instead of looping; a batch referencing multiple missing codes
heals them across successive attempts. Any non-code failure is left to the
caller's existing path. Turns the mainnet-soak "code arrived later" self-heal
from eventual into immediate.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.40625% with 43 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (lmartins/stateless-incomplete-state-guard@7448950). Learn more about missing BASE report.

Files with missing lines Patch % Lines
eth/protocols/snap/ondemand.go 66.29% 23 Missing and 7 partials ⚠️
eth/downloader/bor_downloader.go 62.96% 9 Missing and 1 partial ⚠️
core/state/state_object.go 25.00% 3 Missing ⚠️

❌ Your patch check has failed because the patch coverage (66.40%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                             Coverage Diff                              @@
##             lmartins/stateless-incomplete-state-guard    #2402   +/-   ##
============================================================================
  Coverage                                             ?   55.60%           
============================================================================
  Files                                                ?      919           
  Lines                                                ?   167291           
  Branches                                             ?        0           
============================================================================
  Hits                                                 ?    93024           
  Misses                                               ?    68784           
  Partials                                             ?     5483           
Files with missing lines Coverage Δ
core/stateless.go 78.18% <100.00%> (ø)
eth/protocols/snap/sync.go 76.43% <100.00%> (ø)
core/state/state_object.go 87.27% <25.00%> (ø)
eth/downloader/bor_downloader.go 64.96% <62.96%> (ø)
eth/protocols/snap/ondemand.go 66.29% <66.29%> (ø)
Files with missing lines Coverage Δ
core/stateless.go 78.18% <100.00%> (ø)
eth/protocols/snap/sync.go 76.43% <100.00%> (ø)
core/state/state_object.go 87.27% <25.00%> (ø)
eth/downloader/bor_downloader.go 64.96% <62.96%> (ø)
eth/protocols/snap/ondemand.go 66.29% <66.29%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

1 participant