diff --git a/eth/downloader/api.go b/eth/downloader/api.go deleted file mode 100644 index b3f7113bcd..0000000000 --- a/eth/downloader/api.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "context" - "sync" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/rpc" -) - -// DownloaderAPI provides an API which gives information about the current synchronisation status. -// It offers only methods that operates on data that can be available to anyone without security risks. -type DownloaderAPI struct { - d *Downloader - mux *event.TypeMux - installSyncSubscription chan chan interface{} - uninstallSyncSubscription chan *uninstallSyncSubscriptionRequest -} - -// NewDownloaderAPI create a new DownloaderAPI. The API has an internal event loop that -// listens for events from the downloader through the global event mux. In case it receives one of -// these events it broadcasts it to all syncing subscriptions that are installed through the -// installSyncSubscription channel. -func NewDownloaderAPI(d *Downloader, m *event.TypeMux) *DownloaderAPI { - api := &DownloaderAPI{ - d: d, - mux: m, - installSyncSubscription: make(chan chan interface{}), - uninstallSyncSubscription: make(chan *uninstallSyncSubscriptionRequest), - } - - go api.eventLoop() - - return api -} - -// eventLoop runs a loop until the event mux closes. It will install and uninstall new -// sync subscriptions and broadcasts sync status updates to the installed sync subscriptions. -func (api *DownloaderAPI) eventLoop() { - var ( - sub = api.mux.Subscribe(StartEvent{}, DoneEvent{}, FailedEvent{}) - syncSubscriptions = make(map[chan interface{}]struct{}) - ) - - for { - select { - case i := <-api.installSyncSubscription: - syncSubscriptions[i] = struct{}{} - case u := <-api.uninstallSyncSubscription: - delete(syncSubscriptions, u.c) - close(u.uninstalled) - case event := <-sub.Chan(): - if event == nil { - return - } - - var notification interface{} - switch event.Data.(type) { - case StartEvent: - notification = &SyncingResult{ - Syncing: true, - Status: api.d.Progress(), - } - case DoneEvent, FailedEvent: - notification = false - } - // broadcast - for c := range syncSubscriptions { - c <- notification - } - } - } -} - -// Syncing provides information when this nodes starts synchronising with the Ethereum network and when it's finished. -func (api *DownloaderAPI) Syncing(ctx context.Context) (*rpc.Subscription, error) { - notifier, supported := rpc.NotifierFromContext(ctx) - if !supported { - return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported - } - - rpcSub := notifier.CreateSubscription() - - go func() { - statuses := make(chan interface{}) - sub := api.SubscribeSyncStatus(statuses) - - for { - select { - case status := <-statuses: - notifier.Notify(rpcSub.ID, status) - case <-rpcSub.Err(): - sub.Unsubscribe() - return - case <-notifier.Closed(): - sub.Unsubscribe() - return - } - } - }() - - return rpcSub, nil -} - -// SyncingResult provides information about the current synchronisation status for this node. -type SyncingResult struct { - Syncing bool `json:"syncing"` - Status ethereum.SyncProgress `json:"status"` -} - -// uninstallSyncSubscriptionRequest uninstalls a syncing subscription in the API event loop. -type uninstallSyncSubscriptionRequest struct { - c chan interface{} - uninstalled chan interface{} -} - -// SyncStatusSubscription represents a syncing subscription. -type SyncStatusSubscription struct { - api *DownloaderAPI // register subscription in event loop of this api instance - c chan interface{} // channel where events are broadcasted to - unsubOnce sync.Once // make sure unsubscribe logic is executed once -} - -// Unsubscribe uninstalls the subscription from the DownloadAPI event loop. -// The status channel that was passed to subscribeSyncStatus isn't used anymore -// after this method returns. -func (s *SyncStatusSubscription) Unsubscribe() { - s.unsubOnce.Do(func() { - req := uninstallSyncSubscriptionRequest{s.c, make(chan interface{})} - s.api.uninstallSyncSubscription <- &req - - for { - select { - case <-s.c: - // drop new status events until uninstall confirmation - continue - case <-req.uninstalled: - return - } - } - }) -} - -// SubscribeSyncStatus creates a subscription that will broadcast new synchronisation updates. -// The given channel must receive interface values, the result can either. -func (api *DownloaderAPI) SubscribeSyncStatus(status chan interface{}) *SyncStatusSubscription { - api.installSyncSubscription <- status - return &SyncStatusSubscription{api: api, c: status} -} diff --git a/eth/downloader/beacondevsync.go b/eth/downloader/beacondevsync.go deleted file mode 100644 index 9a38fedd46..0000000000 --- a/eth/downloader/beacondevsync.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2023 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "errors" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" -) - -// BeaconDevSync is a development helper to test synchronization by providing -// a block hash instead of header to run the beacon sync against. -// -// The method will reach out to the network to retrieve the header of the sync -// target instead of receiving it from the consensus node. -// -// Note, this must not be used in live code. If the forkchcoice endpoint where -// to use this instead of giving us the payload first, then essentially nobody -// in the network would have the block yet that we'd attempt to retrieve. -func (d *Downloader) BeaconDevSync(mode SyncMode, hash common.Hash, stop chan struct{}) error { - // Be very loud that this code should not be used in a live node - log.Warn("----------------------------------") - log.Warn("Beacon syncing with hash as target", "hash", hash) - log.Warn("This is unhealthy for a live node!") - log.Warn("----------------------------------") - - log.Info("Waiting for peers to retrieve sync target") - for { - // If the node is going down, unblock - select { - case <-stop: - return errors.New("stop requested") - default: - } - // Pick a random peer to sync from and keep retrying if none are yet - // available due to fresh startup - d.peers.lock.RLock() - var peer *peerConnection - for _, peer = range d.peers.peers { - break - } - d.peers.lock.RUnlock() - - if peer == nil { - time.Sleep(time.Second) - continue - } - // Found a peer, attempt to retrieve the header whilst blocking and - // retry if it fails for whatever reason - log.Info("Attempting to retrieve sync target", "peer", peer.id) - headers, metas, err := d.fetchHeadersByHash(peer, hash, 1, 0, false) - if err != nil || len(headers) != 1 { - log.Warn("Failed to fetch sync target", "headers", len(headers), "err", err) - time.Sleep(time.Second) - continue - } - // Head header retrieved, if the hash matches, start the actual sync - if metas[0] != hash { - log.Error("Received invalid sync target", "want", hash, "have", metas[0]) - time.Sleep(time.Second) - continue - } - return d.BeaconSync(mode, headers[0], headers[0]) - } -} diff --git a/eth/downloader/beaconsync.go b/eth/downloader/beaconsync.go deleted file mode 100644 index df8af68bc7..0000000000 --- a/eth/downloader/beaconsync.go +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "fmt" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" -) - -// beaconBackfiller is the chain and state backfilling that can be commenced once -// the skeleton syncer has successfully reverse downloaded all the headers up to -// the genesis block or an existing header in the database. Its operation is fully -// directed by the skeleton sync's head/tail events. -type beaconBackfiller struct { - downloader *Downloader // Downloader to direct via this callback implementation - syncMode SyncMode // Sync mode to use for backfilling the skeleton chains - success func() // Callback to run on successful sync cycle completion - filling bool // Flag whether the downloader is backfilling or not - filled *types.Header // Last header filled by the last terminated sync loop - started chan struct{} // Notification channel whether the downloader inited - lock sync.Mutex // Mutex protecting the sync lock -} - -// newBeaconBackfiller is a helper method to create the backfiller. -func newBeaconBackfiller(dl *Downloader, success func()) backfiller { - return &beaconBackfiller{ - downloader: dl, - success: success, - } -} - -// suspend cancels any background downloader threads and returns the last header -// that has been successfully backfilled. -func (b *beaconBackfiller) suspend() *types.Header { - // If no filling is running, don't waste cycles - b.lock.Lock() - filling := b.filling - filled := b.filled - started := b.started - b.lock.Unlock() - - if !filling { - return filled // Return the filled header on the previous sync completion - } - // A previous filling should be running, though it may happen that it hasn't - // yet started (being done on a new goroutine). Many concurrent beacon head - // announcements can lead to sync start/stop thrashing. In that case we need - // to wait for initialization before we can safely cancel it. It is safe to - // read this channel multiple times, it gets closed on startup. - <-started - - // Now that we're sure the downloader successfully started up, we can cancel - // it safely without running the risk of data races. - b.downloader.Cancel() - - // Sync cycle was just terminated, retrieve and return the last filled header. - // Can't use `filled` as that contains a stale value from before cancellation. - return b.downloader.blockchain.CurrentSnapBlock() -} - -// resume starts the downloader threads for backfilling state and chain data. -func (b *beaconBackfiller) resume() { - b.lock.Lock() - if b.filling { - // If a previous filling cycle is still running, just ignore this start - // request. // TODO(karalabe): We should make this channel driven - b.lock.Unlock() - return - } - b.filling = true - b.filled = nil - b.started = make(chan struct{}) - mode := b.syncMode - b.lock.Unlock() - - // Start the backfilling on its own thread since the downloader does not have - // its own lifecycle runloop. - go func() { - // Set the backfiller to non-filling when download completes - defer func() { - b.lock.Lock() - b.filling = false - b.filled = b.downloader.blockchain.CurrentSnapBlock() - b.lock.Unlock() - }() - // If the downloader fails, report an error as in beacon chain mode there - // should be no errors as long as the chain we're syncing to is valid. - if err := b.downloader.synchronise("", common.Hash{}, nil, nil, mode, true, b.started); err != nil { - log.Error("Beacon backfilling failed", "err", err) - return - } - // Synchronization succeeded. Since this happens async, notify the outer - // context to disable snap syncing and enable transaction propagation. - if b.success != nil { - b.success() - } - }() -} - -// setMode updates the sync mode from the current one to the requested one. If -// there's an active sync in progress, it will be cancelled and restarted. -func (b *beaconBackfiller) setMode(mode SyncMode) { - // Update the old sync mode and track if it was changed - b.lock.Lock() - updated := b.syncMode != mode - filling := b.filling - b.syncMode = mode - b.lock.Unlock() - - // If the sync mode was changed mid-sync, restart. This should never ever - // really happen, we just handle it to detect programming errors. - if !updated || !filling { - return - } - log.Error("Downloader sync mode changed mid-run", "old", mode.String(), "new", mode.String()) - b.suspend() - b.resume() -} - -// SetBadBlockCallback sets the callback to run when a bad block is hit by the -// block processor. This method is not thread safe and should be set only once -// on startup before system events are fired. -func (d *Downloader) SetBadBlockCallback(onBadBlock badBlockFn) { - d.badBlock = onBadBlock -} - -// BeaconSync is the post-merge version of the chain synchronization, where the -// chain is not downloaded from genesis onward, rather from trusted head announces -// backwards. -// -// Internally backfilling and state sync is done the same way, but the header -// retrieval and scheduling is replaced. -func (d *Downloader) BeaconSync(mode SyncMode, head *types.Header, final *types.Header) error { - return d.beaconSync(mode, head, final, true) -} - -// BeaconExtend is an optimistic version of BeaconSync, where an attempt is made -// to extend the current beacon chain with a new header, but in case of a mismatch, -// the old sync will not be terminated and reorged, rather the new head is dropped. -// -// This is useful if a beacon client is feeding us large chunks of payloads to run, -// but is not setting the head after each. -func (d *Downloader) BeaconExtend(mode SyncMode, head *types.Header) error { - return d.beaconSync(mode, head, nil, false) -} - -// beaconSync is the post-merge version of the chain synchronization, where the -// chain is not downloaded from genesis onward, rather from trusted head announces -// backwards. -// -// Internally backfilling and state sync is done the same way, but the header -// retrieval and scheduling is replaced. -func (d *Downloader) beaconSync(mode SyncMode, head *types.Header, final *types.Header, force bool) error { - // When the downloader starts a sync cycle, it needs to be aware of the sync - // mode to use (full, snap). To keep the skeleton chain oblivious, inject the - // mode into the backfiller directly. - // - // Super crazy dangerous type cast. Should be fine (TM), we're only using a - // different backfiller implementation for skeleton tests. - d.skeleton.filler.(*beaconBackfiller).setMode(mode) - - // Signal the skeleton sync to switch to a new head, however it wants - if err := d.skeleton.Sync(head, final, force); err != nil { - return err - } - return nil -} - -// findBeaconAncestor tries to locate the common ancestor link of the local chain -// and the beacon chain just requested. In the general case when our node was in -// sync and on the correct chain, checking the top N links should already get us -// a match. 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 ancestor. -func (d *Downloader) findBeaconAncestor() (uint64, error) { - // Figure out the current local head position - var chainHead *types.Header - - switch d.getMode() { - case FullSync: - chainHead = d.blockchain.CurrentBlock() - case SnapSync: - chainHead = d.blockchain.CurrentSnapBlock() - default: - chainHead = d.lightchain.CurrentHeader() - } - number := chainHead.Number.Uint64() - - // Retrieve the skeleton bounds and ensure they are linked to the local chain - beaconHead, beaconTail, _, err := d.skeleton.Bounds() - if err != nil { - // This is a programming error. The chain backfiller was called with an - // invalid beacon sync state. Ideally we would panic here, but erroring - // gives us at least a remote chance to recover. It's still a big fault! - log.Error("Failed to retrieve beacon bounds", "err", err) - return 0, err - } - var linked bool - switch d.getMode() { - case FullSync: - linked = d.blockchain.HasBlock(beaconTail.ParentHash, beaconTail.Number.Uint64()-1) - case SnapSync: - linked = d.blockchain.HasFastBlock(beaconTail.ParentHash, beaconTail.Number.Uint64()-1) - default: - linked = d.blockchain.HasHeader(beaconTail.ParentHash, beaconTail.Number.Uint64()-1) - } - if !linked { - // This is a programming error. The chain backfiller was called with a - // tail that's not linked to the local chain. Whilst this should never - // happen, there might be some weirdnesses if beacon sync backfilling - // races with the user (or beacon client) calling setHead. Whilst panic - // would be the ideal thing to do, it is safer long term to attempt a - // recovery and fix any noticed issue after the fact. - log.Error("Beacon sync linkup unavailable", "number", beaconTail.Number.Uint64()-1, "hash", beaconTail.ParentHash) - return 0, fmt.Errorf("beacon linkup unavailable locally: %d [%x]", beaconTail.Number.Uint64()-1, beaconTail.ParentHash) - } - // Binary search to find the ancestor - start, end := beaconTail.Number.Uint64()-1, number - if number := beaconHead.Number.Uint64(); end > number { - // This shouldn't really happen in a healthy network, but if the consensus - // clients feeds us a shorter chain as the canonical, we should not attempt - // to access non-existent skeleton items. - log.Warn("Beacon head lower than local chain", "beacon", number, "local", end) - end = number - } - for start+1 < end { - // Split our chain interval in two, and request the hash to cross check - check := (start + end) / 2 - - h := d.skeleton.Header(check) - n := h.Number.Uint64() - - var known bool - switch d.getMode() { - case FullSync: - known = d.blockchain.HasBlock(h.Hash(), n) - case SnapSync: - known = d.blockchain.HasFastBlock(h.Hash(), n) - default: - known = d.lightchain.HasHeader(h.Hash(), n) - } - if !known { - end = check - continue - } - start = check - } - return start, nil -} - -// fetchBeaconHeaders feeds skeleton headers to the downloader queue for scheduling -// until sync errors or is finished. -func (d *Downloader) fetchBeaconHeaders(from uint64) error { - var head *types.Header - _, tail, _, err := d.skeleton.Bounds() - if err != nil { - return err - } - // A part of headers are not in the skeleton space, try to resolve - // them from the local chain. Note the range should be very short - // and it should only happen when there are less than 64 post-merge - // blocks in the network. - var localHeaders []*types.Header - if from < tail.Number.Uint64() { - count := tail.Number.Uint64() - from - if count > uint64(fsMinFullBlocks) { - return fmt.Errorf("invalid origin (%d) of beacon sync (%d)", from, tail.Number) - } - localHeaders = d.readHeaderRange(tail, int(count)) - log.Warn("Retrieved beacon headers from local", "from", from, "count", count) - } - for { - // Some beacon headers might have appeared since the last cycle, make - // sure we're always syncing to all available ones - head, _, _, err = d.skeleton.Bounds() - if err != nil { - return err - } - // If the pivot became stale (older than 2*64-8 (bit of wiggle room)), - // move it ahead to HEAD-64 - d.pivotLock.Lock() - if d.pivotHeader != nil { - if head.Number.Uint64() > d.pivotHeader.Number.Uint64()+2*uint64(fsMinFullBlocks)-8 { - // Retrieve the next pivot header, either from skeleton chain - // or the filled chain - number := head.Number.Uint64() - uint64(fsMinFullBlocks) - - log.Warn("Pivot seemingly stale, moving", "old", d.pivotHeader.Number, "new", number) - if d.pivotHeader = d.skeleton.Header(number); d.pivotHeader == nil { - if number < tail.Number.Uint64() { - dist := tail.Number.Uint64() - number - if len(localHeaders) >= int(dist) { - d.pivotHeader = localHeaders[dist-1] - log.Warn("Retrieved pivot header from local", "number", d.pivotHeader.Number, "hash", d.pivotHeader.Hash(), "latest", head.Number, "oldest", tail.Number) - } - } - } - // Print an error log and return directly in case the pivot header - // is still not found. It means the skeleton chain is not linked - // correctly with local chain. - if d.pivotHeader == nil { - log.Error("Pivot header is not found", "number", number) - d.pivotLock.Unlock() - return errNoPivotHeader - } - // Write out the pivot into the database so a rollback beyond - // it will reenable snap sync and update the state root that - // the state syncer will be downloading - rawdb.WriteLastPivotNumber(d.stateDB, d.pivotHeader.Number.Uint64()) - } - } - d.pivotLock.Unlock() - - // Retrieve a batch of headers and feed it to the header processor - var ( - headers = make([]*types.Header, 0, maxHeadersProcess) - hashes = make([]common.Hash, 0, maxHeadersProcess) - ) - for i := 0; i < maxHeadersProcess && from <= head.Number.Uint64(); i++ { - header := d.skeleton.Header(from) - - // The header is not found in skeleton space, try to find it in local chain. - if header == nil && from < tail.Number.Uint64() { - dist := tail.Number.Uint64() - from - if len(localHeaders) >= int(dist) { - header = localHeaders[dist-1] - } - } - // The header is still missing, the beacon sync is corrupted and bail out - // the error here. - if header == nil { - return fmt.Errorf("missing beacon header %d", from) - } - headers = append(headers, header) - hashes = append(hashes, headers[i].Hash()) - from++ - } - if len(headers) > 0 { - log.Trace("Scheduling new beacon headers", "count", len(headers), "from", from-uint64(len(headers))) - select { - case d.headerProcCh <- &headerTask{ - headers: headers, - hashes: hashes, - }: - case <-d.cancelCh: - return errCanceled - } - } - // If we still have headers to import, loop and keep pushing them - if from <= head.Number.Uint64() { - continue - } - // If the pivot block is committed, signal header sync termination - if d.committed.Load() { - select { - case d.headerProcCh <- nil: - return nil - case <-d.cancelCh: - return errCanceled - } - } - // State sync still going, wait a bit for new headers and retry - log.Trace("Pivot not yet committed, waiting...") - select { - case <-time.After(fsHeaderContCheck): - case <-d.cancelCh: - return errCanceled - } - } -} diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go deleted file mode 100644 index f1cfa92d5d..0000000000 --- a/eth/downloader/downloader.go +++ /dev/null @@ -1,1844 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Package downloader contains the manual full chain synchronisation. -package downloader - -import ( - "errors" - "fmt" - "math/big" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state/snapshot" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" -) - -var ( - MaxBlockFetch = 128 // Amount of blocks to be fetched per retrieval request - MaxHeaderFetch = 192 // Amount of block headers to be fetched per retrieval request - MaxSkeletonSize = 128 // Number of header fetches to need for a skeleton assembly - MaxReceiptFetch = 256 // Amount of transaction receipts to allow fetching per request - - maxQueuedHeaders = 32 * 1024 // [eth/62] Maximum number of headers to queue for import (DOS protection) - maxHeadersProcess = 2048 // Number of header download results to import at once into the chain - maxResultsProcess = 2048 // Number of content download results to import at once into the chain - fullMaxForkAncestry uint64 = params.FullImmutabilityThreshold // Maximum chain reorganisation (locally redeclared so tests can reduce it) - lightMaxForkAncestry uint64 = params.LightImmutabilityThreshold // Maximum chain reorganisation (locally redeclared so tests can reduce it) - - reorgProtThreshold = 48 // Threshold number of recent blocks to disable mini reorg protection - reorgProtHeaderDelay = 2 // Number of headers to delay delivering to cover mini reorgs - - 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 - fsMinFullBlocks = 64 // Number of blocks to retrieve fully even in snap sync -) - -var ( - errBusy = errors.New("busy") - errUnknownPeer = errors.New("peer is unknown or unhealthy") - errBadPeer = errors.New("action from bad peer ignored") - errStallingPeer = errors.New("peer is stalling") - errUnsyncedPeer = errors.New("unsynced peer") - errNoPeers = errors.New("no peers to keep download active") - errTimeout = errors.New("timeout") - errEmptyHeaderSet = errors.New("empty header set by peer") - errPeersUnavailable = errors.New("no peers available or all tried for download") - errInvalidAncestor = errors.New("retrieved ancestor is invalid") - errInvalidChain = errors.New("retrieved hash chain is invalid") - errInvalidBody = errors.New("retrieved block body is invalid") - errInvalidReceipt = errors.New("retrieved receipt is invalid") - errCancelStateFetch = errors.New("state data download canceled (requested)") - errCancelContentProcessing = errors.New("content processing canceled (requested)") - errCanceled = errors.New("syncing canceled (requested)") - errTooOld = errors.New("peer's protocol version too old") - errNoAncestorFound = errors.New("no common ancestor found") - errNoPivotHeader = errors.New("pivot header is not found") - ErrMergeTransition = errors.New("legacy sync reached the merge") -) - -// peerDropFn is a callback type for dropping a peer detected as malicious. -type peerDropFn func(id string) - -// badBlockFn is a callback for the async beacon sync to notify the caller that -// the origin header requested to sync to, produced a chain with a bad block. -type badBlockFn func(invalid *types.Header, origin *types.Header) - -// headerTask is a set of downloaded headers to queue along with their precomputed -// hashes to avoid constant rehashing. -type headerTask struct { - headers []*types.Header - hashes []common.Hash -} - -type Downloader struct { - mode atomic.Uint32 // Synchronisation mode defining the strategy used (per sync cycle), use d.getMode() to get the SyncMode - mux *event.TypeMux // Event multiplexer to announce sync operation events - - genesis uint64 // Genesis block number to limit sync to (e.g. light client CHT) - queue *queue // Scheduler for selecting the hashes to download - peers *peerSet // Set of active peers from which download can proceed - - stateDB ethdb.Database // Database to state sync into (and deduplicate via) - - // Statistics - syncStatsChainOrigin uint64 // Origin block number where syncing started at - syncStatsChainHeight uint64 // Highest block number known when syncing started - syncStatsLock sync.RWMutex // Lock protecting the sync stats fields - - lightchain LightChain - blockchain BlockChain - - // Callbacks - dropPeer peerDropFn // Drops a peer for misbehaving - badBlock badBlockFn // Reports a block as rejected by the chain - - // Status - synchroniseMock func(id string, hash common.Hash) error // Replacement for synchronise during testing - synchronising atomic.Bool - notified atomic.Bool - committed atomic.Bool - ancientLimit uint64 // The maximum block number which can be regarded as ancient data. - - // Channels - headerProcCh chan *headerTask // Channel to feed the header processor new tasks - - // Skeleton sync - skeleton *skeleton // Header skeleton to backfill the chain with (eth2 mode) - - // State sync - pivotHeader *types.Header // Pivot block header to dynamically push the syncing state root - pivotLock sync.RWMutex // Lock protecting pivot header reads from updates - - SnapSyncer *snap.Syncer // TODO(karalabe): make private! hack for now - stateSyncStart chan *stateSync - - // Cancellation and termination - cancelPeer string // Identifier of the peer currently being used as the master (cancel on drop) - cancelCh chan struct{} // Channel to cancel mid-flight syncs - cancelLock sync.RWMutex // Lock to protect the cancel channel and peer in delivers - cancelWg sync.WaitGroup // Make sure all fetcher goroutines have exited. - - quitCh chan struct{} // Quit channel to signal termination - quitLock sync.Mutex // Lock to prevent double closes - - // Testing hooks - syncInitHook func(uint64, uint64) // Method to call upon initiating a new sync run - bodyFetchHook func([]*types.Header) // Method to call upon starting a block body fetch - receiptFetchHook func([]*types.Header) // Method to call upon starting a receipt fetch - chainInsertHook func([]*fetchResult) // Method to call upon inserting a chain of blocks (possibly in multiple invocations) - - // Progress reporting metrics - syncStartBlock uint64 // Head snap block when Geth was started - syncStartTime time.Time // Time instance when chain sync started - syncLogTime time.Time // Time instance when status was last reported -} - -// LightChain encapsulates functions required to synchronise a light chain. -type LightChain interface { - // HasHeader verifies a header's presence in the local chain. - HasHeader(common.Hash, uint64) bool - - // GetHeaderByHash retrieves a header from the local chain. - GetHeaderByHash(common.Hash) *types.Header - - // CurrentHeader retrieves the head header from the local chain. - CurrentHeader() *types.Header - - // GetTd returns the total difficulty of a local block. - GetTd(common.Hash, uint64) *big.Int - - // InsertHeaderChain inserts a batch of headers into the local chain. - InsertHeaderChain([]*types.Header) (int, error) - - // SetHead rewinds the local chain to a new head. - SetHead(uint64) error -} - -// BlockChain encapsulates functions required to sync a (full or snap) blockchain. -type BlockChain interface { - LightChain - - // HasBlock verifies a block's presence in the local chain. - HasBlock(common.Hash, uint64) bool - - // HasFastBlock verifies a snap block's presence in the local chain. - HasFastBlock(common.Hash, uint64) bool - - // GetBlockByHash retrieves a block from the local chain. - GetBlockByHash(common.Hash) *types.Block - - // CurrentBlock retrieves the head block from the local chain. - CurrentBlock() *types.Header - - // CurrentSnapBlock retrieves the head snap block from the local chain. - CurrentSnapBlock() *types.Header - - // SnapSyncCommitHead directly commits the head block to a certain entity. - SnapSyncCommitHead(common.Hash) error - - // InsertChain inserts a batch of blocks into the local chain. - InsertChain(types.Blocks) (int, error) - - // InsertReceiptChain inserts a batch of receipts into the local chain. - InsertReceiptChain(types.Blocks, []types.Receipts, uint64) (int, error) - - // Snapshots returns the blockchain snapshot tree to paused it during sync. - Snapshots() *snapshot.Tree - - // TrieDB retrieves the low level trie database used for interacting - // with trie nodes. - TrieDB() *trie.Database -} - -// New creates a new downloader to fetch hashes and blocks from remote peers. -func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, lightchain LightChain, dropPeer peerDropFn, success func()) *Downloader { - if lightchain == nil { - lightchain = chain - } - dl := &Downloader{ - stateDB: stateDb, - mux: mux, - queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), - peers: newPeerSet(), - blockchain: chain, - lightchain: lightchain, - dropPeer: dropPeer, - headerProcCh: make(chan *headerTask, 1), - quitCh: make(chan struct{}), - SnapSyncer: snap.NewSyncer(stateDb, chain.TrieDB().Scheme()), - stateSyncStart: make(chan *stateSync), - syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), - } - // Create the post-merge skeleton syncer and start the process - dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success)) - - go dl.stateFetcher() - return dl -} - -// Progress retrieves the synchronisation boundaries, specifically the origin -// block where synchronisation started at (may have failed/suspended); the block -// or header sync is currently at; and the latest known block which the sync targets. -// -// In addition, during the state download phase of snap synchronisation the number -// of processed and the total number of known states are also returned. Otherwise -// these are zero. -func (d *Downloader) Progress() ethereum.SyncProgress { - // Lock the current stats and return the progress - d.syncStatsLock.RLock() - defer d.syncStatsLock.RUnlock() - - current := uint64(0) - mode := d.getMode() - switch { - case d.blockchain != nil && mode == FullSync: - current = d.blockchain.CurrentBlock().Number.Uint64() - case d.blockchain != nil && mode == SnapSync: - current = d.blockchain.CurrentSnapBlock().Number.Uint64() - case d.lightchain != nil: - current = d.lightchain.CurrentHeader().Number.Uint64() - default: - log.Error("Unknown downloader chain/mode combo", "light", d.lightchain != nil, "full", d.blockchain != nil, "mode", mode) - } - progress, pending := d.SnapSyncer.Progress() - - return ethereum.SyncProgress{ - StartingBlock: d.syncStatsChainOrigin, - CurrentBlock: current, - HighestBlock: d.syncStatsChainHeight, - SyncedAccounts: progress.AccountSynced, - SyncedAccountBytes: uint64(progress.AccountBytes), - SyncedBytecodes: progress.BytecodeSynced, - SyncedBytecodeBytes: uint64(progress.BytecodeBytes), - SyncedStorage: progress.StorageSynced, - SyncedStorageBytes: uint64(progress.StorageBytes), - HealedTrienodes: progress.TrienodeHealSynced, - HealedTrienodeBytes: uint64(progress.TrienodeHealBytes), - HealedBytecodes: progress.BytecodeHealSynced, - HealedBytecodeBytes: uint64(progress.BytecodeHealBytes), - HealingTrienodes: pending.TrienodeHeal, - HealingBytecode: pending.BytecodeHeal, - } -} - -// RegisterPeer injects a new download peer into the set of block source to be -// used for fetching hashes and blocks from. -func (d *Downloader) RegisterPeer(id string, version uint, peer Peer) error { - var logger log.Logger - if len(id) < 16 { - // Tests use short IDs, don't choke on them - logger = log.New("peer", id) - } else { - logger = log.New("peer", id[:8]) - } - logger.Trace("Registering sync peer") - if err := d.peers.Register(newPeerConnection(id, version, peer, logger)); err != nil { - logger.Error("Failed to register sync peer", "err", err) - return err - } - return nil -} - -// UnregisterPeer remove a peer from the known list, preventing any action from -// the specified peer. An effort is also made to return any pending fetches into -// the queue. -func (d *Downloader) UnregisterPeer(id string) error { - // Unregister the peer from the active peer set and revoke any fetch tasks - var logger log.Logger - if len(id) < 16 { - // Tests use short IDs, don't choke on them - logger = log.New("peer", id) - } else { - logger = log.New("peer", id[:8]) - } - logger.Trace("Unregistering sync peer") - if err := d.peers.Unregister(id); err != nil { - logger.Error("Failed to unregister sync peer", "err", err) - return err - } - d.queue.Revoke(id) - - return nil -} - -// LegacySync tries to sync up our local block chain with a remote peer, both -// adding various sanity checks as well as wrapping it with various log entries. -func (d *Downloader) LegacySync(id string, head common.Hash, td, ttd *big.Int, mode SyncMode) error { - err := d.synchronise(id, head, td, ttd, mode, false, nil) - - switch err { - case nil, errBusy, errCanceled: - return err - } - if errors.Is(err, errInvalidChain) || errors.Is(err, errBadPeer) || errors.Is(err, errTimeout) || - errors.Is(err, errStallingPeer) || errors.Is(err, errUnsyncedPeer) || errors.Is(err, errEmptyHeaderSet) || - errors.Is(err, errPeersUnavailable) || errors.Is(err, errTooOld) || errors.Is(err, errInvalidAncestor) { - log.Warn("Synchronisation failed, dropping peer", "peer", id, "err", err) - if d.dropPeer == nil { - // The dropPeer method is nil when `--copydb` is used for a local copy. - // Timeouts can occur if e.g. compaction hits at the wrong time, and can be ignored - log.Warn("Downloader wants to drop peer, but peerdrop-function is not set", "peer", id) - } else { - d.dropPeer(id) - } - return err - } - if errors.Is(err, ErrMergeTransition) { - return err // This is an expected fault, don't keep printing it in a spin-loop - } - log.Warn("Synchronisation failed, retrying", "err", err) - return err -} - -// synchronise will select the peer and use it for synchronising. If an empty string is given -// it will use the best peer possible and synchronize if its TD is higher than our own. If any of the -// checks fail an error will be returned. This method is synchronous -func (d *Downloader) synchronise(id string, hash common.Hash, td, ttd *big.Int, mode SyncMode, beaconMode bool, beaconPing chan struct{}) error { - // The beacon header syncer is async. It will start this synchronization and - // will continue doing other tasks. However, if synchronization needs to be - // cancelled, the syncer needs to know if we reached the startup point (and - // inited the cancel channel) or not yet. Make sure that we'll signal even in - // case of a failure. - if beaconPing != nil { - defer func() { - select { - case <-beaconPing: // already notified - default: - close(beaconPing) // weird exit condition, notify that it's safe to cancel (the nothing) - } - }() - } - // Mock out the synchronisation if testing - if d.synchroniseMock != nil { - return d.synchroniseMock(id, hash) - } - // Make sure only one goroutine is ever allowed past this point at once - if !d.synchronising.CompareAndSwap(false, true) { - return errBusy - } - defer d.synchronising.Store(false) - - // Post a user notification of the sync (only once per session) - if d.notified.CompareAndSwap(false, true) { - log.Info("Block synchronisation started") - } - if mode == SnapSync { - // Snap sync will directly modify the persistent state, making the entire - // trie database unusable until the state is fully synced. To prevent any - // subsequent state reads, explicitly disable the trie database and state - // syncer is responsible to address and correct any state missing. - if d.blockchain.TrieDB().Scheme() == rawdb.PathScheme { - if err := d.blockchain.TrieDB().Disable(); err != nil { - return err - } - } - // Snap sync uses the snapshot namespace to store potentially flaky data until - // sync completely heals and finishes. Pause snapshot maintenance in the mean- - // time to prevent access. - if snapshots := d.blockchain.Snapshots(); snapshots != nil { // Only nil in tests - snapshots.Disable() - } - } - // Reset the queue, peer set and wake channels to clean any internal leftover state - d.queue.Reset(blockCacheMaxItems, blockCacheInitialItems) - d.peers.Reset() - - for _, ch := range []chan bool{d.queue.blockWakeCh, d.queue.receiptWakeCh} { - select { - case <-ch: - default: - } - } - for empty := false; !empty; { - select { - case <-d.headerProcCh: - default: - empty = true - } - } - // Create cancel channel for aborting mid-flight and mark the master peer - d.cancelLock.Lock() - d.cancelCh = make(chan struct{}) - d.cancelPeer = id - d.cancelLock.Unlock() - - defer d.Cancel() // No matter what, we can't leave the cancel channel open - - // Atomically set the requested sync mode - d.mode.Store(uint32(mode)) - - // Retrieve the origin peer and initiate the downloading process - var p *peerConnection - if !beaconMode { // Beacon mode doesn't need a peer to sync from - p = d.peers.Peer(id) - if p == nil { - return errUnknownPeer - } - } - if beaconPing != nil { - close(beaconPing) - } - return d.syncWithPeer(p, hash, td, ttd, beaconMode) -} - -func (d *Downloader) getMode() SyncMode { - return SyncMode(d.mode.Load()) -} - -// syncWithPeer starts a block synchronization based on the hash chain from the -// specified peer and head hash. -func (d *Downloader) syncWithPeer(p *peerConnection, hash common.Hash, td, ttd *big.Int, beaconMode bool) (err error) { - d.mux.Post(StartEvent{}) - defer func() { - // reset on error - if err != nil { - d.mux.Post(FailedEvent{err}) - } else { - latest := d.lightchain.CurrentHeader() - d.mux.Post(DoneEvent{latest}) - } - }() - mode := d.getMode() - - if !beaconMode { - log.Debug("Synchronising with the network", "peer", p.id, "eth", p.version, "head", hash, "td", td, "mode", mode) - } else { - log.Debug("Backfilling with the network", "mode", mode) - } - defer func(start time.Time) { - log.Debug("Synchronisation terminated", "elapsed", common.PrettyDuration(time.Since(start))) - }(time.Now()) - - // Look up the sync boundaries: the common ancestor and the target block - var latest, pivot, final *types.Header - if !beaconMode { - // In legacy mode, use the master peer to retrieve the headers from - latest, pivot, err = d.fetchHead(p) - if err != nil { - return err - } - } else { - // In beacon mode, use the skeleton chain to retrieve the headers from - latest, _, final, err = d.skeleton.Bounds() - if err != nil { - return err - } - if latest.Number.Uint64() > uint64(fsMinFullBlocks) { - number := latest.Number.Uint64() - uint64(fsMinFullBlocks) - - // Retrieve the pivot header from the skeleton chain segment but - // fallback to local chain if it's not found in skeleton space. - if pivot = d.skeleton.Header(number); pivot == nil { - _, oldest, _, _ := d.skeleton.Bounds() // error is already checked - if number < oldest.Number.Uint64() { - count := int(oldest.Number.Uint64() - number) // it's capped by fsMinFullBlocks - headers := d.readHeaderRange(oldest, count) - if len(headers) == count { - pivot = headers[len(headers)-1] - log.Warn("Retrieved pivot header from local", "number", pivot.Number, "hash", pivot.Hash(), "latest", latest.Number, "oldest", oldest.Number) - } - } - } - // Print an error log and return directly in case the pivot header - // is still not found. It means the skeleton chain is not linked - // correctly with local chain. - if pivot == nil { - log.Error("Pivot header is not found", "number", number) - return errNoPivotHeader - } - } - } - // If no pivot block was returned, the head is below the min full block - // threshold (i.e. new chain). In that case we won't really snap sync - // anyway, but still need a valid pivot block to avoid some code hitting - // nil panics on access. - if mode == SnapSync && pivot == nil { - pivot = d.blockchain.CurrentBlock() - } - height := latest.Number.Uint64() - - var origin uint64 - if !beaconMode { - // In legacy mode, reach out to the network and find the ancestor - origin, err = d.findAncestor(p, latest) - if err != nil { - return err - } - } else { - // In beacon mode, use the skeleton chain for the ancestor lookup - origin, err = d.findBeaconAncestor() - if err != nil { - return err - } - } - d.syncStatsLock.Lock() - if d.syncStatsChainHeight <= origin || d.syncStatsChainOrigin > origin { - d.syncStatsChainOrigin = origin - } - d.syncStatsChainHeight = height - d.syncStatsLock.Unlock() - - // Ensure our origin point is below any snap sync pivot point - if mode == SnapSync { - if height <= uint64(fsMinFullBlocks) { - origin = 0 - } else { - pivotNumber := pivot.Number.Uint64() - if pivotNumber <= origin { - origin = pivotNumber - 1 - } - // Write out the pivot into the database so a rollback beyond it will - // reenable snap sync - rawdb.WriteLastPivotNumber(d.stateDB, pivotNumber) - } - } - d.committed.Store(true) - if mode == SnapSync && pivot.Number.Uint64() != 0 { - d.committed.Store(false) - } - if mode == SnapSync { - // Set the ancient data limitation. If we are running snap sync, all block - // data older than ancientLimit will be written to the ancient store. More - // recent data will be written to the active database and will wait for the - // freezer to migrate. - // - // If the network is post-merge, use either the last announced finalized - // block as the ancient limit, or if we haven't yet received one, the head- - // a max fork ancestry limit. One quirky case if we've already passed the - // finalized block, in which case the skeleton.Bounds will return nil and - // we'll revert to head - 90K. That's fine, we're finishing sync anyway. - // - // For non-merged networks, if there is a checkpoint available, then calculate - // the ancientLimit through that. Otherwise calculate the ancient limit through - // the advertised height of the remote peer. This most is mostly a fallback for - // legacy networks, but should eventually be dropped. TODO(karalabe). - if beaconMode { - // Beacon sync, use the latest finalized block as the ancient limit - // or a reasonable height if no finalized block is yet announced. - if final != nil { - d.ancientLimit = final.Number.Uint64() - } else if height > fullMaxForkAncestry+1 { - d.ancientLimit = height - fullMaxForkAncestry - 1 - } else { - d.ancientLimit = 0 - } - } else { - // Legacy sync, use the best announcement we have from the remote peer. - // TODO(karalabe): Drop this pathway. - if height > fullMaxForkAncestry+1 { - d.ancientLimit = height - fullMaxForkAncestry - 1 - } else { - d.ancientLimit = 0 - } - } - frozen, _ := d.stateDB.Ancients() // Ignore the error here since light client can also hit here. - - // If a part of blockchain data has already been written into active store, - // disable the ancient style insertion explicitly. - if origin >= frozen && frozen != 0 { - d.ancientLimit = 0 - log.Info("Disabling direct-ancient mode", "origin", origin, "ancient", frozen-1) - } else if d.ancientLimit > 0 { - log.Debug("Enabling direct-ancient mode", "ancient", d.ancientLimit) - } - // Rewind the ancient store and blockchain if reorg happens. - if origin+1 < frozen { - if err := d.lightchain.SetHead(origin); err != nil { - return err - } - } - } - // Initiate the sync using a concurrent header and content retrieval algorithm - d.queue.Prepare(origin+1, mode) - if d.syncInitHook != nil { - d.syncInitHook(origin, height) - } - var headerFetcher func() error - if !beaconMode { - // In legacy mode, headers are retrieved from the network - headerFetcher = func() error { return d.fetchHeaders(p, origin+1, latest.Number.Uint64()) } - } else { - // In beacon mode, headers are served by the skeleton syncer - headerFetcher = func() error { return d.fetchBeaconHeaders(origin + 1) } - } - fetchers := []func() error{ - headerFetcher, // Headers are always retrieved - func() error { return d.fetchBodies(origin+1, beaconMode) }, // Bodies are retrieved during normal and snap sync - func() error { return d.fetchReceipts(origin+1, beaconMode) }, // Receipts are retrieved during snap sync - func() error { return d.processHeaders(origin+1, td, ttd, beaconMode) }, - } - if mode == SnapSync { - d.pivotLock.Lock() - d.pivotHeader = pivot - d.pivotLock.Unlock() - - fetchers = append(fetchers, func() error { return d.processSnapSyncContent() }) - } else if mode == FullSync { - fetchers = append(fetchers, func() error { return d.processFullSyncContent(ttd, beaconMode) }) - } - return d.spawnSync(fetchers) -} - -// spawnSync runs d.process and all given fetcher functions to completion in -// separate goroutines, returning the first error that appears. -func (d *Downloader) spawnSync(fetchers []func() error) error { - errc := make(chan error, len(fetchers)) - d.cancelWg.Add(len(fetchers)) - for _, fn := range fetchers { - fn := fn - go func() { defer d.cancelWg.Done(); errc <- fn() }() - } - // Wait for the first error, then terminate the others. - var err error - for i := 0; i < len(fetchers); i++ { - if i == len(fetchers)-1 { - // Close the queue when all fetchers have exited. - // This will cause the block processor to end when - // it has processed the queue. - d.queue.Close() - } - if got := <-errc; got != nil { - err = got - if got != errCanceled { - break // receive a meaningful error, bubble it up - } - } - } - d.queue.Close() - d.Cancel() - return err -} - -// cancel aborts all of the operations and resets the queue. However, cancel does -// not wait for the running download goroutines to finish. This method should be -// used when cancelling the downloads from inside the downloader. -func (d *Downloader) cancel() { - // Close the current cancel channel - d.cancelLock.Lock() - defer d.cancelLock.Unlock() - - if d.cancelCh != nil { - select { - case <-d.cancelCh: - // Channel was already closed - default: - close(d.cancelCh) - } - } -} - -// Cancel aborts all of the operations and waits for all download goroutines to -// finish before returning. -func (d *Downloader) Cancel() { - d.cancel() - d.cancelWg.Wait() -} - -// Terminate interrupts the downloader, canceling all pending operations. -// The downloader cannot be reused after calling Terminate. -func (d *Downloader) Terminate() { - // Close the termination channel (make sure double close is allowed) - d.quitLock.Lock() - select { - case <-d.quitCh: - default: - close(d.quitCh) - - // Terminate the internal beacon syncer - d.skeleton.Terminate() - } - d.quitLock.Unlock() - - // Cancel any pending download requests - d.Cancel() -} - -// fetchHead retrieves the head header and prior pivot block (if available) from -// a remote peer. -func (d *Downloader) fetchHead(p *peerConnection) (head *types.Header, pivot *types.Header, err error) { - p.log.Debug("Retrieving remote chain head") - mode := d.getMode() - - // Request the advertised remote head block and wait for the response - latest, _ := p.peer.Head() - fetch := 1 - if mode == SnapSync { - fetch = 2 // head + pivot headers - } - headers, hashes, err := d.fetchHeadersByHash(p, latest, fetch, fsMinFullBlocks-1, true) - if err != nil { - return nil, nil, err - } - // Make sure the peer gave us at least one and at most the requested headers - if len(headers) == 0 || len(headers) > fetch { - return nil, nil, fmt.Errorf("%w: returned headers %d != requested %d", errBadPeer, len(headers), fetch) - } - // The first header needs to be the head, validate against the request. If - // only 1 header was returned, make sure there's no pivot or there was not - // one requested. - head = headers[0] - if len(headers) == 1 { - if mode == SnapSync && head.Number.Uint64() > uint64(fsMinFullBlocks) { - return nil, nil, fmt.Errorf("%w: no pivot included along head header", errBadPeer) - } - p.log.Debug("Remote head identified, no pivot", "number", head.Number, "hash", hashes[0]) - return head, nil, nil - } - // At this point we have 2 headers in total and the first is the - // validated head of the chain. Check the pivot number and return, - pivot = headers[1] - if pivot.Number.Uint64() != head.Number.Uint64()-uint64(fsMinFullBlocks) { - return nil, nil, fmt.Errorf("%w: remote pivot %d != requested %d", errInvalidChain, pivot.Number, head.Number.Uint64()-uint64(fsMinFullBlocks)) - } - return head, pivot, nil -} - -// calculateRequestSpan calculates what headers to request from a peer when trying to determine the -// common ancestor. -// It returns parameters to be used for peer.RequestHeadersByNumber: -// -// from - starting block number -// count - number of headers to request -// skip - number of headers to skip -// -// and also returns 'max', the last block which is expected to be returned by the remote peers, -// given the (from,count,skip) -func calculateRequestSpan(remoteHeight, localHeight uint64) (int64, int, int, uint64) { - var ( - from int - count int - MaxCount = MaxHeaderFetch / 16 - ) - // requestHead is the highest block that we will ask for. If requestHead is not offset, - // the highest block that we will get is 16 blocks back from head, which means we - // will fetch 14 or 15 blocks unnecessarily in the case the height difference - // between us and the peer is 1-2 blocks, which is most common - requestHead := int(remoteHeight) - 1 - if requestHead < 0 { - requestHead = 0 - } - // requestBottom is the lowest block we want included in the query - // Ideally, we want to include the one just below our own head - requestBottom := int(localHeight - 1) - if requestBottom < 0 { - requestBottom = 0 - } - totalSpan := requestHead - requestBottom - span := 1 + totalSpan/MaxCount - if span < 2 { - span = 2 - } - if span > 16 { - span = 16 - } - - count = 1 + totalSpan/span - if count > MaxCount { - count = MaxCount - } - if count < 2 { - count = 2 - } - from = requestHead - (count-1)*span - if from < 0 { - from = 0 - } - max := from + (count-1)*span - return int64(from), count, span - 1, uint64(max) -} - -// findAncestor tries to locate the common ancestor link of the local chain and -// a remote peers blockchain. In the general case when our node was in sync and -// on the correct chain, checking the top N links should already get us a match. -// 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. -func (d *Downloader) findAncestor(p *peerConnection, remoteHeader *types.Header) (uint64, error) { - // Figure out the valid ancestor range to prevent rewrite attacks - var ( - floor = int64(-1) - localHeight uint64 - remoteHeight = remoteHeader.Number.Uint64() - ) - mode := d.getMode() - switch mode { - case FullSync: - localHeight = d.blockchain.CurrentBlock().Number.Uint64() - case SnapSync: - localHeight = d.blockchain.CurrentSnapBlock().Number.Uint64() - default: - localHeight = d.lightchain.CurrentHeader().Number.Uint64() - } - p.log.Debug("Looking for common ancestor", "local", localHeight, "remote", remoteHeight) - - // Recap floor value for binary search - maxForkAncestry := fullMaxForkAncestry - if d.getMode() == LightSync { - maxForkAncestry = lightMaxForkAncestry - } - if localHeight >= maxForkAncestry { - // We're above the max reorg threshold, find the earliest fork point - floor = int64(localHeight - maxForkAncestry) - } - // If we're doing a light sync, ensure the floor doesn't go below the CHT, as - // all headers before that point will be missing. - if mode == LightSync { - // If we don't know the current CHT position, find it - if d.genesis == 0 { - header := d.lightchain.CurrentHeader() - for header != nil { - d.genesis = header.Number.Uint64() - if floor >= int64(d.genesis)-1 { - break - } - header = d.lightchain.GetHeaderByHash(header.ParentHash) - } - } - // We already know the "genesis" block number, cap floor to that - if floor < int64(d.genesis)-1 { - floor = int64(d.genesis) - 1 - } - } - - ancestor, err := d.findAncestorSpanSearch(p, mode, remoteHeight, localHeight, floor) - if err == nil { - return ancestor, nil - } - // The returned error was not nil. - // If the error returned does not reflect that a common ancestor was not found, return it. - // If the error reflects that a common ancestor was not found, continue to binary search, - // where the error value will be reassigned. - if !errors.Is(err, errNoAncestorFound) { - return 0, err - } - - ancestor, err = d.findAncestorBinarySearch(p, mode, remoteHeight, floor) - if err != nil { - return 0, err - } - return ancestor, nil -} - -func (d *Downloader) findAncestorSpanSearch(p *peerConnection, mode SyncMode, remoteHeight, localHeight uint64, floor int64) (uint64, error) { - from, count, skip, max := calculateRequestSpan(remoteHeight, localHeight) - - p.log.Trace("Span searching for common ancestor", "count", count, "from", from, "skip", skip) - headers, hashes, err := d.fetchHeadersByNumber(p, uint64(from), count, skip, false) - if err != nil { - return 0, err - } - // Wait for the remote response to the head fetch - number, hash := uint64(0), common.Hash{} - - // Make sure the peer actually gave something valid - if len(headers) == 0 { - p.log.Warn("Empty head header set") - return 0, errEmptyHeaderSet - } - // Make sure the peer's reply conforms to the request - for i, header := range headers { - expectNumber := from + int64(i)*int64(skip+1) - if number := header.Number.Int64(); number != expectNumber { - p.log.Warn("Head headers broke chain ordering", "index", i, "requested", expectNumber, "received", number) - return 0, fmt.Errorf("%w: %v", errInvalidChain, errors.New("head headers broke chain ordering")) - } - } - // Check if a common ancestor was found - for i := len(headers) - 1; i >= 0; i-- { - // Skip any headers that underflow/overflow our requested set - if headers[i].Number.Int64() < from || headers[i].Number.Uint64() > max { - continue - } - // Otherwise check if we already know the header or not - h := hashes[i] - n := headers[i].Number.Uint64() - - var known bool - switch mode { - case FullSync: - known = d.blockchain.HasBlock(h, n) - case SnapSync: - known = d.blockchain.HasFastBlock(h, n) - default: - known = d.lightchain.HasHeader(h, n) - } - if known { - number, hash = n, h - break - } - } - // If the head fetch already found an ancestor, return - if hash != (common.Hash{}) { - if int64(number) <= floor { - p.log.Warn("Ancestor below allowance", "number", number, "hash", hash, "allowance", floor) - return 0, errInvalidAncestor - } - p.log.Debug("Found common ancestor", "number", number, "hash", hash) - return number, nil - } - return 0, errNoAncestorFound -} - -func (d *Downloader) findAncestorBinarySearch(p *peerConnection, mode SyncMode, remoteHeight uint64, floor int64) (uint64, error) { - hash := common.Hash{} - - // Ancestor not found, we need to binary search over our chain - start, end := uint64(0), remoteHeight - if floor > 0 { - start = uint64(floor) - } - p.log.Trace("Binary searching for common ancestor", "start", start, "end", end) - - for start+1 < end { - // Split our chain interval in two, and request the hash to cross check - check := (start + end) / 2 - - headers, hashes, err := d.fetchHeadersByNumber(p, check, 1, 0, false) - if err != nil { - return 0, err - } - // Make sure the peer actually gave something valid - if len(headers) != 1 { - p.log.Warn("Multiple headers for single request", "headers", len(headers)) - return 0, fmt.Errorf("%w: multiple headers (%d) for single request", errBadPeer, len(headers)) - } - // Modify the search interval based on the response - h := hashes[0] - n := headers[0].Number.Uint64() - - var known bool - switch mode { - case FullSync: - known = d.blockchain.HasBlock(h, n) - case SnapSync: - known = d.blockchain.HasFastBlock(h, n) - default: - known = d.lightchain.HasHeader(h, n) - } - if !known { - end = check - continue - } - header := d.lightchain.GetHeaderByHash(h) // Independent of sync mode, header surely exists - if header.Number.Uint64() != check { - p.log.Warn("Received non requested header", "number", header.Number, "hash", header.Hash(), "request", check) - return 0, fmt.Errorf("%w: non-requested header (%d)", errBadPeer, header.Number) - } - start = check - hash = h - } - // Ensure valid ancestry and return - if int64(start) <= floor { - p.log.Warn("Ancestor below allowance", "number", start, "hash", hash, "allowance", floor) - return 0, errInvalidAncestor - } - p.log.Debug("Found common ancestor", "number", start, "hash", hash) - return start, nil -} - -// fetchHeaders keeps retrieving headers concurrently from the number -// requested, until no more are returned, potentially throttling on the way. To -// facilitate concurrency but still protect against malicious nodes sending bad -// headers, we construct a header chain skeleton using the "origin" peer we are -// syncing with, and fill in the missing headers using anyone else. Headers from -// other peers are only accepted if they map cleanly to the skeleton. If no one -// can fill in the skeleton - not even the origin peer - it's assumed invalid and -// the origin is dropped. -func (d *Downloader) fetchHeaders(p *peerConnection, from uint64, head uint64) error { - p.log.Debug("Directing header downloads", "origin", from) - defer p.log.Debug("Header download terminated") - - // Start pulling the header chain skeleton until all is done - var ( - skeleton = true // Skeleton assembly phase or finishing up - pivoting = false // Whether the next request is pivot verification - ancestor = from - mode = d.getMode() - ) - for { - // Pull the next batch of headers, it either: - // - Pivot check to see if the chain moved too far - // - Skeleton retrieval to permit concurrent header fetches - // - Full header retrieval if we're near the chain head - var ( - headers []*types.Header - hashes []common.Hash - err error - ) - switch { - case pivoting: - d.pivotLock.RLock() - pivot := d.pivotHeader.Number.Uint64() - d.pivotLock.RUnlock() - - p.log.Trace("Fetching next pivot header", "number", pivot+uint64(fsMinFullBlocks)) - headers, hashes, err = d.fetchHeadersByNumber(p, pivot+uint64(fsMinFullBlocks), 2, fsMinFullBlocks-9, false) // move +64 when it's 2x64-8 deep - - case skeleton: - p.log.Trace("Fetching skeleton headers", "count", MaxHeaderFetch, "from", from) - headers, hashes, err = d.fetchHeadersByNumber(p, from+uint64(MaxHeaderFetch)-1, MaxSkeletonSize, MaxHeaderFetch-1, false) - - default: - p.log.Trace("Fetching full headers", "count", MaxHeaderFetch, "from", from) - headers, hashes, err = d.fetchHeadersByNumber(p, from, MaxHeaderFetch, 0, false) - } - switch err { - case nil: - // Headers retrieved, continue with processing - - case errCanceled: - // Sync cancelled, no issue, propagate up - return err - - default: - // Header retrieval either timed out, or the peer failed in some strange way - // (e.g. disconnect). Consider the master peer bad and drop - d.dropPeer(p.id) - - // Finish the sync gracefully instead of dumping the gathered data though - for _, ch := range []chan bool{d.queue.blockWakeCh, d.queue.receiptWakeCh} { - select { - case ch <- false: - case <-d.cancelCh: - } - } - select { - case d.headerProcCh <- nil: - case <-d.cancelCh: - } - return fmt.Errorf("%w: header request failed: %v", errBadPeer, err) - } - // If the pivot is being checked, move if it became stale and run the real retrieval - var pivot uint64 - - d.pivotLock.RLock() - if d.pivotHeader != nil { - pivot = d.pivotHeader.Number.Uint64() - } - d.pivotLock.RUnlock() - - if pivoting { - if len(headers) == 2 { - if have, want := headers[0].Number.Uint64(), pivot+uint64(fsMinFullBlocks); have != want { - log.Warn("Peer sent invalid next pivot", "have", have, "want", want) - return fmt.Errorf("%w: next pivot number %d != requested %d", errInvalidChain, have, want) - } - if have, want := headers[1].Number.Uint64(), pivot+2*uint64(fsMinFullBlocks)-8; have != want { - log.Warn("Peer sent invalid pivot confirmer", "have", have, "want", want) - return fmt.Errorf("%w: next pivot confirmer number %d != requested %d", errInvalidChain, have, want) - } - log.Warn("Pivot seemingly stale, moving", "old", pivot, "new", headers[0].Number) - pivot = headers[0].Number.Uint64() - - d.pivotLock.Lock() - d.pivotHeader = headers[0] - d.pivotLock.Unlock() - - // Write out the pivot into the database so a rollback beyond - // it will reenable snap sync and update the state root that - // the state syncer will be downloading. - rawdb.WriteLastPivotNumber(d.stateDB, pivot) - } - // Disable the pivot check and fetch the next batch of headers - pivoting = false - continue - } - // If the skeleton's finished, pull any remaining head headers directly from the origin - if skeleton && len(headers) == 0 { - // A malicious node might withhold advertised headers indefinitely - if from+uint64(MaxHeaderFetch)-1 <= head { - p.log.Warn("Peer withheld skeleton headers", "advertised", head, "withheld", from+uint64(MaxHeaderFetch)-1) - return fmt.Errorf("%w: withheld skeleton headers: advertised %d, withheld #%d", errStallingPeer, head, from+uint64(MaxHeaderFetch)-1) - } - p.log.Debug("No skeleton, fetching headers directly") - skeleton = false - continue - } - // If no more headers are inbound, notify the content fetchers and return - if len(headers) == 0 { - // Don't abort header fetches while the pivot is downloading - if !d.committed.Load() && pivot <= from { - p.log.Debug("No headers, waiting for pivot commit") - select { - case <-time.After(fsHeaderContCheck): - continue - case <-d.cancelCh: - return errCanceled - } - } - // Pivot done (or not in snap sync) and no more headers, terminate the process - p.log.Debug("No more headers available") - select { - case d.headerProcCh <- nil: - return nil - case <-d.cancelCh: - return errCanceled - } - } - // If we received a skeleton batch, resolve internals concurrently - var progressed bool - if skeleton { - filled, hashset, proced, err := d.fillHeaderSkeleton(from, headers) - if err != nil { - p.log.Debug("Skeleton chain invalid", "err", err) - return fmt.Errorf("%w: %v", errInvalidChain, err) - } - headers = filled[proced:] - hashes = hashset[proced:] - - progressed = proced > 0 - from += uint64(proced) - } else { - // A malicious node might withhold advertised headers indefinitely - if n := len(headers); n < MaxHeaderFetch && headers[n-1].Number.Uint64() < head { - p.log.Warn("Peer withheld headers", "advertised", head, "delivered", headers[n-1].Number.Uint64()) - return fmt.Errorf("%w: withheld headers: advertised %d, delivered %d", errStallingPeer, head, headers[n-1].Number.Uint64()) - } - // If we're closing in on the chain head, but haven't yet reached it, delay - // the last few headers so mini reorgs on the head don't cause invalid hash - // chain errors. - if n := len(headers); n > 0 { - // Retrieve the current head we're at - var head uint64 - if mode == LightSync { - head = d.lightchain.CurrentHeader().Number.Uint64() - } else { - head = d.blockchain.CurrentSnapBlock().Number.Uint64() - if full := d.blockchain.CurrentBlock().Number.Uint64(); head < full { - head = full - } - } - // If the head is below the common ancestor, we're actually deduplicating - // already existing chain segments, so use the ancestor as the fake head. - // Otherwise, we might end up delaying header deliveries pointlessly. - if head < ancestor { - head = ancestor - } - // If the head is way older than this batch, delay the last few headers - if head+uint64(reorgProtThreshold) < headers[n-1].Number.Uint64() { - delay := reorgProtHeaderDelay - if delay > n { - delay = n - } - headers = headers[:n-delay] - hashes = hashes[:n-delay] - } - } - } - // If no headers have been delivered, or all of them have been delayed, - // sleep a bit and retry. Take care with headers already consumed during - // skeleton filling - if len(headers) == 0 && !progressed { - p.log.Trace("All headers delayed, waiting") - select { - case <-time.After(fsHeaderContCheck): - continue - case <-d.cancelCh: - return errCanceled - } - } - // Insert any remaining new headers and fetch the next batch - if len(headers) > 0 { - p.log.Trace("Scheduling new headers", "count", len(headers), "from", from) - select { - case d.headerProcCh <- &headerTask{ - headers: headers, - hashes: hashes, - }: - case <-d.cancelCh: - return errCanceled - } - from += uint64(len(headers)) - } - // If we're still skeleton filling snap sync, check pivot staleness - // before continuing to the next skeleton filling - if skeleton && pivot > 0 { - pivoting = true - } - } -} - -// fillHeaderSkeleton concurrently retrieves headers from all our available peers -// and maps them to the provided skeleton header chain. -// -// Any partial results from the beginning of the skeleton is (if possible) forwarded -// immediately to the header processor to keep the rest of the pipeline full even -// in the case of header stalls. -// -// The method returns the entire filled skeleton and also the number of headers -// already forwarded for processing. -func (d *Downloader) fillHeaderSkeleton(from uint64, skeleton []*types.Header) ([]*types.Header, []common.Hash, int, error) { - log.Debug("Filling up skeleton", "from", from) - d.queue.ScheduleSkeleton(from, skeleton) - - err := d.concurrentFetch((*headerQueue)(d), false) - if err != nil { - log.Debug("Skeleton fill failed", "err", err) - } - filled, hashes, proced := d.queue.RetrieveHeaders() - if err == nil { - log.Debug("Skeleton fill succeeded", "filled", len(filled), "processed", proced) - } - return filled, hashes, proced, err -} - -// fetchBodies iteratively downloads the scheduled block bodies, taking any -// available peers, reserving a chunk of blocks for each, waiting for delivery -// and also periodically checking for timeouts. -func (d *Downloader) fetchBodies(from uint64, beaconMode bool) error { - log.Debug("Downloading block bodies", "origin", from) - err := d.concurrentFetch((*bodyQueue)(d), beaconMode) - - log.Debug("Block body download terminated", "err", err) - return err -} - -// fetchReceipts iteratively downloads the scheduled block receipts, taking any -// available peers, reserving a chunk of receipts for each, waiting for delivery -// and also periodically checking for timeouts. -func (d *Downloader) fetchReceipts(from uint64, beaconMode bool) error { - log.Debug("Downloading receipts", "origin", from) - err := d.concurrentFetch((*receiptQueue)(d), beaconMode) - - log.Debug("Receipt download terminated", "err", err) - return err -} - -// processHeaders takes batches of retrieved headers from an input channel and -// keeps processing and scheduling them into the header chain and downloader's -// queue until the stream ends or a failure occurs. -func (d *Downloader) processHeaders(origin uint64, td, ttd *big.Int, beaconMode bool) error { - var ( - mode = d.getMode() - gotHeaders = false // Wait for batches of headers to process - ) - for { - select { - case <-d.cancelCh: - return errCanceled - - case task := <-d.headerProcCh: - // Terminate header processing if we synced up - if task == nil || len(task.headers) == 0 { - // Notify everyone that headers are fully processed - for _, ch := range []chan bool{d.queue.blockWakeCh, d.queue.receiptWakeCh} { - select { - case ch <- false: - case <-d.cancelCh: - } - } - // If we're in legacy sync mode, we need to check total difficulty - // violations from malicious peers. That is not needed in beacon - // mode and we can skip to terminating sync. - if !beaconMode { - // If no headers were retrieved at all, the peer violated its TD promise that it had a - // better chain compared to ours. The only exception is if its promised blocks were - // already imported by other means (e.g. fetcher): - // - // R , L : Both at block 10 - // R: Mine block 11, and propagate it to L - // L: Queue block 11 for import - // L: Notice that R's head and TD increased compared to ours, start sync - // L: Import of block 11 finishes - // L: Sync begins, and finds common ancestor at 11 - // L: Request new headers up from 11 (R's TD was higher, it must have something) - // R: Nothing to give - if mode != LightSync { - head := d.blockchain.CurrentBlock() - if !gotHeaders && td.Cmp(d.blockchain.GetTd(head.Hash(), head.Number.Uint64())) > 0 { - return errStallingPeer - } - } - // If snap or light syncing, ensure promised headers are indeed delivered. This is - // needed to detect scenarios where an attacker feeds a bad pivot and then bails out - // of delivering the post-pivot blocks that would flag the invalid content. - // - // This check cannot be executed "as is" for full imports, since blocks may still be - // queued for processing when the header download completes. However, as long as the - // peer gave us something useful, we're already happy/progressed (above check). - if mode == SnapSync || mode == LightSync { - head := d.lightchain.CurrentHeader() - if td.Cmp(d.lightchain.GetTd(head.Hash(), head.Number.Uint64())) > 0 { - return errStallingPeer - } - } - } - return nil - } - // Otherwise split the chunk of headers into batches and process them - headers, hashes := task.headers, task.hashes - - gotHeaders = true - for len(headers) > 0 { - // Terminate if something failed in between processing chunks - select { - case <-d.cancelCh: - return errCanceled - default: - } - // Select the next chunk of headers to import - limit := maxHeadersProcess - if limit > len(headers) { - limit = len(headers) - } - chunkHeaders := headers[:limit] - chunkHashes := hashes[:limit] - - // In case of header only syncing, validate the chunk immediately - if mode == SnapSync || mode == LightSync { - // Although the received headers might be all valid, a legacy - // PoW/PoA sync must not accept post-merge headers. Make sure - // that any transition is rejected at this point. - var ( - rejected []*types.Header - td *big.Int - ) - if !beaconMode && ttd != nil { - td = d.blockchain.GetTd(chunkHeaders[0].ParentHash, chunkHeaders[0].Number.Uint64()-1) - if td == nil { - // This should never really happen, but handle gracefully for now - log.Error("Failed to retrieve parent header TD", "number", chunkHeaders[0].Number.Uint64()-1, "hash", chunkHeaders[0].ParentHash) - return fmt.Errorf("%w: parent TD missing", errInvalidChain) - } - for i, header := range chunkHeaders { - td = new(big.Int).Add(td, header.Difficulty) - if td.Cmp(ttd) >= 0 { - // Terminal total difficulty reached, allow the last header in - if new(big.Int).Sub(td, header.Difficulty).Cmp(ttd) < 0 { - chunkHeaders, rejected = chunkHeaders[:i+1], chunkHeaders[i+1:] - if len(rejected) > 0 { - // Make a nicer user log as to the first TD truly rejected - td = new(big.Int).Add(td, rejected[0].Difficulty) - } - } else { - chunkHeaders, rejected = chunkHeaders[:i], chunkHeaders[i:] - } - break - } - } - } - if len(chunkHeaders) > 0 { - if n, err := d.lightchain.InsertHeaderChain(chunkHeaders); err != nil { - log.Warn("Invalid header encountered", "number", chunkHeaders[n].Number, "hash", chunkHashes[n], "parent", chunkHeaders[n].ParentHash, "err", err) - return fmt.Errorf("%w: %v", errInvalidChain, err) - } - } - if len(rejected) != 0 { - log.Info("Legacy sync reached merge threshold", "number", rejected[0].Number, "hash", rejected[0].Hash(), "td", td, "ttd", ttd) - return ErrMergeTransition - } - } - // Unless we're doing light chains, schedule the headers for associated content retrieval - if mode == FullSync || mode == SnapSync { - // If we've reached the allowed number of pending headers, stall a bit - for d.queue.PendingBodies() >= maxQueuedHeaders || d.queue.PendingReceipts() >= maxQueuedHeaders { - select { - case <-d.cancelCh: - return errCanceled - case <-time.After(time.Second): - } - } - // Otherwise insert the headers for content retrieval - inserts := d.queue.Schedule(chunkHeaders, chunkHashes, origin) - if len(inserts) != len(chunkHeaders) { - return fmt.Errorf("%w: stale headers", errBadPeer) - } - } - headers = headers[limit:] - hashes = hashes[limit:] - origin += uint64(limit) - } - // Update the highest block number we know if a higher one is found. - d.syncStatsLock.Lock() - if d.syncStatsChainHeight < origin { - d.syncStatsChainHeight = origin - 1 - } - d.syncStatsLock.Unlock() - - // Signal the content downloaders of the availability of new tasks - for _, ch := range []chan bool{d.queue.blockWakeCh, d.queue.receiptWakeCh} { - select { - case ch <- true: - default: - } - } - } - } -} - -// processFullSyncContent takes fetch results from the queue and imports them into the chain. -func (d *Downloader) processFullSyncContent(ttd *big.Int, beaconMode bool) error { - for { - results := d.queue.Results(true) - if len(results) == 0 { - return nil - } - if d.chainInsertHook != nil { - d.chainInsertHook(results) - } - // Although the received blocks might be all valid, a legacy PoW/PoA sync - // must not accept post-merge blocks. Make sure that pre-merge blocks are - // imported, but post-merge ones are rejected. - var ( - rejected []*fetchResult - td *big.Int - ) - if !beaconMode && ttd != nil { - td = d.blockchain.GetTd(results[0].Header.ParentHash, results[0].Header.Number.Uint64()-1) - if td == nil { - // This should never really happen, but handle gracefully for now - log.Error("Failed to retrieve parent block TD", "number", results[0].Header.Number.Uint64()-1, "hash", results[0].Header.ParentHash) - return fmt.Errorf("%w: parent TD missing", errInvalidChain) - } - for i, result := range results { - td = new(big.Int).Add(td, result.Header.Difficulty) - if td.Cmp(ttd) >= 0 { - // Terminal total difficulty reached, allow the last block in - if new(big.Int).Sub(td, result.Header.Difficulty).Cmp(ttd) < 0 { - results, rejected = results[:i+1], results[i+1:] - if len(rejected) > 0 { - // Make a nicer user log as to the first TD truly rejected - td = new(big.Int).Add(td, rejected[0].Header.Difficulty) - } - } else { - results, rejected = results[:i], results[i:] - } - break - } - } - } - if err := d.importBlockResults(results); err != nil { - return err - } - if len(rejected) != 0 { - log.Info("Legacy sync reached merge threshold", "number", rejected[0].Header.Number, "hash", rejected[0].Header.Hash(), "td", td, "ttd", ttd) - return ErrMergeTransition - } - } -} - -func (d *Downloader) importBlockResults(results []*fetchResult) error { - // Check for any early termination requests - if len(results) == 0 { - return nil - } - select { - case <-d.quitCh: - return errCancelContentProcessing - default: - } - // Retrieve a batch of results to import - first, last := results[0].Header, results[len(results)-1].Header - log.Debug("Inserting downloaded chain", "items", len(results), - "firstnum", first.Number, "firsthash", first.Hash(), - "lastnum", last.Number, "lasthash", last.Hash(), - ) - blocks := make([]*types.Block, len(results)) - for i, result := range results { - blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles).WithWithdrawals(result.Withdrawals) - } - // Downloaded blocks are always regarded as trusted after the - // transition. Because the downloaded chain is guided by the - // consensus-layer. - if index, err := d.blockchain.InsertChain(blocks); err != nil { - if index < len(results) { - log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err) - - // In post-merge, notify the engine API of encountered bad chains - if d.badBlock != nil { - head, _, _, err := d.skeleton.Bounds() - if err != nil { - log.Error("Failed to retrieve beacon bounds for bad block reporting", "err", err) - } else { - d.badBlock(blocks[index].Header(), head) - } - } - } else { - // The InsertChain method in blockchain.go will sometimes return an out-of-bounds index, - // when it needs to preprocess blocks to import a sidechain. - // The importer will put together a new list of blocks to import, which is a superset - // of the blocks delivered from the downloader, and the indexing will be off. - log.Debug("Downloaded item processing failed on sidechain import", "index", index, "err", err) - } - return fmt.Errorf("%w: %v", errInvalidChain, err) - } - return nil -} - -// processSnapSyncContent takes fetch results from the queue and writes them to the -// database. It also controls the synchronisation of state nodes of the pivot block. -func (d *Downloader) processSnapSyncContent() error { - // Start syncing state of the reported head block. This should get us most of - // the state of the pivot block. - d.pivotLock.RLock() - sync := d.syncState(d.pivotHeader.Root) - d.pivotLock.RUnlock() - - defer func() { - // The `sync` object is replaced every time the pivot moves. We need to - // defer close the very last active one, hence the lazy evaluation vs. - // calling defer sync.Cancel() !!! - sync.Cancel() - }() - - closeOnErr := func(s *stateSync) { - if err := s.Wait(); err != nil && err != errCancelStateFetch && err != errCanceled && err != snap.ErrCancelled { - d.queue.Close() // wake up Results - } - } - go closeOnErr(sync) - - // To cater for moving pivot points, track the pivot block and subsequently - // accumulated download results separately. - // - // These will be nil up to the point where we reach the pivot, and will only - // be set temporarily if the synced blocks are piling up, but the pivot is - // still busy downloading. In that case, we need to occasionally check for - // pivot moves, so need to unblock the loop. These fields will accumulate - // the results in the meantime. - // - // Note, there's no issue with memory piling up since after 64 blocks the - // pivot will forcefully move so these accumulators will be dropped. - var ( - oldPivot *fetchResult // Locked in pivot block, might change eventually - oldTail []*fetchResult // Downloaded content after the pivot - ) - for { - // Wait for the next batch of downloaded data to be available. If we have - // not yet reached the pivot point, wait blockingly as there's no need to - // spin-loop check for pivot moves. If we reached the pivot but have not - // yet processed it, check for results async, so we might notice pivot - // moves while state syncing. If the pivot was passed fully, block again - // as there's no more reason to check for pivot moves at all. - results := d.queue.Results(oldPivot == nil) - if len(results) == 0 { - // If pivot sync is done, stop - if d.committed.Load() { - d.reportSnapSyncProgress(true) - return sync.Cancel() - } - // If sync failed, stop - select { - case <-d.cancelCh: - sync.Cancel() - return errCanceled - default: - } - } - if d.chainInsertHook != nil { - d.chainInsertHook(results) - } - d.reportSnapSyncProgress(false) - - // If we haven't downloaded the pivot block yet, check pivot staleness - // notifications from the header downloader - d.pivotLock.RLock() - pivot := d.pivotHeader - d.pivotLock.RUnlock() - - if oldPivot == nil { // no results piling up, we can move the pivot - if !d.committed.Load() { // not yet passed the pivot, we can move the pivot - if pivot.Root != sync.root { // pivot position changed, we can move the pivot - sync.Cancel() - sync = d.syncState(pivot.Root) - - go closeOnErr(sync) - } - } - } else { // results already piled up, consume before handling pivot move - results = append(append([]*fetchResult{oldPivot}, oldTail...), results...) - } - // Split around the pivot block and process the two sides via snap/full sync - if !d.committed.Load() { - latest := results[len(results)-1].Header - // If the height is above the pivot block by 2 sets, it means the pivot - // become stale in the network, and it was garbage collected, move to a - // new pivot. - // - // Note, we have `reorgProtHeaderDelay` number of blocks withheld, Those - // need to be taken into account, otherwise we're detecting the pivot move - // late and will drop peers due to unavailable state!!! - if height := latest.Number.Uint64(); height >= pivot.Number.Uint64()+2*uint64(fsMinFullBlocks)-uint64(reorgProtHeaderDelay) { - log.Warn("Pivot became stale, moving", "old", pivot.Number.Uint64(), "new", height-uint64(fsMinFullBlocks)+uint64(reorgProtHeaderDelay)) - pivot = results[len(results)-1-fsMinFullBlocks+reorgProtHeaderDelay].Header // must exist as lower old pivot is uncommitted - - d.pivotLock.Lock() - d.pivotHeader = pivot - d.pivotLock.Unlock() - - // Write out the pivot into the database so a rollback beyond it will - // reenable snap sync - rawdb.WriteLastPivotNumber(d.stateDB, pivot.Number.Uint64()) - } - } - P, beforeP, afterP := splitAroundPivot(pivot.Number.Uint64(), results) - if err := d.commitSnapSyncData(beforeP, sync); err != nil { - return err - } - if P != nil { - // If new pivot block found, cancel old state retrieval and restart - if oldPivot != P { - sync.Cancel() - sync = d.syncState(P.Header.Root) - - go closeOnErr(sync) - oldPivot = P - } - // Wait for completion, occasionally checking for pivot staleness - select { - case <-sync.done: - if sync.err != nil { - return sync.err - } - if err := d.commitPivotBlock(P); err != nil { - return err - } - oldPivot = nil - - case <-time.After(time.Second): - oldTail = afterP - continue - } - } - // Fast sync done, pivot commit done, full import - if err := d.importBlockResults(afterP); err != nil { - return err - } - } -} - -func splitAroundPivot(pivot uint64, results []*fetchResult) (p *fetchResult, before, after []*fetchResult) { - if len(results) == 0 { - return nil, nil, nil - } - if lastNum := results[len(results)-1].Header.Number.Uint64(); lastNum < pivot { - // the pivot is somewhere in the future - return nil, results, nil - } - // This can also be optimized, but only happens very seldom - for _, result := range results { - num := result.Header.Number.Uint64() - switch { - case num < pivot: - before = append(before, result) - case num == pivot: - p = result - default: - after = append(after, result) - } - } - return p, before, after -} - -func (d *Downloader) commitSnapSyncData(results []*fetchResult, stateSync *stateSync) error { - // Check for any early termination requests - if len(results) == 0 { - return nil - } - select { - case <-d.quitCh: - return errCancelContentProcessing - case <-stateSync.done: - if err := stateSync.Wait(); err != nil { - return err - } - default: - } - // Retrieve the batch of results to import - first, last := results[0].Header, results[len(results)-1].Header - log.Debug("Inserting snap-sync blocks", "items", len(results), - "firstnum", first.Number, "firsthash", first.Hash(), - "lastnumn", last.Number, "lasthash", last.Hash(), - ) - blocks := make([]*types.Block, len(results)) - receipts := make([]types.Receipts, len(results)) - for i, result := range results { - blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles).WithWithdrawals(result.Withdrawals) - receipts[i] = result.Receipts - } - if index, err := d.blockchain.InsertReceiptChain(blocks, receipts, d.ancientLimit); err != nil { - log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err) - return fmt.Errorf("%w: %v", errInvalidChain, err) - } - return nil -} - -func (d *Downloader) commitPivotBlock(result *fetchResult) error { - block := types.NewBlockWithHeader(result.Header).WithBody(result.Transactions, result.Uncles).WithWithdrawals(result.Withdrawals) - log.Debug("Committing snap sync pivot as new head", "number", block.Number(), "hash", block.Hash()) - - // Commit the pivot block as the new head, will require full sync from here on - if _, err := d.blockchain.InsertReceiptChain([]*types.Block{block}, []types.Receipts{result.Receipts}, d.ancientLimit); err != nil { - return err - } - if err := d.blockchain.SnapSyncCommitHead(block.Hash()); err != nil { - return err - } - d.committed.Store(true) - return nil -} - -// DeliverSnapPacket is invoked from a peer's message handler when it transmits a -// data packet for the local node to consume. -func (d *Downloader) DeliverSnapPacket(peer *snap.Peer, packet snap.Packet) error { - switch packet := packet.(type) { - case *snap.AccountRangePacket: - hashes, accounts, err := packet.Unpack() - if err != nil { - return err - } - return d.SnapSyncer.OnAccounts(peer, packet.ID, hashes, accounts, packet.Proof) - - case *snap.StorageRangesPacket: - hashset, slotset := packet.Unpack() - return d.SnapSyncer.OnStorage(peer, packet.ID, hashset, slotset, packet.Proof) - - case *snap.ByteCodesPacket: - return d.SnapSyncer.OnByteCodes(peer, packet.ID, packet.Codes) - - case *snap.TrieNodesPacket: - return d.SnapSyncer.OnTrieNodes(peer, packet.ID, packet.Nodes) - - default: - return fmt.Errorf("unexpected snap packet type: %T", packet) - } -} - -// readHeaderRange returns a list of headers, using the given last header as the base, -// and going backwards towards genesis. This method assumes that the caller already has -// placed a reasonable cap on count. -func (d *Downloader) readHeaderRange(last *types.Header, count int) []*types.Header { - var ( - current = last - headers []*types.Header - ) - for { - parent := d.lightchain.GetHeaderByHash(current.ParentHash) - if parent == nil { - break // The chain is not continuous, or the chain is exhausted - } - headers = append(headers, parent) - if len(headers) >= count { - break - } - current = parent - } - return headers -} - -// reportSnapSyncProgress calculates various status reports and provides it to the user. -func (d *Downloader) reportSnapSyncProgress(force bool) { - // Initialize the sync start time if it's the first time we're reporting - if d.syncStartTime.IsZero() { - d.syncStartTime = time.Now().Add(-time.Millisecond) // -1ms offset to avoid division by zero - } - // Don't report all the events, just occasionally - if !force && time.Since(d.syncLogTime) < 8*time.Second { - return - } - // Don't report anything until we have a meaningful progress - var ( - headerBytes, _ = d.stateDB.AncientSize(rawdb.ChainFreezerHeaderTable) - bodyBytes, _ = d.stateDB.AncientSize(rawdb.ChainFreezerBodiesTable) - receiptBytes, _ = d.stateDB.AncientSize(rawdb.ChainFreezerReceiptTable) - ) - syncedBytes := common.StorageSize(headerBytes + bodyBytes + receiptBytes) - if syncedBytes == 0 { - return - } - var ( - header = d.blockchain.CurrentHeader() - block = d.blockchain.CurrentSnapBlock() - ) - syncedBlocks := block.Number.Uint64() - d.syncStartBlock - if syncedBlocks == 0 { - return - } - // Retrieve the current chain head and calculate the ETA - latest, _, _, err := d.skeleton.Bounds() - if err != nil { - // We're going to cheat for non-merged networks, but that's fine - latest = d.pivotHeader - } - if latest == nil { - // This should really never happen, but add some defensive code for now. - // TODO(karalabe): Remove it eventually if we don't see it blow. - log.Error("Nil latest block in sync progress report") - return - } - var ( - left = latest.Number.Uint64() - block.Number.Uint64() - eta = time.Since(d.syncStartTime) / time.Duration(syncedBlocks) * time.Duration(left) - - progress = fmt.Sprintf("%.2f%%", float64(block.Number.Uint64())*100/float64(latest.Number.Uint64())) - headers = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(header.Number.Uint64()), common.StorageSize(headerBytes).TerminalString()) - bodies = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(block.Number.Uint64()), common.StorageSize(bodyBytes).TerminalString()) - receipts = fmt.Sprintf("%v@%v", log.FormatLogfmtUint64(block.Number.Uint64()), common.StorageSize(receiptBytes).TerminalString()) - ) - log.Info("Syncing: chain download in progress", "synced", progress, "chain", syncedBytes, "headers", headers, "bodies", bodies, "receipts", receipts, "eta", common.PrettyDuration(eta)) - d.syncLogTime = time.Now() -} diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go deleted file mode 100644 index e4875b959a..0000000000 --- a/eth/downloader/downloader_test.go +++ /dev/null @@ -1,1379 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "fmt" - "math/big" - "os" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/eth/protocols/snap" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" - "github.com/ethereum/go-ethereum/trie" -) - -// downloadTester is a test simulator for mocking out local block chain. -type downloadTester struct { - freezer string - chain *core.BlockChain - downloader *Downloader - - peers map[string]*downloadTesterPeer - lock sync.RWMutex -} - -// newTester creates a new downloader test mocker. -func newTester(t *testing.T) *downloadTester { - return newTesterWithNotification(t, nil) -} - -// newTester creates a new downloader test mocker. -func newTesterWithNotification(t *testing.T, success func()) *downloadTester { - freezer := t.TempDir() - db, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), freezer, "", false) - if err != nil { - panic(err) - } - t.Cleanup(func() { - db.Close() - }) - gspec := &core.Genesis{ - Config: params.TestChainConfig, - Alloc: core.GenesisAlloc{testAddress: {Balance: big.NewInt(1000000000000000)}}, - BaseFee: big.NewInt(params.InitialBaseFee), - } - chain, err := core.NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) - if err != nil { - panic(err) - } - tester := &downloadTester{ - freezer: freezer, - chain: chain, - peers: make(map[string]*downloadTesterPeer), - } - tester.downloader = New(db, new(event.TypeMux), tester.chain, nil, tester.dropPeer, success) - return tester -} - -// terminate aborts any operations on the embedded downloader and releases all -// held resources. -func (dl *downloadTester) terminate() { - dl.downloader.Terminate() - dl.chain.Stop() - - os.RemoveAll(dl.freezer) -} - -// sync starts synchronizing with a remote peer, blocking until it completes. -func (dl *downloadTester) sync(id string, td *big.Int, mode SyncMode) error { - head := dl.peers[id].chain.CurrentBlock() - if td == nil { - // If no particular TD was requested, load from the peer's blockchain - td = dl.peers[id].chain.GetTd(head.Hash(), head.Number.Uint64()) - } - // Synchronise with the chosen peer and ensure proper cleanup afterwards - err := dl.downloader.synchronise(id, head.Hash(), td, nil, mode, false, nil) - select { - case <-dl.downloader.cancelCh: - // Ok, downloader fully cancelled after sync cycle - default: - // Downloader is still accepting packets, can block a peer up - panic("downloader active post sync cycle") // panic will be caught by tester - } - return err -} - -// newPeer registers a new block download source into the downloader. -func (dl *downloadTester) newPeer(id string, version uint, blocks []*types.Block) *downloadTesterPeer { - dl.lock.Lock() - defer dl.lock.Unlock() - - peer := &downloadTesterPeer{ - dl: dl, - id: id, - chain: newTestBlockchain(blocks), - withholdHeaders: make(map[common.Hash]struct{}), - } - dl.peers[id] = peer - - if err := dl.downloader.RegisterPeer(id, version, peer); err != nil { - panic(err) - } - if err := dl.downloader.SnapSyncer.Register(peer); err != nil { - panic(err) - } - return peer -} - -// dropPeer simulates a hard peer removal from the connection pool. -func (dl *downloadTester) dropPeer(id string) { - dl.lock.Lock() - defer dl.lock.Unlock() - - delete(dl.peers, id) - dl.downloader.SnapSyncer.Unregister(id) - dl.downloader.UnregisterPeer(id) -} - -type downloadTesterPeer struct { - dl *downloadTester - id string - chain *core.BlockChain - - withholdHeaders map[common.Hash]struct{} -} - -// Head constructs a function to retrieve a peer's current head hash -// and total difficulty. -func (dlp *downloadTesterPeer) Head() (common.Hash, *big.Int) { - head := dlp.chain.CurrentBlock() - return head.Hash(), dlp.chain.GetTd(head.Hash(), head.Number.Uint64()) -} - -func unmarshalRlpHeaders(rlpdata []rlp.RawValue) []*types.Header { - var headers = make([]*types.Header, len(rlpdata)) - for i, data := range rlpdata { - var h types.Header - if err := rlp.DecodeBytes(data, &h); err != nil { - panic(err) - } - headers[i] = &h - } - return headers -} - -// RequestHeadersByHash constructs a GetBlockHeaders function based on a hashed -// origin; associated with a particular peer in the download tester. The returned -// function can be used to retrieve batches of headers from the particular peer. -func (dlp *downloadTesterPeer) RequestHeadersByHash(origin common.Hash, amount int, skip int, reverse bool, sink chan *eth.Response) (*eth.Request, error) { - // Service the header query via the live handler code - rlpHeaders := eth.ServiceGetBlockHeadersQuery(dlp.chain, ð.GetBlockHeadersRequest{ - Origin: eth.HashOrNumber{ - Hash: origin, - }, - Amount: uint64(amount), - Skip: uint64(skip), - Reverse: reverse, - }, nil) - headers := unmarshalRlpHeaders(rlpHeaders) - // If a malicious peer is simulated withholding headers, delete them - for hash := range dlp.withholdHeaders { - for i, header := range headers { - if header.Hash() == hash { - headers = append(headers[:i], headers[i+1:]...) - break - } - } - } - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - // Deliver the headers to the downloader - req := ð.Request{ - Peer: dlp.id, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockHeadersRequest)(&headers), - Meta: hashes, - Time: 1, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil -} - -// RequestHeadersByNumber constructs a GetBlockHeaders function based on a numbered -// origin; associated with a particular peer in the download tester. The returned -// function can be used to retrieve batches of headers from the particular peer. -func (dlp *downloadTesterPeer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool, sink chan *eth.Response) (*eth.Request, error) { - // Service the header query via the live handler code - rlpHeaders := eth.ServiceGetBlockHeadersQuery(dlp.chain, ð.GetBlockHeadersRequest{ - Origin: eth.HashOrNumber{ - Number: origin, - }, - Amount: uint64(amount), - Skip: uint64(skip), - Reverse: reverse, - }, nil) - headers := unmarshalRlpHeaders(rlpHeaders) - // If a malicious peer is simulated withholding headers, delete them - for hash := range dlp.withholdHeaders { - for i, header := range headers { - if header.Hash() == hash { - headers = append(headers[:i], headers[i+1:]...) - break - } - } - } - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - // Deliver the headers to the downloader - req := ð.Request{ - Peer: dlp.id, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockHeadersRequest)(&headers), - Meta: hashes, - Time: 1, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil -} - -// RequestBodies constructs a getBlockBodies method associated with a particular -// peer in the download tester. The returned function can be used to retrieve -// batches of block bodies from the particularly requested peer. -func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *eth.Response) (*eth.Request, error) { - blobs := eth.ServiceGetBlockBodiesQuery(dlp.chain, hashes) - - bodies := make([]*eth.BlockBody, len(blobs)) - for i, blob := range blobs { - bodies[i] = new(eth.BlockBody) - rlp.DecodeBytes(blob, bodies[i]) - } - var ( - txsHashes = make([]common.Hash, len(bodies)) - uncleHashes = make([]common.Hash, len(bodies)) - withdrawalHashes = make([]common.Hash, len(bodies)) - ) - hasher := trie.NewStackTrie(nil) - for i, body := range bodies { - txsHashes[i] = types.DeriveSha(types.Transactions(body.Transactions), hasher) - uncleHashes[i] = types.CalcUncleHash(body.Uncles) - } - req := ð.Request{ - Peer: dlp.id, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockBodiesResponse)(&bodies), - Meta: [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes}, - Time: 1, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil -} - -// RequestReceipts constructs a getReceipts method associated with a particular -// peer in the download tester. The returned function can be used to retrieve -// batches of block receipts from the particularly requested peer. -func (dlp *downloadTesterPeer) RequestReceipts(hashes []common.Hash, sink chan *eth.Response) (*eth.Request, error) { - blobs := eth.ServiceGetReceiptsQuery(dlp.chain, hashes) - - receipts := make([][]*types.Receipt, len(blobs)) - for i, blob := range blobs { - rlp.DecodeBytes(blob, &receipts[i]) - } - hasher := trie.NewStackTrie(nil) - hashes = make([]common.Hash, len(receipts)) - for i, receipt := range receipts { - hashes[i] = types.DeriveSha(types.Receipts(receipt), hasher) - } - req := ð.Request{ - Peer: dlp.id, - } - res := ð.Response{ - Req: req, - Res: (*eth.ReceiptsResponse)(&receipts), - Meta: hashes, - Time: 1, - Done: make(chan error, 1), // Ignore the returned status - } - go func() { - sink <- res - }() - return req, nil -} - -// ID retrieves the peer's unique identifier. -func (dlp *downloadTesterPeer) ID() string { - return dlp.id -} - -// RequestAccountRange fetches a batch of accounts rooted in a specific account -// trie, starting with the origin. -func (dlp *downloadTesterPeer) RequestAccountRange(id uint64, root, origin, limit common.Hash, bytes uint64) error { - // Create the request and service it - req := &snap.GetAccountRangePacket{ - ID: id, - Root: root, - Origin: origin, - Limit: limit, - Bytes: bytes, - } - slimaccs, proofs := snap.ServiceGetAccountRangeQuery(dlp.chain, req) - - // We need to convert to non-slim format, delegate to the packet code - res := &snap.AccountRangePacket{ - ID: id, - Accounts: slimaccs, - Proof: proofs, - } - hashes, accounts, _ := res.Unpack() - - go dlp.dl.downloader.SnapSyncer.OnAccounts(dlp, id, hashes, accounts, proofs) - return nil -} - -// RequestStorageRanges fetches a batch of storage slots belonging to one or -// more accounts. If slots from only one account is requested, an origin marker -// may also be used to retrieve from there. -func (dlp *downloadTesterPeer) RequestStorageRanges(id uint64, root common.Hash, accounts []common.Hash, origin, limit []byte, bytes uint64) error { - // Create the request and service it - req := &snap.GetStorageRangesPacket{ - ID: id, - Accounts: accounts, - Root: root, - Origin: origin, - Limit: limit, - Bytes: bytes, - } - storage, proofs := snap.ServiceGetStorageRangesQuery(dlp.chain, req) - - // We need to convert to demultiplex, delegate to the packet code - res := &snap.StorageRangesPacket{ - ID: id, - Slots: storage, - Proof: proofs, - } - hashes, slots := res.Unpack() - - go dlp.dl.downloader.SnapSyncer.OnStorage(dlp, id, hashes, slots, proofs) - return nil -} - -// RequestByteCodes fetches a batch of bytecodes by hash. -func (dlp *downloadTesterPeer) RequestByteCodes(id uint64, hashes []common.Hash, bytes uint64) error { - req := &snap.GetByteCodesPacket{ - ID: id, - Hashes: hashes, - Bytes: bytes, - } - codes := snap.ServiceGetByteCodesQuery(dlp.chain, req) - go dlp.dl.downloader.SnapSyncer.OnByteCodes(dlp, id, codes) - return nil -} - -// RequestTrieNodes fetches a batch of account or storage trie nodes rooted in -// a specific state trie. -func (dlp *downloadTesterPeer) RequestTrieNodes(id uint64, root common.Hash, paths []snap.TrieNodePathSet, bytes uint64) error { - req := &snap.GetTrieNodesPacket{ - ID: id, - Root: root, - Paths: paths, - Bytes: bytes, - } - nodes, _ := snap.ServiceGetTrieNodesQuery(dlp.chain, req, time.Now()) - go dlp.dl.downloader.SnapSyncer.OnTrieNodes(dlp, id, nodes) - return nil -} - -// Log retrieves the peer's own contextual logger. -func (dlp *downloadTesterPeer) Log() log.Logger { - return log.New("peer", dlp.id) -} - -// assertOwnChain checks if the local chain contains the correct number of items -// of the various chain components. -func assertOwnChain(t *testing.T, tester *downloadTester, length int) { - // Mark this method as a helper to report errors at callsite, not in here - t.Helper() - - headers, blocks, receipts := length, length, length - if tester.downloader.getMode() == LightSync { - blocks, receipts = 1, 1 - } - if hs := int(tester.chain.CurrentHeader().Number.Uint64()) + 1; hs != headers { - t.Fatalf("synchronised headers mismatch: have %v, want %v", hs, headers) - } - if bs := int(tester.chain.CurrentBlock().Number.Uint64()) + 1; bs != blocks { - t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, blocks) - } - if rs := int(tester.chain.CurrentSnapBlock().Number.Uint64()) + 1; rs != receipts { - t.Fatalf("synchronised receipts mismatch: have %v, want %v", rs, receipts) - } -} - -func TestCanonicalSynchronisation68Full(t *testing.T) { testCanonSync(t, eth.ETH68, FullSync) } -func TestCanonicalSynchronisation68Snap(t *testing.T) { testCanonSync(t, eth.ETH68, SnapSync) } -func TestCanonicalSynchronisation68Light(t *testing.T) { testCanonSync(t, eth.ETH68, LightSync) } -func TestCanonicalSynchronisation67Full(t *testing.T) { testCanonSync(t, eth.ETH67, FullSync) } -func TestCanonicalSynchronisation67Snap(t *testing.T) { testCanonSync(t, eth.ETH67, SnapSync) } -func TestCanonicalSynchronisation67Light(t *testing.T) { testCanonSync(t, eth.ETH67, LightSync) } - -func testCanonSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create a small enough block chain to download - chain := testChainBase.shorten(blockCacheMaxItems - 15) - tester.newPeer("peer", protocol, chain.blocks[1:]) - - // Synchronise with the peer and make sure all relevant data was retrieved - if err := tester.sync("peer", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) -} - -// Tests that if a large batch of blocks are being downloaded, it is throttled -// until the cached blocks are retrieved. -func TestThrottling68Full(t *testing.T) { testThrottling(t, eth.ETH68, FullSync) } -func TestThrottling68Snap(t *testing.T) { testThrottling(t, eth.ETH68, SnapSync) } -func TestThrottling67Full(t *testing.T) { testThrottling(t, eth.ETH67, FullSync) } -func TestThrottling67Snap(t *testing.T) { testThrottling(t, eth.ETH67, SnapSync) } - -func testThrottling(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create a long block chain to download and the tester - targetBlocks := len(testChainBase.blocks) - 1 - tester.newPeer("peer", protocol, testChainBase.blocks[1:]) - - // Wrap the importer to allow stepping - var blocked atomic.Uint32 - proceed := make(chan struct{}) - tester.downloader.chainInsertHook = func(results []*fetchResult) { - blocked.Store(uint32(len(results))) - <-proceed - } - // Start a synchronisation concurrently - errc := make(chan error, 1) - go func() { - errc <- tester.sync("peer", nil, mode) - }() - // Iteratively take some blocks, always checking the retrieval count - for { - // Check the retrieval count synchronously (! reason for this ugly block) - tester.lock.RLock() - retrieved := int(tester.chain.CurrentSnapBlock().Number.Uint64()) + 1 - tester.lock.RUnlock() - if retrieved >= targetBlocks+1 { - break - } - // Wait a bit for sync to throttle itself - var cached, frozen int - for start := time.Now(); time.Since(start) < 3*time.Second; { - time.Sleep(25 * time.Millisecond) - - tester.lock.Lock() - tester.downloader.queue.lock.Lock() - tester.downloader.queue.resultCache.lock.Lock() - { - cached = tester.downloader.queue.resultCache.countCompleted() - frozen = int(blocked.Load()) - retrieved = int(tester.chain.CurrentSnapBlock().Number.Uint64()) + 1 - } - tester.downloader.queue.resultCache.lock.Unlock() - tester.downloader.queue.lock.Unlock() - tester.lock.Unlock() - - if cached == blockCacheMaxItems || - cached == blockCacheMaxItems-reorgProtHeaderDelay || - retrieved+cached+frozen == targetBlocks+1 || - retrieved+cached+frozen == targetBlocks+1-reorgProtHeaderDelay { - break - } - } - // Make sure we filled up the cache, then exhaust it - time.Sleep(25 * time.Millisecond) // give it a chance to screw up - tester.lock.RLock() - retrieved = int(tester.chain.CurrentSnapBlock().Number.Uint64()) + 1 - tester.lock.RUnlock() - if cached != blockCacheMaxItems && cached != blockCacheMaxItems-reorgProtHeaderDelay && retrieved+cached+frozen != targetBlocks+1 && retrieved+cached+frozen != targetBlocks+1-reorgProtHeaderDelay { - t.Fatalf("block count mismatch: have %v, want %v (owned %v, blocked %v, target %v)", cached, blockCacheMaxItems, retrieved, frozen, targetBlocks+1) - } - // Permit the blocked blocks to import - if blocked.Load() > 0 { - blocked.Store(uint32(0)) - proceed <- struct{}{} - } - } - // Check that we haven't pulled more blocks than available - assertOwnChain(t, tester, targetBlocks+1) - if err := <-errc; err != nil { - t.Fatalf("block synchronization failed: %v", err) - } -} - -// Tests that simple synchronization against a forked chain works correctly. In -// this test common ancestor lookup should *not* be short circuited, and a full -// binary search should be executed. -func TestForkedSync68Full(t *testing.T) { testForkedSync(t, eth.ETH68, FullSync) } -func TestForkedSync68Snap(t *testing.T) { testForkedSync(t, eth.ETH68, SnapSync) } -func TestForkedSync68Light(t *testing.T) { testForkedSync(t, eth.ETH68, LightSync) } -func TestForkedSync67Full(t *testing.T) { testForkedSync(t, eth.ETH67, FullSync) } -func TestForkedSync67Snap(t *testing.T) { testForkedSync(t, eth.ETH67, SnapSync) } -func TestForkedSync67Light(t *testing.T) { testForkedSync(t, eth.ETH67, LightSync) } - -func testForkedSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chainA := testChainForkLightA.shorten(len(testChainBase.blocks) + 80) - chainB := testChainForkLightB.shorten(len(testChainBase.blocks) + 81) - tester.newPeer("fork A", protocol, chainA.blocks[1:]) - tester.newPeer("fork B", protocol, chainB.blocks[1:]) - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("fork A", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainA.blocks)) - - // Synchronise with the second peer and make sure that fork is pulled too - if err := tester.sync("fork B", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainB.blocks)) -} - -// Tests that synchronising against a much shorter but much heavier fork works -// currently and is not dropped. -func TestHeavyForkedSync68Full(t *testing.T) { testHeavyForkedSync(t, eth.ETH68, FullSync) } -func TestHeavyForkedSync68Snap(t *testing.T) { testHeavyForkedSync(t, eth.ETH68, SnapSync) } -func TestHeavyForkedSync68Light(t *testing.T) { testHeavyForkedSync(t, eth.ETH68, LightSync) } -func TestHeavyForkedSync67Full(t *testing.T) { testHeavyForkedSync(t, eth.ETH67, FullSync) } -func TestHeavyForkedSync67Snap(t *testing.T) { testHeavyForkedSync(t, eth.ETH67, SnapSync) } -func TestHeavyForkedSync67Light(t *testing.T) { testHeavyForkedSync(t, eth.ETH67, LightSync) } - -func testHeavyForkedSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chainA := testChainForkLightA.shorten(len(testChainBase.blocks) + 80) - chainB := testChainForkHeavy.shorten(len(testChainBase.blocks) + 79) - tester.newPeer("light", protocol, chainA.blocks[1:]) - tester.newPeer("heavy", protocol, chainB.blocks[1:]) - - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("light", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainA.blocks)) - - // Synchronise with the second peer and make sure that fork is pulled too - if err := tester.sync("heavy", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainB.blocks)) -} - -// Tests that chain forks are contained within a certain interval of the current -// chain head, ensuring that malicious peers cannot waste resources by feeding -// long dead chains. -func TestBoundedForkedSync68Full(t *testing.T) { testBoundedForkedSync(t, eth.ETH68, FullSync) } -func TestBoundedForkedSync68Snap(t *testing.T) { testBoundedForkedSync(t, eth.ETH68, SnapSync) } -func TestBoundedForkedSync68Light(t *testing.T) { testBoundedForkedSync(t, eth.ETH68, LightSync) } -func TestBoundedForkedSync67Full(t *testing.T) { testBoundedForkedSync(t, eth.ETH67, FullSync) } -func TestBoundedForkedSync67Snap(t *testing.T) { testBoundedForkedSync(t, eth.ETH67, SnapSync) } -func TestBoundedForkedSync67Light(t *testing.T) { testBoundedForkedSync(t, eth.ETH67, LightSync) } - -func testBoundedForkedSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chainA := testChainForkLightA - chainB := testChainForkLightB - tester.newPeer("original", protocol, chainA.blocks[1:]) - tester.newPeer("rewriter", protocol, chainB.blocks[1:]) - - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("original", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainA.blocks)) - - // Synchronise with the second peer and ensure that the fork is rejected to being too old - if err := tester.sync("rewriter", nil, mode); err != errInvalidAncestor { - t.Fatalf("sync failure mismatch: have %v, want %v", err, errInvalidAncestor) - } -} - -// Tests that chain forks are contained within a certain interval of the current -// chain head for short but heavy forks too. These are a bit special because they -// take different ancestor lookup paths. -func TestBoundedHeavyForkedSync68Full(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH68, FullSync) -} -func TestBoundedHeavyForkedSync68Snap(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH68, SnapSync) -} -func TestBoundedHeavyForkedSync68Light(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH68, LightSync) -} -func TestBoundedHeavyForkedSync67Full(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH67, FullSync) -} -func TestBoundedHeavyForkedSync67Snap(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH67, SnapSync) -} -func TestBoundedHeavyForkedSync67Light(t *testing.T) { - testBoundedHeavyForkedSync(t, eth.ETH67, LightSync) -} - -func testBoundedHeavyForkedSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create a long enough forked chain - chainA := testChainForkLightA - chainB := testChainForkHeavy - tester.newPeer("original", protocol, chainA.blocks[1:]) - - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("original", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chainA.blocks)) - - tester.newPeer("heavy-rewriter", protocol, chainB.blocks[1:]) - // Synchronise with the second peer and ensure that the fork is rejected to being too old - if err := tester.sync("heavy-rewriter", nil, mode); err != errInvalidAncestor { - t.Fatalf("sync failure mismatch: have %v, want %v", err, errInvalidAncestor) - } -} - -// Tests that a canceled download wipes all previously accumulated state. -func TestCancel68Full(t *testing.T) { testCancel(t, eth.ETH68, FullSync) } -func TestCancel68Snap(t *testing.T) { testCancel(t, eth.ETH68, SnapSync) } -func TestCancel68Light(t *testing.T) { testCancel(t, eth.ETH68, LightSync) } -func TestCancel67Full(t *testing.T) { testCancel(t, eth.ETH67, FullSync) } -func TestCancel67Snap(t *testing.T) { testCancel(t, eth.ETH67, SnapSync) } -func TestCancel67Light(t *testing.T) { testCancel(t, eth.ETH67, LightSync) } - -func testCancel(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(MaxHeaderFetch) - tester.newPeer("peer", protocol, chain.blocks[1:]) - - // Make sure canceling works with a pristine downloader - tester.downloader.Cancel() - if !tester.downloader.queue.Idle() { - t.Errorf("download queue not idle") - } - // Synchronise with the peer, but cancel afterwards - if err := tester.sync("peer", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - tester.downloader.Cancel() - if !tester.downloader.queue.Idle() { - t.Errorf("download queue not idle") - } -} - -// Tests that synchronisation from multiple peers works as intended (multi thread sanity test). -func TestMultiSynchronisation68Full(t *testing.T) { testMultiSynchronisation(t, eth.ETH68, FullSync) } -func TestMultiSynchronisation68Snap(t *testing.T) { testMultiSynchronisation(t, eth.ETH68, SnapSync) } -func TestMultiSynchronisation68Light(t *testing.T) { testMultiSynchronisation(t, eth.ETH68, LightSync) } -func TestMultiSynchronisation67Full(t *testing.T) { testMultiSynchronisation(t, eth.ETH67, FullSync) } -func TestMultiSynchronisation67Snap(t *testing.T) { testMultiSynchronisation(t, eth.ETH67, SnapSync) } -func TestMultiSynchronisation67Light(t *testing.T) { testMultiSynchronisation(t, eth.ETH67, LightSync) } - -func testMultiSynchronisation(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create various peers with various parts of the chain - targetPeers := 8 - chain := testChainBase.shorten(targetPeers * 100) - - for i := 0; i < targetPeers; i++ { - id := fmt.Sprintf("peer #%d", i) - tester.newPeer(id, protocol, chain.shorten(len(chain.blocks) / (i + 1)).blocks[1:]) - } - if err := tester.sync("peer #0", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) -} - -// Tests that synchronisations behave well in multi-version protocol environments -// and not wreak havoc on other nodes in the network. -func TestMultiProtoSynchronisation68Full(t *testing.T) { testMultiProtoSync(t, eth.ETH68, FullSync) } -func TestMultiProtoSynchronisation68Snap(t *testing.T) { testMultiProtoSync(t, eth.ETH68, SnapSync) } -func TestMultiProtoSynchronisation68Light(t *testing.T) { testMultiProtoSync(t, eth.ETH68, LightSync) } -func TestMultiProtoSynchronisation67Full(t *testing.T) { testMultiProtoSync(t, eth.ETH67, FullSync) } -func TestMultiProtoSynchronisation67Snap(t *testing.T) { testMultiProtoSync(t, eth.ETH67, SnapSync) } -func TestMultiProtoSynchronisation67Light(t *testing.T) { testMultiProtoSync(t, eth.ETH67, LightSync) } - -func testMultiProtoSync(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create a small enough block chain to download - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - // Create peers of every type - tester.newPeer("peer 68", eth.ETH68, chain.blocks[1:]) - tester.newPeer("peer 67", eth.ETH67, chain.blocks[1:]) - - // Synchronise with the requested peer and make sure all blocks were retrieved - if err := tester.sync(fmt.Sprintf("peer %d", protocol), nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) - - // Check that no peers have been dropped off - for _, version := range []int{68, 67} { - peer := fmt.Sprintf("peer %d", version) - if _, ok := tester.peers[peer]; !ok { - t.Errorf("%s dropped", peer) - } - } -} - -// Tests that if a block is empty (e.g. header only), no body request should be -// made, and instead the header should be assembled into a whole block in itself. -func TestEmptyShortCircuit68Full(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, FullSync) } -func TestEmptyShortCircuit68Snap(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, SnapSync) } -func TestEmptyShortCircuit68Light(t *testing.T) { testEmptyShortCircuit(t, eth.ETH68, LightSync) } -func TestEmptyShortCircuit67Full(t *testing.T) { testEmptyShortCircuit(t, eth.ETH67, FullSync) } -func TestEmptyShortCircuit67Snap(t *testing.T) { testEmptyShortCircuit(t, eth.ETH67, SnapSync) } -func TestEmptyShortCircuit67Light(t *testing.T) { testEmptyShortCircuit(t, eth.ETH67, LightSync) } - -func testEmptyShortCircuit(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - // Create a block chain to download - chain := testChainBase - tester.newPeer("peer", protocol, chain.blocks[1:]) - - // Instrument the downloader to signal body requests - var bodiesHave, receiptsHave atomic.Int32 - tester.downloader.bodyFetchHook = func(headers []*types.Header) { - bodiesHave.Add(int32(len(headers))) - } - tester.downloader.receiptFetchHook = func(headers []*types.Header) { - receiptsHave.Add(int32(len(headers))) - } - // Synchronise with the peer and make sure all blocks were retrieved - if err := tester.sync("peer", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) - - // Validate the number of block bodies that should have been requested - bodiesNeeded, receiptsNeeded := 0, 0 - for _, block := range chain.blocks[1:] { - if mode != LightSync && (len(block.Transactions()) > 0 || len(block.Uncles()) > 0) { - bodiesNeeded++ - } - } - for _, block := range chain.blocks[1:] { - if mode == SnapSync && len(block.Transactions()) > 0 { - receiptsNeeded++ - } - } - if int(bodiesHave.Load()) != bodiesNeeded { - t.Errorf("body retrieval count mismatch: have %v, want %v", bodiesHave.Load(), bodiesNeeded) - } - if int(receiptsHave.Load()) != receiptsNeeded { - t.Errorf("receipt retrieval count mismatch: have %v, want %v", receiptsHave.Load(), receiptsNeeded) - } -} - -// Tests that headers are enqueued continuously, preventing malicious nodes from -// stalling the downloader by feeding gapped header chains. -func TestMissingHeaderAttack68Full(t *testing.T) { testMissingHeaderAttack(t, eth.ETH68, FullSync) } -func TestMissingHeaderAttack68Snap(t *testing.T) { testMissingHeaderAttack(t, eth.ETH68, SnapSync) } -func TestMissingHeaderAttack68Light(t *testing.T) { testMissingHeaderAttack(t, eth.ETH68, LightSync) } -func TestMissingHeaderAttack67Full(t *testing.T) { testMissingHeaderAttack(t, eth.ETH67, FullSync) } -func TestMissingHeaderAttack67Snap(t *testing.T) { testMissingHeaderAttack(t, eth.ETH67, SnapSync) } -func TestMissingHeaderAttack67Light(t *testing.T) { testMissingHeaderAttack(t, eth.ETH67, LightSync) } - -func testMissingHeaderAttack(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - attacker := tester.newPeer("attack", protocol, chain.blocks[1:]) - attacker.withholdHeaders[chain.blocks[len(chain.blocks)/2-1].Hash()] = struct{}{} - - if err := tester.sync("attack", nil, mode); err == nil { - t.Fatalf("succeeded attacker synchronisation") - } - // Synchronise with the valid peer and make sure sync succeeds - tester.newPeer("valid", protocol, chain.blocks[1:]) - if err := tester.sync("valid", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) -} - -// Tests that if requested headers are shifted (i.e. first is missing), the queue -// detects the invalid numbering. -func TestShiftedHeaderAttack68Full(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH68, FullSync) } -func TestShiftedHeaderAttack68Snap(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH68, SnapSync) } -func TestShiftedHeaderAttack68Light(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH68, LightSync) } -func TestShiftedHeaderAttack67Full(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH67, FullSync) } -func TestShiftedHeaderAttack67Snap(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH67, SnapSync) } -func TestShiftedHeaderAttack67Light(t *testing.T) { testShiftedHeaderAttack(t, eth.ETH67, LightSync) } - -func testShiftedHeaderAttack(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - // Attempt a full sync with an attacker feeding shifted headers - attacker := tester.newPeer("attack", protocol, chain.blocks[1:]) - attacker.withholdHeaders[chain.blocks[1].Hash()] = struct{}{} - - if err := tester.sync("attack", nil, mode); err == nil { - t.Fatalf("succeeded attacker synchronisation") - } - // Synchronise with the valid peer and make sure sync succeeds - tester.newPeer("valid", protocol, chain.blocks[1:]) - if err := tester.sync("valid", nil, mode); err != nil { - t.Fatalf("failed to synchronise blocks: %v", err) - } - assertOwnChain(t, tester, len(chain.blocks)) -} - -// Tests that a peer advertising a high TD doesn't get to stall the downloader -// afterwards by not sending any useful hashes. -func TestHighTDStarvationAttack68Full(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH68, FullSync) -} -func TestHighTDStarvationAttack68Snap(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH68, SnapSync) -} -func TestHighTDStarvationAttack68Light(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH68, LightSync) -} -func TestHighTDStarvationAttack67Full(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH67, FullSync) -} -func TestHighTDStarvationAttack67Snap(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH67, SnapSync) -} -func TestHighTDStarvationAttack67Light(t *testing.T) { - testHighTDStarvationAttack(t, eth.ETH67, LightSync) -} - -func testHighTDStarvationAttack(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(1) - tester.newPeer("attack", protocol, chain.blocks[1:]) - if err := tester.sync("attack", big.NewInt(1000000), mode); err != errStallingPeer { - t.Fatalf("synchronisation error mismatch: have %v, want %v", err, errStallingPeer) - } -} - -// Tests that misbehaving peers are disconnected, whilst behaving ones are not. -func TestBlockHeaderAttackerDropping68(t *testing.T) { testBlockHeaderAttackerDropping(t, eth.ETH68) } -func TestBlockHeaderAttackerDropping67(t *testing.T) { testBlockHeaderAttackerDropping(t, eth.ETH67) } - -func testBlockHeaderAttackerDropping(t *testing.T, protocol uint) { - // Define the disconnection requirement for individual hash fetch errors - tests := []struct { - result error - drop bool - }{ - {nil, false}, // Sync succeeded, all is well - {errBusy, false}, // Sync is already in progress, no problem - {errUnknownPeer, false}, // Peer is unknown, was already dropped, don't double drop - {errBadPeer, true}, // Peer was deemed bad for some reason, drop it - {errStallingPeer, true}, // Peer was detected to be stalling, drop it - {errUnsyncedPeer, true}, // Peer was detected to be unsynced, drop it - {errNoPeers, false}, // No peers to download from, soft race, no issue - {errTimeout, true}, // No hashes received in due time, drop the peer - {errEmptyHeaderSet, true}, // No headers were returned as a response, drop as it's a dead end - {errPeersUnavailable, true}, // Nobody had the advertised blocks, drop the advertiser - {errInvalidAncestor, true}, // Agreed upon ancestor is not acceptable, drop the chain rewriter - {errInvalidChain, true}, // Hash chain was detected as invalid, definitely drop - {errInvalidBody, false}, // A bad peer was detected, but not the sync origin - {errInvalidReceipt, false}, // A bad peer was detected, but not the sync origin - {errCancelContentProcessing, false}, // Synchronisation was canceled, origin may be innocent, don't drop - } - // Run the tests and check disconnection status - tester := newTester(t) - defer tester.terminate() - chain := testChainBase.shorten(1) - - for i, tt := range tests { - // Register a new peer and ensure its presence - id := fmt.Sprintf("test %d", i) - tester.newPeer(id, protocol, chain.blocks[1:]) - if _, ok := tester.peers[id]; !ok { - t.Fatalf("test %d: registered peer not found", i) - } - // Simulate a synchronisation and check the required result - tester.downloader.synchroniseMock = func(string, common.Hash) error { return tt.result } - - tester.downloader.LegacySync(id, tester.chain.Genesis().Hash(), big.NewInt(1000), nil, FullSync) - if _, ok := tester.peers[id]; !ok != tt.drop { - t.Errorf("test %d: peer drop mismatch for %v: have %v, want %v", i, tt.result, !ok, tt.drop) - } - } -} - -// Tests that synchronisation progress (origin block number, current block number -// and highest block number) is tracked and updated correctly. -func TestSyncProgress68Full(t *testing.T) { testSyncProgress(t, eth.ETH68, FullSync) } -func TestSyncProgress68Snap(t *testing.T) { testSyncProgress(t, eth.ETH68, SnapSync) } -func TestSyncProgress68Light(t *testing.T) { testSyncProgress(t, eth.ETH68, LightSync) } -func TestSyncProgress67Full(t *testing.T) { testSyncProgress(t, eth.ETH67, FullSync) } -func TestSyncProgress67Snap(t *testing.T) { testSyncProgress(t, eth.ETH67, SnapSync) } -func TestSyncProgress67Light(t *testing.T) { testSyncProgress(t, eth.ETH67, LightSync) } - -func testSyncProgress(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - // Set a sync init hook to catch progress changes - starting := make(chan struct{}) - progress := make(chan struct{}) - - tester.downloader.syncInitHook = func(origin, latest uint64) { - starting <- struct{}{} - <-progress - } - checkProgress(t, tester.downloader, "pristine", ethereum.SyncProgress{}) - - // Synchronise half the blocks and check initial progress - tester.newPeer("peer-half", protocol, chain.shorten(len(chain.blocks) / 2).blocks[1:]) - pending := new(sync.WaitGroup) - pending.Add(1) - - go func() { - defer pending.Done() - if err := tester.sync("peer-half", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - checkProgress(t, tester.downloader, "initial", ethereum.SyncProgress{ - HighestBlock: uint64(len(chain.blocks)/2 - 1), - }) - progress <- struct{}{} - pending.Wait() - - // Synchronise all the blocks and check continuation progress - tester.newPeer("peer-full", protocol, chain.blocks[1:]) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("peer-full", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - checkProgress(t, tester.downloader, "completing", ethereum.SyncProgress{ - StartingBlock: uint64(len(chain.blocks)/2 - 1), - CurrentBlock: uint64(len(chain.blocks)/2 - 1), - HighestBlock: uint64(len(chain.blocks) - 1), - }) - - // Check final progress after successful sync - progress <- struct{}{} - pending.Wait() - checkProgress(t, tester.downloader, "final", ethereum.SyncProgress{ - StartingBlock: uint64(len(chain.blocks)/2 - 1), - CurrentBlock: uint64(len(chain.blocks) - 1), - HighestBlock: uint64(len(chain.blocks) - 1), - }) -} - -func checkProgress(t *testing.T, d *Downloader, stage string, want ethereum.SyncProgress) { - // Mark this method as a helper to report errors at callsite, not in here - t.Helper() - - p := d.Progress() - if p.StartingBlock != want.StartingBlock || p.CurrentBlock != want.CurrentBlock || p.HighestBlock != want.HighestBlock { - t.Fatalf("%s progress mismatch:\nhave %+v\nwant %+v", stage, p, want) - } -} - -// Tests that synchronisation progress (origin block number and highest block -// number) is tracked and updated correctly in case of a fork (or manual head -// revertal). -func TestForkedSyncProgress68Full(t *testing.T) { testForkedSyncProgress(t, eth.ETH68, FullSync) } -func TestForkedSyncProgress68Snap(t *testing.T) { testForkedSyncProgress(t, eth.ETH68, SnapSync) } -func TestForkedSyncProgress68Light(t *testing.T) { testForkedSyncProgress(t, eth.ETH68, LightSync) } -func TestForkedSyncProgress67Full(t *testing.T) { testForkedSyncProgress(t, eth.ETH67, FullSync) } -func TestForkedSyncProgress67Snap(t *testing.T) { testForkedSyncProgress(t, eth.ETH67, SnapSync) } -func TestForkedSyncProgress67Light(t *testing.T) { testForkedSyncProgress(t, eth.ETH67, LightSync) } - -func testForkedSyncProgress(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chainA := testChainForkLightA.shorten(len(testChainBase.blocks) + MaxHeaderFetch) - chainB := testChainForkLightB.shorten(len(testChainBase.blocks) + MaxHeaderFetch) - - // Set a sync init hook to catch progress changes - starting := make(chan struct{}) - progress := make(chan struct{}) - - tester.downloader.syncInitHook = func(origin, latest uint64) { - starting <- struct{}{} - <-progress - } - checkProgress(t, tester.downloader, "pristine", ethereum.SyncProgress{}) - - // Synchronise with one of the forks and check progress - tester.newPeer("fork A", protocol, chainA.blocks[1:]) - pending := new(sync.WaitGroup) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("fork A", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - - checkProgress(t, tester.downloader, "initial", ethereum.SyncProgress{ - HighestBlock: uint64(len(chainA.blocks) - 1), - }) - progress <- struct{}{} - pending.Wait() - - // Simulate a successful sync above the fork - tester.downloader.syncStatsChainOrigin = tester.downloader.syncStatsChainHeight - - // Synchronise with the second fork and check progress resets - tester.newPeer("fork B", protocol, chainB.blocks[1:]) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("fork B", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - checkProgress(t, tester.downloader, "forking", ethereum.SyncProgress{ - StartingBlock: uint64(len(testChainBase.blocks)) - 1, - CurrentBlock: uint64(len(chainA.blocks) - 1), - HighestBlock: uint64(len(chainB.blocks) - 1), - }) - - // Check final progress after successful sync - progress <- struct{}{} - pending.Wait() - checkProgress(t, tester.downloader, "final", ethereum.SyncProgress{ - StartingBlock: uint64(len(testChainBase.blocks)) - 1, - CurrentBlock: uint64(len(chainB.blocks) - 1), - HighestBlock: uint64(len(chainB.blocks) - 1), - }) -} - -// Tests that if synchronisation is aborted due to some failure, then the progress -// origin is not updated in the next sync cycle, as it should be considered the -// continuation of the previous sync and not a new instance. -func TestFailedSyncProgress68Full(t *testing.T) { testFailedSyncProgress(t, eth.ETH68, FullSync) } -func TestFailedSyncProgress68Snap(t *testing.T) { testFailedSyncProgress(t, eth.ETH68, SnapSync) } -func TestFailedSyncProgress68Light(t *testing.T) { testFailedSyncProgress(t, eth.ETH68, LightSync) } -func TestFailedSyncProgress67Full(t *testing.T) { testFailedSyncProgress(t, eth.ETH67, FullSync) } -func TestFailedSyncProgress67Snap(t *testing.T) { testFailedSyncProgress(t, eth.ETH67, SnapSync) } -func TestFailedSyncProgress67Light(t *testing.T) { testFailedSyncProgress(t, eth.ETH67, LightSync) } - -func testFailedSyncProgress(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - // Set a sync init hook to catch progress changes - starting := make(chan struct{}) - progress := make(chan struct{}) - - tester.downloader.syncInitHook = func(origin, latest uint64) { - starting <- struct{}{} - <-progress - } - checkProgress(t, tester.downloader, "pristine", ethereum.SyncProgress{}) - - // Attempt a full sync with a faulty peer - missing := len(chain.blocks)/2 - 1 - - faulter := tester.newPeer("faulty", protocol, chain.blocks[1:]) - faulter.withholdHeaders[chain.blocks[missing].Hash()] = struct{}{} - - pending := new(sync.WaitGroup) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("faulty", nil, mode); err == nil { - panic("succeeded faulty synchronisation") - } - }() - <-starting - checkProgress(t, tester.downloader, "initial", ethereum.SyncProgress{ - HighestBlock: uint64(len(chain.blocks) - 1), - }) - progress <- struct{}{} - pending.Wait() - afterFailedSync := tester.downloader.Progress() - - // Synchronise with a good peer and check that the progress origin remind the same - // after a failure - tester.newPeer("valid", protocol, chain.blocks[1:]) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("valid", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - checkProgress(t, tester.downloader, "completing", afterFailedSync) - - // Check final progress after successful sync - progress <- struct{}{} - pending.Wait() - checkProgress(t, tester.downloader, "final", ethereum.SyncProgress{ - CurrentBlock: uint64(len(chain.blocks) - 1), - HighestBlock: uint64(len(chain.blocks) - 1), - }) -} - -// Tests that if an attacker fakes a chain height, after the attack is detected, -// the progress height is successfully reduced at the next sync invocation. -func TestFakedSyncProgress68Full(t *testing.T) { testFakedSyncProgress(t, eth.ETH68, FullSync) } -func TestFakedSyncProgress68Snap(t *testing.T) { testFakedSyncProgress(t, eth.ETH68, SnapSync) } -func TestFakedSyncProgress68Light(t *testing.T) { testFakedSyncProgress(t, eth.ETH68, LightSync) } -func TestFakedSyncProgress67Full(t *testing.T) { testFakedSyncProgress(t, eth.ETH67, FullSync) } -func TestFakedSyncProgress67Snap(t *testing.T) { testFakedSyncProgress(t, eth.ETH67, SnapSync) } -func TestFakedSyncProgress67Light(t *testing.T) { testFakedSyncProgress(t, eth.ETH67, LightSync) } - -func testFakedSyncProgress(t *testing.T, protocol uint, mode SyncMode) { - tester := newTester(t) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - - // Set a sync init hook to catch progress changes - starting := make(chan struct{}) - progress := make(chan struct{}) - tester.downloader.syncInitHook = func(origin, latest uint64) { - starting <- struct{}{} - <-progress - } - checkProgress(t, tester.downloader, "pristine", ethereum.SyncProgress{}) - - // Create and sync with an attacker that promises a higher chain than available. - attacker := tester.newPeer("attack", protocol, chain.blocks[1:]) - numMissing := 5 - for i := len(chain.blocks) - 2; i > len(chain.blocks)-numMissing; i-- { - attacker.withholdHeaders[chain.blocks[i].Hash()] = struct{}{} - } - pending := new(sync.WaitGroup) - pending.Add(1) - go func() { - defer pending.Done() - if err := tester.sync("attack", nil, mode); err == nil { - panic("succeeded attacker synchronisation") - } - }() - <-starting - checkProgress(t, tester.downloader, "initial", ethereum.SyncProgress{ - HighestBlock: uint64(len(chain.blocks) - 1), - }) - progress <- struct{}{} - pending.Wait() - afterFailedSync := tester.downloader.Progress() - - // Synchronise with a good peer and check that the progress height has been reduced to - // the true value. - validChain := chain.shorten(len(chain.blocks) - numMissing) - tester.newPeer("valid", protocol, validChain.blocks[1:]) - pending.Add(1) - - go func() { - defer pending.Done() - if err := tester.sync("valid", nil, mode); err != nil { - panic(fmt.Sprintf("failed to synchronise blocks: %v", err)) - } - }() - <-starting - checkProgress(t, tester.downloader, "completing", ethereum.SyncProgress{ - CurrentBlock: afterFailedSync.CurrentBlock, - HighestBlock: uint64(len(validChain.blocks) - 1), - }) - // Check final progress after successful sync. - progress <- struct{}{} - pending.Wait() - checkProgress(t, tester.downloader, "final", ethereum.SyncProgress{ - CurrentBlock: uint64(len(validChain.blocks) - 1), - HighestBlock: uint64(len(validChain.blocks) - 1), - }) -} - -func TestRemoteHeaderRequestSpan(t *testing.T) { - testCases := []struct { - remoteHeight uint64 - localHeight uint64 - expected []int - }{ - // Remote is way higher. We should ask for the remote head and go backwards - {1500, 1000, - []int{1323, 1339, 1355, 1371, 1387, 1403, 1419, 1435, 1451, 1467, 1483, 1499}, - }, - {15000, 13006, - []int{14823, 14839, 14855, 14871, 14887, 14903, 14919, 14935, 14951, 14967, 14983, 14999}, - }, - // Remote is pretty close to us. We don't have to fetch as many - {1200, 1150, - []int{1149, 1154, 1159, 1164, 1169, 1174, 1179, 1184, 1189, 1194, 1199}, - }, - // Remote is equal to us (so on a fork with higher td) - // We should get the closest couple of ancestors - {1500, 1500, - []int{1497, 1499}, - }, - // We're higher than the remote! Odd - {1000, 1500, - []int{997, 999}, - }, - // Check some weird edgecases that it behaves somewhat rationally - {0, 1500, - []int{0, 2}, - }, - {6000000, 0, - []int{5999823, 5999839, 5999855, 5999871, 5999887, 5999903, 5999919, 5999935, 5999951, 5999967, 5999983, 5999999}, - }, - {0, 0, - []int{0, 2}, - }, - } - reqs := func(from, count, span int) []int { - var r []int - num := from - for len(r) < count { - r = append(r, num) - num += span + 1 - } - return r - } - for i, tt := range testCases { - from, count, span, max := calculateRequestSpan(tt.remoteHeight, tt.localHeight) - data := reqs(int(from), count, span) - - if max != uint64(data[len(data)-1]) { - t.Errorf("test %d: wrong last value %d != %d", i, data[len(data)-1], max) - } - failed := false - if len(data) != len(tt.expected) { - failed = true - t.Errorf("test %d: length wrong, expected %d got %d", i, len(tt.expected), len(data)) - } else { - for j, n := range data { - if n != tt.expected[j] { - failed = true - break - } - } - } - if failed { - res := strings.ReplaceAll(fmt.Sprint(data), " ", ",") - exp := strings.ReplaceAll(fmt.Sprint(tt.expected), " ", ",") - t.Logf("got: %v\n", res) - t.Logf("exp: %v\n", exp) - t.Errorf("test %d: wrong values", i) - } - } -} - -// Tests that peers below a pre-configured checkpoint block are prevented from -// being fast-synced from, avoiding potential cheap eclipse attacks. -func TestBeaconSync68Full(t *testing.T) { testBeaconSync(t, eth.ETH68, FullSync) } -func TestBeaconSync68Snap(t *testing.T) { testBeaconSync(t, eth.ETH68, SnapSync) } -func TestBeaconSync67Full(t *testing.T) { testBeaconSync(t, eth.ETH67, FullSync) } -func TestBeaconSync67Snap(t *testing.T) { testBeaconSync(t, eth.ETH67, SnapSync) } - -func testBeaconSync(t *testing.T, protocol uint, mode SyncMode) { - //log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) - - var cases = []struct { - name string // The name of testing scenario - local int // The length of local chain(canonical chain assumed), 0 means genesis is the head - }{ - {name: "Beacon sync since genesis", local: 0}, - {name: "Beacon sync with short local chain", local: 1}, - {name: "Beacon sync with long local chain", local: blockCacheMaxItems - 15 - fsMinFullBlocks/2}, - {name: "Beacon sync with full local chain", local: blockCacheMaxItems - 15 - 1}, - } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - success := make(chan struct{}) - tester := newTesterWithNotification(t, func() { - close(success) - }) - defer tester.terminate() - - chain := testChainBase.shorten(blockCacheMaxItems - 15) - tester.newPeer("peer", protocol, chain.blocks[1:]) - - // Build the local chain segment if it's required - if c.local > 0 { - tester.chain.InsertChain(chain.blocks[1 : c.local+1]) - } - if err := tester.downloader.BeaconSync(mode, chain.blocks[len(chain.blocks)-1].Header(), nil); err != nil { - t.Fatalf("Failed to beacon sync chain %v %v", c.name, err) - } - select { - case <-success: - // Ok, downloader fully cancelled after sync cycle - if bs := int(tester.chain.CurrentBlock().Number.Uint64()) + 1; bs != len(chain.blocks) { - t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, len(chain.blocks)) - } - case <-time.NewTimer(time.Second * 3).C: - t.Fatalf("Failed to sync chain in three seconds") - } - }) - } -} diff --git a/eth/downloader/events.go b/eth/downloader/events.go deleted file mode 100644 index 25255a3a72..0000000000 --- a/eth/downloader/events.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import "github.com/ethereum/go-ethereum/core/types" - -type DoneEvent struct { - Latest *types.Header -} -type StartEvent struct{} -type FailedEvent struct{ Err error } diff --git a/eth/downloader/fetchers.go b/eth/downloader/fetchers.go deleted file mode 100644 index cc4279b0da..0000000000 --- a/eth/downloader/fetchers.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/protocols/eth" -) - -// fetchHeadersByHash is a blocking version of Peer.RequestHeadersByHash which -// handles all the cancellation, interruption and timeout mechanisms of a data -// retrieval to allow blocking API calls. -func (d *Downloader) fetchHeadersByHash(p *peerConnection, hash common.Hash, amount int, skip int, reverse bool) ([]*types.Header, []common.Hash, error) { - // Create the response sink and send the network request - start := time.Now() - resCh := make(chan *eth.Response) - - req, err := p.peer.RequestHeadersByHash(hash, amount, skip, reverse, resCh) - if err != nil { - return nil, nil, err - } - defer req.Close() - - // Wait until the response arrives, the request is cancelled or times out - ttl := d.peers.rates.TargetTimeout() - - timeoutTimer := time.NewTimer(ttl) - defer timeoutTimer.Stop() - - select { - case <-d.cancelCh: - return nil, nil, errCanceled - - case <-timeoutTimer.C: - // Header retrieval timed out, update the metrics - p.log.Debug("Header request timed out", "elapsed", ttl) - headerTimeoutMeter.Mark(1) - - return nil, nil, errTimeout - - case res := <-resCh: - // Headers successfully retrieved, update the metrics - headerReqTimer.Update(time.Since(start)) - headerInMeter.Mark(int64(len(*res.Res.(*eth.BlockHeadersRequest)))) - - // Don't reject the packet even if it turns out to be bad, downloader will - // disconnect the peer on its own terms. Simply delivery the headers to - // be processed by the caller - res.Done <- nil - - return *res.Res.(*eth.BlockHeadersRequest), res.Meta.([]common.Hash), nil - } -} - -// fetchHeadersByNumber is a blocking version of Peer.RequestHeadersByNumber which -// handles all the cancellation, interruption and timeout mechanisms of a data -// retrieval to allow blocking API calls. -func (d *Downloader) fetchHeadersByNumber(p *peerConnection, number uint64, amount int, skip int, reverse bool) ([]*types.Header, []common.Hash, error) { - // Create the response sink and send the network request - start := time.Now() - resCh := make(chan *eth.Response) - - req, err := p.peer.RequestHeadersByNumber(number, amount, skip, reverse, resCh) - if err != nil { - return nil, nil, err - } - defer req.Close() - - // Wait until the response arrives, the request is cancelled or times out - ttl := d.peers.rates.TargetTimeout() - - timeoutTimer := time.NewTimer(ttl) - defer timeoutTimer.Stop() - - select { - case <-d.cancelCh: - return nil, nil, errCanceled - - case <-timeoutTimer.C: - // Header retrieval timed out, update the metrics - p.log.Debug("Header request timed out", "elapsed", ttl) - headerTimeoutMeter.Mark(1) - - return nil, nil, errTimeout - - case res := <-resCh: - // Headers successfully retrieved, update the metrics - headerReqTimer.Update(time.Since(start)) - headerInMeter.Mark(int64(len(*res.Res.(*eth.BlockHeadersRequest)))) - - // Don't reject the packet even if it turns out to be bad, downloader will - // disconnect the peer on its own terms. Simply delivery the headers to - // be processed by the caller - res.Done <- nil - - return *res.Res.(*eth.BlockHeadersRequest), res.Meta.([]common.Hash), nil - } -} diff --git a/eth/downloader/fetchers_concurrent.go b/eth/downloader/fetchers_concurrent.go deleted file mode 100644 index 649aa27615..0000000000 --- a/eth/downloader/fetchers_concurrent.go +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "errors" - "sort" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/prque" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -// timeoutGracePeriod is the amount of time to allow for a peer to deliver a -// response to a locally already timed out request. Timeouts are not penalized -// as a peer might be temporarily overloaded, however, they still must reply -// to each request. Failing to do so is considered a protocol violation. -var timeoutGracePeriod = 2 * time.Minute - -// typedQueue is an interface defining the adaptor needed to translate the type -// specific downloader/queue schedulers into the type-agnostic general concurrent -// fetcher algorithm calls. -type typedQueue interface { - // waker returns a notification channel that gets pinged in case more fetches - // have been queued up, so the fetcher might assign it to idle peers. - waker() chan bool - - // pending returns the number of wrapped items that are currently queued for - // fetching by the concurrent downloader. - pending() int - - // capacity is responsible for calculating how many items of the abstracted - // type a particular peer is estimated to be able to retrieve within the - // allotted round trip time. - capacity(peer *peerConnection, rtt time.Duration) int - - // updateCapacity is responsible for updating how many items of the abstracted - // type a particular peer is estimated to be able to retrieve in a unit time. - updateCapacity(peer *peerConnection, items int, elapsed time.Duration) - - // reserve is responsible for allocating a requested number of pending items - // from the download queue to the specified peer. - reserve(peer *peerConnection, items int) (*fetchRequest, bool, bool) - - // unreserve is responsible for removing the current retrieval allocation - // assigned to a specific peer and placing it back into the pool to allow - // reassigning to some other peer. - unreserve(peer string) int - - // request is responsible for converting a generic fetch request into a typed - // one and sending it to the remote peer for fulfillment. - request(peer *peerConnection, req *fetchRequest, resCh chan *eth.Response) (*eth.Request, error) - - // deliver is responsible for taking a generic response packet from the - // concurrent fetcher, unpacking the type specific data and delivering - // it to the downloader's queue. - deliver(peer *peerConnection, packet *eth.Response) (int, error) -} - -// concurrentFetch iteratively downloads scheduled block parts, taking available -// peers, reserving a chunk of fetch requests for each and waiting for delivery -// or timeouts. -func (d *Downloader) concurrentFetch(queue typedQueue, beaconMode bool) error { - // Create a delivery channel to accept responses from all peers - responses := make(chan *eth.Response) - - // Track the currently active requests and their timeout order - pending := make(map[string]*eth.Request) - defer func() { - // Abort all requests on sync cycle cancellation. The requests may still - // be fulfilled by the remote side, but the dispatcher will not wait to - // deliver them since nobody's going to be listening. - for _, req := range pending { - req.Close() - } - }() - ordering := make(map[*eth.Request]int) - timeouts := prque.New[int64, *eth.Request](func(data *eth.Request, index int) { - ordering[data] = index - }) - - timeout := time.NewTimer(0) - if !timeout.Stop() { - <-timeout.C - } - defer timeout.Stop() - - // Track the timed-out but not-yet-answered requests separately. We want to - // keep tracking which peers are busy (potentially overloaded), so removing - // all trace of a timed out request is not good. We also can't just cancel - // the pending request altogether as that would prevent a late response from - // being delivered, thus never unblocking the peer. - stales := make(map[string]*eth.Request) - defer func() { - // Abort all requests on sync cycle cancellation. The requests may still - // be fulfilled by the remote side, but the dispatcher will not wait to - // deliver them since nobody's going to be listening. - for _, req := range stales { - req.Close() - } - }() - // Subscribe to peer lifecycle events to schedule tasks to new joiners and - // reschedule tasks upon disconnections. We don't care which event happened - // for simplicity, so just use a single channel. - peering := make(chan *peeringEvent, 64) // arbitrary buffer, just some burst protection - - peeringSub := d.peers.SubscribeEvents(peering) - defer peeringSub.Unsubscribe() - - // Prepare the queue and fetch block parts until the block header fetcher's done - finished := false - for { - // Short circuit if we lost all our peers - if d.peers.Len() == 0 && !beaconMode { - return errNoPeers - } - // If there's nothing more to fetch, wait or terminate - if queue.pending() == 0 { - if len(pending) == 0 && finished { - return nil - } - } else { - // Send a download request to all idle peers, until throttled - var ( - idles []*peerConnection - caps []int - ) - for _, peer := range d.peers.AllPeers() { - pending, stale := pending[peer.id], stales[peer.id] - if pending == nil && stale == nil { - idles = append(idles, peer) - caps = append(caps, queue.capacity(peer, time.Second)) - } else if stale != nil { - if waited := time.Since(stale.Sent); waited > timeoutGracePeriod { - // Request has been in flight longer than the grace period - // permitted it, consider the peer malicious attempting to - // stall the sync. - peer.log.Warn("Peer stalling, dropping", "waited", common.PrettyDuration(waited)) - d.dropPeer(peer.id) - } - } - } - sort.Sort(&peerCapacitySort{idles, caps}) - - var ( - progressed bool - throttled bool - queued = queue.pending() - ) - for _, peer := range idles { - // Short circuit if throttling activated or there are no more - // queued tasks to be retrieved - if throttled { - break - } - if queued = queue.pending(); queued == 0 { - break - } - // Reserve a chunk of fetches for a peer. A nil can mean either that - // no more headers are available, or that the peer is known not to - // have them. - request, progress, throttle := queue.reserve(peer, queue.capacity(peer, d.peers.rates.TargetRoundTrip())) - if progress { - progressed = true - } - if throttle { - throttled = true - throttleCounter.Inc(1) - } - if request == nil { - continue - } - // Fetch the chunk and make sure any errors return the hashes to the queue - req, err := queue.request(peer, request, responses) - if err != nil { - // Sending the request failed, which generally means the peer - // was disconnected in between assignment and network send. - // Although all peer removal operations return allocated tasks - // to the queue, that is async, and we can do better here by - // immediately pushing the unfulfilled requests. - queue.unreserve(peer.id) // TODO(karalabe): This needs a non-expiration method - continue - } - pending[peer.id] = req - - ttl := d.peers.rates.TargetTimeout() - ordering[req] = timeouts.Size() - - timeouts.Push(req, -time.Now().Add(ttl).UnixNano()) - if timeouts.Size() == 1 { - timeout.Reset(ttl) - } - } - // Make sure that we have peers available for fetching. If all peers have been tried - // and all failed throw an error - if !progressed && !throttled && len(pending) == 0 && len(idles) == d.peers.Len() && queued > 0 && !beaconMode { - return errPeersUnavailable - } - } - // Wait for something to happen - select { - case <-d.cancelCh: - // If sync was cancelled, tear down the parallel retriever. Pending - // requests will be cancelled locally, and the remote responses will - // be dropped when they arrive - return errCanceled - - case event := <-peering: - // A peer joined or left, the tasks queue and allocations need to be - // checked for potential assignment or reassignment - peerid := event.peer.id - - if event.join { - // Sanity check the internal state; this can be dropped later - if _, ok := pending[peerid]; ok { - event.peer.log.Error("Pending request exists for joining peer") - } - if _, ok := stales[peerid]; ok { - event.peer.log.Error("Stale request exists for joining peer") - } - // Loop back to the entry point for task assignment - continue - } - // A peer left, any existing requests need to be untracked, pending - // tasks returned and possible reassignment checked - if req, ok := pending[peerid]; ok { - queue.unreserve(peerid) // TODO(karalabe): This needs a non-expiration method - delete(pending, peerid) - req.Close() - - if index, live := ordering[req]; live { - timeouts.Remove(index) - if index == 0 { - if !timeout.Stop() { - <-timeout.C - } - if timeouts.Size() > 0 { - _, exp := timeouts.Peek() - timeout.Reset(time.Until(time.Unix(0, -exp))) - } - } - delete(ordering, req) - } - } - if req, ok := stales[peerid]; ok { - delete(stales, peerid) - req.Close() - } - - case <-timeout.C: - // Retrieve the next request which should have timed out. The check - // below is purely for to catch programming errors, given the correct - // code, there's no possible order of events that should result in a - // timeout firing for a non-existent event. - req, exp := timeouts.Peek() - if now, at := time.Now(), time.Unix(0, -exp); now.Before(at) { - log.Error("Timeout triggered but not reached", "left", at.Sub(now)) - timeout.Reset(at.Sub(now)) - continue - } - // Stop tracking the timed out request from a timing perspective, - // cancel it, so it's not considered in-flight anymore, but keep - // the peer marked busy to prevent assigning a second request and - // overloading it further. - delete(pending, req.Peer) - stales[req.Peer] = req - - timeouts.Pop() // Popping an item will reorder indices in `ordering`, delete after, otherwise will resurrect! - if timeouts.Size() > 0 { - _, exp := timeouts.Peek() - timeout.Reset(time.Until(time.Unix(0, -exp))) - } - delete(ordering, req) - - // New timeout potentially set if there are more requests pending, - // reschedule the failed one to a free peer - fails := queue.unreserve(req.Peer) - - // Finally, update the peer's retrieval capacity, or if it's already - // below the minimum allowance, drop the peer. If a lot of retrieval - // elements expired, we might have overestimated the remote peer or - // perhaps ourselves. Only reset to minimal throughput but don't drop - // just yet. - // - // The reason the minimum threshold is 2 is that the downloader tries - // to estimate the bandwidth and latency of a peer separately, which - // requires pushing the measured capacity a bit and seeing how response - // times reacts, to it always requests one more than the minimum (i.e. - // min 2). - peer := d.peers.Peer(req.Peer) - if peer == nil { - // If the peer got disconnected in between, we should really have - // short-circuited it already. Just in case there's some strange - // codepath, leave this check in not to crash. - log.Error("Delivery timeout from unknown peer", "peer", req.Peer) - continue - } - if fails > 2 { - queue.updateCapacity(peer, 0, 0) - } else { - d.dropPeer(peer.id) - - // If this peer was the master peer, abort sync immediately - d.cancelLock.RLock() - master := peer.id == d.cancelPeer - d.cancelLock.RUnlock() - - if master { - d.cancel() - return errTimeout - } - } - - case res := <-responses: - // Response arrived, it may be for an existing or an already timed - // out request. If the former, update the timeout heap and perhaps - // reschedule the timeout timer. - index, live := ordering[res.Req] - if live { - timeouts.Remove(index) - if index == 0 { - if !timeout.Stop() { - <-timeout.C - } - if timeouts.Size() > 0 { - _, exp := timeouts.Peek() - timeout.Reset(time.Until(time.Unix(0, -exp))) - } - } - delete(ordering, res.Req) - } - // Delete the pending request (if it still exists) and mark the peer idle - delete(pending, res.Req.Peer) - delete(stales, res.Req.Peer) - - // Signal the dispatcher that the round trip is done. We'll drop the - // peer if the data turns out to be junk. - res.Done <- nil - res.Req.Close() - - // If the peer was previously banned and failed to deliver its pack - // in a reasonable time frame, ignore its message. - if peer := d.peers.Peer(res.Req.Peer); peer != nil { - // Deliver the received chunk of data and check chain validity - accepted, err := queue.deliver(peer, res) - if errors.Is(err, errInvalidChain) { - return err - } - // Unless a peer delivered something completely else than requested (usually - // caused by a timed out request which came through in the end), set it to - // idle. If the delivery's stale, the peer should have already been idled. - if !errors.Is(err, errStaleDelivery) { - queue.updateCapacity(peer, accepted, res.Time) - } - } - - case cont := <-queue.waker(): - // The header fetcher sent a continuation flag, check if it's done - if !cont { - finished = true - } - } - } -} diff --git a/eth/downloader/fetchers_concurrent_bodies.go b/eth/downloader/fetchers_concurrent_bodies.go deleted file mode 100644 index 5105fda66b..0000000000 --- a/eth/downloader/fetchers_concurrent_bodies.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -// bodyQueue implements typedQueue and is a type adapter between the generic -// concurrent fetcher and the downloader. -type bodyQueue Downloader - -// waker returns a notification channel that gets pinged in case more body -// fetches have been queued up, so the fetcher might assign it to idle peers. -func (q *bodyQueue) waker() chan bool { - return q.queue.blockWakeCh -} - -// pending returns the number of bodies that are currently queued for fetching -// by the concurrent downloader. -func (q *bodyQueue) pending() int { - return q.queue.PendingBodies() -} - -// capacity is responsible for calculating how many bodies a particular peer is -// estimated to be able to retrieve within the allotted round trip time. -func (q *bodyQueue) capacity(peer *peerConnection, rtt time.Duration) int { - return peer.BodyCapacity(rtt) -} - -// updateCapacity is responsible for updating how many bodies a particular peer -// is estimated to be able to retrieve in a unit time. -func (q *bodyQueue) updateCapacity(peer *peerConnection, items int, span time.Duration) { - peer.UpdateBodyRate(items, span) -} - -// reserve is responsible for allocating a requested number of pending bodies -// from the download queue to the specified peer. -func (q *bodyQueue) reserve(peer *peerConnection, items int) (*fetchRequest, bool, bool) { - return q.queue.ReserveBodies(peer, items) -} - -// unreserve is responsible for removing the current body retrieval allocation -// assigned to a specific peer and placing it back into the pool to allow -// reassigning to some other peer. -func (q *bodyQueue) unreserve(peer string) int { - fails := q.queue.ExpireBodies(peer) - if fails > 2 { - log.Trace("Body delivery timed out", "peer", peer) - } else { - log.Debug("Body delivery stalling", "peer", peer) - } - return fails -} - -// request is responsible for converting a generic fetch request into a body -// one and sending it to the remote peer for fulfillment. -func (q *bodyQueue) request(peer *peerConnection, req *fetchRequest, resCh chan *eth.Response) (*eth.Request, error) { - peer.log.Trace("Requesting new batch of bodies", "count", len(req.Headers), "from", req.Headers[0].Number) - if q.bodyFetchHook != nil { - q.bodyFetchHook(req.Headers) - } - - hashes := make([]common.Hash, 0, len(req.Headers)) - for _, header := range req.Headers { - hashes = append(hashes, header.Hash()) - } - return peer.peer.RequestBodies(hashes, resCh) -} - -// deliver is responsible for taking a generic response packet from the concurrent -// fetcher, unpacking the body data and delivering it to the downloader's queue. -func (q *bodyQueue) deliver(peer *peerConnection, packet *eth.Response) (int, error) { - txs, uncles, withdrawals := packet.Res.(*eth.BlockBodiesResponse).Unpack() - hashsets := packet.Meta.([][]common.Hash) // {txs hashes, uncle hashes, withdrawal hashes} - - accepted, err := q.queue.DeliverBodies(peer.id, txs, hashsets[0], uncles, hashsets[1], withdrawals, hashsets[2]) - switch { - case err == nil && len(txs) == 0: - peer.log.Trace("Requested bodies delivered") - case err == nil: - peer.log.Trace("Delivered new batch of bodies", "count", len(txs), "accepted", accepted) - default: - peer.log.Debug("Failed to deliver retrieved bodies", "err", err) - } - return accepted, err -} diff --git a/eth/downloader/fetchers_concurrent_headers.go b/eth/downloader/fetchers_concurrent_headers.go deleted file mode 100644 index 8201f4ca74..0000000000 --- a/eth/downloader/fetchers_concurrent_headers.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -// headerQueue implements typedQueue and is a type adapter between the generic -// concurrent fetcher and the downloader. -type headerQueue Downloader - -// waker returns a notification channel that gets pinged in case more header -// fetches have been queued up, so the fetcher might assign it to idle peers. -func (q *headerQueue) waker() chan bool { - return q.queue.headerContCh -} - -// pending returns the number of headers that are currently queued for fetching -// by the concurrent downloader. -func (q *headerQueue) pending() int { - return q.queue.PendingHeaders() -} - -// capacity is responsible for calculating how many headers a particular peer is -// estimated to be able to retrieve within the allotted round trip time. -func (q *headerQueue) capacity(peer *peerConnection, rtt time.Duration) int { - return peer.HeaderCapacity(rtt) -} - -// updateCapacity is responsible for updating how many headers a particular peer -// is estimated to be able to retrieve in a unit time. -func (q *headerQueue) updateCapacity(peer *peerConnection, items int, span time.Duration) { - peer.UpdateHeaderRate(items, span) -} - -// reserve is responsible for allocating a requested number of pending headers -// from the download queue to the specified peer. -func (q *headerQueue) reserve(peer *peerConnection, items int) (*fetchRequest, bool, bool) { - return q.queue.ReserveHeaders(peer, items), false, false -} - -// unreserve is responsible for removing the current header retrieval allocation -// assigned to a specific peer and placing it back into the pool to allow -// reassigning to some other peer. -func (q *headerQueue) unreserve(peer string) int { - fails := q.queue.ExpireHeaders(peer) - if fails > 2 { - log.Trace("Header delivery timed out", "peer", peer) - } else { - log.Debug("Header delivery stalling", "peer", peer) - } - return fails -} - -// request is responsible for converting a generic fetch request into a header -// one and sending it to the remote peer for fulfillment. -func (q *headerQueue) request(peer *peerConnection, req *fetchRequest, resCh chan *eth.Response) (*eth.Request, error) { - peer.log.Trace("Requesting new batch of headers", "from", req.From) - return peer.peer.RequestHeadersByNumber(req.From, MaxHeaderFetch, 0, false, resCh) -} - -// deliver is responsible for taking a generic response packet from the concurrent -// fetcher, unpacking the header data and delivering it to the downloader's queue. -func (q *headerQueue) deliver(peer *peerConnection, packet *eth.Response) (int, error) { - headers := *packet.Res.(*eth.BlockHeadersRequest) - hashes := packet.Meta.([]common.Hash) - - accepted, err := q.queue.DeliverHeaders(peer.id, headers, hashes, q.headerProcCh) - switch { - case err == nil && len(headers) == 0: - peer.log.Trace("Requested headers delivered") - case err == nil: - peer.log.Trace("Delivered new batch of headers", "count", len(headers), "accepted", accepted) - default: - peer.log.Debug("Failed to deliver retrieved headers", "err", err) - } - return accepted, err -} diff --git a/eth/downloader/fetchers_concurrent_receipts.go b/eth/downloader/fetchers_concurrent_receipts.go deleted file mode 100644 index 3169f030ba..0000000000 --- a/eth/downloader/fetchers_concurrent_receipts.go +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2021 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -// receiptQueue implements typedQueue and is a type adapter between the generic -// concurrent fetcher and the downloader. -type receiptQueue Downloader - -// waker returns a notification channel that gets pinged in case more receipt -// fetches have been queued up, so the fetcher might assign it to idle peers. -func (q *receiptQueue) waker() chan bool { - return q.queue.receiptWakeCh -} - -// pending returns the number of receipt that are currently queued for fetching -// by the concurrent downloader. -func (q *receiptQueue) pending() int { - return q.queue.PendingReceipts() -} - -// capacity is responsible for calculating how many receipts a particular peer is -// estimated to be able to retrieve within the allotted round trip time. -func (q *receiptQueue) capacity(peer *peerConnection, rtt time.Duration) int { - return peer.ReceiptCapacity(rtt) -} - -// updateCapacity is responsible for updating how many receipts a particular peer -// is estimated to be able to retrieve in a unit time. -func (q *receiptQueue) updateCapacity(peer *peerConnection, items int, span time.Duration) { - peer.UpdateReceiptRate(items, span) -} - -// reserve is responsible for allocating a requested number of pending receipts -// from the download queue to the specified peer. -func (q *receiptQueue) reserve(peer *peerConnection, items int) (*fetchRequest, bool, bool) { - return q.queue.ReserveReceipts(peer, items) -} - -// unreserve is responsible for removing the current receipt retrieval allocation -// assigned to a specific peer and placing it back into the pool to allow -// reassigning to some other peer. -func (q *receiptQueue) unreserve(peer string) int { - fails := q.queue.ExpireReceipts(peer) - if fails > 2 { - log.Trace("Receipt delivery timed out", "peer", peer) - } else { - log.Debug("Receipt delivery stalling", "peer", peer) - } - return fails -} - -// request is responsible for converting a generic fetch request into a receipt -// one and sending it to the remote peer for fulfillment. -func (q *receiptQueue) request(peer *peerConnection, req *fetchRequest, resCh chan *eth.Response) (*eth.Request, error) { - peer.log.Trace("Requesting new batch of receipts", "count", len(req.Headers), "from", req.Headers[0].Number) - if q.receiptFetchHook != nil { - q.receiptFetchHook(req.Headers) - } - hashes := make([]common.Hash, 0, len(req.Headers)) - for _, header := range req.Headers { - hashes = append(hashes, header.Hash()) - } - return peer.peer.RequestReceipts(hashes, resCh) -} - -// deliver is responsible for taking a generic response packet from the concurrent -// fetcher, unpacking the receipt data and delivering it to the downloader's queue. -func (q *receiptQueue) deliver(peer *peerConnection, packet *eth.Response) (int, error) { - receipts := *packet.Res.(*eth.ReceiptsResponse) - hashes := packet.Meta.([]common.Hash) // {receipt hashes} - - accepted, err := q.queue.DeliverReceipts(peer.id, receipts, hashes) - switch { - case err == nil && len(receipts) == 0: - peer.log.Trace("Requested receipts delivered") - case err == nil: - peer.log.Trace("Delivered new batch of receipts", "count", len(receipts), "accepted", accepted) - default: - peer.log.Debug("Failed to deliver retrieved receipts", "err", err) - } - return accepted, err -} diff --git a/eth/downloader/metrics.go b/eth/downloader/metrics.go deleted file mode 100644 index 23c033a8ad..0000000000 --- a/eth/downloader/metrics.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Contains the metrics collected by the downloader. - -package downloader - -import ( - "github.com/ethereum/go-ethereum/metrics" -) - -var ( - headerInMeter = metrics.NewRegisteredMeter("eth/downloader/headers/in", nil) - headerReqTimer = metrics.NewRegisteredTimer("eth/downloader/headers/req", nil) - headerDropMeter = metrics.NewRegisteredMeter("eth/downloader/headers/drop", nil) - headerTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/headers/timeout", nil) - - bodyInMeter = metrics.NewRegisteredMeter("eth/downloader/bodies/in", nil) - bodyReqTimer = metrics.NewRegisteredTimer("eth/downloader/bodies/req", nil) - bodyDropMeter = metrics.NewRegisteredMeter("eth/downloader/bodies/drop", nil) - bodyTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/bodies/timeout", nil) - - receiptInMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/in", nil) - receiptReqTimer = metrics.NewRegisteredTimer("eth/downloader/receipts/req", nil) - receiptDropMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/drop", nil) - receiptTimeoutMeter = metrics.NewRegisteredMeter("eth/downloader/receipts/timeout", nil) - - throttleCounter = metrics.NewRegisteredCounter("eth/downloader/throttle", nil) -) diff --git a/eth/downloader/modes.go b/eth/downloader/modes.go deleted file mode 100644 index d388b9ee4d..0000000000 --- a/eth/downloader/modes.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import "fmt" - -// SyncMode represents the synchronisation mode of the downloader. -// It is a uint32 as it is used with atomic operations. -type SyncMode uint32 - -const ( - FullSync SyncMode = iota // Synchronise the entire blockchain history from full blocks - SnapSync // Download the chain and the state via compact snapshots - LightSync // Download only the headers and terminate afterwards -) - -func (mode SyncMode) IsValid() bool { - return mode >= FullSync && mode <= LightSync -} - -// String implements the stringer interface. -func (mode SyncMode) String() string { - switch mode { - case FullSync: - return "full" - case SnapSync: - return "snap" - case LightSync: - return "light" - default: - return "unknown" - } -} - -func (mode SyncMode) MarshalText() ([]byte, error) { - switch mode { - case FullSync: - return []byte("full"), nil - case SnapSync: - return []byte("snap"), nil - case LightSync: - return []byte("light"), nil - default: - return nil, fmt.Errorf("unknown sync mode %d", mode) - } -} - -func (mode *SyncMode) UnmarshalText(text []byte) error { - switch string(text) { - case "full": - *mode = FullSync - case "snap": - *mode = SnapSync - case "light": - *mode = LightSync - default: - return fmt.Errorf(`unknown sync mode %q, want "full", "snap" or "light"`, text) - } - return nil -} diff --git a/eth/downloader/peer.go b/eth/downloader/peer.go deleted file mode 100644 index 4c43af5270..0000000000 --- a/eth/downloader/peer.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Contains the active peer-set of the downloader, maintaining both failures -// as well as reputation metrics to prioritize the block retrievals. - -package downloader - -import ( - "errors" - "math/big" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p/msgrate" -) - -const ( - maxLackingHashes = 4096 // Maximum number of entries allowed on the list or lacking items -) - -var ( - errAlreadyRegistered = errors.New("peer is already registered") - errNotRegistered = errors.New("peer is not registered") -) - -// peerConnection represents an active peer from which hashes and blocks are retrieved. -type peerConnection struct { - id string // Unique identifier of the peer - - rates *msgrate.Tracker // Tracker to hone in on the number of items retrievable per second - lacking map[common.Hash]struct{} // Set of hashes not to request (didn't have previously) - - peer Peer - - version uint // Eth protocol version number to switch strategies - log log.Logger // Contextual logger to add extra infos to peer logs - lock sync.RWMutex -} - -// Peer encapsulates the methods required to synchronise with a remote full peer. -type Peer interface { - Head() (common.Hash, *big.Int) - RequestHeadersByHash(common.Hash, int, int, bool, chan *eth.Response) (*eth.Request, error) - RequestHeadersByNumber(uint64, int, int, bool, chan *eth.Response) (*eth.Request, error) - - RequestBodies([]common.Hash, chan *eth.Response) (*eth.Request, error) - RequestReceipts([]common.Hash, chan *eth.Response) (*eth.Request, error) -} - -// newPeerConnection creates a new downloader peer. -func newPeerConnection(id string, version uint, peer Peer, logger log.Logger) *peerConnection { - return &peerConnection{ - id: id, - lacking: make(map[common.Hash]struct{}), - peer: peer, - version: version, - log: logger, - } -} - -// Reset clears the internal state of a peer entity. -func (p *peerConnection) Reset() { - p.lock.Lock() - defer p.lock.Unlock() - - p.lacking = make(map[common.Hash]struct{}) -} - -// UpdateHeaderRate updates the peer's estimated header retrieval throughput with -// the current measurement. -func (p *peerConnection) UpdateHeaderRate(delivered int, elapsed time.Duration) { - p.rates.Update(eth.BlockHeadersMsg, elapsed, delivered) -} - -// UpdateBodyRate updates the peer's estimated body retrieval throughput with the -// current measurement. -func (p *peerConnection) UpdateBodyRate(delivered int, elapsed time.Duration) { - p.rates.Update(eth.BlockBodiesMsg, elapsed, delivered) -} - -// UpdateReceiptRate updates the peer's estimated receipt retrieval throughput -// with the current measurement. -func (p *peerConnection) UpdateReceiptRate(delivered int, elapsed time.Duration) { - p.rates.Update(eth.ReceiptsMsg, elapsed, delivered) -} - -// HeaderCapacity retrieves the peer's header download allowance based on its -// previously discovered throughput. -func (p *peerConnection) HeaderCapacity(targetRTT time.Duration) int { - cap := p.rates.Capacity(eth.BlockHeadersMsg, targetRTT) - if cap > MaxHeaderFetch { - cap = MaxHeaderFetch - } - return cap -} - -// BodyCapacity retrieves the peer's body download allowance based on its -// previously discovered throughput. -func (p *peerConnection) BodyCapacity(targetRTT time.Duration) int { - cap := p.rates.Capacity(eth.BlockBodiesMsg, targetRTT) - if cap > MaxBlockFetch { - cap = MaxBlockFetch - } - return cap -} - -// ReceiptCapacity retrieves the peers receipt download allowance based on its -// previously discovered throughput. -func (p *peerConnection) ReceiptCapacity(targetRTT time.Duration) int { - cap := p.rates.Capacity(eth.ReceiptsMsg, targetRTT) - if cap > MaxReceiptFetch { - cap = MaxReceiptFetch - } - return cap -} - -// MarkLacking appends a new entity to the set of items (blocks, receipts, states) -// that a peer is known not to have (i.e. have been requested before). If the -// set reaches its maximum allowed capacity, items are randomly dropped off. -func (p *peerConnection) MarkLacking(hash common.Hash) { - p.lock.Lock() - defer p.lock.Unlock() - - for len(p.lacking) >= maxLackingHashes { - for drop := range p.lacking { - delete(p.lacking, drop) - break - } - } - p.lacking[hash] = struct{}{} -} - -// Lacks retrieves whether the hash of a blockchain item is on the peers lacking -// list (i.e. whether we know that the peer does not have it). -func (p *peerConnection) Lacks(hash common.Hash) bool { - p.lock.RLock() - defer p.lock.RUnlock() - - _, ok := p.lacking[hash] - return ok -} - -// peeringEvent is sent on the peer event feed when a remote peer connects or -// disconnects. -type peeringEvent struct { - peer *peerConnection - join bool -} - -// peerSet represents the collection of active peer participating in the chain -// download procedure. -type peerSet struct { - peers map[string]*peerConnection - rates *msgrate.Trackers // Set of rate trackers to give the sync a common beat - events event.Feed // Feed to publish peer lifecycle events on - - lock sync.RWMutex -} - -// newPeerSet creates a new peer set top track the active download sources. -func newPeerSet() *peerSet { - return &peerSet{ - peers: make(map[string]*peerConnection), - rates: msgrate.NewTrackers(log.New("proto", "eth")), - } -} - -// SubscribeEvents subscribes to peer arrival and departure events. -func (ps *peerSet) SubscribeEvents(ch chan<- *peeringEvent) event.Subscription { - return ps.events.Subscribe(ch) -} - -// Reset iterates over the current peer set, and resets each of the known peers -// to prepare for a next batch of block retrieval. -func (ps *peerSet) Reset() { - ps.lock.RLock() - defer ps.lock.RUnlock() - - for _, peer := range ps.peers { - peer.Reset() - } -} - -// Register injects a new peer into the working set, or returns an error if the -// peer is already known. -// -// The method also sets the starting throughput values of the new peer to the -// average of all existing peers, to give it a realistic chance of being used -// for data retrievals. -func (ps *peerSet) Register(p *peerConnection) error { - // Register the new peer with some meaningful defaults - ps.lock.Lock() - if _, ok := ps.peers[p.id]; ok { - ps.lock.Unlock() - return errAlreadyRegistered - } - p.rates = msgrate.NewTracker(ps.rates.MeanCapacities(), ps.rates.MedianRoundTrip()) - if err := ps.rates.Track(p.id, p.rates); err != nil { - ps.lock.Unlock() - return err - } - ps.peers[p.id] = p - ps.lock.Unlock() - - ps.events.Send(&peeringEvent{peer: p, join: true}) - return nil -} - -// Unregister removes a remote peer from the active set, disabling any further -// actions to/from that particular entity. -func (ps *peerSet) Unregister(id string) error { - ps.lock.Lock() - p, ok := ps.peers[id] - if !ok { - ps.lock.Unlock() - return errNotRegistered - } - delete(ps.peers, id) - ps.rates.Untrack(id) - ps.lock.Unlock() - - ps.events.Send(&peeringEvent{peer: p, join: false}) - return nil -} - -// Peer retrieves the registered peer with the given id. -func (ps *peerSet) Peer(id string) *peerConnection { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return ps.peers[id] -} - -// Len returns if the current number of peers in the set. -func (ps *peerSet) Len() int { - ps.lock.RLock() - defer ps.lock.RUnlock() - - return len(ps.peers) -} - -// AllPeers retrieves a flat list of all the peers within the set. -func (ps *peerSet) AllPeers() []*peerConnection { - ps.lock.RLock() - defer ps.lock.RUnlock() - - list := make([]*peerConnection, 0, len(ps.peers)) - for _, p := range ps.peers { - list = append(list, p) - } - return list -} - -// peerCapacitySort implements sort.Interface. -// It sorts peer connections by capacity (descending). -type peerCapacitySort struct { - peers []*peerConnection - caps []int -} - -func (ps *peerCapacitySort) Len() int { - return len(ps.peers) -} - -func (ps *peerCapacitySort) Less(i, j int) bool { - return ps.caps[i] > ps.caps[j] -} - -func (ps *peerCapacitySort) Swap(i, j int) { - ps.peers[i], ps.peers[j] = ps.peers[j], ps.peers[i] - ps.caps[i], ps.caps[j] = ps.caps[j], ps.caps[i] -} diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go deleted file mode 100644 index e557158797..0000000000 --- a/eth/downloader/queue.go +++ /dev/null @@ -1,956 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Contains the block download scheduler to collect download tasks and schedule -// them in an ordered, and throttled way. - -package downloader - -import ( - "errors" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/prque" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/metrics" - "github.com/ethereum/go-ethereum/params" -) - -const ( - bodyType = uint(0) - receiptType = uint(1) -) - -var ( - blockCacheMaxItems = 8192 // Maximum number of blocks to cache before throttling the download - blockCacheInitialItems = 2048 // Initial number of blocks to start fetching, before we know the sizes of the blocks - blockCacheMemory = 256 * 1024 * 1024 // Maximum amount of memory to use for block caching - blockCacheSizeWeight = 0.1 // Multiplier to approximate the average block size based on past ones -) - -var ( - errNoFetchesPending = errors.New("no fetches pending") - errStaleDelivery = errors.New("stale delivery") -) - -// fetchRequest is a currently running data retrieval operation. -type fetchRequest struct { - Peer *peerConnection // Peer to which the request was sent - From uint64 // Requested chain element index (used for skeleton fills only) - Headers []*types.Header // Requested headers, sorted by request order - Time time.Time // Time when the request was made -} - -// fetchResult is a struct collecting partial results from data fetchers until -// all outstanding pieces complete and the result as a whole can be processed. -type fetchResult struct { - pending atomic.Int32 // Flag telling what deliveries are outstanding - - Header *types.Header - Uncles []*types.Header - Transactions types.Transactions - Receipts types.Receipts - Withdrawals types.Withdrawals -} - -func newFetchResult(header *types.Header, fastSync bool) *fetchResult { - item := &fetchResult{ - Header: header, - } - if !header.EmptyBody() { - item.pending.Store(item.pending.Load() | (1 << bodyType)) - } else if header.WithdrawalsHash != nil { - item.Withdrawals = make(types.Withdrawals, 0) - } - if fastSync && !header.EmptyReceipts() { - item.pending.Store(item.pending.Load() | (1 << receiptType)) - } - return item -} - -// SetBodyDone flags the body as finished. -func (f *fetchResult) SetBodyDone() { - if v := f.pending.Load(); (v & (1 << bodyType)) != 0 { - f.pending.Add(-1) - } -} - -// AllDone checks if item is done. -func (f *fetchResult) AllDone() bool { - return f.pending.Load() == 0 -} - -// SetReceiptsDone flags the receipts as finished. -func (f *fetchResult) SetReceiptsDone() { - if v := f.pending.Load(); (v & (1 << receiptType)) != 0 { - f.pending.Add(-2) - } -} - -// Done checks if the given type is done already -func (f *fetchResult) Done(kind uint) bool { - v := f.pending.Load() - return v&(1< 0 -} - -// InFlightReceipts retrieves whether there are receipt fetch requests currently -// in flight. -func (q *queue) InFlightReceipts() bool { - q.lock.Lock() - defer q.lock.Unlock() - - return len(q.receiptPendPool) > 0 -} - -// Idle returns if the queue is fully idle or has some data still inside. -func (q *queue) Idle() bool { - q.lock.Lock() - defer q.lock.Unlock() - - queued := q.blockTaskQueue.Size() + q.receiptTaskQueue.Size() - pending := len(q.blockPendPool) + len(q.receiptPendPool) - - return (queued + pending) == 0 -} - -// ScheduleSkeleton adds a batch of header retrieval tasks to the queue to fill -// up an already retrieved header skeleton. -func (q *queue) ScheduleSkeleton(from uint64, skeleton []*types.Header) { - q.lock.Lock() - defer q.lock.Unlock() - - // No skeleton retrieval can be in progress, fail hard if so (huge implementation bug) - if q.headerResults != nil { - panic("skeleton assembly already in progress") - } - // Schedule all the header retrieval tasks for the skeleton assembly - q.headerTaskPool = make(map[uint64]*types.Header) - q.headerTaskQueue = prque.New[int64, uint64](nil) - q.headerPeerMiss = make(map[string]map[uint64]struct{}) // Reset availability to correct invalid chains - q.headerResults = make([]*types.Header, len(skeleton)*MaxHeaderFetch) - q.headerHashes = make([]common.Hash, len(skeleton)*MaxHeaderFetch) - q.headerProced = 0 - q.headerOffset = from - q.headerContCh = make(chan bool, 1) - - for i, header := range skeleton { - index := from + uint64(i*MaxHeaderFetch) - - q.headerTaskPool[index] = header - q.headerTaskQueue.Push(index, -int64(index)) - } -} - -// RetrieveHeaders retrieves the header chain assemble based on the scheduled -// skeleton. -func (q *queue) RetrieveHeaders() ([]*types.Header, []common.Hash, int) { - q.lock.Lock() - defer q.lock.Unlock() - - headers, hashes, proced := q.headerResults, q.headerHashes, q.headerProced - q.headerResults, q.headerHashes, q.headerProced = nil, nil, 0 - - return headers, hashes, proced -} - -// Schedule adds a set of headers for the download queue for scheduling, returning -// the new headers encountered. -func (q *queue) Schedule(headers []*types.Header, hashes []common.Hash, from uint64) []*types.Header { - q.lock.Lock() - defer q.lock.Unlock() - - // Insert all the headers prioritised by the contained block number - inserts := make([]*types.Header, 0, len(headers)) - for i, header := range headers { - // Make sure chain order is honoured and preserved throughout - hash := hashes[i] - if header.Number == nil || header.Number.Uint64() != from { - log.Warn("Header broke chain ordering", "number", header.Number, "hash", hash, "expected", from) - break - } - if q.headerHead != (common.Hash{}) && q.headerHead != header.ParentHash { - log.Warn("Header broke chain ancestry", "number", header.Number, "hash", hash) - break - } - // Make sure no duplicate requests are executed - // We cannot skip this, even if the block is empty, since this is - // what triggers the fetchResult creation. - if _, ok := q.blockTaskPool[hash]; ok { - log.Warn("Header already scheduled for block fetch", "number", header.Number, "hash", hash) - } else { - q.blockTaskPool[hash] = header - q.blockTaskQueue.Push(header, -int64(header.Number.Uint64())) - } - // Queue for receipt retrieval - if q.mode == SnapSync && !header.EmptyReceipts() { - if _, ok := q.receiptTaskPool[hash]; ok { - log.Warn("Header already scheduled for receipt fetch", "number", header.Number, "hash", hash) - } else { - q.receiptTaskPool[hash] = header - q.receiptTaskQueue.Push(header, -int64(header.Number.Uint64())) - } - } - inserts = append(inserts, header) - q.headerHead = hash - from++ - } - return inserts -} - -// Results retrieves and permanently removes a batch of fetch results from -// the cache. the result slice will be empty if the queue has been closed. -// Results can be called concurrently with Deliver and Schedule, -// but assumes that there are not two simultaneous callers to Results -func (q *queue) Results(block bool) []*fetchResult { - // Abort early if there are no items and non-blocking requested - if !block && !q.resultCache.HasCompletedItems() { - return nil - } - closed := false - for !closed && !q.resultCache.HasCompletedItems() { - // In order to wait on 'active', we need to obtain the lock. - // That may take a while, if someone is delivering at the same - // time, so after obtaining the lock, we check again if there - // are any results to fetch. - // Also, in-between we ask for the lock and the lock is obtained, - // someone can have closed the queue. In that case, we should - // return the available results and stop blocking - q.lock.Lock() - if q.resultCache.HasCompletedItems() || q.closed { - q.lock.Unlock() - break - } - // No items available, and not closed - q.active.Wait() - closed = q.closed - q.lock.Unlock() - } - // Regardless if closed or not, we can still deliver whatever we have - results := q.resultCache.GetCompleted(maxResultsProcess) - for _, result := range results { - // Recalculate the result item weights to prevent memory exhaustion - size := result.Header.Size() - for _, uncle := range result.Uncles { - size += uncle.Size() - } - for _, receipt := range result.Receipts { - size += receipt.Size() - } - for _, tx := range result.Transactions { - size += common.StorageSize(tx.Size()) - } - q.resultSize = common.StorageSize(blockCacheSizeWeight)*size + - (1-common.StorageSize(blockCacheSizeWeight))*q.resultSize - } - // Using the newly calibrated resultsize, figure out the new throttle limit - // on the result cache - throttleThreshold := uint64((common.StorageSize(blockCacheMemory) + q.resultSize - 1) / q.resultSize) - throttleThreshold = q.resultCache.SetThrottleThreshold(throttleThreshold) - - // With results removed from the cache, wake throttled fetchers - for _, ch := range []chan bool{q.blockWakeCh, q.receiptWakeCh} { - select { - case ch <- true: - default: - } - } - // Log some info at certain times - if time.Since(q.logTime) >= 60*time.Second { - q.logTime = time.Now() - - info := q.Stats() - info = append(info, "throttle", throttleThreshold) - log.Debug("Downloader queue stats", info...) - } - return results -} - -func (q *queue) Stats() []interface{} { - q.lock.RLock() - defer q.lock.RUnlock() - - return q.stats() -} - -func (q *queue) stats() []interface{} { - return []interface{}{ - "receiptTasks", q.receiptTaskQueue.Size(), - "blockTasks", q.blockTaskQueue.Size(), - "itemSize", q.resultSize, - } -} - -// ReserveHeaders reserves a set of headers for the given peer, skipping any -// previously failed batches. -func (q *queue) ReserveHeaders(p *peerConnection, count int) *fetchRequest { - q.lock.Lock() - defer q.lock.Unlock() - - // Short circuit if the peer's already downloading something (sanity check to - // not corrupt state) - if _, ok := q.headerPendPool[p.id]; ok { - return nil - } - // Retrieve a batch of hashes, skipping previously failed ones - send, skip := uint64(0), []uint64{} - for send == 0 && !q.headerTaskQueue.Empty() { - from, _ := q.headerTaskQueue.Pop() - if q.headerPeerMiss[p.id] != nil { - if _, ok := q.headerPeerMiss[p.id][from]; ok { - skip = append(skip, from) - continue - } - } - send = from - } - // Merge all the skipped batches back - for _, from := range skip { - q.headerTaskQueue.Push(from, -int64(from)) - } - // Assemble and return the block download request - if send == 0 { - return nil - } - request := &fetchRequest{ - Peer: p, - From: send, - Time: time.Now(), - } - q.headerPendPool[p.id] = request - return request -} - -// ReserveBodies reserves a set of body fetches for the given peer, skipping any -// previously failed downloads. Beside the next batch of needed fetches, it also -// returns a flag whether empty blocks were queued requiring processing. -func (q *queue) ReserveBodies(p *peerConnection, count int) (*fetchRequest, bool, bool) { - q.lock.Lock() - defer q.lock.Unlock() - - return q.reserveHeaders(p, count, q.blockTaskPool, q.blockTaskQueue, q.blockPendPool, bodyType) -} - -// ReserveReceipts reserves a set of receipt fetches for the given peer, skipping -// any previously failed downloads. Beside the next batch of needed fetches, it -// also returns a flag whether empty receipts were queued requiring importing. -func (q *queue) ReserveReceipts(p *peerConnection, count int) (*fetchRequest, bool, bool) { - q.lock.Lock() - defer q.lock.Unlock() - - return q.reserveHeaders(p, count, q.receiptTaskPool, q.receiptTaskQueue, q.receiptPendPool, receiptType) -} - -// reserveHeaders reserves a set of data download operations for a given peer, -// skipping any previously failed ones. This method is a generic version used -// by the individual special reservation functions. -// -// Note, this method expects the queue lock to be already held for writing. The -// reason the lock is not obtained in here is because the parameters already need -// to access the queue, so they already need a lock anyway. -// -// Returns: -// -// item - the fetchRequest -// progress - whether any progress was made -// throttle - if the caller should throttle for a while -func (q *queue) reserveHeaders(p *peerConnection, count int, taskPool map[common.Hash]*types.Header, taskQueue *prque.Prque[int64, *types.Header], - pendPool map[string]*fetchRequest, kind uint) (*fetchRequest, bool, bool) { - // Short circuit if the pool has been depleted, or if the peer's already - // downloading something (sanity check not to corrupt state) - if taskQueue.Empty() { - return nil, false, true - } - if _, ok := pendPool[p.id]; ok { - return nil, false, false - } - // Retrieve a batch of tasks, skipping previously failed ones - send := make([]*types.Header, 0, count) - skip := make([]*types.Header, 0) - progress := false - throttled := false - for proc := 0; len(send) < count && !taskQueue.Empty(); proc++ { - // the task queue will pop items in order, so the highest prio block - // is also the lowest block number. - header, _ := taskQueue.Peek() - - // we can ask the resultcache if this header is within the - // "prioritized" segment of blocks. If it is not, we need to throttle - - stale, throttle, item, err := q.resultCache.AddFetch(header, q.mode == SnapSync) - if stale { - // Don't put back in the task queue, this item has already been - // delivered upstream - taskQueue.PopItem() - progress = true - delete(taskPool, header.Hash()) - proc = proc - 1 - log.Error("Fetch reservation already delivered", "number", header.Number.Uint64()) - continue - } - if throttle { - // There are no resultslots available. Leave it in the task queue - // However, if there are any left as 'skipped', we should not tell - // the caller to throttle, since we still want some other - // peer to fetch those for us - throttled = len(skip) == 0 - break - } - if err != nil { - // this most definitely should _not_ happen - log.Warn("Failed to reserve headers", "err", err) - // There are no resultslots available. Leave it in the task queue - break - } - if item.Done(kind) { - // If it's a noop, we can skip this task - delete(taskPool, header.Hash()) - taskQueue.PopItem() - proc = proc - 1 - progress = true - continue - } - // Remove it from the task queue - taskQueue.PopItem() - // Otherwise unless the peer is known not to have the data, add to the retrieve list - if p.Lacks(header.Hash()) { - skip = append(skip, header) - } else { - send = append(send, header) - } - } - // Merge all the skipped headers back - for _, header := range skip { - taskQueue.Push(header, -int64(header.Number.Uint64())) - } - if q.resultCache.HasCompletedItems() { - // Wake Results, resultCache was modified - q.active.Signal() - } - // Assemble and return the block download request - if len(send) == 0 { - return nil, progress, throttled - } - request := &fetchRequest{ - Peer: p, - Headers: send, - Time: time.Now(), - } - pendPool[p.id] = request - return request, progress, throttled -} - -// Revoke cancels all pending requests belonging to a given peer. This method is -// meant to be called during a peer drop to quickly reassign owned data fetches -// to remaining nodes. -func (q *queue) Revoke(peerID string) { - q.lock.Lock() - defer q.lock.Unlock() - - if request, ok := q.headerPendPool[peerID]; ok { - q.headerTaskQueue.Push(request.From, -int64(request.From)) - delete(q.headerPendPool, peerID) - } - if request, ok := q.blockPendPool[peerID]; ok { - for _, header := range request.Headers { - q.blockTaskQueue.Push(header, -int64(header.Number.Uint64())) - } - delete(q.blockPendPool, peerID) - } - if request, ok := q.receiptPendPool[peerID]; ok { - for _, header := range request.Headers { - q.receiptTaskQueue.Push(header, -int64(header.Number.Uint64())) - } - delete(q.receiptPendPool, peerID) - } -} - -// ExpireHeaders cancels a request that timed out and moves the pending fetch -// task back into the queue for rescheduling. -func (q *queue) ExpireHeaders(peer string) int { - q.lock.Lock() - defer q.lock.Unlock() - - headerTimeoutMeter.Mark(1) - return q.expire(peer, q.headerPendPool, q.headerTaskQueue) -} - -// ExpireBodies checks for in flight block body requests that exceeded a timeout -// allowance, canceling them and returning the responsible peers for penalisation. -func (q *queue) ExpireBodies(peer string) int { - q.lock.Lock() - defer q.lock.Unlock() - - bodyTimeoutMeter.Mark(1) - return q.expire(peer, q.blockPendPool, q.blockTaskQueue) -} - -// ExpireReceipts checks for in flight receipt requests that exceeded a timeout -// allowance, canceling them and returning the responsible peers for penalisation. -func (q *queue) ExpireReceipts(peer string) int { - q.lock.Lock() - defer q.lock.Unlock() - - receiptTimeoutMeter.Mark(1) - return q.expire(peer, q.receiptPendPool, q.receiptTaskQueue) -} - -// expire is the generic check that moves a specific expired task from a pending -// pool back into a task pool. The syntax on the passed taskQueue is a bit weird -// as we would need a generic expire method to handle both types, but that is not -// supported at the moment at least (Go 1.19). -// -// Note, this method expects the queue lock to be already held. The reason the -// lock is not obtained in here is that the parameters already need to access -// the queue, so they already need a lock anyway. -func (q *queue) expire(peer string, pendPool map[string]*fetchRequest, taskQueue interface{}) int { - // Retrieve the request being expired and log an error if it's non-existent, - // as there's no order of events that should lead to such expirations. - req := pendPool[peer] - if req == nil { - log.Error("Expired request does not exist", "peer", peer) - return 0 - } - delete(pendPool, peer) - - // Return any non-satisfied requests to the pool - if req.From > 0 { - taskQueue.(*prque.Prque[int64, uint64]).Push(req.From, -int64(req.From)) - } - for _, header := range req.Headers { - taskQueue.(*prque.Prque[int64, *types.Header]).Push(header, -int64(header.Number.Uint64())) - } - return len(req.Headers) -} - -// DeliverHeaders injects a header retrieval response into the header results -// cache. This method either accepts all headers it received, or none of them -// if they do not map correctly to the skeleton. -// -// If the headers are accepted, the method makes an attempt to deliver the set -// of ready headers to the processor to keep the pipeline full. However, it will -// not block to prevent stalling other pending deliveries. -func (q *queue) DeliverHeaders(id string, headers []*types.Header, hashes []common.Hash, headerProcCh chan *headerTask) (int, error) { - q.lock.Lock() - defer q.lock.Unlock() - - var logger log.Logger - if len(id) < 16 { - // Tests use short IDs, don't choke on them - logger = log.New("peer", id) - } else { - logger = log.New("peer", id[:16]) - } - // Short circuit if the data was never requested - request := q.headerPendPool[id] - if request == nil { - headerDropMeter.Mark(int64(len(headers))) - return 0, errNoFetchesPending - } - delete(q.headerPendPool, id) - - headerReqTimer.UpdateSince(request.Time) - headerInMeter.Mark(int64(len(headers))) - - // Ensure headers can be mapped onto the skeleton chain - target := q.headerTaskPool[request.From].Hash() - - accepted := len(headers) == MaxHeaderFetch - if accepted { - if headers[0].Number.Uint64() != request.From { - logger.Trace("First header broke chain ordering", "number", headers[0].Number, "hash", hashes[0], "expected", request.From) - accepted = false - } else if hashes[len(headers)-1] != target { - logger.Trace("Last header broke skeleton structure ", "number", headers[len(headers)-1].Number, "hash", hashes[len(headers)-1], "expected", target) - accepted = false - } - } - if accepted { - parentHash := hashes[0] - for i, header := range headers[1:] { - hash := hashes[i+1] - if want := request.From + 1 + uint64(i); header.Number.Uint64() != want { - logger.Warn("Header broke chain ordering", "number", header.Number, "hash", hash, "expected", want) - accepted = false - break - } - if parentHash != header.ParentHash { - logger.Warn("Header broke chain ancestry", "number", header.Number, "hash", hash) - accepted = false - break - } - // Set-up parent hash for next round - parentHash = hash - } - } - // If the batch of headers wasn't accepted, mark as unavailable - if !accepted { - logger.Trace("Skeleton filling not accepted", "from", request.From) - headerDropMeter.Mark(int64(len(headers))) - - miss := q.headerPeerMiss[id] - if miss == nil { - q.headerPeerMiss[id] = make(map[uint64]struct{}) - miss = q.headerPeerMiss[id] - } - miss[request.From] = struct{}{} - - q.headerTaskQueue.Push(request.From, -int64(request.From)) - return 0, errors.New("delivery not accepted") - } - // Clean up a successful fetch and try to deliver any sub-results - copy(q.headerResults[request.From-q.headerOffset:], headers) - copy(q.headerHashes[request.From-q.headerOffset:], hashes) - - delete(q.headerTaskPool, request.From) - - ready := 0 - for q.headerProced+ready < len(q.headerResults) && q.headerResults[q.headerProced+ready] != nil { - ready += MaxHeaderFetch - } - if ready > 0 { - // Headers are ready for delivery, gather them and push forward (non blocking) - processHeaders := make([]*types.Header, ready) - copy(processHeaders, q.headerResults[q.headerProced:q.headerProced+ready]) - - processHashes := make([]common.Hash, ready) - copy(processHashes, q.headerHashes[q.headerProced:q.headerProced+ready]) - - select { - case headerProcCh <- &headerTask{ - headers: processHeaders, - hashes: processHashes, - }: - logger.Trace("Pre-scheduled new headers", "count", len(processHeaders), "from", processHeaders[0].Number) - q.headerProced += len(processHeaders) - default: - } - } - // Check for termination and return - if len(q.headerTaskPool) == 0 { - q.headerContCh <- false - } - return len(headers), nil -} - -// DeliverBodies injects a block body retrieval response into the results queue. -// The method returns the number of blocks bodies accepted from the delivery and -// also wakes any threads waiting for data delivery. -func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListHashes []common.Hash, - uncleLists [][]*types.Header, uncleListHashes []common.Hash, - withdrawalLists [][]*types.Withdrawal, withdrawalListHashes []common.Hash) (int, error) { - q.lock.Lock() - defer q.lock.Unlock() - - validate := func(index int, header *types.Header) error { - if txListHashes[index] != header.TxHash { - return errInvalidBody - } - if uncleListHashes[index] != header.UncleHash { - return errInvalidBody - } - if header.WithdrawalsHash == nil { - // nil hash means that withdrawals should not be present in body - if withdrawalLists[index] != nil { - return errInvalidBody - } - } else { // non-nil hash: body must have withdrawals - if withdrawalLists[index] == nil { - return errInvalidBody - } - if withdrawalListHashes[index] != *header.WithdrawalsHash { - return errInvalidBody - } - } - // Blocks must have a number of blobs corresponding to the header gas usage, - // and zero before the Cancun hardfork. - var blobs int - for _, tx := range txLists[index] { - // Count the number of blobs to validate against the header's blobGasUsed - blobs += len(tx.BlobHashes()) - - // Validate the data blobs individually too - if tx.Type() == types.BlobTxType { - if len(tx.BlobHashes()) == 0 { - return errInvalidBody - } - for _, hash := range tx.BlobHashes() { - if hash[0] != params.BlobTxHashVersion { - return errInvalidBody - } - } - if tx.BlobTxSidecar() != nil { - return errInvalidBody - } - } - } - if header.BlobGasUsed != nil { - if want := *header.BlobGasUsed / params.BlobTxBlobGasPerBlob; uint64(blobs) != want { // div because the header is surely good vs the body might be bloated - return errInvalidBody - } - } else { - if blobs != 0 { - return errInvalidBody - } - } - return nil - } - - reconstruct := func(index int, result *fetchResult) { - result.Transactions = txLists[index] - result.Uncles = uncleLists[index] - result.Withdrawals = withdrawalLists[index] - result.SetBodyDone() - } - return q.deliver(id, q.blockTaskPool, q.blockTaskQueue, q.blockPendPool, - bodyReqTimer, bodyInMeter, bodyDropMeter, len(txLists), validate, reconstruct) -} - -// DeliverReceipts injects a receipt retrieval response into the results queue. -// The method returns the number of transaction receipts accepted from the delivery -// and also wakes any threads waiting for data delivery. -func (q *queue) DeliverReceipts(id string, receiptList [][]*types.Receipt, receiptListHashes []common.Hash) (int, error) { - q.lock.Lock() - defer q.lock.Unlock() - - validate := func(index int, header *types.Header) error { - if receiptListHashes[index] != header.ReceiptHash { - return errInvalidReceipt - } - return nil - } - reconstruct := func(index int, result *fetchResult) { - result.Receipts = receiptList[index] - result.SetReceiptsDone() - } - return q.deliver(id, q.receiptTaskPool, q.receiptTaskQueue, q.receiptPendPool, - receiptReqTimer, receiptInMeter, receiptDropMeter, len(receiptList), validate, reconstruct) -} - -// deliver injects a data retrieval response into the results queue. -// -// Note, this method expects the queue lock to be already held for writing. The -// reason this lock is not obtained in here is because the parameters already need -// to access the queue, so they already need a lock anyway. -func (q *queue) deliver(id string, taskPool map[common.Hash]*types.Header, - taskQueue *prque.Prque[int64, *types.Header], pendPool map[string]*fetchRequest, - reqTimer metrics.Timer, resInMeter metrics.Meter, resDropMeter metrics.Meter, - results int, validate func(index int, header *types.Header) error, - reconstruct func(index int, result *fetchResult)) (int, error) { - // Short circuit if the data was never requested - request := pendPool[id] - if request == nil { - resDropMeter.Mark(int64(results)) - return 0, errNoFetchesPending - } - delete(pendPool, id) - - reqTimer.UpdateSince(request.Time) - resInMeter.Mark(int64(results)) - - // If no data items were retrieved, mark them as unavailable for the origin peer - if results == 0 { - for _, header := range request.Headers { - request.Peer.MarkLacking(header.Hash()) - } - } - // Assemble each of the results with their headers and retrieved data parts - var ( - accepted int - failure error - i int - hashes []common.Hash - ) - for _, header := range request.Headers { - // Short circuit assembly if no more fetch results are found - if i >= results { - break - } - // Validate the fields - if err := validate(i, header); err != nil { - failure = err - break - } - hashes = append(hashes, header.Hash()) - i++ - } - - for _, header := range request.Headers[:i] { - if res, stale, err := q.resultCache.GetDeliverySlot(header.Number.Uint64()); err == nil && !stale { - reconstruct(accepted, res) - } else { - // else: between here and above, some other peer filled this result, - // or it was indeed a no-op. This should not happen, but if it does it's - // not something to panic about - log.Error("Delivery stale", "stale", stale, "number", header.Number.Uint64(), "err", err) - failure = errStaleDelivery - } - // Clean up a successful fetch - delete(taskPool, hashes[accepted]) - accepted++ - } - resDropMeter.Mark(int64(results - accepted)) - - // Return all failed or missing fetches to the queue - for _, header := range request.Headers[accepted:] { - taskQueue.Push(header, -int64(header.Number.Uint64())) - } - // Wake up Results - if accepted > 0 { - q.active.Signal() - } - if failure == nil { - return accepted, nil - } - // If none of the data was good, it's a stale delivery - if accepted > 0 { - return accepted, fmt.Errorf("partial failure: %v", failure) - } - return accepted, fmt.Errorf("%w: %v", failure, errStaleDelivery) -} - -// Prepare configures the result cache to allow accepting and caching inbound -// fetch results. -func (q *queue) Prepare(offset uint64, mode SyncMode) { - q.lock.Lock() - defer q.lock.Unlock() - - // Prepare the queue for sync results - q.resultCache.Prepare(offset) - q.mode = mode -} diff --git a/eth/downloader/queue_test.go b/eth/downloader/queue_test.go deleted file mode 100644 index 50b9031a27..0000000000 --- a/eth/downloader/queue_test.go +++ /dev/null @@ -1,474 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "fmt" - "math/big" - "math/rand" - "os" - "sync" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" - "golang.org/x/exp/slog" -) - -// makeChain creates a chain of n blocks starting at and including parent. -// the returned hash chain is ordered head->parent. In addition, every 3rd block -// contains a transaction and every 5th an uncle to allow testing correct block -// reassembly. -func makeChain(n int, seed byte, parent *types.Block, empty bool) ([]*types.Block, []types.Receipts) { - blocks, receipts := core.GenerateChain(params.TestChainConfig, parent, ethash.NewFaker(), testDB, n, func(i int, block *core.BlockGen) { - block.SetCoinbase(common.Address{seed}) - // Add one tx to every secondblock - if !empty && i%2 == 0 { - signer := types.MakeSigner(params.TestChainConfig, block.Number(), block.Timestamp()) - tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddress), common.Address{seed}, big.NewInt(1000), params.TxGas, block.BaseFee(), nil), signer, testKey) - if err != nil { - panic(err) - } - block.AddTx(tx) - } - }) - return blocks, receipts -} - -type chainData struct { - blocks []*types.Block - offset int -} - -var chain *chainData -var emptyChain *chainData - -func init() { - // Create a chain of blocks to import - targetBlocks := 128 - blocks, _ := makeChain(targetBlocks, 0, testGenesis, false) - chain = &chainData{blocks, 0} - - blocks, _ = makeChain(targetBlocks, 0, testGenesis, true) - emptyChain = &chainData{blocks, 0} -} - -func (chain *chainData) headers() []*types.Header { - hdrs := make([]*types.Header, len(chain.blocks)) - for i, b := range chain.blocks { - hdrs[i] = b.Header() - } - return hdrs -} - -func (chain *chainData) Len() int { - return len(chain.blocks) -} - -func dummyPeer(id string) *peerConnection { - p := &peerConnection{ - id: id, - lacking: make(map[common.Hash]struct{}), - } - return p -} - -func TestBasics(t *testing.T) { - numOfBlocks := len(emptyChain.blocks) - numOfReceipts := len(emptyChain.blocks) / 2 - - q := newQueue(10, 10) - if !q.Idle() { - t.Errorf("new queue should be idle") - } - q.Prepare(1, SnapSync) - if res := q.Results(false); len(res) != 0 { - t.Fatal("new queue should have 0 results") - } - - // Schedule a batch of headers - headers := chain.headers() - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - q.Schedule(headers, hashes, 1) - if q.Idle() { - t.Errorf("queue should not be idle") - } - if got, exp := q.PendingBodies(), chain.Len(); got != exp { - t.Errorf("wrong pending block count, got %d, exp %d", got, exp) - } - // Only non-empty receipts get added to task-queue - if got, exp := q.PendingReceipts(), 64; got != exp { - t.Errorf("wrong pending receipt count, got %d, exp %d", got, exp) - } - // Items are now queued for downloading, next step is that we tell the - // queue that a certain peer will deliver them for us - { - peer := dummyPeer("peer-1") - fetchReq, _, throttle := q.ReserveBodies(peer, 50) - if !throttle { - // queue size is only 10, so throttling should occur - t.Fatal("should throttle") - } - // But we should still get the first things to fetch - if got, exp := len(fetchReq.Headers), 5; got != exp { - t.Fatalf("expected %d requests, got %d", exp, got) - } - if got, exp := fetchReq.Headers[0].Number.Uint64(), uint64(1); got != exp { - t.Fatalf("expected header %d, got %d", exp, got) - } - } - if exp, got := q.blockTaskQueue.Size(), numOfBlocks-10; exp != got { - t.Errorf("expected block task queue to be %d, got %d", exp, got) - } - if exp, got := q.receiptTaskQueue.Size(), numOfReceipts; exp != got { - t.Errorf("expected receipt task queue to be %d, got %d", exp, got) - } - { - peer := dummyPeer("peer-2") - fetchReq, _, throttle := q.ReserveBodies(peer, 50) - - // The second peer should hit throttling - if !throttle { - t.Fatalf("should throttle") - } - // And not get any fetches at all, since it was throttled to begin with - if fetchReq != nil { - t.Fatalf("should have no fetches, got %d", len(fetchReq.Headers)) - } - } - if exp, got := q.blockTaskQueue.Size(), numOfBlocks-10; exp != got { - t.Errorf("expected block task queue to be %d, got %d", exp, got) - } - if exp, got := q.receiptTaskQueue.Size(), numOfReceipts; exp != got { - t.Errorf("expected receipt task queue to be %d, got %d", exp, got) - } - { - // The receipt delivering peer should not be affected - // by the throttling of body deliveries - peer := dummyPeer("peer-3") - fetchReq, _, throttle := q.ReserveReceipts(peer, 50) - if !throttle { - // queue size is only 10, so throttling should occur - t.Fatal("should throttle") - } - // But we should still get the first things to fetch - if got, exp := len(fetchReq.Headers), 5; got != exp { - t.Fatalf("expected %d requests, got %d", exp, got) - } - if got, exp := fetchReq.Headers[0].Number.Uint64(), uint64(1); got != exp { - t.Fatalf("expected header %d, got %d", exp, got) - } - } - if exp, got := q.blockTaskQueue.Size(), numOfBlocks-10; exp != got { - t.Errorf("expected block task queue to be %d, got %d", exp, got) - } - if exp, got := q.receiptTaskQueue.Size(), numOfReceipts-5; exp != got { - t.Errorf("expected receipt task queue to be %d, got %d", exp, got) - } - if got, exp := q.resultCache.countCompleted(), 0; got != exp { - t.Errorf("wrong processable count, got %d, exp %d", got, exp) - } -} - -func TestEmptyBlocks(t *testing.T) { - numOfBlocks := len(emptyChain.blocks) - - q := newQueue(10, 10) - - q.Prepare(1, SnapSync) - - // Schedule a batch of headers - headers := emptyChain.headers() - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - q.Schedule(headers, hashes, 1) - if q.Idle() { - t.Errorf("queue should not be idle") - } - if got, exp := q.PendingBodies(), len(emptyChain.blocks); got != exp { - t.Errorf("wrong pending block count, got %d, exp %d", got, exp) - } - if got, exp := q.PendingReceipts(), 0; got != exp { - t.Errorf("wrong pending receipt count, got %d, exp %d", got, exp) - } - // They won't be processable, because the fetchresults haven't been - // created yet - if got, exp := q.resultCache.countCompleted(), 0; got != exp { - t.Errorf("wrong processable count, got %d, exp %d", got, exp) - } - - // Items are now queued for downloading, next step is that we tell the - // queue that a certain peer will deliver them for us - // That should trigger all of them to suddenly become 'done' - { - // Reserve blocks - peer := dummyPeer("peer-1") - fetchReq, _, _ := q.ReserveBodies(peer, 50) - - // there should be nothing to fetch, blocks are empty - if fetchReq != nil { - t.Fatal("there should be no body fetch tasks remaining") - } - } - if q.blockTaskQueue.Size() != numOfBlocks-10 { - t.Errorf("expected block task queue to be %d, got %d", numOfBlocks-10, q.blockTaskQueue.Size()) - } - if q.receiptTaskQueue.Size() != 0 { - t.Errorf("expected receipt task queue to be %d, got %d", 0, q.receiptTaskQueue.Size()) - } - { - peer := dummyPeer("peer-3") - fetchReq, _, _ := q.ReserveReceipts(peer, 50) - - // there should be nothing to fetch, blocks are empty - if fetchReq != nil { - t.Fatal("there should be no receipt fetch tasks remaining") - } - } - if q.blockTaskQueue.Size() != numOfBlocks-10 { - t.Errorf("expected block task queue to be %d, got %d", numOfBlocks-10, q.blockTaskQueue.Size()) - } - if q.receiptTaskQueue.Size() != 0 { - t.Errorf("expected receipt task queue to be %d, got %d", 0, q.receiptTaskQueue.Size()) - } - if got, exp := q.resultCache.countCompleted(), 10; got != exp { - t.Errorf("wrong processable count, got %d, exp %d", got, exp) - } -} - -// XTestDelivery does some more extensive testing of events that happen, -// blocks that become known and peers that make reservations and deliveries. -// disabled since it's not really a unit-test, but can be executed to test -// some more advanced scenarios -func XTestDelivery(t *testing.T) { - // the outside network, holding blocks - blo, rec := makeChain(128, 0, testGenesis, false) - world := newNetwork() - world.receipts = rec - world.chain = blo - world.progress(10) - if false { - log.SetDefault(log.NewLogger(slog.NewTextHandler(os.Stdout, nil))) - } - q := newQueue(10, 10) - var wg sync.WaitGroup - q.Prepare(1, SnapSync) - wg.Add(1) - go func() { - // deliver headers - defer wg.Done() - c := 1 - for { - //fmt.Printf("getting headers from %d\n", c) - headers := world.headers(c) - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - l := len(headers) - //fmt.Printf("scheduling %d headers, first %d last %d\n", - // l, headers[0].Number.Uint64(), headers[len(headers)-1].Number.Uint64()) - q.Schedule(headers, hashes, uint64(c)) - c += l - } - }() - wg.Add(1) - go func() { - // collect results - defer wg.Done() - tot := 0 - for { - res := q.Results(true) - tot += len(res) - fmt.Printf("got %d results, %d tot\n", len(res), tot) - // Now we can forget about these - world.forget(res[len(res)-1].Header.Number.Uint64()) - } - }() - wg.Add(1) - go func() { - defer wg.Done() - // reserve body fetch - i := 4 - for { - peer := dummyPeer(fmt.Sprintf("peer-%d", i)) - f, _, _ := q.ReserveBodies(peer, rand.Intn(30)) - if f != nil { - var ( - emptyList []*types.Header - txset [][]*types.Transaction - uncleset [][]*types.Header - ) - numToSkip := rand.Intn(len(f.Headers)) - for _, hdr := range f.Headers[0 : len(f.Headers)-numToSkip] { - txset = append(txset, world.getTransactions(hdr.Number.Uint64())) - uncleset = append(uncleset, emptyList) - } - var ( - txsHashes = make([]common.Hash, len(txset)) - uncleHashes = make([]common.Hash, len(uncleset)) - ) - hasher := trie.NewStackTrie(nil) - for i, txs := range txset { - txsHashes[i] = types.DeriveSha(types.Transactions(txs), hasher) - } - for i, uncles := range uncleset { - uncleHashes[i] = types.CalcUncleHash(uncles) - } - time.Sleep(100 * time.Millisecond) - _, err := q.DeliverBodies(peer.id, txset, txsHashes, uncleset, uncleHashes, nil, nil) - if err != nil { - fmt.Printf("delivered %d bodies %v\n", len(txset), err) - } - } else { - i++ - time.Sleep(200 * time.Millisecond) - } - } - }() - go func() { - defer wg.Done() - // reserve receiptfetch - peer := dummyPeer("peer-3") - for { - f, _, _ := q.ReserveReceipts(peer, rand.Intn(50)) - if f != nil { - var rcs [][]*types.Receipt - for _, hdr := range f.Headers { - rcs = append(rcs, world.getReceipts(hdr.Number.Uint64())) - } - hasher := trie.NewStackTrie(nil) - hashes := make([]common.Hash, len(rcs)) - for i, receipt := range rcs { - hashes[i] = types.DeriveSha(types.Receipts(receipt), hasher) - } - _, err := q.DeliverReceipts(peer.id, rcs, hashes) - if err != nil { - fmt.Printf("delivered %d receipts %v\n", len(rcs), err) - } - time.Sleep(100 * time.Millisecond) - } else { - time.Sleep(200 * time.Millisecond) - } - } - }() - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 50; i++ { - time.Sleep(300 * time.Millisecond) - //world.tick() - //fmt.Printf("trying to progress\n") - world.progress(rand.Intn(100)) - } - for i := 0; i < 50; i++ { - time.Sleep(2990 * time.Millisecond) - } - }() - wg.Add(1) - go func() { - defer wg.Done() - for { - time.Sleep(990 * time.Millisecond) - fmt.Printf("world block tip is %d\n", - world.chain[len(world.chain)-1].Header().Number.Uint64()) - fmt.Println(q.Stats()) - } - }() - wg.Wait() -} - -func newNetwork() *network { - var l sync.RWMutex - return &network{ - cond: sync.NewCond(&l), - offset: 1, // block 1 is at blocks[0] - } -} - -// represents the network -type network struct { - offset int - chain []*types.Block - receipts []types.Receipts - lock sync.RWMutex - cond *sync.Cond -} - -func (n *network) getTransactions(blocknum uint64) types.Transactions { - index := blocknum - uint64(n.offset) - return n.chain[index].Transactions() -} -func (n *network) getReceipts(blocknum uint64) types.Receipts { - index := blocknum - uint64(n.offset) - if got := n.chain[index].Header().Number.Uint64(); got != blocknum { - fmt.Printf("Err, got %d exp %d\n", got, blocknum) - panic("sd") - } - return n.receipts[index] -} - -func (n *network) forget(blocknum uint64) { - index := blocknum - uint64(n.offset) - n.chain = n.chain[index:] - n.receipts = n.receipts[index:] - n.offset = int(blocknum) -} -func (n *network) progress(numBlocks int) { - n.lock.Lock() - defer n.lock.Unlock() - //fmt.Printf("progressing...\n") - newBlocks, newR := makeChain(numBlocks, 0, n.chain[len(n.chain)-1], false) - n.chain = append(n.chain, newBlocks...) - n.receipts = append(n.receipts, newR...) - n.cond.Broadcast() -} - -func (n *network) headers(from int) []*types.Header { - numHeaders := 128 - var hdrs []*types.Header - index := from - n.offset - - for index >= len(n.chain) { - // wait for progress - n.cond.L.Lock() - //fmt.Printf("header going into wait\n") - n.cond.Wait() - index = from - n.offset - n.cond.L.Unlock() - } - n.lock.RLock() - defer n.lock.RUnlock() - for i, b := range n.chain[index:] { - hdrs = append(hdrs, b.Header()) - if i >= numHeaders { - break - } - } - return hdrs -} diff --git a/eth/downloader/resultstore.go b/eth/downloader/resultstore.go deleted file mode 100644 index e4323c04eb..0000000000 --- a/eth/downloader/resultstore.go +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2020 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "fmt" - "sync" - "sync/atomic" - - "github.com/ethereum/go-ethereum/core/types" -) - -// resultStore implements a structure for maintaining fetchResults, tracking their -// download-progress and delivering (finished) results. -type resultStore struct { - items []*fetchResult // Downloaded but not yet delivered fetch results - resultOffset uint64 // Offset of the first cached fetch result in the block chain - - // Internal index of first non-completed entry, updated atomically when needed. - // If all items are complete, this will equal length(items), so - // *important* : is not safe to use for indexing without checking against length - indexIncomplete atomic.Int32 - - // throttleThreshold is the limit up to which we _want_ to fill the - // results. If blocks are large, we want to limit the results to less - // than the number of available slots, and maybe only fill 1024 out of - // 8192 possible places. The queue will, at certain times, recalibrate - // this index. - throttleThreshold uint64 - - lock sync.RWMutex -} - -func newResultStore(size int) *resultStore { - return &resultStore{ - resultOffset: 0, - items: make([]*fetchResult, size), - throttleThreshold: uint64(size), - } -} - -// SetThrottleThreshold updates the throttling threshold based on the requested -// limit and the total queue capacity. It returns the (possibly capped) threshold -func (r *resultStore) SetThrottleThreshold(threshold uint64) uint64 { - r.lock.Lock() - defer r.lock.Unlock() - - limit := uint64(len(r.items)) - if threshold >= limit { - threshold = limit - } - r.throttleThreshold = threshold - return r.throttleThreshold -} - -// AddFetch adds a header for body/receipt fetching. This is used when the queue -// wants to reserve headers for fetching. -// -// It returns the following: -// -// stale - if true, this item is already passed, and should not be requested again -// throttled - if true, the store is at capacity, this particular header is not prio now -// item - the result to store data into -// err - any error that occurred -func (r *resultStore) AddFetch(header *types.Header, fastSync bool) (stale, throttled bool, item *fetchResult, err error) { - r.lock.Lock() - defer r.lock.Unlock() - - var index int - item, index, stale, throttled, err = r.getFetchResult(header.Number.Uint64()) - if err != nil || stale || throttled { - return stale, throttled, item, err - } - if item == nil { - item = newFetchResult(header, fastSync) - r.items[index] = item - } - return stale, throttled, item, err -} - -// GetDeliverySlot returns the fetchResult for the given header. If the 'stale' flag -// is true, that means the header has already been delivered 'upstream'. This method -// does not bubble up the 'throttle' flag, since it's moot at the point in time when -// the item is downloaded and ready for delivery -func (r *resultStore) GetDeliverySlot(headerNumber uint64) (*fetchResult, bool, error) { - r.lock.RLock() - defer r.lock.RUnlock() - - res, _, stale, _, err := r.getFetchResult(headerNumber) - return res, stale, err -} - -// getFetchResult returns the fetchResult corresponding to the given item, and -// the index where the result is stored. -func (r *resultStore) getFetchResult(headerNumber uint64) (item *fetchResult, index int, stale, throttle bool, err error) { - index = int(int64(headerNumber) - int64(r.resultOffset)) - throttle = index >= int(r.throttleThreshold) - stale = index < 0 - - if index >= len(r.items) { - err = fmt.Errorf("%w: index allocation went beyond available resultStore space "+ - "(index [%d] = header [%d] - resultOffset [%d], len(resultStore) = %d", errInvalidChain, - index, headerNumber, r.resultOffset, len(r.items)) - return nil, index, stale, throttle, err - } - if stale { - return nil, index, stale, throttle, nil - } - item = r.items[index] - return item, index, stale, throttle, nil -} - -// HasCompletedItems returns true if there are processable items available -// this method is cheaper than countCompleted -func (r *resultStore) HasCompletedItems() bool { - r.lock.RLock() - defer r.lock.RUnlock() - - if len(r.items) == 0 { - return false - } - if item := r.items[0]; item != nil && item.AllDone() { - return true - } - return false -} - -// countCompleted returns the number of items ready for delivery, stopping at -// the first non-complete item. -// -// The method assumes (at least) rlock is held. -func (r *resultStore) countCompleted() int { - // We iterate from the already known complete point, and see - // if any more has completed since last count - index := r.indexIncomplete.Load() - for ; ; index++ { - if index >= int32(len(r.items)) { - break - } - result := r.items[index] - if result == nil || !result.AllDone() { - break - } - } - r.indexIncomplete.Store(index) - return int(index) -} - -// GetCompleted returns the next batch of completed fetchResults -func (r *resultStore) GetCompleted(limit int) []*fetchResult { - r.lock.Lock() - defer r.lock.Unlock() - - completed := r.countCompleted() - if limit > completed { - limit = completed - } - results := make([]*fetchResult, limit) - copy(results, r.items[:limit]) - - // Delete the results from the cache and clear the tail. - copy(r.items, r.items[limit:]) - for i := len(r.items) - limit; i < len(r.items); i++ { - r.items[i] = nil - } - // Advance the expected block number of the first cache entry - r.resultOffset += uint64(limit) - r.indexIncomplete.Add(int32(-limit)) - - return results -} - -// Prepare initialises the offset with the given block number -func (r *resultStore) Prepare(offset uint64) { - r.lock.Lock() - defer r.lock.Unlock() - - if r.resultOffset < offset { - r.resultOffset = offset - } -} diff --git a/eth/downloader/skeleton.go b/eth/downloader/skeleton.go deleted file mode 100644 index f40ca24d99..0000000000 --- a/eth/downloader/skeleton.go +++ /dev/null @@ -1,1232 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "encoding/json" - "errors" - "fmt" - "math/rand" - "sort" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/log" -) - -// scratchHeaders is the number of headers to store in a scratch space to allow -// concurrent downloads. A header is about 0.5KB in size, so there is no worry -// about using too much memory. The only catch is that we can only validate gaps -// after they're linked to the head, so the bigger the scratch space, the larger -// potential for invalid headers. -// -// The current scratch space of 131072 headers is expected to use 64MB RAM. -const scratchHeaders = 131072 - -// requestHeaders is the number of header to request from a remote peer in a single -// network packet. Although the skeleton downloader takes into consideration peer -// capacities when picking idlers, the packet size was decided to remain constant -// since headers are relatively small and it's easier to work with fixed batches -// vs. dynamic interval fillings. -const requestHeaders = 512 - -// errSyncLinked is an internal helper error to signal that the current sync -// cycle linked up to the genesis block, this the skeleton syncer should ping -// the backfiller to resume. Since we already have that logic on sync start, -// piggy-back on that instead of 2 entrypoints. -var errSyncLinked = errors.New("sync linked") - -// errSyncMerged is an internal helper error to signal that the current sync -// cycle merged with a previously aborted subchain, thus the skeleton syncer -// should abort and restart with the new state. -var errSyncMerged = errors.New("sync merged") - -// errSyncReorged is an internal helper error to signal that the head chain of -// the current sync cycle was (partially) reorged, thus the skeleton syncer -// should abort and restart with the new state. -var errSyncReorged = errors.New("sync reorged") - -// errTerminated is returned if the sync mechanism was terminated for this run of -// the process. This is usually the case when Geth is shutting down and some events -// might still be propagating. -var errTerminated = errors.New("terminated") - -// errChainReorged is an internal helper error to signal that the header chain -// of the current sync cycle was (partially) reorged. -var errChainReorged = errors.New("chain reorged") - -// errChainGapped is an internal helper error to signal that the header chain -// of the current sync cycle is gaped with the one advertised by consensus client. -var errChainGapped = errors.New("chain gapped") - -// errChainForked is an internal helper error to signal that the header chain -// of the current sync cycle is forked with the one advertised by consensus client. -var errChainForked = errors.New("chain forked") - -func init() { - // Tuning parameters is nice, but the scratch space must be assignable in - // full to peers. It's a useless cornercase to support a dangling half-group. - if scratchHeaders%requestHeaders != 0 { - panic("Please make scratchHeaders divisible by requestHeaders") - } -} - -// subchain is a contiguous header chain segment that is backed by the database, -// but may not be linked to the live chain. The skeleton downloader may produce -// a new one of these every time it is restarted until the subchain grows large -// enough to connect with a previous subchain. -// -// The subchains use the exact same database namespace and are not disjoint from -// each other. As such, extending one to overlap the other entails reducing the -// second one first. This combined buffer model is used to avoid having to move -// data on disk when two subchains are joined together. -type subchain struct { - Head uint64 // Block number of the newest header in the subchain - Tail uint64 // Block number of the oldest header in the subchain - Next common.Hash // Block hash of the next oldest header in the subchain -} - -// skeletonProgress is a database entry to allow suspending and resuming a chain -// sync. As the skeleton header chain is downloaded backwards, restarts can and -// will produce temporarily disjoint subchains. There is no way to restart a -// suspended skeleton sync without prior knowledge of all prior suspension points. -type skeletonProgress struct { - Subchains []*subchain // Disjoint subchains downloaded until now - Finalized *uint64 // Last known finalized block number -} - -// headUpdate is a notification that the beacon sync should switch to a new target. -// The update might request whether to forcefully change the target, or only try to -// extend it and fail if it's not possible. -type headUpdate struct { - header *types.Header // Header to update the sync target to - final *types.Header // Finalized header to use as thresholds - force bool // Whether to force the update or only extend if possible - errc chan error // Channel to signal acceptance of the new head -} - -// headerRequest tracks a pending header request to ensure responses are to -// actual requests and to validate any security constraints. -// -// Concurrency note: header requests and responses are handled concurrently from -// the main runloop to allow Keccak256 hash verifications on the peer's thread and -// to drop on invalid response. The request struct must contain all the data to -// construct the response without accessing runloop internals (i.e. subchains). -// That is only included to allow the runloop to match a response to the task being -// synced without having yet another set of maps. -type headerRequest struct { - peer string // Peer to which this request is assigned - id uint64 // Request ID of this request - - deliver chan *headerResponse // Channel to deliver successful response on - revert chan *headerRequest // Channel to deliver request failure on - cancel chan struct{} // Channel to track sync cancellation - stale chan struct{} // Channel to signal the request was dropped - - head uint64 // Head number of the requested batch of headers -} - -// headerResponse is an already verified remote response to a header request. -type headerResponse struct { - peer *peerConnection // Peer from which this response originates - reqid uint64 // Request ID that this response fulfils - headers []*types.Header // Chain of headers -} - -// backfiller is a callback interface through which the skeleton sync can tell -// the downloader that it should suspend or resume backfilling on specific head -// events (e.g. suspend on forks or gaps, resume on successful linkups). -type backfiller interface { - // suspend requests the backfiller to abort any running full or snap sync - // based on the skeleton chain as it might be invalid. The backfiller should - // gracefully handle multiple consecutive suspends without a resume, even - // on initial startup. - // - // The method should return the last block header that has been successfully - // backfilled, or nil if the backfiller was not resumed. - suspend() *types.Header - - // resume requests the backfiller to start running fill or snap sync based on - // the skeleton chain as it has successfully been linked. Appending new heads - // to the end of the chain will not result in suspend/resume cycles. - // leaking too much sync logic out to the filler. - resume() -} - -// skeleton represents a header chain synchronized after the merge where blocks -// aren't validated any more via PoW in a forward fashion, rather are dictated -// and extended at the head via the beacon chain and backfilled on the original -// Ethereum block sync protocol. -// -// Since the skeleton is grown backwards from head to genesis, it is handled as -// a separate entity, not mixed in with the logical sequential transition of the -// blocks. Once the skeleton is connected to an existing, validated chain, the -// headers will be moved into the main downloader for filling and execution. -// -// Opposed to the original Ethereum block synchronization which is trustless (and -// uses a master peer to minimize the attack surface), post-merge block sync starts -// from a trusted head. As such, there is no need for a master peer any more and -// headers can be requested fully concurrently (though some batches might be -// discarded if they don't link up correctly). -// -// Although a skeleton is part of a sync cycle, it is not recreated, rather stays -// alive throughout the lifetime of the downloader. This allows it to be extended -// concurrently with the sync cycle, since extensions arrive from an API surface, -// not from within (vs. legacy Ethereum sync). -// -// Since the skeleton tracks the entire header chain until it is consumed by the -// forward block filling, it needs 0.5KB/block storage. At current mainnet sizes -// this is only possible with a disk backend. Since the skeleton is separate from -// the node's header chain, storing the headers ephemerally until sync finishes -// is wasted disk IO, but it's a price we're going to pay to keep things simple -// for now. -type skeleton struct { - db ethdb.Database // Database backing the skeleton - filler backfiller // Chain syncer suspended/resumed by head events - - peers *peerSet // Set of peers we can sync from - idles map[string]*peerConnection // Set of idle peers in the current sync cycle - drop peerDropFn // Drops a peer for misbehaving - - progress *skeletonProgress // Sync progress tracker for resumption and metrics - started time.Time // Timestamp when the skeleton syncer was created - logged time.Time // Timestamp when progress was last logged to the user - pulled uint64 // Number of headers downloaded in this run - - scratchSpace []*types.Header // Scratch space to accumulate headers in (first = recent) - scratchOwners []string // Peer IDs owning chunks of the scratch space (pend or delivered) - scratchHead uint64 // Block number of the first item in the scratch space - - requests map[uint64]*headerRequest // Header requests currently running - - headEvents chan *headUpdate // Notification channel for new heads - terminate chan chan error // Termination channel to abort sync - terminated chan struct{} // Channel to signal that the syncer is dead - - // Callback hooks used during testing - syncStarting func() // callback triggered after a sync cycle is inited but before started -} - -// newSkeleton creates a new sync skeleton that tracks a potentially dangling -// header chain until it's linked into an existing set of blocks. -func newSkeleton(db ethdb.Database, peers *peerSet, drop peerDropFn, filler backfiller) *skeleton { - sk := &skeleton{ - db: db, - filler: filler, - peers: peers, - drop: drop, - requests: make(map[uint64]*headerRequest), - headEvents: make(chan *headUpdate), - terminate: make(chan chan error), - terminated: make(chan struct{}), - } - go sk.startup() - return sk -} - -// startup is an initial background loop which waits for an event to start or -// tear the syncer down. This is required to make the skeleton sync loop once -// per process but at the same time not start before the beacon chain announces -// a new (existing) head. -func (s *skeleton) startup() { - // Close a notification channel so anyone sending us events will know if the - // sync loop was torn down for good. - defer close(s.terminated) - - // Wait for startup or teardown. This wait might loop a few times if a beacon - // client requests sync head extensions, but not forced reorgs (i.e. they are - // giving us new payloads without setting a starting head initially). - for { - select { - case errc := <-s.terminate: - // No head was announced but Geth is shutting down - errc <- nil - return - - case event := <-s.headEvents: - // New head announced, start syncing to it, looping every time a current - // cycle is terminated due to a chain event (head reorg, old chain merge). - if !event.force { - event.errc <- errors.New("forced head needed for startup") - continue - } - event.errc <- nil // forced head accepted for startup - head := event.header - s.started = time.Now() - - for { - // If the sync cycle terminated or was terminated, propagate up when - // higher layers request termination. There's no fancy explicit error - // signalling as the sync loop should never terminate (TM). - newhead, err := s.sync(head) - switch { - case err == errSyncLinked: - // Sync cycle linked up to the genesis block, or the existent chain - // segment. Tear down the loop and restart it so, it can properly - // notify the backfiller. Don't account a new head. - head = nil - - case err == errSyncMerged: - // Subchains were merged, we just need to reinit the internal - // start to continue on the tail of the merged chain. Don't - // announce a new head, - head = nil - - case err == errSyncReorged: - // The subchain being synced got modified at the head in a - // way that requires resyncing it. Restart sync with the new - // head to force a cleanup. - head = newhead - - case err == errTerminated: - // Sync was requested to be terminated from within, stop and - // return (no need to pass a message, was already done internally) - return - - default: - // Sync either successfully terminated or failed with an unhandled - // error. Abort and wait until Geth requests a termination. - errc := <-s.terminate - errc <- err - return - } - } - } - } -} - -// Terminate tears down the syncer indefinitely. -func (s *skeleton) Terminate() error { - // Request termination and fetch any errors - errc := make(chan error) - s.terminate <- errc - err := <-errc - - // Wait for full shutdown (not necessary, but cleaner) - <-s.terminated - return err -} - -// Sync starts or resumes a previous sync cycle to download and maintain a reverse -// header chain starting at the head and leading towards genesis to an available -// ancestor. -// -// This method does not block, rather it just waits until the syncer receives the -// fed header. What the syncer does with it is the syncer's problem. -func (s *skeleton) Sync(head *types.Header, final *types.Header, force bool) error { - log.Trace("New skeleton head announced", "number", head.Number, "hash", head.Hash(), "force", force) - errc := make(chan error) - - select { - case s.headEvents <- &headUpdate{header: head, final: final, force: force, errc: errc}: - return <-errc - case <-s.terminated: - return errTerminated - } -} - -// sync is the internal version of Sync that executes a single sync cycle, either -// until some termination condition is reached, or until the current cycle merges -// with a previously aborted run. -func (s *skeleton) sync(head *types.Header) (*types.Header, error) { - // If we're continuing a previous merge interrupt, just access the existing - // old state without initing from disk. - if head == nil { - head = rawdb.ReadSkeletonHeader(s.db, s.progress.Subchains[0].Head) - } else { - // Otherwise, initialize the sync, trimming and previous leftovers until - // we're consistent with the newly requested chain head - s.initSync(head) - } - // Create the scratch space to fill with concurrently downloaded headers - s.scratchSpace = make([]*types.Header, scratchHeaders) - defer func() { s.scratchSpace = nil }() // don't hold on to references after sync - - s.scratchOwners = make([]string, scratchHeaders/requestHeaders) - defer func() { s.scratchOwners = nil }() // don't hold on to references after sync - - s.scratchHead = s.progress.Subchains[0].Tail - 1 // tail must not be 0! - - // If the sync is already done, resume the backfiller. When the loop stops, - // terminate the backfiller too. - linked := len(s.progress.Subchains) == 1 && - rawdb.HasHeader(s.db, s.progress.Subchains[0].Next, s.scratchHead) && - rawdb.HasBody(s.db, s.progress.Subchains[0].Next, s.scratchHead) && - rawdb.HasReceipts(s.db, s.progress.Subchains[0].Next, s.scratchHead) - if linked { - s.filler.resume() - } - defer func() { - // The filler needs to be suspended, but since it can block for a while - // when there are many blocks queued up for full-sync importing, run it - // on a separate goroutine and consume head messages that need instant - // replies. - done := make(chan struct{}) - go func() { - defer close(done) - if filled := s.filler.suspend(); filled != nil { - // If something was filled, try to delete stale sync helpers. If - // unsuccessful, warn the user, but not much else we can do (it's - // a programming error, just let users report an issue and don't - // choke in the meantime). - if err := s.cleanStales(filled); err != nil { - log.Error("Failed to clean stale beacon headers", "err", err) - } - } - }() - // Wait for the suspend to finish, consuming head events in the meantime - // and dropping them on the floor. - for { - select { - case <-done: - return - case event := <-s.headEvents: - event.errc <- errors.New("beacon syncer reorging") - } - } - }() - // Create a set of unique channels for this sync cycle. We need these to be - // ephemeral so a data race doesn't accidentally deliver something stale on - // a persistent channel across syncs (yup, this happened) - var ( - requestFails = make(chan *headerRequest) - responses = make(chan *headerResponse) - ) - cancel := make(chan struct{}) - defer close(cancel) - - log.Debug("Starting reverse header sync cycle", "head", head.Number, "hash", head.Hash(), "cont", s.scratchHead) - - // Whether sync completed or not, disregard any future packets - defer func() { - log.Debug("Terminating reverse header sync cycle", "head", head.Number, "hash", head.Hash(), "cont", s.scratchHead) - s.requests = make(map[uint64]*headerRequest) - }() - - // Start tracking idle peers for task assignments - peering := make(chan *peeringEvent, 64) // arbitrary buffer, just some burst protection - - peeringSub := s.peers.SubscribeEvents(peering) - defer peeringSub.Unsubscribe() - - s.idles = make(map[string]*peerConnection) - for _, peer := range s.peers.AllPeers() { - s.idles[peer.id] = peer - } - // Notify any tester listening for startup events - if s.syncStarting != nil { - s.syncStarting() - } - for { - // Something happened, try to assign new tasks to any idle peers - if !linked { - s.assignTasks(responses, requestFails, cancel) - } - // Wait for something to happen - select { - case event := <-peering: - // A peer joined or left, the tasks queue and allocations need to be - // checked for potential assignment or reassignment - peerid := event.peer.id - if event.join { - log.Debug("Joining skeleton peer", "id", peerid) - s.idles[peerid] = event.peer - } else { - log.Debug("Leaving skeleton peer", "id", peerid) - s.revertRequests(peerid) - delete(s.idles, peerid) - } - - case errc := <-s.terminate: - errc <- nil - return nil, errTerminated - - case event := <-s.headEvents: - // New head was announced, try to integrate it. If successful, nothing - // needs to be done as the head simply extended the last range. For now - // we don't seamlessly integrate reorgs to keep things simple. If the - // network starts doing many mini reorgs, it might be worthwhile handling - // a limited depth without an error. - if err := s.processNewHead(event.header, event.final); err != nil { - // If a reorg is needed, and we're forcing the new head, signal - // the syncer to tear down and start over. Otherwise, drop the - // non-force reorg. - if event.force { - event.errc <- nil // forced head reorg accepted - log.Info("Restarting sync cycle", "reason", err) - return event.header, errSyncReorged - } - event.errc <- err - continue - } - event.errc <- nil // head extension accepted - - // New head was integrated into the skeleton chain. If the backfiller - // is still running, it will pick it up. If it already terminated, - // a new cycle needs to be spun up. - if linked { - s.filler.resume() - } - - case req := <-requestFails: - s.revertRequest(req) - - case res := <-responses: - // Process the batch of headers. If though processing we managed to - // link the current subchain to a previously downloaded one, abort the - // sync and restart with the merged subchains. - // - // If we managed to link to the existing local chain or genesis block, - // abort sync altogether. - linked, merged := s.processResponse(res) - if linked { - log.Debug("Beacon sync linked to local chain") - return nil, errSyncLinked - } - if merged { - log.Debug("Beacon sync merged subchains") - return nil, errSyncMerged - } - // We still have work to do, loop and repeat - } - } -} - -// initSync attempts to get the skeleton sync into a consistent state wrt any -// past state on disk and the newly requested head to sync to. If the new head -// is nil, the method will return and continue from the previous head. -func (s *skeleton) initSync(head *types.Header) { - // Extract the head number, we'll need it all over - number := head.Number.Uint64() - - // Retrieve the previously saved sync progress - if status := rawdb.ReadSkeletonSyncStatus(s.db); len(status) > 0 { - s.progress = new(skeletonProgress) - if err := json.Unmarshal(status, s.progress); err != nil { - log.Error("Failed to decode skeleton sync status", "err", err) - } else { - // Previous sync was available, print some continuation logs - for _, subchain := range s.progress.Subchains { - log.Debug("Restarting skeleton subchain", "head", subchain.Head, "tail", subchain.Tail) - } - // Create a new subchain for the head (unless the last can be extended), - // trimming anything it would overwrite - headchain := &subchain{ - Head: number, - Tail: number, - Next: head.ParentHash, - } - for len(s.progress.Subchains) > 0 { - // If the last chain is above the new head, delete altogether - lastchain := s.progress.Subchains[0] - if lastchain.Tail >= headchain.Tail { - log.Debug("Dropping skeleton subchain", "head", lastchain.Head, "tail", lastchain.Tail) - s.progress.Subchains = s.progress.Subchains[1:] - continue - } - // Otherwise truncate the last chain if needed and abort trimming - if lastchain.Head >= headchain.Tail { - log.Debug("Trimming skeleton subchain", "oldhead", lastchain.Head, "newhead", headchain.Tail-1, "tail", lastchain.Tail) - lastchain.Head = headchain.Tail - 1 - } - break - } - // 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 { - lastchain := s.progress.Subchains[0] - if lastchain.Head == headchain.Tail-1 { - lasthead := rawdb.ReadSkeletonHeader(s.db, lastchain.Head) - if lasthead.Hash() == head.ParentHash { - log.Debug("Extended skeleton subchain with new head", "head", headchain.Tail, "tail", lastchain.Tail) - lastchain.Head = headchain.Tail - extended = true - } - } - } - if !extended { - log.Debug("Created new skeleton subchain", "head", number, "tail", number) - s.progress.Subchains = append([]*subchain{headchain}, s.progress.Subchains...) - } - // Update the database with the new sync stats and insert the new - // head header. We won't delete any trimmed skeleton headers since - // those will be outside the index space of the many subchains and - // the database space will be reclaimed eventually when processing - // blocks above the current head (TODO(karalabe): don't forget). - batch := s.db.NewBatch() - - rawdb.WriteSkeletonHeader(batch, head) - s.saveSyncStatus(batch) - - if err := batch.Write(); err != nil { - log.Crit("Failed to write skeleton sync status", "err", err) - } - return - } - } - // Either we've failed to decode the previous state, or there was none. Start - // a fresh sync with a single subchain represented by the currently sent - // chain head. - s.progress = &skeletonProgress{ - Subchains: []*subchain{ - { - Head: number, - Tail: number, - Next: head.ParentHash, - }, - }, - } - batch := s.db.NewBatch() - - rawdb.WriteSkeletonHeader(batch, head) - s.saveSyncStatus(batch) - - if err := batch.Write(); err != nil { - log.Crit("Failed to write initial skeleton sync status", "err", err) - } - log.Debug("Created initial skeleton subchain", "head", number, "tail", number) -} - -// saveSyncStatus marshals the remaining sync tasks into leveldb. -func (s *skeleton) saveSyncStatus(db ethdb.KeyValueWriter) { - status, err := json.Marshal(s.progress) - if err != nil { - panic(err) // This can only fail during implementation - } - rawdb.WriteSkeletonSyncStatus(db, status) -} - -// processNewHead does the internal shuffling for a new head marker and either -// accepts and integrates it into the skeleton or requests a reorg. Upon reorg, -// the syncer will tear itself down and restart with a fresh head. It is simpler -// to reconstruct the sync state than to mutate it and hope for the best. -func (s *skeleton) processNewHead(head *types.Header, final *types.Header) error { - // If a new finalized block was announced, update the sync process independent - // of what happens with the sync head below - if final != nil { - if number := final.Number.Uint64(); s.progress.Finalized == nil || *s.progress.Finalized != number { - s.progress.Finalized = new(uint64) - *s.progress.Finalized = final.Number.Uint64() - - s.saveSyncStatus(s.db) - } - } - // If the header cannot be inserted without interruption, return an error for - // the outer loop to tear down the skeleton sync and restart it - number := head.Number.Uint64() - - lastchain := s.progress.Subchains[0] - if lastchain.Tail >= number { - // If the chain is down to a single beacon header, and it is re-announced - // once more, ignore it instead of tearing down sync for a noop. - if lastchain.Head == lastchain.Tail { - if current := rawdb.ReadSkeletonHeader(s.db, number); current.Hash() == head.Hash() { - return nil - } - } - // Not a noop / double head announce, abort with a reorg - return fmt.Errorf("%w, tail: %d, head: %d, newHead: %d", errChainReorged, lastchain.Tail, lastchain.Head, number) - } - if lastchain.Head+1 < number { - return fmt.Errorf("%w, head: %d, newHead: %d", errChainGapped, lastchain.Head, number) - } - if parent := rawdb.ReadSkeletonHeader(s.db, number-1); parent.Hash() != head.ParentHash { - return fmt.Errorf("%w, ancestor: %d, hash: %s, want: %s", errChainForked, number-1, parent.Hash(), head.ParentHash) - } - // New header seems to be in the last subchain range. Unwind any extra headers - // from the chain tip and insert the new head. We won't delete any trimmed - // skeleton headers since those will be outside the index space of the many - // subchains and the database space will be reclaimed eventually when processing - // blocks above the current head (TODO(karalabe): don't forget). - batch := s.db.NewBatch() - - rawdb.WriteSkeletonHeader(batch, head) - lastchain.Head = number - s.saveSyncStatus(batch) - - if err := batch.Write(); err != nil { - log.Crit("Failed to write skeleton sync status", "err", err) - } - return nil -} - -// assignTasks attempts to match idle peers to pending header retrievals. -func (s *skeleton) assignTasks(success chan *headerResponse, fail chan *headerRequest, cancel chan struct{}) { - // Sort the peers by download capacity to use faster ones if many available - idlers := &peerCapacitySort{ - peers: make([]*peerConnection, 0, len(s.idles)), - caps: make([]int, 0, len(s.idles)), - } - targetTTL := s.peers.rates.TargetTimeout() - for _, peer := range s.idles { - idlers.peers = append(idlers.peers, peer) - idlers.caps = append(idlers.caps, s.peers.rates.Capacity(peer.id, eth.BlockHeadersMsg, targetTTL)) - } - if len(idlers.peers) == 0 { - return - } - sort.Sort(idlers) - - // Find header regions not yet downloading and fill them - for task, owner := range s.scratchOwners { - // If we're out of idle peers, stop assigning tasks - if len(idlers.peers) == 0 { - return - } - // Skip any tasks already filling - if owner != "" { - continue - } - // If we've reached the genesis, stop assigning tasks - if uint64(task*requestHeaders) >= s.scratchHead { - return - } - // Found a task and have peers available, assign it - idle := idlers.peers[0] - - idlers.peers = idlers.peers[1:] - idlers.caps = idlers.caps[1:] - - // Matched a pending task to an idle peer, allocate a unique request id - var reqid uint64 - for { - reqid = uint64(rand.Int63()) - if reqid == 0 { - continue - } - if _, ok := s.requests[reqid]; ok { - continue - } - break - } - // Generate the network query and send it to the peer - req := &headerRequest{ - peer: idle.id, - id: reqid, - deliver: success, - revert: fail, - cancel: cancel, - stale: make(chan struct{}), - head: s.scratchHead - uint64(task*requestHeaders), - } - s.requests[reqid] = req - delete(s.idles, idle.id) - - // Generate the network query and send it to the peer - go s.executeTask(idle, req) - - // Inject the request into the task to block further assignments - s.scratchOwners[task] = idle.id - } -} - -// executeTask executes a single fetch request, blocking until either a result -// arrives or a timeouts / cancellation is triggered. The method should be run -// on its own goroutine and will deliver on the requested channels. -func (s *skeleton) executeTask(peer *peerConnection, req *headerRequest) { - start := time.Now() - resCh := make(chan *eth.Response) - - // Figure out how many headers to fetch. Usually this will be a full batch, - // but for the very tail of the chain, trim the request to the number left. - // Since nodes may or may not return the genesis header for a batch request, - // don't even request it. The parent hash of block #1 is enough to link. - requestCount := requestHeaders - if req.head < requestHeaders { - requestCount = int(req.head) - } - peer.log.Trace("Fetching skeleton headers", "from", req.head, "count", requestCount) - netreq, err := peer.peer.RequestHeadersByNumber(req.head, requestCount, 0, true, resCh) - if err != nil { - peer.log.Trace("Failed to request headers", "err", err) - s.scheduleRevertRequest(req) - return - } - defer netreq.Close() - - // Wait until the response arrives, the request is cancelled or times out - ttl := s.peers.rates.TargetTimeout() - - timeoutTimer := time.NewTimer(ttl) - defer timeoutTimer.Stop() - - select { - case <-req.cancel: - peer.log.Debug("Header request cancelled") - s.scheduleRevertRequest(req) - - case <-timeoutTimer.C: - // Header retrieval timed out, update the metrics - peer.log.Warn("Header request timed out, dropping peer", "elapsed", ttl) - headerTimeoutMeter.Mark(1) - s.peers.rates.Update(peer.id, eth.BlockHeadersMsg, 0, 0) - s.scheduleRevertRequest(req) - - // At this point we either need to drop the offending peer, or we need a - // mechanism to allow waiting for the response and not cancel it. For now - // lets go with dropping since the header sizes are deterministic and the - // beacon sync runs exclusive (downloader is idle) so there should be no - // other load to make timeouts probable. If we notice that timeouts happen - // more often than we'd like, we can introduce a tracker for the requests - // gone stale and monitor them. However, in that case too, we need a way - // to protect against malicious peers never responding, so it would need - // a second, hard-timeout mechanism. - s.drop(peer.id) - - case res := <-resCh: - // Headers successfully retrieved, update the metrics - headers := *res.Res.(*eth.BlockHeadersRequest) - - headerReqTimer.Update(time.Since(start)) - s.peers.rates.Update(peer.id, eth.BlockHeadersMsg, res.Time, len(headers)) - - // Cross validate the headers with the requests - switch { - case len(headers) == 0: - // No headers were delivered, reject the response and reschedule - peer.log.Debug("No headers delivered") - res.Done <- errors.New("no headers delivered") - s.scheduleRevertRequest(req) - - case headers[0].Number.Uint64() != req.head: - // Header batch anchored at non-requested number - peer.log.Debug("Invalid header response head", "have", headers[0].Number, "want", req.head) - res.Done <- errors.New("invalid header batch anchor") - s.scheduleRevertRequest(req) - - case req.head >= requestHeaders && len(headers) != requestHeaders: - // Invalid number of non-genesis headers delivered, reject the response and reschedule - peer.log.Debug("Invalid non-genesis header count", "have", len(headers), "want", requestHeaders) - res.Done <- errors.New("not enough non-genesis headers delivered") - s.scheduleRevertRequest(req) - - case req.head < requestHeaders && uint64(len(headers)) != req.head: - // Invalid number of genesis headers delivered, reject the response and reschedule - peer.log.Debug("Invalid genesis header count", "have", len(headers), "want", headers[0].Number.Uint64()) - res.Done <- errors.New("not enough genesis headers delivered") - s.scheduleRevertRequest(req) - - default: - // Packet seems structurally valid, check hash progression and if it - // is correct too, deliver for storage - for i := 0; i < len(headers)-1; i++ { - if headers[i].ParentHash != headers[i+1].Hash() { - peer.log.Debug("Invalid hash progression", "index", i, "wantparenthash", headers[i].ParentHash, "haveparenthash", headers[i+1].Hash()) - res.Done <- errors.New("invalid hash progression") - s.scheduleRevertRequest(req) - return - } - } - // Hash chain is valid. The delivery might still be junk as we're - // downloading batches concurrently (so no way to link the headers - // until gaps are filled); in that case, we'll nuke the peer when - // we detect the fault. - res.Done <- nil - - select { - case req.deliver <- &headerResponse{ - peer: peer, - reqid: req.id, - headers: headers, - }: - case <-req.cancel: - } - } - } -} - -// revertRequests locates all the currently pending requests from a particular -// peer and reverts them, rescheduling for others to fulfill. -func (s *skeleton) revertRequests(peer string) { - // Gather the requests first, revertals need the lock too - var requests []*headerRequest - for _, req := range s.requests { - if req.peer == peer { - requests = append(requests, req) - } - } - // Revert all the requests matching the peer - for _, req := range requests { - s.revertRequest(req) - } -} - -// scheduleRevertRequest asks the event loop to clean up a request and return -// all failed retrieval tasks to the scheduler for reassignment. -func (s *skeleton) scheduleRevertRequest(req *headerRequest) { - select { - case req.revert <- req: - // Sync event loop notified - case <-req.cancel: - // Sync cycle got cancelled - case <-req.stale: - // Request already reverted - } -} - -// revertRequest cleans up a request and returns all failed retrieval tasks to -// the scheduler for reassignment. -// -// Note, this needs to run on the event runloop thread to reschedule to idle peers. -// On peer threads, use scheduleRevertRequest. -func (s *skeleton) revertRequest(req *headerRequest) { - log.Trace("Reverting header request", "peer", req.peer, "reqid", req.id) - select { - case <-req.stale: - log.Trace("Header request already reverted", "peer", req.peer, "reqid", req.id) - return - default: - } - close(req.stale) - - // Remove the request from the tracked set - delete(s.requests, req.id) - - // Remove the request from the tracked set and mark the task as not-pending, - // ready for rescheduling - s.scratchOwners[(s.scratchHead-req.head)/requestHeaders] = "" -} - -func (s *skeleton) processResponse(res *headerResponse) (linked bool, merged bool) { - res.peer.log.Trace("Processing header response", "head", res.headers[0].Number, "hash", res.headers[0].Hash(), "count", len(res.headers)) - - // Whether the response is valid, we can mark the peer as idle and notify - // the scheduler to assign a new task. If the response is invalid, we'll - // drop the peer in a bit. - s.idles[res.peer.id] = res.peer - - // Ensure the response is for a valid request - if _, ok := s.requests[res.reqid]; !ok { - // Some internal accounting is broken. A request either times out or it - // gets fulfilled successfully. It should not be possible to deliver a - // response to a non-existing request. - res.peer.log.Error("Unexpected header packet") - return false, false - } - delete(s.requests, res.reqid) - - // Insert the delivered headers into the scratch space independent of the - // content or continuation; those will be validated in a moment - head := res.headers[0].Number.Uint64() - copy(s.scratchSpace[s.scratchHead-head:], res.headers) - - // If there's still a gap in the head of the scratch space, abort - if s.scratchSpace[0] == nil { - return false, false - } - // Try to consume any head headers, validating the boundary conditions - batch := s.db.NewBatch() - for s.scratchSpace[0] != nil { - // Next batch of headers available, cross-reference with the subchain - // we are extending and either accept or discard - if s.progress.Subchains[0].Next != s.scratchSpace[0].Hash() { - // Print a log messages to track what's going on - tail := s.progress.Subchains[0].Tail - want := s.progress.Subchains[0].Next - have := s.scratchSpace[0].Hash() - - log.Warn("Invalid skeleton headers", "peer", s.scratchOwners[0], "number", tail-1, "want", want, "have", have) - - // The peer delivered junk, or at least not the subchain we are - // syncing to. Free up the scratch space and assignment, reassign - // and drop the original peer. - for i := 0; i < requestHeaders; i++ { - s.scratchSpace[i] = nil - } - s.drop(s.scratchOwners[0]) - s.scratchOwners[0] = "" - break - } - // Scratch delivery matches required subchain, deliver the batch of - // headers and push the subchain forward - var consumed int - for _, header := range s.scratchSpace[:requestHeaders] { - if header != nil { // nil when the genesis is reached - consumed++ - - rawdb.WriteSkeletonHeader(batch, header) - s.pulled++ - - s.progress.Subchains[0].Tail-- - s.progress.Subchains[0].Next = header.ParentHash - - // If we've reached an existing block in the chain, stop retrieving - // headers. Note, if we want to support light clients with the same - // code we'd need to switch here based on the downloader mode. That - // said, there's no such functionality for now, so don't complicate. - // - // In the case of full sync it would be enough to check for the body, - // but even a full syncing node will generate a receipt once block - // processing is done, so it's just one more "needless" check. - // - // The weird cascading checks are done to minimize the database reads. - linked = rawdb.HasHeader(s.db, header.ParentHash, header.Number.Uint64()-1) && - rawdb.HasBody(s.db, header.ParentHash, header.Number.Uint64()-1) && - rawdb.HasReceipts(s.db, header.ParentHash, header.Number.Uint64()-1) - if linked { - break - } - } - } - head := s.progress.Subchains[0].Head - tail := s.progress.Subchains[0].Tail - next := s.progress.Subchains[0].Next - - log.Trace("Primary subchain extended", "head", head, "tail", tail, "next", next) - - // If the beacon chain was linked to the local chain, completely swap out - // all internal progress and abort header synchronization. - if linked { - // Linking into the local chain should also mean that there are no - // leftover subchains, but in the case of importing the blocks via - // the engine API, we will not push the subchains forward. This will - // lead to a gap between an old sync cycle and a future one. - if subchains := len(s.progress.Subchains); subchains > 1 { - switch { - // If there are only 2 subchains - the current one and an older - // one - and the old one consists of a single block, then it's - // the expected new sync cycle after some propagated blocks. Log - // it for debugging purposes, explicitly clean and don't escalate. - case subchains == 2 && s.progress.Subchains[1].Head == s.progress.Subchains[1].Tail: - // Remove the leftover skeleton header associated with old - // skeleton chain only if it's not covered by the current - // skeleton range. - if s.progress.Subchains[1].Head < s.progress.Subchains[0].Tail { - log.Debug("Cleaning previous beacon sync state", "head", s.progress.Subchains[1].Head) - rawdb.DeleteSkeletonHeader(batch, s.progress.Subchains[1].Head) - } - // Drop the leftover skeleton chain since it's stale. - s.progress.Subchains = s.progress.Subchains[:1] - - // If we have more than one header or more than one leftover chain, - // the syncer's internal state is corrupted. Do try to fix it, but - // be very vocal about the fault. - default: - var context []interface{} - - for i := range s.progress.Subchains[1:] { - context = append(context, fmt.Sprintf("stale_head_%d", i+1)) - context = append(context, s.progress.Subchains[i+1].Head) - context = append(context, fmt.Sprintf("stale_tail_%d", i+1)) - context = append(context, s.progress.Subchains[i+1].Tail) - context = append(context, fmt.Sprintf("stale_next_%d", i+1)) - context = append(context, s.progress.Subchains[i+1].Next) - } - log.Error("Cleaning spurious beacon sync leftovers", context...) - s.progress.Subchains = s.progress.Subchains[:1] - - // Note, here we didn't actually delete the headers at all, - // just the metadata. We could implement a cleanup mechanism, - // but further modifying corrupted state is kind of asking - // for it. Unless there's a good enough reason to risk it, - // better to live with the small database junk. - } - } - break - } - // Batch of headers consumed, shift the download window forward - copy(s.scratchSpace, s.scratchSpace[requestHeaders:]) - for i := 0; i < requestHeaders; i++ { - s.scratchSpace[scratchHeaders-i-1] = nil - } - copy(s.scratchOwners, s.scratchOwners[1:]) - s.scratchOwners[scratchHeaders/requestHeaders-1] = "" - - s.scratchHead -= uint64(consumed) - - // If the subchain extended into the next subchain, we need to handle - // the overlap. Since there could be many overlaps (come on), do this - // in a loop. - for len(s.progress.Subchains) > 1 && s.progress.Subchains[1].Head >= s.progress.Subchains[0].Tail { - // Extract some stats from the second subchain - head := s.progress.Subchains[1].Head - tail := s.progress.Subchains[1].Tail - next := s.progress.Subchains[1].Next - - // Since we just overwrote part of the next subchain, we need to trim - // its head independent of matching or mismatching content - if s.progress.Subchains[1].Tail >= s.progress.Subchains[0].Tail { - // Fully overwritten, get rid of the subchain as a whole - log.Debug("Previous subchain fully overwritten", "head", head, "tail", tail, "next", next) - s.progress.Subchains = append(s.progress.Subchains[:1], s.progress.Subchains[2:]...) - continue - } else { - // Partially overwritten, trim the head to the overwritten size - log.Debug("Previous subchain partially overwritten", "head", head, "tail", tail, "next", next) - s.progress.Subchains[1].Head = s.progress.Subchains[0].Tail - 1 - } - // If the old subchain is an extension of the new one, merge the two - // and let the skeleton syncer restart (to clean internal state) - if rawdb.ReadSkeletonHeader(s.db, s.progress.Subchains[1].Head).Hash() == s.progress.Subchains[0].Next { - log.Debug("Previous subchain merged", "head", head, "tail", tail, "next", next) - s.progress.Subchains[0].Tail = s.progress.Subchains[1].Tail - s.progress.Subchains[0].Next = s.progress.Subchains[1].Next - - s.progress.Subchains = append(s.progress.Subchains[:1], s.progress.Subchains[2:]...) - merged = true - } - } - // If subchains were merged, all further available headers in the scratch - // space are invalid since we skipped ahead. Stop processing the scratch - // space to avoid dropping peers thinking they delivered invalid data. - if merged { - break - } - } - s.saveSyncStatus(batch) - if err := batch.Write(); err != nil { - log.Crit("Failed to write skeleton headers and progress", "err", err) - } - // Print a progress report making the UX a bit nicer - left := s.progress.Subchains[0].Tail - 1 - if linked { - left = 0 - } - if time.Since(s.logged) > 8*time.Second || left == 0 { - s.logged = time.Now() - - if s.pulled == 0 { - log.Info("Beacon sync starting", "left", left) - } else { - eta := float64(time.Since(s.started)) / float64(s.pulled) * float64(left) - log.Info("Syncing beacon headers", "downloaded", s.pulled, "left", left, "eta", common.PrettyDuration(eta)) - } - } - return linked, merged -} - -// cleanStales removes previously synced beacon headers that have become stale -// due to the downloader backfilling past the tracked tail. -func (s *skeleton) cleanStales(filled *types.Header) error { - number := filled.Number.Uint64() - log.Trace("Cleaning stale beacon headers", "filled", number, "hash", filled.Hash()) - - // If the filled header is below the linked subchain, something's - // corrupted internally. Report and error and refuse to do anything. - if number < s.progress.Subchains[0].Tail { - return fmt.Errorf("filled header below beacon header tail: %d < %d", number, s.progress.Subchains[0].Tail) - } - // Subchain seems trimmable, push the tail forward up to the last - // filled header and delete everything before it - if available. In - // case we filled past the head, recreate the subchain with a new - // head to keep it consistent with the data on disk. - var ( - start = s.progress.Subchains[0].Tail // start deleting from the first known header - end = number // delete until the requested threshold - batch = s.db.NewBatch() - ) - s.progress.Subchains[0].Tail = number - s.progress.Subchains[0].Next = filled.ParentHash - - if s.progress.Subchains[0].Head < number { - // If more headers were filled than available, push the entire - // subchain forward to keep tracking the node's block imports - end = s.progress.Subchains[0].Head + 1 // delete the entire original range, including the head - s.progress.Subchains[0].Head = number // assign a new head (tail is already assigned to this) - - // The entire original skeleton chain was deleted and a new one - // defined. Make sure the new single-header chain gets pushed to - // disk to keep internal state consistent. - rawdb.WriteSkeletonHeader(batch, filled) - } - // Execute the trimming and the potential rewiring of the progress - s.saveSyncStatus(batch) - for n := start; n < end; n++ { - // If the batch grew too big, flush it and continue with a new batch. - // The catch is that the sync metadata needs to reflect the actually - // flushed state, so temporarily change the subchain progress and - // revert after the flush. - if batch.ValueSize() >= ethdb.IdealBatchSize { - tmpTail := s.progress.Subchains[0].Tail - tmpNext := s.progress.Subchains[0].Next - - s.progress.Subchains[0].Tail = n - s.progress.Subchains[0].Next = rawdb.ReadSkeletonHeader(s.db, n).ParentHash - s.saveSyncStatus(batch) - - if err := batch.Write(); err != nil { - log.Crit("Failed to write beacon trim data", "err", err) - } - batch.Reset() - - s.progress.Subchains[0].Tail = tmpTail - s.progress.Subchains[0].Next = tmpNext - s.saveSyncStatus(batch) - } - rawdb.DeleteSkeletonHeader(batch, n) - } - if err := batch.Write(); err != nil { - log.Crit("Failed to write beacon trim data", "err", err) - } - return nil -} - -// Bounds retrieves the current head and tail tracked by the skeleton syncer -// and optionally the last known finalized header if any was announced and if -// it is still in the sync range. This method is used by the backfiller, whose -// life cycle is controlled by the skeleton syncer. -// -// Note, the method will not use the internal state of the skeleton, but will -// rather blindly pull stuff from the database. This is fine, because the back- -// filler will only run when the skeleton chain is fully downloaded and stable. -// There might be new heads appended, but those are atomic from the perspective -// of this method. Any head reorg will first tear down the backfiller and only -// then make the modification. -func (s *skeleton) Bounds() (head *types.Header, tail *types.Header, final *types.Header, err error) { - // Read the current sync progress from disk and figure out the current head. - // Although there's a lot of error handling here, these are mostly as sanity - // checks to avoid crashing if a programming error happens. These should not - // happen in live code. - status := rawdb.ReadSkeletonSyncStatus(s.db) - if len(status) == 0 { - return nil, nil, nil, errors.New("beacon sync not yet started") - } - progress := new(skeletonProgress) - if err := json.Unmarshal(status, progress); err != nil { - return nil, nil, nil, err - } - head = rawdb.ReadSkeletonHeader(s.db, progress.Subchains[0].Head) - if head == nil { - return nil, nil, nil, fmt.Errorf("head skeleton header %d is missing", progress.Subchains[0].Head) - } - tail = rawdb.ReadSkeletonHeader(s.db, progress.Subchains[0].Tail) - if tail == nil { - return nil, nil, nil, fmt.Errorf("tail skeleton header %d is missing", progress.Subchains[0].Tail) - } - if progress.Finalized != nil && tail.Number.Uint64() <= *progress.Finalized && *progress.Finalized <= head.Number.Uint64() { - final = rawdb.ReadSkeletonHeader(s.db, *progress.Finalized) - if final == nil { - return nil, nil, nil, fmt.Errorf("finalized skeleton header %d is missing", *progress.Finalized) - } - } - return head, tail, final, nil -} - -// Header retrieves a specific header tracked by the skeleton syncer. This method -// is meant to be used by the backfiller, whose life cycle is controlled by the -// skeleton syncer. -// -// Note, outside the permitted runtimes, this method might return nil results and -// subsequent calls might return headers from different chains. -func (s *skeleton) Header(number uint64) *types.Header { - return rawdb.ReadSkeletonHeader(s.db, number) -} diff --git a/eth/downloader/skeleton_test.go b/eth/downloader/skeleton_test.go deleted file mode 100644 index aceadd00d3..0000000000 --- a/eth/downloader/skeleton_test.go +++ /dev/null @@ -1,975 +0,0 @@ -// Copyright 2022 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "encoding/json" - "errors" - "fmt" - "math/big" - "sync/atomic" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/eth/protocols/eth" - "github.com/ethereum/go-ethereum/log" -) - -// hookedBackfiller is a tester backfiller with all interface methods mocked and -// hooked so tests can implement only the things they need. -type hookedBackfiller struct { - // suspendHook is an optional hook to be called when the filler is requested - // to be suspended. - suspendHook func() *types.Header - - // resumeHook is an optional hook to be called when the filler is requested - // to be resumed. - resumeHook func() -} - -// newHookedBackfiller creates a hooked backfiller with all callbacks disabled, -// essentially acting as a noop. -func newHookedBackfiller() backfiller { - return new(hookedBackfiller) -} - -// suspend requests the backfiller to abort any running full or snap sync -// based on the skeleton chain as it might be invalid. The backfiller should -// gracefully handle multiple consecutive suspends without a resume, even -// on initial startup. -func (hf *hookedBackfiller) suspend() *types.Header { - if hf.suspendHook != nil { - return hf.suspendHook() - } - return nil // we don't really care about header cleanups for now -} - -// resume requests the backfiller to start running fill or snap sync based on -// the skeleton chain as it has successfully been linked. Appending new heads -// to the end of the chain will not result in suspend/resume cycles. -func (hf *hookedBackfiller) resume() { - if hf.resumeHook != nil { - hf.resumeHook() - } -} - -// skeletonTestPeer is a mock peer that can only serve header requests from a -// pre-perated header chain (which may be arbitrarily wrong for testing). -// -// Requesting anything else from these peers will hard panic. Note, do *not* -// implement any other methods. We actually want to make sure that the skeleton -// syncer only depends on - and will only ever do so - on header requests. -type skeletonTestPeer struct { - id string // Unique identifier of the mock peer - headers []*types.Header // Headers to serve when requested - - serve func(origin uint64) []*types.Header // Hook to allow custom responses - - served atomic.Uint64 // Number of headers served by this peer - dropped atomic.Uint64 // Flag whether the peer was dropped (stop responding) -} - -// newSkeletonTestPeer creates a new mock peer to test the skeleton sync with. -func newSkeletonTestPeer(id string, headers []*types.Header) *skeletonTestPeer { - return &skeletonTestPeer{ - id: id, - headers: headers, - } -} - -// newSkeletonTestPeer creates a new mock peer to test the skeleton sync with, -// and sets an optional serve hook that can return headers for delivery instead -// of the predefined chain. Useful for emulating malicious behavior that would -// otherwise require dedicated peer types. -func newSkeletonTestPeerWithHook(id string, headers []*types.Header, serve func(origin uint64) []*types.Header) *skeletonTestPeer { - return &skeletonTestPeer{ - id: id, - headers: headers, - serve: serve, - } -} - -// RequestHeadersByNumber constructs a GetBlockHeaders function based on a numbered -// origin; associated with a particular peer in the download tester. The returned -// function can be used to retrieve batches of headers from the particular peer. -func (p *skeletonTestPeer) RequestHeadersByNumber(origin uint64, amount int, skip int, reverse bool, sink chan *eth.Response) (*eth.Request, error) { - // Since skeleton test peer are in-memory mocks, dropping the does not make - // them inaccessible. As such, check a local `dropped` field to see if the - // peer has been dropped and should not respond any more. - if p.dropped.Load() != 0 { - return nil, errors.New("peer already dropped") - } - // Skeleton sync retrieves batches of headers going backward without gaps. - // This ensures we can follow a clean parent progression without any reorg - // hiccups. There is no need for any other type of header retrieval, so do - // panic if there's such a request. - if !reverse || skip != 0 { - // Note, if other clients want to do these kinds of requests, it's their - // problem, it will still work. We just don't want *us* making complicated - // requests without a very strong reason to. - panic(fmt.Sprintf("invalid header retrieval: reverse %v, want true; skip %d, want 0", reverse, skip)) - } - // If the skeleton syncer requests the genesis block, panic. Whilst it could - // be considered a valid request, our code specifically should not request it - // ever since we want to link up headers to an existing local chain, which at - // worse will be the genesis. - if int64(origin)-int64(amount) < 0 { - panic(fmt.Sprintf("headers requested before (or at) genesis: origin %d, amount %d", origin, amount)) - } - // To make concurrency easier, the skeleton syncer always requests fixed size - // batches of headers. Panic if the peer is requested an amount other than the - // configured batch size (apart from the request leading to the genesis). - if amount > requestHeaders || (amount < requestHeaders && origin > uint64(amount)) { - panic(fmt.Sprintf("non-chunk size header batch requested: requested %d, want %d, origin %d", amount, requestHeaders, origin)) - } - // Simple reverse header retrieval. Fill from the peer's chain and return. - // If the tester has a serve hook set, try to use that before falling back - // to the default behavior. - var headers []*types.Header - if p.serve != nil { - headers = p.serve(origin) - } - if headers == nil { - headers = make([]*types.Header, 0, amount) - if len(p.headers) > int(origin) { // Don't serve headers if we're missing the origin - for i := 0; i < amount; i++ { - // Consider nil headers as a form of attack and withhold them. Nil - // cannot be decoded from RLP, so it's not possible to produce an - // attack by sending/receiving those over eth. - header := p.headers[int(origin)-i] - if header == nil { - continue - } - headers = append(headers, header) - } - } - } - p.served.Add(uint64(len(headers))) - - hashes := make([]common.Hash, len(headers)) - for i, header := range headers { - hashes[i] = header.Hash() - } - // Deliver the headers to the downloader - req := ð.Request{ - Peer: p.id, - } - res := ð.Response{ - Req: req, - Res: (*eth.BlockHeadersRequest)(&headers), - Meta: hashes, - Time: 1, - Done: make(chan error), - } - go func() { - sink <- res - if err := <-res.Done; err != nil { - log.Warn("Skeleton test peer response rejected", "err", err) - p.dropped.Add(1) - } - }() - return req, nil -} - -func (p *skeletonTestPeer) Head() (common.Hash, *big.Int) { - panic("skeleton sync must not request the remote head") -} - -func (p *skeletonTestPeer) RequestHeadersByHash(common.Hash, int, int, bool, chan *eth.Response) (*eth.Request, error) { - panic("skeleton sync must not request headers by hash") -} - -func (p *skeletonTestPeer) RequestBodies([]common.Hash, chan *eth.Response) (*eth.Request, error) { - panic("skeleton sync must not request block bodies") -} - -func (p *skeletonTestPeer) RequestReceipts([]common.Hash, chan *eth.Response) (*eth.Request, error) { - panic("skeleton sync must not request receipts") -} - -// Tests various sync initializations based on previous leftovers in the database -// and announced heads. -func TestSkeletonSyncInit(t *testing.T) { - // Create a few key headers - var ( - genesis = &types.Header{Number: big.NewInt(0)} - block49 = &types.Header{Number: big.NewInt(49)} - block49B = &types.Header{Number: big.NewInt(49), Extra: []byte("B")} - block50 = &types.Header{Number: big.NewInt(50), ParentHash: block49.Hash()} - ) - tests := []struct { - headers []*types.Header // Database content (beside the genesis) - oldstate []*subchain // Old sync state with various interrupted subchains - head *types.Header // New head header to announce to reorg to - newstate []*subchain // Expected sync state after the reorg - }{ - // Completely empty database with only the genesis set. The sync is expected - // to create a single subchain with the requested head. - { - head: block50, - newstate: []*subchain{{Head: 50, Tail: 50}}, - }, - // Empty database with only the genesis set with a leftover empty sync - // progress. This is a synthetic case, just for the sake of covering things. - { - oldstate: []*subchain{}, - head: block50, - newstate: []*subchain{{Head: 50, Tail: 50}}, - }, - // A single leftover subchain is present, older than the new head. The - // old subchain should be left as is and a new one appended to the sync - // status. - { - oldstate: []*subchain{{Head: 10, Tail: 5}}, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 10, Tail: 5}, - }, - }, - // Multiple leftover subchains are present, older than the new head. The - // old subchains should be left as is and a new one appended to the sync - // status. - { - oldstate: []*subchain{ - {Head: 20, Tail: 15}, - {Head: 10, Tail: 5}, - }, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 20, Tail: 15}, - {Head: 10, Tail: 5}, - }, - }, - // A single leftover subchain is present, newer than the new head. The - // newer subchain should be deleted and a fresh one created for the head. - { - oldstate: []*subchain{{Head: 65, Tail: 60}}, - head: block50, - newstate: []*subchain{{Head: 50, Tail: 50}}, - }, - // Multiple leftover subchain is present, newer than the new head. The - // newer subchains should be deleted and a fresh one created for the head. - { - oldstate: []*subchain{ - {Head: 75, Tail: 70}, - {Head: 65, Tail: 60}, - }, - head: block50, - newstate: []*subchain{{Head: 50, Tail: 50}}, - }, - - // Two leftover subchains are present, one fully older and one fully - // newer than the announced head. The head should delete the newer one, - // keeping the older one. - { - oldstate: []*subchain{ - {Head: 65, Tail: 60}, - {Head: 10, Tail: 5}, - }, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 10, Tail: 5}, - }, - }, - // Multiple leftover subchains are present, some fully older and some - // fully newer than the announced head. The head should delete the newer - // ones, keeping the older ones. - { - oldstate: []*subchain{ - {Head: 75, Tail: 70}, - {Head: 65, Tail: 60}, - {Head: 20, Tail: 15}, - {Head: 10, Tail: 5}, - }, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 20, Tail: 15}, - {Head: 10, Tail: 5}, - }, - }, - // A single leftover subchain is present and the new head is extending - // it with one more header. We expect the subchain head to be pushed - // forward. - { - headers: []*types.Header{block49}, - oldstate: []*subchain{{Head: 49, Tail: 5}}, - head: block50, - newstate: []*subchain{{Head: 50, Tail: 5}}, - }, - // A single leftover subchain is present and although the new head does - // extend it number wise, the hash chain does not link up. We expect a - // new subchain to be created for the dangling head. - { - headers: []*types.Header{block49B}, - oldstate: []*subchain{{Head: 49, Tail: 5}}, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 49, Tail: 5}, - }, - }, - // A single leftover subchain is present. A new head is announced that - // links into the middle of it, correctly anchoring into an existing - // header. We expect the old subchain to be truncated and extended with - // the new head. - { - headers: []*types.Header{block49}, - oldstate: []*subchain{{Head: 100, Tail: 5}}, - head: block50, - newstate: []*subchain{{Head: 50, Tail: 5}}, - }, - // A single leftover subchain is present. A new head is announced that - // links into the middle of it, but does not anchor into an existing - // header. We expect the old subchain to be truncated and a new chain - // be created for the dangling head. - { - headers: []*types.Header{block49B}, - oldstate: []*subchain{{Head: 100, Tail: 5}}, - head: block50, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - {Head: 49, Tail: 5}, - }, - }, - } - for i, tt := range tests { - // Create a fresh database and initialize it with the starting state - db := rawdb.NewMemoryDatabase() - - rawdb.WriteHeader(db, genesis) - for _, header := range tt.headers { - rawdb.WriteSkeletonHeader(db, header) - } - if tt.oldstate != nil { - blob, _ := json.Marshal(&skeletonProgress{Subchains: tt.oldstate}) - rawdb.WriteSkeletonSyncStatus(db, blob) - } - // Create a skeleton sync and run a cycle - wait := make(chan struct{}) - - skeleton := newSkeleton(db, newPeerSet(), nil, newHookedBackfiller()) - skeleton.syncStarting = func() { close(wait) } - skeleton.Sync(tt.head, nil, true) - - <-wait - skeleton.Terminate() - - // Ensure the correct resulting sync status - var progress skeletonProgress - json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress) - - if len(progress.Subchains) != len(tt.newstate) { - t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate)) - continue - } - for j := 0; j < len(progress.Subchains); j++ { - if progress.Subchains[j].Head != tt.newstate[j].Head { - t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head) - } - if progress.Subchains[j].Tail != tt.newstate[j].Tail { - t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail) - } - } - } -} - -// Tests that a running skeleton sync can be extended with properly linked up -// headers but not with side chains. -func TestSkeletonSyncExtend(t *testing.T) { - // Create a few key headers - var ( - genesis = &types.Header{Number: big.NewInt(0)} - block49 = &types.Header{Number: big.NewInt(49)} - block49B = &types.Header{Number: big.NewInt(49), Extra: []byte("B")} - block50 = &types.Header{Number: big.NewInt(50), ParentHash: block49.Hash()} - block51 = &types.Header{Number: big.NewInt(51), ParentHash: block50.Hash()} - ) - tests := []struct { - head *types.Header // New head header to announce to reorg to - extend *types.Header // New head header to announce to extend with - newstate []*subchain // Expected sync state after the reorg - err error // Whether extension succeeds or not - }{ - // Initialize a sync and try to extend it with a subsequent block. - { - head: block49, - extend: block50, - newstate: []*subchain{ - {Head: 50, Tail: 49}, - }, - }, - // Initialize a sync and try to extend it with the existing head block. - { - head: block49, - extend: block49, - newstate: []*subchain{ - {Head: 49, Tail: 49}, - }, - }, - // Initialize a sync and try to extend it with a sibling block. - { - head: block49, - extend: block49B, - newstate: []*subchain{ - {Head: 49, Tail: 49}, - }, - err: errChainReorged, - }, - // Initialize a sync and try to extend it with a number-wise sequential - // header, but a hash wise non-linking one. - { - head: block49B, - extend: block50, - newstate: []*subchain{ - {Head: 49, Tail: 49}, - }, - err: errChainForked, - }, - // Initialize a sync and try to extend it with a non-linking future block. - { - head: block49, - extend: block51, - newstate: []*subchain{ - {Head: 49, Tail: 49}, - }, - err: errChainGapped, - }, - // Initialize a sync and try to extend it with a past canonical block. - { - head: block50, - extend: block49, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - }, - err: errChainReorged, - }, - // Initialize a sync and try to extend it with a past sidechain block. - { - head: block50, - extend: block49B, - newstate: []*subchain{ - {Head: 50, Tail: 50}, - }, - err: errChainReorged, - }, - } - for i, tt := range tests { - // Create a fresh database and initialize it with the starting state - db := rawdb.NewMemoryDatabase() - rawdb.WriteHeader(db, genesis) - - // Create a skeleton sync and run a cycle - wait := make(chan struct{}) - - skeleton := newSkeleton(db, newPeerSet(), nil, newHookedBackfiller()) - skeleton.syncStarting = func() { close(wait) } - skeleton.Sync(tt.head, nil, true) - - <-wait - if err := skeleton.Sync(tt.extend, nil, false); !errors.Is(err, tt.err) { - t.Errorf("test %d: extension failure mismatch: have %v, want %v", i, err, tt.err) - } - skeleton.Terminate() - - // Ensure the correct resulting sync status - var progress skeletonProgress - json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress) - - if len(progress.Subchains) != len(tt.newstate) { - t.Errorf("test %d: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.newstate)) - continue - } - for j := 0; j < len(progress.Subchains); j++ { - if progress.Subchains[j].Head != tt.newstate[j].Head { - t.Errorf("test %d: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.newstate[j].Head) - } - if progress.Subchains[j].Tail != tt.newstate[j].Tail { - t.Errorf("test %d: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.newstate[j].Tail) - } - } - } -} - -// Tests that the skeleton sync correctly retrieves headers from one or more -// peers without duplicates or other strange side effects. -func TestSkeletonSyncRetrievals(t *testing.T) { - //log.Root().SetHandler(log.LvlFilterHandler(log.LvlTrace, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) - - // Since skeleton headers don't need to be meaningful, beyond a parent hash - // progression, create a long fake chain to test with. - chain := []*types.Header{{Number: big.NewInt(0)}} - for i := 1; i < 10000; i++ { - chain = append(chain, &types.Header{ - ParentHash: chain[i-1].Hash(), - 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 { - 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 - midstate []*subchain // Expected sync state after initial cycle - midserve uint64 // Expected number of header retrievals after initial cycle - middrop uint64 // Expected number of peers dropped after initial cycle - - newHead *types.Header // New header to anoint on top of the old one - newPeer *skeletonTestPeer // New peer to join the skeleton syncer - endstate []*subchain // Expected sync state after the post-init event - endserve uint64 // Expected number of header retrievals after the post-init event - enddrop uint64 // Expected number of peers dropped after the post-init event - }{ - // Completely empty database with only the genesis set. The sync is expected - // to create a single subchain with the requested head. No peers however, so - // the sync should be stuck without any progression. - // - // When a new peer is added, it should detect the join and fill the headers - // to the genesis block. - { - head: chain[len(chain)-1], - midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: uint64(len(chain) - 1)}}, - - newPeer: newSkeletonTestPeer("test-peer", chain), - endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}}, - endserve: uint64(len(chain) - 2), // len - head - genesis - }, - // Completely empty database with only the genesis set. The sync is expected - // to create a single subchain with the requested head. With one valid peer, - // the sync is expected to complete already in the initial round. - // - // Adding a second peer should not have any effect. - { - head: chain[len(chain)-1], - peers: []*skeletonTestPeer{newSkeletonTestPeer("test-peer-1", chain)}, - midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}}, - midserve: uint64(len(chain) - 2), // len - head - genesis - - newPeer: newSkeletonTestPeer("test-peer-2", chain), - endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}}, - endserve: uint64(len(chain) - 2), // len - head - genesis - }, - // Completely empty database with only the genesis set. The sync is expected - // to create a single subchain with the requested head. With many valid peers, - // the sync is expected to complete already in the initial round. - // - // Adding a new peer should not have any effect. - { - head: chain[len(chain)-1], - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("test-peer-1", chain), - newSkeletonTestPeer("test-peer-2", chain), - newSkeletonTestPeer("test-peer-3", chain), - }, - midstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}}, - midserve: uint64(len(chain) - 2), // len - head - genesis - - newPeer: newSkeletonTestPeer("test-peer-4", chain), - endstate: []*subchain{{Head: uint64(len(chain) - 1), Tail: 1}}, - endserve: uint64(len(chain) - 2), // len - head - genesis - }, - // This test checks if a peer tries to withhold a header - *on* the sync - // boundary - instead of sending the requested amount. The malicious short - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:99]...), nil), chain[100:]...)), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 3, // len - head - genesis - missing - middrop: 1, // penalize shortened header deliveries - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test checks if a peer tries to withhold a header - *off* the sync - // boundary - instead of sending the requested amount. The malicious short - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-skipper", append(append(append([]*types.Header{}, chain[:50]...), nil), chain[51:]...)), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 3, // len - head - genesis - missing - middrop: 1, // penalize shortened header deliveries - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 3) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test checks if a peer tries to duplicate a header - *on* the sync - // boundary - instead of sending the correct sequence. The malicious duped - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], // We want to force the 100th header to be a request boundary - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:99]...), chain[98]), chain[100:]...)), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 2, // len - head - genesis - middrop: 1, // penalize invalid header sequences - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test checks if a peer tries to duplicate a header - *off* the sync - // boundary - instead of sending the correct sequence. The malicious duped - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], // We want to force the 100th header to be a request boundary - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-duper", append(append(append([]*types.Header{}, chain[:50]...), chain[49]), chain[51:]...)), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 2, // len - head - genesis - middrop: 1, // penalize invalid header sequences - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test checks if a peer tries to inject a different header - *on* - // the sync boundary - instead of sending the correct sequence. The bad - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], // We want to force the 100th header to be a request boundary - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-changer", - append( - append( - append([]*types.Header{}, chain[:99]...), - &types.Header{ - ParentHash: chain[98].Hash(), - Number: big.NewInt(int64(99)), - GasLimit: 1, - }, - ), chain[100:]..., - ), - ), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 2, // len - head - genesis - middrop: 1, // different set of headers, drop // TODO(karalabe): maybe just diff sync? - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test checks if a peer tries to inject a different header - *off* - // the sync boundary - instead of sending the correct sequence. The bad - // package should not be accepted. - // - // Joining with a new peer should however unblock the sync. - { - head: chain[requestHeaders+100], // We want to force the 100th header to be a request boundary - peers: []*skeletonTestPeer{ - newSkeletonTestPeer("header-changer", - append( - append( - append([]*types.Header{}, chain[:50]...), - &types.Header{ - ParentHash: chain[49].Hash(), - Number: big.NewInt(int64(50)), - GasLimit: 1, - }, - ), chain[51:]..., - ), - ), - }, - midstate: []*subchain{{Head: requestHeaders + 100, Tail: 100}}, - midserve: requestHeaders + 101 - 2, // len - head - genesis - middrop: 1, // different set of headers, drop - - newPeer: newSkeletonTestPeer("good-peer", chain), - endstate: []*subchain{{Head: requestHeaders + 100, Tail: 1}}, - endserve: (requestHeaders + 101 - 2) + (100 - 1), // midserve + lenrest - genesis - enddrop: 1, // no new drops - }, - // This test reproduces a bug caught during review (kudos to @holiman) - // where a subchain is merged with a previously interrupted one, causing - // pending data in the scratch space to become "invalid" (since we jump - // ahead during subchain merge). In that case it is expected to ignore - // the queued up data instead of trying to process on top of a shifted - // task set. - // - // The test is a bit convoluted since it needs to trigger a concurrency - // issue. First we sync up an initial chain of 2x512 items. Then announce - // 2x512+2 as head and delay delivering the head batch to fill the scratch - // space first. The delivery head should merge with the previous download - // and the scratch space must not be consumed further. - { - head: chain[2*requestHeaders], - peers: []*skeletonTestPeer{ - newSkeletonTestPeerWithHook("peer-1", chain, func(origin uint64) []*types.Header { - if origin == chain[2*requestHeaders+1].Number.Uint64() { - time.Sleep(100 * time.Millisecond) - } - return nil // Fallback to default behavior, just delayed - }), - newSkeletonTestPeerWithHook("peer-2", chain, func(origin uint64) []*types.Header { - if origin == chain[2*requestHeaders+1].Number.Uint64() { - time.Sleep(100 * time.Millisecond) - } - return nil // Fallback to default behavior, just delayed - }), - }, - midstate: []*subchain{{Head: 2 * requestHeaders, Tail: 1}}, - midserve: 2*requestHeaders - 1, // len - head - genesis - - newHead: chain[2*requestHeaders+2], - 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.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() - for _, peer := range tt.peers { - peerset.Register(newPeerConnection(peer.id, eth.ETH67, peer, log.New("id", peer.id))) - } - // Create a peer dropper to track malicious peers - dropped := make(map[string]int) - drop := func(peer string) { - if p := peerset.Peer(peer); p != nil { - p.peer.(*skeletonTestPeer).dropped.Add(1) - } - 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, filler) - skeleton.Sync(tt.head, nil, true) - - var progress skeletonProgress - // Wait a bit (bleah) for the initial sync loop to go to idle. This might - // be either a finish or a never-start hence why there's no event to hook. - check := func() error { - if len(progress.Subchains) != len(tt.midstate) { - return fmt.Errorf("test %d, mid state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.midstate)) - } - for j := 0; j < len(progress.Subchains); j++ { - if progress.Subchains[j].Head != tt.midstate[j].Head { - return fmt.Errorf("test %d, mid state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.midstate[j].Head) - } - if progress.Subchains[j].Tail != tt.midstate[j].Tail { - return fmt.Errorf("test %d, mid state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.midstate[j].Tail) - } - } - return nil - } - - waitStart := time.Now() - for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 { - time.Sleep(waitTime) - // Check the post-init end state if it matches the required results - json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress) - if err := check(); err == nil { - break - } - } - if err := check(); err != nil { - t.Error(err) - continue - } - if !tt.unpredictable { - var served uint64 - for _, peer := range tt.peers { - served += peer.served.Load() - } - 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 += peer.dropped.Load() - } - 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 { - skeleton.Sync(tt.newHead, nil, true) - } - if tt.newPeer != nil { - if err := peerset.Register(newPeerConnection(tt.newPeer.id, eth.ETH67, tt.newPeer, log.New("id", tt.newPeer.id))); err != nil { - t.Errorf("test %d: failed to register new peer: %v", i, err) - } - } - // Wait a bit (bleah) for the second sync loop to go to idle. This might - // be either a finish or a never-start hence why there's no event to hook. - check = func() error { - if len(progress.Subchains) != len(tt.endstate) { - return fmt.Errorf("test %d, end state: subchain count mismatch: have %d, want %d", i, len(progress.Subchains), len(tt.endstate)) - } - for j := 0; j < len(progress.Subchains); j++ { - if progress.Subchains[j].Head != tt.endstate[j].Head { - return fmt.Errorf("test %d, end state: subchain %d head mismatch: have %d, want %d", i, j, progress.Subchains[j].Head, tt.endstate[j].Head) - } - if progress.Subchains[j].Tail != tt.endstate[j].Tail { - return fmt.Errorf("test %d, end state: subchain %d tail mismatch: have %d, want %d", i, j, progress.Subchains[j].Tail, tt.endstate[j].Tail) - } - } - return nil - } - waitStart = time.Now() - for waitTime := 20 * time.Millisecond; time.Since(waitStart) < 2*time.Second; waitTime = waitTime * 2 { - time.Sleep(waitTime) - // Check the post-init end state if it matches the required results - json.Unmarshal(rawdb.ReadSkeletonSyncStatus(db), &progress) - if err := check(); err == nil { - break - } - } - if err := check(); err != nil { - t.Error(err) - continue - } - // Check that the peers served no more headers than we actually needed - if !tt.unpredictable { - served := uint64(0) - for _, peer := range tt.peers { - served += peer.served.Load() - } - if tt.newPeer != nil { - served += tt.newPeer.served.Load() - } - 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 += peer.dropped.Load() - } - if tt.newPeer != nil { - drops += tt.newPeer.dropped.Load() - } - 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() - } -} diff --git a/eth/downloader/statesync.go b/eth/downloader/statesync.go deleted file mode 100644 index 501af63ed5..0000000000 --- a/eth/downloader/statesync.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/log" -) - -// syncState starts downloading state with the given root hash. -func (d *Downloader) syncState(root common.Hash) *stateSync { - // Create the state sync - s := newStateSync(d, root) - select { - case d.stateSyncStart <- s: - // If we tell the statesync to restart with a new root, we also need - // to wait for it to actually also start -- when old requests have timed - // out or been delivered - <-s.started - case <-d.quitCh: - s.err = errCancelStateFetch - close(s.done) - } - return s -} - -// stateFetcher manages the active state sync and accepts requests -// on its behalf. -func (d *Downloader) stateFetcher() { - for { - select { - case s := <-d.stateSyncStart: - for next := s; next != nil; { - next = d.runStateSync(next) - } - case <-d.quitCh: - return - } - } -} - -// runStateSync runs a state synchronisation until it completes or another root -// hash is requested to be switched over to. -func (d *Downloader) runStateSync(s *stateSync) *stateSync { - log.Trace("State sync starting", "root", s.root) - - go s.run() - defer s.Cancel() - - for { - select { - case next := <-d.stateSyncStart: - return next - - case <-s.done: - return nil - } - } -} - -// stateSync schedules requests for downloading a particular state trie defined -// by a given state root. -type stateSync struct { - d *Downloader // Downloader instance to access and manage current peerset - root common.Hash // State root currently being synced - - started chan struct{} // Started is signalled once the sync loop starts - cancel chan struct{} // Channel to signal a termination request - cancelOnce sync.Once // Ensures cancel only ever gets called once - done chan struct{} // Channel to signal termination completion - err error // Any error hit during sync (set before completion) -} - -// newStateSync creates a new state trie download scheduler. This method does not -// yet start the sync. The user needs to call run to initiate. -func newStateSync(d *Downloader, root common.Hash) *stateSync { - return &stateSync{ - d: d, - root: root, - cancel: make(chan struct{}), - done: make(chan struct{}), - started: make(chan struct{}), - } -} - -// run starts the task assignment and response processing loop, blocking until -// it finishes, and finally notifying any goroutines waiting for the loop to -// finish. -func (s *stateSync) run() { - close(s.started) - s.err = s.d.SnapSyncer.Sync(s.root, s.cancel) - close(s.done) -} - -// Wait blocks until the sync is done or canceled. -func (s *stateSync) Wait() error { - <-s.done - return s.err -} - -// Cancel cancels the sync and waits until it has shut down. -func (s *stateSync) Cancel() error { - s.cancelOnce.Do(func() { - close(s.cancel) - }) - return s.Wait() -} diff --git a/eth/downloader/testchain_test.go b/eth/downloader/testchain_test.go deleted file mode 100644 index 1bf03411d1..0000000000 --- a/eth/downloader/testchain_test.go +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2018 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package downloader - -import ( - "fmt" - "math/big" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/consensus/ethash" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/trie" -) - -// Test chain parameters. -var ( - testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") - testAddress = crypto.PubkeyToAddress(testKey.PublicKey) - testDB = rawdb.NewMemoryDatabase() - - testGspec = &core.Genesis{ - Config: params.TestChainConfig, - Alloc: core.GenesisAlloc{testAddress: {Balance: big.NewInt(1000000000000000)}}, - BaseFee: big.NewInt(params.InitialBaseFee), - } - testGenesis = testGspec.MustCommit(testDB, trie.NewDatabase(testDB, trie.HashDefaults)) -) - -// The common prefix of all test chains: -var testChainBase *testChain - -// Different forks on top of the base chain: -var testChainForkLightA, testChainForkLightB, testChainForkHeavy *testChain - -var pregenerated bool - -func init() { - // Reduce some of the parameters to make the tester faster - fullMaxForkAncestry = 10000 - lightMaxForkAncestry = 10000 - blockCacheMaxItems = 1024 - fsHeaderSafetyNet = 256 - fsHeaderContCheck = 500 * time.Millisecond - - testChainBase = newTestChain(blockCacheMaxItems+200, testGenesis) - - var forkLen = int(fullMaxForkAncestry + 50) - var wg sync.WaitGroup - - // Generate the test chains to seed the peers with - wg.Add(3) - go func() { testChainForkLightA = testChainBase.makeFork(forkLen, false, 1); wg.Done() }() - go func() { testChainForkLightB = testChainBase.makeFork(forkLen, false, 2); wg.Done() }() - go func() { testChainForkHeavy = testChainBase.makeFork(forkLen, true, 3); wg.Done() }() - wg.Wait() - - // Generate the test peers used by the tests to avoid overloading during testing. - // These seemingly random chains are used in various downloader tests. We're just - // pre-generating them here. - chains := []*testChain{ - testChainBase, - testChainForkLightA, - testChainForkLightB, - testChainForkHeavy, - testChainBase.shorten(1), - testChainBase.shorten(blockCacheMaxItems - 15), - testChainBase.shorten((blockCacheMaxItems - 15) / 2), - testChainBase.shorten(blockCacheMaxItems - 15 - 5), - testChainBase.shorten(MaxHeaderFetch), - testChainBase.shorten(800), - testChainBase.shorten(800 / 2), - testChainBase.shorten(800 / 3), - testChainBase.shorten(800 / 4), - testChainBase.shorten(800 / 5), - testChainBase.shorten(800 / 6), - testChainBase.shorten(800 / 7), - testChainBase.shorten(800 / 8), - testChainBase.shorten(3*fsHeaderSafetyNet + 256 + fsMinFullBlocks), - testChainBase.shorten(fsMinFullBlocks + 256 - 1), - testChainForkLightA.shorten(len(testChainBase.blocks) + 80), - testChainForkLightB.shorten(len(testChainBase.blocks) + 81), - testChainForkLightA.shorten(len(testChainBase.blocks) + MaxHeaderFetch), - testChainForkLightB.shorten(len(testChainBase.blocks) + MaxHeaderFetch), - testChainForkHeavy.shorten(len(testChainBase.blocks) + 79), - } - wg.Add(len(chains)) - for _, chain := range chains { - go func(blocks []*types.Block) { - newTestBlockchain(blocks) - wg.Done() - }(chain.blocks[1:]) - } - wg.Wait() - - // Mark the chains pregenerated. Generating a new one will lead to a panic. - pregenerated = true -} - -type testChain struct { - blocks []*types.Block -} - -// newTestChain creates a blockchain of the given length. -func newTestChain(length int, genesis *types.Block) *testChain { - tc := &testChain{ - blocks: []*types.Block{genesis}, - } - tc.generate(length-1, 0, genesis, false) - return tc -} - -// makeFork creates a fork on top of the test chain. -func (tc *testChain) makeFork(length int, heavy bool, seed byte) *testChain { - fork := tc.copy(len(tc.blocks) + length) - fork.generate(length, seed, tc.blocks[len(tc.blocks)-1], heavy) - return fork -} - -// shorten creates a copy of the chain with the given length. It panics if the -// length is longer than the number of available blocks. -func (tc *testChain) shorten(length int) *testChain { - if length > len(tc.blocks) { - panic(fmt.Errorf("can't shorten test chain to %d blocks, it's only %d blocks long", length, len(tc.blocks))) - } - return tc.copy(length) -} - -func (tc *testChain) copy(newlen int) *testChain { - if newlen > len(tc.blocks) { - newlen = len(tc.blocks) - } - cpy := &testChain{ - blocks: append([]*types.Block{}, tc.blocks[:newlen]...), - } - return cpy -} - -// generate creates a chain of n blocks starting at and including parent. -// the returned hash chain is ordered head->parent. In addition, every 22th block -// contains a transaction and every 5th an uncle to allow testing correct block -// reassembly. -func (tc *testChain) generate(n int, seed byte, parent *types.Block, heavy bool) { - blocks, _ := core.GenerateChain(testGspec.Config, parent, ethash.NewFaker(), testDB, n, func(i int, block *core.BlockGen) { - block.SetCoinbase(common.Address{seed}) - // If a heavy chain is requested, delay blocks to raise difficulty - if heavy { - block.OffsetTime(-9) - } - // Include transactions to the miner to make blocks more interesting. - if parent == tc.blocks[0] && i%22 == 0 { - signer := types.MakeSigner(params.TestChainConfig, block.Number(), block.Timestamp()) - tx, err := types.SignTx(types.NewTransaction(block.TxNonce(testAddress), common.Address{seed}, big.NewInt(1000), params.TxGas, block.BaseFee(), nil), signer, testKey) - if err != nil { - panic(err) - } - block.AddTx(tx) - } - // if the block number is a multiple of 5, add a bonus uncle to the block - if i > 0 && i%5 == 0 { - block.AddUncle(&types.Header{ - ParentHash: block.PrevBlock(i - 2).Hash(), - Number: big.NewInt(block.Number().Int64() - 1), - }) - } - }) - tc.blocks = append(tc.blocks, blocks...) -} - -var ( - testBlockchains = make(map[common.Hash]*testBlockchain) - testBlockchainsLock sync.Mutex -) - -type testBlockchain struct { - chain *core.BlockChain - gen sync.Once -} - -// newTestBlockchain creates a blockchain database built by running the given blocks, -// either actually running them, or reusing a previously created one. The returned -// chains are *shared*, so *do not* mutate them. -func newTestBlockchain(blocks []*types.Block) *core.BlockChain { - // Retrieve an existing database, or create a new one - head := testGenesis.Hash() - if len(blocks) > 0 { - head = blocks[len(blocks)-1].Hash() - } - testBlockchainsLock.Lock() - if _, ok := testBlockchains[head]; !ok { - testBlockchains[head] = new(testBlockchain) - } - tbc := testBlockchains[head] - testBlockchainsLock.Unlock() - - // Ensure that the database is generated - tbc.gen.Do(func() { - if pregenerated { - panic("Requested chain generation outside of init") - } - chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), nil, testGspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil) - if err != nil { - panic(err) - } - if n, err := chain.InsertChain(blocks); err != nil { - panic(fmt.Sprintf("block %d: %v", n, err)) - } - tbc.chain = chain - }) - return tbc.chain -}