---
# System prepended metadata

title: Message Validation

---

## 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
}
```