github.com/amazechain/amc@v0.1.3/internal/sync/initial-sync/blocks_fetcher.go (about)

     1  package initialsync
     2  
     3  import (
     4  	"context"
     5  	"github.com/amazechain/amc/api/protocol/sync_pb"
     6  	"github.com/amazechain/amc/api/protocol/types_pb"
     7  	"github.com/amazechain/amc/common"
     8  	"github.com/amazechain/amc/common/crypto/rand"
     9  	"github.com/amazechain/amc/internal/p2p"
    10  	leakybucket "github.com/amazechain/amc/internal/p2p/leaky-bucket"
    11  	amcsync "github.com/amazechain/amc/internal/sync"
    12  	"github.com/amazechain/amc/utils"
    13  	"github.com/holiman/uint256"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/libp2p/go-libp2p/core/peer"
    18  	"github.com/pkg/errors"
    19  	"go.opencensus.io/trace"
    20  )
    21  
    22  const (
    23  	// maxPendingRequests limits how many concurrent fetch request one can initiate.
    24  	maxPendingRequests = 64
    25  	// peersPercentagePerRequest caps percentage of peers to be used in a request.
    26  	peersPercentagePerRequest = 0.75
    27  	// handshakePollingInterval is a polling interval for checking the number of received handshakes.
    28  	handshakePollingInterval = 5 * time.Second
    29  	// peerLocksPollingInterval is a polling interval for checking if there are stale peer locks.
    30  	peerLocksPollingInterval = 5 * time.Minute
    31  	// peerLockMaxAge is maximum time before stale lock is purged.
    32  	peerLockMaxAge = 60 * time.Minute
    33  	// peerFilterCapacityWeight defines how peer's capacity affects peer's score. Provided as
    34  	// percentage, i.e. 0.3 means capacity will determine 30% of peer's score.
    35  	peerFilterCapacityWeight = 0.2
    36  )
    37  
    38  var (
    39  	errNoPeersAvailable      = errors.New("no peers available, waiting for reconnect")
    40  	errFetcherCtxIsDone      = errors.New("fetcher's context is done, reinitialize")
    41  	errBlockNrIsTooHigh      = errors.New("block number is higher than the target block number")
    42  	errBlockAlreadyProcessed = errors.New("block is already processed")
    43  	errParentDoesNotExist    = errors.New("node doesn't have a parent in db with root")
    44  	errNoPeersWithAltBlocks  = errors.New("no peers with alternative blocks found")
    45  )
    46  
    47  // blocksFetcherConfig is a config to setup the block fetcher.
    48  type blocksFetcherConfig struct {
    49  	chain                    common.IBlockChain
    50  	p2p                      p2p.P2P
    51  	peerFilterCapacityWeight float64
    52  	mode                     syncMode
    53  }
    54  
    55  // blocksFetcher is a service to fetch chain data from peers.
    56  // On an incoming requests, requested block range is evenly divided
    57  // among available peers (for fair network load distribution).
    58  type blocksFetcher struct {
    59  	sync.Mutex
    60  	ctx             context.Context
    61  	cancel          context.CancelFunc
    62  	rand            *rand.Rand
    63  	chain           common.IBlockChain
    64  	p2p             p2p.P2P
    65  	blocksPerPeriod uint64
    66  	rateLimiter     *leakybucket.Collector
    67  	peerLocks       map[peer.ID]*peerLock
    68  	fetchRequests   chan *fetchRequestParams
    69  	fetchResponses  chan *fetchRequestResponse
    70  	capacityWeight  float64       // how remaining capacity affects peer selection
    71  	mode            syncMode      // allows to use fetcher in different sync scenarios
    72  	quit            chan struct{} // termination notifier
    73  }
    74  
    75  // peerLock restricts fetcher actions on per peer basis. Currently, used for rate limiting.
    76  type peerLock struct {
    77  	sync.Mutex
    78  	accessed time.Time
    79  }
    80  
    81  // fetchRequestParams holds parameters necessary to schedule a fetch request.
    82  type fetchRequestParams struct {
    83  	ctx   context.Context // if provided, it is used instead of global fetcher's context
    84  	start *uint256.Int    // starting slot
    85  	count uint64          // how many slots to receive (fetcher may return fewer slots)
    86  }
    87  
    88  // fetchRequestResponse is a combined type to hold results of both successful executions and errors.
    89  // Valid usage pattern will be to check whether result's `err` is nil, before using `blocks`.
    90  type fetchRequestResponse struct {
    91  	pid    peer.ID
    92  	start  *uint256.Int
    93  	count  uint64
    94  	blocks []*types_pb.Block
    95  	err    error
    96  }
    97  
    98  // newBlocksFetcher creates ready to use fetcher.
    99  func newBlocksFetcher(ctx context.Context, cfg *blocksFetcherConfig) *blocksFetcher {
   100  
   101  	// Initialize block limits.
   102  	allowedBlocksPerSecond := float64(cfg.p2p.GetConfig().P2PLimit.BlockBatchLimit)
   103  	allowedBlocksBurst := int64(cfg.p2p.GetConfig().P2PLimit.BlockBatchLimitBurstFactor * cfg.p2p.GetConfig().P2PLimit.BlockBatchLimit)
   104  
   105  	blockLimiterPeriod := time.Duration(cfg.p2p.GetConfig().P2PLimit.BlockBatchLimiterPeriod) * time.Second
   106  
   107  	// Allow fetcher to go almost to the full burst capacity (less a single batch).
   108  	//rateLimiter := leakybucket.NewCollector(allowedBlocksPerSecond, allowedBlocksBurst-allowedBlocksBurst, blockLimiterPeriod, false /* deleteEmptyBuckets */)
   109  	rateLimiter := leakybucket.NewCollector(allowedBlocksPerSecond, allowedBlocksBurst, blockLimiterPeriod, false /* deleteEmptyBuckets */)
   110  
   111  	capacityWeight := cfg.peerFilterCapacityWeight
   112  	if capacityWeight >= 1 {
   113  		capacityWeight = peerFilterCapacityWeight
   114  	}
   115  
   116  	ctx, cancel := context.WithCancel(ctx)
   117  	return &blocksFetcher{
   118  		ctx:             ctx,
   119  		cancel:          cancel,
   120  		rand:            rand.NewGenerator(),
   121  		chain:           cfg.chain,
   122  		p2p:             cfg.p2p,
   123  		blocksPerPeriod: uint64(allowedBlocksPerSecond),
   124  		rateLimiter:     rateLimiter,
   125  		peerLocks:       make(map[peer.ID]*peerLock),
   126  		fetchRequests:   make(chan *fetchRequestParams, maxPendingRequests),
   127  		fetchResponses:  make(chan *fetchRequestResponse, maxPendingRequests),
   128  		capacityWeight:  capacityWeight,
   129  		mode:            cfg.mode,
   130  		quit:            make(chan struct{}),
   131  	}
   132  }
   133  
   134  // start boots up the fetcher, which starts listening for incoming fetch requests.
   135  func (f *blocksFetcher) start() error {
   136  	select {
   137  	case <-f.ctx.Done():
   138  		return errFetcherCtxIsDone
   139  	default:
   140  		go f.loop()
   141  		return nil
   142  	}
   143  }
   144  
   145  // stop terminates all fetcher operations.
   146  func (f *blocksFetcher) stop() {
   147  	defer func() {
   148  		if f.rateLimiter != nil {
   149  			f.rateLimiter.Free()
   150  			f.rateLimiter = nil
   151  		}
   152  	}()
   153  	f.cancel()
   154  	<-f.quit // make sure that loop() is done
   155  }
   156  
   157  // requestResponses exposes a channel into which fetcher pushes generated request responses.
   158  func (f *blocksFetcher) requestResponses() <-chan *fetchRequestResponse {
   159  	return f.fetchResponses
   160  }
   161  
   162  // loop is a main fetcher loop, listens for incoming requests/cancellations, forwards outgoing responses.
   163  func (f *blocksFetcher) loop() {
   164  	defer close(f.quit)
   165  
   166  	// Wait for all loop's goroutines to finish, and safely release resources.
   167  	wg := &sync.WaitGroup{}
   168  	defer func() {
   169  		wg.Wait()
   170  		close(f.fetchResponses)
   171  	}()
   172  
   173  	// Periodically remove stale peer locks.
   174  	go func() {
   175  		ticker := time.NewTicker(peerLocksPollingInterval)
   176  		defer ticker.Stop()
   177  		for {
   178  			select {
   179  			case <-ticker.C:
   180  				f.removeStalePeerLocks(peerLockMaxAge)
   181  			case <-f.ctx.Done():
   182  				return
   183  			}
   184  		}
   185  	}()
   186  
   187  	// Main loop.
   188  	for {
   189  		// Make sure there is are available peers before processing requests.
   190  		if _, err := f.waitForMinimumPeers(f.ctx); err != nil {
   191  			log.Error("cannot wait peers", "err", err)
   192  		}
   193  
   194  		select {
   195  		case <-f.ctx.Done():
   196  			log.Debug("Context closed, exiting goroutine (blocks fetcher)")
   197  			return
   198  		case req := <-f.fetchRequests:
   199  			wg.Add(1)
   200  			go func() {
   201  				defer wg.Done()
   202  				select {
   203  				case <-f.ctx.Done():
   204  				case f.fetchResponses <- f.handleRequest(req.ctx, req.start, req.count):
   205  				}
   206  			}()
   207  		}
   208  	}
   209  }
   210  
   211  // scheduleRequest adds request to incoming queue.
   212  func (f *blocksFetcher) scheduleRequest(ctx context.Context, start *uint256.Int, count uint64) error {
   213  	if ctx.Err() != nil {
   214  		return ctx.Err()
   215  	}
   216  
   217  	request := &fetchRequestParams{
   218  		ctx:   ctx,
   219  		start: start,
   220  		count: count,
   221  	}
   222  	select {
   223  	case <-f.ctx.Done():
   224  		return errFetcherCtxIsDone
   225  	case f.fetchRequests <- request:
   226  	}
   227  	return nil
   228  }
   229  
   230  // handleRequest parses fetch request and forwards it to response builder.
   231  func (f *blocksFetcher) handleRequest(ctx context.Context, start *uint256.Int, count uint64) *fetchRequestResponse {
   232  	ctx, span := trace.StartSpan(ctx, "initialsync.handleRequest")
   233  	defer span.End()
   234  
   235  	response := &fetchRequestResponse{
   236  		start:  start,
   237  		count:  count,
   238  		blocks: []*types_pb.Block{},
   239  		err:    nil,
   240  	}
   241  
   242  	if ctx.Err() != nil {
   243  		response.err = ctx.Err()
   244  		return response
   245  	}
   246  
   247  	_, peers := f.p2p.Peers().BestPeers(f.p2p.GetConfig().MinSyncPeers, f.chain.CurrentBlock().Number64())
   248  	if len(peers) == 0 {
   249  		response.err = errNoPeersAvailable
   250  		return response
   251  	}
   252  
   253  	response.blocks, response.pid, response.err = f.fetchBlocksFromPeer(ctx, start, count, peers)
   254  	return response
   255  }
   256  
   257  // fetchBlocksFromPeer fetches blocks from a single randomly selected peer.
   258  func (f *blocksFetcher) fetchBlocksFromPeer(ctx context.Context, start *uint256.Int, count uint64, peers []peer.ID) ([]*types_pb.Block, peer.ID, error) {
   259  	ctx, span := trace.StartSpan(ctx, "initialsync.fetchBlocksFromPeer")
   260  	defer span.End()
   261  
   262  	peers = f.filterPeers(ctx, peers, peersPercentagePerRequest)
   263  	req := &sync_pb.BodiesByRangeRequest{
   264  		StartBlockNumber: utils.ConvertUint256IntToH256(start),
   265  		Count:            count,
   266  		Step:             1,
   267  	}
   268  	for i := 0; i < len(peers); i++ {
   269  		blocks, err := f.requestBlocks(ctx, req, peers[i])
   270  		if err == nil {
   271  			f.p2p.Peers().Scorers().BlockProviderScorer().Touch(peers[i])
   272  			return blocks, peers[i], err
   273  		} else {
   274  			log.Warn("Could not request blocks by range", "err", err)
   275  		}
   276  	}
   277  	return nil, "", errNoPeersAvailable
   278  }
   279  
   280  // requestBlocks is a wrapper for handling BeaconBlocksByRangeRequest requests/streams.
   281  func (f *blocksFetcher) requestBlocks(ctx context.Context, req *sync_pb.BodiesByRangeRequest, pid peer.ID) ([]*types_pb.Block, error) {
   282  	if ctx.Err() != nil {
   283  		return nil, ctx.Err()
   284  	}
   285  	l := f.peerLock(pid)
   286  	l.Lock()
   287  	log.Debug("Requesting blocks",
   288  		"peer", pid,
   289  		"start", utils.ConvertH256ToUint256Int(req.StartBlockNumber).Uint64(),
   290  		"count", req.Count,
   291  		"step", req.Step,
   292  		"capacity", f.rateLimiter.Remaining(pid.String()),
   293  		"score", f.p2p.Peers().Scorers().BlockProviderScorer().FormatScorePretty(pid),
   294  	)
   295  
   296  	if f.rateLimiter.Remaining(pid.String()) < int64(req.Count) {
   297  		if err := f.waitForBandwidth(pid, req.Count); err != nil {
   298  			l.Unlock()
   299  			return nil, err
   300  		}
   301  	}
   302  	f.rateLimiter.Add(pid.String(), int64(req.Count))
   303  	l.Unlock()
   304  	return amcsync.SendBodiesByRangeRequest(ctx, f.chain, f.p2p, pid, req, nil)
   305  }
   306  
   307  // waitForBandwidth blocks up until peer's bandwidth is restored.
   308  func (f *blocksFetcher) waitForBandwidth(pid peer.ID, count uint64) error {
   309  
   310  	rem := f.rateLimiter.Remaining(pid.String())
   311  	if uint64(rem) >= count {
   312  		// Exit early if we have sufficient capacity
   313  		return nil
   314  	}
   315  	//todo
   316  	//intCount, err := math.Int(count)
   317  	//if err != nil {
   318  	//	return err
   319  	//}
   320  	toWait := timeToWait(int64(count), rem, f.rateLimiter.Capacity(), f.rateLimiter.TillEmpty(pid.String()))
   321  	timer := time.NewTimer(toWait)
   322  
   323  	log.Debug("Slowing down for rate limit", "peer", pid, "timeToWait", common.PrettyDuration(toWait))
   324  	defer timer.Stop()
   325  	select {
   326  	case <-f.ctx.Done():
   327  		return errFetcherCtxIsDone
   328  	case <-timer.C:
   329  		// Peer has gathered enough capacity to be polled again.
   330  	}
   331  	return nil
   332  }
   333  
   334  // Determine how long it will take for us to have the required number of blocks allowed by our rate limiter.
   335  // We do this by calculating the duration till the rate limiter can request these blocks without exceeding
   336  // the provided bandwidth limits per peer.
   337  func timeToWait(wanted, rem, capacity int64, timeTillEmpty time.Duration) time.Duration {
   338  	// Defensive check if we have more than enough blocks
   339  	// to request from the peer.
   340  	if rem >= wanted {
   341  		return 0
   342  	}
   343  	// Handle edge case where capacity is equal to the remaining amount
   344  	// of blocks. This also handles the impossible case in where remaining blocks
   345  	// exceed the limiter's capacity.
   346  	if capacity <= rem {
   347  		return 0
   348  	}
   349  	blocksNeeded := wanted - rem
   350  	currentNumBlks := capacity - rem
   351  	expectedTime := int64(timeTillEmpty) * blocksNeeded / currentNumBlks
   352  	return time.Duration(expectedTime)
   353  }