github.com/Finschia/ostracon@v1.1.5/blockchain/v0/reactor.go (about) 1 package v0 2 3 import ( 4 "fmt" 5 "reflect" 6 "time" 7 8 "github.com/gogo/protobuf/proto" 9 10 bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" 11 12 bc "github.com/Finschia/ostracon/blockchain" 13 "github.com/Finschia/ostracon/libs/log" 14 "github.com/Finschia/ostracon/p2p" 15 ocbcproto "github.com/Finschia/ostracon/proto/ostracon/blockchain" 16 sm "github.com/Finschia/ostracon/state" 17 "github.com/Finschia/ostracon/store" 18 "github.com/Finschia/ostracon/types" 19 ) 20 21 const ( 22 // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) 23 BlockchainChannel = byte(0x40) 24 25 trySyncIntervalMS = 10 26 27 // stop syncing when last block's time is 28 // within this much of the system time. 29 // stopSyncingDurationMinutes = 10 30 31 // ask for best height every 10s 32 statusUpdateIntervalSeconds = 10 33 // check if we should switch to consensus reactor 34 switchToConsensusIntervalSeconds = 1 35 ) 36 37 type consensusReactor interface { 38 // for when we switch from blockchain reactor and fast sync to 39 // the consensus machine 40 SwitchToConsensus(state sm.State, skipWAL bool) 41 } 42 43 type peerError struct { 44 err error 45 peerID p2p.ID 46 } 47 48 func (e peerError) Error() string { 49 return fmt.Sprintf("error with peer %v: %s", e.peerID, e.err.Error()) 50 } 51 52 // BlockchainReactor handles long-term catchup syncing. 53 type BlockchainReactor struct { 54 p2p.BaseReactor 55 56 // immutable 57 initialState sm.State 58 59 blockExec *sm.BlockExecutor 60 store *store.BlockStore 61 pool *BlockPool 62 fastSync bool 63 64 requestsCh <-chan BlockRequest 65 errorsCh <-chan peerError 66 } 67 68 // NewBlockchainReactor returns new reactor instance. 69 func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, 70 fastSync bool, async bool, recvBufSize int) *BlockchainReactor { 71 72 if state.LastBlockHeight != store.Height() { 73 panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, 74 store.Height())) 75 } 76 77 requestsCh := make(chan BlockRequest, maxTotalRequesters) 78 79 const capacity = 1000 // must be bigger than peers count 80 errorsCh := make(chan peerError, capacity) // so we don't block in #Receive#pool.AddBlock 81 82 startHeight := store.Height() + 1 83 if startHeight == 1 { 84 startHeight = state.InitialHeight 85 } 86 pool := NewBlockPool(startHeight, requestsCh, errorsCh) 87 88 bcR := &BlockchainReactor{ 89 initialState: state, 90 blockExec: blockExec, 91 store: store, 92 pool: pool, 93 fastSync: fastSync, 94 requestsCh: requestsCh, 95 errorsCh: errorsCh, 96 } 97 bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR, async, recvBufSize) 98 return bcR 99 } 100 101 // SetLogger implements service.Service by setting the logger on reactor and pool. 102 func (bcR *BlockchainReactor) SetLogger(l log.Logger) { 103 bcR.BaseService.Logger = l 104 bcR.pool.Logger = l 105 } 106 107 // OnStart implements service.Service. 108 func (bcR *BlockchainReactor) OnStart() error { 109 // call BaseReactor's OnStart() 110 err := bcR.BaseReactor.OnStart() 111 if err != nil { 112 return err 113 } 114 115 if bcR.fastSync { 116 err = bcR.pool.Start() 117 if err != nil { 118 return err 119 } 120 go bcR.poolRoutine(false) 121 } 122 return nil 123 } 124 125 // SwitchToFastSync is called by the state sync reactor when switching to fast sync. 126 func (bcR *BlockchainReactor) SwitchToFastSync(state sm.State) error { 127 bcR.fastSync = true 128 bcR.initialState = state 129 130 bcR.pool.height = state.LastBlockHeight + 1 131 err := bcR.pool.Start() 132 if err != nil { 133 return err 134 } 135 go bcR.poolRoutine(true) 136 return nil 137 } 138 139 // OnStop implements service.Service. 140 func (bcR *BlockchainReactor) OnStop() { 141 if bcR.fastSync { 142 if err := bcR.pool.Stop(); err != nil { 143 bcR.Logger.Error("Error stopping pool", "err", err) 144 } 145 } 146 } 147 148 // GetChannels implements Reactor 149 func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { 150 return []*p2p.ChannelDescriptor{ 151 { 152 ID: BlockchainChannel, 153 Priority: 5, 154 SendQueueCapacity: 1000, 155 RecvBufferCapacity: 50 * 4096, 156 RecvMessageCapacity: bc.MaxMsgSize, 157 MessageType: &ocbcproto.Message{}, 158 }, 159 } 160 } 161 162 // AddPeer implements Reactor by sending our state to peer. 163 func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { 164 p2p.SendEnvelopeShim(peer, p2p.Envelope{ //nolint: staticcheck 165 ChannelID: BlockchainChannel, 166 Message: &bcproto.StatusResponse{ 167 Base: bcR.store.Base(), 168 Height: bcR.store.Height(), 169 }, 170 }, bcR.Logger) 171 // it's OK if send fails. will try later in poolRoutine 172 173 // peer is added to the pool once we receive the first 174 // bcStatusResponseMessage from the peer and call pool.SetPeerRange 175 } 176 177 // RemovePeer implements Reactor by removing peer from the pool. 178 func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { 179 bcR.pool.RemovePeer(peer.ID()) 180 } 181 182 // respondToPeer loads a block and sends it to the requesting peer, 183 // if we have it. Otherwise, we'll respond saying we don't have it. 184 func (bcR *BlockchainReactor) respondToPeer(msg *bcproto.BlockRequest, 185 src p2p.Peer) (queued bool) { 186 187 block := bcR.store.LoadBlock(msg.Height) 188 if block != nil { 189 bl, err := block.ToProto() 190 if err != nil { 191 bcR.Logger.Error("could not convert msg to protobuf", "err", err) 192 return false 193 } 194 return p2p.TrySendEnvelopeShim(src, p2p.Envelope{ //nolint: staticcheck 195 ChannelID: BlockchainChannel, 196 Message: &ocbcproto.BlockResponse{Block: bl}, 197 }, bcR.Logger) 198 } 199 200 return p2p.TrySendEnvelopeShim(src, p2p.Envelope{ //nolint: staticcheck 201 ChannelID: BlockchainChannel, 202 Message: &bcproto.NoBlockResponse{Height: msg.Height}, 203 }, bcR.Logger) 204 } 205 206 func (bcR *BlockchainReactor) ReceiveEnvelope(e p2p.Envelope) { 207 if err := bc.ValidateMsg(e.Message); err != nil { 208 bcR.Logger.Error("Peer sent us invalid msg", "peer", e.Src, "msg", e.Message, "err", err) 209 bcR.Switch.StopPeerForError(e.Src, err) 210 return 211 } 212 213 bcR.Logger.Debug("Receive", "e.Src", e.Src, "chID", e.ChannelID, "msg", e.Message) 214 215 switch msg := e.Message.(type) { 216 case *bcproto.BlockRequest: 217 bcR.respondToPeer(msg, e.Src) 218 case *ocbcproto.BlockResponse: 219 bi, err := types.BlockFromProto(msg.Block) 220 if err != nil { 221 bcR.Logger.Error("Block content is invalid", "err", err) 222 return 223 } 224 bcR.pool.AddBlock(e.Src.ID(), bi, msg.Block.Size()) 225 case *bcproto.StatusRequest: 226 // Send peer our state. 227 p2p.TrySendEnvelopeShim(e.Src, p2p.Envelope{ //nolint: staticcheck 228 ChannelID: BlockchainChannel, 229 Message: &bcproto.StatusResponse{ 230 Height: bcR.store.Height(), 231 Base: bcR.store.Base(), 232 }, 233 }, bcR.Logger) 234 case *bcproto.StatusResponse: 235 // Got a peer status. Unverified. 236 bcR.pool.SetPeerRange(e.Src.ID(), msg.Base, msg.Height) 237 case *bcproto.NoBlockResponse: 238 bcR.Logger.Debug("Peer does not have requested block", "peer", e.Src, "height", msg.Height) 239 default: 240 bcR.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) 241 } 242 } 243 244 func (bcR *BlockchainReactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) { 245 msg := &ocbcproto.Message{} 246 err := proto.Unmarshal(msgBytes, msg) 247 if err != nil { 248 panic(err) 249 } 250 uw, err := msg.Unwrap() 251 if err != nil { 252 panic(err) 253 } 254 bcR.ReceiveEnvelope(p2p.Envelope{ 255 ChannelID: chID, 256 Src: peer, 257 Message: uw, 258 }) 259 } 260 261 // Handle messages from the poolReactor telling the reactor what to do. 262 // NOTE: Don't sleep in the FOR_LOOP or otherwise slow it down! 263 func (bcR *BlockchainReactor) poolRoutine(stateSynced bool) { 264 265 trySyncTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) 266 defer trySyncTicker.Stop() 267 268 statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) 269 defer statusUpdateTicker.Stop() 270 271 switchToConsensusTicker := time.NewTicker(switchToConsensusIntervalSeconds * time.Second) 272 defer switchToConsensusTicker.Stop() 273 274 blocksSynced := uint64(0) 275 276 chainID := bcR.initialState.ChainID 277 state := bcR.initialState 278 279 lastHundred := time.Now() 280 lastRate := 0.0 281 282 didProcessCh := make(chan struct{}, 1) 283 284 go func() { 285 for { 286 select { 287 case <-bcR.Quit(): 288 return 289 case <-bcR.pool.Quit(): 290 return 291 case request := <-bcR.requestsCh: 292 peer := bcR.Switch.Peers().Get(request.PeerID) 293 if peer == nil { 294 continue 295 } 296 queued := p2p.TrySendEnvelopeShim(peer, p2p.Envelope{ //nolint: staticcheck 297 ChannelID: BlockchainChannel, 298 Message: &bcproto.BlockRequest{Height: request.Height}, 299 }, bcR.Logger) 300 if !queued { 301 bcR.Logger.Debug("Send queue is full, drop block request", "peer", peer.ID(), "height", request.Height) 302 } 303 case err := <-bcR.errorsCh: 304 peer := bcR.Switch.Peers().Get(err.peerID) 305 if peer != nil { 306 bcR.Switch.StopPeerForError(peer, err) 307 } 308 309 case <-statusUpdateTicker.C: 310 // ask for status updates 311 go bcR.BroadcastStatusRequest() // nolint: errcheck 312 313 } 314 } 315 }() 316 317 FOR_LOOP: 318 for { 319 select { 320 case <-switchToConsensusTicker.C: 321 height, numPending, lenRequesters := bcR.pool.GetStatus() 322 outbound, inbound, _ := bcR.Switch.NumPeers() 323 bcR.Logger.Debug("Consensus ticker", "numPending", numPending, "total", lenRequesters, 324 "outbound", outbound, "inbound", inbound) 325 if bcR.pool.IsCaughtUp() { 326 bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) 327 if err := bcR.pool.Stop(); err != nil { 328 bcR.Logger.Error("Error stopping pool", "err", err) 329 } 330 conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) 331 if ok { 332 conR.SwitchToConsensus(state, blocksSynced > 0 || stateSynced) 333 } 334 // else { 335 // should only happen during testing 336 // } 337 338 break FOR_LOOP 339 } 340 341 case <-trySyncTicker.C: // chan time 342 select { 343 case didProcessCh <- struct{}{}: 344 default: 345 } 346 347 case <-didProcessCh: 348 // NOTE: It is a subtle mistake to process more than a single block 349 // at a time (e.g. 10) here, because we only TrySend 1 request per 350 // loop. The ratio mismatch can result in starving of blocks, a 351 // sudden burst of requests and responses, and repeat. 352 // Consequently, it is better to split these routines rather than 353 // coupling them as it's written here. TODO uncouple from request 354 // routine. 355 356 // See if there are any blocks to sync. 357 first, second := bcR.pool.PeekTwoBlocks() 358 // bcR.Logger.Info("TrySync peeked", "first", first, "second", second) 359 if first == nil || second == nil { 360 // We need both to sync the first block. 361 continue FOR_LOOP 362 } else { 363 // Try again quickly next loop. 364 didProcessCh <- struct{}{} 365 } 366 367 firstParts := first.MakePartSet(types.BlockPartSizeBytes) 368 firstPartSetHeader := firstParts.Header() 369 firstID := types.BlockID{Hash: first.Hash(), PartSetHeader: firstPartSetHeader} 370 // Finally, verify the first block using the second's commit 371 // NOTE: we can probably make this more efficient, but note that calling 372 // first.Hash() doesn't verify the tx contents, so MakePartSet() is 373 // currently necessary. 374 err := state.Validators.VerifyCommitLight(chainID, firstID, first.Height, second.LastCommit) 375 if err == nil { 376 // validate the block before we persist it 377 err = bcR.blockExec.ValidateBlock(state, first.Round, first) 378 } 379 380 // If either of the checks failed we log the error and request for a new block 381 // at that height 382 if err != nil { 383 bcR.Logger.Error("Error in validation", "err", err) 384 peerID := bcR.pool.RedoRequest(first.Height) 385 peer := bcR.Switch.Peers().Get(peerID) 386 if peer != nil { 387 // NOTE: we've already removed the peer's request, but we 388 // still need to clean up the rest. 389 bcR.Switch.StopPeerForError(peer, fmt.Errorf("blockchainReactor validation error: %v", err)) 390 } 391 peerID2 := bcR.pool.RedoRequest(second.Height) 392 peer2 := bcR.Switch.Peers().Get(peerID2) 393 if peer2 != nil && peer2 != peer { 394 // NOTE: we've already removed the peer's request, but we 395 // still need to clean up the rest. 396 bcR.Switch.StopPeerForError(peer2, fmt.Errorf("blockchainReactor validation error: %v", err)) 397 } 398 continue FOR_LOOP 399 } 400 401 bcR.pool.PopRequest() 402 403 // TODO: batch saves so we dont persist to disk every block 404 bcR.store.SaveBlock(first, firstParts, second.LastCommit) 405 406 // TODO: same thing for app - but we would need a way to 407 // get the hash without persisting the state 408 state, _, err = bcR.blockExec.ApplyBlock(state, firstID, first, nil) 409 if err != nil { 410 // TODO This is bad, are we zombie? 411 panic(fmt.Sprintf("Failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) 412 } 413 blocksSynced++ 414 415 if blocksSynced%100 == 0 { 416 lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) 417 bcR.Logger.Info("Fast Sync Rate", "height", bcR.pool.height, 418 "max_peer_height", bcR.pool.MaxPeerHeight(), "blocks/s", lastRate) 419 lastHundred = time.Now() 420 } 421 422 continue FOR_LOOP 423 424 case <-bcR.Quit(): 425 break FOR_LOOP 426 } 427 } 428 } 429 430 // BroadcastStatusRequest broadcasts `BlockStore` base and height. 431 func (bcR *BlockchainReactor) BroadcastStatusRequest() error { 432 bcR.Switch.BroadcastEnvelope(p2p.Envelope{ 433 ChannelID: BlockchainChannel, 434 Message: &bcproto.StatusRequest{}, 435 }) 436 return nil 437 }