diff --git a/api/submitaggregateattestationsopts.go b/api/submitaggregateattestationsopts.go new file mode 100644 index 00000000..e611ead0 --- /dev/null +++ b/api/submitaggregateattestationsopts.go @@ -0,0 +1,24 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import "github.com/attestantio/go-eth2-client/spec" + +// SubmitAggregateAttestationsOpts are the options for submitting attestations. +type SubmitAggregateAttestationsOpts struct { + Common CommonOpts + + // SignedAggregateAndProofs are the aggregate and proofs to submit. + SignedAggregateAndProofs []*spec.VersionedSignedAggregateAndProof +} diff --git a/http/aggregateattestation.go b/http/aggregateattestation.go index 8b8e1e9f..ee95fc9f 100644 --- a/http/aggregateattestation.go +++ b/http/aggregateattestation.go @@ -21,14 +21,14 @@ import ( client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/spec" ) // AggregateAttestation fetches the aggregate attestation for the given options. func (s *Service) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) { if err := s.assertIsSynced(ctx); err != nil { @@ -41,29 +41,37 @@ func (s *Service) AggregateAttestation(ctx context.Context, return nil, errors.Join(errors.New("no attestation data root specified"), client.ErrInvalidOptions) } - endpoint := "/eth/v1/validator/aggregate_attestation" + endpoint := "/eth/v2/validator/aggregate_attestation" query := fmt.Sprintf("slot=%d&attestation_data_root=%#x", opts.Slot, opts.AttestationDataRoot) httpResponse, err := s.get(ctx, endpoint, query, &opts.Common, false) if err != nil { return nil, err } - data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), phase0.Attestation{}) + data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), spec.VersionedAttestation{}) if err != nil { return nil, err } // Confirm the attestation is for the requested slot. - if data.Data.Slot != opts.Slot { + attestationData, err := data.Data() + if err != nil { + return nil, + errors.Join( + errors.New("failed to extract attestation data from response"), + err, + ) + } + if attestationData.Slot != opts.Slot { return nil, errors.Join( - fmt.Errorf("aggregate attestation for slot %d; expected %d", data.Data.Slot, opts.Slot), + fmt.Errorf("aggregate attestation for slot %d; expected %d", attestationData.Slot, opts.Slot), client.ErrInconsistentResult, ) } // Confirm the attestation data is correct. - dataRoot, err := data.Data.HashTreeRoot() + dataRoot, err := attestationData.HashTreeRoot() if err != nil { return nil, errors.Join(errors.New("failed to obtain hash tree root of aggregate attestation data"), err) } @@ -74,7 +82,7 @@ func (s *Service) AggregateAttestation(ctx context.Context, ) } - return &api.Response[*phase0.Attestation]{ + return &api.Response[*spec.VersionedAttestation]{ Metadata: metadata, Data: &data, }, nil diff --git a/http/submitaggregateattestations.go b/http/submitaggregateattestations.go index 36419288..c04b5a24 100644 --- a/http/submitaggregateattestations.go +++ b/http/submitaggregateattestations.go @@ -18,35 +18,88 @@ import ( "context" "encoding/json" "errors" + "strings" + client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/spec" ) // SubmitAggregateAttestations submits aggregate attestations. -func (s *Service) SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*phase0.SignedAggregateAndProof) error { +func (s *Service) SubmitAggregateAttestations(ctx context.Context, opts *api.SubmitAggregateAttestationsOpts) error { if err := s.assertIsSynced(ctx); err != nil { return err } + if opts == nil { + return client.ErrNoOptions + } + if len(opts.SignedAggregateAndProofs) == 0 { + return errors.Join(errors.New("no aggregate and proofs supplied"), client.ErrInvalidOptions) + } + aggregateAndProofs := opts.SignedAggregateAndProofs + unversionedAggregates, err := createUnversionedAggregates(aggregateAndProofs) + if err != nil { + return err + } - specJSON, err := json.Marshal(aggregateAndProofs) + specJSON, err := json.Marshal(unversionedAggregates) if err != nil { return errors.Join(errors.New("failed to marshal JSON"), err) } - endpoint := "/eth/v1/validator/aggregate_and_proofs" + endpoint := "/eth/v2/validator/aggregate_and_proofs" query := "" - if _, err := s.post(ctx, + headers := make(map[string]string) + headers["Eth-Consensus-Version"] = strings.ToLower(aggregateAndProofs[0].Version.String()) + if _, err = s.post(ctx, endpoint, query, - &api.CommonOpts{}, + &opts.Common, bytes.NewReader(specJSON), ContentTypeJSON, - map[string]string{}, + headers, ); err != nil { - return errors.Join(errors.New("failed to submit aggregate and proofs"), err) + return errors.Join(errors.New("failed to submit versioned aggregate and proofs"), err) } return nil } + +func createUnversionedAggregates(aggregateAndProofs []*spec.VersionedSignedAggregateAndProof) ([]any, error) { + var version spec.DataVersion + var unversionedAggregates []any + + for i := range aggregateAndProofs { + if aggregateAndProofs[i] == nil { + return nil, errors.Join(errors.New("nil aggregate and proof version supplied"), client.ErrInvalidOptions) + } + + // Ensure consistent versioning. + if version == spec.DataVersionUnknown { + version = aggregateAndProofs[i].Version + } else if version != aggregateAndProofs[i].Version { + return nil, errors.Join(errors.New("aggregate and proofs must all be of the same version"), client.ErrInvalidOptions) + } + + // Append to unversionedAggregates. + switch aggregateAndProofs[i].Version { + case spec.DataVersionPhase0: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Phase0) + case spec.DataVersionAltair: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Altair) + case spec.DataVersionBellatrix: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Bellatrix) + case spec.DataVersionCapella: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Capella) + case spec.DataVersionDeneb: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Deneb) + case spec.DataVersionElectra: + unversionedAggregates = append(unversionedAggregates, aggregateAndProofs[i].Electra) + default: + return nil, errors.Join(errors.New("unknown aggregate and proof version"), client.ErrInvalidOptions) + } + } + + return unversionedAggregates, nil +} diff --git a/mock/aggregateattestation.go b/mock/aggregateattestation.go index 8f5765d1..b39a7617 100644 --- a/mock/aggregateattestation.go +++ b/mock/aggregateattestation.go @@ -17,21 +17,25 @@ import ( "context" "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" ) -// AggregateAttestation fetches the aggregate attestation given an attestation. +// AggregateAttestation fetches the aggregate attestation for the given options. func (*Service) AggregateAttestation(_ context.Context, _ *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) { - return &api.Response[*phase0.Attestation]{ - Data: &phase0.Attestation{ - Data: &phase0.AttestationData{ - Source: &phase0.Checkpoint{}, - Target: &phase0.Checkpoint{}, + return &api.Response[*spec.VersionedAttestation]{ + Data: &spec.VersionedAttestation{ + Version: spec.DataVersionPhase0, + Phase0: &phase0.Attestation{ + Data: &phase0.AttestationData{ + Source: &phase0.Checkpoint{}, + Target: &phase0.Checkpoint{}, + }, }, }, Metadata: make(map[string]any), diff --git a/mock/submitaggregateattestations.go b/mock/submitaggregateattestations.go index 8e20adba..8e9623cc 100644 --- a/mock/submitaggregateattestations.go +++ b/mock/submitaggregateattestations.go @@ -16,10 +16,10 @@ package mock import ( "context" - spec "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/api" ) // SubmitAggregateAttestations submits aggregate attestations. -func (*Service) SubmitAggregateAttestations(_ context.Context, _ []*spec.SignedAggregateAndProof) error { +func (*Service) SubmitAggregateAttestations(_ context.Context, _ *api.SubmitAggregateAttestationsOpts) error { return nil } diff --git a/multi/aggregateattestation.go b/multi/aggregateattestation.go index ff0f7f8d..290e08ee 100644 --- a/multi/aggregateattestation.go +++ b/multi/aggregateattestation.go @@ -18,14 +18,14 @@ import ( consensusclient "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/spec" ) -// AggregateAttestation fetches the aggregate attestation given an attestation. +// AggregateAttestation fetches the aggregate attestation for the given options. func (s *Service) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) { res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { @@ -40,7 +40,7 @@ func (s *Service) AggregateAttestation(ctx context.Context, return nil, err } - response, isResponse := res.(*api.Response[*phase0.Attestation]) + response, isResponse := res.(*api.Response[*spec.VersionedAttestation]) if !isResponse { return nil, ErrIncorrectType } diff --git a/multi/submitaggregateattestations.go b/multi/submitaggregateattestations.go index 0329d201..2dc0e840 100644 --- a/multi/submitaggregateattestations.go +++ b/multi/submitaggregateattestations.go @@ -17,15 +17,15 @@ import ( "context" consensusclient "github.com/attestantio/go-eth2-client" - "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/attestantio/go-eth2-client/api" ) // SubmitAggregateAttestations submits aggregate attestations. func (s *Service) SubmitAggregateAttestations(ctx context.Context, - aggregateAndProofs []*phase0.SignedAggregateAndProof, + opts *api.SubmitAggregateAttestationsOpts, ) error { _, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { - err := client.(consensusclient.AggregateAttestationsSubmitter).SubmitAggregateAttestations(ctx, aggregateAndProofs) + err := client.(consensusclient.AggregateAttestationsSubmitter).SubmitAggregateAttestations(ctx, opts) if err != nil { return nil, err } diff --git a/multi/submitaggregateattestations_test.go b/multi/submitaggregateattestations_test.go index 0d917668..4d62bce7 100644 --- a/multi/submitaggregateattestations_test.go +++ b/multi/submitaggregateattestations_test.go @@ -15,12 +15,13 @@ package multi_test import ( "context" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/spec" "testing" consensusclient "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/mock" "github.com/attestantio/go-eth2-client/multi" - "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/attestantio/go-eth2-client/testclients" "github.com/rs/zerolog" "github.com/stretchr/testify/require" @@ -51,7 +52,11 @@ func TestSubmitAggregateAttestations(t *testing.T) { require.NoError(t, err) for i := 0; i < 128; i++ { - err := multiClient.(consensusclient.AggregateAttestationsSubmitter).SubmitAggregateAttestations(ctx, []*phase0.SignedAggregateAndProof{}) + opts := &api.SubmitAggregateAttestationsOpts{ + Common: api.CommonOpts{}, + SignedAggregateAndProofs: []*spec.VersionedSignedAggregateAndProof{}, + } + err := multiClient.(consensusclient.AggregateAttestationsSubmitter).SubmitAggregateAttestations(ctx, opts) require.NoError(t, err) } // At this point we expect mock 3 to be in active (unless probability hates us). diff --git a/service.go b/service.go index 7619b1aa..ea7888d5 100644 --- a/service.go +++ b/service.go @@ -161,7 +161,7 @@ type AggregateAttestationProvider interface { AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) } @@ -169,7 +169,7 @@ type AggregateAttestationProvider interface { // AggregateAttestationsSubmitter is the interface for submitting aggregate attestations. type AggregateAttestationsSubmitter interface { // SubmitAggregateAttestations submits aggregate attestations. - SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*phase0.SignedAggregateAndProof) error + SubmitAggregateAttestations(ctx context.Context, opts *api.SubmitAggregateAttestationsOpts) error } // AttestationDataProvider is the interface for providing attestation data. diff --git a/spec/versionedaggregateandproof.go b/spec/versionedaggregateandproof.go new file mode 100644 index 00000000..ef5f60cf --- /dev/null +++ b/spec/versionedaggregateandproof.go @@ -0,0 +1,125 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "errors" + + "github.com/attestantio/go-eth2-client/spec/electra" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// VersionedAggregateAndProof contains a versioned aggregate and proof. +type VersionedAggregateAndProof struct { + Version DataVersion + Phase0 *phase0.AggregateAndProof + Altair *phase0.AggregateAndProof + Bellatrix *phase0.AggregateAndProof + Capella *phase0.AggregateAndProof + Deneb *phase0.AggregateAndProof + Electra *electra.AggregateAndProof +} + +// HashTreeRoot returns the hash tree root of the aggregate and proof. +func (v *VersionedAggregateAndProof) HashTreeRoot() ([32]byte, error) { + switch v.Version { + case DataVersionPhase0: + if v.Phase0 == nil { + return [32]byte{}, errors.New("no phase0 aggregate and proof") + } + + return v.Phase0.HashTreeRoot() + case DataVersionAltair: + if v.Altair == nil { + return [32]byte{}, errors.New("no altair aggregate and proof") + } + + return v.Altair.HashTreeRoot() + case DataVersionBellatrix: + if v.Bellatrix == nil { + return [32]byte{}, errors.New("no bellatrix aggregate and proof") + } + + return v.Bellatrix.HashTreeRoot() + case DataVersionCapella: + if v.Capella == nil { + return [32]byte{}, errors.New("no capella aggregate and proof") + } + + return v.Capella.HashTreeRoot() + case DataVersionDeneb: + if v.Deneb == nil { + return [32]byte{}, errors.New("no deneb aggregate and proof") + } + + return v.Deneb.HashTreeRoot() + case DataVersionElectra: + if v.Electra == nil { + return [32]byte{}, errors.New("no electra aggregate and proof") + } + + return v.Electra.HashTreeRoot() + default: + return [32]byte{}, errors.New("unknown version") + } +} + +// IsEmpty returns true if there is no aggregate and proof. +func (v *VersionedAggregateAndProof) IsEmpty() bool { + return v.Phase0 == nil && v.Altair == nil && v.Bellatrix == nil && v.Capella == nil && v.Deneb == nil && v.Electra == nil +} + +// String returns a string version of the structure. +func (v *VersionedAggregateAndProof) String() string { + switch v.Version { + case DataVersionPhase0: + if v.Phase0 == nil { + return "" + } + + return v.Phase0.String() + case DataVersionAltair: + if v.Altair == nil { + return "" + } + + return v.Altair.String() + case DataVersionBellatrix: + if v.Bellatrix == nil { + return "" + } + + return v.Bellatrix.String() + case DataVersionCapella: + if v.Capella == nil { + return "" + } + + return v.Capella.String() + case DataVersionDeneb: + if v.Deneb == nil { + return "" + } + + return v.Deneb.String() + case DataVersionElectra: + if v.Electra == nil { + return "" + } + + return v.Electra.String() + default: + return "unknown version" + } +} diff --git a/spec/versionedsignedaggregateandproof.go b/spec/versionedsignedaggregateandproof.go new file mode 100644 index 00000000..01852c50 --- /dev/null +++ b/spec/versionedsignedaggregateandproof.go @@ -0,0 +1,125 @@ +// Copyright © 2025 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spec + +import ( + "errors" + + "github.com/attestantio/go-eth2-client/spec/electra" + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// VersionedSignedAggregateAndProof contains a versioned signed aggregate and proof. +type VersionedSignedAggregateAndProof struct { + Version DataVersion + Phase0 *phase0.SignedAggregateAndProof + Altair *phase0.SignedAggregateAndProof + Bellatrix *phase0.SignedAggregateAndProof + Capella *phase0.SignedAggregateAndProof + Deneb *phase0.SignedAggregateAndProof + Electra *electra.SignedAggregateAndProof +} + +// IsEmpty returns true if there is no aggregate and proof. +func (v *VersionedSignedAggregateAndProof) IsEmpty() bool { + return v.Phase0 == nil && v.Altair == nil && v.Bellatrix == nil && v.Capella == nil && v.Deneb == nil && v.Electra == nil +} + +// Slot returns the slot of the signed aggregate and proof. +func (v *VersionedSignedAggregateAndProof) Slot() (phase0.Slot, error) { + switch v.Version { + case DataVersionPhase0: + if v.Phase0 == nil { + return 0, errors.New("no phase0 signed aggregate and proof") + } + + return v.Phase0.Message.Aggregate.Data.Slot, nil + case DataVersionAltair: + if v.Altair == nil { + return 0, errors.New("no altair signed aggregate and proof") + } + + return v.Altair.Message.Aggregate.Data.Slot, nil + case DataVersionBellatrix: + if v.Bellatrix == nil { + return 0, errors.New("no bellatrix signed aggregate and proof") + } + + return v.Bellatrix.Message.Aggregate.Data.Slot, nil + case DataVersionCapella: + if v.Capella == nil { + return 0, errors.New("no capella signed aggregate and proof") + } + + return v.Capella.Message.Aggregate.Data.Slot, nil + case DataVersionDeneb: + if v.Deneb == nil { + return 0, errors.New("no deneb signed aggregate and proof") + } + + return v.Deneb.Message.Aggregate.Data.Slot, nil + case DataVersionElectra: + if v.Electra == nil { + return 0, errors.New("no electra signed aggregate and proof") + } + + return v.Electra.Message.Aggregate.Data.Slot, nil + default: + return 0, errors.New("unknown version") + } +} + +// String returns a string version of the structure. +func (v *VersionedSignedAggregateAndProof) String() string { + switch v.Version { + case DataVersionPhase0: + if v.Phase0 == nil { + return "" + } + + return v.Phase0.String() + case DataVersionAltair: + if v.Altair == nil { + return "" + } + + return v.Altair.String() + case DataVersionBellatrix: + if v.Bellatrix == nil { + return "" + } + + return v.Bellatrix.String() + case DataVersionCapella: + if v.Capella == nil { + return "" + } + + return v.Capella.String() + case DataVersionDeneb: + if v.Deneb == nil { + return "" + } + + return v.Deneb.String() + case DataVersionElectra: + if v.Electra == nil { + return "" + } + + return v.Electra.String() + default: + return "unknown version" + } +} diff --git a/testclients/erroring.go b/testclients/erroring.go index 3377f32b..febe58de 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -197,11 +197,11 @@ func (s *Erroring) TargetAggregatorsPerCommittee(ctx context.Context) (uint64, e return next.TargetAggregatorsPerCommittee(ctx) } -// AggregateAttestation fetches the aggregate attestation given an attestation. +// AggregateAttestation fetches the aggregate attestation for the given options. func (s *Erroring) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) { if err := s.maybeError(ctx); err != nil { @@ -216,7 +216,7 @@ func (s *Erroring) AggregateAttestation(ctx context.Context, } // SubmitAggregateAttestations submits aggregate attestations. -func (s *Erroring) SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*phase0.SignedAggregateAndProof) error { +func (s *Erroring) SubmitAggregateAttestations(ctx context.Context, opts *api.SubmitAggregateAttestationsOpts) error { if err := s.maybeError(ctx); err != nil { return err } @@ -225,7 +225,7 @@ func (s *Erroring) SubmitAggregateAttestations(ctx context.Context, aggregateAnd return fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) } - return next.SubmitAggregateAttestations(ctx, aggregateAndProofs) + return next.SubmitAggregateAttestations(ctx, opts) } // AttestationData fetches the attestation data for the given slot and committee index. diff --git a/testclients/sleepy.go b/testclients/sleepy.go index ee96bcb7..04389fee 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -181,11 +181,11 @@ func (s *Sleepy) TargetAggregatorsPerCommittee(ctx context.Context) (uint64, err return next.TargetAggregatorsPerCommittee(ctx) } -// AggregateAttestation fetches the aggregate attestation given an attestation. +// AggregateAttestation fetches the aggregate attestation for the given options. func (s *Sleepy) AggregateAttestation(ctx context.Context, opts *api.AggregateAttestationOpts, ) ( - *api.Response[*phase0.Attestation], + *api.Response[*spec.VersionedAttestation], error, ) { s.sleep(ctx) @@ -198,14 +198,14 @@ func (s *Sleepy) AggregateAttestation(ctx context.Context, } // SubmitAggregateAttestations submits aggregate attestations. -func (s *Sleepy) SubmitAggregateAttestations(ctx context.Context, aggregateAndProofs []*phase0.SignedAggregateAndProof) error { +func (s *Sleepy) SubmitAggregateAttestations(ctx context.Context, opts *api.SubmitAggregateAttestationsOpts) error { s.sleep(ctx) next, isNext := s.next.(consensusclient.AggregateAttestationsSubmitter) if !isNext { return errors.New("next does not support this call") } - return next.SubmitAggregateAttestations(ctx, aggregateAndProofs) + return next.SubmitAggregateAttestations(ctx, opts) } // AttestationData fetches the attestation data for the given slot and committee index.