diff --git a/beacon-chain/sync/validate_beacon_attestation.go b/beacon-chain/sync/validate_beacon_attestation.go index 67953b9677..90d33ce2f0 100644 --- a/beacon-chain/sync/validate_beacon_attestation.go +++ b/beacon-chain/sync/validate_beacon_attestation.go @@ -268,10 +268,23 @@ func (s *Service) validateCommitteeIndexAndCount( a eth.Att, bs state.ReadOnlyBeaconState, ) (primitives.CommitteeIndex, uint64, pubsub.ValidationResult, error) { - // - [REJECT] attestation.data.index == 0 - if a.Version() >= version.Electra && a.GetData().CommitteeIndex != 0 { - return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0") + // Validate committee index based on fork. + if a.Version() >= version.Electra { + data := a.GetData() + attEpoch := slots.ToEpoch(data.Slot) + isGloas := attEpoch >= params.BeaconConfig().GloasForkEpoch + if isGloas { + if result, err := s.validateGloasCommitteeIndex(data); result != pubsub.ValidationAccept { + return 0, 0, result, err + } + } else { + // [REJECT] attestation.data.index == 0 (New in Electra, removed in Gloas) + if data.CommitteeIndex != 0 { + return 0, 0, pubsub.ValidationReject, errors.New("attestation data's committee index must be 0") + } + } } + valCount, err := helpers.ActiveValidatorCount(ctx, bs, slots.ToEpoch(a.GetData().Slot)) if err != nil { return 0, 0, pubsub.ValidationIgnore, err @@ -356,6 +369,29 @@ func validateAttestingIndex( return pubsub.ValidationAccept, nil } +// validateGloasCommitteeIndex validates committee index rules for Gloas fork. +// [REJECT] attestation.data.index < 2. (New in Gloas) +// [REJECT] attestation.data.index == 0 if block.slot == attestation.data.slot. (New in Gloas) +func (s *Service) validateGloasCommitteeIndex(data *eth.AttestationData) (pubsub.ValidationResult, error) { + if data.CommitteeIndex >= 2 { + return pubsub.ValidationReject, errors.New("attestation data's committee index must be < 2") + } + + // Same-slot attestations must use committee index 0 + if data.CommitteeIndex != 0 { + blockRoot := bytesutil.ToBytes32(data.BeaconBlockRoot) + slot, err := s.cfg.chain.RecentBlockSlot(blockRoot) + if err != nil { + return pubsub.ValidationIgnore, err + } + if slot == data.Slot { + return pubsub.ValidationReject, errors.New("same slot attestations must use committee index 0") + } + } + + return pubsub.ValidationAccept, nil +} + // generateUnaggregatedAttCacheKey generates the cache key for unaggregated attestation tracking. func generateUnaggregatedAttCacheKey(att eth.Att) (string, error) { var attester uint64 diff --git a/beacon-chain/sync/validate_beacon_attestation_test.go b/beacon-chain/sync/validate_beacon_attestation_test.go index 45b066bea8..7067354a14 100644 --- a/beacon-chain/sync/validate_beacon_attestation_test.go +++ b/beacon-chain/sync/validate_beacon_attestation_test.go @@ -684,3 +684,75 @@ func Test_validateCommitteeIndexAndCount_Boundary(t *testing.T) { require.ErrorContains(t, "committee index", err) require.Equal(t, pubsub.ValidationReject, res) } + +func Test_validateGloasCommitteeIndex(t *testing.T) { + tests := []struct { + name string + committeeIndex primitives.CommitteeIndex + attestationSlot primitives.Slot + blockSlot primitives.Slot + wantResult pubsub.ValidationResult + wantErr string + }{ + { + name: "committee index >= 2 should reject", + committeeIndex: 2, + attestationSlot: 10, + blockSlot: 10, + wantResult: pubsub.ValidationReject, + wantErr: "committee index must be < 2", + }, + { + name: "committee index 0 should accept", + committeeIndex: 0, + attestationSlot: 10, + blockSlot: 10, + wantResult: pubsub.ValidationAccept, + wantErr: "", + }, + { + name: "committee index 1 different-slot should accept", + committeeIndex: 1, + attestationSlot: 10, + blockSlot: 9, + wantResult: pubsub.ValidationAccept, + wantErr: "", + }, + { + name: "committee index 1 same-slot should reject", + committeeIndex: 1, + attestationSlot: 10, + blockSlot: 10, + wantResult: pubsub.ValidationReject, + wantErr: "same slot attestations must use committee index 0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockChain := &mockChain.ChainService{ + BlockSlot: tt.blockSlot, + } + s := &Service{ + cfg: &config{ + chain: mockChain, + }, + } + + data := ðpb.AttestationData{ + Slot: tt.attestationSlot, + CommitteeIndex: tt.committeeIndex, + BeaconBlockRoot: bytesutil.PadTo([]byte("blockroot"), 32), + } + + result, err := s.validateGloasCommitteeIndex(data) + + require.Equal(t, tt.wantResult, result) + if tt.wantErr != "" { + require.ErrorContains(t, tt.wantErr, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/changelog/terence_gloas-attestation-validation.md b/changelog/terence_gloas-attestation-validation.md new file mode 100644 index 0000000000..94749d9328 --- /dev/null +++ b/changelog/terence_gloas-attestation-validation.md @@ -0,0 +1,3 @@ +### Added + +- Add gossip beacon attestation validation conditions for Gloas fork