eth/downloader: bypass peer validation if remote peer is far away (#1219)

* eth/downloader: bypass peer validation if remote peer is far away

* eth/downloader: parameterise threshold, fix condition

* eth: add tests for bypassing validation

* eth/downloader: declare max validation threshold instead of using direct value

* address comments

* simplify
This commit is contained in:
Manav Darji 2024-04-15 17:17:18 +05:30 committed by GitHub
parent d8db72ae01
commit a0c7d0c177
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 23 deletions

View file

@ -57,6 +57,8 @@ var (
fsHeaderSafetyNet = 2048 // Number of headers to discard in case a chain violation is detected fsHeaderSafetyNet = 2048 // Number of headers to discard in case a chain violation is detected
fsHeaderContCheck = 3 * time.Second // Time interval to check for header continuations during state download fsHeaderContCheck = 3 * time.Second // Time interval to check for header continuations during state download
fsMinFullBlocks = 64 // Number of blocks to retrieve fully even in snap sync fsMinFullBlocks = 64 // Number of blocks to retrieve fully even in snap sync
maxValidationThreshold = uint64(1024) // Number of block difference from remote peer to start validation
) )
var ( var (
@ -147,7 +149,9 @@ type Downloader struct {
quitCh chan struct{} // Quit channel to signal termination quitCh chan struct{} // Quit channel to signal termination
quitLock sync.Mutex // Lock to prevent double closes quitLock sync.Mutex // Lock to prevent double closes
// Validation
ethereum.ChainValidator ethereum.ChainValidator
maxValidationThreshold uint64 // Number of block difference from remote peer to start validation
// Testing hooks // Testing hooks
syncInitHook func(uint64, uint64) // Method to call upon initiating a new sync run syncInitHook func(uint64, uint64) // Method to call upon initiating a new sync run
@ -241,6 +245,7 @@ func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchai
stateSyncStart: make(chan *stateSync), stateSyncStart: make(chan *stateSync),
syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(),
ChainValidator: whitelistService, ChainValidator: whitelistService,
maxValidationThreshold: maxValidationThreshold,
} }
// Create the post-merge skeleton syncer and start the process // Create the post-merge skeleton syncer and start the process
dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success)) dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success))
@ -893,13 +898,6 @@ func (d *Downloader) getFetchHeadersByNumber(p *peerConnection) func(number uint
// In the rare scenario when we ended up on a long reorganisation (i.e. none of // In the rare scenario when we ended up on a long reorganisation (i.e. none of
// the head links match), we do a binary search to find the common ancestor. // the head links match), we do a binary search to find the common ancestor.
func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) (uint64, error) { func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) (uint64, error) {
// Check the validity of peer from which the chain is to be downloaded
if d.ChainValidator != nil {
if _, err := d.IsValidPeer(d.getFetchHeadersByNumber(p)); err != nil {
return 0, err
}
}
// Figure out the valid ancestor range to prevent rewrite attacks // Figure out the valid ancestor range to prevent rewrite attacks
var ( var (
floor = int64(-1) floor = int64(-1)
@ -917,6 +915,25 @@ func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header)
localHeight = d.lightchain.CurrentHeader().Number.Uint64() localHeight = d.lightchain.CurrentHeader().Number.Uint64()
} }
// Check the validity of peer from which the chain is to be downloaded
if d.ChainValidator != nil {
_, err := d.IsValidPeer(d.getFetchHeadersByNumber(p))
if errors.Is(err, whitelist.ErrMismatch) {
return 0, err
}
if errors.Is(err, whitelist.ErrNoRemote) {
// Don't validate the peer against whitelisted milestones until the different of
// our local height and remote peer's height is less than `maxValidationThreshold`
if localHeight >= remoteHeight-d.maxValidationThreshold {
log.Info("Remote peer didn't respond", "id", p.id, "local", localHeight, "remote", remoteHeight, "err", err)
return 0, err
}
log.Info("Remote peer didn't respond but is far ahead, skipping validation", "id", p.id, "local", localHeight, "remote", remoteHeight, "err", err)
}
}
p.log.Debug("Looking for common ancestor", "local", localHeight, "remote", remoteHeight) p.log.Debug("Looking for common ancestor", "local", localHeight, "remote", remoteHeight)
// Recap floor value for binary search // Recap floor value for binary search

View file

@ -1649,14 +1649,42 @@ func TestFakedSyncProgress67NoRemoteCheckpoint(t *testing.T) {
chainA := testChainForkLightA.blocks chainA := testChainForkLightA.blocks
tester.newPeer("light", protocol, chainA[1:]) tester.newPeer("light", protocol, chainA[1:])
// Set the max validation threshold equal to chain length to enforce validation
tester.downloader.maxValidationThreshold = uint64(len(chainA) - 1)
// Synchronise with the peer and make sure all blocks were retrieved // Synchronise with the peer and make sure all blocks were retrieved
// Should fail in first attempt // Should fail in first attempt
if err := tester.sync("light", nil, mode); err != nil { err := tester.sync("light", nil, mode)
assert.Equal(t, whitelist.ErrNoRemote, err, "failed synchronisation") assert.Equal(t, whitelist.ErrNoRemote, err, "failed synchronisation")
}
// Try syncing again, should succeed // Try syncing again, should succeed
if err := tester.sync("light", nil, mode); err != nil { if err := tester.sync("light", nil, mode); err != nil {
t.Fatal("succeeded attacker synchronisation") t.Fatal("succeeded attacker synchronisation")
} }
} }
// TestFakedSyncProgress67BypassWhitelistValidation tests if peer validation
// via whitelist is bypassed when remote peer is far away or not
func TestFakedSyncProgress67BypassWhitelistValidation(t *testing.T) {
protocol := uint(eth.ETH67)
mode := FullSync
tester := newTester(t)
validate := func(count int) (bool, error) {
return false, whitelist.ErrNoRemote
}
tester.downloader.ChainValidator = newWhitelistFake(validate)
defer tester.terminate()
// 1223 length chain
chainA := testChainBase.blocks
tester.newPeer("light", protocol, chainA[1:])
// Although the validate function above returns an error (which says that
// remote peer doesn't have that block), sync will go through as the chain
// import length is 1223 which is more than the default threshold of 1024
err := tester.sync("light", nil, mode)
assert.NoError(t, err, "failed synchronisation")
}