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

     1  package validator
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math/rand"
     7  	"testing"
     8  
     9  	"github.com/onflow/flow-go/module/signature"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	"github.com/stretchr/testify/require"
    14  	"github.com/stretchr/testify/suite"
    15  
    16  	"github.com/onflow/flow-go/consensus/hotstuff/committees"
    17  	"github.com/onflow/flow-go/consensus/hotstuff/helper"
    18  	"github.com/onflow/flow-go/consensus/hotstuff/mocks"
    19  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/model/flow/filter"
    22  	"github.com/onflow/flow-go/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  	indices      []byte
    33  	leader       *flow.IdentitySkeleton
    34  	finalized    uint64
    35  	parent       *model.Block
    36  	block        *model.Block
    37  	voters       flow.IdentitySkeletonList
    38  	proposal     *model.Proposal
    39  	vote         *model.Vote
    40  	voter        *flow.IdentitySkeleton
    41  	committee    *mocks.Replicas
    42  	verifier     *mocks.Verifier
    43  	validator    *Validator
    44  }
    45  
    46  func (ps *ProposalSuite) SetupTest() {
    47  	// the leader is a random node for now
    48  	ps.finalized = uint64(rand.Uint32() + 1)
    49  	ps.participants = unittest.IdentityListFixture(8, unittest.WithRole(flow.RoleConsensus)).Sort(flow.Canonical[flow.Identity])
    50  	ps.leader = &ps.participants[0].IdentitySkeleton
    51  
    52  	// the parent is the last finalized block, followed directly by a block from the leader
    53  	ps.parent = helper.MakeBlock(
    54  		helper.WithBlockView(ps.finalized),
    55  	)
    56  
    57  	var err error
    58  
    59  	ps.indices, err = signature.EncodeSignersToIndices(ps.participants.NodeIDs(), ps.participants.NodeIDs())
    60  	require.NoError(ps.T(), err)
    61  
    62  	ps.block = helper.MakeBlock(
    63  		helper.WithBlockView(ps.finalized+1),
    64  		helper.WithBlockProposer(ps.leader.NodeID),
    65  		helper.WithParentBlock(ps.parent),
    66  		helper.WithParentSigners(ps.indices),
    67  	)
    68  
    69  	voterIDs, err := signature.DecodeSignerIndicesToIdentifiers(ps.participants.NodeIDs(), ps.block.QC.SignerIndices)
    70  	require.NoError(ps.T(), err)
    71  
    72  	ps.voters = ps.participants.Filter(filter.HasNodeID[flow.Identity](voterIDs...)).ToSkeleton()
    73  	ps.proposal = &model.Proposal{Block: ps.block}
    74  	ps.vote = ps.proposal.ProposerVote()
    75  	ps.voter = ps.leader
    76  
    77  	// set up the mocked hotstuff Replicas state
    78  	ps.committee = &mocks.Replicas{}
    79  	ps.committee.On("LeaderForView", ps.block.View).Return(ps.leader.NodeID, nil)
    80  	ps.committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(ps.participants.ToSkeleton().TotalWeight()), nil)
    81  	ps.committee.On("IdentitiesByEpoch", mock.Anything).Return(
    82  		func(_ uint64) flow.IdentitySkeletonList {
    83  			return ps.participants.ToSkeleton()
    84  		},
    85  		nil,
    86  	)
    87  	for _, participant := range ps.participants {
    88  		ps.committee.On("IdentityByEpoch", mock.Anything, participant.NodeID).Return(&participant.IdentitySkeleton, nil)
    89  	}
    90  
    91  	// set up the mocked verifier
    92  	ps.verifier = &mocks.Verifier{}
    93  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(nil).Maybe()
    94  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil).Maybe()
    95  
    96  	// set up the validator with the mocked dependencies
    97  	ps.validator = New(ps.committee, ps.verifier)
    98  }
    99  
   100  func (ps *ProposalSuite) TestProposalOK() {
   101  	err := ps.validator.ValidateProposal(ps.proposal)
   102  	assert.NoError(ps.T(), err, "a valid proposal should be accepted")
   103  }
   104  
   105  func (ps *ProposalSuite) TestProposalSignatureError() {
   106  
   107  	// change the verifier to error on signature validation with unspecific error
   108  	*ps.verifier = mocks.Verifier{}
   109  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(nil)
   110  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(errors.New("dummy error"))
   111  
   112  	// check that validation now fails
   113  	err := ps.validator.ValidateProposal(ps.proposal)
   114  	assert.Error(ps.T(), err, "a proposal should be rejected if signature check fails")
   115  
   116  	// check that the error is not one that leads to invalid
   117  	assert.False(ps.T(), model.IsInvalidProposalError(err), "if signature check fails, we should not receive an ErrorInvalidBlock")
   118  }
   119  
   120  func (ps *ProposalSuite) TestProposalSignatureInvalidFormat() {
   121  
   122  	// change the verifier to fail signature validation with InvalidFormatError error
   123  	*ps.verifier = mocks.Verifier{}
   124  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(nil)
   125  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(model.NewInvalidFormatErrorf(""))
   126  
   127  	// check that validation now fails
   128  	err := ps.validator.ValidateProposal(ps.proposal)
   129  	assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
   130  
   131  	// check that the error is an invalid proposal error to allow creating slashing challenge
   132  	assert.True(ps.T(), model.IsInvalidProposalError(err), "if signature is invalid, we should generate an invalid error")
   133  }
   134  
   135  func (ps *ProposalSuite) TestProposalSignatureInvalid() {
   136  
   137  	// change the verifier to fail signature validation
   138  	*ps.verifier = mocks.Verifier{}
   139  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(nil)
   140  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(model.ErrInvalidSignature)
   141  
   142  	// check that validation now fails
   143  	err := ps.validator.ValidateProposal(ps.proposal)
   144  	assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
   145  
   146  	// check that the error is an invalid proposal error to allow creating slashing challenge
   147  	assert.True(ps.T(), model.IsInvalidProposalError(err), "if signature is invalid, we should generate an invalid error")
   148  }
   149  
   150  func (ps *ProposalSuite) TestProposalWrongLeader() {
   151  
   152  	// change the hotstuff.Replicas to return a different leader
   153  	*ps.committee = mocks.Replicas{}
   154  	ps.committee.On("LeaderForView", ps.block.View).Return(ps.participants[1].NodeID, nil)
   155  	for _, participant := range ps.participants.ToSkeleton() {
   156  		ps.committee.On("IdentityByEpoch", mock.Anything, participant.NodeID).Return(participant, nil)
   157  	}
   158  
   159  	// check that validation fails now
   160  	err := ps.validator.ValidateProposal(ps.proposal)
   161  	assert.Error(ps.T(), err, "a proposal from the wrong proposer should be rejected")
   162  
   163  	// check that the error is an invalid proposal error to allow creating slashing challenge
   164  	assert.True(ps.T(), model.IsInvalidProposalError(err), "if the proposal has wrong proposer, we should generate a invalid error")
   165  }
   166  
   167  // TestProposalQCInvalid checks that Validator handles the verifier's error returns correctly.
   168  // In case of `model.InvalidFormatError` and model.ErrInvalidSignature`, we expect the Validator
   169  // to recognize those as an invalid QC, i.e. returns an `model.InvalidProposalError`.
   170  // In contrast, unexpected exceptions and `model.InvalidSignerError` should _not_ be
   171  // interpreted as a sign of an invalid QC.
   172  func (ps *ProposalSuite) TestProposalQCInvalid() {
   173  	ps.Run("invalid-signature", func() {
   174  		*ps.verifier = mocks.Verifier{}
   175  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(
   176  			fmt.Errorf("invalid qc: %w", model.ErrInvalidSignature))
   177  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   178  
   179  		// check that validation fails and the failure case is recognized as an invalid block
   180  		err := ps.validator.ValidateProposal(ps.proposal)
   181  		assert.True(ps.T(), model.IsInvalidProposalError(err), "if the block's QC signature is invalid, an ErrorInvalidBlock error should be raised")
   182  	})
   183  
   184  	ps.Run("invalid-format", func() {
   185  		*ps.verifier = mocks.Verifier{}
   186  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(model.NewInvalidFormatErrorf("invalid qc"))
   187  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   188  
   189  		// check that validation fails and the failure case is recognized as an invalid block
   190  		err := ps.validator.ValidateProposal(ps.proposal)
   191  		assert.True(ps.T(), model.IsInvalidProposalError(err), "if the block's QC has an invalid format, an ErrorInvalidBlock error should be raised")
   192  	})
   193  
   194  	// Theoretically, `VerifyQC` could also return a `model.InvalidSignerError`. However,
   195  	// for the time being, we assume that _every_ HotStuff participant is also a member of
   196  	// the random beacon committee. Consequently, `InvalidSignerError` should not occur atm.
   197  	// TODO: if the random beacon committee is a strict subset of the HotStuff committee,
   198  	//       we expect `model.InvalidSignerError` here during normal operations.
   199  	ps.Run("invalid-signer", func() {
   200  		*ps.verifier = mocks.Verifier{}
   201  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(
   202  			fmt.Errorf("invalid qc: %w", model.NewInvalidSignerErrorf("")))
   203  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   204  
   205  		// check that validation fails and the failure case is recognized as an invalid block
   206  		err := ps.validator.ValidateProposal(ps.proposal)
   207  		assert.Error(ps.T(), err)
   208  		assert.False(ps.T(), model.IsInvalidProposalError(err))
   209  	})
   210  
   211  	ps.Run("unknown-exception", func() {
   212  		exception := errors.New("exception")
   213  		*ps.verifier = mocks.Verifier{}
   214  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(exception)
   215  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   216  
   217  		// check that validation fails and the failure case is recognized as an invalid block
   218  		err := ps.validator.ValidateProposal(ps.proposal)
   219  		assert.ErrorIs(ps.T(), err, exception)
   220  		assert.False(ps.T(), model.IsInvalidProposalError(err))
   221  	})
   222  
   223  	ps.Run("verify-qc-err-view-for-unknown-epoch", func() {
   224  		*ps.verifier = mocks.Verifier{}
   225  		ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(model.ErrViewForUnknownEpoch)
   226  		ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   227  
   228  		// check that validation fails and the failure is considered internal exception and NOT an InvalidProposal error
   229  		err := ps.validator.ValidateProposal(ps.proposal)
   230  		assert.Error(ps.T(), err)
   231  		assert.NotErrorIs(ps.T(), err, model.ErrViewForUnknownEpoch)
   232  		assert.False(ps.T(), model.IsInvalidProposalError(err))
   233  	})
   234  }
   235  
   236  func (ps *ProposalSuite) TestProposalQCError() {
   237  
   238  	// change verifier to fail on QC validation
   239  	*ps.verifier = mocks.Verifier{}
   240  	ps.verifier.On("VerifyQC", ps.voters, ps.block.QC.SigData, ps.parent.View, ps.parent.BlockID).Return(fmt.Errorf("some exception"))
   241  	ps.verifier.On("VerifyVote", ps.voter, ps.vote.SigData, ps.block.View, ps.block.BlockID).Return(nil)
   242  
   243  	// check that validation fails now
   244  	err := ps.validator.ValidateProposal(ps.proposal)
   245  	assert.Error(ps.T(), err, "a proposal with an invalid QC should be rejected")
   246  
   247  	// check that the error is an invalid proposal error to allow creating slashing challenge
   248  	assert.False(ps.T(), model.IsInvalidProposalError(err), "if we can't verify the QC, we should not generate a invalid error")
   249  }
   250  
   251  // TestProposalWithLastViewTC tests different scenarios where last view has ended with TC
   252  // this requires including a valid LastViewTC.
   253  func (ps *ProposalSuite) TestProposalWithLastViewTC() {
   254  	// assume all proposals are created by valid leader
   255  	ps.verifier.On("VerifyVote", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
   256  	ps.committee.On("LeaderForView", mock.Anything).Return(ps.leader.NodeID, nil)
   257  
   258  	ps.Run("happy-path", func() {
   259  		proposal := helper.MakeProposal(
   260  			helper.WithBlock(helper.MakeBlock(
   261  				helper.WithBlockView(ps.block.View+2),
   262  				helper.WithBlockProposer(ps.leader.NodeID),
   263  				helper.WithParentSigners(ps.indices),
   264  				helper.WithBlockQC(ps.block.QC)),
   265  			),
   266  			helper.WithLastViewTC(helper.MakeTC(
   267  				helper.WithTCSigners(ps.indices),
   268  				helper.WithTCView(ps.block.View+1),
   269  				helper.WithTCNewestQC(ps.block.QC))),
   270  		)
   271  		ps.verifier.On("VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   272  			proposal.LastViewTC.View, proposal.LastViewTC.NewestQCViews).Return(nil).Once()
   273  		err := ps.validator.ValidateProposal(proposal)
   274  		require.NoError(ps.T(), err)
   275  	})
   276  	ps.Run("no-tc", func() {
   277  		proposal := helper.MakeProposal(
   278  			helper.WithBlock(helper.MakeBlock(
   279  				helper.WithBlockView(ps.block.View+2),
   280  				helper.WithBlockProposer(ps.leader.NodeID),
   281  				helper.WithParentSigners(ps.indices),
   282  				helper.WithBlockQC(ps.block.QC)),
   283  			),
   284  			// in this case proposal without LastViewTC is considered invalid
   285  		)
   286  		err := ps.validator.ValidateProposal(proposal)
   287  		require.True(ps.T(), model.IsInvalidProposalError(err))
   288  		ps.verifier.AssertNotCalled(ps.T(), "VerifyQC")
   289  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   290  	})
   291  	ps.Run("tc-for-wrong-view", func() {
   292  		proposal := helper.MakeProposal(
   293  			helper.WithBlock(helper.MakeBlock(
   294  				helper.WithBlockView(ps.block.View+2),
   295  				helper.WithBlockProposer(ps.leader.NodeID),
   296  				helper.WithParentSigners(ps.indices),
   297  				helper.WithBlockQC(ps.block.QC)),
   298  			),
   299  			helper.WithLastViewTC(helper.MakeTC(
   300  				helper.WithTCSigners(ps.indices),
   301  				helper.WithTCView(ps.block.View+10), // LastViewTC.View must be equal to Block.View-1
   302  				helper.WithTCNewestQC(ps.block.QC))),
   303  		)
   304  		err := ps.validator.ValidateProposal(proposal)
   305  		require.True(ps.T(), model.IsInvalidProposalError(err))
   306  		ps.verifier.AssertNotCalled(ps.T(), "VerifyQC")
   307  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   308  	})
   309  	ps.Run("proposal-not-safe-to-extend", func() {
   310  		proposal := helper.MakeProposal(
   311  			helper.WithBlock(helper.MakeBlock(
   312  				helper.WithBlockView(ps.block.View+2),
   313  				helper.WithBlockProposer(ps.leader.NodeID),
   314  				helper.WithParentSigners(ps.indices),
   315  				helper.WithBlockQC(ps.block.QC)),
   316  			),
   317  			helper.WithLastViewTC(helper.MakeTC(
   318  				helper.WithTCSigners(ps.indices),
   319  				helper.WithTCView(ps.block.View+1),
   320  				// proposal is not safe to extend because included QC.View is higher that Block.QC.View
   321  				helper.WithTCNewestQC(helper.MakeQC(helper.WithQCView(ps.block.View+1))))),
   322  		)
   323  		err := ps.validator.ValidateProposal(proposal)
   324  		require.True(ps.T(), model.IsInvalidProposalError(err))
   325  		ps.verifier.AssertNotCalled(ps.T(), "VerifyQC")
   326  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   327  	})
   328  	ps.Run("included-tc-highest-qc-not-highest", func() {
   329  		proposal := helper.MakeProposal(
   330  			helper.WithBlock(helper.MakeBlock(
   331  				helper.WithBlockView(ps.block.View+2),
   332  				helper.WithBlockProposer(ps.leader.NodeID),
   333  				helper.WithParentSigners(ps.indices),
   334  				helper.WithBlockQC(ps.block.QC)),
   335  			),
   336  			helper.WithLastViewTC(helper.MakeTC(
   337  				helper.WithTCSigners(ps.indices),
   338  				helper.WithTCView(ps.block.View+1),
   339  				helper.WithTCNewestQC(ps.block.QC),
   340  			)),
   341  		)
   342  		ps.verifier.On("VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   343  			proposal.LastViewTC.View, mock.Anything).Return(nil).Once()
   344  
   345  		// this is considered an invalid TC, because highest QC's view is not equal to max{NewestQCViews}
   346  		proposal.LastViewTC.NewestQCViews[0] = proposal.LastViewTC.NewestQC.View + 1
   347  		err := ps.validator.ValidateProposal(proposal)
   348  		require.True(ps.T(), model.IsInvalidProposalError(err) && model.IsInvalidTCError(err))
   349  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   350  	})
   351  	ps.Run("included-tc-threshold-not-reached", func() {
   352  		// TC is signed by only one signer - insufficient to reach weight threshold
   353  		insufficientSignerIndices, err := signature.EncodeSignersToIndices(ps.participants.NodeIDs(), ps.participants.NodeIDs()[:1])
   354  		require.NoError(ps.T(), err)
   355  		proposal := helper.MakeProposal(
   356  			helper.WithBlock(helper.MakeBlock(
   357  				helper.WithBlockView(ps.block.View+2),
   358  				helper.WithBlockProposer(ps.leader.NodeID),
   359  				helper.WithParentSigners(ps.indices),
   360  				helper.WithBlockQC(ps.block.QC)),
   361  			),
   362  			helper.WithLastViewTC(helper.MakeTC(
   363  				helper.WithTCSigners(insufficientSignerIndices), // one signer is not enough to reach threshold
   364  				helper.WithTCView(ps.block.View+1),
   365  				helper.WithTCNewestQC(ps.block.QC),
   366  			)),
   367  		)
   368  		err = ps.validator.ValidateProposal(proposal)
   369  		require.True(ps.T(), model.IsInvalidProposalError(err) && model.IsInvalidTCError(err))
   370  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   371  	})
   372  	ps.Run("included-tc-highest-qc-invalid", func() {
   373  		// QC included in TC has view below QC included in proposal
   374  		qc := helper.MakeQC(
   375  			helper.WithQCView(ps.block.QC.View-1),
   376  			helper.WithQCSigners(ps.indices))
   377  
   378  		proposal := helper.MakeProposal(
   379  			helper.WithBlock(helper.MakeBlock(
   380  				helper.WithBlockView(ps.block.View+2),
   381  				helper.WithBlockProposer(ps.leader.NodeID),
   382  				helper.WithParentSigners(ps.indices),
   383  				helper.WithBlockQC(ps.block.QC)),
   384  			),
   385  			helper.WithLastViewTC(helper.MakeTC(
   386  				helper.WithTCSigners(ps.indices),
   387  				helper.WithTCView(ps.block.View+1),
   388  				helper.WithTCNewestQC(qc))),
   389  		)
   390  		ps.verifier.On("VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   391  			proposal.LastViewTC.View, proposal.LastViewTC.NewestQCViews).Return(nil).Once()
   392  		ps.verifier.On("VerifyQC", ps.voters, qc.SigData,
   393  			qc.View, qc.BlockID).Return(model.ErrInvalidSignature).Once()
   394  		err := ps.validator.ValidateProposal(proposal)
   395  		require.True(ps.T(), model.IsInvalidProposalError(err) && model.IsInvalidTCError(err))
   396  	})
   397  	ps.Run("verify-qc-err-view-for-unknown-epoch", func() {
   398  		newestQC := helper.MakeQC(
   399  			helper.WithQCView(ps.block.QC.View-2),
   400  			helper.WithQCSigners(ps.indices))
   401  
   402  		proposal := helper.MakeProposal(
   403  			helper.WithBlock(helper.MakeBlock(
   404  				helper.WithBlockView(ps.block.View+2),
   405  				helper.WithBlockProposer(ps.leader.NodeID),
   406  				helper.WithParentSigners(ps.indices),
   407  				helper.WithBlockQC(ps.block.QC)),
   408  			),
   409  			helper.WithLastViewTC(helper.MakeTC(
   410  				helper.WithTCSigners(ps.indices),
   411  				helper.WithTCView(ps.block.View+1),
   412  				helper.WithTCNewestQC(newestQC))),
   413  		)
   414  		ps.verifier.On("VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   415  			proposal.LastViewTC.View, proposal.LastViewTC.NewestQCViews).Return(nil).Once()
   416  		// Validating QC included in TC returns ErrViewForUnknownEpoch
   417  		ps.verifier.On("VerifyQC", ps.voters, newestQC.SigData,
   418  			newestQC.View, newestQC.BlockID).Return(model.ErrViewForUnknownEpoch).Once()
   419  		err := ps.validator.ValidateProposal(proposal)
   420  		require.Error(ps.T(), err)
   421  		require.False(ps.T(), model.IsInvalidProposalError(err))
   422  		require.False(ps.T(), model.IsInvalidTCError(err))
   423  		require.NotErrorIs(ps.T(), err, model.ErrViewForUnknownEpoch)
   424  	})
   425  	ps.Run("included-tc-invalid-sig", func() {
   426  		proposal := helper.MakeProposal(
   427  			helper.WithBlock(helper.MakeBlock(
   428  				helper.WithBlockView(ps.block.View+2),
   429  				helper.WithBlockProposer(ps.leader.NodeID),
   430  				helper.WithParentSigners(ps.indices),
   431  				helper.WithBlockQC(ps.block.QC)),
   432  			),
   433  			helper.WithLastViewTC(helper.MakeTC(
   434  				helper.WithTCSigners(ps.indices),
   435  				helper.WithTCView(ps.block.View+1),
   436  				helper.WithTCNewestQC(ps.block.QC))),
   437  		)
   438  		ps.verifier.On("VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   439  			proposal.LastViewTC.View, proposal.LastViewTC.NewestQCViews).Return(model.ErrInvalidSignature).Once()
   440  		err := ps.validator.ValidateProposal(proposal)
   441  		require.True(ps.T(), model.IsInvalidProposalError(err) && model.IsInvalidTCError(err))
   442  		ps.verifier.AssertCalled(ps.T(), "VerifyTC", ps.voters, []byte(proposal.LastViewTC.SigData),
   443  			proposal.LastViewTC.View, proposal.LastViewTC.NewestQCViews)
   444  	})
   445  	ps.Run("last-view-successful-but-includes-tc", func() {
   446  		proposal := helper.MakeProposal(
   447  			helper.WithBlock(helper.MakeBlock(
   448  				helper.WithBlockView(ps.finalized+1),
   449  				helper.WithBlockProposer(ps.leader.NodeID),
   450  				helper.WithParentSigners(ps.indices),
   451  				helper.WithParentBlock(ps.parent)),
   452  			),
   453  			helper.WithLastViewTC(helper.MakeTC()),
   454  		)
   455  		err := ps.validator.ValidateProposal(proposal)
   456  		require.True(ps.T(), model.IsInvalidProposalError(err))
   457  		ps.verifier.AssertNotCalled(ps.T(), "VerifyTC")
   458  	})
   459  	ps.verifier.AssertExpectations(ps.T())
   460  }
   461  
   462  func TestValidateVote(t *testing.T) {
   463  	suite.Run(t, new(VoteSuite))
   464  }
   465  
   466  type VoteSuite struct {
   467  	suite.Suite
   468  	signer    *flow.IdentitySkeleton
   469  	block     *model.Block
   470  	vote      *model.Vote
   471  	verifier  *mocks.Verifier
   472  	committee *mocks.Replicas
   473  	validator *Validator
   474  }
   475  
   476  func (vs *VoteSuite) SetupTest() {
   477  
   478  	// create a random signing identity
   479  	vs.signer = &unittest.IdentityFixture(unittest.WithRole(flow.RoleConsensus)).IdentitySkeleton
   480  
   481  	// create a block that should be signed
   482  	vs.block = helper.MakeBlock()
   483  
   484  	// create a vote for this block
   485  	vs.vote = &model.Vote{
   486  		View:     vs.block.View,
   487  		BlockID:  vs.block.BlockID,
   488  		SignerID: vs.signer.NodeID,
   489  		SigData:  []byte{},
   490  	}
   491  
   492  	// set up the mocked verifier
   493  	vs.verifier = &mocks.Verifier{}
   494  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block.View, vs.block.BlockID).Return(nil)
   495  
   496  	// the leader for the block view is the correct one
   497  	vs.committee = &mocks.Replicas{}
   498  	vs.committee.On("IdentityByEpoch", mock.Anything, vs.signer.NodeID).Return(vs.signer, nil)
   499  
   500  	// set up the validator with the mocked dependencies
   501  	vs.validator = New(vs.committee, vs.verifier)
   502  }
   503  
   504  // TestVoteOK checks the happy case, which is the default for the suite
   505  func (vs *VoteSuite) TestVoteOK() {
   506  	_, err := vs.validator.ValidateVote(vs.vote)
   507  	assert.NoError(vs.T(), err, "a valid vote should be accepted")
   508  }
   509  
   510  // TestVoteSignatureError checks that the Validator does not misinterpret
   511  // unexpected exceptions for invalid votes.
   512  func (vs *VoteSuite) TestVoteSignatureError() {
   513  	*vs.verifier = mocks.Verifier{}
   514  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block.View, vs.block.BlockID).Return(fmt.Errorf("some exception"))
   515  
   516  	// check that the vote is no longer validated
   517  	_, err := vs.validator.ValidateVote(vs.vote)
   518  	assert.Error(vs.T(), err, "a vote with error on signature validation should be rejected")
   519  	assert.False(vs.T(), model.IsInvalidVoteError(err), "internal exception should not be interpreted as invalid vote")
   520  }
   521  
   522  // TestVoteVerifyVote_ErrViewForUnknownEpoch tests if ValidateVote correctly handles VerifyVote's ErrViewForUnknownEpoch sentinel error
   523  // Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
   524  func (vs *VoteSuite) TestVoteVerifyVote_ErrViewForUnknownEpoch() {
   525  	*vs.verifier = mocks.Verifier{}
   526  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block.View, vs.block.BlockID).Return(model.ErrViewForUnknownEpoch)
   527  
   528  	// check that the vote is no longer validated
   529  	_, err := vs.validator.ValidateVote(vs.vote)
   530  	assert.Error(vs.T(), err)
   531  	assert.False(vs.T(), model.IsInvalidVoteError(err), "internal exception should not be interpreted as invalid vote")
   532  	assert.NotErrorIs(vs.T(), err, model.ErrViewForUnknownEpoch, "we don't expect a sentinel error here")
   533  }
   534  
   535  // TestVoteInvalidSignerID checks that the Validator correctly handles a vote
   536  // with a SignerID that does not correspond to a valid consensus participant.
   537  // In this case, the `hotstuff.DynamicCommittee` returns a `model.InvalidSignerError`,
   538  // which the Validator should recognize as a symptom for an invalid vote.
   539  // Hence, we expect the validator to return a `model.InvalidVoteError`.
   540  func (vs *VoteSuite) TestVoteInvalidSignerID() {
   541  	*vs.committee = mocks.Replicas{}
   542  	vs.committee.On("IdentityByEpoch", vs.block.View, vs.vote.SignerID).Return(nil, model.NewInvalidSignerErrorf(""))
   543  
   544  	// A `model.InvalidSignerError` from the committee should be interpreted as
   545  	// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
   546  	_, err := vs.validator.ValidateVote(vs.vote)
   547  	assert.Error(vs.T(), err, "a vote with unknown SignerID should be rejected")
   548  	assert.True(vs.T(), model.IsInvalidVoteError(err), "a vote with unknown SignerID should be rejected")
   549  }
   550  
   551  // TestVoteSignatureInvalid checks that the Validator correctly handles votes
   552  // with cryptographically invalid signature. In this case, the `hotstuff.Verifier`
   553  // returns a `model.ErrInvalidSignature`, which the Validator should recognize as
   554  // a symptom for an invalid vote.
   555  // Hence, we expect the validator to return a `model.InvalidVoteError`.
   556  func (vs *VoteSuite) TestVoteSignatureInvalid() {
   557  	*vs.verifier = mocks.Verifier{}
   558  	vs.verifier.On("VerifyVote", vs.signer, vs.vote.SigData, vs.block.View, vs.block.BlockID).Return(fmt.Errorf("staking sig is invalid: %w", model.ErrInvalidSignature))
   559  
   560  	// A `model.ErrInvalidSignature` from the `hotstuff.Verifier` should be interpreted as
   561  	// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
   562  	_, err := vs.validator.ValidateVote(vs.vote)
   563  	assert.Error(vs.T(), err, "a vote with an invalid signature should be rejected")
   564  	assert.True(vs.T(), model.IsInvalidVoteError(err), "a vote with an invalid signature should be rejected")
   565  }
   566  
   567  func TestValidateQC(t *testing.T) {
   568  	suite.Run(t, new(QCSuite))
   569  }
   570  
   571  type QCSuite struct {
   572  	suite.Suite
   573  	participants flow.IdentitySkeletonList
   574  	signers      flow.IdentitySkeletonList
   575  	block        *model.Block
   576  	qc           *flow.QuorumCertificate
   577  	committee    *mocks.Replicas
   578  	verifier     *mocks.Verifier
   579  	validator    *Validator
   580  }
   581  
   582  func (qs *QCSuite) SetupTest() {
   583  	// create a list of 10 nodes with 1-weight each
   584  	qs.participants = unittest.IdentityListFixture(10,
   585  		unittest.WithRole(flow.RoleConsensus),
   586  		unittest.WithInitialWeight(1),
   587  	).Sort(flow.Canonical[flow.Identity]).ToSkeleton()
   588  
   589  	// signers are a qualified majority at 7
   590  	qs.signers = qs.participants[:7]
   591  
   592  	// create a block that has the signers in its QC
   593  	qs.block = helper.MakeBlock()
   594  	indices, err := signature.EncodeSignersToIndices(qs.participants.NodeIDs(), qs.signers.NodeIDs())
   595  	require.NoError(qs.T(), err)
   596  
   597  	qs.qc = helper.MakeQC(helper.WithQCBlock(qs.block), helper.WithQCSigners(indices))
   598  
   599  	// return the correct participants and identities from view state
   600  	qs.committee = &mocks.Replicas{}
   601  	qs.committee.On("IdentitiesByEpoch", mock.Anything).Return(
   602  		func(_ uint64) flow.IdentitySkeletonList {
   603  			return qs.participants
   604  		},
   605  		nil,
   606  	)
   607  	qs.committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(qs.participants.TotalWeight()), nil)
   608  
   609  	// set up the mocked verifier to verify the QC correctly
   610  	qs.verifier = &mocks.Verifier{}
   611  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.qc.View, qs.qc.BlockID).Return(nil)
   612  
   613  	// set up the validator with the mocked dependencies
   614  	qs.validator = New(qs.committee, qs.verifier)
   615  }
   616  
   617  // TestQCOK verifies the default happy case
   618  func (qs *QCSuite) TestQCOK() {
   619  
   620  	// check the default happy case passes
   621  	err := qs.validator.ValidateQC(qs.qc)
   622  	assert.NoError(qs.T(), err, "a valid QC should be accepted")
   623  }
   624  
   625  // TestQCRetrievingParticipantsError tests that validation errors if:
   626  // there is an error retrieving identities of consensus participants
   627  func (qs *QCSuite) TestQCRetrievingParticipantsError() {
   628  	// change the hotstuff.DynamicCommittee to fail on retrieving participants
   629  	*qs.committee = mocks.Replicas{}
   630  	qs.committee.On("IdentitiesByEpoch", mock.Anything).Return(qs.participants, errors.New("FATAL internal error"))
   631  
   632  	// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidQC
   633  	err := qs.validator.ValidateQC(qs.qc)
   634  	assert.Error(qs.T(), err, "unspecific error when retrieving consensus participants should be escalated to surrounding logic")
   635  	assert.False(qs.T(), model.IsInvalidQCError(err), "unspecific internal errors should not result in ErrorInvalidQC error")
   636  }
   637  
   638  // TestQCSignersError tests that a qc fails validation if:
   639  // QC signer's have insufficient weight (but are all valid consensus participants otherwise)
   640  func (qs *QCSuite) TestQCInsufficientWeight() {
   641  	// signers only have weight 6 out of 10 total (NOT have a supermajority)
   642  	qs.signers = qs.participants[:6]
   643  	indices, err := signature.EncodeSignersToIndices(qs.participants.NodeIDs(), qs.signers.NodeIDs())
   644  	require.NoError(qs.T(), err)
   645  
   646  	qs.qc = helper.MakeQC(helper.WithQCBlock(qs.block), helper.WithQCSigners(indices))
   647  
   648  	// the QC should not be validated anymore
   649  	err = qs.validator.ValidateQC(qs.qc)
   650  	assert.Error(qs.T(), err, "a QC should be rejected if it has insufficient voted weight")
   651  
   652  	// we should get a threshold error to bubble up for extra info
   653  	assert.True(qs.T(), model.IsInvalidQCError(err), "if there is insufficient voted weight, an invalid block error should be raised")
   654  }
   655  
   656  // TestQCSignatureError tests that validation errors if:
   657  // there is an unspecific internal error while validating the signature
   658  func (qs *QCSuite) TestQCSignatureError() {
   659  
   660  	// set up the verifier to fail QC verification
   661  	*qs.verifier = mocks.Verifier{}
   662  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.qc.View, qs.qc.BlockID).Return(errors.New("dummy error"))
   663  
   664  	// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidQC
   665  	err := qs.validator.ValidateQC(qs.qc)
   666  	assert.Error(qs.T(), err, "unspecific sig verification error should be escalated to surrounding logic")
   667  	assert.False(qs.T(), model.IsInvalidQCError(err), "unspecific internal errors should not result in ErrorInvalidQC error")
   668  }
   669  
   670  // TestQCSignatureInvalid verifies that the Validator correctly handles the model.ErrInvalidSignature.
   671  // This error return from `Verifier.VerifyQC` is an expected failure case in case of a byzantine input, where
   672  // one of the signatures in the QC is broken. Hence, the Validator should wrap it as InvalidProposalError.
   673  func (qs *QCSuite) TestQCSignatureInvalid() {
   674  	// change the verifier to fail the QC signature
   675  	*qs.verifier = mocks.Verifier{}
   676  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.qc.View, qs.qc.BlockID).Return(fmt.Errorf("invalid qc: %w", model.ErrInvalidSignature))
   677  
   678  	// the QC should no longer pass validation
   679  	err := qs.validator.ValidateQC(qs.qc)
   680  	assert.True(qs.T(), model.IsInvalidQCError(err), "if the signature is invalid an ErrorInvalidQC error should be raised")
   681  }
   682  
   683  // TestQCVerifyQC_ErrViewForUnknownEpoch tests if ValidateQC correctly handles VerifyQC's ErrViewForUnknownEpoch sentinel error
   684  // Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
   685  func (qs *QCSuite) TestQCVerifyQC_ErrViewForUnknownEpoch() {
   686  	*qs.verifier = mocks.Verifier{}
   687  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.qc.View, qs.qc.BlockID).Return(model.ErrViewForUnknownEpoch)
   688  	err := qs.validator.ValidateQC(qs.qc)
   689  	assert.Error(qs.T(), err)
   690  	assert.False(qs.T(), model.IsInvalidQCError(err), "we don't expect a sentinel error here")
   691  	assert.NotErrorIs(qs.T(), err, model.ErrViewForUnknownEpoch, "we don't expect a sentinel error here")
   692  }
   693  
   694  // TestQCSignatureInvalidFormat verifies that the Validator correctly handles the model.InvalidFormatError.
   695  // This error return from `Verifier.VerifyQC` is an expected failure case in case of a byzantine input, where
   696  // some binary vector (e.g. `sigData`) is broken. Hence, the Validator should wrap it as InvalidProposalError.
   697  func (qs *QCSuite) TestQCSignatureInvalidFormat() {
   698  	// change the verifier to fail the QC signature
   699  	*qs.verifier = mocks.Verifier{}
   700  	qs.verifier.On("VerifyQC", qs.signers, qs.qc.SigData, qs.qc.View, qs.qc.BlockID).Return(model.NewInvalidFormatErrorf("invalid sigType"))
   701  
   702  	// the QC should no longer pass validation
   703  	err := qs.validator.ValidateQC(qs.qc)
   704  	assert.True(qs.T(), model.IsInvalidQCError(err), "if the signature has an invalid format, an ErrorInvalidQC error should be raised")
   705  }
   706  
   707  // TestQCEmptySigners verifies that the Validator correctly handles the model.InsufficientSignaturesError:
   708  // In the validator, we previously checked the total weight of all signers meets the supermajority threshold,
   709  // which is a _positive_ number. Hence, there must be at least one signer. Hence, `Verifier.VerifyQC`
   710  // returning this error would be a symptom of a fatal internal bug. The Validator should _not_ interpret
   711  // this error as an invalid QC / invalid block, i.e. it should _not_ return an `InvalidProposalError`.
   712  func (qs *QCSuite) TestQCEmptySigners() {
   713  	*qs.verifier = mocks.Verifier{}
   714  	qs.verifier.On("VerifyQC", mock.Anything, qs.qc.SigData, qs.block.View, qs.block.BlockID).Return(
   715  		fmt.Errorf("%w", model.NewInsufficientSignaturesErrorf("")))
   716  
   717  	// the Validator should _not_ interpret this as a invalid QC, but as an internal error
   718  	err := qs.validator.ValidateQC(qs.qc)
   719  	assert.True(qs.T(), model.IsInsufficientSignaturesError(err)) // unexpected error should be wrapped and propagated upwards
   720  	assert.False(qs.T(), model.IsInvalidProposalError(err), err, "should _not_ interpret this as a invalid QC, but as an internal error")
   721  }
   722  
   723  func TestValidateTC(t *testing.T) {
   724  	suite.Run(t, new(TCSuite))
   725  }
   726  
   727  type TCSuite struct {
   728  	suite.Suite
   729  	participants flow.IdentitySkeletonList
   730  	signers      flow.IdentitySkeletonList
   731  	indices      []byte
   732  	block        *model.Block
   733  	tc           *flow.TimeoutCertificate
   734  	committee    *mocks.DynamicCommittee
   735  	verifier     *mocks.Verifier
   736  	validator    *Validator
   737  }
   738  
   739  func (s *TCSuite) SetupTest() {
   740  
   741  	// create a list of 10 nodes with 1-weight each
   742  	s.participants = unittest.IdentityListFixture(10,
   743  		unittest.WithRole(flow.RoleConsensus),
   744  		unittest.WithInitialWeight(1),
   745  	).Sort(flow.Canonical[flow.Identity]).ToSkeleton()
   746  
   747  	// signers are a qualified majority at 7
   748  	s.signers = s.participants[:7]
   749  
   750  	var err error
   751  	s.indices, err = signature.EncodeSignersToIndices(s.participants.NodeIDs(), s.signers.NodeIDs())
   752  	require.NoError(s.T(), err)
   753  
   754  	view := uint64(int(rand.Uint32()) + len(s.participants))
   755  
   756  	highQCViews := make([]uint64, 0, len(s.signers))
   757  	for i := range s.signers {
   758  		highQCViews = append(highQCViews, view-uint64(i)-1)
   759  	}
   760  
   761  	rand.Shuffle(len(highQCViews), func(i, j int) {
   762  		highQCViews[i], highQCViews[j] = highQCViews[j], highQCViews[i]
   763  	})
   764  
   765  	// create a block that has the signers in its QC
   766  	parent := helper.MakeBlock(helper.WithBlockView(view - 1))
   767  	s.block = helper.MakeBlock(helper.WithBlockView(view),
   768  		helper.WithParentBlock(parent),
   769  		helper.WithParentSigners(s.indices))
   770  	s.tc = helper.MakeTC(helper.WithTCNewestQC(s.block.QC),
   771  		helper.WithTCView(view+1),
   772  		helper.WithTCSigners(s.indices),
   773  		helper.WithTCHighQCViews(highQCViews))
   774  
   775  	// return the correct participants and identities from view state
   776  	s.committee = &mocks.DynamicCommittee{}
   777  	s.committee.On("IdentitiesByEpoch", mock.Anything, mock.Anything).Return(
   778  		func(view uint64) flow.IdentitySkeletonList {
   779  			return s.participants
   780  		},
   781  		nil,
   782  	)
   783  	s.committee.On("QuorumThresholdForView", mock.Anything).Return(committees.WeightThresholdToBuildQC(s.participants.TotalWeight()), nil)
   784  
   785  	s.verifier = &mocks.Verifier{}
   786  	s.verifier.On("VerifyQC", s.signers, s.block.QC.SigData, parent.View, parent.BlockID).Return(nil)
   787  
   788  	// set up the validator with the mocked dependencies
   789  	s.validator = New(s.committee, s.verifier)
   790  }
   791  
   792  // TestTCOk tests if happy-path returns correct result
   793  func (s *TCSuite) TestTCOk() {
   794  	s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(nil).Once()
   795  
   796  	// check the default happy case passes
   797  	err := s.validator.ValidateTC(s.tc)
   798  	assert.NoError(s.T(), err, "a valid TC should be accepted")
   799  }
   800  
   801  // TestTCNewestQCFromFuture tests if correct error is returned when included QC is higher than TC's view
   802  func (s *TCSuite) TestTCNewestQCFromFuture() {
   803  	// highest QC from future view
   804  	s.tc.NewestQC.View = s.tc.View + 1
   805  	err := s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   806  	assert.True(s.T(), model.IsInvalidTCError(err), "if NewestQC.View > TC.View, an ErrorInvalidTC error should be raised")
   807  }
   808  
   809  // TestTCNewestQCIsNotHighest tests if correct error is returned when included QC is not highest
   810  func (s *TCSuite) TestTCNewestQCIsNotHighest() {
   811  	s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData),
   812  		s.tc.View, s.tc.NewestQCViews).Return(nil).Once()
   813  
   814  	// highest QC view is not equal to max(TONewestQCViews)
   815  	s.tc.NewestQCViews[0] = s.tc.NewestQC.View + 1
   816  	err := s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   817  	assert.True(s.T(), model.IsInvalidTCError(err), "if max(highQCViews) != NewestQC.View, an ErrorInvalidTC error should be raised")
   818  }
   819  
   820  // TestTCInvalidSigners tests if correct error is returned when signers are invalid
   821  func (s *TCSuite) TestTCInvalidSigners() {
   822  	s.participants = s.participants[1:] // remove participant[0] from the list of valid consensus participant
   823  	err := s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   824  	assert.True(s.T(), model.IsInvalidTCError(err), "if some signers are invalid consensus participants, an ErrorInvalidTC error should be raised")
   825  }
   826  
   827  // TestTCThresholdNotReached tests if correct error is returned when TC's singers don't have enough weight
   828  func (s *TCSuite) TestTCThresholdNotReached() {
   829  	// signers only have weight 1 out of 10 total (NOT have a supermajority)
   830  	s.signers = s.participants[:1]
   831  	indices, err := signature.EncodeSignersToIndices(s.participants.NodeIDs(), s.signers.NodeIDs())
   832  	require.NoError(s.T(), err)
   833  
   834  	s.tc.SignerIndices = indices
   835  
   836  	// adjust signers to be less than total weight
   837  	err = s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   838  	assert.True(s.T(), model.IsInvalidTCError(err), "if signers don't have enough weight, an ErrorInvalidTC error should be raised")
   839  }
   840  
   841  // TestTCInvalidNewestQC tests if correct error is returned when included highest QC is invalid
   842  func (s *TCSuite) TestTCInvalidNewestQC() {
   843  	*s.verifier = mocks.Verifier{}
   844  	s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(nil).Once()
   845  	s.verifier.On("VerifyQC", s.signers, s.tc.NewestQC.SigData, s.tc.NewestQC.View, s.tc.NewestQC.BlockID).Return(model.NewInvalidFormatErrorf("invalid qc")).Once()
   846  	err := s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   847  	assert.True(s.T(), model.IsInvalidTCError(err), "if included QC is invalid, an ErrorInvalidTC error should be raised")
   848  }
   849  
   850  // TestTCVerifyQC_ErrViewForUnknownEpoch tests if ValidateTC correctly handles VerifyQC's ErrViewForUnknownEpoch sentinel error
   851  // Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
   852  func (s *TCSuite) TestTCVerifyQC_ErrViewForUnknownEpoch() {
   853  	*s.verifier = mocks.Verifier{}
   854  	s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(nil).Once()
   855  	s.verifier.On("VerifyQC", s.signers, s.tc.NewestQC.SigData, s.tc.NewestQC.View, s.tc.NewestQC.BlockID).Return(model.ErrViewForUnknownEpoch).Once()
   856  	err := s.validator.ValidateTC(s.tc) // the QC should not be validated anymore
   857  	assert.Error(s.T(), err)
   858  	assert.False(s.T(), model.IsInvalidTCError(err), "we don't expect a sentinel error here")
   859  	assert.NotErrorIs(s.T(), err, model.ErrViewForUnknownEpoch, "we don't expect a sentinel error here")
   860  }
   861  
   862  // TestTCInvalidSignature tests a few scenarios when the signature is invalid or TC signers is malformed
   863  func (s *TCSuite) TestTCInvalidSignature() {
   864  	s.Run("insufficient-signatures", func() {
   865  		*s.verifier = mocks.Verifier{}
   866  		s.verifier.On("VerifyQC", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
   867  		s.verifier.On("VerifyTC", mock.Anything, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(model.NewInsufficientSignaturesErrorf("")).Once()
   868  
   869  		// the Validator should _not_ interpret this as an invalid TC, but as an internal error
   870  		err := s.validator.ValidateTC(s.tc)
   871  		assert.True(s.T(), model.IsInsufficientSignaturesError(err)) // unexpected error should be wrapped and propagated upwards
   872  		assert.False(s.T(), model.IsInvalidTCError(err), err, "should _not_ interpret this as a invalid TC, but as an internal error")
   873  	})
   874  	s.Run("invalid-format", func() {
   875  		*s.verifier = mocks.Verifier{}
   876  		s.verifier.On("VerifyQC", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
   877  		s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(model.NewInvalidFormatErrorf("")).Once()
   878  		err := s.validator.ValidateTC(s.tc)
   879  		assert.True(s.T(), model.IsInvalidTCError(err), "if included TC's inputs are invalid, an ErrorInvalidTC error should be raised")
   880  	})
   881  	s.Run("invalid-signature", func() {
   882  		*s.verifier = mocks.Verifier{}
   883  		s.verifier.On("VerifyQC", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
   884  		s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(model.ErrInvalidSignature).Once()
   885  		err := s.validator.ValidateTC(s.tc)
   886  		assert.True(s.T(), model.IsInvalidTCError(err), "if included TC's signature is invalid, an ErrorInvalidTC error should be raised")
   887  	})
   888  	s.Run("verify-sig-exception", func() {
   889  		exception := errors.New("verify-sig-exception")
   890  		*s.verifier = mocks.Verifier{}
   891  		s.verifier.On("VerifyQC", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
   892  		s.verifier.On("VerifyTC", s.signers, []byte(s.tc.SigData), s.tc.View, s.tc.NewestQCViews).Return(exception).Once()
   893  		err := s.validator.ValidateTC(s.tc)
   894  		assert.ErrorIs(s.T(), err, exception, "if included TC's signature is invalid, an exception should be propagated")
   895  		assert.False(s.T(), model.IsInvalidTCError(err))
   896  	})
   897  }