diff --git a/http/submitattestations.go b/http/submitattestations.go index 9950be6b..48e6f203 100644 --- a/http/submitattestations.go +++ b/http/submitattestations.go @@ -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 == nil { + 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 +} diff --git a/http/submitattestations_test.go b/http/submitattestations_test.go index dbed1787..07b557de 100644 --- a/http/submitattestations_test.go +++ b/http/submitattestations_test.go @@ -15,6 +15,7 @@ package http_test import ( "context" + "github.com/attestantio/go-eth2-client/spec" "os" "strings" "testing" @@ -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) diff --git a/http/submitversionedattestations.go b/http/submitversionedattestations.go deleted file mode 100644 index edc76e96..00000000 --- a/http/submitversionedattestations.go +++ /dev/null @@ -1,93 +0,0 @@ -// 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 http - -import ( - "bytes" - "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" -) - -// SubmitVersionedAttestations submits versioned attestations. -func (s *Service) SubmitVersionedAttestations(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 - var version *spec.DataVersion - var unversionedAttestations []any - - for i := range attestations { - if attestations[i] == nil { - return errors.Join(errors.New("nil attestation version supplied"), client.ErrInvalidOptions) - } - - // Ensure consistent versioning. - if version == nil { - version = &attestations[i].Version - } else if *version != attestations[i].Version { - return errors.Join(errors.New("attestations must all be of the same version"), client.ErrInvalidOptions) - } - - // Append to unversionedAttestations. - switch attestations[i].Version { - case spec.DataVersionElectra: - unversionedAttestations = append(unversionedAttestations, attestations[i].Electra) - default: - return errors.Join(errors.New("unknown attestation version"), client.ErrInvalidOptions) - } - } - - specJSON, err := json.Marshal(unversionedAttestations) - if err != nil { - return errors.Join(errors.New("failed to marshal JSON"), err) - } - - // TODO: It looks like this endpoint accepts both electra.Attestation and SingleAttestation containers. - // Reference: https://github.com/ethereum/beacon-APIs/blob/cee75f936fb1c1d8b1daf68f9be8c4d463f9fde9/apis/beacon/pool/attestations.v2.yaml#L55-L85. - // Should we consider introducing a SubmitSingleAttestations interface or even transform the versioned Attestation to SingleAttestation on the fly in this method? - // I'm not sure what benefits we get from submitting the SingleAttestation, but I'm wary if we have a SubmitVersionedAttestation and a SubmitSingleAttestation, then - // this could lead to a SubmitVersionedSingleAttestation etc and start to become quite a lot of overhead. - endpoint := "/eth/v2/beacon/pool/attestations" - query := "" - - headers := make(map[string]string) - headers["Eth-Consensus-Version"] = strings.ToLower(attestations[0].Version.String()) - if _, err = s.post(ctx, - endpoint, - query, - &opts.Common, - bytes.NewReader(specJSON), - ContentTypeJSON, - headers, - ); err != nil { - return errors.Join(errors.New("failed to submit versioned beacon attestations"), err) - } - - return nil -} diff --git a/mock/submitattestations.go b/mock/submitattestations.go index b1b0349e..952c8846 100644 --- a/mock/submitattestations.go +++ b/mock/submitattestations.go @@ -17,15 +17,9 @@ import ( "context" "github.com/attestantio/go-eth2-client/api" - spec "github.com/attestantio/go-eth2-client/spec/phase0" ) // SubmitAttestations submits attestations. -func (*Service) SubmitAttestations(_ context.Context, _ []*spec.Attestation) error { - return nil -} - -// SubmitVersionedAttestations submits versioned attestations. -func (*Service) SubmitVersionedAttestations(_ context.Context, _ *api.SubmitAttestationsOpts) error { +func (*Service) SubmitAttestations(_ context.Context, _ *api.SubmitAttestationsOpts) error { return nil } diff --git a/multi/submitattestations.go b/multi/submitattestations.go index faa0baa5..cbbce400 100644 --- a/multi/submitattestations.go +++ b/multi/submitattestations.go @@ -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 } diff --git a/multi/submitattestations_test.go b/multi/submitattestations_test.go index 8ee6e0c3..12934da2 100644 --- a/multi/submitattestations_test.go +++ b/multi/submitattestations_test.go @@ -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" @@ -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). diff --git a/multi/submitversionedattestations.go b/multi/submitversionedattestations.go deleted file mode 100644 index 58575ea5..00000000 --- a/multi/submitversionedattestations.go +++ /dev/null @@ -1,62 +0,0 @@ -// 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 multi - -import ( - "context" - "strings" - - consensusclient "github.com/attestantio/go-eth2-client" - "github.com/attestantio/go-eth2-client/api" -) - -// SubmitVersionedAttestations submits versioned attestations. -func (s *Service) SubmitVersionedAttestations(ctx context.Context, - opts *api.SubmitAttestationsOpts, -) error { - _, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { - err := client.(consensusclient.VersionedAttestationsSubmitter).SubmitVersionedAttestations(ctx, opts) - if err != nil { - return nil, err - } - - return true, nil - }, func(ctx context.Context, client consensusclient.Service, err error) (bool, error) { - // We have received an error, decide if it requires us to fail over or not. - provider := s.providerInfo(ctx, client) - switch { - case provider == "lighthouse" && strings.Contains(err.Error(), "PriorAttestationKnown"): - // Lighthouse rejects duplicate attestations. It is possible that an attestation sent - // to another node already propagated to this node, or the caller is attempting to resend - // an existing attestation, but either way it is not a failover-worthy error. - log := s.log.With().Logger() - log.Trace().Msg("Lighthouse rejected submission as it already knew about it") - - return false /* failover */, err - case provider == "lighthouse" && strings.Contains(err.Error(), "UnknownHeadBlock"): - // Lighthouse rejects an attestation for a block that is not its current head. We assume that - // the request is valid and it is the node that it is somehow out of sync, so failover. - log := s.log.With().Logger() - log.Trace().Err(err).Msg("Lighthouse rejected submission as it did not know about the relevant head block") - - return true /* failover */, err - default: - // Any other error should result in a failover. - - return true /* failover */, err - } - }) - - return err -} diff --git a/multi/submitversionedattestations_test.go b/multi/submitversionedattestations_test.go deleted file mode 100644 index 9eadd584..00000000 --- a/multi/submitversionedattestations_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 multi_test - -import ( - "context" - "github.com/attestantio/go-eth2-client/api" - "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/testclients" - "github.com/rs/zerolog" - "github.com/stretchr/testify/require" -) - -func TestSubmitVersionedAttestations(t *testing.T) { - ctx := context.Background() - - client1, err := mock.New(ctx, mock.WithName("mock 1")) - require.NoError(t, err) - erroringClient1, err := testclients.NewErroring(ctx, 0.1, client1) - require.NoError(t, err) - client2, err := mock.New(ctx, mock.WithName("mock 2")) - require.NoError(t, err) - erroringClient2, err := testclients.NewErroring(ctx, 0.1, client2) - require.NoError(t, err) - client3, err := mock.New(ctx, mock.WithName("mock 3")) - require.NoError(t, err) - - multiClient, err := multi.New(ctx, - multi.WithLogLevel(zerolog.Disabled), - multi.WithClients([]consensusclient.Service{ - erroringClient1, - erroringClient2, - client3, - }), - ) - require.NoError(t, err) - - for i := 0; i < 128; i++ { - err := multiClient.(consensusclient.VersionedAttestationsSubmitter).SubmitVersionedAttestations(ctx, &api.SubmitAttestationsOpts{}) - require.NoError(t, err) - } - // At this point we expect mock 3 to be in active (unless probability hates us). - require.Equal(t, "mock 3", multiClient.Address()) -} diff --git a/service.go b/service.go index a71f222c..689c7edf 100644 --- a/service.go +++ b/service.go @@ -197,13 +197,7 @@ type AttestationPoolProvider interface { // AttestationsSubmitter is the interface for submitting attestations. type AttestationsSubmitter interface { // SubmitAttestations submits attestations. - SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error -} - -// VersionedAttestationsSubmitter is the interface for submitting versioned attestations. -type VersionedAttestationsSubmitter interface { - // SubmitVersionedAttestations submits attestations. - SubmitVersionedAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error + SubmitAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error } // AttesterSlashingSubmitter is the interface for submitting attester slashings. diff --git a/spec/electra/attestation.go b/spec/electra/attestation.go index 8403dd1a..72896604 100644 --- a/spec/electra/attestation.go +++ b/spec/electra/attestation.go @@ -29,13 +29,12 @@ import ( // Attestation is the Ethereum 2 attestation structure. 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"` Data *phase0.AttestationData Signature phase0.BLSSignature `ssz-size:"96"` - // TODO: Check dynssz-size is correct as the spec states this to be of size MAX_COMMITTEES_PER_SLOT. - // I suspect this could be a bit to byte conversion, but wanted to make sure. - // https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#attestation - CommitteeBits bitfield.Bitvector64 `dynssz-size:"MAX_COMMITTEES_PER_SLOT/8" ssz-size:"8"` + // 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. @@ -143,3 +142,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 +} diff --git a/spec/electra/attestation_ssz.go b/spec/electra/attestation_ssz.go index 899f6eb9..6046b63d 100644 --- a/spec/electra/attestation_ssz.go +++ b/spec/electra/attestation_ssz.go @@ -1,5 +1,5 @@ // Code generated by fastssz. DO NOT EDIT. -// Hash: 3a9a3226e05c9a4f4fe19244999dacfbc39962ae1ff5d0ae99f7634afef8ea5c +// Hash: a08e95de8ee579e2cf340dad317beb7bd17c9384d835e81d0cfc62344fdbb378 // Version: 0.1.3 package electra @@ -20,7 +20,6 @@ func (a *Attestation) MarshalSSZTo(buf []byte) (dst []byte, err error) { // Offset (0) 'AggregationBits' dst = ssz.WriteOffset(dst, offset) - offset += len(a.AggregationBits) // Field (1) 'Data' if a.Data == nil { @@ -41,8 +40,8 @@ func (a *Attestation) MarshalSSZTo(buf []byte) (dst []byte, err error) { dst = append(dst, a.CommitteeBits...) // Field (0) 'AggregationBits' - if size := len(a.AggregationBits); size > 131072 { - err = ssz.ErrBytesLengthFn("Attestation.AggregationBits", size, 131072) + if size := len(a.AggregationBits); size > 16385 { + err = ssz.ErrBytesLengthFn("Attestation.AggregationBits", size, 16385) return } dst = append(dst, a.AggregationBits...) @@ -66,7 +65,7 @@ func (a *Attestation) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - if o0 < 236 { + if o0 != 236 { return ssz.ErrInvalidVariableOffset } @@ -90,7 +89,7 @@ func (a *Attestation) UnmarshalSSZ(buf []byte) error { // Field (0) 'AggregationBits' { buf = tail[o0:] - if err = ssz.ValidateBitlist(buf, 131072); err != nil { + if err = ssz.ValidateBitlist(buf, 16385); err != nil { return err } if cap(a.AggregationBits) == 0 { @@ -125,7 +124,7 @@ func (a *Attestation) HashTreeRootWith(hh ssz.HashWalker) (err error) { err = ssz.ErrEmptyBitlist return } - hh.PutBitlist(a.AggregationBits, 131072) + hh.PutBitlist(a.AggregationBits, 16385) // Field (1) 'Data' if a.Data == nil { diff --git a/spec/electra/attestation_test.go b/spec/electra/attestation_test.go new file mode 100644 index 00000000..60a8de6f --- /dev/null +++ b/spec/electra/attestation_test.go @@ -0,0 +1,178 @@ +// 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 electra_test + +import ( + "github.com/stretchr/testify/require" + "testing" + + "github.com/attestantio/go-eth2-client/spec/electra" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" +) + +func TestAttestation_CommitteeIndex(t *testing.T) { + // Test cases + tests := []struct { + name string + expectedIndices []phase0.CommitteeIndex + errorMsg string + doNotSet bool + }{ + { + name: "Valid index 0", + expectedIndices: []phase0.CommitteeIndex{0}, + }, + { + name: "Valid index 4", + expectedIndices: []phase0.CommitteeIndex{4}, + }, + { + name: "Valid index 40", + expectedIndices: []phase0.CommitteeIndex{40}, + }, + { + name: "Invalid index 64", + expectedIndices: []phase0.CommitteeIndex{64}, + errorMsg: "no committee index found in committee bits", + }, + { + name: "Invalid no index set", + expectedIndices: []phase0.CommitteeIndex{4}, + errorMsg: "no committee index found in committee bits", + doNotSet: true, + }, + { + name: "Invalid multiple index set", + expectedIndices: []phase0.CommitteeIndex{4, 40}, + errorMsg: "multiple committee indices found in committee bits", + }, + } + // Run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + committeeBits := bitfield.NewBitvector64() + if !tt.doNotSet { + for _, expectedIndex := range tt.expectedIndices { + committeeBits.SetBitAt(uint64(expectedIndex), true) + } + } + attestation := &electra.Attestation{CommitteeBits: committeeBits} + + committeeIndex, err := attestation.CommitteeIndex() + if tt.errorMsg == "" { + require.NoError(t, err) + for _, expectedIndex := range tt.expectedIndices { + assert.Equal(t, expectedIndex, committeeIndex) + } + return + } + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + }) + } +} + +func TestAttestation_AggregateValidatorIndex(t *testing.T) { + // Test cases + tests := []struct { + name string + expectedIndices []phase0.ValidatorIndex + errorMsg string + doNotSet bool + }{ + { + name: "Valid index 0", + expectedIndices: []phase0.ValidatorIndex{0}, + }, + { + name: "Valid index 4", + expectedIndices: []phase0.ValidatorIndex{4}, + }, + { + name: "Valid index 140", + expectedIndices: []phase0.ValidatorIndex{140}, + }, + { + name: "Invalid index 160", + expectedIndices: []phase0.ValidatorIndex{160}, + errorMsg: "no validator index found in aggregation bits", + }, + { + name: "Invalid no index set", + expectedIndices: []phase0.ValidatorIndex{64}, + errorMsg: "no validator index found in aggregation bits", + doNotSet: true, + }, + { + name: "Invalid multiple index set", + expectedIndices: []phase0.ValidatorIndex{4, 40}, + errorMsg: "multiple validator indices found in aggregation bits", + }, + } + // Run tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aggregateBits := bitfield.NewBitlist(160) + if !tt.doNotSet { + for _, expectedIndex := range tt.expectedIndices { + aggregateBits.SetBitAt(uint64(expectedIndex), true) + } + } + attestation := &electra.Attestation{AggregationBits: aggregateBits} + + aggregateValidatorIndex, err := attestation.AggregateValidatorIndex() + if tt.errorMsg == "" { + require.NoError(t, err) + for _, expectedIndex := range tt.expectedIndices { + assert.Equal(t, expectedIndex, aggregateValidatorIndex) + } + return + } + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + }) + } +} + +func TestAttestation_SSZ(t *testing.T) { + aggregateSize := uint64(131072) + aggregateBits := bitfield.NewBitlist(aggregateSize) + committeeSize := uint64(64) + committeeBits := bitfield.NewBitvector64() + attestation := electra.Attestation{ + AggregationBits: aggregateBits, + CommitteeBits: committeeBits, + } + // Set a bit beyond the bit list. + aggregateBits.SetBitAt(aggregateSize, true) + // Set a bit in the last bit of the list. + aggregateBits.SetBitAt(aggregateSize-1, true) + + // Should only have the bit that was set on the last bit. + require.Equal(t, 1, len(aggregateBits.BitIndices())) + + // Set a bit beyond the bit vector. + committeeBits.SetBitAt(committeeSize, true) + // Set a bit in the last bit of the vector. + committeeBits.SetBitAt(committeeSize-1, true) + + // Should only have the bit that was set on the last bit. + require.Equal(t, 1, len(committeeBits.BitIndices())) + + // Ensure we can actually serialise. + _, err := attestation.MarshalSSZ() + require.NoError(t, err) +} diff --git a/spec/electra/singleattestation_ssz.go b/spec/electra/singleattestation_ssz.go index 97af586d..03cf2f81 100644 --- a/spec/electra/singleattestation_ssz.go +++ b/spec/electra/singleattestation_ssz.go @@ -1,5 +1,5 @@ // Code generated by fastssz. DO NOT EDIT. -// Hash: fb166eff3bbca34883327c3b21fe6565e8ee24248c1a3e60a67f63f7c380a6a6 +// Hash: a08e95de8ee579e2cf340dad317beb7bd17c9384d835e81d0cfc62344fdbb378 // Version: 0.1.3 package electra diff --git a/spec/versionedattestation.go b/spec/versionedattestation.go index 6d0950fd..5b2aa9ef 100644 --- a/spec/versionedattestation.go +++ b/spec/versionedattestation.go @@ -144,20 +144,46 @@ func (v *VersionedAttestation) CommitteeBits() (bitfield.Bitvector64, error) { // CommitteeIndex returns the index if only one bit is set, otherwise error. func (v *VersionedAttestation) CommitteeIndex() (phase0.CommitteeIndex, error) { - bits, err := v.CommitteeBits() - if err != nil { - return 0, err - } + switch v.Version { + case DataVersionPhase0: + if v.Phase0 == nil { + return 0, errors.New("no Phase0 attestation") + } - 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 v.Phase0.Data.Index, nil + case DataVersionAltair: + if v.Altair == nil { + return 0, errors.New("no Altair attestation") + } + + return v.Altair.Data.Index, nil + case DataVersionBellatrix: + if v.Bellatrix == nil { + return 0, errors.New("no Bellatrix attestation") + } - return foundIndex, nil + return v.Bellatrix.Data.Index, nil + case DataVersionCapella: + if v.Capella == nil { + return 0, errors.New("no Capella attestation") + } + + return v.Capella.Data.Index, nil + case DataVersionDeneb: + if v.Deneb == nil { + return 0, errors.New("no Deneb attestation") + } + + return v.Deneb.Data.Index, nil + case DataVersionElectra: + if v.Electra == nil { + return 0, errors.New("no Electra attestation") + } + + return v.Electra.CommitteeIndex() + default: + return 0, errors.New("unknown version") + } } // Signature returns the signature of the attestation. diff --git a/testclients/erroring.go b/testclients/erroring.go index 7a8a826c..95069e66 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -265,7 +265,7 @@ func (s *Erroring) AttestationPool(ctx context.Context, } // SubmitAttestations submits attestations. -func (s *Erroring) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error { +func (s *Erroring) SubmitAttestations(ctx context.Context, attestations *api.SubmitAttestationsOpts) error { if err := s.maybeError(ctx); err != nil { return err } @@ -277,19 +277,6 @@ func (s *Erroring) SubmitAttestations(ctx context.Context, attestations []*phase return next.SubmitAttestations(ctx, attestations) } -// SubmitVersionedAttestations submits versioned attestations. -func (s *Erroring) SubmitVersionedAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error { - if err := s.maybeError(ctx); err != nil { - return err - } - next, isNext := s.next.(consensusclient.VersionedAttestationsSubmitter) - if !isNext { - return fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) - } - - return next.SubmitVersionedAttestations(ctx, opts) -} - // SubmitProposalPreparations submits proposal preparations. func (s *Erroring) SubmitProposalPreparations(ctx context.Context, preparations []*apiv1.ProposalPreparation) error { if err := s.maybeError(ctx); err != nil { diff --git a/testclients/sleepy.go b/testclients/sleepy.go index d8f7291d..8be96773 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -241,7 +241,7 @@ func (s *Sleepy) AttestationPool(ctx context.Context, } // SubmitAttestations submits attestations. -func (s *Sleepy) SubmitAttestations(ctx context.Context, attestations []*phase0.Attestation) error { +func (s *Sleepy) SubmitAttestations(ctx context.Context, attestations *api.SubmitAttestationsOpts) error { s.sleep(ctx) next, isNext := s.next.(consensusclient.AttestationsSubmitter) if !isNext { @@ -251,17 +251,6 @@ func (s *Sleepy) SubmitAttestations(ctx context.Context, attestations []*phase0. return next.SubmitAttestations(ctx, attestations) } -// SubmitVersionedAttestations submits versioned attestations. -func (s *Sleepy) SubmitVersionedAttestations(ctx context.Context, opts *api.SubmitAttestationsOpts) error { - s.sleep(ctx) - next, isNext := s.next.(consensusclient.VersionedAttestationsSubmitter) - if !isNext { - return errors.New("next does not support this call") - } - - return next.SubmitVersionedAttestations(ctx, opts) -} - // AttesterDuties obtains attester duties. // If validatorIndicess is nil it will return all duties for the given epoch. func (s *Sleepy) AttesterDuties(ctx context.Context,