Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eth/api_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (b *EthAPIBackend) CurrentBlock() *types.Header {

func (b *EthAPIBackend) SetHead(number uint64) {
b.eth.handler.downloader.Cancel()
b.eth.handler.downloader.ResetSkeleton()
b.eth.blockchain.SetHead(number)
}

Expand Down
10 changes: 10 additions & 0 deletions eth/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type Downloader struct {
// Callbacks
dropPeer peerDropFn // Drops a peer for misbehaving
badBlock badBlockFn // Reports a block as rejected by the chain
success func() // Callback to signal successful sync completion

// Status
synchronising atomic.Bool
Expand Down Expand Up @@ -237,6 +238,7 @@ func New(stateDb ethdb.Database, mode ethconfig.SyncMode, mux *event.TypeMux, ch
chainCutoffNumber: cutoffNumber,
chainCutoffHash: cutoffHash,
dropPeer: dropPeer,
success: success,
headerProcCh: make(chan *headerTask, 1),
quitCh: make(chan struct{}),
SnapSyncer: snap.NewSyncer(stateDb, chain.TrieDB().Scheme()),
Expand Down Expand Up @@ -662,6 +664,14 @@ func (d *Downloader) Cancel() {
d.blockchain.InterruptInsert(false)
}

// ResetSkeleton terminates the skeleton syncer and reinitializes it.
func (d *Downloader) ResetSkeleton() {
log.Debug("Resetting skeleton syncer due to chain rewind")
d.skeleton.Terminate()
rawdb.DeleteSkeletonSyncStatus(d.stateDB)
d.skeleton = newSkeleton(d.stateDB, d.peers, d.dropPeer, newBeaconBackfiller(d, d.success))
}

// Terminate interrupts the downloader, canceling all pending operations.
// The downloader cannot be reused after calling Terminate.
func (d *Downloader) Terminate() {
Expand Down
43 changes: 43 additions & 0 deletions eth/downloader/downloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,3 +734,46 @@ func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) {
t.Fatalf("Failed to sync chain in three seconds")
}
}

// TestSkeletonResetAfterSetHead tests that the skeleton syncer is properly reset
// when the chain is rewound using SetHead, preventing data inconsistency issues.
func TestSkeletonResetAfterSetHead(t *testing.T) {
tester := newTester(t, ethconfig.SnapSync)
defer tester.terminate()

chain := testChainBase.shorten(800)
tester.newPeer("peer", eth.ETH68, chain.blocks[1:])

if _, err := tester.chain.InsertChain(chain.blocks[1:401]); err != nil {
t.Fatalf("Failed to insert chain: %v", err)
}

// Start beacon sync to populate the skeleton
header := chain.blocks[400].Header()
if err := tester.downloader.BeaconSync(header, header); err != nil {
t.Fatalf("Failed to start beacon sync: %v", err)
}

// Wait for the skeleton state
time.Sleep(20 * time.Millisecond)

// Check skeleton sync status exists in database before SetHead
if skeleton := rawdb.ReadSkeletonSyncStatus(tester.downloader.stateDB); len(skeleton) == 0 {
t.Fatal("Skeleton sync status should exist in database before SetHead")
}

// Simulate chain rewind by calling SetHead
tester.downloader.Cancel()
tester.downloader.ResetSkeleton()
tester.chain.SetHead(200)

// Verify skeleton sync status was cleared from database
if skeleton := rawdb.ReadSkeletonSyncStatus(tester.downloader.stateDB); len(skeleton) != 0 {
t.Fatal("Skeleton sync status should be cleared from database after SetHead")
}

// Verify we can start a new sync after reset
if err := tester.downloader.BeaconSync(header, header); err != nil {
t.Fatalf("Failed to start beacon sync after reset: %v", err)
}
}