Skip to content

cl/phase1/forkchoice/fork_graph: skip re-dumping an already dumped head state - #25539

Merged
domiwei merged 5 commits into
mainfrom
kewei/caplin-skip-repeat-head-dump
Nov 1, 2026
Merged

domiwei merged 5 commits into
mainfrom
kewei/caplin-skip-repeat-head-dump

Conversation

@domiwei

@domiwei domiwei commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Problem

On Gloas the ForkChoice stage runs again for the same head root when the head's payload status changes from EMPTY to FULL (GetExecutionHead changes and sleep_for_slot.go wakes the stage), and again about once per second while the forkchoice update returns busy. Each run calls postForkchoiceOperations, which dumps the head state with DumpBeaconStateOnDisk. The envelope does not change the beacon state, so every extra run encodes and rewrites the same state for the same root.

A head-state dump takes about 405 ms p50 on a mainnet node with the current dump code (no fsync since #25251), and it holds stateDumpLock, which disk state reads (replays) also need. By code reasoning this can add up to 8 redundant dumps per epoch on Gloas (one per dump slot, more while the forkchoice update is busy). There is no Gloas mainnet data for this yet.

Fix

forkGraphDisk remembers the block root and slot of its last successful state dump, guarded by stateDumpLock. DumpBeaconStateOnDisk returns early when both match. The slot is part of the key because a state advanced through empty slots keeps its block root but has different content. The marker is cleared before each write and set only after the write finished and the file closed without error, so a failed or panicked write is retried (stage panics are recovered, so the next run would otherwise skip a truncated file).

Tests

New tests in fork_graph_test.go count state-file writes through the existing failing-fs wrapper:

  • the same root and slot dumped twice writes once (red on main: 2 writes);
  • a close error is retried (TestDumpBeaconStateReturnsCloseError);
  • a panic during the write is retried (TestDumpBeaconStateRetriesAfterPanic);
  • a different root is still written;
  • the same root at a different slot is still written, and the file holds the later state;
  • a failed later-slot write does not block a new dump of the earlier slot.

Each test fails when the production branch it targets is removed.

Out of scope

  • stateDumpLock contention between state dumps and replays in general, and making the state dump atomic (temp file + rename).
  • Avoiding the repeated ForkChoice stage run itself.

Related to #25120 and #25136.

…ady dumped root

On Gloas the ForkChoice stage runs again for the same head root when the
head's payload status changes from EMPTY to FULL, and about once per second
while the forkchoice update returns busy. Each run dumped the same unchanged
head state again: a full SSZ encode and file rewrite under stateDumpLock,
which disk state reads also need.

DumpBeaconStateOnDisk now remembers the block root and slot of its last
successful dump and returns early when both match. The slot is part of the
key because a state advanced through empty slots keeps its block root. The
marker is cleared before each write and set only after a successful close,
so a failed write is retried and never leaves a truncated file behind a
skip.
@domiwei
domiwei requested review from lystopad and yperbasis October 29, 2026 18:10
Copilot AI balanced review requested due to automatic review settings October 29, 2026 18:10

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

… done

DumpBeaconStateOnDisk recorded the last dumped root and slot in a defer
that only checked err == nil. A panic during encode, open, write or
header lookup leaves err nil, so the marker was set although the file
was truncated or only partly written. The stage runner recovers panics,
so the next dump of the same root and slot was skipped and the broken
file stayed on disk.

Record the marker in the close defer, only after the function reached
its normal return and Close succeeded.

Also simplify the dump tests: count writes with the existing
envelopeWriteFailureFs close counter, use forced dumps and SetSlot
instead of processing slots, and merge the close-error retry test into
TestDumpBeaconStateReturnsCloseError.
Copilot AI balanced review requested due to automatic review settings October 30, 2026 08:12

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@yperbasis yperbasis added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Oct 30, 2026
@yperbasis
yperbasis requested a balanced review from Copilot October 30, 2026 11:22

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.

🟢 Approval recommended

No active review issues are recorded for the change to suppress redundant Caplin head-state dumps.

No open issues 🎉

Review effort: Balanced

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

Approving at 6b00deb.

Two optional nits:

  • The struct comment "reusable disk I/O buffers, each guarded by the mutex above it" now also heads lastDumpedRoot and lastDumpedSlot, which are not buffers.
  • Clearing only lastDumpedRoot before a write leaves a stale lastDumpedSlot. A zero block root cannot occur, so this is harmless, but clearing both fields would make the "nothing dumped" state explicit.

Both commits cherry-pick cleanly onto release/3.8 and the new tests pass there. The related performance fixes were all backported, so this one probably should be too.

Copilot AI balanced review requested due to automatic review settings November 1, 2026 09:35
@domiwei

domiwei commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

@yperbasis thanks, both nits are addressed:

  • The struct comment: each mutex now has its own comment. stateDumpLock guards the reusable state buffer and the key of the last state dump, and envelopeMu guards the reusable envelope buffer (4a52476, 31582b1). Comment-only change.
  • Both lastDumpedRoot and lastDumpedSlot are now cleared before the write (a0a7ef6), so a failed or panicked write leaves the same zero state as a fresh forkGraphDisk. As you said, a block root is never zero, so behaviour does not change; the existing retry tests still pass.

Backport: agreed, I will open a backport PR to release/3.8 after this one merges. With the new commits, all five commits still cherry-pick cleanly onto release/3.8, and the fork_graph tests pass there.

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.

🟢 Approval recommended

There are no active review issues for the beacon-state dump deduplication change.

No open issues 🎉

Review effort: Balanced

@domiwei
domiwei added this pull request to the merge queue Nov 1, 2026
@domiwei
domiwei enabled auto-merge November 1, 2026 09:46
Merged via the queue into main with commit 3d7abd9 Nov 1, 2026
135 checks passed
@domiwei
domiwei deleted the kewei/caplin-skip-repeat-head-dump branch November 1, 2026 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Glamsterdam https://eips.ethereum.org/EIPS/eip-7773

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants