From eb1038b5542d5a581f4f7a9e8280a78992b4a40c Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 11:29:39 +0200 Subject: [PATCH 01/17] feat(server): add constraint cache, refactor proof verification --- mev-boost/server/constraints.go | 47 +++++++ mev-boost/server/service.go | 232 +++++++++++++++----------------- mev-boost/server/utils.go | 10 -- 3 files changed, 154 insertions(+), 135 deletions(-) create mode 100644 mev-boost/server/constraints.go diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go new file mode 100644 index 00000000..fa16cc87 --- /dev/null +++ b/mev-boost/server/constraints.go @@ -0,0 +1,47 @@ +package server + +import "github.com/attestantio/go-eth2-client/spec/phase0" + +// Constraints is a map of constraints for a block. +type Constraints = map[phase0.Hash32]*Constraint + +// Constraint is a constraint on a block. For now just preconfirmations +// or inclusion constraints. +type Constraint struct { + RawTx Transaction `json:"rawTx"` +} + +// ConstraintCache is a cache for constraints. +type ConstraintCache struct { + // map of slots to constraints + constraints map[uint64]Constraints +} + +// NewConstraintCache creates a new constraint cache. +func NewConstraintCache() *ConstraintCache { + return &ConstraintCache{ + // TODO: there should be a maximum length here that we can pre-allocate (probably the lookahead window size) + constraints: make(map[uint64]Constraints), + } +} + +// AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. +func (c *ConstraintCache) AddInclusionConstraint(slot uint64, txHash phase0.Hash32, rawTx Transaction) { + if _, exists := c.constraints[slot]; !exists { + c.constraints[slot] = make(map[phase0.Hash32]*Constraint) + } + + c.constraints[slot][txHash] = &Constraint{ + RawTx: rawTx, + } +} + +// Get gets the constraints at the given slot. +func (c *ConstraintCache) Get(slot uint64) Constraints { + return c.constraints[slot] +} + +// Delete deletes the constraints at the given slot. +func (c *ConstraintCache) Delete(slot uint64) { + delete(c.constraints, slot) +} diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 57c92446..9f5b529f 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -3,7 +3,6 @@ package server import ( "bytes" "context" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -34,6 +33,7 @@ import ( "github.com/sirupsen/logrus" ) +// Standard errors var ( errNoRelays = errors.New("no relays") errInvalidSlot = errors.New("invalid slot") @@ -43,6 +43,13 @@ var ( errServerAlreadyRunning = errors.New("server already running") ) +// Bolt errors +var ( + errNilProof = errors.New("nil proof") + errInvalidProofs = errors.New("proof verification failed") + errInvalidRoot = errors.New("failed getting tx root from bid") +) + var ( nilHash = phase0.Hash32{} nilResponse = struct{}{} @@ -106,6 +113,8 @@ type BoostService struct { // BOLT: sidecar connection sidecar *boltSidecar + + constraints *ConstraintCache } // NewBoostService created a new BoostService @@ -149,6 +158,8 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { requestMaxRetries: opts.RequestMaxRetries, sidecar: boltSidecar, + // Initialize the constraint cache + constraints: NewConstraintCache(), }, nil } @@ -318,6 +329,82 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error()) } +// verifyConstraintProofs verifies the proofs against the constraints, and returns an error if the proofs are invalid. +func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithPreconfirmationsProofs, constraints Constraints) error { + log := m.log.WithFields(logrus.Fields{}) + // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. + if responsePayload.Proofs != nil { + // BOLT: remove unnecessary fields while logging + log.WithFields(logrus.Fields{}) + + log.Info("[BOLT]: Verifying preconfirmation proofs", len(responsePayload.Proofs)) + + transactionsRoot, err := responsePayload.Bid.TransactionsRoot() + if err != nil { + log.WithError(err).Error("[BOLT]: error getting tx root from bid") + return errInvalidRoot + } + + for _, proof := range responsePayload.Proofs { + if proof == nil { + log.Warn("[BOLT]: Nil proof!") + return errNilProof + } + + // Find the raw tx with the hash specified + constraint, ok := constraints[proof.TxHash] + if !ok { + log.Warnf("[BOLT]: Tx hash %s not found in constraints", proof.TxHash.String()) + // We don't actually have to return an error here, the relay just provided a proof that was unnecessary + continue + } + + rawTx := constraint.RawTx + + log.Infof("[BOLT]: Raw tx: %x", rawTx) + + // Compute the hash tree root for the raw preconfirmed transaction + // and use it as "Leaf" in the proof to be verified against + txHashTreeRoot, err := rawTx.HashTreeRoot() + if err != nil { + log.WithError(err).Error("[BOLT]: error getting tx hash tree root") + return errInvalidRoot + } + + log.Infof("[BOLT]: Tx hash tree root: %x", txHashTreeRoot) + + // Verify the proof + sszProof := proof.MerkleProof.ToFastSszProof(txHashTreeRoot[:]) + + log.Infof("[BOLT]: Fast sszProof index: %d", sszProof.Index) + log.Infof("[BOLT]: Fast sszProof hashes: %x", sszProof.Hashes) + log.Infof("[BOLT]: Fast sszProof leaf: %x. Raw tx: %x", sszProof.Leaf, rawTx) + + currentTime := time.Now() + ok, err = fastSsz.VerifyProof(transactionsRoot[:], sszProof) + elapsed := time.Since(currentTime) + + if err != nil { + log.WithError(err).Error("error verifying merkle proof") + // BOLT: we need to skip the bid in this case! + // for now we just return and complain loudly + return err + } + + if !ok { + log.Error("[BOLT]: proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String()) + // BOLT: we need to skip the bid in this case! + // for now we just return and complain loudly + return errInvalidProofs + } else { + log.Info(fmt.Sprintf("[BOLT]: Preconfirmation proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed)) + } + } + } + + return nil +} + // 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 @@ -337,7 +424,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) }) log.Debug("getHeader") - _slot, err := strconv.ParseUint(slot, 10, 64) + slotUint, err := strconv.ParseUint(slot, 10, 64) if err != nil { m.respondError(w, http.StatusBadRequest, errInvalidSlot.Error()) return @@ -355,8 +442,8 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) // Make sure we have a uid for this slot m.slotUIDLock.Lock() - if m.slotUID.slot < _slot { - m.slotUID.slot = _slot + if m.slotUID.slot < slotUint { + m.slotUID.slot = slotUint m.slotUID.uid = uuid.New() } slotUID := m.slotUID.uid @@ -364,13 +451,13 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) log = log.WithField("slotUID", slotUID) // Log how late into the slot the request starts - slotStartTimestamp := m.genesisTime + _slot*config.SlotTimeSec + slotStartTimestamp := m.genesisTime + slotUint*config.SlotTimeSec msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 log.WithFields(logrus.Fields{ "genesisTime": m.genesisTime, "slotTimeSec": config.SlotTimeSec, "msIntoSlot": msIntoSlot, - }).Infof("getHeader request start - %d milliseconds into slot %d", msIntoSlot, _slot) + }).Infof("getHeader request start - %d milliseconds into slot %d", msIntoSlot, slotUint) // Add request headers headers := map[string]string{ @@ -471,128 +558,23 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) return } - // BOLT: verify preconfirmation inclusion proofs - if responsePayload.Proofs != nil { - // BOLT: remove unnecessary fields while logging - log.WithFields(logrus.Fields{}) + // BOLT: Get the inclusion constraints for this slot + inclusionConstraints := m.constraints.Get(slotUint) - log.Info("[BOLT]: Verifying preconfirmation proofs", len(responsePayload.Proofs)) - - // BOLT: we should check these against the preconfirmation requests accepted by - // bolt-sidecar, but for now it's ok that the provided proofs are valid - preconfirmationsFromSidecar, err := m.sidecar.GetPreconfirmations(_slot) - if err != nil { - log.WithError(err).Error("[BOLT]: error fetching preconfirmed transactions from sidecar") - return - } - - if len(responsePayload.Proofs) > 0 { - message := fmt.Sprintf("BOLT-MEV-BOOST: received payload header with %d preconfirmations for slot %d", len(responsePayload.Proofs), _slot) - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"%s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } - } - - if len(responsePayload.Proofs) != len(preconfirmationsFromSidecar) { - log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != preconfs %d", - len(responsePayload.Proofs), len(preconfirmationsFromSidecar)) - w.WriteHeader(http.StatusBadRequest) - return - } + // BOLT: verify matching proofs & constraints + if len(responsePayload.Proofs) != len(inclusionConstraints) { + log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != preconfs %d", + len(responsePayload.Proofs), len(inclusionConstraints)) + return + } - transactionsRoot, err := responsePayload.Bid.TransactionsRoot() - if err != nil { - log.WithError(err).Error("[BOLT]: error getting tx root from bid") - w.WriteHeader(http.StatusBadRequest) + // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. + if responsePayload.Proofs != nil { + // BOLT: verify the proofs against the constraints. If they don't match, we don't consider the bid to be valid. + if err := m.verifyConstraintProofs(responsePayload, inclusionConstraints); err != nil { + log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err) return } - - for _, proof := range responsePayload.Proofs { - if proof == nil { - log.Warn("[BOLT]: Nil proof!") - // BOLT: we should probably skip the bid as well here - continue - } - - // Find the raw tx with the hash specified i - rawTxs := filter(preconfirmationsFromSidecar, func(preconfs *rawPreconfirmation) bool { - return preconfs.TxHash == proof.TxHash.String() - }) - - if len(rawTxs) == 0 { - log.Warn("[BOLT]: proof verification failed - tx hash not found") - w.WriteHeader(http.StatusBadRequest) - return - } - - // BOLT: We assume only 1 preconf, for now this is fine - rawTxStringWithout0x := strings.TrimPrefix(rawTxs[0].RawTx, "0x") - rawTxBytes, err := hex.DecodeString(rawTxStringWithout0x) - if err != nil { - log.WithError(err).Error("[BOLT]: error decoding raw tx from sidecar") - w.WriteHeader(http.StatusInternalServerError) - return - } - - rawTx := Transaction(rawTxBytes) - log.Infof("[BOLT]: Raw tx: %x", rawTx) - - // Compute the hash tree root for the raw preconfirmed transaction - // and use it as "Leaf" in the proof to be verified against - txHashTreeRoot, err := rawTx.HashTreeRoot() - if err != nil { - log.WithError(err).Error("[BOLT]: error getting tx hash tree root") - w.WriteHeader(http.StatusInternalServerError) - return - } - - log.Infof("[BOLT]: Tx hash tree root: %x", txHashTreeRoot) - - // Verify the proof - sszProof := proof.MerkleProof.ToFastSszProof(txHashTreeRoot[:]) - - log.Infof("[BOLT]: Fast sszProof index: %d", sszProof.Index) - log.Infof("[BOLT]: Fast sszProof hashes: %x", sszProof.Hashes) - log.Infof("[BOLT]: Fast sszProof leaf: %x. Raw tx: %x", sszProof.Leaf, rawTx) - - currentTime := time.Now() - ok, err := fastSsz.VerifyProof(transactionsRoot[:], sszProof) - elapsed := time.Since(currentTime) - - if err != nil { - log.WithError(err).Error("error verifying merkle proof") - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly - w.WriteHeader(http.StatusNoContent) - return - } - - if !ok { - log.Error("[BOLT]: proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String()) - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly - w.WriteHeader(http.StatusBadRequest) - return - } else { - message := fmt.Sprintf("BOLT-MEV-BOOST: Preconfirmation proof verified for tx hash %s in slot %d in %s", proof.TxHash.String(), _slot, elapsed) - log.Info(message) - - // BOLT: Log this event in the web demo backend - event := strings.NewReader(fmt.Sprintf("{ \"message\": \"%s\"}", message)) - eventRes, err := http.Post("http://host.docker.internal:3001/events", "application/json", event) - if err != nil { - log.Error("Failed to log preconfirms event: ", err) - } - if eventRes != nil { - defer eventRes.Body.Close() - } - } - } } mu.Lock() @@ -643,7 +625,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) }).Info("best bid") // Remember the bid, for future logging in case of withholding - bidKey := bidRespKey{slot: _slot, blockHash: result.bidInfo.blockHash.String()} + bidKey := bidRespKey{slot: slotUint, blockHash: result.bidInfo.blockHash.String()} m.bidsLock.Lock() m.bids[bidKey] = result m.bidsLock.Unlock() diff --git a/mev-boost/server/utils.go b/mev-boost/server/utils.go index bd4c1d63..fb779d53 100644 --- a/mev-boost/server/utils.go +++ b/mev-boost/server/utils.go @@ -270,13 +270,3 @@ func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockRe } return false } - -func filter[T any](slice []T, f func(T) bool) []T { - var n []T - for _, e := range slice { - if f(e) { - n = append(n, e) - } - } - return n -} From 564f6eced41678fec275781d8326acabd4a2529c Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 11:34:23 +0200 Subject: [PATCH 02/17] refactor(server): renamings --- mev-boost/server/{preconf.go => proofs.go} | 15 +++++++-------- mev-boost/server/service.go | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) rename mev-boost/server/{preconf.go => proofs.go} (85%) diff --git a/mev-boost/server/preconf.go b/mev-boost/server/proofs.go similarity index 85% rename from mev-boost/server/preconf.go rename to mev-boost/server/proofs.go index 4ad1da22..02f317ef 100644 --- a/mev-boost/server/preconf.go +++ b/mev-boost/server/proofs.go @@ -14,14 +14,14 @@ import ( "github.com/attestantio/go-eth2-client/spec/phase0" ) -type BidWithPreconfirmationsProofs struct { +type BidWithInclusionProofs struct { // The block bid Bid *builderSpec.VersionedSignedBuilderBid `json:"bid"` - // The preconfirmations with proofs - Proofs []*PreconfirmationWithProof `json:"proofs"` + // The inclusion proofs + Proofs []*InclusionProof `json:"proofs"` } -func (b *BidWithPreconfirmationsProofs) String() string { +func (b *BidWithInclusionProofs) String() string { out, err := json.Marshal(b) if err != nil { return err.Error() @@ -29,7 +29,7 @@ func (b *BidWithPreconfirmationsProofs) String() string { return string(out) } -func (p *PreconfirmationWithProof) String() string { +func (p *InclusionProof) String() string { proofs, err := json.Marshal(p) if err != nil { return err.Error() @@ -100,9 +100,8 @@ func (s *SerializedMerkleProof) ToFastSszProof(leaf []byte) *fastSsz.Proof { return p } -// PreconfirmationWithProof is a preconfirmed transaction in the block with -// proof of inclusion, using Merkle Trees. -type PreconfirmationWithProof struct { +// InclusionProof is a Merkle inclusion proof for a transaction hash. +type InclusionProof struct { // The transaction hash of the preconfirmation TxHash phase0.Hash32 `ssz-size:"32" json:"txHash"` // The Merkle proof of the preconfirmation diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 9f5b529f..0f82b2c8 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -330,7 +330,7 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. } // verifyConstraintProofs verifies the proofs against the constraints, and returns an error if the proofs are invalid. -func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithPreconfirmationsProofs, constraints Constraints) error { +func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionProofs, constraints Constraints) error { log := m.log.WithFields(logrus.Fields{}) // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. if responsePayload.Proofs != nil { @@ -478,7 +478,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) path := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, parentHashHex, pubkey) url := relay.GetURI(path) log := log.WithField("url", url) - responsePayload := new(BidWithPreconfirmationsProofs) + responsePayload := new(BidWithInclusionProofs) code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) if err != nil { log.WithError(err).Warn("error making request to relay") From 5ceaa985994d7dc9362f601a8147e428125cb525 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 11:37:05 +0200 Subject: [PATCH 03/17] refactor(server): remove sidecar connection --- mev-boost/server/boltSidecar.go | 56 --------------------------------- mev-boost/server/service.go | 10 ++---- 2 files changed, 2 insertions(+), 64 deletions(-) delete mode 100644 mev-boost/server/boltSidecar.go diff --git a/mev-boost/server/boltSidecar.go b/mev-boost/server/boltSidecar.go deleted file mode 100644 index 377e7eae..00000000 --- a/mev-boost/server/boltSidecar.go +++ /dev/null @@ -1,56 +0,0 @@ -package server - -import ( - "encoding/json" - "fmt" - - "github.com/ethereum/go-ethereum/log" - "github.com/flashbots/go-utils/jsonrpc" -) - -// boltSidecar is a thin http client that communicates with the -// bolt sidecar server to fetch preconfirmed transactions. -type boltSidecar struct { - endpoint string -} - -// newBoltSidecar creates a new boltSidecar instance. -func newBoltSidecar(endpoint string) *boltSidecar { - return &boltSidecar{ - endpoint: endpoint, - } -} - -type rawPreconfirmation struct { - Slot uint64 `json:"slot"` - TxHash string `json:"txHash"` - RawTx string `json:"rawTx"` -} - -func (b *boltSidecar) GetPreconfirmations(slot uint64) ([]*rawPreconfirmation, error) { - var preconfirms = new([]*rawPreconfirmation) - - params := map[string]interface{}{ - "slot": slot, - } - - // Request preconfirmations directly from the next proposer in line. - // In a real version, this would be done through a mempool / DA service. - req := jsonrpc.NewJSONRPCRequest("1", "eth_getPreconfirmations", params) - res, err := jsonrpc.SendJSONRPCRequest(*req, b.endpoint) - if err != nil { - log.Error("Error getting preconfs via RPC: ", err) - return nil, err - } - - // Unmarshal the JSON data - err = json.Unmarshal(res.Result, &preconfirms) - if err != nil { - log.Error("Error unmarshaling data: ", err) - return nil, err - } - - log.Info(fmt.Sprintf("Preconf Response Body: %s", string(res.Result))) - - return *preconfirms, nil -} diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 0f82b2c8..3e5e9acd 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -111,9 +111,7 @@ type BoostService struct { slotUID *slotUID slotUIDLock sync.Mutex - // BOLT: sidecar connection - sidecar *boltSidecar - + // BOLT: constraint cache constraints *ConstraintCache } @@ -128,9 +126,6 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { return nil, err } - // TODO: pass these from config options - boltSidecar := newBoltSidecar("http://mev-sidecar-api:9061") - return &BoostService{ listenAddr: opts.ListenAddr, relays: opts.Relays, @@ -157,8 +152,7 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { }, requestMaxRetries: opts.RequestMaxRetries, - sidecar: boltSidecar, - // Initialize the constraint cache + // BOLT: Initialize the constraint cache constraints: NewConstraintCache(), }, nil } From 67612fbad9da79d3f151175c0fad09dd63c47861 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 14:22:34 +0200 Subject: [PATCH 04/17] feat(server): add submitConstraints proxy functionality --- mev-boost/server/backend.go | 1 + mev-boost/server/constraints.go | 100 +++++++++++++++++++++++++++++++- mev-boost/server/service.go | 94 +++++++++++++++++++++++++----- 3 files changed, 181 insertions(+), 14 deletions(-) diff --git a/mev-boost/server/backend.go b/mev-boost/server/backend.go index 3309d541..086d382b 100644 --- a/mev-boost/server/backend.go +++ b/mev-boost/server/backend.go @@ -4,6 +4,7 @@ const ( // Router paths pathStatus = "/eth/v1/builder/status" pathRegisterValidator = "/eth/v1/builder/validators" + pathSubmitConstraint = "/eth/v1/builder/constraints" pathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" pathGetPayload = "/eth/v1/builder/blinded_blocks" diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go index fa16cc87..945614ca 100644 --- a/mev-boost/server/constraints.go +++ b/mev-boost/server/constraints.go @@ -1,6 +1,104 @@ package server -import "github.com/attestantio/go-eth2-client/spec/phase0" +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +type SignedConstraintSubmission struct { + Message *ConstraintSubmission + Signature phase0.BLSSignature `ssz-size:"96"` +} + +type signedConstraintSubmissionJSON struct { + Message *ConstraintSubmission `json:"message"` + Signature string `json:"signature"` +} + +func (s *SignedConstraintSubmission) MarshalJSON() ([]byte, error) { + return json.Marshal(&signedConstraintSubmissionJSON{ + Message: s.Message, + Signature: fmt.Sprintf("%#x", s.Signature), + }) +} + +func (s *SignedConstraintSubmission) UnmarshalJSON(input []byte) error { + var data signedConstraintSubmissionJSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if data.Message == nil { + return errors.New("message missing") + } + + s.Message = data.Message + + if data.Signature == "" { + return errors.New("signature missing") + } + + signature, err := hex.DecodeString(strings.TrimPrefix(data.Signature, "0x")) + if err != nil { + return errors.Wrap(err, "invalid signature") + } + + if len(signature) != phase0.SignatureLength { + return errors.New("incorrect length for signature") + } + copy(s.Signature[:], signature) + + return nil +} + +type ConstraintSubmission struct { + Slot uint64 + TxHash phase0.Hash32 `ssz-size:"32"` + RawTx Transaction `ssz-max:"1073741824"` +} + +type constraintSubmissionJSON struct { + Slot uint64 `json:"slot"` + TxHash string `json:"txHash"` + RawTx string `json:"rawTx"` +} + +func (c *ConstraintSubmission) MarshalJSON() ([]byte, error) { + return json.Marshal(&constraintSubmissionJSON{ + Slot: c.Slot, + TxHash: c.TxHash.String(), + RawTx: fmt.Sprintf("%#x", c.RawTx), + }) +} + +func (c *ConstraintSubmission) UnmarshalJSON(input []byte) error { + var data constraintSubmissionJSON + if err := json.Unmarshal(input, &data); err != nil { + return err + } + c.Slot = data.Slot + + txHash, err := hex.DecodeString((strings.TrimPrefix(data.TxHash, "0x"))) + if err != nil { + return errors.Wrap(err, "invalid tx hash") + } + + copy(c.TxHash[:], txHash) + + rawTx, err := hex.DecodeString((strings.TrimPrefix(data.RawTx, "0x"))) + if err != nil { + return errors.Wrap(err, "invalid raw tx") + } + + copy(c.RawTx[:], rawTx) + + return nil +} // Constraints is a map of constraints for a block. type Constraints = map[phase0.Hash32]*Constraint diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 3e5e9acd..4930020b 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -82,10 +82,11 @@ type BoostServiceOpts struct { RelayCheck bool RelayMinBid types.U256Str - RequestTimeoutGetHeader time.Duration - RequestTimeoutGetPayload time.Duration - RequestTimeoutRegVal time.Duration - RequestMaxRetries int + RequestTimeoutGetHeader time.Duration + RequestTimeoutGetPayload time.Duration + RequestTimeoutRegVal time.Duration + RequestTimeoutSubmitConstraint time.Duration + RequestMaxRetries int } // BoostService - the mev-boost service @@ -99,11 +100,12 @@ type BoostService struct { relayMinBid types.U256Str genesisTime uint64 - builderSigningDomain phase0.Domain - httpClientGetHeader http.Client - httpClientGetPayload http.Client - httpClientRegVal http.Client - requestMaxRetries int + builderSigningDomain phase0.Domain + httpClientGetHeader http.Client + httpClientGetPayload http.Client + httpClientRegVal http.Client + httpClientSubmitConstraint http.Client + requestMaxRetries int bids map[bidRespKey]bidResp // keeping track of bids, to log the originating relay on withholding bidsLock sync.Mutex @@ -150,6 +152,10 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { Timeout: opts.RequestTimeoutRegVal, CheckRedirect: httpClientDisallowRedirects, }, + httpClientSubmitConstraint: http.Client{ + Timeout: opts.RequestTimeoutSubmitConstraint, + CheckRedirect: httpClientDisallowRedirects, + }, requestMaxRetries: opts.RequestMaxRetries, // BOLT: Initialize the constraint cache @@ -182,6 +188,7 @@ func (m *BoostService) getRouter() http.Handler { r.HandleFunc(pathStatus, m.handleStatus).Methods(http.MethodGet) r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost) + r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost) r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet) r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost) @@ -380,15 +387,11 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP if err != nil { log.WithError(err).Error("error verifying merkle proof") - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly return err } if !ok { log.Error("[BOLT]: proof verification failed: 'not ok' for tx hash: ", proof.TxHash.String()) - // BOLT: we need to skip the bid in this case! - // for now we just return and complain loudly return errInvalidProofs } else { log.Info(fmt.Sprintf("[BOLT]: Preconfirmation proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed)) @@ -399,6 +402,56 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP 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.Debug("submitConstraint") + + payload := new(SignedConstraintSubmission) + if err := DecodeJSON(req.Body, payload); err != nil { + m.respondError(w, http.StatusBadRequest, err.Error()) + return + } + + constraint := payload.Message + + // Add the constraint to the cache. They will be cleared when we receive a payload for the slot + // in `handleGetPayload` + m.constraints.AddInclusionConstraint(constraint.Slot, constraint.TxHash, constraint.RawTx) + + relayRespCh := make(chan error, len(m.relays)) + + for _, relay := range m.relays { + go func(relay RelayEntry) { + url := relay.GetURI(pathSubmitConstraint) + log := log.WithField("url", url) + + _, err := SendHTTPRequest(context.Background(), m.httpClientSubmitConstraint, http.MethodPost, url, ua, nil, payload, nil) + relayRespCh <- err + if err != nil { + log.WithError(err).Warn("error calling submitConstraint on relay") + return + } + }(relay) + } + + for i := 0; i < len(m.relays); i++ { + respErr := <-relayRespCh + if respErr == nil { + m.respondOK(w, nilResponse) + return + } + } + + m.respondError(w, http.StatusBadGateway, errNoSuccessfulRelayResponse.Error()) +} + // handleGetHeader requests bids from the relays // BOLT: receiving preconfirmation proofs from relays along with bids, and // verify them. If not valid, the bid is discarded @@ -569,6 +622,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err) return } + } mu.Lock() @@ -905,6 +959,8 @@ func (m *BoostService) processDenebPayload(w http.ResponseWriter, req *http.Requ m.respondOK(w, result) } +// handleGetPayload submits a signed blinded header to receive the payload body from the relays. +// BOLT: when receiving the payload, we also remove the associated constraints for this slot. func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request) { log := m.log.WithField("method", "getPayload") log.Debug("getPayload request starts") @@ -917,6 +973,14 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request return } + slot := uint64(0) + + // BOLT: Make sure we remove the constraints for this slot after we've received the payload. + defer func() { + // This will use the value of `slot` at execution time + m.constraints.Delete(slot) + }() + // Decode the body now payload := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock) if err := DecodeJSON(bytes.NewReader(body), payload); err != nil { @@ -927,9 +991,13 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request m.respondError(w, http.StatusBadRequest, err.Error()) return } + + slot = uint64(payload.Message.Slot) m.processCapellaPayload(w, req, log, payload, body) return } + + slot = uint64(payload.Message.Slot) m.processDenebPayload(w, req, log, payload) } From 79bbddf30ad9d81fe505076b63301eb3bde9cdb5 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 14:51:47 +0200 Subject: [PATCH 05/17] test(server): add simple submitConstraint endpoint test --- mev-boost/server/mock_relay.go | 25 ++++++++++++++++++ mev-boost/server/service.go | 18 ++++++++----- mev-boost/server/service_test.go | 44 ++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 17 deletions(-) diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index fe6c6daa..2731ea9a 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -55,6 +55,7 @@ type mockRelay struct { // Overriders handlerOverrideRegisterValidator func(w http.ResponseWriter, req *http.Request) + handlerOverrideSubmitConstraint func(w http.ResponseWriter, req *http.Request) handlerOverrideGetHeader func(w http.ResponseWriter, req *http.Request) handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request) @@ -115,6 +116,7 @@ func (m *mockRelay) getRouter() http.Handler { r.HandleFunc(pathStatus, m.handleStatus).Methods(http.MethodGet) r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost) r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet) + r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost) r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost) return m.newTestMiddleware(r) @@ -164,6 +166,29 @@ func (m *mockRelay) defaultHandleRegisterValidator(w http.ResponseWriter, req *h w.WriteHeader(http.StatusOK) } +func (m *mockRelay) handleSubmitConstraint(w http.ResponseWriter, req *http.Request) { + fmt.Println("handleSubmitConstraint") + 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) { + fmt.Println("defaultHandleSubmitConstraint") + payload := []SignedConstraintSubmission{} + 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) +} + // 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 { diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 4930020b..c1004f60 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -411,19 +411,23 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R "ua": ua, }) - log.Debug("submitConstraint") + log.Info("submitConstraint") - payload := new(SignedConstraintSubmission) - if err := DecodeJSON(req.Body, payload); err != nil { + payload := []SignedConstraintSubmission{} + if err := DecodeJSON(req.Body, &payload); err != nil { + log.Error("error decoding payload: ", err) m.respondError(w, http.StatusBadRequest, err.Error()) return } - constraint := payload.Message + // Add all constraints to the cache + for _, signedConstraint := range payload { + constraint := signedConstraint.Message - // Add the constraint to the cache. They will be cleared when we receive a payload for the slot - // in `handleGetPayload` - m.constraints.AddInclusionConstraint(constraint.Slot, constraint.TxHash, constraint.RawTx) + // Add the constraint to the cache. They will be cleared when we receive a payload for the slot + // in `handleGetPayload` + m.constraints.AddInclusionConstraint(constraint.Slot, constraint.TxHash, constraint.RawTx) + } relayRespCh := make(chan error, len(m.relays)) diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 72bcfade..9f21b10b 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -53,16 +53,17 @@ func newTestBackend(t *testing.T, numRelays int, relayTimeout time.Duration) *te } opts := BoostServiceOpts{ - Log: testLog, - ListenAddr: "localhost:12345", - Relays: relayEntries, - GenesisForkVersionHex: "0x00000000", - RelayCheck: true, - RelayMinBid: types.IntToU256(12345), - RequestTimeoutGetHeader: relayTimeout, - RequestTimeoutGetPayload: relayTimeout, - RequestTimeoutRegVal: relayTimeout, - RequestMaxRetries: 5, + Log: testLog, + ListenAddr: "localhost:12345", + Relays: relayEntries, + GenesisForkVersionHex: "0x00000000", + RelayCheck: true, + RelayMinBid: types.IntToU256(12345), + RequestTimeoutGetHeader: relayTimeout, + RequestTimeoutGetPayload: relayTimeout, + RequestTimeoutRegVal: relayTimeout, + RequestTimeoutSubmitConstraint: relayTimeout, + RequestMaxRetries: 5, } service, err := NewBoostService(opts) require.NoError(t, err) @@ -308,6 +309,29 @@ func TestRegisterValidator(t *testing.T) { }) } +func TestSubmitConstraint(t *testing.T) { + path := pathSubmitConstraint + + constraint := SignedConstraintSubmission{ + Message: &ConstraintSubmission{ + Slot: 12345, + TxHash: _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49"), + RawTx: _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f"), + }, + Signature: _HexToSignature( + "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"), + } + + payload := []SignedConstraintSubmission{constraint} + + 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 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()) } From bd32b4492bed9098462bec59c7feef592cf8545e Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Wed, 1 May 2024 14:59:32 +0200 Subject: [PATCH 06/17] test(server): add submitConstraint cache check --- mev-boost/server/service_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 9f21b10b..93699c52 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -311,10 +311,11 @@ func TestRegisterValidator(t *testing.T) { func TestSubmitConstraint(t *testing.T) { path := pathSubmitConstraint + slot := uint64(12345) constraint := SignedConstraintSubmission{ Message: &ConstraintSubmission{ - Slot: 12345, + Slot: slot, TxHash: _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49"), RawTx: _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f"), }, @@ -329,6 +330,7 @@ func TestSubmitConstraint(t *testing.T) { rr := backend.request(t, http.MethodPost, path, payload) require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) + require.Equal(t, len(backend.boost.constraints.Get(slot)), 1) }) } From 124669d791acb4995ba1e4c7257c5761eda111bb Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Thu, 2 May 2024 09:59:18 +0200 Subject: [PATCH 07/17] fix(server): nil check payload --- mev-boost/server/service.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index c1004f60..48dfdf74 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -543,6 +543,11 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) return } + if responsePayload.Bid == nil { + log.Warn("Bid in response is nil") + return + } + // Skip if payload is empty if responsePayload.Bid.IsEmpty() { return From 9bdad5d443b992a7ecad61082a35c7a89cddb183 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Thu, 2 May 2024 10:07:12 +0200 Subject: [PATCH 08/17] fix(test): fix tests with mock relay --- mev-boost/server/mock_relay.go | 30 ++++++++++++++++++------------ mev-boost/server/service_test.go | 4 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index 2731ea9a..746164e9 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -60,7 +60,7 @@ type mockRelay struct { handlerOverrideGetPayload func(w http.ResponseWriter, req *http.Request) // Default responses placeholders, used if overrider does not exist - GetHeaderResponse *builderSpec.VersionedSignedBuilderBid + GetHeaderResponse *BidWithInclusionProofs GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse // Server section @@ -191,7 +191,7 @@ func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *ht // 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 { +func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion) *BidWithInclusionProofs { switch version { case spec.DataVersionCapella: // Fill the payload with custom values. @@ -209,14 +209,17 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &builderSpec.VersionedSignedBuilderBid{ - Version: spec.DataVersionCapella, - Capella: &builderApiCapella.SignedBuilderBid{ - Message: message, - Signature: signature, + return &BidWithInclusionProofs{ + Bid: &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionCapella, + Capella: &builderApiCapella.SignedBuilderBid{ + Message: message, + Signature: signature, + }, }, } case spec.DataVersionDeneb: + message := &builderApiDeneb.BuilderBid{ Header: &deneb.ExecutionPayloadHeader{ BlockHash: _HexToHash(blockHash), @@ -233,11 +236,13 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &builderSpec.VersionedSignedBuilderBid{ - Version: spec.DataVersionDeneb, - Deneb: &builderApiDeneb.SignedBuilderBid{ - Message: message, - Signature: signature, + return &BidWithInclusionProofs{ + Bid: &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.SignedBuilderBid{ + Message: message, + Signature: signature, + }, }, } case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: @@ -272,6 +277,7 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", spec.DataVersionCapella, ) + if m.GetHeaderResponse != nil { response = m.GetHeaderResponse } diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 93699c52..1b5489d2 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -376,7 +376,7 @@ func TestGetHeader(t *testing.T) { "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", spec.DataVersionCapella, ) - resp.Capella.Message.Header.BlockHash = nilHash + resp.Bid.Capella.Message.Header.BlockHash = nilHash // 1/2 failing responses are okay backend.relays[0].GetHeaderResponse = resp @@ -427,7 +427,7 @@ func TestGetHeader(t *testing.T) { ) // Scramble the signature - backend.relays[0].GetHeaderResponse.Capella.Signature = phase0.BLSSignature{} + backend.relays[0].GetHeaderResponse.Bid.Capella.Signature = phase0.BLSSignature{} rr := backend.request(t, http.MethodGet, path, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) From 713d86e3fc997473bab39bd4d553408d333adbec Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Thu, 2 May 2024 12:45:42 +0200 Subject: [PATCH 09/17] test(server): add proof verification integration test --- mev-boost/server/constraints.go | 3 +- mev-boost/server/mock_relay.go | 112 +++++++++++++++++++++++++++++++ mev-boost/server/service.go | 15 ++++- mev-boost/server/service_test.go | 68 +++++++++++++++++-- 4 files changed, 190 insertions(+), 8 deletions(-) diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go index 945614ca..18a84b11 100644 --- a/mev-boost/server/constraints.go +++ b/mev-boost/server/constraints.go @@ -81,6 +81,7 @@ func (c *ConstraintSubmission) UnmarshalJSON(input []byte) error { if err := json.Unmarshal(input, &data); err != nil { return err } + c.Slot = data.Slot txHash, err := hex.DecodeString((strings.TrimPrefix(data.TxHash, "0x"))) @@ -95,7 +96,7 @@ func (c *ConstraintSubmission) UnmarshalJSON(input []byte) error { return errors.Wrap(err, "invalid raw tx") } - copy(c.RawTx[:], rawTx) + c.RawTx = rawTx return nil } diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index 746164e9..ad79a2d2 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -3,6 +3,7 @@ package server import ( "encoding/json" "fmt" + "math" "net/http" "net/http/httptest" "net/url" @@ -16,9 +17,11 @@ import ( builderApiV1 "github.com/attestantio/go-builder-client/api/v1" builderSpec "github.com/attestantio/go-builder-client/spec" "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/bellatrix" "github.com/attestantio/go-eth2-client/spec/capella" "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" + utilbellatrix "github.com/attestantio/go-eth2-client/util/bellatrix" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/flashbots/go-boost-utils/bls" "github.com/flashbots/go-boost-utils/ssz" @@ -189,6 +192,51 @@ func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *ht w.WriteHeader(http.StatusOK) } +// TODO: +func (m *mockRelay) MakeGetHeaderResponseWithConstraints(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, constraints []struct { + tx Transaction + hash phase0.Hash32 +}) *BidWithInclusionProofs { + + transactions := new(utilbellatrix.ExecutionPayloadTransactions) + + for _, con := range constraints { + transactions.Transactions = append(transactions.Transactions, bellatrix.Transaction(con.tx)) + } + + rootNode, err := transactions.GetTree() + if err != nil { + panic(err) + } + + // BOLT: Set the value of nodes. This is MANDATORY for the proof calculation + // to output the leaf correctly. This is also never documented in fastssz. -__- + // Also calculates the transactions_root + txsRoot := rootNode.Hash() + + bidWithProofs := m.MakeGetHeaderResponseWithTxsRoot(value, blockHash, parentHash, publicKey, version, phase0.Root(txsRoot)) + bidWithProofs.Proofs = make([]*InclusionProof, len(constraints)) + + for i, con := range constraints { + generalizedIndex := int(math.Pow(float64(2), float64(21))) + i + + proof, err := rootNode.Prove(generalizedIndex) + if err != nil { + panic(err) + } + + merkleProof := new(SerializedMerkleProof) + merkleProof.FromFastSszProof(proof) + + bidWithProofs.Proofs[i] = &InclusionProof{ + TxHash: con.hash, + MerkleProof: merkleProof, + } + } + + return bidWithProofs +} + // MakeGetHeaderResponse is used to create the default or can be used to create a custom response to the getHeader // method func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion) *BidWithInclusionProofs { @@ -251,6 +299,70 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p return nil } +// MakeGetHeaderResponse is used to create the default or can be used to create a custom response to the getHeader +// method +func (m *mockRelay) MakeGetHeaderResponseWithTxsRoot(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, txsRoot phase0.Root) *BidWithInclusionProofs { + switch version { + case spec.DataVersionCapella: + // Fill the payload with custom values. + message := &builderApiCapella.BuilderBid{ + Header: &capella.ExecutionPayloadHeader{ + BlockHash: _HexToHash(blockHash), + ParentHash: _HexToHash(parentHash), + WithdrawalsRoot: phase0.Root{}, + TransactionsRoot: txsRoot, + }, + Value: uint256.NewInt(value), + Pubkey: _HexToPubkey(publicKey), + } + + // Sign the message. + signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) + require.NoError(m.t, err) + + return &BidWithInclusionProofs{ + Bid: &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionCapella, + Capella: &builderApiCapella.SignedBuilderBid{ + Message: message, + Signature: signature, + }, + }, + } + case spec.DataVersionDeneb: + + message := &builderApiDeneb.BuilderBid{ + Header: &deneb.ExecutionPayloadHeader{ + BlockHash: _HexToHash(blockHash), + ParentHash: _HexToHash(parentHash), + WithdrawalsRoot: phase0.Root{}, + BaseFeePerGas: uint256.NewInt(0), + TransactionsRoot: txsRoot, + }, + BlobKZGCommitments: make([]deneb.KZGCommitment, 0), + Value: uint256.NewInt(value), + Pubkey: _HexToPubkey(publicKey), + } + + // Sign the message. + signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) + require.NoError(m.t, err) + + return &BidWithInclusionProofs{ + Bid: &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.SignedBuilderBid{ + Message: message, + Signature: signature, + }, + }, + } + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: + return nil + } + return nil +} + // handleGetHeader handles incoming requests to server.pathGetHeader func (m *mockRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) { m.mu.Lock() diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 48dfdf74..a3c3431b 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -338,7 +338,7 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP // BOLT: remove unnecessary fields while logging log.WithFields(logrus.Fields{}) - log.Info("[BOLT]: Verifying preconfirmation proofs", len(responsePayload.Proofs)) + log.WithField("len", len(responsePayload.Proofs)).Info("[BOLT]: Verifying constraint proofs") transactionsRoot, err := responsePayload.Bid.TransactionsRoot() if err != nil { @@ -364,6 +364,11 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP log.Infof("[BOLT]: Raw tx: %x", rawTx) + if len(rawTx) == 0 { + log.Warnf("[BOLT]: Raw tx is empty for tx hash %s", proof.TxHash.String()) + continue + } + // Compute the hash tree root for the raw preconfirmed transaction // and use it as "Leaf" in the proof to be verified against txHashTreeRoot, err := rawTx.HashTreeRoot() @@ -424,6 +429,12 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R for _, signedConstraint := range payload { constraint := signedConstraint.Message + log.WithFields(logrus.Fields{ + "slot": constraint.Slot, + "txHash": constraint.TxHash.String(), + "rawTx": fmt.Sprintf("%#x", constraint.RawTx), + }).Info("[BOLT]: adding inclusion constraint to cache") + // Add the constraint to the cache. They will be cleared when we receive a payload for the slot // in `handleGetPayload` m.constraints.AddInclusionConstraint(constraint.Slot, constraint.TxHash, constraint.RawTx) @@ -619,7 +630,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) // BOLT: verify matching proofs & constraints if len(responsePayload.Proofs) != len(inclusionConstraints) { - log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != preconfs %d", + log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != constraints %d", len(responsePayload.Proofs), len(inclusionConstraints)) return } diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 1b5489d2..ad412be8 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -82,6 +82,7 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) * } else { payloadBytes, err2 := json.Marshal(payload) require.NoError(t, err2) + fmt.Println("payload:", string(payloadBytes)) req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes)) } @@ -309,15 +310,19 @@ func TestRegisterValidator(t *testing.T) { }) } -func TestSubmitConstraint(t *testing.T) { +func TestConstraintsAndProofs(t *testing.T) { path := pathSubmitConstraint - slot := uint64(12345) + slot := uint64(8978583) + txHash := _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49") + rawTx := _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f") + + // Build the constraint constraint := SignedConstraintSubmission{ Message: &ConstraintSubmission{ Slot: slot, - TxHash: _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49"), - RawTx: _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f"), + TxHash: txHash, + RawTx: rawTx, }, Signature: _HexToSignature( "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"), @@ -325,12 +330,65 @@ func TestSubmitConstraint(t *testing.T) { payload := []SignedConstraintSubmission{constraint} + // Build getHeader request + hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") + pubkey := _HexToPubkey( + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249") + getHeaderPath := getHeaderPath(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)) - require.Equal(t, len(backend.boost.constraints.Get(slot)), 1) + require.Equal(t, 1, len(backend.boost.constraints.Get(slot))) + require.Equal(t, Transaction(rawTx), backend.boost.constraints.Get(slot)[txHash].RawTx) + }) + + 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].MakeGetHeaderResponseWithConstraints( + slot, + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", + spec.DataVersionDeneb, + []struct { + tx Transaction + hash phase0.Hash32 + }{{rawTx, txHash}}, + ) + backend.relays[0].GetHeaderResponse = resp + + rr := backend.request(t, http.MethodGet, getHeaderPath, nil) + require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderPath)) + }) + + t.Run("No proofs given", func(t *testing.T) { + backend := newTestBackend(t, 1, time.Second) + + // Submit constraint + backend.request(t, http.MethodPost, path, payload) + + resp := backend.relays[0].MakeGetHeaderResponse( + slot, + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", + spec.DataVersionDeneb, + ) + backend.relays[0].GetHeaderResponse = resp + + rr := backend.request(t, http.MethodGet, getHeaderPath, nil) + // When we have constraints registered, but the relay does not return any proofs, we should return no content. + // This will force a locally built block. + require.Equal(t, http.StatusNoContent, rr.Code, rr.Body.String()) + require.Equal(t, 1, backend.relays[0].GetRequestCount(getHeaderPath)) }) } From 185d7ae5a3c49ecebdd45f9d6b3ed6ad3beb6ef0 Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Tue, 14 May 2024 14:13:43 +0200 Subject: [PATCH 10/17] feat(server): separate getHeaderWithProofs endpoint, fix tests --- mev-boost/server/backend.go | 11 +- mev-boost/server/mock_relay.go | 88 +++++++++---- mev-boost/server/service.go | 213 ++++++++++++++++++++++++++++++- mev-boost/server/service_test.go | 14 +- 4 files changed, 286 insertions(+), 40 deletions(-) diff --git a/mev-boost/server/backend.go b/mev-boost/server/backend.go index 086d382b..631a587a 100644 --- a/mev-boost/server/backend.go +++ b/mev-boost/server/backend.go @@ -2,11 +2,12 @@ package server const ( // Router paths - pathStatus = "/eth/v1/builder/status" - pathRegisterValidator = "/eth/v1/builder/validators" - pathSubmitConstraint = "/eth/v1/builder/constraints" - pathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" - pathGetPayload = "/eth/v1/builder/blinded_blocks" + pathStatus = "/eth/v1/builder/status" + pathRegisterValidator = "/eth/v1/builder/validators" + pathSubmitConstraint = "/eth/v1/builder/constraints" + pathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" + pathGetHeaderWithProofs = "/eth/v1/builder/header_with_proofs/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" + pathGetPayload = "/eth/v1/builder/blinded_blocks" // // Relay Monitor paths // pathAuctionTranscript = "/monitor/v1/transcript" diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index ad79a2d2..77f23e4e 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -57,14 +57,16 @@ type mockRelay struct { requestCount map[string]int // Overriders - handlerOverrideRegisterValidator func(w http.ResponseWriter, req *http.Request) - handlerOverrideSubmitConstraint 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 *BidWithInclusionProofs - GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse + GetHeaderResponse *builderSpec.VersionedSignedBuilderBid + GetHeaderWithProofsResponse *BidWithInclusionProofs + GetPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse // Server section Server *httptest.Server @@ -119,6 +121,7 @@ func (m *mockRelay) getRouter() http.Handler { r.HandleFunc(pathStatus, m.handleStatus).Methods(http.MethodGet) r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost) r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet) + r.HandleFunc(pathGetHeaderWithProofs, m.handleGetHeaderWithProofs).Methods(http.MethodGet) r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost) r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost) @@ -170,7 +173,6 @@ func (m *mockRelay) defaultHandleRegisterValidator(w http.ResponseWriter, req *h } func (m *mockRelay) handleSubmitConstraint(w http.ResponseWriter, req *http.Request) { - fmt.Println("handleSubmitConstraint") m.mu.Lock() defer m.mu.Unlock() if m.handlerOverrideSubmitConstraint != nil { @@ -181,7 +183,6 @@ func (m *mockRelay) handleSubmitConstraint(w http.ResponseWriter, req *http.Requ } func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *http.Request) { - fmt.Println("defaultHandleSubmitConstraint") payload := []SignedConstraintSubmission{} if err := DecodeJSON(req.Body, &payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -192,8 +193,7 @@ func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *ht w.WriteHeader(http.StatusOK) } -// TODO: -func (m *mockRelay) MakeGetHeaderResponseWithConstraints(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, constraints []struct { +func (m *mockRelay) MakeGetHeaderWithConstraintsResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, constraints []struct { tx Transaction hash phase0.Hash32 }) *BidWithInclusionProofs { @@ -214,7 +214,7 @@ func (m *mockRelay) MakeGetHeaderResponseWithConstraints(value uint64, blockHash // Also calculates the transactions_root txsRoot := rootNode.Hash() - bidWithProofs := m.MakeGetHeaderResponseWithTxsRoot(value, blockHash, parentHash, publicKey, version, phase0.Root(txsRoot)) + bidWithProofs := m.MakeGetHeaderWithProofsResponseWithTxsRoot(value, blockHash, parentHash, publicKey, version, phase0.Root(txsRoot)) bidWithProofs.Proofs = make([]*InclusionProof, len(constraints)) for i, con := range constraints { @@ -239,7 +239,7 @@ func (m *mockRelay) MakeGetHeaderResponseWithConstraints(value uint64, blockHash // 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) *BidWithInclusionProofs { +func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion) *builderSpec.VersionedSignedBuilderBid { switch version { case spec.DataVersionCapella: // Fill the payload with custom values. @@ -257,13 +257,11 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &BidWithInclusionProofs{ - Bid: &builderSpec.VersionedSignedBuilderBid{ - Version: spec.DataVersionCapella, - Capella: &builderApiCapella.SignedBuilderBid{ - Message: message, - Signature: signature, - }, + return &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionCapella, + Capella: &builderApiCapella.SignedBuilderBid{ + Message: message, + Signature: signature, }, } case spec.DataVersionDeneb: @@ -284,13 +282,11 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey) require.NoError(m.t, err) - return &BidWithInclusionProofs{ - Bid: &builderSpec.VersionedSignedBuilderBid{ - Version: spec.DataVersionDeneb, - Deneb: &builderApiDeneb.SignedBuilderBid{ - Message: message, - Signature: signature, - }, + return &builderSpec.VersionedSignedBuilderBid{ + Version: spec.DataVersionDeneb, + Deneb: &builderApiDeneb.SignedBuilderBid{ + Message: message, + Signature: signature, }, } case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: @@ -301,7 +297,7 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p // MakeGetHeaderResponse is used to create the default or can be used to create a custom response to the getHeader // method -func (m *mockRelay) MakeGetHeaderResponseWithTxsRoot(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, txsRoot phase0.Root) *BidWithInclusionProofs { +func (m *mockRelay) MakeGetHeaderWithProofsResponseWithTxsRoot(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, txsRoot phase0.Root) *BidWithInclusionProofs { switch version { case spec.DataVersionCapella: // Fill the payload with custom values. @@ -400,6 +396,44 @@ func (m *mockRelay) defaultHandleGetHeader(w http.ResponseWriter) { } } +// handleGetHeaderWithProofs handles incoming requests to server.pathGetHeader +func (m *mockRelay) handleGetHeaderWithProofs(w http.ResponseWriter, req *http.Request) { + m.mu.Lock() + defer m.mu.Unlock() + // Try to override default behavior is custom handler is specified. + if m.handlerOverrideGetHeader != nil { + m.handlerOverrideGetHeaderWithProofs(w, req) + return + } + m.defaultHandleGetHeaderWithProofs(w) +} + +// defaultHandleGetHeaderWithProofs returns the default handler for handleGetHeaderWithProofs +func (m *mockRelay) defaultHandleGetHeaderWithProofs(w http.ResponseWriter) { + // By default, everything will be ok. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Build the default response. + response := m.MakeGetHeaderWithConstraintsResponse( + 12345, + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", + "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", + spec.DataVersionCapella, + nil, + ) + + if m.GetHeaderWithProofsResponse != nil { + response = m.GetHeaderWithProofsResponse + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + // MakeGetPayloadResponse is used to create the default or can be used to create a custom response to the getPayload // method func (m *mockRelay) MakeGetPayloadResponse(parentHash, blockHash, feeRecipient string, blockNumber uint64, version spec.DataVersion) *builderApi.VersionedSubmitBlindedBlockResponse { diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index a3c3431b..39122a1c 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -190,6 +190,7 @@ func (m *BoostService) getRouter() http.Handler { r.HandleFunc(pathRegisterValidator, m.handleRegisterValidator).Methods(http.MethodPost) r.HandleFunc(pathSubmitConstraint, m.handleSubmitConstraint).Methods(http.MethodPost) r.HandleFunc(pathGetHeader, m.handleGetHeader).Methods(http.MethodGet) + r.HandleFunc(pathGetHeaderWithProofs, m.handleGetHeaderWithProofs).Methods(http.MethodGet) r.HandleFunc(pathGetPayload, m.handleGetPayload).Methods(http.MethodPost) r.Use(mux.CORSMethodMiddleware(r)) @@ -468,8 +469,6 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R } // handleGetHeader requests bids from the relays -// BOLT: receiving preconfirmation proofs from relays along with bids, and -// verify them. If not valid, the bid is discarded func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) slot := vars["slot"] @@ -486,6 +485,214 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) }) log.Debug("getHeader") + _slot, 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 < _slot { + m.slotUID.slot = _slot + 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 + _slot*config.SlotTimeSec + msIntoSlot := uint64(time.Now().UTC().UnixMilli()) - slotStartTimestamp*1000 + log.WithFields(logrus.Fields{ + "genesisTime": m.genesisTime, + "slotTimeSec": config.SlotTimeSec, + "msIntoSlot": msIntoSlot, + }).Infof("getHeader request start - %d milliseconds into slot %d", msIntoSlot, _slot) + + // 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/%s/%s/%s", slot, parentHashHex, pubkey) + url := relay.GetURI(path) + log := log.WithField("url", url) + responsePayload := new(builderSpec.VersionedSignedBuilderBid) + code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload) + if err != nil { + log.WithError(err).Warn("error making request to relay") + return + } + + if code == http.StatusNoContent { + log.Debug("no-content response") + return + } + + // Skip if payload is empty + if responsePayload.IsEmpty() { + return + } + + // Getting the bid info will check if there are missing fields in the response + bidInfo, err := parseBidInfo(responsePayload) + 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, m.builderSigningDomain, relay.PublicKey) + if err != nil { + log.WithError(err).Error("error verifying relay signature") + return + } + if !ok { + log.Error("failed to verify relay signature") + return + } + } + + // Verify response coherence with proposer's input data + if bidInfo.parentHash.String() != parentHashHex { + log.WithFields(logrus.Fields{ + "originalParentHash": parentHashHex, + "responseParentHash": bidInfo.parentHash.String(), + }).Error("proposer and relay parent hashes are not the same") + return + } + + isZeroValue := bidInfo.value.IsZero() + isEmptyListTxRoot := bidInfo.txRoot.String() == "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1" + if isZeroValue || isEmptyListTxRoot { + log.Warn("ignoring bid with 0 value") + return + } + log.Debug("bid received") + + // Skip if value (fee) is lower than the minimum bid + if bidInfo.value.CmpBig(m.relayMinBid.BigInt()) == -1 { + log.Debug("ignoring bid below min-bid value") + return + } + + 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.Debug("new best bid") + result.response = *responsePayload + 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), ", "), + }).Info("best bid") + + // Remember the bid, for future logging in case of withholding + bidKey := bidRespKey{slot: _slot, blockHash: result.bidInfo.blockHash.String()} + m.bidsLock.Lock() + m.bids[bidKey] = result + m.bidsLock.Unlock() + + // 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()) @@ -537,7 +744,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request) wg.Add(1) go func(relay RelayEntry) { defer wg.Done() - path := fmt.Sprintf("/eth/v1/builder/header/%s/%s/%s", slot, parentHashHex, pubkey) + path := fmt.Sprintf("/eth/v1/builder/header_with_proofs/%s/%s/%s", slot, parentHashHex, pubkey) url := relay.GetURI(path) log := log.WithField("url", url) responsePayload := new(BidWithInclusionProofs) diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index ad412be8..6d6fef4e 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -334,7 +334,7 @@ func TestConstraintsAndProofs(t *testing.T) { hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") pubkey := _HexToPubkey( "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249") - getHeaderPath := getHeaderPath(slot, hash, pubkey) + getHeaderPath := getHeaderWithProofsPath(slot, hash, pubkey) t.Run("Normal function", func(t *testing.T) { backend := newTestBackend(t, 1, time.Second) @@ -351,7 +351,7 @@ func TestConstraintsAndProofs(t *testing.T) { // Submit constraint backend.request(t, http.MethodPost, path, payload) - resp := backend.relays[0].MakeGetHeaderResponseWithConstraints( + resp := backend.relays[0].MakeGetHeaderWithConstraintsResponse( slot, "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", "0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7", @@ -362,7 +362,7 @@ func TestConstraintsAndProofs(t *testing.T) { hash phase0.Hash32 }{{rawTx, txHash}}, ) - backend.relays[0].GetHeaderResponse = resp + backend.relays[0].GetHeaderWithProofsResponse = resp rr := backend.request(t, http.MethodGet, getHeaderPath, nil) require.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) @@ -396,6 +396,10 @@ func getHeaderPath(slot uint64, parentHash phase0.Hash32, pubkey phase0.BLSPubKe return fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHash.String(), pubkey.String()) } +func getHeaderWithProofsPath(slot uint64, parentHash phase0.Hash32, pubkey phase0.BLSPubKey) string { + return fmt.Sprintf("/eth/v1/builder/header_with_proofs/%d/%s/%s", slot, parentHash.String(), pubkey.String()) +} + func TestGetHeader(t *testing.T) { hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") pubkey := _HexToPubkey( @@ -434,7 +438,7 @@ func TestGetHeader(t *testing.T) { "0x8a1d7b8dd64e0aafe7ea7b6c95065c9364cf99d38470c12ee807d55f7de1529ad29ce2c422e0b65e3d5a05c02caca249", spec.DataVersionCapella, ) - resp.Bid.Capella.Message.Header.BlockHash = nilHash + resp.Capella.Message.Header.BlockHash = nilHash // 1/2 failing responses are okay backend.relays[0].GetHeaderResponse = resp @@ -485,7 +489,7 @@ func TestGetHeader(t *testing.T) { ) // Scramble the signature - backend.relays[0].GetHeaderResponse.Bid.Capella.Signature = phase0.BLSSignature{} + backend.relays[0].GetHeaderResponse.Capella.Signature = phase0.BLSSignature{} rr := backend.request(t, http.MethodGet, path, nil) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) From 46a30040eff91869fa1cb784a6cd7708f765f96d Mon Sep 17 00:00:00 2001 From: Jonas Bostoen Date: Tue, 14 May 2024 14:15:15 +0200 Subject: [PATCH 11/17] doc(server): fix doc comment --- mev-boost/server/mock_relay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index 77f23e4e..852e92dd 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -295,7 +295,7 @@ func (m *mockRelay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, p return nil } -// MakeGetHeaderResponse is used to create the default or can be used to create a custom response to the getHeader +// MakeGetHeaderWithProofsResponseWithTxsRoot is used to create the default or can be used to create a custom response to the getHeaderWithProofs // method func (m *mockRelay) MakeGetHeaderWithProofsResponseWithTxsRoot(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion, txsRoot phase0.Root) *BidWithInclusionProofs { switch version { From 7b4210a4ddf410a115487d981a7ad79933d4dea9 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:45:59 +0200 Subject: [PATCH 12/17] feat: updated constraints API --- mev-boost/server/constraints.go | 156 ++++++++-------------------- mev-boost/server/mock_relay.go | 2 +- mev-boost/server/proofs.go | 4 + mev-boost/server/service.go | 60 +++++------ mev-boost/server/service_test.go | 24 ++--- mev-boost/server/transaction_ssz.go | 12 +++ 6 files changed, 105 insertions(+), 153 deletions(-) diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go index 18a84b11..88b44d0b 100644 --- a/mev-boost/server/constraints.go +++ b/mev-boost/server/constraints.go @@ -1,145 +1,81 @@ package server -import ( - "encoding/hex" - "encoding/json" - "fmt" - "strings" +type BatchedSignedConstraints = []*SignedConstraints - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/pkg/errors" -) - -type SignedConstraintSubmission struct { - Message *ConstraintSubmission - Signature phase0.BLSSignature `ssz-size:"96"` -} - -type signedConstraintSubmissionJSON struct { - Message *ConstraintSubmission `json:"message"` - Signature string `json:"signature"` -} - -func (s *SignedConstraintSubmission) MarshalJSON() ([]byte, error) { - return json.Marshal(&signedConstraintSubmissionJSON{ - Message: s.Message, - Signature: fmt.Sprintf("%#x", s.Signature), - }) -} - -func (s *SignedConstraintSubmission) UnmarshalJSON(input []byte) error { - var data signedConstraintSubmissionJSON - if err := json.Unmarshal(input, &data); err != nil { - return errors.Wrap(err, "invalid JSON") - } - - if data.Message == nil { - return errors.New("message missing") - } - - s.Message = data.Message - - if data.Signature == "" { - return errors.New("signature missing") - } - - signature, err := hex.DecodeString(strings.TrimPrefix(data.Signature, "0x")) - if err != nil { - return errors.Wrap(err, "invalid signature") - } - - if len(signature) != phase0.SignatureLength { - return errors.New("incorrect length for signature") - } - copy(s.Signature[:], signature) - - return nil -} - -type ConstraintSubmission struct { - Slot uint64 - TxHash phase0.Hash32 `ssz-size:"32"` - RawTx Transaction `ssz-max:"1073741824"` -} - -type constraintSubmissionJSON struct { - Slot uint64 `json:"slot"` - TxHash string `json:"txHash"` - RawTx string `json:"rawTx"` -} - -func (c *ConstraintSubmission) MarshalJSON() ([]byte, error) { - return json.Marshal(&constraintSubmissionJSON{ - Slot: c.Slot, - TxHash: c.TxHash.String(), - RawTx: fmt.Sprintf("%#x", c.RawTx), - }) +type SignedConstraints struct { + Message ConstraintsMessage `json:"message"` + Signature HexBytes `json:"signature"` } -func (c *ConstraintSubmission) UnmarshalJSON(input []byte) error { - var data constraintSubmissionJSON - if err := json.Unmarshal(input, &data); err != nil { - return err - } - - c.Slot = data.Slot - - txHash, err := hex.DecodeString((strings.TrimPrefix(data.TxHash, "0x"))) - if err != nil { - return errors.Wrap(err, "invalid tx hash") - } - - copy(c.TxHash[:], txHash) - - rawTx, err := hex.DecodeString((strings.TrimPrefix(data.RawTx, "0x"))) - if err != nil { - return errors.Wrap(err, "invalid raw tx") - } - - c.RawTx = rawTx - - return nil +type ConstraintsMessage struct { + ValidatorIndex uint64 + Slot uint64 + Constraints []*Constraint } -// Constraints is a map of constraints for a block. -type Constraints = map[phase0.Hash32]*Constraint - -// Constraint is a constraint on a block. For now just preconfirmations -// or inclusion constraints. type Constraint struct { - RawTx Transaction `json:"rawTx"` + Tx Transaction + Index *uint64 } // ConstraintCache is a cache for constraints. type ConstraintCache struct { - // map of slots to constraints - constraints map[uint64]Constraints + // map of slots to all constraints for that slot + constraints map[uint64][]*Constraint } // NewConstraintCache creates a new constraint cache. +// cap is the maximum number of slots to store constraints for. func NewConstraintCache() *ConstraintCache { return &ConstraintCache{ // TODO: there should be a maximum length here that we can pre-allocate (probably the lookahead window size) - constraints: make(map[uint64]Constraints), + constraints: make(map[uint64][]*Constraint), } } // AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. -func (c *ConstraintCache) AddInclusionConstraint(slot uint64, txHash phase0.Hash32, rawTx Transaction) { +func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) { if _, exists := c.constraints[slot]; !exists { - c.constraints[slot] = make(map[phase0.Hash32]*Constraint) + c.constraints[slot] = make([]*Constraint, 0) } - c.constraints[slot][txHash] = &Constraint{ - RawTx: rawTx, + c.constraints[slot] = append(c.constraints[slot], &Constraint{ + Tx: tx, + Index: index, + }) +} + +// AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot +func (c *ConstraintCache) AddInclusionConstraints(slot uint64, constraints []*Constraint) { + if _, exists := c.constraints[slot]; !exists { + c.constraints[slot] = make([]*Constraint, 0) } + + c.constraints[slot] = append(c.constraints[slot], constraints...) } // Get gets the constraints at the given slot. -func (c *ConstraintCache) Get(slot uint64) Constraints { +func (c *ConstraintCache) Get(slot uint64) []*Constraint { return c.constraints[slot] } +// FindTransactionByHash finds the constraint for the given transaction hash and returns it. +func (c *ConstraintCache) FindTransactionByHash(txHash [32]byte) (*Constraint, bool) { + for _, constraints := range c.constraints { + for _, constraint := range constraints { + hash, err := constraint.Tx.Hash() + if err != nil { + continue + } + + if hash == txHash { + return constraint, true + } + } + } + return nil, false +} + // Delete deletes the constraints at the given slot. func (c *ConstraintCache) Delete(slot uint64) { delete(c.constraints, slot) diff --git a/mev-boost/server/mock_relay.go b/mev-boost/server/mock_relay.go index 852e92dd..11bb54f5 100644 --- a/mev-boost/server/mock_relay.go +++ b/mev-boost/server/mock_relay.go @@ -183,7 +183,7 @@ func (m *mockRelay) handleSubmitConstraint(w http.ResponseWriter, req *http.Requ } func (m *mockRelay) defaultHandleSubmitConstraint(w http.ResponseWriter, req *http.Request) { - payload := []SignedConstraintSubmission{} + payload := BatchedSignedConstraints{} if err := DecodeJSON(req.Body, &payload); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/mev-boost/server/proofs.go b/mev-boost/server/proofs.go index 02f317ef..5342bada 100644 --- a/mev-boost/server/proofs.go +++ b/mev-boost/server/proofs.go @@ -39,6 +39,10 @@ func (p *InclusionProof) String() string { type HexBytes []byte +func (h HexBytes) Equal(other HexBytes) bool { + return bytes.Equal(h, other) +} + // MarshalJSON implements json.Marshaler. func (h HexBytes) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf(`"%#x"`, h)), nil diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 39122a1c..b9ad2c1a 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -45,9 +45,10 @@ var ( // Bolt errors var ( - errNilProof = errors.New("nil proof") - errInvalidProofs = errors.New("proof verification failed") - errInvalidRoot = errors.New("failed getting tx root from bid") + errNilProof = errors.New("nil proof") + errMismatchProofSize = errors.New("proof size mismatch") + errInvalidProofs = errors.New("proof verification failed") + errInvalidRoot = errors.New("failed getting tx root from bid") ) var ( @@ -332,8 +333,18 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http. } // verifyConstraintProofs verifies the proofs against the constraints, and returns an error if the proofs are invalid. -func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionProofs, constraints Constraints) error { +func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionProofs, slot uint64) error { log := m.log.WithFields(logrus.Fields{}) + + // BOLT: get constraints for the slot + inclusionConstraints := m.constraints.Get(slot) + + if len(responsePayload.Proofs) != len(inclusionConstraints) { + log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != constraints %d", + len(responsePayload.Proofs), len(inclusionConstraints)) + return errMismatchProofSize + } + // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. if responsePayload.Proofs != nil { // BOLT: remove unnecessary fields while logging @@ -353,15 +364,15 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP return errNilProof } - // Find the raw tx with the hash specified - constraint, ok := constraints[proof.TxHash] + // Find the constraint associated with this transaction in the cache + constraint, ok := m.constraints.FindTransactionByHash(proof.TxHash) if !ok { log.Warnf("[BOLT]: Tx hash %s not found in constraints", proof.TxHash.String()) // We don't actually have to return an error here, the relay just provided a proof that was unnecessary continue } - rawTx := constraint.RawTx + rawTx := constraint.Tx log.Infof("[BOLT]: Raw tx: %x", rawTx) @@ -419,7 +430,7 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R log.Info("submitConstraint") - payload := []SignedConstraintSubmission{} + payload := BatchedSignedConstraints{} if err := DecodeJSON(req.Body, &payload); err != nil { log.Error("error decoding payload: ", err) m.respondError(w, http.StatusBadRequest, err.Error()) @@ -427,18 +438,18 @@ func (m *BoostService) handleSubmitConstraint(w http.ResponseWriter, req *http.R } // Add all constraints to the cache - for _, signedConstraint := range payload { - constraint := signedConstraint.Message + for _, signedConstraints := range payload { + constraintMessage := signedConstraints.Message log.WithFields(logrus.Fields{ - "slot": constraint.Slot, - "txHash": constraint.TxHash.String(), - "rawTx": fmt.Sprintf("%#x", constraint.RawTx), - }).Info("[BOLT]: adding inclusion constraint to cache") - - // Add the constraint to the cache. They will be cleared when we receive a payload for the slot - // in `handleGetPayload` - m.constraints.AddInclusionConstraint(constraint.Slot, constraint.TxHash, constraint.RawTx) + "slot": constraintMessage.Slot, + "validatorIndex": constraintMessage.ValidatorIndex, + "count": len(constraintMessage.Constraints), + }).Info("[BOLT]: adding inclusion constraints to cache") + + // Add the constraints to the cache. + // They will be cleared when we receive a payload for the slot in `handleGetPayload` + m.constraints.AddInclusionConstraints(constraintMessage.Slot, constraintMessage.Constraints) } relayRespCh := make(chan error, len(m.relays)) @@ -832,24 +843,13 @@ func (m *BoostService) handleGetHeaderWithProofs(w http.ResponseWriter, req *htt return } - // BOLT: Get the inclusion constraints for this slot - inclusionConstraints := m.constraints.Get(slotUint) - - // BOLT: verify matching proofs & constraints - if len(responsePayload.Proofs) != len(inclusionConstraints) { - log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != constraints %d", - len(responsePayload.Proofs), len(inclusionConstraints)) - return - } - // BOLT: verify preconfirmation inclusion proofs. If they don't match, we don't consider the bid to be valid. if responsePayload.Proofs != nil { // BOLT: verify the proofs against the constraints. If they don't match, we don't consider the bid to be valid. - if err := m.verifyConstraintProofs(responsePayload, inclusionConstraints); err != nil { + if err := m.verifyConstraintProofs(responsePayload, slotUint); err != nil { log.Warnf("[BOLT]: Proof verification failed for relay %s: %s", relay.URL, err) return } - } mu.Lock() diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 6d6fef4e..b35b3f5a 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -317,18 +317,15 @@ func TestConstraintsAndProofs(t *testing.T) { txHash := _HexToHash("0xba40436abdc8adc037e2c92ea1099a5849053510c3911037ff663085ce44bc49") rawTx := _HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f") - // Build the constraint - constraint := SignedConstraintSubmission{ - Message: &ConstraintSubmission{ - Slot: slot, - TxHash: txHash, - RawTx: rawTx, + payload := BatchedSignedConstraints{&SignedConstraints{ + Message: ConstraintsMessage{ + ValidatorIndex: 12345, + Slot: slot, + Constraints: []*Constraint{{Transaction(rawTx), nil}}, }, - Signature: _HexToSignature( + Signature: _HexToBytes( "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c"), - } - - payload := []SignedConstraintSubmission{constraint} + }} // Build getHeader request hash := _HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7") @@ -341,8 +338,11 @@ func TestConstraintsAndProofs(t *testing.T) { rr := backend.request(t, http.MethodPost, path, payload) require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) - require.Equal(t, 1, len(backend.boost.constraints.Get(slot))) - require.Equal(t, Transaction(rawTx), backend.boost.constraints.Get(slot)[txHash].RawTx) + + got, ok := backend.boost.constraints.FindTransactionByHash(txHash) + require.True(t, ok) + require.Equal(t, Transaction(rawTx), got.Tx) + require.Nil(t, got.Index) }) t.Run("Normal function with constraints", func(t *testing.T) { diff --git a/mev-boost/server/transaction_ssz.go b/mev-boost/server/transaction_ssz.go index 8f46f8c4..d978d56d 100644 --- a/mev-boost/server/transaction_ssz.go +++ b/mev-boost/server/transaction_ssz.go @@ -1,6 +1,7 @@ package server import ( + "github.com/ethereum/go-ethereum/core/types" ssz "github.com/ferranbt/fastssz" ) @@ -50,3 +51,14 @@ func (tx *Transaction) GetTree() (*ssz.Node, error) { tx.HashTreeRootWith(w) return w.Node(), nil } + +// Hash returns the hash of the transaction +func (tx *Transaction) Hash() ([32]byte, error) { + var parsed = new(types.Transaction) + err := parsed.UnmarshalBinary(*tx) + if err != nil { + return [32]byte{}, err + } + + return parsed.Hash(), nil +} From ded8f4289b4bac8ad0cea9cf99dd1eecb4a17f1e Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:54:12 +0200 Subject: [PATCH 13/17] chore: cleaned cache impl --- mev-boost/server/constraints.go | 56 ++++++++++++++++++----------- mev-boost/server/service.go | 3 +- mev-boost/server/service_test.go | 3 +- mev-boost/server/transaction_ssz.go | 12 ------- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go index 88b44d0b..f4656f3d 100644 --- a/mev-boost/server/constraints.go +++ b/mev-boost/server/constraints.go @@ -1,5 +1,10 @@ package server +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + type BatchedSignedConstraints = []*SignedConstraints type SignedConstraints struct { @@ -21,7 +26,7 @@ type Constraint struct { // ConstraintCache is a cache for constraints. type ConstraintCache struct { // map of slots to all constraints for that slot - constraints map[uint64][]*Constraint + constraints map[uint64]map[common.Hash]*Constraint } // NewConstraintCache creates a new constraint cache. @@ -29,50 +34,61 @@ type ConstraintCache struct { func NewConstraintCache() *ConstraintCache { return &ConstraintCache{ // TODO: there should be a maximum length here that we can pre-allocate (probably the lookahead window size) - constraints: make(map[uint64][]*Constraint), + constraints: make(map[uint64]map[common.Hash]*Constraint), } } // AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. -func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) { +func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) error { if _, exists := c.constraints[slot]; !exists { - c.constraints[slot] = make([]*Constraint, 0) + c.constraints[slot] = make(map[common.Hash]*Constraint) + } + + var parsedTx = new(types.Transaction) + err := parsedTx.UnmarshalBinary(tx) + if err != nil { + return err } - c.constraints[slot] = append(c.constraints[slot], &Constraint{ + c.constraints[slot][parsedTx.Hash()] = &Constraint{ Tx: tx, Index: index, - }) + } + + return nil } // AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot -func (c *ConstraintCache) AddInclusionConstraints(slot uint64, constraints []*Constraint) { +func (c *ConstraintCache) AddInclusionConstraints(slot uint64, constraints []*Constraint) error { if _, exists := c.constraints[slot]; !exists { - c.constraints[slot] = make([]*Constraint, 0) + c.constraints[slot] = make(map[common.Hash]*Constraint) } - c.constraints[slot] = append(c.constraints[slot], constraints...) + for _, constraint := range constraints { + var parsedTx = new(types.Transaction) + err := parsedTx.UnmarshalBinary(constraint.Tx) + if err != nil { + return err + } + c.constraints[slot][parsedTx.Hash()] = constraint + } + + return nil } // Get gets the constraints at the given slot. -func (c *ConstraintCache) Get(slot uint64) []*Constraint { +func (c *ConstraintCache) Get(slot uint64) map[common.Hash]*Constraint { return c.constraints[slot] } // FindTransactionByHash finds the constraint for the given transaction hash and returns it. -func (c *ConstraintCache) FindTransactionByHash(txHash [32]byte) (*Constraint, bool) { +func (c *ConstraintCache) FindTransactionByHash(txHash common.Hash) (*Constraint, bool) { for _, constraints := range c.constraints { - for _, constraint := range constraints { - hash, err := constraint.Tx.Hash() - if err != nil { - continue - } - - if hash == txHash { - return constraint, true - } + if constraint, exists := constraints[txHash]; exists { + return constraint, true } } + return nil, false } diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index b9ad2c1a..8af28019 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -22,6 +22,7 @@ import ( eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella" eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/common" fastSsz "github.com/ferranbt/fastssz" "github.com/flashbots/go-boost-utils/ssz" "github.com/flashbots/go-boost-utils/types" @@ -365,7 +366,7 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP } // Find the constraint associated with this transaction in the cache - constraint, ok := m.constraints.FindTransactionByHash(proof.TxHash) + constraint, ok := m.constraints.FindTransactionByHash(common.HexToHash(proof.TxHash.String())) if !ok { log.Warnf("[BOLT]: Tx hash %s not found in constraints", proof.TxHash.String()) // We don't actually have to return an error here, the relay just provided a proof that was unnecessary diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index b35b3f5a..4b8a1ed3 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -27,6 +27,7 @@ import ( "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" eth2UtilBellatrix "github.com/attestantio/go-eth2-client/util/bellatrix" + "github.com/ethereum/go-ethereum/common" "github.com/flashbots/go-boost-utils/types" "github.com/holiman/uint256" "github.com/prysmaticlabs/go-bitfield" @@ -339,7 +340,7 @@ func TestConstraintsAndProofs(t *testing.T) { require.Equal(t, http.StatusOK, rr.Code) require.Equal(t, 1, backend.relays[0].GetRequestCount(path)) - got, ok := backend.boost.constraints.FindTransactionByHash(txHash) + got, ok := backend.boost.constraints.FindTransactionByHash(common.HexToHash(txHash.String())) require.True(t, ok) require.Equal(t, Transaction(rawTx), got.Tx) require.Nil(t, got.Index) diff --git a/mev-boost/server/transaction_ssz.go b/mev-boost/server/transaction_ssz.go index d978d56d..8f46f8c4 100644 --- a/mev-boost/server/transaction_ssz.go +++ b/mev-boost/server/transaction_ssz.go @@ -1,7 +1,6 @@ package server import ( - "github.com/ethereum/go-ethereum/core/types" ssz "github.com/ferranbt/fastssz" ) @@ -51,14 +50,3 @@ func (tx *Transaction) GetTree() (*ssz.Node, error) { tx.HashTreeRootWith(w) return w.Node(), nil } - -// Hash returns the hash of the transaction -func (tx *Transaction) Hash() ([32]byte, error) { - var parsed = new(types.Transaction) - err := parsed.UnmarshalBinary(*tx) - if err != nil { - return [32]byte{}, err - } - - return parsed.Hash(), nil -} From fb46e83598ed3ec3e4ec709af2297c5cd028b472 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:36:17 +0200 Subject: [PATCH 14/17] feat: added decoding test for constraints api --- mev-boost/server/service_test.go | 24 ++++++++++++++++++++++++ mev-boost/server/transaction_ssz.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/mev-boost/server/service_test.go b/mev-boost/server/service_test.go index 4b8a1ed3..0c3474c0 100644 --- a/mev-boost/server/service_test.go +++ b/mev-boost/server/service_test.go @@ -311,6 +311,30 @@ func TestRegisterValidator(t *testing.T) { }) } +func TestParseConstraints(t *testing.T) { + jsonStr := `[{ + "message": { + "validatorIndex": 12345, + "slot": 8978583, + "constraints": [{ + "tx": "0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f", + "index": null + }] + }, + "signature": "0x81510b571e22f89d1697545aac01c9ad0c1e7a3e778b3078bef524efae14990e58a6e960a152abd49de2e18d7fd3081c15d5c25867ccfad3d47beef6b39ac24b6b9fbf2cfa91c88f67aff750438a6841ec9e4a06a94ae41410c4f97b75ab284c" + }]` + + constraints := BatchedSignedConstraints{} + err := json.Unmarshal([]byte(jsonStr), &constraints) + require.NoError(t, err) + require.Len(t, constraints, 1) + require.Equal(t, uint64(12345), constraints[0].Message.ValidatorIndex) + require.Equal(t, uint64(8978583), constraints[0].Message.Slot) + require.Len(t, constraints[0].Message.Constraints, 1) + require.Equal(t, constraints[0].Message.Constraints[0].Tx, Transaction(_HexToBytes("0x02f871018304a5758085025ff11caf82565f94388c818ca8b9251b393131c08a736a67ccb1929787a41bb7ee22b41380c001a0c8630f734aba7acb4275a8f3b0ce831cf0c7c487fd49ee7bcca26ac622a28939a04c3745096fa0130a188fa249289fd9e60f9d6360854820dba22ae779ea6f573f"))) + require.Nil(t, constraints[0].Message.Constraints[0].Index) +} + func TestConstraintsAndProofs(t *testing.T) { path := pathSubmitConstraint slot := uint64(8978583) diff --git a/mev-boost/server/transaction_ssz.go b/mev-boost/server/transaction_ssz.go index 8f46f8c4..27f74c03 100644 --- a/mev-boost/server/transaction_ssz.go +++ b/mev-boost/server/transaction_ssz.go @@ -1,6 +1,10 @@ package server import ( + "encoding/hex" + "fmt" + "strings" + ssz "github.com/ferranbt/fastssz" ) @@ -50,3 +54,27 @@ func (tx *Transaction) GetTree() (*ssz.Node, error) { tx.HashTreeRootWith(w) return w.Node(), nil } + +// UnmarshalJSON custom unmarshal function for the Transaction type +func (tx *Transaction) UnmarshalJSON(data []byte) error { + // Remove the quotes from the JSON string + jsonString := string(data) + if len(jsonString) < 2 || jsonString[0] != '"' || jsonString[len(jsonString)-1] != '"' { + return fmt.Errorf("invalid JSON string") + } + jsonString = jsonString[1 : len(jsonString)-1] + + // Remove the "0x" prefix if present + jsonString = strings.TrimPrefix(jsonString, "0x") + + // Decode the hex string into the Transaction byte slice + decodedBytes, err := hex.DecodeString(jsonString) + if err != nil { + return err + } + + // Set the decoded bytes to the Transaction + *tx = Transaction(decodedBytes) + + return nil +} From 45a81f65defa254f21cd8acccee2dacb23421cc8 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:35:17 +0200 Subject: [PATCH 15/17] feat: added shardmap impl --- mev-boost/go.mod | 1 + mev-boost/go.sum | 2 ++ mev-boost/server/constraints.go | 38 ++++++++++++++--------------- mev-boost/server/service.go | 19 ++++++--------- mev-boost/server/transaction_ssz.go | 34 +++++++------------------- 5 files changed, 37 insertions(+), 57 deletions(-) diff --git a/mev-boost/go.mod b/mev-boost/go.mod index f6d36d87..1d89364f 100644 --- a/mev-boost/go.mod +++ b/mev-boost/go.mod @@ -65,6 +65,7 @@ require ( github.com/btcsuite/btcd v0.22.0-beta // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chainbound/shardmap v0.0.2 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 diff --git a/mev-boost/go.sum b/mev-boost/go.sum index 0aa577d7..6a9494d4 100644 --- a/mev-boost/go.sum +++ b/mev-boost/go.sum @@ -45,6 +45,8 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chainbound/shardmap v0.0.2 h1:yB1weccdm2vC6dnqzzLwPIvyAnRj7815mJWbkPybiYw= +github.com/chainbound/shardmap v0.0.2/go.mod h1:TBvIzhHyFUbt+oa3UzbijobTUh221st6xIbuki7WzPc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= diff --git a/mev-boost/server/constraints.go b/mev-boost/server/constraints.go index f4656f3d..6e438cfe 100644 --- a/mev-boost/server/constraints.go +++ b/mev-boost/server/constraints.go @@ -1,6 +1,7 @@ package server import ( + "github.com/chainbound/shardmap" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" ) @@ -26,31 +27,33 @@ type Constraint struct { // ConstraintCache is a cache for constraints. type ConstraintCache struct { // map of slots to all constraints for that slot - constraints map[uint64]map[common.Hash]*Constraint + constraints shardmap.FIFOMap[uint64, map[common.Hash]*Constraint] } // NewConstraintCache creates a new constraint cache. // cap is the maximum number of slots to store constraints for. -func NewConstraintCache() *ConstraintCache { +func NewConstraintCache(cap int) *ConstraintCache { return &ConstraintCache{ - // TODO: there should be a maximum length here that we can pre-allocate (probably the lookahead window size) - constraints: make(map[uint64]map[common.Hash]*Constraint), + constraints: *shardmap.NewFIFOMap[uint64, map[common.Hash]*Constraint](int(cap), 1, shardmap.HashUint64), } } // AddInclusionConstraint adds an inclusion constraint to the cache at the given slot for the given transaction. func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, index *uint64) error { - if _, exists := c.constraints[slot]; !exists { - c.constraints[slot] = make(map[common.Hash]*Constraint) + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Put(slot, make(map[common.Hash]*Constraint)) } + // parse transaction to get its hash and store it in the cache + // for constant time lookup later var parsedTx = new(types.Transaction) err := parsedTx.UnmarshalBinary(tx) if err != nil { return err } - c.constraints[slot][parsedTx.Hash()] = &Constraint{ + m, _ := c.constraints.Get(slot) + m[parsedTx.Hash()] = &Constraint{ Tx: tx, Index: index, } @@ -60,39 +63,34 @@ func (c *ConstraintCache) AddInclusionConstraint(slot uint64, tx Transaction, in // AddInclusionConstraints adds multiple inclusion constraints to the cache at the given slot func (c *ConstraintCache) AddInclusionConstraints(slot uint64, constraints []*Constraint) error { - if _, exists := c.constraints[slot]; !exists { - c.constraints[slot] = make(map[common.Hash]*Constraint) + if _, exists := c.constraints.Get(slot); !exists { + c.constraints.Put(slot, make(map[common.Hash]*Constraint)) } + m, _ := c.constraints.Get(slot) for _, constraint := range constraints { var parsedTx = new(types.Transaction) err := parsedTx.UnmarshalBinary(constraint.Tx) if err != nil { return err } - c.constraints[slot][parsedTx.Hash()] = constraint + m[parsedTx.Hash()] = constraint } return nil } // Get gets the constraints at the given slot. -func (c *ConstraintCache) Get(slot uint64) map[common.Hash]*Constraint { - return c.constraints[slot] +func (c *ConstraintCache) Get(slot uint64) (map[common.Hash]*Constraint, bool) { + return c.constraints.Get(slot) } // FindTransactionByHash finds the constraint for the given transaction hash and returns it. func (c *ConstraintCache) FindTransactionByHash(txHash common.Hash) (*Constraint, bool) { - for _, constraints := range c.constraints { - if constraint, exists := constraints[txHash]; exists { + for kv := range c.constraints.Iter() { + if constraint, exists := kv.Value[txHash]; exists { return constraint, true } } - return nil, false } - -// Delete deletes the constraints at the given slot. -func (c *ConstraintCache) Delete(slot uint64) { - delete(c.constraints, slot) -} diff --git a/mev-boost/server/service.go b/mev-boost/server/service.go index 8af28019..74eee484 100644 --- a/mev-boost/server/service.go +++ b/mev-boost/server/service.go @@ -47,6 +47,7 @@ var ( // 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") @@ -161,7 +162,7 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) { requestMaxRetries: opts.RequestMaxRetries, // BOLT: Initialize the constraint cache - constraints: NewConstraintCache(), + constraints: NewConstraintCache(64), }, nil } @@ -338,7 +339,11 @@ func (m *BoostService) verifyConstraintProofs(responsePayload *BidWithInclusionP log := m.log.WithFields(logrus.Fields{}) // BOLT: get constraints for the slot - inclusionConstraints := m.constraints.Get(slot) + inclusionConstraints, exists := m.constraints.Get(slot) + if !exists { + log.Warnf("[BOLT]: No constraints found for slot %d", slot) + return errMissingConstraint + } if len(responsePayload.Proofs) != len(inclusionConstraints) { log.Warnf("[BOLT]: Proof verification failed - number of preconfirmations mismatch: proofs %d != constraints %d", @@ -1201,14 +1206,6 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request return } - slot := uint64(0) - - // BOLT: Make sure we remove the constraints for this slot after we've received the payload. - defer func() { - // This will use the value of `slot` at execution time - m.constraints.Delete(slot) - }() - // Decode the body now payload := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock) if err := DecodeJSON(bytes.NewReader(body), payload); err != nil { @@ -1220,12 +1217,10 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request return } - slot = uint64(payload.Message.Slot) m.processCapellaPayload(w, req, log, payload, body) return } - slot = uint64(payload.Message.Slot) m.processDenebPayload(w, req, log, payload) } diff --git a/mev-boost/server/transaction_ssz.go b/mev-boost/server/transaction_ssz.go index 27f74c03..ad6d25ca 100644 --- a/mev-boost/server/transaction_ssz.go +++ b/mev-boost/server/transaction_ssz.go @@ -1,10 +1,6 @@ package server import ( - "encoding/hex" - "fmt" - "strings" - ssz "github.com/ferranbt/fastssz" ) @@ -12,7 +8,7 @@ import ( var MaxBytesPerTransaction uint64 = 1_073_741_824 // 2**30 // Transaction is a wrapper type of byte slice to implement the ssz.HashRoot interface -type Transaction []byte +type Transaction HexBytes // HashTreeRoot calculates the hash tree root of the transaction, which // is a list of basic types (byte). @@ -55,26 +51,14 @@ func (tx *Transaction) GetTree() (*ssz.Node, error) { return w.Node(), nil } -// UnmarshalJSON custom unmarshal function for the Transaction type -func (tx *Transaction) UnmarshalJSON(data []byte) error { - // Remove the quotes from the JSON string - jsonString := string(data) - if len(jsonString) < 2 || jsonString[0] != '"' || jsonString[len(jsonString)-1] != '"' { - return fmt.Errorf("invalid JSON string") - } - jsonString = jsonString[1 : len(jsonString)-1] - - // Remove the "0x" prefix if present - jsonString = strings.TrimPrefix(jsonString, "0x") - - // Decode the hex string into the Transaction byte slice - decodedBytes, err := hex.DecodeString(jsonString) - if err != nil { - return err - } +func (tx *Transaction) Equal(other *Transaction) bool { + return HexBytes(*tx).Equal(HexBytes(*other)) +} - // Set the decoded bytes to the Transaction - *tx = Transaction(decodedBytes) +func (tx *Transaction) MarshalJSON() ([]byte, error) { + return (*HexBytes)(tx).MarshalJSON() +} - return nil +func (tx *Transaction) UnmarshalJSON(input []byte) error { + return (*HexBytes)(tx).UnmarshalJSON(input) } From 8d7235109eadcc4a46ba30b37cb40cbe783373b8 Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:38:12 +0200 Subject: [PATCH 16/17] chore: update shardmap --- mev-boost/go.mod | 2 +- mev-boost/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mev-boost/go.mod b/mev-boost/go.mod index 1d89364f..c3a1de63 100644 --- a/mev-boost/go.mod +++ b/mev-boost/go.mod @@ -18,6 +18,7 @@ require ( github.com/DataDog/zstd v1.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect + github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c // indirect github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/pebble v0.0.0-20230928194634-aa077af62593 // indirect @@ -65,7 +66,6 @@ require ( github.com/btcsuite/btcd v0.22.0-beta // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chainbound/shardmap v0.0.2 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 diff --git a/mev-boost/go.sum b/mev-boost/go.sum index 6a9494d4..c1d27a7b 100644 --- a/mev-boost/go.sum +++ b/mev-boost/go.sum @@ -47,6 +47,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chainbound/shardmap v0.0.2 h1:yB1weccdm2vC6dnqzzLwPIvyAnRj7815mJWbkPybiYw= github.com/chainbound/shardmap v0.0.2/go.mod h1:TBvIzhHyFUbt+oa3UzbijobTUh221st6xIbuki7WzPc= +github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c h1:7GkJDinsgr9Wcr5yJZGGtYFcCAxS9WSdOBr4DXBaM7c= +github.com/chainbound/shardmap v0.0.3-0.20240604113309-43d9072efe8c/go.mod h1:TBvIzhHyFUbt+oa3UzbijobTUh221st6xIbuki7WzPc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= From 0a32ce9c2bce9a864e882fdccdd4b3d0d38db9ea Mon Sep 17 00:00:00 2001 From: nicolas <48695862+merklefruit@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:41:23 +0200 Subject: [PATCH 17/17] fix: recursive marshaljson in hexbytes --- mev-boost/server/proofs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mev-boost/server/proofs.go b/mev-boost/server/proofs.go index 5342bada..fce43624 100644 --- a/mev-boost/server/proofs.go +++ b/mev-boost/server/proofs.go @@ -45,7 +45,7 @@ func (h HexBytes) Equal(other HexBytes) bool { // MarshalJSON implements json.Marshaler. func (h HexBytes) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%#x"`, h)), nil + return []byte(fmt.Sprintf(`"%#x"`, []byte(h))), nil } // UnmarshalJSON implements json.Unmarshaler.