Bolt MEV-Boost

diff: ignored:
+1486
-41
+434
-409

This is an overview of the changes made to the canonical Flashbots MEV-Boost package to support the Constraints API.

All the changes are part of the core server module:

The Constraints API spec supports communication with PBS relays with the addition of 2 endpoints to the Builder API:

We added two new Builder API endpoints to communicate with PBS Relays, described above.

In particular, MEV-Boost now also performs proof verification every time a new header is received. If the inclusion proof is invalid, the block is rejected and the block will be built with a local fallback mechanism.

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

These are implementations of the types defined in the Bolt specs here.

diff --git mev-boost/server/constraints.go bolt-mev-boost/server/constraints.go new file mode 100644 index 0000000000000000000000000000000000000000..817ab1692d61428e9944c62e4a3fe66518d568e0 --- /dev/null +++ bolt-mev-boost/server/constraints.go @@ -0,0 +1,138 @@ +package server + +import ( + "github.com/attestantio/go-eth2-client/spec/phase0" + gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + lru "github.com/hashicorp/golang-lru/v2" +) + +type ( + BatchedSignedConstraints = []*SignedConstraints + HashToTransactionDecoded = map[gethCommon.Hash]*types.Transaction +) + +// SignedConstraints represents the signed constraints. +// Reference: https://docs.boltprotocol.xyz/api/builder +type SignedConstraints struct { + Message ConstraintsMessage `json:"message"` + Signature phase0.BLSSignature `json:"signature"` +} + +// ConstraintsMessage represents the constraints message. +// Reference: https://docs.boltprotocol.xyz/api/builder +type ConstraintsMessage struct { + Pubkey phase0.BLSPubKey `json:"pubkey"` + Slot uint64 `json:"slot"` + Top bool `json:"top"` + Transactions []Transaction `json:"transactions"` +} + +func (s *SignedConstraints) String() string { + return JSONStringify(s) +} + +func (m *ConstraintsMessage) String() string { + return JSONStringify(m) +} + +// ConstraintsCache is a cache for constraints. +type ConstraintsCache struct { + // map of slots to all constraints for that slot + constraints *lru.Cache[uint64, map[gethCommon.Hash]*Transaction] +} + +// NewConstraintsCache creates a new constraint cache. +// cap is the maximum number of slots to store constraints for. +func NewConstraintsCache(cap int) *ConstraintsCache { + constraints, _ := lru.New[uint64, map[gethCommon.Hash]*Transaction](cap) + return &ConstraintsCache{ + constraints: constraints, + } +} + +// AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. +func (c *ConstraintsCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) error { + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Add(slot, make(map[gethCommon.Hash]*Transaction)) + } + + // parse transaction to get its hash and store it in the cache + // for constant time lookup later + parsedTx := new(types.Transaction) + err := parsedTx.UnmarshalBinary(tx) + if err != nil { + return err + } + + m, _ := c.constraints.Get(slot) + m[parsedTx.Hash()] = &tx + + return nil +} + +// AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot +func (c *ConstraintsCache) AddInclusionConstraints(slot uint64, transactions []Transaction) error { + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Add(slot, make(map[gethCommon.Hash]*Transaction)) + } + + m, _ := c.constraints.Get(slot) + for _, tx := range transactions { + parsedTx := new(types.Transaction) + err := parsedTx.UnmarshalBinary(tx) + if err != nil { + return err + } + m[parsedTx.Hash()] = &tx + } + + return nil +} + +// Get gets the constraints at the given slot. +func (c *ConstraintsCache) Get(slot uint64) (map[gethCommon.Hash]*Transaction, bool) { + return c.constraints.Get(slot) +} + +// FindTransactionByHash finds the constraint for the given transaction hash and returns it. +func (c *ConstraintsCache) FindTransactionByHash(txHash gethCommon.Hash) (*Transaction, bool) { + for _, hashToTx := range c.constraints.Values() { + if tx, exists := hashToTx[txHash]; exists { + return tx, true + } + } + return nil, false +} + +// SignedDelegation represents the delegation signed by the proposer pubkey to +// authorize the delegatee pubkey to submit constraints on their behalf. +// +// Specs: https://docs.boltprotocol.xyz/api/builder#delegate +type SignedDelegation struct { + Message Delegation `json:"message"` + Signature phase0.BLSSignature `json:"signature"` +} + +// Delegation as from Specs: https://docs.boltprotocol.xyz/api/builder#delegate +type Delegation struct { + Action uint8 `json:"action"` + ValidatorPubkey phase0.BLSPubKey `json:"validator_pubkey"` + DelegateePubkey phase0.BLSPubKey `json:"delegatee_pubkey"` +} + +// SignedRevocation represents the revocation signed by the proposer pubkey to +// revoke the delegatee pubkey's ability to submit constraints on their behalf. +// +// Specs: https://docs.boltprotocol.xyz/api/builder#revoke +type SignedRevocation struct { + Message Revocation `json:"message"` + Signature phase0.BLSSignature `json:"signature"` +} + +// Revocation as from Specs: https://docs.boltprotocol.xyz/api/builder#revoke +type Revocation struct { + Action uint8 `json:"action"` + ValidatorPubkey phase0.BLSPubKey `json:"validator_pubkey"` + DelegateePubkey phase0.BLSPubKey `json:"delegatee_pubkey"` +}
diff --git mev-boost/server/proofs.go bolt-mev-boost/server/proofs.go new file mode 100644 index 0000000000000000000000000000000000000000..5ba5f40baed850d280a43bfe9307749fa630238c --- /dev/null +++ bolt-mev-boost/server/proofs.go @@ -0,0 +1,211 @@ +package server + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strings" + + fastSsz "github.com/ferranbt/fastssz" + + builderApiBellatrix "github.com/attestantio/go-builder-client/api/bellatrix" + builderApiCapella "github.com/attestantio/go-builder-client/api/capella" + builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderSpec "github.com/attestantio/go-builder-client/spec" + consensusSpec "github.com/attestantio/go-eth2-client/spec" + + "github.com/attestantio/go-eth2-client/spec/phase0" +) + +// A wrapper struct over `builderSpec.VersionedSignedBuilderBid` +// to include constraint inclusion proofs +type VersionedSignedBuilderBidWithProofs struct { + Proofs *InclusionProof + *builderSpec.VersionedSignedBuilderBid +} + +// this is necessary, because the mev-boost-relay deserialization doesn't expect a "Version" and "Data" wrapper object +// for deserialization. Instead, it tries to decode the object into the "Deneb" version first and if that fails, it tries +// the "Capella" version. This is a workaround to make the deserialization work. +// +// NOTE(bolt): struct embedding of the VersionedSignedBuilderBid is not possible for some reason because it causes the json +// encoding to omit the `proofs` field. Embedding all of the fields directly does the job. +func (v *VersionedSignedBuilderBidWithProofs) MarshalJSON() ([]byte, error) { + switch v.Version { + case consensusSpec.DataVersionBellatrix: + return json.Marshal(struct { + Message *builderApiBellatrix.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Bellatrix.Message, + Signature: v.Bellatrix.Signature, + Proofs: v.Proofs, + }) + case consensusSpec.DataVersionCapella: + return json.Marshal(struct { + Message *builderApiCapella.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Capella.Message, + Signature: v.Capella.Signature, + Proofs: v.Proofs, + }) + case consensusSpec.DataVersionDeneb: + return json.Marshal(struct { + Message *builderApiDeneb.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + }{ + Message: v.Deneb.Message, + Signature: v.Deneb.Signature, + Proofs: v.Proofs, + }) + } + + return nil, fmt.Errorf("unknown data version %d", v.Version) +} + +func (v *VersionedSignedBuilderBidWithProofs) UnmarshalJSON(data []byte) error { + var err error + + // Try Deneb + var deneb struct { + Message *builderApiDeneb.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + } + err = json.Unmarshal(data, &deneb) + if err == nil && deneb.Message != nil { + v.Proofs = deneb.Proofs + + v.VersionedSignedBuilderBid = &builderSpec.VersionedSignedBuilderBid{} + v.Deneb = &builderApiDeneb.SignedBuilderBid{ + Message: deneb.Message, + Signature: deneb.Signature, + } + v.Version = consensusSpec.DataVersionDeneb + return nil + } + + // Try Capella + var capella struct { + Message *builderApiCapella.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + } + err = json.Unmarshal(data, &capella) + if err == nil && capella.Message != nil { + v.Proofs = capella.Proofs + + v.VersionedSignedBuilderBid = &builderSpec.VersionedSignedBuilderBid{} + v.Capella = &builderApiCapella.SignedBuilderBid{ + Message: capella.Message, + Signature: capella.Signature, + } + v.Version = consensusSpec.DataVersionCapella + return nil + } + + // Try Bellatrix + var bellatrix struct { + Message *builderApiBellatrix.BuilderBid `json:"message"` + Signature phase0.BLSSignature `json:"signature"` + Proofs *InclusionProof `json:"proofs"` + } + err = json.Unmarshal(data, &bellatrix) + if err == nil && bellatrix.Message != nil { + v.Proofs = bellatrix.Proofs + + v.VersionedSignedBuilderBid = &builderSpec.VersionedSignedBuilderBid{} + v.Bellatrix = &builderApiBellatrix.SignedBuilderBid{ + Message: bellatrix.Message, + Signature: bellatrix.Signature, + } + v.Version = consensusSpec.DataVersionBellatrix + return nil + } + + return fmt.Errorf("failed to unmarshal VersionedSignedBuilderBidWithProofs: %v", err) +} + +func (v *VersionedSignedBuilderBidWithProofs) String() string { + return JSONStringify(v) +} + +func (p *InclusionProof) String() string { + proofs, err := json.Marshal(p) + if err != nil { + return err.Error() + } + return string(proofs) +} + +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"`, []byte(h))), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (h *HexBytes) UnmarshalJSON(input []byte) error { + if len(input) == 0 { + return errors.New("input missing") + } + + if !bytes.HasPrefix(input, []byte{'"', '0', 'x'}) { + return errors.New("invalid prefix") + } + + if !bytes.HasSuffix(input, []byte{'"'}) { + return errors.New("invalid suffix") + } + + var data string + json.Unmarshal(input, &data) + + res, _ := hex.DecodeString(strings.TrimPrefix(data, "0x")) + + *h = res + + return nil +} + +// InclusionProof is a Merkle Multiproof of inclusion of a set of TransactionHashes +type InclusionProof struct { + TransactionHashes []phase0.Hash32 `json:"transaction_hashes"` + GeneralizedIndexes []uint64 `json:"generalized_indexes"` + MerkleHashes []*HexBytes `json:"merkle_hashes"` +} + +// InclusionProofFromMultiProof converts a fastssz.Multiproof into an InclusionProof, without +// filling the TransactionHashes +func InclusionProofFromMultiProof(mp *fastSsz.Multiproof) *InclusionProof { + merkleHashes := make([]*HexBytes, len(mp.Hashes)) + for i, h := range mp.Hashes { + merkleHashes[i] = new(HexBytes) + *(merkleHashes[i]) = h + } + + leaves := make([]*HexBytes, len(mp.Leaves)) + for i, h := range mp.Leaves { + leaves[i] = new(HexBytes) + *(leaves[i]) = h + } + generalIndexes := make([]uint64, len(mp.Indices)) + for i, idx := range mp.Indices { + generalIndexes[i] = uint64(idx) + } + return &InclusionProof{ + MerkleHashes: merkleHashes, + GeneralizedIndexes: generalIndexes, + } +}

We added logic to create and verify merkle inclusion proofs based on the SSZ Transactions beacon container.

diff --git mev-boost/server/transaction_ssz.go bolt-mev-boost/server/transaction_ssz.go new file mode 100644 index 0000000000000000000000000000000000000000..ad6d25cae79ee25f11718bd1bfa376c9c3442871 --- /dev/null +++ bolt-mev-boost/server/transaction_ssz.go @@ -0,0 +1,64 @@ +package server + +import ( + ssz "github.com/ferranbt/fastssz" +) + +// MaxBytesPerTransaction is the maximum length in bytes of a raw RLP-encoded transaction +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 HexBytes + +// HashTreeRoot calculates the hash tree root of the transaction, which +// is a list of basic types (byte). +// +// Reference: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +func (tx *Transaction) HashTreeRoot() ([32]byte, error) { + hasher := ssz.NewHasher() + tx.HashTreeRootWith(hasher) + root, err := hasher.HashRoot() + + return root, err +} + +func (tx *Transaction) HashTreeRootWith(hh ssz.HashWalker) error { + var err error + byteLen := uint64(len(*tx)) + + if byteLen > MaxBytesPerTransaction { + err = ssz.ErrIncorrectListSize + return err + } + + // Load the bytes of the transaction into the hasher + hh.AppendBytes32(*tx) + // Perform `mix_in_length(merkleize(pack(value), limit=chunk_count(type)), len(value))` + // Reference: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization + // + // The `indx` parameters is set to `0` as we need to consider the whole hh.buf buffer for this. + // In an implementation of more complex types, this parameter would be used to indicate the starting + // index of the buffer to be merkleized. It is used a single buffer to do everything for + // optimization purposes. + hh.MerkleizeWithMixin(0, byteLen, (1073741824+31)/32) + + return nil +} + +func (tx *Transaction) GetTree() (*ssz.Node, error) { + w := &ssz.Wrapper{} + 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 mev-boost/server/utils.go bolt-mev-boost/server/utils.go index fb779d531218e9dba6e97a37f59e7a24dea88284..9e51b0e471da089f99841cc9c61479e26a0fe9c3 100644 --- mev-boost/server/utils.go +++ bolt-mev-boost/server/utils.go @@ -7,6 +7,7 @@ "encoding/json" "errors" "fmt" "io" + "math" "math/big" "net/http" "net/url" @@ -19,11 +20,12 @@ "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + fastssz "github.com/ferranbt/fastssz" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/ssz" "github.com/flashbots/mev-boost/config" "github.com/holiman/uint256" - "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" )   const ( @@ -105,7 +107,7 @@ return resp.StatusCode, nil }   // SendHTTPRequestWithRetries - prepare and send HTTP request, retrying the request if within the client timeout -func SendHTTPRequestWithRetries(ctx context.Context, client http.Client, method, url string, userAgent UserAgent, headers map[string]string, payload, dst any, maxRetries int, log *logrus.Entry) (code int, err error) { +func SendHTTPRequestWithRetries(ctx context.Context, client http.Client, method, url string, userAgent UserAgent, headers map[string]string, payload, dst any, maxRetries int, log *log.Entry) (code int, err error) { var requestCtx context.Context var cancel context.CancelFunc if client.Timeout > 0 { @@ -270,3 +272,55 @@ return true } return false } + +func Map[T any, U any](slice []*T, mapper func(el *T) *U) []*U { + result := make([]*U, len(slice)) + for i, el := range slice { + result[i] = mapper(el) + } + return result +} + +func JSONStringify(obj any) string { + b, err := json.Marshal(obj) + if err != nil { + return "" + } + return string(b) +} + +func CalculateMerkleMultiProofs(rootNode *fastssz.Node, constraints []struct { + tx Transaction + hash phase0.Hash32 +}) (inclusionProof *InclusionProof, err error) { + // using our gen index formula: 2 * 2^21 + preconfIndex + baseGeneralizedIndex := int(math.Pow(float64(2), float64(21))) + generalizedIndexes := make([]int, len(constraints)) + transactionHashes := make([]phase0.Hash32, len(constraints)) + j := 0 + + for i, con := range constraints { + generalizedIndex := baseGeneralizedIndex + i + generalizedIndexes[i] = generalizedIndex + transactionHashes[j] = con.hash + j++ + } + + log.Info(fmt.Sprintf("[BOLT]: Calculating merkle multiproof for %d preconfirmed transaction", + len(constraints))) + + timeStart := time.Now() + multiProof, err := rootNode.ProveMulti(generalizedIndexes) + if err != nil { + log.Error(fmt.Sprintf("[BOLT]: could not calculate merkle multiproof for %d preconf %s", len(constraints), err)) + return + } + + timeForProofs := time.Since(timeStart) + log.Info(fmt.Sprintf("[BOLT]: Calculated merkle multiproof for %d preconf in %s", len(constraints), timeForProofs)) + + inclusionProof = InclusionProofFromMultiProof(multiProof) + inclusionProof.TransactionHashes = transactionHashes + + return inclusionProof, nil +}
diff --git mev-boost/server/utils_test.go bolt-mev-boost/server/utils_test.go index 3ae76f686eb89ee6c3871cbc55b310d5228b1562..684476ebe5743ff7d14d5371a56c16214f2b8c04 100644 --- mev-boost/server/utils_test.go +++ bolt-mev-boost/server/utils_test.go @@ -4,6 +4,7 @@ import ( "bytes" "compress/gzip" "context" + "encoding/json" "fmt" "math/big" "net/http" @@ -13,9 +14,13 @@ builderApi "github.com/attestantio/go-builder-client/api" builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" "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/core/types" + fastssz "github.com/ferranbt/fastssz" "github.com/flashbots/mev-boost/config" "github.com/stretchr/testify/require" ) @@ -46,7 +51,7 @@ // Test with custom UA customUA := "test-user-agent" expectedUA := fmt.Sprintf("mev-boost/%s %s", config.Version, customUA) - ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, expectedUA, r.Header.Get("User-Agent")) done <- true })) @@ -58,7 +63,7 @@ <-done   // Test without custom UA expectedUA = fmt.Sprintf("mev-boost/%s", config.Version) - ts = httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, expectedUA, r.Header.Get("User-Agent")) done <- true })) @@ -209,3 +214,72 @@ require.Equal(t, tt.expected, getPayloadResponseIsEmpty(tt.payload)) }) } } + +func TestGenerateMerkleMultiProofs(t *testing.T) { + // https://etherscan.io/tx/0x138a5f8ba7950521d9dec66ee760b101e0c875039e695c9fcfb34f5ef02a881b + // 0x02f873011a8405f5e10085037fcc60e182520894f7eaaf75cb6ec4d0e2b53964ce6733f54f7d3ffc880b6139a7cbd2000080c080a095a7a3cbb7383fc3e7d217054f861b890a935adc1adf4f05e3a2f23688cf2416a00875cdc45f4395257e44d709d04990349b105c22c11034a60d7af749ffea2765 + // https://etherscan.io/tx/0xfb0ee9de8941c8ad50e6a3d2999cd6ef7a541ec9cb1ba5711b76fcfd1662dfa9 + // 0xf8708305dc6885029332e35883019a2894500b0107e172e420561565c8177c28ac0f62017f8810ffb80e6cc327008025a0e9c0b380c68f040ae7affefd11979f5ed18ae82c00e46aa3238857c372a358eca06b26e179dd2f7a7f1601755249f4cff56690c4033553658f0d73e26c36fe7815 + // https://etherscan.io/tx/0x45e7ee9ba1a1d0145de29a764a33bb7fc5620486b686d68ec8cb3182d137bc90 + // 0xf86c0785028fa6ae0082520894098d880c4753d0332ca737aa592332ed2522cd22880d2f09f6558750008026a0963e58027576b3a8930d7d9b4a49253b6e1a2060e259b2102e34a451d375ce87a063f802538d3efed17962c96fcea431388483bbe3860ea9bb3ef01d4781450fbf + // https://etherscan.io/tx/0x9d48b4a021898a605b7ae49bf93ad88fa6bd7050e9448f12dde064c10f22fe9c + // 0x02f87601836384348477359400850517683ba883019a28943678fce4028b6745eb04fa010d9c8e4b36d6288c872b0f1366ad800080c080a0b6b7aba1954160d081b2c8612e039518b9c46cd7df838b405a03f927ad196158a071d2fb6813e5b5184def6bd90fb5f29e0c52671dea433a7decb289560a58416e + + raw := `["0x02f873011a8405f5e10085037fcc60e182520894f7eaaf75cb6ec4d0e2b53964ce6733f54f7d3ffc880b6139a7cbd2000080c080a095a7a3cbb7383fc3e7d217054f861b890a935adc1adf4f05e3a2f23688cf2416a00875cdc45f4395257e44d709d04990349b105c22c11034a60d7af749ffea2765","0xf8708305dc6885029332e35883019a2894500b0107e172e420561565c8177c28ac0f62017f8810ffb80e6cc327008025a0e9c0b380c68f040ae7affefd11979f5ed18ae82c00e46aa3238857c372a358eca06b26e179dd2f7a7f1601755249f4cff56690c4033553658f0d73e26c36fe7815", "0xf86c0785028fa6ae0082520894098d880c4753d0332ca737aa592332ed2522cd22880d2f09f6558750008026a0963e58027576b3a8930d7d9b4a49253b6e1a2060e259b2102e34a451d375ce87a063f802538d3efed17962c96fcea431388483bbe3860ea9bb3ef01d4781450fbf", "0x02f87601836384348477359400850517683ba883019a28943678fce4028b6745eb04fa010d9c8e4b36d6288c872b0f1366ad800080c080a0b6b7aba1954160d081b2c8612e039518b9c46cd7df838b405a03f927ad196158a071d2fb6813e5b5184def6bd90fb5f29e0c52671dea433a7decb289560a58416e"]` + + // Unmarshal the raw transactions + byteTxs := make([]*HexBytes, 0, 2) + err := json.Unmarshal([]byte(raw), &byteTxs) + require.NoError(t, err) + + // Create payload transactions + payloadTransactions := Map(byteTxs, func(rawTx *HexBytes) *types.Transaction { + transaction := new(types.Transaction) + err = transaction.UnmarshalBinary([]byte(*rawTx)) + require.NoError(t, err) + return transaction + }) + + // Constraints + constraints := []struct { + tx Transaction + hash phase0.Hash32 + }{ + {tx: Transaction(*byteTxs[0]), hash: phase0.Hash32(payloadTransactions[0].Hash())}, + {tx: Transaction(*byteTxs[1]), hash: phase0.Hash32(payloadTransactions[1].Hash())}, + } + + // Create root node + transactions := new(utilbellatrix.ExecutionPayloadTransactions) + + for _, con := range constraints { + transactions.Transactions = append(transactions.Transactions, bellatrix.Transaction(con.tx)) + } + + rootNode, err := transactions.GetTree() + require.NoError(t, err) + + // Call the function to test + inclusionProof, err := CalculateMerkleMultiProofs(rootNode, constraints) + require.NoError(t, err) + + // Verify the inclusion proof + rootHash := rootNode.Hash() + hashesBytes := make([][]byte, len(inclusionProof.MerkleHashes)) + for i, hash := range inclusionProof.MerkleHashes { + hashesBytes[i] = (*hash)[:] + } + leavesBytes := make([][]byte, len(constraints)) + for i, con := range constraints { + root, err := con.tx.HashTreeRoot() + require.NoError(t, err) + leavesBytes[i] = root[:] + } + indicesInt := make([]int, len(inclusionProof.GeneralizedIndexes)) + for i, index := range inclusionProof.GeneralizedIndexes { + indicesInt[i] = int(index) + } + + _, err = fastssz.VerifyMultiproof(rootHash, hashesBytes, leavesBytes, indicesInt) + require.NoError(t, err) +}
diff --git mev-boost/server/errors.go bolt-mev-boost/server/errors.go index a9757b38e7874d35c26d54923613b5d5a2ce2aa7..7a75a9e5141b30a269b9ed4f68c6778108db9ccf 100644 --- mev-boost/server/errors.go +++ bolt-mev-boost/server/errors.go @@ -1,9 +1,9 @@ package server   -import "errors" +import "fmt"   // ErrMissingRelayPubkey is returned if a new RelayEntry URL has no public key. -var ErrMissingRelayPubkey = errors.New("missing relay public key") +var ErrMissingRelayPubkey = fmt.Errorf("missing relay public key")   // ErrPointAtInfinityPubkey is returned if a new RelayEntry URL has an all-zero public key. -var ErrPointAtInfinityPubkey = errors.New("relay public key cannot be the point-at-infinity") +var ErrPointAtInfinityPubkey = fmt.Errorf("relay public key cannot be the point-at-infinity")
diff --git mev-boost/cmd/test-cli/main.go bolt-mev-boost/cmd/test-cli/main.go index e249af91a327e36553a895d0d63578a2705fc856..f9ec4f580deb523e21f6543ab6a0a5c5bb2b6f24 100644 --- mev-boost/cmd/test-cli/main.go +++ bolt-mev-boost/cmd/test-cli/main.go @@ -36,6 +36,7 @@ if err != nil { log.WithError(err).Fatal("Could not prepare registration message") } _, err = server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, boostEndpoint+"/eth/v1/builder/validators", "test-cli", nil, message, nil) + if err != nil { log.WithError(err).Fatal("Validator registration not successful") }
diff --git mev-boost/config/vars.go bolt-mev-boost/config/vars.go index 4f4b2732030b80965c003be809ea32610f6d7041..4965dddf893e39d01dd8d7483d884b4c27796d6f 100644 --- mev-boost/config/vars.go +++ bolt-mev-boost/config/vars.go @@ -8,7 +8,7 @@ )   var ( // Version is set at build time (must be a var, not a const!) - Version = "v1.7.1" + Version = "v1.7"   // RFC3339Milli is a time format string based on time.RFC3339 but with millisecond precision RFC3339Milli = "2006-01-02T15:04:05.999Z07:00"
diff --git mev-boost/.github/workflows/lint.yml bolt-mev-boost/.github/workflows/lint.yml index 69e7b301a72d3f6a09e68c6a0cd218229bb02adb..c77a1426b0ea6d6a5c9e1b40d77a7d6c5cd31295 100644 --- mev-boost/.github/workflows/lint.yml +++ bolt-mev-boost/.github/workflows/lint.yml @@ -17,7 +17,7 @@ - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ^1.22.0 + go-version: ^1.21.0 id: go   - name: Ensure go mod tidy runs without changes @@ -27,13 +27,13 @@ git diff-index HEAD git diff-index --quiet HEAD   - name: Install gofumpt - run: go install mvdan.cc/gofumpt@v0.6.0 + run: go install mvdan.cc/gofumpt@v0.4.0   - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@v0.4.7   - name: Install golangci-lint - run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.56.1 + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.0   - name: Lint run: make lint
diff --git mev-boost/.github/workflows/release.yaml bolt-mev-boost/.github/workflows/release.yaml index 959e65c105fb037e208ecc1cc7cae73beab97aef..9a84b32332a9ceacfd3912f944a9ae67807dd9bd 100644 --- mev-boost/.github/workflows/release.yaml +++ bolt-mev-boost/.github/workflows/release.yaml @@ -71,7 +71,7 @@ run: git fetch --force --tags - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ^1.22 + go-version: ^1.21 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3 with: @@ -101,7 +101,7 @@ run: git fetch --force --tags - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ^1.22 + go-version: ^1.21 - name: Make directories run: | mkdir -p ./build
diff --git mev-boost/.github/workflows/tests.yml bolt-mev-boost/.github/workflows/tests.yml index 5d69e5db6efb73b03b836120c6d4c08e7d63daee..5c6ed73cd6ba8d37c7a1d13b0f03093cb63af2e7 100644 --- mev-boost/.github/workflows/tests.yml +++ bolt-mev-boost/.github/workflows/tests.yml @@ -14,7 +14,7 @@ steps: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: ^1.22 + go-version: ^1.21 id: go   - name: Checkout sources
diff --git mev-boost/.golangci.yml bolt-mev-boost/.golangci.yml index 074ca1cae869c4503defeac5a1fbd2ebd514a9fc..52baf613c859d8b553734d6e64c5d094136a9732 100644 --- mev-boost/.golangci.yml +++ bolt-mev-boost/.golangci.yml @@ -31,7 +31,6 @@ - gosec - ireturn - noctx - tagliatelle - - perfsprint   # # Disabled because of generics:
diff --git mev-boost/CONTRIBUTING.md bolt-mev-boost/CONTRIBUTING.md index e640fe8ce8dd3e9c147a4810f2138f54349dade6..9ae93632fadd9a01d12df5e61638d53b0edca800 100644 --- mev-boost/CONTRIBUTING.md +++ bolt-mev-boost/CONTRIBUTING.md @@ -6,7 +6,14 @@ Please start by reading our [code of conduct](CODE_OF_CONDUCT.md).   ## Set up   -Install a few dev dependencies for `make lint`: https://github.com/flashbots/mev-boost/blob/go122/.github/workflows/lint.yml#L29-L37 +Install a few dev dependencies for `make lint`: + +```bash +go install github.com/mgechev/revive@v1.1.3 +go install mvdan.cc/gofumpt@v0.3.1 +go install honnef.co/go/tools/cmd/staticcheck@v0.4.2 +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2 +```   Look at the [README for instructions to install the dependencies and build `mev-boost`](README.md#installing)
diff --git mev-boost/Dockerfile bolt-mev-boost/Dockerfile index 32477fb6a504a517554cbf55667ae3cbe1d80934..495f5045bcc0a415e2afd9f075eaf7aa9e7a4252 100644 --- mev-boost/Dockerfile +++ bolt-mev-boost/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -FROM golang:1.22 as builder +FROM golang:1.22-alpine AS builder ARG VERSION WORKDIR /build   @@ -10,10 +10,10 @@ RUN go mod download   ADD . . RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux go build \ - -trimpath \ - -v \ - -ldflags "-w -s -X 'github.com/flashbots/mev-boost/config.Version=$VERSION'" \ - -o mev-boost . + -trimpath \ + -v \ + -ldflags "-w -s -X 'github.com/flashbots/mev-boost/config.Version=$VERSION'" \ + -o mev-boost .   FROM alpine WORKDIR /app
diff --git mev-boost/Makefile bolt-mev-boost/Makefile index 06fc69fe19e856843433aa4f76b75d832d795429..a5ccd47e09dbf576a982d38fba22f52a6b4a4c8c 100644 --- mev-boost/Makefile +++ bolt-mev-boost/Makefile @@ -23,7 +23,6 @@ @echo "${VERSION}"   .PHONY: build build: - @go version CGO_ENABLED=0 go build $(GO_BUILD_FLAGS) -o mev-boost   .PHONY: build-testcli
diff --git mev-boost/README.flashbots.md bolt-mev-boost/README.flashbots.md new file mode 100644 index 0000000000000000000000000000000000000000..4111105955ea42abf1ce78e7bf3f03c5fd655b65 --- /dev/null +++ bolt-mev-boost/README.flashbots.md @@ -0,0 +1,389 @@ +![mev-boost](https://user-images.githubusercontent.com/116939/224986867-3d1916c6-3219-4d61-b1ce-213fc663070c.png) + +# + +[![Goreport status](https://goreportcard.com/badge/github.com/flashbots/mev-boost)](https://goreportcard.com/report/github.com/flashbots/mev-boost) +[![Test status](https://github.com/flashbots/mev-boost/workflows/Tests/badge.svg?branch=develop)](https://github.com/flashbots/mev-boost/actions?query=workflow%3A%22Tests%22) +[![Docker hub](https://badgen.net/docker/size/flashbots/mev-boost?icon=docker&label=image)](https://hub.docker.com/r/flashbots/mev-boost/tags) + +## What is MEV-Boost? + +`mev-boost` is open source middleware run by validators to access a competitive block-building market. MEV-Boost is an initial implementation of [proposer-builder separation (PBS)](https://ethresear.ch/t/proposer-block-builder-separation-friendly-fee-market-designs/9725) for proof-of-stake (PoS) Ethereum. + +With MEV-Boost, validators can access blocks from a marketplace of builders. Builders produce blocks containing transaction orderflow and a fee for the block proposing validator. Separating the role of proposers from block builders promotes greater competition, decentralization, and censorship-resistance for Ethereum. + +## How does MEV-Boost work? + +PoS node operators must run three pieces of software: a validator client, a consensus client, and an execution client. MEV-boost is a sidecar for the beacon node - a separate piece of open source software, which queries and outsources block-building to a network of builders. Block builders prepare full blocks, optimizing for MEV extraction and fair distribution of rewards. They then submit their blocks to relays. + +Relays aggregate blocks from **multiple** builders in order to select the block with the highest fees. One instance of MEV-boost can be configured by a validator to connect to **multiple** relays. The consensus layer client of a validator proposes the most profitable block received from MEV-boost to the Ethereum network for attestation and block inclusion. + +A MEV-Boost security assessment was conducted on 2022-06-20 by [lotusbumi](https://github.com/lotusbumi). Additional information can be found in the [Security](#security) section of this repository. + + +![MEV-Boost service integration overview](https://raw.githubusercontent.com/flashbots/mev-boost/54567443e718b09f8034d677723476b679782fb7/docs/mev-boost-integration-overview.png) + +## Who can run MEV-Boost? + +MEV-Boost is a piece of software that any PoS Ethereum node operator (including solo validators) can run as part of their Beacon Client software. It is compatible with any Ethereum consensus client. Support and installation instructions for each client can be found [here](#installing). + +--- + +See also: + +* [MEV-Boost Docker images](https://hub.docker.com/r/flashbots/mev-boost) +* [Wiki](https://github.com/flashbots/mev-boost/wiki) +* Specs: + * [Builder API](https://ethereum.github.io/builder-specs) + +--- + +# Table of Contents + +- [Background](#background) +- [Installing](#installing) + - [Binaries](#binaries) + - [From source](#from-source) + - [From Docker image](#from-docker-image) + - [Systemd configuration](#systemd-configuration) +- [Usage](#usage) + - [Mainnet](#mainnet) + - [Goerli testnet](#goerli-testnet) + - [Sepolia testnet](#sepolia-testnet) + - [Holesky testnet](#holesky-testnet) + - [`test-cli`](#test-cli) + - [mev-boost cli arguments](#mev-boost-cli-arguments) +- [API](#api) +- [Maintainers](#maintainers) +- [Contributing](#contributing) +- [Security](#security) + - [Bug Bounty](#bug-bounty) + - [Audits](#audits) +- [License](#license) + +--- + +# Background + +MEV is a centralizing force on Ethereum. Unattended, the competition for MEV opportunities leads to consensus security instability and permissioned communication infrastructure between traders and block producers. This erodes neutrality, transparency, decentralization, and permissionlessness. + +Proposer/block-builder separation (PBS) was initially proposed by Ethereum researchers as a response to the risk that MEV poses to decentralization of consensus networks. They have suggested that uncontrolled MEV extraction promotes economies of scale which are centralizing in nature, and complicate decentralized pooling. + + +In the future, [proposer/builder separation](https://ethresear.ch/t/two-slot-proposer-builder-separation/10980) will be enshrined in the Ethereum protocol itself to further harden its trust model. + +Read more in [Why run MEV-Boost?](https://writings.flashbots.net/why-run-mevboost/) and in the [Frequently Asked Questions](https://github.com/flashbots/mev-boost/wiki/Frequently-Asked-Questions). + +# Installing + +The most common setup is to install MEV-Boost on the same machine as the beacon client. Multiple beacon-clients can use a single MEV-Boost instance. The default port is 18550. + +See also [Rémy Roy's guide](https://github.com/remyroy/ethstaker/blob/main/prepare-for-the-merge.md#installing-mev-boost) for comprehensive instructions on installing, configuring and running MEV-Boost. + +## Binaries + +Each release includes binaries from Linux, Windows and macOS. You can find the latest release at +https://github.com/flashbots/mev-boost/releases + + +## From source + +Requires [Go 1.18+](https://go.dev/doc/install). + +### `go install` + +Install the latest MEV-Boost release with `go install`: + +```bash +go install github.com/flashbots/mev-boost@latest +mev-boost -help +``` + +### Clone and Build + +Ensure you are downloading the most updated MEV-Boost release. Releases are available at https://github.com/flashbots/mev-boost/releases + +clone the repository and build it: + +```bash +# By default, the develop branch includes ongoing merged PRs a future release. +git clone https://github.com/flashbots/mev-boost.git +cd mev-boost + +# You can use the stable branch, which is always updated with the latest released version +git checkout stable + +# If you want to build a specific release, check out the tag. See also https://github.com/flashbots/mev-boost/releases +git checkout tags/YOUR_VERSION + +# Build most recent version of MEV-Boost +make build + +# Show help. This confirms MEV-Boost is able to start +./mev-boost -help +``` + +## From Docker image + +We maintain a MEV-Boost Docker images at https://hub.docker.com/r/flashbots/mev-boost + +- [Install Docker Engine](https://docs.docker.com/engine/install/) +- Pull & run the latest image: + +```bash +# Get the MEV-Boost image +docker pull flashbots/mev-boost:latest + +# Run it +docker run flashbots/mev-boost -help +``` + +## Systemd configuration + +You can run MEV-Boost with a systemd config like this: + +<details> +<summary><code>/etc/systemd/system/mev-boost.service</code></summary> + +```ini +[Unit] +Description=mev-boost +Wants=network-online.target +After=network-online.target + +[Service] +User=mev-boost +Group=mev-boost +WorkingDirectory=/home/mev-boost +Type=simple +Restart=always +RestartSec=5 +ExecStart=/home/mev-boost/bin/mev-boost \ + -relay-check \ + -relay YOUR_RELAY_CHOICE_A \ + -relay YOUR_RELAY_CHOICE_B \ + -relay YOUR_RELAY_CHOICE_C + +[Install] +WantedBy=multi-user.target +``` +</details> + + +# Usage + +A single MEV-Boost instance can be used by multiple beacon nodes and validators. + +Aside from running MEV-Boost on your local network, you must configure: +* individual **beacon nodes** to connect to MEV-Boost. Beacon Node configuration varies by Consensus client. Guides for each client can be found on the [MEV-boost website](https://boost.flashbots.net/#block-356364ebd7cc424fb524428ed0134b21). +* individual **validators** to configure a preferred relay selection. Note: validators should take precautions to only connect to trusted relays. Read more about [the role of relays here](https://docs.flashbots.net/flashbots-mev-boost/relay). + +Lists of available relays are maintained by +* [Ethstaker](https://github.com/remyroy/ethstaker/blob/main/MEV-relay-list.md) [[2]](https://ethstaker.cc/mev-relay-list/) +* [Lido](https://research.lido.fi/t/lido-on-ethereum-call-for-relay-providers/2844) + +## Note on usage documentation + +The documentation in this README reflects the latest state of the `main` branch, which may have cli flags or functionality not present in the latest release. + +Please take a look at the specific release documentation about the available command line flags: https://github.com/flashbots/mev-boost/releases + +## Mainnet + +Run MEV-Boost pointed at a mainnet relay: + +``` +./mev-boost -relay-check -relay URL-OF-TRUSTED-RELAY +``` + +## Goerli testnet + +Run MEV-Boost pointed at a Goerli relay: + +``` +./mev-boost -goerli -relay-check -relay URL-OF-TRUSTED-RELAY +``` + +## Sepolia testnet + +Run MEV-Boost pointed at a Sepolia relay: + +``` +./mev-boost -sepolia -relay-check -relay URL-OF-TRUSTED-RELAY +``` + +## Holesky testnet + +Run MEV-Boost pointed at a Holesky relay: + +``` +./mev-boost -holesky -relay-check -relay URL-OF-TRUSTED-RELAY +``` + +## `test-cli` + +`test-cli` is a utility to execute all proposer requests against MEV-Boost + relay. See also the [test-cli readme](cmd/test-cli/README.md). + + +## mev-boost cli arguments + +These are the CLI arguments for the develop branch. For arguments available in a specific release, check the [release page](https://github.com/flashbots/mev-boost/releases). + +``` +$ ./mev-boost -help +Usage of mev-boost: + -addr string + listen-address for mev-boost server (default "localhost:18550") + -debug + shorthand for '-loglevel debug' + -genesis-fork-version string + use a custom genesis fork version + -goerli + use Goerli + -holesky + use Holesky + -json + log in JSON format instead of text + -log-no-version + disables adding the version to every log entry + -log-service string + add a 'service=...' tag to all log messages + -loglevel string + minimum loglevel: trace, debug, info, warn/warning, error, fatal, panic (default "info") + -mainnet + use Mainnet (default true) + -min-bid float + minimum bid to accept from a relay [eth] + -relay value + a single relay, can be specified multiple times + -relay-check + check relay status on startup and on the status API call + -relay-monitor value + a single relay monitor, can be specified multiple times + -relay-monitors string + relay monitor urls - single entry or comma-separated list (scheme://host) + -relays string + relay urls - single entry or comma-separated list (scheme://pubkey@host) + -request-timeout-getheader int + timeout for getHeader requests to the relay [ms] (default 950) + -request-timeout-getpayload int + timeout for getPayload requests to the relay [ms] (default 4000) + -request-timeout-regval int + timeout for registerValidator requests [ms] (default 3000) + -sepolia + use Sepolia + -version + only print version +``` + +### `-relays` vs `-relay` + +There are two different flags for specifying relays: `-relays` and `-relay`. +The `-relays` flag is a comma separated string of relays. On the other hand, +the `-relay` flag is used to specify a single relay, but can be used multiple +times for multiple relays. Use whichever method suits your preferences. + +These two MEV-Boost commands are equivalent: + +``` +./mev-boost -relay-check \ + -relays $YOUR_RELAY_CHOICE_A,$YOUR_RELAY_CHOICE_B,$YOUR_RELAY_CHOICE_C +``` + +``` +./mev-boost -relay-check \ + -relay $YOUR_RELAY_CHOICE_A \ + -relay $YOUR_RELAY_CHOICE_B \ + -relay $YOUR_RELAY_CHOICE_C +``` + + +### Setting a minimum bid value with `-min-bid` + +The `-min-bid` flag allows setting a minimum bid value. If no bid from the builder network delivers at least this value, MEV-Boost will not return a bid +to the beacon node, making it fall back to local block production. + +Example for setting a minimum bid value of 0.06 ETH: + +``` +./mev-boost \ + -min-bid 0.06 \ + -relay $YOUR_RELAY_CHOICE_A \ + -relay $YOUR_RELAY_CHOICE_B \ + -relay $YOUR_RELAY_CHOICE_C +``` + +--- + +# API + +`mev-boost` implements the latest [Builder Specification](https://github.com/ethereum/builder-specs). + +```mermaid +sequenceDiagram + participant consensus + participant mev_boost + participant relays + Title: Block Proposal + Note over consensus: validator starts up + consensus->>mev_boost: registerValidator + mev_boost->>relays: registerValidator + Note over consensus: wait for allocated slot + consensus->>mev_boost: getHeader + mev_boost->>relays: getHeader + relays-->>mev_boost: getHeader response + Note over mev_boost: verify response matches expected + Note over mev_boost: select best payload + mev_boost-->>consensus: getHeader response + Note over consensus: sign the header + consensus->>mev_boost: submitBlindedBlock + Note over mev_boost: identify payload source + mev_boost->>relays: submitBlindedBlock + Note over relays: validate signature + relays-->>mev_boost: submitBlindedBlock response + Note over mev_boost: verify response matches expected + mev_boost-->>consensus: submitBlindedBlock response +``` + +# Maintainers + +- [@metachris](https://github.com/metachris) +- [@jtraglia](https://github.com/jtraglia) +- [@ralexstokes](https://github.com/ralexstokes) +- [@terencechain](https://github.com/terencechain) +- [@lightclient](https://github.com/lightclient) +- [@avalonche](https://github.com/avalonche) +- [@Ruteri](https://github.com/Ruteri) + +# Contributing + +You are welcome here <3. + +- If you have a question, feedback or a bug report for this project, please [open a new Issue](https://github.com/flashbots/mev-boost/issues). +- If you would like to contribute with code, check the [CONTRIBUTING file](CONTRIBUTING.md) for further info about the development environment. +- We just ask you to be nice. Read our [code of conduct](CODE_OF_CONDUCT.md). + +# Security + +If you find a security vulnerability in this project or any other initiative +related to proposer/builder separation in ethereum, please let us know sending +an email to security@flashbots.net. Refer to the [SECURITY file](SECURITY.md) +for details. + +## Bug Bounty + +We have a bug bounty program! Get up to $25k USD for a critical vulnerability. + +We would like to welcome node operators, builders, searchers, and other +participants in the ecosystem to contribute to this bounty pool to help make the +ecosystem more secure. + +## Audits + +- [20220620](docs/audit-20220620.md), by [lotusbumi](https://github.com/lotusbumi). + +# License + +The code in this project is free software under the [MIT License](LICENSE). + +Logo by [@lekevicius](https://twitter.com/lekevicius) on CC0 license.
diff --git mev-boost/README.md bolt-mev-boost/README.md index 4111105955ea42abf1ce78e7bf3f03c5fd655b65..13906c68855eca1a01ec3570878c162f3cf16b14 100644 --- mev-boost/README.md +++ bolt-mev-boost/README.md @@ -1,389 +1,15 @@ -![mev-boost](https://user-images.githubusercontent.com/116939/224986867-3d1916c6-3219-4d61-b1ce-213fc663070c.png) - -# - -[![Goreport status](https://goreportcard.com/badge/github.com/flashbots/mev-boost)](https://goreportcard.com/report/github.com/flashbots/mev-boost) -[![Test status](https://github.com/flashbots/mev-boost/workflows/Tests/badge.svg?branch=develop)](https://github.com/flashbots/mev-boost/actions?query=workflow%3A%22Tests%22) -[![Docker hub](https://badgen.net/docker/size/flashbots/mev-boost?icon=docker&label=image)](https://hub.docker.com/r/flashbots/mev-boost/tags) - -## What is MEV-Boost? - -`mev-boost` is open source middleware run by validators to access a competitive block-building market. MEV-Boost is an initial implementation of [proposer-builder separation (PBS)](https://ethresear.ch/t/proposer-block-builder-separation-friendly-fee-market-designs/9725) for proof-of-stake (PoS) Ethereum. - -With MEV-Boost, validators can access blocks from a marketplace of builders. Builders produce blocks containing transaction orderflow and a fee for the block proposing validator. Separating the role of proposers from block builders promotes greater competition, decentralization, and censorship-resistance for Ethereum. - -## How does MEV-Boost work? - -PoS node operators must run three pieces of software: a validator client, a consensus client, and an execution client. MEV-boost is a sidecar for the beacon node - a separate piece of open source software, which queries and outsources block-building to a network of builders. Block builders prepare full blocks, optimizing for MEV extraction and fair distribution of rewards. They then submit their blocks to relays. - -Relays aggregate blocks from **multiple** builders in order to select the block with the highest fees. One instance of MEV-boost can be configured by a validator to connect to **multiple** relays. The consensus layer client of a validator proposes the most profitable block received from MEV-boost to the Ethereum network for attestation and block inclusion. - -A MEV-Boost security assessment was conducted on 2022-06-20 by [lotusbumi](https://github.com/lotusbumi). Additional information can be found in the [Security](#security) section of this repository. - - -![MEV-Boost service integration overview](https://raw.githubusercontent.com/flashbots/mev-boost/54567443e718b09f8034d677723476b679782fb7/docs/mev-boost-integration-overview.png) - -## Who can run MEV-Boost? - -MEV-Boost is a piece of software that any PoS Ethereum node operator (including solo validators) can run as part of their Beacon Client software. It is compatible with any Ethereum consensus client. Support and installation instructions for each client can be found [here](#installing). - ---- - -See also: - -* [MEV-Boost Docker images](https://hub.docker.com/r/flashbots/mev-boost) -* [Wiki](https://github.com/flashbots/mev-boost/wiki) -* Specs: - * [Builder API](https://ethereum.github.io/builder-specs) - ---- - -# Table of Contents - -- [Background](#background) -- [Installing](#installing) - - [Binaries](#binaries) - - [From source](#from-source) - - [From Docker image](#from-docker-image) - - [Systemd configuration](#systemd-configuration) -- [Usage](#usage) - - [Mainnet](#mainnet) - - [Goerli testnet](#goerli-testnet) - - [Sepolia testnet](#sepolia-testnet) - - [Holesky testnet](#holesky-testnet) - - [`test-cli`](#test-cli) - - [mev-boost cli arguments](#mev-boost-cli-arguments) -- [API](#api) -- [Maintainers](#maintainers) -- [Contributing](#contributing) -- [Security](#security) - - [Bug Bounty](#bug-bounty) - - [Audits](#audits) -- [License](#license) - ---- - -# Background - -MEV is a centralizing force on Ethereum. Unattended, the competition for MEV opportunities leads to consensus security instability and permissioned communication infrastructure between traders and block producers. This erodes neutrality, transparency, decentralization, and permissionlessness. - -Proposer/block-builder separation (PBS) was initially proposed by Ethereum researchers as a response to the risk that MEV poses to decentralization of consensus networks. They have suggested that uncontrolled MEV extraction promotes economies of scale which are centralizing in nature, and complicate decentralized pooling. - - -In the future, [proposer/builder separation](https://ethresear.ch/t/two-slot-proposer-builder-separation/10980) will be enshrined in the Ethereum protocol itself to further harden its trust model. - -Read more in [Why run MEV-Boost?](https://writings.flashbots.net/why-run-mevboost/) and in the [Frequently Asked Questions](https://github.com/flashbots/mev-boost/wiki/Frequently-Asked-Questions). - -# Installing - -The most common setup is to install MEV-Boost on the same machine as the beacon client. Multiple beacon-clients can use a single MEV-Boost instance. The default port is 18550. - -See also [RĂ©my Roy's guide](https://github.com/remyroy/ethstaker/blob/main/prepare-for-the-merge.md#installing-mev-boost) for comprehensive instructions on installing, configuring and running MEV-Boost. - -## Binaries - -Each release includes binaries from Linux, Windows and macOS. You can find the latest release at -https://github.com/flashbots/mev-boost/releases - - -## From source - -Requires [Go 1.18+](https://go.dev/doc/install). - -### `go install` - -Install the latest MEV-Boost release with `go install`: - -```bash -go install github.com/flashbots/mev-boost@latest -mev-boost -help -``` - -### Clone and Build - -Ensure you are downloading the most updated MEV-Boost release. Releases are available at https://github.com/flashbots/mev-boost/releases - -clone the repository and build it: - -```bash -# By default, the develop branch includes ongoing merged PRs a future release. -git clone https://github.com/flashbots/mev-boost.git -cd mev-boost - -# You can use the stable branch, which is always updated with the latest released version -git checkout stable - -# If you want to build a specific release, check out the tag. See also https://github.com/flashbots/mev-boost/releases -git checkout tags/YOUR_VERSION - -# Build most recent version of MEV-Boost -make build - -# Show help. This confirms MEV-Boost is able to start -./mev-boost -help -``` - -## From Docker image - -We maintain a MEV-Boost Docker images at https://hub.docker.com/r/flashbots/mev-boost - -- [Install Docker Engine](https://docs.docker.com/engine/install/) -- Pull & run the latest image: - -```bash -# Get the MEV-Boost image -docker pull flashbots/mev-boost:latest - -# Run it -docker run flashbots/mev-boost -help -``` - -## Systemd configuration - -You can run MEV-Boost with a systemd config like this: - -<details> -<summary><code>/etc/systemd/system/mev-boost.service</code></summary> - -```ini -[Unit] -Description=mev-boost -Wants=network-online.target -After=network-online.target - -[Service] -User=mev-boost -Group=mev-boost -WorkingDirectory=/home/mev-boost -Type=simple -Restart=always -RestartSec=5 -ExecStart=/home/mev-boost/bin/mev-boost \ - -relay-check \ - -relay YOUR_RELAY_CHOICE_A \ - -relay YOUR_RELAY_CHOICE_B \ - -relay YOUR_RELAY_CHOICE_C - -[Install] -WantedBy=multi-user.target -``` -</details> - - -# Usage - -A single MEV-Boost instance can be used by multiple beacon nodes and validators. - -Aside from running MEV-Boost on your local network, you must configure: -* individual **beacon nodes** to connect to MEV-Boost. Beacon Node configuration varies by Consensus client. Guides for each client can be found on the [MEV-boost website](https://boost.flashbots.net/#block-356364ebd7cc424fb524428ed0134b21). -* individual **validators** to configure a preferred relay selection. Note: validators should take precautions to only connect to trusted relays. Read more about [the role of relays here](https://docs.flashbots.net/flashbots-mev-boost/relay). - -Lists of available relays are maintained by -* [Ethstaker](https://github.com/remyroy/ethstaker/blob/main/MEV-relay-list.md) [[2]](https://ethstaker.cc/mev-relay-list/) -* [Lido](https://research.lido.fi/t/lido-on-ethereum-call-for-relay-providers/2844) - -## Note on usage documentation - -The documentation in this README reflects the latest state of the `main` branch, which may have cli flags or functionality not present in the latest release. - -Please take a look at the specific release documentation about the available command line flags: https://github.com/flashbots/mev-boost/releases - -## Mainnet - -Run MEV-Boost pointed at a mainnet relay: - -``` -./mev-boost -relay-check -relay URL-OF-TRUSTED-RELAY -``` - -## Goerli testnet - -Run MEV-Boost pointed at a Goerli relay: - -``` -./mev-boost -goerli -relay-check -relay URL-OF-TRUSTED-RELAY -``` - -## Sepolia testnet - -Run MEV-Boost pointed at a Sepolia relay: - -``` -./mev-boost -sepolia -relay-check -relay URL-OF-TRUSTED-RELAY -``` - -## Holesky testnet - -Run MEV-Boost pointed at a Holesky relay: - -``` -./mev-boost -holesky -relay-check -relay URL-OF-TRUSTED-RELAY -``` - -## `test-cli` +[flashbots mev-boost readme](README.flashbots.md)   -`test-cli` is a utility to execute all proposer requests against MEV-Boost + relay. See also the [test-cli readme](cmd/test-cli/README.md). +# Bolt MEV-Boost   +Bolt MEV-Boost is a fork of the Flashbots MEV-Boost package that +implements the functionality of the Constraints API.   -## mev-boost cli arguments +## How it works   -These are the CLI arguments for the develop branch. For arguments available in a specific release, check the [release page](https://github.com/flashbots/mev-boost/releases). +The MEV-Boost package has the standard functionality of the Flashbots MEV-Boost, but with the added functionality of the +Constraints API which can be summarized as follows:   -``` -$ ./mev-boost -help -Usage of mev-boost: - -addr string - listen-address for mev-boost server (default "localhost:18550") - -debug - shorthand for '-loglevel debug' - -genesis-fork-version string - use a custom genesis fork version - -goerli - use Goerli - -holesky - use Holesky - -json - log in JSON format instead of text - -log-no-version - disables adding the version to every log entry - -log-service string - add a 'service=...' tag to all log messages - -loglevel string - minimum loglevel: trace, debug, info, warn/warning, error, fatal, panic (default "info") - -mainnet - use Mainnet (default true) - -min-bid float - minimum bid to accept from a relay [eth] - -relay value - a single relay, can be specified multiple times - -relay-check - check relay status on startup and on the status API call - -relay-monitor value - a single relay monitor, can be specified multiple times - -relay-monitors string - relay monitor urls - single entry or comma-separated list (scheme://host) - -relays string - relay urls - single entry or comma-separated list (scheme://pubkey@host) - -request-timeout-getheader int - timeout for getHeader requests to the relay [ms] (default 950) - -request-timeout-getpayload int - timeout for getPayload requests to the relay [ms] (default 4000) - -request-timeout-regval int - timeout for registerValidator requests [ms] (default 3000) - -sepolia - use Sepolia - -version - only print version -``` - -### `-relays` vs `-relay` - -There are two different flags for specifying relays: `-relays` and `-relay`. -The `-relays` flag is a comma separated string of relays. On the other hand, -the `-relay` flag is used to specify a single relay, but can be used multiple -times for multiple relays. Use whichever method suits your preferences. - -These two MEV-Boost commands are equivalent: - -``` -./mev-boost -relay-check \ - -relays $YOUR_RELAY_CHOICE_A,$YOUR_RELAY_CHOICE_B,$YOUR_RELAY_CHOICE_C -``` - -``` -./mev-boost -relay-check \ - -relay $YOUR_RELAY_CHOICE_A \ - -relay $YOUR_RELAY_CHOICE_B \ - -relay $YOUR_RELAY_CHOICE_C -``` - - -### Setting a minimum bid value with `-min-bid` - -The `-min-bid` flag allows setting a minimum bid value. If no bid from the builder network delivers at least this value, MEV-Boost will not return a bid -to the beacon node, making it fall back to local block production. - -Example for setting a minimum bid value of 0.06 ETH: - -``` -./mev-boost \ - -min-bid 0.06 \ - -relay $YOUR_RELAY_CHOICE_A \ - -relay $YOUR_RELAY_CHOICE_B \ - -relay $YOUR_RELAY_CHOICE_C -``` - ---- - -# API - -`mev-boost` implements the latest [Builder Specification](https://github.com/ethereum/builder-specs). - -```mermaid -sequenceDiagram - participant consensus - participant mev_boost - participant relays - Title: Block Proposal - Note over consensus: validator starts up - consensus->>mev_boost: registerValidator - mev_boost->>relays: registerValidator - Note over consensus: wait for allocated slot - consensus->>mev_boost: getHeader - mev_boost->>relays: getHeader - relays-->>mev_boost: getHeader response - Note over mev_boost: verify response matches expected - Note over mev_boost: select best payload - mev_boost-->>consensus: getHeader response - Note over consensus: sign the header - consensus->>mev_boost: submitBlindedBlock - Note over mev_boost: identify payload source - mev_boost->>relays: submitBlindedBlock - Note over relays: validate signature - relays-->>mev_boost: submitBlindedBlock response - Note over mev_boost: verify response matches expected - mev_boost-->>consensus: submitBlindedBlock response -``` - -# Maintainers - -- [@metachris](https://github.com/metachris) -- [@jtraglia](https://github.com/jtraglia) -- [@ralexstokes](https://github.com/ralexstokes) -- [@terencechain](https://github.com/terencechain) -- [@lightclient](https://github.com/lightclient) -- [@avalonche](https://github.com/avalonche) -- [@Ruteri](https://github.com/Ruteri) - -# Contributing - -You are welcome here <3. - -- If you have a question, feedback or a bug report for this project, please [open a new Issue](https://github.com/flashbots/mev-boost/issues). -- If you would like to contribute with code, check the [CONTRIBUTING file](CONTRIBUTING.md) for further info about the development environment. -- We just ask you to be nice. Read our [code of conduct](CODE_OF_CONDUCT.md). - -# Security - -If you find a security vulnerability in this project or any other initiative -related to proposer/builder separation in ethereum, please let us know sending -an email to security@flashbots.net. Refer to the [SECURITY file](SECURITY.md) -for details. - -## Bug Bounty - -We have a bug bounty program! Get up to $25k USD for a critical vulnerability. - -We would like to welcome node operators, builders, searchers, and other -participants in the ecosystem to contribute to this bounty pool to help make the -ecosystem more secure. - -## Audits - -- [20220620](docs/audit-20220620.md), by [lotusbumi](https://github.com/lotusbumi). - -# License - -The code in this project is free software under the [MIT License](LICENSE). - -Logo by [@lekevicius](https://twitter.com/lekevicius) on CC0 license. +1. Propagate incoming constraint messages to relays +2. Validate incoming headers and inclusion proofs +3. Forward the best bid to the proposer for signing
diff --git mev-boost/RELEASE.md bolt-mev-boost/RELEASE.md index f14d8f302a25eb801250c5f75380f17226ade1a4..d00d82693c20b33dcc2ce853fb35fbc151f0653e 100644 --- mev-boost/RELEASE.md +++ bolt-mev-boost/RELEASE.md @@ -44,11 +44,10 @@ For example, creating a new release `v1.9`:   1. Create a Github issue about the upcoming release ([example](https://github.com/flashbots/mev-boost/issues/524)) -2. Create a release branch: `release/v1.9` (note: use the target version as branch name, don't add the `-alpha` suffix) -3. Tag an alpha version: `v1.9-alpha1` -4. Test in testnets, iterate as needed, create more alpha versions if needed -5. When tests are complete, create the final tag and release -6. +1. Create a release branch (`release/v1.9`) +1. Create an alpha release: `v1.9-alpha1` +1. Test in testnets, iterate as needed, create more alpha versions +1. When tests are complete, create the final release   ```bash # create a new branch @@ -84,18 +83,16 @@ ## Tagging a version and pushing the release   To create a new version (with tag), follow all these steps! They are necessary to have the correct build version inside, and work with `go install`.   -* In the release branch * Update [`Version`](/config/vars.go) to final version to `v1.9`, and commit * Create final tags, both semver and pep440: * `git tag -s v1.9` * `git tag -s v1.9.0` * Update the `stable` branch: * `git checkout stable` - * `git merge tags/v1.9 --ff-only` (ff-only is important, otherwise git doesn't know the stable branch is based off the v1.9 tag!) -* Update the `develop` branch: + * `git merge tags/v1.9 --ff-only` * `git checkout develop` * `git merge tags/v1.9 --ff-only` -* Update `Version` in `config/vars.go` to next patch with `dev` suffix (eg. `v1.10-dev`) and commit to `develop` branch +* Update `Version` in `config/vars.go` to next patch with `dev` suffix (eg. `v1.10-dev`) and commit to develop * Now push the `develop` and `stable` branches, as well as the tag: `git push origin develop stable --tags`   Now check the Github CI actions for release activity: https://github.com/flashbots/mev-boost/actions
diff --git mev-boost/go.mod bolt-mev-boost/go.mod index f6d36d87a037cadeb0e72aa027055381d4190a45..d745164165fe55e3c4158c3ba3597116035e3740 100644 --- mev-boost/go.mod +++ bolt-mev-boost/go.mod @@ -1,6 +1,6 @@ module github.com/flashbots/mev-boost   -go 1.21 +go 1.22   require ( github.com/ethereum/go-ethereum v1.13.10 @@ -8,6 +8,7 @@ github.com/flashbots/go-boost-utils v1.8.0 github.com/flashbots/go-utils v0.5.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 + github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/holiman/uint256 v1.2.4 github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 github.com/sirupsen/logrus v1.9.3 @@ -27,6 +28,7 @@ github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 // indirect github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect + github.com/emicklei/dot v1.6.2 // indirect github.com/ethereum/c-kzg-4844 v0.4.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 // indirect @@ -67,7 +69,7 @@ github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect - github.com/ferranbt/fastssz v0.1.3 // indirect + github.com/ferranbt/fastssz v0.1.4-0.20240724090034-31cd371f8688 github.com/go-ole/go-ole v1.2.6 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
diff --git mev-boost/go.sum bolt-mev-boost/go.sum index 0aa577d7bd680f119ff0793e03558a69ce83b42e..63d6c536f33a1513bbb5f9ef1b4c1e26d87d6d9e 100644 --- mev-boost/go.sum +++ bolt-mev-boost/go.sum @@ -88,6 +88,8 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= @@ -103,6 +105,8 @@ github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= +github.com/ferranbt/fastssz v0.1.4-0.20240724090034-31cd371f8688 h1:k70X5h1haHaSbpD/9fcjtvAUEVlRlOKtdpvN7Mzhcv4= +github.com/ferranbt/fastssz v0.1.4-0.20240724090034-31cd371f8688/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/flashbots/go-boost-utils v1.8.0 h1:z3K1hw+Fbl9AGMNQKnK7Bvf0M/rKgjfruAEvra+Z8Mg= github.com/flashbots/go-boost-utils v1.8.0/go.mod h1:Ry1Rw8Lx5v1rpAR0+IvR4sV10jYAeQaGVM3vRD8mYdM= github.com/flashbots/go-utils v0.5.0 h1:ldjWta9B9//DJU2QcwRbErez3+1aKhSn6EoFc6d5kPY= @@ -187,6 +191,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=