mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-05-24 08:49:29 +00:00
eth/downloader: add test for deferred head during skeleton sync restart
Test that when a forced head event arrives during the suspend window of a sync restart, it is captured and used as the restart target instead of the stale reorg head.
This commit is contained in:
parent
913720b201
commit
be444c29a3
1 changed files with 121 additions and 0 deletions
|
|
@ -941,6 +941,127 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tests that when a forced head event arrives during the suspend window of a
|
||||||
|
// sync restart (errSyncReorged), it is captured and used as the restart head
|
||||||
|
// instead of the stale reorg head. This prevents a death spiral on chains with
|
||||||
|
// fast block times where suspend() blocks long enough for new heads to arrive.
|
||||||
|
func TestSkeletonSyncDeferredHead(t *testing.T) {
|
||||||
|
// Create a short chain and a peer that can serve it
|
||||||
|
chain := []*types.Header{{Number: big.NewInt(0)}}
|
||||||
|
for i := 1; i <= 100; i++ {
|
||||||
|
chain = append(chain, &types.Header{
|
||||||
|
ParentHash: chain[i-1].Hash(),
|
||||||
|
Number: big.NewInt(int64(i)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Create a reorg head at the same height but with a non-linking parent hash.
|
||||||
|
// This triggers errChainForked in processNewHead, which with force=true
|
||||||
|
// becomes errSyncReorged.
|
||||||
|
reorgHead := &types.Header{
|
||||||
|
ParentHash: common.Hash{0xff}, // doesn't match chain[99]
|
||||||
|
Number: big.NewInt(100),
|
||||||
|
Extra: []byte("reorg"),
|
||||||
|
}
|
||||||
|
// Create a later "deferred" head that should be used instead of reorgHead.
|
||||||
|
// It's on a completely separate chain so the skeleton reinits with it.
|
||||||
|
deferredChain := []*types.Header{{Number: big.NewInt(0)}}
|
||||||
|
for i := 1; i <= 200; i++ {
|
||||||
|
deferredChain = append(deferredChain, &types.Header{
|
||||||
|
ParentHash: deferredChain[i-1].Hash(),
|
||||||
|
Number: big.NewInt(int64(i)),
|
||||||
|
Extra: []byte("deferred"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
deferredHead := deferredChain[200]
|
||||||
|
|
||||||
|
// Set up database with genesis
|
||||||
|
db := rawdb.NewMemoryDatabase()
|
||||||
|
rawdb.WriteBlock(db, types.NewBlockWithHeader(chain[0]))
|
||||||
|
rawdb.WriteReceipts(db, chain[0].Hash(), 0, types.Receipts{})
|
||||||
|
|
||||||
|
// Channels to coordinate the suspend hook timing. The first suspend() call
|
||||||
|
// happens during errSyncLinked (initial header fill). We use it as a
|
||||||
|
// synchronization point: once it fires, we know the skeleton has linked
|
||||||
|
// and restarted into a linked sync loop. Only the second suspend (triggered
|
||||||
|
// by errSyncReorged) is blocked to create the window for deferred heads.
|
||||||
|
firstSuspendDone := make(chan struct{})
|
||||||
|
suspendStarted := make(chan struct{})
|
||||||
|
suspendUnblock := make(chan struct{})
|
||||||
|
var suspendCount atomic.Int32
|
||||||
|
|
||||||
|
filler := &hookedBackfiller{
|
||||||
|
resumeHook: func() {},
|
||||||
|
suspendHook: func() *types.Header {
|
||||||
|
if suspendCount.Add(1) < 2 {
|
||||||
|
close(firstSuspendDone) // signal that initial fill completed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Signal that the reorg-triggered suspend started, then block
|
||||||
|
select {
|
||||||
|
case suspendStarted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
<-suspendUnblock
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
peer := newSkeletonTestPeer("test-peer", chain)
|
||||||
|
peerset := newPeerSet()
|
||||||
|
peerset.Register(newPeerConnection(peer.id, eth.ETH69, peer, log.New("id", peer.id)))
|
||||||
|
|
||||||
|
skeleton := newSkeleton(db, peerset, nil, filler, &fakeChainReader{})
|
||||||
|
|
||||||
|
// Start sync and wait for the first suspend to confirm the chain linked
|
||||||
|
// and the skeleton restarted into a linked sync loop.
|
||||||
|
skeleton.Sync(chain[100], nil, true)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-firstSuspendDone:
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for initial sync to link")
|
||||||
|
}
|
||||||
|
// Send the reorg head — this triggers errSyncReorged, whose defer calls
|
||||||
|
// filler.suspend() which will block on our hook
|
||||||
|
go skeleton.Sync(reorgHead, nil, true)
|
||||||
|
|
||||||
|
// Wait for suspend to start (meaning we're in the defer draining head events)
|
||||||
|
select {
|
||||||
|
case <-suspendStarted:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for suspend to start")
|
||||||
|
}
|
||||||
|
// While suspend is blocked, send a new forced head. This should be captured
|
||||||
|
// as the deferred head.
|
||||||
|
go skeleton.Sync(deferredHead, nil, true)
|
||||||
|
|
||||||
|
// Give the head event a moment to be received by the drain loop
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// Unblock suspend — the skeleton should restart with deferredHead
|
||||||
|
close(suspendUnblock)
|
||||||
|
|
||||||
|
// Wait for the skeleton to process the deferred head and re-init
|
||||||
|
waitStart := time.Now()
|
||||||
|
for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 5*time.Second; waitTime = waitTime * 2 {
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
var progress skeletonProgress
|
||||||
|
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||||
|
if len(progress.Subchains) > 0 && progress.Subchains[0].Head == 200 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var progress skeletonProgress
|
||||||
|
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||||
|
|
||||||
|
if len(progress.Subchains) == 0 {
|
||||||
|
t.Fatal("no subchains after deferred head sync")
|
||||||
|
}
|
||||||
|
if progress.Subchains[0].Head != 200 {
|
||||||
|
t.Errorf("skeleton restarted with wrong head: have %d, want %d (deferred head was not used)", progress.Subchains[0].Head, 200)
|
||||||
|
}
|
||||||
|
skeleton.Terminate()
|
||||||
|
}
|
||||||
|
|
||||||
func checkSkeletonProgress(db ethdb.KeyValueReader, unpredictable bool, peers []*skeletonTestPeer, expected skeletonExpect) error {
|
func checkSkeletonProgress(db ethdb.KeyValueReader, unpredictable bool, peers []*skeletonTestPeer, expected skeletonExpect) error {
|
||||||
var progress skeletonProgress
|
var progress skeletonProgress
|
||||||
// Check the post-init end state if it matches the required results
|
// Check the post-init end state if it matches the required results
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue