Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Round-Change validation #358

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qbft/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (i *Instance) BaseMsgValidation(msg *SignedMessage) error {
i.State.Share.Committee,
)
case RoundChangeMsgType:
return validRoundChangeForData(i.State, i.config, msg, i.State.Height, msg.Message.Round, msg.FullData)
return validSignedRoundChangeForData(i.State, i.config, msg, i.State.Height, msg.Message.Round, msg.FullData)
default:
return errors.New("signed message type not supported")
}
Expand Down
32 changes: 27 additions & 5 deletions qbft/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package qbft

import (
"bytes"

"github.com/bloxapp/ssv-spec/types"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -46,6 +47,7 @@ func (i *Instance) uponPrepare(signedPrepare *SignedMessage, prepareMsgContainer

// getRoundChangeJustification returns the round change justification for the current round.
// The justification is a quorum of signed prepare messages that agree on state.LastPreparedValue
// It assumes that the prepareMsgContainer has only messages with valid signatures
func getRoundChangeJustification(state *State, config IConfig, prepareMsgContainer *MsgContainer) ([]*SignedMessage, error) {
if state.LastPreparedValue == nil {
return nil, nil
Expand All @@ -59,7 +61,9 @@ func getRoundChangeJustification(state *State, config IConfig, prepareMsgContain
prepareMsgs := prepareMsgContainer.MessagesForRound(state.LastPreparedRound)
ret := make([]*SignedMessage, 0)
for _, msg := range prepareMsgs {
if err := validSignedPrepareForHeightRoundAndRoot(
// Calls validPrepareForHeightRoundAndRoot instead of validSignedPrepareForHeightRoundAndRoot
// since signatures are assumed to be valid
if err := validPrepareForHeightRoundAndRoot(
config,
msg,
state.Height,
Expand All @@ -86,6 +90,28 @@ func validSignedPrepareForHeightRoundAndRoot(
round Round,
root [32]byte,
operators []*types.Operator) error {

if err := validPrepareForHeightRoundAndRoot(config, signedPrepare, height, round, root, operators); err != nil {
return err
}

if err := signedPrepare.Signature.VerifyByOperators(signedPrepare, config.GetSignatureDomainType(), types.QBFTSignatureType, operators); err != nil {
return errors.Wrap(err, "msg signature invalid")
}

return nil
}

// validPrepareForHeightRoundAndRoot verifies if a Prepare message is prepared for a certain round and root
// similar to the dafny's validSignedPrepareForHeightRoundAndDigest predicate.
// However, it doesn't verify the message signature
func validPrepareForHeightRoundAndRoot(
config IConfig,
signedPrepare *SignedMessage,
height Height,
round Round,
root [32]byte,
operators []*types.Operator) error {
if signedPrepare.Message.MsgType != PrepareMsgType {
return errors.New("prepare msg type is wrong")
}
Expand All @@ -108,10 +134,6 @@ func validSignedPrepareForHeightRoundAndRoot(
return errors.New("msg allows 1 signer")
}

if err := signedPrepare.Signature.VerifyByOperators(signedPrepare, config.GetSignatureDomainType(), types.QBFTSignatureType, operators); err != nil {
return errors.Wrap(err, "msg signature invalid")
}

return nil
}

Expand Down
45 changes: 34 additions & 11 deletions qbft/proposal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package qbft

import (
"bytes"

"github.com/bloxapp/ssv-spec/types"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -93,6 +94,7 @@ func isValidProposal(
signedProposal.Message.Round,
signedProposal.FullData,
valCheck,
true,
); err != nil {
return errors.Wrap(err, "proposal not justified")
}
Expand All @@ -114,6 +116,7 @@ func isProposalJustification(
round Round,
fullData []byte,
valCheck ProposedValueCheckF,
verifySignatures bool,
) error {
if err := valCheck(fullData); err != nil {
return errors.Wrap(err, "proposal fullData invalid")
Expand All @@ -126,9 +129,16 @@ func isProposalJustification(
// no quorum, duplicate signers, invalid still has quorum, invalid no quorum
// prepared
for _, rc := range roundChangeMsgs {
if err := validRoundChangeForData(state, config, rc, height, round, fullData); err != nil {
return errors.Wrap(err, "change round msg not valid")
if verifySignatures {
if err := validSignedRoundChangeForData(state, config, rc, height, round, fullData); err != nil {
return errors.Wrap(err, "change round msg not valid")
}
} else {
if err := validRoundChangeForData(state, config, rc, height, round, fullData); err != nil {
return errors.Wrap(err, "change round msg not valid")
}
}

}

// check there is a quorum
Expand Down Expand Up @@ -178,15 +188,28 @@ func isProposalJustification(

// validate each prepare message against the highest previously prepared fullData and round
for _, pm := range prepareMsgs {
if err := validSignedPrepareForHeightRoundAndRoot(
config,
pm,
height,
rcm.Message.DataRound,
rcm.Message.Root,
state.Share.Committee,
); err != nil {
return errors.New("signed prepare not valid")
if verifySignatures {
if err := validSignedPrepareForHeightRoundAndRoot(
config,
pm,
height,
rcm.Message.DataRound,
rcm.Message.Root,
state.Share.Committee,
); err != nil {
return errors.New("signed prepare not valid")
}
} else {
if err := validPrepareForHeightRoundAndRoot(
config,
pm,
height,
rcm.Message.DataRound,
rcm.Message.Root,
state.Share.Committee,
); err != nil {
return errors.New("signed prepare not valid")
}
}
}
return nil
Expand Down
70 changes: 70 additions & 0 deletions qbft/round_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package qbft

import (
"bytes"

"github.com/bloxapp/ssv-spec/types"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -200,12 +201,15 @@ func isReceivedProposalJustification(
newRound,
value,
valCheck,
false,
); err != nil {
return errors.Wrap(err, "proposal not justified")
}
return nil
}

// validRoundChangeForData is similar to the validSignedRoundChangeForData function
// However, it does not verify signatures
func validRoundChangeForData(
state *State,
config IConfig,
Expand All @@ -227,6 +231,72 @@ func validRoundChangeForData(
return errors.New("msg allows 1 signer")
}

if err := signedMsg.Message.Validate(); err != nil {
return errors.Wrap(err, "roundChange invalid")
}

// Addition to formal spec
// We add this extra tests on the msg itself to filter round change msgs with invalid justifications, before they are inserted into msg containers
if signedMsg.Message.RoundChangePrepared() {
r, err := HashDataRoot(fullData)
if err != nil {
return errors.Wrap(err, "could not hash input data")
}

// validate prepare message justifications
prepareMsgs, _ := signedMsg.Message.GetRoundChangeJustifications() // no need to check error, checked on signedMsg.Message.Validate()
for _, pm := range prepareMsgs {
if err := validPrepareForHeightRoundAndRoot(
config,
pm,
state.Height,
signedMsg.Message.DataRound,
signedMsg.Message.Root,
state.Share.Committee); err != nil {
return errors.Wrap(err, "round change justification invalid")
}
}

if !bytes.Equal(r[:], signedMsg.Message.Root[:]) {
return errors.New("H(data) != root")
}

if !HasQuorum(state.Share, prepareMsgs) {
return errors.New("no justifications quorum")
}

if signedMsg.Message.DataRound > round {
return errors.New("prepared round > round")
}

return nil
}
return nil
}

// validSignedRoundChangeForData is based on dafny's validRoundChange function
// with the addition that nested Prepare messages are also validated
func validSignedRoundChangeForData(
state *State,
config IConfig,
signedMsg *SignedMessage,
height Height,
round Round,
fullData []byte,
) error {
if signedMsg.Message.MsgType != RoundChangeMsgType {
return errors.New("round change msg type is wrong")
}
if signedMsg.Message.Height != height {
return errors.New("wrong msg height")
}
if signedMsg.Message.Round != round {
return errors.New("wrong msg round")
}
if len(signedMsg.GetSigners()) != 1 {
return errors.New("msg allows 1 signer")
}

if err := signedMsg.Signature.VerifyByOperators(signedMsg, config.GetSignatureDomainType(), types.QBFTSignatureType, state.Share.Committee); err != nil {
return errors.Wrap(err, "msg signature invalid")
}
Expand Down
Loading