## Brief Today, SSV nodes perform most of the validations on messages only in the consensus flow. ## Purpose Shield honest nodes from wasteful compute and I/O resource consumption by malicious/faulty nodes. ## Goals - [x] Completeness — Validate *all* incoming messages before reacting on them - Syntactic: is the message well-formed? - Semantic: is the message playing by the protocol's rules? - Cryptographic: is the signature correct? - [x] Accountability — Penalize peers who transmit invalid messages - [x] Efficiency — BLS signature verifications are heavy! - Only verify signatures for messages which passed the synactic & semantic validations. - Batch BLS signature verifications to reduce CPU usage ## Pseudocode The following is a pseudocode demonstrating how to perform the syntactic & semantic validations on messages. ```go package validation import ( "errors" "time" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/bloxapp/ssv/protocol/v2/ssv/queue" specqbft "github.com/bloxapp/ssv-spec/qbft" spectypes "github.com/bloxapp/ssv-spec/types" beaconprotocol "github.com/bloxapp/ssv/protocol/v2/blockchain/beacon" ) const ( // lateMessageMargin is the duration past a message's TTL in which it is still considered valid. lateMessageMargin = time.Second * 3 // clockErrorTolerance is the maximum amount of clock error we expect to see between nodes. clockErrorTolerance = time.Millisecond * 50 ) var ( ErrUnknownValidator = errors.New("unknown validator") ErrInvalidRole = errors.New("invalid role") ErrMalformedMessage = errors.New("malformed message") ErrEarlyMessage = errors.New("early message") ErrLateMessage = errors.New("late message") ErrInvalidSigners = errors.New("invalid signers") ErrBadSignerBehavior = errors.New("bad signer behavior") ) // maxMessageCounts is the maximum number of acceptable messages from a signer within a slot & round. func maxMessageCounts(committeeSize uint64) MessageCounts { return MessageCounts{ PreConsensus: 1, Proposals: 1, Prepares: 1, // TODO: max commits should adapt to the committeeSize. // (see Gal's formula: https://hackmd.io/zT1hct3oRDW3QicByFkqsA) Commits: 0, PostConsensus: 1, } } type ConsensusID struct { PubKey phase0.BLSPubKey Role spectypes.BeaconRole } type ConsensusState struct { Signers map[spectypes.OperatorID]*SignerState } type MessageCounts struct { PreConsensus int Proposals int Prepares int Commits int PostConsensus int } func (c *MessageCounts) Record(msg *queue.DecodedSSVMessage, limits MessageCounts) bool { switch m := msg.Body.(type) { case *specqbft.SignedMessage: switch m.Message.MsgType { case specqbft.ProposalMsgType: c.Proposals++ case specqbft.PrepareMsgType: c.Prepares++ case specqbft.CommitMsgType: c.Commits++ } case *spectypes.SignedPartialSignatureMessage: if m.Message.Type == spectypes.PostConsensusPartialSig { c.PostConsensus++ } else { c.PreConsensus++ } } return c.Exceeds() } func (c *MessageCounts) Exceeds(limits MessageCounts) bool { return c.PreConsensus > limits.PreConsensus || c.Proposals > limits.Proposals || c.Prepares > limits.Prepares || c.Commits > limits.Commits || c.PostConsensus > limits.PostConsensus } type SignerState struct { Start time.Time Slot phase0.Slot Round specqbft.Round MessageCounts MessageCounts } func (s *SignerState) Reset(slot phase0.Slot, round specqbft.Round) { s.Start = time.Now() s.Slot = slot s.MessageCounts = MessageCounts{} } type MessageValidator struct { network beaconprotocol.Network index map[ConsensusID]*ConsensusState } func (v *MessageValidator) ValidateMessage(ssvMessage *spectypes.SSVMessage) (*queue.DecodedSSVMessage, error) { // Pre-decode validations. id := ConsensusID{ PubKey: ssvMessage.MsgID.GetPubKey(), Role: ssvMessage.MsgID.GetRoleType(), } if !v.knownValidator(id.PubKey) { // Validator doesn't exist or is liquidated. return nil, ErrUnknownValidator } if !v.validRole(id.Role) { return nil, ErrInvalidRole } // Decode. msg, err := queue.DecodeSSVMessage(ssvMessage) if err != nil { return nil, errors.Join(ErrMalformedMessage, err) } // Validate that the specified signers belong in the validator's committee. if !v.validSigners(id.PubKey, msg) { return nil, ErrInvalidSigners } // Validate timing. if v.earlyMessage(msg.Slot) { return nil, ErrEarlyMessage } if v.lateMessage(msg.Slot, id.Role) { return nil, ErrLateMessage } // Validate each signer's behavior. state := v.consensusState(id) for _, signer := range msg.Signers { // TODO: should we verify signature before doing this validation? // Reason being that this modifies the signer's state and so message fakers // would be able to alter states of any signer using invalid signatures. if err := v.validateSignerBehavior(state, signer, msg); err != nil { return nil, errors.Join(ErrBadSignerBehavior, err) } } return msg, nil } func (v *MessageValidator) earlyMessage(slot phase0.Slot) bool { return v.network.GetSlotEndTime(v.network.EstimatedCurrentSlot()). Add(-clockErrorTolerance).Before(v.network.GetSlotStartTime(slot)) } func (v *MessageValidator) lateMessage(slot phase0.Slot, role spectypes.BeaconRole) bool { // Note: this is just an example, should be according to ssv-spec. var ttl phase0.Slot switch role { case spectypes.BNRoleProposer: ttl = 1 case spectypes.BNRoleAttester: ttl = 32 case spectypes.BNRoleSyncCommittee: ttl = 1 // ... } deadline := v.network.GetSlotStartTime(slot + ttl). Add(lateMessageMargin + clockErrorTolerance) return time.Now().After(deadline) } func (v *MessageValidator) consensusState(id ConsensusID) *ConsensusState { if _, ok := v.index[id]; !ok { v.index[id] = &ConsensusState{ Signers: make(map[spectypes.OperatorID]*SignerState), } } return v.index[id] } func (v *MessageValidator) validateSignerBehavior( state ConsensusState, signer spectypes.OperatorID, msg *queue.DecodedSSVMessage, ) error { signerState := state.Signers[signer] if signerState == nil { signerState = &SignerState{} } // Check slot: Signers aren't allowed to decrease their slot. if msg.Slot < signerState.Slot { return errors.New("signer has already advanced to a later slot") } // Check round: Signers aren't allowed to decrease their round. var msgRound specqbft.Round if m, ok := msg.Body.(*specqbft.SignedMessage); ok { msgRound = m.Message.Round } if msgRound < signerState.Round { return errors.New("signer has already advanced to a later round") } // TODO: check that the message's round is sensible according to the roundTimeout // and the slot. For example: // // maxRound := (currentTime-messageSlotTime)/roundTimeout // assert msg.Round < maxRound // Advance slot & round, if needed. if msg.Slot > signerState.Slot || msgRound > signerState.Round { signerState.Reset(msg.Slot, msgRound) } // TODO: if this is a round change message, we should somehow validate that it's not being sent too frequently. // Validate message counts within the current slot & round. if !signerState.MessageCounts.Record(msg, maxMessageCounts(committeeSize)) { return errors.New("too many messages") } return nil } ```