github.com/koko1123/flow-go-1@v0.29.6/consensus/hotstuff/votecollector/statemachine_test.go (about) 1 package votecollector 2 3 import ( 4 "errors" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/gammazero/workerpool" 10 "github.com/rs/zerolog" 11 "github.com/stretchr/testify/require" 12 "github.com/stretchr/testify/suite" 13 14 "github.com/koko1123/flow-go-1/consensus/hotstuff" 15 "github.com/koko1123/flow-go-1/consensus/hotstuff/helper" 16 "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks" 17 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 18 "github.com/koko1123/flow-go-1/model/flow" 19 "github.com/koko1123/flow-go-1/utils/unittest" 20 ) 21 22 func TestStateMachine(t *testing.T) { 23 suite.Run(t, new(StateMachineTestSuite)) 24 } 25 26 var factoryError = errors.New("factory error") 27 28 // StateMachineTestSuite is a test suite for testing VoteCollector. It stores mocked 29 // VoteProcessors internally for testing behavior and state transitions for VoteCollector. 30 type StateMachineTestSuite struct { 31 suite.Suite 32 33 view uint64 34 notifier *mocks.Consumer 35 workerPool *workerpool.WorkerPool 36 factoryMethod VerifyingVoteProcessorFactory 37 mockedProcessors map[flow.Identifier]*mocks.VerifyingVoteProcessor 38 collector *VoteCollector 39 } 40 41 func (s *StateMachineTestSuite) TearDownTest() { 42 // Without this line we are risking running into weird situations where one test has finished but there are active workers 43 // that are executing some work on the shared pool. Need to ensure that all pending work has been executed before 44 // starting next test. 45 s.workerPool.StopWait() 46 } 47 48 func (s *StateMachineTestSuite) SetupTest() { 49 s.view = 1000 50 s.mockedProcessors = make(map[flow.Identifier]*mocks.VerifyingVoteProcessor) 51 s.notifier = &mocks.Consumer{} 52 53 s.factoryMethod = func(log zerolog.Logger, block *model.Proposal) (hotstuff.VerifyingVoteProcessor, error) { 54 if processor, found := s.mockedProcessors[block.Block.BlockID]; found { 55 return processor, nil 56 } 57 return nil, fmt.Errorf("mocked processor %v not found: %w", block.Block.BlockID, factoryError) 58 } 59 60 s.workerPool = workerpool.New(4) 61 s.collector = NewStateMachine(s.view, unittest.Logger(), s.workerPool, s.notifier, s.factoryMethod) 62 } 63 64 // prepareMockedProcessor prepares a mocked processor and stores it in map, later it will be used 65 // to mock behavior of verifying vote processor. 66 func (s *StateMachineTestSuite) prepareMockedProcessor(block *model.Block) *mocks.VerifyingVoteProcessor { 67 processor := &mocks.VerifyingVoteProcessor{} 68 processor.On("Block").Return(func() *model.Block { 69 return block 70 }).Maybe() 71 processor.On("Status").Return(hotstuff.VoteCollectorStatusVerifying) 72 s.mockedProcessors[block.BlockID] = processor 73 return processor 74 } 75 76 // TestStatus_StateTransitions tests that Status returns correct state of VoteCollector in different scenarios 77 // when proposal processing can possibly change state of collector 78 func (s *StateMachineTestSuite) TestStatus_StateTransitions() { 79 block := helper.MakeBlock(helper.WithBlockView(s.view)) 80 proposal := helper.MakeProposal(helper.WithBlock(block)) 81 s.prepareMockedProcessor(block) 82 83 // by default, we should create in caching status 84 require.Equal(s.T(), hotstuff.VoteCollectorStatusCaching, s.collector.Status()) 85 86 // after processing block we should get into verifying status 87 err := s.collector.ProcessBlock(proposal) 88 require.NoError(s.T(), err) 89 require.Equal(s.T(), hotstuff.VoteCollectorStatusVerifying, s.collector.Status()) 90 91 // after submitting double proposal we should transfer into invalid state 92 err = s.collector.ProcessBlock(helper.MakeProposal( 93 helper.WithBlock( 94 helper.MakeBlock(helper.WithBlockView(s.view))))) 95 require.NoError(s.T(), err) 96 require.Equal(s.T(), hotstuff.VoteCollectorStatusInvalid, s.collector.Status()) 97 } 98 99 // TestStatus_FactoryErrorPropagation verifies that errors from the injected 100 // factory are handed through (potentially wrapped), but are not replaced. 101 func (s *StateMachineTestSuite) Test_FactoryErrorPropagation() { 102 factoryError := errors.New("factory error") 103 factory := func(log zerolog.Logger, block *model.Proposal) (hotstuff.VerifyingVoteProcessor, error) { 104 return nil, factoryError 105 } 106 s.collector.createVerifyingProcessor = factory 107 108 // failing to create collector has to result in error and won't change state 109 err := s.collector.ProcessBlock(helper.MakeProposal(helper.WithBlock(helper.MakeBlock(helper.WithBlockView(s.view))))) 110 require.ErrorIs(s.T(), err, factoryError) 111 require.Equal(s.T(), hotstuff.VoteCollectorStatusCaching, s.collector.Status()) 112 } 113 114 // TestAddVote_VerifyingState tests that AddVote correctly process valid and invalid votes as well 115 // as repeated, invalid and double votes in verifying state 116 func (s *StateMachineTestSuite) TestAddVote_VerifyingState() { 117 block := helper.MakeBlock(helper.WithBlockView(s.view)) 118 proposal := helper.MakeProposal(helper.WithBlock(block)) 119 processor := s.prepareMockedProcessor(block) 120 err := s.collector.ProcessBlock(proposal) 121 require.NoError(s.T(), err) 122 s.T().Run("add-valid-vote", func(t *testing.T) { 123 vote := unittest.VoteForBlockFixture(block) 124 processor.On("Process", vote).Return(nil).Once() 125 err := s.collector.AddVote(vote) 126 require.NoError(t, err) 127 processor.AssertCalled(t, "Process", vote) 128 }) 129 s.T().Run("add-double-vote", func(t *testing.T) { 130 firstVote := unittest.VoteForBlockFixture(block) 131 processor.On("Process", firstVote).Return(nil).Once() 132 err := s.collector.AddVote(firstVote) 133 require.NoError(t, err) 134 135 secondVote := unittest.VoteFixture(func(vote *model.Vote) { 136 vote.View = firstVote.View 137 vote.SignerID = firstVote.SignerID 138 }) // voted blockID is randomly sampled, i.e. it will be different from firstVote 139 s.notifier.On("OnDoubleVotingDetected", firstVote, secondVote).Return(nil).Once() 140 141 err = s.collector.AddVote(secondVote) 142 // we shouldn't get an error 143 require.NoError(t, err) 144 145 // but should get notified about double voting 146 s.notifier.AssertCalled(t, "OnDoubleVotingDetected", firstVote, secondVote) 147 processor.AssertCalled(t, "Process", firstVote) 148 }) 149 s.T().Run("add-invalid-vote", func(t *testing.T) { 150 vote := unittest.VoteForBlockFixture(block, unittest.WithVoteView(s.view)) 151 processor.On("Process", vote).Return(model.NewInvalidVoteErrorf(vote, "")).Once() 152 153 s.notifier.On("OnInvalidVoteDetected", vote).Return(nil).Once() 154 err := s.collector.AddVote(vote) 155 // in case process returns model.InvalidVoteError we should silently ignore this error 156 require.NoError(t, err) 157 158 // but should get notified about invalid vote 159 s.notifier.AssertCalled(t, "OnInvalidVoteDetected", vote) 160 processor.AssertCalled(t, "Process", vote) 161 }) 162 s.T().Run("add-repeated-vote", func(t *testing.T) { 163 vote := unittest.VoteForBlockFixture(block) 164 processor.On("Process", vote).Return(nil).Once() 165 err := s.collector.AddVote(vote) 166 require.NoError(t, err) 167 168 // calling with same vote should exit early without error and don't do any extra processing 169 err = s.collector.AddVote(vote) 170 require.NoError(t, err) 171 172 processor.AssertCalled(t, "Process", vote) 173 }) 174 s.T().Run("add-incompatible-view-vote", func(t *testing.T) { 175 vote := unittest.VoteForBlockFixture(block, unittest.WithVoteView(s.view+1)) 176 err := s.collector.AddVote(vote) 177 require.ErrorIs(t, err, VoteForIncompatibleViewError) 178 }) 179 s.T().Run("add-incompatible-block-vote", func(t *testing.T) { 180 vote := unittest.VoteForBlockFixture(block, unittest.WithVoteView(s.view)) 181 processor.On("Process", vote).Return(VoteForIncompatibleBlockError).Once() 182 err := s.collector.AddVote(vote) 183 // in case process returns VoteForIncompatibleBlockError we should silently ignore this error 184 require.NoError(t, err) 185 processor.AssertCalled(t, "Process", vote) 186 }) 187 s.T().Run("unexpected-VoteProcessor-errors-are-passed-up", func(t *testing.T) { 188 unexpectedError := errors.New("some unexpected error") 189 vote := unittest.VoteForBlockFixture(block, unittest.WithVoteView(s.view)) 190 processor.On("Process", vote).Return(unexpectedError).Once() 191 err := s.collector.AddVote(vote) 192 require.ErrorIs(t, err, unexpectedError) 193 }) 194 } 195 196 // TestProcessBlock_ProcessingOfCachedVotes tests that after processing block proposal are cached votes 197 // are sent to vote processor 198 func (s *StateMachineTestSuite) TestProcessBlock_ProcessingOfCachedVotes() { 199 votes := 10 200 block := helper.MakeBlock(helper.WithBlockView(s.view)) 201 proposal := helper.MakeProposal(helper.WithBlock(block)) 202 processor := s.prepareMockedProcessor(block) 203 for i := 0; i < votes; i++ { 204 vote := unittest.VoteForBlockFixture(block) 205 // eventually it has to be process by processor 206 processor.On("Process", vote).Return(nil).Once() 207 require.NoError(s.T(), s.collector.AddVote(vote)) 208 } 209 210 err := s.collector.ProcessBlock(proposal) 211 require.NoError(s.T(), err) 212 213 time.Sleep(100 * time.Millisecond) 214 215 processor.AssertExpectations(s.T()) 216 } 217 218 // Test_VoteProcessorErrorPropagation verifies that unexpected errors from the `VoteProcessor` 219 // are propagated up the call stack (potentially wrapped), but are not replaced. 220 func (s *StateMachineTestSuite) Test_VoteProcessorErrorPropagation() { 221 block := helper.MakeBlock(helper.WithBlockView(s.view)) 222 processor := s.prepareMockedProcessor(block) 223 224 err := s.collector.ProcessBlock(helper.MakeProposal(helper.WithBlock(block))) 225 require.NoError(s.T(), err) 226 227 unexpectedError := errors.New("some unexpected error") 228 vote := unittest.VoteForBlockFixture(block, unittest.WithVoteView(s.view)) 229 processor.On("Process", vote).Return(unexpectedError).Once() 230 err = s.collector.AddVote(vote) 231 require.ErrorIs(s.T(), err, unexpectedError) 232 } 233 234 // RegisterVoteConsumer verifies that after registering vote consumer we are receiving all new and past votes 235 // in strict ordering of arrival. 236 func (s *StateMachineTestSuite) RegisterVoteConsumer() { 237 votes := 10 238 block := helper.MakeBlock(helper.WithBlockView(s.view)) 239 processor := s.prepareMockedProcessor(block) 240 expectedVotes := make([]*model.Vote, 0) 241 for i := 0; i < votes; i++ { 242 vote := unittest.VoteForBlockFixture(block) 243 // eventually it has to be process by processor 244 processor.On("Process", vote).Return(nil).Once() 245 require.NoError(s.T(), s.collector.AddVote(vote)) 246 expectedVotes = append(expectedVotes, vote) 247 } 248 249 actualVotes := make([]*model.Vote, 0) 250 consumer := func(vote *model.Vote) { 251 actualVotes = append(actualVotes, vote) 252 } 253 254 s.collector.RegisterVoteConsumer(consumer) 255 256 for i := 0; i < votes; i++ { 257 vote := unittest.VoteForBlockFixture(block) 258 // eventually it has to be process by processor 259 processor.On("Process", vote).Return(nil).Once() 260 require.NoError(s.T(), s.collector.AddVote(vote)) 261 expectedVotes = append(expectedVotes, vote) 262 } 263 264 require.Equal(s.T(), expectedVotes, actualVotes) 265 }