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 }