Skip to content

Commit 59918ab

Browse files
committed
eth/protocols/snap: added read lock in catchup and some reorg
documentation and additional test coverage for deep reorg
1 parent e0a8f61 commit 59918ab

File tree

4 files changed

+72
-36
lines changed

4 files changed

+72
-36
lines changed

core/state/snapshot/snapshot.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -733,12 +733,10 @@ func (t *Tree) Rebuild(root common.Hash) {
733733
func (t *Tree) RebuildFromSyncedState(root common.Hash) {
734734
t.lock.Lock()
735735
defer t.lock.Unlock()
736-
737736
rawdb.DeleteSnapshotRecoveryNumber(t.diskdb)
738737
rawdb.DeleteSnapshotDisabled(t.diskdb)
739738
rawdb.WriteSnapshotRoot(t.diskdb, root)
740739
journalProgress(t.diskdb, nil, nil)
741-
742740
log.Info("Setting up snapshot from synced state", "root", root)
743741
t.layers = map[common.Hash]snapshot{
744742
root: &diskLayer{

eth/downloader/downloader.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,11 @@ func (d *Downloader) importBlockResults(results []*fetchResult) error {
872872
// the old pivot's block number has a different state root, the syncer's flat
873873
// state is from the old fork and must be wiped. Returns true if a deep reorg
874874
// was detected.
875+
//
876+
// Returns false (no reorg) when the canonical hash or header is missing. This
877+
// avoids false positives from pruned or not-yet-downloaded data. If the chain
878+
// really did shorten past the old pivot, sync.catchUp's from > to guard will
879+
// catch this.
875880
func checkDeepReorg(db ethdb.Database, oldNumber uint64, oldRoot common.Hash) bool {
876881
oldHash := rawdb.ReadCanonicalHash(db, oldNumber)
877882
if oldHash == (common.Hash{}) {

eth/downloader/downloader_test.go

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -703,34 +703,60 @@ func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) {
703703
// TestDeepReorgDetection verifies that checkDeepReorg correctly identifies
704704
// when the canonical chain has reorged past the old pivot.
705705
func TestDeepReorgDetection(t *testing.T) {
706-
db := rawdb.NewMemoryDatabase()
707-
pivotNumber := uint64(100)
708-
pivotRoot := common.HexToHash("0xaaaa")
709-
reorgedRoot := common.HexToHash("0xbbbb")
706+
t.Parallel()
707+
708+
// Case 1: Chain reorged to a shorter fork — old pivot number no longer
709+
// has a canonical hash. checkDeepReorg returns false (can't confirm reorg
710+
// without data). The syncer's catchUp guard handles this case instead
711+
// (see TestCatchUpInvertedRange in eth/protocols/snap/sync_test.go).
712+
t.Run("ShorterChain", func(t *testing.T) {
713+
db := rawdb.NewMemoryDatabase()
714+
// No canonical hash written at block 100 — chain is shorter
715+
if checkDeepReorg(db, 100, common.HexToHash("0xaaaa")) {
716+
t.Fatal("should not detect reorg when canonical hash is missing")
717+
}
718+
})
710719

711-
// Write a header at the pivot number with a different root (simulating reorg)
712-
header := &types.Header{
713-
Number: new(big.Int).SetUint64(pivotNumber),
714-
Root: reorgedRoot,
715-
Difficulty: common.Big0,
716-
}
717-
rawdb.WriteHeader(db, header)
718-
rawdb.WriteCanonicalHash(db, header.Hash(), pivotNumber)
720+
// Case 2: Chain reorged to a same-length (or longer) fork — old pivot
721+
// number exists but has a different state root. checkDeepReorg detects it.
722+
t.Run("DifferentRoot", func(t *testing.T) {
723+
db := rawdb.NewMemoryDatabase()
724+
pivotNumber := uint64(100)
725+
pivotRoot := common.HexToHash("0xaaaa")
726+
reorgedRoot := common.HexToHash("0xbbbb")
727+
728+
header := &types.Header{
729+
Number: new(big.Int).SetUint64(pivotNumber),
730+
Root: reorgedRoot,
731+
Difficulty: common.Big0,
732+
}
733+
rawdb.WriteHeader(db, header)
734+
rawdb.WriteCanonicalHash(db, header.Hash(), pivotNumber)
719735

720-
// Should detect reorg: canonical root differs from our pivot root
721-
if !checkDeepReorg(db, pivotNumber, pivotRoot) {
722-
t.Fatal("expected deep reorg detection when roots differ")
723-
}
736+
if !checkDeepReorg(db, pivotNumber, pivotRoot) {
737+
t.Fatal("expected deep reorg detection when roots differ")
738+
}
739+
})
724740

725-
// Should NOT detect reorg: canonical root matches our pivot root
726-
if checkDeepReorg(db, pivotNumber, reorgedRoot) {
727-
t.Fatal("should not detect reorg when roots match")
728-
}
741+
// Case 3: Same block at old pivot — no reorg at the pivot level.
742+
// Blocks above may differ, but catch-up handles that normally.
743+
t.Run("SameRoot", func(t *testing.T) {
744+
db := rawdb.NewMemoryDatabase()
745+
pivotNumber := uint64(100)
746+
pivotRoot := common.HexToHash("0xaaaa")
747+
748+
header := &types.Header{
749+
Number: new(big.Int).SetUint64(pivotNumber),
750+
Root: pivotRoot,
751+
Difficulty: common.Big0,
752+
}
753+
rawdb.WriteHeader(db, header)
754+
rawdb.WriteCanonicalHash(db, header.Hash(), pivotNumber)
729755

730-
// Should NOT detect reorg: no canonical hash at that number
731-
if checkDeepReorg(db, 999, pivotRoot) {
732-
t.Fatal("should not detect reorg when block number is unknown")
733-
}
756+
if checkDeepReorg(db, pivotNumber, pivotRoot) {
757+
t.Fatal("should not detect reorg when roots match")
758+
}
759+
})
734760
}
735761

736762
// TestDeepReorgWipesProgress verifies that when a deep reorg is detected,

eth/protocols/snap/sync.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,10 @@ type SyncPeer interface {
332332
Log() log.Logger
333333
}
334334

335-
// Syncer is an Ethereum account and storage trie syncer based on snapshots and
336-
// the snap protocol. It's purpose is to download all the accounts and storage
337-
// slots from remote peers and reassemble chunks of the state trie, on top of
338-
// which a state sync can be run to fix any gaps / overlaps.
335+
// Syncer is an Ethereum account and storage trie syncer based on the snap
336+
// protocol. It downloads all accounts, storage slots, and bytecodes from
337+
// remote peers as flat state, applies BAL diffs on pivot moves,
338+
// and triggers a final trie rebuild once flat state is consistent.
339339
//
340340
// Every network request has a variety of failure events:
341341
// - The peer disconnects after task assignment, failing to send the request
@@ -657,14 +657,21 @@ func (s *Syncer) resetDownload(root common.Hash, number uint64) {
657657
// root), it fetches BALs for the gap blocks, verifies them against
658658
// block headers, and applies the diffs to roll flat state forward.
659659
func (s *Syncer) catchUp(cancel chan struct{}) error {
660+
s.lock.RLock()
660661
from := s.previousNumber + 1
661662
to := s.number
663+
s.lock.RUnlock()
662664

663-
// The new pivot must be ahead of the old one. This can
664-
// fail if a reorg replaced the block at the pivot height (same number,
665-
// different root) or if a deep reorg shortened the chain past the old
666-
// pivot. In either case, catch-up can't roll forward, so wipe progress
667-
// and let the caller restart with a fresh sync.
665+
// The new pivot must be ahead of the old one. This can fail if a reorg
666+
// replaced the block at the pivot height (same number, different root)
667+
// or if a deep reorg shortened the chain past the old pivot. In either
668+
// case, catch-up can't roll forward, so wipe progress and return an
669+
// error so the caller restarts with a fresh sync.
670+
//
671+
// Note: this check lives here rather than in checkDeepReorg because
672+
// catchUp is reached both when the downloader actively moves the pivot
673+
// (via restartSnapSync) and when the syncer resumes from persisted
674+
// progress after a restart. checkDeepReorg only covers the former.
668675
if from > to {
669676
log.Warn("Catch-up range inverted, wiping sync progress", "from", from, "to", to)
670677
rawdb.WriteSnapshotSyncStatus(s.db, nil)
@@ -1921,7 +1928,7 @@ func (s *Syncer) forwardAccountTask(task *accountTask) {
19211928

19221929
// Persist the received account segments. These flat state maybe
19231930
// outdated during the sync, but it can be fixed later during the
1924-
// snapshot generation.
1931+
// trie rebuild.
19251932
oldAccountBytes := s.accountBytes
19261933

19271934
batch := ethdb.HookedBatch{

0 commit comments

Comments
 (0)