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