github.com/prysmaticlabs/prysm@v1.4.4/beacon-chain/sync/initial-sync/blocks_fetcher_utils.go (about)

     1  package initialsync
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  
     8  	"github.com/libp2p/go-libp2p-core/peer"
     9  	"github.com/pkg/errors"
    10  	types "github.com/prysmaticlabs/eth2-types"
    11  	"github.com/prysmaticlabs/prysm/beacon-chain/core/helpers"
    12  	p2pTypes "github.com/prysmaticlabs/prysm/beacon-chain/p2p/types"
    13  	"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
    14  	p2ppb "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1"
    15  	"github.com/prysmaticlabs/prysm/proto/interfaces"
    16  	"github.com/prysmaticlabs/prysm/shared/bytesutil"
    17  	"github.com/prysmaticlabs/prysm/shared/params"
    18  	"github.com/sirupsen/logrus"
    19  	"go.opencensus.io/trace"
    20  )
    21  
    22  // forkData represents alternative chain path supported by a given peer.
    23  // Blocks are stored in an ascending slot order. The first block is guaranteed to have parent
    24  // either in DB or initial sync cache.
    25  type forkData struct {
    26  	peer   peer.ID
    27  	blocks []interfaces.SignedBeaconBlock
    28  }
    29  
    30  // nonSkippedSlotAfter checks slots after the given one in an attempt to find a non-empty future slot.
    31  // For efficiency only one random slot is checked per epoch, so returned slot might not be the first
    32  // non-skipped slot. This shouldn't be a problem, as in case of adversary peer, we might get incorrect
    33  // data anyway, so code that relies on this function must be robust enough to re-request, if no progress
    34  // is possible with a returned value.
    35  func (f *blocksFetcher) nonSkippedSlotAfter(ctx context.Context, slot types.Slot) (types.Slot, error) {
    36  	ctx, span := trace.StartSpan(ctx, "initialsync.nonSkippedSlotAfter")
    37  	defer span.End()
    38  
    39  	headEpoch, targetEpoch, peers := f.calculateHeadAndTargetEpochs()
    40  	log.WithFields(logrus.Fields{
    41  		"start":       slot,
    42  		"headEpoch":   headEpoch,
    43  		"targetEpoch": targetEpoch,
    44  	}).Debug("Searching for non-skipped slot")
    45  
    46  	// Exit early if no peers with epoch higher than our known head are found.
    47  	if targetEpoch <= headEpoch {
    48  		return 0, errSlotIsTooHigh
    49  	}
    50  
    51  	// Transform peer list to avoid eclipsing (filter, shuffle, trim).
    52  	peers = f.filterPeers(ctx, peers, peersPercentagePerRequest)
    53  	return f.nonSkippedSlotAfterWithPeersTarget(ctx, slot, peers, targetEpoch)
    54  }
    55  
    56  // nonSkippedSlotWithPeersTarget traverse peers (supporting a given target epoch), in an attempt
    57  // to find non-skipped slot among returned blocks.
    58  func (f *blocksFetcher) nonSkippedSlotAfterWithPeersTarget(
    59  	ctx context.Context, slot types.Slot, peers []peer.ID, targetEpoch types.Epoch,
    60  ) (types.Slot, error) {
    61  	// Exit early if no peers are ready.
    62  	if len(peers) == 0 {
    63  		return 0, errNoPeersAvailable
    64  	}
    65  
    66  	slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
    67  	pidInd := 0
    68  
    69  	fetch := func(pid peer.ID, start types.Slot, count, step uint64) (types.Slot, error) {
    70  		req := &p2ppb.BeaconBlocksByRangeRequest{
    71  			StartSlot: start,
    72  			Count:     count,
    73  			Step:      step,
    74  		}
    75  		blocks, err := f.requestBlocks(ctx, req, pid)
    76  		if err != nil {
    77  			return 0, err
    78  		}
    79  		if len(blocks) > 0 {
    80  			for _, block := range blocks {
    81  				if block.Block().Slot() > slot {
    82  					return block.Block().Slot(), nil
    83  				}
    84  			}
    85  		}
    86  		return 0, nil
    87  	}
    88  
    89  	// Start by checking several epochs fully, w/o resorting to random sampling.
    90  	start := slot + 1
    91  	end := start + nonSkippedSlotsFullSearchEpochs*slotsPerEpoch
    92  	for ind := start; ind < end; ind += slotsPerEpoch {
    93  		nextSlot, err := fetch(peers[pidInd%len(peers)], ind, uint64(slotsPerEpoch), 1)
    94  		if err != nil {
    95  			return 0, err
    96  		}
    97  		if nextSlot > slot {
    98  			return nextSlot, nil
    99  		}
   100  		pidInd++
   101  	}
   102  
   103  	// Quickly find the close enough epoch where a non-empty slot definitely exists.
   104  	// Only single random slot per epoch is checked - allowing to move forward relatively quickly.
   105  	slot += nonSkippedSlotsFullSearchEpochs * slotsPerEpoch
   106  	upperBoundSlot, err := helpers.StartSlot(targetEpoch + 1)
   107  	if err != nil {
   108  		return 0, err
   109  	}
   110  	for ind := slot + 1; ind < upperBoundSlot; ind += (slotsPerEpoch * slotsPerEpoch) / 2 {
   111  		start := ind.Add(uint64(f.rand.Intn(int(slotsPerEpoch))))
   112  		nextSlot, err := fetch(peers[pidInd%len(peers)], start, uint64(slotsPerEpoch/2), uint64(slotsPerEpoch))
   113  		if err != nil {
   114  			return 0, err
   115  		}
   116  		pidInd++
   117  		if nextSlot > slot && upperBoundSlot >= nextSlot {
   118  			upperBoundSlot = nextSlot
   119  			break
   120  		}
   121  	}
   122  
   123  	// Epoch with non-empty slot is located. Check all slots within two nearby epochs.
   124  	if upperBoundSlot > slotsPerEpoch {
   125  		upperBoundSlot -= slotsPerEpoch
   126  	}
   127  	upperBoundSlot, err = helpers.StartSlot(helpers.SlotToEpoch(upperBoundSlot))
   128  	if err != nil {
   129  		return 0, err
   130  	}
   131  	nextSlot, err := fetch(peers[pidInd%len(peers)], upperBoundSlot, uint64(slotsPerEpoch*2), 1)
   132  	if err != nil {
   133  		return 0, err
   134  	}
   135  	s, err := helpers.StartSlot(targetEpoch + 1)
   136  	if err != nil {
   137  		return 0, err
   138  	}
   139  	if nextSlot < slot || s < nextSlot {
   140  		return 0, errors.New("invalid range for non-skipped slot")
   141  	}
   142  	return nextSlot, nil
   143  }
   144  
   145  // findFork queries all peers that have higher head slot, in an attempt to find
   146  // ones that feature blocks from alternative branches. Once found, peer is further queried
   147  // to find common ancestor slot. On success, all obtained blocks and peer is returned.
   148  func (f *blocksFetcher) findFork(ctx context.Context, slot types.Slot) (*forkData, error) {
   149  	ctx, span := trace.StartSpan(ctx, "initialsync.findFork")
   150  	defer span.End()
   151  
   152  	// Safe-guard, since previous epoch is used when calculating.
   153  	slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
   154  	if slot < slotsPerEpoch*2 {
   155  		return nil, fmt.Errorf("slot is too low to backtrack, min. expected %d", slotsPerEpoch*2)
   156  	}
   157  
   158  	// The current slot's epoch must be after the finalization epoch,
   159  	// triggering backtracking on earlier epochs is unnecessary.
   160  	finalizedEpoch := f.chain.FinalizedCheckpt().Epoch
   161  	epoch := helpers.SlotToEpoch(slot)
   162  	if epoch <= finalizedEpoch {
   163  		return nil, errors.New("slot is not after the finalized epoch, no backtracking is necessary")
   164  	}
   165  	// Update slot to the beginning of the current epoch (preserve original slot for comparison).
   166  	slot, err := helpers.StartSlot(epoch)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	// Select peers that have higher head slot, and potentially blocks from more favourable fork.
   172  	// Exit early if no peers are ready.
   173  	_, peers := f.p2p.Peers().BestNonFinalized(1, epoch+1)
   174  	if len(peers) == 0 {
   175  		return nil, errNoPeersAvailable
   176  	}
   177  	f.rand.Shuffle(len(peers), func(i, j int) {
   178  		peers[i], peers[j] = peers[j], peers[i]
   179  	})
   180  
   181  	// Query all found peers, stop on peer with alternative blocks, and try backtracking.
   182  	for i, pid := range peers {
   183  		log.WithFields(logrus.Fields{
   184  			"peer": pid,
   185  			"step": fmt.Sprintf("%d/%d", i+1, len(peers)),
   186  		}).Debug("Searching for alternative blocks")
   187  		fork, err := f.findForkWithPeer(ctx, pid, slot)
   188  		if err != nil {
   189  			log.WithFields(logrus.Fields{
   190  				"peer":  pid,
   191  				"error": err.Error(),
   192  			}).Debug("No alternative blocks found for peer")
   193  			continue
   194  		}
   195  		return fork, nil
   196  	}
   197  	return nil, errNoPeersWithAltBlocks
   198  }
   199  
   200  // findForkWithPeer loads some blocks from a peer in an attempt to find alternative blocks.
   201  func (f *blocksFetcher) findForkWithPeer(ctx context.Context, pid peer.ID, slot types.Slot) (*forkData, error) {
   202  	// Safe-guard, since previous epoch is used when calculating.
   203  	slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch
   204  	if slot < slotsPerEpoch*2 {
   205  		return nil, fmt.Errorf("slot is too low to backtrack, min. expected %d", slotsPerEpoch*2)
   206  	}
   207  
   208  	// Locate non-skipped slot, supported by a given peer (can survive long periods of empty slots).
   209  	// When searching for non-empty slot, start an epoch earlier - for those blocks we
   210  	// definitely have roots. So, spotting a fork will be easier. It is not a problem if unknown
   211  	// block of the current fork is found: we are searching for forks when FSMs are stuck, so
   212  	// being able to progress on any fork is good.
   213  	pidState, err := f.p2p.Peers().ChainState(pid)
   214  	if err != nil {
   215  		return nil, fmt.Errorf("cannot obtain peer's status: %w", err)
   216  	}
   217  	nonSkippedSlot, err := f.nonSkippedSlotAfterWithPeersTarget(
   218  		ctx, slot-slotsPerEpoch, []peer.ID{pid}, helpers.SlotToEpoch(pidState.HeadSlot))
   219  	if err != nil {
   220  		return nil, fmt.Errorf("cannot locate non-empty slot for a peer: %w", err)
   221  	}
   222  
   223  	// Request blocks starting from the first non-empty slot.
   224  	req := &p2ppb.BeaconBlocksByRangeRequest{
   225  		StartSlot: nonSkippedSlot,
   226  		Count:     uint64(slotsPerEpoch.Mul(2)),
   227  		Step:      1,
   228  	}
   229  	blocks, err := f.requestBlocks(ctx, req, pid)
   230  	if err != nil {
   231  		return nil, fmt.Errorf("cannot fetch blocks: %w", err)
   232  	}
   233  
   234  	// Traverse blocks, and if we've got one that doesn't have parent in DB, backtrack on it.
   235  	for i, block := range blocks {
   236  		parentRoot := bytesutil.ToBytes32(block.Block().ParentRoot())
   237  		if !f.db.HasBlock(ctx, parentRoot) && !f.chain.HasInitSyncBlock(parentRoot) {
   238  			log.WithFields(logrus.Fields{
   239  				"peer": pid,
   240  				"slot": block.Block().Slot(),
   241  				"root": fmt.Sprintf("%#x", parentRoot),
   242  			}).Debug("Block with unknown parent root has been found")
   243  			// Backtrack only if the first block is diverging,
   244  			// otherwise we already know the common ancestor slot.
   245  			if i == 0 {
   246  				// Backtrack on a root, to find a common ancestor from which we can resume syncing.
   247  				fork, err := f.findAncestor(ctx, pid, block)
   248  				if err != nil {
   249  					return nil, fmt.Errorf("failed to find common ancestor: %w", err)
   250  				}
   251  				return fork, nil
   252  			}
   253  			return &forkData{peer: pid, blocks: blocks}, nil
   254  		}
   255  	}
   256  	return nil, errors.New("no alternative blocks exist within scanned range")
   257  }
   258  
   259  // findAncestor tries to figure out common ancestor slot that connects a given root to known block.
   260  func (f *blocksFetcher) findAncestor(ctx context.Context, pid peer.ID, block interfaces.SignedBeaconBlock) (*forkData, error) {
   261  	outBlocks := []interfaces.SignedBeaconBlock{block}
   262  	for i := uint64(0); i < backtrackingMaxHops; i++ {
   263  		parentRoot := bytesutil.ToBytes32(outBlocks[len(outBlocks)-1].Block().ParentRoot())
   264  		if f.db.HasBlock(ctx, parentRoot) || f.chain.HasInitSyncBlock(parentRoot) {
   265  			// Common ancestor found, forward blocks back to processor.
   266  			sort.Slice(outBlocks, func(i, j int) bool {
   267  				return outBlocks[i].Block().Slot() < outBlocks[j].Block().Slot()
   268  			})
   269  			return &forkData{
   270  				peer:   pid,
   271  				blocks: outBlocks,
   272  			}, nil
   273  		}
   274  		// Request block's parent.
   275  		req := &p2pTypes.BeaconBlockByRootsReq{parentRoot}
   276  		blocks, err := f.requestBlocksByRoot(ctx, req, pid)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		if len(blocks) == 0 {
   281  			break
   282  		}
   283  		outBlocks = append(outBlocks, blocks[0])
   284  	}
   285  	return nil, errors.New("no common ancestor found")
   286  }
   287  
   288  // bestFinalizedSlot returns the highest finalized slot of the majority of connected peers.
   289  func (f *blocksFetcher) bestFinalizedSlot() types.Slot {
   290  	finalizedEpoch, _ := f.p2p.Peers().BestFinalized(
   291  		params.BeaconConfig().MaxPeersToSync, f.chain.FinalizedCheckpt().Epoch)
   292  	return params.BeaconConfig().SlotsPerEpoch.Mul(uint64(finalizedEpoch))
   293  }
   294  
   295  // bestNonFinalizedSlot returns the highest non-finalized slot of enough number of connected peers.
   296  func (f *blocksFetcher) bestNonFinalizedSlot() types.Slot {
   297  	headEpoch := helpers.SlotToEpoch(f.chain.HeadSlot())
   298  	targetEpoch, _ := f.p2p.Peers().BestNonFinalized(flags.Get().MinimumSyncPeers*2, headEpoch)
   299  	return params.BeaconConfig().SlotsPerEpoch.Mul(uint64(targetEpoch))
   300  }
   301  
   302  // calculateHeadAndTargetEpochs return node's current head epoch, along with the best known target
   303  // epoch. For the latter peers supporting that target epoch are returned as well.
   304  func (f *blocksFetcher) calculateHeadAndTargetEpochs() (headEpoch, targetEpoch types.Epoch, peers []peer.ID) {
   305  	if f.mode == modeStopOnFinalizedEpoch {
   306  		headEpoch = f.chain.FinalizedCheckpt().Epoch
   307  		targetEpoch, peers = f.p2p.Peers().BestFinalized(params.BeaconConfig().MaxPeersToSync, headEpoch)
   308  	} else {
   309  		headEpoch = helpers.SlotToEpoch(f.chain.HeadSlot())
   310  		targetEpoch, peers = f.p2p.Peers().BestNonFinalized(flags.Get().MinimumSyncPeers, headEpoch)
   311  	}
   312  	return headEpoch, targetEpoch, peers
   313  }