Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 16 additions & 28 deletions api/v1/attestationrewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ type AttestationRewards struct {
}

// IdealAttestationRewards are the ideal attestation rewards for an attestation.
// Head, target, source and inactivity are signed Gwei values, as in the beacon API spec.
type IdealAttestationRewards struct {
EffectiveBalance phase0.Gwei
Head phase0.Gwei
Target phase0.Gwei
Source phase0.Gwei
Head int64
Target int64
Source int64
InclusionDelay *phase0.Gwei
Inactivity phase0.Gwei
Inactivity int64
}

// idealAttestationRewardsJSON is the spec representation of the struct.
Expand Down Expand Up @@ -89,35 +90,29 @@ func (i *IdealAttestationRewards) UnmarshalJSON(input []byte) error {
return errors.New("head missing")
}

head, err := strconv.ParseUint(data.Head, 10, 64)
i.Head, err = strconv.ParseInt(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)
i.Target, err = strconv.ParseInt(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)
i.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 {
Expand All @@ -132,13 +127,11 @@ func (i *IdealAttestationRewards) UnmarshalJSON(input []byte) error {
return errors.New("inactivity missing")
}

inactivity, err := strconv.ParseUint(data.Inactivity, 10, 64)
i.Inactivity, err = strconv.ParseInt(data.Inactivity, 10, 64)
if err != nil {
return errors.Wrap(err, "invalid value for inactivity")
}

i.Inactivity = phase0.Gwei(inactivity)

return nil
}

Expand All @@ -152,16 +145,15 @@ func (i *IdealAttestationRewards) String() string {
return string(data)
}

// ValidatorAttestationRewards are the ideal attestation rewards for a validator.
// ValidatorAttestationRewards are the attestation rewards for a validator.
// Head, target, source and inactivity are signed Gwei values, as in the beacon API spec.
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).
Head int64
Target int64
Source int64
InclusionDelay *phase0.Gwei
Inactivity phase0.Gwei
Inactivity int64
}

// validatorAttestationRewardsJSON is the spec representation of the struct.
Expand Down Expand Up @@ -215,13 +207,11 @@ func (v *ValidatorAttestationRewards) UnmarshalJSON(input []byte) error {
return errors.New("head missing")
}

head, err := strconv.ParseUint(data.Head, 10, 64)
v.Head, err = strconv.ParseInt(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")
}
Expand Down Expand Up @@ -254,13 +244,11 @@ func (v *ValidatorAttestationRewards) UnmarshalJSON(input []byte) error {
return errors.New("inactivity missing")
}

inactivity, err := strconv.ParseUint(data.Inactivity, 10, 64)
v.Inactivity, err = strconv.ParseInt(data.Inactivity, 10, 64)
if err != nil {
return errors.Wrap(err, "invalid value for inactivity")
}

v.Inactivity = phase0.Gwei(inactivity)

return nil
}

Expand Down
133 changes: 133 additions & 0 deletions api/v1/attestationrewards_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright © 2026 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_test

import (
"encoding/json"
"testing"

api "github.com/ethpandaops/go-eth2-client/api/v1"
"github.com/stretchr/testify/assert"
require "github.com/stretchr/testify/require"
)

func TestValidatorAttestationRewardsJSON(t *testing.T) {
tests := []struct {
name string
input []byte
head int64
target int64
source int64
inactivity int64
err string
}{
{
// Mainnet epoch 200759, during the May 2023 inactivity leak.
name: "InactivityLeakPenalty",
input: []byte(`{"validator_index":"9","head":"0","target":"-6162","source":"-3318","inactivity":"-5722"}`),
target: -6162,
source: -3318,
inactivity: -5722,
},
{
name: "Rewarded",
input: []byte(`{"validator_index":"2","head":"1825","target":"5034","source":"2543","inactivity":"0"}`),
head: 1825,
target: 5034,
source: 2543,
},
{
name: "HeadPenalty",
input: []byte(`{"validator_index":"2","head":"-1825","target":"5034","source":"2543","inactivity":"0"}`),
head: -1825,
target: 5034,
source: 2543,
},
{
name: "InactivityInvalid",
input: []byte(`{"validator_index":"2","head":"0","target":"0","source":"0","inactivity":"x"}`),
err: "invalid value for inactivity: strconv.ParseInt: parsing \"x\": invalid syntax",
},
{
name: "InactivityMissing",
input: []byte(`{"validator_index":"2","head":"0","target":"0","source":"0"}`),
err: "inactivity missing",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var res api.ValidatorAttestationRewards
err := json.Unmarshal(test.input, &res)
if test.err != "" {
require.EqualError(t, err, test.err)

return
}

require.NoError(t, err)
assert.Equal(t, test.head, res.Head)
assert.Equal(t, test.target, res.Target)
assert.Equal(t, test.source, res.Source)
assert.Equal(t, test.inactivity, res.Inactivity)

rt, err := json.Marshal(&res)
require.NoError(t, err)
assert.JSONEq(t, string(test.input), string(rt))
})
}
}

func TestIdealAttestationRewardsJSON(t *testing.T) {
tests := []struct {
name string
input []byte
inactivity int64
err string
}{
{
name: "Good",
input: []byte(`{"effective_balance":"32000000000","head":"1825","target":"5034","source":"2543","inactivity":"0"}`),
},
{
name: "InactivityPenalty",
input: []byte(`{"effective_balance":"32000000000","head":"1825","target":"5034","source":"2543","inactivity":"-4768"}`),
inactivity: -4768,
},
{
name: "EffectiveBalanceNegative",
input: []byte(`{"effective_balance":"-1","head":"1825","target":"5034","source":"2543","inactivity":"0"}`),
err: "invalid value for effective balance: strconv.ParseUint: parsing \"-1\": invalid syntax",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var res api.IdealAttestationRewards
err := json.Unmarshal(test.input, &res)
if test.err != "" {
require.EqualError(t, err, test.err)

return
}

require.NoError(t, err)
assert.Equal(t, test.inactivity, res.Inactivity)

rt, err := json.Marshal(&res)
require.NoError(t, err)
assert.JSONEq(t, string(test.input), string(rt))
})
}
}
8 changes: 4 additions & 4 deletions spec/versionedattesterslashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (v *VersionedAttesterSlashing) Attestation1() (*VersionedIndexedAttestation
}

versionedIndexedAttestation := VersionedIndexedAttestation{
Version: DataVersionPhase0,
Version: DataVersionAltair,
Altair: v.Altair.Attestation1,
}

Expand All @@ -72,7 +72,7 @@ func (v *VersionedAttesterSlashing) Attestation1() (*VersionedIndexedAttestation
}

versionedIndexedAttestation := VersionedIndexedAttestation{
Version: DataVersionPhase0,
Version: DataVersionBellatrix,
Bellatrix: v.Bellatrix.Attestation1,
}

Expand Down Expand Up @@ -168,7 +168,7 @@ func (v *VersionedAttesterSlashing) Attestation2() (*VersionedIndexedAttestation
}

versionedIndexedAttestation := VersionedIndexedAttestation{
Version: DataVersionPhase0,
Version: DataVersionAltair,
Altair: v.Altair.Attestation2,
}

Expand All @@ -179,7 +179,7 @@ func (v *VersionedAttesterSlashing) Attestation2() (*VersionedIndexedAttestation
}

versionedIndexedAttestation := VersionedIndexedAttestation{
Version: DataVersionPhase0,
Version: DataVersionBellatrix,
Bellatrix: v.Bellatrix.Attestation2,
}

Expand Down
91 changes: 91 additions & 0 deletions spec/versionedattesterslashing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright © 2026 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package spec_test

import (
"testing"

"github.com/ethpandaops/go-eth2-client/spec"
"github.com/ethpandaops/go-eth2-client/spec/electra"
"github.com/ethpandaops/go-eth2-client/spec/gloas"
"github.com/ethpandaops/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestVersionedAttesterSlashingAttestations(t *testing.T) {
data := func(slot phase0.Slot) *phase0.AttestationData {
return &phase0.AttestationData{
Slot: slot,
Source: &phase0.Checkpoint{},
Target: &phase0.Checkpoint{},
}
}

phase0Slashing := &phase0.AttesterSlashing{
Attestation1: &phase0.IndexedAttestation{AttestingIndices: []uint64{1, 2}, Data: data(10)},
Attestation2: &phase0.IndexedAttestation{AttestingIndices: []uint64{2, 3}, Data: data(11)},
}
electraSlashing := &electra.AttesterSlashing{
Attestation1: &electra.IndexedAttestation{AttestingIndices: []uint64{1, 2}, Data: data(10)},
Attestation2: &electra.IndexedAttestation{AttestingIndices: []uint64{2, 3}, Data: data(11)},
}
gloasSlashing := &gloas.AttesterSlashing{
Attestation1: &gloas.IndexedAttestation{AttestingIndices: []uint64{1, 2}, Data: data(10)},
Attestation2: &gloas.IndexedAttestation{AttestingIndices: []uint64{2, 3}, Data: data(11)},
}

tests := []spec.VersionedAttesterSlashing{
{Version: spec.DataVersionPhase0, Phase0: phase0Slashing},
{Version: spec.DataVersionAltair, Altair: phase0Slashing},
{Version: spec.DataVersionBellatrix, Bellatrix: phase0Slashing},
{Version: spec.DataVersionCapella, Capella: phase0Slashing},
{Version: spec.DataVersionDeneb, Deneb: phase0Slashing},
{Version: spec.DataVersionElectra, Electra: electraSlashing},
{Version: spec.DataVersionFulu, Fulu: electraSlashing},
{Version: spec.DataVersionGloas, Gloas: gloasSlashing},
{Version: spec.DataVersionHeze, Heze: gloasSlashing},
}

for _, slashing := range tests {
t.Run(slashing.Version.String(), func(t *testing.T) {
att1, err := slashing.Attestation1()
require.NoError(t, err)
assert.Equal(t, slashing.Version, att1.Version)

indices, err := att1.AttestingIndices()
require.NoError(t, err)
assert.Equal(t, []uint64{1, 2}, indices)

attData, err := att1.Data()
require.NoError(t, err)
assert.Equal(t, phase0.Slot(10), attData.Slot)

_, err = att1.Signature()
require.NoError(t, err)

att2, err := slashing.Attestation2()
require.NoError(t, err)
assert.Equal(t, slashing.Version, att2.Version)

indices, err = att2.AttestingIndices()
require.NoError(t, err)
assert.Equal(t, []uint64{2, 3}, indices)

attData, err = att2.Data()
require.NoError(t, err)
assert.Equal(t, phase0.Slot(11), attData.Slot)
})
}
}
Loading