github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/votecollector/combined_vote_processor_v2_test.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"errors"
     5  	"math/rand"
     6  	"sync"
     7  	"testing"
     8  
     9  	"github.com/onflow/crypto"
    10  	"github.com/stretchr/testify/mock"
    11  	"github.com/stretchr/testify/require"
    12  	"github.com/stretchr/testify/suite"
    13  	"go.uber.org/atomic"
    14  	"pgregory.net/rapid"
    15  
    16  	bootstrapDKG "github.com/onflow/flow-go/cmd/bootstrap/dkg"
    17  	"github.com/onflow/flow-go/consensus/hotstuff"
    18  	"github.com/onflow/flow-go/consensus/hotstuff/committees"
    19  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    20  	mockhotstuff "github.com/onflow/flow-go/consensus/hotstuff/mocks"
    21  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    22  	"github.com/onflow/flow-go/consensus/hotstuff/signature"
    23  	hsig "github.com/onflow/flow-go/consensus/hotstuff/signature"
    24  	hotstuffvalidator "github.com/onflow/flow-go/consensus/hotstuff/validator"
    25  	"github.com/onflow/flow-go/consensus/hotstuff/verification"
    26  	"github.com/onflow/flow-go/model/encodable"
    27  	"github.com/onflow/flow-go/model/flow"
    28  	"github.com/onflow/flow-go/module/local"
    29  	modulemock "github.com/onflow/flow-go/module/mock"
    30  	msig "github.com/onflow/flow-go/module/signature"
    31  	"github.com/onflow/flow-go/state/protocol/inmem"
    32  	storagemock "github.com/onflow/flow-go/storage/mock"
    33  	"github.com/onflow/flow-go/utils/unittest"
    34  )
    35  
    36  func TestCombinedVoteProcessorV2(t *testing.T) {
    37  	suite.Run(t, new(CombinedVoteProcessorV2TestSuite))
    38  }
    39  
    40  // CombinedVoteProcessorV2TestSuite is a test suite that holds mocked state for isolated testing of CombinedVoteProcessorV2.
    41  type CombinedVoteProcessorV2TestSuite struct {
    42  	VoteProcessorTestSuiteBase
    43  
    44  	rbSharesTotal uint64
    45  
    46  	packer *mockhotstuff.Packer
    47  
    48  	reconstructor *mockhotstuff.RandomBeaconReconstructor
    49  
    50  	minRequiredShares uint64
    51  	processor         *CombinedVoteProcessorV2
    52  }
    53  
    54  func (s *CombinedVoteProcessorV2TestSuite) SetupTest() {
    55  	s.VoteProcessorTestSuiteBase.SetupTest()
    56  
    57  	s.reconstructor = &mockhotstuff.RandomBeaconReconstructor{}
    58  	s.packer = &mockhotstuff.Packer{}
    59  	s.proposal = helper.MakeProposal()
    60  
    61  	s.minRequiredShares = 9 // we require 9 RB shares to reconstruct signature
    62  	s.rbSharesTotal = 0
    63  
    64  	// setup rb reconstructor
    65  	s.reconstructor.On("TrustedAdd", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
    66  		s.rbSharesTotal++
    67  	}).Return(func(signerID flow.Identifier, sig crypto.Signature) bool {
    68  		return s.rbSharesTotal >= s.minRequiredShares
    69  	}, func(signerID flow.Identifier, sig crypto.Signature) error {
    70  		return nil
    71  	}).Maybe()
    72  	s.reconstructor.On("EnoughShares").Return(func() bool {
    73  		return s.rbSharesTotal >= s.minRequiredShares
    74  	}).Maybe()
    75  
    76  	s.processor = &CombinedVoteProcessorV2{
    77  		log:               unittest.Logger(),
    78  		block:             s.proposal.Block,
    79  		stakingSigAggtor:  s.stakingAggregator,
    80  		rbRector:          s.reconstructor,
    81  		onQCCreated:       s.onQCCreated,
    82  		packer:            s.packer,
    83  		minRequiredWeight: s.minRequiredWeight,
    84  		done:              *atomic.NewBool(false),
    85  	}
    86  }
    87  
    88  // TestInitialState tests that Block() and Status() return correct values after calling constructor
    89  func (s *CombinedVoteProcessorV2TestSuite) TestInitialState() {
    90  	require.Equal(s.T(), s.proposal.Block, s.processor.Block())
    91  	require.Equal(s.T(), hotstuff.VoteCollectorStatusVerifying, s.processor.Status())
    92  }
    93  
    94  // TestProcess_VoteNotForProposal tests that CombinedVoteProcessorV2 accepts only votes for the block it was initialized with
    95  // according to interface specification of `VoteProcessor`, we expect dedicated sentinel errors for votes
    96  // for different views (`VoteForIncompatibleViewError`) _or_ block (`VoteForIncompatibleBlockError`).
    97  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_VoteNotForProposal() {
    98  	err := s.processor.Process(unittest.VoteFixture(unittest.WithVoteView(s.proposal.Block.View)))
    99  	require.ErrorAs(s.T(), err, &VoteForIncompatibleBlockError)
   100  	require.False(s.T(), model.IsInvalidVoteError(err))
   101  
   102  	err = s.processor.Process(unittest.VoteFixture(unittest.WithVoteBlockID(s.proposal.Block.BlockID)))
   103  	require.ErrorAs(s.T(), err, &VoteForIncompatibleViewError)
   104  	require.False(s.T(), model.IsInvalidVoteError(err))
   105  
   106  	s.stakingAggregator.AssertNotCalled(s.T(), "Verify")
   107  	s.reconstructor.AssertNotCalled(s.T(), "Verify")
   108  }
   109  
   110  // TestProcess_InvalidSignatureFormat ensures that we process signatures only with valid format.
   111  // If we have received vote with signature in invalid format we should return with sentinel error
   112  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_InvalidSignatureFormat() {
   113  
   114  	// valid length is SigLen or 2*SigLen
   115  	generator := rapid.IntRange(0, 128).Filter(func(value int) bool {
   116  		return value != msig.SigLen && value != 2*msig.SigLen
   117  	})
   118  	rapid.Check(s.T(), func(t *rapid.T) {
   119  		// create a signature with invalid length
   120  		vote := unittest.VoteForBlockFixture(s.proposal.Block, func(vote *model.Vote) {
   121  			vote.SigData = unittest.RandomBytes(generator.Draw(t, "sig-size"))
   122  		})
   123  		err := s.processor.Process(vote)
   124  		require.Error(s.T(), err)
   125  		require.True(s.T(), model.IsInvalidVoteError(err), err)
   126  		require.True(s.T(), errors.Is(err, msig.ErrInvalidSignatureFormat), err)
   127  	})
   128  }
   129  
   130  // TestProcess_InvalidSignature tests that CombinedVoteProcessorV2 rejects invalid votes for the following scenarios:
   131  //  1. vote where `SignerID` is not a valid consensus participant;
   132  //     we test correct handling of votes that only a staking signature as well as votes with staking+beacon signatures
   133  //  2. vote from a valid consensus participant
   134  //     case 2a: vote contains only a staking signatures that is invalid
   135  //     case 2b: vote contains _invalid staking sig_, and valid random beacon sig
   136  //     case 2c: vote contains valid staking sig, but _invalid beacon sig_
   137  //
   138  // In all cases, the CombinedVoteProcessor should interpret these failure cases as invalid votes
   139  // and return an InvalidVoteError.
   140  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_InvalidSignature() {
   141  	// Scenario 1) vote where `SignerID` is not a valid consensus participant;
   142  	// sentinel error `InvalidSignerError` from WeightedSignatureAggregator should be wrapped as `InvalidVoteError`
   143  	s.Run("vote with invalid signerID", func() {
   144  		// vote with only a staking signature
   145  		stakingOnlyVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   146  		s.stakingAggregator.On("Verify", stakingOnlyVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Once()
   147  		err := s.processor.Process(stakingOnlyVote)
   148  		require.Error(s.T(), err)
   149  		require.True(s.T(), model.IsInvalidVoteError(err))
   150  		require.True(s.T(), model.IsInvalidSignerError(err))
   151  
   152  		// vote with staking+beacon signatures
   153  		doubleSigVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   154  		s.stakingAggregator.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Maybe()
   155  		s.reconstructor.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(model.NewInvalidSignerErrorf("")).Maybe()
   156  		err = s.processor.Process(doubleSigVote)
   157  		require.Error(s.T(), err)
   158  		require.True(s.T(), model.IsInvalidVoteError(err))
   159  		require.True(s.T(), model.IsInvalidSignerError(err))
   160  
   161  		s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd")
   162  		s.reconstructor.AssertNotCalled(s.T(), "TrustedAdd")
   163  	})
   164  
   165  	// Scenario 2) vote from a valid consensus participant but included sig(s) are cryptographically invalid;
   166  	// sentinel error `ErrInvalidSignature` from WeightedSignatureAggregator should be wrapped as `InvalidVoteError`
   167  	s.Run("vote from valid participant but with invalid signature(s)", func() {
   168  		// case 2a: vote contains only a staking signatures that is invalid
   169  		voteA := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   170  		s.stakingAggregator.On("Verify", voteA.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once()
   171  		err := s.processor.Process(voteA)
   172  		require.Error(s.T(), err)
   173  		require.True(s.T(), model.IsInvalidVoteError(err))
   174  		require.ErrorAs(s.T(), err, &model.ErrInvalidSignature)
   175  
   176  		// case 2b: vote contains _invalid staking sig_, and valid random beacon sig
   177  		voteB := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   178  		s.stakingAggregator.On("Verify", voteB.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once()
   179  		s.reconstructor.On("Verify", voteB.SignerID, mock.Anything).Return(nil).Maybe()
   180  		err = s.processor.Process(voteB)
   181  		require.Error(s.T(), err)
   182  		require.True(s.T(), model.IsInvalidVoteError(err))
   183  		require.ErrorAs(s.T(), err, &model.ErrInvalidSignature)
   184  
   185  		// case 2c: vote contains valid staking sig, but _invalid beacon sig_
   186  		voteC := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   187  		s.stakingAggregator.On("Verify", voteC.SignerID, mock.Anything).Return(nil).Maybe()
   188  		s.reconstructor.On("Verify", voteC.SignerID, mock.Anything).Return(model.ErrInvalidSignature).Once()
   189  		err = s.processor.Process(voteC)
   190  		require.Error(s.T(), err)
   191  		require.True(s.T(), model.IsInvalidVoteError(err))
   192  		require.ErrorAs(s.T(), err, &model.ErrInvalidSignature)
   193  
   194  		s.stakingAggregator.AssertNotCalled(s.T(), "TrustedAdd")
   195  		s.reconstructor.AssertNotCalled(s.T(), "TrustedAdd")
   196  	})
   197  }
   198  
   199  // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by
   200  // WeightedSignatureAggregator.Verify(..) are _not_ interpreted as invalid votes
   201  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_Verify_Exception() {
   202  	exception := errors.New("unexpected-exception")
   203  
   204  	s.Run("vote with staking-sig only", func() {
   205  		stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   206  		s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(exception).Once()
   207  		err := s.processor.Process(stakingVote)
   208  		require.ErrorIs(s.T(), err, exception)
   209  		require.False(s.T(), model.IsInvalidVoteError(err))
   210  	})
   211  
   212  	s.Run("vote with staking+beacon sig", func() {
   213  		// verifying staking sig leads to unexpected exception
   214  		doubleSigVoteA := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   215  		s.stakingAggregator.On("Verify", doubleSigVoteA.SignerID, mock.Anything).Return(exception).Once()
   216  		s.reconstructor.On("Verify", doubleSigVoteA.SignerID, mock.Anything).Return(nil).Maybe()
   217  		err := s.processor.Process(doubleSigVoteA)
   218  		require.ErrorIs(s.T(), err, exception)
   219  		require.False(s.T(), model.IsInvalidVoteError(err))
   220  
   221  		// verifying beacon sig leads to unexpected exception
   222  		doubleSigVoteB := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   223  		s.stakingAggregator.On("Verify", doubleSigVoteB.SignerID, mock.Anything).Return(nil).Maybe()
   224  		s.reconstructor.On("Verify", doubleSigVoteB.SignerID, mock.Anything).Return(exception).Once()
   225  		err = s.processor.Process(doubleSigVoteB)
   226  		require.Error(s.T(), err)
   227  		require.ErrorIs(s.T(), err, exception)
   228  		require.False(s.T(), model.IsInvalidVoteError(err))
   229  	})
   230  }
   231  
   232  // TestProcess_TrustedAdd_Exception tests that unexpected exceptions returned by
   233  // WeightedSignatureAggregator.Verify(..) are propagated, but _not_ interpreted as invalid votes
   234  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_TrustedAdd_Exception() {
   235  	exception := errors.New("unexpected-exception")
   236  	s.Run("staking-sig", func() {
   237  		stakingVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   238  		*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   239  		s.stakingAggregator.On("Verify", stakingVote.SignerID, mock.Anything).Return(nil).Once()
   240  		s.stakingAggregator.On("TrustedAdd", stakingVote.SignerID, mock.Anything).Return(uint64(0), exception).Once()
   241  		err := s.processor.Process(stakingVote)
   242  		require.ErrorIs(s.T(), err, exception)
   243  		require.False(s.T(), model.IsInvalidVoteError(err))
   244  	})
   245  	s.Run("beacon-sig", func() {
   246  		doubleSigVote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   247  		*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   248  		*s.reconstructor = mockhotstuff.RandomBeaconReconstructor{}
   249  
   250  		// first we will collect staking sig
   251  		s.stakingAggregator.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(nil).Once()
   252  		s.stakingAggregator.On("TrustedAdd", doubleSigVote.SignerID, mock.Anything).Return(uint64(0), nil).Once()
   253  
   254  		// then beacon sig
   255  		s.reconstructor.On("Verify", doubleSigVote.SignerID, mock.Anything).Return(nil).Once()
   256  		s.reconstructor.On("TrustedAdd", doubleSigVote.SignerID, mock.Anything).Return(false, exception).Once()
   257  
   258  		err := s.processor.Process(doubleSigVote)
   259  		require.ErrorIs(s.T(), err, exception)
   260  		require.False(s.T(), model.IsInvalidVoteError(err))
   261  	})
   262  }
   263  
   264  // TestProcess_BuildQCError tests all error paths during process of building QC.
   265  // Building QC is a one time operation, we need to make sure that failing in one of the steps leads to exception.
   266  // Since it's a one time operation we need a complicated test to test all conditions.
   267  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_BuildQCError() {
   268  	mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) {
   269  		aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil)
   270  		aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil)
   271  		aggregator.On("TotalWeight").Return(s.minRequiredWeight)
   272  	}
   273  
   274  	stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{}
   275  	reconstructor := &mockhotstuff.RandomBeaconReconstructor{}
   276  	packer := &mockhotstuff.Packer{}
   277  
   278  	identities := unittest.IdentifierListFixture(5)
   279  
   280  	// In this test we will mock all dependencies for happy path, and replace some branches with unhappy path
   281  	// to simulate errors along the branches.
   282  
   283  	mockAggregator(stakingSigAggregator)
   284  	stakingSigAggregator.On("Aggregate").Return(identities, unittest.RandomBytes(128), nil)
   285  
   286  	reconstructor.On("EnoughShares").Return(true)
   287  	reconstructor.On("Reconstruct").Return(unittest.SignatureFixture(), nil)
   288  
   289  	packer.On("Pack", mock.Anything, mock.Anything).Return(unittest.RandomBytes(100), unittest.RandomBytes(128), nil)
   290  
   291  	// Helper factory function to create processors. We need new processor for every test case
   292  	// because QC creation is one time operation and is triggered as soon as we have collected enough weight and shares.
   293  	createProcessor := func(stakingAggregator *mockhotstuff.WeightedSignatureAggregator,
   294  		rbReconstructor *mockhotstuff.RandomBeaconReconstructor,
   295  		packer *mockhotstuff.Packer) *CombinedVoteProcessorV2 {
   296  		return &CombinedVoteProcessorV2{
   297  			log:               unittest.Logger(),
   298  			block:             s.proposal.Block,
   299  			stakingSigAggtor:  stakingAggregator,
   300  			rbRector:          rbReconstructor,
   301  			onQCCreated:       s.onQCCreated,
   302  			packer:            packer,
   303  			minRequiredWeight: s.minRequiredWeight,
   304  			done:              *atomic.NewBool(false),
   305  		}
   306  	}
   307  
   308  	vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   309  
   310  	// in this test case we aren't able to aggregate staking signature
   311  	s.Run("staking-sig-aggregate", func() {
   312  		exception := errors.New("staking-aggregate-exception")
   313  		stakingSigAggregator := &mockhotstuff.WeightedSignatureAggregator{}
   314  		mockAggregator(stakingSigAggregator)
   315  		stakingSigAggregator.On("Aggregate").Return(nil, nil, exception)
   316  		processor := createProcessor(stakingSigAggregator, reconstructor, packer)
   317  		err := processor.Process(vote)
   318  		require.ErrorIs(s.T(), err, exception)
   319  		require.False(s.T(), model.IsInvalidVoteError(err))
   320  	})
   321  	// in this test case we aren't able to reconstruct signature
   322  	s.Run("reconstruct", func() {
   323  		exception := errors.New("reconstruct-exception")
   324  		reconstructor := &mockhotstuff.RandomBeaconReconstructor{}
   325  		reconstructor.On("EnoughShares").Return(true)
   326  		reconstructor.On("Reconstruct").Return(nil, exception)
   327  		processor := createProcessor(stakingSigAggregator, reconstructor, packer)
   328  		err := processor.Process(vote)
   329  		require.ErrorIs(s.T(), err, exception)
   330  		require.False(s.T(), model.IsInvalidVoteError(err))
   331  	})
   332  	// in this test case we aren't able to pack signatures
   333  	s.Run("pack", func() {
   334  		exception := errors.New("pack-qc-exception")
   335  		packer := &mockhotstuff.Packer{}
   336  		packer.On("Pack", mock.Anything, mock.Anything).Return(nil, nil, exception)
   337  		processor := createProcessor(stakingSigAggregator, reconstructor, packer)
   338  		err := processor.Process(vote)
   339  		require.ErrorIs(s.T(), err, exception)
   340  		require.False(s.T(), model.IsInvalidVoteError(err))
   341  	})
   342  }
   343  
   344  // TestProcess_EnoughWeightNotEnoughShares tests a scenario where we first don't have enough weight,
   345  // then we iteratively increase it to the point where we have enough staking weight. No QC should be created
   346  // in this scenario since there is not enough random beacon shares.
   347  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_EnoughWeightNotEnoughShares() {
   348  	for i := uint64(0); i < s.minRequiredWeight; i += s.sigWeight {
   349  		vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   350  		s.stakingAggregator.On("Verify", vote.SignerID, mock.Anything).Return(nil)
   351  		err := s.processor.Process(vote)
   352  		require.NoError(s.T(), err)
   353  	}
   354  
   355  	require.False(s.T(), s.processor.done.Load())
   356  	s.reconstructor.AssertCalled(s.T(), "EnoughShares")
   357  	s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated")
   358  }
   359  
   360  // TestProcess_EnoughSharesNotEnoughWeight tests a scenario where we are collecting votes with staking and beacon sigs
   361  // to the point where we have enough shares to reconstruct RB signature. No QC should be created
   362  // in this scenario since there is not enough staking weight.
   363  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_EnoughSharesNotEnoughWeight() {
   364  	// change sig weight to be really low, so we don't reach min staking weight while collecting
   365  	// beacon signatures
   366  	s.sigWeight = 10
   367  	for i := uint64(0); i < s.minRequiredShares; i++ {
   368  		vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithDoubleSig())
   369  		s.stakingAggregator.On("Verify", vote.SignerID, mock.Anything).Return(nil).Once()
   370  		s.reconstructor.On("Verify", vote.SignerID, mock.Anything).Return(nil).Once()
   371  		err := s.processor.Process(vote)
   372  		require.NoError(s.T(), err)
   373  	}
   374  
   375  	require.False(s.T(), s.processor.done.Load())
   376  	s.reconstructor.AssertNotCalled(s.T(), "EnoughShares")
   377  	s.onQCCreatedState.AssertNotCalled(s.T(), "onQCCreated")
   378  	// verify if we indeed have enough shares
   379  	require.True(s.T(), s.reconstructor.EnoughShares())
   380  }
   381  
   382  // TestProcess_ConcurrentCreatingQC tests a scenario where multiple goroutines process vote at same time,
   383  // we expect only one QC created in this scenario.
   384  func (s *CombinedVoteProcessorV2TestSuite) TestProcess_ConcurrentCreatingQC() {
   385  	stakingSigners := unittest.IdentifierListFixture(10)
   386  	mockAggregator := func(aggregator *mockhotstuff.WeightedSignatureAggregator) {
   387  		aggregator.On("Verify", mock.Anything, mock.Anything).Return(nil)
   388  		aggregator.On("TrustedAdd", mock.Anything, mock.Anything).Return(s.minRequiredWeight, nil)
   389  		aggregator.On("TotalWeight").Return(s.minRequiredWeight)
   390  		aggregator.On("Aggregate").Return(stakingSigners, unittest.RandomBytes(128), nil)
   391  	}
   392  
   393  	// mock aggregators, so we have enough weight and shares for creating QC
   394  	*s.stakingAggregator = mockhotstuff.WeightedSignatureAggregator{}
   395  	mockAggregator(s.stakingAggregator)
   396  	*s.reconstructor = mockhotstuff.RandomBeaconReconstructor{}
   397  	s.reconstructor.On("Verify", mock.Anything, mock.Anything).Return(nil)
   398  	s.reconstructor.On("Reconstruct").Return(unittest.SignatureFixture(), nil)
   399  	s.reconstructor.On("EnoughShares").Return(true)
   400  
   401  	// at this point sending any vote should result in creating QC.
   402  	s.packer.On("Pack", s.proposal.Block.View, mock.Anything).Return(unittest.RandomBytes(100), unittest.RandomBytes(128), nil)
   403  	s.onQCCreatedState.On("onQCCreated", mock.Anything).Return(nil).Once()
   404  
   405  	var startupWg, shutdownWg sync.WaitGroup
   406  
   407  	vote := unittest.VoteForBlockFixture(s.proposal.Block, VoteWithStakingSig())
   408  	startupWg.Add(1)
   409  	// prepare goroutines, so they are ready to submit a vote at roughly same time
   410  	for i := 0; i < 5; i++ {
   411  		shutdownWg.Add(1)
   412  		go func() {
   413  			defer shutdownWg.Done()
   414  			startupWg.Wait()
   415  			err := s.processor.Process(vote)
   416  			require.NoError(s.T(), err)
   417  		}()
   418  	}
   419  
   420  	startupWg.Done()
   421  
   422  	// wait for all routines to finish
   423  	shutdownWg.Wait()
   424  
   425  	s.onQCCreatedState.AssertNumberOfCalls(s.T(), "onQCCreated", 1)
   426  }
   427  
   428  // TestCombinedVoteProcessorV2_PropertyCreatingQCCorrectness uses property testing to test correctness of concurrent votes processing.
   429  // We randomly draw a committee with some number of staking, random beacon and byzantine nodes.
   430  // Values are drawn in a way that 1 <= honestParticipants <= participants <= maxParticipants
   431  // In each test iteration we expect to create a valid QC with all provided data as part of constructed QC.
   432  func TestCombinedVoteProcessorV2_PropertyCreatingQCCorrectness(testifyT *testing.T) {
   433  	maxParticipants := uint64(53)
   434  
   435  	rapid.Check(testifyT, func(t *rapid.T) {
   436  		// draw participants in range 1 <= participants <= maxParticipants
   437  		participants := rapid.Uint64Range(1, maxParticipants).Draw(t, "participants")
   438  		beaconSignersCount := rapid.Uint64Range(participants/2+1, participants).Draw(t, "beaconSigners")
   439  		stakingSignersCount := participants - beaconSignersCount
   440  		require.Equal(t, participants, stakingSignersCount+beaconSignersCount)
   441  
   442  		// setup how many votes we need to create a QC
   443  		// 1 <= honestParticipants <= participants <= maxParticipants
   444  		honestParticipants := participants*2/3 + 1
   445  		sigWeight := uint64(100)
   446  		minRequiredWeight := honestParticipants * sigWeight
   447  
   448  		// proposing block
   449  		block := helper.MakeBlock()
   450  
   451  		t.Logf("running conf\n\t"+
   452  			"staking signers: %v, beacon signers: %v\n\t"+
   453  			"required weight: %v", stakingSignersCount, beaconSignersCount, minRequiredWeight)
   454  
   455  		stakingTotalWeight, collectedShares := *atomic.NewUint64(0), *atomic.NewUint64(0)
   456  
   457  		// setup aggregators and reconstructor
   458  		stakingAggregator := &mockhotstuff.WeightedSignatureAggregator{}
   459  		reconstructor := &mockhotstuff.RandomBeaconReconstructor{}
   460  
   461  		stakingSigners := unittest.IdentifierListFixture(int(stakingSignersCount))
   462  		beaconSigners := unittest.IdentifierListFixture(int(beaconSignersCount))
   463  
   464  		// lists to track signers that actually contributed their signatures
   465  		var (
   466  			aggregatedStakingSigners flow.IdentifierList
   467  		)
   468  
   469  		// need separate locks to safely update vectors of voted signers
   470  		stakingAggregatorLock := &sync.Mutex{}
   471  
   472  		stakingAggregator.On("TotalWeight").Return(func() uint64 {
   473  			return stakingTotalWeight.Load()
   474  		})
   475  		reconstructor.On("EnoughShares").Return(func() bool {
   476  			return collectedShares.Load() >= beaconSignersCount
   477  		})
   478  
   479  		// mock expected calls to aggregators and reconstructor
   480  		combinedSigs := unittest.SignaturesFixture(2)
   481  		stakingAggregator.On("Aggregate").Return(
   482  			func() flow.IdentifierList {
   483  				stakingAggregatorLock.Lock()
   484  				defer stakingAggregatorLock.Unlock()
   485  				return aggregatedStakingSigners
   486  			},
   487  			func() []byte { return combinedSigs[0] },
   488  			func() error { return nil }).Once()
   489  		reconstructor.On("Reconstruct").Return(combinedSigs[1], nil).Once()
   490  
   491  		// mock expected call to Packer
   492  		mergedSignerIDs := make(flow.IdentifierList, 0)
   493  		packedSigData := unittest.RandomBytes(128)
   494  		pcker := &mockhotstuff.Packer{}
   495  		pcker.On("Pack", block.View, mock.Anything).Run(func(args mock.Arguments) {
   496  			blockSigData := args.Get(1).(*hotstuff.BlockSignatureData)
   497  
   498  			// check that aggregated signers are part of all votes signers
   499  			// due to concurrent processing it is possible that Aggregate will return less that we have actually aggregated
   500  			// but still enough to construct the QC
   501  			stakingAggregatorLock.Lock()
   502  			require.Subset(t, aggregatedStakingSigners, blockSigData.StakingSigners)
   503  			stakingAggregatorLock.Unlock()
   504  			require.Nil(t, blockSigData.RandomBeaconSigners)
   505  			require.Nil(t, blockSigData.AggregatedRandomBeaconSig)
   506  			require.GreaterOrEqual(t, uint64(len(blockSigData.StakingSigners)),
   507  				honestParticipants)
   508  
   509  			expectedBlockSigData := &hotstuff.BlockSignatureData{
   510  				StakingSigners:               blockSigData.StakingSigners,
   511  				RandomBeaconSigners:          nil,
   512  				AggregatedStakingSig:         []byte(combinedSigs[0]),
   513  				AggregatedRandomBeaconSig:    nil,
   514  				ReconstructedRandomBeaconSig: combinedSigs[1],
   515  			}
   516  
   517  			require.Equal(t, expectedBlockSigData, blockSigData)
   518  
   519  			// fill merged signers with collected signers
   520  			mergedSignerIDs = append(expectedBlockSigData.StakingSigners, expectedBlockSigData.RandomBeaconSigners...)
   521  		}).Return(
   522  			func(uint64, *hotstuff.BlockSignatureData) []byte {
   523  				signerIndices, _ := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs)
   524  				return signerIndices
   525  			},
   526  			func(uint64, *hotstuff.BlockSignatureData) []byte { return packedSigData },
   527  			func(uint64, *hotstuff.BlockSignatureData) error { return nil }).Once()
   528  
   529  		// track if QC was created
   530  		qcCreated := atomic.NewBool(false)
   531  
   532  		// expected QC
   533  		onQCCreated := func(qc *flow.QuorumCertificate) {
   534  			// QC should be created only once
   535  			if !qcCreated.CompareAndSwap(false, true) {
   536  				t.Fatalf("QC created more than once")
   537  			}
   538  
   539  			signerIndices, err := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs)
   540  			require.NoError(t, err)
   541  
   542  			// ensure that QC contains correct field
   543  			expectedQC := &flow.QuorumCertificate{
   544  				View:          block.View,
   545  				BlockID:       block.BlockID,
   546  				SignerIndices: signerIndices,
   547  				SigData:       packedSigData,
   548  			}
   549  			require.Equalf(t, expectedQC, qc, "QC should be equal to what we expect")
   550  		}
   551  
   552  		processor := &CombinedVoteProcessorV2{
   553  			log:               unittest.Logger(),
   554  			block:             block,
   555  			stakingSigAggtor:  stakingAggregator,
   556  			rbRector:          reconstructor,
   557  			onQCCreated:       onQCCreated,
   558  			packer:            pcker,
   559  			minRequiredWeight: minRequiredWeight,
   560  			done:              *atomic.NewBool(false),
   561  		}
   562  
   563  		votes := make([]*model.Vote, 0, stakingSignersCount+beaconSignersCount)
   564  
   565  		expectStakingAggregatorCalls := func(vote *model.Vote) {
   566  			expectedSig := crypto.Signature(vote.SigData[:msig.SigLen])
   567  			stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe()
   568  			stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) {
   569  				signerID := args.Get(0).(flow.Identifier)
   570  				stakingAggregatorLock.Lock()
   571  				defer stakingAggregatorLock.Unlock()
   572  				stakingTotalWeight.Add(sigWeight)
   573  				aggregatedStakingSigners = append(aggregatedStakingSigners, signerID)
   574  			}).Return(uint64(0), nil).Maybe()
   575  		}
   576  
   577  		// prepare votes
   578  		for _, signer := range stakingSigners {
   579  			vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithStakingSig())
   580  			vote.SignerID = signer
   581  			// this will set up mock
   582  			expectStakingAggregatorCalls(vote)
   583  			votes = append(votes, vote)
   584  		}
   585  		for _, signer := range beaconSigners {
   586  			vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithDoubleSig())
   587  			vote.SignerID = signer
   588  			expectStakingAggregatorCalls(vote)
   589  			expectedSig := crypto.Signature(vote.SigData[msig.SigLen:])
   590  			reconstructor.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe()
   591  			reconstructor.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) {
   592  				collectedShares.Inc()
   593  			}).Return(true, nil).Maybe()
   594  			votes = append(votes, vote)
   595  		}
   596  
   597  		// shuffle votes in random order
   598  		rand.Shuffle(len(votes), func(i, j int) {
   599  			votes[i], votes[j] = votes[j], votes[i]
   600  		})
   601  
   602  		var startProcessing, finishProcessing sync.WaitGroup
   603  		startProcessing.Add(1)
   604  		// process votes concurrently by multiple workers
   605  		for _, vote := range votes {
   606  			finishProcessing.Add(1)
   607  			go func(vote *model.Vote) {
   608  				defer finishProcessing.Done()
   609  				startProcessing.Wait()
   610  				err := processor.Process(vote)
   611  				require.NoError(t, err)
   612  			}(vote)
   613  		}
   614  
   615  		// start all goroutines at the same time
   616  		startProcessing.Done()
   617  		finishProcessing.Wait()
   618  
   619  		passed := processor.done.Load()
   620  		passed = passed && qcCreated.Load()
   621  		passed = passed && stakingAggregator.AssertExpectations(t)
   622  		passed = passed && reconstructor.AssertExpectations(t)
   623  
   624  		if !passed {
   625  			t.Fatalf("Assertions weren't met, staking weight: %v, shares collected: %v", stakingTotalWeight, collectedShares.Load())
   626  		}
   627  
   628  		//processing extra votes shouldn't result in creating new QCs
   629  		vote := unittest.VoteForBlockFixture(block, VoteWithDoubleSig())
   630  		err := processor.Process(vote)
   631  		require.NoError(t, err)
   632  	})
   633  }
   634  
   635  // TestCombinedVoteProcessorV2_PropertyCreatingQCLiveness uses property testing to test liveness of concurrent votes processing.
   636  // We randomly draw a committee and check if we are able to create a QC with minimal number of nodes.
   637  // In each test iteration we expect to create a QC, we don't check correctness of data since it's checked by another test.
   638  func TestCombinedVoteProcessorV2_PropertyCreatingQCLiveness(testifyT *testing.T) {
   639  	rapid.Check(testifyT, func(t *rapid.T) {
   640  		// draw beacon signers in range 1 <= beaconSignersCount <= 53
   641  		beaconSignersCount := rapid.Uint64Range(1, 53).Draw(t, "beaconSigners")
   642  		// draw staking signers in range 0 <= stakingSignersCount <= 10
   643  		stakingSignersCount := rapid.Uint64Range(0, 10).Draw(t, "stakingSigners")
   644  
   645  		stakingWeightRange, beaconWeightRange := rapid.Uint64Range(1, 10), rapid.Uint64Range(1, 10)
   646  
   647  		minRequiredWeight := uint64(0)
   648  		// draw weight for each signer randomly
   649  		stakingSigners := unittest.IdentityListFixture(int(stakingSignersCount), func(identity *flow.Identity) {
   650  			identity.InitialWeight = stakingWeightRange.Draw(t, identity.String())
   651  			minRequiredWeight += identity.InitialWeight
   652  		})
   653  		beaconSigners := unittest.IdentityListFixture(int(beaconSignersCount), func(identity *flow.Identity) {
   654  			identity.InitialWeight = beaconWeightRange.Draw(t, identity.String())
   655  			minRequiredWeight += identity.InitialWeight
   656  		})
   657  
   658  		// proposing block
   659  		block := helper.MakeBlock()
   660  
   661  		t.Logf("running conf\n\t"+
   662  			"staking signers: %v, beacon signers: %v\n\t"+
   663  			"required weight: %v", stakingSignersCount, beaconSignersCount, minRequiredWeight)
   664  
   665  		stakingTotalWeight, collectedShares := atomic.NewUint64(0), atomic.NewUint64(0)
   666  
   667  		// setup aggregators and reconstructor
   668  		stakingAggregator := &mockhotstuff.WeightedSignatureAggregator{}
   669  		reconstructor := &mockhotstuff.RandomBeaconReconstructor{}
   670  
   671  		stakingAggregator.On("TotalWeight").Return(func() uint64 {
   672  			return stakingTotalWeight.Load()
   673  		})
   674  		// don't require shares
   675  		reconstructor.On("EnoughShares").Return(func() bool {
   676  			return collectedShares.Load() >= beaconSignersCount
   677  		})
   678  
   679  		// mock expected calls to aggregator and reconstructor
   680  		combinedSigs := unittest.SignaturesFixture(2)
   681  		stakingAggregator.On("Aggregate").Return(stakingSigners.NodeIDs(), []byte(combinedSigs[0]), nil).Once()
   682  		reconstructor.On("Reconstruct").Return(combinedSigs[1], nil).Once()
   683  
   684  		// mock expected call to Packer
   685  		mergedSignerIDs := append(stakingSigners.NodeIDs(), beaconSigners.NodeIDs()...)
   686  		packedSigData := unittest.RandomBytes(128)
   687  		pcker := &mockhotstuff.Packer{}
   688  
   689  		signerIndices, err := msig.EncodeSignersToIndices(mergedSignerIDs, mergedSignerIDs)
   690  		require.NoError(t, err)
   691  		pcker.On("Pack", block.View, mock.Anything).Return(signerIndices, packedSigData, nil)
   692  
   693  		// track if QC was created
   694  		qcCreated := atomic.NewBool(false)
   695  
   696  		// expected QC
   697  		onQCCreated := func(qc *flow.QuorumCertificate) {
   698  			// QC should be created only once
   699  			if !qcCreated.CompareAndSwap(false, true) {
   700  				t.Fatalf("QC created more than once")
   701  			}
   702  		}
   703  
   704  		processor := &CombinedVoteProcessorV2{
   705  			log:               unittest.Logger(),
   706  			block:             block,
   707  			stakingSigAggtor:  stakingAggregator,
   708  			rbRector:          reconstructor,
   709  			onQCCreated:       onQCCreated,
   710  			packer:            pcker,
   711  			minRequiredWeight: minRequiredWeight,
   712  			done:              *atomic.NewBool(false),
   713  		}
   714  
   715  		votes := make([]*model.Vote, 0, stakingSignersCount+beaconSignersCount)
   716  
   717  		expectStakingAggregatorCalls := func(vote *model.Vote, weight uint64) {
   718  			expectedSig := crypto.Signature(vote.SigData[:msig.SigLen])
   719  			stakingAggregator.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe()
   720  			stakingAggregator.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) {
   721  				stakingTotalWeight.Add(weight)
   722  			}).Return(uint64(0), nil).Maybe()
   723  		}
   724  
   725  		// prepare votes
   726  		for _, signer := range stakingSigners {
   727  			vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithStakingSig())
   728  			vote.SignerID = signer.ID()
   729  			expectStakingAggregatorCalls(vote, signer.InitialWeight)
   730  			votes = append(votes, vote)
   731  		}
   732  		for _, signer := range beaconSigners {
   733  			vote := unittest.VoteForBlockFixture(processor.Block(), VoteWithDoubleSig())
   734  			vote.SignerID = signer.ID()
   735  			expectStakingAggregatorCalls(vote, signer.InitialWeight)
   736  			expectedSig := crypto.Signature(vote.SigData[msig.SigLen:])
   737  			reconstructor.On("Verify", vote.SignerID, expectedSig).Return(nil).Maybe()
   738  			reconstructor.On("TrustedAdd", vote.SignerID, expectedSig).Run(func(args mock.Arguments) {
   739  				collectedShares.Inc()
   740  			}).Return(true, nil).Maybe()
   741  			votes = append(votes, vote)
   742  		}
   743  
   744  		// shuffle votes in random order
   745  		rand.Shuffle(len(votes), func(i, j int) {
   746  			votes[i], votes[j] = votes[j], votes[i]
   747  		})
   748  
   749  		var startProcessing, finishProcessing sync.WaitGroup
   750  		startProcessing.Add(1)
   751  		// process votes concurrently by multiple workers
   752  		for _, vote := range votes {
   753  			finishProcessing.Add(1)
   754  			go func(vote *model.Vote) {
   755  				defer finishProcessing.Done()
   756  				startProcessing.Wait()
   757  				err := processor.Process(vote)
   758  				require.NoError(t, err)
   759  			}(vote)
   760  		}
   761  
   762  		// start all goroutines at the same time
   763  		startProcessing.Done()
   764  		finishProcessing.Wait()
   765  
   766  		passed := processor.done.Load()
   767  		passed = passed && qcCreated.Load()
   768  		passed = passed && stakingAggregator.AssertExpectations(t)
   769  		passed = passed && reconstructor.AssertExpectations(t)
   770  
   771  		if !passed {
   772  			t.Fatalf("Assertions weren't met, staking weight: %v, collected shares: %v", stakingTotalWeight.Load(), collectedShares.Load())
   773  		}
   774  	})
   775  }
   776  
   777  // TestCombinedVoteProcessorV2_BuildVerifyQC tests a complete path from creating votes to collecting votes and then
   778  // building & verifying QC.
   779  // We start with leader proposing a block, then new leader collects votes and builds a QC.
   780  // Need to verify that QC that was produced is valid and can be embedded in new proposal.
   781  func TestCombinedVoteProcessorV2_BuildVerifyQC(t *testing.T) {
   782  	epochCounter := uint64(3)
   783  	epochLookup := &modulemock.EpochLookup{}
   784  	view := uint64(20)
   785  	epochLookup.On("EpochForViewWithFallback", view).Return(epochCounter, nil)
   786  
   787  	// all committee members run DKG
   788  	dkgData, err := bootstrapDKG.RandomBeaconKG(11, unittest.RandomBytes(32))
   789  	require.NoError(t, err)
   790  
   791  	// signers hold objects that are created with private key and can sign votes and proposals
   792  	signers := make(map[flow.Identifier]*verification.CombinedSigner)
   793  
   794  	// prepare staking signers, each signer has it's own private/public key pair
   795  	// stakingSigners sign only with staking key, meaning they have failed DKG
   796  	stakingSigners := unittest.IdentityListFixture(3)
   797  	beaconSigners := unittest.IdentityListFixture(8)
   798  	allIdentities := append(stakingSigners, beaconSigners...)
   799  	require.Equal(t, len(dkgData.PubKeyShares), len(allIdentities))
   800  	dkgParticipants := make(map[flow.Identifier]flow.DKGParticipant)
   801  	// fill dkg participants data
   802  	for index, identity := range allIdentities {
   803  		dkgParticipants[identity.NodeID] = flow.DKGParticipant{
   804  			Index:    uint(index),
   805  			KeyShare: dkgData.PubKeyShares[index],
   806  		}
   807  	}
   808  
   809  	for _, identity := range stakingSigners {
   810  		stakingPriv := unittest.StakingPrivKeyFixture()
   811  		identity.StakingPubKey = stakingPriv.PublicKey()
   812  
   813  		keys := &storagemock.SafeBeaconKeys{}
   814  		// there is no DKG key for this epoch
   815  		keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(nil, false, nil)
   816  
   817  		beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys)
   818  
   819  		me, err := local.New(identity.IdentitySkeleton, stakingPriv)
   820  		require.NoError(t, err)
   821  
   822  		signers[identity.NodeID] = verification.NewCombinedSigner(me, beaconSignerStore)
   823  	}
   824  
   825  	for _, identity := range beaconSigners {
   826  		stakingPriv := unittest.StakingPrivKeyFixture()
   827  		identity.StakingPubKey = stakingPriv.PublicKey()
   828  
   829  		participantData := dkgParticipants[identity.NodeID]
   830  
   831  		dkgKey := encodable.RandomBeaconPrivKey{
   832  			PrivateKey: dkgData.PrivKeyShares[participantData.Index],
   833  		}
   834  
   835  		keys := &storagemock.SafeBeaconKeys{}
   836  		// there is DKG key for this epoch
   837  		keys.On("RetrieveMyBeaconPrivateKey", epochCounter).Return(dkgKey, true, nil)
   838  
   839  		beaconSignerStore := hsig.NewEpochAwareRandomBeaconKeyStore(epochLookup, keys)
   840  
   841  		me, err := local.New(identity.IdentitySkeleton, stakingPriv)
   842  		require.NoError(t, err)
   843  
   844  		signers[identity.NodeID] = verification.NewCombinedSigner(me, beaconSignerStore)
   845  	}
   846  
   847  	leader := stakingSigners[0]
   848  
   849  	block := helper.MakeBlock(helper.WithBlockView(view),
   850  		helper.WithBlockProposer(leader.NodeID))
   851  
   852  	inmemDKG, err := inmem.DKGFromEncodable(inmem.EncodableDKG{
   853  		GroupKey: encodable.RandomBeaconPubKey{
   854  			PublicKey: dkgData.PubGroupKey,
   855  		},
   856  		Participants: dkgParticipants,
   857  	})
   858  	require.NoError(t, err)
   859  
   860  	committee := &mockhotstuff.DynamicCommittee{}
   861  	committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(allIdentities.ToSkeleton().TotalWeight()), nil)
   862  	committee.On("IdentitiesByEpoch", block.View).Return(allIdentities.ToSkeleton(), nil)
   863  	committee.On("IdentitiesByBlock", block.BlockID).Return(allIdentities, nil)
   864  	committee.On("DKG", block.View).Return(inmemDKG, nil)
   865  
   866  	votes := make([]*model.Vote, 0, len(allIdentities))
   867  
   868  	// first staking signer will be leader collecting votes for proposal
   869  	// prepare votes for every member of committee except leader
   870  	for _, signer := range allIdentities[1:] {
   871  		vote, err := signers[signer.NodeID].CreateVote(block)
   872  		require.NoError(t, err)
   873  		votes = append(votes, vote)
   874  	}
   875  
   876  	// create and sign proposal
   877  	proposal, err := signers[leader.NodeID].CreateProposal(block)
   878  	require.NoError(t, err)
   879  
   880  	qcCreated := false
   881  	onQCCreated := func(qc *flow.QuorumCertificate) {
   882  		packer := hsig.NewConsensusSigDataPacker(committee)
   883  
   884  		// create verifier that will do crypto checks of created QC
   885  		verifier := verification.NewCombinedVerifier(committee, packer)
   886  		// create validator which will do compliance and crypto checked of created QC
   887  		validator := hotstuffvalidator.New(committee, verifier)
   888  		// check if QC is valid against parent
   889  		err := validator.ValidateQC(qc)
   890  		require.NoError(t, err)
   891  
   892  		qcCreated = true
   893  	}
   894  
   895  	voteProcessorFactory := NewCombinedVoteProcessorFactory(committee, onQCCreated)
   896  	voteProcessor, err := voteProcessorFactory.Create(unittest.Logger(), proposal)
   897  	require.NoError(t, err)
   898  
   899  	// process votes by new leader, this will result in producing new QC
   900  	for _, vote := range votes {
   901  		err := voteProcessor.Process(vote)
   902  		require.NoError(t, err)
   903  	}
   904  
   905  	require.True(t, qcCreated)
   906  }
   907  
   908  func VoteWithStakingSig() func(*model.Vote) {
   909  	return func(vote *model.Vote) {
   910  		vote.SigData = unittest.RandomBytes(msig.SigLen)
   911  	}
   912  }
   913  
   914  func VoteWithDoubleSig() func(*model.Vote) {
   915  	return func(vote *model.Vote) {
   916  		vote.SigData = unittest.RandomBytes(96)
   917  	}
   918  }
   919  
   920  // TestReadRandomSourceFromPackedQCV2 tests that a constructed QC can be unpacked and from which the random source can be
   921  // retrieved
   922  func TestReadRandomSourceFromPackedQCV2(t *testing.T) {
   923  
   924  	// for V2 there is no aggregated random beacon sig or random beacon signers
   925  	allSigners := unittest.IdentityListFixture(5)
   926  	// staking signers are only subset of all signers
   927  	stakingSigners := allSigners.NodeIDs()[:3]
   928  
   929  	aggregatedStakingSig := unittest.SignatureFixture()
   930  	reconstructedBeaconSig := unittest.SignatureFixture()
   931  
   932  	blockSigData := buildBlockSignatureDataForV2(stakingSigners, aggregatedStakingSig, reconstructedBeaconSig)
   933  
   934  	// making a mock block
   935  	header := unittest.BlockHeaderFixture()
   936  	block := model.BlockFromFlow(header)
   937  
   938  	// create a packer
   939  	committee := &mockhotstuff.DynamicCommittee{}
   940  	committee.On("IdentitiesByBlock", block.BlockID).Return(allSigners, nil)
   941  	committee.On("IdentitiesByEpoch", block.View).Return(allSigners.ToSkeleton(), nil)
   942  	packer := signature.NewConsensusSigDataPacker(committee)
   943  
   944  	qc, err := buildQCWithPackerAndSigData(packer, block, blockSigData)
   945  	require.NoError(t, err)
   946  
   947  	randomSource, err := model.BeaconSignature(qc)
   948  	require.NoError(t, err)
   949  
   950  	randomSourceAgain, err := model.BeaconSignature(qc)
   951  	require.NoError(t, err)
   952  
   953  	// verify the random source is deterministic
   954  	require.Equal(t, randomSource, randomSourceAgain)
   955  }