Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add versioned attestations submission #178

Merged
merged 6 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
58 changes: 55 additions & 3 deletions spec/electra/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ import (
)

// Attestation is the Ethereum 2 attestation structure.
//
//nolint:tagalign
type Attestation struct {
AggregationBits bitfield.Bitlist `ssz-max:"131072"`
// bitfield.Bitlist has size of n bits + 1 length bit, e.g. an 8 bit list will require a 2 byte array.
AggregationBits bitfield.Bitlist `ssz-max:"16385" dynssz-size:"((MAX_VALIDATORS_PER_COMMITTEE*MAX_COMMITTEES_PER_SLOT)/8 + 1)"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep this as 131072. Although from the sounds of it, it doesn't do what it should, we should raise the issue with the fastssz repository and have the correct values in place here for when the bug is fixed.

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"`
mcdee marked this conversation as resolved.
Show resolved Hide resolved
}

// attestationJSON is a raw representation of the struct.
Expand Down Expand Up @@ -140,3 +144,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
}
13 changes: 6 additions & 7 deletions spec/electra/attestation_ssz.go

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

Loading
Loading