Skip to content

stagedsync: Fix logIndex reset and missing websocket notifications in parallel execution - #22110

Merged
yperbasis merged 7 commits into
erigontech:mainfrom
Sahil-4555:fix/parallel-log-index-inconsistency
Jul 3, 2026
Merged

yperbasis merged 7 commits into
erigontech:mainfrom
Sahil-4555:fix/parallel-log-index-inconsistency

Conversation

@Sahil-4555

@Sahil-4555 Sahil-4555 commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator

Issue

When Erigon is doing parallel block execution (EXEC3_PARALLEL=true), the state snapshots can end at step boundaries which fall mid-block. So when we restart the node or resume sync from these snapshots, the execution stage needs to start/resume execution from the middle of the block (running only the remaining transactions of that block).

While finalizing the transaction receipts for this partial block execution, the code in exec3_parallel.go was checking if txVersion.TxIndex > 0 && tx > 0 to fetch the previous transaction's receipt from the memory results map. But for the first transaction task of this new batch (where the local slice task index tx is 0, but the actual block-level index txVersion.TxIndex is > 0), this check was failing and prevReceipt remained nil. Because of this, it called CreateNextReceipt(nil) and the starting log index and cumulative gas for that mid-block transaction got reset to 0. This wrong log index got written directly to the database ReceiptDomain and caused inconsistent logIndex values when clients queried eth_getLogs.

Additionally, for partial blocks the websocket notification path was publishing tail-only receipts (only the resumed portion) via RecentReceipts.Add, which eth_subscribe log/receipt clients would receive as the complete block — diverging from eth_getLogs results.

Fix

The main fix is in the parallel executor's receipt finalization loop in exec3_parallel.go. We changed the finalize function signature to accept cumulativeGasUsed and firstLogIndex directly instead of a prevReceipt pointer, and it now calls CreateReceipt instead of CreateNextReceipt. This matches how the serial executor handles receipt creation. We also branch explicitly on tx == 0 (batch boundary) instead of checking prevReceipt == nil. When the first task in a batch has TxIndex > 0 (meaning we are resuming mid-block), we query the temporal database usingrawtemporaldb.ReceiptAsOf to fetch the previous transaction's cumulative gas used, cumulative blob gas used, and log index offset. We excluded IsBlockEnd tasks from this fallback since finalizeSystemTx never consumes prevReceipt anyway. Also added a two-step nil check on be.finalizedResults[tx-1] before accessing .Receipt since finalizedResults is a map and a missing key would return nil and cause a panic.

For the notification issue, we gated RecentReceipts.Add behind !applyResult.isPartial and moved it outside the block validation scope so that incomplete tail-only receipts are not published for partial blocks. We now use applyResult.Txs and applyResult.Header directly instead of fetching from the database. This is consistent with how the serial path gates behind startTxIndex == 0 in exec3_serial.go:421.

For the blob gas issue, we added a cumulativeBlobGasUsed field to execResult and snapshot be.blobGasUsed into it at finalization time. The publish loop now uses this per-result snapshot instead of reading be.blobGasUsed directly, which could have been overwritten by a later transaction. We also initialize be.blobGasUsed from the database cumBlobGasUsed value at the resume boundary.

Added TestParallelResumeBoundaryAndNotifications unit test to pin theresume boundary receipt reconstruction (first task with TxIndex > 0 at slice index 0 → correct offsets from DB) and the isPartial flag that controls notification skipping.

Closes: #22106

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

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 fixes receipt log-index continuity and websocket log notification dispatch in the staged sync parallel execution path, addressing incorrect logIndex values observed by eth_getLogs when execution resumes mid-block across batches/restarts.

Changes:

  • Fix receipt finalization when a batch starts mid-block by loading prior receipt state from ReceiptDomain via rawtemporaldb.ReceiptAsOf.
  • Adjust receipt notification enqueue logic to (intended) allow websocket log subscribers to receive notifications when a block completes even if execution started in a partial batch.

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

Comment thread execution/stagedsync/exec3_parallel.go Outdated

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

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread execution/stagedsync/exec3_parallel.go Outdated
@Sahil-4555
Sahil-4555 force-pushed the fix/parallel-log-index-inconsistency branch from 97116e5 to 62e28e2 Compare July 2, 2026 03:22
@sudeepdino008
sudeepdino008 self-requested a review July 2, 2026 07:52
@sudeepdino008

Copy link
Copy Markdown
Member

like when the batch limit is reached mid-block, or when we restart the node and resume sync

we don't "execute blocks incompletely". State snapshots can end at midblock however and so execution needs to resume from middle of the block.

@sudeepdino008

Copy link
Copy Markdown
Member

one weird thing:

  • prevReceipt is usually coming from current batch's execution, so it have logs.
  • when we use prevReceipt to construct next receipt, we do currentReceipt.FirstLogIndexWithinBlock = prevReceipt.FirstLogIndexWithinBlock + len(logs)
  • in edge case when we retrieve from db, receipt has no logs (rcache has it, receipt doesn't). Also, prevReceipt created with no logs.
  • Also, in this case, logIndexAfterTx retrieved from db is actually the index after adding logs...

but it works out in end
prevReceipt.FirstLogIndexWithinBlock = logIndexAfterTx. <-wrong assignment
but
currentReceipt.FirstLogIndexWithinBlock = prevReceipt.FirstLogIndexWithinBlock + len(logs)

logs is empty, so currentReceipt.FirstLogIndexWithinBlock gets set with the right value..

I'd suggest adding a comment about this


  • exec3_serial did it properly, by re-executing the prev tx and getting logs etc. But seems we don't need it.
  • I wanna get rid of the receipt version check, but bloatnet still has it. I think bloatnet is dead, but if it isn't - i'll regen the receipt and get rid of this check.

@Sahil-4555

Copy link
Copy Markdown
Collaborator Author

You are totally right that prevReceipt.FirstLogIndexWithinBlock = logIndexAfterTx isn’t technically the previous starting index, but since we set Logs = nil (so len(Logs) == 0) the calculation logIndexAfterTx + 0 gives the starting index for the next transaction. This allows us to reuse CreateNextReceipt directly, without extra DB fetching or re-executing the previous transaction.

I’ve added a clear code comment explaining this mathematical trick. Thanks for catching this!

@Sahil-4555
Sahil-4555 force-pushed the fix/parallel-log-index-inconsistency branch from 62e28e2 to 51a09e2 Compare July 2, 2026 10:20
Comment thread execution/stagedsync/exec3_parallel.go Outdated
Comment thread execution/stagedsync/exec3_parallel.go Outdated
@lupin012
lupin012 self-requested a review July 2, 2026 14:18
@AskAlexSharov
AskAlexSharov added this pull request to the merge queue Jul 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 2, 2026
@lupin012
lupin012 enabled auto-merge July 2, 2026 18:27
@lupin012
lupin012 disabled auto-merge July 2, 2026 18:30

@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.

The receipt-index reconstruction itself checks out, but a few things need fixing before merge:

  1. Partial blocks now notify with tail-only receipts (exec3_parallel.go:579). For an isPartial block, blockResult.Receipts contains only the resumed tail (built from be.results; the task producer skips the prefix), while Txs/Header are the full block's. RecentReceipts.Add stores this as the block's complete set, so eth_subscribe log/receipt clients get the tail presented as the whole block — diverging from eth_getLogs, replaying Removed on unwind for the tail only, and bypassing the !isPartial-gated receipts-root/bloom validation. Please either reconstruct the prefix (receipts.DerivePriorReceipts, as the serial path does for Finalize) or keep skipping partial blocks like serial does (startTxIndex == 0, exec3_serial.go:421); publishing an incomplete set is worse than either.

  2. The resume-at-block-end offset still gets zero notifications (exec3_parallel.go:2505). If the boundary falls between the last user tx and the block-end txNum, the resumed executor's only task is block-end: Receipts is empty, Add early-returns, and the completed block emits newHeads but no logs/receipts — the #22106 symptom remains. The fallback also runs for block-end tasks even though finalizeSystemTx never consumes prevReceipt; please exclude IsBlockEnd tasks.

  3. The legacy-schema check can never fire (exec3_parallel.go:2506). ReceiptStoresFirstLogIdx tests CurrentDomainVersion < v1.1, but the exec stage only ever sees {3,0} (schema default) or {1,1} (AdjustReceiptCurrentVersionIfNeeded pins legacy datadirs to exactly v1.1), so the guard is dead code — while 3.0-written data tails still pass through and get exactly the shifted indexes the error message claims to prevent. The underlying problem is the predicate threshold (arguably Less(V2_0); dates to #16677) — better to fix that than ship a check that suggests protection it doesn't provide.

  4. be.blobGasUsed = cumBlobGasUsed seeds from a slot the parallel executor writes per-tx, not cumulative (exec3_parallel.go:2525): the publish loop passes tx.GetBlobGas() as ApplyTxIndexes' cummulativeBlobGas arg (exec3_parallel.go:2740), unlike serial which passes the running cumulative (exec3_serial.go:556). On parallel-synced datadirs the seed is the previous tx's own blob gas (0 for non-blob txs). Latent today because the validator is skipped for partial blocks, but wrong — fix the write side to store the running cumulative, or drop the seed.

  5. Shape: branch explicitly on tx == 0 instead of prevReceipt == nil (the nil case is otherwise impossible; if that invariant ever breaks, this silently reconstructs from pre-batch DB state instead of failing loudly), and consider passing the two scalars through finalize into the existing CreateReceipt like serial does (exec3_serial.go:452-461) — finalize has a single call site — rather than a synthetic receipt whose FirstLogIndexWithinBlock holds the next tx's index. That also removes the need for the 7-line comment (which restates CreateNextReceipt's math and already drifts from it); the // Initialize cumulative blob gas... comment just restates the assignment and can go.

  6. Per repo guidelines (CLAUDE.md, TDD for bug fixes), please add a test pinning the resume boundary (first task with TxIndex > 0 at slice index 0 → reconstructed receipt values) and the notification behavior — nothing currently prevents a regression of the #22106 corruption.

@Sahil-4555
Sahil-4555 force-pushed the fix/parallel-log-index-inconsistency branch from 548f3be to 190a5aa Compare July 3, 2026 01:01
@Sahil-4555

Copy link
Copy Markdown
Collaborator Author

Hi @yperbasis,

Thanks for the detailed feedback. I have gone through all your points and done the needful changes. Please find the updates below:

  1. Partial block notifications: We are now skipping notifications for partial blocks completely, same like how the serial execution path is doing. We check !applyResult.isPartial before calling RecentReceipts.Add now.
  2. Excluding block-end: Excluded the IsBlockEnd tasks from the fallback database query since finalize system tx does not need it anyway.
  3. Dead code removal: Removed the legacy-schema check completely as it was dead code and not firing.
  4. Blob gas seed: We initialized be.blobGasUsed from the db cumBlobGasUsed as per the comment.
  5. Code shape: We changed the branching logic to check tx > 0 directly instead of checking if receipt is nil. Also, we are passing the offset values directly into CreateReceipt via finalize now, and cleaned up those long comments.
  6. Unit test: Added TestParallelResumeBoundaryAndNotifications unit test to verify this boundary fallback and partial block notification skipping. All tests are passing fine.

Please review and let me know if it looks good now.

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

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

Comment thread execution/stagedsync/exec3_parallel.go
Comment thread execution/stagedsync/exec3_parallel.go
Comment thread execution/stagedsync/exec3_parallel_test.go
Comment thread execution/stagedsync/exec3_parallel_test.go
Comment thread execution/stagedsync/exec3_parallel.go
auto-merge was automatically disabled July 3, 2026 08:31

Head branch was pushed to by a user without write access

@Sahil-4555
Sahil-4555 force-pushed the fix/parallel-log-index-inconsistency branch from 2e23ca9 to 484611a Compare July 3, 2026 08:31
@yperbasis
yperbasis requested a review from Copilot July 3, 2026 10:54

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

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

Comment thread execution/stagedsync/exec3_parallel.go
Comment thread execution/stagedsync/exec3_parallel.go
@yperbasis
yperbasis enabled auto-merge July 3, 2026 11:43
@yperbasis
yperbasis added this pull request to the merge queue Jul 3, 2026
Merged via the queue into erigontech:main with commit a01ede3 Jul 3, 2026
93 checks passed
@Sahil-4555
Sahil-4555 deleted the fix/parallel-log-index-inconsistency branch July 4, 2026 15:42
sudeepdino008 pushed a commit that referenced this pull request Jul 6, 2026
… parallel execution (#22110)

### Issue

When Erigon is doing parallel block execution (`EXEC3_PARALLEL=true`),
the state snapshots can end at step boundaries which fall mid-block. So
when we restart the node or resume sync from these snapshots, the
execution stage needs to start/resume execution from the middle of the
block (running only the remaining transactions of that block).

While finalizing the transaction receipts for this partial block
execution, the code in `exec3_parallel.go` was checking `if
txVersion.TxIndex > 0 && tx > 0` to fetch the previous transaction's
receipt from the memory results map. But for the first transaction task
of this new batch (where the local slice task index `tx` is `0`, but the
actual block-level index `txVersion.TxIndex` is `> 0`), this check was
failing and `prevReceipt` remained `nil`. Because of this, it called
`CreateNextReceipt(nil)` and the starting log index and cumulative gas
for that mid-block transaction got reset to `0`. This wrong log index
got written directly to the database `ReceiptDomain` and caused
inconsistent `logIndex` values when clients queried `eth_getLogs`.

Additionally, for partial blocks the websocket notification path was
publishing tail-only receipts (only the resumed portion) via
`RecentReceipts.Add`, which `eth_subscribe` log/receipt clients would
receive as the complete block — diverging from `eth_getLogs` results.

### Fix

The main fix is in the parallel executor's receipt finalization loop in
`exec3_parallel.go`. We changed the `finalize` function signature to
accept `cumulativeGasUsed` and `firstLogIndex` directly instead of a
`prevReceipt` pointer, and it now calls `CreateReceipt` instead of
`CreateNextReceipt`. This matches how the serial executor handles
receipt creation. We also branch explicitly on `tx == 0` (batch
boundary) instead of checking `prevReceipt == nil`. When the first task
in a batch has `TxIndex > 0` (meaning we are resuming mid-block), we
query the temporal database using`rawtemporaldb.ReceiptAsOf` to fetch
the previous transaction's cumulative gas used, cumulative blob gas
used, and log index offset. We excluded `IsBlockEnd` tasks from this
fallback since `finalizeSystemTx` never consumes `prevReceipt` anyway.
Also added a two-step nil check on `be.finalizedResults[tx-1]` before
accessing `.Receipt` since `finalizedResults` is a map and a missing key
would return nil and cause a panic.

For the notification issue, we gated `RecentReceipts.Add` behind
`!applyResult.isPartial` and moved it outside the block validation scope
so that incomplete tail-only receipts are not published for partial
blocks. We now use `applyResult.Txs` and `applyResult.Header` directly
instead of fetching from the database. This is consistent with how the
serial path gates behind `startTxIndex == 0` in `exec3_serial.go:421`.

For the blob gas issue, we added a `cumulativeBlobGasUsed` field to
`execResult` and snapshot `be.blobGasUsed` into it at finalization time.
The publish loop now uses this per-result snapshot instead of reading
`be.blobGasUsed` directly, which could have been overwritten by a later
transaction. We also initialize `be.blobGasUsed` from the database
`cumBlobGasUsed` value at the resume boundary.

Added `TestParallelResumeBoundaryAndNotifications` unit test to pin
theresume boundary receipt reconstruction (first task with `TxIndex > 0`
at slice index 0 → correct offsets from DB) and the `isPartial` flag
that controls notification skipping.

Closes: #22106
(cherry picked from commit a01ede3)
AskAlexSharov pushed a commit that referenced this pull request Jul 6, 2026
…arallel execution (#22155)

Cherry-pick of #22110 (merged squash commit a01ede3) to release/3.5.

Co-authored-by: Sahil Sojitra <88416181+Sahil-4555@users.noreply.github.com>
pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Jul 6, 2026
…s, blooms, blob gas (erigontech#22235)

Follow-up to erigontech#22110. Completes mid-block resume handling in both
executors and extends the erigontech#21332 receipt-bloom reuse to the remaining
call sites.

## Changes

### 1. Prefix receipts for resumed blocks (parallel executor)

`blockExecutor.nextResult` passed only the resumed batch's receipts to
`engine.Finalize`. Post-Prague, `Merge.Finalize` derives EIP-6110
deposit requests from the receipts' logs and validates
`header.RequestsHash` internally (the `!isPartial` skip of
`blockValidator` does not cover it), and AuRa's `Finalize` reads receipt
logs unconditionally (epoch-end signalling) — so a mid-block resume of a
block whose already-executed prefix emitted relevant logs would reject a
valid block as `ErrInvalidBlock`. Both executors now re-derive the
prefix receipts through a shared `txExecutor.reconstructPriorReceipts`
helper wrapping `receipts.DerivePriorReceipts` (RCacheV2 read first,
replay fallback), so `Finalize` sees the full set.

A reconstruction failure fails the batch with a retryable error rather
than proceeding into a receipts-dependent `Finalize` that would
misclassify the valid block as invalid (parallel: a false INVALID
verdict to the CL plus bad-header-LRU poisoning; serial: a `BadBlock`
unwind). Serial's plain-error path unwinds with `ExecUnwind` and
self-heals by re-executing the block from its start.

### 2. Notifications for resumed blocks (both executors)

With the prefix reconstructed the full receipt set is available, so
resumed blocks now reach `RecentReceipts.Add`: `blockResult` carries
`receiptsComplete` (true for full blocks, successfully reconstructed
partial ones, and resumes at tx index 0) and the apply loop gates `Add`
on it instead of `!isPartial`; the serial path mirrors this with
`priorComplete`/`finalizeReceipts`. This delivers the remaining half of
erigontech#22106 — the notification dispatcher has no DB fallback, so a block
absent from the cache never reaches websocket log/receipt subscribers —
including the resume-at-block-end shape, where the completed block
emitted newHeads but no log notifications.

Published partial-block sets are completed via `receipts.DeriveFields`,
which fills `Bloom` when missing: the post-exec block validator —
otherwise the only bloom filler on this path — is skipped for partial
blocks, so resumed-block receipts would reach
`eth_subscribe("transactionReceipts")` / `eth_sendRawTransactionSync`
with `logsBloom = 0x0`.

### 3. Fail loudly on a missing in-memory prev receipt

A missing `finalizedResults[tx-1]` receipt in the parallel finalize path
is an error instead of silently persisting zero
`cumulativeGasUsed`/`firstLogIndex` — the corruption class this PR
eliminates. In-order finalization makes it unreachable today. Falling
through to `ReceiptAsOf` would not be safe there: the DB cannot see this
batch's receipts, which are still unflushed in `SharedDomains.mem`.
Non-chain task types (test/benchmark tasks) are excluded from the
offsets lookup — `finalize()` legitimately creates no receipts for them.

### 4. Serial executor: seed cumulative blob gas on resume

Serial's resume branch folds `ReceiptAsOf`'s pre-resume `cumBlobGasUsed`
into `se.blobGasUsed` (which restarts at 0), so `ApplyTxIndexes`
persists correct cumulative blob gas for the whole resumed tail —
matching the parallel executor's boundary seeding and producing the
values a later resume's fallback reads.

### 5. Receipt-bloom reuse (extends erigontech#21332)

Per-receipt blooms are computed once and reused instead of re-hashing
the same logs:

- `types.Receipts.MergedBloom()` ORs per-receipt blooms.
- `ExecuteBlockEphemerally` derives the block bloom via `MergedBloom` —
`ApplyTransaction` populated every receipt's bloom, so
`CreateBloom(receipts)` hashed all logs a second time. Mainly benefits
the verify snapshot/history re-execution tools and t8n.
- `ethutils.MarshalReceipt` reuses `receipt.Bloom` when set — previously
every `eth_getTransactionReceipt` / `eth_getBlockReceipts` response
re-hashed each receipt's logs — and hashes logs only for sources that
leave it unset.
- `receipts.DeriveFields` fills `Bloom` only when missing, so
replay-produced receipts are not re-hashed.

`NewBlock`'s `CreateBloom(receipts)` stays as-is: its callers do not
guarantee per-receipt blooms. `DefaultBlockPostValidation` already
carries erigontech#21332.

### 6. Cleanup

- `txResult.blobGasUsed` was write-only; removed.
- The resume tests share `newResumeTestDB` / `seedResumeTestDB` /
`newResumeTestExec` helpers instead of ~45 duplicated scaffold lines
each.

## Tests

- `TestParallelResumeReconstructsPriorReceipts` — prefix receipts
re-derived with correct offsets (21000 / 31000); block marked
`receiptsComplete`.
- `TestParallelResumeReconstructionFailureErrors` — reconstruction
failure fails the batch instead of degrading.
- `TestParallelFinalizeMissingPrevReceiptErrors` — missing prev receipt
errors instead of zero offsets.
- `TestParallelResumeBoundaryOffsets` — resume-boundary `ReceiptAsOf`
fallback offsets; `receiptsComplete == false` when reconstruction is
skipped.
- `TestDeriveFieldsSetsBloom`, `TestDeriveFieldsPreservesExistingBloom`,
`TestMarshalReceiptReusesReceiptBloom`, `TestReceiptsMergedBloom` —
bloom fill and reuse.
- The serial half of items 1, 2 and 4 has no unit harness that drives
`serialExecutor` directly; it mirrors the tested parallel logic.
Flagging that explicitly rather than pretending coverage — suggestions
welcome.

## Deliberately not addressed

- `blockValidator` (receipts-root/bloom) stays skipped for partial
blocks: `be.blockGasUsed` and `ApplyCount` are still tail-scoped, so
enabling it needs counter reconstruction, not just receipts. Tracked in
erigontech#22237.
- The notification dispatcher still has no fallback for cache misses it
can hit for other reasons (eviction, fork-validator `Clear`); a
dispatcher-side regeneration would be the deeper fix for those classes.
Tracked in erigontech#22240.
- The `ReceiptStoresFirstLogIdx` predicate threshold (v1.1 vs v2.0,
dating to erigontech#16677) — separate pre-existing issue.
- Datadirs written by the pre-erigontech#22110 parallel executor hold per-tx
values in the cumulative blob-gas column; nothing migrates them.
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.

eth_getLogs returns inconsistent log indexes

5 participants