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  }