diff --git a/p2p/MessageValidation/ReceivedMessagesEstimation.md b/p2p/MessageValidation/ReceivedMessagesEstimation.md
index f16349e..391eccd 100644
--- a/p2p/MessageValidation/ReceivedMessagesEstimation.md
+++ b/p2p/MessageValidation/ReceivedMessagesEstimation.md
@@ -1,5 +1,8 @@
# Estimation of amount of messages received
+> [!WARNING]
+> This document is deprecated due to protocol changes as of the Alan and Boole forks.
+
To play with the below formulas, follow this [google sheet link](https://docs.google.com/spreadsheets/d/1TpXnVFzF4eGiQarXuPBrOIU4tJXhzOHav9PkGve3_Qc/edit?usp=sharing).
## Probability of having a duty per slot
diff --git a/p2p/MessageValidation/Rules.md b/p2p/MessageValidation/Rules.md
index 4959479..a623eaf 100644
--- a/p2p/MessageValidation/Rules.md
+++ b/p2p/MessageValidation/Rules.md
@@ -118,15 +118,15 @@ var (
ErrUnexpectedPrepareJustifications = Error{text: "message has a prepare justification but it's not a proposal", reject: true}
- ErrPartialSigOneSigner = Error{text: "partial signature message with len(signers) != 1", reject: true}
- ErrTooManyPartialSignatureMessages = Error{text: "too many signatures for cluster in partial signature message"}
- ErrTripleValidatorIndexInPartialSignatures = Error{text: "validator index appear 3 times in partial signature message", reject: true}
- ErrNoPartialSignatureMessages = Error{text: "no partial signature messages", reject: true}
- ErrInconsistentSigners = Error{text: "inconsistent signers", reject: true}
- ErrValidatorIndexMismatch = Error{text: "validator index mismatch"}
- ErrInvalidPartialSignatureType = Error{text: "invalid partial signature type", reject: true}
- ErrPartialSignatureTypeRoleMismatch = Error{text: "partial signature type and role don't match", reject: true}
- ErrInvalidPartialSignatureTypeCount = Error{text: "sent more partial signature messages of a certain type than allowed", reject: true}
+ ErrPartialSigOneSigner = Error{text: "partial signature message with len(signers) != 1", reject: true}
+ ErrTooManyPartialSignatureMessages = Error{text: "too many signatures for cluster in partial signature message"}
+ ErrTooManyEqualValidatorIndicesInPartialSignatures = Error{text: "validator index appears too many times in partial signature message", reject: true}
+ ErrNoPartialSignatureMessages = Error{text: "no partial signature messages", reject: true}
+ ErrInconsistentSigners = Error{text: "inconsistent signers", reject: true}
+ ErrValidatorIndexMismatch = Error{text: "validator index mismatch"}
+ ErrInvalidPartialSignatureType = Error{text: "invalid partial signature type", reject: true}
+ ErrPartialSignatureTypeRoleMismatch = Error{text: "partial signature type and role don't match", reject: true}
+ ErrInvalidPartialSignatureTypeCount = Error{text: "sent more partial signature messages of a certain type than allowed", reject: true}
ErrTooManyDutiesPerEpoch = Error{text: "too many duties per epoch"}
ErrNoDuty = Error{text: "no duty for this epoch"}
@@ -137,17 +137,26 @@ var (
The main structure is the `MessageValidation` structure which has a `ValidatePubsubMessage` function to serve as a handle for the GossipSub extended validator.
+The function calls `ValidateMessage` which recursively calls every validation chain: Syntax -> Semantics -> QBFT Semantics | Partial Signature Semantics -> QBFT Logic -> Duty Rules.
+All rules are verified against the peer-specific state, and after it:
+- If there's any error, the appropriate `Ignore` or `Reject` is returned.
+- Else, the message is again validated but this time against the global shared state.
+This prevents the node from propagating duplicated messages. Then:
+ - In case there's any error, `Ignore` is returned (even if the triggered rule has `reject: true`) as we can't blame the peer for the error.
+ - Else, `Accept` is returned.
+
+More about the global shared state validation is discussed [below](#global-shared-state-validations).
+
```go
const (
- MaxMsgSize = 4945164
- maxConsensusMsgSize = 722412
- maxPartialSignatureMsgSize = 144020
+ MaxMsgSize = 9114816 // Source: https://github.com/ssvlabs/ssv-spec/blob/a65134ed45c932588c17d421173923f0216c6c73/types/spectest/tests/maxmsgsize/max_signed_ssv_message.go#L11
+ maxConsensusMsgSize = 722480 // Source: https://github.com/ssvlabs/ssv-spec/blob/a65134ed45c932588c17d421173923f0216c6c73/types/spectest/tests/maxmsgsize/max_ssv_message.go#L9
+ maxPartialSignatureMsgSize = 727000 // Source: https://github.com/ssvlabs/ssv-spec/blob/a65134ed45c932588c17d421173923f0216c6c73/types/spectest/tests/maxmsgsize/max_ssv_message.go#L10
maxSSVMessageDataSize = max(maxConsensusMsgSize, maxPartialSignatureMsgSize)
- PartialSignatureSize = 48
- MessageSignatureSize = 256
- SyncCommitteeSize = 512
- MaxSignaturesInSyncCommitteeContribution = 13
+ PartialSignatureSize = 96 // Source: https://github.com/ssvlabs/ssv-spec/blob/a65134ed45c932588c17d421173923f0216c6c73/types/partial_sig_message.go#L80 and https://eth2book.info/latest/part2/building_blocks/signatures/#signing
+ MessageSignatureSize = 256 // Source: https://github.com/ssvlabs/ssv-spec/blob/a65134ed45c932588c17d421173923f0216c6c73/types/messages.go#L128
+ SyncCommitteeSize = 512 // Source: https://github.com/ethereum/consensus-specs/blob/master/specs/altair/beacon-chain.md#sync-committee
)
type MessageValidation struct {
@@ -176,27 +185,35 @@ func (mv *MessageValidation) Validate(_ context.Context, _ peer.ID, pmsg *pubsub
// Check error
if err != nil {
-
- var valErr Error
- if errors.As(err, &valErr) {
- // Update state
- peerState.OnError(valErr)
-
- if valErr.Reject() {
- // Reject
- return pubsub.ValidationReject
- } else {
- // Ignore
- return pubsub.ValidationIgnore
- }
- } else {
- panic(err)
- }
+ return GetPubSubValidationResult(peerState, err)
} else {
+ // If the message is successful after all rules are tested on the peer-specific state,
+ // test it as well on the global shared state to avoid propagating duplicated
+ err = mv.ValidateAgainstGlobalSharedState(pmsg)
+ if err != nil {
+ return pubsub.ValidationIgnore
+ }
return pubsub.ValidationAccept
}
}
+func GetPubSubValidationResult(peerState *PeerState, err error) pubsub.ValidationResult {
+ var valErr Error
+ if errors.As(err, &valErr) {
+ // Update state
+ peerState.OnError(valErr)
+ if valErr.Reject() {
+ // Reject
+ return pubsub.ValidationReject
+ } else {
+ // Ignore
+ return pubsub.ValidationIgnore
+ }
+ } else {
+ panic(err)
+ }
+}
+
func (mv *MessageValidation) VerifyMessageSignature(pmsg *pubsub.Message) error {
// Already verified
signedSSVMessage := &types.SignedSSVMessage{}
@@ -423,19 +440,19 @@ func (mv *MessageValidation) ValidatePubSubMessage(pmsg *pubsub.Message) error {
### Semantics General Rules
-| Verification | Error | Classification | Explanation |
-| -------------------- | ------------------------- | -------------- | --------------------------------------------------------------------- |
-| Signers in committee | ErrSignerNotInCommittee | Reject | Signers must belong to validator's or CommitteeID's committee. |
-| Different Domain | ErrWrongDomain | Ignore | MsgID.Domain is different than self domain. |
-| Invalid Role | ErrInvalidRole | Reject | MsgID.Role is not known. |
-| Validator exists | ErrUnknownValidator | Ignore | If MsgID.SenderID is a validator, it must exist. |
-| Active Validator ID | ErrValidatorNotAttesting | Ignore | If MsgID.SenderID is a validator, it must be active active validator. |
-| Validator Liquidated | ErrValidatorLiquidated | Ignore | If MsgID.SenderID is a validator, it must not be liquidated. |
-| CommitteeID exists | ErrNonExistentCommitteeID | Ignore | If MsgID.SenderID is a committee, it must exist. |
-| Wrong topic | ErrIncorrectTopic | Ignore | The message should be sent in the correct topic |
-| Event Message | ErrEventMessage | Reject | MsgType can't be of event message. |
-| DKG Message | ErrDKGMessage | Reject | MsgType can't be of DKG message. |
-| Unknown MsgType | ErrUnknownSSVMessageType | Reject | MsgType is not known. |
+| Verification | Error | Classification | Explanation |
+| -------------------- | ------------------------- | -------------- |--------------------------------------------------------------------------------------------------------------------------------------------|
+| Signers in committee | ErrSignerNotInCommittee | Reject | Signers must belong to validator's or CommitteeID's committee. |
+| Different Domain | ErrWrongDomain | Ignore | MsgID.Domain is different than self domain. |
+| Invalid Role | ErrInvalidRole | Reject | MsgID.Role is wrong (not `RoleCommittee`, `RoleProposer`, `RoleValidatorRegistration`, `RoleVoluntaryExit`, or `RoleAggregatorCommittee`). |
+| Validator exists | ErrUnknownValidator | Ignore | If MsgID.SenderID is a validator, it must exist. |
+| Active Validator ID | ErrValidatorNotAttesting | Ignore | If MsgID.SenderID is a validator, it must be active active validator. |
+| Validator Liquidated | ErrValidatorLiquidated | Ignore | If MsgID.SenderID is a validator, it must not be liquidated. |
+| CommitteeID exists | ErrNonExistentCommitteeID | Ignore | If MsgID.SenderID is a committee, it must exist. |
+| Wrong topic | ErrIncorrectTopic | Ignore | The message should be sent in the correct topic |
+| Event Message | ErrEventMessage | Reject | MsgType can't be of event message. |
+| DKG Message | ErrDKGMessage | Reject | MsgType can't be of DKG message. |
+| Unknown MsgType | ErrUnknownSSVMessageType | Reject | MsgType is not known. |
```go
@@ -477,7 +494,7 @@ func (mv *MessageValidation) ValidateSemantics(peerID peer.ID, signedSSVMessage
}
senderID := signedSSVMessage.SSVMessage.MsgID.GetSenderID()
- if role != types.RoleCommittee {
+ if !isCommitteeRole(role) {
validatorPK := senderID
// Rule: Validator does not exist
@@ -521,6 +538,10 @@ func (mv *MessageValidation) ValidateSemantics(peerID peer.ID, signedSSVMessage
return ErrUnknownSSVMessageType
}
}
+
+func isCommitteeRole(role types.RunnerRole) bool {
+ return role == types.RoleCommittee || role == types.RoleAggregatorCommittee
+}
```
### Consensus
@@ -732,14 +753,14 @@ func (mv *MessageValidation) ValidateQBFTLogic(peerID peer.ID, signedSSVMessage
#### Duty Logic
-| Verification | Error | Classification | Explanation |
-| ------------------------- | ----------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Already advanced slot | ErrSlotAlreadyAdvanced | Ignore | (Non-committee roles) Signer already advanced to later slot. |
-| Invalid role for consensus | ErrUnexpectedConsensusMessage | Reject | SSVMessage.MsgID.Role must not be ValidatorRegistration or VoluntaryExit. |
-| No beacon duty | ErrNoDuty | Ignore | If Proposal or Sync committee contribution duty, check if duty exists with beacon node. |
-| Slot not in time for role | ErrEarlySlotMessage or ErrLateSlotMessage | Ignore | Current time must be between duty's starting time and
+34 (committee and aggregator) or +3 (else) slots. |
-| Too many duties per epoch | ErrTooManyDutiesPerEpoch | Ignore | If role is either aggregator, voluntary exit and validator registration,
it's allowed 2 duties per epoch. Else if committee,
2*V (if no validator is doing sync committee).
Else accept. |
-| Valid round for role | ErrRoundTooHigh | Reject | For committee and aggregation, round can go up to 12. Else, it can go up to 6. |
+| Verification | Error | Classification | Explanation |
+| ------------------------- | ----------------------------- | -------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Already advanced slot | ErrSlotAlreadyAdvanced | Ignore | (Non-committee roles) Signer already advanced to later slot. |
+| Invalid role for consensus | ErrUnexpectedConsensusMessage | Reject | SSVMessage.MsgID.Role must not be ValidatorRegistration or VoluntaryExit. |
+| No beacon duty | ErrNoDuty | Ignore | If Proposal duty, check if duty exists with beacon node. |
+| Slot not in time for role | ErrEarlySlotMessage or ErrLateSlotMessage | Ignore | Current time must be between duty's starting time and
+34 (committee and aggregator committee) or +3 (else) slots. |
+| Too many duties per epoch | ErrTooManyDutiesPerEpoch | Ignore | If role is either voluntary exit and validator registration,
it's allowed 2 duties per epoch. Else if committee or aggregator committee,
2*V (if no validator is doing sync committee).
Else accept. |
+| Valid round for role | ErrRoundTooHigh | Reject | For committee and aggregator committee, round can go up to 12. Else, it can go up to 6. |
```go
@@ -757,7 +778,7 @@ func (mv *MessageValidation) ValidateQBFTMessageByDutyLogic(peerID peer.ID, sign
}
type SSVMessage struct {
MsgType MsgType
- MsgID MessageID -> Role must have consensus, Must be assigned to duty if role is Proposal or Sync Committee Aggregator
+ MsgID MessageID -> Role must have consensus, Must be assigned to duty if role is Proposal
Data []byte
}
type Message struct {
@@ -777,7 +798,7 @@ func (mv *MessageValidation) ValidateQBFTMessageByDutyLogic(peerID peer.ID, sign
_ = qbftMessage.Decode(signedSSVMessage.SSVMessage.Data)
// Rule: Height must not be "old". I.e., signer must not have already advanced to a later slot.
- if signedSSVMessage.SSVMessage.MsgID.GetRoleType() != types.RoleCommittee { // Rule only for validator runners
+ if !isCommitteeRole(signedSSVMessage.SSVMessage.MsgID.GetRoleType()) { // Rule only for validator runners
if !mv.MessageFromOldSlot(peerID, signedSSVMessage.SSVMessage.MsgID, qbftMessage.Height) {
return ErrSlotAlreadyAdvanced
}
@@ -788,13 +809,13 @@ func (mv *MessageValidation) ValidateQBFTMessageByDutyLogic(peerID peer.ID, sign
return ErrUnexpectedConsensusMessage
}
- // Rule: For proposal and sync committee aggregation duties, we check if the validator is assigned to it
- if err := mv.ValidBeaconDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), signedSSVMessage.SSVMessage.MsgID.GetRoleType(), phase0.Slot(qbftMessage.Height)); err != nil {
+ // Rule: For proposer duty, we check if the validator is assigned to it
+ if err := mv.ValidProposerDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), signedSSVMessage.SSVMessage.MsgID.GetRoleType(), phase0.Slot(qbftMessage.Height)); err != nil {
return err
}
// Rule: current slot(height) must be between duty's starting slot and:
- // - duty's starting slot + 34 (committee and aggregation)
+ // - duty's starting slot + 34 (committee and aggregator committee)
// - duty's starting slot + 3 (other types)
if err != mv.ValidDutySlot(peerID, phase0.Slot(qbftMessage.Height), signedSSVMessage.SSVMessage.MsgID.GetRoleType()); err != nil {
// Err should be ErrEarlySlotMessage or ErrLateSlotMessage
@@ -802,15 +823,15 @@ func (mv *MessageValidation) ValidateQBFTMessageByDutyLogic(peerID peer.ID, sign
}
// Rule: valid number of duties per epoch:
- // - 2 for aggregation, voluntary exit and validator registration
- // - 2*V for Committee duty (where V is the number of validators in the cluster) (if no validator is doing sync committee in this epoch)
+ // - 2 for voluntary exit and validator registration
+ // - 2*V for committee and aggregator committee duty (where V is the number of validators in the cluster) (if no validator is doing sync committee in this epoch)
// - else, accept
if !mv.ValidNumberOfDutiesPerEpoch(peerID, signedSSVMessage.SSVMessage.MsgID, phase0.Slot(qbftMessage.Height)) {
return ErrTooManyDutiesPerEpoch
}
// Rule: Round cut-offs for roles:
- // - 12 (committee and aggregation)
+ // - 12 (committee and aggregator committee)
// - 6 (other types)
if !mv.ValidRoundForRole(qbftMessage.Round, signedSSVMessage.SSVMessage.MsgID.GetRoleType()) {
return ErrRoundTooHigh
@@ -819,7 +840,7 @@ func (mv *MessageValidation) ValidateQBFTMessageByDutyLogic(peerID peer.ID, sign
return nil
}
-func (mv *MessageValidation) ValidBeaconDuty() error {
+func (mv *MessageValidation) ValidProposerDuty() error {
// Rule: For a proposal duty message, we check if the validator is assigned to it
if signedSSVMessage.SSVMessage.MsgID.GetRoleType() == types.RoleProposer {
@@ -828,13 +849,6 @@ func (mv *MessageValidation) ValidBeaconDuty() error {
}
}
- // Rule: For a sync committee aggregation duty message, we check if the validator is assigned to it
- if signedSSVMessage.SSVMessage.MsgID.GetRoleType() == types.RoleSyncCommitteeContribution {
- if !mv.HasSyncCommitteeDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), phase0.Slot(qbftMessage.Height)) {
- return ErrNoDuty
- }
- }
-
return nil
}
@@ -844,16 +858,16 @@ func (mv *MessageValidation) ValidBeaconDuty() error {
#### Semantics
-| Verification | Error | Classification | Explanation |
-| -------------------------- | ----------------------------------- | -------------- | ----------- |
-| More than one signer | ErrPartialSigOneSigner | Reject | Must have only 1 signer. |
-| Unexpected FullData | ErrFullDataNotInConsensusMessage | Reject | Must not have FullData. |
-| Unknown type | ErrInvalidPartialSignatureType | Reject | Type not known. |
-| Wrong type for role | ErrPartialSignatureTypeRoleMismatch | Reject | Type must match role:
PostConsensusPartialSig for Committee,
RandaoPartialSig or PostConsensusPartialSig for Proposer,
SelectionProofPartialSig or PostConsensusPartialSig for Aggregator,
SelectionProofPartialSig or PostConsensusPartialSig for Sync committee contribution,
ValidatorRegistrationPartialSig for Validator Registration,
VoluntaryExitPartialSig for Voluntary Exit |
-| No PartialSignatureMessage | ErrNoPartialSignatureMessages | Reject | Message must have at least one PartialSignatureMessage. |
-| Wrong BLS Signature Size | ErrWrongBLSSignatureSize | Reject | $\forall i$ PartialSignatureMessages.Message[i].Signature must have the correct length. |
-| Inconsistent signer | ErrInconsistentSigners | Reject | $\forall i$ PartialSignatureMessages.Message[i].Signer must be the same as the
SignedSSVMessage.OperatorIDs[i]. |
-| Validtor's index mismatch | ErrValidatorIndexMismatch | Ignore | $\forall i$ PartialSignatureMessages.Message[i].ValidatorIndex must belong to SSVMessage.SenderID(). |
+| Verification | Error | Classification | Explanation |
+|----------------------------| ----------------------------------- | -------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| More than one signer | ErrPartialSigOneSigner | Reject | Must have only 1 signer. |
+| Unexpected FullData | ErrFullDataNotInConsensusMessage | Reject | Must not have FullData. |
+| Unknown type | ErrInvalidPartialSignatureType | Reject | Type not known. |
+| Wrong type for role | ErrPartialSignatureTypeRoleMismatch | Reject | Type must match role:
PostConsensusPartialSig for Committee,
RandaoPartialSig or PostConsensusPartialSig for Proposer,
AggregatorCommitteePartialSig or PostConsensusPartialSig for AggregatorCommittee,
ValidatorRegistrationPartialSig for Validator Registration,
VoluntaryExitPartialSig for Voluntary Exit |
+| No PartialSignatureMessage | ErrNoPartialSignatureMessages | Reject | Message must have at least one PartialSignatureMessage. |
+| Wrong BLS Signature Size | ErrWrongBLSSignatureSize | Reject | $\forall i$ PartialSignatureMessages.Message[i].Signature must have the correct length. |
+| Inconsistent signer | ErrInconsistentSigners | Reject | $\forall i$ PartialSignatureMessages.Message[i].Signer must be the same as the
SignedSSVMessage.OperatorIDs[i]. |
+| Validator's index mismatch | ErrValidatorIndexMismatch | Ignore | $\forall i$ PartialSignatureMessages.Message[i].ValidatorIndex must belong to SSVMessage.SenderID(). |
```go
@@ -911,8 +925,7 @@ func (mv *MessageValidation) ValidatePartialSignatureMessageSemantics(peerID pee
// Rule: Partial signature type must match expected type:
// - PostConsensusPartialSig, for Committee duty
// - RandaoPartialSig or PostConsensusPartialSig for Proposer
- // - SelectionProofPartialSig or PostConsensusPartialSig for Aggregator
- // - SelectionProofPartialSig or PostConsensusPartialSig for Sync committee contribution
+ // - AggregatorCommitteePartialSig or PostConsensusPartialSig for AggregatorCommittee
// - ValidatorRegistrationPartialSig for Validator Registration
// - VoluntaryExitPartialSig for Voluntary Exit
if !mv.ExpectedPartialSignatureTypeForRole(partialSignatureMessages.Type, signedSSVMessage.SSVMessage.MsgID) {
@@ -945,15 +958,15 @@ func (mv *MessageValidation) ValidatePartialSignatureMessageSemantics(peerID pee
#### Duty Logic
-| Verification | Error | Classification | Explanation |
-| ---------------------------- | ------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Already advanced slot | ErrSlotAlreadyAdvanced | Ignore | (Non-committee roles) Signer already advanced to later slot. |
-| No beacon duty | ErrNoDuty | Ignore | If Proposal or Sync committee contribution duty, check if duty exists with beacon node. |
-| Invalid signature type count | ErrInvalidPartialSignatureTypeCount | Reject | It's allow only:
1 PostConsensusPartialSig, for Committee duty,
1 RandaoPartialSig and 1 PostConsensusPartialSig for Proposer,
1 SelectionProofPartialSig and 1 PostConsensusPartialSig for Aggregator,
1 SelectionProofPartialSig and 1 PostConsensusPartialSig for Sync committee contribution,
1 ValidatorRegistrationPartialSig for Validator Registration,
1 VoluntaryExitPartialSig for Voluntary Exit. |
-| Slot not in time for role | ErrEarlySlotMessage or ErrLateSlotMessage | Ignore | Current time must be between duty's starting time and
+34 (committee and aggregator) or +3 (else) slots. |
-| Too many duties per epoch | ErrTooManyDutiesPerEpoch | Ignore | If role is either aggregator, voluntary exit and validator registration,
it's allowed 2 duties per epoch. Else if committee,
2*V (if no validator is doing sync committee).
Else accept. |
-| Too many partial signatures | ErrTooManyPartialSignatureMessages | Reject | For the committee role, it's allowed $min(2*V, V + $ SYNC_COMMITTEE_SIZE $)$
where $V$ is the number of committee's validatos.
For sync committee contribution, it's allowed 13.
Else, only 1. |
-| Triple validator index | ErrTripleValidatorIndexInPartialSignatures | Reject | A validator index can not be associated to more than 2 signatures. |
+| Verification | Error | Classification | Explanation |
+|----------------------------------|----------------------------------------------------| -------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Already advanced slot | ErrSlotAlreadyAdvanced | Ignore | (Non-committee roles) Signer already advanced to later slot. |
+| No beacon duty | ErrNoDuty | Ignore | If Proposal duty, check if duty exists with beacon node. |
+| Invalid signature type count | ErrInvalidPartialSignatureTypeCount | Reject | It allows only:
1 PostConsensusPartialSig, for Committee duty,
1 RandaoPartialSig and 1 PostConsensusPartialSig for Proposer,
1 AggregatorCommitteePartialSig and 1 PostConsensusPartialSig for AggregatorCommittee,
1 ValidatorRegistrationPartialSig for Validator Registration,
1 VoluntaryExitPartialSig for Voluntary Exit.|
+| Slot not in time for role | ErrEarlySlotMessage or ErrLateSlotMessage | Ignore | Current time must be between duty's starting time and
+34 (committee and aggregator committee) or +3 (else) slots. |
+| Too many duties per epoch | ErrTooManyDutiesPerEpoch | Ignore | If role is either aggregator, voluntary exit and validator registration,
it's allowed 2 duties per epoch. Else if committee or aggregator committee,
2*V (if no validator is doing sync committee).
Else accept. |
+| Too many partial signatures | ErrTooManyPartialSignatureMessages | Reject | For the committee role, it's allowed $min(2*V, V + $ SYNC_COMMITTEE_SIZE $)$
where $V$ is the number of committee's validators.
For the aggregator committee role, it's allowed $min((1+4)*V, V + 4 \times$ SYNC_COMMITTEE_SIZE $)$.
Else, only 1. |
+| Too many equal validator indices | ErrTooManyEqualValidatorIndicesInPartialSignatures | Reject | A validator index can not be associated with more than 2 signatures for the committee role and more than 5 for the aggregator committee role. |
```go
@@ -971,20 +984,23 @@ func (mv *MessageValidation) ValidatePartialSigMessagesByDutyLogic(peerID peer.I
}
type SSVMessage struct {
MsgType MsgType
- MsgID MessageID -> Must be assigned to duty if role is Proposal or Sync Committee Aggregator
+ MsgID MessageID -> Must be assigned to duty if role is Proposal
Data []byte
}
type PartialSignatureMessages struct {
Type PartialSigMsgType -> Message count rules
Slot phase0.Slot -> Must belong to allowed spread, Satisfies a maximum number of duties per epoch for role, Must not be "old"
- Messages []*PartialSignatureMessage -> Valid number of signatures (3 cases: committee duty, sync committee contribution, others)
+ Messages []*PartialSignatureMessage -> Valid number of signatures. 3 cases:
+ - min(2*V, V + SYNC_COMMITTEE_SIZE) for committee duty
+ - min((1+4)*V, V + 4 * SYNC_COMMITTEE_SIZE) for aggregator committee duty
+ - 1 for other duty types
}
type PartialSignatureMessage struct {
PartialSignature Signature
SigningRoot [32]byte
Signer OperatorID
- ValidatorIndex phase0.ValidatorIndex -> Can't appear more than 2 times for role Committee
+ ValidatorIndex phase0.ValidatorIndex -> Can't appear more than 2 (resp. 5) times for role Committee (resp. AggregatorCommittee)
}
*/
@@ -992,25 +1008,25 @@ func (mv *MessageValidation) ValidatePartialSigMessagesByDutyLogic(peerID peer.I
var partialSignatureMessages types.PartialSignatureMessages
_ = partialSignatureMessages.Decode(signedSSVMessage.SSVMessage.Data)
+ msgRole := signedSSVMessage.SSVMessage.MsgID.GetRoleType()
// Rule: Height must not be "old". I.e., signer must not have already advanced to a later slot.
- if signedSSVMessage.SSVMessage.MsgID.GetRoleType() != types.RoleCommittee { // Rule only for validator runners
+ if !isCommitteeRole(msgRole) { // Rule only for validator runners
if !mv.MessageFromOldSlot(peerID, signedSSVMessage.SSVMessage.MsgID, partialSignatureMessages.Slot) {
return ErrSlotAlreadyAdvanced
}
}
- // Rule: For proposal and sync committee aggregation duties, we check if the validator is assigned to it
- if err := mv.ValidBeaconDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), signedSSVMessage.SSVMessage.MsgID.GetRoleType(), partialSignatureMessages.Slot); err != nil {
+ // Rule: For proposer duty, we check if the validator is assigned to it
+ if err := mv.ValidProposerDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), signedSSVMessage.SSVMessage.MsgID.GetRoleType(), partialSignatureMessages.Slot); err != nil {
return err
}
// Rule: peer must send only:
// - 1 PostConsensusPartialSig, for Committee duty
// - 1 RandaoPartialSig and 1 PostConsensusPartialSig for Proposer
- // - 1 SelectionProofPartialSig and 1 PostConsensusPartialSig for Aggregator
- // - 1 SelectionProofPartialSig and 1 PostConsensusPartialSig for Sync committee contribution
+ // - 1 AggregatorCommitteePartialSig and 1 PostConsensusPartialSig for AggregatorCommittee
// - 1 ValidatorRegistrationPartialSig for Validator Registration
// - 1 VoluntaryExitPartialSig for Voluntary Exit
if err := mv.ValidPartialSigMessageCount(peerID, signedSSVMessage.SSVMessage.MsgID, &partialSignatureMessages); err != nil {
@@ -1018,7 +1034,7 @@ func (mv *MessageValidation) ValidatePartialSigMessagesByDutyLogic(peerID peer.I
}
// Rule: current slot must be between duty's starting slot and:
- // - duty's starting slot + 34 (committee and aggregation)
+ // - duty's starting slot + 34 (committee and aggregator committee)
// - duty's starting slot + 3 (other duties)
if err := mv.ValidDutySlot(peerID, partialSignatureMessages.Slot, signedSSVMessage.SSVMessage.MsgID.GetRoleType()); err != nil {
// Err should be ErrEarlySlotMessage or ErrLateSlotMessage
@@ -1026,28 +1042,32 @@ func (mv *MessageValidation) ValidatePartialSigMessagesByDutyLogic(peerID peer.I
}
// Rule: valid number of duties per epoch:
- // - 2 for aggregation, voluntary exit and validator registration
- // - 2*V for Committee duty (where V is the number of validators in the cluster) (if no validator is doing sync committee in this epoch)
+ // - 2 for voluntary exit and validator registration
+ // - 2*V for Committee and AggregatorCommittee duty (where V is the number of validators in the cluster) (if no validator is doing sync committee in this epoch)
// - else, accept
if !mv.ValidNumberOfDutiesPerEpoch(peerID, signedSSVMessage.SSVMessage.MsgID, partialSignatureMessages.Slot) {
return ErrTooManyDutiesPerEpoch
}
- if signedSSVMessage.SSVMessage.MsgID.GetRoleType() == types.RoleCommittee {
-
- // Rule: The number of signatures must be <= min(2*V, V + SYNC_COMMITTEE_SIZE) where V is the number of validators assigned to the cluster
- if !mv.ValidNumberOfSignaturesForCommitteeDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), &partialSignatureMessages) {
- return ErrTooManyPartialSignatureMessages
- }
-
- // Rule: a ValidatorIndex can't appear more than 2 times in the []*PartialSignatureMessage list
- if !mv.NoTripleValidatorOccurrence(&partialSignatureMessages) {
- return ErrTripleValidatorIndexInPartialSignatures
- }
- } else if signedSSVMessage.SSVMessage.MsgID.GetRoleType() == types.RoleSyncCommitteeContribution {
- // Rule: The number of signatures must be <= MaxSignaturesInSyncCommitteeContribution for the sync comittee contribution duty
- if len(partialSignatureMessages.Messages) > MaxSignaturesInSyncCommitteeContribution {
- return ErrTooManyPartialSignatureMessages
+ if isCommitteeRole(msgRole) {
+
+ if msgRole == types.RoleCommittee {
+ // Rule: The number of signatures must be <= min(2*V, V + SYNC_COMMITTEE_SIZE) where V is the number of validators assigned to the cluster
+ if !mv.ValidNumberOfSignaturesForCommitteeDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), &partialSignatureMessages) {
+ return ErrTooManyPartialSignatureMessages
+ }
+ }
+
+ if msgRole == types.RoleAggregatorCommittee {
+ // Rule: The number of signatures must be <= min(5*V, V + 4*SYNC_COMMITTEE_SIZE) where V is the number of validators assigned to the cluster
+ if !mv.ValidNumberOfSignaturesForAggregatorCommitteeDuty(signedSSVMessage.SSVMessage.MsgID.GetSenderID(), &partialSignatureMessages) {
+ return ErrTooManyPartialSignatureMessages
+ }
+ }
+
+ // Rule: a ValidatorIndex can't appear more than 2 (resp. 5) times in the []*PartialSignatureMessage list for role Committee (resp. AggregatorCommittee)
+ if mv.TooManyEqualValidatorOccurrence(&partialSignatureMessages, msgRole) {
+ return ErrTooManyEqualValidatorIndicesInPartialSignatures
}
} else {
// Rule: The number of signatures must be 1 for the other types of duties
@@ -1065,7 +1085,45 @@ func (mv *MessageValidation) ValidatePartialSigMessagesByDutyLogic(peerID peer.I
- Proposal and round-change justifications were not included because they are too complex to implement at the message validation level. The cost of adding this complexity is not justified since the message count check already prevents any related attack.
+### Global Shared State Validation
+
+When processing an incoming message, an implementation MUST first validate the message against all rules using the peer-specific state. This ensures that penalty and enforcement actions are based solely on the behavior and historical actions of the sending peer, thereby preventing any peer from manipulating the system to unfairly penalize other participants.
+
+However, peer-specific validation alone does not preclude the local node from violating protocol constraints itself, for instance in scenarios such as the following:
+
+
+
+In this "Covert Attack" scenario, a malicious peer transmits two logically duplicated messages (e.g., `Prepare(data=1)` and `Prepare(data=2)` for the same duty and QBFT round, both signed by the same operator) to two different peers (A and B). Peer C may then receive conflicting messages without either A or B being in violation, since their independent histories would not indicate double signing. Nevertheless, if peer C were to accept both messages, it would be at risk of subsequently producing conflicting behavior and thus receiving penalties for duplication from others.
+
+To mitigate this class of attack and ensure protocol correctness, each node MUST maintain a *global shared state* that summarizes the node's total, network-derived view of accepted messages. This state MUST be updated following the acceptance of any message, regardless of origin peer. After a message passes all peer-specific state validations, the implementation MUST re-validate the message against all rules using the global shared state.
+
+Crucially, a node's global shared state SHOULD be comparable to the peer-specific state that other peers maintain for that node (i.e., for appropriate synchronization, `A.global_shared_state` SHOULD equal `B.peer_specific_state[A]` for any peers A and B). Acceptance of a message against this state ensures other correctly-behaving peers will also accept the same message.
+
+If any rule is triggered during the global shared state validation, the implementation MUST strictly ignore the message (even if peer-specific rules would otherwise demand an explicit rejection). This aligns with the protocol's requirement that a peer MUST NOT be penalized for actions or state outside its own control. Only if the message passes both sets of validation checks (peer-specific and shared/global) MAY it be processed further and accepted by the application.
+
+In practical terms, as seen in the [code example](#the-main-structure-function-and-constant-values), a function such as `mv.ValidateAgainstGlobalSharedState(pmsg)` MUST perform a complete revalidation of the message using the global shared state as the reference.
+
+```mermaid
+flowchart LR
+ A[Receive message from peer]
+ B[Validate with peer-specific state]
+ C{Any rule triggered?}
+ D[Reject/Ignore as per rule]
+ E[Validate with global shared state]
+ F{Any rule triggered?}
+ G[Ignore]
+ H[Accept]
+
+ A --> B
+ B --> C
+ C -- Yes --> D
+ C -- No --> E
+ E --> F
+ F -- Yes --> G
+ F -- No --> H
+```
+
### Rules suggestions for future
- Priority-based message handling: priority based on type and sender.
-- Message aggregation: aggregate similar messages (e.g. wait for a quorum of prepares, commits, round-changes, partial-sig) before delivering to the app.
\ No newline at end of file
+- Message aggregation: aggregate similar messages (e.g. wait for a quorum of prepares, commits, round-changes, partial-sig) before delivering to the app.
diff --git a/p2p/MessageValidation/images/covert_attack.png b/p2p/MessageValidation/images/covert_attack.png
new file mode 100644
index 0000000..71cf3f6
Binary files /dev/null and b/p2p/MessageValidation/images/covert_attack.png differ