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  }