diff --git a/app/eth2wrap/eth2wrap_gen.go b/app/eth2wrap/eth2wrap_gen.go index 7c75adefe..00173ac44 100644 --- a/app/eth2wrap/eth2wrap_gen.go +++ b/app/eth2wrap/eth2wrap_gen.go @@ -40,6 +40,7 @@ type Client interface { eth2client.AttesterDutiesProvider eth2client.BeaconBlockProposalProvider eth2client.BeaconBlockSubmitter + eth2client.BeaconCommitteeSubscriptionsSubmitter eth2client.BeaconCommitteesProvider eth2client.BlindedBeaconBlockProposalProvider eth2client.BlindedBeaconBlockSubmitter @@ -280,6 +281,25 @@ func (m multi) SubmitBeaconBlock(ctx context.Context, block *spec.VersionedSigne return err } +// SubmitBeaconCommitteeSubscriptions subscribes to beacon committees. +func (m multi) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*apiv1.BeaconCommitteeSubscription) error { + const label = "submit_beacon_committee_subscriptions" + defer latency(label)() + + err := submit(ctx, m.clients, + func(ctx context.Context, cl Client) error { + return cl.SubmitBeaconCommitteeSubscriptions(ctx, subscriptions) + }, + ) + + if err != nil { + incError(label) + err = errors.Wrap(err, "eth2wrap") + } + + return err +} + // BlindedBeaconBlockProposal fetches a blinded proposed beacon block for signing. func (m multi) BlindedBeaconBlockProposal(ctx context.Context, slot phase0.Slot, randaoReveal phase0.BLSSignature, graffiti []byte) (*api.VersionedBlindedBeaconBlock, error) { const label = "blinded_beacon_block_proposal" diff --git a/app/eth2wrap/genwrap/genwrap.go b/app/eth2wrap/genwrap/genwrap.go index 3db095f8a..944a77f6a 100644 --- a/app/eth2wrap/genwrap/genwrap.go +++ b/app/eth2wrap/genwrap/genwrap.go @@ -89,30 +89,31 @@ type Client interface { // interfaces defines all the interfaces to implement and whether to measure latency for each. interfaces = map[string]bool{ - "AttestationDataProvider": true, - "AttestationsSubmitter": true, - "AttesterDutiesProvider": true, - "BeaconBlockProposalProvider": true, - "BeaconBlockSubmitter": true, - "BeaconCommitteesProvider": true, - "BlindedBeaconBlockProposalProvider": true, - "BlindedBeaconBlockSubmitter": true, - "DepositContractProvider": false, - "DomainProvider": false, - "EventsProvider": true, - "ForkProvider": true, - "ForkScheduleProvider": true, - "GenesisProvider": false, - "GenesisTimeProvider": false, - "NodeSyncingProvider": true, - "NodeVersionProvider": false, - "ProposerDutiesProvider": true, - "SlotDurationProvider": false, - "SlotsPerEpochProvider": false, - "SpecProvider": false, - "ValidatorsProvider": true, - "ValidatorRegistrationsSubmitter": true, - "VoluntaryExitSubmitter": true, + "AttestationDataProvider": true, + "AttestationsSubmitter": true, + "AttesterDutiesProvider": true, + "BeaconBlockProposalProvider": true, + "BeaconBlockSubmitter": true, + "BeaconCommitteesProvider": true, + "BeaconCommitteeSubscriptionsSubmitter": true, + "BlindedBeaconBlockProposalProvider": true, + "BlindedBeaconBlockSubmitter": true, + "DepositContractProvider": false, + "DomainProvider": false, + "EventsProvider": true, + "ForkProvider": true, + "ForkScheduleProvider": true, + "GenesisProvider": false, + "GenesisTimeProvider": false, + "NodeSyncingProvider": true, + "NodeVersionProvider": false, + "ProposerDutiesProvider": true, + "SlotDurationProvider": false, + "SlotsPerEpochProvider": false, + "SpecProvider": false, + "ValidatorsProvider": true, + "ValidatorRegistrationsSubmitter": true, + "VoluntaryExitSubmitter": true, } // addImport indicates which types need hardcoded imports. diff --git a/core/bcast/bcast.go b/core/bcast/bcast.go index 73bff635a..c56db212f 100644 --- a/core/bcast/bcast.go +++ b/core/bcast/bcast.go @@ -23,6 +23,7 @@ import ( "time" eth2api "github.com/attestantio/go-eth2-client/api" + eth2v1 "github.com/attestantio/go-eth2-client/api/v1" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/obolnetwork/charon/app/errors" @@ -144,12 +145,35 @@ func (b Broadcaster) Broadcast(ctx context.Context, duty core.Duty, pubkey core. // Randao is an internal duty, not broadcasted to beacon chain return nil case core.DutyPrepareAggregator: - subscription, ok := aggData.(core.SignedBeaconCommitteeSubscription) + sub, ok := aggData.(core.SignedBeaconCommitteeSubscription) if !ok { - return errors.New("invalid beacon committee subscription") + return errors.New("invalid beacon committee sub") } - _, err = b.eth2Cl.SubmitBeaconCommitteeSubscriptionsV2(ctx, []*eth2exp.BeaconCommitteeSubscription{&subscription.BeaconCommitteeSubscription}) + _, err = b.eth2Cl.SubmitBeaconCommitteeSubscriptionsV2(ctx, []*eth2exp.BeaconCommitteeSubscription{&sub.BeaconCommitteeSubscription}) + if err == nil { + return nil + } + + log.Debug(ctx, "V2 submit beacon committee subscriptions failed") + + // Beacon node doesn't support v2 SubmitBeaconCommitteeSubscriptions endpoint (yet). Try with v1. + res, err := eth2exp.CalculateCommitteeSubscriptionResponse(ctx, b.eth2Cl, &sub.BeaconCommitteeSubscription) + if err != nil { + return err + } + + subs := []*eth2v1.BeaconCommitteeSubscription{ + { + ValidatorIndex: sub.ValidatorIndex, + Slot: sub.Slot, + CommitteeIndex: sub.CommitteeIndex, + CommitteesAtSlot: sub.CommitteesAtSlot, + IsAggregator: res.IsAggregator, + }, + } + + err = b.eth2Cl.SubmitBeaconCommitteeSubscriptions(ctx, subs) if err == nil { log.Info(ctx, "Beacon committee subscription successfully submitted to beacon node", nil) } diff --git a/core/bcast/bcast_test.go b/core/bcast/bcast_test.go index d4fc25394..304b82414 100644 --- a/core/bcast/bcast_test.go +++ b/core/bcast/bcast_test.go @@ -17,6 +17,7 @@ package bcast_test import ( "context" + mrand "math/rand" "testing" eth2api "github.com/attestantio/go-eth2-client/api" @@ -28,7 +29,11 @@ import ( "github.com/obolnetwork/charon/app/errors" "github.com/obolnetwork/charon/core" "github.com/obolnetwork/charon/core/bcast" + "github.com/obolnetwork/charon/eth2util" "github.com/obolnetwork/charon/eth2util/eth2exp" + "github.com/obolnetwork/charon/eth2util/signing" + "github.com/obolnetwork/charon/tbls" + "github.com/obolnetwork/charon/tbls/tblsconv" "github.com/obolnetwork/charon/testutil" "github.com/obolnetwork/charon/testutil/beaconmock" ) @@ -176,7 +181,7 @@ func TestBroadcastExit(t *testing.T) { <-ctx.Done() } -func TestBroadcastBeaconCommitteeSubscription(t *testing.T) { +func TestBroadcastBeaconCommitteeSubscriptionV2(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) mock, err := beaconmock.New() require.NoError(t, err) @@ -184,13 +189,98 @@ func TestBroadcastBeaconCommitteeSubscription(t *testing.T) { subscription := testutil.RandomBeaconCommitteeSubscription() aggData := core.SignedBeaconCommitteeSubscription{BeaconCommitteeSubscription: *subscription} - mock.SubmitBeaconCommitteeSubscriptionsFunc = func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { + mock.SubmitBeaconCommitteeSubscriptionsV2Func = func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { require.Equal(t, aggData.BeaconCommitteeSubscription, *subscriptions[0]) cancel() return []*eth2exp.BeaconCommitteeSubscriptionResponse{}, ctx.Err() } + // To avoid further call to v1 SubmitBeaconCommitteeSubscriptions. + mock.SlotsPerEpochFunc = func(ctx context.Context) (uint64, error) { + return 0, ctx.Err() + } + + bcaster, err := bcast.New(ctx, mock) + require.NoError(t, err) + + err = bcaster.Broadcast(ctx, core.Duty{Type: core.DutyPrepareAggregator}, "", aggData) + require.ErrorIs(t, err, context.Canceled) + + <-ctx.Done() +} + +func TestBroadcastBeaconCommitteeSubscriptionV1(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + const ( + slot = 1 + vIdx = 1 + commIdx = 1 + commLen = 43 + ) + + mock, err := beaconmock.New(beaconmock.WithValidatorSet(beaconmock.ValidatorSetA)) + require.NoError(t, err) + + _, secret, err := tbls.KeygenWithSeed(mrand.New(mrand.NewSource(1))) + require.NoError(t, err) + + sigRoot, err := eth2util.SlotHashRoot(slot) + require.NoError(t, err) + + slotsPerEpoch, err := mock.SlotsPerEpoch(ctx) + require.NoError(t, err) + + sigData, err := signing.GetDataRoot(ctx, mock, signing.DomainSelectionProof, eth2p0.Epoch(uint64(1)/slotsPerEpoch), sigRoot) + require.NoError(t, err) + + sig, _ := tbls.Sign(secret, sigData[:]) + blssig := tblsconv.SigToETH2(sig) + + mock.BeaconCommitteesAtEpochFunc = func(_ context.Context, stateID string, epoch eth2p0.Epoch) ([]*eth2v1.BeaconCommittee, error) { + require.Equal(t, "head", stateID) + require.Equal(t, eth2p0.Epoch(uint64(slot)/slotsPerEpoch), epoch) + + var vals []eth2p0.ValidatorIndex + for idx := 1; idx <= commLen; idx++ { + vals = append(vals, eth2p0.ValidatorIndex(idx)) + } + + return []*eth2v1.BeaconCommittee{ + { + Slot: slot, + Index: commIdx, + Validators: vals, + }, + }, nil + } + + subscription := eth2exp.BeaconCommitteeSubscription{ + ValidatorIndex: vIdx, + Slot: slot, + CommitteeIndex: commIdx, + SlotSignature: blssig, + } + aggData := core.SignedBeaconCommitteeSubscription{BeaconCommitteeSubscription: subscription} + + mock.SubmitBeaconCommitteeSubscriptionsV2Func = func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { + require.Equal(t, aggData.BeaconCommitteeSubscription, *subscriptions[0]) + + return nil, errors.New("404 not found") + } + mock.SubmitBeaconCommitteeSubscriptionsFunc = func(ctx context.Context, subscriptions []*eth2v1.BeaconCommitteeSubscription) error { + require.Equal(t, eth2v1.BeaconCommitteeSubscription{ + ValidatorIndex: vIdx, + Slot: slot, + CommitteeIndex: commIdx, + IsAggregator: true, + }, *subscriptions[0]) + cancel() + + return ctx.Err() + } + bcaster, err := bcast.New(ctx, mock) require.NoError(t, err) diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index 3933cbfe9..b5073fe83 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -567,7 +567,7 @@ func TestBeaconCommitteeSubscriptionsV2(t *testing.T) { bmock, err := beaconmock.New(beaconmock.WithAttestationAggregation(aggregators)) require.NoError(t, err) - testHandler := testHandler{SubmitBeaconCommitteeSubscriptionsV2Func: bmock.SubmitBeaconCommitteeSubscriptionsFunc} + testHandler := testHandler{SubmitBeaconCommitteeSubscriptionsV2Func: bmock.SubmitBeaconCommitteeSubscriptionsV2Func} proxy := httptest.NewServer(testHandler.newBeaconHandler(t)) defer proxy.Close() diff --git a/testutil/beaconmock/beaconmock.go b/testutil/beaconmock/beaconmock.go index 44baaa0cb..62f104834 100644 --- a/testutil/beaconmock/beaconmock.go +++ b/testutil/beaconmock/beaconmock.go @@ -121,25 +121,26 @@ type Mock struct { overrides []staticOverride clock clockwork.Clock - AttestationDataFunc func(context.Context, eth2p0.Slot, eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error) - AttesterDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) - BlindedBeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*eth2api.VersionedBlindedBeaconBlock, error) - BeaconCommitteesFunc func(ctx context.Context, stateID string) ([]*eth2v1.BeaconCommittee, error) - BeaconCommitteesAtEpochFunc func(ctx context.Context, stateID string, epoch eth2p0.Epoch) ([]*eth2v1.BeaconCommittee, error) - BeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) - ProposerDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) - SubmitAttestationsFunc func(context.Context, []*eth2p0.Attestation) error - SubmitBeaconBlockFunc func(context.Context, *spec.VersionedSignedBeaconBlock) error - SubmitBlindedBeaconBlockFunc func(context.Context, *eth2api.VersionedSignedBlindedBeaconBlock) error - SubmitVoluntaryExitFunc func(context.Context, *eth2p0.SignedVoluntaryExit) error - ValidatorsByPubKeyFunc func(context.Context, string, []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) - ValidatorsFunc func(context.Context, string, []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) - GenesisTimeFunc func(context.Context) (time.Time, error) - NodeSyncingFunc func(context.Context) (*eth2v1.SyncState, error) - EventsFunc func(context.Context, []string, eth2client.EventHandlerFunc) error - SubmitValidatorRegistrationsFunc func(context.Context, []*eth2api.VersionedSignedValidatorRegistration) error - SlotsPerEpochFunc func(context.Context) (uint64, error) - SubmitBeaconCommitteeSubscriptionsFunc func(context.Context, []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) + AttestationDataFunc func(context.Context, eth2p0.Slot, eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error) + AttesterDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) + BlindedBeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*eth2api.VersionedBlindedBeaconBlock, error) + BeaconCommitteesFunc func(ctx context.Context, stateID string) ([]*eth2v1.BeaconCommittee, error) + BeaconCommitteesAtEpochFunc func(ctx context.Context, stateID string, epoch eth2p0.Epoch) ([]*eth2v1.BeaconCommittee, error) + BeaconBlockProposalFunc func(ctx context.Context, slot eth2p0.Slot, randaoReveal eth2p0.BLSSignature, graffiti []byte) (*spec.VersionedBeaconBlock, error) + ProposerDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) + SubmitAttestationsFunc func(context.Context, []*eth2p0.Attestation) error + SubmitBeaconBlockFunc func(context.Context, *spec.VersionedSignedBeaconBlock) error + SubmitBlindedBeaconBlockFunc func(context.Context, *eth2api.VersionedSignedBlindedBeaconBlock) error + SubmitVoluntaryExitFunc func(context.Context, *eth2p0.SignedVoluntaryExit) error + ValidatorsByPubKeyFunc func(context.Context, string, []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) + ValidatorsFunc func(context.Context, string, []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) + GenesisTimeFunc func(context.Context) (time.Time, error) + NodeSyncingFunc func(context.Context) (*eth2v1.SyncState, error) + EventsFunc func(context.Context, []string, eth2client.EventHandlerFunc) error + SubmitValidatorRegistrationsFunc func(context.Context, []*eth2api.VersionedSignedValidatorRegistration) error + SlotsPerEpochFunc func(context.Context) (uint64, error) + SubmitBeaconCommitteeSubscriptionsV2Func func(context.Context, []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) + SubmitBeaconCommitteeSubscriptionsFunc func(ctx context.Context, subscriptions []*eth2v1.BeaconCommitteeSubscription) error } func (m Mock) SubmitAttestations(ctx context.Context, attestations []*eth2p0.Attestation) error { @@ -211,6 +212,10 @@ func (m Mock) SubmitValidatorRegistrations(ctx context.Context, registrations [] } func (m Mock) SubmitBeaconCommitteeSubscriptionsV2(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { + return m.SubmitBeaconCommitteeSubscriptionsV2Func(ctx, subscriptions) +} + +func (m Mock) SubmitBeaconCommitteeSubscriptions(ctx context.Context, subscriptions []*eth2v1.BeaconCommitteeSubscription) error { return m.SubmitBeaconCommitteeSubscriptionsFunc(ctx, subscriptions) } diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go index 3001d8a15..141f17f43 100644 --- a/testutil/beaconmock/options.go +++ b/testutil/beaconmock/options.go @@ -337,10 +337,10 @@ func WithClock(clock clockwork.Clock) Option { } } -// WithAttestationAggregation configures the mock to override SubmitBeaconCommitteeSubscriptionsFunc. +// WithAttestationAggregation configures the mock to override SubmitBeaconCommitteeSubscriptionsV2Func. func WithAttestationAggregation(aggregators map[eth2p0.Slot]eth2p0.ValidatorIndex) Option { return func(mock *Mock) { - mock.SubmitBeaconCommitteeSubscriptionsFunc = func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { + mock.SubmitBeaconCommitteeSubscriptionsV2Func = func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) { var resp []*eth2exp.BeaconCommitteeSubscriptionResponse for _, sub := range subscriptions { resp = append(resp, ð2exp.BeaconCommitteeSubscriptionResponse{