Skip to content

core: reject stateless blocks on incomplete state or code - #2401

Draft
lucca30 wants to merge 1 commit into
developfrom
lmartins/stateless-incomplete-state-guard
Draft

core: reject stateless blocks on incomplete state or code#2401
lucca30 wants to merge 1 commit into
developfrom
lmartins/stateless-incomplete-state-guard

Conversation

@lucca30

@lucca30 lucca30 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

A WIT2 stateless node reads contract code from its local disk — witnesses
carry no code. When a called contract's bytecode is absent (a bytecode-heal /
fast-forward completeness gap), StateDB records a sticky error and serves the
read as phantom-zero, so execution silently continues against wrong state.
ExecuteStateless never consulted db.Error(), so the miss surfaced only as a
misleading ErrGasUsedMismatch — or, when the divergent execution then hit
"gas limit reached", as that Process error — masquerading as consensus
divergence.

Observed on mainnet-soak: block 93472327 diverged (gasUsed 46,716,714 vs
47,007,392) because code 0xf98d41f7…e56e64 was missing from local disk. The
true cause was invisible in the logs.

Fix

Check db.Error() in ExecuteStateless immediately after Process, before
the Process-error branch — a failed state read invalidates the computed
result, so it is the true cause regardless of which downstream symptom fires
first. On a hit, return a new ErrStatelessIncompleteState sentinel wrapping the
underlying error, and bump a chain/stateless/incomplete_state meter so a real
occurrence is visible fleet-wide.

The test-only serial replay (executeStatelessSerial) already gated on
db.Error(); this brings the production path in line.

Test

TestExecuteStatelessRejectsMissingCode drives the real ExecuteStateless over
a mainnet witness fixture, removes each contract code the block reads, and
asserts ErrStatelessIncompleteState naming the hash — never a gas mismatch, a
Process symptom, or a silent pass. Writing it caught a real ordering bug in
the fix: a missing code first surfaced as "gas limit reached" from Process,
which is exactly why the db.Error() check must precede the Process-error
branch.

Scope / risk

Stateless-verification path only. A valid block whose code is fully present is
unaffected: db.Error() is nil and the existing ValidateState path runs
unchanged. This turns a silently mis-attributed failure into an explicit,
metered rejection — it does not change which valid blocks are accepted.

This is the mechanism-agnostic detection half of the fix; the companion PR adds
peer self-heal so a detected miss recovers instead of stalling.

…king it

A WIT2 stateless node reads contract code from local disk (witnesses carry no
code). When a called contract's bytecode is absent — a bytecode-heal /
fast-forward completeness gap — StateDB records a sticky error and nil-serves
the read, so execution continues against phantom-zero state. ExecuteStateless
never consulted db.Error(), so the miss surfaced only as a misleading
ErrGasUsedMismatch (or, when the divergent execution then hit "gas limit
reached", as that Process error), masquerading as consensus divergence.

Check db.Error() in ExecuteStateless immediately after Process and prefer it
over any Process error, returning ErrStatelessIncompleteState wrapping the
missing hash. A state-read failure invalidates the computed result, so this is
the true cause regardless of which downstream symptom fires first. Add a
chain/stateless/incomplete_state meter so a real occurrence is visible
fleet-wide. The test-only serial replay (executeStatelessSerial) already gated
on db.Error(); this makes the production path do the same.

Test drives the real ExecuteStateless over a mainnet witness fixture, removes
each contract code the block reads, and asserts ErrStatelessIncompleteState
naming the hash — never a gas mismatch, a Process symptom, or a silent pass.
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 55.60%. Comparing base (aae7e66) to head (7448950).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop    #2401   +/-   ##
========================================
  Coverage    55.59%   55.60%           
========================================
  Files          918      918           
  Lines       167145   167167   +22     
========================================
+ Hits         92926    92950   +24     
- Misses       68740    68747    +7     
+ Partials      5479     5470    -9     
Files with missing lines Coverage Δ
core/stateless.go 78.18% <100.00%> (+14.54%) ⬆️

... and 24 files with indirect coverage changes

Files with missing lines Coverage Δ
core/stateless.go 78.18% <100.00%> (+14.54%) ⬆️

... and 24 files with indirect coverage changes

🚀 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