Skip to content

Commit

Permalink
feat: bolt-boost changes
Browse files Browse the repository at this point in the history
Co-authored-by: nicolas <[email protected]>
  • Loading branch information
thedevbirb and merklefruit committed Apr 29, 2024
1 parent 4408178 commit 16863c7
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 6 deletions.
56 changes: 56 additions & 0 deletions mev-boost/server/boltSidecar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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
}
110 changes: 110 additions & 0 deletions mev-boost/server/preconf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package server

import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"

fastSsz "github.com/ferranbt/fastssz"

builderSpec "github.com/attestantio/go-builder-client/spec"
"github.com/attestantio/go-eth2-client/spec/phase0"
)

type BidWithPreconfirmationsProofs struct {
// The block bid
Bid *builderSpec.VersionedSignedBuilderBid `json:"bid"`
// The preconfirmations with proofs
Proofs []*PreconfirmationWithProof `json:"proofs"`
}

func (b *BidWithPreconfirmationsProofs) String() string {
out, err := json.Marshal(b)
if err != nil {
return err.Error()
}
return string(out)
}

func (p *PreconfirmationWithProof) String() string {
proofs, err := json.Marshal(p)
if err != nil {
return err.Error()
}
return string(proofs)
}

type HexBytes []byte

// MarshalJSON implements json.Marshaler.
func (h HexBytes) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%#x"`, h)), nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (h *HexBytes) UnmarshalJSON(input []byte) error {
if len(input) == 0 {
return errors.New("input missing")
}

if !bytes.HasPrefix(input, []byte{'"', '0', 'x'}) {
return errors.New("invalid prefix")
}

if !bytes.HasSuffix(input, []byte{'"'}) {
return errors.New("invalid suffix")
}

var data string
json.Unmarshal(input, &data)

res, _ := hex.DecodeString(strings.TrimPrefix(data, "0x"))

*h = res

return nil
}

// SerializedMerkleProof contains a serialized Merkle proof of transaction inclusion.
// - `Index“ is the generalized index of the included transaction from the SSZ tree
// created from the list of transactions.
// - `Hashes` are the other branch hashes needed to reconstruct the Merkle proof.
//
// For reference, see https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md
type SerializedMerkleProof struct {
Index int `json:"index"`
Hashes []HexBytes `ssz-size:"dynamic" json:"hashes"`
}

func (s *SerializedMerkleProof) FromFastSszProof(p *fastSsz.Proof) {
s.Index = p.Index
s.Hashes = make([]HexBytes, len(p.Hashes))
for i, h := range p.Hashes {
s.Hashes[i] = h
}
}

// ToFastSszProof converts a SerializedMerkleProof to a fastssz.Proof.
func (s *SerializedMerkleProof) ToFastSszProof(leaf []byte) *fastSsz.Proof {
p := &fastSsz.Proof{
Index: s.Index,
Leaf: leaf,
Hashes: make([][]byte, len(s.Hashes)),
}
for i, h := range s.Hashes {
p.Hashes[i] = h
}
return p
}

// PreconfirmationWithProof is a preconfirmed transaction in the block with
// proof of inclusion, using Merkle Trees.
type PreconfirmationWithProof struct {
// The transaction hash of the preconfirmation
TxHash phase0.Hash32 `ssz-size:"32" json:"txHash"`
// The Merkle proof of the preconfirmation
MerkleProof *SerializedMerkleProof `json:"merkleProof"`
}
128 changes: 122 additions & 6 deletions mev-boost/server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package server
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand All @@ -22,6 +23,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"
fastSsz "github.com/ferranbt/fastssz"
"github.com/flashbots/go-boost-utils/ssz"
"github.com/flashbots/go-boost-utils/types"
"github.com/flashbots/go-boost-utils/utils"
Expand Down Expand Up @@ -101,6 +103,9 @@ type BoostService struct {

slotUID *slotUID
slotUIDLock sync.Mutex

// BOLT: sidecar connection
sidecar *boltSidecar
}

// NewBoostService created a new BoostService
Expand All @@ -114,6 +119,9 @@ 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,
Expand All @@ -139,6 +147,8 @@ func NewBoostService(opts BoostServiceOpts) (*BoostService, error) {
CheckRedirect: httpClientDisallowRedirects,
},
requestMaxRetries: opts.RequestMaxRetries,

sidecar: boltSidecar,
}, nil
}

Expand Down Expand Up @@ -309,6 +319,8 @@ func (m *BoostService) handleRegisterValidator(w http.ResponseWriter, req *http.
}

// 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"]
Expand Down Expand Up @@ -379,25 +391,27 @@ 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(builderSpec.VersionedSignedBuilderBid)
responsePayload := new(BidWithPreconfirmationsProofs)
code, err := SendHTTPRequest(context.Background(), m.httpClientGetHeader, http.MethodGet, url, ua, headers, nil, responsePayload)
if err != nil {
log.WithError(err).Warn("error making request to relay")
return
}

log.Infof("[BOLT]: DECODED RESPONSE PAYLOAD FROM RELAY: %s", responsePayload)

if code == http.StatusNoContent {
log.Debug("no-content response")
log.Warn("no-content response")
return
}

// Skip if payload is empty
if responsePayload.IsEmpty() {
if responsePayload.Bid.IsEmpty() {
return
}

// Getting the bid info will check if there are missing fields in the response
bidInfo, err := parseBidInfo(responsePayload)
bidInfo, err := parseBidInfo(responsePayload.Bid)
if err != nil {
log.WithError(err).Warn("error parsing bid info")
return
Expand All @@ -423,7 +437,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)

// Verify the relay signature in the relay response
if !config.SkipRelaySignatureCheck {
ok, err := checkRelaySignature(responsePayload, m.builderSigningDomain, relay.PublicKey)
ok, err := checkRelaySignature(responsePayload.Bid, m.builderSigningDomain, relay.PublicKey)
if err != nil {
log.WithError(err).Error("error verifying relay signature")
return
Expand Down Expand Up @@ -457,6 +471,108 @@ 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{})

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
slot, _ := responsePayload.Bid.BlockNumber() // TODO: use slot instead of bn
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) != 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
}

transactionsRoot, err := responsePayload.Bid.TransactionsRoot()
if err != nil {
log.WithError(err).Error("[BOLT]: error getting tx root from bid")
w.WriteHeader(http.StatusBadRequest)
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 {
log.Info(fmt.Sprintf("[BOLT]: Preconfirmation proof verified for tx hash %s in %s", proof.TxHash.String(), elapsed))
}
}
}

mu.Lock()
defer mu.Unlock()

Expand All @@ -478,7 +594,7 @@ func (m *BoostService) handleGetHeader(w http.ResponseWriter, req *http.Request)

// Use this relay's response as mev-boost response because it's most profitable
log.Debug("new best bid")
result.response = *responsePayload
result.response = *responsePayload.Bid
result.bidInfo = bidInfo
result.t = time.Now()
}(relay)
Expand Down
Loading

0 comments on commit 16863c7

Please sign in to comment.