Skip to content

exec: fix partial block receipt reconstruction (#20452)#20467

Merged
mh0lt merged 2 commits into
mainfrom
fix/partial-block-receipts
Apr 13, 2026
Merged

exec: fix partial block receipt reconstruction (#20452)#20467
mh0lt merged 2 commits into
mainfrom
fix/partial-block-receipts

Conversation

@mh0lt

@mh0lt mh0lt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • When execution resumes from a snapshot boundary mid-block (initialBlockTxOffset > 0), the receipts passed to engine.Finalize() only contained receipts from the re-executed portion. This causes Pectra requests hash validation to fail because deposit request extraction needs ALL receipt logs.
  • Fixes the isPartial detection in the parallel executor — tasks[0] is the system tx (TxIndex=-1), so the first user tx must be found by scanning for TxIndex >= 0.
  • Adds replayPriorTxsForReceipts functions that use a HistoryReaderV3 to re-execute prior transactions and reconstruct their receipts, prepending them before calling Finalize.
  • Applied to both parallel (exec3_parallel.go) and serial (exec3_serial.go) executors.

Closes #20452

Test plan

  • make erigon and make integration build successfully
  • make lint passes (run twice for non-determinism)
  • go test -short ./execution/stagedsync/... passes
  • Run node through a Pectra-era snapshot boundary where initialBlockTxOffset > 0 and verify requests hash validation passes
  • Verify deposit request extraction from logs includes all block transactions

🤖 Generated with Claude Code

@mh0lt mh0lt requested a review from yperbasis as a code owner April 10, 2026 08:40
@mh0lt mh0lt force-pushed the fix/partial-block-receipts branch 2 times, most recently from 4b4a40a to 5940b86 Compare April 10, 2026 09:09
@mh0lt mh0lt requested a review from taratorio April 10, 2026 09:10
@yperbasis yperbasis requested a review from Copilot April 10, 2026 10:24

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

Fixes incorrect receipt handling when execution resumes mid-block from a snapshot boundary, ensuring engine.Finalize() receives a complete receipt list so Pectra requests hash validation (deposit log extraction) works correctly.

Changes:

  • Detect partial blocks correctly in the parallel executor by locating the first user transaction (TxIndex >= 0).
  • Reconstruct missing prior receipts for partial blocks by replaying earlier transactions with a HistoryReaderV3, then prepend them before Finalize.
  • Apply the receipts reconstruction logic to both serial and parallel executors.

Reviewed changes

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

File Description
execution/stagedsync/exec3_serial.go Prepends reconstructed prior receipts before calling engine.Finalize() when resuming mid-block.
execution/stagedsync/exec3_parallel.go Fixes partial-block detection, prepends reconstructed prior receipts for Finalize, and adds shared replay helper.

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

Comment thread execution/stagedsync/exec3_serial.go Outdated
Comment thread execution/stagedsync/exec3_parallel.go Outdated
Comment thread execution/stagedsync/exec3_parallel.go Outdated

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why replay transactions instead of looking up receipts?

@mh0lt

mh0lt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

Why replay transactions instead of looking up receipts?

Why replay transactions instead of looking up receipts?

As we only do this once at start-up I thought it was safer to just process the transactions. They will always be there, if we start doing look-ups I think we'll need all of the code in: receipts.Generator. Which would then need to be ported into execution and joiuntly referenced.

Its probably a better fix as then we have a common path for reciept generation.

Would you prefer me to do this instead.

@yperbasis

Copy link
Copy Markdown
Member

As we only do this once at start-up I thought it was safer to just process the transactions. They will always be there, if we start doing look-ups I think we'll need all of the code in: receipts.Generator. Which would then need to be ported into execution and joiuntly referenced.

Its probably a better fix as then we have a common path for reciept generation.

Would you prefer me to do this instead.

Yes, that sounds like a more robust approach.

When execution resumes mid-block from a snapshot boundary, Finalize
needs the complete receipt set for requests hash computation (deposit
extraction from logs). Previously this was attempted with inline
transaction replay that had multiple bugs (wrong task, unsafe type
assertion, missing Release).

Fix: extract receipt derivation into execution/receipts/ — a shared
package that both the execution pipeline and RPC layer can use. The
new receipts.DerivePriorReceipts() replays transactions 0..startIdx-1
using a HistoryReaderV3 and returns their receipts.

Both serial and parallel executors now call DerivePriorReceipts when
startTxIndex > 0, prepending the result before passing to Finalize.

Fixes #20452
@mh0lt mh0lt force-pushed the fix/partial-block-receipts branch from 5940b86 to dd47724 Compare April 10, 2026 16:56
@mh0lt

mh0lt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

Updated approach: shared execution/receipts/ package

Replaced the inline replayPriorTxsForReceipts() with a shared derivation package at execution/receipts/. This addresses @yperbasis's feedback about using a common receipt generation path, and fixes the three bugs identified in review (wrong task, unsafe type assertion, missing Release).

Current structure (this PR)

execution/receipts/derive.go
├── DeriveForRange(ctx, cfg, engine, header, txns, from, to, ibs, gp, getHeader) → Receipts
├── DeriveBlockReceipts(...)    — convenience: DeriveForRange(0, len(txns))
└── DerivePriorReceipts(...)    — convenience: DeriveForRange(0, startTxIndex)

Both exec3_serial.go and exec3_parallel.go call DerivePriorReceipts when startTxIndex > 0 to reconstruct the missing receipts before Finalize.

Planned follow-up (separate PR)

Wire rpc/jsonrpc/receipts/Generator to call execution/receipts.DeriveForRange() on cache miss, replacing the inline replay in GetReceipt() / GetReceipts(). This gives a single replay implementation used by both the execution pipeline and RPC layer:

execution/receipts/              ← shared derivation (no caching)
├── used by execution/stagedsync ← partial block prior receipts
└── used by rpc/jsonrpc/receipts ← RPC receipt queries (wrapped with LRU + RCacheV2)

@mh0lt

mh0lt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up PR for the RPC Generator wire-up + RCacheV2 fast path: #20485

- serial: derive blockStartTxNum from first user tx task instead of block-end task
- parallel: safe type switch for result.Task instead of panic-prone double assertion
- parallel: add defer ibs.Release(true) to prevent pooled resource leak in finalize closure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@yperbasis yperbasis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Issues

  1. Silent error swallowing defeats the fix (Medium)

Both executors log-and-continue when prior receipt reconstruction fails:

if priorErr != nil {
pe.logger.Warn("[parallel] failed to reconstruct prior receipts for partial block", ...)
} else {
finalizeReceipts = append(priorReceipts, blockReceipts...)
}

If reconstruction fails, Finalize is called with the same incomplete receipts that caused #20452. The block will fail requests hash validation anyway, but with a confusing "requests hash mismatch" error instead
of the clear "failed to reconstruct prior receipts" message. Consider returning the error rather than swallowing it — if this code path is hit, continuing with partial receipts is the exact bug being fixed.

  1. Serial executor has an unchecked type assertion (Low)

exec3_serial.go:
firstTask := tasks[0].(*exec.TxTask)

The parallel executor uses a safe type switch for the same operation. In practice tasks[0] is always *exec.TxTask in the serial path, but it's inconsistent. A tasks[0].(exec.Task) → check pattern would be more
defensive.

  1. DeriveForRange warmup path is untested (Low)

The warmup loop (replay 0..fromIdx-1 before collecting fromIdx..toIdx-1) only activates when fromIdx > 0. Current callers (DerivePriorReceipts, DeriveBlockReceipts) always pass fromIdx=0, so this code path is
dead for now. Fine for the API design, but worth noting it's untested.

@AskAlexSharov

Copy link
Copy Markdown
Collaborator

@mh0lt do we need it in 3.4?

@mh0lt mh0lt added this pull request to the merge queue Apr 13, 2026
Merged via the queue into main with commit 50fd650 Apr 13, 2026
35 checks passed
@mh0lt mh0lt deleted the fix/partial-block-receipts branch April 13, 2026 17:55
github-merge-queue Bot pushed a commit that referenced this pull request Apr 16, 2026
…heV2 fast path (#20485)

## Summary

Follow-up to #20467. Wires the RPC receipts Generator to the shared
`execution/receipts/` package and adds an RCacheV2 fast path in the
execution pipeline.

### Changes

**`rpc/jsonrpc/receipts/receipts_generator.go`**
- `GetReceipts` post-Byzantium path: replaced inline replay loop with
`execution/receipts.DeriveBlockReceipts()` call
- Pre-Byzantium path (commitment history for post-state root): stays
inline — specific to RPC
- RPC layer now only owns LRU caching and concurrency control

**`execution/receipts/derive.go`**
- `DerivePriorReceipts`: tries RCacheV2 (persistent receipt cache) first
before falling back to transaction replay
- If all prior receipts are already cached from a previous execution
run, no replay is needed

### Final structure

```
execution/receipts/derive.go              ← shared derivation + RCacheV2 lookup
├── DeriveForRange()                      ← core replay (no caching)
├── DeriveBlockReceipts()                 ← convenience: full block
└── DerivePriorReceipts()                 ← RCacheV2 fast path, then replay fallback

execution/stagedsync/exec3_serial.go      ← calls DerivePriorReceipts for partial blocks
execution/stagedsync/exec3_parallel.go    ← calls DerivePriorReceipts for partial blocks

rpc/jsonrpc/receipts/receipts_generator.go
├── GetReceipts() post-Byzantium          ← calls DeriveBlockReceipts (+ LRU cache)
└── GetReceipts() pre-Byzantium           ← inline replay with commitment history
```

## Test plan

- [x] `make lint` passes
- [x] `make erigon integration` builds
- [ ] CI passes

**Depends on:** #20467 (partial block receipt fix)

---------

Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AskAlexSharov added a commit that referenced this pull request Apr 28, 2026
Cherry-pick of #20467 to release/3.4.

When execution resumes from a snapshot boundary mid-block
(`initialBlockTxOffset > 0`), the receipts passed to `engine.Finalize()`
only contained receipts from the re-executed portion. This causes Pectra
requests hash validation to fail because deposit request extraction
needs ALL receipt logs.

Reproduces as: `invalid requests root hash in header` at block 24966723
on mainnet re-sync.

Co-authored-by: Mark Holt <135143369+mh0lt@users.noreply.github.com>
Co-authored-by: Mark Holt <erigon@dev-bm-e3-ethmainnet-n4.erigon.io>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exec: partial block at snapshot boundary passes incomplete receipts to Finalize

4 participants