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