From d2a63aae8666181cc62b915d3293aa1e91e015b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Isabel=20Sch=C3=B6ps=20Thiel?=
<155141998+IST-Github@users.noreply.github.com>
Date: Mon, 1 Jan 2024 00:49:56 +0100
Subject: [PATCH] Delete eth/downloader directory
---
eth/downloader/api.go | 166 --
eth/downloader/beacondevsync.go | 81 -
eth/downloader/beaconsync.go | 389 ----
eth/downloader/downloader.go | 1844 -----------------
eth/downloader/downloader_test.go | 1379 ------------
eth/downloader/events.go | 25 -
eth/downloader/fetchers.go | 115 -
eth/downloader/fetchers_concurrent.go | 380 ----
eth/downloader/fetchers_concurrent_bodies.go | 105 -
eth/downloader/fetchers_concurrent_headers.go | 97 -
.../fetchers_concurrent_receipts.go | 104 -
eth/downloader/metrics.go | 42 -
eth/downloader/modes.go | 74 -
eth/downloader/peer.go | 290 ---
eth/downloader/queue.go | 956 ---------
eth/downloader/queue_test.go | 474 -----
eth/downloader/resultstore.go | 195 --
eth/downloader/skeleton.go | 1232 -----------
eth/downloader/skeleton_test.go | 975 ---------
eth/downloader/statesync.go | 123 --
eth/downloader/testchain_test.go | 231 ---
21 files changed, 9277 deletions(-)
delete mode 100644 eth/downloader/api.go
delete mode 100644 eth/downloader/beacondevsync.go
delete mode 100644 eth/downloader/beaconsync.go
delete mode 100644 eth/downloader/downloader.go
delete mode 100644 eth/downloader/downloader_test.go
delete mode 100644 eth/downloader/events.go
delete mode 100644 eth/downloader/fetchers.go
delete mode 100644 eth/downloader/fetchers_concurrent.go
delete mode 100644 eth/downloader/fetchers_concurrent_bodies.go
delete mode 100644 eth/downloader/fetchers_concurrent_headers.go
delete mode 100644 eth/downloader/fetchers_concurrent_receipts.go
delete mode 100644 eth/downloader/metrics.go
delete mode 100644 eth/downloader/modes.go
delete mode 100644 eth/downloader/peer.go
delete mode 100644 eth/downloader/queue.go
delete mode 100644 eth/downloader/queue_test.go
delete mode 100644 eth/downloader/resultstore.go
delete mode 100644 eth/downloader/skeleton.go
delete mode 100644 eth/downloader/skeleton_test.go
delete mode 100644 eth/downloader/statesync.go
delete mode 100644 eth/downloader/testchain_test.go
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
-}