Skip to content

execution/stagedsync: prune in-RAM overlay when execution unwind is a disk no-op - #21824

Merged
yperbasis merged 1 commit into
release/3.4from
jklondon/fix_unwind_overlay_prune_34
Jun 16, 2026
Merged

yperbasis merged 1 commit into
release/3.4from
jklondon/fix_unwind_overlay_prune_34

Conversation

@JkLondon

Copy link
Copy Markdown
Contributor

What

Fixes a gas used by execution mismatch that puts the node into an infinite
unwind/retry loop. Observed on the Hoodi snapshotter (e3.4.3, serial batch
execution) stuck on block 3004265:

gas used mismatch block=3004265 header=58278345 execution=58257300 diff=-21045 txCount=45

Root cause

#20625 / #21538 correctly prunes the in-RAM TemporalMemBatch overlay by
txNum inside TemporalMemBatch.Unwind (for accounts and storage). That
prune is reached from unwindExec3.

The gap is one level up, in UnwindExecutionStage:

if u.UnwindPoint >= s.BlockNumber {
    return nil   // early return — unwindExec3 (and thus sd.Unwind) never runs
}

When a block fails its post-execution gas check mid-batch, its writes are
already in the overlay but the block was never committed, so the committed
execution-stage progress s.BlockNumber is at or below the unwind point and
this early return fires. The overlay prune is skipped entirely. Because the
same SharedDomains is reused across the unwind→retry loop inside one
sync.Run, the stale write survives:

  • tx19 of 3004265 does a first-time SSTORE to ca5daf64 slot0 → overlay holds
    …a3a34 at tx19's txNum.
  • block fails the gas check → UnwindTo(3004264)UnwindExecutionStage
    no-ops (3004264 ≥ committed progress) → overlay not pruned.
  • retry: tx19 reads the stale …a3a34 instead of the committed 0, takes the
    "already initialised" branch, skips the SSTORE_SET (20000 gas) + compute →
    block is 21045 gas short → fails again → loops forever.

The committed DB is correct (eth_getStorageAt ca5daf64 0x0 = 0x0); only the
in-memory overlay was stale, which is why the state root still matches and
only gasUsed diverges.

Fix

In the u.UnwindPoint >= s.BlockNumber branch, still prune the in-RAM overlay
to the unwind boundary (same txNum / sd.Unwind call the non-no-op path
already uses). When the overlay has no uncommitted writes above the unwind
point (the normal case) this is a harmless no-op.

Test

TestUnwindExecutionStage_PrunesUncommittedOverlayWrite seeds the overlay with
a first-time storage write at a txNum belonging to a block above the unwind
point, drives UnwindExecutionStage with u.UnwindPoint >= s.BlockNumber, and
asserts the stale write is gone while a write at/below the unwind point
survives.

  • Without the fix (early return restored): FAILS — the overlay still returns
    the stale value (d7549f2a…a3a34, the real bug value).
  • With the fix: PASSES.

Verification

  • go build ./execution/stagedsync/... ./db/state/... — clean
  • go test ./execution/stagedsync/ -short — pass
  • TestSharedDomain_Unwind* (the #20625 regression tests) — still pass
  • TestFindExecutedDiffsetAtHeight_FallsBackAfterCanonicalReorg — still pass
  • gofmt / go vet / golangci-lint run ./execution/stagedsync/ — 0 issues

Notes

Relates to #21681 (the gas used mismatch in 3.4 reports that persist after
#20625). Not auto-closing — the same root cause is the most likely
explanation for those, but they should be confirmed on the snapshotter (incl.
block 25202264) first.

The fix lives in the single execution-unwind chokepoint, so it covers both the
serial and parallel executors. A forward-port to main will be opened
separately.

Co-authored-by: Claude noreply@anthropic.com

… disk no-op

When a block fails its post-execution gas check mid-batch, its writes sit in
the in-RAM SharedDomains / TemporalMemBatch overlay but were never committed.
The committed execution-stage progress is therefore at or below the unwind
point, so UnwindExecutionStage took the `u.UnwindPoint >= s.BlockNumber` early
return and never called sd.Unwind — the overlay prune added by #20625 only runs
via unwindExec3. The same overlay is reused across the unwind/retry loop inside
one sync.Run, so the stale write survived and the re-execution read it.

Observed on Hoodi block 3004265: ca5daf64 slot0 kept tx19's own first-write
(...a3a34), the contract took the "already initialised" branch, skipped an
SSTORE_SET (20000 gas), gasUsed came up exactly 21045 short, and the node spun
in an unwind/retry loop. The committed DB was correct (slot0 = 0); only the
overlay was stale.

Prune the overlay to the unwind boundary in that branch too, and add a
regression test driving UnwindExecutionStage on the no-op-disk-unwind path.

Relates to #21681

Co-authored-by: Claude <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

Fixes an execution-stage unwind edge case where a disk unwind becomes a no-op (u.UnwindPoint >= s.BlockNumber) but the in-RAM SharedDomains/TemporalMemBatch overlay can still contain uncommitted writes from a failed mid-batch execution, causing stale reads and repeated gas-used mismatches/infinite unwind→retry loops.

Changes:

  • In UnwindExecutionStage, when the disk unwind would be skipped, still compute the unwind boundary txNum and call doms.Unwind(txNum, nil) to prune uncommitted overlay writes.
  • Add a regression test that seeds overlay writes above/below the unwind boundary and asserts only the above-boundary write is pruned in the no-op-disk-unwind path.

Reviewed changes

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

File Description
execution/stagedsync/stage_execute.go Ensures in-RAM overlay is pruned even when execution unwind is a disk no-op, preventing stale overlay reads across retry loops.
execution/stagedsync/stage_execute_unwind_test.go Adds a focused regression test validating overlay pruning occurs in the early-return (disk no-op) unwind branch.

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

AskAlexSharov pushed a commit that referenced this pull request Jun 16, 2026
…nd is a disk no-op (#21826)

Cherry-pick of #21824 (release/3.4) / #21825 (main) to `release/3.5`.

`release/3.5` has the identical gap: `UnwindExecutionStage`
(`execution/stagedsync/stage_execute.go`) early-returns when
`u.UnwindPoint >= s.BlockNumber` without calling `sd.Unwind`, so the
in-RAM
`SharedDomains` / `TemporalMemBatch` overlay is not pruned when a block
fails
its post-execution gas check mid-batch (before its step is committed).
The same
overlay is reused across the unwind→retry loop in `sync.Run`, so the
stale
write survives and the re-execution reads it — skipping an `SSTORE_SET`,
undercharging gas, and looping. Full root-cause analysis in #21824.

## Adaptations

Clean cherry-pick of the `main` commit (`release/3.5` is main-like): the
early
return is byte-identical, `stage_execute_unwind_test.go` does not exist
on
`release/3.5` so the new test is added fresh, and
`common.Address`/`common.Hash`
are raw array types (the test slices them with `[:]`). No manual
conflict
resolution was needed.

## Verification on release/3.5 + this patch

- `go build ./execution/stagedsync/...` — clean
- `go test ./execution/stagedsync/ -run
TestUnwindExecutionStage_PrunesUncommittedOverlayWrite` — pass
- red-check (revert just the prune): the test fails with the stale value
`d7549f2a…a3a34` surviving the unwind, then passes with the fix restored
- `gofmt` / `golangci-lint run ./execution/stagedsync/` — 0 issues

Relates to #21681.

Co-authored-by: Claude <noreply@anthropic.com>

Co-authored-by: JkLondon <me@ilyamikheev.com>
Co-authored-by: Claude <noreply@anthropic.com>
@@ -469,6 +469,25 @@ func SpawnExecuteBlocksStage(s *StageState, u Unwinder, doms *execctx.SharedDoma
func UnwindExecutionStage(u *UnwindState, s *StageState, doms *execctx.SharedDomains, rwTx kv.TemporalRwTx, ctx context.Context, cfg ExecuteBlockCfg, logger log.Logger) (err error) {
//fmt.Printf("unwind: %d -> %d\n", u.CurrentBlockNumber, u.UnwindPoint)
if u.UnwindPoint >= s.BlockNumber {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ok. but smells like: this if is for early-return, probably instead of adding biz-logic inside this if - need modify it's condition logic so it "doesn't early return in some cases. Because maybe unwind has (or will have in future) important biz-logic which doesn't exists in current if. For example: it's unclear for me why this if doesn't have u.Done(rwTx) (outer biz-logic has it)

@AskAlexSharov
AskAlexSharov enabled auto-merge (squash) June 16, 2026 03:05
pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Jun 16, 2026
… disk no-op (erigontech#21825)

Forward-port of erigontech#21824 to `main`.

`main` has the identical gap: `UnwindExecutionStage`
(`execution/stagedsync/stage_execute.go`) early-returns when
`u.UnwindPoint >= s.BlockNumber` without calling `sd.Unwind`, so the
in-RAM
`SharedDomains` / `TemporalMemBatch` overlay is not pruned when a block
fails
its post-execution gas check mid-batch (before its step is committed).
The same
overlay is reused across the unwind→retry loop in `sync.Run`, so the
stale
write survives and the re-execution reads it — skipping an `SSTORE_SET`,
undercharging gas, and looping. Full root-cause analysis in erigontech#21824.

## Adaptations vs the release/3.4 PR

- **`stage_execute.go`**: clean cherry-pick (the early return is
byte-identical
  on `main`).
- **`stage_execute_unwind_test.go`**: created fresh on `main`. The file
is
release/3.4-only there (its existing `TestFindExecutedDiffsetAtHeight…`
test
was never forward-ported and is unrelated to this fix), so only the new
test
is added. `common.Address`/`common.Hash` are raw array types on `main`
(no
  `.Bytes()` method), so the test slices them with `[:]` instead.

## Verification on main + this patch

- `go build ./execution/stagedsync/...` — clean
- `go test ./execution/stagedsync/ -run
TestUnwindExecutionStage_PrunesUncommittedOverlayWrite` — pass
- red-check (revert just the prune): the test fails with the stale value
`d7549f2a…a3a34` surviving the unwind, then passes with the fix restored
- `gofmt` — clean

Relates to erigontech#21681.

Co-authored-by: Claude <noreply@anthropic.com>

Co-authored-by: JkLondon <me@ilyamikheev.com>
Co-authored-by: Claude <noreply@anthropic.com>
@yperbasis
yperbasis disabled auto-merge June 16, 2026 07:57

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

Two substantive points and a couple of nits (reviewed statically against the PR branch).

1. Prune boundary is unwindPoint+1, but re-execution resumes from committed progress

The no-op path correctly does not call u.Done (lowering progress to unwindPoint would be wrong — (C, unwindPoint] was never flushed), so the Execution-stage progress stays at the committed value C (= s.BlockNumber). Re-execution then resumes from doms.SeekCommitment (exec3.go:125) = C+1, not unwindPoint+1.

With BatchCommitments=true (the default / snapshotter mode), commitment state is persisted only on the ~20s timer or when the batch fills, so C can sit several blocks below unwindPoint. The prune at Min(unwindPoint+1) then keeps the overlay writes for blocks (C, unwindPoint], but those blocks are re-executed from C+1 and re-read their own stale overlay — the same bug class. It doesn't loop forever (the unwind point walks back to C, then the prune clears everything — ~2 cycles), but it costs an extra partial re-execution and emits spurious "gas used mismatch" warnings for blocks that aren't actually bad. For the reported incident the two boundaries coincide (C == unwindPoint, i.e. one block since the last commit), which is why it works there.

Suggest pruning to the committed boundary instead — Min(ctx, rwTx, s.BlockNumber+1) (or the SeekCommitment txNum). That clears all uncommitted overlay in one step, aligns the prune with the resume point, and can't over-prune (committed state is in the DB).

This interacts with the test: keepKey is at block 7, above committedBlock=5 and below unwindPoint=7, and is asserted to survive ("no over-pruning"). But under resume-from-committed, block 7 is re-executed, so keeping its write is exactly what triggers the extra cycle — adopting the committed boundary flips that assertion.

2. State cache isn't reverted on this path

GetLatest consults sd.stateCache after the overlay (domain_shared.go:407); the disk path calls stateCache.RevertWithDiffset(...), the no-op path doesn't. Mitigated in practice — both executors call ValidateAndPrepare per block (exec3_serial.go:118, exec3_parallel.go:754), which clears the cache on retry, and the snapshotter never attaches a cache (SetStateCache is execmodule-only). So not a hole today, but the fix leans on that implicit clear rather than the explicit revert; worth a note for anyone enabling the cache on other paths that share this chokepoint.

Nits

  • doms.SetTxNum(txNum) is overwritten by the next run's SeekCommitment → redundant (harmless; mirrors the disk path).
  • The regression test exercises the prune mechanism directly (no state cache, no end-to-end retry loop) — reasonable scope, but it can't catch either point above.

@yperbasis
yperbasis merged commit 22db204 into release/3.4 Jun 16, 2026
24 checks passed
@yperbasis
yperbasis deleted the jklondon/fix_unwind_overlay_prune_34 branch June 16, 2026 09:32
yperbasis pushed a commit that referenced this pull request Jun 17, 2026
…o-op execution unwind (#21847)

Follow-up to #21824, addressing review feedback from @yperbasis and
@AskAlexSharov.

## The gap in #21824

#21824 made `UnwindExecutionStage`'s no-op-disk-unwind branch prune the
in-RAM
overlay, but to `Min(u.UnwindPoint+1)`. That branch deliberately does
**not**
call `u.Done`, so re-execution resumes from the *committed* progress
(`doms.SeekCommitment == s.BlockNumber+1`), **not** `u.UnwindPoint+1`.

When committed progress sits below `u.UnwindPoint` — common with
`BatchCommitments` (commitment is persisted on a ~20s timer or when the
batch
fills), so `s.BlockNumber` can be several blocks behind — the overlay
writes for
blocks `(s.BlockNumber, u.UnwindPoint]` are **kept** but then
re-executed from
`s.BlockNumber+1`, where they re-read their own stale overlay: the same
bug
class, just deferred. It converges (the unwind point walks back to the
committed
block, then the prune clears everything) instead of looping forever, but
it
wastes a partial re-execution and emits spurious `gas used mismatch`
warnings for
blocks that aren't actually bad. For the original Hoodi 3004265 incident
`s.BlockNumber == u.UnwindPoint` (one block since the last commit), so
the two
boundaries coincided and #21824 worked.

## This PR

- **Prune to the committed boundary** (`Min(s.BlockNumber+1)`) instead
of
`Min(u.UnwindPoint+1)`. Clears all uncommitted overlay in one step,
matches the
  resume point, can't over-prune (committed state is in the DB).
- **Restructure** `UnwindExecutionStage` into an explicit `disk-unwind`
/
`overlay-only` `if`/`else` with a shared `SeekCommitment` tail, so the
no-op
case is no longer a bare early-return that buries logic / risks skipping
future
shared steps (@AskAlexSharov). This also drops the redundant `SetTxNum`
(@yperbasis nit — `SeekCommitment` sets it) and documents **why** the
no-op
path must not call `u.Done`, plus the state-cache caveat (@yperbasis #2:
not a
hole today — executors clear the cache per-block via
`ValidateAndPrepare` and
the snapshotter attaches no cache — but the path relies on that implicit
clear).
- **Test**: now asserts the `(s.BlockNumber, u.UnwindPoint]` write is
pruned too,
not just the failed block's; a write at/below committed still survives.

## Verification

- red→green: with the merged `Min(u.UnwindPoint+1)` boundary the new
test fails
(the `(committed, unwindPoint]` write `0x1122` survives); with this PR
it passes.
- `go build ./execution/stagedsync/...` — clean
- `go test ./execution/stagedsync/ -run
'TestUnwindExecutionStage_PrunesUncommittedOverlayWrite|TestFindExecutedDiffsetAtHeight'`
— pass
- `gofmt` / `golangci-lint run ./execution/stagedsync/` — 0 issues

Relates to #21681. Will be forward-ported to `main` and `release/3.5`
(which carry #21825 / #21826).

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: JkLondon <me@ilyamikheev.com>
Co-authored-by: Claude <noreply@anthropic.com>
@yperbasis yperbasis mentioned this pull request Jun 17, 2026
pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Jun 30, 2026
…rigontech#21973)

Part of erigontech#21860.

Adds integration and stage-level regression tests for the unwind/reorg
state-correctness issues tracked in erigontech#21860, built around a
self-checking, storage-churning contract so that any state left
inconsistent by a faulty unwind or reorg surfaces as a failed
transaction or a wrong read. The multi-step domain-unwind fix this work
uncovered (erigontech#21981) is already merged into main.

Production changes: `findExecutedDiffsetAtHeight` is extracted from
`unwindExec3` so the erigontech#21515 fallback can be unit-tested, plus fixes for
two latent infrastructure bugs the new tests surfaced (see Fixes below).

## `StateChurn` contract

A fixed ring of 16 pseudo-random storage keys; each `poke(seed)`
overwrites the next key round-robin with `keccak(seed, cursor) % 3`. So
~⅓ of writes set a slot to 0 (deletion) and slots are continually
deleted and recreated — the storage pattern that exposes buggy unwinds.
`trackedSum` is maintained incrementally and `require`d to equal the sum
recomputed from storage.

## Regression tests (red-on-revert — fail without the fix)

- **`TestUnwindExecutionStage_PrunesUncommittedOverlayWrite`** (erigontech#21681 /
erigontech#21824) — when a block fails mid-batch before its step is committed, the
no-disk-rollback execution unwind must still prune the uncommitted
in-RAM `SharedDomains` overlay to the committed boundary; otherwise the
stale writes survive into the unwind→retry loop and re-execution reads a
wrong value.
- **`TestFindExecutedDiffsetAtHeight_FallsBackAfterCanonicalReorg`**
(erigontech#21515) — on a reorg the canonical hash for the unwound range is
cleared, so the diffset lookup (keyed on the canonical hash) must fall
back to the stored header; otherwise the unwind silently no-ops and the
unwound block's state survives as phantom data (→ `CREATE2` collision →
gas-used mismatch). Tested at the stage level because the path is only
reachable through the full staged-sync pipeline.
- **`TestEngineApiUnwindAcrossDomainStepBoundaries`** (erigontech#21981) — full
node: builds real domain snapshot files, then unwinds a multi-step range
crossing several domain-step boundaries and checks the StateChurn
invariant against ground truth. Fails on main without the
multi-step-unwind fix.

## Integration coverage (unwind/reorg correctness)

- **`TestEngineApiUnwindRedoStateChurnPreservesState`** — one node
records `trackedSum` at every height, then unwinds and redoes across
shallow and deep targets, asserting live state matches the recorded
value at each. Catches "head moved but state not rolled back".
- **`TestEngineApiReorgToSideChainSwitchesStateChurn`** — reorgs a
victim onto a competing side chain that churns different storage,
asserting live state switches to the side fork.

## Fixes (latent bugs the new tests surfaced)

- **`db/state` — data race between the background merge and chain-tip
worker presets.** The merge path read per-domain compressor/accessor
worker counts that chain-tip execution (`PresetChainTipConcurrency`)
mutates concurrently; only the collate/build path pinned them.
`mergeLoop` now pins worker config for its duration, and the pin is
reentrant so an overlapping background build and merge can't re-enable
edits mid-operation. (Surfaced by the `-race` run of
`TestEngineApiUnwindAcrossDomainStepBoundaries`.)
- **`db/kv/temporal` — the state aggregator was never released at
shutdown.** `DB.Close()` closed the underlying MDBX and the forkable
aggregators but not the state aggregator it owns, leaving its
snapshot-file mmaps open. Harmless on POSIX, but on Windows an open mmap
can't be unlinked, so deleting the datadir after shutdown failed.
`DB.Close()` now closes the state aggregator too, before the MDBX
handle, so its background goroutines release their read transactions
first.
pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Jul 12, 2026
…g/unwind gaps (erigontech#22300)

Part of erigontech#21860, follow-up to erigontech#21973.

Revert-testing every root-cause fix behind the bugs cited in erigontech#21860
showed that several failure classes were invisible to the existing
integration tests — only their purpose-built unit/stage tests went red.
This PR extends the StateChurn suite and the exec-module tests to close
the gaps that are honestly reachable from outside, and documents the
ones that are not.

## New coverage

- **Restart durability** —
`TestEngineApiUnwindAcrossDomainStepBoundaries` now closes the node
after its 60-block unwind, restarts on the same datadir (carrying the
mock-CL state), re-asserts the churn invariant and keeps churning.
Unwind correctness must be durable in MDBX+files rather than masked by
the in-RAM overlay / unwind-changeset of the original process; this also
covers the "corruption survives restart" property that made erigontech#21515
painful.
- **Fork bounce** — `TestEngineApiForkBounceStateChurn` bounces
canonical → side → canonical → side with full churn asserts after every
switch, then keeps building on the final fork. Targets per-height state
(diffsets, changesets, caches) leaking across fork switches.
- **Re-orgs with pruning interfering** (the scenario erigontech#21860 names
explicitly; previously the tester had no prune wiring at all) —
`TestEngineApiReorgWithPruningInterference` runs a custom bite-sized
prune distance (standard modes' 100k+ block distances never engage at
test scale), so collate+prune runs on every forkchoice, interleaved with
a shallow unwind+redo, continued churn, and a deep unwind into prune
territory that must either be performed exactly or rejected loudly.
- **Reorg across contract creation at the same address** —
`TestEngineApiReorgAcrossContractCreationAtSameAddress` deploys the
churn contract on both forks from the same sender+nonce (same address,
diverging deploy blocks) and reorgs across the creation in both
directions: leftover code/nonce from the losing fork triggers the
EIP-684 create-collision behind the phantom-CREATE2 incidents (erigontech#20995),
leftover storage trips the churn invariant.
- **Unwind to the snapshot-file boundary** —
`TestEngineApiUnwindToSnapshotBoundaryPreservesDeletedSlots` unwinds to
just above the visible-files boundary after pruning has evacuated the
filed range from MDBX, so post-unwind reads resolve through the snapshot
files (verified: thousands of file reads) rather than being masked by
MDBX chain-tip retention as in the shallower files test.
- **Over-deep unwind rejection** —
`TestEngineApiUnwindBeyondRetainedChangesetsRejectedCleanly` (no
`AlwaysGenerateChangesets`) pins that a forkchoice below the retained
history fails loudly — never silently no-ops or partially applies — and
that the head stays restorable with correct reads afterwards.
- **Bad block mid-stream (devp2p path)** —
`TestUpdateForkChoiceBadBlockMidBatchThenRecovery` and
`TestUpdateForkChoiceBadBlockAtLongBatchTailThenRecovery` feed the
module never-validated segments containing a block that fails its
post-execution gas check (mid-batch, first-of-batch, and at the tail of
a 9-block segment), pinning that the bad block is rejected without
condemning valid ancestors and that the untampered sibling at the same
height recovers cleanly. Nothing previously exercised post-execution
rejection outside `newPayload` validation.

Shared helpers: `churnAndAssert` (continue poking with per-block
invariant asserts), `buildRecordedChurnChain`, and a variadic
transact-opts tweak on `buildChurnChain` so a fork can diverge at the
deploy transaction itself.

## Red-on-revert verification

Each test was verified against semantic reverts of the fixes it guards:

- files+restart test: red under reverts of erigontech#21981 (multi-step unwind),
erigontech#20483 read side (tombstone-as-miss), erigontech#20710 (per-step Flush orphans);
- snapshot-boundary test: red under the erigontech#20483 read-side revert;
- bounce, prune and create-collision tests: red when `StateCache.Unwind`
invalidation is disabled; prune test additionally red when
`StateCache.Delete` is disabled.

## Classes that remain guarded at unit/stage level only (verified
unreachable from outside on current main)

- erigontech#21824/erigontech#21848 overlay prune on no-op unwind: the module aborts and
rolls back its FCU tx on a bad block instead of in-loop retrying, and
each FCU gets a fresh SharedDomains — the stage-level test remains the
guard.
- erigontech#21157 diffset fallback: `updateForkChoice` unwinds *before* rewriting
canonical markers within one atomic MDBX tx, so the
cleared-markers-before-unwind ordering has no producer in current flows;
the stage-level test remains the guard.
- erigontech#20483 write side (unwind loses deletion markers): instrumentation
shows the lost tombstone restores have no observable effect at any test
scale because unmerged file layers retain deletion markers — the
production resurrection required mainnet-scale compaction.
`TestDomain_UnwindRestoresDeletionMarker` remains the guard.
- erigontech#21088 hash-aware changeset lookup: both call sites live behind
batch/streaming-commitment paths inactive at chain tip.

## Bugs surfaced while building these

- erigontech#22298 — after a rejected bad-tip FCU, forkchoice to the
already-canonical valid ancestor re-executes the rejected block (the
module tests pin the working recovery path; a regression test for the
broken path is a one-line change described in the issue).
- erigontech#22299 — txpool pending nonce stays durably stale after fork bounces;
`churnAndAssert` pins the nonce from latest state as a workaround.
- erigontech#22301 — a too-deep FCU passes the changeset-based unwind gate,
partially unwinds, fails at `SeekCommitment`, and wedges block
production; the rejection test stops at the read-side contract and the
issue carries the one-line extension that reproduces the wedge.
beeant added a commit to hachiari/erigon-node that referenced this pull request Aug 6, 2026
)

v3.4.3 hit the known post-reorg 'gas used mismatch' bug family
(erigontech/erigon#21824/#21847, 4th variant #22423): execution
declares canonical mainnet block 25393073 invalid (txnIdx=1158,
gas 59338474 vs header 59632361), unwinds one block, retries,
fails identically — wedged at block 25392043 since 2026-06-25,
302k blocks behind tip. Restarts do not clear it.

v3.4.4 shipped the third fix for this bug class; the v3.5 line
carries the rest. v3.5.4 is current stable (2026-07-29).
In-place datadir upgrade, no resync required.

Fixes hachiari/bearbullbunny#1305 (42d-old block ages on /ethereum)
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.

4 participants