exec: fix partial block receipt reconstruction (#20452)#20467
Conversation
4b4a40a to
5940b86
Compare
There was a problem hiding this comment.
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 beforeFinalize. - 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.
yperbasis
left a comment
There was a problem hiding this comment.
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. |
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
5940b86 to
dd47724
Compare
Updated approach: shared
|
|
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
left a comment
There was a problem hiding this comment.
Issues
- 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.
- 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.
- 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.
|
@mh0lt do we need it in 3.4? |
…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>
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>
Summary
initialBlockTxOffset > 0), the receipts passed toengine.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.isPartialdetection in the parallel executor —tasks[0]is the system tx (TxIndex=-1), so the first user tx must be found by scanning forTxIndex >= 0.replayPriorTxsForReceiptsfunctions that use aHistoryReaderV3to re-execute prior transactions and reconstruct their receipts, prepending them before callingFinalize.exec3_parallel.go) and serial (exec3_serial.go) executors.Closes #20452
Test plan
make erigonandmake integrationbuild successfullymake lintpasses (run twice for non-determinism)go test -short ./execution/stagedsync/...passesinitialBlockTxOffset > 0and verify requests hash validation passes🤖 Generated with Claude Code