From 2f6ccbedde6f110f599885c926f6b8197f7a256f Mon Sep 17 00:00:00 2001 From: avalonche Date: Wed, 2 Aug 2023 12:52:54 +1000 Subject: [PATCH] Add custom json marshalling for versioned structs --- beaconclient/mock_beacon_instance.go | 3 +- beaconclient/mock_multi_beacon_client.go | 4 +- beaconclient/multi_beacon_client.go | 14 +- beaconclient/prod_beacon_instance.go | 7 +- common/ssz_test.go | 29 +++- common/test_utils.go | 24 +-- common/types_spec.go | 151 ++++++++++++++++-- common/types_spec_test.go | 60 +++++++ common/utils.go | 30 ++-- database/database.go | 21 ++- database/database_test.go | 10 +- database/mockdb.go | 11 +- database/typesconv.go | 3 +- datastore/memcached_test.go | 69 ++++---- datastore/redis.go | 2 +- go.mod | 4 +- go.sum | 8 +- services/api/service.go | 41 ++--- services/api/service_test.go | 52 +++--- services/api/utils.go | 10 +- testdata/signedBeaconBlock_Goerli.json.gz | Bin 0 -> 38859 bytes .../signedBlindedBeaconBlock_Goerli.json.gz | Bin 0 -> 13765 bytes 22 files changed, 380 insertions(+), 173 deletions(-) create mode 100644 common/types_spec_test.go create mode 100644 testdata/signedBeaconBlock_Goerli.json.gz create mode 100644 testdata/signedBlindedBeaconBlock_Goerli.json.gz diff --git a/beaconclient/mock_beacon_instance.go b/beaconclient/mock_beacon_instance.go index 937c1c15..0e87b178 100644 --- a/beaconclient/mock_beacon_instance.go +++ b/beaconclient/mock_beacon_instance.go @@ -4,7 +4,6 @@ import ( "sync" "time" - "github.com/attestantio/go-eth2-client/spec" "github.com/flashbots/mev-boost-relay/common" ) @@ -107,7 +106,7 @@ func (c *MockBeaconInstance) addDelay() { } } -func (c *MockBeaconInstance) PublishBlock(block *spec.VersionedSignedBeaconBlock, broadcastValidation BroadcastValidation) (code int, err error) { +func (c *MockBeaconInstance) PublishBlock(block *common.VersionedSignedBlockRequest, broadcastValidation BroadcastValidation) (code int, err error) { return 0, nil } diff --git a/beaconclient/mock_multi_beacon_client.go b/beaconclient/mock_multi_beacon_client.go index e5935b5e..da3bed4f 100644 --- a/beaconclient/mock_multi_beacon_client.go +++ b/beaconclient/mock_multi_beacon_client.go @@ -1,8 +1,8 @@ package beaconclient import ( - "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/flashbots/mev-boost-relay/common" ) type MockMultiBeaconClient struct{} @@ -28,7 +28,7 @@ func (*MockMultiBeaconClient) GetProposerDuties(epoch uint64) (*ProposerDutiesRe return nil, nil } -func (*MockMultiBeaconClient) PublishBlock(block *spec.VersionedSignedBeaconBlock) (code int, err error) { +func (*MockMultiBeaconClient) PublishBlock(block *common.VersionedSignedBlockRequest) (code int, err error) { return 0, nil } diff --git a/beaconclient/multi_beacon_client.go b/beaconclient/multi_beacon_client.go index 04ba81fd..c4b2b464 100644 --- a/beaconclient/multi_beacon_client.go +++ b/beaconclient/multi_beacon_client.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - "github.com/attestantio/go-eth2-client/spec" + "github.com/flashbots/mev-boost-relay/common" "github.com/sirupsen/logrus" uberatomic "go.uber.org/atomic" ) @@ -29,7 +29,11 @@ const ( ) func (b BroadcastValidation) String() string { - return [...]string{"gossip", "consensus", "consensus_and_equivocation"}[b] + broadcastValidationStrings := [...]string{"gossip", "consensus", "consensus_and_equivocation"} + if int(b) >= len(broadcastValidationStrings) { + return "invalid broadcast validation value" + } + return broadcastValidationStrings[b] } // IMultiBeaconClient is the interface for the MultiBeaconClient, which can manage several beacon client instances under the hood @@ -42,7 +46,7 @@ type IMultiBeaconClient interface { // GetStateValidators returns all active and pending validators from the beacon node GetStateValidators(stateID string) (*GetStateValidatorsResponse, error) GetProposerDuties(epoch uint64) (*ProposerDutiesResponse, error) - PublishBlock(block *spec.VersionedSignedBeaconBlock) (code int, err error) + PublishBlock(block *common.VersionedSignedBlockRequest) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) @@ -60,7 +64,7 @@ type IBeaconInstance interface { GetStateValidators(stateID string) (*GetStateValidatorsResponse, error) GetProposerDuties(epoch uint64) (*ProposerDutiesResponse, error) GetURI() string - PublishBlock(block *spec.VersionedSignedBeaconBlock, broadcastValidation BroadcastValidation) (code int, err error) + PublishBlock(block *common.VersionedSignedBlockRequest, broadcastValidation BroadcastValidation) (code int, err error) GetGenesis() (*GetGenesisResponse, error) GetSpec() (spec *GetSpecResponse, err error) GetForkSchedule() (spec *GetForkScheduleResponse, err error) @@ -254,7 +258,7 @@ type publishResp struct { } // PublishBlock publishes the signed beacon block via https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/publishBlock -func (c *MultiBeaconClient) PublishBlock(block *spec.VersionedSignedBeaconBlock) (code int, err error) { +func (c *MultiBeaconClient) PublishBlock(block *common.VersionedSignedBlockRequest) (code int, err error) { slot, err := block.Slot() if err != nil { c.log.WithError(err).Warn("failed to publish block as block slot is missing") diff --git a/beaconclient/prod_beacon_instance.go b/beaconclient/prod_beacon_instance.go index 86898154..7ab306b7 100644 --- a/beaconclient/prod_beacon_instance.go +++ b/beaconclient/prod_beacon_instance.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/flashbots/mev-boost-relay/common" "github.com/r3labs/sse/v2" @@ -256,11 +255,11 @@ func (c *ProdBeaconInstance) GetURI() string { return c.beaconURI } -func (c *ProdBeaconInstance) PublishBlock(block *spec.VersionedSignedBeaconBlock, broadcastValidation BroadcastValidation) (code int, err error) { +func (c *ProdBeaconInstance) PublishBlock(block *common.VersionedSignedBlockRequest, broadcastValidation BroadcastValidation) (code int, err error) { uri := fmt.Sprintf("%s/eth/v2/beacon/blocks?broadcast_validation=%s", c.beaconURI, broadcastValidation.String()) headers := http.Header{} - headers.Add("Eth-Consensus-Version", common.ForkVersionStringCapella) - return fetchBeacon(http.MethodPost, uri, block.Capella, nil, nil, headers) + headers.Add("Eth-Consensus-Version", block.Version.String()) + return fetchBeacon(http.MethodPost, uri, block, nil, nil, headers) } type GetGenesisResponse struct { diff --git a/common/ssz_test.go b/common/ssz_test.go index 790b57e6..3726e763 100644 --- a/common/ssz_test.go +++ b/common/ssz_test.go @@ -1,6 +1,7 @@ package common import ( + "bytes" "encoding/json" "os" "testing" @@ -12,21 +13,35 @@ import ( ) func TestSSZBuilderSubmission(t *testing.T) { - byteValue := LoadGzippedBytes(t, "../testdata/submitBlockPayloadCapella_Goerli.json.gz") + // json matches marshalled SSZ + jsonBytes := LoadGzippedBytes(t, "../testdata/submitBlockPayloadCapella_Goerli.json.gz") - depositData := new(capella.SubmitBlockRequest) - err := json.Unmarshal(byteValue, &depositData) + submitBlockData := new(VersionedSubmitBlockRequest) + err := json.Unmarshal(jsonBytes, &submitBlockData) require.NoError(t, err) - ssz, err := depositData.MarshalSSZ() + require.NotNil(t, submitBlockData.Capella) + marshalledSszBytes, err := submitBlockData.Capella.MarshalSSZ() require.NoError(t, err) - sszExpectedBytes := LoadGzippedBytes(t, "../testdata/submitBlockPayloadCapella_Goerli.ssz.gz") - require.Equal(t, sszExpectedBytes, ssz) + sszBytes := LoadGzippedBytes(t, "../testdata/submitBlockPayloadCapella_Goerli.ssz.gz") + require.Equal(t, sszBytes, marshalledSszBytes) - htr, err := depositData.HashTreeRoot() + htr, err := submitBlockData.Capella.HashTreeRoot() require.NoError(t, err) require.Equal(t, "0x014c218ba41c2ed5388e7f0ed055e109b83692c772de5c2800140a95a4b66d13", hexutil.Encode(htr[:])) + + // marshalled json matches ssz + submitBlockSSZ := new(VersionedSubmitBlockRequest) + err = submitBlockSSZ.UnmarshalSSZ(sszBytes) + require.NoError(t, err) + marshalledJSONBytes, err := json.Marshal(submitBlockSSZ) + require.NoError(t, err) + // trim white space from expected json + buffer := new(bytes.Buffer) + err = json.Compact(buffer, jsonBytes) + require.NoError(t, err) + require.Equal(t, buffer.Bytes(), bytes.ToLower(marshalledJSONBytes)) } func TestSSZGetHeaderResponse(t *testing.T) { diff --git a/common/test_utils.go b/common/test_utils.go index eabbd7d8..4d55463e 100644 --- a/common/test_utils.go +++ b/common/test_utils.go @@ -74,19 +74,21 @@ var ValidPayloadRegisterValidator = apiv1.SignedValidatorRegistration{ "0xaf12df007a0c78abb5575067e5f8b089cfcc6227e4a91db7dd8cf517fe86fb944ead859f0781277d9b78c672e4a18c5d06368b603374673cf2007966cece9540f3a1b3f6f9e1bf421d779c4e8010368e6aac134649c7a009210780d401a778a5"), } -func TestBuilderSubmitBlockRequest(sk *bls.SecretKey, bid *BidTraceV2) spec.VersionedSubmitBlockRequest { +func TestBuilderSubmitBlockRequest(sk *bls.SecretKey, bid *BidTraceV2) VersionedSubmitBlockRequest { signature, err := ssz.SignMessage(bid, ssz.DomainBuilder, sk) check(err, " SignMessage: ", bid, sk) - return spec.VersionedSubmitBlockRequest{ //nolint:exhaustruct - Version: consensusspec.DataVersionCapella, - Capella: &capella.SubmitBlockRequest{ - Message: &bid.BidTrace, - Signature: [96]byte(signature), - ExecutionPayload: &consensuscapella.ExecutionPayload{ //nolint:exhaustruct - Transactions: []bellatrix.Transaction{[]byte{0x03}}, - Timestamp: bid.Slot * 12, // 12 seconds per slot. - PrevRandao: _HexToHash("0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"), - Withdrawals: []*consensuscapella.Withdrawal{}, + return VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ //nolint:exhaustruct + Version: consensusspec.DataVersionCapella, + Capella: &capella.SubmitBlockRequest{ + Message: &bid.BidTrace, + Signature: [96]byte(signature), + ExecutionPayload: &consensuscapella.ExecutionPayload{ //nolint:exhaustruct + Transactions: []bellatrix.Transaction{[]byte{0x03}}, + Timestamp: bid.Slot * 12, // 12 seconds per slot. + PrevRandao: _HexToHash("0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"), + Withdrawals: []*consensuscapella.Withdrawal{}, + }, }, }, } diff --git a/common/types_spec.go b/common/types_spec.go index 5fb2685c..bff5a524 100644 --- a/common/types_spec.go +++ b/common/types_spec.go @@ -6,8 +6,11 @@ import ( "github.com/attestantio/go-builder-client/api" "github.com/attestantio/go-builder-client/api/capella" + "github.com/attestantio/go-builder-client/api/deneb" "github.com/attestantio/go-builder-client/spec" consensusapi "github.com/attestantio/go-eth2-client/api" + apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" + apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" consensusspec "github.com/attestantio/go-eth2-client/spec" consensuscapella "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -36,7 +39,7 @@ var NilResponse = struct{}{} var ZeroU256 = boostTypes.IntToU256(0) -func BuildGetHeaderResponse(payload *spec.VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*spec.VersionedSignedBuilderBid, error) { +func BuildGetHeaderResponse(payload *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*spec.VersionedSignedBuilderBid, error) { if payload == nil { return nil, ErrMissingRequest } @@ -59,7 +62,7 @@ func BuildGetHeaderResponse(payload *spec.VersionedSubmitBlockRequest, sk *bls.S return nil, ErrEmptyPayload } -func BuildGetPayloadResponse(payload *spec.VersionedSubmitBlockRequest) (*api.VersionedExecutionPayload, error) { +func BuildGetPayloadResponse(payload *VersionedSubmitBlockRequest) (*api.VersionedExecutionPayload, error) { if payload.Capella != nil { return &api.VersionedExecutionPayload{ Version: consensusspec.DataVersionCapella, @@ -70,7 +73,7 @@ func BuildGetPayloadResponse(payload *spec.VersionedSubmitBlockRequest) (*api.Ve return nil, ErrEmptyPayload } -func BuilderSubmitBlockRequestToSignedBuilderBid(req *spec.VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*spec.VersionedSignedBuilderBid, error) { +func BuilderSubmitBlockRequestToSignedBuilderBid(req *VersionedSubmitBlockRequest, sk *bls.SecretKey, pubkey *phase0.BLSPubKey, domain phase0.Domain) (*spec.VersionedSignedBuilderBid, error) { value, err := req.Value() if err != nil { return nil, err @@ -167,8 +170,8 @@ func CapellaPayloadToPayloadHeader(p *consensuscapella.ExecutionPayload) (*conse }, nil } -func SignedBlindedBeaconBlockToBeaconBlock(signedBlindedBeaconBlock *consensusapi.VersionedSignedBlindedBeaconBlock, executionPayload *api.VersionedExecutionPayload) *consensusspec.VersionedSignedBeaconBlock { - var signedBeaconBlock consensusspec.VersionedSignedBeaconBlock +func SignedBlindedBeaconBlockToBeaconBlock(signedBlindedBeaconBlock *VersionedSignedBlindedBlockRequest, executionPayload *api.VersionedExecutionPayload) *VersionedSignedBlockRequest { + var signedBeaconBlock VersionedSignedBlockRequest capellaBlindedBlock := signedBlindedBeaconBlock.Capella if capellaBlindedBlock != nil { signedBeaconBlock.Capella = &consensuscapella.SignedBeaconBlock{ @@ -198,20 +201,12 @@ func SignedBlindedBeaconBlockToBeaconBlock(signedBlindedBeaconBlock *consensusap } type BuilderBlockValidationRequest struct { - spec.VersionedSubmitBlockRequest + VersionedSubmitBlockRequest RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` } func (r *BuilderBlockValidationRequest) MarshalJSON() ([]byte, error) { - var blockRequest []byte - var err error - - switch r.VersionedSubmitBlockRequest.Version { //nolint:exhaustive - case consensusspec.DataVersionCapella: - blockRequest, err = r.VersionedSubmitBlockRequest.Capella.MarshalJSON() - default: - return nil, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%d is not supported", r.VersionedSubmitBlockRequest.Version)) - } + blockRequest, err := json.Marshal(r.VersionedSubmitBlockRequest) if err != nil { return nil, err } @@ -226,3 +221,129 @@ func (r *BuilderBlockValidationRequest) MarshalJSON() ([]byte, error) { gasLimit[0] = ',' return append(blockRequest[:len(blockRequest)-1], gasLimit...), nil } + +type VersionedSubmitBlockRequest struct { + spec.VersionedSubmitBlockRequest +} + +func (r *VersionedSubmitBlockRequest) UnmarshalSSZ(input []byte) error { + var err error + + deneb := new(deneb.SubmitBlockRequest) + if err = deneb.UnmarshalSSZ(input); err == nil { + r.Version = consensusspec.DataVersionDeneb + r.Deneb = deneb + return nil + } + + capella := new(capella.SubmitBlockRequest) + if err = capella.UnmarshalSSZ(input); err == nil { + r.Version = consensusspec.DataVersionCapella + r.Capella = capella + return nil + } + return errors.Wrap(err, "failed to unmarshal SubmitBlockRequest SSZ") +} + +func (r *VersionedSubmitBlockRequest) MarshalJSON() ([]byte, error) { + switch r.Version { + case consensusspec.DataVersionCapella: + return json.Marshal(r.Capella) + case consensusspec.DataVersionDeneb: + return json.Marshal(r.Deneb) + case consensusspec.DataVersionUnknown, consensusspec.DataVersionPhase0, consensusspec.DataVersionAltair, consensusspec.DataVersionBellatrix: + fallthrough + default: + return nil, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%d is not supported", r.Version)) + } +} + +func (r *VersionedSubmitBlockRequest) UnmarshalJSON(input []byte) error { + var err error + + deneb := new(deneb.SubmitBlockRequest) + if err = json.Unmarshal(input, deneb); err == nil { + r.Version = consensusspec.DataVersionDeneb + r.Deneb = deneb + return nil + } + capella := new(capella.SubmitBlockRequest) + if err = json.Unmarshal(input, capella); err == nil { + r.Version = consensusspec.DataVersionCapella + r.Capella = capella + return nil + } + return errors.Wrap(err, "failed to unmarshal SubmitBlockRequest") +} + +type VersionedSignedBlockRequest struct { + consensusapi.VersionedBlockRequest +} + +func (r *VersionedSignedBlockRequest) MarshalJSON() ([]byte, error) { + switch r.Version { + case consensusspec.DataVersionCapella: + return json.Marshal(r.Capella) + case consensusspec.DataVersionDeneb: + return json.Marshal(r.Deneb) + case consensusspec.DataVersionUnknown, consensusspec.DataVersionPhase0, consensusspec.DataVersionAltair, consensusspec.DataVersionBellatrix: + fallthrough + default: + return nil, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%d is not supported", r.Version)) + } +} + +func (r *VersionedSignedBlockRequest) UnmarshalJSON(input []byte) error { + var err error + + deneb := new(apiv1deneb.SignedBlockContents) + if err = json.Unmarshal(input, deneb); err == nil { + r.Version = consensusspec.DataVersionDeneb + r.Deneb = deneb + return nil + } + + capella := new(consensuscapella.SignedBeaconBlock) + if err = json.Unmarshal(input, capella); err == nil { + r.Version = consensusspec.DataVersionCapella + r.Capella = capella + return nil + } + return errors.Wrap(err, "failed to unmarshal SignedBeaconBlockRequest") +} + +type VersionedSignedBlindedBlockRequest struct { + consensusapi.VersionedBlindedBlockRequest +} + +func (r *VersionedSignedBlindedBlockRequest) MarshalJSON() ([]byte, error) { + switch r.Version { + case consensusspec.DataVersionCapella: + return json.Marshal(r.Capella) + case consensusspec.DataVersionDeneb: + return json.Marshal(r.Deneb) + case consensusspec.DataVersionUnknown, consensusspec.DataVersionPhase0, consensusspec.DataVersionAltair, consensusspec.DataVersionBellatrix: + fallthrough + default: + return nil, errors.Wrap(ErrInvalidVersion, fmt.Sprintf("%d is not supported", r.Version)) + } +} + +func (r *VersionedSignedBlindedBlockRequest) UnmarshalJSON(input []byte) error { + var err error + + deneb := new(apiv1deneb.SignedBlindedBlockContents) + if err = json.Unmarshal(input, deneb); err == nil { + r.Version = consensusspec.DataVersionDeneb + r.Deneb = deneb + return nil + } + + capella := new(apiv1capella.SignedBlindedBeaconBlock) + if err = json.Unmarshal(input, capella); err == nil { + r.Version = consensusspec.DataVersionCapella + r.Capella = capella + return nil + } + return errors.Wrap(err, "failed to unmarshal SignedBlindedBeaconBlock") +} diff --git a/common/types_spec_test.go b/common/types_spec_test.go new file mode 100644 index 00000000..a8774f36 --- /dev/null +++ b/common/types_spec_test.go @@ -0,0 +1,60 @@ +package common + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSubmitBuilderBlockJSON(t *testing.T) { + jsonBytes := LoadGzippedBytes(t, "../testdata/submitBlockPayloadCapella_Goerli.json.gz") + + submitBlockData := new(VersionedSubmitBlockRequest) + err := json.Unmarshal(jsonBytes, &submitBlockData) + require.NoError(t, err) + + marshalledJSONBytes, err := json.Marshal(submitBlockData) + require.NoError(t, err) + buffer := new(bytes.Buffer) + err = json.Compact(buffer, jsonBytes) + require.NoError(t, err) + expectedJSONBytes := buffer.Bytes() + + require.Equal(t, expectedJSONBytes, bytes.ToLower(marshalledJSONBytes)) +} + +func TestSignedBeaconBlockJSON(t *testing.T) { + jsonBytes := LoadGzippedBytes(t, "../testdata/signedBeaconBlock_Goerli.json.gz") + buffer := new(bytes.Buffer) + err := json.Compact(buffer, jsonBytes) + require.NoError(t, err) + expectedJSONBytes := buffer.Bytes() + + blockRequest := new(VersionedSignedBlockRequest) + err = json.Unmarshal(jsonBytes, blockRequest) + require.NoError(t, err) + + marshalledJSONBytes, err := json.Marshal(blockRequest) + require.NoError(t, err) + + require.Equal(t, expectedJSONBytes, bytes.ToLower(marshalledJSONBytes)) +} + +func TestSignedBlindedBlockJSON(t *testing.T) { + jsonBytes := LoadGzippedBytes(t, "../testdata/signedBlindedBeaconBlock_Goerli.json.gz") + buffer := new(bytes.Buffer) + err := json.Compact(buffer, jsonBytes) + require.NoError(t, err) + expectedJSONBytes := buffer.Bytes() + + blockRequest := new(VersionedSignedBlindedBlockRequest) + err = json.Unmarshal(jsonBytes, blockRequest) + require.NoError(t, err) + + marshalledJSONBytes, err := json.Marshal(blockRequest) + require.NoError(t, err) + + require.Equal(t, expectedJSONBytes, bytes.ToLower(marshalledJSONBytes)) +} diff --git a/common/utils.go b/common/utils.go index 33b33468..11a946c3 100644 --- a/common/utils.go +++ b/common/utils.go @@ -185,7 +185,7 @@ type CreateTestBlockSubmissionOpts struct { ProposerPubkey string } -func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint256.Int, opts *CreateTestBlockSubmissionOpts) (payload *spec.VersionedSubmitBlockRequest, getPayloadResponse *api.VersionedExecutionPayload, getHeaderResponse *spec.VersionedSignedBuilderBid) { +func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint256.Int, opts *CreateTestBlockSubmissionOpts) (payload *VersionedSubmitBlockRequest, getPayloadResponse *api.VersionedExecutionPayload, getHeaderResponse *spec.VersionedSignedBuilderBid) { t.Helper() var err error @@ -216,18 +216,20 @@ func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint25 builderPk, err := StrToPhase0Pubkey(builderPubkey) require.NoError(t, err) - payload = &spec.VersionedSubmitBlockRequest{ //nolint:exhaustruct - Version: consensusspec.DataVersionCapella, - Capella: &capella.SubmitBlockRequest{ - Message: &apiv1.BidTrace{ //nolint:exhaustruct - BuilderPubkey: builderPk, - Value: value, - Slot: slot, - ParentHash: parentHash, - ProposerPubkey: proposerPk, + payload = &VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ //nolint:exhaustruct + Version: consensusspec.DataVersionCapella, + Capella: &capella.SubmitBlockRequest{ + Message: &apiv1.BidTrace{ //nolint:exhaustruct + BuilderPubkey: builderPk, + Value: value, + Slot: slot, + ParentHash: parentHash, + ProposerPubkey: proposerPk, + }, + ExecutionPayload: &capellaspec.ExecutionPayload{}, //nolint:exhaustruct + Signature: phase0.BLSSignature{}, }, - ExecutionPayload: &capellaspec.ExecutionPayload{}, //nolint:exhaustruct - Signature: phase0.BLSSignature{}, }, } @@ -252,7 +254,7 @@ func GetEnvDurationSec(key string, defaultValueSec int) time.Duration { return time.Duration(defaultValueSec) * time.Second } -func GetBlockSubmissionInfo(submission *spec.VersionedSubmitBlockRequest) (*BlockSubmissionInfo, error) { +func GetBlockSubmissionInfo(submission *VersionedSubmitBlockRequest) (*BlockSubmissionInfo, error) { bidTrace, err := submission.BidTrace() if err != nil { return nil, err @@ -347,7 +349,7 @@ func GetBlockSubmissionInfo(submission *spec.VersionedSubmitBlockRequest) (*Bloc }, nil } -func GetBlockSubmissionExecutionPayload(submission *spec.VersionedSubmitBlockRequest) (*api.VersionedExecutionPayload, error) { +func GetBlockSubmissionExecutionPayload(submission *VersionedSubmitBlockRequest) (*api.VersionedExecutionPayload, error) { if submission.Capella != nil { return &api.VersionedExecutionPayload{ Version: consensusspec.DataVersionCapella, diff --git a/database/database.go b/database/database.go index 578ca62a..2998325c 100644 --- a/database/database.go +++ b/database/database.go @@ -10,9 +10,6 @@ import ( "time" apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-builder-client/spec" - consensusapi "github.com/attestantio/go-eth2-client/api" - consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/flashbots/mev-boost-relay/common" "github.com/flashbots/mev-boost-relay/database/migrations" "github.com/flashbots/mev-boost-relay/database/vars" @@ -28,7 +25,7 @@ type IDatabaseService interface { GetValidatorRegistration(pubkey string) (*ValidatorRegistrationEntry, error) GetValidatorRegistrationsForPubkeys(pubkeys []string) ([]*ValidatorRegistrationEntry, error) - SaveBuilderBlockSubmission(payload *spec.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) + SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) GetBlockSubmissionEntry(slot uint64, proposerPubkey, blockHash string) (entry *BuilderBlockSubmissionEntry, err error) GetBuilderSubmissions(filters GetBuilderSubmissionsFilters) ([]*BuilderBlockSubmissionEntry, error) GetBuilderSubmissionsBySlots(slotFrom, slotTo uint64) (entries []*BuilderBlockSubmissionEntry, err error) @@ -37,7 +34,7 @@ type IDatabaseService interface { GetExecutionPayloads(idFirst, idLast uint64) (entries []*ExecutionPayloadEntry, err error) DeleteExecutionPayloads(idFirst, idLast uint64) error - SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *consensusapi.VersionedSignedBlindedBeaconBlock, signedAt time.Time, publishMs uint64) error + SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *common.VersionedSignedBlindedBlockRequest, signedAt time.Time, publishMs uint64) error GetNumDeliveredPayloads() (uint64, error) GetRecentDeliveredPayloads(filters GetPayloadsFilters) ([]*DeliveredPayloadEntry, error) GetDeliveredPayloads(idFirst, idLast uint64) (entries []*DeliveredPayloadEntry, err error) @@ -50,8 +47,8 @@ type IDatabaseService interface { UpsertBlockBuilderEntryAfterSubmission(lastSubmission *BuilderBlockSubmissionEntry, isError bool) error IncBlockBuilderStatsAfterGetPayload(builderPubkey string) error - InsertBuilderDemotion(submitBlockRequest *spec.VersionedSubmitBlockRequest, simError error) error - UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *consensusspec.VersionedSignedBeaconBlock, signedRegistration *apiv1.SignedValidatorRegistration) error + InsertBuilderDemotion(submitBlockRequest *common.VersionedSubmitBlockRequest, simError error) error + UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *common.VersionedSignedBlockRequest, signedRegistration *apiv1.SignedValidatorRegistration) error GetBuilderDemotion(trace *common.BidTraceV2) (*BuilderDemotionEntry, error) GetTooLateGetPayload(slot uint64) (entries []*TooLateGetPayloadEntry, err error) @@ -178,7 +175,7 @@ func (s *DatabaseService) GetLatestValidatorRegistrations(timestampOnly bool) ([ return registrations, err } -func (s *DatabaseService) SaveBuilderBlockSubmission(payload *spec.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { +func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { // Save execution_payload: insert, or if already exists update to be able to return the id ('on conflict do nothing' doesn't return an id) execPayloadEntry, err := PayloadToExecPayloadEntry(payload) if err != nil { @@ -275,7 +272,7 @@ func (s *DatabaseService) GetExecutionPayloadEntryBySlotPkHash(slot uint64, prop return entry, err } -func (s *DatabaseService) SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *consensusapi.VersionedSignedBlindedBeaconBlock, signedAt time.Time, publishMs uint64) error { +func (s *DatabaseService) SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *common.VersionedSignedBlindedBlockRequest, signedAt time.Time, publishMs uint64) error { _signedBlindedBeaconBlock, err := json.Marshal(signedBlindedBeaconBlock) if err != nil { return err @@ -544,7 +541,7 @@ func (s *DatabaseService) DeleteExecutionPayloads(idFirst, idLast uint64) error return err } -func (s *DatabaseService) InsertBuilderDemotion(submitBlockRequest *spec.VersionedSubmitBlockRequest, simError error) error { +func (s *DatabaseService) InsertBuilderDemotion(submitBlockRequest *common.VersionedSubmitBlockRequest, simError error) error { _submitBlockRequest, err := json.Marshal(submitBlockRequest.Capella) if err != nil { return err @@ -577,8 +574,8 @@ func (s *DatabaseService) InsertBuilderDemotion(submitBlockRequest *spec.Version return err } -func (s *DatabaseService) UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *consensusspec.VersionedSignedBeaconBlock, signedRegistration *apiv1.SignedValidatorRegistration) error { - _signedBeaconBlock, err := json.Marshal(signedBlock.Capella) +func (s *DatabaseService) UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *common.VersionedSignedBlockRequest, signedRegistration *apiv1.SignedValidatorRegistration) error { + _signedBeaconBlock, err := json.Marshal(signedBlock) if err != nil { return err } diff --git a/database/database_test.go b/database/database_test.go index e457cc32..2de0bb31 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -8,7 +8,8 @@ import ( "time" apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-eth2-client/spec" + consensusapi "github.com/attestantio/go-eth2-client/api" + consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" consensuscapella "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -356,8 +357,11 @@ func TestUpdateBuilderDemotion(t *testing.T) { require.Empty(t, demotion.SignedValidatorRegistration.String) // Update demotion with the signedBlock and signedRegistration. - bb := &spec.VersionedSignedBeaconBlock{ - Capella: &consensuscapella.SignedBeaconBlock{}, + bb := &common.VersionedSignedBlockRequest{ + VersionedBlockRequest: consensusapi.VersionedBlockRequest{ + Version: consensusspec.DataVersionCapella, + Capella: &consensuscapella.SignedBeaconBlock{}, + }, } err = db.UpdateBuilderDemotion(bt, bb, &apiv1.SignedValidatorRegistration{}) require.NoError(t, err) diff --git a/database/mockdb.go b/database/mockdb.go index 0c332ec4..8a1f5fd3 100644 --- a/database/mockdb.go +++ b/database/mockdb.go @@ -6,9 +6,6 @@ import ( "time" apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-builder-client/spec" - consensusapi "github.com/attestantio/go-eth2-client/api" - consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/flashbots/mev-boost-relay/common" ) @@ -39,7 +36,7 @@ func (db MockDB) GetLatestValidatorRegistrations(timestampOnly bool) ([]*Validat return nil, nil } -func (db MockDB) SaveBuilderBlockSubmission(payload *spec.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { +func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { return nil, nil } @@ -88,7 +85,7 @@ func (db MockDB) GetBuilderSubmissionsBySlots(slotFrom, slotTo uint64) (entries return nil, nil } -func (db MockDB) SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *consensusapi.VersionedSignedBlindedBeaconBlock, signedAt time.Time, publishMs uint64) error { +func (db MockDB) SaveDeliveredPayload(bidTrace *common.BidTraceV2, signedBlindedBeaconBlock *common.VersionedSignedBlindedBlockRequest, signedAt time.Time, publishMs uint64) error { return nil } @@ -156,7 +153,7 @@ func (db MockDB) IncBlockBuilderStatsAfterGetPayload(builderPubkey string) error return nil } -func (db MockDB) InsertBuilderDemotion(submitBlockRequest *spec.VersionedSubmitBlockRequest, simError error) error { +func (db MockDB) InsertBuilderDemotion(submitBlockRequest *common.VersionedSubmitBlockRequest, simError error) error { pubkey, err := submitBlockRequest.Builder() if err != nil { return err @@ -165,7 +162,7 @@ func (db MockDB) InsertBuilderDemotion(submitBlockRequest *spec.VersionedSubmitB return nil } -func (db MockDB) UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *consensusspec.VersionedSignedBeaconBlock, signedRegistration *apiv1.SignedValidatorRegistration) error { +func (db MockDB) UpdateBuilderDemotion(trace *common.BidTraceV2, signedBlock *common.VersionedSignedBlockRequest, signedRegistration *apiv1.SignedValidatorRegistration) error { pubkey := trace.BuilderPubkey.String() _, ok := db.Builders[pubkey] if !ok { diff --git a/database/typesconv.go b/database/typesconv.go index 08e205e4..135d0ad8 100644 --- a/database/typesconv.go +++ b/database/typesconv.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/attestantio/go-builder-client/api" - "github.com/attestantio/go-builder-client/spec" consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/flashbots/mev-boost-relay/common" @@ -13,7 +12,7 @@ import ( var ErrUnsupportedExecutionPayload = errors.New("unsupported execution payload version") -func PayloadToExecPayloadEntry(payload *spec.VersionedSubmitBlockRequest) (*ExecutionPayloadEntry, error) { +func PayloadToExecPayloadEntry(payload *common.VersionedSubmitBlockRequest) (*ExecutionPayloadEntry, error) { var _payload []byte var version string var err error diff --git a/datastore/memcached_test.go b/datastore/memcached_test.go index 415ed3e8..ad54809b 100644 --- a/datastore/memcached_test.go +++ b/datastore/memcached_test.go @@ -33,38 +33,41 @@ var ( ErrNoMemcachedServers = errors.New("no memcached servers specified") ) -func testBuilderSubmitBlockRequest(pubkey phase0.BLSPubKey, signature phase0.BLSSignature, version consensusspec.DataVersion) spec.VersionedSubmitBlockRequest { +func testBuilderSubmitBlockRequest(pubkey phase0.BLSPubKey, signature phase0.BLSSignature, version consensusspec.DataVersion) common.VersionedSubmitBlockRequest { switch version { case consensusspec.DataVersionCapella: - return spec.VersionedSubmitBlockRequest{ - Capella: &capella.SubmitBlockRequest{ - Signature: signature, - Message: &apiv1.BidTrace{ - Slot: 1, - ParentHash: phase0.Hash32{0x01}, - BlockHash: phase0.Hash32{0x09}, - BuilderPubkey: pubkey, - ProposerPubkey: phase0.BLSPubKey{0x03}, - ProposerFeeRecipient: bellatrix.ExecutionAddress{0x04}, - Value: uint256.NewInt(123), - GasLimit: 5002, - GasUsed: 5003, - }, - ExecutionPayload: &capellaspec.ExecutionPayload{ - ParentHash: phase0.Hash32{0x01}, - FeeRecipient: bellatrix.ExecutionAddress{0x02}, - StateRoot: phase0.Root{0x03}, - ReceiptsRoot: phase0.Root{0x04}, - LogsBloom: [256]byte{0x05}, - PrevRandao: phase0.Hash32{0x06}, - BlockNumber: 5001, - GasLimit: 5002, - GasUsed: 5003, - Timestamp: 5004, - ExtraData: []byte{0x07}, - BaseFeePerGas: types.IntToU256(123), - BlockHash: phase0.Hash32{0x09}, - Transactions: []bellatrix.Transaction{}, + return common.VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ + Version: consensusspec.DataVersionCapella, + Capella: &capella.SubmitBlockRequest{ + Signature: signature, + Message: &apiv1.BidTrace{ + Slot: 1, + ParentHash: phase0.Hash32{0x01}, + BlockHash: phase0.Hash32{0x09}, + BuilderPubkey: pubkey, + ProposerPubkey: phase0.BLSPubKey{0x03}, + ProposerFeeRecipient: bellatrix.ExecutionAddress{0x04}, + Value: uint256.NewInt(123), + GasLimit: 5002, + GasUsed: 5003, + }, + ExecutionPayload: &capellaspec.ExecutionPayload{ + ParentHash: phase0.Hash32{0x01}, + FeeRecipient: bellatrix.ExecutionAddress{0x02}, + StateRoot: phase0.Root{0x03}, + ReceiptsRoot: phase0.Root{0x04}, + LogsBloom: [256]byte{0x05}, + PrevRandao: phase0.Hash32{0x06}, + BlockNumber: 5001, + GasLimit: 5002, + GasUsed: 5003, + Timestamp: 5004, + ExtraData: []byte{0x07}, + BaseFeePerGas: types.IntToU256(123), + BlockHash: phase0.Hash32{0x09}, + Transactions: []bellatrix.Transaction{}, + }, }, }, } @@ -73,9 +76,7 @@ func testBuilderSubmitBlockRequest(pubkey phase0.BLSPubKey, signature phase0.BLS case consensusspec.DataVersionUnknown, consensusspec.DataVersionPhase0, consensusspec.DataVersionAltair, consensusspec.DataVersionBellatrix: fallthrough default: - return spec.VersionedSubmitBlockRequest{ - Capella: nil, - } + return common.VersionedSubmitBlockRequest{} } } @@ -110,7 +111,7 @@ func initMemcached(t *testing.T) (mem *Memcached, err error) { // RUN_INTEGRATION_TESTS=1 MEMCACHED_URIS="localhost:11211" go test -v -run ".*Memcached.*" ./... func TestMemcached(t *testing.T) { type test struct { - Input spec.VersionedSubmitBlockRequest + Input common.VersionedSubmitBlockRequest Description string TestSuite func(tc *test) func(*testing.T) } diff --git a/datastore/redis.go b/datastore/redis.go index e109214e..9ecbf3be 100644 --- a/datastore/redis.go +++ b/datastore/redis.go @@ -457,7 +457,7 @@ type SaveBidAndUpdateTopBidResponse struct { TimeUpdateFloor time.Duration } -func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, tx redis.Pipeliner, trace *common.BidTraceV2, payload *spec.VersionedSubmitBlockRequest, getPayloadResponse *api.VersionedExecutionPayload, getHeaderResponse *spec.VersionedSignedBuilderBid, reqReceivedAt time.Time, isCancellationEnabled bool, floorValue *big.Int) (state SaveBidAndUpdateTopBidResponse, err error) { +func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, tx redis.Pipeliner, trace *common.BidTraceV2, payload *common.VersionedSubmitBlockRequest, getPayloadResponse *api.VersionedExecutionPayload, getHeaderResponse *spec.VersionedSignedBuilderBid, reqReceivedAt time.Time, isCancellationEnabled bool, floorValue *big.Int) (state SaveBidAndUpdateTopBidResponse, err error) { var prevTime, nextTime time.Time prevTime = time.Now() diff --git a/go.mod b/go.mod index a0839777..d4146009 100644 --- a/go.mod +++ b/go.mod @@ -115,4 +115,6 @@ retract ( v1.0.0-alpha1 ) -replace github.com/attestantio/go-builder-client => github.com/avalonche/go-builder-client v0.0.0-20230711071258-c1ca05d6e8b7 +replace github.com/attestantio/go-builder-client => github.com/avalonche/go-builder-client v0.0.0-20230731215616-c49675dd2f87 + +replace github.com/attestantio/go-eth2-client => github.com/avalonche/go-eth2-client v0.0.0-20230801235812-ab38c7f8cba1 diff --git a/go.sum b/go.sum index 21b871d4..8125d9d7 100644 --- a/go.sum +++ b/go.sum @@ -22,10 +22,10 @@ github.com/alicebob/miniredis/v2 v2.30.4/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6u github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/attestantio/go-eth2-client v0.18.1-0.20230728160410-bc2888aaf7d7 h1:7CCFg+rn8EppcURkhMLdNv0rl0lwFcAxkwV7kKOSF14= -github.com/attestantio/go-eth2-client v0.18.1-0.20230728160410-bc2888aaf7d7/go.mod h1:KSVlZSW1A3jUg5H8O89DLtqxgJprRfTtI7k89fLdhu0= -github.com/avalonche/go-builder-client v0.0.0-20230711071258-c1ca05d6e8b7 h1:NQt83tMbCmK2V1OuPUUEIudvi8EeULl3iWG3Ht1bdCk= -github.com/avalonche/go-builder-client v0.0.0-20230711071258-c1ca05d6e8b7/go.mod h1:DwesMTOqnCp4u+n3uZ+fWL8wwnSBZVD9VMIVPDR+AZE= +github.com/avalonche/go-builder-client v0.0.0-20230731215616-c49675dd2f87 h1:+GPbCP8XCYteTDwr+e2vWsilPj/7rfXnfAE4ZqjVJQk= +github.com/avalonche/go-builder-client v0.0.0-20230731215616-c49675dd2f87/go.mod h1:8NZx9L/rC7nLhSMbbVR6PXr37tC28wTE4u65n+Pa3BQ= +github.com/avalonche/go-eth2-client v0.0.0-20230801235812-ab38c7f8cba1 h1:jLmNo92ji/g8pdnA+Nuc9b7rTC6BZCv0POhmiFqX9cY= +github.com/avalonche/go-eth2-client v0.0.0-20230801235812-ab38c7f8cba1/go.mod h1:KSVlZSW1A3jUg5H8O89DLtqxgJprRfTtI7k89fLdhu0= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/services/api/service.go b/services/api/service.go index 6617a80a..d680b547 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -21,12 +21,7 @@ import ( "time" "github.com/NYTimes/gziphandler" - builderCapella "github.com/attestantio/go-builder-client/api/capella" apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-builder-client/spec" - consensusapi "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/api/v1/capella" - consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/buger/jsonparser" "github.com/flashbots/go-boost-utils/bls" @@ -426,10 +421,10 @@ func (api *RelayAPI) StartServer() (err error) { } // Print fork version information - if hasReachedFork(currentSlot, api.capellaEpoch) { - log.Infof("capella fork detected (currentEpoch: %d / capellaEpoch: %d)", common.SlotToEpoch(currentSlot), api.capellaEpoch) - } else if hasReachedFork(currentSlot, api.denebEpoch) { + if hasReachedFork(currentSlot, api.denebEpoch) { log.Infof("deneb fork detected (currentEpoch: %d / denebEpoch: %d)", common.SlotToEpoch(currentSlot), api.denebEpoch) + } else if hasReachedFork(currentSlot, api.capellaEpoch) { + log.Infof("capella fork detected (currentEpoch: %d / capellaEpoch: %d)", common.SlotToEpoch(currentSlot), api.capellaEpoch) } else { return ErrMismatchedForkVersions } @@ -582,7 +577,7 @@ func (api *RelayAPI) simulateBlock(ctx context.Context, opts blockSimOptions) (r return nil, nil } -func (api *RelayAPI) demoteBuilder(pubkey string, req *spec.VersionedSubmitBlockRequest, simError error) { +func (api *RelayAPI) demoteBuilder(pubkey string, req *common.VersionedSubmitBlockRequest, simError error) { builderEntry, ok := api.blockBuildersCache[pubkey] if !ok { api.log.Warnf("builder %v not in the builder cache", pubkey) @@ -1231,10 +1226,8 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) } // Decode payload - payload := new(consensusapi.VersionedSignedBlindedBeaconBlock) - // TODO: add deneb support. - payload.Capella = new(capella.SignedBlindedBeaconBlock) - if err := json.NewDecoder(bytes.NewReader(body)).Decode(payload.Capella); err != nil { + payload := new(common.VersionedSignedBlindedBlockRequest) + if err := json.NewDecoder(bytes.NewReader(body)).Decode(payload); err != nil { log.WithError(err).Warn("failed to decode capella getPayload request") api.RespondError(w, http.StatusBadRequest, "failed to decode capella payload") return @@ -1611,20 +1604,17 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque return } - payload := &spec.VersionedSubmitBlockRequest{ //nolint:exhaustruct - Version: consensusspec.DataVersionCapella, - } - payload.Capella = new(builderCapella.SubmitBlockRequest) + payload := new(common.VersionedSubmitBlockRequest) // Check for SSZ encoding contentType := req.Header.Get("Content-Type") if contentType == "application/octet-stream" { log = log.WithField("reqContentType", "ssz") - if err = payload.Capella.UnmarshalSSZ(requestPayloadBytes); err != nil { + if err = payload.UnmarshalSSZ(requestPayloadBytes); err != nil { log.WithError(err).Warn("could not decode payload - SSZ") // SSZ decoding failed. try JSON as fallback (some builders used octet-stream for json before) - if err2 := json.Unmarshal(requestPayloadBytes, payload.Capella); err2 != nil { + if err2 := json.Unmarshal(requestPayloadBytes, payload); err2 != nil { log.WithError(fmt.Errorf("%w / %w", err, err2)).Warn("could not decode payload - SSZ or JSON") api.RespondError(w, http.StatusBadRequest, err.Error()) return @@ -1635,7 +1625,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque } } else { log = log.WithField("reqContentType", "json") - if err := json.Unmarshal(requestPayloadBytes, payload.Capella); err != nil { + if err := json.Unmarshal(requestPayloadBytes, payload); err != nil { log.WithError(err).Warn("could not decode payload - JSON") api.RespondError(w, http.StatusBadRequest, err.Error()) return @@ -1667,10 +1657,13 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque "isLargeRequest": isLargeRequest, }) - // TODO: add deneb support. - if payload.Capella == nil { + if hasReachedFork(submission.Slot, api.denebEpoch) && payload.Deneb == nil { + log.Info("rejecting submission - non deneb payload for deneb fork") + api.RespondError(w, http.StatusBadRequest, "non deneb payload") + return + } else if hasReachedFork(submission.Slot, api.capellaEpoch) && payload.Capella == nil { log.Info("rejecting submission - non capella payload for capella fork") - api.RespondError(w, http.StatusBadRequest, "not capella payload") + api.RespondError(w, http.StatusBadRequest, "non capella payload") return } @@ -1902,7 +1895,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque builder: builderEntry, req: &common.BuilderBlockValidationRequest{ VersionedSubmitBlockRequest: *payload, - RegisteredGasLimit: gasLimit, + RegisteredGasLimit: gasLimit, }, } // With sufficient collateral, process the block optimistically. diff --git a/services/api/service_test.go b/services/api/service_test.go index f8dd563a..7e3424ed 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -15,7 +15,9 @@ import ( builderCapella "github.com/attestantio/go-builder-client/api/capella" apiv1 "github.com/attestantio/go-builder-client/api/v1" "github.com/attestantio/go-builder-client/spec" + consensusspec "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/utils" @@ -34,8 +36,8 @@ const ( ) var ( - testAddress = bellatrix.ExecutionAddress([20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) - testAddress2 = bellatrix.ExecutionAddress([20]byte{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) + testAddress = bellatrix.ExecutionAddress([20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) + testAddress2 = bellatrix.ExecutionAddress([20]byte{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}) ) type testBackend struct { @@ -304,7 +306,7 @@ func TestDataApiGetDataProposerPayloadDelivered(t *testing.T) { func TestBuilderSubmitBlockSSZ(t *testing.T) { requestPayloadJSONBytes := common.LoadGzippedBytes(t, "../../testdata/submitBlockPayloadCapella_Goerli.json.gz") - req := new(spec.VersionedSubmitBlockRequest) + req := new(common.VersionedSubmitBlockRequest) req.Capella = new(builderCapella.SubmitBlockRequest) err := json.Unmarshal(requestPayloadJSONBytes, req.Capella) require.NoError(t, err) @@ -358,7 +360,7 @@ func TestBuilderSubmitBlock(t *testing.T) { } // Prepare the request payload - req := new(spec.VersionedSubmitBlockRequest) + req := new(common.VersionedSubmitBlockRequest) req.Capella = new(builderCapella.SubmitBlockRequest) requestPayloadJSONBytes := common.LoadGzippedBytes(t, payloadJSONFilename) require.NoError(t, err) @@ -417,7 +419,7 @@ func TestCheckSubmissionFeeRecipient(t *testing.T) { cases := []struct { description string slotDuty *common.BuilderGetValidatorsResponseEntry - payload *spec.VersionedSubmitBlockRequest + payload *common.VersionedSubmitBlockRequest expectCont bool expectGasLimit uint64 }{ @@ -431,11 +433,15 @@ func TestCheckSubmissionFeeRecipient(t *testing.T) { }, }, }, - payload: &spec.VersionedSubmitBlockRequest{ - Capella: &builderCapella.SubmitBlockRequest{ - Message: &apiv1.BidTrace{ - Slot: testSlot, - ProposerFeeRecipient: bellatrix.ExecutionAddress(testAddress), + payload: &common.VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ + Version: consensusspec.DataVersionCapella, + Capella: &builderCapella.SubmitBlockRequest{ + Message: &apiv1.BidTrace{ + Slot: testSlot, + ProposerFeeRecipient: testAddress, + }, + ExecutionPayload: &capella.ExecutionPayload{}, }, }, }, @@ -445,10 +451,14 @@ func TestCheckSubmissionFeeRecipient(t *testing.T) { { description: "failure_nil_slot_duty", slotDuty: nil, - payload: &spec.VersionedSubmitBlockRequest{ - Capella: &builderCapella.SubmitBlockRequest{ - Message: &apiv1.BidTrace{ - Slot: testSlot, + payload: &common.VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ + Version: consensusspec.DataVersionCapella, + Capella: &builderCapella.SubmitBlockRequest{ + Message: &apiv1.BidTrace{ + Slot: testSlot, + }, + ExecutionPayload: &capella.ExecutionPayload{}, }, }, }, @@ -465,11 +475,15 @@ func TestCheckSubmissionFeeRecipient(t *testing.T) { }, }, }, - payload: &spec.VersionedSubmitBlockRequest{ - Capella: &builderCapella.SubmitBlockRequest{ - Message: &apiv1.BidTrace{ - Slot: testSlot, - ProposerFeeRecipient: bellatrix.ExecutionAddress(testAddress2), + payload: &common.VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: spec.VersionedSubmitBlockRequest{ + Version: consensusspec.DataVersionCapella, + Capella: &builderCapella.SubmitBlockRequest{ + Message: &apiv1.BidTrace{ + Slot: testSlot, + ProposerFeeRecipient: testAddress2, + }, + ExecutionPayload: &capella.ExecutionPayload{}, }, }, }, diff --git a/services/api/utils.go b/services/api/utils.go index e3ec2375..48b0fd7e 100644 --- a/services/api/utils.go +++ b/services/api/utils.go @@ -4,8 +4,6 @@ import ( "errors" "github.com/attestantio/go-builder-client/api" - "github.com/attestantio/go-builder-client/spec" - consensusapi "github.com/attestantio/go-eth2-client/api" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" utilcapella "github.com/attestantio/go-eth2-client/util/capella" @@ -25,7 +23,7 @@ var ( ErrHeaderHTRMismatch = errors.New("beacon-block and payload header mismatch") ) -func SanityCheckBuilderBlockSubmission(payload *spec.VersionedSubmitBlockRequest) error { +func SanityCheckBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest) error { submission, err := common.GetBlockSubmissionInfo(payload) if err != nil { return err @@ -49,7 +47,7 @@ func ComputeWithdrawalsRoot(w []*capella.Withdrawal) (phase0.Root, error) { return withdrawals.HashTreeRoot() } -func EqExecutionPayloadToHeader(bb *consensusapi.VersionedSignedBlindedBeaconBlock, payload *api.VersionedExecutionPayload) error { +func EqExecutionPayloadToHeader(bb *common.VersionedSignedBlindedBlockRequest, payload *api.VersionedExecutionPayload) error { if bb.Capella != nil { // process Capella beacon block if payload.Capella == nil { return ErrPayloadMismatchCapella @@ -93,12 +91,12 @@ func hasReachedFork(slot, forkEpoch uint64) bool { return currentEpoch >= forkEpoch } -func checkProposerSignature(block *consensusapi.VersionedSignedBlindedBeaconBlock, domain phase0.Domain, pubKey []byte) (bool, error) { +func checkProposerSignature(block *common.VersionedSignedBlindedBlockRequest, domain phase0.Domain, pubKey []byte) (bool, error) { root, err := block.Root() if err != nil { return false, err } - sig, err := block.Signature() + sig, err := block.BeaconBlockSignature() if err != nil { return false, err } diff --git a/testdata/signedBeaconBlock_Goerli.json.gz b/testdata/signedBeaconBlock_Goerli.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..9700c63c6590e74419ba2b58cb36157a06101ecb GIT binary patch literal 38859 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 Zl0C;FCG|i2m;d=+{y&09*G+x;0RSVosKx*Q literal 0 HcmV?d00001 diff --git a/testdata/signedBlindedBeaconBlock_Goerli.json.gz b/testdata/signedBlindedBeaconBlock_Goerli.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..0c2df95f60ddc9c5ed251dc2245f3e5a6f05e740 GIT binary patch literal 13765 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