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

     1  package validator
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math/rand"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/koko1123/flow-go-1/module/signature"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/mock"
    14  	"github.com/stretchr/testify/require"
    15  	"github.com/stretchr/testify/suite"
    16  
    17  	"github.com/koko1123/flow-go-1/consensus/hotstuff/helper"
    18  	"github.com/koko1123/flow-go-1/consensus/hotstuff/mocks"
    19  	"github.com/koko1123/flow-go-1/consensus/hotstuff/model"
    20  	"github.com/koko1123/flow-go-1/model/flow"
    21  	"github.com/koko1123/flow-go-1/model/flow/filter"
    22  	"github.com/koko1123/flow-go-1/utils/unittest"
    23  )
    24  
    25  func TestValidateProposal(t *testing.T) {
    26  	suite.Run(t, new(ProposalSuite))
    27  }
    28  
    29  type ProposalSuite struct {
    30  	suite.Suite
    31  	participants flow.IdentityList
    32  	leader       *flow.Identity
    33  	finalized    uint64
    34  	parent       *model.Block
    35  	block        *model.Block
    36  	voters       flow.IdentityList
    37  	proposal     *model.Proposal
    38  	vote         *model.Vote
    39  	voter        *flow.Identity
    40  	committee    *mocks.Committee
    41  	forks        *mocks.Forks
    42  	verifier     *mocks.Verifier
    43  	validator    *Validator
    44  }
    45  
    46  func (ps *ProposalSuite) SetupTest() {
    47  	// the leader is a random node for now
    48  	rand.Seed(time.Now().UnixNano())
    49  	ps.finalized = uint64(rand.Uint32() + 1)
    50  	ps.participants = unittest.IdentityListFixture(8, unittest.WithRole(flow.RoleConsensus))
    51  	ps.leader = ps.participants[0]
    52  
    53  	// the parent is the last finalized block, followed directly by a block from the leader
    54  	ps.parent = helper.MakeBlock(
    55  		helper.WithBlockView(ps.finalized),
    56  	)
    57  
    58  	indices, err := signature.EncodeSignersToIndices(ps.participants.NodeIDs(), ps.participants.NodeIDs())
    59  	require.NoError(ps.T(), err)
    60  
    61  	ps.block = helper.MakeBlock(
    62  		helper.WithBlockView(ps.finalized+1),
    63  		helper.WithBlockProposer(ps.leader.NodeID),
    64  		helper.WithParentBlock(ps.parent),
    65  		helper.WithParentSigners(indices),
    66  	)
    67  
    68  	voterIDs, err := signature.DecodeSignerIndicesToIdentifiers(ps.participants.NodeIDs(), ps.block.QC.SignerIndices)
    69  	require.NoError(ps.T(), err)
    70  
    71  	ps.voters = ps.participants.Filter(filter.HasNodeID(voterIDs...))
    72  	ps.proposal = &model.Proposal{Block: ps.block}
    73  	ps.vote = ps.proposal.ProposerVote()
    74  	ps.voter = ps.leader
    75  
    76  	// set up the mocked hotstuff Committee state
    77  	ps.committee = &mocks.Committee{}
    78  	ps.committee.On("LeaderForView", ps.block.View).Return(ps.leader.NodeID, nil)
    79  	ps.committee.On("Identities", mock.Anything).Return(
    80  		func(blockID flow.Identifier) flow.IdentityList {
    81  			return ps.participants
    82  		},
    83  		nil,
    84  	)
    85  	for _, participant := range ps.participants {
    86  		ps.committee.On("Identity", mock.Anything, participant.NodeID).Return(participant, nil)
    87  	}
    88  
    89  	// the finalized view is the one of the parent of the
    90  	ps.forks = &mocks.Forks{}
    91  	ps.forks.On("FinalizedView").Return(ps.finalized)
    92  	ps.forks.On("GetBlock", ps.parent.BlockID).Return(ps.parent, true)
    93  	ps.forks.On("GetBlock", ps.block.BlockID).Return(ps.block, true)
    94  
    95  	// set up the mocked verifier
    96  	ps.verifier = &mocks.Verifier{}
    97  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(nil)
    98  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
    99  
   100  	// set up the validator with the mocked dependencies
   101  	ps.validator = New(ps.committee, ps.forks, ps.verifier)
   102  }
   103  
   104  func (ps *ProposalSuite) TestProposalOK() {
   105  
   106  	err := ps.validator.ValidateProposal(ps.proposal)
   107  	assert.NoError(ps.T(), err, "a valid proposal should be accepted")
   108  }
   109  
   110  func (ps *ProposalSuite) TestProposalSignatureError() {
   111  
   112  	// change the verifier to error on signature validation with unspecific error
   113  	*ps.verifier = mocks.Verifier{}
   114  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(nil)
   115  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(errors.New("dummy error"))
   116  
   117  	// check that validation now fails
   118  	err := ps.validator.ValidateProposal(ps.proposal)
   119  	assert.Error(ps.T(), err, "a proposal should be rejected if signature check fails")
   120  
   121  	// check that the error is not one that leads to invalid
   122  	assert.False(ps.T(), model.IsInvalidBlockError(err), "if signature check fails, we should not receive an ErrorInvalidBlock")
   123  }
   124  
   125  func (ps *ProposalSuite) TestProposalSignatureInvalidFormat() {
   126  
   127  	// change the verifier to fail signature validation with InvalidFormatError error
   128  	*ps.verifier = mocks.Verifier{}
   129  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(nil)
   130  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(model.NewInvalidFormatErrorf(""))
   131  
   132  	// check that validation now fails
   133  	err := ps.validator.ValidateProposal(ps.proposal)
   134  	assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
   135  
   136  	// check that the error is an invalid proposal error to allow creating slashing challenge
   137  	assert.True(ps.T(), model.IsInvalidBlockError(err), "if signature is invalid, we should generate an invalid error")
   138  }
   139  
   140  func (ps *ProposalSuite) TestProposalSignatureInvalid() {
   141  
   142  	// change the verifier to fail signature validation
   143  	*ps.verifier = mocks.Verifier{}
   144  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(nil)
   145  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(model.ErrInvalidSignature)
   146  
   147  	// check that validation now fails
   148  	err := ps.validator.ValidateProposal(ps.proposal)
   149  	assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
   150  
   151  	// check that the error is an invalid proposal error to allow creating slashing challenge
   152  	assert.True(ps.T(), model.IsInvalidBlockError(err), "if signature is invalid, we should generate an invalid error")
   153  }
   154  
   155  func (ps *ProposalSuite) TestProposalWrongLeader() {
   156  
   157  	// change the hotstuff.Committee to return a different leader
   158  	*ps.committee = mocks.Committee{}
   159  	ps.committee.On("LeaderForView", ps.block.View).Return(ps.participants[1].NodeID, nil)
   160  	for _, participant := range ps.participants {
   161  		ps.committee.On("Identity", mock.Anything, participant.NodeID).Return(participant, nil)
   162  	}
   163  
   164  	// check that validation fails now
   165  	err := ps.validator.ValidateProposal(ps.proposal)
   166  	assert.Error(ps.T(), err, "a proposal from the wrong proposer should be rejected")
   167  
   168  	// check that the error is an invalid proposal error to allow creating slashing challenge
   169  	assert.True(ps.T(), model.IsInvalidBlockError(err), "if the proposal has wrong proposer, we should generate a invalid error")
   170  }
   171  
   172  func (ps *ProposalSuite) TestProposalMismatchingView() {
   173  
   174  	// change the QC's view to be different from the parent
   175  	ps.proposal.Block.QC.View++
   176  
   177  	// check that validation fails now
   178  	err := ps.validator.ValidateProposal(ps.proposal)
   179  	assert.Error(ps.T(), err, "a proposal with a mismatching QC view should be rejected")
   180  
   181  	// check that the error is an invalid proposal error to allow creating slashing challenge
   182  	assert.True(ps.T(), model.IsInvalidBlockError(err), "if the QC has a mismatching view, we should generate a invalid error")
   183  }
   184  
   185  func (ps *ProposalSuite) TestProposalMissingParentHigher() {
   186  
   187  	// change forks to not find the parent
   188  	ps.block.QC.View = ps.finalized
   189  	*ps.forks = mocks.Forks{}
   190  	ps.forks.On("FinalizedView").Return(ps.finalized)
   191  	ps.forks.On("GetBlock", ps.block.QC.BlockID).Return(nil, false)
   192  
   193  	// check that validation fails now
   194  	err := ps.validator.ValidateProposal(ps.proposal)
   195  	assert.Error(ps.T(), err, "a proposal with a missing parent should be rejected")
   196  
   197  	// check that the error is a missing block error because we should have the block but we don't
   198  	assert.True(ps.T(), model.IsMissingBlockError(err), "if we don't have the proposal parent for a QC above or equal finalized view, we should generate an missing block error")
   199  }
   200  
   201  func (ps *ProposalSuite) TestProposalMissingParentLower() {
   202  
   203  	// change forks to not find the parent
   204  	ps.block.QC.View = ps.finalized - 1
   205  	*ps.forks = mocks.Forks{}
   206  	ps.forks.On("FinalizedView").Return(ps.finalized)
   207  	ps.forks.On("GetBlock", ps.block.QC.BlockID).Return(nil, false)
   208  
   209  	// check that validation fails now
   210  	err := ps.validator.ValidateProposal(ps.proposal)
   211  	assert.Error(ps.T(), err, "a proposal with a missing parent should be rejected")
   212  
   213  	// check that the error is an unverifiable block because we can't verify the block
   214  	assert.True(ps.T(), errors.Is(err, model.ErrUnverifiableBlock), "if we don't have the proposal parent for a QC below finalized view, we should generate an unverifiable block error")
   215  }
   216  
   217  // TestProposalQCInvalid checks that Validator handles the verifier's error returns correctly.
   218  // In case of `model.InvalidFormatError` and model.ErrInvalidSignature`, we expect the Validator
   219  // to recognize those as an invalid QC, i.e. returns an `model.InvalidBlockError`.
   220  // In contrast, unexpected exceptions and `model.InvalidSignerError` should _not_ be
   221  // interpreted as a sign of an invalid QC.
   222  func (ps *ProposalSuite) TestProposalQCInvalid() {
   223  	ps.Run("invalid signature", func() {
   224  		*ps.verifier = mocks.Verifier{}
   225  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(
   226  			fmt.Errorf("invalid qc: %w", model.ErrInvalidSignature))
   227  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
   228  
   229  		// check that validation fails and the failure case is recognized as an invalid block
   230  		err := ps.validator.ValidateProposal(ps.proposal)
   231  		assert.True(ps.T(), model.IsInvalidBlockError(err), "if the block's QC signature is invalid, an ErrorInvalidBlock error should be raised")
   232  	})
   233  
   234  	ps.Run("invalid format", func() {
   235  		*ps.verifier = mocks.Verifier{}
   236  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(model.NewInvalidFormatErrorf("invalid qc"))
   237  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
   238  
   239  		// check that validation fails and the failure case is recognized as an invalid block
   240  		err := ps.validator.ValidateProposal(ps.proposal)
   241  		assert.True(ps.T(), model.IsInvalidBlockError(err), "if the block's QC has an invalid format, an ErrorInvalidBlock error should be raised")
   242  	})
   243  
   244  	// Theoretically, `VerifyQC` could also return a `model.InvalidSignerError`. However,
   245  	// for the time being, we assume that _every_ HotStuff participant is also a member of
   246  	// the random beacon committee. Consequently, `InvalidSignerError` should not occur atm.
   247  	// TODO: if the random beacon committee is a strict subset of the HotStuff committee,
   248  	//       we expect `model.InvalidSignerError` here during normal operations.
   249  	ps.Run("invalid signer", func() {
   250  		*ps.verifier = mocks.Verifier{}
   251  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(
   252  			fmt.Errorf("invalid qc: %w", model.NewInvalidSignerErrorf("")))
   253  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
   254  
   255  		// check that validation fails and the failure case is recognized as an invalid block
   256  		err := ps.validator.ValidateProposal(ps.proposal)
   257  		assert.Error(ps.T(), err)
   258  		assert.False(ps.T(), model.IsInvalidBlockError(err))
   259  	})
   260  
   261  	ps.Run("unknown exception", func() {
   262  		exception := errors.New("exception")
   263  		*ps.verifier = mocks.Verifier{}
   264  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(exception)
   265  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
   266  
   267  		// check that validation fails and the failure case is recognized as an invalid block
   268  		err := ps.validator.ValidateProposal(ps.proposal)
   269  		assert.ErrorIs(ps.T(), err, exception)
   270  		assert.False(ps.T(), model.IsInvalidBlockError(err))
   271  	})
   272  }
   273  
   274  func (ps *ProposalSuite) TestProposalQCError() {
   275  
   276  	// change verifier to fail on QC validation
   277  	*ps.verifier = mocks.Verifier{}
   278  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent).Return(fmt.Errorf("some exception"))
   279  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block).Return(nil)
   280  
   281  	// check that validation fails now
   282  	err := ps.validator.ValidateProposal(ps.proposal)
   283  	assert.Error(ps.T(), err, "a proposal with an invalid QC should be rejected")
   284  
   285  	// check that the error is an invalid proposal error to allow creating slashing challenge
   286  	assert.False(ps.T(), model.IsInvalidBlockError(err), "if we can't verify the QC, we should not generate a invalid error")
   287  }
   288  
   289  func TestValidateVote(t *testing.T) {
   290  	suite.Run(t, new(VoteSuite))
   291  }
   292  
   293  type VoteSuite struct {
   294  	suite.Suite
   295  	signer    *flow.Identity
   296  	block     *model.Block
   297  	vote      *model.Vote
   298  	forks     *mocks.Forks
   299  	verifier  *mocks.Verifier
   300  	committee *mocks.Committee
   301  	validator *Validator
   302  }
   303  
   304  func (vs *VoteSuite) SetupTest() {
   305  
   306  	// create a random signing identity
   307  	vs.signer = unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus))
   308  
   309  	// create a block that should be signed
   310  	vs.block = helper.MakeBlock()
   311  
   312  	// create a vote for this block
   313  	vs.vote = &model.Vote{
   314  		View:     vs.block.View,
   315  		BlockID:  vs.block.BlockID,
   316  		SignerID: vs.signer.NodeID,
   317  		SigData:  []byte{},
   318  	}
   319  
   320  	// set up the mocked forks
   321  	vs.forks = &mocks.Forks{}
   322  	vs.forks.On("GetBlock", vs.block.BlockID).Return(vs.block, true)
   323  
   324  	// set up the mocked verifier
   325  	vs.verifier = &mocks.Verifier{}
   326  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block).Return(nil)
   327  
   328  	// the leader for the block view is the correct one
   329  	vs.committee = &mocks.Committee{}
   330  	vs.committee.On("Identity", mock.Anything, vs.signer.NodeID).Return(vs.signer, nil)
   331  
   332  	// set up the validator with the mocked dependencies
   333  	vs.validator = New(vs.committee, vs.forks, vs.verifier)
   334  }
   335  
   336  // TestVoteOK checks the happy case, which is the default for the suite
   337  func (vs *VoteSuite) TestVoteOK() {
   338  	_, err := vs.validator.ValidateVote(vs.vote, vs.block)
   339  	assert.NoError(vs.T(), err, "a valid vote should be accepted")
   340  }
   341  
   342  // TestVoteMismatchingView checks that the Validator handles the case where the
   343  // vote contains a mismatching `View` value. In this case, the vote is invalid.
   344  // Hence, we expect the Validator to return a `model.InvalidVoteError`.
   345  func (vs *VoteSuite) TestVoteMismatchingView() {
   346  	vs.vote.View++
   347  
   348  	// check that the vote is no longer validated
   349  	_, err := vs.validator.ValidateVote(vs.vote, vs.block)
   350  	assert.Error(vs.T(), err, "a vote with a mismatching view should be rejected")
   351  
   352  	// TODO: this should raise an error that allows a slashing challenge
   353  	assert.True(vs.T(), model.IsInvalidVoteError(err), "a mismatching view should create a invalid vote error")
   354  }
   355  
   356  // TestVoteSignatureError checks that the Validator does not misinterpret
   357  // unexpected exceptions for invalid votes.
   358  func (vs *VoteSuite) TestVoteSignatureError() {
   359  	*vs.verifier = mocks.Verifier{}
   360  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block).Return(fmt.Errorf("some exception"))
   361  
   362  	// check that the vote is no longer validated
   363  	_, err := vs.validator.ValidateVote(vs.vote, vs.block)
   364  	assert.Error(vs.T(), err, "a vote with error on signature validation should be rejected")
   365  	assert.False(vs.T(), model.IsInvalidVoteError(err), "internal exception should not be interpreted as invalid vote")
   366  }
   367  
   368  // TestVoteInvalidSignerID checks that the Validator correctly handles a vote
   369  // with a SignerID that does not correspond to a valid consensus participant.
   370  // In this case, the `hotstuff.Committee` returns a `model.InvalidSignerError`,
   371  // which the Validator should recognize as a symptom for an invalid vote.
   372  // Hence, we expect the validator to return a `model.InvalidVoteError`.
   373  func (vs *VoteSuite) TestVoteInvalidSignerID() {
   374  	*vs.committee = mocks.Committee{}
   375  	vs.committee.On("Identity", vs.block.BlockID, vs.vote.SignerID).Return(nil, model.NewInvalidSignerErrorf(""))
   376  
   377  	// A `model.InvalidSignerError` from the committee should be interpreted as
   378  	// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
   379  	_, err := vs.validator.ValidateVote(vs.vote, vs.block)
   380  	assert.Error(vs.T(), err, "a vote with unknown SignerID should be rejected")
   381  	assert.True(vs.T(), model.IsInvalidVoteError(err), "a vote with unknown SignerID should be rejected")
   382  }
   383  
   384  // TestVoteSignatureInvalid checks that the Validator correctly handles votes
   385  // with cryptographically invalid signature. In this case, the `hotstuff.Verifier`
   386  // returns a `model.ErrInvalidSignature`, which the Validator should recognize as
   387  // a symptom for an invalid vote.
   388  // Hence, we expect the validator to return a `model.InvalidVoteError`.
   389  func (vs *VoteSuite) TestVoteSignatureInvalid() {
   390  	*vs.verifier = mocks.Verifier{}
   391  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block).Return(fmt.Errorf("staking sig is invalid: %w", model.ErrInvalidSignature))
   392  
   393  	// A `model.ErrInvalidSignature` from the `hotstuff.Verifier` should be interpreted as
   394  	// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
   395  	_, err := vs.validator.ValidateVote(vs.vote, vs.block)
   396  	assert.Error(vs.T(), err, "a vote with an invalid signature should be rejected")
   397  	assert.True(vs.T(), model.IsInvalidVoteError(err), "a vote with an invalid signature should be rejected")
   398  }
   399  
   400  func TestValidateQC(t *testing.T) {
   401  	suite.Run(t, new(QCSuite))
   402  }
   403  
   404  type QCSuite struct {
   405  	suite.Suite
   406  	participants flow.IdentityList
   407  	signers      flow.IdentityList
   408  	block        *model.Block
   409  	qc           *flow.QuorumCertificate
   410  	committee    *mocks.Committee
   411  	verifier     *mocks.Verifier
   412  	validator    *Validator
   413  }
   414  
   415  func (qs *QCSuite) SetupTest() {
   416  	// create a list of 10 nodes with 1-weight each
   417  	qs.participants = unittest.IdentityListFixture(10,
   418  		unittest.WithRole(flow.RoleConsensus),
   419  		unittest.WithWeight(1),
   420  	)
   421  
   422  	// signers are a qualified majority at 7
   423  	qs.signers = qs.participants[:7]
   424  
   425  	// create a block that has the signers in its QC
   426  	qs.block = helper.MakeBlock()
   427  	indices, err := signature.EncodeSignersToIndices(qs.participants.NodeIDs(), qs.signers.NodeIDs())
   428  	require.NoError(qs.T(), err)
   429  
   430  	qs.qc = helper.MakeQC(helper.WithQCBlock(qs.block), helper.WithQCSigners(indices))
   431  
   432  	// return the correct participants and identities from view state
   433  	qs.committee = &mocks.Committee{}
   434  	qs.committee.On("Identities", mock.Anything).Return(
   435  		func(blockID flow.Identifier) flow.IdentityList {
   436  			return qs.participants
   437  		},
   438  		nil,
   439  	)
   440  
   441  	// set up the mocked verifier to verify the QC correctly
   442  	qs.verifier = &mocks.Verifier{}
   443  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.block).Return(nil)
   444  
   445  	// set up the validator with the mocked dependencies
   446  	qs.validator = New(qs.committee, nil, qs.verifier)
   447  }
   448  
   449  // TestQCOK verifies the default happy case
   450  func (qs *QCSuite) TestQCOK() {
   451  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   452  	assert.NoError(qs.T(), err, "a valid QC should be accepted")
   453  }
   454  
   455  // TestQCRetrievingParticipantsError tests that validation errors if:
   456  // there is an error retrieving identities of consensus participants
   457  func (qs *QCSuite) TestQCRetrievingParticipantsError() {
   458  	// change the hotstuff.Committee to fail on retrieving participants
   459  	*qs.committee = mocks.Committee{}
   460  	qs.committee.On("Identities", mock.Anything, mock.Anything).Return(qs.participants, errors.New("FATAL internal error"))
   461  
   462  	// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidBlock
   463  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   464  	assert.Error(qs.T(), err, "unspecific error when retrieving consensus participants should be escalated to surrounding logic")
   465  	assert.False(qs.T(), model.IsInvalidBlockError(err), "unspecific internal errors should not result in ErrorInvalidBlock error")
   466  }
   467  
   468  // TestQCSignersError tests that a qc fails validation if:
   469  // QC signer's have insufficient weight (but are all valid consensus participants otherwise)
   470  func (qs *QCSuite) TestQCInsufficientWeight() {
   471  	// signers only have weight 6 out of 10 total (NOT have a supermajority)
   472  	qs.signers = qs.participants[:6]
   473  	indices, err := signature.EncodeSignersToIndices(qs.participants.NodeIDs(), qs.signers.NodeIDs())
   474  	require.NoError(qs.T(), err)
   475  
   476  	qs.qc = helper.MakeQC(helper.WithQCBlock(qs.block), helper.WithQCSigners(indices))
   477  
   478  	// the QC should not be validated anymore
   479  	err = qs.validator.ValidateQC(qs.qc, qs.block)
   480  	assert.Error(qs.T(), err, "a QC should be rejected if it has insufficient voted weight")
   481  
   482  	// we should get a threshold error to bubble up for extra info
   483  	assert.True(qs.T(), model.IsInvalidBlockError(err), "if there is insufficient voted weight, an invalid block error should be raised")
   484  }
   485  
   486  // TestQCSignatureError tests that validation errors if:
   487  // there is an unspecific internal error while validating the signature
   488  func (qs *QCSuite) TestQCSignatureError() {
   489  
   490  	// set up the verifier to fail QC verification
   491  	*qs.verifier = mocks.Verifier{}
   492  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.block).Return(errors.New("dummy error"))
   493  
   494  	// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidBlock
   495  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   496  	assert.Error(qs.T(), err, "unspecific sig verification error should be escalated to surrounding logic")
   497  	assert.False(qs.T(), model.IsInvalidBlockError(err), "unspecific internal errors should not result in ErrorInvalidBlock error")
   498  }
   499  
   500  // TestQCSignatureInvalid verifies that the Validator correctly handles the model.ErrInvalidSignature.
   501  // This error return from `Verifier.VerifyQC` is an expected failure case in case of a byzantine input, where
   502  // one of the signatures in the QC is broken. Hence, the Validator should wrap it as InvalidBlockError.
   503  func (qs *QCSuite) TestQCSignatureInvalid() {
   504  	// change the verifier to fail the QC signature
   505  	*qs.verifier = mocks.Verifier{}
   506  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.block).Return(
   507  		fmt.Errorf("invalid signer sig: %w", model.ErrInvalidSignature))
   508  
   509  	// the QC be considered as invalid
   510  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   511  	assert.True(qs.T(), model.IsInvalidBlockError(err), "if the signature is invalid an ErrorInvalidBlock error should be raised")
   512  }
   513  
   514  // TestQCSignatureInvalidFormat verifies that the Validator correctly handles the model.InvalidFormatError.
   515  // This error return from `Verifier.VerifyQC` is an expected failure case in case of a byzantine input, where
   516  // some binary vector (e.g. `sigData`) is broken. Hence, the Validator should wrap it as InvalidBlockError.
   517  func (qs *QCSuite) TestQCSignatureInvalidFormat() {
   518  	// change the verifier to fail the QC signature
   519  	*qs.verifier = mocks.Verifier{}
   520  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.block).Return(
   521  		fmt.Errorf("%w", model.NewInvalidFormatErrorf("invalid sigType")))
   522  
   523  	// the QC be considered as invalid
   524  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   525  	assert.True(qs.T(), model.IsInvalidBlockError(err), "if the signature has an invalid format, an ErrorInvalidBlock error should be raised")
   526  }
   527  
   528  // TestQCEmptySigners verifies that the Validator correctly handles the model.InsufficientSignaturesError:
   529  // In the validator, we previously checked the total weight of all signers meets the supermajority threshold,
   530  // which is a _positive_ number. Hence, there must be at least one signer. Hence, `Verifier.VerifyQC`
   531  // returning this error would be a symptom of a fatal internal bug. The Validator should _not_ interpret
   532  // this error as an invalid QC / invalid block, i.e. it should _not_ return an `InvalidBlockError`.
   533  func (qs *QCSuite) TestQCEmptySigners() {
   534  	*qs.verifier = mocks.Verifier{}
   535  	qs.verifier.On("VerifyQC", mock.Anything, qs.qc.SigData, qs.block).Return(
   536  		fmt.Errorf("%w", model.NewInsufficientSignaturesErrorf("")))
   537  
   538  	// the Validator should _not_ interpret this as a invalid QC, but as an internal error
   539  	err := qs.validator.ValidateQC(qs.qc, qs.block)
   540  	assert.True(qs.T(), model.IsInsufficientSignaturesError(err)) // unexpected error should be wrapped and propagated upwards
   541  	assert.False(qs.T(), model.IsInvalidBlockError(err), err, "should _not_ interpret this as a invalid QC, but as an internal error")
   542  }