From 8910181b413ea0d482141be8d5440cd01c37349f Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Sat, 4 Jan 2025 00:26:21 +0000 Subject: [PATCH 1/4] Add rewards endpoints. --- api/attestationrewardsopts.go | 30 ++++ api/blockrewardsopts.go | 22 +++ api/synccommitteerewardsopts.go | 30 ++++ api/v1/attestationrewards.go | 255 +++++++++++++++++++++++++++++ api/v1/blockrewards.go | 131 +++++++++++++++ api/v1/synccommitteereward.go | 83 ++++++++++ http/attestationrewards.go | 79 +++++++++ http/attestationrewards_test.go | 95 +++++++++++ http/blockrewards.go | 65 ++++++++ http/blockrewards_test.go | 77 +++++++++ http/signedbeaconblock.go | 3 + http/synccommitteerewards.go | 82 ++++++++++ http/synccommitteerewards_test.go | 94 +++++++++++ http/validators.go | 3 + mock/attestationrewards.go | 37 +++++ mock/blockrewards.go | 34 ++++ mock/synccommitteerewards.go | 34 ++++ multi/attestationrewards.go | 49 ++++++ multi/attestationrewards_test.go | 60 +++++++ multi/blockrewards.go | 49 ++++++ multi/blockrewards_test.go | 60 +++++++ multi/service_test.go | 3 + multi/synccommitteerewards.go | 49 ++++++ multi/synccommitteerewards_test.go | 60 +++++++ service.go | 33 ++++ testclients/erroring.go | 56 ++++++- testclients/sleepy.go | 48 ++++++ 27 files changed, 1620 insertions(+), 1 deletion(-) create mode 100644 api/attestationrewardsopts.go create mode 100644 api/blockrewardsopts.go create mode 100644 api/synccommitteerewardsopts.go create mode 100644 api/v1/attestationrewards.go create mode 100644 api/v1/blockrewards.go create mode 100644 api/v1/synccommitteereward.go create mode 100644 http/attestationrewards.go create mode 100644 http/attestationrewards_test.go create mode 100644 http/blockrewards.go create mode 100644 http/blockrewards_test.go create mode 100644 http/synccommitteerewards.go create mode 100644 http/synccommitteerewards_test.go create mode 100644 mock/attestationrewards.go create mode 100644 mock/blockrewards.go create mode 100644 mock/synccommitteerewards.go create mode 100644 multi/attestationrewards.go create mode 100644 multi/attestationrewards_test.go create mode 100644 multi/blockrewards.go create mode 100644 multi/blockrewards_test.go create mode 100644 multi/synccommitteerewards.go create mode 100644 multi/synccommitteerewards_test.go diff --git a/api/attestationrewardsopts.go b/api/attestationrewardsopts.go new file mode 100644 index 00000000..23365a15 --- /dev/null +++ b/api/attestationrewardsopts.go @@ -0,0 +1,30 @@ +// 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/phase0" + +// AttestationRewardsOpts are the options for obtaining attestation rewards. +type AttestationRewardsOpts struct { + Common CommonOpts + + // Epoch is the epoch for which the data is obtained. + Epoch phase0.Epoch + // Indices is a list of validator indices to restrict the returned values. + // If no indices are supplied then no filter will be applied. + Indices []phase0.ValidatorIndex + // PubKeys is a list of validator public keys to restrict the returned values. + // If no public keys are supplied then no filter will be applied. + PubKeys []phase0.BLSPubKey +} diff --git a/api/blockrewardsopts.go b/api/blockrewardsopts.go new file mode 100644 index 00000000..63902d9e --- /dev/null +++ b/api/blockrewardsopts.go @@ -0,0 +1,22 @@ +// 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 + +// BlockRewardsOpts are the options for proposing a block. +type BlockRewardsOpts struct { + Common CommonOpts + + // Block is the ID of the block which the data is obtained. + Block string +} diff --git a/api/synccommitteerewardsopts.go b/api/synccommitteerewardsopts.go new file mode 100644 index 00000000..3d116e2a --- /dev/null +++ b/api/synccommitteerewardsopts.go @@ -0,0 +1,30 @@ +// 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/phase0" + +// SyncCommitteeRewardsOpts are the options for obtaining sync committee rewards. +type SyncCommitteeRewardsOpts struct { + Common CommonOpts + + // Block is the ID of the block which the data is obtained. + Block string + // Indices is a list of validator indices to restrict the returned values. + // If no indices are supplied then no filter will be applied. + Indices []phase0.ValidatorIndex + // PubKeys is a list of validator public keys to restrict the returned values. + // If no public keys are supplied then no filter will be applied. + PubKeys []phase0.BLSPubKey +} diff --git a/api/v1/attestationrewards.go b/api/v1/attestationrewards.go new file mode 100644 index 00000000..6c6ede00 --- /dev/null +++ b/api/v1/attestationrewards.go @@ -0,0 +1,255 @@ +// 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 v1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// AttestationRewards are the rewards for a number of attesting validators. +type AttestationRewards struct { + IdealRewards []IdealAttestationRewards `json:"ideal_rewards"` + TotalRewards []ValidatorAttestationRewards `json:"total_rewards"` +} + +// IdealAttestationRewards are the ideal attestation rewards for an attestation. +type IdealAttestationRewards struct { + EffectiveBalance phase0.Gwei + Head phase0.Gwei + Target phase0.Gwei + Source phase0.Gwei + InclusionDelay *phase0.Gwei + Inactivity phase0.Gwei +} + +// idealAttestationRewardsJSON is the spec representation of the struct. +type idealAttestationRewardsJSON struct { + EffectiveBalance string `json:"effective_balance"` + Head string `json:"head"` + Target string `json:"target"` + Source string `json:"source"` + InclusionDelay string `json:"inclusion_delay,omitempty"` + Inactivity string `json:"inactivity"` +} + +// MarshalJSON implements json.Marshaler. +func (i *IdealAttestationRewards) MarshalJSON() ([]byte, error) { + inclusionDelay := "" + if i.InclusionDelay != nil { + inclusionDelay = fmt.Sprintf("%d", *i.InclusionDelay) + } + + return json.Marshal(&idealAttestationRewardsJSON{ + EffectiveBalance: fmt.Sprintf("%d", i.EffectiveBalance), + Head: fmt.Sprintf("%d", i.Head), + Target: fmt.Sprintf("%d", i.Target), + Source: fmt.Sprintf("%d", i.Source), + InclusionDelay: inclusionDelay, + Inactivity: fmt.Sprintf("%d", i.Inactivity), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (i *IdealAttestationRewards) UnmarshalJSON(input []byte) error { + var err error + + var data idealAttestationRewardsJSON + if err = json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.EffectiveBalance == "" { + return errors.New("effective balance missing") + } + effectiveBalance, err := strconv.ParseUint(data.EffectiveBalance, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for effective balance") + } + i.EffectiveBalance = phase0.Gwei(effectiveBalance) + + if data.Head == "" { + return errors.New("head missing") + } + head, err := strconv.ParseUint(data.Head, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for head") + } + i.Head = phase0.Gwei(head) + + if data.Target == "" { + return errors.New("target missing") + } + target, err := strconv.ParseUint(data.Target, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for target") + } + i.Target = phase0.Gwei(target) + + if data.Source == "" { + return errors.New("source missing") + } + source, err := strconv.ParseInt(data.Source, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for source") + } + i.Source = phase0.Gwei(source) + + if data.InclusionDelay != "" { + inclusionDelay, err := strconv.ParseUint(data.InclusionDelay, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for inclusion delay") + } + tmp := phase0.Gwei(inclusionDelay) + i.InclusionDelay = &tmp + } + + if data.Inactivity == "" { + return errors.New("inactivity missing") + } + inactivity, err := strconv.ParseUint(data.Inactivity, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for inactivity") + } + i.Inactivity = phase0.Gwei(inactivity) + + return nil +} + +// String returns a string version of the structure. +func (i *IdealAttestationRewards) String() string { + data, err := json.Marshal(i) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} + +// ValidatorAttestationRewards are the ideal attestation rewards for a validator. +type ValidatorAttestationRewards struct { + ValidatorIndex phase0.ValidatorIndex + Head phase0.Gwei + // Target can be negative, so it is an int64 (but still a Gwei value). + Target int64 + // Source can be negative, so it is an int64 (but still a Gwei value). + Source int64 + InclusionDelay *phase0.Gwei + Inactivity phase0.Gwei +} + +// validatorAttestationRewardsJSON is the spec representation of the struct. +type validatorAttestationRewardsJSON struct { + ValidatorIndex string `json:"validator_index"` + Head string `json:"head"` + Target string `json:"target"` + Source string `json:"source"` + InclusionDelay string `json:"inclusion_delay,omitempty"` + Inactivity string `json:"inactivity"` +} + +// MarshalJSON implements json.Marshaler. +func (v *ValidatorAttestationRewards) MarshalJSON() ([]byte, error) { + inclusionDelay := "" + if v.InclusionDelay != nil { + inclusionDelay = fmt.Sprintf("%d", *v.InclusionDelay) + } + + return json.Marshal(&validatorAttestationRewardsJSON{ + ValidatorIndex: fmt.Sprintf("%d", v.ValidatorIndex), + Head: fmt.Sprintf("%d", v.Head), + Target: fmt.Sprintf("%d", v.Target), + Source: fmt.Sprintf("%d", v.Source), + InclusionDelay: inclusionDelay, + Inactivity: fmt.Sprintf("%d", v.Inactivity), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (v *ValidatorAttestationRewards) UnmarshalJSON(input []byte) error { + var err error + + var data validatorAttestationRewardsJSON + if err = json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.ValidatorIndex == "" { + return errors.New("validator index missing") + } + validatorIndex, err := strconv.ParseUint(data.ValidatorIndex, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for validator index") + } + v.ValidatorIndex = phase0.ValidatorIndex(validatorIndex) + + if data.Head == "" { + return errors.New("head missing") + } + head, err := strconv.ParseUint(data.Head, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for head") + } + v.Head = phase0.Gwei(head) + + if data.Target == "" { + return errors.New("target missing") + } + v.Target, err = strconv.ParseInt(data.Target, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for target") + } + + if data.Source == "" { + return errors.New("source missing") + } + v.Source, err = strconv.ParseInt(data.Source, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for source") + } + + if data.InclusionDelay != "" { + inclusionDelay, err := strconv.ParseUint(data.InclusionDelay, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for inclusion delay") + } + tmp := phase0.Gwei(inclusionDelay) + v.InclusionDelay = &tmp + } + + if data.Inactivity == "" { + return errors.New("inactivity missing") + } + inactivity, err := strconv.ParseUint(data.Inactivity, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for inactivity") + } + v.Inactivity = phase0.Gwei(inactivity) + + return nil +} + +// String returns a string version of the structure. +func (v *ValidatorAttestationRewards) String() string { + data, err := json.Marshal(v) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/api/v1/blockrewards.go b/api/v1/blockrewards.go new file mode 100644 index 00000000..8c36fe02 --- /dev/null +++ b/api/v1/blockrewards.go @@ -0,0 +1,131 @@ +// 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 v1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// BlockRewards are the rewards for proposing a block. +type BlockRewards struct { + ProposerIndex phase0.ValidatorIndex + Total phase0.Gwei + Attestations phase0.Gwei + SyncAggregate phase0.Gwei + ProposerSlashings phase0.Gwei + AttesterSlashings phase0.Gwei +} + +// blockRewardsJSON is the spec representation of the struct. +type blockRewardsJSON struct { + ProposerIndex string `json:"proposer_index"` + Total string `json:"total"` + Attestations string `json:"attestations"` + SyncAggregate string `json:"sync_aggregate"` + ProposerSlashings string `json:"proposer_slashings"` + AttesterSlashings string `json:"attester_slashings"` +} + +// MarshalJSON implements json.Marshaler. +func (b *BlockRewards) MarshalJSON() ([]byte, error) { + return json.Marshal(&blockRewardsJSON{ + ProposerIndex: fmt.Sprintf("%d", b.ProposerIndex), + Total: fmt.Sprintf("%d", b.Total), + Attestations: fmt.Sprintf("%d", b.Attestations), + SyncAggregate: fmt.Sprintf("%d", b.SyncAggregate), + ProposerSlashings: fmt.Sprintf("%d", b.ProposerSlashings), + AttesterSlashings: fmt.Sprintf("%d", b.AttesterSlashings), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *BlockRewards) UnmarshalJSON(input []byte) error { + var err error + + var data blockRewardsJSON + if err = json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.ProposerIndex == "" { + return errors.New("proposer index missing") + } + proposerIndex, err := strconv.ParseUint(data.ProposerIndex, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for proposer index") + } + b.ProposerIndex = phase0.ValidatorIndex(proposerIndex) + + if data.Total == "" { + return errors.New("total missing") + } + total, err := strconv.ParseUint(data.Total, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for total") + } + b.Total = phase0.Gwei(total) + + if data.Attestations == "" { + return errors.New("attestations missing") + } + attestations, err := strconv.ParseUint(data.Attestations, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for attestations") + } + b.Attestations = phase0.Gwei(attestations) + + if data.SyncAggregate == "" { + return errors.New("sync aggregate missing") + } + syncAggregate, err := strconv.ParseUint(data.SyncAggregate, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for sync aggregate") + } + b.SyncAggregate = phase0.Gwei(syncAggregate) + + if data.ProposerSlashings == "" { + return errors.New("proposer slashings missing") + } + proposerSlashings, err := strconv.ParseUint(data.ProposerSlashings, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for proposer slashings") + } + b.ProposerSlashings = phase0.Gwei(proposerSlashings) + + if data.AttesterSlashings == "" { + return errors.New("attester slashings missing") + } + attesterSlashings, err := strconv.ParseUint(data.AttesterSlashings, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for attester slashings") + } + b.AttesterSlashings = phase0.Gwei(attesterSlashings) + + return nil +} + +// String returns a string version of the structure. +func (b *BlockRewards) String() string { + data, err := json.Marshal(b) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/api/v1/synccommitteereward.go b/api/v1/synccommitteereward.go new file mode 100644 index 00000000..4b763476 --- /dev/null +++ b/api/v1/synccommitteereward.go @@ -0,0 +1,83 @@ +// 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 v1 + +import ( + "encoding/json" + "fmt" + "strconv" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// SyncCommitteeReward is the rewards for a validator in a sync committee. +type SyncCommitteeReward struct { + ValidatorIndex phase0.ValidatorIndex + // Reward can be negative, so it is an int64 (but still a Gwei value). + Reward int64 +} + +// syncCommitteeRewardJSON is the spec representation of the struct. +type syncCommitteeRewardJSON struct { + ValidatorIndex string `json:"validator_index"` + Reward string `json:"reward"` +} + +// MarshalJSON implements json.Marshaler. +func (s *SyncCommitteeReward) MarshalJSON() ([]byte, error) { + return json.Marshal(&syncCommitteeRewardJSON{ + ValidatorIndex: fmt.Sprintf("%d", s.ValidatorIndex), + Reward: fmt.Sprintf("%d", s.Reward), + }) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (s *SyncCommitteeReward) UnmarshalJSON(input []byte) error { + var err error + + var data syncCommitteeRewardJSON + if err = json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.ValidatorIndex == "" { + return errors.New("validator index missing") + } + validatorIndex, err := strconv.ParseUint(data.ValidatorIndex, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for validator index") + } + s.ValidatorIndex = phase0.ValidatorIndex(validatorIndex) + + if data.Reward == "" { + return errors.New("reward missing") + } + s.Reward, err = strconv.ParseInt(data.Reward, 10, 64) + if err != nil { + return errors.Wrap(err, "invalid value for reward") + } + + return nil +} + +// String returns a string version of the structure. +func (s *SyncCommitteeReward) String() string { + data, err := json.Marshal(s) + if err != nil { + return fmt.Sprintf("ERR: %v", err) + } + + return string(data) +} diff --git a/http/attestationrewards.go b/http/attestationrewards.go new file mode 100644 index 00000000..b67169a7 --- /dev/null +++ b/http/attestationrewards.go @@ -0,0 +1,79 @@ +// 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" + "fmt" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +// AttestationRewards provides rewards to the given validators for attesting. +func (s *Service) AttestationRewards(ctx context.Context, + opts *api.AttestationRewardsOpts, +) ( + *api.Response[*apiv1.AttestationRewards], + error, +) { + ctx, span := otel.Tracer("attestantio.go-eth2-client.http").Start(ctx, "AttestationRewards") + defer span.End() + + if err := s.assertIsActive(ctx); err != nil { + return nil, err + } + if opts == nil { + return nil, client.ErrNoOptions + } + span.SetAttributes(attribute.Int("validators", len(opts.Indices)+len(opts.PubKeys))) + + endpoint := fmt.Sprintf("/eth/v1/beacon/rewards/attestations/%d", opts.Epoch) + query := "" + + body := make([]string, 0, len(opts.Indices)+len(opts.PubKeys)) + + for i := range opts.Indices { + body = append(body, fmt.Sprintf("%d", opts.Indices[i])) + } + for i := range opts.PubKeys { + body = append(body, opts.PubKeys[i].String()) + } + + reqData, err := json.Marshal(body) + if err != nil { + return nil, errors.Join(errors.New("failed to marshal request data"), err) + } + + httpResponse, err := s.post(ctx, endpoint, query, &opts.Common, bytes.NewReader(reqData), ContentTypeJSON, map[string]string{}) + if err != nil { + return nil, errors.Join(errors.New("failed to request attestation rewards"), err) + } + + data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), apiv1.AttestationRewards{}) + if err != nil { + return nil, err + } + + return &api.Response[*apiv1.AttestationRewards]{ + Data: &data, + Metadata: metadata, + }, nil +} diff --git a/http/attestationrewards_test.go b/http/attestationrewards_test.go new file mode 100644 index 00000000..51e3d74f --- /dev/null +++ b/http/attestationrewards_test.go @@ -0,0 +1,95 @@ +// 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_test + +import ( + "context" + "encoding/json" + "os" + "strconv" + "testing" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" +) + +func TestAttestationRewards(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tests := []struct { + name string + opts *api.AttestationRewardsOpts + expectedErrorCode int + expectedResponse string + }{ + { + name: "EpochFarFuture", + opts: &api.AttestationRewardsOpts{Epoch: 0xffffffffffffffff}, + expectedErrorCode: 404, + }, + { + name: "MixedIndicesAndPubKeys", + opts: &api.AttestationRewardsOpts{ + Epoch: 335909, + Indices: []phase0.ValidatorIndex{ + 0, 1, + }, + PubKeys: []phase0.BLSPubKey{ + *mustParsePubKey("0xb2ff4716ed345b05dd1dfc6a5a9fa70856d8c75dcc9e881dd2f766d5f891326f0d10e96f3a444ce6c912b69c22c6754d"), + *mustParsePubKey("0x8e323fd501233cd4d1b9d63d74076a38de50f2f584b001a5ac2412e4e46adb26d2fb2a6041e7e8c57cd4df0916729219"), + }, + }, + expectedResponse: `{"ideal_rewards":[{"effective_balance":"1000000000","head":"75","target":"140","source":"75","inactivity":"0"},{"effective_balance":"2000000000","head":"151","target":"281","source":"151","inactivity":"0"},{"effective_balance":"3000000000","head":"226","target":"422","source":"227","inactivity":"0"},{"effective_balance":"4000000000","head":"302","target":"562","source":"302","inactivity":"0"},{"effective_balance":"5000000000","head":"377","target":"703","source":"378","inactivity":"0"},{"effective_balance":"6000000000","head":"453","target":"844","source":"454","inactivity":"0"},{"effective_balance":"7000000000","head":"528","target":"985","source":"530","inactivity":"0"},{"effective_balance":"8000000000","head":"604","target":"1125","source":"605","inactivity":"0"},{"effective_balance":"9000000000","head":"679","target":"1266","source":"681","inactivity":"0"},{"effective_balance":"10000000000","head":"755","target":"1407","source":"757","inactivity":"0"},{"effective_balance":"11000000000","head":"830","target":"1547","source":"833","inactivity":"0"},{"effective_balance":"12000000000","head":"906","target":"1688","source":"908","inactivity":"0"},{"effective_balance":"13000000000","head":"981","target":"1829","source":"984","inactivity":"0"},{"effective_balance":"14000000000","head":"1057","target":"1970","source":"1060","inactivity":"0"},{"effective_balance":"15000000000","head":"1133","target":"2110","source":"1136","inactivity":"0"},{"effective_balance":"16000000000","head":"1208","target":"2251","source":"1211","inactivity":"0"},{"effective_balance":"17000000000","head":"1284","target":"2392","source":"1287","inactivity":"0"},{"effective_balance":"18000000000","head":"1359","target":"2532","source":"1363","inactivity":"0"},{"effective_balance":"19000000000","head":"1435","target":"2673","source":"1439","inactivity":"0"},{"effective_balance":"20000000000","head":"1510","target":"2814","source":"1514","inactivity":"0"},{"effective_balance":"21000000000","head":"1586","target":"2955","source":"1590","inactivity":"0"},{"effective_balance":"22000000000","head":"1661","target":"3095","source":"1666","inactivity":"0"},{"effective_balance":"23000000000","head":"1737","target":"3236","source":"1742","inactivity":"0"},{"effective_balance":"24000000000","head":"1812","target":"3377","source":"1817","inactivity":"0"},{"effective_balance":"25000000000","head":"1888","target":"3517","source":"1893","inactivity":"0"},{"effective_balance":"26000000000","head":"1963","target":"3658","source":"1969","inactivity":"0"},{"effective_balance":"27000000000","head":"2039","target":"3799","source":"2045","inactivity":"0"},{"effective_balance":"28000000000","head":"2115","target":"3940","source":"2120","inactivity":"0"},{"effective_balance":"29000000000","head":"2190","target":"4080","source":"2196","inactivity":"0"},{"effective_balance":"30000000000","head":"2266","target":"4221","source":"2272","inactivity":"0"},{"effective_balance":"31000000000","head":"2341","target":"4362","source":"2348","inactivity":"0"},{"effective_balance":"32000000000","head":"2417","target":"4502","source":"2423","inactivity":"0"}],"total_rewards":[{"validator_index":"0","head":"2417","target":"4502","source":"2423","inactivity":"0"},{"validator_index":"1","head":"2417","target":"4502","source":"2423","inactivity":"0"},{"validator_index":"2","head":"2417","target":"4502","source":"2423","inactivity":"0"},{"validator_index":"3","head":"2417","target":"4502","source":"2423","inactivity":"0"}]}`, + }, + { + name: "NegativeRewards", + opts: &api.AttestationRewardsOpts{ + Epoch: 335909, + Indices: []phase0.ValidatorIndex{ + 63, + }, + }, + expectedResponse: `{"ideal_rewards":[{"effective_balance":"1000000000","head":"75","target":"140","source":"75","inactivity":"0"},{"effective_balance":"2000000000","head":"151","target":"281","source":"151","inactivity":"0"},{"effective_balance":"3000000000","head":"226","target":"422","source":"227","inactivity":"0"},{"effective_balance":"4000000000","head":"302","target":"562","source":"302","inactivity":"0"},{"effective_balance":"5000000000","head":"377","target":"703","source":"378","inactivity":"0"},{"effective_balance":"6000000000","head":"453","target":"844","source":"454","inactivity":"0"},{"effective_balance":"7000000000","head":"528","target":"985","source":"530","inactivity":"0"},{"effective_balance":"8000000000","head":"604","target":"1125","source":"605","inactivity":"0"},{"effective_balance":"9000000000","head":"679","target":"1266","source":"681","inactivity":"0"},{"effective_balance":"10000000000","head":"755","target":"1407","source":"757","inactivity":"0"},{"effective_balance":"11000000000","head":"830","target":"1547","source":"833","inactivity":"0"},{"effective_balance":"12000000000","head":"906","target":"1688","source":"908","inactivity":"0"},{"effective_balance":"13000000000","head":"981","target":"1829","source":"984","inactivity":"0"},{"effective_balance":"14000000000","head":"1057","target":"1970","source":"1060","inactivity":"0"},{"effective_balance":"15000000000","head":"1133","target":"2110","source":"1136","inactivity":"0"},{"effective_balance":"16000000000","head":"1208","target":"2251","source":"1211","inactivity":"0"},{"effective_balance":"17000000000","head":"1284","target":"2392","source":"1287","inactivity":"0"},{"effective_balance":"18000000000","head":"1359","target":"2532","source":"1363","inactivity":"0"},{"effective_balance":"19000000000","head":"1435","target":"2673","source":"1439","inactivity":"0"},{"effective_balance":"20000000000","head":"1510","target":"2814","source":"1514","inactivity":"0"},{"effective_balance":"21000000000","head":"1586","target":"2955","source":"1590","inactivity":"0"},{"effective_balance":"22000000000","head":"1661","target":"3095","source":"1666","inactivity":"0"},{"effective_balance":"23000000000","head":"1737","target":"3236","source":"1742","inactivity":"0"},{"effective_balance":"24000000000","head":"1812","target":"3377","source":"1817","inactivity":"0"},{"effective_balance":"25000000000","head":"1888","target":"3517","source":"1893","inactivity":"0"},{"effective_balance":"26000000000","head":"1963","target":"3658","source":"1969","inactivity":"0"},{"effective_balance":"27000000000","head":"2039","target":"3799","source":"2045","inactivity":"0"},{"effective_balance":"28000000000","head":"2115","target":"3940","source":"2120","inactivity":"0"},{"effective_balance":"29000000000","head":"2190","target":"4080","source":"2196","inactivity":"0"},{"effective_balance":"30000000000","head":"2266","target":"4221","source":"2272","inactivity":"0"},{"effective_balance":"31000000000","head":"2341","target":"4362","source":"2348","inactivity":"0"},{"effective_balance":"32000000000","head":"2417","target":"4502","source":"2423","inactivity":"0"}],"total_rewards":[{"validator_index":"63","head":"0","target":"-4511","source":"-2429","inactivity":"0"}]}`, + }, + } + + service, err := http.New(ctx, + http.WithTimeout(timeout), + http.WithAddress(os.Getenv("HTTP_ADDRESS")), + ) + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + response, err := service.(client.AttestationRewardsProvider).AttestationRewards(ctx, test.opts) + if test.expectedErrorCode != 0 { + require.Contains(t, err.Error(), strconv.Itoa(test.expectedErrorCode)) + } else { + require.NoError(t, err) + require.NotNil(t, response) + require.NotNil(t, response.Data) + require.NotNil(t, response.Metadata) + if test.expectedResponse != "" { + responseJSON, err := json.Marshal(response.Data) + require.NoError(t, err) + require.Equal(t, test.expectedResponse, string(responseJSON)) + } + } + }) + } +} diff --git a/http/blockrewards.go b/http/blockrewards.go new file mode 100644 index 00000000..69b77025 --- /dev/null +++ b/http/blockrewards.go @@ -0,0 +1,65 @@ +// 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" + "errors" + "fmt" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "go.opentelemetry.io/otel" +) + +// BlockRewards provides rewards for proposing a block. +func (s *Service) BlockRewards(ctx context.Context, + opts *api.BlockRewardsOpts, +) ( + *api.Response[*apiv1.BlockRewards], + error, +) { + ctx, span := otel.Tracer("attestantio.go-eth2-client.http").Start(ctx, "BlockRewards") + defer span.End() + + if err := s.assertIsActive(ctx); err != nil { + return nil, err + } + if opts == nil { + return nil, client.ErrNoOptions + } + if opts.Block == "" { + return nil, errors.Join(errors.New("no block specified"), client.ErrInvalidOptions) + } + + endpoint := fmt.Sprintf("/eth/v1/beacon/rewards/blocks/%s", opts.Block) + query := "" + + httpResponse, err := s.get(ctx, endpoint, query, &opts.Common, false) + if err != nil { + return nil, errors.Join(errors.New("failed to request block rewards"), err) + } + + data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), &apiv1.BlockRewards{}) + if err != nil { + return nil, err + } + + return &api.Response[*apiv1.BlockRewards]{ + Data: data, + Metadata: metadata, + }, nil +} diff --git a/http/blockrewards_test.go b/http/blockrewards_test.go new file mode 100644 index 00000000..84e5df5b --- /dev/null +++ b/http/blockrewards_test.go @@ -0,0 +1,77 @@ +// 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_test + +import ( + "context" + "encoding/json" + "os" + "strconv" + "testing" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/stretchr/testify/require" +) + +func TestBlockRewards(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tests := []struct { + name string + opts *api.BlockRewardsOpts + expectedErrorCode int + expectedResponse string + }{ + { + name: "BlockInvalid", + opts: &api.BlockRewardsOpts{Block: "current"}, + expectedErrorCode: 400, + }, + { + name: "Good", + opts: &api.BlockRewardsOpts{ + Block: "10760040", + }, + expectedResponse: `{"proposer_index":"1515282","total":"42294581","attestations":"40696997","sync_aggregate":"1597584","proposer_slashings":"0","attester_slashings":"0"}`, + }, + } + + service, err := http.New(ctx, + http.WithTimeout(timeout), + http.WithAddress(os.Getenv("HTTP_ADDRESS")), + ) + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + response, err := service.(client.BlockRewardsProvider).BlockRewards(ctx, test.opts) + if test.expectedErrorCode != 0 { + require.Contains(t, err.Error(), strconv.Itoa(test.expectedErrorCode)) + } else { + require.NoError(t, err) + require.NotNil(t, response) + require.NotNil(t, response.Data) + require.NotNil(t, response.Metadata) + if test.expectedResponse != "" { + responseJSON, err := json.Marshal(response.Data) + require.NoError(t, err) + require.Equal(t, test.expectedResponse, string(responseJSON)) + } + } + }) + } +} diff --git a/http/signedbeaconblock.go b/http/signedbeaconblock.go index 140c84fc..e5916375 100644 --- a/http/signedbeaconblock.go +++ b/http/signedbeaconblock.go @@ -43,6 +43,9 @@ func (s *Service) SignedBeaconBlock(ctx context.Context, if opts == nil { return nil, client.ErrNoOptions } + if opts.Block == "" { + return nil, errors.Join(errors.New("no block specified"), client.ErrInvalidOptions) + } endpoint := fmt.Sprintf("/eth/v2/beacon/blocks/%s", opts.Block) httpResponse, err := s.get(ctx, endpoint, "", &opts.Common, true) diff --git a/http/synccommitteerewards.go b/http/synccommitteerewards.go new file mode 100644 index 00000000..7b5b5712 --- /dev/null +++ b/http/synccommitteerewards.go @@ -0,0 +1,82 @@ +// 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" + "fmt" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) + +// SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. +func (s *Service) SyncCommitteeRewards(ctx context.Context, + opts *api.SyncCommitteeRewardsOpts, +) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, +) { + ctx, span := otel.Tracer("attestantio.go-eth2-client.http").Start(ctx, "SyncCommitteeRewards") + defer span.End() + + if err := s.assertIsActive(ctx); err != nil { + return nil, err + } + if opts == nil { + return nil, client.ErrNoOptions + } + if opts.Block == "" { + return nil, errors.Join(errors.New("no block specified"), client.ErrInvalidOptions) + } + span.SetAttributes(attribute.Int("validators", len(opts.Indices)+len(opts.PubKeys))) + + endpoint := fmt.Sprintf("/eth/v1/beacon/rewards/sync_committee/%s", opts.Block) + query := "" + + body := make([]string, 0, len(opts.Indices)+len(opts.PubKeys)) + + for i := range opts.Indices { + body = append(body, fmt.Sprintf("%d", opts.Indices[i])) + } + for i := range opts.PubKeys { + body = append(body, opts.PubKeys[i].String()) + } + + reqData, err := json.Marshal(body) + if err != nil { + return nil, errors.Join(errors.New("failed to marshal request data"), err) + } + + httpResponse, err := s.post(ctx, endpoint, query, &opts.Common, bytes.NewReader(reqData), ContentTypeJSON, map[string]string{}) + if err != nil { + return nil, errors.Join(errors.New("failed to request sync committee rewards"), err) + } + + data, metadata, err := decodeJSONResponse(bytes.NewReader(httpResponse.body), []*apiv1.SyncCommitteeReward{}) + if err != nil { + return nil, err + } + + return &api.Response[[]*apiv1.SyncCommitteeReward]{ + Data: data, + Metadata: metadata, + }, nil +} diff --git a/http/synccommitteerewards_test.go b/http/synccommitteerewards_test.go new file mode 100644 index 00000000..96e309cd --- /dev/null +++ b/http/synccommitteerewards_test.go @@ -0,0 +1,94 @@ +// 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_test + +import ( + "context" + "encoding/json" + "os" + "strconv" + "testing" + + client "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "github.com/attestantio/go-eth2-client/http" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/require" +) + +func TestSyncCommitteeRewards(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tests := []struct { + name string + opts *api.SyncCommitteeRewardsOpts + expectedErrorCode int + expectedResponse string + }{ + { + name: "BlockInvalid", + opts: &api.SyncCommitteeRewardsOpts{Block: "current"}, + expectedErrorCode: 400, + }, + { + name: "MixedIndicesAndPubKeys", + opts: &api.SyncCommitteeRewardsOpts{ + Block: "10760058", + Indices: []phase0.ValidatorIndex{ + 286437, + }, + PubKeys: []phase0.BLSPubKey{ + *mustParsePubKey("0xb7dd1c63cfe60163ffcb889d502b0af3b8ab41cb0dc95edb46eccfeb79e984886fe54f800e813ae09d48e98087010a10"), + }, + }, + expectedResponse: `[{"validator_index":"286437","reward":"22456"},{"validator_index":"1674334","reward":"22456"}]`, + }, + { + name: "NegativeRewards", + opts: &api.SyncCommitteeRewardsOpts{ + Block: "10760058", + Indices: []phase0.ValidatorIndex{ + 1055307, + }, + }, + expectedResponse: `[{"validator_index":"1055307","reward":"-22456"}]`, + }, + } + + service, err := http.New(ctx, + http.WithTimeout(timeout), + http.WithAddress(os.Getenv("HTTP_ADDRESS")), + ) + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + response, err := service.(client.SyncCommitteeRewardsProvider).SyncCommitteeRewards(ctx, test.opts) + if test.expectedErrorCode != 0 { + require.Contains(t, err.Error(), strconv.Itoa(test.expectedErrorCode)) + } else { + require.NoError(t, err) + require.NotNil(t, response) + require.NotNil(t, response.Data) + require.NotNil(t, response.Metadata) + if test.expectedResponse != "" { + responseJSON, err := json.Marshal(response.Data) + require.NoError(t, err) + require.Equal(t, test.expectedResponse, string(responseJSON)) + } + } + }) + } +} diff --git a/http/validators.go b/http/validators.go index 2ba33902..92957f30 100644 --- a/http/validators.go +++ b/http/validators.go @@ -49,6 +49,9 @@ func (s *Service) Validators(ctx context.Context, if opts == nil { return nil, client.ErrNoOptions } + if opts.State == "" { + return nil, errors.Join(errors.New("no state specified"), client.ErrInvalidOptions) + } span.SetAttributes(attribute.Int("validators", len(opts.Indices)+len(opts.PubKeys))) if len(opts.Indices) == 0 && len(opts.PubKeys) == 0 { diff --git a/mock/attestationrewards.go b/mock/attestationrewards.go new file mode 100644 index 00000000..359ac90b --- /dev/null +++ b/mock/attestationrewards.go @@ -0,0 +1,37 @@ +// 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 mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// AttestationRewards provides rewards to the given validators for attesting. +func (*Service) AttestationRewards(_ context.Context, + _ *api.AttestationRewardsOpts, +) ( + *api.Response[*apiv1.AttestationRewards], + error, +) { + return &api.Response[*apiv1.AttestationRewards]{ + Data: &apiv1.AttestationRewards{ + IdealRewards: []apiv1.IdealAttestationRewards{}, + TotalRewards: []apiv1.ValidatorAttestationRewards{}, + }, + Metadata: make(map[string]any), + }, nil +} diff --git a/mock/blockrewards.go b/mock/blockrewards.go new file mode 100644 index 00000000..c6983eab --- /dev/null +++ b/mock/blockrewards.go @@ -0,0 +1,34 @@ +// 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 mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// BlockRewards provides rewards for proposing a block. +func (*Service) BlockRewards(_ context.Context, + _ *api.BlockRewardsOpts, +) ( + *api.Response[*apiv1.BlockRewards], + error, +) { + return &api.Response[*apiv1.BlockRewards]{ + Data: &apiv1.BlockRewards{}, + Metadata: make(map[string]any), + }, nil +} diff --git a/mock/synccommitteerewards.go b/mock/synccommitteerewards.go new file mode 100644 index 00000000..8939aa4b --- /dev/null +++ b/mock/synccommitteerewards.go @@ -0,0 +1,34 @@ +// 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 mock + +import ( + "context" + + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. +func (*Service) SyncCommitteeRewards(_ context.Context, + _ *api.SyncCommitteeRewardsOpts, +) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, +) { + return &api.Response[[]*apiv1.SyncCommitteeReward]{ + Data: []*apiv1.SyncCommitteeReward{}, + Metadata: make(map[string]any), + }, nil +} diff --git a/multi/attestationrewards.go b/multi/attestationrewards.go new file mode 100644 index 00000000..09c6b5c8 --- /dev/null +++ b/multi/attestationrewards.go @@ -0,0 +1,49 @@ +// 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" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// AttestationRewards provides rewards to the given validators for attesting. +func (s *Service) AttestationRewards(ctx context.Context, + opts *api.AttestationRewardsOpts, +) ( + *api.Response[*apiv1.AttestationRewards], + error, +) { + res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { + attestationData, err := client.(consensusclient.AttestationRewardsProvider).AttestationRewards(ctx, opts) + if err != nil { + return nil, err + } + + return attestationData, nil + }, nil) + if err != nil { + return nil, err + } + + response, isResponse := res.(*api.Response[*apiv1.AttestationRewards]) + if !isResponse { + return nil, ErrIncorrectType + } + + return response, nil +} diff --git a/multi/attestationrewards_test.go b/multi/attestationrewards_test.go new file mode 100644 index 00000000..75828dcd --- /dev/null +++ b/multi/attestationrewards_test.go @@ -0,0 +1,60 @@ +// 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" + "testing" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "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 TestAttestationRewards(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++ { + res, err := multiClient.(consensusclient.AttestationRewardsProvider).AttestationRewards(ctx, &api.AttestationRewardsOpts{}) + require.NoError(t, err) + require.NotNil(t, res) + } + // 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/multi/blockrewards.go b/multi/blockrewards.go new file mode 100644 index 00000000..20162ea2 --- /dev/null +++ b/multi/blockrewards.go @@ -0,0 +1,49 @@ +// 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" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// BlockRewards provides rewards for proposing a block. +func (s *Service) BlockRewards(ctx context.Context, + opts *api.BlockRewardsOpts, +) ( + *api.Response[*apiv1.BlockRewards], + error, +) { + res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { + attestationData, err := client.(consensusclient.BlockRewardsProvider).BlockRewards(ctx, opts) + if err != nil { + return nil, err + } + + return attestationData, nil + }, nil) + if err != nil { + return nil, err + } + + response, isResponse := res.(*api.Response[*apiv1.BlockRewards]) + if !isResponse { + return nil, ErrIncorrectType + } + + return response, nil +} diff --git a/multi/blockrewards_test.go b/multi/blockrewards_test.go new file mode 100644 index 00000000..fa0175bf --- /dev/null +++ b/multi/blockrewards_test.go @@ -0,0 +1,60 @@ +// 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" + "testing" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "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 TestBlockRewards(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++ { + res, err := multiClient.(consensusclient.BlockRewardsProvider).BlockRewards(ctx, &api.BlockRewardsOpts{}) + require.NoError(t, err) + require.NotNil(t, res) + } + // 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/multi/service_test.go b/multi/service_test.go index e5cfd1d3..d9c1391d 100644 --- a/multi/service_test.go +++ b/multi/service_test.go @@ -105,6 +105,7 @@ func TestInterfaces(t *testing.T) { assert.Implements(t, (*client.AggregateAttestationsSubmitter)(nil), s) assert.Implements(t, (*client.AttestationDataProvider)(nil), s) assert.Implements(t, (*client.AttestationPoolProvider)(nil), s) + assert.Implements(t, (*client.AttestationRewardsProvider)(nil), s) assert.Implements(t, (*client.AttestationsSubmitter)(nil), s) assert.Implements(t, (*client.AttesterDutiesProvider)(nil), s) assert.Implements(t, (*client.BeaconBlockHeadersProvider)(nil), s) @@ -113,6 +114,7 @@ func TestInterfaces(t *testing.T) { assert.Implements(t, (*client.BeaconCommitteeSubscriptionsSubmitter)(nil), s) assert.Implements(t, (*client.BeaconStateProvider)(nil), s) assert.Implements(t, (*client.BlindedBeaconBlockSubmitter)(nil), s) + assert.Implements(t, (*client.BlockRewardsProvider)(nil), s) assert.Implements(t, (*client.BlobSidecarsProvider)(nil), s) assert.Implements(t, (*client.ValidatorRegistrationsSubmitter)(nil), s) assert.Implements(t, (*client.DepositContractProvider)(nil), s) @@ -131,6 +133,7 @@ func TestInterfaces(t *testing.T) { assert.Implements(t, (*client.SyncCommitteeContributionsSubmitter)(nil), s) assert.Implements(t, (*client.SyncCommitteeDutiesProvider)(nil), s) assert.Implements(t, (*client.SyncCommitteeMessagesSubmitter)(nil), s) + assert.Implements(t, (*client.SyncCommitteeRewardsProvider)(nil), s) assert.Implements(t, (*client.SyncCommitteesProvider)(nil), s) assert.Implements(t, (*client.SyncCommitteeSubscriptionsSubmitter)(nil), s) assert.Implements(t, (*client.ValidatorBalancesProvider)(nil), s) diff --git a/multi/synccommitteerewards.go b/multi/synccommitteerewards.go new file mode 100644 index 00000000..71a2d763 --- /dev/null +++ b/multi/synccommitteerewards.go @@ -0,0 +1,49 @@ +// 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" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + apiv1 "github.com/attestantio/go-eth2-client/api/v1" +) + +// SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. +func (s *Service) SyncCommitteeRewards(ctx context.Context, + opts *api.SyncCommitteeRewardsOpts, +) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, +) { + res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (any, error) { + attestationData, err := client.(consensusclient.SyncCommitteeRewardsProvider).SyncCommitteeRewards(ctx, opts) + if err != nil { + return nil, err + } + + return attestationData, nil + }, nil) + if err != nil { + return nil, err + } + + response, isResponse := res.(*api.Response[[]*apiv1.SyncCommitteeReward]) + if !isResponse { + return nil, ErrIncorrectType + } + + return response, nil +} diff --git a/multi/synccommitteerewards_test.go b/multi/synccommitteerewards_test.go new file mode 100644 index 00000000..fb3b537b --- /dev/null +++ b/multi/synccommitteerewards_test.go @@ -0,0 +1,60 @@ +// 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" + "testing" + + consensusclient "github.com/attestantio/go-eth2-client" + "github.com/attestantio/go-eth2-client/api" + "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 TestSyncCommitteeRewards(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++ { + res, err := multiClient.(consensusclient.SyncCommitteeRewardsProvider).SyncCommitteeRewards(ctx, &api.SyncCommitteeRewardsOpts{}) + require.NoError(t, err) + require.NotNil(t, res) + } + // 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 fbef2cae..fd50730f 100644 --- a/service.go +++ b/service.go @@ -194,6 +194,17 @@ type AttestationPoolProvider interface { ) } +// AttestationRewardsProvider is the interface for providing attestation rewards. +type AttestationRewardsProvider interface { + // AttestationRewards provides rewards to the given validators for attesting. + AttestationRewards(ctx context.Context, + opts *api.AttestationRewardsOpts, + ) ( + *api.Response[*apiv1.AttestationRewards], + error, + ) +} + // AttestationsSubmitter is the interface for submitting attestations. type AttestationsSubmitter interface { // SubmitAttestations submits attestations. @@ -217,6 +228,17 @@ type AttesterDutiesProvider interface { ) } +// BlockRewardsProvider is the interface for providing block rewards. +type BlockRewardsProvider interface { + // BlockRewards provides rewards for proposing a block. + BlockRewards(ctx context.Context, + opts *api.BlockRewardsOpts, + ) ( + *api.Response[*apiv1.BlockRewards], + error, + ) +} + // DepositContractProvider is the interface for providing details about the deposit contract. type DepositContractProvider interface { // DepositContract provides details of the execution deposit contract for the chain. @@ -269,6 +291,17 @@ type SyncCommitteeContributionsSubmitter interface { SubmitSyncCommitteeContributions(ctx context.Context, contributionAndProofs []*altair.SignedContributionAndProof) error } +// SyncCommitteeRewardsProvider is the interface for providing sync committee rewards. +type SyncCommitteeRewardsProvider interface { + // SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. + SyncCommitteeRewards(ctx context.Context, + opts *api.SyncCommitteeRewardsOpts, + ) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, + ) +} + // BLSToExecutionChangesSubmitter is the interface for submitting BLS to execution address changes. type BLSToExecutionChangesSubmitter interface { // SubmitBLSToExecutionChanges submits BLS to execution address change operations. diff --git a/testclients/erroring.go b/testclients/erroring.go index 71c39a56..d3957698 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -1,4 +1,4 @@ -// Copyright © 2021 - 2023 Attestant Limited. +// Copyright © 2021 - 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 @@ -911,3 +911,57 @@ func (s *Erroring) ForkChoice(ctx context.Context, return next.ForkChoice(ctx, opts) } + +// AttestationRewards provides rewards to the given validators for attesting. +func (s *Erroring) AttestationRewards(ctx context.Context, + opts *api.AttestationRewardsOpts, +) ( + *api.Response[*apiv1.AttestationRewards], + error, +) { + if err := s.maybeError(ctx); err != nil { + return nil, err + } + next, isNext := s.next.(consensusclient.AttestationRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.AttestationRewards(ctx, opts) +} + +// BlockRewards provides rewards for proposing a block. +func (s *Erroring) BlockRewards(ctx context.Context, + opts *api.BlockRewardsOpts, +) ( + *api.Response[*apiv1.BlockRewards], + error, +) { + if err := s.maybeError(ctx); err != nil { + return nil, err + } + next, isNext := s.next.(consensusclient.BlockRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.BlockRewards(ctx, opts) +} + +// SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. +func (s *Erroring) SyncCommitteeRewards(ctx context.Context, + opts *api.SyncCommitteeRewardsOpts, +) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, +) { + if err := s.maybeError(ctx); err != nil { + return nil, err + } + next, isNext := s.next.(consensusclient.SyncCommitteeRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.SyncCommitteeRewards(ctx, opts) +} diff --git a/testclients/sleepy.go b/testclients/sleepy.go index dd85f06a..fbab50b5 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -641,3 +641,51 @@ func (s *Sleepy) BlobSidecars(ctx context.Context, return next.BlobSidecars(ctx, opts) } + +// AttestationRewards provides rewards to the given validators for attesting. +func (s *Sleepy) AttestationRewards(ctx context.Context, + opts *api.AttestationRewardsOpts, +) ( + *api.Response[*apiv1.AttestationRewards], + error, +) { + s.sleep(ctx) + next, isNext := s.next.(consensusclient.AttestationRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.AttestationRewards(ctx, opts) +} + +// BlockRewards provides rewards for proposing a block. +func (s *Sleepy) BlockRewards(ctx context.Context, + opts *api.BlockRewardsOpts, +) ( + *api.Response[*apiv1.BlockRewards], + error, +) { + s.sleep(ctx) + next, isNext := s.next.(consensusclient.BlockRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.BlockRewards(ctx, opts) +} + +// SyncCommitteeRewards provides rewards to the given validators for being members of a sync committee. +func (s *Sleepy) SyncCommitteeRewards(ctx context.Context, + opts *api.SyncCommitteeRewardsOpts, +) ( + *api.Response[[]*apiv1.SyncCommitteeReward], + error, +) { + s.sleep(ctx) + next, isNext := s.next.(consensusclient.SyncCommitteeRewardsProvider) + if !isNext { + return nil, fmt.Errorf("%s@%s does not support this call", s.next.Name(), s.next.Address()) + } + + return next.SyncCommitteeRewards(ctx, opts) +} From b0c715f47a04ecefdb676ff51e231a5c1e50928b Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Sat, 4 Jan 2025 00:28:01 +0000 Subject: [PATCH 2/4] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4117082..9414548c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ dev: - add attester_slashing, block_gossip, bls_to_execution_change and proposer_slashing events + - add AttestationRewards, BlockRewards, and SyncCommitteeRewards functions 0.21.10: - better validator state when balance not supplied From c4c1a8416d4690d5871fe21bab263e0d43ae63a2 Mon Sep 17 00:00:00 2001 From: Chris Berry Date: Mon, 6 Jan 2025 10:58:12 +0000 Subject: [PATCH 3/4] Correct various typos and grammar --- README.md | 2 +- api/attestationpoolopts.go | 2 +- api/blindedproposalopts.go | 2 +- api/v1/beaconcommitteesubscription.go | 2 +- api/v1/forkchoice.go | 4 ++-- api/v1/proposalpreparation.go | 2 +- api/validatorsopts.go | 2 +- http/farfutureepoch.go | 2 +- http/http.go | 2 +- http/parameters.go | 2 +- http/service_test.go | 2 +- http/submitattesterslashing_test.go | 4 ++-- http/synccommitteeduties_test.go | 2 +- mock/attesterduties.go | 2 +- mock/farfutureepoch.go | 2 +- multi/attesterduties.go | 2 +- multi/service.go | 2 +- multi/service_test.go | 2 +- multi/synccommitteeduties.go | 2 +- service.go | 2 +- spec/altair/consensusspec_test.go | 4 ++-- spec/altair/types.go | 2 +- spec/bellatrix/consensusspec_test.go | 4 ++-- spec/capella/consensusspec_test.go | 4 ++-- spec/versionedsignedbeaconblock.go | 2 +- testclients/erroring.go | 4 ++-- testclients/sleepy.go | 2 +- 27 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index dd609591..f2e0ee86 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Go library providing an abstraction to multiple Ethereum 2 beacon nodes. Its external API follows the official [Ethereum beacon APIs](https://github.com/ethereum/beacon-APIs) specification. -This library is under development; expect APIs and data structures to change until it reaches version 1.0. In addition, clients' implementations of both their own and the standard API are themselves under development so implementation of the the full API can be incomplete. +This library is under development; expect APIs and data structures to change until it reaches version 1.0. In addition, clients' implementations of both their own and the standard API are themselves under development so implementation of the full API can be incomplete. > Between versions 0.18.0 and 0.19.0 the API has undergone a number of changes. Please see [the detailed documentation](docs/0.19.0-changes.md) regarding these changes. diff --git a/api/attestationpoolopts.go b/api/attestationpoolopts.go index 9994e109..fc5923a4 100644 --- a/api/attestationpoolopts.go +++ b/api/attestationpoolopts.go @@ -23,7 +23,7 @@ type AttestationPoolOpts struct { // data for all slots will be obtained. Slot *phase0.Slot - // CommmitteeIndex is the committee index for which the data is obtained. + // CommitteeIndex is the committee index for which the data is obtained. // If not present then data for all committee indices will be obtained. CommitteeIndex *phase0.CommitteeIndex } diff --git a/api/blindedproposalopts.go b/api/blindedproposalopts.go index 0ba2fe96..d1ea3d17 100644 --- a/api/blindedproposalopts.go +++ b/api/blindedproposalopts.go @@ -23,7 +23,7 @@ type BlindedProposalOpts struct { Slot phase0.Slot // RandaoReveal is the RANDAO reveal for the proposal. RandaoReveal phase0.BLSSignature - // Graffit is the graffiti to be included in the beacon block body. + // Graffiti is the graffiti to be included in the beacon block body. Graffiti [32]byte // SkipRandaoVerification is true if we do not want the server to verify our RANDAO reveal. // If this is set then the RANDAO reveal should be passed as the point at infinity (0xc0…00) diff --git a/api/v1/beaconcommitteesubscription.go b/api/v1/beaconcommitteesubscription.go index e45d0081..3ac3793b 100644 --- a/api/v1/beaconcommitteesubscription.go +++ b/api/v1/beaconcommitteesubscription.go @@ -24,7 +24,7 @@ import ( // BeaconCommitteeSubscription is the data required for a beacon committee subscription. type BeaconCommitteeSubscription struct { - // ValidatorIdex is the index of the validator making the subscription request. + // ValidatorIndex is the index of the validator making the subscription request. ValidatorIndex phase0.ValidatorIndex // Slot is the slot for which the validator is attesting. Slot phase0.Slot diff --git a/api/v1/forkchoice.go b/api/v1/forkchoice.go index 0de7e006..1678ad8a 100644 --- a/api/v1/forkchoice.go +++ b/api/v1/forkchoice.go @@ -162,7 +162,7 @@ type ForkChoiceNode struct { BlockRoot phase0.Root // ParentRoot is the parent root of the node. ParentRoot phase0.Root - // JustifiedEpcih is the justified epoch of the node. + // JustifiedEpcoh is the justified epoch of the node. JustifiedEpoch phase0.Epoch // FinalizedEpoch is the finalized epoch of the node. FinalizedEpoch phase0.Epoch @@ -170,7 +170,7 @@ type ForkChoiceNode struct { Weight uint64 // Validity is the validity of the node. Validity ForkChoiceNodeValidity - // ExecutiionBlockHash is the execution block hash of the node. + // ExecutionBlockHash is the execution block hash of the node. ExecutionBlockHash phase0.Root // ExtraData is the extra data of the node. ExtraData map[string]any diff --git a/api/v1/proposalpreparation.go b/api/v1/proposalpreparation.go index f1394489..9a9aace9 100644 --- a/api/v1/proposalpreparation.go +++ b/api/v1/proposalpreparation.go @@ -27,7 +27,7 @@ import ( // ProposalPreparation is the data required for proposal preparation. type ProposalPreparation struct { - // ValidatorIdex is the index of the validator making the proposal request. + // ValidatorIndex is the index of the validator making the proposal request. ValidatorIndex phase0.ValidatorIndex // FeeRecipient is the execution address to be used with preparing blocks. FeeRecipient bellatrix.ExecutionAddress `ssz-size:"20"` diff --git a/api/validatorsopts.go b/api/validatorsopts.go index ef7c0ebd..3e1429bb 100644 --- a/api/validatorsopts.go +++ b/api/validatorsopts.go @@ -31,7 +31,7 @@ type ValidatorsOpts struct { // PubKeys is a list of validator public keys to restrict the returned values. // If no public keys are supplied then no filter will be applied. PubKeys []phase0.BLSPubKey - // ValidatorStates is a list of validator states to restric the returned values. + // ValidatorStates is a list of validator states to restrict the returned values. // If no validator states are supplied then no filter will be applied. ValidatorStates []apiv1.ValidatorState } diff --git a/http/farfutureepoch.go b/http/farfutureepoch.go index d15b10a7..7f3f1570 100644 --- a/http/farfutureepoch.go +++ b/http/farfutureepoch.go @@ -19,7 +19,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" ) -// FarFutureEpoch provides the values for FAR_FUTURE_EOPCH of the chain. +// FarFutureEpoch provides the values for FAR_FUTURE_EPOCH of the chain. func (*Service) FarFutureEpoch(_ context.Context) (phase0.Epoch, error) { return phase0.Epoch(0xffffffffffffffff), nil } diff --git a/http/http.go b/http/http.go index 18f90c04..1342841d 100644 --- a/http/http.go +++ b/http/http.go @@ -274,7 +274,7 @@ func (s *Service) get(ctx context.Context, case errors.Is(err, context.DeadlineExceeded): // We don't consider context deadline exceeded to be a potential connection issue, as the user selected the deadline. case strings.HasSuffix(callURL.String(), "/node/syncing"): - // Special case; if we have called the syncing endpoint and it failed then we don't check the connectino status, as + // Special case; if we have called the syncing endpoint and it failed then we don't check the connection status, as // that calls the syncing endpoint itself and so we find ourselves in an endless loop. default: // We consider other errors to be potential connection issues. diff --git a/http/parameters.go b/http/parameters.go index ab679a5b..c6f2e21f 100644 --- a/http/parameters.go +++ b/http/parameters.go @@ -84,7 +84,7 @@ func WithIndexChunkSize(indexChunkSize int) Parameter { }) } -// WithPubKeyChunkSize sets the maximum number of public kyes to send for individual validator requests. +// WithPubKeyChunkSize sets the maximum number of public keys to send for individual validator requests. func WithPubKeyChunkSize(pubKeyChunkSize int) Parameter { return parameterFunc(func(p *parameters) { p.pubKeyChunkSize = pubKeyChunkSize diff --git a/http/service_test.go b/http/service_test.go index aff505e6..f85f8950 100644 --- a/http/service_test.go +++ b/http/service_test.go @@ -118,7 +118,7 @@ func TestInterfaces(t *testing.T) { s, err := v1.New(ctx, v1.WithAddress(os.Getenv("HTTP_ADDRESS")), v1.WithTimeout(5*time.Second)) require.NoError(t, err) - // Standard interfacs. + // Standard interfaces. assert.Implements(t, (*client.AggregateAttestationProvider)(nil), s) assert.Implements(t, (*client.AggregateAttestationsSubmitter)(nil), s) assert.Implements(t, (*client.AttestationDataProvider)(nil), s) diff --git a/http/submitattesterslashing_test.go b/http/submitattesterslashing_test.go index f313c706..6ddf7d92 100644 --- a/http/submitattesterslashing_test.go +++ b/http/submitattesterslashing_test.go @@ -79,8 +79,8 @@ func TestSubmitAttesterSlashing(t *testing.T) { t.Run(test.name, func(t *testing.T) { // Create proposal slashing and submit. err := service.(client.AttesterSlashingSubmitter).SubmitAttesterSlashing(context.Background(), &test.slashing) - // This should error on bad slashing - require.Error(t, err) //we should be getting errors about non-slashable attestations or signatures + // This should error on bad slashing. + require.Error(t, err) // We should be getting errors about non-slashable attestations or signatures. }) } } diff --git a/http/synccommitteeduties_test.go b/http/synccommitteeduties_test.go index a55bd430..0b3a4ad1 100644 --- a/http/synccommitteeduties_test.go +++ b/http/synccommitteeduties_test.go @@ -75,7 +75,7 @@ func TestSyncCommitteeDuties(t *testing.T) { require.NoError(t, err) require.NotNil(t, response) require.NotNil(t, response.Data) - // No guaratee that any included indices will be have a sync duty. + // No guarantee that any included indices will have a sync duty. }) } } diff --git a/mock/attesterduties.go b/mock/attesterduties.go index e2150057..8aafe275 100644 --- a/mock/attesterduties.go +++ b/mock/attesterduties.go @@ -21,7 +21,7 @@ import ( ) // AttesterDuties obtains attester duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (*Service) AttesterDuties(_ context.Context, opts *api.AttesterDutiesOpts) (*api.Response[[]*apiv1.AttesterDuty], error) { data := make([]*apiv1.AttesterDuty, len(opts.Indices)) for i := range opts.Indices { diff --git a/mock/farfutureepoch.go b/mock/farfutureepoch.go index da1057f8..5fc13c17 100644 --- a/mock/farfutureepoch.go +++ b/mock/farfutureepoch.go @@ -19,7 +19,7 @@ import ( spec "github.com/attestantio/go-eth2-client/spec/phase0" ) -// FarFutureEpoch provides the values for FAR_FUTURE_EOPCH of the chain. +// FarFutureEpoch provides the values for FAR_FUTURE_EPOCH of the chain. func (*Service) FarFutureEpoch(_ context.Context) (spec.Epoch, error) { return spec.Epoch(0xffffffffffffffff), nil } diff --git a/multi/attesterduties.go b/multi/attesterduties.go index de0d0ce1..52977ba0 100644 --- a/multi/attesterduties.go +++ b/multi/attesterduties.go @@ -22,7 +22,7 @@ import ( ) // AttesterDuties obtains attester duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (s *Service) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesOpts, ) ( diff --git a/multi/service.go b/multi/service.go index e28d0ac6..6d03a6d9 100644 --- a/multi/service.go +++ b/multi/service.go @@ -36,7 +36,7 @@ type Service struct { } // New creates a new Ethereum 2 client with multiple endpoints. -// The endpoints are periodiclaly checked to see if they are active, +// The endpoints are periodically checked to see if they are active, // and requests will retry a different client if the currently active // client fails to respond. func New(ctx context.Context, params ...Parameter) (consensusclient.Service, error) { diff --git a/multi/service_test.go b/multi/service_test.go index d9c1391d..e1f503b1 100644 --- a/multi/service_test.go +++ b/multi/service_test.go @@ -100,7 +100,7 @@ func TestInterfaces(t *testing.T) { ) require.NoError(t, err) - // Standard interfacs. + // Standard interfaces. assert.Implements(t, (*client.AggregateAttestationProvider)(nil), s) assert.Implements(t, (*client.AggregateAttestationsSubmitter)(nil), s) assert.Implements(t, (*client.AttestationDataProvider)(nil), s) diff --git a/multi/synccommitteeduties.go b/multi/synccommitteeduties.go index 5723323a..beb85e7c 100644 --- a/multi/synccommitteeduties.go +++ b/multi/synccommitteeduties.go @@ -22,7 +22,7 @@ import ( ) // SyncCommitteeDuties obtains attester duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (s *Service) SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommitteeDutiesOpts, ) ( diff --git a/service.go b/service.go index fd50730f..84d35174 100644 --- a/service.go +++ b/service.go @@ -253,7 +253,7 @@ type DepositContractProvider interface { // SyncCommitteeDutiesProvider is the interface for providing sync committee duties. type SyncCommitteeDutiesProvider interface { // SyncCommitteeDuties obtains sync committee duties. - // If validatorIndicess is nil it will return all duties for the given epoch. + // If validatorIndices is nil it will return all duties for the given epoch. SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommitteeDutiesOpts, ) ( diff --git a/spec/altair/consensusspec_test.go b/spec/altair/consensusspec_test.go index b9172be7..8257ef5b 100644 --- a/spec/altair/consensusspec_test.go +++ b/spec/altair/consensusspec_test.go @@ -128,7 +128,7 @@ func TestConsensusSpec(t *testing.T) { s: &phase0.SignedBeaconBlockHeader{}, }, { - name: "SignedContributionAndproof", + name: "SignedContributionAndProof", s: &altair.SignedContributionAndProof{}, }, { @@ -140,7 +140,7 @@ func TestConsensusSpec(t *testing.T) { s: &altair.SyncAggregate{}, }, { - name: "SyncCommitteeContribuion", + name: "SyncCommitteeContribution", s: &altair.SyncCommitteeContribution{}, }, { diff --git a/spec/altair/types.go b/spec/altair/types.go index 4450bd20..8cda55e7 100644 --- a/spec/altair/types.go +++ b/spec/altair/types.go @@ -13,7 +13,7 @@ package altair -// ParticipationFlag is an individual particiation flag for a validator. +// ParticipationFlag is an individual participation flag for a validator. type ParticipationFlag int const ( diff --git a/spec/bellatrix/consensusspec_test.go b/spec/bellatrix/consensusspec_test.go index d5dabfbf..7cc7a066 100644 --- a/spec/bellatrix/consensusspec_test.go +++ b/spec/bellatrix/consensusspec_test.go @@ -137,7 +137,7 @@ func TestConsensusSpec(t *testing.T) { s: &phase0.SignedBeaconBlockHeader{}, }, { - name: "SignedContributionAndproof", + name: "SignedContributionAndProof", s: &altair.SignedContributionAndProof{}, }, { @@ -149,7 +149,7 @@ func TestConsensusSpec(t *testing.T) { s: &altair.SyncAggregate{}, }, { - name: "SyncCommitteeContribuion", + name: "SyncCommitteeContribution", s: &altair.SyncCommitteeContribution{}, }, { diff --git a/spec/capella/consensusspec_test.go b/spec/capella/consensusspec_test.go index 8b000b8a..65e7e23e 100644 --- a/spec/capella/consensusspec_test.go +++ b/spec/capella/consensusspec_test.go @@ -141,7 +141,7 @@ func TestConsensusSpec(t *testing.T) { s: &phase0.SignedBeaconBlockHeader{}, }, { - name: "SignedContributionAndproof", + name: "SignedContributionAndProof", s: &altair.SignedContributionAndProof{}, }, { @@ -153,7 +153,7 @@ func TestConsensusSpec(t *testing.T) { s: &altair.SyncAggregate{}, }, { - name: "SyncCommitteeContribuion", + name: "SyncCommitteeContribution", s: &altair.SyncCommitteeContribution{}, }, { diff --git a/spec/versionedsignedbeaconblock.go b/spec/versionedsignedbeaconblock.go index e5033a52..9bbcf739 100644 --- a/spec/versionedsignedbeaconblock.go +++ b/spec/versionedsignedbeaconblock.go @@ -173,7 +173,7 @@ func (v *VersionedSignedBeaconBlock) ExecutionBlockNumber() (uint64, error) { } } -// ExecutionTransactions returs the execution payload transactions for the block. +// ExecutionTransactions returns the execution payload transactions for the block. func (v *VersionedSignedBeaconBlock) ExecutionTransactions() ([]bellatrix.Transaction, error) { switch v.Version { case DataVersionPhase0: diff --git a/testclients/erroring.go b/testclients/erroring.go index d3957698..12306539 100644 --- a/testclients/erroring.go +++ b/testclients/erroring.go @@ -319,7 +319,7 @@ func (s *Erroring) SubmitSyncCommitteeMessages(ctx context.Context, messages []* } // AttesterDuties obtains attester duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (s *Erroring) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesOpts, ) ( @@ -678,7 +678,7 @@ func (s *Erroring) SyncCommitteeContribution(ctx context.Context, } // SyncCommitteeDuties obtains sync committee duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (s *Erroring) SyncCommitteeDuties(ctx context.Context, opts *api.SyncCommitteeDutiesOpts, ) ( diff --git a/testclients/sleepy.go b/testclients/sleepy.go index fbab50b5..162b1160 100644 --- a/testclients/sleepy.go +++ b/testclients/sleepy.go @@ -252,7 +252,7 @@ func (s *Sleepy) SubmitAttestations(ctx context.Context, attestations []*phase0. } // AttesterDuties obtains attester duties. -// If validatorIndicess is nil it will return all duties for the given epoch. +// If validatorIndices is nil it will return all duties for the given epoch. func (s *Sleepy) AttesterDuties(ctx context.Context, opts *api.AttesterDutiesOpts, ) ( From 23718427a5b5dcff6c634d46e1cdfb0fac8d72bf Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Mon, 13 Jan 2025 11:57:00 +0000 Subject: [PATCH 4/4] Update version. --- CHANGELOG.md | 2 +- http/http.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9414548c..39d2675c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -dev: +0.23.0: - add attester_slashing, block_gossip, bls_to_execution_change and proposer_slashing events - add AttestationRewards, BlockRewards, and SyncCommitteeRewards functions diff --git a/http/http.go b/http/http.go index 1342841d..8497bc3c 100644 --- a/http/http.go +++ b/http/http.go @@ -35,7 +35,7 @@ import ( ) // defaultUserAgent is sent with requests if no other user agent has been supplied. -const defaultUserAgent = "go-eth2-client/0.22.0" +const defaultUserAgent = "go-eth2-client/0.23.0" // post sends an HTTP post request and returns the body. func (s *Service) post(ctx context.Context,