eth/downloader: create repro testcase for beacon header loss
This commit is contained in:
parent
686f7438d3
commit
71f7988b0f
|
@ -520,7 +520,7 @@ func (s *skeleton) initSync(head *types.Header) {
|
|||
}
|
||||
break
|
||||
}
|
||||
// If the last subchain can be extended, we're lucky. Otherwise create
|
||||
// If the last subchain can be extended, we're lucky. Otherwise, create
|
||||
// a new subchain sync task.
|
||||
var extended bool
|
||||
if n := len(s.progress.Subchains); n > 0 {
|
||||
|
|
|
@ -37,7 +37,7 @@ import (
|
|||
type hookedBackfiller struct {
|
||||
// suspendHook is an optional hook to be called when the filler is requested
|
||||
// to be suspended.
|
||||
suspendHook func()
|
||||
suspendHook func() *types.Header
|
||||
|
||||
// resumeHook is an optional hook to be called when the filler is requested
|
||||
// to be resumed.
|
||||
|
@ -56,7 +56,7 @@ func newHookedBackfiller() backfiller {
|
|||
// on initial startup.
|
||||
func (hf *hookedBackfiller) suspend() *types.Header {
|
||||
if hf.suspendHook != nil {
|
||||
hf.suspendHook()
|
||||
return hf.suspendHook()
|
||||
}
|
||||
return nil // we don't really care about header cleanups for now
|
||||
}
|
||||
|
@ -525,9 +525,21 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
Number: big.NewInt(int64(i)),
|
||||
})
|
||||
}
|
||||
// Some tests require a forking side chain to trigger cornercases.
|
||||
var sidechain []*types.Header
|
||||
for i := 0; i < len(chain)/2; i++ { // Fork at block #5000
|
||||
sidechain = append(sidechain, chain[i])
|
||||
}
|
||||
for i := len(chain) / 2; i < len(chain); i++ {
|
||||
sidechain = append(sidechain, &types.Header{
|
||||
ParentHash: sidechain[i-1].Hash(),
|
||||
Number: big.NewInt(int64(i)),
|
||||
Extra: []byte("B"), // force a different hash
|
||||
})
|
||||
}
|
||||
tests := []struct {
|
||||
headers []*types.Header // Database content (beside the genesis)
|
||||
oldstate []*subchain // Old sync state with various interrupted subchains
|
||||
fill bool // Whether to run a real backfiller in this test case
|
||||
unpredictable bool // Whether to ignore drops/serves due to uncertain packet assignments
|
||||
|
||||
head *types.Header // New head header to announce to reorg to
|
||||
peers []*skeletonTestPeer // Initial peer set to start the sync with
|
||||
|
@ -760,11 +772,41 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
endstate: []*subchain{{Head: 2*requestHeaders + 2, Tail: 1}},
|
||||
endserve: 4 * requestHeaders,
|
||||
},
|
||||
// This test reproduces a bug caught by (@rjl493456442) where a skeleton
|
||||
// header goes missing, causing the sync to get stuck and/or panic.
|
||||
//
|
||||
// The setup requires a previously successfully synced chain up to a block
|
||||
// height N. That results is a single skeleton header (block N) and a single
|
||||
// subchain (head N, Tail N) being stored on disk.
|
||||
//
|
||||
// The following step requires a new sync cycle to a new side chain of a
|
||||
// height higher than N, and an ancestor lower than N (e.g. N-2, N+2).
|
||||
// In this scenario, when processing a batch of headers, a link point of
|
||||
// N-2 will be found, meaning that N-1 and N have been overwritten.
|
||||
//
|
||||
// The link event triggers an early exit, noticing that the previous sub-
|
||||
// chain is a leftover and deletes it (with it's skeleton header N). But
|
||||
// since skeleton header N has been overwritten to the new side chain, we
|
||||
// end up losing it and creating a gap.
|
||||
{
|
||||
fill: true,
|
||||
unpredictable: true, // We have good and bad peer too, bad may be dropped, test too short for certainty
|
||||
|
||||
head: chain[len(chain)/2+1], // Sync up until the sidechain common ancestor + 2
|
||||
peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-oldchain", chain)},
|
||||
midstate: []*subchain{{Head: uint64(len(chain)/2 + 1), Tail: 1}},
|
||||
|
||||
newHead: sidechain[len(sidechain)/2+3], // Sync up until the sidechain common ancestor + 4
|
||||
newPeer: newSkeletonTestPeer("test-peer-newchain", sidechain),
|
||||
endstate: []*subchain{{Head: uint64(len(sidechain)/2 + 3), Tail: uint64(len(chain) / 2)}},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
// Create a fresh database and initialize it with the starting state
|
||||
db := rawdb.NewMemoryDatabase()
|
||||
rawdb.WriteHeader(db, chain[0])
|
||||
|
||||
rawdb.WriteBlock(db, types.NewBlockWithHeader(chain[0]))
|
||||
rawdb.WriteReceipts(db, chain[0].Hash(), chain[0].Number.Uint64(), types.Receipts{})
|
||||
|
||||
// Create a peer set to feed headers through
|
||||
peerset := newPeerSet()
|
||||
|
@ -780,8 +822,43 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
peerset.Unregister(peer)
|
||||
dropped[peer]++
|
||||
}
|
||||
// Create a backfiller if we need to run more advanced tests
|
||||
filler := newHookedBackfiller()
|
||||
if tt.fill {
|
||||
var filled *types.Header
|
||||
|
||||
filler = &hookedBackfiller{
|
||||
resumeHook: func() {
|
||||
var progress skeletonProgress
|
||||
json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress)
|
||||
|
||||
for progress.Subchains[0].Tail < progress.Subchains[0].Head {
|
||||
header := rawdb.ReadSkeletonHeader(db, progress.Subchains[0].Tail)
|
||||
|
||||
rawdb.WriteBlock(db, types.NewBlockWithHeader(header))
|
||||
rawdb.WriteReceipts(db, header.Hash(), header.Number.Uint64(), types.Receipts{})
|
||||
|
||||
rawdb.DeleteSkeletonHeader(db, header.Number.Uint64())
|
||||
|
||||
progress.Subchains[0].Tail++
|
||||
progress.Subchains[0].Next = header.Hash()
|
||||
}
|
||||
filled = rawdb.ReadSkeletonHeader(db, progress.Subchains[0].Tail)
|
||||
|
||||
rawdb.WriteBlock(db, types.NewBlockWithHeader(filled))
|
||||
rawdb.WriteReceipts(db, filled.Hash(), filled.Number.Uint64(), types.Receipts{})
|
||||
},
|
||||
|
||||
suspendHook: func() *types.Header {
|
||||
prev := filled
|
||||
filled = nil
|
||||
|
||||
return prev
|
||||
},
|
||||
}
|
||||
}
|
||||
// Create a skeleton sync and run a cycle
|
||||
skeleton := newSkeleton(db, peerset, drop, newHookedBackfiller())
|
||||
skeleton := newSkeleton(db, peerset, drop, filler)
|
||||
skeleton.Sync(tt.head, true)
|
||||
|
||||
var progress skeletonProgress
|
||||
|
@ -815,19 +892,21 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
var served uint64
|
||||
for _, peer := range tt.peers {
|
||||
served += atomic.LoadUint64(&peer.served)
|
||||
}
|
||||
if served != tt.midserve {
|
||||
t.Errorf("test %d, mid state: served headers mismatch: have %d, want %d", i, served, tt.midserve)
|
||||
}
|
||||
var drops uint64
|
||||
for _, peer := range tt.peers {
|
||||
drops += atomic.LoadUint64(&peer.dropped)
|
||||
}
|
||||
if drops != tt.middrop {
|
||||
t.Errorf("test %d, mid state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
if !tt.unpredictable {
|
||||
var served uint64
|
||||
for _, peer := range tt.peers {
|
||||
served += atomic.LoadUint64(&peer.served)
|
||||
}
|
||||
if served != tt.midserve {
|
||||
t.Errorf("test %d, mid state: served headers mismatch: have %d, want %d", i, served, tt.midserve)
|
||||
}
|
||||
var drops uint64
|
||||
for _, peer := range tt.peers {
|
||||
drops += atomic.LoadUint64(&peer.dropped)
|
||||
}
|
||||
if drops != tt.middrop {
|
||||
t.Errorf("test %d, mid state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
}
|
||||
}
|
||||
// Apply the post-init events if there's any
|
||||
if tt.newHead != nil {
|
||||
|
@ -868,25 +947,27 @@ func TestSkeletonSyncRetrievals(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
// Check that the peers served no more headers than we actually needed
|
||||
served = 0
|
||||
for _, peer := range tt.peers {
|
||||
served += atomic.LoadUint64(&peer.served)
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
served += atomic.LoadUint64(&tt.newPeer.served)
|
||||
}
|
||||
if served != tt.endserve {
|
||||
t.Errorf("test %d, end state: served headers mismatch: have %d, want %d", i, served, tt.endserve)
|
||||
}
|
||||
drops = 0
|
||||
for _, peer := range tt.peers {
|
||||
drops += atomic.LoadUint64(&peer.dropped)
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
drops += atomic.LoadUint64(&tt.newPeer.dropped)
|
||||
}
|
||||
if drops != tt.middrop {
|
||||
t.Errorf("test %d, end state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
if !tt.unpredictable {
|
||||
served := uint64(0)
|
||||
for _, peer := range tt.peers {
|
||||
served += atomic.LoadUint64(&peer.served)
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
served += atomic.LoadUint64(&tt.newPeer.served)
|
||||
}
|
||||
if served != tt.endserve {
|
||||
t.Errorf("test %d, end state: served headers mismatch: have %d, want %d", i, served, tt.endserve)
|
||||
}
|
||||
drops := uint64(0)
|
||||
for _, peer := range tt.peers {
|
||||
drops += atomic.LoadUint64(&peer.dropped)
|
||||
}
|
||||
if tt.newPeer != nil {
|
||||
drops += atomic.LoadUint64(&tt.newPeer.dropped)
|
||||
}
|
||||
if drops != tt.enddrop {
|
||||
t.Errorf("test %d, end state: dropped peers mismatch: have %d, want %d", i, drops, tt.middrop)
|
||||
}
|
||||
}
|
||||
// Clean up any leftover skeleton sync resources
|
||||
skeleton.Terminate()
|
||||
|
|
Loading…
Reference in New Issue