From 3e867b8b9252e05072555ca519beeecb5f98a591 Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:23:40 +0200 Subject: [PATCH 1/5] draft --- message/validation/common_checks.go | 14 ++++++++++++-- message/validation/consensus_validation.go | 2 +- message/validation/partial_validation.go | 9 +++++++-- operator/duties/dutystore/duties.go | 8 ++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/message/validation/common_checks.go b/message/validation/common_checks.go index 70e6918616..17007c7e0e 100644 --- a/message/validation/common_checks.go +++ b/message/validation/common_checks.go @@ -117,10 +117,20 @@ func (mv *messageValidator) validateBeaconDuty( role spectypes.RunnerRole, slot phase0.Slot, indices []phase0.ValidatorIndex, + partialMessageType *spectypes.PartialSigMsgType, ) error { + epoch := mv.netCfg.Beacon.EstimatedEpochAtSlot(slot) + // Rule: For a proposal duty message, we check if the validator is assigned to it if role == spectypes.RoleProposer { - epoch := mv.netCfg.Beacon.EstimatedEpochAtSlot(slot) + if partialMessageType != nil && *partialMessageType == spectypes.PostConsensusPartialSig { + if mv.netCfg.Beacon.IsFirstSlotOfEpoch(slot) { + if exists := mv.dutyStore.Proposer.IsEpochSet(epoch); !exists { + return nil // Accept message even if duties are not set + } + } + } + // Non-committee roles always have one validator index. validatorIndex := indices[0] if mv.dutyStore.Proposer.ValidatorDuty(epoch, slot, validatorIndex) == nil { @@ -130,7 +140,7 @@ func (mv *messageValidator) validateBeaconDuty( // Rule: For a sync committee aggregation duty message, we check if the validator is assigned to it if role == spectypes.RoleSyncCommitteeContribution { - period := mv.netCfg.Beacon.EstimatedSyncCommitteePeriodAtEpoch(mv.netCfg.Beacon.EstimatedEpochAtSlot(slot)) + period := mv.netCfg.Beacon.EstimatedSyncCommitteePeriodAtEpoch(epoch) // Non-committee roles always have one validator index. validatorIndex := indices[0] if mv.dutyStore.SyncCommittee.Duty(period, validatorIndex) == nil { diff --git a/message/validation/consensus_validation.go b/message/validation/consensus_validation.go index 47a9327f93..5d2c20b432 100644 --- a/message/validation/consensus_validation.go +++ b/message/validation/consensus_validation.go @@ -269,7 +269,7 @@ func (mv *messageValidator) validateQBFTMessageByDutyLogic( } msgSlot := phase0.Slot(consensusMessage.Height) - if err := mv.validateBeaconDuty(role, msgSlot, validatorIndices); err != nil { + if err := mv.validateBeaconDuty(role, msgSlot, validatorIndices, nil); err != nil { return err } diff --git a/message/validation/partial_validation.go b/message/validation/partial_validation.go index 55678585a8..62a7fcbc1f 100644 --- a/message/validation/partial_validation.go +++ b/message/validation/partial_validation.go @@ -145,7 +145,7 @@ func (mv *messageValidator) validatePartialSigMessagesByDutyLogic( signerStateBySlot := state.GetOrCreate(signer) // 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 role != types.RoleCommittee { // Rule only for validator runners maxSlot := signerStateBySlot.MaxSlot() if maxSlot != 0 && maxSlot > partialSignatureMessages.Slot { e := ErrSlotAlreadyAdvanced @@ -155,7 +155,12 @@ func (mv *messageValidator) validatePartialSigMessagesByDutyLogic( } } - if err := mv.validateBeaconDuty(signedSSVMessage.SSVMessage.GetID().GetRoleType(), messageSlot, committeeInfo.indices); err != nil { + if err := mv.validateBeaconDuty( + role, + messageSlot, + committeeInfo.indices, + &partialSignatureMessages.Type, + ); err != nil { return err } diff --git a/operator/duties/dutystore/duties.go b/operator/duties/dutystore/duties.go index 175d230ca0..b381decc9c 100644 --- a/operator/duties/dutystore/duties.go +++ b/operator/duties/dutystore/duties.go @@ -96,3 +96,11 @@ func (d *Duties[D]) ResetEpoch(epoch phase0.Epoch) { delete(d.m, epoch) } + +func (d *Duties[D]) IsEpochSet(epoch phase0.Epoch) bool { + d.mu.RLock() + defer d.mu.RUnlock() + + _, exists := d.m[epoch] + return exists +} From 3e25566b590802dccbbb93f18ccf59cb8c70e040 Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:13:12 +0200 Subject: [PATCH 2/5] use right partial msg type --- message/validation/common_checks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/message/validation/common_checks.go b/message/validation/common_checks.go index 17007c7e0e..3f2fa21aac 100644 --- a/message/validation/common_checks.go +++ b/message/validation/common_checks.go @@ -123,7 +123,7 @@ func (mv *messageValidator) validateBeaconDuty( // Rule: For a proposal duty message, we check if the validator is assigned to it if role == spectypes.RoleProposer { - if partialMessageType != nil && *partialMessageType == spectypes.PostConsensusPartialSig { + if partialMessageType != nil && *partialMessageType == spectypes.RandaoPartialSig { if mv.netCfg.Beacon.IsFirstSlotOfEpoch(slot) { if exists := mv.dutyStore.Proposer.IsEpochSet(epoch); !exists { return nil // Accept message even if duties are not set From 9cb38c267afa5f9007fd275e2dd9c3dbe8190e66 Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:19:07 +0200 Subject: [PATCH 3/5] clean --- message/validation/common_checks.go | 2 +- message/validation/partial_validation.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/message/validation/common_checks.go b/message/validation/common_checks.go index 3f2fa21aac..37fea6fdd4 100644 --- a/message/validation/common_checks.go +++ b/message/validation/common_checks.go @@ -125,7 +125,7 @@ func (mv *messageValidator) validateBeaconDuty( if role == spectypes.RoleProposer { if partialMessageType != nil && *partialMessageType == spectypes.RandaoPartialSig { if mv.netCfg.Beacon.IsFirstSlotOfEpoch(slot) { - if exists := mv.dutyStore.Proposer.IsEpochSet(epoch); !exists { + if !mv.dutyStore.Proposer.IsEpochSet(epoch) { return nil // Accept message even if duties are not set } } diff --git a/message/validation/partial_validation.go b/message/validation/partial_validation.go index 62a7fcbc1f..3206ec94ca 100644 --- a/message/validation/partial_validation.go +++ b/message/validation/partial_validation.go @@ -155,12 +155,7 @@ func (mv *messageValidator) validatePartialSigMessagesByDutyLogic( } } - if err := mv.validateBeaconDuty( - role, - messageSlot, - committeeInfo.indices, - &partialSignatureMessages.Type, - ); err != nil { + if err := mv.validateBeaconDuty(role, messageSlot, committeeInfo.indices, &partialSignatureMessages.Type); err != nil { return err } From 1d29d4d20d35402d037b97425be747fa36cdc36c Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:57:11 +0200 Subject: [PATCH 4/5] simplify validation --- message/validation/common_checks.go | 10 ++++------ message/validation/consensus_validation.go | 2 +- message/validation/partial_validation.go | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/message/validation/common_checks.go b/message/validation/common_checks.go index 37fea6fdd4..9a886c0cf5 100644 --- a/message/validation/common_checks.go +++ b/message/validation/common_checks.go @@ -117,17 +117,15 @@ func (mv *messageValidator) validateBeaconDuty( role spectypes.RunnerRole, slot phase0.Slot, indices []phase0.ValidatorIndex, - partialMessageType *spectypes.PartialSigMsgType, + randao bool, ) error { epoch := mv.netCfg.Beacon.EstimatedEpochAtSlot(slot) // Rule: For a proposal duty message, we check if the validator is assigned to it if role == spectypes.RoleProposer { - if partialMessageType != nil && *partialMessageType == spectypes.RandaoPartialSig { - if mv.netCfg.Beacon.IsFirstSlotOfEpoch(slot) { - if !mv.dutyStore.Proposer.IsEpochSet(epoch) { - return nil // Accept message even if duties are not set - } + if randao && mv.netCfg.Beacon.IsFirstSlotOfEpoch(slot) { + if !mv.dutyStore.Proposer.IsEpochSet(epoch) { + return nil // Accept message even if duties are not set } } diff --git a/message/validation/consensus_validation.go b/message/validation/consensus_validation.go index 5d2c20b432..da65937d29 100644 --- a/message/validation/consensus_validation.go +++ b/message/validation/consensus_validation.go @@ -269,7 +269,7 @@ func (mv *messageValidator) validateQBFTMessageByDutyLogic( } msgSlot := phase0.Slot(consensusMessage.Height) - if err := mv.validateBeaconDuty(role, msgSlot, validatorIndices, nil); err != nil { + if err := mv.validateBeaconDuty(role, msgSlot, validatorIndices, false); err != nil { return err } diff --git a/message/validation/partial_validation.go b/message/validation/partial_validation.go index 3206ec94ca..72d16e449f 100644 --- a/message/validation/partial_validation.go +++ b/message/validation/partial_validation.go @@ -155,7 +155,8 @@ func (mv *messageValidator) validatePartialSigMessagesByDutyLogic( } } - if err := mv.validateBeaconDuty(role, messageSlot, committeeInfo.indices, &partialSignatureMessages.Type); err != nil { + randao := partialSignatureMessages.Type == spectypes.RandaoPartialSig + if err := mv.validateBeaconDuty(role, messageSlot, committeeInfo.indices, randao); err != nil { return err } From ce0b3ff47ac0edfe6629b7c0bf272b2603ad58e0 Mon Sep 17 00:00:00 2001 From: olegshmuelov <45327364+olegshmuelov@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:57:18 +0200 Subject: [PATCH 5/5] add tests --- message/validation/validation_test.go | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/message/validation/validation_test.go b/message/validation/validation_test.go index c172f60080..a5fc6b6529 100644 --- a/message/validation/validation_test.go +++ b/message/validation/validation_test.go @@ -14,6 +14,7 @@ import ( eth2apiv1 "github.com/attestantio/go-eth2-client/api/v1" "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/herumi/bls-eth-go-binary/bls" pubsub "github.com/libp2p/go-libp2p-pubsub" pspb "github.com/libp2p/go-libp2p-pubsub/pb" specqbft "github.com/ssvlabs/ssv-spec/qbft" @@ -630,6 +631,67 @@ func Test_ValidateSSVMessage(t *testing.T) { require.NoError(t, err) }) + t.Run("accept pre-consensus randao message when epoch duties are not set", func(t *testing.T) { + const epoch = 1 + slot := netCfg.Beacon.FirstSlotAtEpoch(epoch) + + ds := dutystore.New() + + validator := New(netCfg, validatorStore, ds, signatureVerifier).(*messageValidator) + + messages := generateRandaoMsg(ks.Shares[1], 1, epoch, slot) + encodedMessages, err := messages.Encode() + require.NoError(t, err) + + dutyExecutorID := shares.active.ValidatorPubKey[:] + ssvMessage := &spectypes.SSVMessage{ + MsgType: spectypes.SSVPartialSignatureMsgType, + MsgID: spectypes.NewMsgID(spectestingutils.TestingSSVDomainType, dutyExecutorID, spectypes.RoleProposer), + Data: encodedMessages, + } + + signedSSVMessage := spectestingutils.SignedSSVMessageWithSigner(1, ks.OperatorKeys[1], ssvMessage) + + receivedAt := netCfg.Beacon.GetSlotStartTime(slot) + topicID := commons.CommitteeTopicID(committeeID)[0] + + require.False(t, ds.Proposer.IsEpochSet(epoch)) + + _, err = validator.handleSignedSSVMessage(signedSSVMessage, topicID, receivedAt) + require.NoError(t, err) + }) + + t.Run("reject pre-consensus randao message when epoch duties are set", func(t *testing.T) { + const epoch = 1 + slot := netCfg.Beacon.FirstSlotAtEpoch(epoch) + + ds := dutystore.New() + ds.Proposer.Set(epoch, make([]dutystore.StoreDuty[eth2apiv1.ProposerDuty], 0)) + + validator := New(netCfg, validatorStore, ds, signatureVerifier).(*messageValidator) + + messages := generateRandaoMsg(ks.Shares[1], 1, epoch, slot) + encodedMessages, err := messages.Encode() + require.NoError(t, err) + + dutyExecutorID := shares.active.ValidatorPubKey[:] + ssvMessage := &spectypes.SSVMessage{ + MsgType: spectypes.SSVPartialSignatureMsgType, + MsgID: spectypes.NewMsgID(spectestingutils.TestingSSVDomainType, dutyExecutorID, spectypes.RoleProposer), + Data: encodedMessages, + } + + signedSSVMessage := spectestingutils.SignedSSVMessageWithSigner(1, ks.OperatorKeys[1], ssvMessage) + + receivedAt := netCfg.Beacon.GetSlotStartTime(slot) + topicID := commons.CommitteeTopicID(committeeID)[0] + + require.True(t, ds.Proposer.IsEpochSet(epoch)) + + _, err = validator.handleSignedSSVMessage(signedSSVMessage, topicID, receivedAt) + require.ErrorContains(t, err, ErrNoDuty.Error()) + }) + //// Get error when receiving a message with over 13 partial signatures t.Run("partial message too big", func(t *testing.T) { slot := netCfg.Beacon.FirstSlotAtEpoch(1) @@ -1878,3 +1940,29 @@ func generateMultiSignedMessage( return signedSSVMessage } + +var generateRandaoMsg = func( + sk *bls.SecretKey, + id spectypes.OperatorID, + epoch phase0.Epoch, + slot phase0.Slot, +) *spectypes.PartialSignatureMessages { + signer := spectestingutils.NewTestingKeyManager() + beacon := spectestingutils.NewTestingBeaconNode() + d, _ := beacon.DomainData(epoch, spectypes.DomainRandao) + signed, root, _ := signer.SignBeaconObject(spectypes.SSZUint64(epoch), d, sk.GetPublicKey().Serialize(), spectypes.DomainRandao) + + msgs := spectypes.PartialSignatureMessages{ + Type: spectypes.RandaoPartialSig, + Slot: slot, + Messages: []*spectypes.PartialSignatureMessage{}, + } + msgs.Messages = append(msgs.Messages, &spectypes.PartialSignatureMessage{ + PartialSignature: signed[:], + SigningRoot: root, + Signer: id, + ValidatorIndex: spectestingutils.TestingValidatorIndex, + }) + + return &msgs +}