diff --git a/mev-boost/go.mod b/mev-boost/go.mod index f6d36d87..c3a1de63 100644 --- a/mev-boost/go.mod +++ b/mev-boost/go.mod @@ -18,6 +18,7 @@ require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c // indirect github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 // indirect diff --git a/mev-boost/go.sum b/mev-boost/go.sum index 0aa577d7..c1d27a7b 100644 --- a/mev-boost/go.sum +++ b/mev-boost/go.sum @@ -45,6 +45,10 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chainbound/shardmap v0.0.2 h1:yB1weccdm2vC6dnqzzLwPIvyAnRj7815mJWbkPybiYw= +github.com/chainbound/shardmap v0.0.2/go.mod h1:TBvIzhHyFUbt+oa3UzbijobTUh221st6xIbuki7WzPc= +github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c h1:7GkJDinsgr9Wcr5yJZGGtYFcCAxS9WSdOBr4DXBaM7c= +github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c/go.mod h1:TBvIzhHyFUbt+oa3UzbijobTUh221st6xIbuki7WzPc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= diff --git a/mev-boost/server/backend.go b/mev-boost/server/backend.go index 3309d541..631a587a 100644 --- a/mev-boost/server/backend.go +++ b/mev-boost/server/backend.go @@ -2,10 +2,12 @@ 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" + pathSubmitConstraint = "/eth/v1/builder/constraints" + 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" // // Relay Monitor paths // pathAuctionTranscript = "/monitor/v1/transcript" diff --git a/mev-boost/server/boltSidecar.go b/mev-boost/server/boltSidecar.go deleted file mode 100644 index 377e7eae..00000000 --- a/mev-boost/server/boltSidecar.go +++ /dev/null @@ -1,56 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - - "github.com/ethereum/go-ethereum/log" - "github.com/flashbots/go-utils/jsonrpc" -) - -// boltSidecar is a thin http client that communicates with the -// bolt sidecar server to fetch preconfirmed transactions. -type boltSidecar struct { - endpoint string -} - -// newBoltSidecar creates a new boltSidecar instance. -func newBoltSidecar(endpoint string) *boltSidecar { - return &boltSidecar{ - endpoint: endpoint, - } -} - -type rawPreconfirmation struct { - Slot uint64 `json:"slot"` - TxHash string `json:"txHash"` - RawTx string `json:"rawTx"` -} - -func (b *boltSidecar) GetPreconfirmations(slot uint64) ([]*rawPreconfirmation, error) { - var preconfirms = new([]*rawPreconfirmation) - - params := map[string]interface{}{ - "slot": slot, - } - - // Request preconfirmations directly from the next proposer in line. - // In a real version, this would be done through a mempool / DA service. - req := jsonrpc.NewJSONRPCRequest("1", "eth_getPreconfirmations", params) - res, err := jsonrpc.SendJSONRPCRequest(*req, b.endpoint) - if err != nil { - log.Error("Error getting preconfs via RPC: ", err) - return nil, err - } - - // Unmarshal the JSON data - err = json.Unmarshal(res.Result, &preconfirms) - if err != nil { - log.Error("Error unmarshaling data: ", err) - return nil, err - } - - log.Info(fmt.Sprintf("Preconf Response Body: %s", string(res.Result))) - - return *preconfirms, nil -} diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go new file mode 100644 index 00000000..6e438cfe --- /dev/null +++ b/mev-boost/server/constraints.go @@ -0,0 +1,96 @@ +package server + +import ( + "github.com/chainbound/shardmap" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type BatchedSignedConstraints = []*SignedConstraints + +type SignedConstraints struct { + Message ConstraintsMessage `json:"message"` + Signature HexBytes `json:"signature"` +} + +type ConstraintsMessage struct { + ValidatorIndex uint64 + Slot uint64 + Constraints []*Constraint +} + +type Constraint struct { + Tx Transaction + Index *uint64 +} + +// ConstraintCache is a cache for constraints. +type ConstraintCache struct { + // map of slots to all constraints for that slot + constraints shardmap.FIFOMap[uint64, map[common.Hash]*Constraint] +} + +// NewConstraintCache creates a new constraint cache. +// cap is the maximum number of slots to store constraints for. +func NewConstraintCache(cap int) *ConstraintCache { + return &ConstraintCache{ + constraints: *shardmap.NewFIFOMap[uint64, map[common.Hash]*Constraint](int(cap), 1, shardmap.HashUint64), + } +} + +// AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. +func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) error { + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Put(slot, make(map[common.Hash]*Constraint)) + } + + // parse transaction to get its hash and store it in the cache + // for constant time lookup later + var parsedTx = new(types.Transaction) + err := parsedTx.UnmarshalBinary(tx) + if err != nil { + return err + } + + m, _ := c.constraints.Get(slot) + m[parsedTx.Hash()] = &Constraint{ + Tx: tx, + Index: index, + } + + return nil +} + +// AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot +func (c *ConstraintCache) AddInclusionConstraints(slot uint64, constraints []*Constraint) error { + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Put(slot, make(map[common.Hash]*Constraint)) + } + + m, _ := c.constraints.Get(slot) + for _, constraint := range constraints { + var parsedTx = new(types.Transaction) + err := parsedTx.UnmarshalBinary(constraint.Tx) + if err != nil { + return err + } + m[parsedTx.Hash()] = constraint + } + + return nil +} + +// Get gets the constraints at the given slot. +func (c *ConstraintCache) Get(slot uint64) (map[common.Hash]*Constraint, bool) { + return c.constraints.Get(slot) +} + +// FindTransactionByHash finds the constraint for the given transaction hash and returns it. +func (c *ConstraintCache) FindTransactionByHash(txHash common.Hash) (*Constraint, bool) { + for kv := range c.constraints.Iter() { + if constraint, exists := kv.Value[txHash]; exists { + return constraint, true + } + } + return nil, false +} diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index fe6c6daa..11bb54f5 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "fmt" + "math" "net/http" "net/http/httptest" "net/url" @@ -16,9 +17,11 @@ import ( 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" @@ -54,13 +57,16 @@ type mockRelay struct { 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 *BidWithInclusionProofs + GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse // Server section Server *httptest.Server @@ -115,6 +121,8 @@ func (m *mockRelay) getRouter() http.Handler { 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(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost) return m.newTestMiddleware(r) @@ -164,6 +172,71 @@ func (m *mockRelay) defaultHandleRegisterValidator(w http.ResponseWriter, req *h 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 +}) *BidWithInclusionProofs { + + 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)) + bidWithProofs.Proofs = make([]*InclusionProof, len(constraints)) + + for i, con := range constraints { + generalizedIndex := int(math.Pow(float64(2), float64(21))) + i + + proof, err := rootNode.Prove(generalizedIndex) + if err != nil { + panic(err) + } + + merkleProof := new(SerializedMerkleProof) + merkleProof.FromFastSszProof(proof) + + bidWithProofs.Proofs[i] = &InclusionProof{ + TxHash: con.hash, + MerkleProof: merkleProof, + } + } + + 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 +265,7 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p }, } case spec.DataVersionDeneb: + message := &builderApiDeneb.BuilderBid{ Header: &deneb.ExecutionPayloadHeader{ BlockHash: _HexToHash(blockHash), @@ -221,6 +295,70 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p 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) *BidWithInclusionProofs { + 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 &BidWithInclusionProofs{ + Bid: &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 &BidWithInclusionProofs{ + Bid: &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,6 +385,7 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", spec.DataVersionCapella, ) + if m.GetHeaderResponse != nil { response = m.GetHeaderResponse } @@ -257,6 +396,44 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { } } +// 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.DataVersionCapella, + nil, + ) + + if m.GetHeaderWithProofsResponse != nil { + response = m.GetHeaderWithProofsResponse + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + // MakeGetPayloadResponse is used to create the default or can be used to create a custom response to the getPayload // method func (m *mockRelay) MakeGetPayloadResponse(parentHash, blockHash, feeRecipient string, blockNumber uint64, version spec.DataVersion) *builderApi.VersionedSubmitBlindedBlockResponse { diff --git a/mev-boost/server/preconf.go b/mev-boost/server/proofs.go similarity index 84% rename from mev-boost/server/preconf.go rename to mev-boost/server/proofs.go index 4ad1da22..fce43624 100644 --- a/mev-boost/server/preconf.go +++ b/mev-boost/server/proofs.go @@ -14,14 +14,14 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" ) -type BidWithPreconfirmationsProofs struct { +type BidWithInclusionProofs struct { // The block bid Bid *builderSpec.VersionedSignedBuilderBid `json:"bid"` - // The preconfirmations with proofs - Proofs []*PreconfirmationWithProof `json:"proofs"` + // The inclusion proofs + Proofs []*InclusionProof `json:"proofs"` } -func (b *BidWithPreconfirmationsProofs) String() string { +func (b *BidWithInclusionProofs) String() string { out, err := json.Marshal(b) if err != nil { return err.Error() @@ -29,7 +29,7 @@ func (b *BidWithPreconfirmationsProofs) String() string { return string(out) } -func (p *PreconfirmationWithProof) String() string { +func (p *InclusionProof) String() string { proofs, err := json.Marshal(p) if err != nil { return err.Error() @@ -39,9 +39,13 @@ func (p *PreconfirmationWithProof) String() string { type HexBytes []byte +func (h HexBytes) Equal(other HexBytes) bool { + return bytes.Equal(h, other) +} + // MarshalJSON implements json.Marshaler. func (h HexBytes) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%#x"`, h)), nil + return []byte(fmt.Sprintf(`"%#x"`, []byte(h))), nil } // UnmarshalJSON implements json.Unmarshaler. @@ -100,9 +104,8 @@ func (s *SerializedMerkleProof) ToFastSszProof(leaf []byte) *fastSsz.Proof { return p } -// PreconfirmationWithProof is a preconfirmed transaction in the block with -// proof of inclusion, using Merkle Trees. -type PreconfirmationWithProof struct { +// InclusionProof is a Merkle inclusion proof for a transaction hash. +type InclusionProof struct { // The transaction hash of the preconfirmation TxHash phase0.Hash32 `ssz-size:"32" json:"txHash"` // The Merkle proof of the preconfirmation diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 57c92446..74eee484 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -3,7 +3,6 @@ package server import ( "bytes" "context" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -23,6 +22,7 @@ import ( eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/common" fastSsz "github.com/ferranbt/fastssz" "github.com/flashbots/go-boost-utils/ssz" "github.com/flashbots/go-boost-utils/types" @@ -34,6 +34,7 @@ import ( "github.com/sirupsen/logrus" ) +// Standard errors var ( errNoRelays = errors.New("no relays") errInvalidSlot = errors.New("invalid slot") @@ -43,6 +44,15 @@ var ( 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") +) + var ( nilHash = phase0.Hash32{} nilResponse = struct{}{} @@ -75,10 +85,11 @@ type BoostServiceOpts struct { 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 @@ -92,11 +103,12 @@ type BoostService struct { 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 + requestMaxRetries int bids map[bidRespKey]bidResp // keeping track of bids, to log the originating relay on withholding bidsLock sync.Mutex @@ -104,8 +116,8 @@ type BoostService struct { slotUID *slotUID slotUIDLock sync.Mutex - // BOLT: sidecar connection - sidecar *boltSidecar + // BOLT: constraint cache + constraints *ConstraintCache } // NewBoostService created a new BoostService @@ -119,9 +131,6 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { return nil, err } - // TODO: pass these from config options - boltSidecar := newBoltSidecar("http://mev-sidecar-api:9061") - return &BoostService{ listenAddr: opts.ListenAddr, relays: opts.Relays, @@ -146,9 +155,14 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { Timeout: opts.RequestTimeoutRegVal, CheckRedirect: httpClientDisallowRedirects, }, + httpClientSubmitConstraint: http.Client{ + Timeout: opts.RequestTimeoutSubmitConstraint, + CheckRedirect: httpClientDisallowRedirects, + }, requestMaxRetries: opts.RequestMaxRetries, - sidecar: boltSidecar, + // BOLT: Initialize the constraint cache + constraints: NewConstraintCache(64), }, nil } @@ -177,7 +191,9 @@ func (m *BoostService) getRouter() http.Handler { 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.Use(mux.CORSMethodMiddleware(r)) @@ -318,9 +334,158 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error()) } +// verifyConstraintProofs verifies the proofs against the constraints, and returns an error if the proofs are invalid. +func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionProofs, 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 len(responsePayload.Proofs) != len(inclusionConstraints) { + log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != constraints %d", + len(responsePayload.Proofs), len(inclusionConstraints)) + return errMismatchProofSize + } + + // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. + if responsePayload.Proofs != nil { + // BOLT: remove unnecessary fields while logging + log.WithFields(logrus.Fields{}) + + log.WithField("len", len(responsePayload.Proofs)).Info("[BOLT]: Verifying constraint proofs") + + transactionsRoot, err := responsePayload.Bid.TransactionsRoot() + if err != nil { + log.WithError(err).Error("[BOLT]: error getting tx root from bid") + return errInvalidRoot + } + + for _, proof := range responsePayload.Proofs { + if proof == nil { + log.Warn("[BOLT]: Nil proof!") + return errNilProof + } + + // Find the constraint associated with this transaction in the cache + constraint, ok := m.constraints.FindTransactionByHash(common.HexToHash(proof.TxHash.String())) + if !ok { + log.Warnf("[BOLT]: Tx hash %s not found in constraints", proof.TxHash.String()) + // We don't actually have to return an error here, the relay just provided a proof that was unnecessary + continue + } + + rawTx := constraint.Tx + + log.Infof("[BOLT]: Raw tx: %x", rawTx) + + if len(rawTx) == 0 { + log.Warnf("[BOLT]: Raw tx is empty for tx hash %s", proof.TxHash.String()) + continue + } + + // Compute the hash tree root for the raw preconfirmed transaction + // and use it as "Leaf" in the proof to be verified against + txHashTreeRoot, err := rawTx.HashTreeRoot() + if err != nil { + log.WithError(err).Error("[BOLT]: error getting tx hash tree root") + return errInvalidRoot + } + + log.Infof("[BOLT]: Tx hash tree root: %x", txHashTreeRoot) + + // Verify the proof + sszProof := proof.MerkleProof.ToFastSszProof(txHashTreeRoot[:]) + + log.Infof("[BOLT]: Fast sszProof index: %d", sszProof.Index) + log.Infof("[BOLT]: Fast sszProof hashes: %x", sszProof.Hashes) + log.Infof("[BOLT]: Fast sszProof leaf: %x. Raw tx: %x", sszProof.Leaf, rawTx) + + currentTime := time.Now() + ok, err = fastSsz.VerifyProof(transactionsRoot[:], sszProof) + 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: 'not ok' for tx hash: ", proof.TxHash.String()) + return errInvalidProofs + } else { + log.Info(fmt.Sprintf("[BOLT]: Preconfirmation proof verified for tx hash %s in %s", proof.TxHash.String(), 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 { + constraintMessage := signedConstraints.Message + + log.WithFields(logrus.Fields{ + "slot": constraintMessage.Slot, + "validatorIndex": constraintMessage.ValidatorIndex, + "count": len(constraintMessage.Constraints), + }).Info("[BOLT]: adding inclusion constraints to cache") + + // Add the constraints to the cache. + // They will be cleared when we receive a payload for the slot in `handleGetPayload` + m.constraints.AddInclusionConstraints(constraintMessage.Slot, constraintMessage.Constraints) + } + + 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) + + _, err := SendHTTPRequest(context.Background(), m.httpClientSubmitConstraint, http.MethodPost, url, ua, nil, payload, nil) + 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 -// BOLT: receiving preconfirmation proofs from relays along with bids, and -// verify them. If not valid, the bid is discarded func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) slot := vars["slot"] @@ -391,27 +556,25 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) path := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, parentHashHex, pubkey) url := relay.GetURI(path) log := log.WithField("url", url) - responsePayload := new(BidWithPreconfirmationsProofs) + responsePayload := new(builderSpec.VersionedSignedBuilderBid) code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) if err != nil { log.WithError(err).Warn("error making request to relay") return } - log.Infof("[BOLT]: DECODED RESPONSE PAYLOAD FROM RELAY: %s", responsePayload) - if code == http.StatusNoContent { - log.Warn("no-content response") + log.Debug("no-content response") return } // Skip if payload is empty - if responsePayload.Bid.IsEmpty() { + if responsePayload.IsEmpty() { return } // Getting the bid info will check if there are missing fields in the response - bidInfo, err := parseBidInfo(responsePayload.Bid) + bidInfo, err := parseBidInfo(responsePayload) if err != nil { log.WithError(err).Warn("error parsing bid info") return @@ -437,7 +600,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) // Verify the relay signature in the relay response if !config.SkipRelaySignatureCheck { - ok, err := checkRelaySignature(responsePayload.Bid, m.builderSigningDomain, relay.PublicKey) + ok, err := checkRelaySignature(responsePayload, m.builderSigningDomain, relay.PublicKey) if err != nil { log.WithError(err).Error("error verifying relay signature") return @@ -471,127 +634,227 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) return } - // BOLT: verify preconfirmation inclusion proofs - if responsePayload.Proofs != nil { - // BOLT: remove unnecessary fields while logging - log.WithFields(logrus.Fields{}) + mu.Lock() + defer mu.Unlock() - log.Info("[BOLT]: Verifying preconfirmation proofs", len(responsePayload.Proofs)) + // 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) - // BOLT: we should check these against the preconfirmation requests accepted by - // bolt-sidecar, but for now it's ok that the provided proofs are valid - preconfirmationsFromSidecar, err := m.sidecar.GetPreconfirmations(_slot) - if err != nil { - log.WithError(err).Error("[BOLT]: error fetching preconfirmed transactions from sidecar") + // 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 - } - - if len(responsePayload.Proofs) > 0 { - message := fmt.Sprintf("BOLT-MEV-BOOST: received payload header with %d preconfirmations for slot %d", len(responsePayload.Proofs), _slot) - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"%s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() + } 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 } } + } - if len(responsePayload.Proofs) != len(preconfirmationsFromSidecar) { - log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != preconfs %d", - len(responsePayload.Proofs), len(preconfirmationsFromSidecar)) - w.WriteHeader(http.StatusBadRequest) - return - } + // Use this relay's response as mev-boost response because it's most profitable + log.Debug("new best bid") + result.response = *responsePayload + result.bidInfo = bidInfo + result.t = time.Now() + }(relay) + } - transactionsRoot, err := responsePayload.Bid.TransactionsRoot() - if err != nil { - log.WithError(err).Error("[BOLT]: error getting tx root from bid") - w.WriteHeader(http.StatusBadRequest) - return - } + // Wait for all requests to complete... + wg.Wait() - for _, proof := range responsePayload.Proofs { - if proof == nil { - log.Warn("[BOLT]: Nil proof!") - // BOLT: we should probably skip the bid as well here - continue - } + if result.response.IsEmpty() { + log.Info("no bid received") + w.WriteHeader(http.StatusNoContent) + return + } - // Find the raw tx with the hash specified i - rawTxs := filter(preconfirmationsFromSidecar, func(preconfs *rawPreconfirmation) bool { - return preconfs.TxHash == proof.TxHash.String() - }) + // 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), ", "), + }).Info("best bid") - if len(rawTxs) == 0 { - log.Warn("[BOLT]: proof verification failed - tx hash not found") - w.WriteHeader(http.StatusBadRequest) - return - } + // Remember the bid, for future logging in case of withholding + bidKey := bidRespKey{slot: _slot, blockHash: result.bidInfo.blockHash.String()} + m.bidsLock.Lock() + m.bids[bidKey] = result + m.bidsLock.Unlock() - // BOLT: We assume only 1 preconf, for now this is fine - rawTxStringWithout0x := strings.TrimPrefix(rawTxs[0].RawTx, "0x") - rawTxBytes, err := hex.DecodeString(rawTxStringWithout0x) - if err != nil { - log.WithError(err).Error("[BOLT]: error decoding raw tx from sidecar") - w.WriteHeader(http.StatusInternalServerError) - return - } + // 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"] - rawTx := Transaction(rawTxBytes) - log.Infof("[BOLT]: Raw tx: %x", rawTx) + 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") - // Compute the hash tree root for the raw preconfirmed transaction - // and use it as "Leaf" in the proof to be verified against - txHashTreeRoot, err := rawTx.HashTreeRoot() - if err != nil { - log.WithError(err).Error("[BOLT]: error getting tx hash tree root") - w.WriteHeader(http.StatusInternalServerError) - return - } + 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.Infof("[BOLT]: Tx hash tree root: %x", txHashTreeRoot) + // 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("getHeader request start - %d milliseconds into slot %d", msIntoSlot, slotUint) - // Verify the proof - sszProof := proof.MerkleProof.ToFastSszProof(txHashTreeRoot[:]) + // Add request headers + headers := map[string]string{ + HeaderKeySlotUID: slotUID.String(), + } - log.Infof("[BOLT]: Fast sszProof index: %d", sszProof.Index) - log.Infof("[BOLT]: Fast sszProof hashes: %x", sszProof.Hashes) - log.Infof("[BOLT]: Fast sszProof leaf: %x. Raw tx: %x", sszProof.Leaf, rawTx) + // 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 - currentTime := time.Now() - ok, err := fastSsz.VerifyProof(transactionsRoot[:], sszProof) - elapsed := time.Since(currentTime) + // 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(BidWithInclusionProofs) + 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 err != nil { - log.WithError(err).Error("error verifying merkle proof") - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly - w.WriteHeader(http.StatusNoContent) - return - } + log.Infof("[BOLT]: DECODED RESPONSE PAYLOAD FROM RELAY: %s", responsePayload) - if !ok { - log.Error("[BOLT]: proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String()) - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly - w.WriteHeader(http.StatusBadRequest) - return - } else { - message := fmt.Sprintf("BOLT-MEV-BOOST: Preconfirmation proof verified for tx hash %s in slot %d in %s", proof.TxHash.String(), _slot, elapsed) - log.Info(message) - - // BOLT: Log this event in the web demo backend - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"%s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } - } + if code == http.StatusNoContent { + log.Warn("no-content response") + return + } + + if responsePayload.Bid == nil { + log.Warn("Bid in response is nil") + return + } + + // Skip if payload is empty + if responsePayload.Bid.IsEmpty() { + return + } + + // Getting the bid info will check if there are missing fields in the response + bidInfo, err := parseBidInfo(responsePayload.Bid) + 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.Bid, 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.Debug("ignoring bid below min-bid value") + return + } + + // BOLT: verify preconfirmation 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. + if err := m.verifyConstraintProofs(responsePayload, slotUint); err != nil { + log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err) + return } } @@ -643,7 +906,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) }).Info("best bid") // Remember the bid, for future logging in case of withholding - bidKey := bidRespKey{slot: _slot, blockHash: result.bidInfo.blockHash.String()} + bidKey := bidRespKey{slot: slotUint, blockHash: result.bidInfo.blockHash.String()} m.bidsLock.Lock() m.bids[bidKey] = result m.bidsLock.Unlock() @@ -929,6 +1192,8 @@ func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Requ 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") @@ -951,9 +1216,11 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request m.respondError(w, http.StatusBadRequest, err.Error()) return } + m.processCapellaPayload(w, req, log, payload, body) return } + m.processDenebPayload(w, req, log, payload) } diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 72bcfade..0c3474c0 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -27,6 +27,7 @@ import ( "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 @@ func newTestBackend(t *testing.T, numRelays int, relayTimeout time.Duration) *te } 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 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) * } else { payloadBytes, err2 := json.Marshal(payload) require.NoError(t, err2) + fmt.Println("payload:", string(payloadBytes)) req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes)) } @@ -308,10 +311,120 @@ func TestRegisterValidator(t *testing.T) { }) } +func TestParseConstraints(t *testing.T) { + jsonStr := `[{ + "message": { + "validatorIndex": 12345, + "slot": 8978583, + "constraints": [{ + "tx": "0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f", + "index": null + }] + }, + "signature": "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c" + }]` + + constraints := BatchedSignedConstraints{} + err := json.Unmarshal([]byte(jsonStr), &constraints) + require.NoError(t, err) + require.Len(t, constraints, 1) + require.Equal(t, uint64(12345), constraints[0].Message.ValidatorIndex) + require.Equal(t, uint64(8978583), constraints[0].Message.Slot) + require.Len(t, constraints[0].Message.Constraints, 1) + require.Equal(t, constraints[0].Message.Constraints[0].Tx, Transaction(_HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f"))) + require.Nil(t, constraints[0].Message.Constraints[0].Index) +} + +func TestConstraintsAndProofs(t *testing.T) { + path := pathSubmitConstraint + slot := uint64(8978583) + + txHash := _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49") + rawTx := _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f") + + payload := BatchedSignedConstraints{&SignedConstraints{ + Message: ConstraintsMessage{ + ValidatorIndex: 12345, + Slot: slot, + Constraints: []*Constraint{{Transaction(rawTx), nil}}, + }, + Signature: _HexToBytes( + "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"), + }} + + // Build getHeader request + hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") + pubkey := _HexToPubkey( + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249") + getHeaderPath := 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)) + + got, ok := backend.boost.constraints.FindTransactionByHash(common.HexToHash(txHash.String())) + require.True(t, ok) + require.Equal(t, Transaction(rawTx), got.Tx) + require.Nil(t, got.Index) + }) + + 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, getHeaderPath, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderPath)) + }) + + 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, getHeaderPath, 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(getHeaderPath)) + }) +} + 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) { hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") pubkey := _HexToPubkey( diff --git a/mev-boost/server/transaction_ssz.go b/mev-boost/server/transaction_ssz.go index 8f46f8c4..ad6d25ca 100644 --- a/mev-boost/server/transaction_ssz.go +++ b/mev-boost/server/transaction_ssz.go @@ -8,7 +8,7 @@ import ( var MaxBytesPerTransaction uint64 = 1_073_741_824 // 2**30 // Transaction is a wrapper type of byte slice to implement the ssz.HashRoot interface -type Transaction []byte +type Transaction HexBytes // HashTreeRoot calculates the hash tree root of the transaction, which // is a list of basic types (byte). @@ -50,3 +50,15 @@ func (tx *Transaction) GetTree() (*ssz.Node, error) { tx.HashTreeRootWith(w) return w.Node(), nil } + +func (tx *Transaction) Equal(other *Transaction) bool { + return HexBytes(*tx).Equal(HexBytes(*other)) +} + +func (tx *Transaction) MarshalJSON() ([]byte, error) { + return (*HexBytes)(tx).MarshalJSON() +} + +func (tx *Transaction) UnmarshalJSON(input []byte) error { + return (*HexBytes)(tx).UnmarshalJSON(input) +} diff --git a/mev-boost/server/utils.go b/mev-boost/server/utils.go index bd4c1d63..fb779d53 100644 --- a/mev-boost/server/utils.go +++ b/mev-boost/server/utils.go @@ -270,13 +270,3 @@ func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockRe } return false } - -func filter[T any](slice []T, f func(T) bool) []T { - var n []T - for _, e := range slice { - if f(e) { - n = append(n, e) - } - } - return n -}