-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,54 +9,66 @@ import ( | |
spectypes "github.com/ssvlabs/ssv-spec/types" | ||
) | ||
|
||
// MessageCounts tracks the number of various message types received for validation. | ||
type MessageCounts struct { | ||
PreConsensus int | ||
Proposal int | ||
Prepare int | ||
Commit int | ||
RoundChange int | ||
PostConsensus int | ||
} | ||
|
||
// String provides a formatted representation of the MessageCounts. | ||
func (c *MessageCounts) String() string { | ||
const ( | ||
preConsensusIdx = iota | ||
proposalIdx | ||
prepareIdx | ||
commitIdx | ||
roundChangeIdx | ||
postConsensusIdx | ||
) | ||
|
||
// SeenMsgTypes tracks whether various message types were received for validation. | ||
// It stores them as a bitset. It is enough because limit of all messages is 1. | ||
type SeenMsgTypes struct { | ||
v uint8 // wrapped into a struct to avoid incorrect usage | ||
} | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
nkryuchkov
Author
Contributor
|
||
|
||
// String provides a formatted representation of the SeenMsgTypes. | ||
func (c *SeenMsgTypes) String() string { | ||
b2i := func(b bool) int { | ||
if b { | ||
return 1 | ||
} | ||
return 0 | ||
} | ||
|
||
return fmt.Sprintf("pre-consensus: %v, proposal: %v, prepare: %v, commit: %v, round change: %v, post-consensus: %v", | ||
c.PreConsensus, | ||
c.Proposal, | ||
c.Prepare, | ||
c.Commit, | ||
c.RoundChange, | ||
c.PostConsensus, | ||
b2i(c.reachedPreConsensusLimit()), | ||
b2i(c.reachedProposalLimit()), | ||
b2i(c.reachedPrepareLimit()), | ||
b2i(c.reachedCommitLimit()), | ||
b2i(c.reachedRoundChangeLimit()), | ||
b2i(c.reachedPostConsensusLimit()), | ||
) | ||
} | ||
|
||
// ValidateConsensusMessage checks if the provided consensus message exceeds the set limits. | ||
// Returns an error if the message type exceeds its respective count limit. | ||
func (c *MessageCounts) ValidateConsensusMessage(signedSSVMessage *spectypes.SignedSSVMessage, msg *specqbft.Message, limits MessageCounts) error { | ||
func (c *SeenMsgTypes) ValidateConsensusMessage(signedSSVMessage *spectypes.SignedSSVMessage, msg *specqbft.Message) error { | ||
switch msg.MsgType { | ||
case specqbft.ProposalMsgType: | ||
if c.Proposal >= limits.Proposal { | ||
if c.reachedProposalLimit() { | ||
err := ErrDuplicatedMessage | ||
err.got = fmt.Sprintf("proposal, having %v", c.String()) | ||
return err | ||
} | ||
case specqbft.PrepareMsgType: | ||
if c.Prepare >= limits.Prepare { | ||
if c.reachedPrepareLimit() { | ||
err := ErrDuplicatedMessage | ||
err.got = fmt.Sprintf("prepare, having %v", c.String()) | ||
return err | ||
} | ||
case specqbft.CommitMsgType: | ||
if len(signedSSVMessage.OperatorIDs) == 1 { | ||
if c.Commit >= limits.Commit { | ||
if c.reachedCommitLimit() { | ||
err := ErrDuplicatedMessage | ||
err.got = fmt.Sprintf("commit, having %v", c.String()) | ||
return err | ||
} | ||
} | ||
case specqbft.RoundChangeMsgType: | ||
if c.RoundChange >= limits.RoundChange { | ||
if c.reachedRoundChangeLimit() { | ||
err := ErrDuplicatedMessage | ||
|
||
err.got = fmt.Sprintf("round change, having %v", c.String()) | ||
|
@@ -71,16 +83,16 @@ func (c *MessageCounts) ValidateConsensusMessage(signedSSVMessage *spectypes.Sig | |
|
||
// ValidatePartialSignatureMessage checks if the provided partial signature message exceeds the set limits. | ||
// Returns an error if the message type exceeds its respective count limit. | ||
func (c *MessageCounts) ValidatePartialSignatureMessage(m *spectypes.PartialSignatureMessages, limits MessageCounts) error { | ||
func (c *SeenMsgTypes) ValidatePartialSignatureMessage(m *spectypes.PartialSignatureMessages) error { | ||
switch m.Type { | ||
case spectypes.RandaoPartialSig, spectypes.SelectionProofPartialSig, spectypes.ContributionProofs, spectypes.ValidatorRegistrationPartialSig, spectypes.VoluntaryExitPartialSig: | ||
if c.PreConsensus >= limits.PreConsensus { | ||
if c.reachedPreConsensusLimit() { | ||
err := ErrInvalidPartialSignatureTypeCount | ||
err.got = fmt.Sprintf("pre-consensus, having %v", c.String()) | ||
return err | ||
} | ||
case spectypes.PostConsensusPartialSig: | ||
if c.PostConsensus >= limits.PostConsensus { | ||
if c.reachedPostConsensusLimit() { | ||
err := ErrInvalidPartialSignatureTypeCount | ||
err.got = fmt.Sprintf("post-consensus, having %v", c.String()) | ||
return err | ||
|
@@ -93,45 +105,81 @@ func (c *MessageCounts) ValidatePartialSignatureMessage(m *spectypes.PartialSign | |
} | ||
|
||
// RecordConsensusMessage updates the counts based on the provided consensus message type. | ||
func (c *MessageCounts) RecordConsensusMessage(signedSSVMessage *spectypes.SignedSSVMessage, msg *specqbft.Message) error { | ||
func (c *SeenMsgTypes) RecordConsensusMessage(signedSSVMessage *spectypes.SignedSSVMessage, msg *specqbft.Message) error { | ||
switch msg.MsgType { | ||
case specqbft.ProposalMsgType: | ||
c.Proposal++ | ||
c.recordProposal() | ||
case specqbft.PrepareMsgType: | ||
c.Prepare++ | ||
c.recordPrepare() | ||
case specqbft.CommitMsgType: | ||
if len(signedSSVMessage.OperatorIDs) == 1 { | ||
c.Commit++ | ||
c.recordCommit() | ||
} | ||
case specqbft.RoundChangeMsgType: | ||
c.RoundChange++ | ||
c.recordRoundChange() | ||
default: | ||
return fmt.Errorf("unexpected signed message type") // should be checked before | ||
} | ||
return nil | ||
} | ||
|
||
// RecordPartialSignatureMessage updates the counts based on the provided partial signature message type. | ||
func (c *MessageCounts) RecordPartialSignatureMessage(messages *spectypes.PartialSignatureMessages) error { | ||
func (c *SeenMsgTypes) RecordPartialSignatureMessage(messages *spectypes.PartialSignatureMessages) error { | ||
switch messages.Type { | ||
case spectypes.RandaoPartialSig, spectypes.SelectionProofPartialSig, spectypes.ContributionProofs, spectypes.ValidatorRegistrationPartialSig, spectypes.VoluntaryExitPartialSig: | ||
c.PreConsensus++ | ||
c.recordPreConsensus() | ||
case spectypes.PostConsensusPartialSig: | ||
c.PostConsensus++ | ||
c.recordPostConsensus() | ||
default: | ||
return fmt.Errorf("unexpected partial signature message type") // should be checked before | ||
} | ||
return nil | ||
} | ||
|
||
// maxMessageCounts is the maximum number of acceptable messages from a signer within a slot & round. | ||
func maxMessageCounts() MessageCounts { | ||
return MessageCounts{ | ||
PreConsensus: 1, | ||
Proposal: 1, | ||
Prepare: 1, | ||
Commit: 1, | ||
RoundChange: 1, | ||
PostConsensus: 1, | ||
} | ||
func (c *SeenMsgTypes) recordPreConsensus() { | ||
c.v |= 1 << preConsensusIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) recordProposal() { | ||
c.v |= 1 << proposalIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) recordPrepare() { | ||
c.v |= 1 << prepareIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) recordCommit() { | ||
c.v |= 1 << commitIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) recordRoundChange() { | ||
c.v |= 1 << roundChangeIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) recordPostConsensus() { | ||
c.v |= 1 << postConsensusIdx | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedPreConsensusLimit() bool { | ||
return (c.v & (1 << preConsensusIdx)) != 0 | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedProposalLimit() bool { | ||
return (c.v & (1 << proposalIdx)) != 0 | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedPrepareLimit() bool { | ||
return (c.v & (1 << prepareIdx)) != 0 | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedCommitLimit() bool { | ||
return (c.v & (1 << commitIdx)) != 0 | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedRoundChangeLimit() bool { | ||
return (c.v & (1 << roundChangeIdx)) != 0 | ||
} | ||
|
||
func (c *SeenMsgTypes) reachedPostConsensusLimit() bool { | ||
return (c.v & (1 << postConsensusIdx)) != 0 | ||
} |
this is a very impressive optimization!
however is it really significant in practice to justify the added complexity?
lets say with 50k validators and 5 roles, we'd have about 250k records times 8 bytes times 6 fields = 12mb
that is a lot!
however we can reduce that to 1.5mb if we use byte instead of int (8 bytes) with very minimal code changes from before
also, getting/setting bits is more CPU-intensive than integers (which includes bytes), and i think we value CPU usage a bit more than 1.5mb savings
i'm not saying we should do it yet, just asking WDYT?