Skip to content

stagedsync, execmodule: halt on invalid block during parallel initial sync - #19870

Merged
mh0lt merged 3 commits into
mainfrom
fix/halt-on-invalid-block-initial-sync
Mar 15, 2026
Merged

stagedsync, execmodule: halt on invalid block during parallel initial sync#19870
mh0lt merged 3 commits into
mainfrom
fix/halt-on-invalid-block-initial-sync

Conversation

@mh0lt

@mh0lt mh0lt commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add missing ErrInvalidBlock handling in parallel executor (parity with serial path at exec3_serial.go:460-475): ReportBadHeaderPoS, badBlockHalt check, and UnwindTo
  • Merge pe.wait() errors into execErr so executor goroutine errors and rw goroutine errors converge into a single handling path
  • Early return on ErrInvalidBlock before the step-frozen commitment check, preventing the original error from being masked
  • Halt process in exec module (ProcessFrozenBlocks) and stop stage loop on ErrInvalidBlock during initial sync

Important: All halt behavior is gated on dbg.Exec3Parallel (EXEC3_PARALLEL env var) to avoid changing serial flow behavior. The serial path is unaffected.

Context

When the parallel executor hits an invalid block during initial sync (e.g. receiptHash mismatch), it previously entered an infinite retry loop — the stage loop caught the error, slept 500ms, and retried the same block. Meanwhile Caplin continued downloading, pushing its backward target further back. The process needed to be killed manually.

Test plan

  • make lint passes
  • make erigon integration builds clean
  • Verify on mainnet parallel node: set BAD_BLOCK_HALT=true EXEC3_PARALLEL=true, inject bad block → process exits cleanly with error log
  • Verify serial path unchanged: same scenario without EXEC3_PARALLEL → existing retry behavior preserved

🤖 Generated with Claude Code

… sync

The parallel executor was missing error handling for ErrInvalidBlock,
causing an infinite retry loop that pushed Caplin's backward target
further back. This adds:

- ReportBadHeaderPoS, badBlockHalt, and UnwindTo handling in parallel
  executor (parity with serial at exec3_serial.go:460-475)
- Merge pe.wait() errors into execErr so both error paths converge
- Early return on ErrInvalidBlock before step-frozen check to prevent
  error masking
- Process halt in exec module and stage loop on ErrInvalidBlock

All halt behavior is gated on dbg.Exec3Parallel to avoid changing
serial flow behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the parallel execution (ExecV3 / exec3_parallel) initial-sync error path so that rules.ErrInvalidBlock is handled deterministically (halt/unwind/report) instead of being retried indefinitely, and ensures invalid-block errors aren’t masked by later commitment/“step frozen” checks.

Changes:

  • Stop the staged sync loop on ErrInvalidBlock during parallel initial sync to avoid infinite retry.
  • Add missing ErrInvalidBlock handling in the parallel executor (PoS bad-header report, optional halt, unwind).
  • Preserve the original ErrInvalidBlock by returning it before the step-frozen commitment check.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
execution/stagedsync/stageloop/stageloop.go Stops the stage loop on ErrInvalidBlock during parallel initial cycle.
execution/stagedsync/exec3_parallel.go Merges wait() errors into execErr and adds invalid-block-specific handling (report/halt/unwind).
execution/stagedsync/exec3.go Early-return on ErrInvalidBlock to prevent masking by commitment “step frozen” logic.
execution/execmodule/exec_module.go Halts the process on ErrInvalidBlock during startup frozen-block processing in parallel mode.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread execution/stagedsync/exec3_parallel.go Outdated
Comment on lines +498 to +499
if err := u.UnwindTo(lastHeader.Number.Uint64()-1, BadBlock(lastHeader.Hash(), execErr), rwTx); err != nil {
return nil, rwTx, err
Comment thread execution/execmodule/exec_module.go Outdated
Comment on lines +503 to +506
// Exit the process so the operator can investigate.
if dbg.Exec3Parallel && errors.Is(err, rules.ErrInvalidBlock) {
e.logger.Error("Invalid block during parallel initial sync — halting process")
os.Exit(1)
Comment thread execution/stagedsync/exec3_parallel.go Outdated

if execErr != nil {
if !(errors.Is(execErr, context.Canceled) || errors.Is(execErr, &ErrLoopExhausted{})) {
pe.logger.Warn(fmt.Sprintf("[%s] Execution failed", pe.logPrefix), "err", execErr)
Comment on lines +478 to +480
if execErr == nil {
execErr = waitErr
}
- Replace os.Exit(1) with stopNode callback for clean shutdown
- Add underflow guard on unwind-to block number
- Add block number/hash context to execution failure log
- Combine execErr and waitErr with errors.Join instead of dropping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mh0lt
mh0lt merged commit 47546d2 into main Mar 15, 2026
44 of 45 checks passed
@mh0lt
mh0lt deleted the fix/halt-on-invalid-block-initial-sync branch March 15, 2026 14:07
lupin012 pushed a commit that referenced this pull request Mar 17, 2026
… sync (#19870)

## Summary

- Add missing `ErrInvalidBlock` handling in parallel executor (parity
with serial path at `exec3_serial.go:460-475`): `ReportBadHeaderPoS`,
`badBlockHalt` check, and `UnwindTo`
- Merge `pe.wait()` errors into `execErr` so executor goroutine errors
and rw goroutine errors converge into a single handling path
- Early return on `ErrInvalidBlock` before the step-frozen commitment
check, preventing the original error from being masked
- Halt process in exec module (`ProcessFrozenBlocks`) and stop stage
loop on `ErrInvalidBlock` during initial sync

**Important:** All halt behavior is gated on `dbg.Exec3Parallel`
(`EXEC3_PARALLEL` env var) to avoid changing serial flow behavior. The
serial path is unaffected.

## Context

When the parallel executor hits an invalid block during initial sync
(e.g. receiptHash mismatch), it previously entered an infinite retry
loop — the stage loop caught the error, slept 500ms, and retried the
same block. Meanwhile Caplin continued downloading, pushing its backward
target further back. The process needed to be killed manually.

## Test plan

- [x] `make lint` passes
- [x] `make erigon integration` builds clean
- [ ] Verify on mainnet parallel node: set `BAD_BLOCK_HALT=true
EXEC3_PARALLEL=true`, inject bad block → process exits cleanly with
error log
- [ ] Verify serial path unchanged: same scenario without
`EXEC3_PARALLEL` → existing retry behavior preserved

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Andrew Ashikhmin <34320705+yperbasis@users.noreply.github.com>
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.

3 participants