core/rawdb, eth/downloader: persist partial-sync completion across restarts

d.partialSyncComplete is consulted by beaconBackfiller.resume() to skip
redundant downloader cycles after the initial partial-state sync has
finished. It was an in-memory atomic.Bool, so every process restart
reset it to false, and the next forkchoiceUpdated from the CL would
re-enter the sync loop.

Persist the flag in leveldb via a new PartialSyncComplete marker:

- Add ReadPartialSyncComplete / WritePartialSyncComplete /
  DeletePartialSyncComplete accessors in core/rawdb/accessors_chain.go
  backed by a single-byte value under the PartialSyncComplete key.
- Write the marker in the downloader right after AdvancePartialHead
  succeeds (same spot we flip the in-memory flag).
- Rehydrate the in-memory flag from leveldb in Downloader.New() so a
  freshly-started process with a completed partial-state sync keeps
  the resume short-circuit active from the first beacon forkchoice.

Without this, the restart invariant relied on HasState(header.Root)
accidentally returning false to reroute the downloader back to
SnapSync; with this the resume guard is the primary protection
regardless of how header-root convergence evolves.
This commit is contained in:
CPerezz 2026-04-18 20:23:42 +02:00
parent e131e7708b
commit e0c5cff4df
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
3 changed files with 42 additions and 0 deletions

View file

@ -200,6 +200,32 @@ func WriteLastPivotNumber(db ethdb.KeyValueWriter, pivot uint64) {
}
}
// ReadPartialSyncComplete reports whether the partial-state initial sync
// completed successfully on this datadir. Returns false if the flag is
// unset or absent (fresh database, non-partial-state node, or sync in
// progress).
func ReadPartialSyncComplete(db ethdb.KeyValueReader) bool {
data, _ := db.Get(partialSyncCompleteKey)
return len(data) > 0 && data[0] == 1
}
// WritePartialSyncComplete marks the partial-state initial sync as finished.
// The downloader uses this on restart to skip redundant sync cycles.
func WritePartialSyncComplete(db ethdb.KeyValueWriter) {
if err := db.Put(partialSyncCompleteKey, []byte{1}); err != nil {
log.Crit("Failed to store partial-sync-complete flag", "err", err)
}
}
// DeletePartialSyncComplete clears the partial-state sync completion flag.
// Used when the node is reset to genesis or rewound behind the pivot so a
// fresh partial sync can run.
func DeletePartialSyncComplete(db ethdb.KeyValueWriter) {
if err := db.Delete(partialSyncCompleteKey); err != nil {
log.Crit("Failed to delete partial-sync-complete flag", "err", err)
}
}
// ReadTxIndexTail retrieves the number of oldest indexed block
// whose transaction indices has been indexed.
func ReadTxIndexTail(db ethdb.KeyValueReader) *uint64 {

View file

@ -104,6 +104,12 @@ var (
// snapSyncStatusFlagKey flags that status of snap sync.
snapSyncStatusFlagKey = []byte("SnapSyncStatus")
// partialSyncCompleteKey flags that the partial-state initial sync
// (snap sync + second state sync to HEAD + AdvancePartialHead) has
// finished successfully on this datadir. Consumed by the downloader
// so beaconBackfiller.resume() keeps short-circuiting across restarts.
partialSyncCompleteKey = []byte("PartialSyncComplete")
// Data item prefixes (use single byte to avoid mixing data types, avoid `i`, used for indexes).
headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
headerTDSuffix = []byte("t") // headerPrefix + num (uint64 big endian) + hash + headerTDSuffix -> td (deprecated)

View file

@ -268,6 +268,13 @@ func New(stateDb ethdb.Database, mode ethconfig.SyncMode, mux *event.TypeMux, ch
stateSyncStart: make(chan *stateSync),
syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(),
}
// Rehydrate the partial-state completion flag across restarts. Without
// this, a freshly-started process would re-enter the downloader loop for
// every beacon forkchoice update, defeating beaconBackfiller.resume()'s
// short-circuit.
if partialFilter != nil && rawdb.ReadPartialSyncComplete(stateDb) {
dl.partialSyncComplete.Store(true)
}
// Create the post-merge skeleton syncer and start the process
dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success), chain)
@ -1027,6 +1034,9 @@ func (d *Downloader) processSnapSyncContent() error {
return err
}
d.partialSyncComplete.Store(true)
// Persist the completion flag so a restart does not
// re-run the sync cycle on every beacon forkchoice.
rawdb.WritePartialSyncComplete(d.stateDB)
log.Info("Partial state initial sync complete")
}
}