mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 18:37:26 +08:00
* 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>
934 lines
45 KiB
Go
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))
|
|
})
|
|
}
|