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
39 changes: 39 additions & 0 deletions core/fetcher/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strings"
"sync"

eth2client "github.com/attestantio/go-eth2-client"
eth2api "github.com/attestantio/go-eth2-client/api"
eth2spec "github.com/attestantio/go-eth2-client/spec"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
Expand Down Expand Up @@ -171,6 +172,11 @@ func (f *Fetcher) Fetch(ctx context.Context, duty core.Duty, defSet core.DutyDef
} else if len(unsignedSet) == 0 { // No sync committee contributors found in this slot
return nil
}
case core.DutyPayloadAttestation:
unsignedSet, err = f.fetchPayloadAttestationData(ctx, duty.Slot, defSet)
if err != nil {
return errors.Wrap(err, "fetch payload attestation data")
}
default:
return errors.New("unsupported duty type", z.Str("type", duty.Type.String()))
}
Expand Down Expand Up @@ -429,6 +435,39 @@ func (f *Fetcher) fetchProposerData(ctx context.Context, slot uint64, defSet cor
return resp, nil
}

// fetchPayloadAttestationData returns the fetched payload attestation data for the slot.
// The data is per-slot, not per-validator, so all payload timeliness committee members
// in the arg set attest to the same data.
func (f *Fetcher) fetchPayloadAttestationData(ctx context.Context, slot uint64, defSet core.DutyDefinitionSet) (core.UnsignedDataSet, error) {
opts := &eth2api.PayloadAttestationDataOpts{
Slot: eth2p0.Slot(slot),
}

eth2Resp, err := f.eth2Cl.PayloadAttestationData(ctx, opts)
if errors.Is(err, eth2client.ErrNoPayloadAttestationData) {
// The beacon node has not seen a block for the slot, so there is nothing to attest.
return nil, errors.New("no block seen for payload attestation slot", z.U64("slot", slot))
} else if err != nil {
return nil, err
}

if eth2Resp.Data == nil {
return nil, errors.New("payload attestation data is nil")
}

data, err := core.NewVersionedPayloadAttestationData(eth2Resp.Data)
if err != nil {
return nil, err
}

resp := make(core.UnsignedDataSet)
for pubkey := range defSet {
resp[pubkey] = data
}

return resp, nil
}

// fetchContributionData fetches the sync committee contribution data.
func (f *Fetcher) fetchContributionData(ctx context.Context, slot uint64, defSet core.DutyDefinitionSet) (core.UnsignedDataSet, error) {
pt := newPubkeysTracker("sync committee contribution")
Expand Down
106 changes: 106 additions & 0 deletions core/fetcher/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"

"github.com/OffchainLabs/go-bitfield"
eth2client "github.com/attestantio/go-eth2-client"
eth2api "github.com/attestantio/go-eth2-client/api"
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
eth2spec "github.com/attestantio/go-eth2-client/spec"
Expand Down Expand Up @@ -843,6 +844,111 @@ func assertRandao(t *testing.T, randao eth2p0.BLSSignature, block core.Versioned
}
}

func TestFetchPayloadAttestation(t *testing.T) {
const slot = 1

pubkeyA := testutil.RandomCorePubKey(t)
pubkeyB := testutil.RandomCorePubKey(t)

defSet := core.DutyDefinitionSet{
pubkeyA: core.NewPTCDefinition(&eth2v1.PTCDuty{Slot: slot, ValidatorIndex: 2}),
pubkeyB: core.NewPTCDefinition(&eth2v1.PTCDuty{Slot: slot, ValidatorIndex: 3}),
}
duty := core.NewPayloadAttestationDuty(slot)

bmock, err := beaconmock.New(t.Context())
require.NoError(t, err)

attData := testutil.RandomPayloadAttestationData()
attData.Slot = slot

bmock.PayloadAttestationDataFunc = func(context.Context, eth2p0.Slot) (*eth2spec.VersionedPayloadAttestationData, error) {
return &eth2spec.VersionedPayloadAttestationData{
Version: eth2spec.DataVersionGloas,
Gloas: attData,
}, nil
}

fetch := mustCreateFetcher(t, bmock)

var subCalled bool

fetch.Subscribe(func(_ context.Context, resDuty core.Duty, resDataSet core.UnsignedDataSet) error {
subCalled = true

require.Equal(t, duty, resDuty)
require.Len(t, resDataSet, 2)

// All committee members attest to the same per-slot data.
for _, pubkey := range []core.PubKey{pubkeyA, pubkeyB} {
data, ok := resDataSet[pubkey].(core.VersionedPayloadAttestationData)
require.True(t, ok)
require.Equal(t, eth2spec.DataVersionGloas, data.Version)
require.Equal(t, attData, data.Gloas)
}

return nil
})

require.NoError(t, fetch.Fetch(t.Context(), duty, defSet))
require.True(t, subCalled)
}

func TestFetchPayloadAttestationError(t *testing.T) {
const slot = 1

defSet := core.DutyDefinitionSet{
testutil.RandomCorePubKey(t): core.NewPTCDefinition(&eth2v1.PTCDuty{Slot: slot, ValidatorIndex: 2}),
}
duty := core.NewPayloadAttestationDuty(slot)

tests := []struct {
name string
dataFunc func(context.Context, eth2p0.Slot) (*eth2spec.VersionedPayloadAttestationData, error)
errContains string
}{
{
name: "no block seen for slot",
dataFunc: func(context.Context, eth2p0.Slot) (*eth2spec.VersionedPayloadAttestationData, error) {
return nil, eth2client.ErrNoPayloadAttestationData
},
errContains: "no block seen for payload attestation slot",
},
{
name: "nil data",
dataFunc: func(context.Context, eth2p0.Slot) (*eth2spec.VersionedPayloadAttestationData, error) {
return nil, nil //nolint:nilnil // Mimics a beacon node responding 200 with an empty body.
},
errContains: "payload attestation data is nil",
},
{
name: "invalid version",
dataFunc: func(context.Context, eth2p0.Slot) (*eth2spec.VersionedPayloadAttestationData, error) {
return &eth2spec.VersionedPayloadAttestationData{Version: eth2spec.DataVersionElectra}, nil
},
errContains: "unknown version",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
bmock, err := beaconmock.New(t.Context())
require.NoError(t, err)

bmock.PayloadAttestationDataFunc = test.dataFunc

fetch := mustCreateFetcher(t, bmock)
fetch.Subscribe(func(context.Context, core.Duty, core.UnsignedDataSet) error {
require.Fail(t, "unexpected subscriber call")
return nil
})

err = fetch.Fetch(t.Context(), duty, defSet)
require.ErrorContains(t, err, test.errContains)
})
}
}

// blsSigFromHex returns the BLS signature from the input hex signature.
func blsSigFromHex(t *testing.T, sig string) eth2p0.BLSSignature {
t.Helper()
Expand Down
Loading