diff --git a/behavior/doc.go b/behavior/doc.go deleted file mode 100644 index c4bd06cce..000000000 --- a/behavior/doc.go +++ /dev/null @@ -1,42 +0,0 @@ -/* -Package Behavior provides a mechanism for reactors to report behavior of peers. - -Instead of a reactor calling the switch directly it will call the behavior module which will -handle the stoping and marking peer as good on behalf of the reactor. - -There are four different behaviors a reactor can report. - -1. bad message - -type badMessage struct { - explanation string -} - -This message will request the peer be stopped for an error - -2. message out of order - -type messageOutOfOrder struct { - explanation string -} - -This message will request the peer be stopped for an error - -3. consesnsus Vote - -type consensusVote struct { - explanation string -} - -This message will request the peer be marked as good - -4. block part - -type blockPart struct { - explanation string -} - -This message will request the peer be marked as good - -*/ -package behavior diff --git a/behavior/peer_behavior.go b/behavior/peer_behavior.go deleted file mode 100644 index c58ef9aca..000000000 --- a/behavior/peer_behavior.go +++ /dev/null @@ -1,49 +0,0 @@ -package behavior - -import ( - "github.com/tendermint/tendermint/p2p" -) - -// PeerBehavior is a struct describing a behavior a peer performed. -// `peerID` identifies the peer and reason characterizes the specific -// behavior performed by the peer. -type PeerBehavior struct { - peerID p2p.ID - reason interface{} -} - -type badMessage struct { - explanation string -} - -// BadMessage returns a badMessage PeerBehavior. -func BadMessage(peerID p2p.ID, explanation string) PeerBehavior { - return PeerBehavior{peerID: peerID, reason: badMessage{explanation}} -} - -type messageOutOfOrder struct { - explanation string -} - -// MessageOutOfOrder returns a messagOutOfOrder PeerBehavior. -func MessageOutOfOrder(peerID p2p.ID, explanation string) PeerBehavior { - return PeerBehavior{peerID: peerID, reason: messageOutOfOrder{explanation}} -} - -type consensusVote struct { - explanation string -} - -// ConsensusVote returns a consensusVote PeerBehavior. -func ConsensusVote(peerID p2p.ID, explanation string) PeerBehavior { - return PeerBehavior{peerID: peerID, reason: consensusVote{explanation}} -} - -type blockPart struct { - explanation string -} - -// BlockPart returns blockPart PeerBehavior. -func BlockPart(peerID p2p.ID, explanation string) PeerBehavior { - return PeerBehavior{peerID: peerID, reason: blockPart{explanation}} -} diff --git a/behavior/reporter.go b/behavior/reporter.go deleted file mode 100644 index b85c88437..000000000 --- a/behavior/reporter.go +++ /dev/null @@ -1,86 +0,0 @@ -package behavior - -import ( - "errors" - - tmsync "github.com/tendermint/tendermint/libs/sync" - "github.com/tendermint/tendermint/p2p" -) - -// Reporter provides an interface for reactors to report the behavior -// of peers synchronously to other components. -type Reporter interface { - Report(behavior PeerBehavior) error -} - -// SwitchReporter reports peer behavior to an internal Switch. -type SwitchReporter struct { - sw *p2p.Switch -} - -// NewSwitchReporter return a new SwitchReporter instance which wraps the Switch. -func NewSwitchReporter(sw *p2p.Switch) *SwitchReporter { - return &SwitchReporter{ - sw: sw, - } -} - -// Report reports the behavior of a peer to the Switch. -func (spbr *SwitchReporter) Report(behavior PeerBehavior) error { - peer := spbr.sw.Peers().Get(behavior.peerID) - if peer == nil { - return errors.New("peer not found") - } - - switch reason := behavior.reason.(type) { - case consensusVote, blockPart: - spbr.sw.MarkPeerAsGood(peer) - case badMessage: - spbr.sw.StopPeerForError(peer, reason.explanation) - case messageOutOfOrder: - spbr.sw.StopPeerForError(peer, reason.explanation) - default: - return errors.New("unknown reason reported") - } - - return nil -} - -// MockReporter is a concrete implementation of the Reporter -// interface used in reactor tests to ensure reactors report the correct -// behavior in manufactured scenarios. -type MockReporter struct { - mtx tmsync.RWMutex - pb map[p2p.ID][]PeerBehavior -} - -// NewMockReporter returns a Reporter which records all reported -// behaviors in memory. -func NewMockReporter() *MockReporter { - return &MockReporter{ - pb: map[p2p.ID][]PeerBehavior{}, - } -} - -// Report stores the PeerBehavior produced by the peer identified by peerID. -func (mpbr *MockReporter) Report(behavior PeerBehavior) error { - mpbr.mtx.Lock() - defer mpbr.mtx.Unlock() - mpbr.pb[behavior.peerID] = append(mpbr.pb[behavior.peerID], behavior) - - return nil -} - -// GetBehaviours returns all behaviors reported on the peer identified by peerID. -func (mpbr *MockReporter) GetBehaviours(peerID p2p.ID) []PeerBehavior { - mpbr.mtx.RLock() - defer mpbr.mtx.RUnlock() - if items, ok := mpbr.pb[peerID]; ok { - result := make([]PeerBehavior, len(items)) - copy(result, items) - - return result - } - - return []PeerBehavior{} -} diff --git a/behavior/reporter_test.go b/behavior/reporter_test.go deleted file mode 100644 index ed07310bd..000000000 --- a/behavior/reporter_test.go +++ /dev/null @@ -1,205 +0,0 @@ -package behavior_test - -import ( - "sync" - "testing" - - bh "github.com/tendermint/tendermint/behavior" - "github.com/tendermint/tendermint/p2p" -) - -// TestMockReporter tests the MockReporter's ability to store reported -// peer behavior in memory indexed by the peerID. -func TestMockReporter(t *testing.T) { - var peerID p2p.ID = "MockPeer" - pr := bh.NewMockReporter() - - behaviors := pr.GetBehaviours(peerID) - if len(behaviors) != 0 { - t.Error("Expected to have no behaviors reported") - } - - badMessage := bh.BadMessage(peerID, "bad message") - if err := pr.Report(badMessage); err != nil { - t.Error(err) - } - behaviors = pr.GetBehaviours(peerID) - if len(behaviors) != 1 { - t.Error("Expected the peer have one reported behavior") - } - - if behaviors[0] != badMessage { - t.Error("Expected Bad Message to have been reported") - } -} - -type scriptItem struct { - peerID p2p.ID - behavior bh.PeerBehavior -} - -// equalBehaviours returns true if a and b contain the same PeerBehaviors with -// the same freequencies and otherwise false. -func equalBehaviours(a []bh.PeerBehavior, b []bh.PeerBehavior) bool { - aHistogram := map[bh.PeerBehavior]int{} - bHistogram := map[bh.PeerBehavior]int{} - - for _, behavior := range a { - aHistogram[behavior]++ - } - - for _, behavior := range b { - bHistogram[behavior]++ - } - - if len(aHistogram) != len(bHistogram) { - return false - } - - for _, behavior := range a { - if aHistogram[behavior] != bHistogram[behavior] { - return false - } - } - - for _, behavior := range b { - if bHistogram[behavior] != aHistogram[behavior] { - return false - } - } - - return true -} - -// TestEqualPeerBehaviors tests that equalBehaviours can tell that two slices -// of peer behaviors can be compared for the behaviors they contain and the -// freequencies that those behaviors occur. -func TestEqualPeerBehaviors(t *testing.T) { - var ( - peerID p2p.ID = "MockPeer" - consensusVote = bh.ConsensusVote(peerID, "voted") - blockPart = bh.BlockPart(peerID, "blocked") - equals = []struct { - left []bh.PeerBehavior - right []bh.PeerBehavior - }{ - // Empty sets - {[]bh.PeerBehavior{}, []bh.PeerBehavior{}}, - // Single behaviors - {[]bh.PeerBehavior{consensusVote}, []bh.PeerBehavior{consensusVote}}, - // Equal Frequencies - {[]bh.PeerBehavior{consensusVote, consensusVote}, - []bh.PeerBehavior{consensusVote, consensusVote}}, - // Equal frequencies different orders - {[]bh.PeerBehavior{consensusVote, blockPart}, - []bh.PeerBehavior{blockPart, consensusVote}}, - } - unequals = []struct { - left []bh.PeerBehavior - right []bh.PeerBehavior - }{ - // Comparing empty sets to non empty sets - {[]bh.PeerBehavior{}, []bh.PeerBehavior{consensusVote}}, - // Different behaviors - {[]bh.PeerBehavior{consensusVote}, []bh.PeerBehavior{blockPart}}, - // Same behavior with different frequencies - {[]bh.PeerBehavior{consensusVote}, - []bh.PeerBehavior{consensusVote, consensusVote}}, - } - ) - - for _, test := range equals { - if !equalBehaviours(test.left, test.right) { - t.Errorf("expected %#v and %#v to be equal", test.left, test.right) - } - } - - for _, test := range unequals { - if equalBehaviours(test.left, test.right) { - t.Errorf("expected %#v and %#v to be unequal", test.left, test.right) - } - } -} - -// TestPeerBehaviorConcurrency constructs a scenario in which -// multiple goroutines are using the same MockReporter instance. -// This test reproduces the conditions in which MockReporter will -// be used within a Reactor `Receive` method tests to ensure thread safety. -func TestMockPeerBehaviorReporterConcurrency(t *testing.T) { - var ( - behaviorScript = []struct { - peerID p2p.ID - behaviors []bh.PeerBehavior - }{ - {"1", []bh.PeerBehavior{bh.ConsensusVote("1", "")}}, - {"2", []bh.PeerBehavior{bh.ConsensusVote("2", ""), bh.ConsensusVote("2", ""), bh.ConsensusVote("2", "")}}, - { - "3", - []bh.PeerBehavior{bh.BlockPart("3", ""), - bh.ConsensusVote("3", ""), - bh.BlockPart("3", ""), - bh.ConsensusVote("3", "")}}, - { - "4", - []bh.PeerBehavior{bh.ConsensusVote("4", ""), - bh.ConsensusVote("4", ""), - bh.ConsensusVote("4", ""), - bh.ConsensusVote("4", "")}}, - { - "5", - []bh.PeerBehavior{bh.BlockPart("5", ""), - bh.ConsensusVote("5", ""), - bh.BlockPart("5", ""), - bh.ConsensusVote("5", "")}}, - } - ) - - var receiveWg sync.WaitGroup - pr := bh.NewMockReporter() - scriptItems := make(chan scriptItem) - done := make(chan int) - numConsumers := 3 - for i := 0; i < numConsumers; i++ { - receiveWg.Add(1) - go func() { - defer receiveWg.Done() - for { - select { - case pb := <-scriptItems: - if err := pr.Report(pb.behavior); err != nil { - t.Error(err) - } - case <-done: - return - } - } - }() - } - - var sendingWg sync.WaitGroup - sendingWg.Add(1) - go func() { - defer sendingWg.Done() - for _, item := range behaviorScript { - for _, reason := range item.behaviors { - scriptItems <- scriptItem{item.peerID, reason} - } - } - }() - - sendingWg.Wait() - - for i := 0; i < numConsumers; i++ { - done <- 1 - } - - receiveWg.Wait() - - for _, items := range behaviorScript { - reported := pr.GetBehaviours(items.peerID) - if !equalBehaviours(reported, items.behaviors) { - t.Errorf("expected peer %s to have behaved \nExpected: %#v \nGot %#v \n", - items.peerID, items.behaviors, reported) - } - } -} diff --git a/blockchain/v0/pool.go b/blockchain/pool.go similarity index 99% rename from blockchain/v0/pool.go rename to blockchain/pool.go index 69e0b55c4..ceab887ad 100644 --- a/blockchain/v0/pool.go +++ b/blockchain/pool.go @@ -1,4 +1,4 @@ -package v0 +package blockchain import ( "errors" diff --git a/blockchain/v0/pool_test.go b/blockchain/pool_test.go similarity index 99% rename from blockchain/v0/pool_test.go rename to blockchain/pool_test.go index 1653fe74a..c67da0afd 100644 --- a/blockchain/v0/pool_test.go +++ b/blockchain/pool_test.go @@ -1,4 +1,4 @@ -package v0 +package blockchain import ( "fmt" diff --git a/blockchain/v0/reactor.go b/blockchain/reactor.go similarity index 85% rename from blockchain/v0/reactor.go rename to blockchain/reactor.go index c14d2d34a..06a77de4d 100644 --- a/blockchain/v0/reactor.go +++ b/blockchain/reactor.go @@ -1,11 +1,10 @@ -package v0 +package blockchain import ( "fmt" "reflect" "time" - bc "github.com/tendermint/tendermint/blockchain" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" @@ -45,8 +44,8 @@ func (e peerError) Error() string { return fmt.Sprintf("error with peer %v: %s", e.peerID, e.err.Error()) } -// BlockchainReactor handles long-term catchup syncing. -type BlockchainReactor struct { +// Reactor handles long-term catchup syncing. +type Reactor struct { p2p.BaseReactor // immutable @@ -61,9 +60,9 @@ type BlockchainReactor struct { errorsCh <-chan peerError } -// NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, - fastSync bool) *BlockchainReactor { +// NewReactor returns new reactor instance. +func NewReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, + fastSync bool) *Reactor { if state.LastBlockHeight != store.Height() { panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, @@ -81,7 +80,7 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *st } pool := NewBlockPool(startHeight, requestsCh, errorsCh) - bcR := &BlockchainReactor{ + bcR := &Reactor{ initialState: state, blockExec: blockExec, store: store, @@ -90,18 +89,18 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *st requestsCh: requestsCh, errorsCh: errorsCh, } - bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) + bcR.BaseReactor = *p2p.NewBaseReactor("Reactor", bcR) return bcR } // SetLogger implements service.Service by setting the logger on reactor and pool. -func (bcR *BlockchainReactor) SetLogger(l log.Logger) { +func (bcR *Reactor) SetLogger(l log.Logger) { bcR.BaseService.Logger = l bcR.pool.Logger = l } // OnStart implements service.Service. -func (bcR *BlockchainReactor) OnStart() error { +func (bcR *Reactor) OnStart() error { if bcR.fastSync { err := bcR.pool.Start() if err != nil { @@ -113,7 +112,7 @@ func (bcR *BlockchainReactor) OnStart() error { } // SwitchToFastSync is called by the state sync reactor when switching to fast sync. -func (bcR *BlockchainReactor) SwitchToFastSync(state sm.State) error { +func (bcR *Reactor) SwitchToFastSync(state sm.State) error { bcR.fastSync = true bcR.initialState = state @@ -127,7 +126,7 @@ func (bcR *BlockchainReactor) SwitchToFastSync(state sm.State) error { } // OnStop implements service.Service. -func (bcR *BlockchainReactor) OnStop() { +func (bcR *Reactor) OnStop() { if bcR.fastSync { if err := bcR.pool.Stop(); err != nil { bcR.Logger.Error("Error stopping pool", "err", err) @@ -136,21 +135,21 @@ func (bcR *BlockchainReactor) OnStop() { } // GetChannels implements Reactor -func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { +func (bcR *Reactor) GetChannels() []*p2p.ChannelDescriptor { return []*p2p.ChannelDescriptor{ { ID: BlockchainChannel, Priority: 5, SendQueueCapacity: 1000, RecvBufferCapacity: 50 * 4096, - RecvMessageCapacity: bc.MaxMsgSize, + RecvMessageCapacity: MaxMsgSize, }, } } // AddPeer implements Reactor by sending our state to peer. -func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { - msgBytes, err := bc.EncodeMsg(&bcproto.StatusResponse{ +func (bcR *Reactor) AddPeer(peer p2p.Peer) { + msgBytes, err := EncodeMsg(&bcproto.StatusResponse{ Base: bcR.store.Base(), Height: bcR.store.Height()}) if err != nil { @@ -166,13 +165,13 @@ func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { } // RemovePeer implements Reactor by removing peer from the pool. -func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { +func (bcR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { bcR.pool.RemovePeer(peer.ID()) } // respondToPeer loads a block and sends it to the requesting peer, // if we have it. Otherwise, we'll respond saying we don't have it. -func (bcR *BlockchainReactor) respondToPeer(msg *bcproto.BlockRequest, +func (bcR *Reactor) respondToPeer(msg *bcproto.BlockRequest, src p2p.Peer) (queued bool) { block := bcR.store.LoadBlock(msg.Height) @@ -183,7 +182,7 @@ func (bcR *BlockchainReactor) respondToPeer(msg *bcproto.BlockRequest, return false } - msgBytes, err := bc.EncodeMsg(&bcproto.BlockResponse{Block: bl}) + msgBytes, err := EncodeMsg(&bcproto.BlockResponse{Block: bl}) if err != nil { bcR.Logger.Error("could not marshal msg", "err", err) return false @@ -194,7 +193,7 @@ func (bcR *BlockchainReactor) respondToPeer(msg *bcproto.BlockRequest, bcR.Logger.Info("Peer asking for a block we don't have", "src", src, "height", msg.Height) - msgBytes, err := bc.EncodeMsg(&bcproto.NoBlockResponse{Height: msg.Height}) + msgBytes, err := EncodeMsg(&bcproto.NoBlockResponse{Height: msg.Height}) if err != nil { bcR.Logger.Error("could not convert msg to protobuf", "err", err) return false @@ -204,15 +203,15 @@ func (bcR *BlockchainReactor) respondToPeer(msg *bcproto.BlockRequest, } // Receive implements Reactor by handling 4 types of messages (look below). -func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := bc.DecodeMsg(msgBytes) +func (bcR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := DecodeMsg(msgBytes) if err != nil { bcR.Logger.Error("Error decoding message", "src", src, "chId", chID, "err", err) bcR.Switch.StopPeerForError(src, err) return } - if err = bc.ValidateMsg(msg); err != nil { + if err = ValidateMsg(msg); err != nil { bcR.Logger.Error("Peer sent us invalid msg", "peer", src, "msg", msg, "err", err) bcR.Switch.StopPeerForError(src, err) return @@ -232,7 +231,7 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) bcR.pool.AddBlock(src.ID(), bi, len(msgBytes)) case *bcproto.StatusRequest: // Send peer our state. - msgBytes, err := bc.EncodeMsg(&bcproto.StatusResponse{ + msgBytes, err := EncodeMsg(&bcproto.StatusResponse{ Height: bcR.store.Height(), Base: bcR.store.Base(), }) @@ -253,7 +252,7 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) // Handle messages from the poolReactor telling the reactor what to do. // NOTE: Don't sleep in the FOR_LOOP or otherwise slow it down! -func (bcR *BlockchainReactor) poolRoutine(stateSynced bool) { +func (bcR *Reactor) poolRoutine(stateSynced bool) { trySyncTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) defer trySyncTicker.Stop() @@ -286,7 +285,7 @@ func (bcR *BlockchainReactor) poolRoutine(stateSynced bool) { if peer == nil { continue } - msgBytes, err := bc.EncodeMsg(&bcproto.BlockRequest{Height: request.Height}) + msgBytes, err := EncodeMsg(&bcproto.BlockRequest{Height: request.Height}) if err != nil { bcR.Logger.Error("could not convert msg to proto", "err", err) continue @@ -382,14 +381,14 @@ FOR_LOOP: if peer != nil { // NOTE: we've already removed the peer's request, but we // still need to clean up the rest. - bcR.Switch.StopPeerForError(peer, fmt.Errorf("blockchainReactor validation error: %v", err)) + bcR.Switch.StopPeerForError(peer, fmt.Errorf("Reactor validation error: %v", err)) } peerID2 := bcR.pool.RedoRequest(second.Height) peer2 := bcR.Switch.Peers().Get(peerID2) if peer2 != nil && peer2 != peer { // NOTE: we've already removed the peer's request, but we // still need to clean up the rest. - bcR.Switch.StopPeerForError(peer2, fmt.Errorf("blockchainReactor validation error: %v", err)) + bcR.Switch.StopPeerForError(peer2, fmt.Errorf("Reactor validation error: %v", err)) } continue FOR_LOOP } @@ -424,8 +423,8 @@ FOR_LOOP: } // BroadcastStatusRequest broadcasts `BlockStore` base and height. -func (bcR *BlockchainReactor) BroadcastStatusRequest() error { - bm, err := bc.EncodeMsg(&bcproto.StatusRequest{}) +func (bcR *Reactor) BroadcastStatusRequest() error { + bm, err := EncodeMsg(&bcproto.StatusRequest{}) if err != nil { bcR.Logger.Error("could not convert msg to proto", "err", err) return fmt.Errorf("could not convert msg to proto: %w", err) diff --git a/blockchain/v0/reactor_test.go b/blockchain/reactor_test.go similarity index 87% rename from blockchain/v0/reactor_test.go rename to blockchain/reactor_test.go index a88b499f4..7625a1bfa 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/reactor_test.go @@ -1,4 +1,4 @@ -package v0 +package blockchain import ( "fmt" @@ -46,16 +46,16 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G }, privValidators } -type BlockchainReactorPair struct { - reactor *BlockchainReactor +type ReactorPair struct { + reactor *Reactor app proxy.AppConns } -func newBlockchainReactor( +func newReactor( logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, - maxBlockHeight int64) BlockchainReactorPair { + maxBlockHeight int64) ReactorPair { if len(privVals) != 1 { panic("only support one validator") } @@ -78,7 +78,7 @@ func newBlockchainReactor( panic(fmt.Errorf("error constructing state from genesis file: %w", err)) } - // Make the BlockchainReactor itself. + // Make the Reactor itself. // NOTE we have to create and commit the blocks first because // pool.height is determined from the store. fastSync := true @@ -125,10 +125,10 @@ func newBlockchainReactor( blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor := NewReactor(state.Copy(), blockExec, blockStore, fastSync) bcReactor.SetLogger(logger.With("module", "blockchain")) - return BlockchainReactorPair{bcReactor, proxyApp} + return ReactorPair{bcReactor, proxyApp} } func TestNoBlockResponse(t *testing.T) { @@ -138,10 +138,10 @@ func TestNoBlockResponse(t *testing.T) { maxBlockHeight := int64(65) - reactorPairs := make([]BlockchainReactorPair, 2) + reactorPairs := make([]ReactorPair, 2) - reactorPairs[0] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) - reactorPairs[1] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) + reactorPairs[0] = newReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) + reactorPairs[1] = newReactor(log.TestingLogger(), genDoc, privVals, 0) p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) @@ -202,7 +202,7 @@ func TestBadBlockStopsPeer(t *testing.T) { // Other chain needs a different validator set otherGenDoc, otherPrivVals := randGenesisDoc(1, false, 30) - otherChain := newBlockchainReactor(log.TestingLogger(), otherGenDoc, otherPrivVals, maxBlockHeight) + otherChain := newReactor(log.TestingLogger(), otherGenDoc, otherPrivVals, maxBlockHeight) defer func() { err := otherChain.reactor.Stop() @@ -211,12 +211,12 @@ func TestBadBlockStopsPeer(t *testing.T) { require.NoError(t, err) }() - reactorPairs := make([]BlockchainReactorPair, 4) + reactorPairs := make([]ReactorPair, 4) - reactorPairs[0] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) - reactorPairs[1] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) - reactorPairs[2] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) - reactorPairs[3] = newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) + reactorPairs[0] = newReactor(log.TestingLogger(), genDoc, privVals, maxBlockHeight) + reactorPairs[1] = newReactor(log.TestingLogger(), genDoc, privVals, 0) + reactorPairs[2] = newReactor(log.TestingLogger(), genDoc, privVals, 0) + reactorPairs[3] = newReactor(log.TestingLogger(), genDoc, privVals, 0) switches := p2p.MakeConnectedSwitches(config.P2P, 4, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("BLOCKCHAIN", reactorPairs[i].reactor) @@ -254,7 +254,7 @@ func TestBadBlockStopsPeer(t *testing.T) { // race, but can't be easily avoided. reactorPairs[3].reactor.store = otherChain.reactor.store - lastReactorPair := newBlockchainReactor(log.TestingLogger(), genDoc, privVals, 0) + lastReactorPair := newReactor(log.TestingLogger(), genDoc, privVals, 0) reactorPairs = append(reactorPairs, lastReactorPair) switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { diff --git a/blockchain/v1/peer.go b/blockchain/v1/peer.go deleted file mode 100644 index ad26585b3..000000000 --- a/blockchain/v1/peer.go +++ /dev/null @@ -1,211 +0,0 @@ -package v1 - -import ( - "fmt" - "math" - "time" - - flow "github.com/tendermint/tendermint/libs/flowrate" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -//-------- -// Peer - -// BpPeerParams stores the peer parameters that are used when creating a peer. -type BpPeerParams struct { - timeout time.Duration - minRecvRate int64 - sampleRate time.Duration - windowSize time.Duration -} - -// BpPeer is the datastructure associated with a fast sync peer. -type BpPeer struct { - logger log.Logger - ID p2p.ID - - Base int64 // the peer reported base - Height int64 // the peer reported height - NumPendingBlockRequests int // number of requests still waiting for block responses - blocks map[int64]*types.Block // blocks received or expected to be received from this peer - blockResponseTimer *time.Timer - recvMonitor *flow.Monitor - params *BpPeerParams // parameters for timer and monitor - - onErr func(err error, peerID p2p.ID) // function to call on error -} - -// NewBpPeer creates a new peer. -func NewBpPeer(peerID p2p.ID, base int64, height int64, - onErr func(err error, peerID p2p.ID), params *BpPeerParams) *BpPeer { - - if params == nil { - params = BpPeerDefaultParams() - } - return &BpPeer{ - ID: peerID, - Base: base, - Height: height, - blocks: make(map[int64]*types.Block, maxRequestsPerPeer), - logger: log.NewNopLogger(), - onErr: onErr, - params: params, - } -} - -// String returns a string representation of a peer. -func (peer *BpPeer) String() string { - return fmt.Sprintf("peer: %v height: %v pending: %v", peer.ID, peer.Height, peer.NumPendingBlockRequests) -} - -// SetLogger sets the logger of the peer. -func (peer *BpPeer) SetLogger(l log.Logger) { - peer.logger = l -} - -// Cleanup performs cleanup of the peer, removes blocks, requests, stops timer and monitor. -func (peer *BpPeer) Cleanup() { - if peer.blockResponseTimer != nil { - peer.blockResponseTimer.Stop() - } - if peer.NumPendingBlockRequests != 0 { - peer.logger.Info("peer with pending requests is being cleaned", "peer", peer.ID) - } - if len(peer.blocks)-peer.NumPendingBlockRequests != 0 { - peer.logger.Info("peer with pending blocks is being cleaned", "peer", peer.ID) - } - for h := range peer.blocks { - delete(peer.blocks, h) - } - peer.NumPendingBlockRequests = 0 - peer.recvMonitor = nil -} - -// BlockAtHeight returns the block at a given height if available and errMissingBlock otherwise. -func (peer *BpPeer) BlockAtHeight(height int64) (*types.Block, error) { - block, ok := peer.blocks[height] - if !ok { - return nil, errMissingBlock - } - if block == nil { - return nil, errMissingBlock - } - return peer.blocks[height], nil -} - -// AddBlock adds a block at peer level. Block must be non-nil and recvSize a positive integer -// The peer must have a pending request for this block. -func (peer *BpPeer) AddBlock(block *types.Block, recvSize int) error { - if block == nil || recvSize < 0 { - panic("bad parameters") - } - existingBlock, ok := peer.blocks[block.Height] - if !ok { - peer.logger.Error("unsolicited block", "blockHeight", block.Height, "peer", peer.ID) - return errMissingBlock - } - if existingBlock != nil { - peer.logger.Error("already have a block for height", "height", block.Height) - return errDuplicateBlock - } - if peer.NumPendingBlockRequests == 0 { - panic("peer does not have pending requests") - } - peer.blocks[block.Height] = block - peer.NumPendingBlockRequests-- - if peer.NumPendingBlockRequests == 0 { - peer.stopMonitor() - peer.stopBlockResponseTimer() - } else { - peer.recvMonitor.Update(recvSize) - peer.resetBlockResponseTimer() - } - return nil -} - -// RemoveBlock removes the block of given height -func (peer *BpPeer) RemoveBlock(height int64) { - delete(peer.blocks, height) -} - -// RequestSent records that a request was sent, and starts the peer timer and monitor if needed. -func (peer *BpPeer) RequestSent(height int64) { - peer.blocks[height] = nil - - if peer.NumPendingBlockRequests == 0 { - peer.startMonitor() - peer.resetBlockResponseTimer() - } - peer.NumPendingBlockRequests++ -} - -// CheckRate verifies that the response rate of the peer is acceptable (higher than the minimum allowed). -func (peer *BpPeer) CheckRate() error { - if peer.NumPendingBlockRequests == 0 { - return nil - } - curRate := peer.recvMonitor.Status().CurRate - // curRate can be 0 on start - if curRate != 0 && curRate < peer.params.minRecvRate { - err := errSlowPeer - peer.logger.Error("SendTimeout", "peer", peer, - "reason", err, - "curRate", fmt.Sprintf("%d KB/s", curRate/1024), - "minRate", fmt.Sprintf("%d KB/s", peer.params.minRecvRate/1024)) - return err - } - return nil -} - -func (peer *BpPeer) onTimeout() { - peer.onErr(errNoPeerResponse, peer.ID) -} - -func (peer *BpPeer) stopMonitor() { - peer.recvMonitor.Done() - peer.recvMonitor = nil -} - -func (peer *BpPeer) startMonitor() { - peer.recvMonitor = flow.New(peer.params.sampleRate, peer.params.windowSize) - initialValue := float64(peer.params.minRecvRate) * math.E - peer.recvMonitor.SetREMA(initialValue) -} - -func (peer *BpPeer) resetBlockResponseTimer() { - if peer.blockResponseTimer == nil { - peer.blockResponseTimer = time.AfterFunc(peer.params.timeout, peer.onTimeout) - } else { - peer.blockResponseTimer.Reset(peer.params.timeout) - } -} - -func (peer *BpPeer) stopBlockResponseTimer() bool { - if peer.blockResponseTimer == nil { - return false - } - return peer.blockResponseTimer.Stop() -} - -// BpPeerDefaultParams returns the default peer parameters. -func BpPeerDefaultParams() *BpPeerParams { - return &BpPeerParams{ - // Timeout for a peer to respond to a block request. - timeout: 15 * time.Second, - - // Minimum recv rate to ensure we're receiving blocks from a peer fast - // enough. If a peer is not sending data at at least that rate, we - // consider them to have timedout and we disconnect. - // - // Assuming a DSL connection (not a good choice) 128 Kbps (upload) ~ 15 KB/s, - // sending data across atlantic ~ 7.5 KB/s. - minRecvRate: int64(7680), - - // Monitor parameters - sampleRate: time.Second, - windowSize: 40 * time.Second, - } -} diff --git a/blockchain/v1/peer_test.go b/blockchain/v1/peer_test.go deleted file mode 100644 index fd9e9f14b..000000000 --- a/blockchain/v1/peer_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package v1 - -import ( - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/libs/log" - tmrand "github.com/tendermint/tendermint/libs/rand" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -func TestPeerMonitor(t *testing.T) { - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) {}, - nil) - peer.SetLogger(log.TestingLogger()) - peer.startMonitor() - assert.NotNil(t, peer.recvMonitor) - peer.stopMonitor() - assert.Nil(t, peer.recvMonitor) -} - -func TestPeerResetBlockResponseTimer(t *testing.T) { - var ( - numErrFuncCalls int // number of calls to the errFunc - lastErr error // last generated error - peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine - ) - params := &BpPeerParams{timeout: 20 * time.Millisecond} - - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) { - peerTestMtx.Lock() - defer peerTestMtx.Unlock() - lastErr = err - numErrFuncCalls++ - }, - params) - - peer.SetLogger(log.TestingLogger()) - checkByStoppingPeerTimer(t, peer, false) - - // initial reset call with peer having a nil timer - peer.resetBlockResponseTimer() - assert.NotNil(t, peer.blockResponseTimer) - // make sure timer is running and stop it - checkByStoppingPeerTimer(t, peer, true) - - // reset with running timer - peer.resetBlockResponseTimer() - time.Sleep(5 * time.Millisecond) - peer.resetBlockResponseTimer() - assert.NotNil(t, peer.blockResponseTimer) - - // let the timer expire and ... - time.Sleep(50 * time.Millisecond) - // ... check timer is not running - checkByStoppingPeerTimer(t, peer, false) - - peerTestMtx.Lock() - // ... check errNoPeerResponse has been sent - assert.Equal(t, 1, numErrFuncCalls) - assert.Equal(t, lastErr, errNoPeerResponse) - peerTestMtx.Unlock() -} - -func TestPeerRequestSent(t *testing.T) { - params := &BpPeerParams{timeout: 2 * time.Millisecond} - - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) {}, - params) - - peer.SetLogger(log.TestingLogger()) - - peer.RequestSent(1) - assert.NotNil(t, peer.recvMonitor) - assert.NotNil(t, peer.blockResponseTimer) - assert.Equal(t, 1, peer.NumPendingBlockRequests) - - peer.RequestSent(1) - assert.NotNil(t, peer.recvMonitor) - assert.NotNil(t, peer.blockResponseTimer) - assert.Equal(t, 2, peer.NumPendingBlockRequests) -} - -func TestPeerGetAndRemoveBlock(t *testing.T) { - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 100, - func(err error, _ p2p.ID) {}, - nil) - - // Change peer height - peer.Height = int64(10) - assert.Equal(t, int64(10), peer.Height) - - // request some blocks and receive few of them - for i := 1; i <= 10; i++ { - peer.RequestSent(int64(i)) - if i > 5 { - // only receive blocks 1..5 - continue - } - _ = peer.AddBlock(makeSmallBlock(i), 10) - } - - tests := []struct { - name string - height int64 - wantErr error - blockPresent bool - }{ - {"no request", 100, errMissingBlock, false}, - {"no block", 6, errMissingBlock, false}, - {"block 1 present", 1, nil, true}, - {"block max present", 5, nil, true}, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // try to get the block - b, err := peer.BlockAtHeight(tt.height) - assert.Equal(t, tt.wantErr, err) - assert.Equal(t, tt.blockPresent, b != nil) - - // remove the block - peer.RemoveBlock(tt.height) - _, err = peer.BlockAtHeight(tt.height) - assert.Equal(t, errMissingBlock, err) - }) - } -} - -func TestPeerAddBlock(t *testing.T) { - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 100, - func(err error, _ p2p.ID) {}, - nil) - - // request some blocks, receive one - for i := 1; i <= 10; i++ { - peer.RequestSent(int64(i)) - if i == 5 { - // receive block 5 - _ = peer.AddBlock(makeSmallBlock(i), 10) - } - } - - tests := []struct { - name string - height int64 - wantErr error - blockPresent bool - }{ - {"no request", 50, errMissingBlock, false}, - {"duplicate block", 5, errDuplicateBlock, true}, - {"block 1 successfully received", 1, nil, true}, - {"block max successfully received", 10, nil, true}, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // try to get the block - err := peer.AddBlock(makeSmallBlock(int(tt.height)), 10) - assert.Equal(t, tt.wantErr, err) - _, err = peer.BlockAtHeight(tt.height) - assert.Equal(t, tt.blockPresent, err == nil) - }) - } -} - -func TestPeerOnErrFuncCalledDueToExpiration(t *testing.T) { - - params := &BpPeerParams{timeout: 10 * time.Millisecond} - var ( - numErrFuncCalls int // number of calls to the onErr function - lastErr error // last generated error - peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine - ) - - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) { - peerTestMtx.Lock() - defer peerTestMtx.Unlock() - lastErr = err - numErrFuncCalls++ - }, - params) - - peer.SetLogger(log.TestingLogger()) - - peer.RequestSent(1) - time.Sleep(50 * time.Millisecond) - // timer should have expired by now, check that the on error function was called - peerTestMtx.Lock() - assert.Equal(t, 1, numErrFuncCalls) - assert.Equal(t, errNoPeerResponse, lastErr) - peerTestMtx.Unlock() -} - -func TestPeerCheckRate(t *testing.T) { - params := &BpPeerParams{ - timeout: time.Second, - minRecvRate: int64(100), // 100 bytes/sec exponential moving average - } - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) {}, - params) - peer.SetLogger(log.TestingLogger()) - - require.Nil(t, peer.CheckRate()) - - for i := 0; i < 40; i++ { - peer.RequestSent(int64(i)) - } - - // monitor starts with a higher rEMA (~ 2*minRecvRate), wait for it to go down - time.Sleep(900 * time.Millisecond) - - // normal peer - send a bit more than 100 bytes/sec, > 10 bytes/100msec, check peer is not considered slow - for i := 0; i < 10; i++ { - _ = peer.AddBlock(makeSmallBlock(i), 11) - time.Sleep(100 * time.Millisecond) - require.Nil(t, peer.CheckRate()) - } - - // slow peer - send a bit less than 10 bytes/100msec - for i := 10; i < 20; i++ { - _ = peer.AddBlock(makeSmallBlock(i), 9) - time.Sleep(100 * time.Millisecond) - } - // check peer is considered slow - assert.Equal(t, errSlowPeer, peer.CheckRate()) -} - -func TestPeerCleanup(t *testing.T) { - params := &BpPeerParams{timeout: 2 * time.Millisecond} - - peer := NewBpPeer( - p2p.ID(tmrand.Str(12)), 0, 10, - func(err error, _ p2p.ID) {}, - params) - peer.SetLogger(log.TestingLogger()) - - assert.Nil(t, peer.blockResponseTimer) - peer.RequestSent(1) - assert.NotNil(t, peer.blockResponseTimer) - - peer.Cleanup() - checkByStoppingPeerTimer(t, peer, false) -} - -// Check if peer timer is running or not (a running timer can be successfully stopped). -// Note: stops the timer. -func checkByStoppingPeerTimer(t *testing.T, peer *BpPeer, running bool) { - assert.NotPanics(t, func() { - stopped := peer.stopBlockResponseTimer() - if running { - assert.True(t, stopped) - } else { - assert.False(t, stopped) - } - }) -} - -func makeSmallBlock(height int) *types.Block { - return types.MakeBlock(int64(height), []types.Tx{types.Tx("foo")}, nil, nil) -} diff --git a/blockchain/v1/pool.go b/blockchain/v1/pool.go deleted file mode 100644 index 27e0f3a04..000000000 --- a/blockchain/v1/pool.go +++ /dev/null @@ -1,370 +0,0 @@ -package v1 - -import ( - "sort" - - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -// BlockPool keeps track of the fast sync peers, block requests and block responses. -type BlockPool struct { - logger log.Logger - // Set of peers that have sent status responses, with height bigger than pool.Height - peers map[p2p.ID]*BpPeer - // Set of block heights and the corresponding peers from where a block response is expected or has been received. - blocks map[int64]p2p.ID - - plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest - nextRequestHeight int64 // next height to be added to plannedRequests - - Height int64 // height of next block to execute - MaxPeerHeight int64 // maximum height of all peers - toBcR bcReactor -} - -// NewBlockPool creates a new BlockPool. -func NewBlockPool(height int64, toBcR bcReactor) *BlockPool { - return &BlockPool{ - Height: height, - MaxPeerHeight: 0, - peers: make(map[p2p.ID]*BpPeer), - blocks: make(map[int64]p2p.ID), - plannedRequests: make(map[int64]struct{}), - nextRequestHeight: height, - toBcR: toBcR, - } -} - -// SetLogger sets the logger of the pool. -func (pool *BlockPool) SetLogger(l log.Logger) { - pool.logger = l -} - -// ReachedMaxHeight check if the pool has reached the maximum peer height. -func (pool *BlockPool) ReachedMaxHeight() bool { - return pool.Height >= pool.MaxPeerHeight -} - -func (pool *BlockPool) rescheduleRequest(peerID p2p.ID, height int64) { - pool.logger.Info("reschedule requests made to peer for height ", "peerID", peerID, "height", height) - pool.plannedRequests[height] = struct{}{} - delete(pool.blocks, height) - pool.peers[peerID].RemoveBlock(height) -} - -// Updates the pool's max height. If no peers are left MaxPeerHeight is set to 0. -func (pool *BlockPool) updateMaxPeerHeight() { - var newMax int64 - for _, peer := range pool.peers { - peerHeight := peer.Height - if peerHeight > newMax { - newMax = peerHeight - } - } - pool.MaxPeerHeight = newMax -} - -// UpdatePeer adds a new peer or updates an existing peer with a new base and height. -// If a peer is short it is not added. -func (pool *BlockPool) UpdatePeer(peerID p2p.ID, base int64, height int64) error { - - peer := pool.peers[peerID] - - if peer == nil { - if height < pool.Height { - pool.logger.Info("Peer height too small", - "peer", peerID, "height", height, "fsm_height", pool.Height) - return errPeerTooShort - } - // Add new peer. - peer = NewBpPeer(peerID, base, height, pool.toBcR.sendPeerError, nil) - peer.SetLogger(pool.logger.With("peer", peerID)) - pool.peers[peerID] = peer - pool.logger.Info("added peer", "peerID", peerID, "base", base, "height", height, "num_peers", len(pool.peers)) - } else { - // Check if peer is lowering its height. This is not allowed. - if height < peer.Height { - pool.RemovePeer(peerID, errPeerLowersItsHeight) - return errPeerLowersItsHeight - } - // Update existing peer. - peer.Base = base - peer.Height = height - } - - // Update the pool's MaxPeerHeight if needed. - pool.updateMaxPeerHeight() - - return nil -} - -// Cleans and deletes the peer. Recomputes the max peer height. -func (pool *BlockPool) deletePeer(peer *BpPeer) { - if peer == nil { - return - } - peer.Cleanup() - delete(pool.peers, peer.ID) - - if peer.Height == pool.MaxPeerHeight { - pool.updateMaxPeerHeight() - } -} - -// RemovePeer removes the blocks and requests from the peer, reschedules them and deletes the peer. -func (pool *BlockPool) RemovePeer(peerID p2p.ID, err error) { - peer := pool.peers[peerID] - if peer == nil { - return - } - pool.logger.Info("removing peer", "peerID", peerID, "error", err) - - // Reschedule the block requests made to the peer, or received and not processed yet. - // Note that some of the requests may be removed further down. - for h := range pool.peers[peerID].blocks { - pool.rescheduleRequest(peerID, h) - } - - oldMaxPeerHeight := pool.MaxPeerHeight - // Delete the peer. This operation may result in the pool's MaxPeerHeight being lowered. - pool.deletePeer(peer) - - // Check if the pool's MaxPeerHeight has been lowered. - // This may happen if the tallest peer has been removed. - if oldMaxPeerHeight > pool.MaxPeerHeight { - // Remove any planned requests for heights over the new MaxPeerHeight. - for h := range pool.plannedRequests { - if h > pool.MaxPeerHeight { - delete(pool.plannedRequests, h) - } - } - // Adjust the nextRequestHeight to the new max plus one. - if pool.nextRequestHeight > pool.MaxPeerHeight { - pool.nextRequestHeight = pool.MaxPeerHeight + 1 - } - } -} - -func (pool *BlockPool) removeShortPeers() { - for _, peer := range pool.peers { - if peer.Height < pool.Height { - pool.RemovePeer(peer.ID, nil) - } - } -} - -func (pool *BlockPool) removeBadPeers() { - pool.removeShortPeers() - for _, peer := range pool.peers { - if err := peer.CheckRate(); err != nil { - pool.RemovePeer(peer.ID, err) - pool.toBcR.sendPeerError(err, peer.ID) - } - } -} - -// MakeNextRequests creates more requests if the block pool is running low. -func (pool *BlockPool) MakeNextRequests(maxNumRequests int) { - heights := pool.makeRequestBatch(maxNumRequests) - if len(heights) != 0 { - pool.logger.Info("makeNextRequests will make following requests", - "number", len(heights), "heights", heights) - } - - for _, height := range heights { - h := int64(height) - if !pool.sendRequest(h) { - // If a good peer was not found for sending the request at height h then return, - // as it shouldn't be possible to find a peer for h+1. - return - } - delete(pool.plannedRequests, h) - } -} - -// Makes a batch of requests sorted by height such that the block pool has up to maxNumRequests entries. -func (pool *BlockPool) makeRequestBatch(maxNumRequests int) []int { - pool.removeBadPeers() - // At this point pool.requests may include heights for requests to be redone due to removal of peers: - // - peers timed out or were removed by switch - // - FSM timed out on waiting to advance the block execution due to missing blocks at h or h+1 - // Determine the number of requests needed by subtracting the number of requests already made from the maximum - // allowed - numNeeded := maxNumRequests - len(pool.blocks) - for len(pool.plannedRequests) < numNeeded { - if pool.nextRequestHeight > pool.MaxPeerHeight { - break - } - pool.plannedRequests[pool.nextRequestHeight] = struct{}{} - pool.nextRequestHeight++ - } - - heights := make([]int, 0, len(pool.plannedRequests)) - for k := range pool.plannedRequests { - heights = append(heights, int(k)) - } - sort.Ints(heights) - return heights -} - -func (pool *BlockPool) sendRequest(height int64) bool { - for _, peer := range pool.peers { - if peer.NumPendingBlockRequests >= maxRequestsPerPeer { - continue - } - if peer.Base > height || peer.Height < height { - continue - } - - err := pool.toBcR.sendBlockRequest(peer.ID, height) - if err == errNilPeerForBlockRequest { - // Switch does not have this peer, remove it and continue to look for another peer. - pool.logger.Error("switch does not have peer..removing peer selected for height", "peer", - peer.ID, "height", height) - pool.RemovePeer(peer.ID, err) - continue - } - - if err == errSendQueueFull { - pool.logger.Error("peer queue is full", "peer", peer.ID, "height", height) - continue - } - - pool.logger.Info("assigned request to peer", "peer", peer.ID, "height", height) - - pool.blocks[height] = peer.ID - peer.RequestSent(height) - - return true - } - pool.logger.Error("could not find peer to send request for block at height", "height", height) - return false -} - -// AddBlock validates that the block comes from the peer it was expected from and stores it in the 'blocks' map. -func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) error { - peer, ok := pool.peers[peerID] - if !ok { - pool.logger.Error("block from unknown peer", "height", block.Height, "peer", peerID) - return errBadDataFromPeer - } - if wantPeerID, ok := pool.blocks[block.Height]; ok && wantPeerID != peerID { - pool.logger.Error("block received from wrong peer", "height", block.Height, - "peer", peerID, "expected_peer", wantPeerID) - return errBadDataFromPeer - } - - return peer.AddBlock(block, blockSize) -} - -// BlockData stores the peer responsible to deliver a block and the actual block if delivered. -type BlockData struct { - block *types.Block - peer *BpPeer -} - -// BlockAndPeerAtHeight retrieves the block and delivery peer at specified height. -// Returns errMissingBlock if a block was not found -func (pool *BlockPool) BlockAndPeerAtHeight(height int64) (bData *BlockData, err error) { - peerID := pool.blocks[height] - peer := pool.peers[peerID] - if peer == nil { - return nil, errMissingBlock - } - - block, err := peer.BlockAtHeight(height) - if err != nil { - return nil, err - } - - return &BlockData{peer: peer, block: block}, nil - -} - -// FirstTwoBlocksAndPeers returns the blocks and the delivery peers at pool's height H and H+1. -func (pool *BlockPool) FirstTwoBlocksAndPeers() (first, second *BlockData, err error) { - first, err = pool.BlockAndPeerAtHeight(pool.Height) - second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) - if err == nil { - err = err2 - } - return -} - -// InvalidateFirstTwoBlocks removes the peers that sent us the first two blocks, blocks are removed by RemovePeer(). -func (pool *BlockPool) InvalidateFirstTwoBlocks(err error) { - first, err1 := pool.BlockAndPeerAtHeight(pool.Height) - second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) - - if err1 == nil { - pool.RemovePeer(first.peer.ID, err) - } - if err2 == nil { - pool.RemovePeer(second.peer.ID, err) - } -} - -// ProcessedCurrentHeightBlock performs cleanup after a block is processed. It removes block at pool height and -// the peers that are now short. -func (pool *BlockPool) ProcessedCurrentHeightBlock() { - peerID, peerOk := pool.blocks[pool.Height] - if peerOk { - pool.peers[peerID].RemoveBlock(pool.Height) - } - delete(pool.blocks, pool.Height) - pool.logger.Debug("removed block at height", "height", pool.Height) - pool.Height++ - pool.removeShortPeers() -} - -// RemovePeerAtCurrentHeights checks if a block at pool's height H exists and if not, it removes the -// delivery peer and returns. If a block at height H exists then the check and peer removal is done for H+1. -// This function is called when the FSM is not able to make progress for some time. -// This happens if either the block H or H+1 have not been delivered. -func (pool *BlockPool) RemovePeerAtCurrentHeights(err error) { - peerID := pool.blocks[pool.Height] - peer, ok := pool.peers[peerID] - if ok { - if _, err := peer.BlockAtHeight(pool.Height); err != nil { - pool.logger.Info("remove peer that hasn't sent block at pool.Height", - "peer", peerID, "height", pool.Height) - pool.RemovePeer(peerID, err) - return - } - } - peerID = pool.blocks[pool.Height+1] - peer, ok = pool.peers[peerID] - if ok { - if _, err := peer.BlockAtHeight(pool.Height + 1); err != nil { - pool.logger.Info("remove peer that hasn't sent block at pool.Height+1", - "peer", peerID, "height", pool.Height+1) - pool.RemovePeer(peerID, err) - return - } - } -} - -// Cleanup performs pool and peer cleanup -func (pool *BlockPool) Cleanup() { - for id, peer := range pool.peers { - peer.Cleanup() - delete(pool.peers, id) - } - pool.plannedRequests = make(map[int64]struct{}) - pool.blocks = make(map[int64]p2p.ID) - pool.nextRequestHeight = 0 - pool.Height = 0 - pool.MaxPeerHeight = 0 -} - -// NumPeers returns the number of peers in the pool -func (pool *BlockPool) NumPeers() int { - return len(pool.peers) -} - -// NeedsBlocks returns true if more blocks are required. -func (pool *BlockPool) NeedsBlocks() bool { - return len(pool.blocks) < maxNumRequests -} diff --git a/blockchain/v1/pool_test.go b/blockchain/v1/pool_test.go deleted file mode 100644 index 31b9d09f7..000000000 --- a/blockchain/v1/pool_test.go +++ /dev/null @@ -1,691 +0,0 @@ -package v1 - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -type testPeer struct { - id p2p.ID - base int64 - height int64 -} - -type testBcR struct { - logger log.Logger -} - -type testValues struct { - numRequestsSent int -} - -var testResults testValues - -func resetPoolTestResults() { - testResults.numRequestsSent = 0 -} - -func (testR *testBcR) sendPeerError(err error, peerID p2p.ID) { -} - -func (testR *testBcR) sendStatusRequest() { -} - -func (testR *testBcR) sendBlockRequest(peerID p2p.ID, height int64) error { - testResults.numRequestsSent++ - return nil -} - -func (testR *testBcR) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { -} - -func (testR *testBcR) switchToConsensus() { - -} - -func newTestBcR() *testBcR { - testBcR := &testBcR{logger: log.TestingLogger()} - return testBcR -} - -type tPBlocks struct { - id p2p.ID - create bool -} - -// Makes a block pool with specified current height, list of peers, block requests and block responses -func makeBlockPool(bcr *testBcR, height int64, peers []BpPeer, blocks map[int64]tPBlocks) *BlockPool { - bPool := NewBlockPool(height, bcr) - bPool.SetLogger(bcr.logger) - - txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} - - var maxH int64 - for _, p := range peers { - if p.Height > maxH { - maxH = p.Height - } - bPool.peers[p.ID] = NewBpPeer(p.ID, p.Base, p.Height, bcr.sendPeerError, nil) - bPool.peers[p.ID].SetLogger(bcr.logger) - - } - bPool.MaxPeerHeight = maxH - for h, p := range blocks { - bPool.blocks[h] = p.id - bPool.peers[p.id].RequestSent(h) - if p.create { - // simulate that a block at height h has been received - _ = bPool.peers[p.id].AddBlock(types.MakeBlock(h, txs, nil, nil), 100) - } - } - return bPool -} - -func assertPeerSetsEquivalent(t *testing.T, set1 map[p2p.ID]*BpPeer, set2 map[p2p.ID]*BpPeer) { - assert.Equal(t, len(set1), len(set2)) - for peerID, peer1 := range set1 { - peer2 := set2[peerID] - assert.NotNil(t, peer2) - assert.Equal(t, peer1.NumPendingBlockRequests, peer2.NumPendingBlockRequests) - assert.Equal(t, peer1.Height, peer2.Height) - assert.Equal(t, peer1.Base, peer2.Base) - assert.Equal(t, len(peer1.blocks), len(peer2.blocks)) - for h, block1 := range peer1.blocks { - block2 := peer2.blocks[h] - // block1 and block2 could be nil if a request was made but no block was received - assert.Equal(t, block1, block2) - } - } -} - -func assertBlockPoolEquivalent(t *testing.T, poolWanted, pool *BlockPool) { - assert.Equal(t, poolWanted.blocks, pool.blocks) - assertPeerSetsEquivalent(t, poolWanted.peers, pool.peers) - assert.Equal(t, poolWanted.MaxPeerHeight, pool.MaxPeerHeight) - assert.Equal(t, poolWanted.Height, pool.Height) - -} - -func TestBlockPoolUpdatePeer(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - args testPeer - poolWanted *BlockPool - errWanted error - }{ - { - name: "add a first short peer", - pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - args: testPeer{"P1", 0, 50}, - errWanted: errPeerTooShort, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "add a first good peer", - pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - args: testPeer{"P1", 0, 101}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 101}}, map[int64]tPBlocks{}), - }, - { - name: "add a first good peer with base", - pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - args: testPeer{"P1", 10, 101}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Base: 10, Height: 101}}, map[int64]tPBlocks{}), - }, - { - name: "increase the height of P1 from 120 to 123", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), - args: testPeer{"P1", 0, 123}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 123}}, map[int64]tPBlocks{}), - }, - { - name: "decrease the height of P1 from 120 to 110", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), - args: testPeer{"P1", 0, 110}, - errWanted: errPeerLowersItsHeight, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "decrease the height of P1 from 105 to 102 with blocks", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 105}}, - map[int64]tPBlocks{ - 100: {"P1", true}, 101: {"P1", true}, 102: {"P1", true}}), - args: testPeer{"P1", 0, 102}, - errWanted: errPeerLowersItsHeight, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, - map[int64]tPBlocks{}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - pool := tt.pool - err := pool.UpdatePeer(tt.args.id, tt.args.base, tt.args.height) - assert.Equal(t, tt.errWanted, err) - assert.Equal(t, tt.poolWanted.blocks, tt.pool.blocks) - assertPeerSetsEquivalent(t, tt.poolWanted.peers, tt.pool.peers) - assert.Equal(t, tt.poolWanted.MaxPeerHeight, tt.pool.MaxPeerHeight) - }) - } -} - -func TestBlockPoolRemovePeer(t *testing.T) { - testBcR := newTestBcR() - - type args struct { - peerID p2p.ID - err error - } - - tests := []struct { - name string - pool *BlockPool - args args - poolWanted *BlockPool - }{ - { - name: "attempt to delete non-existing peer", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), - args: args{"P99", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), - }, - { - name: "delete the only peer without blocks", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), - args: args{"P1", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "delete the shortest of two peers without blocks", - pool: makeBlockPool( - testBcR, - 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, - map[int64]tPBlocks{}), - args: args{"P1", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 120}}, map[int64]tPBlocks{}), - }, - { - name: "delete the tallest of two peers without blocks", - pool: makeBlockPool( - testBcR, - 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, - map[int64]tPBlocks{}), - args: args{"P2", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), - }, - { - name: "delete the only peer with block requests sent and blocks received", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, - map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), - args: args{"P1", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "delete the shortest of two peers with block requests sent and blocks received", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 200}}, - map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), - args: args{"P1", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 200}}, map[int64]tPBlocks{}), - }, - { - name: "delete the tallest of two peers with block requests sent and blocks received", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 110}}, - map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), - args: args{"P1", nil}, - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 110}}, map[int64]tPBlocks{}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - tt.pool.RemovePeer(tt.args.peerID, tt.args.err) - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} - -func TestBlockPoolRemoveShortPeers(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - poolWanted *BlockPool - }{ - { - name: "no short peers", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), - poolWanted: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), - }, - - { - name: "one short peer", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 90}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), - poolWanted: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), - }, - - { - name: "all short peers", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 90}, {ID: "P2", Height: 91}, {ID: "P3", Height: 92}}, map[int64]tPBlocks{}), - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - pool := tt.pool - pool.removeShortPeers() - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} - -func TestBlockPoolSendRequestBatch(t *testing.T) { - type testPeerResult struct { - id p2p.ID - numPendingBlockRequests int - } - - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - maxRequestsPerPeer int - expRequests map[int64]bool - expRequestsSent int - expPeerResults []testPeerResult - }{ - { - name: "one peer - send up to maxRequestsPerPeer block requests", - pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), - maxRequestsPerPeer: 2, - expRequests: map[int64]bool{10: true, 11: true}, - expRequestsSent: 2, - expPeerResults: []testPeerResult{{id: "P1", numPendingBlockRequests: 2}}, - }, - { - name: "multiple peers - stops at gap between height and base", - pool: makeBlockPool(testBcR, 10, []BpPeer{ - {ID: "P1", Base: 1, Height: 12}, - {ID: "P2", Base: 15, Height: 100}, - }, map[int64]tPBlocks{}), - maxRequestsPerPeer: 10, - expRequests: map[int64]bool{10: true, 11: true, 12: true}, - expRequestsSent: 3, - expPeerResults: []testPeerResult{ - {id: "P1", numPendingBlockRequests: 3}, - {id: "P2", numPendingBlockRequests: 0}, - }, - }, - { - name: "n peers - send n*maxRequestsPerPeer block requests", - pool: makeBlockPool( - testBcR, - 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{}), - maxRequestsPerPeer: 2, - expRequests: map[int64]bool{10: true, 11: true}, - expRequestsSent: 4, - expPeerResults: []testPeerResult{ - {id: "P1", numPendingBlockRequests: 2}, - {id: "P2", numPendingBlockRequests: 2}}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - resetPoolTestResults() - - var pool = tt.pool - maxRequestsPerPeer = tt.maxRequestsPerPeer - pool.MakeNextRequests(10) - - assert.Equal(t, tt.expRequestsSent, testResults.numRequestsSent) - for _, tPeer := range tt.expPeerResults { - var peer = pool.peers[tPeer.id] - assert.NotNil(t, peer) - assert.Equal(t, tPeer.numPendingBlockRequests, peer.NumPendingBlockRequests) - } - }) - } -} - -func TestBlockPoolAddBlock(t *testing.T) { - testBcR := newTestBcR() - txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} - - type args struct { - peerID p2p.ID - block *types.Block - blockSize int - } - tests := []struct { - name string - pool *BlockPool - args args - poolWanted *BlockPool - errWanted error - }{ - {name: "block from unknown peer", - pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), - args: args{ - peerID: "P2", - block: types.MakeBlock(int64(10), txs, nil, nil), - blockSize: 100, - }, - poolWanted: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), - errWanted: errBadDataFromPeer, - }, - {name: "unexpected block 11 from known peer - waiting for 10", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", false}}), - args: args{ - peerID: "P1", - block: types.MakeBlock(int64(11), txs, nil, nil), - blockSize: 100, - }, - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", false}}), - errWanted: errMissingBlock, - }, - {name: "unexpected block 10 from known peer - already have 10", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), - args: args{ - peerID: "P1", - block: types.MakeBlock(int64(10), txs, nil, nil), - blockSize: 100, - }, - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), - errWanted: errDuplicateBlock, - }, - {name: "unexpected block 10 from known peer P2 - expected 10 to come from P1", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{10: {"P1", false}}), - args: args{ - peerID: "P2", - block: types.MakeBlock(int64(10), txs, nil, nil), - blockSize: 100, - }, - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{10: {"P1", false}}), - errWanted: errBadDataFromPeer, - }, - {name: "expected block from known peer", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", false}}), - args: args{ - peerID: "P1", - block: types.MakeBlock(int64(10), txs, nil, nil), - blockSize: 100, - }, - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{10: {"P1", true}}), - errWanted: nil, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - err := tt.pool.AddBlock(tt.args.peerID, tt.args.block, tt.args.blockSize) - assert.Equal(t, tt.errWanted, err) - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} - -func TestBlockPoolFirstTwoBlocksAndPeers(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - firstWanted int64 - secondWanted int64 - errWanted error - }{ - { - name: "both blocks missing", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), - errWanted: errMissingBlock, - }, - { - name: "second block missing", - pool: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), - firstWanted: 15, - errWanted: errMissingBlock, - }, - { - name: "first block missing", - pool: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{16: {"P2", true}, 18: {"P2", true}}), - secondWanted: 16, - errWanted: errMissingBlock, - }, - { - name: "both blocks present", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), - firstWanted: 10, - secondWanted: 11, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - pool := tt.pool - gotFirst, gotSecond, err := pool.FirstTwoBlocksAndPeers() - assert.Equal(t, tt.errWanted, err) - - if tt.firstWanted != 0 { - peer := pool.blocks[tt.firstWanted] - block := pool.peers[peer].blocks[tt.firstWanted] - assert.Equal(t, block, gotFirst.block, - "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", - tt.firstWanted, gotFirst.block.Height) - } - - if tt.secondWanted != 0 { - peer := pool.blocks[tt.secondWanted] - block := pool.peers[peer].blocks[tt.secondWanted] - assert.Equal(t, block, gotSecond.block, - "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", - tt.secondWanted, gotSecond.block.Height) - } - }) - } -} - -func TestBlockPoolInvalidateFirstTwoBlocks(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - poolWanted *BlockPool - }{ - { - name: "both blocks missing", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), - }, - { - name: "second block missing", - pool: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), - poolWanted: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P2", Height: 100}}, - map[int64]tPBlocks{18: {"P2", true}}), - }, - { - name: "first block missing", - pool: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{18: {"P1", true}, 16: {"P2", true}}), - poolWanted: makeBlockPool(testBcR, 15, - []BpPeer{{ID: "P1", Height: 100}}, - map[int64]tPBlocks{18: {"P1", true}}), - }, - { - name: "both blocks present", - pool: makeBlockPool(testBcR, 10, - []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, - map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), - poolWanted: makeBlockPool(testBcR, 10, - []BpPeer{}, - map[int64]tPBlocks{}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - tt.pool.InvalidateFirstTwoBlocks(errNoPeerResponse) - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} - -func TestProcessedCurrentHeightBlock(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - poolWanted *BlockPool - }{ - { - name: "one peer", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, - map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", true}}), - poolWanted: makeBlockPool(testBcR, 101, []BpPeer{{ID: "P1", Height: 120}}, - map[int64]tPBlocks{101: {"P1", true}}), - }, - { - name: "multiple peers", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, - 101: {"P2", true}, 103: {"P2", false}, - 102: {"P3", true}, 106: {"P3", true}}), - poolWanted: makeBlockPool(testBcR, 101, - []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 104: {"P1", true}, 105: {"P1", false}, - 101: {"P2", true}, 103: {"P2", false}, - 102: {"P3", true}, 106: {"P3", true}}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - tt.pool.ProcessedCurrentHeightBlock() - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} - -func TestRemovePeerAtCurrentHeight(t *testing.T) { - testBcR := newTestBcR() - - tests := []struct { - name string - pool *BlockPool - poolWanted *BlockPool - }{ - { - name: "one peer, remove peer for block at H", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, - map[int64]tPBlocks{100: {"P1", false}, 101: {"P1", true}}), - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "one peer, remove peer for block at H+1", - pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, - map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), - poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), - }, - { - name: "multiple peers, remove peer for block at H", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 100: {"P1", false}, 104: {"P1", true}, 105: {"P1", false}, - 101: {"P2", true}, 103: {"P2", false}, - 102: {"P3", true}, 106: {"P3", true}}), - poolWanted: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 101: {"P2", true}, 103: {"P2", false}, - 102: {"P3", true}, 106: {"P3", true}}), - }, - { - name: "multiple peers, remove peer for block at H+1", - pool: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, - 101: {"P2", false}, 103: {"P2", false}, - 102: {"P3", true}, 106: {"P3", true}}), - poolWanted: makeBlockPool(testBcR, 100, - []BpPeer{{ID: "P1", Height: 120}, {ID: "P3", Height: 130}}, - map[int64]tPBlocks{ - 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, - 102: {"P3", true}, 106: {"P3", true}}), - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - tt.pool.RemovePeerAtCurrentHeights(errNoPeerResponse) - assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) - }) - } -} diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go deleted file mode 100644 index 2eb4e7618..000000000 --- a/blockchain/v1/reactor.go +++ /dev/null @@ -1,569 +0,0 @@ -package v1 - -import ( - "fmt" - "reflect" - "time" - - "github.com/tendermint/tendermint/behavior" - bc "github.com/tendermint/tendermint/blockchain" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/store" - "github.com/tendermint/tendermint/types" -) - -const ( - // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) - BlockchainChannel = byte(0x40) - trySyncIntervalMS = 10 - trySendIntervalMS = 10 - - // ask for best height every 10s - statusUpdateIntervalSeconds = 10 -) - -var ( - // Maximum number of requests that can be pending per peer, i.e. for which requests have been sent but blocks - // have not been received. - maxRequestsPerPeer = 20 - // Maximum number of block requests for the reactor, pending or for which blocks have been received. - maxNumRequests = 64 -) - -type consensusReactor interface { - // for when we switch from blockchain reactor and fast sync to - // the consensus machine - SwitchToConsensus(state sm.State, skipWAL bool) -} - -// BlockchainReactor handles long-term catchup syncing. -type BlockchainReactor struct { - p2p.BaseReactor - - initialState sm.State // immutable - state sm.State - - blockExec *sm.BlockExecutor - store *store.BlockStore - - fastSync bool - stateSynced bool - - fsm *BcReactorFSM - blocksSynced uint64 - - // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. - messagesForFSMCh chan bcReactorMessage - - // Switch goroutine may send RemovePeer to the blockchain reactor. This is an error message that is relayed - // to this channel to be processed in the context of the poolRoutine. - errorsForFSMCh chan bcReactorMessage - - // This channel is used by the FSM and indirectly the block pool to report errors to the blockchain reactor and - // the switch. - eventsFromFSMCh chan bcFsmMessage - - swReporter *behavior.SwitchReporter -} - -// NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, - fastSync bool) *BlockchainReactor { - - if state.LastBlockHeight != store.Height() { - panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, - store.Height())) - } - - const capacity = 1000 - eventsFromFSMCh := make(chan bcFsmMessage, capacity) - messagesForFSMCh := make(chan bcReactorMessage, capacity) - errorsForFSMCh := make(chan bcReactorMessage, capacity) - - startHeight := store.Height() + 1 - if startHeight == 1 { - startHeight = state.InitialHeight - } - bcR := &BlockchainReactor{ - initialState: state, - state: state, - blockExec: blockExec, - fastSync: fastSync, - store: store, - messagesForFSMCh: messagesForFSMCh, - eventsFromFSMCh: eventsFromFSMCh, - errorsForFSMCh: errorsForFSMCh, - } - fsm := NewFSM(startHeight, bcR) - bcR.fsm = fsm - bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) - // bcR.swReporter = behavior.NewSwitchReporter(bcR.BaseReactor.Switch) - - return bcR -} - -// bcReactorMessage is used by the reactor to send messages to the FSM. -type bcReactorMessage struct { - event bReactorEvent - data bReactorEventData -} - -type bFsmEvent uint - -const ( - // message type events - peerErrorEv = iota + 1 - syncFinishedEv -) - -type bFsmEventData struct { - peerID p2p.ID - err error -} - -// bcFsmMessage is used by the FSM to send messages to the reactor -type bcFsmMessage struct { - event bFsmEvent - data bFsmEventData -} - -// SetLogger implements service.Service by setting the logger on reactor and pool. -func (bcR *BlockchainReactor) SetLogger(l log.Logger) { - bcR.BaseService.Logger = l - bcR.fsm.SetLogger(l) -} - -// OnStart implements service.Service. -func (bcR *BlockchainReactor) OnStart() error { - bcR.swReporter = behavior.NewSwitchReporter(bcR.BaseReactor.Switch) - if bcR.fastSync { - go bcR.poolRoutine() - } - return nil -} - -// OnStop implements service.Service. -func (bcR *BlockchainReactor) OnStop() { - _ = bcR.Stop() -} - -// SwitchToFastSync is called by the state sync reactor when switching to fast sync. -func (bcR *BlockchainReactor) SwitchToFastSync(state sm.State) error { - bcR.fastSync = true - bcR.initialState = state - bcR.state = state - bcR.stateSynced = true - - bcR.fsm = NewFSM(state.LastBlockHeight+1, bcR) - bcR.fsm.SetLogger(bcR.Logger) - go bcR.poolRoutine() - return nil -} - -// GetChannels implements Reactor -func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { - return []*p2p.ChannelDescriptor{ - { - ID: BlockchainChannel, - Priority: 10, - SendQueueCapacity: 2000, - RecvBufferCapacity: 50 * 4096, - RecvMessageCapacity: bc.MaxMsgSize, - }, - } -} - -// AddPeer implements Reactor by sending our state to peer. -func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { - msgBytes, err := bc.EncodeMsg(&bcproto.StatusResponse{ - Base: bcR.store.Base(), - Height: bcR.store.Height(), - }) - if err != nil { - bcR.Logger.Error("could not convert msg to protobuf", "err", err) - return - } - peer.Send(BlockchainChannel, msgBytes) - // it's OK if send fails. will try later in poolRoutine - - // peer is added to the pool once we receive the first - // bcStatusResponseMessage from the peer and call pool.updatePeer() -} - -// sendBlockToPeer loads a block and sends it to the requesting peer. -// If the block doesn't exist a bcNoBlockResponseMessage is sent. -// If all nodes are honest, no node should be requesting for a block that doesn't exist. -func (bcR *BlockchainReactor) sendBlockToPeer(msg *bcproto.BlockRequest, - src p2p.Peer) (queued bool) { - - block := bcR.store.LoadBlock(msg.Height) - if block != nil { - pbbi, err := block.ToProto() - if err != nil { - bcR.Logger.Error("Could not send block message to peer", "err", err) - return false - } - msgBytes, err := bc.EncodeMsg(&bcproto.BlockResponse{Block: pbbi}) - if err != nil { - bcR.Logger.Error("unable to marshal msg", "err", err) - return false - } - return src.TrySend(BlockchainChannel, msgBytes) - } - - bcR.Logger.Info("peer asking for a block we don't have", "src", src, "height", msg.Height) - - msgBytes, err := bc.EncodeMsg(&bcproto.NoBlockResponse{Height: msg.Height}) - if err != nil { - bcR.Logger.Error("unable to marshal msg", "err", err) - return false - } - return src.TrySend(BlockchainChannel, msgBytes) -} - -func (bcR *BlockchainReactor) sendStatusResponseToPeer(msg *bcproto.StatusRequest, src p2p.Peer) (queued bool) { - msgBytes, err := bc.EncodeMsg(&bcproto.StatusResponse{ - Base: bcR.store.Base(), - Height: bcR.store.Height(), - }) - if err != nil { - bcR.Logger.Error("unable to marshal msg", "err", err) - return false - } - - return src.TrySend(BlockchainChannel, msgBytes) -} - -// RemovePeer implements Reactor by removing peer from the pool. -func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { - msgData := bcReactorMessage{ - event: peerRemoveEv, - data: bReactorEventData{ - peerID: peer.ID(), - err: errSwitchRemovesPeer, - }, - } - bcR.errorsForFSMCh <- msgData -} - -// Receive implements Reactor by handling 4 types of messages (look below). -func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := bc.DecodeMsg(msgBytes) - if err != nil { - bcR.Logger.Error("error decoding message", "src", src, "chId", chID, "err", err) - _ = bcR.swReporter.Report(behavior.BadMessage(src.ID(), err.Error())) - return - } - - if err = bc.ValidateMsg(msg); err != nil { - bcR.Logger.Error("peer sent us invalid msg", "peer", src, "msg", msg, "err", err) - _ = bcR.swReporter.Report(behavior.BadMessage(src.ID(), err.Error())) - return - } - - bcR.Logger.Debug("Receive", "src", src, "chID", chID, "msg", msg) - - switch msg := msg.(type) { - case *bcproto.BlockRequest: - if queued := bcR.sendBlockToPeer(msg, src); !queued { - // Unfortunately not queued since the queue is full. - bcR.Logger.Error("Could not send block message to peer", "src", src, "height", msg.Height) - } - - case *bcproto.StatusRequest: - // Send peer our state. - if queued := bcR.sendStatusResponseToPeer(msg, src); !queued { - // Unfortunately not queued since the queue is full. - bcR.Logger.Error("Could not send status message to peer", "src", src) - } - - case *bcproto.BlockResponse: - bi, err := types.BlockFromProto(msg.Block) - if err != nil { - bcR.Logger.Error("error transition block from protobuf", "err", err) - return - } - msgForFSM := bcReactorMessage{ - event: blockResponseEv, - data: bReactorEventData{ - peerID: src.ID(), - height: bi.Height, - block: bi, - length: len(msgBytes), - }, - } - bcR.Logger.Info("Received", "src", src, "height", bi.Height) - bcR.messagesForFSMCh <- msgForFSM - case *bcproto.NoBlockResponse: - msgForFSM := bcReactorMessage{ - event: noBlockResponseEv, - data: bReactorEventData{ - peerID: src.ID(), - height: msg.Height, - }, - } - bcR.Logger.Debug("Peer does not have requested block", "peer", src, "height", msg.Height) - bcR.messagesForFSMCh <- msgForFSM - - case *bcproto.StatusResponse: - // Got a peer status. Unverified. - msgForFSM := bcReactorMessage{ - event: statusResponseEv, - data: bReactorEventData{ - peerID: src.ID(), - height: msg.Height, - length: len(msgBytes), - }, - } - bcR.messagesForFSMCh <- msgForFSM - - default: - bcR.Logger.Error(fmt.Sprintf("unknown message type %v", reflect.TypeOf(msg))) - } -} - -// processBlocksRoutine processes blocks until signlaed to stop over the stopProcessing channel -func (bcR *BlockchainReactor) processBlocksRoutine(stopProcessing chan struct{}) { - - processReceivedBlockTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) - doProcessBlockCh := make(chan struct{}, 1) - - lastHundred := time.Now() - lastRate := 0.0 - -ForLoop: - for { - select { - case <-stopProcessing: - bcR.Logger.Info("finishing block execution") - break ForLoop - case <-processReceivedBlockTicker.C: // try to execute blocks - select { - case doProcessBlockCh <- struct{}{}: - default: - } - case <-doProcessBlockCh: - for { - err := bcR.processBlock() - if err == errMissingBlock { - break - } - // Notify FSM of block processing result. - msgForFSM := bcReactorMessage{ - event: processedBlockEv, - data: bReactorEventData{ - err: err, - }, - } - _ = bcR.fsm.Handle(&msgForFSM) - - if err != nil { - break - } - - bcR.blocksSynced++ - if bcR.blocksSynced%100 == 0 { - lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) - height, maxPeerHeight := bcR.fsm.Status() - bcR.Logger.Info("Fast Sync Rate", "height", height, - "max_peer_height", maxPeerHeight, "blocks/s", lastRate) - lastHundred = time.Now() - } - } - } - } -} - -// poolRoutine receives and handles messages from the Receive() routine and from the FSM. -func (bcR *BlockchainReactor) poolRoutine() { - - bcR.fsm.Start() - - sendBlockRequestTicker := time.NewTicker(trySendIntervalMS * time.Millisecond) - statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) - - stopProcessing := make(chan struct{}, 1) - go bcR.processBlocksRoutine(stopProcessing) - -ForLoop: - for { - select { - - case <-sendBlockRequestTicker.C: - if !bcR.fsm.NeedsBlocks() { - continue - } - _ = bcR.fsm.Handle(&bcReactorMessage{ - event: makeRequestsEv, - data: bReactorEventData{ - maxNumRequests: maxNumRequests}}) - - case <-statusUpdateTicker.C: - // Ask for status updates. - go bcR.sendStatusRequest() - - case msg := <-bcR.messagesForFSMCh: - // Sent from the Receive() routine when status (statusResponseEv) and - // block (blockResponseEv) response events are received - _ = bcR.fsm.Handle(&msg) - - case msg := <-bcR.errorsForFSMCh: - // Sent from the switch.RemovePeer() routine (RemovePeerEv) and - // FSM state timer expiry routine (stateTimeoutEv). - _ = bcR.fsm.Handle(&msg) - - case msg := <-bcR.eventsFromFSMCh: - switch msg.event { - case syncFinishedEv: - stopProcessing <- struct{}{} - // Sent from the FSM when it enters finished state. - break ForLoop - case peerErrorEv: - // Sent from the FSM when it detects peer error - bcR.reportPeerErrorToSwitch(msg.data.err, msg.data.peerID) - if msg.data.err == errNoPeerResponse { - // Sent from the peer timeout handler routine - _ = bcR.fsm.Handle(&bcReactorMessage{ - event: peerRemoveEv, - data: bReactorEventData{ - peerID: msg.data.peerID, - err: msg.data.err, - }, - }) - } - // else { - // For slow peers, or errors due to blocks received from wrong peer - // the FSM had already removed the peers - // } - default: - bcR.Logger.Error("Event from FSM not supported", "type", msg.event) - } - - case <-bcR.Quit(): - break ForLoop - } - } -} - -func (bcR *BlockchainReactor) reportPeerErrorToSwitch(err error, peerID p2p.ID) { - peer := bcR.Switch.Peers().Get(peerID) - if peer != nil { - _ = bcR.swReporter.Report(behavior.BadMessage(peerID, err.Error())) - } -} - -func (bcR *BlockchainReactor) processBlock() error { - - first, second, err := bcR.fsm.FirstTwoBlocks() - if err != nil { - // We need both to sync the first block. - return err - } - - chainID := bcR.initialState.ChainID - - firstParts := first.MakePartSet(types.BlockPartSizeBytes) - firstPartSetHeader := firstParts.Header() - firstID := types.BlockID{Hash: first.Hash(), PartSetHeader: firstPartSetHeader} - // Finally, verify the first block using the second's commit - // NOTE: we can probably make this more efficient, but note that calling - // first.Hash() doesn't verify the tx contents, so MakePartSet() is - // currently necessary. - err = bcR.state.Validators.VerifyCommitLight(chainID, firstID, first.Height, second.LastCommit) - if err != nil { - bcR.Logger.Error("error during commit verification", "err", err, - "first", first.Height, "second", second.Height) - return errBlockVerificationFailure - } - - bcR.store.SaveBlock(first, firstParts, second.LastCommit) - - bcR.state, _, err = bcR.blockExec.ApplyBlock(bcR.state, firstID, first) - if err != nil { - panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) - } - - return nil -} - -// Implements bcRNotifier -// sendStatusRequest broadcasts `BlockStore` height. -func (bcR *BlockchainReactor) sendStatusRequest() { - msgBytes, err := bc.EncodeMsg(&bcproto.StatusRequest{}) - if err != nil { - panic(err) - } - bcR.Switch.Broadcast(BlockchainChannel, msgBytes) -} - -// Implements bcRNotifier -// BlockRequest sends `BlockRequest` height. -func (bcR *BlockchainReactor) sendBlockRequest(peerID p2p.ID, height int64) error { - peer := bcR.Switch.Peers().Get(peerID) - if peer == nil { - return errNilPeerForBlockRequest - } - - msgBytes, err := bc.EncodeMsg(&bcproto.BlockRequest{Height: height}) - if err != nil { - return err - } - queued := peer.TrySend(BlockchainChannel, msgBytes) - if !queued { - return errSendQueueFull - } - return nil -} - -// Implements bcRNotifier -func (bcR *BlockchainReactor) switchToConsensus() { - conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) - if ok { - conR.SwitchToConsensus(bcR.state, bcR.blocksSynced > 0 || bcR.stateSynced) - bcR.eventsFromFSMCh <- bcFsmMessage{event: syncFinishedEv} - } - // else { - // Should only happen during testing. - // } -} - -// Implements bcRNotifier -// Called by FSM and pool: -// - pool calls when it detects slow peer or when peer times out -// - FSM calls when: -// - adding a block (addBlock) fails -// - reactor processing of a block reports failure and FSM sends back the peers of first and second blocks -func (bcR *BlockchainReactor) sendPeerError(err error, peerID p2p.ID) { - bcR.Logger.Info("sendPeerError:", "peer", peerID, "error", err) - msgData := bcFsmMessage{ - event: peerErrorEv, - data: bFsmEventData{ - peerID: peerID, - err: err, - }, - } - bcR.eventsFromFSMCh <- msgData -} - -// Implements bcRNotifier -func (bcR *BlockchainReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { - if timer == nil { - panic("nil timer pointer parameter") - } - if *timer == nil { - *timer = time.AfterFunc(timeout, func() { - msg := bcReactorMessage{ - event: stateTimeoutEv, - data: bReactorEventData{ - stateName: name, - }, - } - bcR.errorsForFSMCh <- msg - }) - } else { - (*timer).Reset(timeout) - } -} diff --git a/blockchain/v1/reactor_fsm.go b/blockchain/v1/reactor_fsm.go deleted file mode 100644 index 2571dff6a..000000000 --- a/blockchain/v1/reactor_fsm.go +++ /dev/null @@ -1,462 +0,0 @@ -package v1 - -import ( - "errors" - "fmt" - "sync" - "time" - - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -// Blockchain Reactor State -type bcReactorFSMState struct { - name string - - // called when transitioning out of current state - handle func(*BcReactorFSM, bReactorEvent, bReactorEventData) (next *bcReactorFSMState, err error) - // called when entering the state - enter func(fsm *BcReactorFSM) - - // timeout to ensure FSM is not stuck in a state forever - // the timer is owned and run by the fsm instance - timeout time.Duration -} - -func (s *bcReactorFSMState) String() string { - return s.name -} - -// BcReactorFSM is the datastructure for the Blockchain Reactor State Machine -type BcReactorFSM struct { - logger log.Logger - mtx sync.Mutex - - startTime time.Time - - state *bcReactorFSMState - stateTimer *time.Timer - pool *BlockPool - - // interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc. - toBcR bcReactor -} - -// NewFSM creates a new reactor FSM. -func NewFSM(height int64, toBcR bcReactor) *BcReactorFSM { - return &BcReactorFSM{ - state: unknown, - startTime: time.Now(), - pool: NewBlockPool(height, toBcR), - toBcR: toBcR, - } -} - -// bReactorEventData is part of the message sent by the reactor to the FSM and used by the state handlers. -type bReactorEventData struct { - peerID p2p.ID - err error // for peer error: timeout, slow; for processed block event if error occurred - base int64 // for status response - height int64 // for status response; for processed block event - block *types.Block // for block response - stateName string // for state timeout events - length int // for block response event, length of received block, used to detect slow peers - maxNumRequests int // for request needed event, maximum number of pending requests -} - -// Blockchain Reactor Events (the input to the state machine) -type bReactorEvent uint - -const ( - // message type events - startFSMEv = iota + 1 - statusResponseEv - blockResponseEv - noBlockResponseEv - processedBlockEv - makeRequestsEv - stopFSMEv - - // other events - peerRemoveEv = iota + 256 - stateTimeoutEv -) - -func (msg *bcReactorMessage) String() string { - var dataStr string - - switch msg.event { - case startFSMEv: - dataStr = "" - case statusResponseEv: - dataStr = fmt.Sprintf("peer=%v base=%v height=%v", msg.data.peerID, msg.data.base, msg.data.height) - case blockResponseEv: - dataStr = fmt.Sprintf("peer=%v block.height=%v length=%v", - msg.data.peerID, msg.data.block.Height, msg.data.length) - case noBlockResponseEv: - dataStr = fmt.Sprintf("peer=%v requested height=%v", - msg.data.peerID, msg.data.height) - case processedBlockEv: - dataStr = fmt.Sprintf("error=%v", msg.data.err) - case makeRequestsEv: - dataStr = "" - case stopFSMEv: - dataStr = "" - case peerRemoveEv: - dataStr = fmt.Sprintf("peer: %v is being removed by the switch", msg.data.peerID) - case stateTimeoutEv: - dataStr = fmt.Sprintf("state=%v", msg.data.stateName) - default: - dataStr = "cannot interpret message data" - } - - return fmt.Sprintf("%v: %v", msg.event, dataStr) -} - -func (ev bReactorEvent) String() string { - switch ev { - case startFSMEv: - return "startFSMEv" - case statusResponseEv: - return "statusResponseEv" - case blockResponseEv: - return "blockResponseEv" - case noBlockResponseEv: - return "noBlockResponseEv" - case processedBlockEv: - return "processedBlockEv" - case makeRequestsEv: - return "makeRequestsEv" - case stopFSMEv: - return "stopFSMEv" - case peerRemoveEv: - return "peerRemoveEv" - case stateTimeoutEv: - return "stateTimeoutEv" - default: - return "event unknown" - } - -} - -// states -var ( - unknown *bcReactorFSMState - waitForPeer *bcReactorFSMState - waitForBlock *bcReactorFSMState - finished *bcReactorFSMState -) - -// timeouts for state timers -const ( - waitForPeerTimeout = 3 * time.Second - waitForBlockAtCurrentHeightTimeout = 10 * time.Second -) - -// errors -var ( - // internal to the package - errNoErrorFinished = errors.New("fast sync is finished") - errInvalidEvent = errors.New("invalid event in current state") - errMissingBlock = errors.New("missing blocks") - errNilPeerForBlockRequest = errors.New("peer for block request does not exist in the switch") - errSendQueueFull = errors.New("block request not made, send-queue is full") - errPeerTooShort = errors.New("peer height too low, old peer removed/ new peer not added") - errSwitchRemovesPeer = errors.New("switch is removing peer") - errTimeoutEventWrongState = errors.New("timeout event for a state different than the current one") - errNoTallerPeer = errors.New("fast sync timed out on waiting for a peer taller than this node") - - // reported eventually to the switch - // handle return - errPeerLowersItsHeight = errors.New("fast sync peer reports a height lower than previous") - // handle return - errNoPeerResponseForCurrentHeights = errors.New("fast sync timed out on peer block response for current heights") - errNoPeerResponse = errors.New("fast sync timed out on peer block response") // xx - errBadDataFromPeer = errors.New("fast sync received block from wrong peer or block is bad") // xx - errDuplicateBlock = errors.New("fast sync received duplicate block from peer") - errBlockVerificationFailure = errors.New("fast sync block verification failure") // xx - errSlowPeer = errors.New("fast sync peer is not sending us data fast enough") // xx - -) - -func init() { - unknown = &bcReactorFSMState{ - name: "unknown", - handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { - switch ev { - case startFSMEv: - // Broadcast Status message. Currently doesn't return non-nil error. - fsm.toBcR.sendStatusRequest() - return waitForPeer, nil - - case stopFSMEv: - return finished, errNoErrorFinished - - default: - return unknown, errInvalidEvent - } - }, - } - - waitForPeer = &bcReactorFSMState{ - name: "waitForPeer", - timeout: waitForPeerTimeout, - enter: func(fsm *BcReactorFSM) { - // Stop when leaving the state. - fsm.resetStateTimer() - }, - handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { - switch ev { - case stateTimeoutEv: - if data.stateName != "waitForPeer" { - fsm.logger.Error("received a state timeout event for different state", - "state", data.stateName) - return waitForPeer, errTimeoutEventWrongState - } - // There was no statusResponse received from any peer. - // Should we send status request again? - return finished, errNoTallerPeer - - case statusResponseEv: - if err := fsm.pool.UpdatePeer(data.peerID, data.base, data.height); err != nil { - if fsm.pool.NumPeers() == 0 { - return waitForPeer, err - } - } - if fsm.stateTimer != nil { - fsm.stateTimer.Stop() - } - return waitForBlock, nil - - case stopFSMEv: - if fsm.stateTimer != nil { - fsm.stateTimer.Stop() - } - return finished, errNoErrorFinished - - default: - return waitForPeer, errInvalidEvent - } - }, - } - - waitForBlock = &bcReactorFSMState{ - name: "waitForBlock", - timeout: waitForBlockAtCurrentHeightTimeout, - enter: func(fsm *BcReactorFSM) { - // Stop when leaving the state. - fsm.resetStateTimer() - }, - handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { - switch ev { - - case statusResponseEv: - err := fsm.pool.UpdatePeer(data.peerID, data.base, data.height) - if fsm.pool.NumPeers() == 0 { - return waitForPeer, err - } - if fsm.pool.ReachedMaxHeight() { - return finished, err - } - return waitForBlock, err - - case blockResponseEv: - fsm.logger.Debug("blockResponseEv", "H", data.block.Height) - err := fsm.pool.AddBlock(data.peerID, data.block, data.length) - if err != nil { - // A block was received that was unsolicited, from unexpected peer, or that we already have it. - // Ignore block, remove peer and send error to switch. - fsm.pool.RemovePeer(data.peerID, err) - fsm.toBcR.sendPeerError(err, data.peerID) - } - if fsm.pool.NumPeers() == 0 { - return waitForPeer, err - } - return waitForBlock, err - case noBlockResponseEv: - fsm.logger.Error("peer does not have requested block", "peer", data.peerID) - - return waitForBlock, nil - case processedBlockEv: - if data.err != nil { - first, second, _ := fsm.pool.FirstTwoBlocksAndPeers() - fsm.logger.Error("error processing block", "err", data.err, - "first", first.block.Height, "second", second.block.Height) - fsm.logger.Error("send peer error for", "peer", first.peer.ID) - fsm.toBcR.sendPeerError(data.err, first.peer.ID) - fsm.logger.Error("send peer error for", "peer", second.peer.ID) - fsm.toBcR.sendPeerError(data.err, second.peer.ID) - // Remove the first two blocks. This will also remove the peers - fsm.pool.InvalidateFirstTwoBlocks(data.err) - } else { - fsm.pool.ProcessedCurrentHeightBlock() - // Since we advanced one block reset the state timer - fsm.resetStateTimer() - } - - // Both cases above may result in achieving maximum height. - if fsm.pool.ReachedMaxHeight() { - return finished, nil - } - - return waitForBlock, data.err - - case peerRemoveEv: - // This event is sent by the switch to remove disconnected and errored peers. - fsm.pool.RemovePeer(data.peerID, data.err) - if fsm.pool.NumPeers() == 0 { - return waitForPeer, nil - } - if fsm.pool.ReachedMaxHeight() { - return finished, nil - } - return waitForBlock, nil - - case makeRequestsEv: - fsm.makeNextRequests(data.maxNumRequests) - return waitForBlock, nil - - case stateTimeoutEv: - if data.stateName != "waitForBlock" { - fsm.logger.Error("received a state timeout event for different state", - "state", data.stateName) - return waitForBlock, errTimeoutEventWrongState - } - // We haven't received the block at current height or height+1. Remove peer. - fsm.pool.RemovePeerAtCurrentHeights(errNoPeerResponseForCurrentHeights) - fsm.resetStateTimer() - if fsm.pool.NumPeers() == 0 { - return waitForPeer, errNoPeerResponseForCurrentHeights - } - if fsm.pool.ReachedMaxHeight() { - return finished, nil - } - return waitForBlock, errNoPeerResponseForCurrentHeights - - case stopFSMEv: - if fsm.stateTimer != nil { - fsm.stateTimer.Stop() - } - return finished, errNoErrorFinished - - default: - return waitForBlock, errInvalidEvent - } - }, - } - - finished = &bcReactorFSMState{ - name: "finished", - enter: func(fsm *BcReactorFSM) { - fsm.logger.Info("Time to switch to consensus reactor!", "height", fsm.pool.Height) - fsm.toBcR.switchToConsensus() - fsm.cleanup() - }, - handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { - return finished, nil - }, - } -} - -// Interface used by FSM for sending Block and Status requests, -// informing of peer errors and state timeouts -// Implemented by BlockchainReactor and tests -type bcReactor interface { - sendStatusRequest() - sendBlockRequest(peerID p2p.ID, height int64) error - sendPeerError(err error, peerID p2p.ID) - resetStateTimer(name string, timer **time.Timer, timeout time.Duration) - switchToConsensus() -} - -// SetLogger sets the FSM logger. -func (fsm *BcReactorFSM) SetLogger(l log.Logger) { - fsm.logger = l - fsm.pool.SetLogger(l) -} - -// Start starts the FSM. -func (fsm *BcReactorFSM) Start() { - _ = fsm.Handle(&bcReactorMessage{event: startFSMEv}) -} - -// Handle processes messages and events sent to the FSM. -func (fsm *BcReactorFSM) Handle(msg *bcReactorMessage) error { - fsm.mtx.Lock() - defer fsm.mtx.Unlock() - fsm.logger.Debug("FSM received", "event", msg, "state", fsm.state) - - if fsm.state == nil { - fsm.state = unknown - } - next, err := fsm.state.handle(fsm, msg.event, msg.data) - if err != nil { - fsm.logger.Error("FSM event handler returned", "err", err, - "state", fsm.state, "event", msg.event) - } - - oldState := fsm.state.name - fsm.transition(next) - if oldState != fsm.state.name { - fsm.logger.Info("FSM changed state", "new_state", fsm.state) - } - return err -} - -func (fsm *BcReactorFSM) transition(next *bcReactorFSMState) { - if next == nil { - return - } - if fsm.state != next { - fsm.state = next - if next.enter != nil { - next.enter(fsm) - } - } -} - -// Called when entering an FSM state in order to detect lack of progress in the state machine. -// Note the use of the 'bcr' interface to facilitate testing without timer expiring. -func (fsm *BcReactorFSM) resetStateTimer() { - fsm.toBcR.resetStateTimer(fsm.state.name, &fsm.stateTimer, fsm.state.timeout) -} - -func (fsm *BcReactorFSM) isCaughtUp() bool { - return fsm.state == finished -} - -func (fsm *BcReactorFSM) makeNextRequests(maxNumRequests int) { - fsm.pool.MakeNextRequests(maxNumRequests) -} - -func (fsm *BcReactorFSM) cleanup() { - fsm.pool.Cleanup() -} - -// NeedsBlocks checks if more block requests are required. -func (fsm *BcReactorFSM) NeedsBlocks() bool { - fsm.mtx.Lock() - defer fsm.mtx.Unlock() - return fsm.state.name == "waitForBlock" && fsm.pool.NeedsBlocks() -} - -// FirstTwoBlocks returns the two blocks at pool height and height+1 -func (fsm *BcReactorFSM) FirstTwoBlocks() (first, second *types.Block, err error) { - fsm.mtx.Lock() - defer fsm.mtx.Unlock() - firstBP, secondBP, err := fsm.pool.FirstTwoBlocksAndPeers() - if err == nil { - first = firstBP.block - second = secondBP.block - } - return -} - -// Status returns the pool's height and the maximum peer height. -func (fsm *BcReactorFSM) Status() (height, maxPeerHeight int64) { - fsm.mtx.Lock() - defer fsm.mtx.Unlock() - return fsm.pool.Height, fsm.pool.MaxPeerHeight -} diff --git a/blockchain/v1/reactor_fsm_test.go b/blockchain/v1/reactor_fsm_test.go deleted file mode 100644 index 99b8d378d..000000000 --- a/blockchain/v1/reactor_fsm_test.go +++ /dev/null @@ -1,944 +0,0 @@ -package v1 - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/tendermint/tendermint/libs/log" - tmmath "github.com/tendermint/tendermint/libs/math" - tmrand "github.com/tendermint/tendermint/libs/rand" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -type lastBlockRequestT struct { - peerID p2p.ID - height int64 -} - -type lastPeerErrorT struct { - peerID p2p.ID - err error -} - -// reactor for FSM testing -type testReactor struct { - logger log.Logger - fsm *BcReactorFSM - numStatusRequests int - numBlockRequests int - lastBlockRequest lastBlockRequestT - lastPeerError lastPeerErrorT - stateTimerStarts map[string]int -} - -func sendEventToFSM(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) error { - return fsm.Handle(&bcReactorMessage{event: ev, data: data}) -} - -type fsmStepTestValues struct { - currentState string - event bReactorEvent - data bReactorEventData - - wantErr error - wantState string - wantStatusReqSent bool - wantReqIncreased bool - wantNewBlocks []int64 - wantRemovedPeers []p2p.ID -} - -// --------------------------------------------------------------------------- -// helper test function for different FSM events, state and expected behavior -func sStopFSMEv(current, expected string) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: stopFSMEv, - wantState: expected, - wantErr: errNoErrorFinished} -} - -func sUnknownFSMEv(current string) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: 1234, - wantState: current, - wantErr: errInvalidEvent} -} - -func sStartFSMEv() fsmStepTestValues { - return fsmStepTestValues{ - currentState: "unknown", - event: startFSMEv, - wantState: "waitForPeer", - wantStatusReqSent: true} -} - -func sStateTimeoutEv(current, expected string, timedoutState string, wantErr error) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: stateTimeoutEv, - data: bReactorEventData{ - stateName: timedoutState, - }, - wantState: expected, - wantErr: wantErr, - } -} - -func sProcessedBlockEv(current, expected string, reactorError error) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: processedBlockEv, - data: bReactorEventData{ - err: reactorError, - }, - wantState: expected, - wantErr: reactorError, - } -} - -func sStatusEv(current, expected string, peerID p2p.ID, height int64, err error) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: statusResponseEv, - data: bReactorEventData{peerID: peerID, height: height}, - wantState: expected, - wantErr: err} -} - -func sMakeRequestsEv(current, expected string, maxPendingRequests int) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: makeRequestsEv, - data: bReactorEventData{maxNumRequests: maxPendingRequests}, - wantState: expected, - wantReqIncreased: true, - } -} - -func sMakeRequestsEvErrored(current, expected string, - maxPendingRequests int, err error, peersRemoved []p2p.ID) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: makeRequestsEv, - data: bReactorEventData{maxNumRequests: maxPendingRequests}, - wantState: expected, - wantErr: err, - wantRemovedPeers: peersRemoved, - wantReqIncreased: true, - } -} - -func sBlockRespEv(current, expected string, peerID p2p.ID, height int64, prevBlocks []int64) fsmStepTestValues { - txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} - return fsmStepTestValues{ - currentState: current, - event: blockResponseEv, - data: bReactorEventData{ - peerID: peerID, - height: height, - block: types.MakeBlock(height, txs, nil, nil), - length: 100}, - wantState: expected, - wantNewBlocks: append(prevBlocks, height), - } -} - -func sBlockRespEvErrored(current, expected string, - peerID p2p.ID, height int64, prevBlocks []int64, wantErr error, peersRemoved []p2p.ID) fsmStepTestValues { - txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} - - return fsmStepTestValues{ - currentState: current, - event: blockResponseEv, - data: bReactorEventData{ - peerID: peerID, - height: height, - block: types.MakeBlock(height, txs, nil, nil), - length: 100}, - wantState: expected, - wantErr: wantErr, - wantRemovedPeers: peersRemoved, - wantNewBlocks: prevBlocks, - } -} - -func sPeerRemoveEv(current, expected string, peerID p2p.ID, err error, peersRemoved []p2p.ID) fsmStepTestValues { - return fsmStepTestValues{ - currentState: current, - event: peerRemoveEv, - data: bReactorEventData{ - peerID: peerID, - err: err, - }, - wantState: expected, - wantRemovedPeers: peersRemoved, - } -} - -// -------------------------------------------- - -func newTestReactor(height int64) *testReactor { - testBcR := &testReactor{logger: log.TestingLogger(), stateTimerStarts: make(map[string]int)} - testBcR.fsm = NewFSM(height, testBcR) - testBcR.fsm.SetLogger(testBcR.logger) - return testBcR -} - -func fixBlockResponseEvStep(step *fsmStepTestValues, testBcR *testReactor) { - // There is currently no good way to know to which peer a block request was sent. - // So in some cases where it does not matter, before we simulate a block response - // we cheat and look where it is expected from. - if step.event == blockResponseEv { - height := step.data.height - peerID, ok := testBcR.fsm.pool.blocks[height] - if ok { - step.data.peerID = peerID - } - } -} - -type testFields struct { - name string - startingHeight int64 - maxRequestsPerPeer int - maxPendingRequests int - steps []fsmStepTestValues -} - -func executeFSMTests(t *testing.T, tests []testFields, matchRespToReq bool) { - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // Create test reactor - testBcR := newTestReactor(tt.startingHeight) - - if tt.maxRequestsPerPeer != 0 { - maxRequestsPerPeer = tt.maxRequestsPerPeer - } - - for _, step := range tt.steps { - step := step - assert.Equal(t, step.currentState, testBcR.fsm.state.name) - - var heightBefore int64 - if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { - heightBefore = testBcR.fsm.pool.Height - } - oldNumStatusRequests := testBcR.numStatusRequests - oldNumBlockRequests := testBcR.numBlockRequests - if matchRespToReq { - fixBlockResponseEvStep(&step, testBcR) - } - - fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) - assert.Equal(t, step.wantErr, fsmErr) - - if step.wantStatusReqSent { - assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) - } else { - assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) - } - - if step.wantReqIncreased { - assert.True(t, oldNumBlockRequests < testBcR.numBlockRequests) - } else { - assert.Equal(t, oldNumBlockRequests, testBcR.numBlockRequests) - } - - for _, height := range step.wantNewBlocks { - _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(height) - assert.Nil(t, err) - } - if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { - heightAfter := testBcR.fsm.pool.Height - assert.Equal(t, heightBefore, heightAfter) - firstAfter, err1 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) - secondAfter, err2 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) - assert.NotNil(t, err1) - assert.NotNil(t, err2) - assert.Nil(t, firstAfter) - assert.Nil(t, secondAfter) - } - - assert.Equal(t, step.wantState, testBcR.fsm.state.name) - - if step.wantState == "finished" { - assert.True(t, testBcR.fsm.isCaughtUp()) - } - } - }) - } -} - -func TestFSMBasic(t *testing.T) { - tests := []testFields{ - { - name: "one block, one peer - TS2", - startingHeight: 1, - maxRequestsPerPeer: 2, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 2, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), - sProcessedBlockEv("waitForBlock", "finished", nil), - }, - }, - { - name: "multi block, multi peer - TS2", - startingHeight: 1, - maxRequestsPerPeer: 2, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 4, nil), - sStatusEv("waitForBlock", "waitForBlock", "P2", 4, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 4, []int64{1, 2, 3}), - - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - sProcessedBlockEv("waitForBlock", "finished", nil), - }, - }, - } - - executeFSMTests(t, tests, true) -} - -func TestFSMBlockVerificationFailure(t *testing.T) { - tests := []testFields{ - { - name: "block verification failure - TS2 variant", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - - // add P1 and get blocks 1-3 from it - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), - - // add P2 - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), - - // process block failure, should remove P1 and all blocks - sProcessedBlockEv("waitForBlock", "waitForBlock", errBlockVerificationFailure), - - // get blocks 1-3 from P2 - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), - - // finish after processing blocks 1 and 2 - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - sProcessedBlockEv("waitForBlock", "finished", nil), - }, - }, - } - - executeFSMTests(t, tests, false) -} - -func TestFSMBadBlockFromPeer(t *testing.T) { - tests := []testFields{ - { - name: "block we haven't asked for", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 and ask for blocks 1-3 - sStatusEv("waitForPeer", "waitForBlock", "P1", 300, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - // blockResponseEv for height 100 should cause an error - sBlockRespEvErrored("waitForBlock", "waitForPeer", - "P1", 100, []int64{}, errMissingBlock, []p2p.ID{}), - }, - }, - { - name: "block we already have", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 and get block 1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 100, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", - "P1", 1, []int64{}), - - // Get block 1 again. Since peer is removed together with block 1, - // the blocks present in the pool should be {} - sBlockRespEvErrored("waitForBlock", "waitForPeer", - "P1", 1, []int64{}, errDuplicateBlock, []p2p.ID{"P1"}), - }, - }, - { - name: "block from unknown peer", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 and get block 1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - - // get block 1 from unknown peer P2 - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEvErrored("waitForBlock", "waitForBlock", - "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), - }, - }, - { - name: "block from wrong peer", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1, make requests for blocks 1-3 to P1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - // add P2 - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), - - // receive block 1 from P2 - sBlockRespEvErrored("waitForBlock", "waitForBlock", - "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), - }, - }, - } - - executeFSMTests(t, tests, false) -} - -func TestFSMBlockAtCurrentHeightDoesNotArriveInTime(t *testing.T) { - tests := []testFields{ - { - name: "block at current height undelivered - TS5", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1, get blocks 1 and 2, process block 1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", - "P1", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", - "P1", 2, []int64{1}), - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - - // add P2 - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), - - // timeout on block 3, P1 should be removed - sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), - - // make requests and finish by receiving blocks 2 and 3 from P2 - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{2}), - sProcessedBlockEv("waitForBlock", "finished", nil), - }, - }, - { - name: "block at current height undelivered, at maxPeerHeight after peer removal - TS3", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1, request blocks 1-3 from P1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - // add P2 (tallest) - sStatusEv("waitForBlock", "waitForBlock", "P2", 30, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - // receive blocks 1-3 from P1 - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), - - // process blocks at heights 1 and 2 - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - - // timeout on block at height 4 - sStateTimeoutEv("waitForBlock", "finished", "waitForBlock", nil), - }, - }, - } - - executeFSMTests(t, tests, true) -} - -func TestFSMPeerRelatedEvents(t *testing.T) { - tests := []testFields{ - { - name: "peer remove event with no blocks", - startingHeight: 1, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1, P2, P3 - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), - sStatusEv("waitForBlock", "waitForBlock", "P3", 3, nil), - - // switch removes P2 - sPeerRemoveEv("waitForBlock", "waitForBlock", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), - }, - }, - { - name: "only peer removed while in waitForBlock state", - startingHeight: 100, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), - - // switch removes P1 - sPeerRemoveEv("waitForBlock", "waitForPeer", "P1", errSwitchRemovesPeer, []p2p.ID{"P1"}), - }, - }, - { - name: "highest peer removed while in waitForBlock state, node reaches maxPeerHeight - TS4 ", - startingHeight: 100, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 and make requests - sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - // add P2 - sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), - - // get blocks 100 and 101 from P1 and process block at height 100 - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - - // switch removes peer P1, should be finished - sPeerRemoveEv("waitForBlock", "finished", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), - }, - }, - { - name: "highest peer lowers its height in waitForBlock state, node reaches maxPeerHeight - TS4", - startingHeight: 100, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 and make requests - sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - - // add P2 - sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), - - // get blocks 100 and 101 from P1 - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), - sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), - - // processed block at heights 100 - sProcessedBlockEv("waitForBlock", "waitForBlock", nil), - - // P2 becomes short - sStatusEv("waitForBlock", "finished", "P2", 100, errPeerLowersItsHeight), - }, - }, - { - name: "new short peer while in waitForPeer state", - startingHeight: 100, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForPeer", "P1", 3, errPeerTooShort), - }, - }, - { - name: "new short peer while in waitForBlock state", - startingHeight: 100, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, errPeerTooShort), - }, - }, - { - name: "only peer updated with low height while in waitForBlock state", - startingHeight: 100, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), - sStatusEv("waitForBlock", "waitForPeer", "P1", 3, errPeerLowersItsHeight), - }, - }, - { - name: "peer does not exist in the switch", - startingHeight: 9999999, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - // add P1 - sStatusEv("waitForPeer", "waitForBlock", "P1", 20000000, nil), - // send request for block 9999999 - // Note: For this block request the "switch missing the peer" error is simulated, - // see implementation of bcReactor interface, sendBlockRequest(), in this file. - sMakeRequestsEvErrored("waitForBlock", "waitForBlock", - maxNumRequests, nil, []p2p.ID{"P1"}), - }, - }, - } - - executeFSMTests(t, tests, true) -} - -func TestFSMStopFSM(t *testing.T) { - tests := []testFields{ - { - name: "stopFSMEv in unknown", - steps: []fsmStepTestValues{ - sStopFSMEv("unknown", "finished"), - }, - }, - { - name: "stopFSMEv in waitForPeer", - startingHeight: 1, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStopFSMEv("waitForPeer", "finished"), - }, - }, - { - name: "stopFSMEv in waitForBlock", - startingHeight: 1, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sStopFSMEv("waitForBlock", "finished"), - }, - }, - } - - executeFSMTests(t, tests, false) -} - -func TestFSMUnknownElements(t *testing.T) { - tests := []testFields{ - { - name: "unknown event for state unknown", - steps: []fsmStepTestValues{ - sUnknownFSMEv("unknown"), - }, - }, - { - name: "unknown event for state waitForPeer", - steps: []fsmStepTestValues{ - sStartFSMEv(), - sUnknownFSMEv("waitForPeer"), - }, - }, - { - name: "unknown event for state waitForBlock", - startingHeight: 1, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sUnknownFSMEv("waitForBlock"), - }, - }, - } - - executeFSMTests(t, tests, false) -} - -func TestFSMPeerStateTimeoutEvent(t *testing.T) { - tests := []testFields{ - { - name: "timeout event for state waitForPeer while in state waitForPeer - TS1", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStateTimeoutEv("waitForPeer", "finished", "waitForPeer", errNoTallerPeer), - }, - }, - { - name: "timeout event for state waitForPeer while in a state != waitForPeer", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStateTimeoutEv("waitForPeer", "waitForPeer", "waitForBlock", errTimeoutEventWrongState), - }, - }, - { - name: "timeout event for state waitForBlock while in state waitForBlock ", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sStateTimeoutEv("waitForBlock", "waitForPeer", "waitForBlock", errNoPeerResponseForCurrentHeights), - }, - }, - { - name: "timeout event for state waitForBlock while in a state != waitForBlock", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForPeer", errTimeoutEventWrongState), - }, - }, - { - name: "timeout event for state waitForBlock with multiple peers", - startingHeight: 1, - maxRequestsPerPeer: 3, - steps: []fsmStepTestValues{ - sStartFSMEv(), - sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), - sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), - }, - }, - } - - executeFSMTests(t, tests, false) -} - -func makeCorrectTransitionSequence(startingHeight int64, numBlocks int64, numPeers int, randomPeerHeights bool, - maxRequestsPerPeer int, maxPendingRequests int) testFields { - - // Generate numPeers peers with random or numBlocks heights according to the randomPeerHeights flag. - peerHeights := make([]int64, numPeers) - for i := 0; i < numPeers; i++ { - if i == 0 { - peerHeights[0] = numBlocks - continue - } - if randomPeerHeights { - peerHeights[i] = int64(tmmath.MaxInt(tmrand.Intn(int(numBlocks)), int(startingHeight)+1)) - } else { - peerHeights[i] = numBlocks - } - } - - // Approximate the slice capacity to save time for appends. - testSteps := make([]fsmStepTestValues, 0, 3*numBlocks+int64(numPeers)) - - testName := fmt.Sprintf("%v-blocks %v-startingHeight %v-peers %v-maxRequestsPerPeer %v-maxNumRequests", - numBlocks, startingHeight, numPeers, maxRequestsPerPeer, maxPendingRequests) - - // Add startFSMEv step. - testSteps = append(testSteps, sStartFSMEv()) - - // For each peer, add statusResponseEv step. - for i := 0; i < numPeers; i++ { - peerName := fmt.Sprintf("P%d", i) - if i == 0 { - testSteps = append( - testSteps, - sStatusEv("waitForPeer", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) - } else { - testSteps = append(testSteps, - sStatusEv("waitForBlock", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) - } - } - - height := startingHeight - numBlocksReceived := 0 - prevBlocks := make([]int64, 0, maxPendingRequests) - -forLoop: - for i := 0; i < int(numBlocks); i++ { - - // Add the makeRequestEv step periodically. - if i%maxRequestsPerPeer == 0 { - testSteps = append( - testSteps, - sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), - ) - } - - // Add the blockRespEv step - testSteps = append( - testSteps, - sBlockRespEv("waitForBlock", "waitForBlock", - "P0", height, prevBlocks)) - prevBlocks = append(prevBlocks, height) - height++ - numBlocksReceived++ - - // Add the processedBlockEv step periodically. - if numBlocksReceived >= maxRequestsPerPeer || height >= numBlocks { - for j := int(height) - numBlocksReceived; j < int(height); j++ { - if j >= int(numBlocks) { - // This is the last block that is processed, we should be in "finished" state. - testSteps = append( - testSteps, - sProcessedBlockEv("waitForBlock", "finished", nil)) - break forLoop - } - testSteps = append( - testSteps, - sProcessedBlockEv("waitForBlock", "waitForBlock", nil)) - } - numBlocksReceived = 0 - prevBlocks = make([]int64, 0, maxPendingRequests) - } - } - - return testFields{ - name: testName, - startingHeight: startingHeight, - maxRequestsPerPeer: maxRequestsPerPeer, - maxPendingRequests: maxPendingRequests, - steps: testSteps, - } -} - -const ( - maxStartingHeightTest = 100 - maxRequestsPerPeerTest = 20 - maxTotalPendingRequestsTest = 600 - maxNumPeersTest = 1000 - maxNumBlocksInChainTest = 10000 // should be smaller than 9999999 -) - -func makeCorrectTransitionSequenceWithRandomParameters() testFields { - // Generate a starting height for fast sync. - startingHeight := int64(tmrand.Intn(maxStartingHeightTest) + 1) - - // Generate the number of requests per peer. - maxRequestsPerPeer := tmrand.Intn(maxRequestsPerPeerTest) + 1 - - // Generate the maximum number of total pending requests, >= maxRequestsPerPeer. - maxPendingRequests := tmrand.Intn(maxTotalPendingRequestsTest-maxRequestsPerPeer) + maxRequestsPerPeer - - // Generate the number of blocks to be synced. - numBlocks := int64(tmrand.Intn(maxNumBlocksInChainTest)) + startingHeight - - // Generate a number of peers. - numPeers := tmrand.Intn(maxNumPeersTest) + 1 - - return makeCorrectTransitionSequence(startingHeight, numBlocks, numPeers, true, maxRequestsPerPeer, maxPendingRequests) -} - -func shouldApplyProcessedBlockEvStep(step *fsmStepTestValues, testBcR *testReactor) bool { - if step.event == processedBlockEv { - _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) - if err == errMissingBlock { - return false - } - _, err = testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) - if err == errMissingBlock { - return false - } - } - return true -} - -func TestFSMCorrectTransitionSequences(t *testing.T) { - - tests := []testFields{ - makeCorrectTransitionSequence(1, 100, 10, true, 10, 40), - makeCorrectTransitionSequenceWithRandomParameters(), - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // Create test reactor - testBcR := newTestReactor(tt.startingHeight) - - if tt.maxRequestsPerPeer != 0 { - maxRequestsPerPeer = tt.maxRequestsPerPeer - } - - for _, step := range tt.steps { - step := step - assert.Equal(t, step.currentState, testBcR.fsm.state.name) - - oldNumStatusRequests := testBcR.numStatusRequests - fixBlockResponseEvStep(&step, testBcR) - if !shouldApplyProcessedBlockEvStep(&step, testBcR) { - continue - } - - fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) - assert.Equal(t, step.wantErr, fsmErr) - - if step.wantStatusReqSent { - assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) - } else { - assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) - } - - assert.Equal(t, step.wantState, testBcR.fsm.state.name) - if step.wantState == "finished" { - assert.True(t, testBcR.fsm.isCaughtUp()) - } - } - - }) - } -} - -// ---------------------------------------- -// implements the bcRNotifier -func (testR *testReactor) sendPeerError(err error, peerID p2p.ID) { - testR.logger.Info("Reactor received sendPeerError call from FSM", "peer", peerID, "err", err) - testR.lastPeerError.peerID = peerID - testR.lastPeerError.err = err -} - -func (testR *testReactor) sendStatusRequest() { - testR.logger.Info("Reactor received sendStatusRequest call from FSM") - testR.numStatusRequests++ -} - -func (testR *testReactor) sendBlockRequest(peerID p2p.ID, height int64) error { - testR.logger.Info("Reactor received sendBlockRequest call from FSM", "peer", peerID, "height", height) - testR.numBlockRequests++ - testR.lastBlockRequest.peerID = peerID - testR.lastBlockRequest.height = height - if height == 9999999 { - // simulate switch does not have peer - return errNilPeerForBlockRequest - } - return nil -} - -func (testR *testReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { - testR.logger.Info("Reactor received resetStateTimer call from FSM", "state", name, "timeout", timeout) - if _, ok := testR.stateTimerStarts[name]; !ok { - testR.stateTimerStarts[name] = 1 - } else { - testR.stateTimerStarts[name]++ - } -} - -func (testR *testReactor) switchToConsensus() { -} - -// ---------------------------------------- diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go deleted file mode 100644 index c0f371905..000000000 --- a/blockchain/v1/reactor_test.go +++ /dev/null @@ -1,365 +0,0 @@ -package v1 - -import ( - "fmt" - "os" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - dbm "github.com/tendermint/tm-db" - - abci "github.com/tendermint/tendermint/abci/types" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/mempool/mock" - "github.com/tendermint/tendermint/p2p" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - "github.com/tendermint/tendermint/proxy" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/store" - "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" -) - -var config *cfg.Config - -func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { - validators := make([]types.GenesisValidator, numValidators) - privValidators := make([]types.PrivValidator, numValidators) - for i := 0; i < numValidators; i++ { - val, privVal := types.RandValidator(randPower, minPower) - validators[i] = types.GenesisValidator{ - PubKey: val.PubKey, - Power: val.VotingPower, - } - privValidators[i] = privVal - } - sort.Sort(types.PrivValidatorsByAddress(privValidators)) - - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: config.ChainID(), - Validators: validators, - }, privValidators -} - -func makeVote( - t *testing.T, - header *types.Header, - blockID types.BlockID, - valset *types.ValidatorSet, - privVal types.PrivValidator) *types.Vote { - - pubKey, err := privVal.GetPubKey() - require.NoError(t, err) - - valIdx, _ := valset.GetByAddress(pubKey.Address()) - vote := &types.Vote{ - ValidatorAddress: pubKey.Address(), - ValidatorIndex: valIdx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: tmproto.PrecommitType, - BlockID: blockID, - } - - vpb := vote.ToProto() - - _ = privVal.SignVote(header.ChainID, vpb) - vote.Signature = vpb.Signature - - return vote -} - -type BlockchainReactorPair struct { - bcR *BlockchainReactor - conR *consensusReactorTest -} - -func newBlockchainReactor( - t *testing.T, - logger log.Logger, - genDoc *types.GenesisDoc, - privVals []types.PrivValidator, - maxBlockHeight int64) *BlockchainReactor { - if len(privVals) != 1 { - panic("only support one validator") - } - - app := &testApp{} - cc := proxy.NewLocalClientCreator(app) - proxyApp := proxy.NewAppConns(cc) - err := proxyApp.Start() - if err != nil { - panic(fmt.Errorf("error start app: %w", err)) - } - - blockDB := dbm.NewMemDB() - stateDB := dbm.NewMemDB() - stateStore := sm.NewStore(stateDB) - blockStore := store.NewBlockStore(blockDB) - - state, err := stateStore.LoadFromDBOrGenesisDoc(genDoc) - if err != nil { - panic(fmt.Errorf("error constructing state from genesis file: %w", err)) - } - - // Make the BlockchainReactor itself. - // NOTE we have to create and commit the blocks first because - // pool.height is determined from the store. - fastSync := true - db := dbm.NewMemDB() - stateStore = sm.NewStore(db) - blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), - mock.Mempool{}, sm.EmptyEvidencePool{}) - if err = stateStore.Save(state); err != nil { - panic(err) - } - - // let's add some blocks in - for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { - lastCommit := types.NewCommit(blockHeight-1, 1, types.BlockID{}, nil) - if blockHeight > 1 { - lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) - lastBlock := blockStore.LoadBlock(blockHeight - 1) - - vote := makeVote(t, &lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]) - lastCommit = types.NewCommit(vote.Height, vote.Round, lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()}) - } - - thisBlock := makeBlock(blockHeight, state, lastCommit) - - thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{Hash: thisBlock.Hash(), PartSetHeader: thisParts.Header()} - - state, _, err = blockExec.ApplyBlock(state, blockID, thisBlock) - if err != nil { - panic(fmt.Errorf("error apply block: %w", err)) - } - - blockStore.SaveBlock(thisBlock, thisParts, lastCommit) - } - - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - bcReactor.SetLogger(logger.With("module", "blockchain")) - - return bcReactor -} - -func newBlockchainReactorPair( - t *testing.T, - logger log.Logger, - genDoc *types.GenesisDoc, - privVals []types.PrivValidator, - maxBlockHeight int64) BlockchainReactorPair { - - consensusReactor := &consensusReactorTest{} - consensusReactor.BaseReactor = *p2p.NewBaseReactor("Consensus reactor", consensusReactor) - - return BlockchainReactorPair{ - newBlockchainReactor(t, logger, genDoc, privVals, maxBlockHeight), - consensusReactor} -} - -type consensusReactorTest struct { - p2p.BaseReactor // BaseService + p2p.Switch - switchedToConsensus bool - mtx sync.Mutex -} - -func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced bool) { - conR.mtx.Lock() - defer conR.mtx.Unlock() - conR.switchedToConsensus = true -} - -func TestFastSyncNoBlockResponse(t *testing.T) { - - config = cfg.ResetTestRoot("blockchain_new_reactor_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - - maxBlockHeight := int64(65) - - reactorPairs := make([]BlockchainReactorPair, 2) - - logger := log.TestingLogger() - reactorPairs[0] = newBlockchainReactorPair(t, logger, genDoc, privVals, maxBlockHeight) - reactorPairs[1] = newBlockchainReactorPair(t, logger, genDoc, privVals, 0) - - p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) - s.AddReactor("CONSENSUS", reactorPairs[i].conR) - moduleName := fmt.Sprintf("blockchain-%v", i) - reactorPairs[i].bcR.SetLogger(logger.With("module", moduleName)) - - return s - - }, p2p.Connect2Switches) - - defer func() { - for _, r := range reactorPairs { - _ = r.bcR.Stop() - _ = r.conR.Stop() - } - }() - - tests := []struct { - height int64 - existent bool - }{ - {maxBlockHeight + 2, false}, - {10, true}, - {1, true}, - {maxBlockHeight + 100, false}, - } - - for { - time.Sleep(10 * time.Millisecond) - reactorPairs[1].conR.mtx.Lock() - if reactorPairs[1].conR.switchedToConsensus { - reactorPairs[1].conR.mtx.Unlock() - break - } - reactorPairs[1].conR.mtx.Unlock() - } - - assert.Equal(t, maxBlockHeight, reactorPairs[0].bcR.store.Height()) - - for _, tt := range tests { - block := reactorPairs[1].bcR.store.LoadBlock(tt.height) - if tt.existent { - assert.True(t, block != nil) - } else { - assert.True(t, block == nil) - } - } -} - -// NOTE: This is too hard to test without -// an easy way to add test peer to switch -// or without significant refactoring of the module. -// Alternatively we could actually dial a TCP conn but -// that seems extreme. -func TestFastSyncBadBlockStopsPeer(t *testing.T) { - numNodes := 4 - maxBlockHeight := int64(148) - - config = cfg.ResetTestRoot("blockchain_reactor_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - - otherChain := newBlockchainReactorPair(t, log.TestingLogger(), genDoc, privVals, maxBlockHeight) - defer func() { - _ = otherChain.bcR.Stop() - _ = otherChain.conR.Stop() - }() - - reactorPairs := make([]BlockchainReactorPair, numNodes) - logger := make([]log.Logger, numNodes) - - for i := 0; i < numNodes; i++ { - logger[i] = log.TestingLogger() - height := int64(0) - if i == 0 { - height = maxBlockHeight - } - reactorPairs[i] = newBlockchainReactorPair(t, logger[i], genDoc, privVals, height) - } - - switches := p2p.MakeConnectedSwitches(config.P2P, numNodes, func(i int, s *p2p.Switch) *p2p.Switch { - reactorPairs[i].conR.mtx.Lock() - s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) - s.AddReactor("CONSENSUS", reactorPairs[i].conR) - moduleName := fmt.Sprintf("blockchain-%v", i) - reactorPairs[i].bcR.SetLogger(logger[i].With("module", moduleName)) - reactorPairs[i].conR.mtx.Unlock() - return s - - }, p2p.Connect2Switches) - - defer func() { - for _, r := range reactorPairs { - _ = r.bcR.Stop() - _ = r.conR.Stop() - } - }() - -outerFor: - for { - time.Sleep(10 * time.Millisecond) - for i := 0; i < numNodes; i++ { - reactorPairs[i].conR.mtx.Lock() - if !reactorPairs[i].conR.switchedToConsensus { - reactorPairs[i].conR.mtx.Unlock() - continue outerFor - } - reactorPairs[i].conR.mtx.Unlock() - } - break - } - - // at this time, reactors[0-3] is the newest - assert.Equal(t, numNodes-1, reactorPairs[1].bcR.Switch.Peers().Size()) - - // mark last reactorPair as an invalid peer - reactorPairs[numNodes-1].bcR.store = otherChain.bcR.store - - lastLogger := log.TestingLogger() - lastReactorPair := newBlockchainReactorPair(t, lastLogger, genDoc, privVals, 0) - reactorPairs = append(reactorPairs, lastReactorPair) - - switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("BLOCKCHAIN", reactorPairs[len(reactorPairs)-1].bcR) - s.AddReactor("CONSENSUS", reactorPairs[len(reactorPairs)-1].conR) - moduleName := fmt.Sprintf("blockchain-%v", len(reactorPairs)-1) - reactorPairs[len(reactorPairs)-1].bcR.SetLogger(lastLogger.With("module", moduleName)) - return s - - }, p2p.Connect2Switches)...) - - for i := 0; i < len(reactorPairs)-1; i++ { - p2p.Connect2Switches(switches, i, len(reactorPairs)-1) - } - - for { - time.Sleep(1 * time.Second) - lastReactorPair.conR.mtx.Lock() - if lastReactorPair.conR.switchedToConsensus { - lastReactorPair.conR.mtx.Unlock() - break - } - lastReactorPair.conR.mtx.Unlock() - - if lastReactorPair.bcR.Switch.Peers().Size() == 0 { - break - } - } - - assert.True(t, lastReactorPair.bcR.Switch.Peers().Size() < len(reactorPairs)-1) -} - -//---------------------------------------------- -// utility funcs - -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - -type testApp struct { - abci.BaseApplication -} diff --git a/blockchain/v2/io.go b/blockchain/v2/io.go deleted file mode 100644 index 4951573ce..000000000 --- a/blockchain/v2/io.go +++ /dev/null @@ -1,140 +0,0 @@ -package v2 - -import ( - "fmt" - - bc "github.com/tendermint/tendermint/blockchain" - "github.com/tendermint/tendermint/p2p" - bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" - "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -type iIO interface { - sendBlockRequest(peerID p2p.ID, height int64) error - sendBlockToPeer(block *types.Block, peerID p2p.ID) error - sendBlockNotFound(height int64, peerID p2p.ID) error - sendStatusResponse(base, height int64, peerID p2p.ID) error - - broadcastStatusRequest() error - - trySwitchToConsensus(state state.State, skipWAL bool) bool -} - -type switchIO struct { - sw *p2p.Switch -} - -func newSwitchIo(sw *p2p.Switch) *switchIO { - return &switchIO{ - sw: sw, - } -} - -const ( - // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) - BlockchainChannel = byte(0x40) -) - -type consensusReactor interface { - // for when we switch from blockchain reactor and fast sync to - // the consensus machine - SwitchToConsensus(state state.State, skipWAL bool) -} - -func (sio *switchIO) sendBlockRequest(peerID p2p.ID, height int64) error { - peer := sio.sw.Peers().Get(peerID) - if peer == nil { - return fmt.Errorf("peer not found") - } - msgBytes, err := bc.EncodeMsg(&bcproto.BlockRequest{Height: height}) - if err != nil { - return err - } - - queued := peer.TrySend(BlockchainChannel, msgBytes) - if !queued { - return fmt.Errorf("send queue full") - } - return nil -} - -func (sio *switchIO) sendStatusResponse(base int64, height int64, peerID p2p.ID) error { - peer := sio.sw.Peers().Get(peerID) - if peer == nil { - return fmt.Errorf("peer not found") - } - - msgBytes, err := bc.EncodeMsg(&bcproto.StatusResponse{Height: height, Base: base}) - if err != nil { - return err - } - - if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { - return fmt.Errorf("peer queue full") - } - - return nil -} - -func (sio *switchIO) sendBlockToPeer(block *types.Block, peerID p2p.ID) error { - peer := sio.sw.Peers().Get(peerID) - if peer == nil { - return fmt.Errorf("peer not found") - } - if block == nil { - panic("trying to send nil block") - } - - bpb, err := block.ToProto() - if err != nil { - return err - } - - msgBytes, err := bc.EncodeMsg(&bcproto.BlockResponse{Block: bpb}) - if err != nil { - return err - } - if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { - return fmt.Errorf("peer queue full") - } - - return nil -} - -func (sio *switchIO) sendBlockNotFound(height int64, peerID p2p.ID) error { - peer := sio.sw.Peers().Get(peerID) - if peer == nil { - return fmt.Errorf("peer not found") - } - msgBytes, err := bc.EncodeMsg(&bcproto.NoBlockResponse{Height: height}) - if err != nil { - return err - } - - if queued := peer.TrySend(BlockchainChannel, msgBytes); !queued { - return fmt.Errorf("peer queue full") - } - - return nil -} - -func (sio *switchIO) trySwitchToConsensus(state state.State, skipWAL bool) bool { - conR, ok := sio.sw.Reactor("CONSENSUS").(consensusReactor) - if ok { - conR.SwitchToConsensus(state, skipWAL) - } - return ok -} - -func (sio *switchIO) broadcastStatusRequest() error { - msgBytes, err := bc.EncodeMsg(&bcproto.StatusRequest{}) - if err != nil { - return err - } - - // XXX: maybe we should use an io specific peer list here - sio.sw.Broadcast(BlockchainChannel, msgBytes) - - return nil -} diff --git a/blockchain/v2/metrics.go b/blockchain/v2/metrics.go deleted file mode 100644 index c68ec6447..000000000 --- a/blockchain/v2/metrics.go +++ /dev/null @@ -1,125 +0,0 @@ -package v2 - -import ( - "github.com/go-kit/kit/metrics" - "github.com/go-kit/kit/metrics/discard" - "github.com/go-kit/kit/metrics/prometheus" - stdprometheus "github.com/prometheus/client_golang/prometheus" -) - -const ( - // MetricsSubsystem is a subsystem shared by all metrics exposed by this - // package. - MetricsSubsystem = "blockchain" -) - -// Metrics contains metrics exposed by this package. -type Metrics struct { - // events_in - EventsIn metrics.Counter - // events_in - EventsHandled metrics.Counter - // events_out - EventsOut metrics.Counter - // errors_in - ErrorsIn metrics.Counter - // errors_handled - ErrorsHandled metrics.Counter - // errors_out - ErrorsOut metrics.Counter - // events_shed - EventsShed metrics.Counter - // events_sent - EventsSent metrics.Counter - // errors_sent - ErrorsSent metrics.Counter - // errors_shed - ErrorsShed metrics.Counter -} - -// PrometheusMetrics returns metrics for in and out events, errors, etc. handled by routines. -// Can we burn in the routine name here? -func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { - labels := []string{} - for i := 0; i < len(labelsAndValues); i += 2 { - labels = append(labels, labelsAndValues[i]) - } - return &Metrics{ - EventsIn: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "events_in", - Help: "Events read from the channel.", - }, labels).With(labelsAndValues...), - EventsHandled: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "events_handled", - Help: "Events handled", - }, labels).With(labelsAndValues...), - EventsOut: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "events_out", - Help: "Events output from routine.", - }, labels).With(labelsAndValues...), - ErrorsIn: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "errors_in", - Help: "Errors read from the channel.", - }, labels).With(labelsAndValues...), - ErrorsHandled: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "errors_handled", - Help: "Errors handled.", - }, labels).With(labelsAndValues...), - ErrorsOut: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "errors_out", - Help: "Errors output from routine.", - }, labels).With(labelsAndValues...), - ErrorsSent: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "errors_sent", - Help: "Errors sent to routine.", - }, labels).With(labelsAndValues...), - ErrorsShed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "errors_shed", - Help: "Errors dropped from sending.", - }, labels).With(labelsAndValues...), - EventsSent: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "events_sent", - Help: "Events sent to routine.", - }, labels).With(labelsAndValues...), - EventsShed: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "events_shed", - Help: "Events dropped from sending.", - }, labels).With(labelsAndValues...), - } -} - -// NopMetrics returns no-op Metrics. -func NopMetrics() *Metrics { - return &Metrics{ - EventsIn: discard.NewCounter(), - EventsHandled: discard.NewCounter(), - EventsOut: discard.NewCounter(), - ErrorsIn: discard.NewCounter(), - ErrorsHandled: discard.NewCounter(), - ErrorsOut: discard.NewCounter(), - EventsShed: discard.NewCounter(), - EventsSent: discard.NewCounter(), - ErrorsSent: discard.NewCounter(), - ErrorsShed: discard.NewCounter(), - } -} diff --git a/blockchain/v2/processor.go b/blockchain/v2/processor.go deleted file mode 100644 index f9036f3b9..000000000 --- a/blockchain/v2/processor.go +++ /dev/null @@ -1,192 +0,0 @@ -package v2 - -import ( - "fmt" - - "github.com/tendermint/tendermint/p2p" - tmState "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -// Events generated by the processor: -// block execution failure, event will indicate the peer(s) that caused the error -type pcBlockVerificationFailure struct { - priorityNormal - height int64 - firstPeerID p2p.ID - secondPeerID p2p.ID -} - -func (e pcBlockVerificationFailure) String() string { - return fmt.Sprintf("pcBlockVerificationFailure{%d 1st peer: %v, 2nd peer: %v}", - e.height, e.firstPeerID, e.secondPeerID) -} - -// successful block execution -type pcBlockProcessed struct { - priorityNormal - height int64 - peerID p2p.ID -} - -func (e pcBlockProcessed) String() string { - return fmt.Sprintf("pcBlockProcessed{%d peer: %v}", e.height, e.peerID) -} - -// processor has finished -type pcFinished struct { - priorityNormal - blocksSynced int - tmState tmState.State -} - -func (p pcFinished) Error() string { - return "finished" -} - -type queueItem struct { - block *types.Block - peerID p2p.ID -} - -type blockQueue map[int64]queueItem - -type pcState struct { - // blocks waiting to be processed - queue blockQueue - - // draining indicates that the next rProcessBlock event with a queue miss constitutes completion - draining bool - - // the number of blocks successfully synced by the processor - blocksSynced int - - // the processorContext which contains the processor dependencies - context processorContext -} - -func (state *pcState) String() string { - return fmt.Sprintf("height: %d queue length: %d draining: %v blocks synced: %d", - state.height(), len(state.queue), state.draining, state.blocksSynced) -} - -// newPcState returns a pcState initialized with the last verified block enqueued -func newPcState(context processorContext) *pcState { - return &pcState{ - queue: blockQueue{}, - draining: false, - blocksSynced: 0, - context: context, - } -} - -// nextTwo returns the next two unverified blocks -func (state *pcState) nextTwo() (queueItem, queueItem, error) { - if first, ok := state.queue[state.height()+1]; ok { - if second, ok := state.queue[state.height()+2]; ok { - return first, second, nil - } - } - return queueItem{}, queueItem{}, fmt.Errorf("not found") -} - -// synced returns true when at most the last verified block remains in the queue -func (state *pcState) synced() bool { - return len(state.queue) <= 1 -} - -func (state *pcState) enqueue(peerID p2p.ID, block *types.Block, height int64) { - if item, ok := state.queue[height]; ok { - panic(fmt.Sprintf( - "duplicate block %d (%X) enqueued by processor (sent by %v; existing block %X from %v)", - height, block.Hash(), peerID, item.block.Hash(), item.peerID)) - } - - state.queue[height] = queueItem{block: block, peerID: peerID} -} - -func (state *pcState) height() int64 { - return state.context.tmState().LastBlockHeight -} - -// purgePeer moves all unprocessed blocks from the queue -func (state *pcState) purgePeer(peerID p2p.ID) { - // what if height is less than state.height? - for height, item := range state.queue { - if item.peerID == peerID { - delete(state.queue, height) - } - } -} - -// handle processes FSM events -func (state *pcState) handle(event Event) (Event, error) { - switch event := event.(type) { - case bcResetState: - state.context.setState(event.state) - return noOp, nil - - case scFinishedEv: - if state.synced() { - return pcFinished{tmState: state.context.tmState(), blocksSynced: state.blocksSynced}, nil - } - state.draining = true - return noOp, nil - - case scPeerError: - state.purgePeer(event.peerID) - return noOp, nil - - case scBlockReceived: - if event.block == nil { - return noOp, nil - } - - // enqueue block if height is higher than state height, else ignore it - if event.block.Height > state.height() { - state.enqueue(event.peerID, event.block, event.block.Height) - } - return noOp, nil - - case rProcessBlock: - tmState := state.context.tmState() - firstItem, secondItem, err := state.nextTwo() - if err != nil { - if state.draining { - return pcFinished{tmState: tmState, blocksSynced: state.blocksSynced}, nil - } - return noOp, nil - } - - var ( - first, second = firstItem.block, secondItem.block - firstParts = first.MakePartSet(types.BlockPartSizeBytes) - firstID = types.BlockID{Hash: first.Hash(), PartSetHeader: firstParts.Header()} - ) - - // verify if +second+ last commit "confirms" +first+ block - err = state.context.verifyCommit(tmState.ChainID, firstID, first.Height, second.LastCommit) - if err != nil { - state.purgePeer(firstItem.peerID) - if firstItem.peerID != secondItem.peerID { - state.purgePeer(secondItem.peerID) - } - return pcBlockVerificationFailure{ - height: first.Height, firstPeerID: firstItem.peerID, secondPeerID: secondItem.peerID}, - nil - } - - state.context.saveBlock(first, firstParts, second.LastCommit) - - if err := state.context.applyBlock(firstID, first); err != nil { - panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) - } - - delete(state.queue, first.Height) - state.blocksSynced++ - - return pcBlockProcessed{height: first.Height, peerID: firstItem.peerID}, nil - } - - return noOp, nil -} diff --git a/blockchain/v2/processor_context.go b/blockchain/v2/processor_context.go deleted file mode 100644 index 6a0466550..000000000 --- a/blockchain/v2/processor_context.go +++ /dev/null @@ -1,100 +0,0 @@ -package v2 - -import ( - "fmt" - - "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -type processorContext interface { - applyBlock(blockID types.BlockID, block *types.Block) error - verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error - saveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) - tmState() state.State - setState(state.State) -} - -type pContext struct { - store blockStore - applier blockApplier - state state.State -} - -func newProcessorContext(st blockStore, ex blockApplier, s state.State) *pContext { - return &pContext{ - store: st, - applier: ex, - state: s, - } -} - -func (pc *pContext) applyBlock(blockID types.BlockID, block *types.Block) error { - newState, _, err := pc.applier.ApplyBlock(pc.state, blockID, block) - pc.state = newState - return err -} - -func (pc pContext) tmState() state.State { - return pc.state -} - -func (pc *pContext) setState(state state.State) { - pc.state = state -} - -func (pc pContext) verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error { - return pc.state.Validators.VerifyCommitLight(chainID, blockID, height, commit) -} - -func (pc *pContext) saveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { - pc.store.SaveBlock(block, blockParts, seenCommit) -} - -type mockPContext struct { - applicationBL []int64 - verificationBL []int64 - state state.State -} - -func newMockProcessorContext( - state state.State, - verificationBlackList []int64, - applicationBlackList []int64) *mockPContext { - return &mockPContext{ - applicationBL: applicationBlackList, - verificationBL: verificationBlackList, - state: state, - } -} - -func (mpc *mockPContext) applyBlock(blockID types.BlockID, block *types.Block) error { - for _, h := range mpc.applicationBL { - if h == block.Height { - return fmt.Errorf("generic application error") - } - } - mpc.state.LastBlockHeight = block.Height - return nil -} - -func (mpc *mockPContext) verifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error { - for _, h := range mpc.verificationBL { - if h == height { - return fmt.Errorf("generic verification error") - } - } - return nil -} - -func (mpc *mockPContext) saveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { - -} - -func (mpc *mockPContext) setState(state state.State) { - mpc.state = state -} - -func (mpc *mockPContext) tmState() state.State { - return mpc.state -} diff --git a/blockchain/v2/processor_test.go b/blockchain/v2/processor_test.go deleted file mode 100644 index 04ce05b6e..000000000 --- a/blockchain/v2/processor_test.go +++ /dev/null @@ -1,306 +0,0 @@ -package v2 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/tendermint/tendermint/p2p" - tmState "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -// pcBlock is a test helper structure with simple types. Its purpose is to help with test readability. -type pcBlock struct { - pid string - height int64 -} - -// params is a test structure used to create processor state. -type params struct { - height int64 - items []pcBlock - blocksSynced int - verBL []int64 - appBL []int64 - draining bool -} - -// makePcBlock makes an empty block. -func makePcBlock(height int64) *types.Block { - return &types.Block{Header: types.Header{Height: height}} -} - -// makeState takes test parameters and creates a specific processor state. -func makeState(p *params) *pcState { - var ( - tmState = tmState.State{LastBlockHeight: p.height} - context = newMockProcessorContext(tmState, p.verBL, p.appBL) - ) - state := newPcState(context) - - for _, item := range p.items { - state.enqueue(p2p.ID(item.pid), makePcBlock(item.height), item.height) - } - - state.blocksSynced = p.blocksSynced - state.draining = p.draining - return state -} - -func mBlockResponse(peerID p2p.ID, height int64) scBlockReceived { - return scBlockReceived{ - peerID: peerID, - block: makePcBlock(height), - } -} - -type pcFsmMakeStateValues struct { - currentState *params - event Event - wantState *params - wantNextEvent Event - wantErr error - wantPanic bool -} - -type testFields struct { - name string - steps []pcFsmMakeStateValues -} - -func executeProcessorTests(t *testing.T, tests []testFields) { - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - var state *pcState - for _, step := range tt.steps { - defer func() { - r := recover() - if (r != nil) != step.wantPanic { - t.Errorf("recover = %v, wantPanic = %v", r, step.wantPanic) - } - }() - - // First step must always initialize the currentState as state. - if step.currentState != nil { - state = makeState(step.currentState) - } - if state == nil { - panic("Bad (initial?) step") - } - - nextEvent, err := state.handle(step.event) - t.Log(state) - assert.Equal(t, step.wantErr, err) - assert.Equal(t, makeState(step.wantState), state) - assert.Equal(t, step.wantNextEvent, nextEvent) - // Next step may use the wantedState as their currentState. - state = makeState(step.wantState) - } - }) - } -} - -func TestRProcessPeerError(t *testing.T) { - tests := []testFields{ - { - name: "error for existing peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, - event: scPeerError{peerID: "P2"}, - wantState: ¶ms{items: []pcBlock{{"P1", 1}}}, - wantNextEvent: noOp, - }, - }, - }, - { - name: "error for unknown peer", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, - event: scPeerError{peerID: "P3"}, - wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, - wantNextEvent: noOp, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} - -func TestPcBlockResponse(t *testing.T) { - tests := []testFields{ - { - name: "add one block", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{}, event: mBlockResponse("P1", 1), - wantState: ¶ms{items: []pcBlock{{"P1", 1}}}, wantNextEvent: noOp, - }, - }, - }, - - { - name: "add two blocks", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{}, event: mBlockResponse("P1", 3), - wantState: ¶ms{items: []pcBlock{{"P1", 3}}}, wantNextEvent: noOp, - }, - { // use previous wantState as currentState, - event: mBlockResponse("P1", 4), - wantState: ¶ms{items: []pcBlock{{"P1", 3}, {"P1", 4}}}, wantNextEvent: noOp, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} - -func TestRProcessBlockSuccess(t *testing.T) { - tests := []testFields{ - { - name: "noop - no blocks over current height", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{}, event: rProcessBlock{}, - wantState: ¶ms{}, wantNextEvent: noOp, - }, - }, - }, - { - name: "noop - high new blocks", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, event: rProcessBlock{}, - wantState: ¶ms{height: 5, items: []pcBlock{{"P1", 30}, {"P2", 31}}}, wantNextEvent: noOp, - }, - }, - }, - { - name: "blocks H+1 and H+2 present", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}}, event: rProcessBlock{}, - wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}}, blocksSynced: 1}, - wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, - }, - }, - }, - { - name: "blocks H+1 and H+2 present after draining", - steps: []pcFsmMakeStateValues{ - { // some contiguous blocks - on stop check draining is set - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}}, - event: scFinishedEv{}, - wantState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P1", 4}}, draining: true}, - wantNextEvent: noOp, - }, - { - event: rProcessBlock{}, - wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, - wantNextEvent: pcBlockProcessed{height: 1, peerID: "P1"}, - }, - { // finish when H+1 or/and H+2 are missing - event: rProcessBlock{}, - wantState: ¶ms{height: 1, items: []pcBlock{{"P2", 2}, {"P1", 4}}, blocksSynced: 1, draining: true}, - wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 1}, blocksSynced: 1}, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} - -func TestRProcessBlockFailures(t *testing.T) { - tests := []testFields{ - { - name: "blocks H+1 and H+2 present from different peers - H+1 verification fails ", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, verBL: []int64{1}}, event: rProcessBlock{}, - wantState: ¶ms{items: []pcBlock{}, verBL: []int64{1}}, - wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P2"}, - }, - }, - }, - { - name: "blocks H+1 and H+2 present from same peer - H+1 applyBlock fails ", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}}, appBL: []int64{1}}, event: rProcessBlock{}, - wantState: ¶ms{items: []pcBlock{}, appBL: []int64{1}}, wantPanic: true, - }, - }, - }, - { - name: "blocks H+1 and H+2 present from same peers - H+1 verification fails ", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 0, items: []pcBlock{{"P1", 1}, {"P1", 2}, {"P2", 3}}, - verBL: []int64{1}}, event: rProcessBlock{}, - wantState: ¶ms{height: 0, items: []pcBlock{{"P2", 3}}, verBL: []int64{1}}, - wantNextEvent: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}, - }, - }, - }, - { - name: "blocks H+1 and H+2 present from different peers - H+1 applyBlock fails ", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{items: []pcBlock{{"P1", 1}, {"P2", 2}, {"P2", 3}}, appBL: []int64{1}}, - event: rProcessBlock{}, - wantState: ¶ms{items: []pcBlock{{"P2", 3}}, appBL: []int64{1}}, wantPanic: true, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} - -func TestScFinishedEv(t *testing.T) { - tests := []testFields{ - { - name: "no blocks", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, event: scFinishedEv{}, - wantState: ¶ms{height: 100, items: []pcBlock{}, blocksSynced: 100}, - wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 100}, blocksSynced: 100}, - }, - }, - }, - { - name: "maxHeight+1 block present", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 100, items: []pcBlock{ - {"P1", 101}}, blocksSynced: 100}, event: scFinishedEv{}, - wantState: ¶ms{height: 100, items: []pcBlock{{"P1", 101}}, blocksSynced: 100}, - wantNextEvent: pcFinished{tmState: tmState.State{LastBlockHeight: 100}, blocksSynced: 100}, - }, - }, - }, - { - name: "more blocks present", - steps: []pcFsmMakeStateValues{ - { - currentState: ¶ms{height: 100, items: []pcBlock{ - {"P1", 101}, {"P1", 102}}, blocksSynced: 100}, event: scFinishedEv{}, - wantState: ¶ms{height: 100, items: []pcBlock{ - {"P1", 101}, {"P1", 102}}, blocksSynced: 100, draining: true}, - wantNextEvent: noOp, - wantErr: nil, - }, - }, - }, - } - - executeProcessorTests(t, tests) -} diff --git a/blockchain/v2/reactor.go b/blockchain/v2/reactor.go deleted file mode 100644 index e091182eb..000000000 --- a/blockchain/v2/reactor.go +++ /dev/null @@ -1,564 +0,0 @@ -package v2 - -import ( - "errors" - "fmt" - "time" - - "github.com/tendermint/tendermint/behavior" - bc "github.com/tendermint/tendermint/blockchain" - "github.com/tendermint/tendermint/libs/log" - tmsync "github.com/tendermint/tendermint/libs/sync" - "github.com/tendermint/tendermint/p2p" - bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" - "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -const ( - // chBufferSize is the buffer size of all event channels. - chBufferSize int = 1000 -) - -type blockStore interface { - LoadBlock(height int64) *types.Block - SaveBlock(*types.Block, *types.PartSet, *types.Commit) - Base() int64 - Height() int64 -} - -// BlockchainReactor handles fast sync protocol. -type BlockchainReactor struct { - p2p.BaseReactor - - fastSync bool // if true, enable fast sync on start - stateSynced bool // set to true when SwitchToFastSync is called by state sync - scheduler *Routine - processor *Routine - logger log.Logger - - mtx tmsync.RWMutex - maxPeerHeight int64 - syncHeight int64 - events chan Event // non-nil during a fast sync - - reporter behavior.Reporter - io iIO - store blockStore -} - -//nolint:unused,deadcode -type blockVerifier interface { - VerifyCommit(chainID string, blockID types.BlockID, height int64, commit *types.Commit) error -} - -type blockApplier interface { - ApplyBlock(state state.State, blockID types.BlockID, block *types.Block) (state.State, int64, error) -} - -// XXX: unify naming in this package around tmState -func newReactor(state state.State, store blockStore, reporter behavior.Reporter, - blockApplier blockApplier, fastSync bool) *BlockchainReactor { - initHeight := state.LastBlockHeight + 1 - if initHeight == 1 { - initHeight = state.InitialHeight - } - scheduler := newScheduler(initHeight, time.Now()) - pContext := newProcessorContext(store, blockApplier, state) - // TODO: Fix naming to just newProcesssor - // newPcState requires a processorContext - processor := newPcState(pContext) - - return &BlockchainReactor{ - scheduler: newRoutine("scheduler", scheduler.handle, chBufferSize), - processor: newRoutine("processor", processor.handle, chBufferSize), - store: store, - reporter: reporter, - logger: log.NewNopLogger(), - fastSync: fastSync, - } -} - -// NewBlockchainReactor creates a new reactor instance. -func NewBlockchainReactor( - state state.State, - blockApplier blockApplier, - store blockStore, - fastSync bool) *BlockchainReactor { - reporter := behavior.NewMockReporter() - return newReactor(state, store, reporter, blockApplier, fastSync) -} - -// SetSwitch implements Reactor interface. -func (r *BlockchainReactor) SetSwitch(sw *p2p.Switch) { - r.Switch = sw - if sw != nil { - r.io = newSwitchIo(sw) - } else { - r.io = nil - } -} - -func (r *BlockchainReactor) setMaxPeerHeight(height int64) { - r.mtx.Lock() - defer r.mtx.Unlock() - if height > r.maxPeerHeight { - r.maxPeerHeight = height - } -} - -func (r *BlockchainReactor) setSyncHeight(height int64) { - r.mtx.Lock() - defer r.mtx.Unlock() - r.syncHeight = height -} - -// SyncHeight returns the height to which the BlockchainReactor has synced. -func (r *BlockchainReactor) SyncHeight() int64 { - r.mtx.RLock() - defer r.mtx.RUnlock() - return r.syncHeight -} - -// SetLogger sets the logger of the reactor. -func (r *BlockchainReactor) SetLogger(logger log.Logger) { - r.logger = logger - r.scheduler.setLogger(logger) - r.processor.setLogger(logger) -} - -// Start implements cmn.Service interface -func (r *BlockchainReactor) Start() error { - r.reporter = behavior.NewSwitchReporter(r.BaseReactor.Switch) - if r.fastSync { - err := r.startSync(nil) - if err != nil { - return fmt.Errorf("failed to start fast sync: %w", err) - } - } - return nil -} - -// startSync begins a fast sync, signaled by r.events being non-nil. If state is non-nil, -// the scheduler and processor is updated with this state on startup. -func (r *BlockchainReactor) startSync(state *state.State) error { - r.mtx.Lock() - defer r.mtx.Unlock() - if r.events != nil { - return errors.New("fast sync already in progress") - } - r.events = make(chan Event, chBufferSize) - go r.scheduler.start() - go r.processor.start() - if state != nil { - <-r.scheduler.ready() - <-r.processor.ready() - r.scheduler.send(bcResetState{state: *state}) - r.processor.send(bcResetState{state: *state}) - } - go r.demux(r.events) - return nil -} - -// endSync ends a fast sync -func (r *BlockchainReactor) endSync() { - r.mtx.Lock() - defer r.mtx.Unlock() - if r.events != nil { - close(r.events) - } - r.events = nil - r.scheduler.stop() - r.processor.stop() -} - -// SwitchToFastSync is called by the state sync reactor when switching to fast sync. -func (r *BlockchainReactor) SwitchToFastSync(state state.State) error { - r.stateSynced = true - state = state.Copy() - return r.startSync(&state) -} - -// reactor generated ticker events: -// ticker for cleaning peers -type rTryPrunePeer struct { - priorityHigh - time time.Time -} - -func (e rTryPrunePeer) String() string { - return fmt.Sprintf("rTryPrunePeer{%v}", e.time) -} - -// ticker event for scheduling block requests -type rTrySchedule struct { - priorityHigh - time time.Time -} - -func (e rTrySchedule) String() string { - return fmt.Sprintf("rTrySchedule{%v}", e.time) -} - -// ticker for block processing -type rProcessBlock struct { - priorityNormal -} - -func (e rProcessBlock) String() string { - return "rProcessBlock" -} - -// reactor generated events based on blockchain related messages from peers: -// blockResponse message received from a peer -type bcBlockResponse struct { - priorityNormal - time time.Time - peerID p2p.ID - size int64 - block *types.Block -} - -func (resp bcBlockResponse) String() string { - return fmt.Sprintf("bcBlockResponse{%d#%X (size: %d bytes) from %v at %v}", - resp.block.Height, resp.block.Hash(), resp.size, resp.peerID, resp.time) -} - -// blockNoResponse message received from a peer -type bcNoBlockResponse struct { - priorityNormal - time time.Time - peerID p2p.ID - height int64 -} - -func (resp bcNoBlockResponse) String() string { - return fmt.Sprintf("bcNoBlockResponse{%v has no block at height %d at %v}", - resp.peerID, resp.height, resp.time) -} - -// statusResponse message received from a peer -type bcStatusResponse struct { - priorityNormal - time time.Time - peerID p2p.ID - base int64 - height int64 -} - -func (resp bcStatusResponse) String() string { - return fmt.Sprintf("bcStatusResponse{%v is at height %d (base: %d) at %v}", - resp.peerID, resp.height, resp.base, resp.time) -} - -// new peer is connected -type bcAddNewPeer struct { - priorityNormal - peerID p2p.ID -} - -func (resp bcAddNewPeer) String() string { - return fmt.Sprintf("bcAddNewPeer{%v}", resp.peerID) -} - -// existing peer is removed -type bcRemovePeer struct { - priorityHigh - peerID p2p.ID - reason interface{} -} - -func (resp bcRemovePeer) String() string { - return fmt.Sprintf("bcRemovePeer{%v due to %v}", resp.peerID, resp.reason) -} - -// resets the scheduler and processor state, e.g. following a switch from state syncing -type bcResetState struct { - priorityHigh - state state.State -} - -func (e bcResetState) String() string { - return fmt.Sprintf("bcResetState{%v}", e.state) -} - -// Takes the channel as a parameter to avoid race conditions on r.events. -func (r *BlockchainReactor) demux(events <-chan Event) { - var lastRate = 0.0 - var lastHundred = time.Now() - - var ( - processBlockFreq = 20 * time.Millisecond - doProcessBlockCh = make(chan struct{}, 1) - doProcessBlockTk = time.NewTicker(processBlockFreq) - ) - defer doProcessBlockTk.Stop() - - var ( - prunePeerFreq = 1 * time.Second - doPrunePeerCh = make(chan struct{}, 1) - doPrunePeerTk = time.NewTicker(prunePeerFreq) - ) - defer doPrunePeerTk.Stop() - - var ( - scheduleFreq = 20 * time.Millisecond - doScheduleCh = make(chan struct{}, 1) - doScheduleTk = time.NewTicker(scheduleFreq) - ) - defer doScheduleTk.Stop() - - var ( - statusFreq = 10 * time.Second - doStatusCh = make(chan struct{}, 1) - doStatusTk = time.NewTicker(statusFreq) - ) - defer doStatusTk.Stop() - doStatusCh <- struct{}{} // immediately broadcast to get status of existing peers - - // XXX: Extract timers to make testing atemporal - for { - select { - // Pacers: send at most per frequency but don't saturate - case <-doProcessBlockTk.C: - select { - case doProcessBlockCh <- struct{}{}: - default: - } - case <-doPrunePeerTk.C: - select { - case doPrunePeerCh <- struct{}{}: - default: - } - case <-doScheduleTk.C: - select { - case doScheduleCh <- struct{}{}: - default: - } - case <-doStatusTk.C: - select { - case doStatusCh <- struct{}{}: - default: - } - - // Tickers: perform tasks periodically - case <-doScheduleCh: - r.scheduler.send(rTrySchedule{time: time.Now()}) - case <-doPrunePeerCh: - r.scheduler.send(rTryPrunePeer{time: time.Now()}) - case <-doProcessBlockCh: - r.processor.send(rProcessBlock{}) - case <-doStatusCh: - if err := r.io.broadcastStatusRequest(); err != nil { - r.logger.Error("Error broadcasting status request", "err", err) - } - - // Events from peers. Closing the channel signals event loop termination. - case event, ok := <-events: - if !ok { - r.logger.Info("Stopping event processing") - return - } - switch event := event.(type) { - case bcStatusResponse: - r.setMaxPeerHeight(event.height) - r.scheduler.send(event) - case bcAddNewPeer, bcRemovePeer, bcBlockResponse, bcNoBlockResponse: - r.scheduler.send(event) - default: - r.logger.Error("Received unexpected event", "event", fmt.Sprintf("%T", event)) - } - - // Incremental events from scheduler - case event := <-r.scheduler.next(): - switch event := event.(type) { - case scBlockReceived: - r.processor.send(event) - case scPeerError: - r.processor.send(event) - if err := r.reporter.Report(behavior.BadMessage(event.peerID, "scPeerError")); err != nil { - r.logger.Error("Error reporting peer", "err", err) - } - case scBlockRequest: - if err := r.io.sendBlockRequest(event.peerID, event.height); err != nil { - r.logger.Error("Error sending block request", "err", err) - } - case scFinishedEv: - r.processor.send(event) - r.scheduler.stop() - case scSchedulerFail: - r.logger.Error("Scheduler failure", "err", event.reason.Error()) - case scPeersPruned: - // Remove peers from the processor. - for _, peerID := range event.peers { - r.processor.send(scPeerError{peerID: peerID, reason: errors.New("peer was pruned")}) - } - r.logger.Debug("Pruned peers", "count", len(event.peers)) - case noOpEvent: - default: - r.logger.Error("Received unexpected scheduler event", "event", fmt.Sprintf("%T", event)) - } - - // Incremental events from processor - case event := <-r.processor.next(): - switch event := event.(type) { - case pcBlockProcessed: - r.setSyncHeight(event.height) - if r.syncHeight%100 == 0 { - lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) - r.logger.Info("Fast Sync Rate", "height", r.syncHeight, - "max_peer_height", r.maxPeerHeight, "blocks/s", lastRate) - lastHundred = time.Now() - } - r.scheduler.send(event) - case pcBlockVerificationFailure: - r.scheduler.send(event) - case pcFinished: - r.logger.Info("Fast sync complete, switching to consensus") - if !r.io.trySwitchToConsensus(event.tmState, event.blocksSynced > 0 || r.stateSynced) { - r.logger.Error("Failed to switch to consensus reactor") - } - r.endSync() - return - case noOpEvent: - default: - r.logger.Error("Received unexpected processor event", "event", fmt.Sprintf("%T", event)) - } - - // Terminal event from scheduler - case err := <-r.scheduler.final(): - switch err { - case nil: - r.logger.Info("Scheduler stopped") - default: - r.logger.Error("Scheduler aborted with error", "err", err) - } - - // Terminal event from processor - case err := <-r.processor.final(): - switch err { - case nil: - r.logger.Info("Processor stopped") - default: - r.logger.Error("Processor aborted with error", "err", err) - } - } - } -} - -// Stop implements cmn.Service interface. -func (r *BlockchainReactor) Stop() error { - r.logger.Info("reactor stopping") - r.endSync() - r.logger.Info("reactor stopped") - return nil -} - -// Receive implements Reactor by handling different message types. -func (r *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := bc.DecodeMsg(msgBytes) - if err != nil { - r.logger.Error("error decoding message", - "src", src.ID(), "chId", chID, "msg", msg, "err", err) - _ = r.reporter.Report(behavior.BadMessage(src.ID(), err.Error())) - return - } - - if err = bc.ValidateMsg(msg); err != nil { - r.logger.Error("peer sent us invalid msg", "peer", src, "msg", msg, "err", err) - _ = r.reporter.Report(behavior.BadMessage(src.ID(), err.Error())) - return - } - - r.logger.Debug("Receive", "src", src.ID(), "chID", chID, "msg", msg) - - switch msg := msg.(type) { - case *bcproto.StatusRequest: - if err := r.io.sendStatusResponse(r.store.Base(), r.store.Height(), src.ID()); err != nil { - r.logger.Error("Could not send status message to peer", "src", src) - } - - case *bcproto.BlockRequest: - block := r.store.LoadBlock(msg.Height) - if block != nil { - if err = r.io.sendBlockToPeer(block, src.ID()); err != nil { - r.logger.Error("Could not send block message to peer: ", err) - } - } else { - r.logger.Info("peer asking for a block we don't have", "src", src, "height", msg.Height) - peerID := src.ID() - if err = r.io.sendBlockNotFound(msg.Height, peerID); err != nil { - r.logger.Error("Couldn't send block not found: ", err) - } - } - - case *bcproto.StatusResponse: - r.mtx.RLock() - if r.events != nil { - r.events <- bcStatusResponse{peerID: src.ID(), base: msg.Base, height: msg.Height} - } - r.mtx.RUnlock() - - case *bcproto.BlockResponse: - bi, err := types.BlockFromProto(msg.Block) - if err != nil { - r.logger.Error("error transitioning block from protobuf", "err", err) - return - } - r.mtx.RLock() - if r.events != nil { - r.events <- bcBlockResponse{ - peerID: src.ID(), - block: bi, - size: int64(len(msgBytes)), - time: time.Now(), - } - } - r.mtx.RUnlock() - - case *bcproto.NoBlockResponse: - r.mtx.RLock() - if r.events != nil { - r.events <- bcNoBlockResponse{peerID: src.ID(), height: msg.Height, time: time.Now()} - } - r.mtx.RUnlock() - } -} - -// AddPeer implements Reactor interface -func (r *BlockchainReactor) AddPeer(peer p2p.Peer) { - err := r.io.sendStatusResponse(r.store.Base(), r.store.Height(), peer.ID()) - if err != nil { - r.logger.Error("Could not send status message to peer new", "src", peer.ID, "height", r.SyncHeight()) - } - r.mtx.RLock() - defer r.mtx.RUnlock() - if r.events != nil { - r.events <- bcAddNewPeer{peerID: peer.ID()} - } -} - -// RemovePeer implements Reactor interface. -func (r *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { - r.mtx.RLock() - defer r.mtx.RUnlock() - if r.events != nil { - r.events <- bcRemovePeer{ - peerID: peer.ID(), - reason: reason, - } - } -} - -// GetChannels implements Reactor -func (r *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { - return []*p2p.ChannelDescriptor{ - { - ID: BlockchainChannel, - Priority: 5, - SendQueueCapacity: 2000, - RecvBufferCapacity: 50 * 4096, - RecvMessageCapacity: bc.MaxMsgSize, - }, - } -} diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go deleted file mode 100644 index ba59b31c1..000000000 --- a/blockchain/v2/reactor_test.go +++ /dev/null @@ -1,555 +0,0 @@ -package v2 - -import ( - "fmt" - "net" - "os" - "sort" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - dbm "github.com/tendermint/tm-db" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/behavior" - bc "github.com/tendermint/tendermint/blockchain" - cfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/libs/service" - "github.com/tendermint/tendermint/mempool/mock" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/p2p/conn" - bcproto "github.com/tendermint/tendermint/proto/tendermint/blockchain" - "github.com/tendermint/tendermint/proxy" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/store" - "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" -) - -type mockPeer struct { - service.Service - id p2p.ID -} - -func (mp mockPeer) FlushStop() {} -func (mp mockPeer) ID() p2p.ID { return mp.id } -func (mp mockPeer) RemoteIP() net.IP { return net.IP{} } -func (mp mockPeer) RemoteAddr() net.Addr { return &net.TCPAddr{IP: mp.RemoteIP(), Port: 8800} } - -func (mp mockPeer) IsOutbound() bool { return true } -func (mp mockPeer) IsPersistent() bool { return true } -func (mp mockPeer) CloseConn() error { return nil } - -func (mp mockPeer) NodeInfo() p2p.NodeInfo { - return p2p.DefaultNodeInfo{ - DefaultNodeID: "", - ListenAddr: "", - } -} -func (mp mockPeer) Status() conn.ConnectionStatus { return conn.ConnectionStatus{} } -func (mp mockPeer) SocketAddr() *p2p.NetAddress { return &p2p.NetAddress{} } - -func (mp mockPeer) Send(byte, []byte) bool { return true } -func (mp mockPeer) TrySend(byte, []byte) bool { return true } - -func (mp mockPeer) Set(string, interface{}) {} -func (mp mockPeer) Get(string) interface{} { return struct{}{} } - -// nolint:unused // ignore -type mockBlockStore struct { - blocks map[int64]*types.Block -} - -// nolint:unused // ignore -func (ml *mockBlockStore) Height() int64 { - return int64(len(ml.blocks)) -} - -// nolint:unused // ignore -func (ml *mockBlockStore) LoadBlock(height int64) *types.Block { - return ml.blocks[height] -} - -// nolint:unused // ignore -func (ml *mockBlockStore) SaveBlock(block *types.Block, part *types.PartSet, commit *types.Commit) { - ml.blocks[block.Height] = block -} - -type mockBlockApplier struct { -} - -// XXX: Add whitelist/blacklist? -func (mba *mockBlockApplier) ApplyBlock( - state sm.State, blockID types.BlockID, block *types.Block, -) (sm.State, int64, error) { - state.LastBlockHeight++ - return state, 0, nil -} - -type mockSwitchIo struct { - mtx sync.Mutex - switchedToConsensus bool - numStatusResponse int - numBlockResponse int - numNoBlockResponse int -} - -func (sio *mockSwitchIo) sendBlockRequest(peerID p2p.ID, height int64) error { - return nil -} - -func (sio *mockSwitchIo) sendStatusResponse(base, height int64, peerID p2p.ID) error { - sio.mtx.Lock() - defer sio.mtx.Unlock() - sio.numStatusResponse++ - return nil -} - -func (sio *mockSwitchIo) sendBlockToPeer(block *types.Block, peerID p2p.ID) error { - sio.mtx.Lock() - defer sio.mtx.Unlock() - sio.numBlockResponse++ - return nil -} - -func (sio *mockSwitchIo) sendBlockNotFound(height int64, peerID p2p.ID) error { - sio.mtx.Lock() - defer sio.mtx.Unlock() - sio.numNoBlockResponse++ - return nil -} - -func (sio *mockSwitchIo) trySwitchToConsensus(state sm.State, skipWAL bool) bool { - sio.mtx.Lock() - defer sio.mtx.Unlock() - sio.switchedToConsensus = true - return true -} - -func (sio *mockSwitchIo) broadcastStatusRequest() error { - return nil -} - -type testReactorParams struct { - logger log.Logger - genDoc *types.GenesisDoc - privVals []types.PrivValidator - startHeight int64 - mockA bool -} - -func newTestReactor(p testReactorParams) *BlockchainReactor { - store, state, _ := newReactorStore(p.genDoc, p.privVals, p.startHeight) - reporter := behavior.NewMockReporter() - - var appl blockApplier - - if p.mockA { - appl = &mockBlockApplier{} - } else { - app := &testApp{} - cc := proxy.NewLocalClientCreator(app) - proxyApp := proxy.NewAppConns(cc) - err := proxyApp.Start() - if err != nil { - panic(fmt.Errorf("error start app: %w", err)) - } - db := dbm.NewMemDB() - stateStore := sm.NewStore(db) - appl = sm.NewBlockExecutor(stateStore, p.logger, proxyApp.Consensus(), mock.Mempool{}, sm.EmptyEvidencePool{}) - if err = stateStore.Save(state); err != nil { - panic(err) - } - } - - r := newReactor(state, store, reporter, appl, true) - logger := log.TestingLogger() - r.SetLogger(logger.With("module", "blockchain")) - - return r -} - -// This test is left here and not deleted to retain the termination cases for -// future improvement in [#4482](https://github.com/tendermint/tendermint/issues/4482). -// func TestReactorTerminationScenarios(t *testing.T) { - -// config := cfg.ResetTestRoot("blockchain_reactor_v2_test") -// defer os.RemoveAll(config.RootDir) -// genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) -// refStore, _, _ := newReactorStore(genDoc, privVals, 20) - -// params := testReactorParams{ -// logger: log.TestingLogger(), -// genDoc: genDoc, -// privVals: privVals, -// startHeight: 10, -// bufferSize: 100, -// mockA: true, -// } - -// type testEvent struct { -// evType string -// peer string -// height int64 -// } - -// tests := []struct { -// name string -// params testReactorParams -// msgs []testEvent -// }{ -// { -// name: "simple termination on max peer height - one peer", -// params: params, -// msgs: []testEvent{ -// {evType: "AddPeer", peer: "P1"}, -// {evType: "ReceiveS", peer: "P1", height: 13}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P1", height: 11}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P1", height: 12}, -// {evType: "Process"}, -// {evType: "ReceiveB", peer: "P1", height: 13}, -// {evType: "Process"}, -// }, -// }, -// { -// name: "simple termination on max peer height - two peers", -// params: params, -// msgs: []testEvent{ -// {evType: "AddPeer", peer: "P1"}, -// {evType: "AddPeer", peer: "P2"}, -// {evType: "ReceiveS", peer: "P1", height: 13}, -// {evType: "ReceiveS", peer: "P2", height: 15}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P1", height: 11}, -// {evType: "ReceiveB", peer: "P2", height: 12}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P1", height: 13}, -// {evType: "Process"}, -// {evType: "ReceiveB", peer: "P2", height: 14}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 15}, -// {evType: "Process"}, -// }, -// }, -// { -// name: "termination on max peer height - two peers, noBlock error", -// params: params, -// msgs: []testEvent{ -// {evType: "AddPeer", peer: "P1"}, -// {evType: "AddPeer", peer: "P2"}, -// {evType: "ReceiveS", peer: "P1", height: 13}, -// {evType: "ReceiveS", peer: "P2", height: 15}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveNB", peer: "P1", height: 11}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 12}, -// {evType: "ReceiveB", peer: "P2", height: 11}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 13}, -// {evType: "Process"}, -// {evType: "ReceiveB", peer: "P2", height: 14}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 15}, -// {evType: "Process"}, -// }, -// }, -// { -// name: "termination on max peer height - two peers, remove one peer", -// params: params, -// msgs: []testEvent{ -// {evType: "AddPeer", peer: "P1"}, -// {evType: "AddPeer", peer: "P2"}, -// {evType: "ReceiveS", peer: "P1", height: 13}, -// {evType: "ReceiveS", peer: "P2", height: 15}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "RemovePeer", peer: "P1"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 12}, -// {evType: "ReceiveB", peer: "P2", height: 11}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 13}, -// {evType: "Process"}, -// {evType: "ReceiveB", peer: "P2", height: 14}, -// {evType: "Process"}, -// {evType: "BlockReq"}, -// {evType: "ReceiveB", peer: "P2", height: 15}, -// {evType: "Process"}, -// }, -// }, -// } - -// for _, tt := range tests { -// tt := tt -// t.Run(tt.name, func(t *testing.T) { -// reactor := newTestReactor(params) -// reactor.Start() -// reactor.reporter = behavior.NewMockReporter() -// mockSwitch := &mockSwitchIo{switchedToConsensus: false} -// reactor.io = mockSwitch -// // time for go routines to start -// time.Sleep(time.Millisecond) - -// for _, step := range tt.msgs { -// switch step.evType { -// case "AddPeer": -// reactor.scheduler.send(bcAddNewPeer{peerID: p2p.ID(step.peer)}) -// case "RemovePeer": -// reactor.scheduler.send(bcRemovePeer{peerID: p2p.ID(step.peer)}) -// case "ReceiveS": -// reactor.scheduler.send(bcStatusResponse{ -// peerID: p2p.ID(step.peer), -// height: step.height, -// time: time.Now(), -// }) -// case "ReceiveB": -// reactor.scheduler.send(bcBlockResponse{ -// peerID: p2p.ID(step.peer), -// block: refStore.LoadBlock(step.height), -// size: 10, -// time: time.Now(), -// }) -// case "ReceiveNB": -// reactor.scheduler.send(bcNoBlockResponse{ -// peerID: p2p.ID(step.peer), -// height: step.height, -// time: time.Now(), -// }) -// case "BlockReq": -// reactor.scheduler.send(rTrySchedule{time: time.Now()}) -// case "Process": -// reactor.processor.send(rProcessBlock{}) -// } -// // give time for messages to propagate between routines -// time.Sleep(time.Millisecond) -// } - -// // time for processor to finish and reactor to switch to consensus -// time.Sleep(20 * time.Millisecond) -// assert.True(t, mockSwitch.hasSwitchedToConsensus()) -// reactor.Stop() -// }) -// } -// } - -func TestReactorHelperMode(t *testing.T) { - var ( - channelID = byte(0x40) - ) - - config := cfg.ResetTestRoot("blockchain_reactor_v2_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) - - params := testReactorParams{ - logger: log.TestingLogger(), - genDoc: genDoc, - privVals: privVals, - startHeight: 20, - mockA: true, - } - - type testEvent struct { - peer string - event interface{} - } - - tests := []struct { - name string - params testReactorParams - msgs []testEvent - }{ - { - name: "status request", - params: params, - msgs: []testEvent{ - {"P1", bcproto.StatusRequest{}}, - {"P1", bcproto.BlockRequest{Height: 13}}, - {"P1", bcproto.BlockRequest{Height: 20}}, - {"P1", bcproto.BlockRequest{Height: 22}}, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - reactor := newTestReactor(params) - mockSwitch := &mockSwitchIo{switchedToConsensus: false} - reactor.io = mockSwitch - err := reactor.Start() - require.NoError(t, err) - - for i := 0; i < len(tt.msgs); i++ { - step := tt.msgs[i] - switch ev := step.event.(type) { - case bcproto.StatusRequest: - old := mockSwitch.numStatusResponse - msg, err := bc.EncodeMsg(&ev) - assert.NoError(t, err) - reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, msg) - assert.Equal(t, old+1, mockSwitch.numStatusResponse) - case bcproto.BlockRequest: - if ev.Height > params.startHeight { - old := mockSwitch.numNoBlockResponse - msg, err := bc.EncodeMsg(&ev) - assert.NoError(t, err) - reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, msg) - assert.Equal(t, old+1, mockSwitch.numNoBlockResponse) - } else { - old := mockSwitch.numBlockResponse - msg, err := bc.EncodeMsg(&ev) - assert.NoError(t, err) - assert.NoError(t, err) - reactor.Receive(channelID, mockPeer{id: p2p.ID(step.peer)}, msg) - assert.Equal(t, old+1, mockSwitch.numBlockResponse) - } - } - } - err = reactor.Stop() - require.NoError(t, err) - }) - } -} - -func TestReactorSetSwitchNil(t *testing.T) { - config := cfg.ResetTestRoot("blockchain_reactor_v2_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) - - reactor := newTestReactor(testReactorParams{ - logger: log.TestingLogger(), - genDoc: genDoc, - privVals: privVals, - }) - reactor.SetSwitch(nil) - - assert.Nil(t, reactor.Switch) - assert.Nil(t, reactor.io) -} - -//---------------------------------------------- -// utility funcs - -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - -type testApp struct { - abci.BaseApplication -} - -func randGenesisDoc(chainID string, numValidators int, randPower bool, minPower int64) ( - *types.GenesisDoc, []types.PrivValidator) { - validators := make([]types.GenesisValidator, numValidators) - privValidators := make([]types.PrivValidator, numValidators) - for i := 0; i < numValidators; i++ { - val, privVal := types.RandValidator(randPower, minPower) - validators[i] = types.GenesisValidator{ - PubKey: val.PubKey, - Power: val.VotingPower, - } - privValidators[i] = privVal - } - sort.Sort(types.PrivValidatorsByAddress(privValidators)) - - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: chainID, - Validators: validators, - }, privValidators -} - -// Why are we importing the entire blockExecutor dependency graph here -// when we have the facilities to -func newReactorStore( - genDoc *types.GenesisDoc, - privVals []types.PrivValidator, - maxBlockHeight int64) (*store.BlockStore, sm.State, *sm.BlockExecutor) { - if len(privVals) != 1 { - panic("only support one validator") - } - app := &testApp{} - cc := proxy.NewLocalClientCreator(app) - proxyApp := proxy.NewAppConns(cc) - err := proxyApp.Start() - if err != nil { - panic(fmt.Errorf("error start app: %w", err)) - } - - stateDB := dbm.NewMemDB() - blockStore := store.NewBlockStore(dbm.NewMemDB()) - stateStore := sm.NewStore(stateDB) - state, err := stateStore.LoadFromDBOrGenesisDoc(genDoc) - if err != nil { - panic(fmt.Errorf("error constructing state from genesis file: %w", err)) - } - - db := dbm.NewMemDB() - stateStore = sm.NewStore(db) - blockExec := sm.NewBlockExecutor(stateStore, log.TestingLogger(), proxyApp.Consensus(), - mock.Mempool{}, sm.EmptyEvidencePool{}) - if err = stateStore.Save(state); err != nil { - panic(err) - } - - // add blocks in - for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { - lastCommit := types.NewCommit(blockHeight-1, 0, types.BlockID{}, nil) - if blockHeight > 1 { - lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) - lastBlock := blockStore.LoadBlock(blockHeight - 1) - vote, err := types.MakeVote( - lastBlock.Header.Height, - lastBlockMeta.BlockID, - state.Validators, - privVals[0], - lastBlock.Header.ChainID, - time.Now(), - ) - if err != nil { - panic(err) - } - lastCommit = types.NewCommit(vote.Height, vote.Round, - lastBlockMeta.BlockID, []types.CommitSig{vote.CommitSig()}) - } - - thisBlock := makeBlock(blockHeight, state, lastCommit) - - thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{Hash: thisBlock.Hash(), PartSetHeader: thisParts.Header()} - - state, _, err = blockExec.ApplyBlock(state, blockID, thisBlock) - if err != nil { - panic(fmt.Errorf("error apply block: %w", err)) - } - - blockStore.SaveBlock(thisBlock, thisParts, lastCommit) - } - return blockStore, state, blockExec -} diff --git a/blockchain/v2/routine.go b/blockchain/v2/routine.go deleted file mode 100644 index dad0e737f..000000000 --- a/blockchain/v2/routine.go +++ /dev/null @@ -1,166 +0,0 @@ -package v2 - -import ( - "fmt" - "strings" - "sync/atomic" - - "github.com/Workiva/go-datastructures/queue" - - "github.com/tendermint/tendermint/libs/log" -) - -type handleFunc = func(event Event) (Event, error) - -const historySize = 25 - -// Routine is a structure that models a finite state machine as serialized -// stream of events processed by a handle function. This Routine structure -// handles the concurrency and messaging guarantees. Events are sent via -// `send` are handled by the `handle` function to produce an iterator -// `next()`. Calling `stop()` on a routine will conclude processing of all -// sent events and produce `final()` event representing the terminal state. -type Routine struct { - name string - handle handleFunc - queue *queue.PriorityQueue - history []Event - out chan Event - fin chan error - rdy chan struct{} - running *uint32 - logger log.Logger - metrics *Metrics -} - -func newRoutine(name string, handleFunc handleFunc, bufferSize int) *Routine { - return &Routine{ - name: name, - handle: handleFunc, - queue: queue.NewPriorityQueue(bufferSize, true), - history: make([]Event, 0, historySize), - out: make(chan Event, bufferSize), - rdy: make(chan struct{}, 1), - fin: make(chan error, 1), - running: new(uint32), - logger: log.NewNopLogger(), - metrics: NopMetrics(), - } -} - -func (rt *Routine) setLogger(logger log.Logger) { - rt.logger = logger -} - -// nolint:unused -func (rt *Routine) setMetrics(metrics *Metrics) { - rt.metrics = metrics -} - -func (rt *Routine) start() { - rt.logger.Info("routine start", "msg", log.NewLazySprintf("%s: run", rt.name)) - running := atomic.CompareAndSwapUint32(rt.running, uint32(0), uint32(1)) - if !running { - panic(fmt.Sprintf("%s is already running", rt.name)) - } - close(rt.rdy) - defer func() { - if r := recover(); r != nil { - var ( - b strings.Builder - j int - ) - for i := len(rt.history) - 1; i >= 0; i-- { - fmt.Fprintf(&b, "%d: %+v\n", j, rt.history[i]) - j++ - } - panic(fmt.Sprintf("%v\nlast events:\n%v", r, b.String())) - } - stopped := atomic.CompareAndSwapUint32(rt.running, uint32(1), uint32(0)) - if !stopped { - panic(fmt.Sprintf("%s is failed to stop", rt.name)) - } - }() - - for { - events, err := rt.queue.Get(1) - if err == queue.ErrDisposed { - rt.terminate(nil) - return - } else if err != nil { - rt.terminate(err) - return - } - oEvent, err := rt.handle(events[0].(Event)) - rt.metrics.EventsHandled.With("routine", rt.name).Add(1) - if err != nil { - rt.terminate(err) - return - } - rt.metrics.EventsOut.With("routine", rt.name).Add(1) - rt.logger.Debug("routine start", "msg", log.NewLazySprintf("%s: produced %T %+v", rt.name, oEvent, oEvent)) - - // Skip rTrySchedule and rProcessBlock events as they clutter the history - // due to their frequency. - switch events[0].(type) { - case rTrySchedule: - case rProcessBlock: - default: - rt.history = append(rt.history, events[0].(Event)) - if len(rt.history) > historySize { - rt.history = rt.history[1:] - } - } - - rt.out <- oEvent - } -} - -// XXX: look into returning OpError in the net package -func (rt *Routine) send(event Event) bool { - rt.logger.Debug("routine send", "msg", log.NewLazySprintf("%s: received %T %+v", rt.name, event, event)) - if !rt.isRunning() { - return false - } - err := rt.queue.Put(event) - if err != nil { - rt.metrics.EventsShed.With("routine", rt.name).Add(1) - rt.logger.Error(fmt.Sprintf("%s: send failed, queue was full/stopped", rt.name)) - return false - } - - rt.metrics.EventsSent.With("routine", rt.name).Add(1) - return true -} - -func (rt *Routine) isRunning() bool { - return atomic.LoadUint32(rt.running) == 1 -} - -func (rt *Routine) next() chan Event { - return rt.out -} - -func (rt *Routine) ready() chan struct{} { - return rt.rdy -} - -func (rt *Routine) stop() { - if !rt.isRunning() { // XXX: this should check rt.queue.Disposed() - return - } - - rt.logger.Info("routine stop", "msg", log.NewLazySprintf("%s: stop", rt.name)) - rt.queue.Dispose() // this should block until all queue items are free? -} - -func (rt *Routine) final() chan error { - return rt.fin -} - -// XXX: Maybe get rid of this -func (rt *Routine) terminate(reason error) { - // We don't close the rt.out channel here, to avoid spinning on the closed channel - // in the event loop. - rt.fin <- reason -} diff --git a/blockchain/v2/routine_test.go b/blockchain/v2/routine_test.go deleted file mode 100644 index 8f92bee3e..000000000 --- a/blockchain/v2/routine_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package v2 - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -type eventA struct { - priorityNormal -} - -var errDone = fmt.Errorf("done") - -func simpleHandler(event Event) (Event, error) { - if _, ok := event.(eventA); ok { - return noOp, errDone - } - return noOp, nil -} - -func TestRoutineFinal(t *testing.T) { - var ( - bufferSize = 10 - routine = newRoutine("simpleRoutine", simpleHandler, bufferSize) - ) - - assert.False(t, routine.isRunning(), - "expected an initialized routine to not be running") - go routine.start() - <-routine.ready() - assert.True(t, routine.isRunning(), - "expected an started routine") - - assert.True(t, routine.send(eventA{}), - "expected sending to a ready routine to succeed") - - assert.Equal(t, errDone, <-routine.final(), - "expected the final event to be done") - - assert.False(t, routine.isRunning(), - "expected an completed routine to no longer be running") -} - -func TestRoutineStop(t *testing.T) { - var ( - bufferSize = 10 - routine = newRoutine("simpleRoutine", simpleHandler, bufferSize) - ) - - assert.False(t, routine.send(eventA{}), - "expected sending to an unstarted routine to fail") - - go routine.start() - <-routine.ready() - - assert.True(t, routine.send(eventA{}), - "expected sending to a running routine to succeed") - - routine.stop() - - assert.False(t, routine.send(eventA{}), - "expected sending to a stopped routine to fail") -} - -type finalCount struct { - count int -} - -func (f finalCount) Error() string { - return "end" -} - -func genStatefulHandler(maxCount int) handleFunc { - counter := 0 - return func(event Event) (Event, error) { - if _, ok := event.(eventA); ok { - counter++ - if counter >= maxCount { - return noOp, finalCount{counter} - } - - return eventA{}, nil - } - return noOp, nil - } -} - -func feedback(r *Routine) { - for event := range r.next() { - r.send(event) - } -} - -func TestStatefulRoutine(t *testing.T) { - var ( - count = 10 - handler = genStatefulHandler(count) - bufferSize = 20 - routine = newRoutine("statefulRoutine", handler, bufferSize) - ) - - go routine.start() - go feedback(routine) - <-routine.ready() - - assert.True(t, routine.send(eventA{}), - "expected sending to a started routine to succeed") - - final := <-routine.final() - if fnl, ok := final.(finalCount); ok { - assert.Equal(t, count, fnl.count, - "expected the routine to count to 10") - } else { - t.Fail() - } -} - -type lowPriorityEvent struct { - priorityLow -} - -type highPriorityEvent struct { - priorityHigh -} - -func handleWithPriority(event Event) (Event, error) { - switch event.(type) { - case lowPriorityEvent: - return noOp, nil - case highPriorityEvent: - return noOp, errDone - } - return noOp, nil -} - -func TestPriority(t *testing.T) { - var ( - bufferSize = 20 - routine = newRoutine("priorityRoutine", handleWithPriority, bufferSize) - ) - - go routine.start() - <-routine.ready() - go func() { - for { - routine.send(lowPriorityEvent{}) - time.Sleep(1 * time.Millisecond) - } - }() - time.Sleep(10 * time.Millisecond) - - assert.True(t, routine.isRunning(), - "expected an started routine") - assert.True(t, routine.send(highPriorityEvent{}), - "expected send to succeed even when saturated") - - assert.Equal(t, errDone, <-routine.final()) - assert.False(t, routine.isRunning(), - "expected an started routine") -} diff --git a/blockchain/v2/scheduler.go b/blockchain/v2/scheduler.go deleted file mode 100644 index 75fe9d46d..000000000 --- a/blockchain/v2/scheduler.go +++ /dev/null @@ -1,712 +0,0 @@ -package v2 - -import ( - "bytes" - "errors" - "fmt" - "math" - "sort" - "time" - - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/types" -) - -// Events generated by the scheduler: -// all blocks have been processed -type scFinishedEv struct { - priorityNormal - reason string -} - -func (e scFinishedEv) String() string { - return fmt.Sprintf("scFinishedEv{%v}", e.reason) -} - -// send a blockRequest message -type scBlockRequest struct { - priorityNormal - peerID p2p.ID - height int64 -} - -func (e scBlockRequest) String() string { - return fmt.Sprintf("scBlockRequest{%d from %v}", e.height, e.peerID) -} - -// a block has been received and validated by the scheduler -type scBlockReceived struct { - priorityNormal - peerID p2p.ID - block *types.Block -} - -func (e scBlockReceived) String() string { - return fmt.Sprintf("scBlockReceived{%d#%X from %v}", e.block.Height, e.block.Hash(), e.peerID) -} - -// scheduler detected a peer error -type scPeerError struct { - priorityHigh - peerID p2p.ID - reason error -} - -func (e scPeerError) String() string { - return fmt.Sprintf("scPeerError{%v errored with %v}", e.peerID, e.reason) -} - -// scheduler removed a set of peers (timed out or slow peer) -type scPeersPruned struct { - priorityHigh - peers []p2p.ID -} - -func (e scPeersPruned) String() string { - return fmt.Sprintf("scPeersPruned{%v}", e.peers) -} - -// XXX: make this fatal? -// scheduler encountered a fatal error -type scSchedulerFail struct { - priorityHigh - reason error -} - -func (e scSchedulerFail) String() string { - return fmt.Sprintf("scSchedulerFail{%v}", e.reason) -} - -type blockState int - -const ( - blockStateUnknown blockState = iota + 1 // no known peer has this block - blockStateNew // indicates that a peer has reported having this block - blockStatePending // indicates that this block has been requested from a peer - blockStateReceived // indicates that this block has been received by a peer - blockStateProcessed // indicates that this block has been applied -) - -func (e blockState) String() string { - switch e { - case blockStateUnknown: - return "Unknown" - case blockStateNew: - return "New" - case blockStatePending: - return "Pending" - case blockStateReceived: - return "Received" - case blockStateProcessed: - return "Processed" - default: - return fmt.Sprintf("invalid blockState: %d", e) - } -} - -type peerState int - -const ( - peerStateNew = iota + 1 - peerStateReady - peerStateRemoved -) - -func (e peerState) String() string { - switch e { - case peerStateNew: - return "New" - case peerStateReady: - return "Ready" - case peerStateRemoved: - return "Removed" - default: - panic(fmt.Sprintf("unknown peerState: %d", e)) - } -} - -type scPeer struct { - peerID p2p.ID - - // initialized as New when peer is added, updated to Ready when statusUpdate is received, - // updated to Removed when peer is removed - state peerState - - base int64 // updated when statusResponse is received - height int64 // updated when statusResponse is received - lastTouched time.Time - lastRate int64 // last receive rate in bytes -} - -func (p scPeer) String() string { - return fmt.Sprintf("{state %v, base %d, height %d, lastTouched %v, lastRate %d, id %v}", - p.state, p.base, p.height, p.lastTouched, p.lastRate, p.peerID) -} - -func newScPeer(peerID p2p.ID) *scPeer { - return &scPeer{ - peerID: peerID, - state: peerStateNew, - base: -1, - height: -1, - lastTouched: time.Time{}, - } -} - -// The scheduler keep track of the state of each block and each peer. The -// scheduler will attempt to schedule new block requests with `trySchedule` -// events and remove slow peers with `tryPrune` events. -type scheduler struct { - initHeight int64 - - // next block that needs to be processed. All blocks with smaller height are - // in Processed state. - height int64 - - // lastAdvance tracks the last time a block execution happened. - // syncTimeout is the maximum time the scheduler waits to advance in the fast sync process before finishing. - // This covers the cases where there are no peers or all peers have a lower height. - lastAdvance time.Time - syncTimeout time.Duration - - // a map of peerID to scheduler specific peer struct `scPeer` used to keep - // track of peer specific state - peers map[p2p.ID]*scPeer - peerTimeout time.Duration // maximum response time from a peer otherwise prune - minRecvRate int64 // minimum receive rate from peer otherwise prune - - // the maximum number of blocks that should be New, Received or Pending at any point - // in time. This is used to enforce a limit on the blockStates map. - targetPending int - // a list of blocks to be scheduled (New), Pending or Received. Its length should be - // smaller than targetPending. - blockStates map[int64]blockState - - // a map of heights to the peer we are waiting a response from - pendingBlocks map[int64]p2p.ID - - // the time at which a block was put in blockStatePending - pendingTime map[int64]time.Time - - // a map of heights to the peers that put the block in blockStateReceived - receivedBlocks map[int64]p2p.ID -} - -func (sc scheduler) String() string { - return fmt.Sprintf("ih: %d, bst: %v, peers: %v, pblks: %v, ptm %v, rblks: %v", - sc.initHeight, sc.blockStates, sc.peers, sc.pendingBlocks, sc.pendingTime, sc.receivedBlocks) -} - -func newScheduler(initHeight int64, startTime time.Time) *scheduler { - sc := scheduler{ - initHeight: initHeight, - lastAdvance: startTime, - syncTimeout: 60 * time.Second, - height: initHeight, - blockStates: make(map[int64]blockState), - peers: make(map[p2p.ID]*scPeer), - pendingBlocks: make(map[int64]p2p.ID), - pendingTime: make(map[int64]time.Time), - receivedBlocks: make(map[int64]p2p.ID), - targetPending: 10, // TODO - pass as param - peerTimeout: 15 * time.Second, // TODO - pass as param - minRecvRate: 0, // int64(7680), TODO - pass as param - } - - return &sc -} - -func (sc *scheduler) ensurePeer(peerID p2p.ID) *scPeer { - if _, ok := sc.peers[peerID]; !ok { - sc.peers[peerID] = newScPeer(peerID) - } - return sc.peers[peerID] -} - -func (sc *scheduler) touchPeer(peerID p2p.ID, time time.Time) error { - peer, ok := sc.peers[peerID] - if !ok { - return fmt.Errorf("couldn't find peer %s", peerID) - } - - if peer.state != peerStateReady { - return fmt.Errorf("tried to touch peer in state %s, must be Ready", peer.state) - } - - peer.lastTouched = time - - return nil -} - -func (sc *scheduler) removePeer(peerID p2p.ID) { - peer, ok := sc.peers[peerID] - if !ok { - return - } - if peer.state == peerStateRemoved { - return - } - - for height, pendingPeerID := range sc.pendingBlocks { - if pendingPeerID == peerID { - sc.setStateAtHeight(height, blockStateNew) - delete(sc.pendingTime, height) - delete(sc.pendingBlocks, height) - } - } - - for height, rcvPeerID := range sc.receivedBlocks { - if rcvPeerID == peerID { - sc.setStateAtHeight(height, blockStateNew) - delete(sc.receivedBlocks, height) - } - } - - // remove the blocks from blockStates if the peer removal causes the max peer height to be lower. - peer.state = peerStateRemoved - maxPeerHeight := int64(0) - for _, otherPeer := range sc.peers { - if otherPeer.state != peerStateReady { - continue - } - if otherPeer.peerID != peer.peerID && otherPeer.height > maxPeerHeight { - maxPeerHeight = otherPeer.height - } - } - for h := range sc.blockStates { - if h > maxPeerHeight { - delete(sc.blockStates, h) - } - } -} - -// check if the blockPool is running low and add new blocks in New state to be requested. -// This function is called when there is an increase in the maximum peer height or when -// blocks are processed. -func (sc *scheduler) addNewBlocks() { - if len(sc.blockStates) >= sc.targetPending { - return - } - - for i := sc.height; i < int64(sc.targetPending)+sc.height; i++ { - if i > sc.maxHeight() { - break - } - if sc.getStateAtHeight(i) == blockStateUnknown { - sc.setStateAtHeight(i, blockStateNew) - } - } -} - -func (sc *scheduler) setPeerRange(peerID p2p.ID, base int64, height int64) error { - peer := sc.ensurePeer(peerID) - - if peer.state == peerStateRemoved { - return nil // noop - } - - if height < peer.height { - sc.removePeer(peerID) - return fmt.Errorf("cannot move peer height lower. from %d to %d", peer.height, height) - } - - if base > height { - sc.removePeer(peerID) - return fmt.Errorf("cannot set peer base higher than its height") - } - - peer.base = base - peer.height = height - peer.state = peerStateReady - - sc.addNewBlocks() - return nil -} - -func (sc *scheduler) getStateAtHeight(height int64) blockState { - if height < sc.height { - return blockStateProcessed - } else if state, ok := sc.blockStates[height]; ok { - return state - } else { - return blockStateUnknown - } -} - -func (sc *scheduler) getPeersWithHeight(height int64) []p2p.ID { - peers := make([]p2p.ID, 0) - for _, peer := range sc.peers { - if peer.state != peerStateReady { - continue - } - if peer.base <= height && peer.height >= height { - peers = append(peers, peer.peerID) - } - } - return peers -} - -func (sc *scheduler) prunablePeers(peerTimout time.Duration, minRecvRate int64, now time.Time) []p2p.ID { - prunable := make([]p2p.ID, 0) - for peerID, peer := range sc.peers { - if peer.state != peerStateReady { - continue - } - if now.Sub(peer.lastTouched) > peerTimout || peer.lastRate < minRecvRate { - prunable = append(prunable, peerID) - } - } - // Tests for handleTryPrunePeer() may fail without sort due to range non-determinism - sort.Sort(PeerByID(prunable)) - return prunable -} - -func (sc *scheduler) setStateAtHeight(height int64, state blockState) { - sc.blockStates[height] = state -} - -// CONTRACT: peer exists and in Ready state. -func (sc *scheduler) markReceived(peerID p2p.ID, height int64, size int64, now time.Time) error { - peer := sc.peers[peerID] - - if state := sc.getStateAtHeight(height); state != blockStatePending || sc.pendingBlocks[height] != peerID { - return fmt.Errorf("received block %d from peer %s without being requested", height, peerID) - } - - pendingTime, ok := sc.pendingTime[height] - if !ok || now.Sub(pendingTime) <= 0 { - return fmt.Errorf("clock error: block %d received at %s but requested at %s", - height, pendingTime, now) - } - - peer.lastRate = size / now.Sub(pendingTime).Nanoseconds() - - sc.setStateAtHeight(height, blockStateReceived) - delete(sc.pendingBlocks, height) - delete(sc.pendingTime, height) - - sc.receivedBlocks[height] = peerID - - return nil -} - -func (sc *scheduler) markPending(peerID p2p.ID, height int64, time time.Time) error { - state := sc.getStateAtHeight(height) - if state != blockStateNew { - return fmt.Errorf("block %d should be in blockStateNew but is %s", height, state) - } - - peer, ok := sc.peers[peerID] - if !ok { - return fmt.Errorf("cannot find peer %s", peerID) - } - - if peer.state != peerStateReady { - return fmt.Errorf("cannot schedule %d from %s in %s", height, peerID, peer.state) - } - - if height > peer.height { - return fmt.Errorf("cannot request height %d from peer %s that is at height %d", - height, peerID, peer.height) - } - - if height < peer.base { - return fmt.Errorf("cannot request height %d for peer %s with base %d", - height, peerID, peer.base) - } - - sc.setStateAtHeight(height, blockStatePending) - sc.pendingBlocks[height] = peerID - sc.pendingTime[height] = time - - return nil -} - -func (sc *scheduler) markProcessed(height int64) error { - // It is possible that a peer error or timeout is handled after the processor - // has processed the block but before the scheduler received this event, so - // when pcBlockProcessed event is received, the block had been requested - // again => don't check the block state. - sc.lastAdvance = time.Now() - sc.height = height + 1 - delete(sc.pendingBlocks, height) - delete(sc.pendingTime, height) - delete(sc.receivedBlocks, height) - delete(sc.blockStates, height) - sc.addNewBlocks() - return nil -} - -func (sc *scheduler) allBlocksProcessed() bool { - if len(sc.peers) == 0 { - return false - } - return sc.height >= sc.maxHeight() -} - -// returns max peer height or the last processed block, i.e. sc.height -func (sc *scheduler) maxHeight() int64 { - max := sc.height - 1 - for _, peer := range sc.peers { - if peer.state != peerStateReady { - continue - } - if max < peer.height { - max = peer.height - } - } - return max -} - -// lowest block in sc.blockStates with state == blockStateNew or -1 if no new blocks -func (sc *scheduler) nextHeightToSchedule() int64 { - var min int64 = math.MaxInt64 - for height, state := range sc.blockStates { - if state == blockStateNew && height < min { - min = height - } - } - if min == math.MaxInt64 { - min = -1 - } - return min -} - -func (sc *scheduler) pendingFrom(peerID p2p.ID) []int64 { - var heights []int64 - for height, pendingPeerID := range sc.pendingBlocks { - if pendingPeerID == peerID { - heights = append(heights, height) - } - } - return heights -} - -func (sc *scheduler) selectPeer(height int64) (p2p.ID, error) { - peers := sc.getPeersWithHeight(height) - if len(peers) == 0 { - return "", fmt.Errorf("cannot find peer for height %d", height) - } - - // create a map from number of pending requests to a list - // of peers having that number of pending requests. - pendingFrom := make(map[int][]p2p.ID) - for _, peerID := range peers { - numPending := len(sc.pendingFrom(peerID)) - pendingFrom[numPending] = append(pendingFrom[numPending], peerID) - } - - // find the set of peers with minimum number of pending requests. - var minPending int64 = math.MaxInt64 - for mp := range pendingFrom { - if int64(mp) < minPending { - minPending = int64(mp) - } - } - - sort.Sort(PeerByID(pendingFrom[int(minPending)])) - return pendingFrom[int(minPending)][0], nil -} - -// PeerByID is a list of peers sorted by peerID. -type PeerByID []p2p.ID - -func (peers PeerByID) Len() int { - return len(peers) -} -func (peers PeerByID) Less(i, j int) bool { - return bytes.Compare([]byte(peers[i]), []byte(peers[j])) == -1 -} - -func (peers PeerByID) Swap(i, j int) { - peers[i], peers[j] = peers[j], peers[i] -} - -// Handlers - -// This handler gets the block, performs some validation and then passes it on to the processor. -func (sc *scheduler) handleBlockResponse(event bcBlockResponse) (Event, error) { - err := sc.touchPeer(event.peerID, event.time) - if err != nil { - // peer does not exist OR not ready - return noOp, nil - } - - err = sc.markReceived(event.peerID, event.block.Height, event.size, event.time) - if err != nil { - sc.removePeer(event.peerID) - return scPeerError{peerID: event.peerID, reason: err}, nil - } - - return scBlockReceived{peerID: event.peerID, block: event.block}, nil -} - -func (sc *scheduler) handleNoBlockResponse(event bcNoBlockResponse) (Event, error) { - // No such peer or peer was removed. - peer, ok := sc.peers[event.peerID] - if !ok || peer.state == peerStateRemoved { - return noOp, nil - } - - // The peer may have been just removed due to errors, low speed or timeouts. - sc.removePeer(event.peerID) - - return scPeerError{peerID: event.peerID, - reason: fmt.Errorf("peer %v with base %d height %d claims no block for %d", - event.peerID, peer.base, peer.height, event.height)}, nil -} - -func (sc *scheduler) handleBlockProcessed(event pcBlockProcessed) (Event, error) { - if event.height != sc.height { - panic(fmt.Sprintf("processed height %d, but expected height %d", event.height, sc.height)) - } - - err := sc.markProcessed(event.height) - if err != nil { - return scSchedulerFail{reason: err}, nil - } - - if sc.allBlocksProcessed() { - return scFinishedEv{reason: "processed all blocks"}, nil - } - - return noOp, nil -} - -// Handles an error from the processor. The processor had already cleaned the blocks from -// the peers included in this event. Just attempt to remove the peers. -func (sc *scheduler) handleBlockProcessError(event pcBlockVerificationFailure) (Event, error) { - // The peers may have been just removed due to errors, low speed or timeouts. - sc.removePeer(event.firstPeerID) - if event.firstPeerID != event.secondPeerID { - sc.removePeer(event.secondPeerID) - } - - if sc.allBlocksProcessed() { - return scFinishedEv{reason: "error on last block"}, nil - } - - return noOp, nil -} - -func (sc *scheduler) handleAddNewPeer(event bcAddNewPeer) (Event, error) { - sc.ensurePeer(event.peerID) - return noOp, nil -} - -func (sc *scheduler) handleRemovePeer(event bcRemovePeer) (Event, error) { - sc.removePeer(event.peerID) - - if sc.allBlocksProcessed() { - return scFinishedEv{reason: "removed peer"}, nil - } - - // Return scPeerError so the peer (and all associated blocks) is removed from - // the processor. - return scPeerError{peerID: event.peerID, reason: errors.New("peer was stopped")}, nil -} - -func (sc *scheduler) handleTryPrunePeer(event rTryPrunePeer) (Event, error) { - // Check behavior of peer responsible to deliver block at sc.height. - timeHeightAsked, ok := sc.pendingTime[sc.height] - if ok && time.Since(timeHeightAsked) > sc.peerTimeout { - // A request was sent to a peer for block at sc.height but a response was not received - // from that peer within sc.peerTimeout. Remove the peer. This is to ensure that a peer - // will be timed out even if it sends blocks at higher heights but prevents progress by - // not sending the block at current height. - sc.removePeer(sc.pendingBlocks[sc.height]) - } - - prunablePeers := sc.prunablePeers(sc.peerTimeout, sc.minRecvRate, event.time) - if len(prunablePeers) == 0 { - return noOp, nil - } - for _, peerID := range prunablePeers { - sc.removePeer(peerID) - } - - // If all blocks are processed we should finish. - if sc.allBlocksProcessed() { - return scFinishedEv{reason: "after try prune"}, nil - } - - return scPeersPruned{peers: prunablePeers}, nil -} - -func (sc *scheduler) handleResetState(event bcResetState) (Event, error) { - initHeight := event.state.LastBlockHeight + 1 - if initHeight == 1 { - initHeight = event.state.InitialHeight - } - sc.initHeight = initHeight - sc.height = initHeight - sc.lastAdvance = time.Now() - sc.addNewBlocks() - return noOp, nil -} - -func (sc *scheduler) handleTrySchedule(event rTrySchedule) (Event, error) { - if time.Since(sc.lastAdvance) > sc.syncTimeout { - return scFinishedEv{reason: "timeout, no advance"}, nil - } - - nextHeight := sc.nextHeightToSchedule() - if nextHeight == -1 { - return noOp, nil - } - - bestPeerID, err := sc.selectPeer(nextHeight) - if err != nil { - return scSchedulerFail{reason: err}, nil - } - if err := sc.markPending(bestPeerID, nextHeight, event.time); err != nil { - return scSchedulerFail{reason: err}, nil // XXX: peerError might be more appropriate - } - return scBlockRequest{peerID: bestPeerID, height: nextHeight}, nil - -} - -func (sc *scheduler) handleStatusResponse(event bcStatusResponse) (Event, error) { - err := sc.setPeerRange(event.peerID, event.base, event.height) - if err != nil { - return scPeerError{peerID: event.peerID, reason: err}, nil - } - return noOp, nil -} - -func (sc *scheduler) handle(event Event) (Event, error) { - switch event := event.(type) { - case bcResetState: - nextEvent, err := sc.handleResetState(event) - return nextEvent, err - case bcStatusResponse: - nextEvent, err := sc.handleStatusResponse(event) - return nextEvent, err - case bcBlockResponse: - nextEvent, err := sc.handleBlockResponse(event) - return nextEvent, err - case bcNoBlockResponse: - nextEvent, err := sc.handleNoBlockResponse(event) - return nextEvent, err - case rTrySchedule: - nextEvent, err := sc.handleTrySchedule(event) - return nextEvent, err - case bcAddNewPeer: - nextEvent, err := sc.handleAddNewPeer(event) - return nextEvent, err - case bcRemovePeer: - nextEvent, err := sc.handleRemovePeer(event) - return nextEvent, err - case rTryPrunePeer: - nextEvent, err := sc.handleTryPrunePeer(event) - return nextEvent, err - case pcBlockProcessed: - nextEvent, err := sc.handleBlockProcessed(event) - return nextEvent, err - case pcBlockVerificationFailure: - nextEvent, err := sc.handleBlockProcessError(event) - return nextEvent, err - default: - return scSchedulerFail{reason: fmt.Errorf("unknown event %v", event)}, nil - } -} diff --git a/blockchain/v2/scheduler_test.go b/blockchain/v2/scheduler_test.go deleted file mode 100644 index 5d4ebd976..000000000 --- a/blockchain/v2/scheduler_test.go +++ /dev/null @@ -1,2249 +0,0 @@ -package v2 - -import ( - "fmt" - "math" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -type scTestParams struct { - peers map[string]*scPeer - initHeight int64 - height int64 - allB []int64 - pending map[int64]p2p.ID - pendingTime map[int64]time.Time - received map[int64]p2p.ID - peerTimeout time.Duration - minRecvRate int64 - targetPending int - startTime time.Time - syncTimeout time.Duration -} - -func verifyScheduler(sc *scheduler) { - missing := 0 - if sc.maxHeight() >= sc.height { - missing = int(math.Min(float64(sc.targetPending), float64(sc.maxHeight()-sc.height+1))) - } - if len(sc.blockStates) != missing { - panic(fmt.Sprintf("scheduler block length %d different than target %d", len(sc.blockStates), missing)) - } -} - -func newTestScheduler(params scTestParams) *scheduler { - peers := make(map[p2p.ID]*scPeer) - var maxHeight int64 - - initHeight := params.initHeight - if initHeight == 0 { - initHeight = 1 - } - sc := newScheduler(initHeight, params.startTime) - if params.height != 0 { - sc.height = params.height - } - - for id, peer := range params.peers { - peer.peerID = p2p.ID(id) - peers[p2p.ID(id)] = peer - if maxHeight < peer.height { - maxHeight = peer.height - } - } - for _, h := range params.allB { - sc.blockStates[h] = blockStateNew - } - for h, pid := range params.pending { - sc.blockStates[h] = blockStatePending - sc.pendingBlocks[h] = pid - } - for h, tm := range params.pendingTime { - sc.pendingTime[h] = tm - } - for h, pid := range params.received { - sc.blockStates[h] = blockStateReceived - sc.receivedBlocks[h] = pid - } - - sc.peers = peers - sc.peerTimeout = params.peerTimeout - if params.syncTimeout == 0 { - sc.syncTimeout = 10 * time.Second - } else { - sc.syncTimeout = params.syncTimeout - } - - if params.targetPending == 0 { - sc.targetPending = 10 - } else { - sc.targetPending = params.targetPending - } - - sc.minRecvRate = params.minRecvRate - - verifyScheduler(sc) - - return sc -} - -func TestScInit(t *testing.T) { - var ( - initHeight int64 = 5 - sc = newScheduler(initHeight, time.Now()) - ) - assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight-1)) - assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight)) - assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1)) -} - -func TestScMaxHeights(t *testing.T) { - - tests := []struct { - name string - sc scheduler - wantMax int64 - }{ - { - name: "no peers", - sc: scheduler{height: 11}, - wantMax: 10, - }, - { - name: "one ready peer", - sc: scheduler{ - height: 3, - peers: map[p2p.ID]*scPeer{"P1": {height: 6, state: peerStateReady}}, - }, - wantMax: 6, - }, - { - name: "ready and removed peers", - sc: scheduler{ - height: 1, - peers: map[p2p.ID]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 10, state: peerStateRemoved}}, - }, - wantMax: 4, - }, - { - name: "removed peers", - sc: scheduler{ - height: 1, - peers: map[p2p.ID]*scPeer{ - "P1": {height: 4, state: peerStateRemoved}, - "P2": {height: 10, state: peerStateRemoved}}, - }, - wantMax: 0, - }, - { - name: "new peers", - sc: scheduler{ - height: 1, - peers: map[p2p.ID]*scPeer{ - "P1": {base: -1, height: -1, state: peerStateNew}, - "P2": {base: -1, height: -1, state: peerStateNew}}, - }, - wantMax: 0, - }, - { - name: "mixed peers", - sc: scheduler{ - height: 1, - peers: map[p2p.ID]*scPeer{ - "P1": {height: -1, state: peerStateNew}, - "P2": {height: 10, state: peerStateReady}, - "P3": {height: 20, state: peerStateRemoved}, - "P4": {height: 22, state: peerStateReady}, - }, - }, - wantMax: 22, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - // maxHeight() should not mutate the scheduler - wantSc := tt.sc - - resMax := tt.sc.maxHeight() - assert.Equal(t, tt.wantMax, resMax) - assert.Equal(t, wantSc, tt.sc) - }) - } -} - -func TestScEnsurePeer(t *testing.T) { - - type args struct { - peerID p2p.ID - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - }{ - { - name: "add first peer", - fields: scTestParams{}, - args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}}, - }, - { - name: "add second peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}}, - args: args{peerID: "P2"}, - wantFields: scTestParams{peers: map[string]*scPeer{ - "P1": {base: -1, height: -1, state: peerStateNew}, - "P2": {base: -1, height: -1, state: peerStateNew}}}, - }, - { - name: "add duplicate peer is fine", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, - args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, - }, - { - name: "add duplicate peer with existing peer in Ready state is noop", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}}, - allB: []int64{1, 2, 3}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}}, - allB: []int64{1, 2, 3}, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - sc.ensurePeer(tt.args.peerID) - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers) - }) - } -} - -func TestScTouchPeer(t *testing.T) { - now := time.Now() - - type args struct { - peerID p2p.ID - time time.Time - } - - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "attempt to touch non existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}}, - allB: []int64{1, 2, 3, 4, 5}, - }, - args: args{peerID: "P2", time: now}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}}, - allB: []int64{1, 2, 3, 4, 5}, - }, - wantErr: true, - }, - { - name: "attempt to touch peer in state New", - fields: scTestParams{peers: map[string]*scPeer{"P1": {}}}, - args: args{peerID: "P1", time: now}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {}}}, - wantErr: true, - }, - { - name: "attempt to touch peer in state Removed", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}}, - args: args{peerID: "P1", time: now}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}}, - wantErr: true, - }, - { - name: "touch peer in state Ready", - fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}}, - args: args{peerID: "P1", time: now.Add(3 * time.Second)}, - wantFields: scTestParams{peers: map[string]*scPeer{ - "P1": {state: peerStateReady, lastTouched: now.Add(3 * time.Second)}}}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - if err := sc.touchPeer(tt.args.peerID, tt.args.time); (err != nil) != tt.wantErr { - t.Errorf("touchPeer() wantErr %v, error = %v", tt.wantErr, err) - } - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers) - }) - } -} - -func TestScPrunablePeers(t *testing.T) { - now := time.Now() - - type args struct { - threshold time.Duration - time time.Time - minSpeed int64 - } - - tests := []struct { - name string - fields scTestParams - args args - wantResult []p2p.ID - }{ - { - name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, - args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100}, - wantResult: []p2p.ID{}, - }, - { - name: "mixed peers", - fields: scTestParams{peers: map[string]*scPeer{ - // X - removed, active, fast - "P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101}, - // X - ready, active, fast - "P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101}, - // X - removed, active, equal - "P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100}, - // V - ready, inactive, equal - "P4": {state: peerStateReady, lastTouched: now, lastRate: 100}, - // V - ready, inactive, slow - "P5": {state: peerStateReady, lastTouched: now, lastRate: 99}, - // V - ready, active, slow - "P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90}, - }}, - args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100}, - wantResult: []p2p.ID{"P4", "P5", "P6"}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // peersSlowerThan should not mutate the scheduler - wantSc := sc - res := sc.prunablePeers(tt.args.threshold, tt.args.minSpeed, tt.args.time) - assert.Equal(t, tt.wantResult, res) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScRemovePeer(t *testing.T) { - - type args struct { - peerID p2p.ID - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "remove non existing peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, - args: args{peerID: "P2"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, - }, - { - name: "remove single New peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}}, - args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}}}, - }, - { - name: "remove one of two New peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}, "P2": {height: -1}}}, - args: args{peerID: "P1"}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}, "P2": {height: -1}}}, - }, - { - name: "remove one Ready peer, all peers removed", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 10, state: peerStateRemoved}, - "P2": {height: 5, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5}, - }, - args: args{peerID: "P2"}, - wantFields: scTestParams{peers: map[string]*scPeer{ - "P1": {height: 10, state: peerStateRemoved}, - "P2": {height: 5, state: peerStateRemoved}}, - }, - }, - { - name: "attempt to remove already removed peer", - fields: scTestParams{ - height: 8, - peers: map[string]*scPeer{ - "P1": {height: 10, state: peerStateRemoved}, - "P2": {height: 11, state: peerStateReady}}, - allB: []int64{8, 9, 10, 11}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - height: 8, - peers: map[string]*scPeer{ - "P1": {height: 10, state: peerStateRemoved}, - "P2": {height: 11, state: peerStateReady}}, - allB: []int64{8, 9, 10, 11}}, - }, - { - name: "remove Ready peer with blocks requested", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{1: "P1"}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}}, - allB: []int64{}, - pending: map[int64]p2p.ID{}, - }, - }, - { - name: "remove Ready peer with blocks received", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - received: map[int64]p2p.ID{1: "P1"}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}}, - allB: []int64{}, - received: map[int64]p2p.ID{}, - }, - }, - { - name: "remove Ready peer with blocks received and requested (not yet received)", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{1: "P1", 3: "P1"}, - received: map[int64]p2p.ID{2: "P1", 4: "P1"}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}, - allB: []int64{}, - pending: map[int64]p2p.ID{}, - received: map[int64]p2p.ID{}, - }, - }, - { - name: "remove Ready peer from multiple peers set, with blocks received and requested (not yet received)", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 6, state: peerStateReady}, - "P2": {height: 6, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4, 5, 6}, - pending: map[int64]p2p.ID{1: "P1", 3: "P2", 6: "P1"}, - received: map[int64]p2p.ID{2: "P1", 4: "P2", 5: "P2"}, - }, - args: args{peerID: "P1"}, - wantFields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 6, state: peerStateRemoved}, - "P2": {height: 6, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4, 5, 6}, - pending: map[int64]p2p.ID{3: "P2"}, - received: map[int64]p2p.ID{4: "P2", 5: "P2"}, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - sc.removePeer(tt.args.peerID) - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers) - }) - } -} - -func TestScSetPeerRange(t *testing.T) { - - type args struct { - peerID p2p.ID - base int64 - height int64 - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "change height of non existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{peerID: "P2", height: 4}, - wantFields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 2, state: peerStateReady}, - "P2": {height: 4, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4}}, - }, - { - name: "increase height of removed peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - args: args{peerID: "P1", height: 4}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - }, - { - name: "decrease height of single peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - args: args{peerID: "P1", height: 2}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}, - allB: []int64{}}, - wantErr: true, - }, - { - name: "increase height of single peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{peerID: "P1", height: 4}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - }, - { - name: "noop height change of single peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - args: args{peerID: "P1", height: 4}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - }, - { - name: "add peer with huge height 10**10 ", - fields: scTestParams{ - peers: map[string]*scPeer{"P2": {height: -1, state: peerStateNew}}, - targetPending: 4, - }, - args: args{peerID: "P2", height: 10000000000}, - wantFields: scTestParams{ - targetPending: 4, - peers: map[string]*scPeer{"P2": {height: 10000000000, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - }, - { - name: "add peer with base > height should error", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - args: args{peerID: "P1", base: 6, height: 5}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, - wantErr: true, - }, - { - name: "add peer with base == height is fine", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateNew}}, - targetPending: 4, - }, - args: args{peerID: "P1", base: 6, height: 6}, - wantFields: scTestParams{ - targetPending: 4, - peers: map[string]*scPeer{"P1": {base: 6, height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - err := sc.setPeerRange(tt.args.peerID, tt.args.base, tt.args.height) - if (err != nil) != tt.wantErr { - t.Errorf("setPeerHeight() wantErr %v, error = %v", tt.wantErr, err) - } - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers) - }) - } -} - -func TestScGetPeersWithHeight(t *testing.T) { - - type args struct { - height int64 - } - tests := []struct { - name string - fields scTestParams - args args - wantResult []p2p.ID - }{ - { - name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, - args: args{height: 10}, - wantResult: []p2p.ID{}, - }, - { - name: "only new peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, - args: args{height: 10}, - wantResult: []p2p.ID{}, - }, - { - name: "only Removed peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, - args: args{height: 2}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready shorter peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 5}, - wantResult: []p2p.ID{}, - }, - { - name: "one Ready equal peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 4}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one Ready higher peer", - fields: scTestParams{ - targetPending: 4, - peers: map[string]*scPeer{"P1": {height: 20, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 4}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one Ready higher peer at base", - fields: scTestParams{ - targetPending: 4, - peers: map[string]*scPeer{"P1": {base: 4, height: 20, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 4}, - wantResult: []p2p.ID{"P1"}, - }, - { - name: "one Ready higher peer with higher base", - fields: scTestParams{ - targetPending: 4, - peers: map[string]*scPeer{"P1": {base: 10, height: 20, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 4}, - wantResult: []p2p.ID{}, - }, - { - name: "multiple mixed peers", - fields: scTestParams{ - height: 8, - peers: map[string]*scPeer{ - "P1": {height: -1, state: peerStateNew}, - "P2": {height: 10, state: peerStateReady}, - "P3": {height: 5, state: peerStateReady}, - "P4": {height: 20, state: peerStateRemoved}, - "P5": {height: 11, state: peerStateReady}}, - allB: []int64{8, 9, 10, 11}, - }, - args: args{height: 8}, - wantResult: []p2p.ID{"P2", "P5"}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // getPeersWithHeight should not mutate the scheduler - wantSc := sc - res := sc.getPeersWithHeight(tt.args.height) - sort.Sort(PeerByID(res)) - assert.Equal(t, tt.wantResult, res) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScMarkPending(t *testing.T) { - now := time.Now() - - type args struct { - peerID p2p.ID - height int64 - tm time.Time - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "attempt mark pending an unknown block above height", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{peerID: "P1", height: 3, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - wantErr: true, - }, - { - name: "attempt mark pending an unknown block below base", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6}}, - args: args{peerID: "P1", height: 3, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6}}, - wantErr: true, - }, - { - name: "attempt mark pending from non existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{peerID: "P2", height: 1, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - wantErr: true, - }, - { - name: "mark pending from Removed peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - args: args{peerID: "P1", height: 1, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - wantErr: true, - }, - { - name: "mark pending from New peer", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 4, state: peerStateNew}, - }, - allB: []int64{1, 2, 3, 4}, - }, - args: args{peerID: "P2", height: 2, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 4, state: peerStateNew}, - }, - allB: []int64{1, 2, 3, 4}, - }, - wantErr: true, - }, - { - name: "mark pending from short peer", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 2, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4}, - }, - args: args{peerID: "P2", height: 3, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 2, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4}, - }, - wantErr: true, - }, - { - name: "mark pending all good", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1"}, - pendingTime: map[int64]time.Time{1: now}, - }, - args: args{peerID: "P1", height: 2, tm: now.Add(time.Millisecond)}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Millisecond)}, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - if err := sc.markPending(tt.args.peerID, tt.args.height, tt.args.tm); (err != nil) != tt.wantErr { - t.Errorf("markPending() wantErr %v, error = %v", tt.wantErr, err) - } - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScMarkReceived(t *testing.T) { - now := time.Now() - - type args struct { - peerID p2p.ID - height int64 - size int64 - tm time.Time - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "received from non existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{peerID: "P2", height: 1, size: 1000, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - wantErr: true, - }, - { - name: "received from removed peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - args: args{peerID: "P1", height: 1, size: 1000, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - wantErr: true, - }, - { - name: "received from unsolicited peer", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 4, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{1: "P1", 2: "P2", 3: "P2", 4: "P1"}, - }, - args: args{peerID: "P1", height: 2, size: 1000, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 4, state: peerStateReady}, - }, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{1: "P1", 2: "P2", 3: "P2", 4: "P1"}, - }, - wantErr: true, - }, - { - name: "received but blockRequest not sent", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{}, - }, - args: args{peerID: "P1", height: 2, size: 1000, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{}, - }, - wantErr: true, - }, - { - name: "received with bad timestamp", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)}, - }, - args: args{peerID: "P1", height: 2, size: 1000, tm: now}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)}, - }, - wantErr: true, - }, - { - name: "received all good", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now}, - }, - args: args{peerID: "P1", height: 2, size: 1000, tm: now.Add(time.Millisecond)}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{1: "P1"}, - pendingTime: map[int64]time.Time{1: now}, - received: map[int64]p2p.ID{2: "P1"}, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - if err := sc.markReceived( - tt.args.peerID, - tt.args.height, - tt.args.size, - now.Add(time.Second)); (err != nil) != tt.wantErr { - t.Errorf("markReceived() wantErr %v, error = %v", tt.wantErr, err) - } - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScMarkProcessed(t *testing.T) { - now := time.Now() - - type args struct { - height int64 - } - tests := []struct { - name string - fields scTestParams - args args - wantFields scTestParams - wantErr bool - }{ - { - name: "processed an unreceived block", - fields: scTestParams{ - height: 2, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{2}, - pending: map[int64]p2p.ID{2: "P1"}, - pendingTime: map[int64]time.Time{2: now}, - targetPending: 1, - }, - args: args{height: 2}, - wantFields: scTestParams{ - height: 3, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{3}, - targetPending: 1, - }, - }, - { - name: "mark processed success", - fields: scTestParams{ - height: 1, - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - pending: map[int64]p2p.ID{2: "P1"}, - pendingTime: map[int64]time.Time{2: now}, - received: map[int64]p2p.ID{1: "P1"}}, - args: args{height: 1}, - wantFields: scTestParams{ - height: 2, - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{2}, - pending: map[int64]p2p.ID{2: "P1"}, - pendingTime: map[int64]time.Time{2: now}}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - oldBlockState := sc.getStateAtHeight(tt.args.height) - if err := sc.markProcessed(tt.args.height); (err != nil) != tt.wantErr { - t.Errorf("markProcessed() wantErr %v, error = %v", tt.wantErr, err) - } - if tt.wantErr { - assert.Equal(t, oldBlockState, sc.getStateAtHeight(tt.args.height)) - } else { - assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(tt.args.height)) - } - wantSc := newTestScheduler(tt.wantFields) - checkSameScheduler(t, wantSc, sc) - }) - } -} - -func TestScResetState(t *testing.T) { - tests := []struct { - name string - fields scTestParams - state state.State - wantFields scTestParams - }{ - { - name: "updates height and initHeight", - fields: scTestParams{ - height: 0, - initHeight: 0, - }, - state: state.State{LastBlockHeight: 7}, - wantFields: scTestParams{ - height: 8, - initHeight: 8, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - e, err := sc.handleResetState(bcResetState{state: tt.state}) - require.NoError(t, err) - assert.Equal(t, e, noOp) - wantSc := newTestScheduler(tt.wantFields) - checkSameScheduler(t, wantSc, sc) - }) - } -} - -func TestScAllBlocksProcessed(t *testing.T) { - now := time.Now() - - tests := []struct { - name string - fields scTestParams - wantResult bool - }{ - { - name: "no blocks, no peers", - fields: scTestParams{}, - wantResult: false, - }, - { - name: "only New blocks", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - wantResult: false, - }, - { - name: "only Pending blocks", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now}, - }, - wantResult: false, - }, - { - name: "only Received blocks", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"}, - }, - wantResult: false, - }, - { - name: "only Processed blocks plus highest is received", - fields: scTestParams{ - height: 4, - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}}, - allB: []int64{4}, - received: map[int64]p2p.ID{4: "P1"}, - }, - wantResult: true, - }, - { - name: "mixed block states", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{2: "P1", 4: "P1"}, - pendingTime: map[int64]time.Time{2: now, 4: now}, - }, - wantResult: false, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // allBlocksProcessed() should not mutate the scheduler - wantSc := sc - res := sc.allBlocksProcessed() - assert.Equal(t, tt.wantResult, res) - checkSameScheduler(t, wantSc, sc) - }) - } -} - -func TestScNextHeightToSchedule(t *testing.T) { - now := time.Now() - - tests := []struct { - name string - fields scTestParams - wantHeight int64 - }{ - { - name: "no blocks", - fields: scTestParams{initHeight: 11, height: 11}, - wantHeight: -1, - }, - { - name: "only New blocks", - fields: scTestParams{ - initHeight: 3, - peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}}, - allB: []int64{3, 4, 5, 6}, - }, - wantHeight: 3, - }, - { - name: "only Pending blocks", - fields: scTestParams{ - initHeight: 1, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"}, - pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now}, - }, - wantHeight: -1, - }, - { - name: "only Received blocks", - fields: scTestParams{ - initHeight: 1, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"}, - }, - wantHeight: -1, - }, - { - name: "only Processed blocks", - fields: scTestParams{ - initHeight: 1, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - wantHeight: 1, - }, - { - name: "mixed block states", - fields: scTestParams{ - initHeight: 1, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - pending: map[int64]p2p.ID{2: "P1"}, - pendingTime: map[int64]time.Time{2: now}, - }, - wantHeight: 1, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // nextHeightToSchedule() should not mutate the scheduler - wantSc := sc - - resMin := sc.nextHeightToSchedule() - assert.Equal(t, tt.wantHeight, resMin) - checkSameScheduler(t, wantSc, sc) - }) - } -} - -func TestScSelectPeer(t *testing.T) { - - type args struct { - height int64 - } - tests := []struct { - name string - fields scTestParams - args args - wantResult p2p.ID - wantError bool - }{ - { - name: "no peers", - fields: scTestParams{peers: map[string]*scPeer{}}, - args: args{height: 10}, - wantResult: "", - wantError: true, - }, - { - name: "only new peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, - args: args{height: 10}, - wantResult: "", - wantError: true, - }, - { - name: "only Removed peers", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, - args: args{height: 2}, - wantResult: "", - wantError: true, - }, - { - name: "one Ready shorter peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 5}, - wantResult: "", - wantError: true, - }, - { - name: "one Ready equal peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}, - }, - args: args{height: 4}, - wantResult: "P1", - }, - { - name: "one Ready higher peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6}, - }, - args: args{height: 4}, - wantResult: "P1", - }, - { - name: "one Ready higher peer with higher base", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6}, - }, - args: args{height: 3}, - wantResult: "", - wantError: true, - }, - { - name: "many Ready higher peers with different number of pending requests", - fields: scTestParams{ - height: 4, - peers: map[string]*scPeer{ - "P1": {height: 8, state: peerStateReady}, - "P2": {height: 9, state: peerStateReady}}, - allB: []int64{4, 5, 6, 7, 8, 9}, - pending: map[int64]p2p.ID{ - 4: "P1", 6: "P1", - 5: "P2", - }, - }, - args: args{height: 4}, - wantResult: "P2", - }, - { - name: "many Ready higher peers with same number of pending requests", - fields: scTestParams{ - peers: map[string]*scPeer{ - "P2": {height: 20, state: peerStateReady}, - "P1": {height: 15, state: peerStateReady}, - "P3": {height: 15, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - pending: map[int64]p2p.ID{ - 1: "P1", 2: "P1", - 3: "P3", 4: "P3", - 5: "P2", 6: "P2", - }, - }, - args: args{height: 7}, - wantResult: "P1", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - // selectPeer should not mutate the scheduler - wantSc := sc - res, err := sc.selectPeer(tt.args.height) - assert.Equal(t, tt.wantResult, res) - assert.Equal(t, tt.wantError, err != nil) - checkSameScheduler(t, wantSc, sc) - }) - } -} - -// makeScBlock makes an empty block. -func makeScBlock(height int64) *types.Block { - return &types.Block{Header: types.Header{Height: height}} -} - -// used in place of assert.Equal(t, want, actual) to avoid failures due to -// scheduler.lastAdvanced timestamp inequalities. -func checkSameScheduler(t *testing.T, want *scheduler, actual *scheduler) { - assert.Equal(t, want.initHeight, actual.initHeight) - assert.Equal(t, want.height, actual.height) - assert.Equal(t, want.peers, actual.peers) - assert.Equal(t, want.blockStates, actual.blockStates) - assert.Equal(t, want.pendingBlocks, actual.pendingBlocks) - assert.Equal(t, want.pendingTime, actual.pendingTime) - assert.Equal(t, want.blockStates, actual.blockStates) - assert.Equal(t, want.receivedBlocks, actual.receivedBlocks) - assert.Equal(t, want.blockStates, actual.blockStates) -} - -// checkScResults checks scheduler handler test results -func checkScResults(t *testing.T, wantErr bool, err error, wantEvent Event, event Event) { - if (err != nil) != wantErr { - t.Errorf("error = %v, wantErr %v", err, wantErr) - return - } - switch wantEvent := wantEvent.(type) { - case scPeerError: - assert.Equal(t, wantEvent.peerID, event.(scPeerError).peerID) - assert.Equal(t, wantEvent.reason != nil, event.(scPeerError).reason != nil) - case scBlockReceived: - assert.Equal(t, wantEvent.peerID, event.(scBlockReceived).peerID) - assert.Equal(t, wantEvent.block, event.(scBlockReceived).block) - case scSchedulerFail: - assert.Equal(t, wantEvent.reason != nil, event.(scSchedulerFail).reason != nil) - } -} - -func TestScHandleBlockResponse(t *testing.T) { - now := time.Now() - block6FromP1 := bcBlockResponse{ - time: now.Add(time.Millisecond), - peerID: p2p.ID("P1"), - size: 100, - block: makeScBlock(6), - } - - type args struct { - event bcBlockResponse - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "empty scheduler", - fields: scTestParams{}, - args: args{event: block6FromP1}, - wantEvent: noOpEvent{}, - }, - { - name: "block from removed peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, - args: args{event: block6FromP1}, - wantEvent: noOpEvent{}, - }, - { - name: "block we haven't asked for", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}}, - args: args{event: block6FromP1}, - wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, - }, - { - name: "block from wrong peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P2"}, - pendingTime: map[int64]time.Time{6: now}, - }, - args: args{event: block6FromP1}, - wantEvent: noOpEvent{}, - }, - { - name: "block with bad timestamp", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now.Add(time.Second)}, - }, - args: args{event: block6FromP1}, - wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, - }, - { - name: "good block, accept", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now}, - }, - args: args{event: block6FromP1}, - wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(6)}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleBlockResponse(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleNoBlockResponse(t *testing.T) { - now := time.Now() - noBlock6FromP1 := bcNoBlockResponse{ - time: now.Add(time.Millisecond), - peerID: p2p.ID("P1"), - height: 6, - } - - tests := []struct { - name string - fields scTestParams - wantEvent Event - wantFields scTestParams - wantErr bool - }{ - { - name: "empty scheduler", - fields: scTestParams{}, - wantEvent: noOpEvent{}, - wantFields: scTestParams{}, - }, - { - name: "noBlock from removed peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, - wantEvent: noOpEvent{}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, - }, - { - name: "for block we haven't asked for", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}}, - wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, - }, - { - name: "noBlock from peer we don't have", - fields: scTestParams{ - peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P2"}, - pendingTime: map[int64]time.Time{6: now}, - }, - wantEvent: noOpEvent{}, - wantFields: scTestParams{ - peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P2"}, - pendingTime: map[int64]time.Time{6: now}, - }, - }, - { - name: "noBlock from existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now}, - }, - wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, - wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleNoBlockResponse(noBlock6FromP1) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - wantSc := newTestScheduler(tt.wantFields) - assert.Equal(t, wantSc, sc) - }) - } -} - -func TestScHandleBlockProcessed(t *testing.T) { - now := time.Now() - processed6FromP1 := pcBlockProcessed{ - peerID: p2p.ID("P1"), - height: 6, - } - - type args struct { - event pcBlockProcessed - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "empty scheduler", - fields: scTestParams{height: 6}, - args: args{event: processed6FromP1}, - wantEvent: noOpEvent{}, - }, - { - name: "processed block we don't have", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now}, - }, - args: args{event: processed6FromP1}, - wantEvent: noOpEvent{}, - }, - { - name: "processed block ok, we processed all blocks", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 7, state: peerStateReady}}, - allB: []int64{6, 7}, - received: map[int64]p2p.ID{6: "P1", 7: "P1"}, - }, - args: args{event: processed6FromP1}, - wantEvent: scFinishedEv{}, - }, - { - name: "processed block ok, we still have blocks to process", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - pending: map[int64]p2p.ID{7: "P1", 8: "P1"}, - received: map[int64]p2p.ID{6: "P1"}, - }, - args: args{event: processed6FromP1}, - wantEvent: noOpEvent{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleBlockProcessed(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleBlockVerificationFailure(t *testing.T) { - now := time.Now() - - type args struct { - event pcBlockVerificationFailure - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "empty scheduler", - fields: scTestParams{}, - args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: noOpEvent{}, - }, - { - name: "failed block we don't have, single peer is still removed", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now}, - }, - args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: scFinishedEv{}, - }, - { - name: "failed block we don't have, one of two peers are removed", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - pending: map[int64]p2p.ID{6: "P1"}, - pendingTime: map[int64]time.Time{6: now}, - }, - args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: noOpEvent{}, - }, - { - name: "failed block, all blocks are processed after removal", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 7, state: peerStateReady}}, - allB: []int64{6, 7}, - received: map[int64]p2p.ID{6: "P1", 7: "P1"}, - }, - args: args{event: pcBlockVerificationFailure{height: 7, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: scFinishedEv{}, - }, - { - name: "failed block, we still have blocks to process", - fields: scTestParams{ - initHeight: 5, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}}, - allB: []int64{5, 6, 7, 8}, - pending: map[int64]p2p.ID{7: "P1", 8: "P1"}, - received: map[int64]p2p.ID{5: "P1", 6: "P1"}, - }, - args: args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: noOpEvent{}, - }, - { - name: "failed block, H+1 and H+2 delivered by different peers, we still have blocks to process", - fields: scTestParams{ - initHeight: 5, - peers: map[string]*scPeer{ - "P1": {height: 8, state: peerStateReady}, - "P2": {height: 8, state: peerStateReady}, - "P3": {height: 8, state: peerStateReady}, - }, - allB: []int64{5, 6, 7, 8}, - pending: map[int64]p2p.ID{7: "P1", 8: "P1"}, - received: map[int64]p2p.ID{5: "P1", 6: "P1"}, - }, - args: args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P2"}}, - wantEvent: noOpEvent{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleBlockProcessError(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleAddNewPeer(t *testing.T) { - addP1 := bcAddNewPeer{ - peerID: p2p.ID("P1"), - } - type args struct { - event bcAddNewPeer - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "add P1 to empty scheduler", - fields: scTestParams{}, - args: args{event: addP1}, - wantEvent: noOpEvent{}, - }, - { - name: "add duplicate peer", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - }, - args: args{event: addP1}, - wantEvent: noOpEvent{}, - }, - { - name: "add P1 to non empty scheduler", - fields: scTestParams{ - initHeight: 6, - peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}}, - allB: []int64{6, 7, 8}, - }, - args: args{event: addP1}, - wantEvent: noOpEvent{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleAddNewPeer(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleTryPrunePeer(t *testing.T) { - now := time.Now() - - pruneEv := rTryPrunePeer{ - time: now.Add(time.Second + time.Millisecond), - } - type args struct { - event rTryPrunePeer - } - - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "no peers", - fields: scTestParams{}, - args: args{event: pruneEv}, - wantEvent: noOpEvent{}, - }, - { - name: "no prunable peers", - fields: scTestParams{ - minRecvRate: 100, - peers: map[string]*scPeer{ - // X - removed, active, fast - "P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101}, - // X - ready, active, fast - "P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101}, - // X - removed, active, equal - "P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100}}, - peerTimeout: time.Second, - }, - args: args{event: pruneEv}, - wantEvent: noOpEvent{}, - }, - { - name: "mixed peers", - fields: scTestParams{ - minRecvRate: 100, - peers: map[string]*scPeer{ - // X - removed, active, fast - "P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5}, - // X - ready, active, fast - "P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5}, - // X - removed, active, equal - "P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5}, - // V - ready, inactive, equal - "P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7}, - // V - ready, inactive, slow - "P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7}, - // V - ready, active, slow - "P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7}, - }, - allB: []int64{1, 2, 3, 4, 5, 6, 7}, - peerTimeout: time.Second}, - args: args{event: pruneEv}, - wantEvent: scPeersPruned{peers: []p2p.ID{"P4", "P5", "P6"}}, - }, - { - name: "mixed peers, finish after pruning", - fields: scTestParams{ - minRecvRate: 100, - height: 6, - peers: map[string]*scPeer{ - // X - removed, active, fast - "P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5}, - // X - ready, active, fast - "P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5}, - // X - removed, active, equal - "P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5}, - // V - ready, inactive, equal - "P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7}, - // V - ready, inactive, slow - "P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7}, - // V - ready, active, slow - "P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7}, - }, - allB: []int64{6, 7}, - peerTimeout: time.Second}, - args: args{event: pruneEv}, - wantEvent: scFinishedEv{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleTryPrunePeer(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleTrySchedule(t *testing.T) { - now := time.Now() - tryEv := rTrySchedule{ - time: now.Add(time.Second + time.Millisecond), - } - - type args struct { - event rTrySchedule - } - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "no peers", - fields: scTestParams{startTime: now, peers: map[string]*scPeer{}}, - args: args{event: tryEv}, - wantEvent: noOpEvent{}, - }, - { - name: "only new peers", - fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}}, - args: args{event: tryEv}, - wantEvent: noOpEvent{}, - }, - { - name: "only Removed peers", - fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}}, - args: args{event: tryEv}, - wantEvent: noOpEvent{}, - }, - { - name: "one Ready shorter peer", - fields: scTestParams{ - startTime: now, - height: 6, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}}, - args: args{event: tryEv}, - wantEvent: noOpEvent{}, - }, - { - name: "one Ready equal peer", - fields: scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4}}, - args: args{event: tryEv}, - wantEvent: scBlockRequest{peerID: "P1", height: 1}, - }, - { - name: "many Ready higher peers with different number of pending requests", - fields: scTestParams{ - startTime: now, - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady}, - "P2": {height: 5, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5}, - pending: map[int64]p2p.ID{ - 1: "P1", 2: "P1", - 3: "P2", - }, - }, - args: args{event: tryEv}, - wantEvent: scBlockRequest{peerID: "P2", height: 4}, - }, - - { - name: "many Ready higher peers with same number of pending requests", - fields: scTestParams{ - startTime: now, - peers: map[string]*scPeer{ - "P2": {height: 8, state: peerStateReady}, - "P1": {height: 8, state: peerStateReady}, - "P3": {height: 8, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}, - pending: map[int64]p2p.ID{ - 1: "P1", 2: "P1", - 3: "P3", 4: "P3", - 5: "P2", 6: "P2", - }, - }, - args: args{event: tryEv}, - wantEvent: scBlockRequest{peerID: "P1", height: 7}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleTrySchedule(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandleStatusResponse(t *testing.T) { - now := time.Now() - statusRespP1Ev := bcStatusResponse{ - time: now.Add(time.Second + time.Millisecond), - peerID: "P1", - height: 6, - } - - type args struct { - event bcStatusResponse - } - tests := []struct { - name string - fields scTestParams - args args - wantEvent Event - wantErr bool - }{ - { - name: "change height of non existing peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P2": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}, - }, - args: args{event: statusRespP1Ev}, - wantEvent: noOpEvent{}, - }, - - { - name: "increase height of removed peer", - fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}}, - args: args{event: statusRespP1Ev}, - wantEvent: noOpEvent{}, - }, - - { - name: "decrease height of single peer", - fields: scTestParams{ - height: 5, - peers: map[string]*scPeer{"P1": {height: 10, state: peerStateReady}}, - allB: []int64{5, 6, 7, 8, 9, 10}, - }, - args: args{event: statusRespP1Ev}, - wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")}, - }, - - { - name: "increase height of single peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}}, - allB: []int64{1, 2}}, - args: args{event: statusRespP1Ev}, - wantEvent: noOpEvent{}, - }, - { - name: "noop height change of single peer", - fields: scTestParams{ - peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}}, - allB: []int64{1, 2, 3, 4, 5, 6}}, - args: args{event: statusRespP1Ev}, - wantEvent: noOpEvent{}, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - sc := newTestScheduler(tt.fields) - event, err := sc.handleStatusResponse(tt.args.event) - checkScResults(t, tt.wantErr, err, tt.wantEvent, event) - }) - } -} - -func TestScHandle(t *testing.T) { - now := time.Now() - - type unknownEv struct { - priorityNormal - } - - t0 := time.Now() - tick := make([]time.Time, 100) - for i := range tick { - tick[i] = t0.Add(time.Duration(i) * time.Millisecond) - } - - type args struct { - event Event - } - type scStep struct { - currentSc *scTestParams - args args - wantEvent Event - wantErr bool - wantSc *scTestParams - } - tests := []struct { - name string - steps []scStep - }{ - { - name: "unknown event", - steps: []scStep{ - { // add P1 - currentSc: &scTestParams{}, - args: args{event: unknownEv{}}, - wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")}, - wantSc: &scTestParams{}, - }, - }, - }, - { - name: "single peer, sync 3 blocks", - steps: []scStep{ - { // add P1 - currentSc: &scTestParams{startTime: now, peers: map[string]*scPeer{}, height: 1}, - args: args{event: bcAddNewPeer{peerID: "P1"}}, - wantEvent: noOpEvent{}, - wantSc: &scTestParams{startTime: now, peers: map[string]*scPeer{ - "P1": {base: -1, height: -1, state: peerStateNew}}, height: 1}, - }, - { // set height of P1 - args: args{event: bcStatusResponse{peerID: "P1", time: tick[0], height: 3}}, - wantEvent: noOpEvent{}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - height: 1, - }, - }, - { // schedule block 1 - args: args{event: rTrySchedule{time: tick[1]}}, - wantEvent: scBlockRequest{peerID: "P1", height: 1}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{1: "P1"}, - pendingTime: map[int64]time.Time{1: tick[1]}, - height: 1, - }, - }, - { // schedule block 2 - args: args{event: rTrySchedule{time: tick[2]}}, - wantEvent: scBlockRequest{peerID: "P1", height: 2}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1"}, - pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2]}, - height: 1, - }, - }, - { // schedule block 3 - args: args{event: rTrySchedule{time: tick[3]}}, - wantEvent: scBlockRequest{peerID: "P1", height: 3}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, - pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2], 3: tick[3]}, - height: 1, - }, - }, - { // block response 1 - args: args{event: bcBlockResponse{peerID: "P1", time: tick[4], size: 100, block: makeScBlock(1)}}, - wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(1)}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[4]}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{2: "P1", 3: "P1"}, - pendingTime: map[int64]time.Time{2: tick[2], 3: tick[3]}, - received: map[int64]p2p.ID{1: "P1"}, - height: 1, - }, - }, - { // block response 2 - args: args{event: bcBlockResponse{peerID: "P1", time: tick[5], size: 100, block: makeScBlock(2)}}, - wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(2)}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[5]}}, - allB: []int64{1, 2, 3}, - pending: map[int64]p2p.ID{3: "P1"}, - pendingTime: map[int64]time.Time{3: tick[3]}, - received: map[int64]p2p.ID{1: "P1", 2: "P1"}, - height: 1, - }, - }, - { // block response 3 - args: args{event: bcBlockResponse{peerID: "P1", time: tick[6], size: 100, block: makeScBlock(3)}}, - wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(3)}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{1, 2, 3}, - received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, - height: 1, - }, - }, - { // processed block 1 - args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 1}}, - wantEvent: noOpEvent{}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{2, 3}, - received: map[int64]p2p.ID{2: "P1", 3: "P1"}, - height: 2, - }, - }, - { // processed block 2 - args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 2}}, - wantEvent: scFinishedEv{}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{3}, - received: map[int64]p2p.ID{3: "P1"}, - height: 3, - }, - }, - }, - }, - { - name: "block verification failure", - steps: []scStep{ - { // failure processing block 1 - currentSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateReady, lastTouched: tick[6]}, - "P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{1, 2, 3, 4}, - received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"}, - height: 1, - }, - args: args{event: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}}, - wantEvent: noOpEvent{}, - wantSc: &scTestParams{ - startTime: now, - peers: map[string]*scPeer{ - "P1": {height: 4, state: peerStateRemoved, lastTouched: tick[6]}, - "P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}}, - allB: []int64{1, 2, 3}, - received: map[int64]p2p.ID{}, - height: 1, - }, - }, - }, - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - var sc *scheduler - for i, step := range tt.steps { - // First step must always initialize the currentState as state. - if step.currentSc != nil { - sc = newTestScheduler(*step.currentSc) - } - if sc == nil { - panic("Bad (initial?) step") - } - - nextEvent, err := sc.handle(step.args.event) - wantSc := newTestScheduler(*step.wantSc) - - t.Logf("step %d(%v): %s", i, step.args.event, sc) - checkSameScheduler(t, wantSc, sc) - - checkScResults(t, step.wantErr, err, step.wantEvent, nextEvent) - - // Next step may use the wantedState as their currentState. - sc = newTestScheduler(*step.wantSc) - } - }) - } -} diff --git a/blockchain/v2/types.go b/blockchain/v2/types.go deleted file mode 100644 index 7a73728e4..000000000 --- a/blockchain/v2/types.go +++ /dev/null @@ -1,65 +0,0 @@ -package v2 - -import ( - "github.com/Workiva/go-datastructures/queue" -) - -// Event is the type that can be added to the priority queue. -type Event queue.Item - -type priority interface { - Compare(other queue.Item) int - Priority() int -} - -type priorityLow struct{} -type priorityNormal struct{} -type priorityHigh struct{} - -func (p priorityLow) Priority() int { - return 1 -} - -func (p priorityNormal) Priority() int { - return 2 -} - -func (p priorityHigh) Priority() int { - return 3 -} - -func (p priorityLow) Compare(other queue.Item) int { - op := other.(priority) - if p.Priority() > op.Priority() { - return 1 - } else if p.Priority() == op.Priority() { - return 0 - } - return -1 -} - -func (p priorityNormal) Compare(other queue.Item) int { - op := other.(priority) - if p.Priority() > op.Priority() { - return 1 - } else if p.Priority() == op.Priority() { - return 0 - } - return -1 -} - -func (p priorityHigh) Compare(other queue.Item) int { - op := other.(priority) - if p.Priority() > op.Priority() { - return 1 - } else if p.Priority() == op.Priority() { - return 0 - } - return -1 -} - -type noOpEvent struct { - priorityLow -} - -var noOp = noOpEvent{} diff --git a/config/config.go b/config/config.go index cdd384dd0..9c8c3c236 100644 --- a/config/config.go +++ b/config/config.go @@ -897,10 +897,8 @@ func (cfg *FastSyncConfig) ValidateBasic() error { switch cfg.Version { case "v0": return nil - case "v1": - return nil - case "v2": - return nil + case "v1", "v2": + return fmt.Errorf("fast sync version %s has been deprecated. Please use v0 instead", cfg.Version) default: return fmt.Errorf("unknown fastsync version %s", cfg.Version) } diff --git a/config/config_test.go b/config/config_test.go index 6a46933bc..cdcddf427 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -134,7 +134,7 @@ func TestFastSyncConfigValidateBasic(t *testing.T) { // tamper with version cfg.Version = "v1" - assert.NoError(t, cfg.ValidateBasic()) + assert.Error(t, cfg.ValidateBasic()) cfg.Version = "invalid" assert.Error(t, cfg.ValidateBasic()) diff --git a/config/toml.go b/config/toml.go index c57b4db84..ada9605d2 100644 --- a/config/toml.go +++ b/config/toml.go @@ -434,9 +434,11 @@ chunk_fetchers = "{{ .StateSync.ChunkFetchers }}" [fastsync] # Fast Sync version to use: -# 1) "v0" (default) - the legacy fast sync implementation -# 2) "v1" - refactor of v0 version for better testability -# 2) "v2" - complete redesign of v0, optimized for testability & readability +# +# In v0.37, v1 and v2 of the fast sync protocol were deprecated. +# Please use v0 instead. +# +# 1) "v0" - the default fast sync implementation version = "{{ .FastSync.Version }}" ####################################################### diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index 17aabcaef..120716bfd 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -324,9 +324,11 @@ temp_dir = "" [fastsync] # Fast Sync version to use: +# +# In v0.37, the v1 and v2 fast sync protocols were deprecated. +# Please use v0 instead. +# # 1) "v0" (default) - the legacy fast sync implementation -# 2) "v1" - refactor of v0 version for better testability -# 2) "v2" - complete redesign of v0, optimized for testability & readability version = "v0" ####################################################### diff --git a/node/node.go b/node/node.go index 8b3f4fc97..46dc6ed70 100644 --- a/node/node.go +++ b/node/node.go @@ -16,9 +16,7 @@ import ( dbm "github.com/tendermint/tm-db" abci "github.com/tendermint/tendermint/abci/types" - bcv0 "github.com/tendermint/tendermint/blockchain/v0" - bcv1 "github.com/tendermint/tendermint/blockchain/v1" - bcv2 "github.com/tendermint/tendermint/blockchain/v2" + bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/crypto" @@ -449,11 +447,9 @@ func createBlockchainReactor(config *cfg.Config, switch config.FastSync.Version { case "v0": - bcReactor = bcv0.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - case "v1": - bcReactor = bcv1.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - case "v2": - bcReactor = bcv2.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor = bc.NewReactor(state.Copy(), blockExec, blockStore, fastSync) + case "v1", "v2": + return nil, fmt.Errorf("fast sync version %s has been deprecated. Please use v0", config.FastSync.Version) default: return nil, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) } @@ -1328,18 +1324,6 @@ func makeNodeInfo( txIndexerStatus = "off" } - var bcChannel byte - switch config.FastSync.Version { - case "v0": - bcChannel = bcv0.BlockchainChannel - case "v1": - bcChannel = bcv1.BlockchainChannel - case "v2": - bcChannel = bcv2.BlockchainChannel - default: - return p2p.DefaultNodeInfo{}, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) - } - nodeInfo := p2p.DefaultNodeInfo{ ProtocolVersion: p2p.NewProtocolVersion( version.P2PProtocol, // global @@ -1350,7 +1334,7 @@ func makeNodeInfo( Network: genDoc.ChainID, Version: version.TMCoreSemVer, Channels: []byte{ - bcChannel, + bc.BlockchainChannel, cs.StateChannel, cs.DataChannel, cs.VoteChannel, cs.VoteSetBitsChannel, mempl.MempoolChannel, evidence.EvidenceChannel, diff --git a/test/e2e/generator/generate.go b/test/e2e/generator/generate.go index db8433d73..e203e4d6b 100644 --- a/test/e2e/generator/generate.go +++ b/test/e2e/generator/generate.go @@ -26,10 +26,9 @@ var ( nodeDatabases = uniformChoice{"goleveldb", "cleveldb", "rocksdb", "boltdb", "badgerdb"} ipv6 = uniformChoice{false, true} // FIXME: grpc disabled due to https://github.com/tendermint/tendermint/issues/5439 - nodeABCIProtocols = uniformChoice{"unix", "tcp", "builtin"} // "grpc" - nodePrivvalProtocols = uniformChoice{"file", "unix", "tcp"} - // FIXME: v2 disabled due to flake - nodeFastSyncs = uniformChoice{"v0"} // "v2" + nodeABCIProtocols = uniformChoice{"unix", "tcp", "builtin"} // "grpc" + nodePrivvalProtocols = uniformChoice{"file", "unix", "tcp"} + nodeFastSyncs = uniformChoice{false, true} nodeStateSyncs = uniformChoice{false, true} nodeMempools = uniformChoice{"v0", "v1"} nodePersistIntervals = uniformChoice{0, 1, 5} @@ -202,7 +201,7 @@ func generateNode( StartAt: startAt, Database: nodeDatabases.Choose(r).(string), PrivvalProtocol: nodePrivvalProtocols.Choose(r).(string), - FastSync: nodeFastSyncs.Choose(r).(string), + FastSync: nodeFastSyncs.Choose(r).(bool), Mempool: nodeMempools.Choose(r).(string), StateSync: nodeStateSyncs.Choose(r).(bool) && startAt > 0, PersistInterval: ptrUint64(uint64(nodePersistIntervals.Choose(r).(int))), diff --git a/test/e2e/networks/ci.toml b/test/e2e/networks/ci.toml index 1960c3e3e..1fff574ca 100644 --- a/test/e2e/networks/ci.toml +++ b/test/e2e/networks/ci.toml @@ -66,7 +66,7 @@ perturb = ["pause"] start_at = 1005 # Becomes part of the validator set at 1010 seeds = ["seed02"] database = "cleveldb" -fast_sync = "v0" +fast_sync = true mempool_version = "v1" # FIXME: should be grpc, disabled due to https://github.com/tendermint/tendermint/issues/5439 #abci_protocol = "grpc" @@ -76,8 +76,7 @@ perturb = ["kill", "pause", "disconnect", "restart"] [node.full01] start_at = 1010 mode = "full" -# FIXME: should be v2, disabled due to flake -fast_sync = "v0" +fast_sync = true persistent_peers = ["validator01", "validator02", "validator03", "validator04", "validator05"] retain_blocks = 1 perturb = ["restart"] @@ -85,8 +84,7 @@ perturb = ["restart"] [node.full02] start_at = 1015 mode = "full" -# FIXME: should be v2, disabled due to flake -fast_sync = "v0" +fast_sync = true state_sync = true seeds = ["seed01"] perturb = ["restart"] diff --git a/test/e2e/pkg/manifest.go b/test/e2e/pkg/manifest.go index d75723be0..44c8e9118 100644 --- a/test/e2e/pkg/manifest.go +++ b/test/e2e/pkg/manifest.go @@ -88,9 +88,8 @@ type ManifestNode struct { // runner will wait for the network to reach at least this block height. StartAt int64 `toml:"start_at"` - // FastSync specifies the fast sync mode: "" (disable), "v0", "v1", or "v2". - // Defaults to disabled. - FastSync string `toml:"fast_sync"` + // FastSync specifies whether to enable the fast sync protocol. + FastSync bool `toml:"fast_sync"` // Mempool specifies which version of mempool to use. Either "v0" or "v1" // This defaults to v0. diff --git a/test/e2e/pkg/testnet.go b/test/e2e/pkg/testnet.go index 8b7ff54ca..1123c26a9 100644 --- a/test/e2e/pkg/testnet.go +++ b/test/e2e/pkg/testnet.go @@ -72,7 +72,7 @@ type Node struct { IP net.IP ProxyPort uint32 StartAt int64 - FastSync string + FastSync bool StateSync bool Mempool string Database string @@ -297,12 +297,6 @@ func (n Node) Validate(testnet Testnet) error { } } } - switch n.FastSync { - case "", "v0", "v1", "v2": - default: - return fmt.Errorf("invalid fast sync setting %q", n.FastSync) - - } switch n.Mempool { case "", "v0", "v1": default: diff --git a/test/e2e/runner/setup.go b/test/e2e/runner/setup.go index 1a15214e6..baca41a6a 100644 --- a/test/e2e/runner/setup.go +++ b/test/e2e/runner/setup.go @@ -285,11 +285,7 @@ func MakeConfig(node *e2e.Node) (*config.Config, error) { cfg.Mempool.Version = node.Mempool } - if node.FastSync == "" { - cfg.FastSyncMode = false - } else { - cfg.FastSync.Version = node.FastSync - } + cfg.FastSyncMode = node.FastSync if node.StateSync { cfg.StateSync.Enable = true