diff --git a/common/ssz_test.go b/common/ssz_test.go index ced962d8e..88955436a 100644 --- a/common/ssz_test.go +++ b/common/ssz_test.go @@ -192,4 +192,15 @@ func BenchmarkDecoding(b *testing.B) { require.NoError(b, err) } }) + + jsonBytes, err = os.ReadFile("../testdata/getHeaderResponseElectra_Goerli.json") + require.NoError(b, err) + + payload = new(builderSpec.VersionedSignedBuilderBid) + b.Run("electra json", func(b *testing.B) { + for range b.N { + err = json.Unmarshal(jsonBytes, &payload) + require.NoError(b, err) + } + }) } diff --git a/common/test_utils.go b/common/test_utils.go index 777b9f356..73f11e838 100644 --- a/common/test_utils.go +++ b/common/test_utils.go @@ -12,12 +12,14 @@ import ( 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" + builderApiElectra "github.com/attestantio/go-builder-client/api/electra" 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/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" + "github.com/attestantio/go-eth2-client/spec/electra" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/ssz" @@ -178,7 +180,31 @@ func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint25 ProposerPubkey: proposerPk, } - if version == spec.DataVersionDeneb { + switch version { + case spec.DataVersionElectra: + payload = &VersionedSubmitBlockRequest{ + VersionedSubmitBlockRequest: builderSpec.VersionedSubmitBlockRequest{ //nolint:exhaustruct + Version: version, + Electra: &builderApiElectra.SubmitBlockRequest{ + Message: bidTrace, + ExecutionPayload: &deneb.ExecutionPayload{ //nolint:exhaustruct + BaseFeePerGas: uint256.NewInt(0), + }, + BlobsBundle: &builderApiDeneb.BlobsBundle{ //nolint:exhaustruct + Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + Blobs: make([]deneb.Blob, 0), + }, + ExecutionRequests: &electra.ExecutionRequests{ //nolint:exhaustruct + Deposits: make([]*electra.DepositRequest, 0), + Withdrawals: make([]*electra.WithdrawalRequest, 0), + Consolidations: make([]*electra.ConsolidationRequest, 0), + }, + Signature: phase0.BLSSignature{}, + }, + }, + } + case spec.DataVersionDeneb: payload = &VersionedSubmitBlockRequest{ VersionedSubmitBlockRequest: builderSpec.VersionedSubmitBlockRequest{ //nolint:exhaustruct Version: version, @@ -189,12 +215,14 @@ func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint25 }, BlobsBundle: &builderApiDeneb.BlobsBundle{ //nolint:exhaustruct Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + Blobs: make([]deneb.Blob, 0), }, Signature: phase0.BLSSignature{}, }, }, } - } else { + default: payload = &VersionedSubmitBlockRequest{ VersionedSubmitBlockRequest: builderSpec.VersionedSubmitBlockRequest{ //nolint:exhaustruct Version: version, diff --git a/common/types_spec_test.go b/common/types_spec_test.go index 87615d7f2..9324352d3 100644 --- a/common/types_spec_test.go +++ b/common/types_spec_test.go @@ -74,6 +74,10 @@ func TestSignedBlindedBlockJSON(t *testing.T) { name: "Deneb", filepath: "../testdata/signedBlindedBeaconBlockDeneb_Goerli.json.gz", }, + { + name: "Electra", + filepath: "../testdata/signedBlindedBeaconBlockElectra.json.gz", + }, } for _, testCase := range testCases { diff --git a/database/typesconv_test.go b/database/typesconv_test.go index 8155c08e3..aa28c6686 100644 --- a/database/typesconv_test.go +++ b/database/typesconv_test.go @@ -46,3 +46,23 @@ func TestExecutionPayloadEntryToExecutionPayloadDeneb(t *testing.T) { require.Equal(t, "0xbd1ae4f7edb2315d2df70a8d9881fab8d6763fb1c00533ae729050928c38d05a", payload.Deneb.ExecutionPayload.BlockHash.String()) require.Len(t, payload.Deneb.BlobsBundle.Blobs, 1) } + +func TestExecutionPayloadEntryToExecutionPayloadElectra(t *testing.T) { + filename := "../testdata/executionPayloadAndBlobsBundleElectra_Goerli.json.gz" + payloadBytes := common.LoadGzippedBytes(t, filename) + entry := &ExecutionPayloadEntry{ + ID: 123, + Slot: 6, + InsertedAt: time.Unix(1685616301, 0), + + ProposerPubkey: "0x8419cf00f2783c430dc861a710984d0429d3b3a7f6db849b4f5c05e0d87339704c5c7f5eede6adfc8776d666587b5932", + BlockHash: "0x8bc5ee08e27659360187285be156c31daa8e37513a5df7e006322e1268f6b4e1", + Version: common.ForkVersionStringElectra, + Payload: string(payloadBytes), + } + + payload, err := ExecutionPayloadEntryToExecutionPayload(entry) + require.NoError(t, err) + require.Equal(t, "0x8bc5ee08e27659360187285be156c31daa8e37513a5df7e006322e1268f6b4e1", payload.Electra.ExecutionPayload.BlockHash.String()) + require.Len(t, payload.Electra.BlobsBundle.Blobs, 1) +} diff --git a/datastore/datastore_test.go b/datastore/datastore_test.go index 973615109..36c10914d 100644 --- a/datastore/datastore_test.go +++ b/datastore/datastore_test.go @@ -48,6 +48,11 @@ func TestGetPayloadDatabaseFallback(t *testing.T) { filename: "../testdata/executionPayloadAndBlobsBundleDeneb_Goerli.json.gz", version: common.ForkVersionStringDeneb, blockHash: "0xbd1ae4f7edb2315d2df70a8d9881fab8d6763fb1c00533ae729050928c38d05a", + }, { + description: "Good Electra Payload", + filename: "../testdata/executionPayloadAndBlobsBundleElectra_Goerli.json.gz", + version: common.ForkVersionStringElectra, + blockHash: "0x8bc5ee08e27659360187285be156c31daa8e37513a5df7e006322e1268f6b4e1", }, } diff --git a/services/api/service_test.go b/services/api/service_test.go index e0e766cfe..43141e5db 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -13,6 +13,7 @@ import ( "github.com/alicebob/miniredis/v2" builderApiCapella "github.com/attestantio/go-builder-client/api/capella" builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiElectra "github.com/attestantio/go-builder-client/api/electra" builderApiV1 "github.com/attestantio/go-builder-client/api/v1" builderSpec "github.com/attestantio/go-builder-client/spec" "github.com/attestantio/go-eth2-client/spec" @@ -420,6 +421,45 @@ func TestGetHeader(t *testing.T) { require.NoError(t, err) require.Equal(t, bidValue.String(), value.String()) + // Create an electra bid + path = fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot+2, parentHash, proposerPubkey) + opts = common.CreateTestBlockSubmissionOpts{ + Slot: slot + 2, + ParentHash: parentHash, + ProposerPubkey: proposerPubkey, + Version: spec.DataVersionElectra, + } + payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, builderPubkey, bidValue, &opts) + _, err = backend.redis.SaveBidAndUpdateTopBid(t.Context(), backend.redis.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + require.NoError(t, err) + + // Check: JSON electra request works and returns a bid + rr = backend.request(http.MethodGet, path, nil) + require.Equal(t, http.StatusOK, rr.Code) + resp = builderSpec.VersionedSignedBuilderBid{} + err = json.Unmarshal(rr.Body.Bytes(), &resp) + require.NoError(t, err) + value, err = resp.Value() + require.NoError(t, err) + require.Equal(t, spec.DataVersionElectra, resp.Version) + require.Equal(t, bidValue.String(), value.String()) + + // Check: SSZ electra request works and returns a bid + rr = backend.requestBytes(http.MethodGet, path, nil, &http.Header{ + "Accept": []string{"application/octet-stream"}, + }) + require.Equal(t, http.StatusOK, rr.Code) + require.Equal(t, ApplicationOctetStream, rr.Header().Get("Content-Type")) + require.Equal(t, common.EthConsensusVersionElectra, rr.Header().Get("Eth-Consensus-Version")) + resp = builderSpec.VersionedSignedBuilderBid{} + resp.Version = spec.DataVersionElectra + resp.Electra = new(builderApiElectra.SignedBuilderBid) + err = resp.Electra.UnmarshalSSZ(rr.Body.Bytes()) + require.NoError(t, err) + value, err = resp.Value() + require.NoError(t, err) + require.Equal(t, bidValue.String(), value.String()) + // Check: Request returns 204 if sending a filtered user agent rr = backend.requestWithUA(http.MethodGet, path, "mev-boost/v1.5.0 Go-http-client/1.1", nil) require.Equal(t, http.StatusNoContent, rr.Code) @@ -1497,6 +1537,31 @@ func TestCheckProposerSignature(t *testing.T) { require.NoError(t, err) require.False(t, ok) }) + + t.Run("Invalid Electra Signature", func(t *testing.T) { + jsonBytes := common.LoadGzippedBytes(t, "../../testdata/signedBlindedBeaconBlockElectra.json.gz") + payload := new(common.VersionedSignedBlindedBeaconBlock) + err := json.Unmarshal(jsonBytes, payload) + require.NoError(t, err) + // change signature + signature, err := utils.HexToSignature( + "0x942d85822e86a182b0a535361b379015a03e5ce4416863d3baa46b42eef06f070462742b79fbc77c0802699ba6d2ab00" + + "11740dad6bfcf05b1f15c5a11687ae2aa6a08c03ad1ff749d7a48e953d13b5d7c2bd1da4cfcf30ba6d918b587d6525f0", + ) + require.NoError(t, err) + payload.Electra.Signature = signature + // start backend with goerli network + _, _, backend := startTestBackend(t) + goerli, err := common.NewEthNetworkDetails(common.EthNetworkGoerli) + require.NoError(t, err) + backend.relay.opts.EthNetDetails = *goerli + // check signature + pubkey, err := utils.HexToPubkey("0x8322b8af5c6d97e855cc75ad19d59b381a880630cded89268c14acb058cf3c5720ebcde5fa6087dcbb64dbd826936148") + require.NoError(t, err) + ok, err := backend.relay.checkProposerSignature(payload, pubkey[:]) + require.NoError(t, err) + require.False(t, ok) + }) } func gzipBytes(t *testing.T, b []byte) []byte { @@ -1542,3 +1607,78 @@ func TestNegotiateRequestResponseType(t *testing.T) { }) } } + +func TestGetHeaderErrorCases(t *testing.T) { + backend := newTestBackend(t, 1) + backend.relay.genesisInfo = &beaconclient.GetGenesisResponse{ + Data: beaconclient.GetGenesisResponseData{ + GenesisTime: uint64(time.Now().UTC().Unix()), //nolint:gosec + }, + } + + currentSlot := uint64(100) + backend.relay.headSlot.Store(currentSlot) + + testCases := []struct { + name string + path string + expectedStatus int + expectedError string + }{ + { + name: "too short proposer pubkey", + path: "/eth/v1/builder/header/101/0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b9747/0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07", + expectedStatus: http.StatusBadRequest, + expectedError: "invalid pubkey", + }, + { + name: "too short parent hash", + path: "/eth/v1/builder/header/101/0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b/0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792", + expectedStatus: http.StatusBadRequest, + expectedError: "invalid hash", + }, + { + name: "past slot", + path: "/eth/v1/builder/header/50/0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b9747/0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792", + expectedStatus: http.StatusBadRequest, + expectedError: "slot is too old", + }, + { + name: "no bid available", + path: "/eth/v1/builder/header/101/0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b9747/0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792", + expectedStatus: http.StatusNoContent, + expectedError: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rr := backend.request(http.MethodGet, tc.path, nil) + require.Equal(t, tc.expectedStatus, rr.Code) + + if tc.expectedError != "" { + require.Contains(t, rr.Body.String(), tc.expectedError) + } + }) + } +} + +func TestDataAPIValidation(t *testing.T) { + backend := newTestBackend(t, 1) + backend.relay.opts.DataAPI = true + + t.Run("invalid block hash", func(t *testing.T) { + invalidBlockHash := "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" // invalid block hash + path := fmt.Sprintf("/relay/v1/data/bidtraces/proposer_payload_delivered?block_hash=%s", invalidBlockHash) + rr := backend.request(http.MethodGet, path, nil) + require.Equal(t, http.StatusBadRequest, rr.Code) + require.Contains(t, rr.Body.String(), "invalid block_hash argument") + }) + + t.Run("no query args passed", func(t *testing.T) { + path := "/relay/v1/data/bidtraces/builder_blocks_received" + rr := backend.request(http.MethodGet, path, nil) + require.Equal(t, http.StatusBadRequest, rr.Code) + require.Contains(t, rr.Body.String(), "need to query for specific") + }) +} diff --git a/testdata/executionPayloadAndBlobsBundleElectra_Goerli.json.gz b/testdata/executionPayloadAndBlobsBundleElectra_Goerli.json.gz new file mode 100644 index 000000000..d9ccee2a2 Binary files /dev/null and b/testdata/executionPayloadAndBlobsBundleElectra_Goerli.json.gz differ diff --git a/testdata/getHeaderResponseElectra_Goerli.json b/testdata/getHeaderResponseElectra_Goerli.json new file mode 100644 index 000000000..d14d742b0 --- /dev/null +++ b/testdata/getHeaderResponseElectra_Goerli.json @@ -0,0 +1,40 @@ +{ + "version": "electra", + "data": { + "message": { + "header": { + "parent_hash": "0x401cb97d8ae89069c36bf027624b56b7d0c89dafaf79ecb9a7f294a5424f58fa", + "fee_recipient": "0xb9e79d19f651a941757b35830232e7efc77e1c79", + "state_root": "0x9dd36d0d093f0592e21991841514d057f9d62cce01d8710b9420f4c22e0af670", + "receipts_root": "0x5ffd9c97a67f9cbecc87bbc9652743972cb44fc5721ff46077c8fb3eecdab0a8", + "logs_bloom": "0x80010013100000200a0c000001010448408808010200404400020002000605004200900604042000500008400000004000c65240243321040018875052640000000a0046a40010080008061de2228001c00000100340209080088004000000a0013200014680400000001880800048520d041c040000000c001000150088080894040a20000008892000028903000041c0010480801410c6807131104440000003089051110000000b0301c08001000983170d03010002c000440224260001204080c00318004040000220164014030824400000008000000000000200042118051003000810804d2802000080800801020010c88200004508080000120e0000", + "prev_randao": "0x40beda946f9d8a953d54bc1391a70b9e43ed7049d428f3eb40cad8f52d5b6620", + "block_number": "61", + "gas_limit": "56528600", + "gas_used": "4091092", + "timestamp": "1757602339", + "extra_data": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465", + "base_fee_per_gas": "2634175", + "block_hash": "0xc7d2184cf2d63b982c903191cc745fac8a458c8355cbd3f3abe65da136fd2f0c", + "transactions_root": "0x5ffd9c97a67f9cbecc87bbc9652743972cb44fc5721ff46077c8fb3eecdab0a8", + "withdrawals_root": "0x70ab14824381c971b39cad93b2066b8e9389c89d683ccdb3b56a2ff911b4ff68", + "blob_gas_used": "786432", + "excess_blob_gas": "86769664" + }, + "blob_kzg_commitments": [ + "0x925dc2f404733af142fc38971923fc0dacba73f05f9d48898db64eaeb5592b8dec479e17c210afe4da1dd5992a420372", + "0xb627f3266ffa6495ce5ed4b65f50e2b36f1c48c01e21623ea31e9d4c0551213bd140e6d1188b51d858fd44501d36a4b6", + "0x97ac0e61c1b61cbeba4c689c9e566d841f2d74c89c9c498b0fa1a4ffb67eb7c318e3751e113e9cdff2cbae1a0d9430a3", + "0xa047e2e792db2145db9bb355d8ec15048092c577515d5ae225f5f4e02d66ddec47a1560e56c66c6ad4a6f167d7378aba", + "0x8675d067e035359584c459351e17d38bee3f3c58ea426a601f15bc9269c62e6742454ba074f2ad555b4977c451101c60", + "0x8a32210f4220c68903a84b98b3a7bf89878265f888cf5ea166aa3cd1bfa54f7778cefe8cfc885cf9f8a90a42780a0926", + "0xb7263d6b80894bbff7528e0adbee73c0241730169c24c8e895cceed5f7da26f86a42ce7a2d3cded362b06dc5e56961df", + "0x8a67bdd84dd9a9467b50db1a65ccdcbfa2b12c2896b2fffdf79dedb50e3c81ea82be5ae6f2e07d5338bbd9437ee56bab" + ], + "value": "37851860177112740", + "pubkey": "0xafa4c6985aa049fb79dd37010438cfebeb0f2bd42b115b89dd678dab0670c1de38da0c4e9138c9290a398ecd9a0b3110" + }, + "signature": "0x8019394ecb7c08e8379a2fffc52a8fcb3d484d4ff75944e3c11c94e11ebb03d0862b82a4298c0503222a9e22d307755919e085af787e2af1956c0a3caf68481be7ab2be69895a155308a267dfe8631af050c820ceeacb5717eedacfa6b72a236" + } + } + \ No newline at end of file diff --git a/testdata/signedBlindedBeaconBlockElectra.json.gz b/testdata/signedBlindedBeaconBlockElectra.json.gz new file mode 100644 index 000000000..944c6920e Binary files /dev/null and b/testdata/signedBlindedBeaconBlockElectra.json.gz differ