Skip to content

Commit

Permalink
Merge pull request #178 from attestantio/electra_submit_attestations
Browse files Browse the repository at this point in the history
Add versioned attestations submission
  • Loading branch information
Bez625 authored Jan 16, 2025
2 parents 1691823 + ea83322 commit b23a5e9
Show file tree
Hide file tree
Showing 15 changed files with 483 additions and 27 deletions.
24 changes: 24 additions & 0 deletions api/submitattestationsopts.go
Original file line number Diff line number Diff line change
@@ -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"

// SubmitAttestationsOpts are the options for submitting attestations.
type SubmitAttestationsOpts struct {
Common CommonOpts

// Attestations are the attestations to submit.
Attestations []*spec.VersionedAttestation
}
75 changes: 66 additions & 9 deletions http/submitattestations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,92 @@ 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"
)

// SubmitAttestations submits attestations.
func (s *Service) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error {
// SubmitAttestations submits versioned attestations.
func (s *Service) SubmitAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error {
if err := s.assertIsSynced(ctx); err != nil {
return err
}
if opts == nil {
return client.ErrNoOptions
}
if len(opts.Attestations) == 0 {
return errors.Join(errors.New("no attestations supplied"), client.ErrInvalidOptions)
}
attestations := opts.Attestations
unversionedAttestations, err := createUnversionedAttestations(attestations)
if err != nil {
return err
}

specJSON, err := json.Marshal(attestations)
specJSON, err := json.Marshal(unversionedAttestations)
if err != nil {
return errors.Join(errors.New("failed to marshal JSON"), err)
}

endpoint := "/eth/v1/beacon/pool/attestations"
endpoint := "/eth/v2/beacon/pool/attestations"
query := ""

if _, err := s.post(ctx,
headers := make(map[string]string)
headers["Eth-Consensus-Version"] = strings.ToLower(attestations[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 beacon attestations"), err)
return errors.Join(errors.New("failed to submit versioned beacon attestations"), err)
}

return nil
}

func createUnversionedAttestations(attestations []*spec.VersionedAttestation) ([]any, error) {
var version spec.DataVersion
var unversionedAttestations []any

for i := range attestations {
if attestations[i] == nil {
return nil, errors.Join(errors.New("nil attestation version supplied"), client.ErrInvalidOptions)
}

// Ensure consistent versioning.
if version == spec.DataVersionUnknown {
version = attestations[i].Version
} else if version != attestations[i].Version {
return nil, errors.Join(errors.New("attestations must all be of the same version"), client.ErrInvalidOptions)
}

// Append to unversionedAttestations.
switch attestations[i].Version {
case spec.DataVersionPhase0:
unversionedAttestations = append(unversionedAttestations, attestations[i].Phase0)
case spec.DataVersionAltair:
unversionedAttestations = append(unversionedAttestations, attestations[i].Altair)
case spec.DataVersionBellatrix:
unversionedAttestations = append(unversionedAttestations, attestations[i].Bellatrix)
case spec.DataVersionCapella:
unversionedAttestations = append(unversionedAttestations, attestations[i].Capella)
case spec.DataVersionDeneb:
unversionedAttestations = append(unversionedAttestations, attestations[i].Deneb)
case spec.DataVersionElectra:
singleAttestation, err := attestations[i].Electra.ToSingleAttestation()
if err != nil {
return nil, errors.Join(errors.New("failed to convert attestation to single attestation"), err)
}
unversionedAttestations = append(unversionedAttestations, singleAttestation)
default:
return nil, errors.Join(errors.New("unknown attestation version"), client.ErrInvalidOptions)
}
}

return unversionedAttestations, nil
}
9 changes: 8 additions & 1 deletion http/submitattestations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package http_test

import (
"context"
"github.com/attestantio/go-eth2-client/spec"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -79,7 +80,13 @@ func TestSubmitAttestations(t *testing.T) {
}),
}

err = service.(client.AttestationsSubmitter).SubmitAttestations(ctx, []*phase0.Attestation{attestation})
versionedAttestations := []*spec.VersionedAttestation{
{Version: spec.DataVersionPhase0, Phase0: attestation},
}
opts := &api.SubmitAttestationsOpts{
Attestations: versionedAttestations,
}
err = service.(client.AttestationsSubmitter).SubmitAttestations(ctx, opts)
switch {
case test.err != "":
require.ErrorContains(t, err, test.err)
Expand Down
4 changes: 2 additions & 2 deletions mock/submitattestations.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ package mock
import (
"context"

spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/api"
)

// SubmitAttestations submits attestations.
func (*Service) SubmitAttestations(_ context.Context, _ []*spec.Attestation) error {
func (*Service) SubmitAttestations(_ context.Context, _ *api.SubmitAttestationsOpts) error {
return nil
}
6 changes: 3 additions & 3 deletions multi/submitattestations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ import (
"strings"

consensusclient "github.com/attestantio/go-eth2-client"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/attestantio/go-eth2-client/api"
)

// SubmitAttestations submits attestations.
func (s *Service) SubmitAttestations(ctx context.Context,
attestations []*phase0.Attestation,
opts *api.SubmitAttestationsOpts,
) error {
_, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) {
err := client.(consensusclient.AttestationsSubmitter).SubmitAttestations(ctx, attestations)
err := client.(consensusclient.AttestationsSubmitter).SubmitAttestations(ctx, opts)
if err != nil {
return nil, err
}
Expand Down
10 changes: 9 additions & 1 deletion multi/submitattestations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ 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"
Expand Down Expand Up @@ -50,8 +52,14 @@ func TestSubmitAttestations(t *testing.T) {
)
require.NoError(t, err)

versionedAttestations := []*spec.VersionedAttestation{
{Version: spec.DataVersionPhase0, Phase0: &phase0.Attestation{}},
}
opts := &api.SubmitAttestationsOpts{
Attestations: versionedAttestations,
}
for i := 0; i < 128; i++ {
err := multiClient.(consensusclient.AttestationsSubmitter).SubmitAttestations(ctx, []*phase0.Attestation{})
err := multiClient.(consensusclient.AttestationsSubmitter).SubmitAttestations(ctx, opts)
require.NoError(t, err)
}
// At this point we expect mock 3 to be in active (unless probability hates us).
Expand Down
2 changes: 1 addition & 1 deletion service.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ type AttestationRewardsProvider interface {
// AttestationsSubmitter is the interface for submitting attestations.
type AttestationsSubmitter interface {
// SubmitAttestations submits attestations.
SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error
SubmitAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error
}

// AttesterSlashingSubmitter is the interface for submitting attester slashings.
Expand Down
57 changes: 54 additions & 3 deletions spec/electra/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ import (
)

// Attestation is the Ethereum 2 attestation structure.
//
//nolint:tagalign
type Attestation struct {
AggregationBits bitfield.Bitlist `ssz-max:"131072"`
AggregationBits bitfield.Bitlist `ssz-max:"131072" dynssz-size:"MAX_VALIDATORS_PER_COMMITTEE*MAX_COMMITTEES_PER_SLOT"`
Data *phase0.AttestationData
Signature phase0.BLSSignature `ssz-size:"96"`
CommitteeBits bitfield.Bitvector64 `dynssz-size:"MAX_COMMITTEES_PER_SLOT/8" ssz-size:"8"`
Signature phase0.BLSSignature `ssz-size:"96"`
// bitfield.Bitvector64 is an 8 byte array so dynamic sizing doesn't make sense.
CommitteeBits bitfield.Bitvector64 `ssz-size:"8"`
}

// attestationJSON is a raw representation of the struct.
Expand Down Expand Up @@ -140,3 +143,51 @@ func (a *Attestation) String() string {

return string(data)
}

// CommitteeIndex returns the index if only one bit is set, otherwise error.
func (a *Attestation) CommitteeIndex() (phase0.CommitteeIndex, error) {
bits := a.CommitteeBits
if len(bits.BitIndices()) == 0 {
return 0, errors.New("no committee index found in committee bits")
}
if len(bits.BitIndices()) > 1 {
return 0, errors.New("multiple committee indices found in committee bits")
}
foundIndex := phase0.CommitteeIndex(bits.BitIndices()[0])

return foundIndex, nil
}

// AggregateValidatorIndex returns the index if only one bit is set, otherwise error.
func (a *Attestation) AggregateValidatorIndex() (phase0.ValidatorIndex, error) {
bits := a.AggregationBits
if len(bits.BitIndices()) == 0 {
return 0, errors.New("no validator index found in aggregation bits")
}
if len(bits.BitIndices()) > 1 {
return 0, errors.New("multiple validator indices found in aggregation bits")
}
foundIndex := phase0.ValidatorIndex(bits.BitIndices()[0])

return foundIndex, nil
}

// ToSingleAttestation returns a SingleAttestation representation of the Attestation.
func (a *Attestation) ToSingleAttestation() (*SingleAttestation, error) {
committeeIndex, err := a.CommitteeIndex()
if err != nil {
return nil, err
}
validatorIndex, err := a.AggregateValidatorIndex()
if err != nil {
return nil, err
}
singleAttestation := SingleAttestation{
CommitteeIndex: committeeIndex,
AttesterIndex: validatorIndex,
Data: a.Data,
Signature: a.Signature,
}

return &singleAttestation, nil
}
7 changes: 3 additions & 4 deletions spec/electra/attestation_ssz.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b23a5e9

Please sign in to comment.