From 6cb845660a3ae2efee06cb33ec7c123628d2b3cd Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:24:53 -0500 Subject: [PATCH] fixing electra attestation inconsistencies (#14331) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fixing electra attestation inconsistencies * adding dependencies * removing helper circular dependency * adding error * simplifying function * improving get committee index function * fixing more instances of GetData().committeeIndex and fixing tests * Update proto/prysm/v1alpha1/attestation.go Co-authored-by: RadosÅ‚aw Kapka * addressing feedback and fixing linting * removing unused functions and associated tests * fixing test error checks * removing helpers.VerifyAttestationBitfieldLengths to reduce beaconCommitteeFromState calls * small optimizations * fixing linting --------- Co-authored-by: RadosÅ‚aw Kapka --- .../blockchain/receive_attestation.go | 28 +++- beacon-chain/core/blocks/attestation.go | 43 +++--- beacon-chain/core/epoch/BUILD.bazel | 2 - beacon-chain/core/epoch/epoch_processing.go | 71 ---------- .../core/epoch/epoch_processing_test.go | 126 ------------------ .../core/epoch/precompute/BUILD.bazel | 1 - .../epoch/precompute/reward_penalty_test.go | 85 ------------ beacon-chain/core/helpers/beacon_committee.go | 18 --- .../core/helpers/beacon_committee_test.go | 7 +- .../rpc/prysm/v1alpha1/validator/attester.go | 14 +- .../prysm/v1alpha1/validator/attester_test.go | 6 +- .../sync/subscriber_beacon_attestation.go | 6 +- beacon-chain/sync/validate_aggregate_proof.go | 61 +++------ .../sync/validate_aggregate_proof_test.go | 24 ++-- .../sync/validate_beacon_attestation.go | 67 ++++------ .../validate_beacon_attestation_electra.go | 18 ++- proto/prysm/v1alpha1/BUILD.bazel | 1 + proto/prysm/v1alpha1/attestation.go | 38 ++++++ 18 files changed, 170 insertions(+), 446 deletions(-) diff --git a/beacon-chain/blockchain/receive_attestation.go b/beacon-chain/blockchain/receive_attestation.go index 89f344383c..3e788b9d1e 100644 --- a/beacon-chain/blockchain/receive_attestation.go +++ b/beacon-chain/blockchain/receive_attestation.go @@ -13,6 +13,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/v5/runtime/version" "github.com/prysmaticlabs/prysm/v5/time/slots" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -190,13 +191,26 @@ func (s *Service) processAttestations(ctx context.Context, disparity time.Durati } if err := s.receiveAttestationNoPubsub(ctx, a, disparity); err != nil { - log.WithFields(logrus.Fields{ - "slot": a.GetData().Slot, - "committeeIndex": a.GetData().CommitteeIndex, - "beaconBlockRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().BeaconBlockRoot)), - "targetRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().Target.Root)), - "aggregationCount": a.GetAggregationBits().Count(), - }).WithError(err).Warn("Could not process attestation for fork choice") + var fields logrus.Fields + if a.Version() >= version.Electra { + fields = logrus.Fields{ + "slot": a.GetData().Slot, + "committeeCount": a.CommitteeBitsVal().Count(), + "committeeIndices": a.CommitteeBitsVal().BitIndices(), + "beaconBlockRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().BeaconBlockRoot)), + "targetRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().Target.Root)), + "aggregatedCount": a.GetAggregationBits().Count(), + } + } else { + fields = logrus.Fields{ + "slot": a.GetData().Slot, + "committeeIndex": a.GetData().CommitteeIndex, + "beaconBlockRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().BeaconBlockRoot)), + "targetRoot": fmt.Sprintf("%#x", bytesutil.Trunc(a.GetData().Target.Root)), + "aggregatedCount": a.GetAggregationBits().Count(), + } + } + log.WithFields(fields).WithError(err).Warn("Could not process attestation for fork choice") } } } diff --git a/beacon-chain/core/blocks/attestation.go b/beacon-chain/core/blocks/attestation.go index fe71802126..67712ecbe6 100644 --- a/beacon-chain/core/blocks/attestation.go +++ b/beacon-chain/core/blocks/attestation.go @@ -110,25 +110,7 @@ func VerifyAttestationNoVerifySignature( var indexedAtt ethpb.IndexedAtt - if att.Version() < version.Electra { - if uint64(att.GetData().CommitteeIndex) >= c { - return fmt.Errorf("committee index %d >= committee count %d", att.GetData().CommitteeIndex, c) - } - - if err = helpers.VerifyAttestationBitfieldLengths(ctx, beaconState, att); err != nil { - return errors.Wrap(err, "could not verify attestation bitfields") - } - - // Verify attesting indices are correct. - committee, err := helpers.BeaconCommitteeFromState(ctx, beaconState, att.GetData().Slot, att.GetData().CommitteeIndex) - if err != nil { - return err - } - indexedAtt, err = attestation.ConvertToIndexed(ctx, att, committee) - if err != nil { - return err - } - } else { + if att.Version() >= version.Electra { if att.GetData().CommitteeIndex != 0 { return errors.New("committee index must be 0 post-Electra") } @@ -154,6 +136,29 @@ func VerifyAttestationNoVerifySignature( if err != nil { return err } + } else { + if uint64(att.GetData().CommitteeIndex) >= c { + return fmt.Errorf("committee index %d >= committee count %d", att.GetData().CommitteeIndex, c) + } + + // Verify attesting indices are correct. + committee, err := helpers.BeaconCommitteeFromState(ctx, beaconState, att.GetData().Slot, att.GetData().CommitteeIndex) + if err != nil { + return err + } + + if committee == nil { + return errors.New("no committee exist for this attestation") + } + + if err := helpers.VerifyBitfieldLength(att.GetAggregationBits(), uint64(len(committee))); err != nil { + return errors.Wrap(err, "failed to verify aggregation bitfield") + } + + indexedAtt, err = attestation.ConvertToIndexed(ctx, att, committee) + if err != nil { + return err + } } return attestation.IsValidAttestationIndices(ctx, indexedAtt) diff --git a/beacon-chain/core/epoch/BUILD.bazel b/beacon-chain/core/epoch/BUILD.bazel index e6f559e559..3a3ea5c5ee 100644 --- a/beacon-chain/core/epoch/BUILD.bazel +++ b/beacon-chain/core/epoch/BUILD.bazel @@ -22,7 +22,6 @@ go_library( "//consensus-types/primitives:go_default_library", "//math:go_default_library", "//proto/prysm/v1alpha1:go_default_library", - "//proto/prysm/v1alpha1/attestation:go_default_library", "//runtime/version:go_default_library", "@com_github_pkg_errors//:go_default_library", ], @@ -53,7 +52,6 @@ go_test( "//testing/util:go_default_library", "@com_github_google_go_cmp//cmp:go_default_library", "@com_github_google_gofuzz//:go_default_library", - "@com_github_prysmaticlabs_go_bitfield//:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/beacon-chain/core/epoch/epoch_processing.go b/beacon-chain/core/epoch/epoch_processing.go index 7856b83d45..1fe1293b53 100644 --- a/beacon-chain/core/epoch/epoch_processing.go +++ b/beacon-chain/core/epoch/epoch_processing.go @@ -20,32 +20,9 @@ import ( "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v5/math" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" - "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1/attestation" "github.com/prysmaticlabs/prysm/v5/runtime/version" ) -// AttestingBalance returns the total balance from all the attesting indices. -// -// WARNING: This method allocates a new copy of the attesting validator indices set and is -// considered to be very memory expensive. Avoid using this unless you really -// need to get attesting balance from attestations. -// -// Spec pseudocode definition: -// -// def get_attesting_balance(state: BeaconState, attestations: Sequence[PendingAttestation]) -> Gwei: -// """ -// Return the combined effective balance of the set of unslashed validators participating in ``attestations``. -// Note: ``get_total_balance`` returns ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. -// """ -// return get_total_balance(state, get_unslashed_attesting_indices(state, attestations)) -func AttestingBalance(ctx context.Context, state state.ReadOnlyBeaconState, atts []*ethpb.PendingAttestation) (uint64, error) { - indices, err := UnslashedAttestingIndices(ctx, state, atts) - if err != nil { - return 0, errors.Wrap(err, "could not get attesting indices") - } - return helpers.TotalBalance(state, indices), nil -} - // ProcessRegistryUpdates rotates validators in and out of active pool. // the amount to rotate is determined churn limit. // @@ -455,51 +432,3 @@ func ProcessFinalUpdates(state state.BeaconState) (state.BeaconState, error) { return state, nil } - -// UnslashedAttestingIndices returns all the attesting indices from a list of attestations, -// it sorts the indices and filters out the slashed ones. -// -// Spec pseudocode definition: -// -// def get_unslashed_attesting_indices(state: BeaconState, -// attestations: Sequence[PendingAttestation]) -> Set[ValidatorIndex]: -// output = set() # type: Set[ValidatorIndex] -// for a in attestations: -// output = output.union(get_attesting_indices(state, a.data, a.aggregation_bits)) -// return set(filter(lambda index: not state.validators[index].slashed, output)) -func UnslashedAttestingIndices(ctx context.Context, state state.ReadOnlyBeaconState, atts []*ethpb.PendingAttestation) ([]primitives.ValidatorIndex, error) { - var setIndices []primitives.ValidatorIndex - seen := make(map[uint64]bool) - - for _, att := range atts { - committee, err := helpers.BeaconCommitteeFromState(ctx, state, att.GetData().Slot, att.GetData().CommitteeIndex) - if err != nil { - return nil, err - } - attestingIndices, err := attestation.AttestingIndices(att, committee) - if err != nil { - return nil, err - } - // Create a set for attesting indices - for _, index := range attestingIndices { - if !seen[index] { - setIndices = append(setIndices, primitives.ValidatorIndex(index)) - } - seen[index] = true - } - } - // Sort the attesting set indices by increasing order. - sort.Slice(setIndices, func(i, j int) bool { return setIndices[i] < setIndices[j] }) - // Remove the slashed validator indices. - for i := 0; i < len(setIndices); i++ { - v, err := state.ValidatorAtIndexReadOnly(setIndices[i]) - if err != nil { - return nil, errors.Wrap(err, "failed to look up validator") - } - if !v.IsNil() && v.Slashed() { - setIndices = append(setIndices[:i], setIndices[i+1:]...) - } - } - - return setIndices, nil -} diff --git a/beacon-chain/core/epoch/epoch_processing_test.go b/beacon-chain/core/epoch/epoch_processing_test.go index dd78dd5ca6..caeee1a1b2 100644 --- a/beacon-chain/core/epoch/epoch_processing_test.go +++ b/beacon-chain/core/epoch/epoch_processing_test.go @@ -6,7 +6,6 @@ import ( "math" "testing" - "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/epoch" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time" @@ -24,131 +23,6 @@ import ( "google.golang.org/protobuf/proto" ) -func TestUnslashedAttestingIndices_CanSortAndFilter(t *testing.T) { - // Generate 2 attestations. - atts := make([]*ethpb.PendingAttestation, 2) - for i := 0; i < len(atts); i++ { - atts[i] = ðpb.PendingAttestation{ - Data: ðpb.AttestationData{Source: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - Target: ðpb.Checkpoint{Epoch: 0, Root: make([]byte, fieldparams.RootLength)}, - }, - AggregationBits: bitfield.Bitlist{0x00, 0xFF, 0xFF, 0xFF}, - } - } - - // Generate validators and state for the 2 attestations. - validatorCount := 1000 - validators := make([]*ethpb.Validator, validatorCount) - for i := 0; i < len(validators); i++ { - validators[i] = ðpb.Validator{ - ExitEpoch: params.BeaconConfig().FarFutureEpoch, - } - } - base := ðpb.BeaconState{ - Validators: validators, - RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), - } - beaconState, err := state_native.InitializeFromProtoPhase0(base) - require.NoError(t, err) - - indices, err := epoch.UnslashedAttestingIndices(context.Background(), beaconState, atts) - require.NoError(t, err) - for i := 0; i < len(indices)-1; i++ { - if indices[i] >= indices[i+1] { - t.Error("sorted indices not sorted or duplicated") - } - } - - // Verify the slashed validator is filtered. - slashedValidator := indices[0] - validators = beaconState.Validators() - validators[slashedValidator].Slashed = true - require.NoError(t, beaconState.SetValidators(validators)) - indices, err = epoch.UnslashedAttestingIndices(context.Background(), beaconState, atts) - require.NoError(t, err) - for i := 0; i < len(indices); i++ { - assert.NotEqual(t, slashedValidator, indices[i], "Slashed validator %d is not filtered", slashedValidator) - } -} - -func TestUnslashedAttestingIndices_DuplicatedAttestations(t *testing.T) { - // Generate 5 of the same attestations. - atts := make([]*ethpb.PendingAttestation, 5) - for i := 0; i < len(atts); i++ { - atts[i] = ðpb.PendingAttestation{ - Data: ðpb.AttestationData{Source: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - Target: ðpb.Checkpoint{Epoch: 0}}, - AggregationBits: bitfield.Bitlist{0x00, 0xFF, 0xFF, 0xFF}, - } - } - - // Generate validators and state for the 5 attestations. - validatorCount := 1000 - validators := make([]*ethpb.Validator, validatorCount) - for i := 0; i < len(validators); i++ { - validators[i] = ðpb.Validator{ - ExitEpoch: params.BeaconConfig().FarFutureEpoch, - } - } - base := ðpb.BeaconState{ - Validators: validators, - RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), - } - beaconState, err := state_native.InitializeFromProtoPhase0(base) - require.NoError(t, err) - - indices, err := epoch.UnslashedAttestingIndices(context.Background(), beaconState, atts) - require.NoError(t, err) - - for i := 0; i < len(indices)-1; i++ { - if indices[i] >= indices[i+1] { - t.Error("sorted indices not sorted or duplicated") - } - } -} - -func TestAttestingBalance_CorrectBalance(t *testing.T) { - helpers.ClearCache() - // Generate 2 attestations. - atts := make([]*ethpb.PendingAttestation, 2) - for i := 0; i < len(atts); i++ { - atts[i] = ðpb.PendingAttestation{ - Data: ðpb.AttestationData{ - Target: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - Source: ðpb.Checkpoint{Root: make([]byte, fieldparams.RootLength)}, - Slot: primitives.Slot(i), - }, - AggregationBits: bitfield.Bitlist{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01}, - } - } - - // Generate validators with balances and state for the 2 attestations. - validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) - balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) - for i := 0; i < len(validators); i++ { - validators[i] = ðpb.Validator{ - ExitEpoch: params.BeaconConfig().FarFutureEpoch, - EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, - } - balances[i] = params.BeaconConfig().MaxEffectiveBalance - } - base := ðpb.BeaconState{ - Slot: 2, - RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), - - Validators: validators, - Balances: balances, - } - beaconState, err := state_native.InitializeFromProtoPhase0(base) - require.NoError(t, err) - - balance, err := epoch.AttestingBalance(context.Background(), beaconState, atts) - require.NoError(t, err) - wanted := 256 * params.BeaconConfig().MaxEffectiveBalance - assert.Equal(t, wanted, balance) -} - func TestProcessSlashings_NotSlashed(t *testing.T) { base := ðpb.BeaconState{ Slot: 0, diff --git a/beacon-chain/core/epoch/precompute/BUILD.bazel b/beacon-chain/core/epoch/precompute/BUILD.bazel index 18a12269b8..588697ca3c 100644 --- a/beacon-chain/core/epoch/precompute/BUILD.bazel +++ b/beacon-chain/core/epoch/precompute/BUILD.bazel @@ -47,7 +47,6 @@ go_test( embed = [":go_default_library"], deps = [ "//beacon-chain/core/altair:go_default_library", - "//beacon-chain/core/epoch:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", diff --git a/beacon-chain/core/epoch/precompute/reward_penalty_test.go b/beacon-chain/core/epoch/precompute/reward_penalty_test.go index 4c4e3c4091..9ab32f65a9 100644 --- a/beacon-chain/core/epoch/precompute/reward_penalty_test.go +++ b/beacon-chain/core/epoch/precompute/reward_penalty_test.go @@ -6,7 +6,6 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/go-bitfield" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/epoch" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" @@ -59,90 +58,6 @@ func TestProcessRewardsAndPenaltiesPrecompute(t *testing.T) { assert.Equal(t, wanted, beaconState.Balances()[0], "Unexpected balance") } -func TestAttestationDeltaPrecompute(t *testing.T) { - e := params.BeaconConfig().SlotsPerEpoch - validatorCount := uint64(2048) - base := buildState(e+2, validatorCount) - atts := make([]*ethpb.PendingAttestation, 3) - var emptyRoot [32]byte - for i := 0; i < len(atts); i++ { - atts[i] = ðpb.PendingAttestation{ - Data: ðpb.AttestationData{ - Target: ðpb.Checkpoint{ - Root: emptyRoot[:], - }, - Source: ðpb.Checkpoint{ - Root: emptyRoot[:], - }, - BeaconBlockRoot: emptyRoot[:], - }, - AggregationBits: bitfield.Bitlist{0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x01}, - InclusionDelay: 1, - } - } - base.PreviousEpochAttestations = atts - beaconState, err := state_native.InitializeFromProtoPhase0(base) - require.NoError(t, err) - slashedAttestedIndices := []primitives.ValidatorIndex{1413} - for _, i := range slashedAttestedIndices { - vs := beaconState.Validators() - vs[i].Slashed = true - require.Equal(t, nil, beaconState.SetValidators(vs)) - } - - vp, bp, err := New(context.Background(), beaconState) - require.NoError(t, err) - vp, bp, err = ProcessAttestations(context.Background(), beaconState, vp, bp) - require.NoError(t, err) - - // Add some variances to target and head balances. - // See: https://github.com/prysmaticlabs/prysm/issues/5593 - bp.PrevEpochTargetAttested /= 2 - bp.PrevEpochHeadAttested = bp.PrevEpochHeadAttested * 2 / 3 - rewards, penalties, err := AttestationsDelta(beaconState, bp, vp) - require.NoError(t, err) - attestedBalance, err := epoch.AttestingBalance(context.Background(), beaconState, atts) - require.NoError(t, err) - totalBalance, err := helpers.TotalActiveBalance(beaconState) - require.NoError(t, err) - - attestedIndices := []primitives.ValidatorIndex{55, 1339, 1746, 1811, 1569} - for _, i := range attestedIndices { - base, err := baseReward(beaconState, i) - require.NoError(t, err, "Could not get base reward") - - // Base rewards for getting source right - wanted := attestedBalance*base/totalBalance + - bp.PrevEpochTargetAttested*base/totalBalance + - bp.PrevEpochHeadAttested*base/totalBalance - // Base rewards for proposer and attesters working together getting attestation - // on chain in the fatest manner - proposerReward := base / params.BeaconConfig().ProposerRewardQuotient - wanted += (base-proposerReward)*uint64(params.BeaconConfig().MinAttestationInclusionDelay) - 1 - assert.Equal(t, wanted, rewards[i], "Unexpected reward balance for validator with index %d", i) - // Since all these validators attested, they shouldn't get penalized. - assert.Equal(t, uint64(0), penalties[i], "Unexpected penalty balance") - } - - for _, i := range slashedAttestedIndices { - base, err := baseReward(beaconState, i) - assert.NoError(t, err, "Could not get base reward") - assert.Equal(t, uint64(0), rewards[i], "Unexpected slashed indices reward balance") - assert.Equal(t, 3*base, penalties[i], "Unexpected slashed indices penalty balance") - } - - nonAttestedIndices := []primitives.ValidatorIndex{434, 677, 872, 791} - for _, i := range nonAttestedIndices { - base, err := baseReward(beaconState, i) - assert.NoError(t, err, "Could not get base reward") - wanted := 3 * base - // Since all these validators did not attest, they shouldn't get rewarded. - assert.Equal(t, uint64(0), rewards[i], "Unexpected reward balance") - // Base penalties for not attesting. - assert.Equal(t, wanted, penalties[i], "Unexpected penalty balance") - } -} - func TestAttestationDeltas_ZeroEpoch(t *testing.T) { e := params.BeaconConfig().SlotsPerEpoch validatorCount := uint64(2048) diff --git a/beacon-chain/core/helpers/beacon_committee.go b/beacon-chain/core/helpers/beacon_committee.go index e8443630a7..2e70cf5f0f 100644 --- a/beacon-chain/core/helpers/beacon_committee.go +++ b/beacon-chain/core/helpers/beacon_committee.go @@ -337,24 +337,6 @@ func VerifyBitfieldLength(bf bitfield.Bitfield, committeeSize uint64) error { return nil } -// VerifyAttestationBitfieldLengths verifies that an attestations aggregation bitfields is -// a valid length matching the size of the committee. -func VerifyAttestationBitfieldLengths(ctx context.Context, state state.ReadOnlyBeaconState, att ethpb.Att) error { - committee, err := BeaconCommitteeFromState(ctx, state, att.GetData().Slot, att.GetData().CommitteeIndex) - if err != nil { - return errors.Wrap(err, "could not retrieve beacon committees") - } - - if committee == nil { - return errors.New("no committee exist for this attestation") - } - - if err := VerifyBitfieldLength(att.GetAggregationBits(), uint64(len(committee))); err != nil { - return errors.Wrap(err, "failed to verify aggregation bitfield") - } - return nil -} - // ShuffledIndices uses input beacon state and returns the shuffled indices of the input epoch, // the shuffled indices then can be used to break up into committees. func ShuffledIndices(s state.ReadOnlyBeaconState, epoch primitives.Epoch) ([]primitives.ValidatorIndex, error) { diff --git a/beacon-chain/core/helpers/beacon_committee_test.go b/beacon-chain/core/helpers/beacon_committee_test.go index 9604ede6f5..007685617e 100644 --- a/beacon-chain/core/helpers/beacon_committee_test.go +++ b/beacon-chain/core/helpers/beacon_committee_test.go @@ -403,7 +403,12 @@ func TestVerifyAttestationBitfieldLengths_OK(t *testing.T) { helpers.ClearCache() require.NoError(t, state.SetSlot(tt.stateSlot)) - err := helpers.VerifyAttestationBitfieldLengths(context.Background(), state, tt.attestation) + att := tt.attestation + // Verify attesting indices are correct. + committee, err := helpers.BeaconCommitteeFromState(context.Background(), state, att.GetData().Slot, att.GetData().CommitteeIndex) + require.NoError(t, err) + require.NotNil(t, committee) + err = helpers.VerifyBitfieldLength(att.GetAggregationBits(), uint64(len(committee))) if tt.verificationFailure { assert.NotNil(t, err, "Verification succeeded when it was supposed to fail") } else { diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/attester.go b/beacon-chain/rpc/prysm/v1alpha1/validator/attester.go index 26ab6d7c75..802d551bd7 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/attester.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/attester.go @@ -66,18 +66,12 @@ func (vs *Server) ProposeAttestationElectra(ctx context.Context, att *ethpb.Atte ctx, span := trace.StartSpan(ctx, "AttesterServer.ProposeAttestationElectra") defer span.End() - if att.GetData().CommitteeIndex != 0 { - return nil, status.Errorf(codes.InvalidArgument, "Committee index must be set to 0") - } - committeeIndices := helpers.CommitteeIndices(att.CommitteeBits) - if len(committeeIndices) == 0 { - return nil, status.Errorf(codes.InvalidArgument, "Committee bits has no bit set") - } - if len(committeeIndices) > 1 { - return nil, status.Errorf(codes.InvalidArgument, "Committee bits has more than one bit set") + committeeIndex, err := att.GetCommitteeIndex() + if err != nil { + return nil, err } - resp, err := vs.proposeAtt(ctx, att, committeeIndices[0]) + resp, err := vs.proposeAtt(ctx, att, committeeIndex) if err != nil { return nil, err } diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/attester_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/attester_test.go index 6fe5e884d4..e293d09f25 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/attester_test.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/attester_test.go @@ -113,7 +113,7 @@ func TestProposeAttestation(t *testing.T) { CommitteeBits: cb, } _, err = attesterServer.ProposeAttestationElectra(context.Background(), req) - assert.ErrorContains(t, "Committee index must be set to 0", err) + assert.ErrorContains(t, "attestation data's committee index must be 0 but was 1", err) }) t.Run("Electra - no committee bit set", func(t *testing.T) { state, err := util.NewBeaconStateElectra() @@ -131,7 +131,7 @@ func TestProposeAttestation(t *testing.T) { CommitteeBits: primitives.NewAttestationCommitteeBits(), } _, err = attesterServer.ProposeAttestationElectra(context.Background(), req) - assert.ErrorContains(t, "Committee bits has no bit set", err) + assert.ErrorContains(t, "exactly 1 committee index must be set but 0 were set", err) }) t.Run("Electra - multiple committee bits set", func(t *testing.T) { state, err := util.NewBeaconStateElectra() @@ -152,7 +152,7 @@ func TestProposeAttestation(t *testing.T) { CommitteeBits: cb, } _, err = attesterServer.ProposeAttestationElectra(context.Background(), req) - assert.ErrorContains(t, "Committee bits has more than one bit set", err) + assert.ErrorContains(t, "exactly 1 committee index must be set but 2 were set", err) }) } diff --git a/beacon-chain/sync/subscriber_beacon_attestation.go b/beacon-chain/sync/subscriber_beacon_attestation.go index d6d280c388..ab440132e4 100644 --- a/beacon-chain/sync/subscriber_beacon_attestation.go +++ b/beacon-chain/sync/subscriber_beacon_attestation.go @@ -25,7 +25,11 @@ func (s *Service) committeeIndexBeaconAttestationSubscriber(_ context.Context, m if data == nil { return errors.New("nil attestation") } - s.setSeenCommitteeIndicesSlot(data.Slot, data.CommitteeIndex, a.GetAggregationBits()) + committeeIndex, err := a.GetCommitteeIndex() + if err != nil { + return errors.Wrap(err, "committeeIndexBeaconAttestationSubscriber failed to get committee index") + } + s.setSeenCommitteeIndicesSlot(data.Slot, committeeIndex, a.GetAggregationBits()) exists, err := s.cfg.attPool.HasAggregatedAttestation(a) if err != nil { diff --git a/beacon-chain/sync/validate_aggregate_proof.go b/beacon-chain/sync/validate_aggregate_proof.go index e5f4d0436a..188ced2430 100644 --- a/beacon-chain/sync/validate_aggregate_proof.go +++ b/beacon-chain/sync/validate_aggregate_proof.go @@ -167,39 +167,32 @@ func (s *Service) validateAggregatedAtt(ctx context.Context, signed ethpb.Signed return pubsub.ValidationIgnore, err } + committeeIndex, _, result, err := s.validateCommitteeIndexAndCount(ctx, aggregate, bs) + if result != pubsub.ValidationAccept { + wrappedErr := errors.Wrapf(err, "could not validate committee index") + tracing.AnnotateError(span, wrappedErr) + return result, err + } + + committee, result, err := s.validateBitLength(ctx, bs, aggregate.GetData().Slot, committeeIndex, aggregate.GetAggregationBits()) + if result != pubsub.ValidationAccept { + return result, err + } + // Verify validator index is within the beacon committee. - result, err := s.validateIndexInCommittee(ctx, bs, aggregate, aggregatorIndex) + result, err = s.validateIndexInCommittee(ctx, aggregate, aggregatorIndex, committee) if result != pubsub.ValidationAccept { wrappedErr := errors.Wrapf(err, "could not validate index in committee") tracing.AnnotateError(span, wrappedErr) return result, wrappedErr } - var committeeIndex primitives.CommitteeIndex - if signed.Version() >= version.Electra { - a, ok := aggregate.(*ethpb.AttestationElectra) - // This will never fail in practice because we asserted the version - if !ok { - err := fmt.Errorf("aggregate attestation has wrong type (expected %T, got %T)", ðpb.AttestationElectra{}, aggregate) - tracing.AnnotateError(span, err) - return pubsub.ValidationIgnore, err - } - committeeIndex, result, err = validateCommitteeIndexElectra(ctx, a) - if result != pubsub.ValidationAccept { - wrappedErr := errors.Wrapf(err, "could not validate committee index for Electra version") - tracing.AnnotateError(span, wrappedErr) - return result, wrappedErr - } - } else { - committeeIndex = data.CommitteeIndex - } - // Verify selection proof reflects to the right validator. selectionSigSet, err := validateSelectionIndex( ctx, bs, data.Slot, - committeeIndex, + committee, aggregatorIndex, aggregateAndProof.GetSelectionProof(), ) @@ -266,24 +259,10 @@ func (s *Service) setAggregatorIndexEpochSeen(epoch primitives.Epoch, aggregator // - [REJECT] The aggregate attestation has participants -- that is, len(get_attesting_indices(state, aggregate.data, aggregate.aggregation_bits)) >= 1. // - [REJECT] The aggregator's validator index is within the committee -- // i.e. `aggregate_and_proof.aggregator_index in get_beacon_committee(state, aggregate.data.slot, aggregate.data.index)`. -// -// Post-Electra: -// - [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(aggregate). -// - [REJECT] aggregate.data.index == 0 -func (s *Service) validateIndexInCommittee(ctx context.Context, bs state.ReadOnlyBeaconState, a ethpb.Att, validatorIndex primitives.ValidatorIndex) (pubsub.ValidationResult, error) { - ctx, span := trace.StartSpan(ctx, "sync.validateIndexInCommittee") +func (s *Service) validateIndexInCommittee(ctx context.Context, a ethpb.Att, validatorIndex primitives.ValidatorIndex, committee []primitives.ValidatorIndex) (pubsub.ValidationResult, error) { + _, span := trace.StartSpan(ctx, "sync.validateIndexInCommittee") defer span.End() - committeeIndex, _, result, err := s.validateCommitteeIndex(ctx, a, bs) - if result != pubsub.ValidationAccept { - return result, err - } - - committee, result, err := s.validateBitLength(ctx, bs, a.GetData().Slot, committeeIndex, a.GetAggregationBits()) - if result != pubsub.ValidationAccept { - return result, err - } - if a.GetAggregationBits().Count() == 0 { return pubsub.ValidationReject, errors.New("no attesting indices") } @@ -308,17 +287,13 @@ func validateSelectionIndex( ctx context.Context, bs state.ReadOnlyBeaconState, slot primitives.Slot, - committeeIndex primitives.CommitteeIndex, + committee []primitives.ValidatorIndex, validatorIndex primitives.ValidatorIndex, proof []byte, ) (*bls.SignatureBatch, error) { - ctx, span := trace.StartSpan(ctx, "sync.validateSelectionIndex") + _, span := trace.StartSpan(ctx, "sync.validateSelectionIndex") defer span.End() - committee, err := helpers.BeaconCommitteeFromState(ctx, bs, slot, committeeIndex) - if err != nil { - return nil, err - } aggregator, err := helpers.IsAggregator(uint64(len(committee)), proof) if err != nil { return nil, err diff --git a/beacon-chain/sync/validate_aggregate_proof_test.go b/beacon-chain/sync/validate_aggregate_proof_test.go index f1dd12292d..6376e3f42d 100644 --- a/beacon-chain/sync/validate_aggregate_proof_test.go +++ b/beacon-chain/sync/validate_aggregate_proof_test.go @@ -50,12 +50,13 @@ func TestVerifyIndexInCommittee_CanVerify(t *testing.T) { assert.NoError(t, err) indices, err := attestation.AttestingIndices(att, committee) require.NoError(t, err) - result, err := service.validateIndexInCommittee(ctx, s, att, primitives.ValidatorIndex(indices[0])) + + result, err := service.validateIndexInCommittee(ctx, att, primitives.ValidatorIndex(indices[0]), committee) require.NoError(t, err) assert.Equal(t, pubsub.ValidationAccept, result) wanted := "validator index 1000 is not within the committee" - result, err = service.validateIndexInCommittee(ctx, s, att, 1000) + result, err = service.validateIndexInCommittee(ctx, att, 1000, committee) assert.ErrorContains(t, wanted, err) assert.Equal(t, pubsub.ValidationReject, result) } @@ -78,28 +79,30 @@ func TestVerifyIndexInCommittee_ExistsInBeaconCommittee(t *testing.T) { att.AggregationBits = bl service := &Service{} - result, err := service.validateIndexInCommittee(ctx, s, att, committee[0]) + result, err := service.validateIndexInCommittee(ctx, att, committee[0], committee) require.ErrorContains(t, "no attesting indices", err) assert.Equal(t, pubsub.ValidationReject, result) att.AggregationBits.SetBitAt(0, true) - result, err = service.validateIndexInCommittee(ctx, s, att, committee[0]) + result, err = service.validateIndexInCommittee(ctx, att, committee[0], committee) require.NoError(t, err) assert.Equal(t, pubsub.ValidationAccept, result) wanted := "validator index 1000 is not within the committee" - result, err = service.validateIndexInCommittee(ctx, s, att, 1000) + result, err = service.validateIndexInCommittee(ctx, att, 1000, committee) assert.ErrorContains(t, wanted, err) assert.Equal(t, pubsub.ValidationReject, result) att.AggregationBits = bitfield.NewBitlist(1) - result, err = service.validateIndexInCommittee(ctx, s, att, committee[0]) + committeeIndex, err := att.GetCommitteeIndex() + require.NoError(t, err) + _, result, err = service.validateBitLength(ctx, s, att.Data.Slot, committeeIndex, att.AggregationBits) require.ErrorContains(t, "wanted participants bitfield length 4, got: 1", err) assert.Equal(t, pubsub.ValidationReject, result) att.Data.CommitteeIndex = 10000 - result, err = service.validateIndexInCommittee(ctx, s, att, committee[0]) + _, _, result, err = service.validateCommitteeIndexAndCount(ctx, att, s) require.ErrorContains(t, "committee index 10000 > 2", err) assert.Equal(t, pubsub.ValidationReject, result) } @@ -117,7 +120,7 @@ func TestVerifyIndexInCommittee_Electra(t *testing.T) { bl.SetBitAt(0, true) att.AggregationBits = bl - result, err := service.validateIndexInCommittee(ctx, s, att, committee[0]) + result, err := service.validateIndexInCommittee(ctx, att, committee[0], committee) require.NoError(t, err) assert.Equal(t, pubsub.ValidationAccept, result) } @@ -131,8 +134,9 @@ func TestVerifySelection_NotAnAggregator(t *testing.T) { sig := privKeys[0].Sign([]byte{'A'}) data := util.HydrateAttestationData(ðpb.AttestationData{}) - - _, err := validateSelectionIndex(ctx, beaconState, data.Slot, data.CommitteeIndex, 0, sig.Marshal()) + committee, err := helpers.BeaconCommitteeFromState(ctx, beaconState, data.Slot, data.CommitteeIndex) + require.NoError(t, err) + _, err = validateSelectionIndex(ctx, beaconState, data.Slot, committee, 0, sig.Marshal()) wanted := "validator is not an aggregator for slot" assert.ErrorContains(t, wanted, err) } diff --git a/beacon-chain/sync/validate_beacon_attestation.go b/beacon-chain/sync/validate_beacon_attestation.go index 79a86f83bf..a57eb9b4b9 100644 --- a/beacon-chain/sync/validate_beacon_attestation.go +++ b/beacon-chain/sync/validate_beacon_attestation.go @@ -94,28 +94,16 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p var validationRes pubsub.ValidationResult - var committeeIndex primitives.CommitteeIndex - if att.Version() >= version.Electra { - a, ok := att.(*eth.AttestationElectra) - // This will never fail in practice because we asserted the version - if !ok { - err := fmt.Errorf("attestation has wrong type (expected %T, got %T)", ð.AttestationElectra{}, att) - tracing.AnnotateError(span, err) - return pubsub.ValidationIgnore, err - } - committeeIndex, validationRes, err = validateCommitteeIndexElectra(ctx, a) - if validationRes != pubsub.ValidationAccept { - wrappedErr := errors.Wrapf(err, "could not validate committee index for Electra version") - tracing.AnnotateError(span, wrappedErr) - return validationRes, wrappedErr - } - } else { - committeeIndex = data.CommitteeIndex + committeeIndex, result, err := s.validateCommitteeIndex(ctx, att) + if result != pubsub.ValidationAccept { + wrappedErr := errors.Wrapf(err, "could not validate committee index for %s version", version.String(att.Version())) + tracing.AnnotateError(span, wrappedErr) + return result, wrappedErr } if !features.Get().EnableSlasher { // Verify this the first attestation received for the participating validator for the slot. - if s.hasSeenCommitteeIndicesSlot(data.Slot, data.CommitteeIndex, att.GetAggregationBits()) { + if s.hasSeenCommitteeIndicesSlot(data.Slot, committeeIndex, att.GetAggregationBits()) { return pubsub.ValidationIgnore, nil } @@ -205,7 +193,7 @@ func (s *Service) validateCommitteeIndexBeaconAttestation(ctx context.Context, p }() } - s.setSeenCommitteeIndicesSlot(data.Slot, data.CommitteeIndex, att.GetAggregationBits()) + s.setSeenCommitteeIndicesSlot(data.Slot, committeeIndex, att.GetAggregationBits()) msg.ValidatorData = att @@ -217,7 +205,7 @@ func (s *Service) validateUnaggregatedAttTopic(ctx context.Context, a eth.Att, b ctx, span := trace.StartSpan(ctx, "sync.validateUnaggregatedAttTopic") defer span.End() - _, valCount, result, err := s.validateCommitteeIndex(ctx, a, bs) + _, valCount, result, err := s.validateCommitteeIndexAndCount(ctx, a, bs) if result != pubsub.ValidationAccept { return result, err } @@ -235,44 +223,45 @@ func (s *Service) validateUnaggregatedAttTopic(ctx context.Context, a eth.Att, b return pubsub.ValidationAccept, nil } -func (s *Service) validateCommitteeIndex( +func (s *Service) validateCommitteeIndexAndCount( ctx context.Context, a eth.Att, bs state.ReadOnlyBeaconState, ) (primitives.CommitteeIndex, uint64, pubsub.ValidationResult, error) { + ci, result, err := s.validateCommitteeIndex(ctx, a) + if result != pubsub.ValidationAccept { + return 0, 0, result, err + } valCount, err := helpers.ActiveValidatorCount(ctx, bs, slots.ToEpoch(a.GetData().Slot)) if err != nil { return 0, 0, pubsub.ValidationIgnore, err } count := helpers.SlotCommitteeCount(valCount) - if uint64(a.GetData().CommitteeIndex) > count { - return 0, 0, pubsub.ValidationReject, errors.Errorf("committee index %d > %d", a.GetData().CommitteeIndex, count) - } - - var ci primitives.CommitteeIndex - if a.Version() >= version.Electra { - dataCi := a.GetData().CommitteeIndex - if dataCi != 0 { - return 0, 0, pubsub.ValidationReject, fmt.Errorf("committee index must be 0 but was %d", dataCi) - } - indices := helpers.CommitteeIndices(a.CommitteeBitsVal()) - if len(indices) != 1 { - return 0, 0, pubsub.ValidationReject, fmt.Errorf("exactly 1 committee index must be set but %d were set", len(indices)) - } - ci = indices[0] - } else { - ci = a.GetData().CommitteeIndex + if uint64(ci) > count { + return 0, 0, pubsub.ValidationReject, fmt.Errorf("committee index %d > %d", a.GetData().CommitteeIndex, count) } return ci, valCount, pubsub.ValidationAccept, nil } +func (s *Service) validateCommitteeIndex(ctx context.Context, a eth.Att) (primitives.CommitteeIndex, pubsub.ValidationResult, error) { + if a.Version() >= version.Electra { + return validateCommitteeIndexElectra(ctx, a) + } + return a.GetData().CommitteeIndex, pubsub.ValidationAccept, nil +} + // This validates beacon unaggregated attestation using the given state, the validation consists of bitfield length and count consistency // and signature verification. func (s *Service) validateUnaggregatedAttWithState(ctx context.Context, a eth.Att, bs state.ReadOnlyBeaconState) (pubsub.ValidationResult, error) { ctx, span := trace.StartSpan(ctx, "sync.validateUnaggregatedAttWithState") defer span.End() - committee, result, err := s.validateBitLength(ctx, bs, a.GetData().Slot, a.GetData().CommitteeIndex, a.GetAggregationBits()) + committeeIndex, err := a.GetCommitteeIndex() + if err != nil { + return pubsub.ValidationIgnore, err + } + + committee, result, err := s.validateBitLength(ctx, bs, a.GetData().Slot, committeeIndex, a.GetAggregationBits()) if result != pubsub.ValidationAccept { return result, err } diff --git a/beacon-chain/sync/validate_beacon_attestation_electra.go b/beacon-chain/sync/validate_beacon_attestation_electra.go index e0693f0f3d..8ceaee41b9 100644 --- a/beacon-chain/sync/validate_beacon_attestation_electra.go +++ b/beacon-chain/sync/validate_beacon_attestation_electra.go @@ -5,7 +5,6 @@ import ( "fmt" pubsub "github.com/libp2p/go-libp2p-pubsub" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" "go.opencensus.io/trace" @@ -14,17 +13,16 @@ import ( // validateCommitteeIndexElectra implements the following checks from the spec: // - [REJECT] len(committee_indices) == 1, where committee_indices = get_committee_indices(attestation). // - [REJECT] attestation.data.index == 0 -func validateCommitteeIndexElectra(ctx context.Context, a *ethpb.AttestationElectra) (primitives.CommitteeIndex, pubsub.ValidationResult, error) { +func validateCommitteeIndexElectra(ctx context.Context, a ethpb.Att) (primitives.CommitteeIndex, pubsub.ValidationResult, error) { _, span := trace.StartSpan(ctx, "sync.validateCommitteeIndexElectra") defer span.End() - - ci := a.Data.CommitteeIndex - if ci != 0 { - return 0, pubsub.ValidationReject, fmt.Errorf("committee index must be 0 but was %d", ci) + _, ok := a.(*ethpb.AttestationElectra) + if !ok { + return 0, pubsub.ValidationIgnore, fmt.Errorf("attestation has wrong type (expected %T, got %T)", ðpb.AttestationElectra{}, a) } - committeeIndices := helpers.CommitteeIndices(a.CommitteeBits) - if len(committeeIndices) != 1 { - return 0, pubsub.ValidationReject, fmt.Errorf("exactly 1 committee index must be set but %d were set", len(committeeIndices)) + committeeIndex, err := a.GetCommitteeIndex() + if err != nil { + return 0, pubsub.ValidationReject, err } - return committeeIndices[0], pubsub.ValidationAccept, nil + return committeeIndex, pubsub.ValidationAccept, nil } diff --git a/proto/prysm/v1alpha1/BUILD.bazel b/proto/prysm/v1alpha1/BUILD.bazel index 0c6f88327c..26453a39ca 100644 --- a/proto/prysm/v1alpha1/BUILD.bazel +++ b/proto/prysm/v1alpha1/BUILD.bazel @@ -332,6 +332,7 @@ go_library( "//proto/engine/v1:go_default_library", "//proto/eth/ext:go_default_library", "//runtime/version:go_default_library", + "@com_github_pkg_errors//:go_default_library", "@com_github_golang_protobuf//proto:go_default_library", "@com_github_grpc_ecosystem_grpc_gateway_v2//protoc-gen-openapiv2/options:options_go_proto", "@com_github_grpc_ecosystem_grpc_gateway_v2//runtime:go_default_library", diff --git a/proto/prysm/v1alpha1/attestation.go b/proto/prysm/v1alpha1/attestation.go index d59d2cfcdb..81d37f3fbb 100644 --- a/proto/prysm/v1alpha1/attestation.go +++ b/proto/prysm/v1alpha1/attestation.go @@ -1,6 +1,9 @@ package eth import ( + "fmt" + + "github.com/pkg/errors" ssz "github.com/prysmaticlabs/fastssz" "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -21,6 +24,7 @@ type Att interface { GetData() *AttestationData CommitteeBitsVal() bitfield.Bitfield GetSignature() []byte + GetCommitteeIndex() (primitives.CommitteeIndex, error) } // IndexedAtt defines common functionality for all indexed attestation types. @@ -123,6 +127,14 @@ func (a *Attestation) CommitteeBitsVal() bitfield.Bitfield { return cb } +// GetCommitteeIndex -- +func (a *Attestation) GetCommitteeIndex() (primitives.CommitteeIndex, error) { + if a == nil || a.Data == nil { + return 0, errors.New("nil attestation data") + } + return a.Data.CommitteeIndex, nil +} + // Version -- func (a *PendingAttestation) Version() int { return version.Phase0 @@ -156,6 +168,14 @@ func (a *PendingAttestation) GetSignature() []byte { return nil } +// GetCommitteeIndex -- +func (a *PendingAttestation) GetCommitteeIndex() (primitives.CommitteeIndex, error) { + if a == nil || a.Data == nil { + return 0, errors.New("nil attestation data") + } + return a.Data.CommitteeIndex, nil +} + // Version -- func (a *AttestationElectra) Version() int { return version.Electra @@ -184,6 +204,24 @@ func (a *AttestationElectra) CommitteeBitsVal() bitfield.Bitfield { return a.CommitteeBits } +// GetCommitteeIndex -- +func (a *AttestationElectra) GetCommitteeIndex() (primitives.CommitteeIndex, error) { + if a == nil || a.Data == nil { + return 0, errors.New("nil attestation data") + } + if len(a.CommitteeBits) == 0 { + return 0, errors.New("no committee bits found in attestation") + } + if a.Data.CommitteeIndex != 0 { + return 0, fmt.Errorf("attestation data's committee index must be 0 but was %d", a.Data.CommitteeIndex) + } + indices := a.CommitteeBits.BitIndices() + if len(indices) != 1 { + return 0, fmt.Errorf("exactly 1 committee index must be set but %d were set", len(indices)) + } + return primitives.CommitteeIndex(uint64(indices[0])), nil +} + // Version -- func (a *IndexedAttestation) Version() int { return version.Phase0