Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capella decode and fallback to bellatrix #431

Merged
merged 2 commits into from
Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,31 @@ require (
github.com/stretchr/testify v1.8.1
)

require (
github.com/fatih/color v1.13.0 // indirect
github.com/goccy/go-yaml v1.9.6 // indirect
github.com/holiman/uint256 v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
)

require (
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
github.com/attestantio/go-builder-client v0.2.6-0.20230105014332-e601ac7db862
github.com/attestantio/go-eth2-client v0.15.1
github.com/btcsuite/btcd v0.22.0-beta // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/ferranbt/fastssz v0.1.2-0.20220723134332-b3d3034a4575 // indirect
github.com/ferranbt/fastssz v0.1.2 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
Expand All @@ -40,7 +52,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/sys v0.2.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
478 changes: 470 additions & 8 deletions go.sum

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions server/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"testing"
"time"

"github.com/attestantio/go-builder-client/api"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/flashbots/go-boost-utils/bls"
"github.com/flashbots/go-boost-utils/types"
Expand Down Expand Up @@ -49,8 +50,9 @@ type mockRelay struct {
handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request)

// Default responses placeholders, used if overrider does not exist
GetHeaderResponse *types.GetHeaderResponse
GetPayloadResponse *types.GetPayloadResponse
GetHeaderResponse *types.GetHeaderResponse
GetBellatrixPayloadResponse *types.GetPayloadResponse
GetCapellaPayloadResponse *api.VersionedExecutionPayload

// Server section
Server *httptest.Server
Expand Down Expand Up @@ -235,15 +237,24 @@ func (m *mockRelay) handleGetPayload(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

if m.GetCapellaPayloadResponse != nil {
if err := json.NewEncoder(w).Encode(m.GetCapellaPayloadResponse); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
return
}

// Build the default response.
response := m.MakeGetPayloadResponse(
"0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0x534809bd2b6832edff8d8ce4cb0e50068804fd1ef432c8362ad708a74fdc0e46",
"0xdb65fEd33dc262Fe09D9a2Ba8F80b329BA25f941",
12345,
)
if m.GetPayloadResponse != nil {
response = m.GetPayloadResponse

if m.GetBellatrixPayloadResponse != nil {
response = m.GetBellatrixPayloadResponse
}

if err := json.NewEncoder(w).Encode(response); err != nil {
Expand Down
165 changes: 145 additions & 20 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"sync/atomic"
"time"

"github.com/attestantio/go-builder-client/api"
"github.com/attestantio/go-eth2-client/api/v1/capella"
"github.com/flashbots/go-boost-utils/types"
"github.com/flashbots/go-utils/httplogger"
"github.com/flashbots/mev-boost/config"
Expand Down Expand Up @@ -450,26 +452,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)
m.respondOK(w, result.response)
}

func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request) {
log := m.log.WithField("method", "getPayload")
log.Debug("getPayload")

// Read the body first, so we can log it later on error
body, err := io.ReadAll(req.Body)
if err != nil {
log.WithError(err).Error("could not read body of request from the beacon node")
m.respondError(w, http.StatusBadRequest, err.Error())
return
}

// Decode the body now
payload := new(types.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
}

func (m *BoostService) processBellatrixPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, payload *types.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")
Expand Down Expand Up @@ -582,6 +565,148 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request
m.respondOK(w, result)
}

func (m *BoostService) processCapellaPayload(w http.ResponseWriter, req *http.Request, log *logrus.Entry, payload *capella.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")
return
}

log = log.WithFields(logrus.Fields{
"slot": payload.Message.Slot,
"blockHash": payload.Message.Body.ExecutionPayloadHeader.BlockHash.String(),
"parentHash": payload.Message.Body.ExecutionPayloadHeader.ParentHash.String(),
})

bidKey := bidRespKey{slot: uint64(payload.Message.Slot), blockHash: payload.Message.Body.ExecutionPayloadHeader.BlockHash.String()}
m.bidsLock.Lock()
originalBid := m.bids[bidKey]
m.bidsLock.Unlock()
if originalBid.blockHash == "" {
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")
}

// send bid and signed block to relay monitor with capella payload
// go m.sendAuctionTranscriptToRelayMonitors(&AuctionTranscript{Bid: originalBid.response.Data, Acceptance: payload})

relays := originalBid.relays
if len(relays) == 0 {
log.Warn("originating relay not found, sending getPayload request to all relays")
relays = m.relays
}

var wg sync.WaitGroup
var mu sync.Mutex
result := new(api.VersionedExecutionPayload)
ua := UserAgent(req.Header.Get("User-Agent"))

// 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 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(api.VersionedExecutionPayload)
_, err := SendHTTPRequest(requestCtx, m.httpClientGetPayload, http.MethodPost, url, ua, payload, responsePayload)
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 responsePayload.Capella == nil || types.Hash(responsePayload.Capella.BlockHash) == nilHash {
log.Error("response with empty data!")
return
}

// Ensure the response blockhash matches the request
if payload.Message.Body.ExecutionPayloadHeader.BlockHash != responsePayload.Capella.BlockHash {
log.WithFields(logrus.Fields{
"responseBlockHash": responsePayload.Capella.BlockHash.String(),
}).Error("requestBlockHash does not equal responseBlockHash")
return
}

// Ensure the response blockhash matches the response block
calculatedBlockHash, err := ComputeBlockHash(responsePayload.Capella)
if err != nil {
log.WithError(err).Error("could not calculate block hash")
} else if responsePayload.Capella.BlockHash != calculatedBlockHash {
log.WithFields(logrus.Fields{
"calculatedBlockHash": calculatedBlockHash.String(),
"responseBlockHash": responsePayload.Capella.BlockHash.String(),
}).Error("responseBlockHash does not equal hash calculated from response block")
}

// 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 result.Capella == nil || types.Hash(result.Capella.BlockHash) == nilHash {
originRelays := RelayEntriesToStrings(originalBid.relays)
log.WithField("relays", 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")

// Read the body first, so we can log it later on error
body, err := io.ReadAll(req.Body)
if err != nil {
log.WithError(err).Error("could not read body of request from the beacon node")
m.respondError(w, http.StatusBadRequest, err.Error())
return
}

// Decode the body now
payload := new(capella.SignedBlindedBeaconBlock)
if err := DecodeJSON(bytes.NewReader(body), payload); err != nil {
log.WithError(err).WithField("body", string(body)).Info("could not decode request payload from the beacon-node (capella signed blinded beacon block)")
log.Debug("attempting to decode payload body with bellatrix")
payload := new(types.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 (bellatrix signed blinded beacon block)")
m.respondError(w, http.StatusBadRequest, err.Error())
return
}
m.processBellatrixPayload(w, req, log, payload, body)
return
}
m.processCapellaPayload(w, req, log, payload, body)
}

// CheckRelays sends a request to each one of the relays previously registered to get their status
func (m *BoostService) CheckRelays() int {
var wg sync.WaitGroup
Expand Down
68 changes: 61 additions & 7 deletions server/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import (
"testing"
"time"

"github.com/attestantio/go-builder-client/api"
apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella"
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/flashbots/go-boost-utils/types"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -74,7 +79,7 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) *
return rr
}

func blindedBlockToExecutionPayload(signedBlindedBeaconBlock *types.SignedBlindedBeaconBlock) *types.ExecutionPayload {
func blindedBlockToExecutionPayloadBellatrix(signedBlindedBeaconBlock *types.SignedBlindedBeaconBlock) *types.ExecutionPayload {
header := signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader
return &types.ExecutionPayload{
ParentHash: header.ParentHash,
Expand All @@ -93,6 +98,27 @@ func blindedBlockToExecutionPayload(signedBlindedBeaconBlock *types.SignedBlinde
}
}

func blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock *apiv1capella.SignedBlindedBeaconBlock) *capella.ExecutionPayload {
header := signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader
return &capella.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),
}
}

func TestNewBoostServiceErrors(t *testing.T) {
t.Run("errors when no relays", func(t *testing.T) {
_, err := NewBoostService(BoostServiceOpts{testLog, ":123", []RelayEntry{}, []*url.URL{}, "0x00000000", true, types.IntToU256(0), time.Second, time.Second, time.Second})
Expand Down Expand Up @@ -564,15 +590,15 @@ func TestGetPayload(t *testing.T) {
resp := new(types.GetPayloadResponse)

// 1/2 failing responses are okay
backend.relays[0].GetPayloadResponse = resp
backend.relays[0].GetBellatrixPayloadResponse = resp
rr := backend.request(t, http.MethodPost, path, payload)
require.GreaterOrEqual(t, backend.relays[1].GetRequestCount(path)+backend.relays[0].GetRequestCount(path), 1)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())

// 2/2 failing responses are okay
backend = newTestBackend(t, 2, time.Second)
backend.relays[0].GetPayloadResponse = resp
backend.relays[1].GetPayloadResponse = resp
backend.relays[0].GetBellatrixPayloadResponse = resp
backend.relays[1].GetBellatrixPayloadResponse = resp
rr = backend.request(t, http.MethodPost, path, payload)
require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
Expand Down Expand Up @@ -648,7 +674,7 @@ func TestGetPayloadWithTestdata(t *testing.T) {
BlockHash: signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash,
},
}
backend.relays[0].GetPayloadResponse = &mockResp
backend.relays[0].GetBellatrixPayloadResponse = &mockResp

rr := backend.request(t, http.MethodPost, path, signedBlindedBeaconBlock)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
Expand All @@ -662,6 +688,34 @@ func TestGetPayloadWithTestdata(t *testing.T) {
}
}

func TestGetPayloadCapella(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)
require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock))

backend := newTestBackend(t, 1, time.Second)

// Prepare getPayload response
backend.relays[0].GetCapellaPayloadResponse = &api.VersionedExecutionPayload{
Version: consensusspec.DataVersionCapella,
Capella: blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock),
}

// 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, signedBlindedBeaconBlock)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
require.Equal(t, 1, backend.relays[0].GetRequestCount(getPayloadPath))

resp := new(api.VersionedExecutionPayload)
err = json.Unmarshal(rr.Body.Bytes(), resp)
require.NoError(t, err)
require.Equal(t, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash)
}

func TestGetPayloadToOriginRelayOnly(t *testing.T) {
// Load the signed blinded beacon block used for getPayload
jsonFile, err := os.Open("../testdata/kiln-signed-blinded-beacon-block-899730.json")
Expand All @@ -687,8 +741,8 @@ func TestGetPayloadToOriginRelayOnly(t *testing.T) {
require.Equal(t, 1, backend.relays[1].GetRequestCount(getHeaderPath))

// Prepare getPayload response
backend.relays[0].GetPayloadResponse = &types.GetPayloadResponse{
Data: blindedBlockToExecutionPayload(signedBlindedBeaconBlock),
backend.relays[0].GetBellatrixPayloadResponse = &types.GetPayloadResponse{
Data: blindedBlockToExecutionPayloadBellatrix(signedBlindedBeaconBlock),
}

// call getPayload, ensure it's only called on relay 0 (origin of the bid)
Expand Down
Loading