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 }