diff --git a/core/interfaces.go b/core/interfaces.go index 9a452d118..d5a173cdd 100644 --- a/core/interfaces.go +++ b/core/interfaces.go @@ -73,6 +73,9 @@ type Consensus interface { // ValidatorAPI provides a beacon node API to validator clients. It serves duty data from the // DutyDB and stores partial signed data in the ParSigDB. type ValidatorAPI interface { + // RegisterAwaitBeaconBlock registers a function to query a unsigned beacon block by slot. + RegisterAwaitBeaconBlock(func(ctx context.Context, slot int64) (PubKey, *spec.VersionedBeaconBlock, error)) + // RegisterAwaitAttestation registers a function to query attestation data. RegisterAwaitAttestation(func(ctx context.Context, slot, commIdx int64) (*eth2p0.AttestationData, error)) diff --git a/core/validatorapi/eth2types.go b/core/validatorapi/eth2types.go index 20004f0a1..5200c22b7 100644 --- a/core/validatorapi/eth2types.go +++ b/core/validatorapi/eth2types.go @@ -22,6 +22,9 @@ import ( "strconv" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/bellatrix" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/obolnetwork/charon/app/errors" @@ -81,6 +84,21 @@ type proposerDutiesResponse struct { Data []*eth2v1.ProposerDuty `json:"data"` } +type proposeBlockResponsePhase0 struct { + Version spec.DataVersion `json:"version"` + Data *eth2p0.BeaconBlock `json:"data"` +} + +type proposeBlockResponseAltair struct { + Version spec.DataVersion `json:"version"` + Data *altair.BeaconBlock `json:"data"` +} + +type proposeBlockResponseBellatrix struct { + Version spec.DataVersion `json:"version"` + Data *bellatrix.BeaconBlock `json:"data"` +} + type validatorsResponse struct { Data []v1Validator `json:"data"` } diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go index e5a06da8e..66e9eb0b7 100644 --- a/core/validatorapi/router.go +++ b/core/validatorapi/router.go @@ -35,6 +35,7 @@ import ( eth2client "github.com/attestantio/go-eth2-client" eth2v1 "github.com/attestantio/go-eth2-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/gorilla/mux" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -99,9 +100,9 @@ func NewRouter(h Handler, beaconNodeAddr string) (*mux.Router, error) { Handler: getValidator(h), }, { - Name: "submit_randao", + Name: "propose_block", Path: "/eth/v2/validator/blocks/{slot}", - Handler: submitRandao(h), + Handler: proposeBlock(h), }, // TODO(corver): Add more endpoints } @@ -319,9 +320,8 @@ func attesterDuties(p eth2client.AttesterDutiesProvider) handlerFunc { } } -// submitRandao receives the randao from the validator. -// TODO(leo): rename to proposeBlock when adding dutyProposer support. -func submitRandao(p eth2client.BeaconBlockProposalProvider) handlerFunc { +// proposeBlock receives the randao from the validator and returns the unsigned BeaconBlock. +func proposeBlock(p eth2client.BeaconBlockProposalProvider) handlerFunc { return func(ctx context.Context, params map[string]string, query url.Values, body []byte) (interface{}, error) { slot, err := uintParam(params, "slot") if err != nil { @@ -347,12 +347,42 @@ func submitRandao(p eth2client.BeaconBlockProposalProvider) handlerFunc { return nil, errors.New("above graffiti length max of 32") } - _, err = p.BeaconBlockProposal(ctx, eth2p0.Slot(slot), randao, graffiti) + block, err := p.BeaconBlockProposal(ctx, eth2p0.Slot(slot), randao, graffiti) if err != nil { return nil, err } - return nil, errors.New("duty proposer not implemented yet") + switch block.Version { + case spec.DataVersionPhase0: + if block.Phase0 == nil { + return 0, errors.New("no phase0 block") + } + + return proposeBlockResponsePhase0{ + Version: block.Version, + Data: block.Phase0, + }, nil + case spec.DataVersionAltair: + if block.Altair == nil { + return 0, errors.New("no altair block") + } + + return proposeBlockResponseAltair{ + Version: block.Version, + Data: block.Altair, + }, nil + case spec.DataVersionBellatrix: + if block.Bellatrix == nil { + return 0, errors.New("no bellatrix block") + } + + return proposeBlockResponseBellatrix{ + Version: block.Version, + Data: block.Bellatrix, + }, nil + default: + return 0, errors.New("invalid block") + } } } diff --git a/core/validatorapi/validatorapi_test.go b/core/validatorapi/validatorapi_test.go index 3563a00f6..0730f7956 100644 --- a/core/validatorapi/validatorapi_test.go +++ b/core/validatorapi/validatorapi_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/attestantio/go-eth2-client/mock" + "github.com/attestantio/go-eth2-client/spec" eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/coinbase/kryptology/pkg/signatures/bls/bls_sig" "github.com/prysmaticlabs/go-bitfield" @@ -313,3 +314,59 @@ func padTo(b []byte, size int) []byte { return append(b, make([]byte, size-len(b))...) } + +func TestComponent_BeaconBlockProposal(t *testing.T) { + ctx := context.Background() + eth2Svc, err := mock.New(ctx) + require.NoError(t, err) + + const ( + slot = 123 + vIdx = 1 + ) + + component, err := validatorapi.NewComponentInsecure(eth2Svc, vIdx) + require.NoError(t, err) + + pk, secret, err := tbls.Keygen() + require.NoError(t, err) + + msg := []byte("randao reveal") + sig, err := tbls.Sign(secret, msg) + require.NoError(t, err) + + randao := tblsconv.SigToETH2(sig) + pubkey, err := tblsconv.KeyToCore(pk) + require.NoError(t, err) + + block1 := &spec.VersionedBeaconBlock{ + Version: spec.DataVersionPhase0, + Phase0: testutil.RandomBeaconBlock(), + } + block1.Phase0.Slot = slot + block1.Phase0.ProposerIndex = vIdx + block1.Phase0.Body.RANDAOReveal = randao + + // TODO(dhruv): Will be replaced by RegisterGetDutyFunc from scheduler + component.RegisterAwaitProposer(func(ctx context.Context, slot int64) (core.PubKey, error) { + return pubkey, nil + }) + + component.RegisterAwaitBeaconBlock(func(ctx context.Context, slot int64) (core.PubKey, *spec.VersionedBeaconBlock, error) { + return pubkey, block1, nil + }) + + component.RegisterParSigDB(func(ctx context.Context, duty core.Duty, set core.ParSignedDataSet) error { + randaoEncoded := core.EncodeRandaoParSignedData(randao, vIdx) + require.Equal(t, set, core.ParSignedDataSet{ + pubkey: randaoEncoded, + }) + require.Equal(t, duty, core.NewRandaoDuty(slot)) + + return nil + }) + + block2, err := component.BeaconBlockProposal(ctx, slot, randao, []byte{}) + require.NoError(t, err) + require.Equal(t, block1, block2) +}