ceremonyclient/consensus/validator/validator_test.go
Cassandra Heart c797d482f9
v2.1.0.5 (#457)
* wip: conversion of hotstuff from flow into Q-oriented model

* bulk of tests

* remaining non-integration tests

* add integration test, adjust log interface, small tweaks

* further adjustments, restore full pacemaker shape

* add component lifecycle management+supervisor

* further refinements

* resolve timeout hanging

* mostly finalized state for consensus

* bulk of engine swap out

* lifecycle-ify most types

* wiring nearly complete, missing needed hooks for proposals

* plugged in, vetting message validation paths

* global consensus, plugged in and verified

* app shard now wired in too

* do not decode empty keys.yml (#456)

* remove obsolete engine.maxFrames config parameter (#454)

* default to Info log level unless debug is enabled (#453)

* respect config's  "logging" section params, remove obsolete single-file logging (#452)

* Trivial code cleanup aiming to reduce Go compiler warnings (#451)

* simplify range traversal

* simplify channel read for single select case

* delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24

* simplify range traversal

* simplify channel read for single select case

* remove redundant type from array

* simplify range traversal

* simplify channel read for single select case

* RC slate

* finalize 2.1.0.5

* Update comments in StrictMonotonicCounter

Fix comment formatting and clarify description.

---------

Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
2025-11-11 05:00:17 -06:00

934 lines
45 KiB
Go

package validator
import (
"errors"
"fmt"
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"source.quilibrium.com/quilibrium/monorepo/consensus/helper"
"source.quilibrium.com/quilibrium/monorepo/consensus/mocks"
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
)
func TestValidateProposal(t *testing.T) {
suite.Run(t, new(ProposalSuite))
}
type ProposalSuite struct {
suite.Suite
participants []models.WeightedIdentity
indices []byte
leader models.WeightedIdentity
finalized uint64
parent *models.State[*helper.TestState]
state *models.State[*helper.TestState]
voters []models.WeightedIdentity
proposal *models.SignedProposal[*helper.TestState, *helper.TestVote]
vote *helper.TestVote
voter models.WeightedIdentity
committee *mocks.Replicas
verifier *mocks.Verifier[*helper.TestVote]
validator *Validator[*helper.TestState, *helper.TestVote]
}
func (ps *ProposalSuite) SetupTest() {
// the leader is a random node for now
ps.finalized = uint64(rand.Uint32() + 1)
ps.participants = helper.WithWeightedIdentityList(8)
ps.leader = ps.participants[0]
// the parent is the last finalized state, followed directly by a state from the leader
ps.parent = helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.finalized),
)
var err error
ps.indices = []byte{0b11111111}
ps.state = helper.MakeState(
helper.WithStateRank[*helper.TestState](ps.finalized+1),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentState(ps.parent),
helper.WithParentSigners[*helper.TestState](ps.indices),
)
ps.voters = ps.participants
vt := &helper.TestVote{
Rank: ps.state.Rank,
ID: ps.leader.Identity(),
Signature: make([]byte, 74),
StateID: ps.state.Identifier,
}
ps.proposal = helper.MakeSignedProposal(
helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(helper.WithState(ps.state))),
helper.WithVote[*helper.TestState, *helper.TestVote](&vt),
)
vote, err := ps.proposal.ProposerVote()
require.NoError(ps.T(), err)
ps.vote = *vote
ps.voter = ps.leader
// set up the mocked hotstuff Replicas state
ps.committee = &mocks.Replicas{}
ps.committee.On("LeaderForRank", ps.state.Rank).Return(ps.leader.Identity(), nil)
ps.committee.On("QuorumThresholdForRank", mock.Anything).Return(uint64(8000), nil)
ps.committee.On("IdentitiesByRank", mock.Anything).Return(
func(_ uint64) []models.WeightedIdentity {
return ps.participants
},
nil,
)
for _, participant := range ps.participants {
ps.committee.On("IdentityByRank", mock.Anything, participant.Identity()).Return(participant, nil)
}
// set up the mocked verifier
ps.verifier = &mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(nil).Maybe()
ps.verifier.On("VerifyVote", &ps.vote).Return(nil).Maybe()
// set up the validator with the mocked dependencies
ps.validator = NewValidator[*helper.TestState, *helper.TestVote](ps.committee, ps.verifier)
}
func (ps *ProposalSuite) TestProposalOK() {
err := ps.validator.ValidateProposal(ps.proposal)
assert.NoError(ps.T(), err, "a valid proposal should be accepted")
}
func (ps *ProposalSuite) TestProposalSignatureError() {
// change the verifier to error on signature validation with unspecific error
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(nil)
ps.verifier.On("VerifyVote", &ps.vote).Return(errors.New("dummy error"))
// check that validation now fails
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err, "a proposal should be rejected if signature check fails")
// check that the error is not one that leads to invalid
assert.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if signature check fails, we should not receive an ErrorInvalidState")
}
func (ps *ProposalSuite) TestProposalSignatureInvalidFormat() {
// change the verifier to fail signature validation with InvalidFormatError error
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(nil)
ps.verifier.On("VerifyVote", &ps.vote).Return(models.NewInvalidFormatErrorf(""))
// check that validation now fails
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
// check that the error is an invalid proposal error to allow creating slashing challenge
assert.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if signature is invalid, we should generate an invalid error")
}
func (ps *ProposalSuite) TestProposalSignatureInvalid() {
// change the verifier to fail signature validation
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(nil)
ps.verifier.On("VerifyVote", &ps.vote).Return(models.ErrInvalidSignature)
// check that validation now fails
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err, "a proposal with an invalid signature should be rejected")
// check that the error is an invalid proposal error to allow creating slashing challenge
assert.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if signature is invalid, we should generate an invalid error")
}
func (ps *ProposalSuite) TestProposalWrongLeader() {
// change the consensus.Replicas to return a different leader
*ps.committee = mocks.Replicas{}
ps.committee.On("LeaderForRank", ps.state.Rank).Return(ps.participants[1].Identity(), nil)
for _, participant := range ps.participants {
ps.committee.On("IdentityByRank", mock.Anything, participant.Identity()).Return(participant, nil)
}
// check that validation fails now
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err, "a proposal from the wrong proposer should be rejected")
// check that the error is an invalid proposal error to allow creating slashing challenge
assert.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if the proposal has wrong proposer, we should generate a invalid error")
}
// TestProposalQCInvalid checks that Validator handles the verifier's error returns correctly.
// In case of `models.InvalidFormatError` and models.ErrInvalidSignature`, we expect the Validator
// to recognize those as an invalid QC, i.e. returns an `models.InvalidProposalError`.
// In contrast, unexpected exceptions and `models.InvalidSignerError` should _not_ be
// interpreted as a sign of an invalid QC.
func (ps *ProposalSuite) TestProposalQCInvalid() {
ps.Run("invalid-signature", func() {
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(
fmt.Errorf("invalid qc: %w", models.ErrInvalidSignature))
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails and the failure case is recognized as an invalid state
err := ps.validator.ValidateProposal(ps.proposal)
assert.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if the state's QC signature is invalid, an ErrorInvalidState error should be raised")
})
ps.Run("invalid-format", func() {
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(models.NewInvalidFormatErrorf("invalid qc"))
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails and the failure case is recognized as an invalid state
err := ps.validator.ValidateProposal(ps.proposal)
assert.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if the state's QC has an invalid format, an ErrorInvalidState error should be raised")
})
ps.Run("invalid-signer", func() {
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(
fmt.Errorf("invalid qc: %w", models.NewInvalidSignerErrorf("")))
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails and the failure case is recognized as an invalid state
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err)
assert.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
})
ps.Run("unknown-exception", func() {
exception := errors.New("exception")
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(exception)
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails and the failure case is recognized as an invalid state
err := ps.validator.ValidateProposal(ps.proposal)
assert.ErrorIs(ps.T(), err, exception)
assert.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
})
ps.Run("verify-qc-err-rank-for-unknown-rank", func() {
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(models.ErrRankUnknown)
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails and the failure is considered internal exception and NOT an InvalidProposal error
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err)
assert.NotErrorIs(ps.T(), err, models.ErrRankUnknown)
assert.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
})
}
func (ps *ProposalSuite) TestProposalQCError() {
// change verifier to fail on QC validation
*ps.verifier = mocks.Verifier[*helper.TestVote]{}
ps.verifier.On("VerifyQuorumCertificate", ps.state.ParentQuorumCertificate).Return(fmt.Errorf("some exception"))
ps.verifier.On("VerifyVote", &ps.vote).Return(nil)
// check that validation fails now
err := ps.validator.ValidateProposal(ps.proposal)
assert.Error(ps.T(), err, "a proposal with an invalid QC should be rejected")
// check that the error is an invalid proposal error to allow creating slashing challenge
assert.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), "if we can't verify the QC, we should not generate a invalid error")
}
// TestProposalWithPreviousRankTimeoutCertificate tests different scenarios where last rank has ended with TC
// this requires including a valid PreviousRankTimeoutCertificate.
func (ps *ProposalSuite) TestProposalWithPreviousRankTimeoutCertificate() {
// assume all proposals are created by valid leader
ps.verifier.On("VerifyVote", mock.Anything).Return(nil)
ps.committee.On("LeaderForRank", mock.Anything).Return(ps.leader.Identity(), nil)
ps.Run("happy-path", func() {
state := helper.MakeState(
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: ps.state.Rank + 2,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal(helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(ps.state.ParentQuorumCertificate))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
ps.verifier.On("VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate).Return(nil).Once()
err := ps.validator.ValidateProposal(proposal)
require.NoError(ps.T(), err)
})
ps.Run("no-tc", func() {
state := helper.MakeState(
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: ps.state.Rank + 2,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal(helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
// in this case proposal without PreviousRankTimeoutCertificate is considered invalid
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyQuorumCertificate")
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.Run("tc-for-wrong-rank", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: ps.state.Rank + 2,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+10), // PreviousRankTimeoutCertificate.Rank must be equal to State.Rank-1
helper.WithTCNewestQC(ps.state.ParentQuorumCertificate))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyQuorumCertificate")
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.Run("proposal-not-safe-to-extend", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
// proposal is not safe to extend because included QC.Rank is higher that State.QC.Rank
helper.WithTCNewestQC(helper.MakeQC(helper.WithQCRank(ps.state.Rank+1))))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyQuorumCertificate")
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.Run("included-tc-highest-qc-not-highest", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(ps.state.ParentQuorumCertificate),
)),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
ps.verifier.On("VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate).Return(nil).Once()
// this is considered an invalid TC, because highest QC's rank is not equal to max{NewestQCRanks}
proposal.PreviousRankTimeoutCertificate.(*helper.TestTimeoutCertificate).LatestRanks[0] = proposal.PreviousRankTimeoutCertificate.GetLatestQuorumCert().GetRank() + 1
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err) && models.IsInvalidTimeoutCertificateError(err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.Run("included-tc-threshold-not-reached", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
// TC is signed by only one signer - insufficient to reach weight threshold
insufficientSignerIndices := []byte{0b00000001}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(insufficientSignerIndices), // one signer is not enough to reach threshold
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(ps.state.ParentQuorumCertificate),
)),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err) && models.IsInvalidTimeoutCertificateError(err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.Run("included-tc-highest-qc-invalid", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
// QC included in TC has rank below QC included in proposal
qc := helper.MakeQC(
helper.WithQCRank(ps.state.ParentQuorumCertificate.GetRank()-1),
helper.WithQCSigners(ps.indices))
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(qc))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
ps.verifier.On("VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate).Return(nil).Once()
ps.verifier.On("VerifyQuorumCertificate", qc).Return(models.ErrInvalidSignature).Once()
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err) && models.IsInvalidTimeoutCertificateError(err))
})
ps.Run("verify-qc-err-rank-for-unknown-rank", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
newestQC := helper.MakeQC(
helper.WithQCRank(ps.state.ParentQuorumCertificate.GetRank()-2),
helper.WithQCSigners(ps.indices))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(newestQC))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
ps.verifier.On("VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate).Return(nil).Once()
// Validating QC included in TC returns ErrRankUnknown
ps.verifier.On("VerifyQuorumCertificate", newestQC).Return(models.ErrRankUnknown).Once()
err := ps.validator.ValidateProposal(proposal)
require.Error(ps.T(), err)
require.False(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
require.False(ps.T(), models.IsInvalidTimeoutCertificateError(err))
require.NotErrorIs(ps.T(), err, models.ErrRankUnknown)
})
ps.Run("included-tc-invalid-sig", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.state.Rank+2),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithStateQC[*helper.TestState](ps.state.ParentQuorumCertificate))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC(
helper.WithTCSigners(ps.indices),
helper.WithTCRank(ps.state.Rank+1),
helper.WithTCNewestQC(ps.state.ParentQuorumCertificate))),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
ps.verifier.On("VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate).Return(models.ErrInvalidSignature).Once()
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err) && models.IsInvalidTimeoutCertificateError(err))
ps.verifier.AssertCalled(ps.T(), "VerifyTimeoutCertificate", proposal.PreviousRankTimeoutCertificate)
})
ps.Run("last-rank-successful-but-includes-tc", func() {
state := helper.MakeState[*helper.TestState](
helper.WithStateRank[*helper.TestState](ps.finalized+1),
helper.WithStateProposer[*helper.TestState](ps.leader.Identity()),
helper.WithParentSigners[*helper.TestState](ps.indices),
helper.WithParentState(ps.parent))
vote := &helper.TestVote{
Rank: state.Rank,
ID: ps.leader.Identity(),
StateID: state.Identifier,
Signature: make([]byte, 74),
}
proposal := helper.MakeSignedProposal[*helper.TestState, *helper.TestVote](helper.WithProposal[*helper.TestState, *helper.TestVote](helper.MakeProposal(
helper.WithState(state),
helper.WithPreviousRankTimeoutCertificate[*helper.TestState](helper.MakeTC()),
)), helper.WithVote[*helper.TestState, *helper.TestVote](&vote))
err := ps.validator.ValidateProposal(proposal)
require.True(ps.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err))
ps.verifier.AssertNotCalled(ps.T(), "VerifyTimeoutCertificate")
})
ps.verifier.AssertExpectations(ps.T())
}
func TestValidateVote(t *testing.T) {
suite.Run(t, new(VoteSuite))
}
type VoteSuite struct {
suite.Suite
signer models.WeightedIdentity
state *models.State[*helper.TestState]
vote *helper.TestVote
verifier *mocks.Verifier[*helper.TestVote]
committee *mocks.Replicas
validator *Validator[*helper.TestState, *helper.TestVote]
}
func (vs *VoteSuite) SetupTest() {
// create a random signing identity
vs.signer = helper.WithWeightedIdentityList(1)[0]
// create a state that should be signed
vs.state = helper.MakeState[*helper.TestState]()
// create a vote for this state
vs.vote = &helper.TestVote{
Rank: vs.state.Rank,
ID: vs.signer.Identity(),
StateID: vs.state.Identifier,
Signature: []byte{},
}
// set up the mocked verifier
vs.verifier = &mocks.Verifier[*helper.TestVote]{}
vs.verifier.On("VerifyVote", &vs.vote).Return(nil)
// the leader for the state rank is the correct one
vs.committee = &mocks.Replicas{}
vs.committee.On("IdentityByRank", mock.Anything, vs.signer.Identity()).Return(vs.signer, nil)
// set up the validator with the mocked dependencies
vs.validator = NewValidator[*helper.TestState, *helper.TestVote](vs.committee, vs.verifier)
}
// TestVoteOK checks the happy case, which is the default for the suite
func (vs *VoteSuite) TestVoteOK() {
_, err := vs.validator.ValidateVote(&vs.vote)
assert.NoError(vs.T(), err, "a valid vote should be accepted")
}
// TestVoteSignatureError checks that the Validator does not misinterpret
// unexpected exceptions for invalid votes.
func (vs *VoteSuite) TestVoteSignatureError() {
*vs.verifier = mocks.Verifier[*helper.TestVote]{}
vs.verifier.On("VerifyVote", &vs.vote).Return(fmt.Errorf("some exception"))
// check that the vote is no longer validated
_, err := vs.validator.ValidateVote(&vs.vote)
assert.Error(vs.T(), err, "a vote with error on signature validation should be rejected")
assert.False(vs.T(), models.IsInvalidVoteError[*helper.TestVote](err), "internal exception should not be interpreted as invalid vote")
}
// TestVoteVerifyVote_ErrRankUnknown tests if ValidateVote correctly handles VerifyVote's ErrRankUnknown sentinel error
// Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
func (vs *VoteSuite) TestVoteVerifyVote_ErrRankUnknown() {
*vs.verifier = mocks.Verifier[*helper.TestVote]{}
vs.verifier.On("VerifyVote", &vs.vote).Return(models.ErrRankUnknown)
// check that the vote is no longer validated
_, err := vs.validator.ValidateVote(&vs.vote)
assert.Error(vs.T(), err)
assert.False(vs.T(), models.IsInvalidVoteError[*helper.TestVote](err), "internal exception should not be interpreted as invalid vote")
assert.NotErrorIs(vs.T(), err, models.ErrRankUnknown, "we don't expect a sentinel error here")
}
// TestVoteInvalidSignerID checks that the Validator correctly handles a vote
// with a SignerID that does not correspond to a valid consensus participant.
// In this case, the `consensus.DynamicCommittee` returns a `models.InvalidSignerError`,
// which the Validator should recognize as a symptom for an invalid vote.
// Hence, we expect the validator to return a `models.InvalidVoteError`.
func (vs *VoteSuite) TestVoteInvalidSignerID() {
*vs.committee = mocks.Replicas{}
vs.committee.On("IdentityByRank", vs.state.Rank, vs.vote.ID).Return(nil, models.NewInvalidSignerErrorf(""))
// A `models.InvalidSignerError` from the committee should be interpreted as
// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
_, err := vs.validator.ValidateVote(&vs.vote)
assert.Error(vs.T(), err, "a vote with unknown SignerID should be rejected")
assert.True(vs.T(), models.IsInvalidVoteError[*helper.TestVote](err), "a vote with unknown SignerID should be rejected")
}
// TestVoteSignatureInvalid checks that the Validator correctly handles votes
// with cryptographically invalid consensus. In this case, the `consensus.Verifier`
// returns a `models.ErrInvalidSignature`, which the Validator should recognize as
// a symptom for an invalid vote.
// Hence, we expect the validator to return a `models.InvalidVoteError`.
func (vs *VoteSuite) TestVoteSignatureInvalid() {
*vs.verifier = mocks.Verifier[*helper.TestVote]{}
vs.verifier.On("VerifyVote", &vs.vote).Return(fmt.Errorf("staking sig is invalid: %w", models.ErrInvalidSignature))
// A `models.ErrInvalidSignature` from the `consensus.Verifier` should be interpreted as
// the Vote being invalid, i.e. we expect an InvalidVoteError to be returned
_, err := vs.validator.ValidateVote(&vs.vote)
assert.Error(vs.T(), err, "a vote with an invalid signature should be rejected")
assert.True(vs.T(), models.IsInvalidVoteError[*helper.TestVote](err), "a vote with an invalid signature should be rejected")
}
func TestValidateQuorumCertificate(t *testing.T) {
suite.Run(t, new(QCSuite))
}
type QCSuite struct {
suite.Suite
participants []models.WeightedIdentity
signers []models.WeightedIdentity
state *models.State[*helper.TestState]
qc models.QuorumCertificate
committee *mocks.Replicas
verifier *mocks.Verifier[*helper.TestVote]
validator *Validator[*helper.TestState, *helper.TestVote]
}
func (qs *QCSuite) SetupTest() {
// create a list of 10 nodes with 1-weight each
qs.participants = helper.WithWeightedIdentityList(10)
// signers are a qualified majority at 7
qs.signers = qs.participants[:7]
// create a state that has the signers in its QC
qs.state = helper.MakeState[*helper.TestState]()
indices := []byte{0b01111111, 0b00000000}
qs.qc = helper.MakeQC(helper.WithQCState[*helper.TestState](qs.state), helper.WithQCSigners(indices))
// return the correct participants and identities from rank state
qs.committee = &mocks.Replicas{}
qs.committee.On("IdentitiesByRank", mock.Anything).Return(
func(_ uint64) []models.WeightedIdentity {
return qs.participants
},
nil,
)
qs.committee.On("QuorumThresholdForRank", mock.Anything).Return(uint64(7000), nil)
// set up the mocked verifier to verify the QC correctly
qs.verifier = &mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(nil)
// set up the validator with the mocked dependencies
qs.validator = NewValidator[*helper.TestState, *helper.TestVote](qs.committee, qs.verifier)
}
// TestQCOK verifies the default happy case
func (qs *QCSuite) TestQCOK() {
// check the default happy case passes
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.NoError(qs.T(), err, "a valid QC should be accepted")
}
// TestQCRetrievingParticipantsError tests that validation errors if:
// there is an error retrieving identities of consensus participants
func (qs *QCSuite) TestQCRetrievingParticipantsError() {
// change the consensus.DynamicCommittee to fail on retrieving participants
*qs.committee = mocks.Replicas{}
qs.committee.On("IdentitiesByRank", mock.Anything).Return(qs.participants, errors.New("FATAL internal error"))
// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidQC
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.Error(qs.T(), err, "unspecific error when retrieving consensus participants should be escalated to surrounding logic")
assert.False(qs.T(), models.IsInvalidQuorumCertificateError(err), "unspecific internal errors should not result in ErrorInvalidQC error")
}
// TestQCSignersError tests that a qc fails validation if:
// QC signer's have insufficient weight (but are all valid consensus participants otherwise)
func (qs *QCSuite) TestQCInsufficientWeight() {
// signers only have weight 6 out of 10 total (NOT have a supermajority)
qs.signers = qs.participants[:6]
indices := []byte{0b00111111, 0b00000000}
qs.qc = helper.MakeQC(helper.WithQCState[*helper.TestState](qs.state), helper.WithQCSigners(indices))
// the QC should not be validated anymore
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.Error(qs.T(), err, "a QC should be rejected if it has insufficient voted weight")
// we should get a threshold error to bubble up for extra info
assert.True(qs.T(), models.IsInvalidQuorumCertificateError(err), "if there is insufficient voted weight, an invalid state error should be raised")
}
// TestQCSignatureError tests that validation errors if:
// there is an unspecific internal error while validating the signature
func (qs *QCSuite) TestQCSignatureError() {
// set up the verifier to fail QC verification
*qs.verifier = mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(errors.New("dummy error"))
// verifier should escalate unspecific internal error to surrounding logic, but NOT as ErrorInvalidQC
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.Error(qs.T(), err, "unspecific sig verification error should be escalated to surrounding logic")
assert.False(qs.T(), models.IsInvalidQuorumCertificateError(err), "unspecific internal errors should not result in ErrorInvalidQC error")
}
// TestQCSignatureInvalid verifies that the Validator correctly handles the models.ErrInvalidSignature.
// This error return from `Verifier.VerifyQuorumCertificate` is an expected failure case in case of a byzantine input, where
// one of the signatures in the QC is broken. Hence, the Validator should wrap it as InvalidProposalError.
func (qs *QCSuite) TestQCSignatureInvalid() {
// change the verifier to fail the QC signature
*qs.verifier = mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(fmt.Errorf("invalid qc: %w", models.ErrInvalidSignature))
// the QC should no longer pass validation
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.True(qs.T(), models.IsInvalidQuorumCertificateError(err), "if the signature is invalid an ErrorInvalidQC error should be raised")
}
// TestQCVerifyQuorumCertificate_ErrRankUnknown tests if ValidateQuorumCertificate correctly handles VerifyQuorumCertificate's ErrRankUnknown sentinel error
// Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
func (qs *QCSuite) TestQCVerifyQuorumCertificate_ErrRankUnknown() {
*qs.verifier = mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(models.ErrRankUnknown)
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.Error(qs.T(), err)
assert.False(qs.T(), models.IsInvalidQuorumCertificateError(err), "we don't expect a sentinel error here")
assert.NotErrorIs(qs.T(), err, models.ErrRankUnknown, "we don't expect a sentinel error here")
}
// TestQCSignatureInvalidFormat verifies that the Validator correctly handles the models.InvalidFormatError.
// This error return from `Verifier.VerifyQuorumCertificate` is an expected failure case in case of a byzantine input, where
// some binary vector (e.g. `sigData`) is broken. Hence, the Validator should wrap it as InvalidProposalError.
func (qs *QCSuite) TestQCSignatureInvalidFormat() {
// change the verifier to fail the QC signature
*qs.verifier = mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(models.NewInvalidFormatErrorf("invalid sigType"))
// the QC should no longer pass validation
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.True(qs.T(), models.IsInvalidQuorumCertificateError(err), "if the signature has an invalid format, an ErrorInvalidQC error should be raised")
}
// TestQCEmptySigners verifies that the Validator correctly handles the models.InsufficientSignaturesError:
// In the validator, we previously checked the total weight of all signers meets the supermajority threshold,
// which is a _positive_ number. Hence, there must be at least one signer. Hence, `Verifier.VerifyQuorumCertificate`
// returning this error would be a symptom of a fatal internal bug. The Validator should _not_ interpret
// this error as an invalid QC / invalid state, i.e. it should _not_ return an `InvalidProposalError`.
func (qs *QCSuite) TestQCEmptySigners() {
*qs.verifier = mocks.Verifier[*helper.TestVote]{}
qs.verifier.On("VerifyQuorumCertificate", qs.qc).Return(
fmt.Errorf("%w", models.NewInsufficientSignaturesErrorf("")))
// the Validator should _not_ interpret this as a invalid QC, but as an internal error
err := qs.validator.ValidateQuorumCertificate(qs.qc)
assert.True(qs.T(), models.IsInsufficientSignaturesError(err)) // unexpected error should be wrapped and propagated upwards
assert.False(qs.T(), models.IsInvalidProposalError[*helper.TestState, *helper.TestVote](err), err, "should _not_ interpret this as a invalid QC, but as an internal error")
}
func TestValidateTimeoutCertificate(t *testing.T) {
suite.Run(t, new(TCSuite))
}
type TCSuite struct {
suite.Suite
participants []models.WeightedIdentity
signers []models.WeightedIdentity
indices []byte
state *models.State[*helper.TestState]
tc models.TimeoutCertificate
committee *mocks.DynamicCommittee
verifier *mocks.Verifier[*helper.TestVote]
validator *Validator[*helper.TestState, *helper.TestVote]
}
func (s *TCSuite) SetupTest() {
// create a list of 10 nodes with 1-weight each
s.participants = helper.WithWeightedIdentityList(10)
// signers are a qualified majority at 7
s.signers = s.participants[:7]
var err error
s.indices = []byte{0b01111111, 0b00000000}
require.NoError(s.T(), err)
rank := uint64(int(rand.Uint32()) + len(s.participants))
highQCRanks := make([]uint64, 0, len(s.signers))
for i := range s.signers {
highQCRanks = append(highQCRanks, rank-uint64(i)-1)
}
rand.Shuffle(len(highQCRanks), func(i, j int) {
highQCRanks[i], highQCRanks[j] = highQCRanks[j], highQCRanks[i]
})
// create a state that has the signers in its QC
parent := helper.MakeState[*helper.TestState](helper.WithStateRank[*helper.TestState](rank - 1))
s.state = helper.MakeState[*helper.TestState](helper.WithStateRank[*helper.TestState](rank),
helper.WithParentState(parent),
helper.WithParentSigners[*helper.TestState](s.indices))
s.tc = helper.MakeTC(helper.WithTCNewestQC(s.state.ParentQuorumCertificate),
helper.WithTCRank(rank+1),
helper.WithTCSigners(s.indices),
helper.WithTCHighQCRanks(highQCRanks))
// return the correct participants and identities from rank state
s.committee = &mocks.DynamicCommittee{}
s.committee.On("IdentitiesByRank", mock.Anything, mock.Anything).Return(
func(rank uint64) []models.WeightedIdentity {
return s.participants
},
nil,
)
s.committee.On("QuorumThresholdForRank", mock.Anything).Return(uint64(7000), nil)
s.verifier = &mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyQuorumCertificate", s.state.ParentQuorumCertificate).Return(nil)
// set up the validator with the mocked dependencies
s.validator = NewValidator[*helper.TestState, *helper.TestVote](s.committee, s.verifier)
}
// TestTCOk tests if happy-path returns correct result
func (s *TCSuite) TestTCOk() {
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(nil).Once()
// check the default happy case passes
err := s.validator.ValidateTimeoutCertificate(s.tc)
assert.NoError(s.T(), err, "a valid TC should be accepted")
}
// TestTCNewestQCFromFuture tests if correct error is returned when included QC is higher than TC's rank
func (s *TCSuite) TestTCNewestQCFromFuture() {
// highest QC from future rank
s.tc.(*helper.TestTimeoutCertificate).LatestQuorumCert.(*helper.TestQuorumCertificate).Rank = s.tc.GetRank() + 1
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if NewestQC.Rank > TC.Rank, an ErrorInvalidTC error should be raised")
}
// TestTCNewestQCIsNotHighest tests if correct error is returned when included QC is not highest
func (s *TCSuite) TestTCNewestQCIsNotHighest() {
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(nil).Once()
// highest QC rank is not equal to max(TONewestQCRanks)
s.tc.(*helper.TestTimeoutCertificate).LatestRanks[0] = s.tc.GetLatestQuorumCert().GetRank() + 1
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if max(highQCRanks) != NewestQC.Rank, an ErrorInvalidTC error should be raised")
}
// TestTCInvalidSigners tests if correct error is returned when signers are invalid
func (s *TCSuite) TestTCInvalidSigners() {
s.participants = s.participants[:6] // remove participant[6+] from the list of valid consensus participant
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if some signers are invalid consensus participants, an ErrorInvalidTC error should be raised")
}
// TestTCThresholdNotReached tests if correct error is returned when TC's singers don't have enough weight
func (s *TCSuite) TestTCThresholdNotReached() {
// signers only have weight 1 out of 10 total (NOT have a supermajority)
s.signers = s.participants[:1]
indices := []byte{0b00000001, 0b00000000}
s.tc.(*helper.TestTimeoutCertificate).AggregatedSignature.(*helper.TestAggregatedSignature).Bitmask = indices
// adjust signers to be less than total weight
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if signers don't have enough weight, an ErrorInvalidTC error should be raised")
}
// TestTCInvalidNewestQC tests if correct error is returned when included highest QC is invalid
func (s *TCSuite) TestTCInvalidNewestQC() {
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(nil).Once()
s.verifier.On("VerifyQuorumCertificate", s.tc.GetLatestQuorumCert()).Return(models.NewInvalidFormatErrorf("invalid qc")).Once()
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if included QC is invalid, an ErrorInvalidTC error should be raised")
}
// TestTCVerifyQuorumCertificate_ErrRankUnknown tests if ValidateTimeoutCertificate correctly handles VerifyQuorumCertificate's ErrRankUnknown sentinel error
// Validator shouldn't return a sentinel error here because this behavior is a symptom of internal bug, this behavior is not expected.
func (s *TCSuite) TestTCVerifyQuorumCertificate_ErrRankUnknown() {
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(nil).Once()
s.verifier.On("VerifyQuorumCertificate", s.tc.GetLatestQuorumCert()).Return(models.ErrRankUnknown).Once()
err := s.validator.ValidateTimeoutCertificate(s.tc) // the QC should not be validated anymore
assert.Error(s.T(), err)
assert.False(s.T(), models.IsInvalidTimeoutCertificateError(err), "we don't expect a sentinel error here")
assert.NotErrorIs(s.T(), err, models.ErrRankUnknown, "we don't expect a sentinel error here")
}
// TestTCInvalidSignature tests a few scenarios when the signature is invalid or TC signers is malformed
func (s *TCSuite) TestTCInvalidSignature() {
s.Run("insufficient-signatures", func() {
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyQuorumCertificate", mock.Anything).Return(nil).Once()
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(models.NewInsufficientSignaturesErrorf("")).Once()
// the Validator should _not_ interpret this as an invalid TC, but as an internal error
err := s.validator.ValidateTimeoutCertificate(s.tc)
assert.True(s.T(), models.IsInsufficientSignaturesError(err)) // unexpected error should be wrapped and propagated upwards
assert.False(s.T(), models.IsInvalidTimeoutCertificateError(err), "should _not_ interpret this as a invalid TC, but as an internal error")
})
s.Run("invalid-format", func() {
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyQuorumCertificate", mock.Anything).Return(nil).Once()
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(models.NewInvalidFormatErrorf("")).Once()
err := s.validator.ValidateTimeoutCertificate(s.tc)
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if included TC's inputs are invalid, an ErrorInvalidTC error should be raised")
})
s.Run("invalid-signature", func() {
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyQuorumCertificate", mock.Anything).Return(nil).Once()
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(models.ErrInvalidSignature).Once()
err := s.validator.ValidateTimeoutCertificate(s.tc)
assert.True(s.T(), models.IsInvalidTimeoutCertificateError(err), "if included TC's signature is invalid, an ErrorInvalidTC error should be raised")
})
s.Run("verify-sig-exception", func() {
exception := errors.New("verify-sig-exception")
*s.verifier = mocks.Verifier[*helper.TestVote]{}
s.verifier.On("VerifyQuorumCertificate", mock.Anything).Return(nil).Once()
s.verifier.On("VerifyTimeoutCertificate", s.tc).Return(exception).Once()
err := s.validator.ValidateTimeoutCertificate(s.tc)
assert.ErrorIs(s.T(), err, exception, "if included TC's signature is invalid, an exception should be propagated")
assert.False(s.T(), models.IsInvalidTimeoutCertificateError(err))
})
}