Skip to content

Commit

Permalink
Capella decode and fallback to bellatrix (#431)
Browse files Browse the repository at this point in the history
* Capella decode and fallback to bellatrix

* update attestant library
  • Loading branch information
avalonche authored Jan 20, 2023
1 parent ef4cc85 commit 12974b4
Show file tree
Hide file tree
Showing 7 changed files with 946 additions and 47 deletions.
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

0 comments on commit 12974b4

Please sign in to comment.