mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 10:27:26 +08:00
570 lines
18 KiB
Go
570 lines
18 KiB
Go
package validator
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus"
|
|
"source.quilibrium.com/quilibrium/monorepo/consensus/models"
|
|
)
|
|
|
|
// Validator is responsible for validating QC, State and Vote
|
|
type Validator[StateT models.Unique, VoteT models.Unique] struct {
|
|
committee consensus.Replicas
|
|
verifier consensus.Verifier[VoteT]
|
|
}
|
|
|
|
var _ consensus.Validator[*nilUnique, *nilUnique] = (*Validator[*nilUnique, *nilUnique])(nil)
|
|
|
|
// New creates a new Validator instance
|
|
func NewValidator[StateT models.Unique, VoteT models.Unique](
|
|
committee consensus.Replicas,
|
|
verifier consensus.Verifier[VoteT],
|
|
) *Validator[StateT, VoteT] {
|
|
return &Validator[StateT, VoteT]{
|
|
committee: committee,
|
|
verifier: verifier,
|
|
}
|
|
}
|
|
|
|
// ValidateTimeoutCertificate validates the TimeoutCertificate `TC`.
|
|
// During normal operations, the following error returns are expected:
|
|
// - models.InvalidTCError if the TC is invalid
|
|
// - models.ErrRankUnknown if the TC refers unknown rank
|
|
//
|
|
// Any other error should be treated as exception
|
|
func (v *Validator[StateT, VoteT]) ValidateTimeoutCertificate(
|
|
tc models.TimeoutCertificate,
|
|
) error {
|
|
newestQC := tc.GetLatestQuorumCert()
|
|
if newestQC == nil {
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf("TC must include a QC but found nil"),
|
|
)
|
|
}
|
|
|
|
// The TC's rank cannot be smaller than the rank of the QC it contains.
|
|
// Note: we specifically allow for the TC to have the same rank as the highest
|
|
// QC. This is useful as a fallback, because it allows replicas other than the
|
|
// designated leader to also collect votes and generate a QC.
|
|
if tc.GetRank() < newestQC.GetRank() {
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf("TC's QC cannot be newer than the TC's rank"),
|
|
)
|
|
}
|
|
|
|
// 1. Check if there is super-majority of votes
|
|
allParticipants, err := v.committee.IdentitiesByRank(tc.GetRank())
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"could not get consensus participants at rank %d: %w",
|
|
tc.GetRank(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
signerIDs := []models.WeightedIdentity{}
|
|
sigIndices := tc.GetAggregatedSignature().GetBitmask()
|
|
totalWeight := uint64(0)
|
|
if len(sigIndices) < (len(allParticipants)+7)/8 {
|
|
return models.NewInsufficientSignaturesErrorf("insufficient signatures")
|
|
}
|
|
for i, member := range allParticipants {
|
|
if sigIndices[i/8]&(1<<(i%8)) == (1 << (i % 8)) {
|
|
signerIDs = append(signerIDs, member)
|
|
totalWeight += member.Weight()
|
|
}
|
|
}
|
|
|
|
// determine whether signers reach minimally required weight threshold for
|
|
// consensus
|
|
threshold, err := v.committee.QuorumThresholdForRank(tc.GetRank())
|
|
if err != nil {
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf(
|
|
"could not get weight threshold for rank %d: %w",
|
|
tc.GetRank(),
|
|
err,
|
|
),
|
|
)
|
|
}
|
|
|
|
if totalWeight < threshold {
|
|
return newInvalidTimeoutCertificateError(tc, fmt.Errorf(
|
|
"tc signers have insufficient weight of %d (required=%d)",
|
|
totalWeight,
|
|
threshold,
|
|
))
|
|
}
|
|
|
|
// Verify multi-message BLS sig of TC, by far the most expensive check
|
|
err = v.verifier.VerifyTimeoutCertificate(tc)
|
|
if err != nil {
|
|
// Considerations about other errors that `VerifyTC` could return:
|
|
// * models.InsufficientSignaturesError: 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,
|
|
// receiving this error would be a symptom of a fatal internal bug.
|
|
switch {
|
|
case models.IsInvalidFormatError(err):
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf("TC's signature data has an invalid structure: %w", err),
|
|
)
|
|
case errors.Is(err, models.ErrInvalidSignature):
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf("TC contains invalid signature(s): %w", err),
|
|
)
|
|
default:
|
|
return fmt.Errorf(
|
|
"cannot verify tc's aggregated signature (tc.Rank: %d): %w",
|
|
tc.GetRank(),
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
// verifying that tc.NewestQC is the QC with the highest rank.
|
|
// Note: A byzantine TC could include `nil` for tc.NewestQCRanks, in which
|
|
// case `tc.NewestQCRanks[0]` would panic. Though, per API specification
|
|
// `verifier.VerifyTC(…)` should return a `models.InvalidFormatError` if
|
|
// `signers` and `tc.NewestQCRanks` have different length. Hence, the
|
|
// following code is safe only if it is executed
|
|
// 1. _after_ checking the quorum threshold (thereby we guarantee that
|
|
// `signers` is not empty); and
|
|
// 2. _after_ `verifier.VerifyTC(…)`, which enforces that `signers` and
|
|
// `tc.NewestQCRanks` have identical length.
|
|
// Only then we can be sure that `tc.NewestQCRanks` cannot be nil.
|
|
newestQCRank := tc.GetLatestRanks()[0]
|
|
for _, rank := range tc.GetLatestRanks() {
|
|
if newestQCRank < rank {
|
|
newestQCRank = rank
|
|
}
|
|
}
|
|
if newestQCRank > tc.GetLatestQuorumCert().GetRank() {
|
|
return newInvalidTimeoutCertificateError(
|
|
tc,
|
|
fmt.Errorf(
|
|
"included QC (rank=%d) should be equal or higher to highest contributed rank: %d",
|
|
tc.GetLatestQuorumCert().GetRank(),
|
|
newestQCRank,
|
|
),
|
|
)
|
|
}
|
|
|
|
// Validate QC
|
|
err = v.ValidateQuorumCertificate(newestQC)
|
|
if err != nil {
|
|
if models.IsInvalidQuorumCertificateError(err) {
|
|
return newInvalidTimeoutCertificateError(tc, fmt.Errorf(
|
|
"invalid QC included in TC: %w",
|
|
err,
|
|
))
|
|
}
|
|
if errors.Is(err, models.ErrRankUnknown) {
|
|
// We require each replica to be bootstrapped with a QC pointing to a
|
|
// finalized state. Consensus safety rules guarantee that a QC at least as
|
|
// new as the root QC must be contained in any TC. This is because the TC
|
|
// must include signatures from a supermajority of replicas, including at
|
|
// least one honest replica, which attest to their locally highest known
|
|
// QC. Hence, any QC included in a TC must be the root QC or newer.
|
|
// Therefore, we should know the rank for any QC we encounter. Receiving
|
|
// a `models.ErrRankUnknown` is conceptually impossible, i.e. a symptom of
|
|
// an internal bug or invalid bootstrapping information.
|
|
return fmt.Errorf(
|
|
"no rank information availalbe for QC that was included in TC; symptom of internal bug or invalid bootstrapping information: %s",
|
|
err.Error(),
|
|
)
|
|
}
|
|
return fmt.Errorf(
|
|
"unexpected internal error while verifying the QC included in the TC: %w",
|
|
err,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateQuorumCertificate validates the Quorum Certificate `qc`.
|
|
// During normal operations, the following error returns are expected:
|
|
// - models.InvalidQCError if the QC is invalid
|
|
// - models.ErrRankUnknown if the QC refers unknown rank
|
|
//
|
|
// Any other error should be treated as exception
|
|
func (v *Validator[StateT, VoteT]) ValidateQuorumCertificate(
|
|
qc models.QuorumCertificate,
|
|
) error {
|
|
// Retrieve the initial identities of consensus participants for this rank,
|
|
// and those that signed the QC. IdentitiesByRank contains all nodes that were
|
|
// authorized to sign during this rank. Ejection and dynamic weight
|
|
// adjustments are not taken into account here. By using an rank-static set
|
|
// of authorized signers, we can check QC validity without needing all
|
|
// ancestor states.
|
|
allParticipants, err := v.committee.IdentitiesByRank(qc.GetRank())
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"could not get consensus participants at rank %d: %w",
|
|
qc.GetRank(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
signerIDs := []models.WeightedIdentity{}
|
|
sigIndices := qc.GetAggregatedSignature().GetBitmask()
|
|
totalWeight := uint64(0)
|
|
if len(sigIndices) < (len(allParticipants)+7)/8 {
|
|
return newInvalidQuorumCertificateError(
|
|
qc,
|
|
models.NewInsufficientSignaturesErrorf("insufficient signatures"),
|
|
)
|
|
}
|
|
for i, member := range allParticipants {
|
|
if sigIndices[i/8]&(1<<(i%8)) == (1 << (i % 8)) {
|
|
signerIDs = append(signerIDs, member)
|
|
totalWeight += member.Weight()
|
|
}
|
|
}
|
|
|
|
// determine whether signers reach minimally required weight threshold for
|
|
// consensus
|
|
threshold, err := v.committee.QuorumThresholdForRank(qc.GetRank())
|
|
if err != nil {
|
|
return newInvalidQuorumCertificateError(
|
|
qc,
|
|
fmt.Errorf(
|
|
"could not get weight threshold for rank %d: %w",
|
|
qc.GetRank(),
|
|
err,
|
|
),
|
|
)
|
|
}
|
|
|
|
if totalWeight < threshold {
|
|
return newInvalidQuorumCertificateError(
|
|
qc,
|
|
fmt.Errorf(
|
|
"QC signers have insufficient weight of %d (required=%d)",
|
|
totalWeight,
|
|
threshold,
|
|
),
|
|
)
|
|
}
|
|
|
|
// verify whether the signature bytes are valid for the QC
|
|
err = v.verifier.VerifyQuorumCertificate(qc)
|
|
if err != nil {
|
|
// Considerations about other errors that `VerifyQC` could return:
|
|
// * models.InvalidSignerError
|
|
// * models.InsufficientSignaturesError: 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,
|
|
// receiving this error would be a symptom of a fatal internal bug.
|
|
switch {
|
|
case models.IsInvalidFormatError(err):
|
|
return newInvalidQuorumCertificateError(
|
|
qc,
|
|
fmt.Errorf("QC's signature data has an invalid structure: %w", err),
|
|
)
|
|
case errors.Is(err, models.ErrInvalidSignature):
|
|
return newInvalidQuorumCertificateError(
|
|
qc,
|
|
fmt.Errorf("QC contains invalid signature(s): %w", err),
|
|
)
|
|
case errors.Is(err, models.ErrRankUnknown):
|
|
// We have earlier queried the Identities for the QC's rank, which must
|
|
// have returned proper values, otherwise, we wouldn't reach this code.
|
|
// Therefore, it should be impossible for `verifier.VerifyQC` to return
|
|
// ErrRankUnknown. To avoid confusion with expected sentinel errors, we
|
|
// only preserve the error messages here, but not the error types.
|
|
return fmt.Errorf(
|
|
"internal error, as querying identities for rank %d succeeded earlier but now the rank supposedly belongs to an unknown rank: %s",
|
|
qc.GetRank(),
|
|
err.Error(),
|
|
)
|
|
default:
|
|
return fmt.Errorf(
|
|
"cannot verify qc's aggregated signature (qc.Identifier: %x): %w",
|
|
qc.Identity(),
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateProposal validates the state proposal
|
|
// A state is considered as valid if it's a valid extension of existing forks.
|
|
// Note it doesn't check if it's conflicting with finalized state
|
|
// During normal operations, the following error returns are expected:
|
|
// - models.InvalidProposalError if the state is invalid
|
|
// - models.ErrRankUnknown if the proposal refers unknown rank
|
|
//
|
|
// Any other error should be treated as exception
|
|
func (v *Validator[StateT, VoteT]) ValidateProposal(
|
|
proposal *models.SignedProposal[StateT, VoteT],
|
|
) error {
|
|
qc := proposal.State.ParentQuorumCertificate
|
|
state := proposal.State
|
|
|
|
// validate the proposer's vote and get their identity
|
|
vote, err := proposal.ProposerVote()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get vote from proposer vote: %w", err)
|
|
}
|
|
_, err = v.ValidateVote(vote)
|
|
if models.IsInvalidVoteError[VoteT](err) {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"invalid proposer signature: %w",
|
|
err,
|
|
)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"error verifying leader signature for state %x: %w",
|
|
state.Identifier,
|
|
err,
|
|
)
|
|
}
|
|
|
|
// check the proposer is the leader for the proposed state's rank
|
|
leader, err := v.committee.LeaderForRank(
|
|
state.Rank,
|
|
state.ParentQuorumCertificate.Identity(),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"error determining leader for state %x: %w",
|
|
state.Identifier,
|
|
err,
|
|
)
|
|
}
|
|
if leader != state.ProposerID {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"proposer %s is not leader (%s) for rank %d",
|
|
state.ProposerID,
|
|
leader,
|
|
state.Rank,
|
|
)
|
|
}
|
|
|
|
// The State must contain a proof that the primary legitimately entered the
|
|
// respective rank. Transitioning to proposal.State.Rank is possible either by
|
|
// observing a QC or a TC for the previous round. If and only if the QC is
|
|
// _not_ for the previous round we require a TC for the previous rank to be
|
|
// present.
|
|
lastRankSuccessful := proposal.State.Rank ==
|
|
proposal.State.ParentQuorumCertificate.GetRank()+1
|
|
if !lastRankSuccessful {
|
|
// check if proposal is correctly structured
|
|
if proposal.PreviousRankTimeoutCertificate == nil {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"QC in state is not for previous rank, so expecting a TC but none is included in state",
|
|
)
|
|
}
|
|
|
|
// check if included TC is for previous rank
|
|
if proposal.State.Rank !=
|
|
proposal.PreviousRankTimeoutCertificate.GetRank()+1 {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"QC in state is not for previous rank, so expecting a TC for rank %d but got TC for rank %d",
|
|
proposal.State.Rank-1,
|
|
proposal.PreviousRankTimeoutCertificate.GetRank(),
|
|
)
|
|
}
|
|
|
|
// Check if proposal extends either the newest QC specified in the TC, or a
|
|
// newer QC in edge cases a leader may construct a TC and QC concurrently
|
|
// such that TC contains an older QC - in these case we still want to build
|
|
// on the newest QC, so this case is allowed.
|
|
if proposal.State.ParentQuorumCertificate.GetRank() <
|
|
proposal.PreviousRankTimeoutCertificate.GetLatestQuorumCert().GetRank() {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"TC in state contains a newer QC than the state itself, which is a protocol violation",
|
|
)
|
|
}
|
|
} else if proposal.PreviousRankTimeoutCertificate != nil {
|
|
// last rank ended with QC, including TC is a protocol violation
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"last rank has ended with QC but proposal includes PreviousRankTimeoutCertificate",
|
|
)
|
|
}
|
|
|
|
// Check signatures, keep the most expensive the last to check
|
|
|
|
// check if included QC is valid
|
|
err = v.ValidateQuorumCertificate(qc)
|
|
if err != nil {
|
|
if models.IsInvalidQuorumCertificateError(err) {
|
|
return models.NewInvalidProposalErrorf(proposal, "invalid qc included: %w", err)
|
|
}
|
|
if errors.Is(err, models.ErrRankUnknown) {
|
|
// We require each replica to be bootstrapped with a QC pointing to a
|
|
// finalized state. Therefore, receiving a `models.ErrRankUnknown` is
|
|
// conceptually impossible, i.e. a symptom of an internal bug or invalid
|
|
// bootstrapping information.
|
|
return fmt.Errorf(
|
|
"no rank information availalbe for QC that was included in proposal; symptom of internal bug or invalid bootstrapping information: %s",
|
|
err.Error(),
|
|
)
|
|
}
|
|
return fmt.Errorf("unexpected error verifying qc: %w", err)
|
|
}
|
|
|
|
if !lastRankSuccessful {
|
|
// check if included TC is valid
|
|
err = v.ValidateTimeoutCertificate(proposal.PreviousRankTimeoutCertificate)
|
|
if err != nil {
|
|
if models.IsInvalidTimeoutCertificateError(err) {
|
|
return models.NewInvalidProposalErrorf(
|
|
proposal,
|
|
"proposals TC's is not valid: %w",
|
|
err,
|
|
)
|
|
}
|
|
if errors.Is(err, models.ErrRankUnknown) {
|
|
// We require each replica to be bootstrapped with a QC pointing to a
|
|
// finalized state. Therefore, we should know the rank for any QC.Rank
|
|
// and TC.Rank we encounter. Receiving a `models.ErrRankUnknown` is
|
|
// conceptually impossible, i.e. a symptom of an internal bug or invalid
|
|
// bootstrapping information.
|
|
return fmt.Errorf(
|
|
"no rank information availalbe for QC that was included in TC; symptom of internal bug or invalid bootstrapping information: %s",
|
|
err.Error(),
|
|
)
|
|
}
|
|
return fmt.Errorf(
|
|
"unexpected internal error while verifying the TC included in state: %w",
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateVote validates the vote and returns the identity of the voter who
|
|
// signed the vote - the vote to be validated
|
|
// During normal operations, the following error returns are expected:
|
|
// - models.InvalidVoteError for invalid votes
|
|
// - models.ErrRankUnknown if the vote refers unknown rank
|
|
//
|
|
// Any other error should be treated as exception
|
|
func (v *Validator[StateT, VoteT]) ValidateVote(vote *VoteT) (
|
|
*models.WeightedIdentity,
|
|
error,
|
|
) {
|
|
voter, err := v.committee.IdentityByRank(
|
|
(*vote).GetRank(),
|
|
(*vote).Identity(),
|
|
)
|
|
if models.IsInvalidSignerError(err) {
|
|
return nil, newInvalidVoteError(vote, err)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf(
|
|
"error retrieving voter Identity at rank %d: %w",
|
|
(*vote).GetRank(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
// check whether the signature data is valid for the vote in the hotstuff
|
|
// context
|
|
err = v.verifier.VerifyVote(vote)
|
|
if err != nil {
|
|
// Theoretically, `VerifyVote` could also return a
|
|
// `models.InvalidSignerError`.
|
|
if models.IsInvalidFormatError(err) ||
|
|
errors.Is(err, models.ErrInvalidSignature) {
|
|
return nil, newInvalidVoteError(vote, err)
|
|
}
|
|
if errors.Is(err, models.ErrRankUnknown) {
|
|
return nil, fmt.Errorf(
|
|
"no rank information available for vote; symptom of internal bug or invalid bootstrapping information: %s",
|
|
err.Error(),
|
|
)
|
|
}
|
|
return nil, fmt.Errorf(
|
|
"cannot verify signature for vote (%x): %w",
|
|
(*vote).Identity(),
|
|
err,
|
|
)
|
|
}
|
|
|
|
return &voter, nil
|
|
}
|
|
|
|
func newInvalidQuorumCertificateError(
|
|
qc models.QuorumCertificate,
|
|
err error,
|
|
) error {
|
|
return models.InvalidQuorumCertificateError{
|
|
Identifier: qc.Identity(),
|
|
Rank: qc.GetRank(),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
func newInvalidTimeoutCertificateError(
|
|
tc models.TimeoutCertificate,
|
|
err error,
|
|
) error {
|
|
return models.InvalidTimeoutCertificateError{
|
|
Rank: tc.GetRank(),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
func newInvalidVoteError[VoteT models.Unique](vote *VoteT, err error) error {
|
|
return models.InvalidVoteError[VoteT]{
|
|
Vote: vote,
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// Type used to satisfy generic arguments in compiler time type assertion check
|
|
type nilUnique struct{}
|
|
|
|
// GetSignature implements models.Unique.
|
|
func (n *nilUnique) GetSignature() []byte {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// GetTimestamp implements models.Unique.
|
|
func (n *nilUnique) GetTimestamp() uint64 {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// Source implements models.Unique.
|
|
func (n *nilUnique) Source() models.Identity {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// Clone implements models.Unique.
|
|
func (n *nilUnique) Clone() models.Unique {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// GetRank implements models.Unique.
|
|
func (n *nilUnique) GetRank() uint64 {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
// Identity implements models.Unique.
|
|
func (n *nilUnique) Identity() models.Identity {
|
|
panic("unimplemented")
|
|
}
|
|
|
|
var _ models.Unique = (*nilUnique)(nil)
|