github.com/koko1123/flow-go-1@v0.29.6/consensus/hotstuff/votecollector/staking_vote_processor_test.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"errors"
     5  	"sync"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/mock"
     9  	"github.com/stretchr/testify/require"
    10  	"github.com/stretchr/testify/suite"
    11  	"go.uber.org/atomic"
    12  
    13  	"github.com/koko1123/flow-go-1/consensus/hotstuff"
    14  	"github.com/koko1123/flow-go-1/consensus/hotstuff/helper"
    15  	mockhotstuff "github.com/koko1123/flow-go-1/consensus/hotstuff/mocks"
    16  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    17  	hotstuffvalidator "github.com/koko1123/flow-go-1/consensus/hotstuff/validator"
    18  	"github.com/koko1123/flow-go-1/consensus/hotstuff/verification"
    19  	"github.com/onflow/flow-go/crypto"
    20  	"github.com/koko1123/flow-go-1/model/flow"
    21  	"github.com/koko1123/flow-go-1/module/local"
    22  	modulemock "github.com/koko1123/flow-go-1/module/mock"
    23  	"github.com/koko1123/flow-go-1/module/signature"
    24  	"github.com/koko1123/flow-go-1/utils/unittest"
    25  )
    26  
    27  func TestStakingVoteProcessor(t *testing.T) {
    28  	suite.Run(t, new(StakingVoteProcessorTestSuite))
    29  }
    30  
    31  // StakingVoteProcessorTestSuite is a test suite that holds mocked state for isolated testing of StakingVoteProcessor.
    32  type StakingVoteProcessorTestSuite struct {
    33  	VoteProcessorTestSuiteBase
    34  
    35  	processor       *StakingVoteProcessor
    36  	allParticipants flow.IdentityList
    37  }
    38  
    39  func (s *StakingVoteProcessorTestSuite) SetupTest() {
    40  	s.VoteProcessorTestSuiteBase.SetupTest()
    41  	s.allParticipants = unittest.IdentityListFixture(14)
    42  	s.processor = &StakingVoteProcessor{
    43  		log:               unittest.Logger(),
    44  		block:             s.proposal.Block,
    45  		stakingSigAggtor:  s.stakingAggregator,
    46  		onQCCreated:       s.onQCCreated,
    47  		minRequiredWeight: s.minRequiredWeight,
    48  		done:              *atomic.NewBool(false),
    49  		allParticipants:   s.allParticipants,
    50  	}
    51  }
    52  
    53  // TestInitialState tests that Block() and Status() return correct values after calling constructor
    54  func (s *StakingVoteProcessorTestSuite) TestInitialState() {
    55  	require.Equal(s.T(), s.proposal.Block, s.processor.Block())
    56  	require.Equal(s.T(), hotstuff.VoteCollectorStatusVerifying, s.processor.Status())
    57  }
    58  
    59  // TestProcess_VoteNotForProposal tests that vote should pass to validation only if it has correct
    60  // view and block ID matching proposal that is locked in StakingVoteProcessor
    61  func (s *StakingVoteProcessorTestSuite) TestProcess_VoteNotForProposal() {
    62  	err := s.processor.Process(unittest.VoteFixture(unittest.WithVoteView(s.proposal.Block.View)))
    63  	require.ErrorAs(s.T(), err, &VoteForIncompatibleBlockError)
    64  	require.False(s.T(), model.IsInvalidVoteError(err))
    65  
    66  	err = s.processor.Process(unittest.VoteFixture(unittest.WithVoteBlockID(s.proposal.Block.BlockID)))
    67  	require.ErrorAs(s.T(), err, &VoteForIncompatibleViewError)
    68  	require.False(s.T(), model.IsInvalidVoteError(err))
    69  
    70  	s.stakingAggregator.AssertNotCalled(s.T(), "Verify")
    71  }
    72  
    73  // TestProcess_InvalidSignature tests that StakingVoteProcessor doesn't collect signatures for votes with invalid signature.
    74  // Checks are made for cases where both staking and threshold signatures were submitted.
    75  func (s *StakingVoteProcessorTestSuite) TestProcess_InvalidSignature() {
    76  	exception := errors.New("unexpected-exception")
    77  
    78  	// sentinel error from `InvalidSignerError` should be wrapped as `InvalidVoteError`
    79  	voteA := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig())
    80  	s.stakingAggregator.On("Verify", voteA.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Once()
    81  	err := s.processor.Process(voteA)
    82  	require.Error(s.T(), err)
    83  	require.True(s.T(), model.IsInvalidVoteError(err))
    84  	require.True(s.T(), model.IsInvalidSignerError(err))
    85  
    86  	// sentinel error from `ErrInvalidSignature` should be wrapped as `InvalidVoteError`
    87  	voteB := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig())
    88  	s.stakingAggregator.On("Verify", voteB.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once()
    89  	err = s.processor.Process(voteB)
    90  	require.Error(s.T(), err)
    91  	require.True(s.T(), model.IsInvalidVoteError(err))
    92  	require.ErrorAs(s.T(), err, &model.ErrInvalidSignature)
    93  
    94  	// unexpected errors from `Verify` should be propagated, but should _not_ be wrapped as `InvalidVoteError`
    95  	voteC := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig())
    96  	s.stakingAggregator.On("Verify", voteC.SignerID, mock.Anything).Return(exception)
    97  	err = s.processor.Process(voteC)
    98  	require.ErrorIs(s.T(), err, exception)              // unexpected errors from verifying the vote signature should be propagated
    99  	require.False(s.T(), model.IsInvalidVoteError(err)) // but not interpreted as an invalid vote
   100  
   101  	s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd")
   102  }
   103  
   104  // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by
   105  // WeightedSignatureAggregator.TrustedAdd(..) are _not_ interpreted as invalid votes
   106  func (s *StakingVoteProcessorTestSuite) TestProcess_TrustedAdd_Exception() {
   107  	exception := errors.New("unexpected-exception")
   108  	stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, unittest.VoteWithStakingSig())
   109  	*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   110  	s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(nil).Once()
   111  	s.stakingAggregator.On("TrustedAdd", stakingVote.SignerID, mock.Anything).Return(uint64(0), exception).Once()
   112  	err := s.processor.Process(stakingVote)
   113  	require.ErrorIs(s.T(), err, exception)
   114  	require.False(s.T(), model.IsInvalidVoteError(err))
   115  	s.stakingAggregator.AssertExpectations(s.T())
   116  }
   117  
   118  // TestProcess_BuildQCError tests error path during process of building QC.
   119  // Building QC is a one time operation, we need to make sure that failing in one of the steps leads to exception.
   120  func (s *StakingVoteProcessorTestSuite) TestProcess_BuildQCError() {
   121  	// In this test we will mock all dependencies for happy path, and replace some branches with unhappy path
   122  	// to simulate errors along the branches.
   123  	vote := unittest.VoteForBlockFixture(s.proposal.Block)
   124  
   125  	// in this test case we aren't able to aggregate staking signature
   126  	exception := errors.New("staking-aggregate-exception")
   127  	stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{}
   128  	stakingSigAggregator.On("Verify", mock.Anything, mock.Anything).Return(nil).Once()
   129  	stakingSigAggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil).Once()
   130  	stakingSigAggregator.On("Aggregate").Return(nil, nil, exception).Once()
   131  
   132  	s.processor.stakingSigAggtor = stakingSigAggregator
   133  	err := s.processor.Process(vote)
   134  	require.ErrorIs(s.T(), err, exception)
   135  	stakingSigAggregator.AssertExpectations(s.T())
   136  }
   137  
   138  // TestProcess_NotEnoughStakingWeight tests a scenario where we first don't have enough weight,
   139  // then we iteratively increase it to the point where we have enough staking weight. No QC should be created.
   140  func (s *StakingVoteProcessorTestSuite) TestProcess_NotEnoughStakingWeight() {
   141  	for i := s.sigWeight; i < s.minRequiredWeight; i += s.sigWeight {
   142  		vote := unittest.VoteForBlockFixture(s.proposal.Block)
   143  		s.stakingAggregator.On("Verify", vote.SignerID, crypto.Signature(vote.SigData)).Return(nil).Once()
   144  		err := s.processor.Process(vote)
   145  		require.NoError(s.T(), err)
   146  	}
   147  	require.False(s.T(), s.processor.done.Load())
   148  	s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated")
   149  	s.stakingAggregator.AssertExpectations(s.T())
   150  }
   151  
   152  // TestProcess_CreatingQC tests a scenario when we have collected enough staking weight
   153  // and proceed to build QC. Created QC has to have all signatures and identities aggregated by
   154  // aggregator.
   155  func (s *StakingVoteProcessorTestSuite) TestProcess_CreatingQC() {
   156  	// prepare test setup: 13 votes with staking sigs
   157  	stakingSigners := s.allParticipants[:14].NodeIDs()
   158  	signerIndices, err := signature.EncodeSignersToIndices(stakingSigners, stakingSigners)
   159  	require.NoError(s.T(), err)
   160  
   161  	// setup aggregator
   162  	*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   163  	expectedSigData := unittest.RandomBytes(128)
   164  	s.stakingAggregator.On("Aggregate").Return(stakingSigners, expectedSigData, nil).Once()
   165  
   166  	// expected QC
   167  	s.onQCCreatedState.On("onQCCreated", mock.Anything).Run(func(args mock.Arguments) {
   168  		qc := args.Get(0).(*flow.QuorumCertificate)
   169  		// ensure that QC contains correct field
   170  		expectedQC := &flow.QuorumCertificate{
   171  			View:          s.proposal.Block.View,
   172  			BlockID:       s.proposal.Block.BlockID,
   173  			SignerIndices: signerIndices,
   174  			SigData:       expectedSigData,
   175  		}
   176  		require.Equal(s.T(), expectedQC, qc)
   177  	}).Return(nil).Once()
   178  
   179  	// add votes
   180  	for _, signer := range stakingSigners {
   181  		vote := unittest.VoteForBlockFixture(s.proposal.Block)
   182  		vote.SignerID = signer
   183  		expectedSig := crypto.Signature(vote.SigData)
   184  		s.stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Once()
   185  		s.stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) {
   186  			s.stakingTotalWeight += s.sigWeight
   187  		}).Return(s.stakingTotalWeight, nil).Once()
   188  		err := s.processor.Process(vote)
   189  		require.NoError(s.T(), err)
   190  	}
   191  
   192  	require.True(s.T(), s.processor.done.Load())
   193  	s.onQCCreatedState.AssertExpectations(s.T())
   194  	s.stakingAggregator.AssertExpectations(s.T())
   195  
   196  	// processing extra votes shouldn't result in creating new QCs
   197  	vote := unittest.VoteForBlockFixture(s.proposal.Block)
   198  	err = s.processor.Process(vote)
   199  	require.NoError(s.T(), err)
   200  
   201  	s.onQCCreatedState.AssertExpectations(s.T())
   202  }
   203  
   204  // TestProcess_ConcurrentCreatingQC tests a scenario where multiple goroutines process vote at same time,
   205  // we expect only one QC created in this scenario.
   206  func (s *StakingVoteProcessorTestSuite) TestProcess_ConcurrentCreatingQC() {
   207  	stakingSigners := s.allParticipants[:10].NodeIDs()
   208  	mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) {
   209  		aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil)
   210  		aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil)
   211  		aggregator.On("TotalWeight").Return(s.minRequiredWeight)
   212  		aggregator.On("Aggregate").Return(stakingSigners, unittest.RandomBytes(128), nil)
   213  	}
   214  
   215  	// mock aggregators, so we have enough weight and shares for creating QC
   216  	*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   217  	mockAggregator(s.stakingAggregator)
   218  
   219  	// at this point sending any vote should result in creating QC.
   220  	s.onQCCreatedState.On("onQCCreated", mock.Anything).Return(nil).Once()
   221  
   222  	var startupWg, shutdownWg sync.WaitGroup
   223  
   224  	vote := unittest.VoteForBlockFixture(s.proposal.Block)
   225  	startupWg.Add(1)
   226  	// prepare goroutines, so they are ready to submit a vote at roughly same time
   227  	for i := 0; i < 5; i++ {
   228  		shutdownWg.Add(1)
   229  		go func() {
   230  			defer shutdownWg.Done()
   231  			startupWg.Wait()
   232  			err := s.processor.Process(vote)
   233  			require.NoError(s.T(), err)
   234  		}()
   235  	}
   236  
   237  	startupWg.Done()
   238  
   239  	// wait for all routines to finish
   240  	shutdownWg.Wait()
   241  
   242  	s.onQCCreatedState.AssertNumberOfCalls(s.T(), "onQCCreated", 1)
   243  }
   244  
   245  // TestStakingVoteProcessorV2_BuildVerifyQC tests a complete path from creating votes to collecting votes and then
   246  // building & verifying QC.
   247  // We start with leader proposing a block, then new leader collects votes and builds a QC.
   248  // Need to verify that QC that was produced is valid and can be embedded in new proposal.
   249  func TestStakingVoteProcessorV2_BuildVerifyQC(t *testing.T) {
   250  	epochCounter := uint64(3)
   251  	epochLookup := &modulemock.EpochLookup{}
   252  	view := uint64(20)
   253  	epochLookup.On("EpochForViewWithFallback", view).Return(epochCounter, nil)
   254  
   255  	// signers hold objects that are created with private key and can sign votes and proposals
   256  	signers := make(map[flow.Identifier]*verification.StakingSigner)
   257  	// prepare staking signers, each signer has its own private/public key pair
   258  	stakingSigners := unittest.IdentityListFixture(7, func(identity *flow.Identity) {
   259  		stakingPriv := unittest.StakingPrivKeyFixture()
   260  		identity.StakingPubKey = stakingPriv.PublicKey()
   261  
   262  		me, err := local.New(identity, stakingPriv)
   263  		require.NoError(t, err)
   264  
   265  		signers[identity.NodeID] = verification.NewStakingSigner(me)
   266  	})
   267  
   268  	leader := stakingSigners[0]
   269  
   270  	block := helper.MakeBlock(helper.WithBlockView(view),
   271  		helper.WithBlockProposer(leader.NodeID))
   272  
   273  	committee := &mockhotstuff.Committee{}
   274  	committee.On("Identities", block.BlockID, mock.Anything).Return(stakingSigners, nil)
   275  
   276  	votes := make([]*model.Vote, 0, len(stakingSigners))
   277  
   278  	// first staking signer will be leader collecting votes for proposal
   279  	// prepare votes for every member of committee except leader
   280  	for _, signer := range stakingSigners[1:] {
   281  		vote, err := signers[signer.NodeID].CreateVote(block)
   282  		require.NoError(t, err)
   283  		votes = append(votes, vote)
   284  	}
   285  
   286  	// create and sign proposal
   287  	proposal, err := signers[leader.NodeID].CreateProposal(block)
   288  	require.NoError(t, err)
   289  
   290  	qcCreated := false
   291  	onQCCreated := func(qc *flow.QuorumCertificate) {
   292  		// create verifier that will do crypto checks of created QC
   293  		verifier := verification.NewStakingVerifier()
   294  		forks := &mockhotstuff.Forks{}
   295  		// create validator which will do compliance and crypto checked of created QC
   296  		validator := hotstuffvalidator.New(committee, forks, verifier)
   297  		// check if QC is valid against parent
   298  		err := validator.ValidateQC(qc, block)
   299  		require.NoError(t, err)
   300  
   301  		qcCreated = true
   302  	}
   303  
   304  	voteProcessorFactory := NewStakingVoteProcessorFactory(committee, onQCCreated)
   305  	voteProcessor, err := voteProcessorFactory.Create(unittest.Logger(), proposal)
   306  	require.NoError(t, err)
   307  
   308  	// process votes by new leader, this will result in producing new QC
   309  	for _, vote := range votes {
   310  		err := voteProcessor.Process(vote)
   311  		require.NoError(t, err)
   312  	}
   313  
   314  	require.True(t, qcCreated)
   315  }