diff --git mev-boost/server/backend.go bolt-mev-boost/server/backend.go
index 3309d5410306960b5f5f1b346c4cf678b339f9be..8e4979dee7567c8807965e40193db303c0206f9e 100644
--- mev-boost/server/backend.go
+++ bolt-mev-boost/server/backend.go
@@ -2,10 +2,19 @@ package server
const (
// Router paths
- pathStatus = "/eth/v1/builder/status"
- pathRegisterValidator = "/eth/v1/builder/validators"
- pathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}"
- pathGetPayload = "/eth/v1/builder/blinded_blocks"
+ pathStatus = "/eth/v1/builder/status"
+ pathRegisterValidator = "/eth/v1/builder/validators"
+ pathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}"
+ pathGetHeaderWithProofs = "/eth/v1/builder/header_with_proofs/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}"
+ pathGetPayload = "/eth/v1/builder/blinded_blocks"
+
+ // Constraints namespace paths
+ // Ref: https://docs.boltprotocol.xyz/api/builder#constraints
+ pathSubmitConstraint = "/constraints/v1/builder/constraints"
+ // Ref: https://docs.boltprotocol.xyz/api/builder#delegate
+ pathDelegate = "/constraints/v1/builder/delegate"
+ // Ref: https://docs.boltprotocol.xyz/api/builder#revoke
+ pathRevoke = "/constraints/v1/builder/revoke"
// // Relay Monitor paths
// pathAuctionTranscript = "/monitor/v1/transcript"
diff --git mev-boost/server/mock_relay.go bolt-mev-boost/server/mock_relay.go
index fe6c6daa53afe223f6191390a64c833fe06b96f2..07fee42aad46bcaaa3e9314b1cef3e20ca03be9d 100644
--- mev-boost/server/mock_relay.go
+++ bolt-mev-boost/server/mock_relay.go
@@ -16,14 +16,17 @@ 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/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"
+ utilbellatrix "github.com/attestantio/go-eth2-client/util/bellatrix"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/flashbots/go-boost-utils/bls"
"github.com/flashbots/go-boost-utils/ssz"
"github.com/gorilla/mux"
"github.com/holiman/uint256"
+ "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
)
@@ -54,13 +57,16 @@ mu sync.Mutex
requestCount map[string]int
// Overriders
- handlerOverrideRegisterValidator func(w http.ResponseWriter, req *http.Request)
- handlerOverrideGetHeader func(w http.ResponseWriter, req *http.Request)
- handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request)
+ handlerOverrideRegisterValidator func(w http.ResponseWriter, req *http.Request)
+ handlerOverrideSubmitConstraint func(w http.ResponseWriter, req *http.Request)
+ handlerOverrideGetHeader func(w http.ResponseWriter, req *http.Request)
+ handlerOverrideGetHeaderWithProofs func(w http.ResponseWriter, req *http.Request)
+ handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request)
// Default responses placeholders, used if overrider does not exist
- GetHeaderResponse *builderSpec.VersionedSignedBuilderBid
- GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse
+ GetHeaderResponse *builderSpec.VersionedSignedBuilderBid
+ GetHeaderWithProofsResponse *VersionedSignedBuilderBidWithProofs
+ GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse
// Server section
Server *httptest.Server
@@ -115,6 +121,10 @@ r.HandleFunc("/", m.handleRoot).Methods(http.MethodGet)
r.HandleFunc(pathStatus, m.handleStatus).Methods(http.MethodGet)
r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost)
r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet)
+ r.HandleFunc(pathGetHeaderWithProofs, m.handleGetHeaderWithProofs).Methods(http.MethodGet)
+ r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost)
+ r.HandleFunc(pathDelegate, m.handleDelegate).Methods(http.MethodPost)
+ r.HandleFunc(pathRevoke, m.handleRevoke).Methods(http.MethodPost)
r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost)
return m.newTestMiddleware(r)
@@ -164,6 +174,84 @@ w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
+func (m *mockRelay) handleDelegate(w http.ResponseWriter, req *http.Request) {
+ payload := SignedDelegation{}
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+}
+
+func (m *mockRelay) handleRevoke(w http.ResponseWriter, req *http.Request) {
+ payload := SignedRevocation{}
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+}
+
+func (m *mockRelay) handleSubmitConstraint(w http.ResponseWriter, req *http.Request) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if m.handlerOverrideSubmitConstraint != nil {
+ m.handlerOverrideSubmitConstraint(w, req)
+ return
+ }
+ m.defaultHandleSubmitConstraint(w, req)
+}
+
+func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *http.Request) {
+ payload := BatchedSignedConstraints{}
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+}
+
+func (m *mockRelay) MakeGetHeaderWithConstraintsResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, constraints []struct {
+ tx Transaction
+ hash phase0.Hash32
+},
+) *VersionedSignedBuilderBidWithProofs {
+ transactions := new(utilbellatrix.ExecutionPayloadTransactions)
+
+ for _, con := range constraints {
+ transactions.Transactions = append(transactions.Transactions, bellatrix.Transaction(con.tx))
+ }
+
+ rootNode, err := transactions.GetTree()
+ if err != nil {
+ panic(err)
+ }
+
+ // BOLT: Set the value of nodes. This is MANDATORY for the proof calculation
+ // to output the leaf correctly. This is also never documented in fastssz. -__-
+ // Also calculates the transactions_root
+ txsRoot := rootNode.Hash()
+
+ bidWithProofs := m.MakeGetHeaderWithProofsResponseWithTxsRoot(value, blockHash, parentHash, publicKey, version, phase0.Root(txsRoot))
+
+ // Calculate the inclusion proof
+ inclusionProof, err := CalculateMerkleMultiProofs(rootNode, constraints)
+ if err != nil {
+ logrus.WithError(err).Error("failed to calculate inclusion proof")
+ return nil
+ }
+
+ bidWithProofs.Proofs = inclusionProof
+
+ return bidWithProofs
+}
+
// 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 spec.DataVersion) *builderSpec.VersionedSignedBuilderBid {
@@ -192,6 +280,7 @@ Signature: signature,
},
}
case spec.DataVersionDeneb:
+
message := &builderApiDeneb.BuilderBid{
Header: &deneb.ExecutionPayloadHeader{
BlockHash: _HexToHash(blockHash),
@@ -221,6 +310,70 @@ }
return nil
}
+// MakeGetHeaderWithProofsResponseWithTxsRoot is used to create the default or can be used to create a custom response to the getHeaderWithProofs
+// method
+func (m *mockRelay) MakeGetHeaderWithProofsResponseWithTxsRoot(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, txsRoot phase0.Root) *VersionedSignedBuilderBidWithProofs {
+ switch version {
+ case spec.DataVersionCapella:
+ // Fill the payload with custom values.
+ message := &builderApiCapella.BuilderBid{
+ Header: &capella.ExecutionPayloadHeader{
+ BlockHash: _HexToHash(blockHash),
+ ParentHash: _HexToHash(parentHash),
+ WithdrawalsRoot: phase0.Root{},
+ TransactionsRoot: txsRoot,
+ },
+ 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 &VersionedSignedBuilderBidWithProofs{
+ VersionedSignedBuilderBid: &builderSpec.VersionedSignedBuilderBid{
+ Version: spec.DataVersionCapella,
+ Capella: &builderApiCapella.SignedBuilderBid{
+ Message: message,
+ Signature: signature,
+ },
+ },
+ }
+ case spec.DataVersionDeneb:
+
+ message := &builderApiDeneb.BuilderBid{
+ Header: &deneb.ExecutionPayloadHeader{
+ BlockHash: _HexToHash(blockHash),
+ ParentHash: _HexToHash(parentHash),
+ WithdrawalsRoot: phase0.Root{},
+ BaseFeePerGas: uint256.NewInt(0),
+ TransactionsRoot: txsRoot,
+ },
+ 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 &VersionedSignedBuilderBidWithProofs{
+ VersionedSignedBuilderBid: &builderSpec.VersionedSignedBuilderBid{
+ Version: spec.DataVersionDeneb,
+ Deneb: &builderApiDeneb.SignedBuilderBid{
+ Message: message,
+ Signature: signature,
+ },
+ },
+ }
+ case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix:
+ return nil
+ }
+ return nil
+}
+
// handleGetHeader handles incoming requests to server.pathGetHeader
func (m *mockRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) {
m.mu.Lock()
@@ -247,8 +400,47 @@ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
"0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
spec.DataVersionCapella,
)
+
if m.GetHeaderResponse != nil {
response = m.GetHeaderResponse
+ }
+
+ if err := json.NewEncoder(w).Encode(response); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
+// handleGetHeaderWithProofs handles incoming requests to server.pathGetHeader
+func (m *mockRelay) handleGetHeaderWithProofs(w http.ResponseWriter, req *http.Request) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ // Try to override default behavior is custom handler is specified.
+ if m.handlerOverrideGetHeader != nil {
+ m.handlerOverrideGetHeaderWithProofs(w, req)
+ return
+ }
+ m.defaultHandleGetHeaderWithProofs(w)
+}
+
+// defaultHandleGetHeaderWithProofs returns the default handler for handleGetHeaderWithProofs
+func (m *mockRelay) defaultHandleGetHeaderWithProofs(w http.ResponseWriter) {
+ // By default, everything will be ok.
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ // Build the default response.
+ response := m.MakeGetHeaderWithConstraintsResponse(
+ 12345,
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
+ spec.DataVersionDeneb,
+ nil,
+ )
+
+ if m.GetHeaderWithProofsResponse != nil {
+ response = m.GetHeaderWithProofsResponse
}
if err := json.NewEncoder(w).Encode(response); err != nil {
diff --git mev-boost/server/service.go bolt-mev-boost/server/service.go
index 897d67ba5b4d4aa8875d620ae7d8f328e3fac68d..d8a2a7bc00624ee06a0fbf3e61cc16f6ab5fb4b5 100644
--- mev-boost/server/service.go
+++ bolt-mev-boost/server/service.go
@@ -22,6 +22,9 @@ 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"
+ gethCommon "github.com/ethereum/go-ethereum/common"
+ gethTypes "github.com/ethereum/go-ethereum/core/types"
+ fastSsz "github.com/ferranbt/fastssz"
"github.com/flashbots/go-boost-utils/ssz"
"github.com/flashbots/go-boost-utils/types"
"github.com/flashbots/go-boost-utils/utils"
@@ -32,6 +35,7 @@ "github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
+// Standard errors
var (
errNoRelays = errors.New("no relays")
errInvalidSlot = errors.New("invalid slot")
@@ -41,6 +45,18 @@ errNoSuccessfulRelayResponse = errors.New("no successful relay response")
errServerAlreadyRunning = errors.New("server already running")
)
+// Bolt errors
+var (
+ errNilProof = errors.New("nil proof")
+ errMissingConstraint = errors.New("missing constraint")
+ errMismatchProofSize = errors.New("proof size mismatch")
+ errInvalidProofs = errors.New("proof verification failed")
+ errInvalidRoot = errors.New("failed getting tx root from bid")
+ errNilConstraint = errors.New("nil constraint")
+ errHashesIndexesMismatch = errors.New("proof transaction hashes and indexes length mismatch")
+ errHashesConstraintsMismatch = errors.New("proof transaction hashes and constraints length mismatch")
+)
+
var (
nilHash = phase0.Hash32{}
nilResponse = struct{}{}
@@ -73,10 +89,11 @@ GenesisTime uint64
RelayCheck bool
RelayMinBid types.U256Str
- RequestTimeoutGetHeader time.Duration
- RequestTimeoutGetPayload time.Duration
- RequestTimeoutRegVal time.Duration
- RequestMaxRetries int
+ RequestTimeoutGetHeader time.Duration
+ RequestTimeoutGetPayload time.Duration
+ RequestTimeoutRegVal time.Duration
+ RequestTimeoutSubmitConstraint time.Duration
+ RequestMaxRetries int
}
// BoostService - the mev-boost service
@@ -90,17 +107,23 @@ relayCheck bool
relayMinBid types.U256Str
genesisTime uint64
- builderSigningDomain phase0.Domain
- httpClientGetHeader http.Client
- httpClientGetPayload http.Client
- httpClientRegVal http.Client
- requestMaxRetries int
+ builderSigningDomain phase0.Domain
+ httpClientGetHeader http.Client
+ httpClientGetPayload http.Client
+ httpClientRegVal http.Client
+ httpClientSubmitConstraint http.Client
+ httpClientDelegate http.Client
+ httpClientRevoke http.Client
+ requestMaxRetries int
bids map[bidRespKey]bidResp // keeping track of bids, to log the originating relay on withholding
bidsLock sync.Mutex
slotUID *slotUID
slotUIDLock sync.Mutex
+
+ // BOLT: constraint cache
+ constraints *ConstraintsCache
}
// NewBoostService created a new BoostService
@@ -138,7 +161,24 @@ httpClientRegVal: http.Client{
Timeout: opts.RequestTimeoutRegVal,
CheckRedirect: httpClientDisallowRedirects,
},
+ httpClientSubmitConstraint: http.Client{
+ Timeout: opts.RequestTimeoutSubmitConstraint,
+ CheckRedirect: httpClientDisallowRedirects,
+ },
+ httpClientDelegate: http.Client{
+ // NOTE: using the same timeout as registerValidator
+ Timeout: opts.RequestTimeoutRegVal,
+ CheckRedirect: httpClientDisallowRedirects,
+ },
+ httpClientRevoke: http.Client{
+ // NOTE: using the same timeout as registerValidator
+ Timeout: opts.RequestTimeoutRegVal,
+ CheckRedirect: httpClientDisallowRedirects,
+ },
requestMaxRetries: opts.RequestMaxRetries,
+
+ // BOLT: Initialize the constraint cache
+ constraints: NewConstraintsCache(64),
}, nil
}
@@ -167,8 +207,14 @@ r.HandleFunc("/", m.handleRoot)
r.HandleFunc(pathStatus, m.handleStatus).Methods(http.MethodGet)
r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost)
+ r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost)
+
r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet)
+ r.HandleFunc(pathGetHeaderWithProofs, m.handleGetHeaderWithProofs).Methods(http.MethodGet)
r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost)
+
+ r.HandleFunc(pathDelegate, m.handleDelegate).Methods(http.MethodPost)
+ r.HandleFunc(pathRevoke, m.handleRevoke).Methods(http.MethodPost)
r.Use(mux.CORSMethodMiddleware(r))
loggedRouter := httplogger.LoggingMiddlewareLogrus(m.log, r)
@@ -308,6 +354,255 @@
m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error())
}
+func (m *BoostService) handleDelegate(w http.ResponseWriter, req *http.Request) {
+ log := m.log.WithField("method", "delegate")
+ log.Debug("delegate:", req.Body)
+
+ payload := make([]SignedDelegation, 0)
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ m.respondError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ for _, signedDelegation := range payload {
+ if signedDelegation.Message.Action != 0 {
+ m.respondError(w, http.StatusBadRequest, "invalid action, expected 0 for delegate")
+ return
+ }
+ }
+
+ ua := UserAgent(req.Header.Get("User-Agent"))
+ log = log.WithFields(logrus.Fields{
+ "ua": ua,
+ })
+
+ relayRespCh := make(chan error, len(m.relays))
+
+ for _, relay := range m.relays {
+ go func(relay RelayEntry) {
+ url := relay.GetURI(pathDelegate)
+ log := log.WithField("url", url)
+
+ _, err := SendHTTPRequest(context.Background(), m.httpClientDelegate, http.MethodPost, url, ua, nil, payload, nil)
+ relayRespCh <- err
+ if err != nil {
+ log.WithError(err).Warn("error calling delegate on relay")
+ return
+ }
+ }(relay)
+ }
+
+ for i := 0; i < len(m.relays); i++ {
+ respErr := <-relayRespCh
+ if respErr == nil {
+ m.respondOK(w, nilResponse)
+ return
+ }
+ }
+
+ m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error())
+}
+
+func (m *BoostService) handleRevoke(w http.ResponseWriter, req *http.Request) {
+ log := m.log.WithField("method", "revoke")
+ log.Debug("revoke:", req.Body)
+
+ payload := make([]SignedRevocation, 0)
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ m.respondError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ for _, signedRevocation := range payload {
+ if signedRevocation.Message.Action != 1 {
+ m.respondError(w, http.StatusBadRequest, "invalid action, expected 1 for revoke")
+ }
+ }
+
+ ua := UserAgent(req.Header.Get("User-Agent"))
+ log = log.WithFields(logrus.Fields{
+ "ua": ua,
+ })
+
+ relayRespCh := make(chan error, len(m.relays))
+
+ for _, relay := range m.relays {
+ go func(relay RelayEntry) {
+ url := relay.GetURI(pathRevoke)
+ log := log.WithField("url", url)
+
+ _, err := SendHTTPRequest(context.Background(), m.httpClientRevoke, http.MethodPost, url, ua, nil, payload, nil)
+ relayRespCh <- err
+ if err != nil {
+ log.WithError(err).Warn("error calling revoke on relay")
+ return
+ }
+ }(relay)
+ }
+
+ for i := 0; i < len(m.relays); i++ {
+ respErr := <-relayRespCh
+ if respErr == nil {
+ m.respondOK(w, nilResponse)
+ return
+ }
+ }
+
+ m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error())
+}
+
+// verifyInclusionProof verifies the proofs against the constraints, and returns an error if the proofs are invalid.
+func (m *BoostService) verifyInclusionProof(transactionsRoot phase0.Root, proof *InclusionProof, slot uint64) error {
+ log := m.log.WithFields(logrus.Fields{})
+
+ // BOLT: get constraints for the slot
+ inclusionConstraints, exists := m.constraints.Get(slot)
+
+ if !exists {
+ log.Warnf("[BOLT]: No constraints found for slot %d", slot)
+ return errMissingConstraint
+ }
+
+ if proof == nil {
+ return errNilProof
+ }
+
+ if len(proof.TransactionHashes) != len(inclusionConstraints) {
+ return errMismatchProofSize
+ }
+ if len(proof.TransactionHashes) != len(proof.GeneralizedIndexes) {
+ return errHashesIndexesMismatch
+ }
+
+ log.Infof("[BOLT]: Verifying merkle multiproofs for %d transactions", len(proof.TransactionHashes))
+
+ // Decode the constraints, and sort them according to the utility function used
+ // TODO: this should be done before verification ideally
+ hashToTransaction := make(HashToTransactionDecoded)
+ for hash, tx := range inclusionConstraints {
+ transaction := new(gethTypes.Transaction)
+ err := transaction.UnmarshalBinary(*tx)
+ if err != nil {
+ log.WithError(err).Error("error unmarshalling transaction while verifying proofs")
+ return err
+ }
+ hashToTransaction[hash] = transaction.WithoutBlobTxSidecar()
+ }
+ leaves := make([][]byte, len(inclusionConstraints))
+ indexes := make([]int, len(proof.GeneralizedIndexes))
+
+ for i, hash := range proof.TransactionHashes {
+ tx, ok := hashToTransaction[gethCommon.Hash(hash)]
+ if tx == nil || !ok {
+ return errNilConstraint
+ }
+
+ // Compute the hash tree root for the raw preconfirmed transaction
+ // and use it as "Leaf" in the proof to be verified against
+ encoded, err := tx.MarshalBinary()
+ if err != nil {
+ log.WithError(err).Error("error marshalling transaction without blob tx sidecar")
+ return err
+ }
+
+ txBytes := Transaction(encoded)
+ txHashTreeRoot, err := txBytes.HashTreeRoot()
+ if err != nil {
+ return errInvalidRoot
+ }
+
+ leaves[i] = txHashTreeRoot[:]
+ indexes[i] = int(proof.GeneralizedIndexes[i])
+ i++
+ }
+
+ hashes := make([][]byte, len(proof.MerkleHashes))
+ for i, hash := range proof.MerkleHashes {
+ hashes[i] = []byte(*hash)
+ }
+
+ currentTime := time.Now()
+ ok, err := fastSsz.VerifyMultiproof(transactionsRoot[:], hashes, leaves, indexes)
+ elapsed := time.Since(currentTime)
+ if err != nil {
+ log.WithError(err).Error("error verifying merkle proof")
+ return err
+ }
+
+ if !ok {
+ log.Error("[BOLT]: proof verification failed")
+ return errInvalidProofs
+ } else {
+ log.Info(fmt.Sprintf("[BOLT]: merkle proof verified in %s", elapsed))
+ }
+
+ return nil
+}
+
+// handleSubmitConstraint forwards a constraint to the relays, and registers them in the local cache.
+// They will later be used to verify the proofs sent by the relays.
+func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.Request) {
+ ua := UserAgent(req.Header.Get("User-Agent"))
+ log := m.log.WithFields(logrus.Fields{
+ "method": "submitConstraint",
+ "ua": ua,
+ })
+
+ log.Info("submitConstraint")
+
+ payload := BatchedSignedConstraints{}
+ if err := DecodeJSON(req.Body, &payload); err != nil {
+ log.Error("error decoding payload: ", err)
+ m.respondError(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ // Add all constraints to the cache
+ for _, signedConstraints := range payload {
+ constraintsMessage := signedConstraints.Message
+
+ log.Infof("[BOLT]: adding inclusion constraints to cache. slot = %d, validatorPubkey = %s, number of relays = %d", constraintsMessage.Slot, constraintsMessage.Pubkey.String(), len(m.relays))
+
+ // Add the constraints to the cache.
+ // They will be cleared when we receive a payload for the slot in `handleGetPayload`
+ err := m.constraints.AddInclusionConstraints(constraintsMessage.Slot, constraintsMessage.Transactions)
+ if err != nil {
+ log.WithError(err).Errorf("error adding inclusion constraints to cache")
+ continue
+ }
+
+ log.Infof("[BOLT]: added inclusion constraints to cache. slot = %d, validatorPubkey = %s, number of relays = %d", constraintsMessage.Slot, constraintsMessage.Pubkey.String(), len(m.relays))
+ }
+
+ relayRespCh := make(chan error, len(m.relays))
+
+ for _, relay := range m.relays {
+ go func(relay RelayEntry) {
+ url := relay.GetURI(pathSubmitConstraint)
+ log := log.WithField("url", url)
+
+ log.Infof("sending request for %d constraint to relay", len(payload))
+ _, err := SendHTTPRequest(context.Background(), m.httpClientSubmitConstraint, http.MethodPost, url, ua, nil, payload, nil)
+ log.Infof("sent request for %d constraint to relay. err = %v", len(payload), err)
+ relayRespCh <- err
+ if err != nil {
+ log.WithError(err).Warn("error calling submitConstraint on relay")
+ return
+ }
+ }(relay)
+ }
+
+ for i := 0; i < len(m.relays); i++ {
+ respErr := <-relayRespCh
+ if respErr == nil {
+ m.respondOK(w, nilResponse)
+ return
+ }
+ }
+
+ m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error())
+}
+
// handleGetHeader requests bids from the relays
func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
@@ -514,6 +809,239 @@ // Return the bid
m.respondOK(w, &result.response)
}
+// handleGetHeader requests bids from the relays
+// BOLT: receiving preconfirmation proofs from relays along with bids, and
+// verify them. If not valid, the bid is discarded
+func (m *BoostService) handleGetHeaderWithProofs(w http.ResponseWriter, req *http.Request) {
+ vars := mux.Vars(req)
+ slot := vars["slot"]
+ parentHashHex := vars["parent_hash"]
+ pubkey := vars["pubkey"]
+
+ ua := UserAgent(req.Header.Get("User-Agent"))
+ log := m.log.WithFields(logrus.Fields{
+ "method": "getHeaderWithProofs",
+ "slot": slot,
+ "parentHash": parentHashHex,
+ "pubkey": pubkey,
+ "ua": ua,
+ })
+ log.Debug("getHeader")
+
+ slotUint, err := strconv.ParseUint(slot, 10, 64)
+ if err != nil {
+ m.respondError(w, http.StatusBadRequest, errInvalidSlot.Error())
+ return
+ }
+
+ if len(pubkey) != 98 {
+ m.respondError(w, http.StatusBadRequest, errInvalidPubkey.Error())
+ return
+ }
+
+ if len(parentHashHex) != 66 {
+ m.respondError(w, http.StatusBadRequest, errInvalidHash.Error())
+ return
+ }
+
+ // Make sure we have a uid for this slot
+ m.slotUIDLock.Lock()
+ if m.slotUID.slot < slotUint {
+ m.slotUID.slot = slotUint
+ m.slotUID.uid = uuid.New()
+ }
+ slotUID := m.slotUID.uid
+ m.slotUIDLock.Unlock()
+ log = log.WithField("slotUID", slotUID)
+
+ // Log how late into the slot the request starts
+ slotStartTimestamp := m.genesisTime + slotUint*config.SlotTimeSec
+ msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000
+ log.WithFields(logrus.Fields{
+ "genesisTime": m.genesisTime,
+ "slotTimeSec": config.SlotTimeSec,
+ "msIntoSlot": msIntoSlot,
+ }).Infof("getHeaderWithProof request start - %d milliseconds into slot %d", msIntoSlot, slotUint)
+
+ // Add request headers
+ headers := map[string]string{
+ HeaderKeySlotUID: slotUID.String(),
+ }
+
+ // Prepare relay responses
+ result := bidResp{} // the final response, containing the highest bid (if any)
+ relays := make(map[BlockHashHex][]RelayEntry) // relays that sent the bid for a specific blockHash
+
+ // Call the relays
+ var mu sync.Mutex
+ var wg sync.WaitGroup
+ for _, relay := range m.relays {
+ wg.Add(1)
+ go func(relay RelayEntry) {
+ defer wg.Done()
+ path := fmt.Sprintf("/eth/v1/builder/header_with_proofs/%s/%s/%s", slot, parentHashHex, pubkey)
+ url := relay.GetURI(path)
+ log := log.WithField("url", url)
+ responsePayload := new(VersionedSignedBuilderBidWithProofs)
+ 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")
+ return
+ }
+
+ if responsePayload.Proofs != nil {
+ log.Infof("[BOLT]: get header with proofs at slot %s, received payload with proofs: %s", slot, responsePayload)
+ }
+
+ if code == http.StatusNoContent {
+ log.Warn("no-content response")
+ return
+ }
+
+ if responsePayload.VersionedSignedBuilderBid == nil {
+ log.Warn("Bid in response is nil")
+ return
+ }
+
+ // Skip if payload is empty
+ if responsePayload.IsEmpty() {
+ log.Warn("Bid is empty")
+ return
+ }
+
+ // Getting the bid info will check if there are missing fields in the response
+ bidInfo, err := parseBidInfo(responsePayload.VersionedSignedBuilderBid)
+ if err != nil {
+ log.WithError(err).Warn("error parsing bid info")
+ return
+ }
+
+ if bidInfo.blockHash == nilHash {
+ log.Warn("relay responded with empty block hash")
+ return
+ }
+
+ valueEth := weiBigIntToEthBigFloat(bidInfo.value.ToBig())
+ log = log.WithFields(logrus.Fields{
+ "blockNumber": bidInfo.blockNumber,
+ "blockHash": bidInfo.blockHash.String(),
+ "txRoot": bidInfo.txRoot.String(),
+ "value": valueEth.Text('f', 18),
+ })
+
+ if relay.PublicKey.String() != bidInfo.pubkey.String() {
+ log.Errorf("bid pubkey mismatch. expected: %s - got: %s", relay.PublicKey.String(), bidInfo.pubkey.String())
+ return
+ }
+
+ // Verify the relay signature in the relay response
+ if !config.SkipRelaySignatureCheck {
+ ok, err := checkRelaySignature(responsePayload.VersionedSignedBuilderBid, m.builderSigningDomain, relay.PublicKey)
+ if err != nil {
+ log.WithError(err).Error("error verifying relay signature")
+ return
+ }
+ if !ok {
+ log.Error("failed to verify relay signature")
+ return
+ }
+ }
+
+ // Verify response coherence with proposer's input data
+ if bidInfo.parentHash.String() != parentHashHex {
+ log.WithFields(logrus.Fields{
+ "originalParentHash": parentHashHex,
+ "responseParentHash": bidInfo.parentHash.String(),
+ }).Error("proposer and relay parent hashes are not the same")
+ return
+ }
+
+ isZeroValue := bidInfo.value.IsZero()
+ isEmptyListTxRoot := bidInfo.txRoot.String() == "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1"
+ if isZeroValue || isEmptyListTxRoot {
+ log.Warn("ignoring bid with 0 value")
+ return
+ }
+ log.Debug("bid received")
+
+ // Skip if value (fee) is lower than the minimum bid
+ if bidInfo.value.CmpBig(m.relayMinBid.BigInt()) == -1 {
+ log.Warn("ignoring bid below min-bid value")
+ return
+ }
+
+ // BOLT: verify inclusion proofs. If they don't match, we don't consider the bid to be valid.
+ if responsePayload.Proofs != nil {
+ // BOLT: verify the proofs against the constraints. If they don't match, we don't consider the bid to be valid.
+ transactionsRoot, err := responsePayload.TransactionsRoot()
+ if err != nil {
+ log.WithError(err).Error("[BOLT]: error getting transaction root")
+ return
+ }
+ if err := m.verifyInclusionProof(transactionsRoot, responsePayload.Proofs, slotUint); err != nil {
+ log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err)
+ return
+ }
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ // Remember which relays delivered which bids (multiple relays might deliver the top bid)
+ relays[BlockHashHex(bidInfo.blockHash.String())] = append(relays[BlockHashHex(bidInfo.blockHash.String())], relay)
+
+ // Compare the bid with already known top bid (if any)
+ if !result.response.IsEmpty() {
+ valueDiff := bidInfo.value.Cmp(result.bidInfo.value)
+ if valueDiff == -1 { // current bid is less profitable than already known one
+ return
+ } else if valueDiff == 0 { // current bid is equally profitable as already known one. Use hash as tiebreaker
+ previousBidBlockHash := result.bidInfo.blockHash
+ if bidInfo.blockHash.String() >= previousBidBlockHash.String() {
+ return
+ }
+ }
+ }
+
+ // Use this relay's response as mev-boost response because it's most profitable
+ log.Infof("new best bid. Has proofs: %v", responsePayload.Proofs != nil)
+ result.response = *responsePayload.VersionedSignedBuilderBid
+ result.bidInfo = bidInfo
+ result.t = time.Now()
+ }(relay)
+ }
+
+ // Wait for all requests to complete...
+ wg.Wait()
+
+ if result.response.IsEmpty() {
+ log.Info("no bid received")
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ // Log result
+ valueEth := weiBigIntToEthBigFloat(result.bidInfo.value.ToBig())
+ result.relays = relays[BlockHashHex(result.bidInfo.blockHash.String())]
+ log.WithFields(logrus.Fields{
+ "blockHash": result.bidInfo.blockHash.String(),
+ "blockNumber": result.bidInfo.blockNumber,
+ "txRoot": result.bidInfo.txRoot.String(),
+ "value": valueEth.Text('f', 18),
+ "relays": strings.Join(RelayEntriesToStrings(result.relays), ", "),
+ }).Infof("best bid")
+
+ // Remember the bid, for future logging in case of withholding
+ bidKey := bidRespKey{slot: slotUint, blockHash: result.bidInfo.blockHash.String()}
+ m.bidsLock.Lock()
+ m.bids[bidKey] = result
+ m.bidsLock.Unlock()
+
+ // Return the bid
+ m.respondOK(w, &result.response)
+ log.Infof("responded with best bid to beacon client")
+}
+
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")
@@ -791,6 +1319,8 @@
m.respondOK(w, result)
}
+// handleGetPayload submits a signed blinded header to receive the payload body from the relays.
+// BOLT: when receiving the payload, we also remove the associated constraints for this slot.
func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request) {
log := m.log.WithField("method", "getPayload")
log.Debug("getPayload request starts")
@@ -813,9 +1343,11 @@ 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.processDenebPayload(w, req, log, payload)
}
diff --git mev-boost/server/service_test.go bolt-mev-boost/server/service_test.go
index 33b2438ec3cc6ac405e283f6555e200edecff487..c4212951dbf99777360622841899d8c88aedffa4 100644
--- mev-boost/server/service_test.go
+++ bolt-mev-boost/server/service_test.go
@@ -27,6 +27,7 @@ "github.com/attestantio/go-eth2-client/spec/capella"
"github.com/attestantio/go-eth2-client/spec/deneb"
"github.com/attestantio/go-eth2-client/spec/phase0"
eth2UtilBellatrix "github.com/attestantio/go-eth2-client/util/bellatrix"
+ "github.com/ethereum/go-ethereum/common"
"github.com/flashbots/go-boost-utils/types"
"github.com/holiman/uint256"
"github.com/prysmaticlabs/go-bitfield"
@@ -53,16 +54,17 @@ relayEntries[i] = backend.relays[i].RelayEntry
}
opts := BoostServiceOpts{
- Log: testLog,
- ListenAddr: "localhost:12345",
- Relays: relayEntries,
- GenesisForkVersionHex: "0x00000000",
- RelayCheck: true,
- RelayMinBid: types.IntToU256(12345),
- RequestTimeoutGetHeader: relayTimeout,
- RequestTimeoutGetPayload: relayTimeout,
- RequestTimeoutRegVal: relayTimeout,
- RequestMaxRetries: 5,
+ Log: testLog,
+ ListenAddr: "localhost:12345",
+ Relays: relayEntries,
+ GenesisForkVersionHex: "0x00000000",
+ RelayCheck: true,
+ RelayMinBid: types.IntToU256(12345),
+ RequestTimeoutGetHeader: relayTimeout,
+ RequestTimeoutGetPayload: relayTimeout,
+ RequestTimeoutRegVal: relayTimeout,
+ RequestTimeoutSubmitConstraint: relayTimeout,
+ RequestMaxRetries: 5,
}
service, err := NewBoostService(opts)
require.NoError(t, err)
@@ -81,6 +83,7 @@ req, err = http.NewRequest(method, path, bytes.NewReader(nil))
} else {
payloadBytes, err2 := json.Marshal(payload)
require.NoError(t, err2)
+ fmt.Println("payload:", string(payloadBytes))
req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes))
}
@@ -207,7 +210,7 @@ addr := "localhost:1234"
backend.boost.listenAddr = addr
go func() {
err := backend.boost.StartHTTPServer()
- require.NoError(t, err) //nolint:testifylint
+ require.NoError(t, err)
}()
time.Sleep(time.Millisecond * 100)
path := "http://" + addr + "?" + strings.Repeat("abc", 4000) // path with characters of size over 4kb
@@ -275,7 +278,7 @@ require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
require.Equal(t, 1, backend.relays[1].GetRequestCount(path))
// Now make one relay return an error
- backend.relays[0].overrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) {
+ backend.relays[0].overrideHandleRegisterValidator(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
})
rr = backend.request(t, http.MethodPost, path, payload)
@@ -284,7 +287,7 @@ require.Equal(t, 2, backend.relays[0].GetRequestCount(path))
require.Equal(t, 2, backend.relays[1].GetRequestCount(path))
// Now make both relays return an error - which should cause the request to fail
- backend.relays[1].overrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) {
+ backend.relays[1].overrideHandleRegisterValidator(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
})
rr = backend.request(t, http.MethodPost, path, payload)
@@ -308,8 +311,175 @@ require.Equal(t, 2, backend.relays[0].GetRequestCount(path))
})
}
+func TestDelegate(t *testing.T) {
+ path := pathDelegate
+ delegate := SignedDelegation{
+ Message: Delegation{
+ Action: 0,
+ ValidatorPubkey: _HexToPubkey("0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759"),
+ DelegateePubkey: _HexToPubkey("0xb8ba260170b9cda2ad54c321d9a8d77e4ca34517106f587eb5ec184bf78f8a0ce4fb55658301b0dc6b129d10adf62391"),
+ },
+ Signature: _HexToSignature("0x8790321eacadd5b869838bc01db2338b0bd88a802d768bff8ddbe12aeff67ebc003af8ecc3bafedfef98d2946e869974075006f22367f77c58ca1f1ba20f0d90bf323d243063db16c631ce4ff89bc4f3f239e0879cc4eb492b9906a16fab6f16"),
+ }
+ payload := delegate
+
+ t.Run("Normal function", func(t *testing.T) {
+ backend := newTestBackend(t, 1, time.Second)
+ rr := backend.request(t, http.MethodPost, path, payload)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
+ })
+}
+
+func TestRevoke(t *testing.T) {
+ path := pathRevoke
+ revoke := SignedRevocation{
+ Message: Revocation{
+ Action: 1,
+ ValidatorPubkey: _HexToPubkey("0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759"),
+ DelegateePubkey: _HexToPubkey("0xb8ba260170b9cda2ad54c321d9a8d77e4ca34517106f587eb5ec184bf78f8a0ce4fb55658301b0dc6b129d10adf62391"),
+ },
+ Signature: _HexToSignature("0x8790321eacadd5b869838bc01db2338b0bd88a802d768bff8ddbe12aeff67ebc003af8ecc3bafedfef98d2946e869974075006f22367f77c58ca1f1ba20f0d90bf323d243063db16c631ce4ff89bc4f3f239e0879cc4eb492b9906a16fab6f16"),
+ }
+ payload := revoke
+
+ t.Run("Normal function", func(t *testing.T) {
+ backend := newTestBackend(t, 1, time.Second)
+ rr := backend.request(t, http.MethodPost, path, payload)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
+ })
+}
+
+func TestParseSignedDelegation(t *testing.T) {
+ jsonStr := `{
+ "message": {
+ "validator_pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
+ "delegatee_pubkey": "0xb8ba260170b9cda2ad54c321d9a8d77e4ca34517106f587eb5ec184bf78f8a0ce4fb55658301b0dc6b129d10adf62391"
+ },
+ "signature": "0x8790321eacadd5b869838bc01db2338b0bd88a802d768bff8ddbe12aeff67ebc003af8ecc3bafedfef98d2946e869974075006f22367f77c58ca1f1ba20f0d90bf323d243063db16c631ce4ff89bc4f3f239e0879cc4eb492b9906a16fab6f16"
+ }`
+
+ delegation := SignedDelegation{}
+ err := json.Unmarshal([]byte(jsonStr), &delegation)
+ require.NoError(t, err)
+ require.Equal(t, phase0.BLSPubKey(_HexToBytes("0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759")), delegation.Message.ValidatorPubkey)
+ require.Equal(t, phase0.BLSPubKey(_HexToBytes("0xb8ba260170b9cda2ad54c321d9a8d77e4ca34517106f587eb5ec184bf78f8a0ce4fb55658301b0dc6b129d10adf62391")), delegation.Message.DelegateePubkey)
+ require.Equal(t, phase0.BLSSignature(_HexToBytes("0x8790321eacadd5b869838bc01db2338b0bd88a802d768bff8ddbe12aeff67ebc003af8ecc3bafedfef98d2946e869974075006f22367f77c58ca1f1ba20f0d90bf323d243063db16c631ce4ff89bc4f3f239e0879cc4eb492b9906a16fab6f16")), delegation.Signature)
+}
+
+func TestParseConstraints(t *testing.T) {
+ jsonStr := `[
+ {
+ "message": {
+ "pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
+ "slot": 32,
+ "top": true,
+ "transactions": [
+ "0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
+ ]
+ },
+ "signature": "0xb8d50ee0d4b269db3d4658c1dac784d273a4160d769e16dce723a9684c390afe5865348416b3bf0f1a4f47098bec9024135d0d95f08bed18eb577a3d8a67f5dc78b13cc62515e280786a73fb267d35dfb7ab46a25ac29bf5bc2fa5b07b3e07a6"
+ }
+ ]`
+
+ constraints := BatchedSignedConstraints{}
+ err := json.Unmarshal([]byte(jsonStr), &constraints)
+ require.NoError(t, err)
+ require.Len(t, constraints, 1)
+ require.Equal(t, phase0.BLSPubKey(_HexToBytes("0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759")), constraints[0].Message.Pubkey)
+ require.Equal(t, uint64(32), constraints[0].Message.Slot)
+ require.Equal(t, constraints[0].Message.Transactions[0], Transaction(_HexToBytes("0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4")))
+}
+
+func TestConstraintsAndProofs(t *testing.T) {
+ path := pathSubmitConstraint
+ slot := uint64(8978583)
+
+ txHash := _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49")
+ rawTx := _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f")
+
+ payload := BatchedSignedConstraints{&SignedConstraints{
+ Message: ConstraintsMessage{
+ Pubkey: phase0.BLSPubKey(_HexToBytes("0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")),
+ Slot: slot,
+ Top: false,
+ Transactions: []Transaction{rawTx},
+ },
+ Signature: phase0.BLSSignature(_HexToBytes(
+ "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c")),
+ }}
+
+ // Build getHeader request
+ hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7")
+ pubkey := _HexToPubkey(
+ "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249")
+ getHeaderWithProofsPath := getHeaderWithProofsPath(slot, hash, pubkey)
+
+ t.Run("Normal function", func(t *testing.T) {
+ backend := newTestBackend(t, 1, time.Second)
+ rr := backend.request(t, http.MethodPost, path, payload)
+ require.Equal(t, http.StatusOK, rr.Code)
+ require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
+
+ tx, ok := backend.boost.constraints.FindTransactionByHash(common.HexToHash(txHash.String()))
+ require.True(t, ok)
+ require.Equal(t, Transaction(rawTx), *tx)
+ })
+
+ t.Run("Normal function with constraints", func(t *testing.T) {
+ backend := newTestBackend(t, 1, time.Second)
+
+ // Submit constraint
+ backend.request(t, http.MethodPost, path, payload)
+
+ resp := backend.relays[0].MakeGetHeaderWithConstraintsResponse(
+ slot,
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
+ spec.DataVersionDeneb,
+ []struct {
+ tx Transaction
+ hash phase0.Hash32
+ }{{rawTx, txHash}},
+ )
+ backend.relays[0].GetHeaderWithProofsResponse = resp
+
+ rr := backend.request(t, http.MethodGet, getHeaderWithProofsPath, nil)
+ require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
+ require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderWithProofsPath))
+ })
+
+ t.Run("No proofs given", func(t *testing.T) {
+ backend := newTestBackend(t, 1, time.Second)
+
+ // Submit constraint
+ backend.request(t, http.MethodPost, path, payload)
+
+ resp := backend.relays[0].MakeGetHeaderResponse(
+ slot,
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7",
+ "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249",
+ spec.DataVersionDeneb,
+ )
+ backend.relays[0].GetHeaderResponse = resp
+
+ rr := backend.request(t, http.MethodGet, getHeaderWithProofsPath, nil)
+ // When we have constraints registered, but the relay does not return any proofs, we should return no content.
+ // This will force a locally built block.
+ require.Equal(t, http.StatusNoContent, rr.Code, rr.Body.String())
+ require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderWithProofsPath))
+ })
+}
+
func getHeaderPath(slot uint64, parentHash phase0.Hash32, pubkey phase0.BLSPubKey) string {
return fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHash.String(), pubkey.String())
+}
+
+func getHeaderWithProofsPath(slot uint64, parentHash phase0.Hash32, pubkey phase0.BLSPubKey) string {
+ return fmt.Sprintf("/eth/v1/builder/header_with_proofs/%d/%s/%s", slot, parentHash.String(), pubkey.String())
}
func TestGetHeader(t *testing.T) {
@@ -688,7 +858,7 @@ t.Run("Retries on error from relay", func(t *testing.T) {
backend := newTestBackend(t, 1, 2*time.Second)
count := 0
- backend.relays[0].handlerOverrideGetPayload = func(w http.ResponseWriter, _ *http.Request) {
+ backend.relays[0].handlerOverrideGetPayload = func(w http.ResponseWriter, r *http.Request) {
if count > 0 {
// success response on the second attempt
backend.relays[0].defaultHandleGetPayload(w)
@@ -709,7 +879,7 @@
count := 0
maxRetries := 5
- backend.relays[0].handlerOverrideGetPayload = func(w http.ResponseWriter, _ *http.Request) {
+ backend.relays[0].handlerOverrideGetPayload = func(w http.ResponseWriter, r *http.Request) {
count++
if count > maxRetries {
// success response after max retry attempts