From be19e2c67e5cccfd9e3edd0c032daaf6ee6ddfd9 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Sat, 18 Apr 2026 20:24:28 +0200 Subject: [PATCH] eth/downloader: short-circuit synchronise once partial-state sync is complete beaconBackfiller.resume() already returns early when partialSyncComplete is set, so in normal CL-driven operation the downloader never reaches synchronise after the initial partial-state sync finishes. Add the same guard at the synchronise entry point as defense in depth: any future caller of synchronise (tests, other wiring) inherits the invariant that partial-state nodes do not run full downloader cycles after initial sync, even if the resume path is bypassed. The check is cheap (one atomic.Load) and sits on the cold path, so the impact on normal full-sync users is nil. --- eth/downloader/downloader.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index ed3531845e..fdc179f752 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -389,6 +389,18 @@ func (d *Downloader) synchronise(beaconPing chan struct{}) (err error) { } defer d.synchronising.Store(false) + // Partial-state nodes must not run a downloader cycle once the initial + // sync has completed; every live block arrives via the Engine API's + // newPayload path and is processed with ApplyBALAndComputeRoot. Running + // the downloader here would try to download + (re-)execute blocks + // against storage we intentionally don't have. beaconBackfiller.resume + // already guards this at a higher layer; this check is defense in depth + // for any other caller of synchronise (tests, future wiring). + if d.partialFilter != nil && d.partialSyncComplete.Load() { + log.Debug("Partial state: sync complete, skipping downloader cycle") + return nil + } + // Post a user notification of the sync (only once per session) if d.notified.CompareAndSwap(false, true) { log.Info("Block synchronisation started")