github.com/iotexproject/iotex-core@v1.14.1-rc1/blocksync/blocksync.go (about)

     1  // Copyright (c) 2019 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package blocksync
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/iotexproject/iotex-proto/golang/iotexrpc"
    16  	"github.com/libp2p/go-libp2p-core/peer"
    17  	"github.com/pkg/errors"
    18  	"go.uber.org/zap"
    19  	"google.golang.org/protobuf/proto"
    20  
    21  	"github.com/iotexproject/iotex-core/blockchain/block"
    22  	"github.com/iotexproject/iotex-core/pkg/fastrand"
    23  	"github.com/iotexproject/iotex-core/pkg/lifecycle"
    24  	"github.com/iotexproject/iotex-core/pkg/log"
    25  	"github.com/iotexproject/iotex-core/pkg/routine"
    26  	"github.com/iotexproject/iotex-core/server/itx/nodestats"
    27  )
    28  
    29  type (
    30  	// Neighbors acquires p2p neighbors in the network
    31  	Neighbors func() ([]peer.AddrInfo, error)
    32  	// UniCastOutbound sends a unicase message to the peer
    33  	UniCastOutbound func(context.Context, peer.AddrInfo, proto.Message) error
    34  	// BlockPeer adds the peer into blacklist in p2p layer
    35  	BlockPeer func(string)
    36  	// TipHeight returns the tip height of blockchain
    37  	TipHeight func() uint64
    38  	// BlockByHeight returns the block of a given height
    39  	BlockByHeight func(uint64) (*block.Block, error)
    40  	// CommitBlock commits a block to blockchain
    41  	CommitBlock func(*block.Block) error
    42  
    43  	// BlockSync defines the interface of blocksyncer
    44  	BlockSync interface {
    45  		lifecycle.StartStopper
    46  		nodestats.StatsReporter
    47  		// TargetHeight returns the target height to sync to
    48  		TargetHeight() uint64
    49  		// ProcessSyncRequest processes a block sync request
    50  		ProcessSyncRequest(context.Context, peer.AddrInfo, uint64, uint64) error
    51  		// ProcessBlock processes an incoming block
    52  		ProcessBlock(context.Context, string, *block.Block) error
    53  		// SyncStatus report block sync status
    54  		SyncStatus() (startingHeight uint64, currentHeight uint64, targetHeight uint64, syncSpeedDesc string)
    55  	}
    56  
    57  	dummyBlockSync struct{}
    58  
    59  	// blockSyncer implements BlockSync interface
    60  	blockSyncer struct {
    61  		cfg Config
    62  		buf *blockBuffer
    63  
    64  		tipHeightHandler     TipHeight
    65  		blockByHeightHandler BlockByHeight
    66  		commitBlockHandler   CommitBlock
    67  		p2pNeighbor          Neighbors
    68  		unicastOutbound      UniCastOutbound
    69  		blockP2pPeer         BlockPeer
    70  
    71  		syncTask      *routine.RecurringTask
    72  		syncStageTask *routine.RecurringTask
    73  
    74  		syncStageHeight   uint64
    75  		syncBlockIncrease uint64
    76  
    77  		startingHeight    uint64 // block number this node started to synchronise from
    78  		lastTip           uint64
    79  		lastTipUpdateTime time.Time
    80  		targetHeight      uint64 // block number of the highest block header this node has received from peers
    81  		mu                sync.RWMutex
    82  	}
    83  
    84  	peerBlock struct {
    85  		pid   string
    86  		block *block.Block
    87  	}
    88  )
    89  
    90  func newPeerBlock(pid string, blk *block.Block) *peerBlock {
    91  	return &peerBlock{
    92  		pid:   pid,
    93  		block: blk,
    94  	}
    95  }
    96  
    97  // NewDummyBlockSyncer creates a dummy BlockSync
    98  func NewDummyBlockSyncer() BlockSync {
    99  	return &dummyBlockSync{}
   100  }
   101  
   102  func (*dummyBlockSync) Start(context.Context) error {
   103  	return nil
   104  }
   105  
   106  func (*dummyBlockSync) Stop(context.Context) error {
   107  	return nil
   108  }
   109  
   110  func (*dummyBlockSync) TargetHeight() uint64 {
   111  	return 0
   112  }
   113  
   114  func (*dummyBlockSync) ProcessSyncRequest(context.Context, peer.AddrInfo, uint64, uint64) error {
   115  	return nil
   116  }
   117  
   118  func (*dummyBlockSync) ProcessBlock(context.Context, string, *block.Block) error {
   119  	return nil
   120  }
   121  
   122  func (*dummyBlockSync) SyncStatus() (uint64, uint64, uint64, string) {
   123  	return 0, 0, 0, ""
   124  }
   125  
   126  func (*dummyBlockSync) BuildReport() string {
   127  	return ""
   128  }
   129  
   130  // NewBlockSyncer returns a new block syncer instance
   131  func NewBlockSyncer(
   132  	cfg Config,
   133  	tipHeightHandler TipHeight,
   134  	blockByHeightHandler BlockByHeight,
   135  	commitBlockHandler CommitBlock,
   136  	p2pNeighbor Neighbors,
   137  	uniCastHandler UniCastOutbound,
   138  	blockP2pPeer BlockPeer,
   139  ) (BlockSync, error) {
   140  	bs := &blockSyncer{
   141  		cfg:                  cfg,
   142  		lastTipUpdateTime:    time.Now(),
   143  		buf:                  newBlockBuffer(cfg.BufferSize, cfg.IntervalSize),
   144  		tipHeightHandler:     tipHeightHandler,
   145  		blockByHeightHandler: blockByHeightHandler,
   146  		commitBlockHandler:   commitBlockHandler,
   147  		p2pNeighbor:          p2pNeighbor,
   148  		unicastOutbound:      uniCastHandler,
   149  		blockP2pPeer:         blockP2pPeer,
   150  		targetHeight:         0,
   151  	}
   152  	if bs.cfg.Interval != 0 {
   153  		bs.syncTask = routine.NewRecurringTask(bs.sync, bs.cfg.Interval)
   154  		bs.syncStageTask = routine.NewRecurringTask(bs.syncStageChecker, bs.cfg.Interval)
   155  	}
   156  	atomic.StoreUint64(&bs.syncBlockIncrease, 0)
   157  	return bs, nil
   158  }
   159  
   160  func (bs *blockSyncer) commitBlocks(blks []*peerBlock) bool {
   161  	for _, blk := range blks {
   162  		if blk == nil {
   163  			continue
   164  		}
   165  		err := bs.commitBlockHandler(blk.block)
   166  		if err == nil {
   167  			return true
   168  		}
   169  		bs.blockP2pPeer(blk.pid)
   170  		log.L().Error("failed to commit block", zap.Error(err), zap.Uint64("height", blk.block.Height()), zap.String("peer", blk.pid))
   171  	}
   172  	return false
   173  }
   174  
   175  func (bs *blockSyncer) flushInfo() (time.Time, uint64) {
   176  	bs.mu.Lock()
   177  	defer bs.mu.Unlock()
   178  
   179  	return bs.lastTipUpdateTime, bs.targetHeight
   180  }
   181  
   182  func (bs *blockSyncer) sync() {
   183  	updateTime, targetHeight := bs.flushInfo()
   184  	if updateTime.Add(bs.cfg.Interval).After(time.Now()) {
   185  		return
   186  	}
   187  	intervals := bs.buf.GetBlocksIntervalsToSync(bs.tipHeightHandler(), targetHeight)
   188  	// no sync
   189  	if len(intervals) == 0 {
   190  		return
   191  	}
   192  	// start syncing
   193  	bs.startingHeight = bs.tipHeightHandler()
   194  	log.L().Info("block sync intervals.",
   195  		zap.Any("intervals", intervals),
   196  		zap.Uint64("targetHeight", targetHeight))
   197  	for i, interval := range intervals {
   198  		bs.requestBlock(context.Background(), interval.Start, interval.End, bs.cfg.MaxRepeat-i/bs.cfg.RepeatDecayStep)
   199  	}
   200  }
   201  
   202  func (bs *blockSyncer) requestBlock(ctx context.Context, start uint64, end uint64, repeat int) {
   203  	peers, err := bs.p2pNeighbor()
   204  	if err != nil {
   205  		log.L().Error("failed to get neighbours", zap.Error(err))
   206  		return
   207  	}
   208  	if len(peers) == 0 {
   209  		log.L().Error("no peers")
   210  		return
   211  	}
   212  	if repeat < 2 {
   213  		repeat = 2
   214  	}
   215  	if repeat > len(peers) {
   216  		repeat = len(peers)
   217  	}
   218  	for i := 0; i < repeat; i++ {
   219  		peer := peers[fastrand.Uint32n(uint32(len(peers)))]
   220  		if err := bs.unicastOutbound(
   221  			ctx,
   222  			peer,
   223  			&iotexrpc.BlockSync{Start: start, End: end},
   224  		); err != nil {
   225  			log.L().Error("failed to request blocks", zap.Error(err), zap.String("peer", peer.ID.Pretty()), zap.Uint64("start", start), zap.Uint64("end", end))
   226  		}
   227  	}
   228  }
   229  
   230  func (bs *blockSyncer) TargetHeight() uint64 {
   231  	bs.mu.RLock()
   232  	defer bs.mu.RUnlock()
   233  	return bs.targetHeight
   234  }
   235  
   236  // Start starts a block syncer
   237  func (bs *blockSyncer) Start(ctx context.Context) error {
   238  	log.L().Debug("Starting block syncer.")
   239  	if bs.syncTask != nil {
   240  		if err := bs.syncTask.Start(ctx); err != nil {
   241  			return err
   242  		}
   243  	}
   244  	if bs.syncStageTask != nil {
   245  		return bs.syncStageTask.Start(ctx)
   246  	}
   247  	return nil
   248  }
   249  
   250  // Stop stops a block syncer
   251  func (bs *blockSyncer) Stop(ctx context.Context) error {
   252  	log.L().Debug("Stopping block syncer.")
   253  	if bs.syncStageTask != nil {
   254  		if err := bs.syncStageTask.Stop(ctx); err != nil {
   255  			return err
   256  		}
   257  	}
   258  	if bs.syncTask != nil {
   259  		if err := bs.syncTask.Stop(ctx); err != nil {
   260  			return err
   261  		}
   262  	}
   263  	return nil
   264  }
   265  
   266  func (bs *blockSyncer) ProcessBlock(ctx context.Context, peer string, blk *block.Block) error {
   267  	if blk == nil {
   268  		return errors.New("block is nil")
   269  	}
   270  
   271  	tip := bs.tipHeightHandler()
   272  	added, targetHeight := bs.buf.AddBlock(tip, newPeerBlock(peer, blk))
   273  	bs.mu.Lock()
   274  	defer bs.mu.Unlock()
   275  	if targetHeight > bs.targetHeight {
   276  		bs.targetHeight = targetHeight
   277  	}
   278  	if !added {
   279  		return nil
   280  	}
   281  	syncedHeight := tip
   282  	for {
   283  		if !bs.commitBlocks(bs.buf.Pop(syncedHeight + 1)) {
   284  			break
   285  		}
   286  		syncedHeight++
   287  	}
   288  	bs.buf.Cleanup(syncedHeight)
   289  	log.L().Debug("flush blocks", zap.Uint64("start", tip), zap.Uint64("end", syncedHeight))
   290  	if syncedHeight > bs.lastTip {
   291  		bs.lastTip = syncedHeight
   292  		bs.lastTipUpdateTime = time.Now()
   293  	}
   294  	return nil
   295  }
   296  
   297  func (bs *blockSyncer) ProcessSyncRequest(ctx context.Context, peer peer.AddrInfo, start uint64, end uint64) error {
   298  	tip := bs.tipHeightHandler()
   299  	if end > tip {
   300  		log.L().Debug(
   301  			"Do not have requested blocks",
   302  			zap.Uint64("start", start),
   303  			zap.Uint64("end", end),
   304  			zap.Uint64("tipHeight", tip),
   305  		)
   306  		end = tip
   307  	}
   308  	// TODO: send back multiple blocks in one shot
   309  	for i := start; i <= end; i++ {
   310  		// TODO: fetch block from buffer
   311  		blk, err := bs.blockByHeightHandler(i)
   312  		if err != nil {
   313  			return err
   314  		}
   315  		syncCtx, cancel := context.WithTimeout(ctx, bs.cfg.ProcessSyncRequestTTL)
   316  		defer cancel()
   317  		if err := bs.unicastOutbound(syncCtx, peer, blk.ConvertToBlockPb()); err != nil {
   318  			return err
   319  		}
   320  	}
   321  	return nil
   322  }
   323  
   324  func (bs *blockSyncer) syncStageChecker() {
   325  	tipHeight := bs.tipHeightHandler()
   326  	atomic.StoreUint64(&bs.syncBlockIncrease, tipHeight-bs.syncStageHeight)
   327  	bs.syncStageHeight = tipHeight
   328  }
   329  
   330  func (bs *blockSyncer) SyncStatus() (uint64, uint64, uint64, string) {
   331  	var syncSpeedDesc string
   332  	syncBlockIncrease := atomic.LoadUint64(&bs.syncBlockIncrease)
   333  	switch {
   334  	case syncBlockIncrease == 1:
   335  		syncSpeedDesc = "synced to blockchain tip"
   336  	case bs.cfg.Interval == 0:
   337  		syncSpeedDesc = "no sync task"
   338  	default:
   339  		syncSpeedDesc = fmt.Sprintf("sync in progress at %.1f blocks/sec", float64(syncBlockIncrease)/bs.cfg.Interval.Seconds())
   340  	}
   341  	return bs.startingHeight, bs.tipHeightHandler(), bs.targetHeight, syncSpeedDesc
   342  }
   343  
   344  // BuildReport builds a report of block syncer
   345  func (bs *blockSyncer) BuildReport() string {
   346  	startingHeight, tipHeight, targetHeight, syncSpeedDesc := bs.SyncStatus()
   347  	return fmt.Sprintf(
   348  		"BlockSync startingHeight: %d, tipHeight: %d, targetHeight: %d, %s",
   349  		startingHeight,
   350  		tipHeight,
   351  		targetHeight,
   352  		syncSpeedDesc,
   353  	)
   354  }