From 4ff4d897785abdaabe053f253b7e11cfcfd5c0e6 Mon Sep 17 00:00:00 2001 From: shana Date: Fri, 19 Jan 2024 02:40:51 -0800 Subject: [PATCH] Add Deneb Types and Flow (#553) * add deneb path * remove redirectives * Check deneb blobs * Make attestantio import names consistent (#560) * update payload fields * Compute block root to calculate block hash * remove computing deneb hash * clean up and add tests * updated builder specs * update mev-boost to updated builder-specs * fix tests * Apply suggestions from code review Co-authored-by: Justin Traglia <95511699+jtraglia@users.noreply.github.com> * rebase * update go-boost-utils * address pr comments --------- Co-authored-by: Justin Traglia <95511699+jtraglia@users.noreply.github.com> Co-authored-by: Chris Hager --- .golangci.yml | 1 + cmd/test-cli/main.go | 18 +- cmd/test-cli/validator.go | 14 +- go.mod | 6 +- go.sum | 12 +- server/mock_relay.go | 71 ++-- server/mock_types_test.go | 4 +- server/service.go | 190 +++++++++-- server/service_test.go | 173 +++++++--- server/utils.go | 90 ++--- server/utils_test.go | 109 ++++++- .../signed-blinded-beacon-block-deneb.json | 307 ++++++++++++++++++ 12 files changed, 790 insertions(+), 205 deletions(-) create mode 100644 testdata/signed-blinded-beacon-block-deneb.json diff --git a/.golangci.yml b/.golangci.yml index e461ee1b..52baf613 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,7 @@ linters: - wrapcheck - wsl - musttag + - depguard # # Maybe fix later: diff --git a/cmd/test-cli/main.go b/cmd/test-cli/main.go index 75a1f952..f9ec4f58 100644 --- a/cmd/test-cli/main.go +++ b/cmd/test-cli/main.go @@ -8,9 +8,9 @@ import ( "os" "strconv" - "github.com/attestantio/go-builder-client/api" - "github.com/attestantio/go-builder-client/spec" - "github.com/attestantio/go-eth2-client/api/v1/capella" + builderApi "github.com/attestantio/go-builder-client/api" + builderSpec "github.com/attestantio/go-builder-client/spec" + eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common" @@ -44,7 +44,7 @@ func doRegisterValidator(v validatorPrivateData, boostEndpoint string, builderSi log.WithError(err).Info("Registered validator") } -func doGetHeader(v validatorPrivateData, boostEndpoint string, beaconNode Beacon, engineEndpoint string, builderSigningDomain phase0.Domain) spec.VersionedSignedBuilderBid { +func doGetHeader(v validatorPrivateData, boostEndpoint string, beaconNode Beacon, engineEndpoint string, builderSigningDomain phase0.Domain) builderSpec.VersionedSignedBuilderBid { // Mergemock needs to call forkchoice update before getHeader, for non-mergemock beacon node this is a no-op err := beaconNode.onGetHeader() if err != nil { @@ -71,7 +71,7 @@ func doGetHeader(v validatorPrivateData, boostEndpoint string, beaconNode Beacon uri := fmt.Sprintf("%s/eth/v1/builder/header/%d/%s/%s", boostEndpoint, currentBlock.Slot+1, currentBlockHash, v.Pk.String()) - var getHeaderResp spec.VersionedSignedBuilderBid + var getHeaderResp builderSpec.VersionedSignedBuilderBid if _, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, uri, "test-cli", nil, nil, &getHeaderResp); err != nil { log.WithError(err).WithField("currentBlockHash", currentBlockHash).Fatal("Could not get header") } @@ -95,12 +95,12 @@ func doGetHeader(v validatorPrivateData, boostEndpoint string, beaconNode Beacon func doGetPayload(v validatorPrivateData, boostEndpoint string, beaconNode Beacon, engineEndpoint string, builderSigningDomain, proposerSigningDomain phase0.Domain) { header := doGetHeader(v, boostEndpoint, beaconNode, engineEndpoint, builderSigningDomain) - blindedBeaconBlock := capella.BlindedBeaconBlock{ + blindedBeaconBlock := eth2ApiV1Capella.BlindedBeaconBlock{ Slot: 0, ProposerIndex: 0, ParentRoot: phase0.Root{}, StateRoot: phase0.Root{}, - Body: &capella.BlindedBeaconBlockBody{ + Body: ð2ApiV1Capella.BlindedBeaconBlockBody{ RANDAOReveal: phase0.BLSSignature{}, ETH1Data: &phase0.ETH1Data{}, Graffiti: phase0.Hash32{}, @@ -119,11 +119,11 @@ func doGetPayload(v validatorPrivateData, boostEndpoint string, beaconNode Beaco log.WithError(err).Fatal("could not sign blinded beacon block") } - payload := capella.SignedBlindedBeaconBlock{ + payload := eth2ApiV1Capella.SignedBlindedBeaconBlock{ Message: &blindedBeaconBlock, Signature: signature, } - var respPayload api.VersionedExecutionPayload + var respPayload builderApi.VersionedExecutionPayload if _, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, boostEndpoint+"/eth/v1/builder/blinded_blocks", "test-cli", nil, payload, &respPayload); err != nil { log.WithError(err).Fatal("could not get payload") } diff --git a/cmd/test-cli/validator.go b/cmd/test-cli/validator.go index 260c8905..ef4eb8a1 100644 --- a/cmd/test-cli/validator.go +++ b/cmd/test-cli/validator.go @@ -6,7 +6,7 @@ import ( "os" "time" - apiv1 "github.com/attestantio/go-builder-client/api/v1" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/flashbots/go-boost-utils/bls" @@ -52,18 +52,18 @@ func newRandomValidator(gasLimit uint64, feeRecipient string) validatorPrivateDa return validatorPrivateData{bls.SecretKeyToBytes(sk), bls.PublicKeyToBytes(pk), hexutil.Uint64(gasLimit), feeRecipient} } -func (v *validatorPrivateData) PrepareRegistrationMessage(builderSigningDomain phase0.Domain) ([]apiv1.SignedValidatorRegistration, error) { +func (v *validatorPrivateData) PrepareRegistrationMessage(builderSigningDomain phase0.Domain) ([]builderApiV1.SignedValidatorRegistration, error) { pk := phase0.BLSPubKey{} if len(v.Pk) != len(pk) { - return []apiv1.SignedValidatorRegistration{}, errInvalidLength + return []builderApiV1.SignedValidatorRegistration{}, errInvalidLength } copy(pk[:], v.Pk) addr, err := utils.HexToAddress(v.FeeRecipientHex) if err != nil { - return []apiv1.SignedValidatorRegistration{}, err + return []builderApiV1.SignedValidatorRegistration{}, err } - msg := apiv1.ValidatorRegistration{ + msg := builderApiV1.ValidatorRegistration{ FeeRecipient: addr, Timestamp: time.Now(), Pubkey: pk, @@ -71,10 +71,10 @@ func (v *validatorPrivateData) PrepareRegistrationMessage(builderSigningDomain p } signature, err := v.Sign(&msg, builderSigningDomain) if err != nil { - return []apiv1.SignedValidatorRegistration{}, err + return []builderApiV1.SignedValidatorRegistration{}, err } - return []apiv1.SignedValidatorRegistration{{ + return []builderApiV1.SignedValidatorRegistration{{ Message: &msg, Signature: signature, }}, nil diff --git a/go.mod b/go.mod index ef2049ef..2480975a 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/flashbots/mev-boost go 1.20 require ( - github.com/ethereum/go-ethereum v1.13.9 - github.com/flashbots/go-boost-utils v1.7.1 + github.com/ethereum/go-ethereum v1.13.10 + github.com/flashbots/go-boost-utils v1.8.0 github.com/flashbots/go-utils v0.5.0 github.com/google/uuid v1.5.0 - github.com/gorilla/mux v1.8.1 + github.com/gorilla/mux v1.8.0 github.com/holiman/uint256 v1.2.4 github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 9a8b5191..27cdef58 100644 --- a/go.sum +++ b/go.sum @@ -92,16 +92,16 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= -github.com/ethereum/go-ethereum v1.13.9 h1:ed4e4c7NWPrO2VX2wsMhWs5+6Lf2D591DmdE8RgmtcU= -github.com/ethereum/go-ethereum v1.13.9/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= +github.com/ethereum/go-ethereum v1.13.10 h1:Ppdil79nN+Vc+mXfge0AuUgmKWuVv4eMqzoIVSdqZek= +github.com/ethereum/go-ethereum v1.13.10/go.mod h1:sc48XYQxCzH3fG9BcrXCOOgQk2JfZzNAmIKnceogzsA= github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= -github.com/flashbots/go-boost-utils v1.7.1 h1:JN0JFOCuuQoPhyZEaFxFHC2dWVScixItJ2nijzfE6IQ= -github.com/flashbots/go-boost-utils v1.7.1/go.mod h1:O2LUD1QAqi1oMzU1mtj7f1NMunNySJizQGrw7xujSe8= +github.com/flashbots/go-boost-utils v1.8.0 h1:z3K1hw+Fbl9AGMNQKnK7Bvf0M/rKgjfruAEvra+Z8Mg= +github.com/flashbots/go-boost-utils v1.8.0/go.mod h1:Ry1Rw8Lx5v1rpAR0+IvR4sV10jYAeQaGVM3vRD8mYdM= github.com/flashbots/go-utils v0.5.0 h1:ldjWta9B9//DJU2QcwRbErez3+1aKhSn6EoFc6d5kPY= github.com/flashbots/go-utils v0.5.0/go.mod h1:LauDwifaRdSK0mS5X34GR59pJtUu1T/lOFNdff1BqtI= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -174,8 +174,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= diff --git a/server/mock_relay.go b/server/mock_relay.go index d59eb89e..fe6c6daa 100644 --- a/server/mock_relay.go +++ b/server/mock_relay.go @@ -10,14 +10,14 @@ import ( "testing" "time" - "github.com/attestantio/go-builder-client/api" - apibellatrix "github.com/attestantio/go-builder-client/api/bellatrix" - apicapella "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" + builderApi "github.com/attestantio/go-builder-client/api" + builderApiCapella "github.com/attestantio/go-builder-client/api/capella" + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + builderSpec "github.com/attestantio/go-builder-client/spec" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/flashbots/go-boost-utils/bls" @@ -59,8 +59,8 @@ type mockRelay struct { handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request) // Default responses placeholders, used if overrider does not exist - GetHeaderResponse *spec.VersionedSignedBuilderBid - GetPayloadResponse *api.VersionedExecutionPayload + GetHeaderResponse *builderSpec.VersionedSignedBuilderBid + GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse // Server section Server *httptest.Server @@ -141,7 +141,7 @@ func (m *mockRelay) handleStatus(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, `{}`) } -// By default, handleRegisterValidator returns a default apiv1.SignedValidatorRegistration +// By default, handleRegisterValidator returns a default builderApiV1.SignedValidatorRegistration func (m *mockRelay) handleRegisterValidator(w http.ResponseWriter, req *http.Request) { m.mu.Lock() defer m.mu.Unlock() @@ -154,7 +154,7 @@ func (m *mockRelay) handleRegisterValidator(w http.ResponseWriter, req *http.Req // defaultHandleRegisterValidator returns the default handler for handleRegisterValidator func (m *mockRelay) defaultHandleRegisterValidator(w http.ResponseWriter, req *http.Request) { - payload := []apiv1.SignedValidatorRegistration{} + payload := []builderApiV1.SignedValidatorRegistration{} if err := DecodeJSON(req.Body, &payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -166,14 +166,15 @@ func (m *mockRelay) defaultHandleRegisterValidator(w http.ResponseWriter, req *h // MakeGetHeaderResponse is used to create the default or can be used to create a custom response to the getHeader // method -func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version consensusspec.DataVersion) *spec.VersionedSignedBuilderBid { +func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion) *builderSpec.VersionedSignedBuilderBid { switch version { - case consensusspec.DataVersionBellatrix: + case spec.DataVersionCapella: // Fill the payload with custom values. - message := &apibellatrix.BuilderBid{ - Header: &bellatrix.ExecutionPayloadHeader{ - BlockHash: _HexToHash(blockHash), - ParentHash: _HexToHash(parentHash), + message := &builderApiCapella.BuilderBid{ + Header: &capella.ExecutionPayloadHeader{ + BlockHash: _HexToHash(blockHash), + ParentHash: _HexToHash(parentHash), + WithdrawalsRoot: phase0.Root{}, }, Value: uint256.NewInt(value), Pubkey: _HexToPubkey(publicKey), @@ -183,37 +184,38 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &spec.VersionedSignedBuilderBid{ - Version: consensusspec.DataVersionCapella, - Bellatrix: &apibellatrix.SignedBuilderBid{ + return &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionCapella, + Capella: &builderApiCapella.SignedBuilderBid{ Message: message, Signature: signature, }, } - case consensusspec.DataVersionCapella: - // Fill the payload with custom values. - message := &apicapella.BuilderBid{ - Header: &capella.ExecutionPayloadHeader{ + case spec.DataVersionDeneb: + message := &builderApiDeneb.BuilderBid{ + Header: &deneb.ExecutionPayloadHeader{ BlockHash: _HexToHash(blockHash), ParentHash: _HexToHash(parentHash), WithdrawalsRoot: phase0.Root{}, + BaseFeePerGas: uint256.NewInt(0), }, - Value: uint256.NewInt(value), - Pubkey: _HexToPubkey(publicKey), + BlobKZGCommitments: make([]deneb.KZGCommitment, 0), + Value: uint256.NewInt(value), + Pubkey: _HexToPubkey(publicKey), } // Sign the message. signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &spec.VersionedSignedBuilderBid{ - Version: consensusspec.DataVersionCapella, - Capella: &apicapella.SignedBuilderBid{ + return &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.SignedBuilderBid{ Message: message, Signature: signature, }, } - case consensusspec.DataVersionUnknown, consensusspec.DataVersionPhase0, consensusspec.DataVersionAltair, consensusspec.DataVersionDeneb: + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: return nil } return nil @@ -243,7 +245,7 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) if m.GetHeaderResponse != nil { response = m.GetHeaderResponse @@ -257,9 +259,9 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { // MakeGetPayloadResponse is used to create the default or can be used to create a custom response to the getPayload // method -func (m *mockRelay) MakeGetPayloadResponse(parentHash, blockHash, feeRecipient string, blockNumber uint64) *api.VersionedExecutionPayload { - return &api.VersionedExecutionPayload{ - Version: consensusspec.DataVersionCapella, +func (m *mockRelay) MakeGetPayloadResponse(parentHash, blockHash, feeRecipient string, blockNumber uint64, version spec.DataVersion) *builderApi.VersionedSubmitBlindedBlockResponse { + return &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: version, Capella: &capella.ExecutionPayload{ ParentHash: _HexToHash(parentHash), BlockHash: _HexToHash(blockHash), @@ -294,6 +296,7 @@ func (m *mockRelay) defaultHandleGetPayload(w http.ResponseWriter) { "0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46", "0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941", 12345, + spec.DataVersionCapella, ) if m.GetPayloadResponse != nil { diff --git a/server/mock_types_test.go b/server/mock_types_test.go index 5002a537..adc156e5 100644 --- a/server/mock_types_test.go +++ b/server/mock_types_test.go @@ -3,7 +3,7 @@ package server import ( "testing" - capellaapi "github.com/attestantio/go-builder-client/api/capella" + builderApiCapella "github.com/attestantio/go-builder-client/api/capella" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -202,7 +202,7 @@ func TestHexToSignature(t *testing.T) { publicKey := hexutil.Encode(bls.PublicKeyToBytes(blsPublicKey)) - message := &capellaapi.BuilderBid{ + message := &builderApiCapella.BuilderBid{ Header: &capella.ExecutionPayloadHeader{ BlockHash: _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7"), }, diff --git a/server/service.go b/server/service.go index 4a9a9b2c..9efc4a87 100644 --- a/server/service.go +++ b/server/service.go @@ -15,14 +15,16 @@ import ( "sync/atomic" "time" - "github.com/attestantio/go-builder-client/api" - apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-builder-client/spec" - apiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" - "github.com/attestantio/go-eth2-client/api/v1/capella" + builderApi "github.com/attestantio/go-builder-client/api" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + builderSpec "github.com/attestantio/go-builder-client/spec" + eth2ApiV1Bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella" + eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/flashbots/go-boost-utils/ssz" "github.com/flashbots/go-boost-utils/types" + "github.com/flashbots/go-boost-utils/utils" "github.com/flashbots/go-utils/httplogger" "github.com/flashbots/mev-boost/config" "github.com/google/uuid" @@ -51,8 +53,8 @@ type httpErrorResp struct { // AuctionTranscript is the bid and blinded block received from the relay send to the relay monitor type AuctionTranscript struct { - Bid *spec.VersionedSignedBuilderBid // TODO: proper json marshalling and unmashalling - Acceptance *apiv1bellatrix.SignedBlindedBeaconBlock `json:"acceptance"` + Bid *builderSpec.VersionedSignedBuilderBid // TODO: proper json marshalling and unmarshalling + Acceptance *eth2ApiV1Bellatrix.SignedBlindedBeaconBlock `json:"acceptance"` } type slotUID struct { @@ -213,7 +215,7 @@ func (m *BoostService) startBidCacheCleanupTask() { } } -func (m *BoostService) sendValidatorRegistrationsToRelayMonitors(payload []apiv1.SignedValidatorRegistration) { +func (m *BoostService) sendValidatorRegistrationsToRelayMonitors(payload []builderApiV1.SignedValidatorRegistration) { log := m.log.WithField("method", "sendValidatorRegistrationsToRelayMonitors").WithField("numRegistrations", len(payload)) for _, relayMonitor := range m.relayMonitors { go func(relayMonitor *url.URL) { @@ -265,7 +267,7 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. log := m.log.WithField("method", "registerValidator") log.Debug("registerValidator") - payload := []apiv1.SignedValidatorRegistration{} + payload := []builderApiV1.SignedValidatorRegistration{} if err := DecodeJSON(req.Body, &payload); err != nil { m.respondError(w, http.StatusBadRequest, err.Error()) return @@ -377,7 +379,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) path := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, parentHashHex, pubkey) url := relay.GetURI(path) log := log.WithField("url", url) - responsePayload := new(spec.VersionedSignedBuilderBid) + responsePayload := new(builderSpec.VersionedSignedBuilderBid) code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) if err != nil { log.WithError(err).Warn("error making request to relay") @@ -497,7 +499,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) log.WithFields(logrus.Fields{ "blockHash": result.bidInfo.blockHash.String(), "blockNumber": result.bidInfo.blockNumber, - "txRoot": result.bidInfo.txRoot, + "txRoot": result.bidInfo.txRoot.String(), "value": valueEth.Text('f', 18), "relays": strings.Join(RelayEntriesToStrings(result.relays), ", "), }).Info("best bid") @@ -512,7 +514,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) m.respondOK(w, &result.response) } -func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, payload *capella.SignedBlindedBeaconBlock, body []byte) { +func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, payload *eth2ApiV1Capella.SignedBlindedBeaconBlock, body []byte) { if payload.Message == nil || payload.Message.Body == nil || payload.Message.Body.ExecutionPayloadHeader == nil { log.WithField("body", string(body)).Error("missing parts of the request payload from the beacon-node") m.respondError(w, http.StatusBadRequest, "missing parts of the payload") @@ -559,7 +561,7 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re log.Warn("bid found but no associated relays") } - // send bid and signed block to relay monitor with capella payload + // send bid and signed block to relay monitor with eth2ApiV1Capella payload // go m.sendAuctionTranscriptToRelayMonitors(&AuctionTranscript{Bid: originalBid.response.Data, Acceptance: payload}) // Add request headers @@ -568,7 +570,7 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re // Prepare for requests var wg sync.WaitGroup var mu sync.Mutex - result := new(api.VersionedExecutionPayload) + result := new(builderApi.VersionedSubmitBlindedBlockResponse) // Prepare the request context, which will be cancelled after the first successful response from a relay requestCtx, requestCtxCancel := context.WithCancel(context.Background()) @@ -582,7 +584,7 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re log := log.WithField("url", url) log.Debug("calling getPayload") - responsePayload := new(api.VersionedExecutionPayload) + responsePayload := new(builderApi.VersionedSubmitBlindedBlockResponse) _, err := SendHTTPRequestWithRetries(requestCtx, m.httpClientGetPayload, http.MethodPost, url, ua, headers, payload, responsePayload, m.requestMaxRetries, log) if err != nil { if errors.Is(requestCtx.Err(), context.Canceled) { @@ -593,7 +595,7 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re return } - if responsePayload.Capella == nil || responsePayload.Capella.BlockHash == nilHash { + if getPayloadResponseIsEmpty(responsePayload) { log.Error("response with empty data!") return } @@ -607,7 +609,10 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re } // Ensure the response blockhash matches the response block - calculatedBlockHash, err := ComputeBlockHash(responsePayload.Capella) + calculatedBlockHash, err := utils.ComputeBlockHash(&builderApi.VersionedExecutionPayload{ + Version: responsePayload.Version, + Capella: responsePayload.Capella, + }, nil) if err != nil { log.WithError(err).Error("could not calculate block hash") } else if responsePayload.Capella.BlockHash != calculatedBlockHash { @@ -646,6 +651,143 @@ func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Re m.respondOK(w, result) } +func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, blindedBlock *eth2ApiV1Deneb.SignedBlindedBeaconBlock) { + // Get the currentSlotUID for this slot + currentSlotUID := "" + m.slotUIDLock.Lock() + if m.slotUID.slot == uint64(blindedBlock.Message.Slot) { + currentSlotUID = m.slotUID.uid.String() + } else { + log.Warnf("latest slotUID is for slot %d rather than payload slot %d", m.slotUID.slot, blindedBlock.Message.Slot) + } + m.slotUIDLock.Unlock() + + // Prepare logger + ua := UserAgent(req.Header.Get("User-Agent")) + log = log.WithFields(logrus.Fields{ + "ua": ua, + "slot": blindedBlock.Message.Slot, + "blockHash": blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash.String(), + "parentHash": blindedBlock.Message.Body.ExecutionPayloadHeader.ParentHash.String(), + "slotUID": currentSlotUID, + }) + + // Log how late into the slot the request starts + slotStartTimestamp := m.genesisTime + uint64(blindedBlock.Message.Slot)*config.SlotTimeSec + msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 + log.WithFields(logrus.Fields{ + "genesisTime": m.genesisTime, + "slotTimeSec": config.SlotTimeSec, + "msIntoSlot": msIntoSlot, + }).Infof("submitBlindedBlock request start - %d milliseconds into slot %d", msIntoSlot, blindedBlock.Message.Slot) + + // Get the bid! + bidKey := bidRespKey{slot: uint64(blindedBlock.Message.Slot), blockHash: blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash.String()} + m.bidsLock.Lock() + originalBid := m.bids[bidKey] + m.bidsLock.Unlock() + if originalBid.response.IsEmpty() { + log.Error("no bid for this getPayload payload found, was getHeader called before?") + } else if len(originalBid.relays) == 0 { + log.Warn("bid found but no associated relays") + } + + // Add request headers + headers := map[string]string{HeaderKeySlotUID: currentSlotUID} + + // Prepare for requests + var wg sync.WaitGroup + var mu sync.Mutex + result := new(builderApi.VersionedSubmitBlindedBlockResponse) + + // Prepare the request context, which will be cancelled after the first successful response from a relay + requestCtx, requestCtxCancel := context.WithCancel(context.Background()) + defer requestCtxCancel() + + for _, relay := range m.relays { + wg.Add(1) + go func(relay RelayEntry) { + defer wg.Done() + url := relay.GetURI(pathGetPayload) + log := log.WithField("url", url) + log.Debug("calling getPayload") + + responsePayload := new(builderApi.VersionedSubmitBlindedBlockResponse) + _, err := SendHTTPRequestWithRetries(requestCtx, m.httpClientGetPayload, http.MethodPost, url, ua, headers, blindedBlock, responsePayload, m.requestMaxRetries, log) + if err != nil { + if errors.Is(requestCtx.Err(), context.Canceled) { + log.Info("request was cancelled") // this is expected, if payload has already been received by another relay + } else { + log.WithError(err).Error("error making request to relay") + } + return + } + + if getPayloadResponseIsEmpty(responsePayload) { + log.Error("response with empty data!") + return + } + + payload := responsePayload.Deneb.ExecutionPayload + blobs := responsePayload.Deneb.BlobsBundle + + // Ensure the response blockhash matches the request + if blindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash != payload.BlockHash { + log.WithFields(logrus.Fields{ + "responseBlockHash": payload.BlockHash.String(), + }).Error("requestBlockHash does not equal responseBlockHash") + return + } + + commitments := blindedBlock.Message.Body.BlobKZGCommitments + // Ensure that blobs are valid and matches the request + if len(commitments) != len(blobs.Blobs) || len(commitments) != len(blobs.Commitments) || len(commitments) != len(blobs.Proofs) { + log.WithFields(logrus.Fields{ + "requestBlobs": len(commitments), + }).Error("block KZG commitment length does not equal responseBlobs length") + return + } + + for i, commitment := range commitments { + if commitment != blobs.Commitments[i] { + log.WithFields(logrus.Fields{ + "requestBlobCommitment": commitment.String(), + "responseBlobCommitment": blobs.Commitments[i].String(), + "index": i, + }).Error("requestBlobCommitment does not equal responseBlobCommitment") + return + } + } + + // Lock before accessing the shared payload + mu.Lock() + defer mu.Unlock() + + if requestCtx.Err() != nil { // request has been cancelled (or deadline exceeded) + return + } + + // Received successful response. Now cancel other requests and return immediately + requestCtxCancel() + *result = *responsePayload + log.Info("received payload from relay") + }(relay) + } + + // Wait for all requests to complete... + wg.Wait() + + // If no payload has been received from relay, log loudly about withholding! + if getPayloadResponseIsEmpty(result) { + originRelays := RelayEntriesToStrings(originalBid.relays) + log.WithField("relaysWithBid", strings.Join(originRelays, ", ")).Error("no payload received from relay!") + m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error()) + return + } + + m.respondOK(w, result) +} + func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request) { log := m.log.WithField("method", "getPayload") log.Debug("getPayload request starts") @@ -659,13 +801,19 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request } // Decode the body now - payload := new(capella.SignedBlindedBeaconBlock) + payload := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock) if err := DecodeJSON(bytes.NewReader(body), payload); err != nil { - log.WithError(err).WithField("body", string(body)).Error("could not decode request payload from the beacon-node (signed blinded beacon block)") - m.respondError(w, http.StatusBadRequest, err.Error()) + log.Debug("could not decode Deneb request payload, attempting to decode body into Capella payload") + payload := new(eth2ApiV1Capella.SignedBlindedBeaconBlock) + if err := DecodeJSON(bytes.NewReader(body), payload); err != nil { + log.WithError(err).WithField("body", string(body)).Error("could not decode request payload from the beacon-node (signed blinded beacon block)") + m.respondError(w, http.StatusBadRequest, err.Error()) + return + } + m.processCapellaPayload(w, req, log, payload, body) return } - m.processCapellaPayload(w, req, log, payload, body) + m.processDenebPayload(w, req, log, payload) } // CheckRelays sends a request to each one of the relays previously registered to get their status diff --git a/server/service_test.go b/server/service_test.go index 01bcc9db..72bcfade 100644 --- a/server/service_test.go +++ b/server/service_test.go @@ -14,16 +14,19 @@ import ( "testing" "time" - "github.com/attestantio/go-builder-client/api" - apiv1 "github.com/attestantio/go-builder-client/api/v1" - "github.com/attestantio/go-builder-client/spec" - apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" - consensusspec "github.com/attestantio/go-eth2-client/spec" + builderApi "github.com/attestantio/go-builder-client/api" + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" + builderSpec "github.com/attestantio/go-builder-client/spec" + eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella" + eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" - bellatrixutil "github.com/attestantio/go-eth2-client/util/bellatrix" + eth2UtilBellatrix "github.com/attestantio/go-eth2-client/util/bellatrix" "github.com/flashbots/go-boost-utils/types" "github.com/holiman/uint256" "github.com/prysmaticlabs/go-bitfield" @@ -87,7 +90,7 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) * return rr } -func blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock *apiv1capella.SignedBlindedBeaconBlock) *capella.ExecutionPayload { +func blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock *eth2ApiV1Capella.SignedBlindedBeaconBlock) *capella.ExecutionPayload { header := signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader return &capella.ExecutionPayload{ ParentHash: header.ParentHash, @@ -108,6 +111,39 @@ func blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock *apiv1capell } } +func blindedBlockContentsToPayloadDeneb(signedBlindedBlockContents *eth2ApiV1Deneb.SignedBlindedBeaconBlock) *builderApiDeneb.ExecutionPayloadAndBlobsBundle { + header := signedBlindedBlockContents.Message.Body.ExecutionPayloadHeader + numBlobs := len(signedBlindedBlockContents.Message.Body.BlobKZGCommitments) + commitments := make([]deneb.KZGCommitment, numBlobs) + copy(commitments, signedBlindedBlockContents.Message.Body.BlobKZGCommitments) + proofs := make([]deneb.KZGProof, numBlobs) + blobs := make([]deneb.Blob, numBlobs) + return &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ + ExecutionPayload: &deneb.ExecutionPayload{ + ParentHash: header.ParentHash, + FeeRecipient: header.FeeRecipient, + StateRoot: header.StateRoot, + ReceiptsRoot: header.ReceiptsRoot, + LogsBloom: header.LogsBloom, + PrevRandao: header.PrevRandao, + BlockNumber: header.BlockNumber, + GasLimit: header.GasLimit, + GasUsed: header.GasUsed, + Timestamp: header.Timestamp, + ExtraData: header.ExtraData, + BaseFeePerGas: header.BaseFeePerGas, + BlockHash: header.BlockHash, + Transactions: make([]bellatrix.Transaction, 0), + Withdrawals: make([]*capella.Withdrawal, 0), + }, + BlobsBundle: &builderApiDeneb.BlobsBundle{ + Commitments: commitments, + Proofs: proofs, + Blobs: blobs, + }, + } +} + func TestNewBoostServiceErrors(t *testing.T) { t.Run("errors when no relays", func(t *testing.T) { _, err := NewBoostService(BoostServiceOpts{ @@ -189,7 +225,7 @@ func TestStatus(t *testing.T) { rr := backend.request(t, http.MethodGet, path, nil) require.Equal(t, http.StatusOK, rr.Code) - require.True(t, len(rr.Header().Get("X-MEVBoost-Version")) > 0) + require.Greater(t, len(rr.Header().Get("X-MEVBoost-Version")), 0) //nolint:testifylint require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) }) @@ -201,15 +237,15 @@ func TestStatus(t *testing.T) { rr := backend.request(t, http.MethodGet, path, nil) require.Equal(t, http.StatusServiceUnavailable, rr.Code) - require.True(t, len(rr.Header().Get("X-MEVBoost-Version")) > 0) + require.Greater(t, len(rr.Header().Get("X-MEVBoost-Version")), 0) //nolint:testifylint require.Equal(t, 0, backend.relays[0].GetRequestCount(path)) }) } func TestRegisterValidator(t *testing.T) { path := "/eth/v1/builder/validators" - reg := apiv1.SignedValidatorRegistration{ - Message: &apiv1.ValidatorRegistration{ + reg := builderApiV1.SignedValidatorRegistration{ + Message: &builderApiV1.ValidatorRegistration{ FeeRecipient: _HexToAddress("0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941"), Timestamp: time.Unix(1234356, 0), Pubkey: _HexToPubkey( @@ -218,7 +254,7 @@ func TestRegisterValidator(t *testing.T) { Signature: _HexToSignature( "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"), } - payload := []apiv1.SignedValidatorRegistration{reg} + payload := []builderApiV1.SignedValidatorRegistration{reg} t.Run("Normal function", func(t *testing.T) { backend := newTestBackend(t, 1, time.Second) @@ -290,6 +326,21 @@ func TestGetHeader(t *testing.T) { require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) }) + t.Run("Okay response from relay deneb", func(t *testing.T) { + backend := newTestBackend(t, 1, time.Second) + resp := backend.relays[0].MakeGetHeaderResponse( + 12345, + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", + spec.DataVersionDeneb, + ) + backend.relays[0].GetHeaderResponse = resp + rr := backend.request(t, http.MethodGet, path, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + }) + t.Run("Bad response from relays", func(t *testing.T) { backend := newTestBackend(t, 2, time.Second) resp := backend.relays[0].MakeGetHeaderResponse( @@ -297,9 +348,9 @@ func TestGetHeader(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionBellatrix, + spec.DataVersionCapella, ) - resp.Bellatrix.Message.Header.BlockHash = nilHash + resp.Capella.Message.Header.BlockHash = nilHash // 1/2 failing responses are okay backend.relays[0].GetHeaderResponse = resp @@ -324,7 +375,7 @@ func TestGetHeader(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionBellatrix, + spec.DataVersionCapella, ) // Simulate a different public key registered to mev-boost @@ -346,11 +397,11 @@ func TestGetHeader(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionBellatrix, + spec.DataVersionCapella, ) // Scramble the signature - backend.relays[0].GetHeaderResponse.Bellatrix.Signature = phase0.BLSSignature{} + backend.relays[0].GetHeaderResponse.Capella.Signature = phase0.BLSSignature{} rr := backend.request(t, http.MethodGet, path, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) @@ -418,7 +469,7 @@ func TestGetHeaderBids(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // First relay will return signed response with value 12347. @@ -427,7 +478,7 @@ func TestGetHeaderBids(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // First relay will return signed response with value 12346. @@ -436,7 +487,7 @@ func TestGetHeaderBids(t *testing.T) { "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // Run the request. @@ -450,7 +501,7 @@ func TestGetHeaderBids(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) // Highest value should be 12347, i.e. second relay. - resp := new(spec.VersionedSignedBuilderBid) + resp := new(builderSpec.VersionedSignedBuilderBid) err := json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) value, err := resp.Value() @@ -467,7 +518,7 @@ func TestGetHeaderBids(t *testing.T) { "0xa38385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) backend.relays[1].GetHeaderResponse = backend.relays[1].MakeGetHeaderResponse( @@ -475,7 +526,7 @@ func TestGetHeaderBids(t *testing.T) { "0xa18385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) backend.relays[2].GetHeaderResponse = backend.relays[2].MakeGetHeaderResponse( @@ -483,7 +534,7 @@ func TestGetHeaderBids(t *testing.T) { "0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // Run the request. @@ -497,7 +548,7 @@ func TestGetHeaderBids(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) // Highest value should be 12347, i.e. second relay. - resp := new(spec.VersionedSignedBuilderBid) + resp := new(builderSpec.VersionedSignedBuilderBid) err := json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) @@ -519,7 +570,7 @@ func TestGetHeaderBids(t *testing.T) { "0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // Run the request. @@ -542,7 +593,7 @@ func TestGetHeaderBids(t *testing.T) { "0xa28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) // Run the request. @@ -552,7 +603,7 @@ func TestGetHeaderBids(t *testing.T) { require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) // Value should be 12345 (min bid is 12345) - resp := new(spec.VersionedSignedBuilderBid) + resp := new(builderSpec.VersionedSignedBuilderBid) err := json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) value, err := resp.Value() @@ -565,15 +616,15 @@ func TestGetPayload(t *testing.T) { path := "/eth/v1/builder/blinded_blocks" blockHash := _HexToHash("0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46") - payload := &apiv1capella.SignedBlindedBeaconBlock{ + payload := ð2ApiV1Capella.SignedBlindedBeaconBlock{ Signature: _HexToSignature( "0x8c795f751f812eabbabdee85100a06730a9904a4b53eedaa7f546fe0e23cd75125e293c6b0d007aa68a9da4441929d16072668abb4323bb04ac81862907357e09271fe414147b3669509d91d8ffae2ec9c789a5fcd4519629b8f2c7de8d0cce9"), - Message: &apiv1capella.BlindedBeaconBlock{ + Message: ð2ApiV1Capella.BlindedBeaconBlock{ Slot: 1, ProposerIndex: 1, ParentRoot: phase0.Root{0x01}, StateRoot: phase0.Root{0x02}, - Body: &apiv1capella.BlindedBeaconBlockBody{ + Body: ð2ApiV1Capella.BlindedBeaconBlockBody{ RANDAOReveal: phase0.BLSSignature{0xa1}, ETH1Data: &phase0.ETH1Data{ BlockHash: blockHash[:], @@ -603,7 +654,7 @@ func TestGetPayload(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) - resp := new(api.VersionedExecutionPayload) + resp := new(builderApi.VersionedSubmitBlindedBlockResponse) err := json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) require.Equal(t, payload.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash) @@ -611,8 +662,8 @@ func TestGetPayload(t *testing.T) { t.Run("Bad response from relays", func(t *testing.T) { backend := newTestBackend(t, 2, time.Second) - resp := &api.VersionedExecutionPayload{ - Version: consensusspec.DataVersionCapella, + resp := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, Capella: &capella.ExecutionPayload{Withdrawals: []*capella.Withdrawal{}}, } @@ -715,7 +766,7 @@ func TestCheckRelays(t *testing.T) { } func TestEmptyTxRoot(t *testing.T) { - transactions := bellatrixutil.ExecutionPayloadTransactions{Transactions: []bellatrix.Transaction{}} + transactions := eth2UtilBellatrix.ExecutionPayloadTransactions{Transactions: []bellatrix.Transaction{}} txroot, _ := transactions.HashTreeRoot() txRootHex := fmt.Sprintf("0x%x", txroot) require.Equal(t, "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1", txRootHex) @@ -733,12 +784,12 @@ func TestGetPayloadWithTestdata(t *testing.T) { jsonFile, err := os.Open(fn) require.NoError(t, err) defer jsonFile.Close() - signedBlindedBeaconBlock := new(apiv1capella.SignedBlindedBeaconBlock) + signedBlindedBeaconBlock := new(eth2ApiV1Capella.SignedBlindedBeaconBlock) require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock)) backend := newTestBackend(t, 1, time.Second) - mockResp := api.VersionedExecutionPayload{ - Version: consensusspec.DataVersionCapella, + mockResp := builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, Capella: &capella.ExecutionPayload{ BlockHash: signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, Withdrawals: make([]*capella.Withdrawal, 0), @@ -750,7 +801,7 @@ func TestGetPayloadWithTestdata(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) - resp := new(api.VersionedExecutionPayload) + resp := new(builderApi.VersionedSubmitBlindedBlockResponse) err = json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) require.Equal(t, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash) @@ -763,14 +814,14 @@ func TestGetPayloadCapella(t *testing.T) { jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-capella.json") require.NoError(t, err) defer jsonFile.Close() - signedBlindedBeaconBlock := new(apiv1capella.SignedBlindedBeaconBlock) + signedBlindedBeaconBlock := new(eth2ApiV1Capella.SignedBlindedBeaconBlock) require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock)) backend := newTestBackend(t, 1, time.Second) // Prepare getPayload response - backend.relays[0].GetPayloadResponse = &api.VersionedExecutionPayload{ - Version: consensusspec.DataVersionCapella, + backend.relays[0].GetPayloadResponse = &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, Capella: blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock), } @@ -780,18 +831,46 @@ func TestGetPayloadCapella(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) require.Equal(t, 1, backend.relays[0].GetRequestCount(getPayloadPath)) - resp := new(api.VersionedExecutionPayload) + resp := new(builderApi.VersionedSubmitBlindedBlockResponse) err = json.Unmarshal(rr.Body.Bytes(), resp) require.NoError(t, err) require.Equal(t, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash) } +func TestGetPayloadDeneb(t *testing.T) { + // Load the signed blinded beacon block used for getPayload + jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-deneb.json") + require.NoError(t, err) + defer jsonFile.Close() + signedBlindedBlock := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock) + require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBlock)) + + backend := newTestBackend(t, 1, time.Second) + + // Prepare getPayload response + backend.relays[0].GetPayloadResponse = &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + Deneb: blindedBlockContentsToPayloadDeneb(signedBlindedBlock), + } + + // call getPayload, ensure it's only called on relay 0 (origin of the bid) + getPayloadPath := "/eth/v1/builder/blinded_blocks" + rr := backend.request(t, http.MethodPost, getPayloadPath, signedBlindedBlock) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(getPayloadPath)) + + resp := new(builderApi.VersionedSubmitBlindedBlockResponse) + err = json.Unmarshal(rr.Body.Bytes(), resp) + require.NoError(t, err) + require.Equal(t, signedBlindedBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Deneb.ExecutionPayload.BlockHash) +} + func TestGetPayloadToAllRelays(t *testing.T) { // Load the signed blinded beacon block used for getPayload jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-capella.json") require.NoError(t, err) defer jsonFile.Close() - signedBlindedBeaconBlock := new(apiv1capella.SignedBlindedBeaconBlock) + signedBlindedBeaconBlock := new(eth2ApiV1Capella.SignedBlindedBeaconBlock) require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock)) // Create a test backend with 2 relays @@ -804,7 +883,7 @@ func TestGetPayloadToAllRelays(t *testing.T) { "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", - consensusspec.DataVersionCapella, + spec.DataVersionCapella, ) rr := backend.request(t, http.MethodGet, getHeaderPath, nil) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) @@ -812,8 +891,8 @@ func TestGetPayloadToAllRelays(t *testing.T) { require.Equal(t, 1, backend.relays[1].GetRequestCount(getHeaderPath)) // Prepare getPayload response - backend.relays[0].GetPayloadResponse = &api.VersionedExecutionPayload{ - Version: consensusspec.DataVersionCapella, + backend.relays[0].GetPayloadResponse = &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, Capella: blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock), } diff --git a/server/utils.go b/server/utils.go index b4576710..fb779d53 100644 --- a/server/utils.go +++ b/server/utils.go @@ -13,13 +13,12 @@ import ( "strings" "time" - "github.com/attestantio/go-builder-client/spec" - "github.com/attestantio/go-eth2-client/spec/capella" + builderApi "github.com/attestantio/go-builder-client/api" + builderSpec "github.com/attestantio/go-builder-client/spec" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/trie" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/ssz" "github.com/flashbots/mev-boost/config" @@ -35,7 +34,6 @@ const ( var ( errHTTPErrorResponse = errors.New("HTTP error response") errInvalidForkVersion = errors.New("invalid fork version") - errInvalidTransaction = errors.New("invalid transaction") errMaxRetriesExceeded = errors.New("max retries exceeded") ) @@ -168,7 +166,7 @@ func GetURI(url *url.URL, path string) string { // bidResp are entries in the bids cache type bidResp struct { t time.Time - response spec.VersionedSignedBuilderBid + response builderSpec.VersionedSignedBuilderBid bidInfo bidInfo relays []RelayEntry } @@ -201,65 +199,7 @@ func weiBigIntToEthBigFloat(wei *big.Int) (ethValue *big.Float) { return } -func ComputeBlockHash(payload *capella.ExecutionPayload) (phase0.Hash32, error) { - header, err := executionPayloadToBlockHeader(payload) - if err != nil { - return phase0.Hash32{}, err - } - return phase0.Hash32(header.Hash()), nil -} - -func executionPayloadToBlockHeader(payload *capella.ExecutionPayload) (*types.Header, error) { - transactionData := make([]*types.Transaction, len(payload.Transactions)) - for i, encTx := range payload.Transactions { - var tx types.Transaction - - if err := tx.UnmarshalBinary(encTx); err != nil { - return nil, errInvalidTransaction - } - transactionData[i] = &tx - } - - withdrawalData := make([]*types.Withdrawal, len(payload.Withdrawals)) - for i, w := range payload.Withdrawals { - withdrawalData[i] = &types.Withdrawal{ - Index: uint64(w.Index), - Validator: uint64(w.ValidatorIndex), - Address: common.Address(w.Address), - Amount: uint64(w.Amount), - } - } - withdrawalsHash := types.DeriveSha(types.Withdrawals(withdrawalData), trie.NewStackTrie(nil)) - - // base fee per gas is stored little-endian but we need it - // big-endian for big.Int. - var baseFeePerGasBytes [32]byte - for i := 0; i < 32; i++ { - baseFeePerGasBytes[i] = payload.BaseFeePerGas[32-1-i] - } - baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBytes[:]) - - return &types.Header{ - ParentHash: common.Hash(payload.ParentHash), - UncleHash: types.EmptyUncleHash, - Coinbase: common.Address(payload.FeeRecipient), - Root: payload.StateRoot, - TxHash: types.DeriveSha(types.Transactions(transactionData), trie.NewStackTrie(nil)), - ReceiptHash: payload.ReceiptsRoot, - Bloom: payload.LogsBloom, - Difficulty: common.Big0, - Number: new(big.Int).SetUint64(payload.BlockNumber), - GasLimit: payload.GasLimit, - GasUsed: payload.GasUsed, - Time: payload.Timestamp, - Extra: payload.ExtraData, - MixDigest: payload.PrevRandao, - BaseFee: baseFeePerGas, - WithdrawalsHash: &withdrawalsHash, - }, nil -} - -func parseBidInfo(bid *spec.VersionedSignedBuilderBid) (bidInfo, error) { +func parseBidInfo(bid *builderSpec.VersionedSignedBuilderBid) (bidInfo, error) { blockHash, err := bid.BlockHash() if err != nil { return bidInfo{}, err @@ -295,7 +235,7 @@ func parseBidInfo(bid *spec.VersionedSignedBuilderBid) (bidInfo, error) { return bidInfo, nil } -func checkRelaySignature(bid *spec.VersionedSignedBuilderBid, domain phase0.Domain, pubKey phase0.BLSPubKey) (bool, error) { +func checkRelaySignature(bid *builderSpec.VersionedSignedBuilderBid, domain phase0.Domain, pubKey phase0.BLSPubKey) (bool, error) { root, err := bid.MessageHashTreeRoot() if err != nil { return false, err @@ -312,3 +252,21 @@ func checkRelaySignature(bid *spec.VersionedSignedBuilderBid, domain phase0.Doma return bls.VerifySignatureBytes(msg[:], sig[:], pubKey[:]) } + +func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockResponse) bool { + switch payload.Version { + case spec.DataVersionCapella: + if payload.Capella == nil || payload.Capella.BlockHash == nilHash { + return true + } + case spec.DataVersionDeneb: + if payload.Deneb == nil || payload.Deneb.ExecutionPayload == nil || + payload.Deneb.ExecutionPayload.BlockHash == nilHash || + payload.Deneb.BlobsBundle == nil { + return true + } + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: + return true + } + return false +} diff --git a/server/utils_test.go b/server/utils_test.go index a2d0f2bd..7cea3ca8 100644 --- a/server/utils_test.go +++ b/server/utils_test.go @@ -8,10 +8,14 @@ import ( "math/big" "net/http" "net/http/httptest" - "os" "testing" + builderApi "github.com/attestantio/go-builder-client/api" + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/flashbots/mev-boost/config" "github.com/stretchr/testify/require" ) @@ -97,15 +101,100 @@ func TestWeiBigIntToEthBigFloat(t *testing.T) { require.Equal(t, "0.000000000000000000", f.Text('f', 18)) } -func TestCapellaComputeBlockHash(t *testing.T) { - jsonFile, err := os.Open("../testdata/zhejiang-execution-payload-capella.json") - require.NoError(t, err) - defer jsonFile.Close() +func TestGetPayloadResponseIsEmpty(t *testing.T) { + t.Run("Non-empty capella payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, + Capella: &capella.ExecutionPayload{ + BlockHash: phase0.Hash32{0x1}, + }, + } + require.False(t, getPayloadResponseIsEmpty(payload)) + }) - payload := new(capella.ExecutionPayload) - require.NoError(t, DecodeJSON(jsonFile, payload)) + t.Run("Non-empty deneb payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ + ExecutionPayload: &deneb.ExecutionPayload{ + BlockHash: phase0.Hash32{0x1}, + }, + BlobsBundle: &builderApiDeneb.BlobsBundle{ + Blobs: make([]deneb.Blob, 0), + Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + }, + }, + } + require.False(t, getPayloadResponseIsEmpty(payload)) + }) - hash, err := ComputeBlockHash(payload) - require.NoError(t, err) - require.Equal(t, "0x08751ea2076d3ecc606231495a90ba91a66a9b8fb1a2b76c333f1957a1c667c3", hash.String()) + t.Run("Empty capella payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Nil block hash for capella payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionCapella, + Capella: &capella.ExecutionPayload{ + BlockHash: nilHash, + }, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Empty deneb payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Empty deneb execution payload", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ + BlobsBundle: &builderApiDeneb.BlobsBundle{ + Blobs: make([]deneb.Blob, 0), + Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + }, + }, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Empty deneb blobs bundle", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ + ExecutionPayload: &deneb.ExecutionPayload{ + BlockHash: phase0.Hash32{0x1}, + }, + }, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Nil block hash for deneb payload response", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ + ExecutionPayload: &deneb.ExecutionPayload{ + BlockHash: nilHash, + }, + }, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) + + t.Run("Unsupported payload version", func(t *testing.T) { + payload := &builderApi.VersionedSubmitBlindedBlockResponse{ + Version: spec.DataVersionBellatrix, + } + require.True(t, getPayloadResponseIsEmpty(payload)) + }) } diff --git a/testdata/signed-blinded-beacon-block-deneb.json b/testdata/signed-blinded-beacon-block-deneb.json new file mode 100644 index 00000000..4a9d67b6 --- /dev/null +++ b/testdata/signed-blinded-beacon-block-deneb.json @@ -0,0 +1,307 @@ +{ + "message": { + "slot": "348241", + "proposer_index": "35822", + "parent_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "state_root": "0x4f6e0857501da4ab1d72f0c122869e1c084e16daa96613b64914aada28d0dc28", + "body": { + "randao_reveal": "0xb2b7d2e89bb4a4aa6a377972651bb9041cb59af8eedd19568d699fc0866189d3fd78cc93c0e63877b7e2bd6d34d1597c0afd4508aa99b6e882c2cb1ac6f424adba29afd46d1737124300ad72177715fcce8584dd25a06c45bfe9a8ccabd6175d", + "eth1_data": { + "deposit_root": "0x704964a5ad034a440f4f29ff1875986db66adbca45dc0014e439349c7e10194f", + "deposit_count": "4933", + "block_hash": "0x9c69de2814a7c3e3751654511372937627dacc3187bf457892789f3f5533c794" + }, + "graffiti": "0x74656b752d626573750000000000000000000000000000000000000000000000", + "proposer_slashings": [], + "attester_slashings": [], + "attestations": [ + { + "aggregation_bits": "0xffffffffffffffffffffffffffffff5fff", + "data": { + "slot": "348240", + "index": "7", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x92da7bf78eb364219f85af2388d7ac7ddbea1934786d75875486ec9fceb310eee131dcfea131bdf4593d3c431b31b2900bb48ebb7ab02e17524d86a4e132883246df8ce427e935dd9e20c422cdf8eb135b3cc45b86fe4c2f592fb4899eb22f7c" + }, + { + "aggregation_bits": "0xffdffffffffffff5fffffffffffffffffd", + "data": { + "slot": "348240", + "index": "3", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x88cce1f9fbf651e52a6ab71dea2f4025702021744aac90fb2997f82bac6c192e295ae39b2a430546cb102bf8b68f687e0f40a5179bc293e1424e37d694ef1ad6b3b8de72a0e7fbbe97aeafe6f47e949d415381fbbb090e3135224d5b324eefcb" + }, + { + "aggregation_bits": "0xffffffffffefffffffffbfffff7fffdfff", + "data": { + "slot": "348240", + "index": "11", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x82186d946dfde2ab3b5fcf5bd858fadeec7fa9729f28527e209d16a1d9b4d635558cad6f8de8cee12caa2a4fc5459fb911ca17cbbecfd22e83c82e244ad7a8c8c849a1e03ee88bf0d338c633c2acfefd142574897cd78f9076b69f6e370e3751" + }, + { + "aggregation_bits": "0xfffdffffffffffffffffffffffffffffff", + "data": { + "slot": "348240", + "index": "4", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0xb9c5ee50f800fe2619d1d0fe46a2fb00a64bcf2613e46a40b579ce6a39c4f6cd71a46790757ccc3df8f5f82e34c77c8d084f525ea8a4bd5bd10190496644be0740ace3d217e43af15229a8023d58e583cfec849fab10169225444f4f4ecc66a8" + }, + { + "aggregation_bits": "0xffffffffeffffffffffffdffffffffffff", + "data": { + "slot": "348240", + "index": "12", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x91e4ee5c0d6bb54d6856cee653c6859f635cebf9c51bef524d6f439cf6d1c69bea5fcb3e4c067c178cfa4518a76baba909b18189a864f38020f44b2cd5223a11e42d58aaedfa2f004710a72a704357874850842a1493017eca6e473d01395932" + }, + { + "aggregation_bits": "0xffffffffffffffffffffffffffffffffff", + "data": { + "slot": "348240", + "index": "13", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x8c376c5bb5ada745ba8cb8ce2aae103f4e3f85549ceaacaf312b1fa8e6d2ee5232149a926dcfd58ffa1f50f710eb4edc10943bbd40a601f2fb4d53104a59c0663a397744b59f1fa0744bba49f22afc3bab47045ebb42e61dac41ad44c6bf28f4" + }, + { + "aggregation_bits": "0xfffffffffffffffffffffffffffeffffff", + "data": { + "slot": "348240", + "index": "1", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0xaf11c64ce957f2a1686d12b07d0fbc170d89e48490e326cd73ef761ba042bddc01e48e5fc39953c6113df0a989d75e750d5b9d75155259508c2bbdd53903967f893e24f2f7f751f4a05b0fb1cb2b9084ce8543690a8a623599308d6c190fca4a" + }, + { + "aggregation_bits": "0xfffffff5fffffffffdfffbbffffff7ffff", + "data": { + "slot": "348240", + "index": "6", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x91075da401796a4341ab9a850ff330c9b0d996ca12b9970ec15a4b40fee652edd043e0c9f9d81529621b3a7970e676f619d7a39af67bf193af4441b5447f199f02d75a26c32181569cddc0a237b7064971539f80811fe40e9362d4d9242404ed" + }, + { + "aggregation_bits": "0xfffffffdfffffffffffffdfffffeffffff", + "data": { + "slot": "348240", + "index": "0", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0xad9aa5aa9b9c022036fbb81a0aca626b19a2ccd7c7ee6efa5b2a454f5ffb5d75d00e5563b31319b3a0ad1e0ef6f512be00fb8c39243004a1133610344473953dfcf06c3bd53f00255de6983927acd8624b0131fe9d8a085062747d70972b4713" + }, + { + "aggregation_bits": "0xffffffffffff7fdfffffffffffffffffff", + "data": { + "slot": "348240", + "index": "2", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x89dde857bc31a4cc5e71d6cc440c00b2b1ee85b758722aadc5c4da0a939523de7532aabcfef4e80f84094924bb69d80d0a3d702b85859c5fce0433b6d0f7bc302af866ef7a9234a75be7bbd91b32256126808ffdf65ac0ce07a33afbaa16c575" + }, + { + "aggregation_bits": "0xffffffffffffffffffffffffffffffffff", + "data": { + "slot": "348240", + "index": "8", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x891fbba500eee2cf2f5734d3bf8445e8684376e47469692d44e87fc8a295616d9f29410afc2d6ff2bc649618b33b417e13de4e152099aac054f4d35df4cd79234b6df1edcf2393b7ebc0f2ecf61f4604232b96830e0dbff9311408dad4479667" + }, + { + "aggregation_bits": "0xfffffffffffbffffffffefffffffff5fff", + "data": { + "slot": "348240", + "index": "14", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0xb9ab0354d0d61eb6b5f2184dc3bd0c8416cca74f2c913c6aaca653a87dd2c4b8ba2471aa450e0fa170573637c49dc8920eb84970fea4230d7b3c3c8c8152c782e912b29bc19a6de05dc36c1b44db2f649f31673b4751e1b22f17021833ca9cc8" + }, + { + "aggregation_bits": "0xfffffebffffbf7ffeffffbffffffffffff", + "data": { + "slot": "348240", + "index": "5", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x8e17aa835e6343f708e73e432b7268741f60565f5bb6ef62b5fba892438ca5474a14c0382609e14624058f3fab120e8902ad3f667cf14418836ce120f3bbf50ea3c15923c881e599227cc3a2758ef9a2cd08bd3b862bd711a875e27477ac347c" + }, + { + "aggregation_bits": "0xfffffffffffefffffffffffeffffffffff", + "data": { + "slot": "348240", + "index": "10", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0xa0909f67a7745300cee6278605e3cb79e5a9564cd6e81ac379b285e0eb6e1849537326b546079d7bf676c8e33a166cad00ab74a396f12c9f4851fb64612f6aeb911db550e0aeae88e1b90831a5a858ae64f9262f67403327d85fcb345df9fca4" + }, + { + "aggregation_bits": "0xffffffffffffffffffffffffffffffffff", + "data": { + "slot": "348240", + "index": "9", + "beacon_block_root": "0x15bd2273ad32344e34f842fc77ad8acb2a2eaedafa6e5328ef799babfe81113d", + "source": { + "epoch": "10881", + "root": "0x12a21e7bb91e09dac76d5d3f170db6358785032f10b9130a1e92e6f4409f2ecf" + }, + "target": { + "epoch": "10882", + "root": "0x1c8a9a3a0d4c9d72a93b9ff2ea442a986f4d6dfde52953e48a146206393e7708" + } + }, + "signature": "0x886d038ddd7598cfda720dfe1caf83e030e24b207bbc0c97d012fbf5accbaa2f63366f32fe643aa1fdf6c8282480cd51165710bb786d77ecfb72ef7cc9d55e342c94fb57f5a75d50a0d486ecdf014bb08e0195f24202911c86efb5b46b2167ab" + } + ], + "deposits": [], + "voluntary_exits": [], + "sync_aggregate": { + "sync_committee_bits": "0xffffffffffffffffffff7edfffffffff7ffffffffffffffffffffffffffff7ffdf7ffffffff7fffffefffffffffffffbfffffffffdffffffffffffffffffffff", + "sync_committee_signature": "0x877de19f5fff89de5af36954d4c7f7f5c2ccf6f8dc39fe7e3eb87c3357fca26f0af747dffd0f992c8844a20763e9f8a51858d0be70ce610055c6421d340160adec0ddcb706a7d7a5c45edff7b5f9b9a039fce093cea41c7a5699834f9de48b94" + }, + "execution_payload_header": { + "parent_hash": "0xa330251430b91a6fb5342f30a1f527dc76499c03a411464235951dbd51b94d9f", + "fee_recipient": "0xf97e180c050e5Ab072211Ad2C213Eb5AEE4DF134", + "state_root": "0x079f2cc22a29388fd4fc20f451cbaa3ff39845d68b2c368ff7be314617418e38", + "receipts_root": "0xed980a4cf6df8ba330c14ed9fe0597ec20515f44e5a9adfd2f7b72aa14890996", + "logs_bloom": "0x0000000400000008000008000040000000000000000000001000104880000200000004000000400000000204000020002000000000000000000000000022000800000004000000000002000c000000000000000000000100000000000000000000000000000000000000000000000040000000000040000001000014000000010002104000000000000000000000000000000000000000000000000000000080020000000000000000002400000000000001000000000002000200102000000040100002000000000000000000000000000000000000000800000000000000000010000000000000000000000000000000000400002000000000000000200000", + "prev_randao": "0x86cc02ef030b0c147321a7f94158c1b33cb730f8baac3c59955b983fda3ae39b", + "block_number": "330714", + "gas_limit": "30000000", + "gas_used": "369098", + "timestamp": "1679442492", + "extra_data": "0x", + "base_fee_per_gas": "7", + "block_hash": "0x4ab1ced57222819bf6a6b6c1456715011585599a1cef18b060eb364811bbb14e", + "transactions_root": "0x6d47bae3b4963cbde00ec39bbd6442540afe26f8005e73722489904836008bfc", + "withdrawals_root": "0x5dc5f3ff8bade8e1dd04e5cf56292b2a194a2829e1c8e8b4a627d95e08296ba3", + "blob_gas_used": "4438756708366371443", + "excess_blob_gas": "12504111653614393862" + }, + "bls_to_execution_changes": [], + "blob_kzg_commitments": [ + "0x95cc5099bbd8420d8ebade383c00a2346dace60a7604f768cd71501757b4d72eeb7d5474a6b615af10379d69aa9f478f", + "0xae9f2d2217013ef61f995f9074faead9ec24e8048440164ec3d6029b87d43686dd0c97c2df9554fc997d0d66c3a78929" + ] + } + }, + "signature": "0x8c3095fd9d3a18e43ceeb7648281e16bb03044839dffea796432c4e5a1372bef22c11a98a31e0c1c5389b98cc6d45917170a0f1634bcf152d896f360dc599fabba2ec4de77898b5dff080fa1628482bdbad5b37d2e64fea3d8721095186cfe50" +}