├── .dockerignore ├── staticcheck.conf ├── docs ├── block-proposal.png ├── logo-horizontal-transparent.png ├── mev-boost-integration-overview.png ├── timing-games.md ├── geth-pos-privnet.md ├── logo-horizontal-transparent.svg └── audit-20220620.md ├── main.go ├── cmd ├── mev-boost │ └── main.go └── test-cli │ ├── engine.go │ ├── requests.go │ ├── beacon.go │ ├── validator.go │ ├── README.md │ └── main.go ├── server ├── types │ ├── U256str.go │ ├── errors.go │ ├── relay_entry.go │ └── relay_entry_test.go ├── params │ └── paths.go ├── mock │ ├── mock_relay_test.go │ ├── mock_types.go │ └── mock_types_test.go ├── constants.go ├── metrics.go ├── register_validator.go ├── utils_test.go ├── utils.go └── register_validator_test.go ├── .goreleaser-release.yaml ├── .github ├── dependabot.yaml ├── pull_request_template.md └── workflows │ ├── tests.yml │ ├── lint.yml │ └── release.yaml ├── .gitignore ├── Dockerfile ├── cli ├── types.go ├── main_test.go ├── config.go ├── flags.go └── main.go ├── .goreleaser-build.yaml ├── LICENSE ├── scripts └── run_mergemock_integration.sh ├── config └── vars.go ├── .golangci.yml ├── .env.example ├── common └── common.go ├── Makefile ├── SECURITY.md ├── config.example.yaml ├── CONTRIBUTING.md ├── RELEASE.md ├── go.mod ├── testdata ├── signed-blinded-beacon-block-fulu.json ├── signed-blinded-beacon-block-electra.json ├── signed-blinded-beacon-block-bellatrix.json └── signed-blinded-beacon-block-capella.json ├── CODE_OF_CONDUCT.md └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | .git -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-ST1000"] 2 | -------------------------------------------------------------------------------- /docs/block-proposal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flashbots/mev-boost/HEAD/docs/block-proposal.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/flashbots/mev-boost/cli" 4 | 5 | func main() { 6 | cli.Main() 7 | } 8 | -------------------------------------------------------------------------------- /docs/logo-horizontal-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flashbots/mev-boost/HEAD/docs/logo-horizontal-transparent.png -------------------------------------------------------------------------------- /docs/mev-boost-integration-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flashbots/mev-boost/HEAD/docs/mev-boost-integration-overview.png -------------------------------------------------------------------------------- /cmd/mev-boost/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/flashbots/mev-boost/cli" 4 | 5 | func main() { 6 | cli.Main() 7 | } 8 | -------------------------------------------------------------------------------- /server/types/U256str.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/flashbots/go-boost-utils/types" 5 | ) 6 | 7 | type U256Str = types.U256Str 8 | 9 | func IntToU256(i uint64) U256Str { 10 | return types.IntToU256(i) 11 | } 12 | -------------------------------------------------------------------------------- /.goreleaser-release.yaml: -------------------------------------------------------------------------------- 1 | # https://goreleaser.com/customization/release/ 2 | builds: 3 | - skip: true 4 | release: 5 | draft: true 6 | extra_files: 7 | - glob: ./build/* 8 | header: | 9 | # 🚀 Features 10 | # 🎄 Enhancements 11 | # 🐞 Notable bug fixes 12 | # 🎠 Community 13 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: gomod 7 | directory: / 8 | schedule: 9 | interval: daily 10 | reviewers: 11 | - "metachris" -------------------------------------------------------------------------------- /server/params/paths.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | const ( 4 | // Router paths 5 | PathStatus = "/eth/v1/builder/status" 6 | PathRegisterValidator = "/eth/v1/builder/validators" 7 | PathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" 8 | PathGetPayload = "/eth/v1/builder/blinded_blocks" 9 | PathGetPayloadV2 = "/eth/v2/builder/blinded_blocks" 10 | ) 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📝 Summary 2 | 3 | 4 | 5 | ## ⛱ Motivation and Context 6 | 7 | 8 | 9 | ## 📚 References 10 | 11 | 12 | 13 | --- 14 | 15 | ## ✅ I have run these commands 16 | 17 | * [ ] `make lint` 18 | * [ ] `make test-race` 19 | * [ ] `go mod tidy` 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Ignore VI/Vim swapfiles 18 | .*.sw? 19 | 20 | # IntelliJ 21 | .idea 22 | .ijwb 23 | /mev-boost 24 | /test-cli 25 | /tmp 26 | /dist 27 | .vscode/ 28 | /README.internal.md 29 | /validator_data.json 30 | /build/ 31 | 32 | # Environemnt variable files 33 | .env* 34 | !.env.example 35 | 36 | -------------------------------------------------------------------------------- /server/mock/mock_relay_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/flashbots/mev-boost/server/params" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_mockRelay(t *testing.T) { 14 | t.Run("bad payload", func(t *testing.T) { 15 | relay := NewRelay(t) 16 | req, err := http.NewRequest(http.MethodPost, params.PathRegisterValidator, bytes.NewReader([]byte("123"))) 17 | require.NoError(t, err) 18 | rr := httptest.NewRecorder() 19 | relay.getRouter().ServeHTTP(rr, req) 20 | require.Equal(t, http.StatusBadRequest, rr.Code) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.24 as builder 3 | ARG VERSION 4 | WORKDIR /build 5 | 6 | COPY go.mod ./ 7 | COPY go.sum ./ 8 | 9 | RUN go mod download 10 | 11 | ADD . . 12 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux go build \ 13 | -trimpath \ 14 | -v \ 15 | -ldflags "-w -s -X 'github.com/flashbots/mev-boost/config.Version=$VERSION'" \ 16 | -o mev-boost . 17 | 18 | FROM alpine 19 | WORKDIR /app 20 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 21 | COPY --from=builder /build/mev-boost /app/mev-boost 22 | EXPOSE 18550 23 | ENTRYPOINT ["/app/mev-boost"] 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: warp-ubuntu-latest-x64-16x 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: ^1.24 18 | id: go 19 | 20 | - name: Checkout sources 21 | uses: actions/checkout@v2 22 | 23 | - name: Run unit tests and generate the coverage report 24 | run: make test-coverage 25 | 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v2 28 | with: 29 | files: ./coverage.out 30 | verbose: false 31 | flags: unittests 32 | -------------------------------------------------------------------------------- /server/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "errors" 4 | 5 | // ErrMissingRelayPubkey is returned if a new RelayEntry URL has no public key. 6 | var ErrMissingRelayPubkey = errors.New("missing relay public key") 7 | 8 | // ErrPointAtInfinityPubkey is returned if a new RelayEntry URL has point-at-infinity public key. 9 | var ErrPointAtInfinityPubkey = errors.New("relay public key cannot be the point-at-infinity") 10 | 11 | // ErrInvalidContentType is returned when the response content type is invalid. 12 | var ErrInvalidContentType = errors.New("invalid content type") 13 | 14 | // ErrMissingEthConsensusVersion is returned when the response is octet-stream but there is no "Eth-Consensus-Version" header. 15 | var ErrMissingEthConsensusVersion = errors.New("missing eth-consensus-version") 16 | -------------------------------------------------------------------------------- /cli/types.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/flashbots/mev-boost/server/types" 8 | ) 9 | 10 | var errDuplicateEntry = errors.New("duplicate entry") 11 | 12 | type relayList []types.RelayEntry 13 | 14 | func (r *relayList) String() string { 15 | return strings.Join(types.RelayEntriesToStrings(*r), ",") 16 | } 17 | 18 | func (r *relayList) Contains(relay types.RelayEntry) bool { 19 | for _, entry := range *r { 20 | if relay.String() == entry.String() { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | 27 | func (r *relayList) Set(value string) error { 28 | relay, err := types.NewRelayEntry(value) 29 | if err != nil { 30 | return err 31 | } 32 | if r.Contains(relay) { 33 | return errDuplicateEntry 34 | } 35 | *r = append(*r, relay) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /.goreleaser-build.yaml: -------------------------------------------------------------------------------- 1 | # https://goreleaser.com/customization/builds/ 2 | project_name: mev-boost 3 | builds: 4 | - id: mev-boost 5 | env: 6 | # Force build to be all Go. 7 | - CGO_ENABLED=0 8 | flags: 9 | # Remove all file system paths from the executable. 10 | - -trimpath 11 | ldflags: 12 | # Disables DWARF debugging information. 13 | - -w 14 | # Disables symbol table information. 15 | - -s 16 | # Sets the value of the symbol. 17 | - -X github.com/flashbots/mev-boost/config.Version={{.Version}} 18 | goos: 19 | - linux 20 | - darwin 21 | - windows 22 | goarch: 23 | - amd64 24 | - arm64 25 | - riscv64 26 | ignore: 27 | - goos: darwin 28 | goarch: riscv64 29 | - goos: windows 30 | goarch: riscv64 31 | -------------------------------------------------------------------------------- /server/constants.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | const ( 4 | HeaderAccept = "Accept" 5 | HeaderContentType = "Content-Type" 6 | HeaderEthConsensusVersion = "Eth-Consensus-Version" 7 | HeaderKeySlotUID = "X-MEVBoost-SlotID" 8 | HeaderKeyVersion = "X-MEVBoost-Version" 9 | HeaderUserAgent = "User-Agent" 10 | // Header which communicates when a request was sent. Used to measure latency. 11 | HeaderDateMilliseconds = "Date-Milliseconds" 12 | // Header which communicates timeout set by client. Used to tweak block creation delay together with Date-Milliseconds. 13 | HeaderTimeoutMs = "X-Timeout-Ms" 14 | 15 | MediaTypeJSON = "application/json" 16 | MediaTypeOctetStream = "application/octet-stream" 17 | 18 | EthConsensusVersionBellatrix = "bellatrix" 19 | EthConsensusVersionCapella = "capella" 20 | EthConsensusVersionDeneb = "deneb" 21 | EthConsensusVersionElectra = "electra" 22 | EthConsensusVersionFulu = "fulu" 23 | ) 24 | -------------------------------------------------------------------------------- /cli/main_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "github.com/flashbots/go-boost-utils/types" 8 | "github.com/flashbots/mev-boost/common" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestFloatEthTo256Wei(t *testing.T) { 13 | // test with small input 14 | i := 0.000000000000012345 15 | weiU256, err := common.FloatEthTo256Wei(i) 16 | require.NoError(t, err) 17 | require.Equal(t, types.IntToU256(12345), *weiU256) 18 | 19 | // test with zero 20 | i = 0 21 | weiU256, err = common.FloatEthTo256Wei(i) 22 | require.NoError(t, err) 23 | require.Equal(t, types.IntToU256(0), *weiU256) 24 | 25 | // test with large input 26 | i = 987654.3 27 | weiU256, err = common.FloatEthTo256Wei(i) 28 | require.NoError(t, err) 29 | 30 | r := big.NewInt(9876543) 31 | r.Mul(r, big.NewInt(1e17)) 32 | referenceWeiU256 := new(types.U256Str) 33 | err = referenceWeiU256.FromBig(r) 34 | require.NoError(t, err) 35 | 36 | require.Equal(t, *referenceWeiU256, *weiU256) 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: warp-ubuntu-latest-x64-16x 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ^1.24.0 21 | id: go 22 | 23 | - name: Ensure go mod tidy runs without changes 24 | run: | 25 | go mod tidy 26 | git diff-index HEAD 27 | git diff-index --quiet HEAD 28 | 29 | - name: Install gofumpt 30 | run: go install mvdan.cc/gofumpt@v0.8.0 31 | 32 | - name: Install staticcheck 33 | run: go install honnef.co/go/tools/cmd/staticcheck@v0.6.1 34 | 35 | - name: Install golangci-lint 36 | run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.2 37 | 38 | - name: Lint 39 | run: make lint 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 Flashbots 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /scripts/run_mergemock_integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | PROJECT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}")/.. && pwd) 5 | 6 | # 7 | # Default to these values for mergemock. 8 | # 9 | MERGEMOCK_DIR=${MERGEMOCK_DIR:-$PROJECT_DIR/../mergemock} 10 | MERGEMOCK_BIN=${MERGEMOCK_BIN:-./mergemock} 11 | 12 | # 13 | # This function will ensure there are no lingering processes afterwards. 14 | # 15 | trap 'kill -9 $(jobs -p)' exit 16 | 17 | # 18 | # Start mev-boost. 19 | # 20 | $PROJECT_DIR/mev-boost -mainnet -relays http://0x821961b64d99b997c934c22b4fd6109790acf00f7969322c4e9dbf1ca278c333148284c01c5ef551a1536ddd14b178b9@127.0.0.1:28545 & 21 | echo "Waiting for mev-boost to become available..." 22 | while ! nc -z localhost 18550; do 23 | sleep 0.1 24 | done 25 | 26 | # 27 | # Mock the relay. 28 | # 29 | pushd $MERGEMOCK_DIR >/dev/null 30 | $MERGEMOCK_BIN relay --listen-addr 127.0.0.1:28545 --secret-key 1e64a14cb06073c2d7c8b0b891e5dc3dc719b86e5bf4c131ddbaa115f09f8f52 & 31 | echo "Waiting for relay to become available..." 32 | while ! nc -z localhost 28545; do 33 | sleep 0.1 34 | done 35 | 36 | # 37 | # Mock the consensus. 38 | # 39 | $MERGEMOCK_BIN consensus --slot-time=4s --engine http://127.0.0.1:8551 --builder http://127.0.0.1:18550 --slot-bound 10 & 40 | MERGEMOCK_CONSENSUS_PID=$! 41 | popd >/dev/null 42 | 43 | # 44 | # The script will exit when this process finishes. 45 | # 46 | wait $MERGEMOCK_CONSENSUS_PID 47 | -------------------------------------------------------------------------------- /config/vars.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/flashbots/mev-boost/common" 7 | ) 8 | 9 | var ( 10 | // Version is set at build time (must be a var, not a const!) 11 | Version = "dev" 12 | 13 | // RFC3339Milli is a time format string based on time.RFC3339 but with millisecond precision 14 | RFC3339Milli = "2006-01-02T15:04:05.999Z07:00" 15 | 16 | // ServerReadTimeoutMs sets the maximum duration for reading the entire request, including the body. A zero or negative value means there will be no timeout. 17 | ServerReadTimeoutMs = common.GetEnvInt("MEV_BOOST_SERVER_READ_TIMEOUT_MS", 1000) 18 | 19 | // ServerReadHeaderTimeoutMs sets the amount of time allowed to read request headers. 20 | ServerReadHeaderTimeoutMs = common.GetEnvInt("MEV_BOOST_SERVER_READ_HEADER_TIMEOUT_MS", 1000) 21 | 22 | // ServerWriteTimeoutMs sets the maximum duration before timing out writes of the response. 23 | ServerWriteTimeoutMs = common.GetEnvInt("MEV_BOOST_SERVER_WRITE_TIMEOUT_MS", 0) 24 | 25 | // ServerIdleTimeoutMs sets the maximum amount of time to wait for the next request when keep-alives are enabled. 26 | ServerIdleTimeoutMs = common.GetEnvInt("MEV_BOOST_SERVER_IDLE_TIMEOUT_MS", 0) 27 | 28 | // ServerMaxHeaderBytes defines the max header byte size for requests (for dos prevention) 29 | ServerMaxHeaderBytes = common.GetEnvInt("MAX_HEADER_BYTES", 4000) 30 | 31 | // SkipRelaySignatureCheck can be used to disable relay signature check 32 | SkipRelaySignatureCheck = os.Getenv("SKIP_RELAY_SIGNATURE_CHECK") == "1" 33 | 34 | SlotTimeSec = uint64(common.GetEnvInt("SLOT_SEC", common.SlotTimeSecMainnet)) 35 | ) 36 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | - canonicalheader 6 | - contextcheck 7 | - copyloopvar 8 | - cyclop 9 | - depguard 10 | - dupl 11 | - exhaustruct 12 | - funcorder 13 | - funlen 14 | - gochecknoglobals 15 | - gochecknoinits 16 | - gocognit 17 | - goconst 18 | - gocritic 19 | - godot 20 | - godox 21 | - gosec 22 | - intrange 23 | - ireturn 24 | - lll 25 | - maintidx 26 | - mnd 27 | - musttag 28 | - nlreturn 29 | - noctx 30 | - nonamedreturns 31 | - paralleltest 32 | - perfsprint 33 | - rowserrcheck 34 | - sqlclosecheck 35 | - tagliatelle 36 | - testpackage 37 | - varnamelen 38 | - wastedassign 39 | - wrapcheck 40 | - wsl 41 | settings: 42 | gomoddirectives: 43 | replace-allow-list: 44 | - github.com/attestantio/go-builder-client 45 | - github.com/attestantio/go-eth2-client 46 | govet: 47 | disable: 48 | - fieldalignment 49 | - shadow 50 | enable-all: true 51 | exclusions: 52 | generated: lax 53 | presets: 54 | - comments 55 | - common-false-positives 56 | - legacy 57 | - std-error-handling 58 | paths: 59 | - third_party$ 60 | - builtin$ 61 | - examples$ 62 | formatters: 63 | enable: 64 | - gci 65 | - gofmt 66 | - gofumpt 67 | - goimports 68 | settings: 69 | gofumpt: 70 | extra-rules: true 71 | exclusions: 72 | generated: lax 73 | paths: 74 | - third_party$ 75 | - builtin$ 76 | - examples$ 77 | -------------------------------------------------------------------------------- /cmd/test-cli/engine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common" 5 | "github.com/ethereum/go-ethereum/common/hexutil" 6 | ) 7 | 8 | type engineBlockData struct { 9 | ParentHash common.Hash `json:"parentHash"` 10 | Hash common.Hash `json:"hash"` 11 | Timestamp hexutil.Uint64 `json:"timestamp"` 12 | } 13 | 14 | func getLatestEngineBlock(engineEndpoint string) (engineBlockData, error) { 15 | dst := engineBlockData{} 16 | payload := map[string]any{"id": 67, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": []any{"latest", false}} 17 | err := sendJSONRequest(engineEndpoint, payload, &dst) 18 | if err != nil { 19 | log.WithError(err).Info("could not get latest block") 20 | return engineBlockData{}, err 21 | } 22 | 23 | return dst, nil 24 | } 25 | 26 | type forkchoiceState struct { 27 | HeadBlockHash common.Hash `json:"headBlockHash"` 28 | SafeBlockHash common.Hash `json:"safeBlockHash"` 29 | FinalizedBlockHash common.Hash `json:"finalizedBlockHash"` 30 | } 31 | 32 | type payloadAttributes struct { 33 | Timestamp hexutil.Uint64 `json:"timestamp"` 34 | PrevRandao common.Hash `json:"prevRandao"` 35 | SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"` 36 | } 37 | 38 | func sendForkchoiceUpdate(engineEndpoint string, block engineBlockData) error { 39 | log.WithField("hash", block.Hash).Info("sending FCU") 40 | params := []any{ 41 | forkchoiceState{block.Hash, block.Hash, block.Hash}, 42 | payloadAttributes{block.Timestamp + 1, common.Hash{0x01}, common.Address{0x02}}, 43 | } 44 | payload := map[string]any{"id": 67, "jsonrpc": "2.0", "method": "engine_forkchoiceUpdatedV1", "params": params} 45 | return sendJSONRequest(engineEndpoint, payload, nil) 46 | } 47 | -------------------------------------------------------------------------------- /server/mock/mock_types.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/attestantio/go-eth2-client/spec/bellatrix" 5 | "github.com/attestantio/go-eth2-client/spec/phase0" 6 | "github.com/ethereum/go-ethereum/common/hexutil" 7 | "github.com/flashbots/go-boost-utils/utils" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // TestLog is used to log information in the test methods 12 | var TestLog = logrus.NewEntry(logrus.New()) 13 | 14 | // HexToBytes converts a hexadecimal string to a byte array 15 | func HexToBytes(hex string) []byte { 16 | res, err := hexutil.Decode(hex) 17 | if err != nil { 18 | panic(err) 19 | } 20 | return res 21 | } 22 | 23 | // HexToHash converts a hexadecimal string to an Ethereum hash 24 | func HexToHash(s string) (ret phase0.Hash32) { 25 | ret, err := utils.HexToHash(s) 26 | if err != nil { 27 | TestLog.Error(err, " _HexToHash: ", s) 28 | panic(err) 29 | } 30 | return ret 31 | } 32 | 33 | // HexToAddress converts a hexadecimal string to an Ethereum address 34 | func HexToAddress(s string) (ret bellatrix.ExecutionAddress) { 35 | ret, err := utils.HexToAddress(s) 36 | if err != nil { 37 | TestLog.Error(err, " _HexToAddress: ", s) 38 | panic(err) 39 | } 40 | return ret 41 | } 42 | 43 | // HexToPubkey converts a hexadecimal string to a BLS Public Key 44 | func HexToPubkey(s string) (ret phase0.BLSPubKey) { 45 | ret, err := utils.HexToPubkey(s) 46 | if err != nil { 47 | TestLog.Error(err, " _HexToPubkey: ", s) 48 | panic(err) 49 | } 50 | return 51 | } 52 | 53 | // HexToSignature converts a hexadecimal string to a BLS Signature 54 | func HexToSignature(s string) (ret phase0.BLSSignature) { 55 | ret, err := utils.HexToSignature(s) 56 | if err != nil { 57 | TestLog.Error(err, " _HexToSignature: ", s) 58 | panic(err) 59 | } 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # General settings 2 | BOOST_LISTEN_ADDR=localhost:18550 # Listen address for mev-boost server 3 | 4 | # Logging and debugging settings 5 | LOG_JSON=false # Set to true to log in JSON format instead of text 6 | DEBUG=false # Set to true to enable debug mode (shorthand for '--loglevel debug') 7 | LOG_LEVEL=info # Log level: trace, debug, info, warn/warning, error, fatal, panic 8 | LOG_SERVICE_TAG= # Optional: add a 'service=...' tag to all log messages 9 | DISABLE_LOG_VERSION=false # Set to true to disable logging the version 10 | 11 | # Genesis settings 12 | GENESIS_FORK_VERSION= # Custom genesis fork version (optional) 13 | GENESIS_TIMESTAMP=-1 # Custom genesis timestamp (in unix seconds) 14 | MAINNET=true # Set to true to use Mainnet 15 | SEPOLIA=false # Set to true to use Sepolia network 16 | HOLESKY=false # Set to true to use Holesky network 17 | 18 | # Relay settings 19 | RELAYS= # Relay URLs: single entry or comma-separated list (scheme://pubkey@host) 20 | MIN_BID_ETH=0 # Minimum bid to accept from a relay (in ETH) 21 | RELAY_STARTUP_CHECK=false # Set to true to check relay status on startup and on status API call 22 | 23 | # Relay timeout settings (in ms) 24 | RELAY_TIMEOUT_MS_GETHEADER=950 # Timeout for getHeader requests to the relay (in ms) 25 | RELAY_TIMEOUT_MS_GETPAYLOAD=4000 # Timeout for getPayload requests to the relay (in ms) 26 | RELAY_TIMEOUT_MS_REGVAL=3000 # Timeout for registerValidator requests (in ms) 27 | 28 | # Retry settings 29 | REQUEST_MAX_RETRIES=5 # Maximum number of retries for a relay get payload request 30 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "math/big" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/flashbots/go-boost-utils/types" 9 | ) 10 | 11 | const ( 12 | SlotTimeSecMainnet = 12 13 | // FieldElementsPerBlob is the number of field elements needed to represent a blob. 14 | FieldElementsPerBlob = 4096 15 | // BlobExpansionFactor is the factor by which we extend a blob for PeerDAS. 16 | BlobExpansionFactor = 2 17 | // FieldElementsPerCell is the number of field elements in a cell. 18 | FieldElementsPerCell = 64 19 | BytesPerFieldElement = 32 20 | // FieldElementsPerExtBlob is the number of field elements needed to represent an extended blob. 21 | FieldElementsPerExtBlob = FieldElementsPerBlob * BlobExpansionFactor 22 | // CellsPerExtBlob is the number of cells in an extended blob. 23 | CellsPerExtBlob = FieldElementsPerExtBlob / FieldElementsPerCell 24 | ) 25 | 26 | func GetEnv(key, defaultValue string) string { 27 | if value, ok := os.LookupEnv(key); ok { 28 | return value 29 | } 30 | return defaultValue 31 | } 32 | 33 | func GetEnvInt(key string, defaultValue int) int { 34 | if value, ok := os.LookupEnv(key); ok { 35 | val, err := strconv.Atoi(value) 36 | if err == nil { 37 | return val 38 | } 39 | } 40 | return defaultValue 41 | } 42 | 43 | func GetEnvFloat64(key string, defaultValue float64) float64 { 44 | if value, ok := os.LookupEnv(key); ok { 45 | val, err := strconv.ParseFloat(value, 64) 46 | if err == nil { 47 | return val 48 | } 49 | } 50 | return defaultValue 51 | } 52 | 53 | // FloatEthTo256Wei converts a float (precision 10) denominated in eth to a U256Str denominated in wei 54 | func FloatEthTo256Wei(val float64) (*types.U256Str, error) { 55 | weiFloat := new(big.Float) 56 | weiFloatLessPrecise := new(big.Float) 57 | 58 | weiFloat.Mul(new(big.Float).SetFloat64(val), big.NewFloat(1e18)) 59 | weiFloatLessPrecise.SetString(weiFloat.String()) 60 | weiInt, _ := weiFloatLessPrecise.Int(nil) 61 | 62 | weiU256 := new(types.U256Str) 63 | err := weiU256.FromBig(weiInt) 64 | return weiU256, err 65 | } 66 | -------------------------------------------------------------------------------- /server/metrics.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/VictoriaMetrics/metrics" 7 | ) 8 | 9 | var winningBidValue = metrics.NewPrometheusHistogram("mev_boost_winning_bid_value") 10 | 11 | const ( 12 | beaconNodeStatusLabel = `mev_boost_beacon_node_status_code_total{http_status_code="%s",endpoint="%s"}` 13 | bidValuesLabel = `mev_boost_bid_values{relay="%s"}` 14 | bidsBelowMinBidLabel = `mev_boost_bids_below_min_bid_total{relay="%s"}` 15 | relayLatencyLabel = `mev_boost_relay_latency{endpoint="%s",relay="%s"}` 16 | relayStatusCodeLabel = `mev_boost_relay_status_code_total{http_status_code="%s",endpoint="%s",relay="%s"}` 17 | relayLastSlotLabel = `mev_boost_relay_last_slot{relay="%s"}` 18 | msIntoSlotLabel = `mev_boost_millisec_into_slot{endpoint="%s"}` 19 | ) 20 | 21 | func IncrementBeaconNodeStatus(status, endpoint string) { 22 | l := fmt.Sprintf(beaconNodeStatusLabel, status, endpoint) 23 | metrics.GetOrCreateCounter(l).Inc() 24 | } 25 | 26 | func RecordBidValue(relay string, value float64) { 27 | l := fmt.Sprintf(bidValuesLabel, relay) 28 | metrics.GetOrCreatePrometheusHistogram(l).Update(value) 29 | } 30 | 31 | func IncrementBidBelowMinBid(relay string) { 32 | l := fmt.Sprintf(bidsBelowMinBidLabel, relay) 33 | metrics.GetOrCreateCounter(l).Inc() 34 | } 35 | 36 | func RecordWinningBidValue(value float64) { 37 | winningBidValue.Update(value) 38 | } 39 | 40 | func RecordRelayLatency(endpoint, relay string, latency float64) { 41 | l := fmt.Sprintf(relayLatencyLabel, endpoint, relay) 42 | metrics.GetOrCreatePrometheusHistogram(l).Update(latency) 43 | } 44 | 45 | func RecordRelayStatusCode(httpStatus, endpoint, relay string) { 46 | l := fmt.Sprintf(relayStatusCodeLabel, httpStatus, endpoint, relay) 47 | metrics.GetOrCreateCounter(l).Inc() 48 | } 49 | 50 | func RecordRelayLastSlot(relay string, slot uint64) { 51 | l := fmt.Sprintf(relayLastSlotLabel, relay) 52 | metrics.GetOrCreateGauge(l, nil).Set(float64(slot)) 53 | } 54 | 55 | func RecordMsIntoSlot(endpoint string, ms float64) { 56 | l := fmt.Sprintf(msIntoSlotLabel, endpoint) 57 | metrics.GetOrCreatePrometheusHistogram(l).Update(ms) 58 | } 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= $(shell git describe --tags --always --dirty="-dev") 2 | DOCKER_REPO := flashbots/mev-boost 3 | 4 | # Set linker flags to: 5 | # -w: disables DWARF debugging information. 6 | GO_BUILD_LDFLAGS += -w 7 | # -s: disables symbol table information. 8 | GO_BUILD_LDFLAGS += -s 9 | # -X: sets the value of the symbol. 10 | GO_BUILD_LDFLAGS += -X 'github.com/flashbots/mev-boost/config.Version=$(VERSION)' 11 | 12 | # Remove all file system paths from the executable. 13 | GO_BUILD_FLAGS += -trimpath 14 | # Add linker flags to build flags. 15 | GO_BUILD_FLAGS += -ldflags "$(GO_BUILD_LDFLAGS)" 16 | 17 | .PHONY: all 18 | all: build 19 | 20 | .PHONY: v 21 | v: 22 | @echo "${VERSION}" 23 | 24 | .PHONY: build 25 | build: 26 | @go version 27 | CGO_ENABLED=0 go build $(GO_BUILD_FLAGS) -o mev-boost ./cmd/mev-boost 28 | 29 | .PHONY: build-testcli 30 | build-testcli: 31 | CGO_ENABLED=0 go build $(GO_BUILD_FLAGS) -o test-cli ./cmd/test-cli 32 | 33 | .PHONY: test 34 | test: 35 | CGO_ENABLED=0 go test ./... 36 | 37 | .PHONY: test-race 38 | test-race: 39 | CGO_ENABLED=1 go test -race ./... 40 | 41 | .PHONY: lint 42 | lint: 43 | gofmt -d -s . 44 | gofumpt -d -extra . 45 | staticcheck ./... 46 | golangci-lint run 47 | 48 | .PHONY: lt 49 | lt: lint test 50 | 51 | .PHONY: fmt 52 | fmt: 53 | gofmt -s -w . 54 | gofumpt -extra -w . 55 | gci write . 56 | go mod tidy 57 | 58 | .PHONY: test-coverage 59 | test-coverage: 60 | CGO_ENABLED=0 go test -v -covermode=atomic -coverprofile=coverage.out ./... 61 | go tool cover -func coverage.out 62 | 63 | .PHONY: cover 64 | cover: 65 | CGO_ENABLED=0 go test -coverprofile=coverage.out ./... 66 | go tool cover -func coverage.out 67 | unlink coverage.out 68 | 69 | .PHONY: cover-html 70 | cover-html: 71 | CGO_ENABLED=0 go test -coverprofile=coverage.out ./... 72 | go tool cover -html=coverage.out 73 | unlink coverage.out 74 | 75 | .PHONY: run-mergemock-integration 76 | run-mergemock-integration: build 77 | ./scripts/run_mergemock_integration.sh 78 | 79 | .PHONY: docker-image 80 | docker-image: 81 | DOCKER_BUILDKIT=1 docker build --platform linux/amd64 --build-arg VERSION=${VERSION} . -t mev-boost 82 | docker tag mev-boost:latest ${DOCKER_REPO}:${VERSION} 83 | docker tag mev-boost:latest ${DOCKER_REPO}:latest 84 | 85 | .PHONY: clean 86 | clean: 87 | git clean -fdx 88 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We appreciate any contributions, responsible disclosures and will make every 4 | effort to acknowledge your contributions. 5 | 6 | ## Supported Versions 7 | 8 | Please see [Releases](https://github.com/flashbots/mev-boost/releases). 9 | Generally it is recommended to use 10 | the [latest release](https://github.com/flashbots/mev-boost/releases/latest). 11 | 12 | ## Reporting a Vulnerability 13 | 14 | To report a vulnerability, please email security@flashbots.net and provide all 15 | the necessary details to reproduce it, such as: 16 | 17 | - Release version 18 | - Operating System 19 | - Consensus / Execution client combination and version 20 | - Network (Mainnet or other testnet) 21 | 22 | Please include the steps to reproduce it using as much detail as possible with 23 | the corresponding logs from `mev-boost` and/or logs from the consensus/execution 24 | client. 25 | 26 | Once we have received your bug report, we will try to reproduce it and provide a 27 | more detailed response. When the reported bug has been successfully reproduced, 28 | the team will work on a fix. 29 | 30 | ## Bug Bounty Program 31 | 32 | To incentive bug reports, there is a bug bounty program. You can receive a 33 | bounty (up to $25k USD) depending on the bug's severity. Severity is based on 34 | impact and likelihood. If a bug is high impact but low likelihood, it will have 35 | a lower severity than a bug with a high impact and high likelihood. 36 | 37 | | Severity | Maximum | Example | 38 | |----------|------------:|------------------------------------------------------------------------| 39 | | Low | $1,000 USD | A bug that causes mev-boost to skip a bid. | 40 | | Medium | $5,000 USD | From a builder message, can cause mev-boost to go offline. | 41 | | High | $12,500 USD | From a builder message, can cause a connected validator to go offline. | 42 | | Critical | $25,000 USD | From a builder message, can remotely access arbitrary files on host. | 43 | 44 | ### Scope 45 | 46 | Bugs that affect the security of the Ethereum protocol in the `mev-boost` 47 | and `mev-boost-relay` repositories are in scope. Bugs in third-party 48 | dependencies are not in scope unless they result in a bug in `mev-boost` with 49 | demonstrable security impact. 50 | -------------------------------------------------------------------------------- /cmd/test-cli/requests.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | var errRequestResponse = errors.New("request response error") 13 | 14 | type jsonrpcMessage struct { 15 | Version string `json:"jsonrpc,omitempty"` 16 | ID json.RawMessage `json:"id,omitempty"` 17 | Method string `json:"method,omitempty"` 18 | Params json.RawMessage `json:"params,omitempty"` 19 | Error *struct { 20 | Code int `json:"code"` 21 | Message string `json:"message"` 22 | Data interface{} `json:"data,omitempty"` 23 | } `json:"error,omitempty"` 24 | Result json.RawMessage `json:"result,omitempty"` 25 | } 26 | 27 | func sendJSONRequest(endpoint string, payload, dst any) error { 28 | payloadBytes, err := json.Marshal(payload) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | body := bytes.NewReader(payloadBytes) 34 | fetchLog := log.WithField("endpoint", endpoint).WithField("method", "POST").WithField("payload", string(payloadBytes)) 35 | fetchLog.Info("sending request") 36 | req, err := http.NewRequest(http.MethodPost, endpoint, body) 37 | if err != nil { 38 | fetchLog.WithError(err).Error("could not prepare request") 39 | return err 40 | } 41 | req.Header.Set("Content-Type", "application/json") 42 | 43 | resp, err := http.DefaultClient.Do(req) 44 | if err != nil { 45 | fetchLog.WithError(err).Error("could not send request") 46 | return err 47 | } 48 | defer func() { 49 | if err = resp.Body.Close(); err != nil { 50 | fetchLog.WithError(err).Error("could not close body") 51 | } 52 | }() 53 | 54 | bodyBytes, err := io.ReadAll(resp.Body) 55 | fetchLog.WithField("bodyBytes", string(bodyBytes)).Info("got response") 56 | if err != nil { 57 | return err 58 | } 59 | 60 | var jsonResp jsonrpcMessage 61 | if err = json.Unmarshal(bodyBytes, &jsonResp); err != nil { 62 | fetchLog.WithField("response", string(bodyBytes)).WithError(err).Error("could not unmarshal response") 63 | return err 64 | } 65 | 66 | if jsonResp.Error != nil { 67 | fetchLog.WithField("code", jsonResp.Error.Code).WithField("err", jsonResp.Error.Message).Error("error response") 68 | return fmt.Errorf("%w: %s", errRequestResponse, jsonResp.Error.Message) 69 | } 70 | 71 | if dst != nil { 72 | if err = json.Unmarshal(jsonResp.Result, dst); err != nil { 73 | fetchLog.WithField("result", string(jsonResp.Result)).WithError(err).Error("could not unmarshal result") 74 | return err 75 | } 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/test-cli/beacon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | "github.com/flashbots/mev-boost/server" 10 | ) 11 | 12 | // Beacon - beacon node interface 13 | type Beacon interface { 14 | onGetHeader() error 15 | getCurrentBeaconBlock() (beaconBlockData, error) 16 | } 17 | 18 | type beaconBlockData struct { 19 | Slot uint64 20 | BlockHash common.Hash 21 | } 22 | 23 | func createBeacon(isMergemock bool, beaconEndpoint, engineEndpoint string) Beacon { 24 | if isMergemock { 25 | return &MergemockBeacon{engineEndpoint} 26 | } 27 | return &BeaconNode{beaconEndpoint} 28 | } 29 | 30 | // BeaconNode - beacon node wrapper 31 | type BeaconNode struct { 32 | beaconEndpoint string 33 | } 34 | 35 | func (b *BeaconNode) onGetHeader() error { return nil } 36 | 37 | func (b *BeaconNode) getCurrentBeaconBlock() (beaconBlockData, error) { 38 | return getCurrentBeaconBlock(b.beaconEndpoint) 39 | } 40 | 41 | type partialSignedBeaconBlock struct { 42 | Data struct { 43 | Message struct { 44 | Slot string `json:"slot"` 45 | Body struct { 46 | ExecutionPayload struct { 47 | BlockHash common.Hash `json:"block_hash"` 48 | } `json:"execution_payload"` 49 | } `json:"body"` 50 | } `json:"message"` 51 | } `json:"data"` 52 | } 53 | 54 | func getCurrentBeaconBlock(beaconEndpoint string) (beaconBlockData, error) { 55 | var blockResp partialSignedBeaconBlock 56 | _, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, beaconEndpoint+"/eth/v2/beacon/blocks/head", "test-cli/beacon", nil, nil, &blockResp) 57 | if err != nil { 58 | return beaconBlockData{}, err 59 | } 60 | 61 | slot, err := strconv.ParseUint(blockResp.Data.Message.Slot, 10, 64) 62 | if err != nil { 63 | return beaconBlockData{}, err 64 | } 65 | 66 | return beaconBlockData{Slot: slot, BlockHash: blockResp.Data.Message.Body.ExecutionPayload.BlockHash}, err 67 | } 68 | 69 | // MergemockBeacon - fake beacon for use with mergemock relay's engine 70 | type MergemockBeacon struct { 71 | mergemockEngineEndpoint string 72 | } 73 | 74 | func (m *MergemockBeacon) onGetHeader() error { 75 | block, err := getLatestEngineBlock(m.mergemockEngineEndpoint) 76 | if err != nil { 77 | return err 78 | } 79 | err = sendForkchoiceUpdate(m.mergemockEngineEndpoint, block) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | return nil 85 | } 86 | 87 | func (m *MergemockBeacon) getCurrentBeaconBlock() (beaconBlockData, error) { 88 | block, err := getLatestEngineBlock(m.mergemockEngineEndpoint) 89 | if err != nil { 90 | log.WithError(err).Info("could not get latest block") 91 | return beaconBlockData{}, err 92 | } 93 | 94 | return beaconBlockData{Slot: 50, BlockHash: block.Hash}, nil 95 | } 96 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | # Example configuration for mev-boost 2 | # This file can be passed to mev-boost using the -config flag: ./mev-boost -config config.yaml 3 | # the configuration supports hot-reloading, changes to this file will be automatically applied without restarts 4 | 5 | # Timeout in milliseconds for get_header requests to relays. 6 | # This value should be less then the timeout on the CL, since 7 | # CL's has their own get_header deadline. 8 | # Default: 950ms 9 | timeout_get_header_ms: 950 10 | 11 | # Threshold in milliseconds that marks when in a slot is considered "too late". 12 | # If a getHeader request arrives after this threshold, mev-boost skips all relay requests 13 | # and forces local block building, to reduce the risk of missed slot. 14 | # Default: 2000ms 15 | late_in_slot_time_ms: 2000 16 | 17 | # Relay Configurations 18 | # Each relay can be configured individually. Relays can also be provided via cli using 19 | # the -relay flag. Cli provided relays will be merged with config file relays. 20 | 21 | relays: 22 | # Relay with timing games enabled 23 | # Timing games allow mev-boost to send multiple requests at strategic intervals 24 | # to capture the latest, most valuable bids right before the proposal deadline. 25 | - url: https://0x9000009807ed12c1f08bf4e81c6da3ba8e3fc3d953898ce0102433094e5f22f21102ec057841fcb81978ed1ea0fa8246@relay.relayer1.net 26 | # Enable timing games strategy for this relay. 27 | # When enabled, mev-boost will delay the first request and send multiple follow up requests. 28 | # Default: false 29 | enable_timing_games: true 30 | 31 | # Target time in milliseconds when the first getHeader request should be sent. 32 | # Only used when enable_timing_games is true. 33 | # Example: 200 means wait until 200ms before sending the first request. 34 | target_first_request_ms: 200 35 | 36 | # Interval in milliseconds between subsequent getHeader requests to the same relay. 37 | # After the first request, mev-boost will keep sending new requests every frequency_get_header_ms 38 | # until the global timeout budget (maxTimeout) is exhausted. 39 | # Only used when enable_timing_games is true. 40 | # Example: 100 means send a new request every 100ms. 41 | frequency_get_header_ms: 100 42 | 43 | # Relay with timing games disabled (standard behavior) 44 | # This relay will receive a single getHeader request immediately when the CL 45 | # calls mev-boost, following the standard mev-boost behavior. 46 | - url: https://0x9000009807ed12c1f08bf4e81c6da3ba8e3fc3d953898ce0102433094e5f22f21102ec057841fcb81978ed1ea0fa8246@relay.relayer2.com 47 | # Disable timing games - use standard single-request behavior 48 | enable_timing_games: false 49 | 50 | # These values are ignored when enable_timing_games is false 51 | target_first_request_ms: 0 52 | frequency_get_header_ms: 0 53 | -------------------------------------------------------------------------------- /server/types/relay_entry.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/attestantio/go-eth2-client/spec/phase0" 8 | "github.com/flashbots/go-boost-utils/utils" 9 | ) 10 | 11 | // RelayEntry represents a relay that mev-boost connects to. 12 | type RelayEntry struct { 13 | PublicKey phase0.BLSPubKey 14 | URL *url.URL 15 | SupportsSSZ bool 16 | } 17 | 18 | type RelayConfig struct { 19 | RelayEntry RelayEntry 20 | EnableTimingGames bool 21 | TargetFirstRequestMs uint64 22 | FrequencyGetHeaderMs uint64 23 | } 24 | 25 | func NewRelayConfig(entry RelayEntry) RelayConfig { 26 | return RelayConfig{ 27 | RelayEntry: entry, 28 | } 29 | } 30 | 31 | func (r *RelayEntry) String() string { 32 | return r.URL.String() 33 | } 34 | 35 | // GetURI returns the full request URI with scheme, host, path and args. 36 | func GetURI(url *url.URL, path string) string { 37 | u2 := *url 38 | u2.User = nil 39 | u2.Path = path 40 | return u2.String() 41 | } 42 | 43 | // GetURI returns the full request URI with scheme, host, path and args for the relay. 44 | func (r *RelayEntry) GetURI(path string) string { 45 | return GetURI(r.URL, path) 46 | } 47 | 48 | // NewRelayEntry creates a new instance based on an input string 49 | // relayURL can be IP@PORT, PUBKEY@IP:PORT, https://IP, etc. 50 | func NewRelayEntry(relayURL string) (entry RelayEntry, err error) { 51 | // Add protocol scheme prefix if it does not exist. 52 | if !strings.HasPrefix(relayURL, "http") { 53 | relayURL = "http://" + relayURL 54 | } 55 | 56 | // Parse the provided relay's URL and save the parsed URL in the RelayEntry. 57 | entry.URL, err = url.ParseRequestURI(relayURL) 58 | if err != nil { 59 | return entry, err 60 | } 61 | 62 | // Extract the relay's public key from the parsed URL. 63 | if entry.URL.User.Username() == "" { 64 | return entry, ErrMissingRelayPubkey 65 | } 66 | 67 | // Convert the username string to a public key. 68 | entry.PublicKey, err = utils.HexToPubkey(entry.URL.User.Username()) 69 | if err != nil { 70 | return entry, err 71 | } 72 | 73 | // Check if the public key is the point-at-infinity. 74 | if entry.PublicKey.IsInfinity() { 75 | return entry, ErrPointAtInfinityPubkey 76 | } 77 | 78 | return entry, nil 79 | } 80 | 81 | // RelayEntriesToStrings returns the string representation of a list of relay entries 82 | func RelayEntriesToStrings(relays []RelayEntry) []string { 83 | ret := make([]string, len(relays)) 84 | for i, entry := range relays { 85 | ret[i] = entry.String() 86 | } 87 | return ret 88 | } 89 | 90 | // Copy returns a deep copy of the relay entry. 91 | func (r *RelayEntry) Copy() (ret RelayEntry) { 92 | ret.PublicKey = r.PublicKey 93 | ret.SupportsSSZ = r.SupportsSSZ 94 | if r.URL != nil { 95 | urlCopy := *r.URL 96 | ret.URL = &urlCopy 97 | } 98 | return 99 | } 100 | -------------------------------------------------------------------------------- /cmd/test-cli/validator.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "os" 7 | "time" 8 | 9 | builderApiV1 "github.com/attestantio/go-builder-client/api/v1" 10 | "github.com/attestantio/go-eth2-client/spec/phase0" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | "github.com/flashbots/go-boost-utils/bls" 13 | "github.com/flashbots/go-boost-utils/ssz" 14 | "github.com/flashbots/go-boost-utils/utils" 15 | ) 16 | 17 | var errInvalidLength = errors.New("invalid length") 18 | 19 | type validatorPrivateData struct { 20 | Sk hexutil.Bytes 21 | Pk hexutil.Bytes 22 | GasLimit hexutil.Uint64 23 | FeeRecipientHex string 24 | } 25 | 26 | func (v *validatorPrivateData) SaveValidator(filePath string) error { 27 | validatorData, err := json.MarshalIndent(v, "", " ") 28 | if err != nil { 29 | return err 30 | } 31 | return os.WriteFile(filePath, validatorData, 0o644) 32 | } 33 | 34 | func mustLoadValidator(filePath string) validatorPrivateData { 35 | fileData, err := os.ReadFile(filePath) 36 | if err != nil { 37 | log.WithField("filePath", filePath).WithError(err).Fatal("could not load validator data") 38 | } 39 | var v validatorPrivateData 40 | err = json.Unmarshal(fileData, &v) 41 | if err != nil { 42 | log.WithField("filePath", filePath).WithField("fileData", fileData).WithError(err).Fatal("could not parse validator data") 43 | } 44 | return v 45 | } 46 | 47 | func newRandomValidator(gasLimit uint64, feeRecipient string) validatorPrivateData { 48 | sk, pk, err := bls.GenerateNewKeypair() 49 | if err != nil { 50 | log.WithError(err).Fatal("unable to generate bls key pair") 51 | } 52 | return validatorPrivateData{bls.SecretKeyToBytes(sk), bls.PublicKeyToBytes(pk), hexutil.Uint64(gasLimit), feeRecipient} 53 | } 54 | 55 | func (v *validatorPrivateData) PrepareRegistrationMessage(builderSigningDomain phase0.Domain) ([]builderApiV1.SignedValidatorRegistration, error) { 56 | pk := phase0.BLSPubKey{} 57 | if len(v.Pk) != len(pk) { 58 | return []builderApiV1.SignedValidatorRegistration{}, errInvalidLength 59 | } 60 | copy(pk[:], v.Pk) 61 | 62 | addr, err := utils.HexToAddress(v.FeeRecipientHex) 63 | if err != nil { 64 | return []builderApiV1.SignedValidatorRegistration{}, err 65 | } 66 | msg := builderApiV1.ValidatorRegistration{ 67 | FeeRecipient: addr, 68 | Timestamp: time.Now(), 69 | Pubkey: pk, 70 | GasLimit: uint64(v.GasLimit), 71 | } 72 | signature, err := v.Sign(&msg, builderSigningDomain) 73 | if err != nil { 74 | return []builderApiV1.SignedValidatorRegistration{}, err 75 | } 76 | 77 | return []builderApiV1.SignedValidatorRegistration{{ 78 | Message: &msg, 79 | Signature: signature, 80 | }}, nil 81 | } 82 | 83 | func (v *validatorPrivateData) Sign(msg ssz.ObjWithHashTreeRoot, domain phase0.Domain) (phase0.BLSSignature, error) { 84 | sk, err := bls.SecretKeyFromBytes(v.Sk) 85 | if err != nil { 86 | return phase0.BLSSignature{}, err 87 | } 88 | return ssz.SignMessage(msg, domain, sk) 89 | } 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Welcome to the MEV-Boost project! We just ask you to be nice when you play with us. 4 | 5 | Please start by reading our [code of conduct](CODE_OF_CONDUCT.md). 6 | 7 | ## Set up 8 | 9 | Install a few dev dependencies for `make lint`: https://github.com/flashbots/mev-boost/blob/develop/.github/workflows/lint.yml#L29-L37 10 | 11 | Look at the [README for instructions to install the dependencies and build `mev-boost`](README.md#installing) 12 | 13 | Alternatively, run mev-boost without build step: 14 | 15 | ```bash 16 | go run . -h 17 | 18 | # Run mev-boost 19 | ./mev-boost -hoodi -relay-check -relay URL-OF-TRUSTED-RELAY 20 | ``` 21 | 22 | Note that you'll need to set the correct genesis fork version (either manually with `-genesis-fork-version` or a helper flag `-mainnet`/`-sepolia`/`-holesky`/`-hoodi`). 23 | 24 | ## Test 25 | 26 | ```bash 27 | make test 28 | make lint 29 | make run-mergemock-integration 30 | ``` 31 | 32 | ### Testing with test-cli 33 | 34 | test-cli is a utility to run through all the proposer requests against mev-boost+relay. See also the [test-cli readme](cmd/test-cli/README.md). 35 | 36 | ### Testing with mergemock 37 | 38 | Mergemock is fully integrated: https://github.com/protolambda/mergemock 39 | 40 | Make sure you've setup and built mergemock first, refer to its [README](https://github.com/protolambda/mergemock/blob/master/README.md) but here's a quick setup guide: 41 | 42 | ```bash 43 | git clone https://github.com/protolambda/mergemock.git 44 | cd mergemock 45 | go build . mergemock 46 | wget https://gist.githubusercontent.com/lightclient/799c727e826483a2804fc5013d0d3e3d/raw/2e8824fa8d9d9b040f351b86b75c66868fb9b115/genesis.json 47 | openssl rand -hex 32 | tr -d "\n" > jwt.hex 48 | ``` 49 | 50 | Then you can run an integration test with mergemock, spawning both a mergemock relay+execution engine and a mergemock consensus client pointing to mev-boost, which in turn points to the mergemock relay: 51 | 52 | ```bash 53 | cd mev-boost 54 | make run-mergemock-integration 55 | ``` 56 | 57 | The path to the mergemock repo is assumed to be `../mergemock`, you can override like so: 58 | 59 | ```bash 60 | make MERGEMOCK_DIR=/PATH-TO-MERGEMOCK-REPO run-mergemock-integration 61 | ``` 62 | 63 | to run mergemock in dev mode: 64 | 65 | ```bash 66 | make MERGEMOCK_BIN='go run .' run-mergemock-integration 67 | ``` 68 | 69 | ## Code style 70 | 71 | Start by making sure that your code is readable, consistent, and pretty. 72 | Follow the [Clean Code](https://flashbots.notion.site/Clean-Code-13016c5c7ca649fba31ae19d797d7304) recommendations. 73 | 74 | ## Send a pull request 75 | 76 | - Your proposed changes should be first described and discussed in an issue. 77 | - Open the branch in a personal fork, not in the team repository. 78 | - Every pull request should be small and represent a single change. If the problem is complicated, split it in multiple issues and pull requests. 79 | - Every pull request should be covered by unit tests. 80 | 81 | We appreciate you, friend <3. 82 | 83 | --- 84 | 85 | For the checklist and guide to releasing a new version, see [RELEASE.md](RELEASE.md). 86 | -------------------------------------------------------------------------------- /server/register_validator.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/flashbots/mev-boost/server/params" 12 | "github.com/flashbots/mev-boost/server/types" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func (m *BoostService) registerValidator(log *logrus.Entry, regBytes []byte, header http.Header) error { 17 | m.relayConfigsLock.RLock() 18 | relayConfigs := m.relayConfigs 19 | m.relayConfigsLock.RUnlock() 20 | 21 | respErrCh := make(chan error, len(relayConfigs)) 22 | 23 | log.WithFields(logrus.Fields{ 24 | "timeout": m.httpClientRegVal.Timeout, 25 | "numRelays": len(relayConfigs), 26 | "regBytes": len(regBytes), 27 | }).Info("calling registerValidator on relays") 28 | 29 | // Forward request to each relay 30 | for _, relayConfig := range relayConfigs { 31 | go func(relay types.RelayEntry) { 32 | // Get the URL for this relay 33 | requestURL := relay.GetURI(params.PathRegisterValidator) 34 | log := log.WithField("url", requestURL) 35 | 36 | // Build the new request 37 | req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, requestURL, bytes.NewReader(regBytes)) 38 | if err != nil { 39 | log.WithError(err).Warn("error creating new request") 40 | respErrCh <- err 41 | return 42 | } 43 | 44 | // Extend the request header with our values 45 | for key, values := range header { 46 | req.Header[key] = values 47 | } 48 | 49 | log.WithFields(logrus.Fields{ 50 | "request": req, 51 | }).Debug("sending the registerValidator request") 52 | 53 | // Send the request 54 | start := time.Now() 55 | resp, err := m.httpClientRegVal.Do(req) 56 | RecordRelayLatency(params.PathRegisterValidator, relay.URL.Hostname(), float64(time.Since(start).Milliseconds())) 57 | if err != nil { 58 | log.WithError(err).Warn("error calling registerValidator on relay") 59 | respErrCh <- err 60 | return 61 | } 62 | resp.Body.Close() 63 | 64 | RecordRelayStatusCode(strconv.Itoa(resp.StatusCode), params.PathRegisterValidator, relay.URL.Hostname()) 65 | // Check if response is successful 66 | if resp.StatusCode == http.StatusOK { 67 | log.Debug("relay accepted registrations") 68 | respErrCh <- nil 69 | } else { 70 | log.WithFields(logrus.Fields{ 71 | "statusCode": resp.StatusCode, 72 | }).Debug("received an error response from relay") 73 | respErrCh <- fmt.Errorf("%w: %d", errHTTPErrorResponse, resp.StatusCode) 74 | } 75 | }(relayConfig.RelayEntry) 76 | } 77 | 78 | // Return OK if any relay responds OK 79 | for range relayConfigs { 80 | respErr := <-respErrCh 81 | if respErr == nil { 82 | // Goroutines are independent, so if there are a lot of configured 83 | // relays and the first one responds OK, this will continue to send 84 | // validator registrations to the other relays. 85 | log.Debug("one or more relays accepted the registrations") 86 | return nil 87 | } 88 | } 89 | 90 | // None of the relays responded OK 91 | log.Debug("no relays accepted the registrations") 92 | return errNoSuccessfulRelayResponse 93 | } 94 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releasing a new version of mev-boost 2 | 3 | This is a guide on how to release a new version of mev-boost. 4 | 5 | The best days to release a new version are Monday to Wednesday. Never release on a Friday. Release preferred with another person present (four eyes principle). 6 | 7 | Process: 8 | 1. Double-check the current build 9 | 2. Create a release candidate (RC) 10 | 3. Test the RC on testnets 11 | 4. Collect signoffs 12 | 5. Create the full release + announcement 13 | 14 | ## Double-check the current build 15 | 16 | First of all, check that the git repository is in the final state, and all the tests and checks are running fine 17 | 18 | Test the current 19 | 20 | ```bash 21 | make lint 22 | make test-race 23 | go mod tidy 24 | git status # should be no changes 25 | 26 | # Start mev-boost with relay check and -relays 27 | go run . -mainnet -relay-check -min-bid 0.12345 -debug -relays https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net,https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com,https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money 28 | 29 | # Start mev-boost with relay check and multiple -relay flags 30 | go run . -mainnet -relay-check -debug -min-bid 0.12345 \ 31 | -relay https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net \ 32 | -relay https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com \ 33 | -relay https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money 34 | 35 | # Call the status endpoint 36 | curl localhost:18550/eth/v1/builder/status 37 | ``` 38 | 39 | ## Make a release 40 | 41 | Let's release `v1.9`: 42 | 43 | 1. Create a GitHub issue about the upcoming release ([example](https://github.com/flashbots/mev-boost/issues/524)) 44 | 1. Tag a release candidate (RC), or alpha version: `v1.9-rc1`, and push the new tag to GitHub. 45 | - The [release CI](https://github.com/flashbots/mev-boost/actions) starts 46 | building the binaries and the Docker image (which is then pushed to [Docker Hub](https://hub.docker.com/r/flashbots/mev-boost/tags)). 47 | - Binaries can be downloaded from the release CI summary website. 48 | 1. Test the RC in testnets. Iterate as needed, create more alpha versions / release candidates as needed. 49 | 2. When tests are complete, create the release: tag, GitHub release, `stable` branch update. 50 | 51 | ```bash 52 | # Create the RC tag 53 | git tag -s v1.9-rc1 54 | 55 | # Push to Github, which kicks off the release CI 56 | git push origin tags/v1.9-rc1 57 | 58 | # When CI is done, the Docker image can be used 59 | docker pull flashbots/mev-boost:v1.9a1 60 | 61 | # Once testing is done, tag the full release 62 | git tag -s v1.9 63 | git tag -s v1.9.0 64 | 65 | # Push to Github, which kicks off the release CI 66 | git push origin tags/v1.9 tags/v1.9.0 67 | 68 | # Finally, update the stable branch 69 | git checkout stable 70 | git merge tags/v1.9 --ff-only 71 | ``` 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flashbots/mev-boost 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/VictoriaMetrics/metrics v1.40.1 7 | github.com/ethereum/go-ethereum v1.15.9 8 | github.com/flashbots/go-boost-utils v1.10.0 9 | github.com/flashbots/go-utils v0.10.0 10 | github.com/fsnotify/fsnotify v1.9.0 11 | github.com/google/uuid v1.6.0 12 | github.com/gorilla/mux v1.8.1 13 | github.com/holiman/uint256 v1.3.2 14 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 15 | github.com/sirupsen/logrus v1.9.3 16 | github.com/spf13/viper v1.21.0 17 | github.com/stretchr/testify v1.11.1 18 | github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 19 | github.com/urfave/cli/v3 v3.2.0 20 | ) 21 | 22 | require ( 23 | github.com/bits-and-blooms/bitset v1.22.0 // indirect 24 | github.com/consensys/bavard v0.1.30 // indirect 25 | github.com/consensys/gnark-crypto v0.17.0 // indirect 26 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 27 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect 28 | github.com/emicklei/dot v1.8.0 // indirect 29 | github.com/ethereum/c-kzg-4844 v1.0.3 // indirect 30 | github.com/ethereum/go-verkle v0.2.2 // indirect 31 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 32 | github.com/goccy/go-yaml v1.17.1 // indirect 33 | github.com/gofrs/flock v0.12.1 // indirect 34 | github.com/mmcloughlin/addchain v0.4.0 // indirect 35 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 36 | github.com/rivo/uniseg v0.4.7 // indirect 37 | github.com/sagikazarmark/locafero v0.11.0 // indirect 38 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 39 | github.com/spf13/afero v1.15.0 // indirect 40 | github.com/spf13/cast v1.10.0 // indirect 41 | github.com/spf13/pflag v1.0.10 // indirect 42 | github.com/subosito/gotenv v1.6.0 // indirect 43 | github.com/supranational/blst v0.3.14 // indirect 44 | github.com/valyala/fastrand v1.1.0 // indirect 45 | github.com/valyala/histogram v1.2.0 // indirect 46 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 47 | go.yaml.in/yaml/v3 v3.0.4 // indirect 48 | golang.org/x/sync v0.16.0 // indirect 49 | golang.org/x/text v0.28.0 // indirect 50 | rsc.io/tmplfunc v0.0.3 // indirect 51 | ) 52 | 53 | require ( 54 | github.com/attestantio/go-builder-client v0.7.2 55 | github.com/attestantio/go-eth2-client v0.27.1 56 | github.com/davecgh/go-spew v1.1.1 // indirect 57 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect 58 | github.com/ferranbt/fastssz v0.1.4 // indirect 59 | github.com/go-ole/go-ole v1.3.0 // indirect 60 | github.com/golang/snappy v1.0.0 // indirect 61 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 62 | github.com/mattn/go-runewidth v0.0.16 // indirect 63 | github.com/minio/sha256-simd v1.0.1 // indirect 64 | github.com/mitchellh/mapstructure v1.5.0 // indirect 65 | github.com/olekukonko/tablewriter v0.0.5 // indirect 66 | github.com/pkg/errors v0.9.1 // indirect 67 | github.com/pmezard/go-difflib v1.0.0 // indirect 68 | github.com/shirou/gopsutil v3.21.11+incompatible // indirect 69 | github.com/tklauser/go-sysconf v0.3.15 // indirect 70 | github.com/tklauser/numcpus v0.10.0 // indirect 71 | go.uber.org/multierr v1.11.0 // indirect 72 | go.uber.org/zap v1.27.0 // indirect 73 | golang.org/x/crypto v0.37.0 // indirect 74 | golang.org/x/sys v0.32.0 // indirect 75 | gopkg.in/yaml.v2 v2.4.0 // indirect 76 | gopkg.in/yaml.v3 v3.0.1 77 | ) 78 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | docker-image: 13 | name: Publish Docker Image 14 | runs-on: warp-ubuntu-latest-x64-16x 15 | 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v2 19 | 20 | - name: Get tag version 21 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 22 | 23 | - name: Print version 24 | run: | 25 | echo $RELEASE_VERSION 26 | echo ${{ env.RELEASE_VERSION }} 27 | 28 | - name: Extract metadata (tags, labels) for Docker images 29 | id: meta 30 | uses: docker/metadata-action@v4 31 | with: 32 | images: flashbots/mev-boost 33 | tags: | 34 | type=sha 35 | type=pep440,pattern={{version}} 36 | type=pep440,pattern={{major}}.{{minor}} 37 | type=raw,value=latest,enable=${{ !contains(env.RELEASE_VERSION, '-') }} 38 | 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v2 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v2 44 | 45 | - name: Login to DockerHub 46 | uses: docker/login-action@v2 47 | with: 48 | username: ${{ secrets.DOCKERHUB_USERNAME }} 49 | password: ${{ secrets.DOCKERHUB_TOKEN }} 50 | 51 | - name: Build and push 52 | uses: docker/build-push-action@v3 53 | with: 54 | context: . 55 | push: true 56 | build-args: | 57 | VERSION=${{ env.RELEASE_VERSION }} 58 | platforms: linux/amd64,linux/arm64 59 | tags: ${{ steps.meta.outputs.tags }} 60 | labels: ${{ steps.meta.outputs.labels }} 61 | 62 | build-all: 63 | name: Build Binaries 64 | runs-on: warp-ubuntu-latest-x64-16x 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@v3 68 | with: 69 | fetch-depth: 0 70 | - name: Fetch all tags 71 | run: git fetch --force --tags 72 | - name: Set up Go 73 | uses: actions/setup-go@v3 74 | with: 75 | go-version: ^1.24 76 | - name: Run GoReleaser 77 | uses: goreleaser/goreleaser-action@v3 78 | with: 79 | distribution: goreleaser 80 | version: latest 81 | args: release --skip=publish --config .goreleaser-build.yaml --clean 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | - name: Upload 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: mev-boost-build 88 | path: | 89 | dist/mev-boost*.tar.gz 90 | dist/mev-boost*.txt 91 | 92 | release: 93 | name: Prepare Github Release 94 | needs: build-all 95 | runs-on: warp-ubuntu-latest-x64-16x 96 | steps: 97 | - name: Checkout 98 | uses: actions/checkout@v3 99 | with: 100 | fetch-depth: 0 101 | - name: Fetch all tags 102 | run: git fetch --force --tags 103 | - name: Set up Go 104 | uses: actions/setup-go@v3 105 | with: 106 | go-version: ^1.24 107 | - name: Make directories 108 | run: | 109 | mkdir -p ./build 110 | - name: Download binaries 111 | uses: actions/download-artifact@v4 112 | with: 113 | name: mev-boost-build 114 | path: ./build 115 | - name: Merge checksum file 116 | run: | 117 | cd ./build 118 | cat ./mev-boost*checksums.txt >> checksums.txt 119 | rm ./mev-boost*checksums.txt 120 | - name: Release 121 | uses: goreleaser/goreleaser-action@v3 122 | with: 123 | args: release --config .goreleaser-release.yaml 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | -------------------------------------------------------------------------------- /docs/timing-games.md: -------------------------------------------------------------------------------- 1 | # Timing Games 2 | 3 | The **Timing Games** feature allows `mev-boost` to optimize block proposal by strategically timing `getHeader` requests to relays. Instead of sending a single request immediately, it can delay the initial request and send multiple follow-up requests to capture the latest, most valuable bids before the proposal deadline. 4 | 5 | ## Configuration 6 | 7 | To enable timing games, you must provide a YAML configuration file using the `-config` flag: 8 | 9 | ```bash 10 | ./mev-boost -config config.yaml 11 | ``` 12 | 13 | To enable hot reloading of the configuration file, add the `-watch-config` flag: 14 | 15 | ```bash 16 | ./mev-boost -config config.yaml -watch-config 17 | ``` 18 | 19 | ## Global Timeouts 20 | 21 | These settings apply to all relays and define the hard boundaries for the `getHeader` operation. 22 | 23 | * **`timeout_get_header_ms`** (optional, default: 950ms) 24 | * It is the maximum timeout in milliseconds for get_header requests to relays. 25 | 26 | * **`late_in_slot_time_ms`** (optional, default: 2000ms) 27 | * It is a safety threshold in milliseconds that marks when in a slot we consider it "too late" to fetch headers from relays. If the request arrives after the threshold, it skips all relay requests and forces local block building. 28 | 29 | ## Per-Relay Configuration 30 | 31 | These parameters are configured individually for each relay in `config.yaml`. 32 | 33 | * **`enable_timing_games`** (optional, default: false) 34 | * Enables the timing games logic for this specific relay. If `false`, standard behavior is used (single request, immediate execution). 35 | 36 | * **`target_first_request_ms`** (`uint64`) 37 | * It is the target time in milliseconds into the slot when the first get_header request should be sent to a relay. It's only used when `enable_timing_games = true` 38 | 39 | * **`frequency_get_header_ms`** (`uint64`) 40 | * The interval in milliseconds between subsequent requests to the same relay. After the first request is sent, `mev-boost` will keep sending new requests every `frequency_get_header_ms` until the global timeout budget is exhausted. 41 | 42 | ## Example Configuration 43 | 44 | See [config.example.yaml](../config.example.yaml) for a fully documented example configuration file. 45 | 46 | ## Visual Representation 47 | 48 | ```mermaid 49 | sequenceDiagram 50 | participant CL as Consensus Client 51 | participant MB as Mev-Boost 52 | participant R as Relay 53 | 54 | Note over CL, R: Slot Start (t=0) 55 | 56 | CL->>MB: getHeader 57 | activate MB 58 | 59 | Note right of MB: Calculate max_timeout
min(timeout_get_header_ms,
late_in_slot - ms_into_slot) 60 | 61 | opt target_first_request_ms - ms_into_slot > 0 62 | Note right of MB: Sleep for target_first_request_ms - ms_into_slot 63 | end 64 | 65 | par Request 1 66 | MB->>R: GET /get_header (Request 1) 67 | R-->>MB: Bid A (Value: 1.0 ETH) 68 | and Request 2 (after Frequency interval) 69 | Note right of MB: Sleep frequency_get_header_ms 70 | MB->>R: GET /get_header (Request 2) 71 | R-->>MB: Bid B (Value: 1.1 ETH) 72 | and Request 3 (after Frequency interval) 73 | Note right of MB: Sleep frequency_get_header_ms 74 | MB->>R: GET /get_header (Request 3) 75 | R-->>MB: Bid C (Value: 1.2 ETH) 76 | end 77 | 78 | Note right of MB: max_timeout Reached or
Replies Finished 79 | 80 | Note right of MB: 1. Select best from Relay:
Bid C (Latest Received) 81 | 82 | Note right of MB: 2. Compare with other relays
(Highest Value Wins) 83 | 84 | MB-->>CL: Return Winning Bid (Bid C) 85 | deactivate MB 86 | ``` 87 | 88 | ## How It Works 89 | 90 | 1. **Calculate budget**: When a `getHeader` request arrives, mev-boost calculates the maximum time budget as `min(timeout_get_header_ms, late_in_slot_time_ms - ms_into_slot)`. 91 | 92 | 2. **Delay first request**: With timing games enabled, If `target_first_request_ms` is set, mev-boost waits until that target time before sending the first request. 93 | 94 | 3. **Delay multiple subsequent requests**: With timing games enabled, if `frequency_get_header_ms` is set, mev-boost sends multiple requests at intervals of `frequency_get_header_ms` until the budget is exhausted. 95 | 96 | 4. **Best Bid Selection**: From all responses received, mev-boost selects the most recently received bid from each relay, then compares across relays to return the highest value bid. 97 | -------------------------------------------------------------------------------- /server/types/relay_entry_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/flashbots/go-boost-utils/types" 8 | "github.com/flashbots/go-boost-utils/utils" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestParseRelaysURLs(t *testing.T) { 13 | // Used to fake a relay's public key. 14 | publicKey, err := utils.HexToPubkey("0x82f6e7cc57a2ce68ec41321bebc55bcb31945fe66a8e67eb8251425fab4c6a38c10c53210aea9796dd0ba0441b46762a") 15 | require.NoError(t, err) 16 | 17 | testCases := []struct { 18 | name string 19 | relayURL string 20 | path string 21 | 22 | expectedErr error 23 | expectedURI string // full URI with scheme, host, path and args 24 | expectedPublicKey string 25 | expectedURL string 26 | }{ 27 | { 28 | name: "Relay URL with protocol scheme", 29 | relayURL: fmt.Sprintf("http://%s@foo.com", publicKey.String()), 30 | expectedURI: "http://foo.com", 31 | expectedPublicKey: publicKey.String(), 32 | expectedURL: fmt.Sprintf("http://%s@foo.com", publicKey.String()), 33 | }, 34 | { 35 | name: "Relay URL without protocol scheme, without public key", 36 | relayURL: "foo.com", 37 | expectedErr: ErrMissingRelayPubkey, 38 | }, 39 | { 40 | name: "Relay URL without protocol scheme and with public key", 41 | relayURL: publicKey.String() + "@foo.com", 42 | expectedURI: "http://foo.com", 43 | expectedPublicKey: publicKey.String(), 44 | expectedURL: "http://" + publicKey.String() + "@foo.com", 45 | }, 46 | { 47 | name: "Relay URL with public key host and port", 48 | relayURL: publicKey.String() + "@foo.com:9999", 49 | expectedURI: "http://foo.com:9999", 50 | expectedPublicKey: publicKey.String(), 51 | expectedURL: "http://" + publicKey.String() + "@foo.com:9999", 52 | }, 53 | { 54 | name: "Relay URL with IP and port", 55 | relayURL: publicKey.String() + "@12.345.678:9999", 56 | expectedURI: "http://12.345.678:9999", 57 | expectedPublicKey: publicKey.String(), 58 | expectedURL: "http://" + publicKey.String() + "@12.345.678:9999", 59 | }, 60 | { 61 | name: "Relay URL with https IP and port", 62 | relayURL: "https://" + publicKey.String() + "@12.345.678:9999", 63 | expectedURI: "https://12.345.678:9999", 64 | expectedPublicKey: publicKey.String(), 65 | expectedURL: "https://" + publicKey.String() + "@12.345.678:9999", 66 | }, 67 | { 68 | name: "Relay URL with an invalid public key (wrong length)", 69 | relayURL: "http://0x123456@foo.com", 70 | expectedErr: types.ErrLength, 71 | }, 72 | { 73 | name: "Relay URL with an invalid public key (not on the curve)", 74 | relayURL: "http://0xac6e78dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@foo.com", 75 | expectedErr: utils.ErrInvalidPubkey, 76 | }, 77 | { 78 | name: "Relay URL with an invalid public key (point-at-infinity)", 79 | relayURL: "http://0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000@foo.com", 80 | expectedErr: ErrPointAtInfinityPubkey, 81 | }, 82 | { 83 | name: "Relay URL with an invalid public key (all zero)", 84 | relayURL: "http://0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000@foo.com", 85 | expectedErr: utils.ErrInvalidPubkey, 86 | }, 87 | { 88 | name: "Relay URL with query arg", 89 | relayURL: fmt.Sprintf("http://%s@foo.com?id=foo&bar=1", publicKey.String()), 90 | expectedURI: "http://foo.com?id=foo&bar=1", 91 | expectedPublicKey: publicKey.String(), 92 | expectedURL: fmt.Sprintf("http://%s@foo.com?id=foo&bar=1", publicKey.String()), 93 | }, 94 | } 95 | 96 | for _, tt := range testCases { 97 | t.Run(tt.name, func(t *testing.T) { 98 | relayEntry, err := NewRelayEntry(tt.relayURL) 99 | 100 | // Check errors. 101 | require.Equal(t, tt.expectedErr, err) 102 | 103 | // Now perform content assertions. 104 | if tt.expectedErr == nil { 105 | require.Equal(t, tt.expectedURI, relayEntry.GetURI(tt.path)) 106 | require.Equal(t, tt.expectedPublicKey, relayEntry.PublicKey.String()) 107 | require.Equal(t, tt.expectedURL, relayEntry.String()) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /testdata/signed-blinded-beacon-block-fulu.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "slot": "252288", 4 | "proposer_index": "4295", 5 | "parent_root": "0x9a8eef2096477e645150fee1a2ce13190382179a0ea843f31173715a1a14e074", 6 | "state_root": "0x4c033e0b4a34c0eb8e0caba126fae3ed303a83254fe39f8daaa673125f4042cc", 7 | "body": { 8 | "randao_reveal": "0xa7a74e03d8ef909abc75b9452d167e15869180fb4b19db79a1136b510495f02d6ae480ee4ad6a2a9e41af866a9a54c681601a3a1dee30f676e93e6c6b3eb3e880c2cb8d32bd730ca9e7def92877c70da09bfc52a531f1be15619c8a3bb38bdf6", 9 | "eth1_data": { 10 | "deposit_root": "0x0cacd599c9cdcee8398b40ef045baf2c137bed4d2b02a465a0414b04015f861d", 11 | "deposit_count": "216773", 12 | "block_hash": "0xd75a680056c50b4e339eda2f91ccd33badc5d59feab830526e342c5ec68d8dce" 13 | }, 14 | "graffiti": "0x6c69676874686f7573652d6e65746865726d696e642d33000000000000000000", 15 | "proposer_slashings": [], 16 | "attester_slashings": [], 17 | "attestations": [ 18 | { 19 | "aggregation_bits": "0xf8fbff9093195acfebcff69cdfef71fbd9eaf19777f7dfb1f9233faf08be7e463a675cefef2d9e03", 20 | "data": { 21 | "slot": "252287", 22 | "index": "0", 23 | "beacon_block_root": "0x9a8eef2096477e645150fee1a2ce13190382179a0ea843f31173715a1a14e074", 24 | "source": { 25 | "epoch": "7682", 26 | "root": "0x83900465836d88fbd48a829ca207db86e32aa7797966df1b0c886128c72a1a0b" 27 | }, 28 | "target": { 29 | "epoch": "7883", 30 | "root": "0x6e20c4503b853781327d750ee818651e5493b04f5ea0f2699725eecb1e0c3ddf" 31 | } 32 | }, 33 | "signature": "0xb8fb8248ce16152eb41f88803445ef64c33da86de7bfd398b12e745a449013f9454c38b42a658291e344e3eb11d1c3ec03d692b9ed299aff0f599ea9145596b5195d11ff49f83a573519616c8b76c9459a1ed5f869e1c5c6bad176adbd3b689c", 34 | "committee_bits": "0x0300000000000000" 35 | } 36 | ], 37 | "deposits": [], 38 | "voluntary_exits": [], 39 | "sync_aggregate": { 40 | "sync_committee_bits": "0xde98bcde844ff76e87b94cfff1cbcc3dfbf93fdf3ee9b994764fafe484f762eb1562e7fa28e96f5d7a887bff689ffb932d5eff467e668d137bc565d37e3fa7fd", 41 | "sync_committee_signature": "0x93a611fb577d17674242e42de06958513013b0cb07d1f284e993ed7d63ac43bbc234e54a886ca9cb979e76eabeeb6ee603556234b7d661bdb7a7ac98813028faf8060e5e9271d4f25de199ce5e2ecc2a6762a49c41bf366b1c4c54bc185c62f9" 42 | }, 43 | "execution_payload_header": { 44 | "parent_hash": "0x98b62322edaa4d91ecc0847fe2f7debc89dec5e808d7e369aa9b535543227f60", 45 | "fee_recipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134", 46 | "state_root": "0x3b6b594d5cbe7ff4c8bdca04f080c57a18fffdaf1c8e700e69b86f7425ed8d73", 47 | "receipts_root": "0x04deb4be6955e1a300123be48007597f67e4229f8ce70f4f10388de6fd3fa267", 48 | "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 49 | "prev_randao": "0x83612b3de54001f74bf36234e373c1fca95dab5e4645bc3faf7108345cf82e34", 50 | "block_number": "220275", 51 | "gas_limit": "30000000", 52 | "gas_used": "84000", 53 | "timestamp": "1732296378", 54 | "extra_data": "0x4e65746865726d696e64", 55 | "base_fee_per_gas": "7", 56 | "block_hash": "0xb65b77e52407ff25f7fcd3f455c991ae67be1bc10cb7ec992a659698cf88870f", 57 | "transactions_root": "0xb1fcd304d8ba402be6e76346395ea7641fbb4c83663b697a0e88ab115d859d3f", 58 | "withdrawals_root": "0x792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535", 59 | "blob_gas_used": "0", 60 | "excess_blob_gas": "0" 61 | }, 62 | "bls_to_execution_changes": [], 63 | "blob_kzg_commitments": [], 64 | "execution_requests": { 65 | "deposits": [], 66 | "withdrawals": [], 67 | "consolidations": [] 68 | } 69 | } 70 | }, 71 | "signature": "0x94cd72a70a0b424f68145115a9a52f6c8557fb40ec8b67c26cbb9b324b72756624a59e8bbe11b78acbbb8e9c606035d10c0dab9a3f4177d7e6954f8ea1863d0b0b00007fb420b4b4cf52e065bda0ad32af0d3a71bd6938180bab6dd1af754d2f" 72 | } 73 | -------------------------------------------------------------------------------- /testdata/signed-blinded-beacon-block-electra.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "slot": "252288", 4 | "proposer_index": "4295", 5 | "parent_root": "0x9a8eef2096477e645150fee1a2ce13190382179a0ea843f31173715a1a14e074", 6 | "state_root": "0x4c033e0b4a34c0eb8e0caba126fae3ed303a83254fe39f8daaa673125f4042cc", 7 | "body": { 8 | "randao_reveal": "0xa7a74e03d8ef909abc75b9452d167e15869180fb4b19db79a1136b510495f02d6ae480ee4ad6a2a9e41af866a9a54c681601a3a1dee30f676e93e6c6b3eb3e880c2cb8d32bd730ca9e7def92877c70da09bfc52a531f1be15619c8a3bb38bdf6", 9 | "eth1_data": { 10 | "deposit_root": "0x0cacd599c9cdcee8398b40ef045baf2c137bed4d2b02a465a0414b04015f861d", 11 | "deposit_count": "216773", 12 | "block_hash": "0xd75a680056c50b4e339eda2f91ccd33badc5d59feab830526e342c5ec68d8dce" 13 | }, 14 | "graffiti": "0x6c69676874686f7573652d6e65746865726d696e642d33000000000000000000", 15 | "proposer_slashings": [], 16 | "attester_slashings": [], 17 | "attestations": [ 18 | { 19 | "aggregation_bits": "0xf8fbff9093195acfebcff69cdfef71fbd9eaf19777f7dfb1f9233faf08be7e463a675cefef2d9e03", 20 | "data": { 21 | "slot": "252287", 22 | "index": "0", 23 | "beacon_block_root": "0x9a8eef2096477e645150fee1a2ce13190382179a0ea843f31173715a1a14e074", 24 | "source": { 25 | "epoch": "7682", 26 | "root": "0x83900465836d88fbd48a829ca207db86e32aa7797966df1b0c886128c72a1a0b" 27 | }, 28 | "target": { 29 | "epoch": "7883", 30 | "root": "0x6e20c4503b853781327d750ee818651e5493b04f5ea0f2699725eecb1e0c3ddf" 31 | } 32 | }, 33 | "signature": "0xb8fb8248ce16152eb41f88803445ef64c33da86de7bfd398b12e745a449013f9454c38b42a658291e344e3eb11d1c3ec03d692b9ed299aff0f599ea9145596b5195d11ff49f83a573519616c8b76c9459a1ed5f869e1c5c6bad176adbd3b689c", 34 | "committee_bits": "0x0300000000000000" 35 | } 36 | ], 37 | "deposits": [], 38 | "voluntary_exits": [], 39 | "sync_aggregate": { 40 | "sync_committee_bits": "0xde98bcde844ff76e87b94cfff1cbcc3dfbf93fdf3ee9b994764fafe484f762eb1562e7fa28e96f5d7a887bff689ffb932d5eff467e668d137bc565d37e3fa7fd", 41 | "sync_committee_signature": "0x93a611fb577d17674242e42de06958513013b0cb07d1f284e993ed7d63ac43bbc234e54a886ca9cb979e76eabeeb6ee603556234b7d661bdb7a7ac98813028faf8060e5e9271d4f25de199ce5e2ecc2a6762a49c41bf366b1c4c54bc185c62f9" 42 | }, 43 | "execution_payload_header": { 44 | "parent_hash": "0x98b62322edaa4d91ecc0847fe2f7debc89dec5e808d7e369aa9b535543227f60", 45 | "fee_recipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134", 46 | "state_root": "0x3b6b594d5cbe7ff4c8bdca04f080c57a18fffdaf1c8e700e69b86f7425ed8d73", 47 | "receipts_root": "0x04deb4be6955e1a300123be48007597f67e4229f8ce70f4f10388de6fd3fa267", 48 | "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 49 | "prev_randao": "0x83612b3de54001f74bf36234e373c1fca95dab5e4645bc3faf7108345cf82e34", 50 | "block_number": "220275", 51 | "gas_limit": "30000000", 52 | "gas_used": "84000", 53 | "timestamp": "1732296378", 54 | "extra_data": "0x4e65746865726d696e64", 55 | "base_fee_per_gas": "7", 56 | "block_hash": "0xb65b77e52407ff25f7fcd3f455c991ae67be1bc10cb7ec992a659698cf88870f", 57 | "transactions_root": "0xb1fcd304d8ba402be6e76346395ea7641fbb4c83663b697a0e88ab115d859d3f", 58 | "withdrawals_root": "0x792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535", 59 | "blob_gas_used": "0", 60 | "excess_blob_gas": "0" 61 | }, 62 | "bls_to_execution_changes": [], 63 | "blob_kzg_commitments": [], 64 | "execution_requests": { 65 | "deposits": [], 66 | "withdrawals": [], 67 | "consolidations": [] 68 | } 69 | } 70 | }, 71 | "signature": "0x94cd72a70a0b424f68145115a9a52f6c8557fb40ec8b67c26cbb9b324b72756624a59e8bbe11b78acbbb8e9c606035d10c0dab9a3f4177d7e6954f8ea1863d0b0b00007fb420b4b4cf52e065bda0ad32af0d3a71bd6938180bab6dd1af754d2f" 72 | } 73 | -------------------------------------------------------------------------------- /cmd/test-cli/README.md: -------------------------------------------------------------------------------- 1 | # test-cli 2 | 3 | test-cli is a utility to run through all the proposer requests against mev-boost+relay. 4 | 5 | ## Build 6 | 7 | ``` 8 | make build-testcli 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | ./test-cli [generate|register|getHeader|getPayload] [-help] 15 | 16 | generate [-vd-file] [-fee-recipient] 17 | register [-vd-file] [-mev-boost] [-genesis-fork-version] [-genesis-validators-root] 18 | getHeader [-vd-file] [-mev-boost] [-bn] [-en] [-mm] [-bellatrix-fork-version] [-genesis-validators-root] 19 | getPayload [-vd-file] [-mev-boost] [-bn] [-en] [-mm] [-bellatrix-fork-version] [-genesis-validators-root] 20 | 21 | Env & defaults: 22 | [generate] 23 | -gas-limit = 30000000 24 | -fee-recipient VALIDATOR_FEE_RECIPIENT = 0x0000000000000000000000000000000000000000 25 | 26 | [register] 27 | -vd-file VALIDATOR_DATA_FILE = ./validator_data.json 28 | -mev-boost MEV_BOOST_ENDPOINT = http://127.0.0.1:18550 29 | -genesis-fork-version GENESIS_FORK_VERSION = "0x00000000" (mainnet) - network fork version 30 | "0x00001020" (goerli) 31 | "0x90000069" (sepolia) 32 | 33 | [getHeader] 34 | -vd-file VALIDATOR_DATA_FILE = ./validator_data.json 35 | -mev-boost MEV_BOOST_ENDPOINT = http://127.0.0.1:18550 36 | -bn BEACON_ENDPOINT = http://localhost:5052 37 | -en ENGINE_ENDPOINT = http://localhost:8545 - only used if -mm is passed or pre-bellatrix 38 | -mm - mergemock mode, use mergemock defaults, only call execution and use fake slot in getHeader 39 | 40 | -genesis-fork-version GENESIS_FORK_VERSION = "0x00000000" (mainnet) - network fork version 41 | "0x00001020" (goerli) 42 | "0x90000069" (sepolia) 43 | 44 | [getPayload] 45 | -vd-file VALIDATOR_DATA_FILE = ./validator_data.json 46 | -mev-boost MEV_BOOST_ENDPOINT = http://127.0.0.1:18550 47 | -bn BEACON_ENDPOINT = http://localhost:5052 48 | -en ENGINE_ENDPOINT = http://localhost:8545 - only used if -mm is passed or pre-bellatrix 49 | -mm - mergemock mode, use mergemock defaults, only call execution and use fake slot in getHeader 50 | 51 | -genesis-validators-root GENESIS_VALIDATORS_ROOT = "0x0000000000000000000000000000000000000000000000000000000000000000" (mainnet) - network genesis validators root 52 | "0x043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb" (goerli) 53 | "0xd8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078" (sepolia) 54 | 55 | -genesis-fork-version GENESIS_FORK_VERSION = "0x00000000" (mainnet) - network fork version 56 | "0x00001020" (goerli) 57 | "0x90000069" (sepolia) 58 | 59 | -bellatrix-fork-version BELLATRIX_FORK_VERSION = "0x02000000" (mainnet) - network bellatrix fork version 60 | "0x02001020" (goerli) 61 | "0x90000071" (sepolia) 62 | ``` 63 | 64 | ### Run mev-boost 65 | 66 | ``` 67 | ./mev-boost -hoodi -relay-check -relay URL-OF-TRUSTED-RELAY 68 | ``` 69 | 70 | ### Run beacon node 71 | 72 | `test-cli` needs beacon node to know current block hash (getHeader) and slot (getPayload). 73 | If running against mergemock, pass `-mm` and disregard `-bn`. 74 | 75 | ### Pre-bellatrix run execution layer 76 | 77 | Beacon node does not reply with block hash, it has to be taken from the execution. 78 | If running against mergemock, `test-cli` will take the block hash from execution and fake the slot. 79 | 80 | ### Generate validator data 81 | 82 | ``` 83 | ./test-cli generate [-gas-limit GAS_LIMIT] [-fee-recipient FEE_RECIPIENT] 84 | ``` 85 | 86 | If you wish you can substitute the randomly generated validator data in the validator data file. 87 | 88 | ### Register the validator 89 | 90 | ``` 91 | ./test-cli register -genesis-fork-version [-mev-boost] 92 | ``` 93 | 94 | ### Test getHeader 95 | 96 | ``` 97 | ./test-cli getHeader -genesis-fork-version [-mev-boost] [-bn beacon_endpoint] [-en execution_endpoint] [-mm] 98 | ``` 99 | 100 | The call will return relay's current header. 101 | 102 | ### Test getPayload 103 | 104 | ``` 105 | ./test-cli getPayload [-mev-boost] [-bn beacon_endpoint] [-en execution_endpoint] [-mm] 106 | ``` 107 | 108 | The call will return relay's best execution payload. 109 | Signature checks on the test relay are disabled to allow ad-hoc testing. 110 | -------------------------------------------------------------------------------- /docs/geth-pos-privnet.md: -------------------------------------------------------------------------------- 1 | # Running PoS private testnet with geth 2 | 3 | It’s surprisingly complex to set up, and there are little up-to-date resources. This guide may be incomplete, but should cover most of it. 4 | If you have any improvements, please create a PR 🙏 5 | 6 | Resources used: [https://medium.com/@pradeep_thomas/how-to-setup-your-own-private-ethereum-network-f80bc6aea088](https://medium.com/@pradeep_thomas/how-to-setup-your-own-private-ethereum-network-f80bc6aea088), https://github.com/skylenet/ethereum-genesis-generator, [https://notes.ethereum.org/@parithosh/H1MSKgm3F](https://notes.ethereum.org/@parithosh/H1MSKgm3F), [https://dev.to/q9/how-to-run-your-own-beacon-chain-e70](https://dev.to/q9/how-to-run-your-own-beacon-chain-e70), [https://lighthouse-book.sigmaprime.io/setup.html](https://lighthouse-book.sigmaprime.io/setup.html) ([https://github.com/sigp/lighthouse/tree/unstable/scripts/local_testnet](https://github.com/sigp/lighthouse/tree/unstable/scripts/local_testnet)) 7 | 8 | Special thanks to Mateusz ([@mmrosum](https://twitter.com/mmrosum)) who put the initial version of this document together. 9 | 10 | ### No tl;dr, should take around 30 minutes to get it right 11 | 12 | ### Prepare PoW chain 13 | you will also need to install `go-ethereum` to run the PoW chain. 14 | 1. Clone https://github.com/skylenet/ethereum-genesis-generator 15 | 2. `cp -r config-example data` 16 | 3. Modify data/el/genesis-config.yaml: 17 | 1. Set chain id to `4242` 18 | 2. Remove clique altogether or set `enabled: false` 19 | 4. Run `docker run -it -u $UID -v $PWD/data:/data -p 127.0.0.1:8000:8000 skylenet/ethereum-genesis-generator:latest el` 20 | 5. Check the genesis file in data/el/geth.json - verify clique is not present. it is probably generated anyways - delete the corresponding JSON entry. 21 | 6. Generate a valid account using your favorite tool, take note of the address. here are 3 options: 22 | 1. `ethkey generate` + geth’s —nodekey 23 | 2. geth console + eth.newAccount 24 | 3. create a new address in something like metamask, and copy the private key. inside a geth console session (`geth --datadir ~/.ethereum/local-testnet/testnet/geth-node-1 console`), run `web3.personal.importRawKey("","")` 25 | 7. Initialize geth from the json: `geth init --datadir ~/.ethereum/local-testnet/testnet/geth-node-1 ~/path/to/ethereum-genesis-generator/data/el/geth.json` 26 | 8. Check the node starts to mine and kill it quickly 27 | You only have 100 blocks until fork is enabled and 400 blocks until node stops mining 28 | `geth --datadir ~/.ethereum/local-testnet/testnet/geth-node-1 --networkid 4242 --http --http.port 8545 --http.api personal,eth,net,web3,engine,debug --discovery.dns "" --port 30303 --mine --miner.etherbase=
--miner.threads 1 --miner.gaslimit 1000000000 --authrpc.jwtsecret ~/.ethereum/local-testnet/testnet/geth-node-1/geth/jwtsecret --authrpc.addr localhost --authrpc.port 8551 --authrpc.vhosts localhost --unlock "
" --password <(echo "") --allow-insecure-unlock > ~/.ethereum/miner.log 2>&1` 29 | where `
` is the public key of the wallet you created in step (6) 30 | 31 | ### Prepare PoS chain 32 | 33 | 1. Clone https://github.com/sigp/lighthouse 34 | 2. Go to `scripts/local_testnet` 35 | 1. Modify vars.env: 36 | 1. Set `ETH1_NETWORK_MNEMONIC`, `DEPOSIT_CONTRACT_ADDRESS`, `GENESIS_FORK_VERSION` to be the same as in PoW’s config (GENESIS_FORK_VERSION is in `ethereum-genesis-generator/data/cl/config.yaml`) 37 | 2. Set `GENESIS_DELAY` to 30 38 | 3. Set `ALTAIR_FORK_EPOCH` to 1 39 | 4. Add `MERGE_FORK_EPOCH=1` 40 | 5. Adjust `SECONDS_PER_SLOT`, `SECONDS_PER_ETH1_BLOCK`, `BN_COUNT` to your preference 41 | 1. There seem to be some issues with multiple beacon nodes using the same EL, if it does not work set `BN_COUNT` to 1 42 | 6. Do not change `VALIDATOR_COUNT`, `GENESIS_VALIDATOR_COUNT` to less than 64 43 | 7. modify VC_ARGS line to `VC_ARGS="--suggested-fee-recipient
"`, where
is the same public key that you registered in the `geth` command #8 above 44 | 2. Modify scripts/local_testnet/beacon_node.sh: 45 | 1. Add merge options to the end of the `exec lighthouse` command at the bottom: `--eth1 --merge --terminal-total-difficulty-override=60000000 --eth1-endpoints http://127.0.0.1:8545/ --execution-endpoints http://127.0.0.1:8551/ --http-allow-sync-stalled --execution-jwt ~/.ethereum/local-testnet/testnet/geth-node-1/geth/jwtsecret` . (don't forget to add `\` between newlines, if any. confirm that the jwtsecret path is the one used by `geth` - you may need to expand the `~`) 46 | 2. Allow all subnets, with the line `SUBSCRIBE_ALL_SUBNETS="--subscribe-all-subnets"` 47 | 3. Modify scripts/local_testnet/setup.sh: 48 | 1. Add `--merge-fork-epoch $MERGE_FORK_EPOCH` 49 | 4. Modify start_local_testnet.sh: 50 | 1. Remove/comment ganache [`https://github.com/sigp/lighthouse/blob/stable/scripts/local_testnet/start_local_testnet.sh#L93`](https://github.com/sigp/lighthouse/blob/stable/scripts/local_testnet/start_local_testnet.sh#L93) 51 | 3. install lighthouse and lcli: 52 | 1. make 53 | 2. make install-lcli 54 | 55 | ### Run and hope for the best 56 | 57 | 1. Run geth, wait until you see blocks in the logs 58 | 2. Run lighthouse’s `start_local_testnet.sh`. If fails make sure to read the log. If it complains about lack of funds check that geth mines blocks and retry. 59 | 3. Monitor the logs, PoS will kick in once `mergeForkBlock` is reached but you should see beacon and validator nodes being active before that (just not finalizing any epochs and not producing blocks and not voting before `mergeForkBlock`) 60 | 4. Once geth reaches `terminal_total_difficulty` it stops mining eth1 blocks (`60000000` ~12 min) and should be used by beacon nodes to create PoS payloads. 61 | -------------------------------------------------------------------------------- /cli/config.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/flashbots/mev-boost/server/types" 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/sirupsen/logrus" 13 | "github.com/spf13/viper" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | var ( 18 | errRelayConfiguredTwice = errors.New("relay is specified in both cli flags and config file") 19 | errNoRelaysSpecified = errors.New("no relays specified, provide via --relays and/or --config") 20 | ) 21 | 22 | type RelayConfigYAML struct { 23 | URL string `yaml:"url"` 24 | EnableTimingGames bool `yaml:"enable_timing_games"` 25 | TargetFirstRequestMs uint64 `yaml:"target_first_request_ms"` 26 | FrequencyGetHeaderMs uint64 `yaml:"frequency_get_header_ms"` 27 | } 28 | 29 | // Config holds all configuration settings from the config file 30 | type Config struct { 31 | TimeoutGetHeaderMs uint64 `yaml:"timeout_get_header_ms"` 32 | LateInSlotTimeMs uint64 `yaml:"late_in_slot_time_ms"` 33 | Relays []RelayConfigYAML `yaml:"relays"` 34 | } 35 | 36 | type ConfigResult struct { 37 | RelayConfigs map[string]types.RelayConfig 38 | TimeoutGetHeaderMs uint64 39 | LateInSlotTimeMs uint64 40 | } 41 | 42 | // ConfigWatcher provides hot reloading of config files 43 | type ConfigWatcher struct { 44 | v *viper.Viper 45 | configPath string 46 | cliRelays []types.RelayEntry 47 | onConfigChange func(*ConfigResult) 48 | log *logrus.Entry 49 | } 50 | 51 | // LoadConfigFile loads configurations from a YAML file 52 | func LoadConfigFile(configPath string) (*ConfigResult, error) { 53 | data, err := os.ReadFile(configPath) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | var config Config 59 | if err := yaml.Unmarshal(data, &config); err != nil { 60 | return nil, err 61 | } 62 | return parseConfig(config) 63 | } 64 | 65 | // NewConfigWatcher creates a new config file watcher 66 | func NewConfigWatcher(configPath string, cliRelays []types.RelayEntry, log *logrus.Entry) (*ConfigWatcher, error) { 67 | v := viper.New() 68 | absPath, err := filepath.Abs(configPath) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | v.SetConfigFile(absPath) 74 | v.SetConfigType("yaml") 75 | 76 | if err := v.ReadInConfig(); err != nil { 77 | return nil, err 78 | } 79 | 80 | return &ConfigWatcher{ 81 | v: v, 82 | configPath: absPath, 83 | cliRelays: cliRelays, 84 | log: log, 85 | }, nil 86 | } 87 | 88 | // Watch starts watching the config file for changes 89 | func (cw *ConfigWatcher) Watch(onConfigChange func(*ConfigResult)) { 90 | cw.onConfigChange = onConfigChange 91 | 92 | cw.v.OnConfigChange(func(_ fsnotify.Event) { 93 | cw.log.Info("config file changed, reloading...") 94 | 95 | // explicitly read the file to get the latest content since viper 96 | // may cache the old value and not read the file immediately. 97 | data, err := os.ReadFile(cw.configPath) 98 | if err != nil { 99 | cw.log.WithError(err).Error("failed to read new config file, keeping old config") 100 | return 101 | } 102 | 103 | var config Config 104 | if err := yaml.Unmarshal(data, &config); err != nil { 105 | cw.log.WithError(err).Error("failed to unmarshal new config, keeping old config") 106 | return 107 | } 108 | newConfig, err := parseConfig(config) 109 | if err != nil { 110 | cw.log.WithError(err).Error("failed to parse new config, keeping old config") 111 | return 112 | } 113 | 114 | cw.log.Infof("successfully loaded new config with %d relays from config file", len(newConfig.RelayConfigs)) 115 | 116 | if cw.onConfigChange != nil { 117 | cw.onConfigChange(newConfig) 118 | } 119 | }) 120 | 121 | cw.v.WatchConfig() 122 | } 123 | 124 | // MergeRelayConfigs merges relays passed via --relay with relays from config file. 125 | // Returns an error if the same relay appears in both places. 126 | // Users should specify each relay in exactly one place either cli or config file. 127 | func MergeRelayConfigs(relays []types.RelayEntry, configMap map[string]types.RelayConfig) ([]types.RelayConfig, error) { 128 | configs := make([]types.RelayConfig, 0) 129 | 130 | for _, entry := range relays { 131 | urlStr := entry.String() 132 | if _, exists := configMap[urlStr]; exists { 133 | return nil, fmt.Errorf("%w: %s", errRelayConfiguredTwice, urlStr) 134 | } 135 | configs = append(configs, types.NewRelayConfig(entry)) 136 | } 137 | 138 | for _, config := range configMap { 139 | configs = append(configs, config) 140 | } 141 | 142 | if len(configs) == 0 { 143 | return nil, errNoRelaysSpecified 144 | } 145 | 146 | return configs, nil 147 | } 148 | 149 | func parseConfig(config Config) (*ConfigResult, error) { 150 | timeoutGetHeaderMs := config.TimeoutGetHeaderMs 151 | if timeoutGetHeaderMs == 0 { 152 | timeoutGetHeaderMs = 950 153 | } 154 | 155 | lateInSlotTimeMs := config.LateInSlotTimeMs 156 | if lateInSlotTimeMs == 0 { 157 | lateInSlotTimeMs = 2000 158 | } 159 | 160 | configMap := make(map[string]types.RelayConfig) 161 | for _, relay := range config.Relays { 162 | relayEntry, err := types.NewRelayEntry(strings.TrimSpace(relay.URL)) 163 | if err != nil { 164 | return nil, err 165 | } 166 | relayConfig := types.RelayConfig{ 167 | RelayEntry: relayEntry, 168 | EnableTimingGames: relay.EnableTimingGames, 169 | TargetFirstRequestMs: relay.TargetFirstRequestMs, 170 | FrequencyGetHeaderMs: relay.FrequencyGetHeaderMs, 171 | } 172 | configMap[relayEntry.String()] = relayConfig 173 | } 174 | 175 | return &ConfigResult{ 176 | RelayConfigs: configMap, 177 | TimeoutGetHeaderMs: timeoutGetHeaderMs, 178 | LateInSlotTimeMs: lateInSlotTimeMs, 179 | }, nil 180 | } 181 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement writing an 63 | email to leo@flashbots.net or contacting elopio#8526 in 64 | [Discord](https://discord.com/invite/7hvTycdNcK). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /server/utils_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "math/big" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | builderApi "github.com/attestantio/go-builder-client/api" 13 | builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" 14 | "github.com/attestantio/go-eth2-client/spec" 15 | "github.com/attestantio/go-eth2-client/spec/deneb" 16 | "github.com/attestantio/go-eth2-client/spec/phase0" 17 | "github.com/flashbots/mev-boost/config" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestMakePostRequest(t *testing.T) { 22 | // Test errors 23 | var x chan bool 24 | code, err := SendHTTPRequest(t.Context(), *http.DefaultClient, http.MethodGet, "", "test", nil, x, nil) 25 | require.Error(t, err) 26 | require.Equal(t, 0, code) 27 | } 28 | 29 | func TestDecodeJSON(t *testing.T) { 30 | // test disallows unknown fields 31 | var x struct { 32 | A int `json:"a"` 33 | B int `json:"b"` 34 | } 35 | payload := bytes.NewReader([]byte(`{"a":1,"b":2,"c":3}`)) 36 | err := DecodeJSON(payload, &x) 37 | require.Error(t, err) 38 | require.Equal(t, "json: unknown field \"c\"", err.Error()) 39 | } 40 | 41 | func TestSendHTTPRequestUserAgent(t *testing.T) { 42 | done := make(chan bool, 1) 43 | 44 | // Test with custom UA 45 | customUA := "test-user-agent" 46 | expectedUA := fmt.Sprintf("mev-boost/%s %s", config.Version, customUA) 47 | ts := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { 48 | require.Equal(t, expectedUA, r.Header.Get("User-Agent")) //nolint:testifylint // if we fail here the test has failed 49 | done <- true 50 | })) 51 | code, err := SendHTTPRequest(t.Context(), *http.DefaultClient, http.MethodGet, ts.URL, UserAgent(customUA), nil, nil, nil) 52 | ts.Close() 53 | require.NoError(t, err) 54 | require.Equal(t, 200, code) 55 | <-done 56 | 57 | // Test without custom UA 58 | expectedUA = fmt.Sprintf("mev-boost/%s", config.Version) 59 | ts = httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { 60 | require.Equal(t, expectedUA, r.Header.Get("User-Agent")) //nolint:testifylint // if we fail here the test has failed 61 | done <- true 62 | })) 63 | code, err = SendHTTPRequest(t.Context(), *http.DefaultClient, http.MethodGet, ts.URL, "", nil, nil, nil) 64 | ts.Close() 65 | require.NoError(t, err) 66 | require.Equal(t, 200, code) 67 | <-done 68 | } 69 | 70 | func TestSendHTTPRequestGzip(t *testing.T) { 71 | // Test with gzip response 72 | var buf bytes.Buffer 73 | zw := gzip.NewWriter(&buf) 74 | _, err := zw.Write([]byte(`{ "msg": "test-message" }`)) 75 | require.NoError(t, err) 76 | require.NoError(t, zw.Close()) 77 | 78 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 79 | require.Equal(t, "gzip", r.Header.Get("Accept-Encoding")) //nolint:testifylint // if this fails the test is invalid 80 | w.Header().Set("Content-Encoding", "gzip") 81 | _, _ = w.Write(buf.Bytes()) 82 | })) 83 | resp := struct{ Msg string }{} 84 | code, err := SendHTTPRequest(t.Context(), *http.DefaultClient, http.MethodGet, ts.URL, "", nil, nil, &resp) 85 | ts.Close() 86 | require.NoError(t, err) 87 | require.Equal(t, 200, code) 88 | require.Equal(t, "test-message", resp.Msg) 89 | } 90 | 91 | func TestWeiBigIntToEthBigFloat(t *testing.T) { 92 | // test with valid input 93 | i := big.NewInt(1) 94 | f := weiBigIntToEthBigFloat(i) 95 | require.Equal(t, "0.000000000000000001", f.Text('f', 18)) 96 | 97 | // test with nil, which results on invalid big.Int input 98 | f = weiBigIntToEthBigFloat(nil) 99 | require.Equal(t, "0.000000000000000000", f.Text('f', 18)) 100 | } 101 | 102 | func TestGetPayloadResponseIsEmpty(t *testing.T) { 103 | testCases := []struct { 104 | name string 105 | payload *builderApi.VersionedSubmitBlindedBlockResponse 106 | expected bool 107 | }{ 108 | { 109 | name: "Non-empty deneb payload response", 110 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 111 | Version: spec.DataVersionDeneb, 112 | Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ 113 | ExecutionPayload: &deneb.ExecutionPayload{ 114 | BlockHash: phase0.Hash32{0x1}, 115 | }, 116 | BlobsBundle: &builderApiDeneb.BlobsBundle{ 117 | Blobs: make([]deneb.Blob, 0), 118 | Commitments: make([]deneb.KZGCommitment, 0), 119 | Proofs: make([]deneb.KZGProof, 0), 120 | }, 121 | }, 122 | }, 123 | expected: false, 124 | }, 125 | { 126 | name: "Empty deneb payload response", 127 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 128 | Version: spec.DataVersionDeneb, 129 | }, 130 | expected: true, 131 | }, 132 | { 133 | name: "Empty deneb execution payload", 134 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 135 | Version: spec.DataVersionDeneb, 136 | Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ 137 | BlobsBundle: &builderApiDeneb.BlobsBundle{ 138 | Blobs: make([]deneb.Blob, 0), 139 | Commitments: make([]deneb.KZGCommitment, 0), 140 | Proofs: make([]deneb.KZGProof, 0), 141 | }, 142 | }, 143 | }, 144 | expected: true, 145 | }, 146 | { 147 | name: "Empty deneb blobs bundle", 148 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 149 | Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ 150 | ExecutionPayload: &deneb.ExecutionPayload{ 151 | BlockHash: phase0.Hash32{0x1}, 152 | }, 153 | }, 154 | }, 155 | expected: true, 156 | }, 157 | { 158 | name: "Nil block hash for deneb payload response", 159 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 160 | Deneb: &builderApiDeneb.ExecutionPayloadAndBlobsBundle{ 161 | ExecutionPayload: &deneb.ExecutionPayload{ 162 | BlockHash: nilHash, 163 | }, 164 | }, 165 | }, 166 | expected: true, 167 | }, 168 | { 169 | name: "Unsupported payload version", 170 | payload: &builderApi.VersionedSubmitBlindedBlockResponse{ 171 | Version: spec.DataVersionAltair, 172 | }, 173 | expected: true, 174 | }, 175 | } 176 | 177 | for _, tt := range testCases { 178 | t.Run(tt.name, func(t *testing.T) { 179 | require.Equal(t, tt.expected, getPayloadResponseIsEmpty(tt.payload)) 180 | }) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /cli/flags.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "github.com/urfave/cli/v3" 4 | 5 | const ( 6 | LoggingCategory = "LOGGING AND DEBUGGING" 7 | GenesisCategory = "GENESIS" 8 | RelayCategory = "RELAYS" 9 | GeneralCategory = "GENERAL" 10 | Metrics = "METRICS" 11 | ) 12 | 13 | var flags = []cli.Flag{ 14 | // general 15 | addrFlag, 16 | versionFlag, 17 | // logging 18 | jsonFlag, 19 | colorFlag, 20 | debugFlag, 21 | logLevelFlag, 22 | logServiceFlag, 23 | logNoVersionFlag, 24 | // genesis 25 | customGenesisForkFlag, 26 | customGenesisTimeFlag, 27 | mainnetFlag, 28 | sepoliaFlag, 29 | holeskyFlag, 30 | hoodiFlag, 31 | // relay 32 | relaysFlag, 33 | relayConfigFlag, 34 | watchConfigFlag, 35 | deprecatedRelayMonitorFlag, 36 | minBidFlag, 37 | relayCheckFlag, 38 | timeoutGetHeaderFlag, 39 | timeoutGetPayloadFlag, 40 | timeoutRegValFlag, 41 | maxRetriesFlag, 42 | 43 | // metrics 44 | metricsFlag, 45 | metricsAddrFlag, 46 | } 47 | 48 | var ( 49 | // General 50 | addrFlag = &cli.StringFlag{ 51 | Name: "addr", 52 | Sources: cli.EnvVars("BOOST_LISTEN_ADDR"), 53 | Value: "localhost:18550", 54 | Usage: "listen-address for mev-boost server", 55 | Category: GeneralCategory, 56 | } 57 | versionFlag = &cli.BoolFlag{ 58 | Name: "version", 59 | Usage: "print version", 60 | Category: GeneralCategory, 61 | } 62 | // Logging and debugging 63 | jsonFlag = &cli.BoolFlag{ 64 | Name: "json", 65 | Sources: cli.EnvVars("LOG_JSON"), 66 | Usage: "log in JSON format instead of text", 67 | Category: LoggingCategory, 68 | } 69 | colorFlag = &cli.BoolFlag{ 70 | Name: "color", 71 | Sources: cli.EnvVars("LOG_COLOR"), 72 | Usage: "enable colored output for text log format", 73 | Category: LoggingCategory, 74 | } 75 | debugFlag = &cli.BoolFlag{ 76 | Name: "debug", 77 | Sources: cli.EnvVars("DEBUG"), 78 | Usage: "shorthand for '--loglevel debug'", 79 | Category: LoggingCategory, 80 | } 81 | logLevelFlag = &cli.StringFlag{ 82 | Name: "loglevel", 83 | Sources: cli.EnvVars("LOG_LEVEL"), 84 | Value: "info", 85 | Usage: "minimum loglevel: trace, debug, info, warn/warning, error, fatal, panic", 86 | Category: LoggingCategory, 87 | } 88 | logServiceFlag = &cli.StringFlag{ 89 | Name: "log-service", 90 | Sources: cli.EnvVars("LOG_SERVICE_TAG"), 91 | Value: "", 92 | Usage: "add a 'service=...' tag to all log messages", 93 | Category: LoggingCategory, 94 | } 95 | logNoVersionFlag = &cli.BoolFlag{ 96 | Name: "log-no-version", 97 | Sources: cli.EnvVars("DISABLE_LOG_VERSION"), 98 | Usage: "disables adding the version to every log entry", 99 | Category: LoggingCategory, 100 | } 101 | // Genesis Flags 102 | customGenesisForkFlag = &cli.StringFlag{ 103 | Name: "genesis-fork-version", 104 | Sources: cli.EnvVars("GENESIS_FORK_VERSION"), 105 | Usage: "use a custom genesis fork version", 106 | Category: GenesisCategory, 107 | } 108 | customGenesisTimeFlag = &cli.UintFlag{ 109 | Name: "genesis-timestamp", 110 | Sources: cli.EnvVars("GENESIS_TIMESTAMP"), 111 | Usage: "use a custom genesis timestamp (unix seconds)", 112 | Category: GenesisCategory, 113 | } 114 | mainnetFlag = &cli.BoolFlag{ 115 | Name: "mainnet", 116 | Sources: cli.EnvVars("MAINNET"), 117 | Usage: "use Mainnet", 118 | Value: true, 119 | Category: GenesisCategory, 120 | } 121 | sepoliaFlag = &cli.BoolFlag{ 122 | Name: "sepolia", 123 | Sources: cli.EnvVars("SEPOLIA"), 124 | Usage: "use Sepolia", 125 | Category: GenesisCategory, 126 | } 127 | holeskyFlag = &cli.BoolFlag{ 128 | Name: "holesky", 129 | Sources: cli.EnvVars("HOLESKY"), 130 | Usage: "use Holesky", 131 | Category: GenesisCategory, 132 | } 133 | hoodiFlag = &cli.BoolFlag{ 134 | Name: "hoodi", 135 | Sources: cli.EnvVars("HOODI"), 136 | Usage: "use Hoodi", 137 | Category: GenesisCategory, 138 | } 139 | // Relay 140 | relaysFlag = &cli.StringSliceFlag{ 141 | Name: "relay", 142 | Aliases: []string{"relays"}, 143 | Sources: cli.EnvVars("RELAYS"), 144 | Usage: "relay urls - single entry or comma-separated list (scheme://pubkey@host)", 145 | Category: RelayCategory, 146 | } 147 | relayConfigFlag = &cli.StringFlag{ 148 | Name: "config", 149 | Sources: cli.EnvVars("CONFIG_FILE"), 150 | Usage: "path to YAML configuration file", 151 | Category: RelayCategory, 152 | } 153 | watchConfigFlag = &cli.BoolFlag{ 154 | Name: "watch-config", 155 | Sources: cli.EnvVars("WATCH_CONFIG"), 156 | Usage: "enable hot reloading of config file (requires --config)", 157 | Category: RelayCategory, 158 | } 159 | deprecatedRelayMonitorFlag = &cli.StringSliceFlag{ 160 | Name: "relay-monitors", 161 | Aliases: []string{"relay-monitor"}, 162 | Usage: "[deprecated]", 163 | Category: RelayCategory, 164 | Hidden: true, 165 | } 166 | minBidFlag = &cli.FloatFlag{ 167 | Name: "min-bid", 168 | Sources: cli.EnvVars("MIN_BID_ETH"), 169 | Usage: "minimum bid to accept from a relay [eth]", 170 | Category: RelayCategory, 171 | } 172 | relayCheckFlag = &cli.BoolFlag{ 173 | Name: "relay-check", 174 | Sources: cli.EnvVars("RELAY_STARTUP_CHECK"), 175 | Usage: "check relay status on startup and on the status API call", 176 | Category: RelayCategory, 177 | } 178 | // mev-boost relay request timeouts (see also https://github.com/flashbots/mev-boost/issues/287) 179 | timeoutGetHeaderFlag = &cli.IntFlag{ 180 | Name: "request-timeout-getheader", 181 | Sources: cli.EnvVars("RELAY_TIMEOUT_MS_GETHEADER"), 182 | Usage: "timeout for getHeader requests to the relay [ms]", 183 | Value: 950, 184 | Category: RelayCategory, 185 | } 186 | timeoutGetPayloadFlag = &cli.IntFlag{ 187 | Name: "request-timeout-getpayload", 188 | Sources: cli.EnvVars("RELAY_TIMEOUT_MS_GETPAYLOAD"), 189 | Usage: "timeout for getPayload requests to the relay [ms]", 190 | Value: 4000, 191 | Category: RelayCategory, 192 | } 193 | timeoutRegValFlag = &cli.IntFlag{ 194 | Name: "request-timeout-regval", 195 | Sources: cli.EnvVars("RELAY_TIMEOUT_MS_REGVAL"), 196 | Usage: "timeout for registerValidator requests [ms]", 197 | Value: 3000, 198 | Category: RelayCategory, 199 | } 200 | maxRetriesFlag = &cli.IntFlag{ 201 | Name: "request-max-retries", 202 | Sources: cli.EnvVars("REQUEST_MAX_RETRIES"), 203 | Usage: "maximum number of retries for a relay get payload request", 204 | Value: 5, 205 | Category: RelayCategory, 206 | } 207 | 208 | // metrics 209 | metricsFlag = &cli.BoolFlag{ 210 | Name: "metrics", 211 | Sources: cli.EnvVars("METRICS_ENABLED"), 212 | Usage: "enables a metrics server", 213 | Category: Metrics, 214 | } 215 | metricsAddrFlag = &cli.StringFlag{ 216 | Name: "metrics-addr", 217 | Sources: cli.EnvVars("METRICS_ADDR"), 218 | Value: "localhost:18551", 219 | Usage: "listening address for the metrics server", 220 | Category: Metrics, 221 | } 222 | ) 223 | -------------------------------------------------------------------------------- /docs/logo-horizontal-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /server/mock/mock_types_test.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "testing" 5 | 6 | builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" 7 | "github.com/attestantio/go-eth2-client/spec/bellatrix" 8 | "github.com/attestantio/go-eth2-client/spec/deneb" 9 | "github.com/attestantio/go-eth2-client/spec/phase0" 10 | "github.com/ethereum/go-ethereum/common" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | "github.com/flashbots/go-boost-utils/bls" 13 | "github.com/flashbots/go-boost-utils/utils" 14 | "github.com/holiman/uint256" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestHexToBytes(t *testing.T) { 19 | testCases := []struct { 20 | name string 21 | hex string 22 | 23 | expectedPanic bool 24 | expectedBytes []byte 25 | }{ 26 | { 27 | name: "Should panic because of invalid hexadecimal input", 28 | hex: "foo", 29 | expectedPanic: true, 30 | expectedBytes: nil, 31 | }, 32 | { 33 | name: "Should not panic and convert hexadecimal input to byte array", 34 | hex: "0x0102", 35 | expectedPanic: false, 36 | expectedBytes: []byte{0x01, 0x02}, 37 | }, 38 | } 39 | 40 | for _, tt := range testCases { 41 | t.Run(tt.name, func(t *testing.T) { 42 | if tt.expectedPanic { 43 | require.Panics(t, func() { 44 | HexToBytes(tt.hex) 45 | }) 46 | } else { 47 | require.NotPanics(t, func() { 48 | actualBytes := HexToBytes(tt.hex) 49 | require.Equal(t, tt.expectedBytes, actualBytes) 50 | }) 51 | } 52 | }) 53 | } 54 | } 55 | 56 | // Providing foo works, definitely a weird behavior. 57 | func TestHexToHash(t *testing.T) { 58 | testCases := []struct { 59 | name string 60 | hex string 61 | 62 | expectedPanic bool 63 | expectedHash *phase0.Hash32 64 | }{ 65 | { 66 | name: "Should panic because of empty hexadecimal input", 67 | hex: "", 68 | expectedPanic: true, 69 | expectedHash: nil, 70 | }, 71 | { 72 | name: "Should panic because of invalid hexadecimal input", 73 | hex: "foo", 74 | expectedPanic: true, 75 | expectedHash: nil, 76 | }, 77 | { 78 | name: "Should not panic and convert hexadecimal input to hash", 79 | hex: common.Hash{0x01}.String(), 80 | expectedPanic: false, 81 | expectedHash: &phase0.Hash32{0x01}, 82 | }, 83 | } 84 | 85 | for _, tt := range testCases { 86 | t.Run(tt.name, func(t *testing.T) { 87 | if tt.expectedPanic { 88 | require.Panics(t, func() { 89 | HexToHash(tt.hex) 90 | }) 91 | } else { 92 | require.NotPanics(t, func() { 93 | actualHash := HexToHash(tt.hex) 94 | require.Equal(t, *tt.expectedHash, actualHash) 95 | }) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | // Providing foo works here too, definitely a weird behavior. 102 | func TestHexToAddress(t *testing.T) { 103 | testCases := []struct { 104 | name string 105 | hex string 106 | 107 | expectedPanic bool 108 | expectedAddress *bellatrix.ExecutionAddress 109 | }{ 110 | { 111 | name: "Should panic because of empty hexadecimal input", 112 | hex: "", 113 | expectedPanic: true, 114 | expectedAddress: nil, 115 | }, 116 | { 117 | name: "Should panic because of invalid hexadecimal input", 118 | hex: "foo", 119 | expectedPanic: true, 120 | expectedAddress: nil, 121 | }, 122 | { 123 | name: "Should not panic and convert hexadecimal input to address", 124 | hex: common.Address{0x01}.String(), 125 | expectedPanic: false, 126 | expectedAddress: &bellatrix.ExecutionAddress{0x01}, 127 | }, 128 | } 129 | 130 | for _, tt := range testCases { 131 | t.Run(tt.name, func(t *testing.T) { 132 | if tt.expectedPanic { 133 | require.Panics(t, func() { 134 | HexToAddress(tt.hex) 135 | }) 136 | } else { 137 | require.NotPanics(t, func() { 138 | actualAddress := HexToAddress(tt.hex) 139 | require.Equal(t, *tt.expectedAddress, actualAddress) 140 | }) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | // Same as for TestHexToHash and TestHexToAddress. 147 | func TestHexToPublicKey(t *testing.T) { 148 | publicKey := phase0.BLSPubKey{ 149 | 0x82, 0xf6, 0xe7, 0xcc, 0x57, 0xa2, 0xce, 0x68, 150 | 0xec, 0x41, 0x32, 0x1b, 0xeb, 0xc5, 0x5b, 0xcb, 151 | 0x31, 0x94, 0x5f, 0xe6, 0x6a, 0x8e, 0x67, 0xeb, 152 | 0x82, 0x51, 0x42, 0x5f, 0xab, 0x4c, 0x6a, 0x38, 153 | 0xc1, 0x0c, 0x53, 0x21, 0x0a, 0xea, 0x97, 0x96, 154 | 0xdd, 0x0b, 0xa0, 0x44, 0x1b, 0x46, 0x76, 0x2a, 155 | } 156 | 157 | testCases := []struct { 158 | name string 159 | hex string 160 | 161 | expectedPanic bool 162 | expectedPublicKey *phase0.BLSPubKey 163 | }{ 164 | { 165 | name: "Should panic because of empty hexadecimal input", 166 | hex: "", 167 | expectedPanic: true, 168 | expectedPublicKey: nil, 169 | }, 170 | { 171 | name: "Should panic because of invalid hexadecimal input", 172 | hex: "foo", 173 | expectedPanic: true, 174 | expectedPublicKey: nil, 175 | }, 176 | { 177 | name: "Should not panic and convert hexadecimal input to public key", 178 | hex: publicKey.String(), 179 | expectedPanic: false, 180 | expectedPublicKey: &publicKey, 181 | }, 182 | } 183 | 184 | for _, tt := range testCases { 185 | t.Run(tt.name, func(t *testing.T) { 186 | if tt.expectedPanic { 187 | require.Panics(t, func() { 188 | HexToPubkey(tt.hex) 189 | }) 190 | } else { 191 | require.NotPanics(t, func() { 192 | actualPublicKey := HexToPubkey(tt.hex) 193 | require.Equal(t, *tt.expectedPublicKey, actualPublicKey) 194 | }) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | // Same as for TestHexToHash, TestHexToAddress and TestHexToPublicKey. 201 | func TestHexToSignature(t *testing.T) { 202 | // Sign a message for further testing. 203 | privateKey, blsPublicKey, err := bls.GenerateNewKeypair() 204 | require.NoError(t, err) 205 | 206 | publicKey := hexutil.Encode(bls.PublicKeyToBytes(blsPublicKey)) 207 | 208 | message := &builderApiDeneb.BuilderBid{ 209 | Header: &deneb.ExecutionPayloadHeader{ 210 | BlockHash: HexToHash("0xe28385e7bd68df656cd0042b74b69c3104b5356ed1f20eb69f1f925df47a3ab7"), 211 | BaseFeePerGas: uint256.NewInt(0), 212 | }, 213 | Value: uint256.NewInt(12345), 214 | Pubkey: HexToPubkey(publicKey), 215 | } 216 | ssz, err := message.MarshalSSZ() 217 | require.NoError(t, err) 218 | 219 | sig := bls.Sign(privateKey, ssz) 220 | sigBytes := bls.SignatureToBytes(sig) 221 | 222 | // Convert bls.Signature bytes to phase0.BLSSignature 223 | signature, err := utils.BlsSignatureToSignature(sig) 224 | require.NoError(t, err) 225 | 226 | testCases := []struct { 227 | name string 228 | hex string 229 | 230 | expectedPanic bool 231 | expectedSignature *phase0.BLSSignature 232 | }{ 233 | { 234 | name: "Should panic because of empty hexadecimal input", 235 | hex: "", 236 | expectedPanic: true, 237 | expectedSignature: nil, 238 | }, 239 | { 240 | name: "Should panic because of invalid hexadecimal input", 241 | hex: "foo", 242 | expectedPanic: true, 243 | expectedSignature: nil, 244 | }, 245 | { 246 | name: "Should not panic and convert hexadecimal input to signature", 247 | hex: hexutil.Encode(sigBytes), 248 | expectedPanic: false, 249 | expectedSignature: &signature, 250 | }, 251 | } 252 | 253 | for _, tt := range testCases { 254 | t.Run(tt.name, func(t *testing.T) { 255 | if tt.expectedPanic { 256 | require.Panics(t, func() { 257 | HexToSignature(tt.hex) 258 | }) 259 | } else { 260 | require.NotPanics(t, func() { 261 | actualSignature := HexToSignature(tt.hex) 262 | require.Equal(t, *tt.expectedSignature, actualSignature) 263 | }) 264 | } 265 | }) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /server/utils.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "math/big" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | builderApi "github.com/attestantio/go-builder-client/api" 16 | builderSpec "github.com/attestantio/go-builder-client/spec" 17 | "github.com/attestantio/go-eth2-client/spec" 18 | "github.com/attestantio/go-eth2-client/spec/phase0" 19 | "github.com/ethereum/go-ethereum/common" 20 | "github.com/ethereum/go-ethereum/common/hexutil" 21 | "github.com/flashbots/go-boost-utils/bls" 22 | "github.com/flashbots/go-boost-utils/ssz" 23 | "github.com/flashbots/mev-boost/config" 24 | "github.com/flashbots/mev-boost/server/types" 25 | "github.com/holiman/uint256" 26 | ) 27 | 28 | var ( 29 | errHTTPErrorResponse = errors.New("HTTP error response") 30 | errInvalidForkVersion = errors.New("invalid fork version") 31 | ) 32 | 33 | // UserAgent is a custom string type to avoid confusing url + userAgent parameters in SendHTTPRequest 34 | type UserAgent string 35 | 36 | // BlockHashHex is a hex-string representation of a block hash 37 | type BlockHashHex string 38 | 39 | // SendHTTPRequest - prepare and send HTTP request, marshaling the payload if any, and decoding the response if dst is set 40 | func SendHTTPRequest(ctx context.Context, client http.Client, method, url string, userAgent UserAgent, headers map[string]string, payload, dst any) (code int, err error) { 41 | var req *http.Request 42 | 43 | if payload == nil { 44 | req, err = http.NewRequestWithContext(ctx, method, url, nil) 45 | if err != nil { 46 | return 0, fmt.Errorf("could not prepare request: %w", err) 47 | } 48 | } else { 49 | payloadBytes, err2 := json.Marshal(payload) 50 | if err2 != nil { 51 | return 0, fmt.Errorf("could not marshal request: %w", err2) 52 | } 53 | req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payloadBytes)) 54 | if err != nil { 55 | return 0, fmt.Errorf("could not prepare request: %w", err) 56 | } 57 | // Set Content-Type header 58 | req.Header.Add("Content-Type", "application/json") 59 | } 60 | 61 | // Set User-Agent header 62 | req.Header.Set("User-Agent", strings.TrimSpace(fmt.Sprintf("mev-boost/%s %s", config.Version, userAgent))) 63 | 64 | // Set other headers 65 | for key, value := range headers { 66 | req.Header.Set(key, value) 67 | } 68 | 69 | // Execute request 70 | resp, err := client.Do(req) 71 | if err != nil { 72 | return 0, err 73 | } 74 | defer resp.Body.Close() 75 | 76 | if resp.StatusCode == http.StatusNoContent { 77 | return resp.StatusCode, nil 78 | } 79 | 80 | if resp.StatusCode > 299 { 81 | bodyBytes, err := io.ReadAll(resp.Body) 82 | if err != nil { 83 | return resp.StatusCode, fmt.Errorf("could not read error response body for status code %d: %w", resp.StatusCode, err) 84 | } 85 | return resp.StatusCode, fmt.Errorf("%w: %d / %s", errHTTPErrorResponse, resp.StatusCode, string(bodyBytes)) 86 | } 87 | 88 | if dst != nil { 89 | bodyBytes, err := io.ReadAll(resp.Body) 90 | if err != nil { 91 | return resp.StatusCode, fmt.Errorf("could not read response body: %w", err) 92 | } 93 | 94 | if err := json.Unmarshal(bodyBytes, dst); err != nil { 95 | return resp.StatusCode, fmt.Errorf("could not unmarshal response %s: %w", string(bodyBytes), err) 96 | } 97 | } 98 | 99 | return resp.StatusCode, nil 100 | } 101 | 102 | // ComputeDomain computes the signing domain 103 | func ComputeDomain(domainType phase0.DomainType, forkVersionHex, genesisValidatorsRootHex string) (domain phase0.Domain, err error) { 104 | genesisValidatorsRoot := phase0.Root(common.HexToHash(genesisValidatorsRootHex)) 105 | forkVersionBytes, err := hexutil.Decode(forkVersionHex) 106 | if err != nil || len(forkVersionBytes) != 4 { 107 | return domain, errInvalidForkVersion 108 | } 109 | var forkVersion [4]byte 110 | copy(forkVersion[:], forkVersionBytes[:4]) 111 | return ssz.ComputeDomain(domainType, forkVersion, genesisValidatorsRoot), nil 112 | } 113 | 114 | // DecodeJSON reads JSON from io.Reader and decodes it into a struct 115 | func DecodeJSON(r io.Reader, dst any) error { 116 | decoder := json.NewDecoder(r) 117 | decoder.DisallowUnknownFields() 118 | return decoder.Decode(dst) 119 | } 120 | 121 | // bidResp are entries in the bids cache 122 | type bidResp struct { 123 | t time.Time 124 | response builderSpec.VersionedSignedBuilderBid 125 | bidInfo bidInfo 126 | relays []types.RelayEntry 127 | } 128 | 129 | // bidInfo is used to store bid response fields for logging and validation 130 | type bidInfo struct { 131 | blockHash phase0.Hash32 132 | parentHash phase0.Hash32 133 | pubkey phase0.BLSPubKey 134 | blockNumber uint64 135 | txRoot phase0.Root 136 | value *uint256.Int 137 | } 138 | 139 | func httpClientDisallowRedirects(_ *http.Request, _ []*http.Request) error { 140 | return http.ErrUseLastResponse 141 | } 142 | 143 | func weiBigIntToEthBigFloat(wei *big.Int) (ethValue *big.Float) { 144 | // wei / 10^18 145 | fbalance := new(big.Float) 146 | fbalance.SetString(wei.String()) 147 | ethValue = new(big.Float).Quo(fbalance, big.NewFloat(1e18)) 148 | return 149 | } 150 | 151 | func parseBidInfo(bid *builderSpec.VersionedSignedBuilderBid) (bidInfo, error) { 152 | blockHash, err := bid.BlockHash() 153 | if err != nil { 154 | return bidInfo{}, err 155 | } 156 | parentHash, err := bid.ParentHash() 157 | if err != nil { 158 | return bidInfo{}, err 159 | } 160 | pubkey, err := bid.Builder() 161 | if err != nil { 162 | return bidInfo{}, err 163 | } 164 | blockNumber, err := bid.BlockNumber() 165 | if err != nil { 166 | return bidInfo{}, err 167 | } 168 | txRoot, err := bid.TransactionsRoot() 169 | if err != nil { 170 | return bidInfo{}, err 171 | } 172 | value, err := bid.Value() 173 | if err != nil { 174 | return bidInfo{}, err 175 | } 176 | return bidInfo{ 177 | blockHash: blockHash, 178 | parentHash: parentHash, 179 | pubkey: pubkey, 180 | blockNumber: blockNumber, 181 | txRoot: txRoot, 182 | value: value, 183 | }, nil 184 | } 185 | 186 | func checkRelaySignature(bid *builderSpec.VersionedSignedBuilderBid, domain phase0.Domain, pubKey phase0.BLSPubKey) (bool, error) { 187 | root, err := bid.MessageHashTreeRoot() 188 | if err != nil { 189 | return false, err 190 | } 191 | sig, err := bid.Signature() 192 | if err != nil { 193 | return false, err 194 | } 195 | signingData := phase0.SigningData{ObjectRoot: root, Domain: domain} 196 | msg, err := signingData.HashTreeRoot() 197 | if err != nil { 198 | return false, err 199 | } 200 | 201 | return bls.VerifySignatureBytes(msg[:], sig[:], pubKey[:]) 202 | } 203 | 204 | func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockResponse) bool { 205 | switch payload.Version { 206 | case spec.DataVersionBellatrix: 207 | if payload.Bellatrix == nil || payload.Bellatrix.BlockHash == nilHash { 208 | return true 209 | } 210 | case spec.DataVersionCapella: 211 | if payload.Capella == nil || payload.Capella.BlockHash == nilHash { 212 | return true 213 | } 214 | case spec.DataVersionDeneb: 215 | if payload.Deneb == nil || payload.Deneb.ExecutionPayload == nil || 216 | payload.Deneb.ExecutionPayload.BlockHash == nilHash || 217 | payload.Deneb.BlobsBundle == nil { 218 | return true 219 | } 220 | case spec.DataVersionElectra: 221 | if payload.Electra == nil || payload.Electra.ExecutionPayload == nil || 222 | payload.Electra.ExecutionPayload.BlockHash == nilHash || 223 | payload.Electra.BlobsBundle == nil { 224 | return true 225 | } 226 | case spec.DataVersionFulu: 227 | if payload.Fulu == nil || payload.Fulu.ExecutionPayload == nil || 228 | payload.Fulu.ExecutionPayload.BlockHash == nilHash || 229 | payload.Fulu.BlobsBundle == nil { 230 | return true 231 | } 232 | case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair: 233 | return true 234 | } 235 | return false 236 | } 237 | 238 | func wrapUserAgent(ua UserAgent) string { 239 | return strings.TrimSpace(fmt.Sprintf("mev-boost/%s %s", config.Version, ua)) 240 | } 241 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/flashbots/go-boost-utils/types" 13 | "github.com/flashbots/mev-boost/common" 14 | "github.com/flashbots/mev-boost/config" 15 | "github.com/flashbots/mev-boost/server" 16 | serverTypes "github.com/flashbots/mev-boost/server/types" 17 | "github.com/sirupsen/logrus" 18 | "github.com/urfave/cli/v3" 19 | ) 20 | 21 | const ( 22 | genesisForkVersionMainnet = "0x00000000" 23 | genesisForkVersionSepolia = "0x90000069" 24 | genesisForkVersionHolesky = "0x01017000" 25 | genesisForkVersionHoodi = "0x10000910" 26 | 27 | genesisTimeMainnet = 1606824023 28 | genesisTimeSepolia = 1655733600 29 | genesisTimeHolesky = 1695902400 30 | genesisTimeHoodi = 1742213400 31 | ) 32 | 33 | type RelaySetupResult struct { 34 | RelayConfigs []serverTypes.RelayConfig 35 | MinBid types.U256Str 36 | RelayCheck bool 37 | TimeoutGetHeaderMs uint64 38 | LateInSlotTimeMs uint64 39 | CLIRelays []serverTypes.RelayEntry // CLI-provided relays for hot-reload merging 40 | } 41 | 42 | var ( 43 | // errors 44 | errInvalidLoglevel = errors.New("invalid loglevel") 45 | errNegativeBid = errors.New("please specify a non-negative minimum bid") 46 | errLargeMinBid = errors.New("minimum bid is too large, please ensure min-bid is denominated in Ethers") 47 | 48 | log = logrus.NewEntry(logrus.New()) 49 | ) 50 | 51 | func Main() { 52 | cmd := &cli.Command{ 53 | Name: "mev-boost", 54 | Usage: "mev-boost implementation, see help for more info", 55 | Action: start, 56 | Flags: flags, 57 | } 58 | 59 | if err := cmd.Run(context.Background(), os.Args); err != nil { 60 | log.Fatal(err) 61 | } 62 | } 63 | 64 | // start starts the mev-boost cli 65 | func start(_ context.Context, cmd *cli.Command) error { 66 | // Only print the version if the flag is set 67 | if cmd.IsSet(versionFlag.Name) { 68 | fmt.Fprintf(cmd.Writer, "mev-boost %s\n", config.Version) 69 | return nil 70 | } 71 | 72 | if err := setupLogging(cmd); err != nil { 73 | flag.Usage() 74 | log.WithError(err).Fatal("failed setting up logging") 75 | } 76 | 77 | var ( 78 | genesisForkVersion, genesisTime = setupGenesis(cmd) 79 | listenAddr = cmd.String(addrFlag.Name) 80 | metricsEnabled = cmd.Bool(metricsFlag.Name) 81 | metricsAddr = cmd.String(metricsAddrFlag.Name) 82 | ) 83 | 84 | relaySetup, err := setupRelays(cmd) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | opts := server.BoostServiceOpts{ 90 | Log: log, 91 | ListenAddr: listenAddr, 92 | RelayConfigs: relaySetup.RelayConfigs, 93 | GenesisForkVersionHex: genesisForkVersion, 94 | GenesisTime: genesisTime, 95 | RelayCheck: relaySetup.RelayCheck, 96 | RelayMinBid: relaySetup.MinBid, 97 | RequestTimeoutGetHeader: time.Duration(cmd.Int(timeoutGetHeaderFlag.Name)) * time.Millisecond, 98 | RequestTimeoutGetPayload: time.Duration(cmd.Int(timeoutGetPayloadFlag.Name)) * time.Millisecond, 99 | RequestTimeoutRegVal: time.Duration(cmd.Int(timeoutRegValFlag.Name)) * time.Millisecond, 100 | RequestMaxRetries: cmd.Int(maxRetriesFlag.Name), 101 | MetricsAddr: metricsAddr, 102 | TimeoutGetHeaderMs: relaySetup.TimeoutGetHeaderMs, 103 | LateInSlotTimeMs: relaySetup.LateInSlotTimeMs, 104 | } 105 | service, err := server.NewBoostService(opts) 106 | if err != nil { 107 | log.WithError(err).Fatal("failed creating the server") 108 | } 109 | 110 | if relaySetup.RelayCheck && service.CheckRelays() == 0 { 111 | log.Error("no relay passed the health-check!") 112 | } 113 | 114 | // enable hot reloading only if both --config and --watch-config flags are set 115 | if cmd.IsSet(relayConfigFlag.Name) && cmd.Bool(watchConfigFlag.Name) { 116 | configPath := cmd.String(relayConfigFlag.Name) 117 | watcher, err := NewConfigWatcher(configPath, relaySetup.CLIRelays, log) 118 | if err != nil { 119 | log.WithError(err).Warn("failed to set up config watcher") 120 | return err 121 | } 122 | // register a callback which gets invoked when config file changes 123 | watcher.Watch(func(newConfig *ConfigResult) { 124 | mergedConfigs, err := MergeRelayConfigs(relaySetup.CLIRelays, newConfig.RelayConfigs) 125 | if err != nil { 126 | log.WithError(err).Error("failed to merge relay configs, keeping old config") 127 | return 128 | } 129 | if len(mergedConfigs) == 0 { 130 | log.Error("merged config has no relays (neither from CLI nor config file), keeping old config") 131 | return 132 | } 133 | service.UpdateConfig(mergedConfigs, newConfig.TimeoutGetHeaderMs, newConfig.LateInSlotTimeMs) 134 | }) 135 | } 136 | 137 | if metricsEnabled { 138 | go func() { 139 | log.Infof("metrics server listening on %v", opts.MetricsAddr) 140 | if err := service.StartMetricsServer(); err != nil { 141 | log.WithError(err).Error("metrics server exited with error") 142 | } 143 | }() 144 | } 145 | 146 | log.Infof("listening on %v", listenAddr) 147 | return service.StartHTTPServer() 148 | } 149 | 150 | func setupRelays(cmd *cli.Command) (*RelaySetupResult, error) { 151 | // For backwards compatibility with the -relays flag. 152 | var relays relayList 153 | if cmd.IsSet(relaysFlag.Name) { 154 | relayURLs := cmd.StringSlice(relaysFlag.Name) 155 | for _, urls := range relayURLs { 156 | for _, url := range strings.Split(urls, ",") { 157 | if err := relays.Set(strings.TrimSpace(url)); err != nil { 158 | log.WithError(err).WithField("relay", url).Fatal("invalid relay URL") 159 | } 160 | } 161 | } 162 | } 163 | 164 | // load configuration via config file 165 | var configMap map[string]serverTypes.RelayConfig 166 | var timeoutGetHeaderMs uint64 = 950 167 | var lateInSlotTimeMs uint64 = 2000 168 | if cmd.IsSet(relayConfigFlag.Name) { 169 | configPath := cmd.String(relayConfigFlag.Name) 170 | log.Infof("loading config from: %s", configPath) 171 | configResult, err := LoadConfigFile(configPath) 172 | if err != nil { 173 | log.WithError(err).Fatal("failed to load config file") 174 | return nil, err 175 | } 176 | configMap = configResult.RelayConfigs 177 | timeoutGetHeaderMs = configResult.TimeoutGetHeaderMs 178 | lateInSlotTimeMs = configResult.LateInSlotTimeMs 179 | } 180 | relayConfigs, err := MergeRelayConfigs(relays, configMap) 181 | if err != nil { 182 | log.WithError(err).Fatal("failed to merge relay configs") 183 | return nil, err 184 | } 185 | 186 | log.Infof("using %d relays", len(relayConfigs)) 187 | for index, config := range relayConfigs { 188 | if config.EnableTimingGames { 189 | log.Infof("relay #%d: %s timing games: enabled", index+1, config.RelayEntry.String()) 190 | } else { 191 | log.Infof("relay #%d: %s", index+1, config.RelayEntry.String()) 192 | } 193 | } 194 | 195 | relayMinBidWei, err := sanitizeMinBid(cmd.Float(minBidFlag.Name)) 196 | if err != nil { 197 | log.WithError(err).Fatal("failed sanitizing min bid") 198 | } 199 | if relayMinBidWei.BigInt().Sign() > 0 { 200 | log.Infof("min bid set to %v eth (%v wei)", cmd.Float(minBidFlag.Name), relayMinBidWei) 201 | } 202 | return &RelaySetupResult{ 203 | RelayConfigs: relayConfigs, 204 | MinBid: *relayMinBidWei, 205 | RelayCheck: cmd.Bool(relayCheckFlag.Name), 206 | TimeoutGetHeaderMs: timeoutGetHeaderMs, 207 | LateInSlotTimeMs: lateInSlotTimeMs, 208 | CLIRelays: []serverTypes.RelayEntry(relays), 209 | }, nil 210 | } 211 | 212 | func setupGenesis(cmd *cli.Command) (string, uint64) { 213 | var ( 214 | genesisForkVersion string 215 | genesisTime uint64 216 | ) 217 | 218 | switch { 219 | case cmd.IsSet(customGenesisForkFlag.Name): 220 | genesisForkVersion = cmd.String(customGenesisForkFlag.Name) 221 | case cmd.Bool(sepoliaFlag.Name): 222 | genesisForkVersion = genesisForkVersionSepolia 223 | genesisTime = genesisTimeSepolia 224 | case cmd.Bool(holeskyFlag.Name): 225 | genesisForkVersion = genesisForkVersionHolesky 226 | genesisTime = genesisTimeHolesky 227 | case cmd.Bool(hoodiFlag.Name): 228 | genesisForkVersion = genesisForkVersionHoodi 229 | genesisTime = genesisTimeHoodi 230 | case cmd.Bool(mainnetFlag.Name): 231 | genesisForkVersion = genesisForkVersionMainnet 232 | genesisTime = genesisTimeMainnet 233 | default: 234 | flag.Usage() 235 | log.Fatal("please specify a genesis fork version (eg. -mainnet / -sepolia / -holesky / -hoodi / -genesis-fork-version flags)") 236 | } 237 | 238 | if cmd.IsSet(customGenesisTimeFlag.Name) { 239 | genesisTime = uint64(cmd.Uint(customGenesisTimeFlag.Name)) 240 | } 241 | log.Infof("using genesis fork version: %s time: %d", genesisForkVersion, genesisTime) 242 | return genesisForkVersion, genesisTime 243 | } 244 | 245 | func setupLogging(cmd *cli.Command) error { 246 | // setup logging 247 | log.Logger.SetOutput(os.Stdout) 248 | if cmd.IsSet(jsonFlag.Name) { 249 | log.Logger.SetFormatter(&logrus.JSONFormatter{ 250 | TimestampFormat: config.RFC3339Milli, 251 | }) 252 | } else { 253 | log.Logger.SetFormatter(&logrus.TextFormatter{ 254 | FullTimestamp: true, 255 | TimestampFormat: config.RFC3339Milli, 256 | ForceColors: cmd.Bool(colorFlag.Name), 257 | }) 258 | } 259 | 260 | logLevel := cmd.String(logLevelFlag.Name) 261 | if cmd.IsSet(debugFlag.Name) { 262 | logLevel = "debug" 263 | } 264 | lvl, err := logrus.ParseLevel(logLevel) 265 | if err != nil { 266 | return fmt.Errorf("%w: %s", errInvalidLoglevel, logLevel) 267 | } 268 | log.Logger.SetLevel(lvl) 269 | 270 | if cmd.IsSet(logServiceFlag.Name) { 271 | log = log.WithField("service", cmd.String(logServiceFlag.Name)) 272 | } 273 | 274 | // Add version to logs and say hello 275 | if cmd.Bool(logNoVersionFlag.Name) { 276 | log.Infof("starting mev-boost %s", config.Version) 277 | } else { 278 | log = log.WithField("version", config.Version) 279 | log.Infof("starting mev-boost") 280 | } 281 | log.Debug("debug logging enabled") 282 | return nil 283 | } 284 | 285 | func sanitizeMinBid(minBid float64) (*types.U256Str, error) { 286 | if minBid < 0.0 { 287 | return nil, errNegativeBid 288 | } 289 | if minBid > 1000000.0 { 290 | return nil, errLargeMinBid 291 | } 292 | return common.FloatEthTo256Wei(minBid) 293 | } 294 | -------------------------------------------------------------------------------- /testdata/signed-blinded-beacon-block-bellatrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "slot": "1", 4 | "proposer_index": "1", 5 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 6 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 7 | "body": { 8 | "randao_reveal": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 9 | "eth1_data": { 10 | "deposit_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 11 | "deposit_count": "1", 12 | "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 13 | }, 14 | "graffiti": "0xdeadbeefc0ffeedeadbeefc0ffeedeadbeefc0ffeedeadbeefc0ffeedeadbeef", 15 | "proposer_slashings": [ 16 | { 17 | "signed_header_1": { 18 | "message": { 19 | "slot": "1", 20 | "proposer_index": "1", 21 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 22 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 23 | "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 24 | }, 25 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 26 | }, 27 | "signed_header_2": { 28 | "message": { 29 | "slot": "1", 30 | "proposer_index": "1", 31 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 32 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 33 | "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 34 | }, 35 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 36 | } 37 | } 38 | ], 39 | "attester_slashings": [ 40 | { 41 | "attestation_1": { 42 | "attesting_indices": [ 43 | "1" 44 | ], 45 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 46 | "data": { 47 | "slot": "1", 48 | "index": "1", 49 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 50 | "source": { 51 | "epoch": "1", 52 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 53 | }, 54 | "target": { 55 | "epoch": "1", 56 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 57 | } 58 | } 59 | }, 60 | "attestation_2": { 61 | "attesting_indices": [ 62 | "1" 63 | ], 64 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 65 | "data": { 66 | "slot": "1", 67 | "index": "1", 68 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 69 | "source": { 70 | "epoch": "1", 71 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 72 | }, 73 | "target": { 74 | "epoch": "1", 75 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 76 | } 77 | } 78 | } 79 | } 80 | ], 81 | "attestations": [ 82 | { 83 | "aggregation_bits": "0x01", 84 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 85 | "data": { 86 | "slot": "1", 87 | "index": "1", 88 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 89 | "source": { 90 | "epoch": "1", 91 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 92 | }, 93 | "target": { 94 | "epoch": "1", 95 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 96 | } 97 | } 98 | } 99 | ], 100 | "deposits": [ 101 | { 102 | "proof": [ 103 | "0xeeffb6c21a01d3abf09cd6c56e5d48f5ea0fc3bb0de906e3beea3e73776329cb", 104 | "0x601c3b24a99d023224d50811bed19449890febb719a31d09ac414c4632f3c0ba", 105 | "0xbb5e485e0a366e16510de33731d71204ad2fe0f7c600861fc2ac4685212c34e3", 106 | "0x0006964745296a3e6ebf3954a1541e73205f1eefaddfc48ca9dc856bf159bca2", 107 | "0x2c6020f1f9712b89f59550aec05b7c23cb1b113762399c0ca5b8fdd2fa85ce57", 108 | "0x1c15634783e1d9d2cb969da66fd72cafca5026191d911b83211318d183c5ea59", 109 | "0xdfbdf99a1fde57899df1545be1f91bc8a8a9f46c4bac619e28e92aff276de41f", 110 | "0xfe9b0f0c05fde6bd26ce63d394058844ad4451f70b6d2547f49c5c2a5c7891a1", 111 | "0x165f84ee467d18dbafdb07275dc42fb988ab696b0a7ad94c52f4d7a27144b994", 112 | "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", 113 | "0xecdbe5e5056b968aa726a08f1aa33f5d41540eed42f59ace020431cf38a5144e", 114 | "0xc4498c5eb1feeb0b225a3f332bdf523dbc013a5b336a851fce1c055b4019a457", 115 | "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", 116 | "0x8a9b66ad79116c9fc6eed14bde76e8f486669e59b0b5bb0c60a6b3caea38b83d", 117 | "0x267c5455e4806b5d0ad5573552d0162e0983595bac25dacd9078174a2766643a", 118 | "0x27e0c6357985de4d6026d6da14f31e8bfe14524056fec69dc06d6f8a239344af", 119 | "0xf8455aebc24849bea870fbcef1235e2d27c8fd27db24e26d30d0173f3b207874", 120 | "0xaba01bf7fe57be4373f47ff8ea6adc4348fab087b69b2518ce630820f95f4150", 121 | "0xd47152335d9460f2b6fb7aba05ced32a52e9f46659ccd3daa2059661d75a6308", 122 | "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", 123 | "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", 124 | "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", 125 | "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", 126 | "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", 127 | "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", 128 | "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", 129 | "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", 130 | "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", 131 | "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", 132 | "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", 133 | "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", 134 | "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", 135 | "0xf7ed070000000000000000000000000000000000000000000000000000000000" 136 | ], 137 | "data": { 138 | "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", 139 | "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 140 | "amount": "1", 141 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 142 | } 143 | } 144 | ], 145 | "voluntary_exits": [ 146 | { 147 | "message": { 148 | "epoch": "1", 149 | "validator_index": "1" 150 | }, 151 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 152 | } 153 | ], 154 | "sync_aggregate": { 155 | "sync_committee_bits": "0x01", 156 | "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 157 | }, 158 | "execution_payload_header": { 159 | "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 160 | "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09", 161 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 162 | "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 163 | "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 164 | "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 165 | "block_number": "1", 166 | "gas_limit": "1", 167 | "gas_used": "1", 168 | "timestamp": "1", 169 | "extra_data": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 170 | "base_fee_per_gas": "1", 171 | "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 172 | "transactions_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 173 | } 174 | } 175 | }, 176 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 177 | } -------------------------------------------------------------------------------- /server/register_validator_test.go: -------------------------------------------------------------------------------- 1 | // register_validator_test.go 2 | package server 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | builderApiV1 "github.com/attestantio/go-builder-client/api/v1" 13 | "github.com/flashbots/mev-boost/server/mock" 14 | "github.com/flashbots/mev-boost/server/params" 15 | "github.com/flashbots/mev-boost/server/types" 16 | "github.com/sirupsen/logrus" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | // TestHandleRegisterValidator_EmptyList verifies that a valid registration returns status ok 21 | func TestHandleRegisterValidator_EmptyList(t *testing.T) { 22 | relay := mock.NewRelay(t) 23 | defer relay.Server.Close() 24 | 25 | m := &BoostService{ 26 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 27 | httpClientRegVal: *http.DefaultClient, 28 | log: logrus.NewEntry(logrus.New()), 29 | } 30 | 31 | reqBody := bytes.NewBufferString("[]") 32 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 33 | req.Header.Set("Content-Type", "application/json") 34 | 35 | rr := httptest.NewRecorder() 36 | m.handleRegisterValidator(rr, req) 37 | 38 | require.Equal(t, http.StatusOK, rr.Code, "expected status ok") 39 | 40 | count := relay.GetRequestCount(params.PathRegisterValidator) 41 | require.Equal(t, 1, count) 42 | } 43 | 44 | // TestHandleRegisterValidator_NotEmptyList verifies that a non-empty list returns status ok 45 | func TestHandleRegisterValidator_NotEmptyList(t *testing.T) { 46 | relay := mock.NewRelay(t) 47 | defer relay.Server.Close() 48 | 49 | m := &BoostService{ 50 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 51 | httpClientRegVal: *http.DefaultClient, 52 | log: logrus.NewEntry(logrus.New()), 53 | } 54 | 55 | validatorRegistrations := []builderApiV1.SignedValidatorRegistration{ 56 | { 57 | Message: &builderApiV1.ValidatorRegistration{ 58 | Timestamp: time.Unix(1, 0), 59 | }, 60 | }, 61 | { 62 | Message: &builderApiV1.ValidatorRegistration{ 63 | Timestamp: time.Unix(2, 0), 64 | }, 65 | }, 66 | } 67 | 68 | encodedValidatorRegistrations, err := json.Marshal(validatorRegistrations) 69 | require.NoError(t, err) 70 | 71 | reqBody := bytes.NewBuffer(encodedValidatorRegistrations) 72 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 73 | req.Header.Set("Content-Type", "application/json") 74 | 75 | rr := httptest.NewRecorder() 76 | m.handleRegisterValidator(rr, req) 77 | 78 | require.Equal(t, http.StatusOK, rr.Code, "expected status ok") 79 | 80 | count := relay.GetRequestCount(params.PathRegisterValidator) 81 | require.Equal(t, 1, count) 82 | } 83 | 84 | // TestHandleRegisterValidator_InvalidJSON verifies that an invalid registration returns bad gateway 85 | func TestHandleRegisterValidator_InvalidJSON(t *testing.T) { 86 | relay := mock.NewRelay(t) 87 | defer relay.Server.Close() 88 | 89 | m := &BoostService{ 90 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 91 | httpClientRegVal: *http.DefaultClient, 92 | log: logrus.NewEntry(logrus.New()), 93 | } 94 | 95 | reqBody := bytes.NewBufferString("invalid json") 96 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 97 | req.Header.Set("Content-Type", "application/json") 98 | 99 | rr := httptest.NewRecorder() 100 | m.handleRegisterValidator(rr, req) 101 | 102 | require.Equal(t, http.StatusBadGateway, rr.Code) 103 | 104 | count := relay.GetRequestCount(params.PathRegisterValidator) 105 | require.Equal(t, 1, count) 106 | } 107 | 108 | // TestHandleRegisterValidator_ValidSSZ verifies that a valid registration returns status ok 109 | func TestHandleRegisterValidator_ValidSSZ(t *testing.T) { 110 | relay := mock.NewRelay(t) 111 | defer relay.Server.Close() 112 | 113 | m := &BoostService{ 114 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 115 | httpClientRegVal: *http.DefaultClient, 116 | log: logrus.NewEntry(logrus.New()), 117 | } 118 | 119 | validatorRegistrations := builderApiV1.SignedValidatorRegistrations{ 120 | Registrations: []*builderApiV1.SignedValidatorRegistration{ 121 | { 122 | Message: &builderApiV1.ValidatorRegistration{ 123 | Timestamp: time.Unix(1, 0), 124 | }, 125 | }, 126 | { 127 | Message: &builderApiV1.ValidatorRegistration{ 128 | Timestamp: time.Unix(2, 0), 129 | }, 130 | }, 131 | }, 132 | } 133 | 134 | encodedValidatorRegistrations, err := validatorRegistrations.MarshalSSZ() 135 | require.NoError(t, err) 136 | 137 | reqBody := bytes.NewBuffer(encodedValidatorRegistrations) 138 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 139 | req.Header.Set("Content-Type", "application/octet-stream") 140 | 141 | rr := httptest.NewRecorder() 142 | m.handleRegisterValidator(rr, req) 143 | 144 | require.Equal(t, http.StatusOK, rr.Code) 145 | 146 | count := relay.GetRequestCount(params.PathRegisterValidator) 147 | require.Equal(t, 1, count) 148 | } 149 | 150 | // TestHandleRegisterValidator_InvalidSSZ verifies that an invalid registration returns bad gateway 151 | func TestHandleRegisterValidator_InvalidSSZ(t *testing.T) { 152 | relay := mock.NewRelay(t) 153 | defer relay.Server.Close() 154 | 155 | m := &BoostService{ 156 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 157 | httpClientRegVal: *http.DefaultClient, 158 | log: logrus.NewEntry(logrus.New()), 159 | } 160 | 161 | reqBody := bytes.NewBufferString("invalid ssz") 162 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 163 | req.Header.Set("Content-Type", "application/octet-stream") 164 | 165 | rr := httptest.NewRecorder() 166 | m.handleRegisterValidator(rr, req) 167 | 168 | require.Equal(t, http.StatusBadGateway, rr.Code) 169 | 170 | count := relay.GetRequestCount(params.PathRegisterValidator) 171 | require.Equal(t, 1, count) 172 | } 173 | 174 | // TestHandleRegisterValidator_MultipleRelaysOneSuccess verifies that if one relay succeeds the response is ok 175 | func TestHandleRegisterValidator_MultipleRelaysOneSuccess(t *testing.T) { 176 | badRelay := mock.NewRelay(t) 177 | defer badRelay.Server.Close() 178 | badRelay.OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) { 179 | http.Error(w, "simulated failure", http.StatusInternalServerError) 180 | }) 181 | 182 | relaySuccess := mock.NewRelay(t) 183 | defer relaySuccess.Server.Close() 184 | 185 | m := &BoostService{ 186 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(badRelay.RelayEntry), types.NewRelayConfig(relaySuccess.RelayEntry)}, 187 | httpClientRegVal: *http.DefaultClient, 188 | log: logrus.NewEntry(logrus.New()), 189 | } 190 | 191 | reqBody := bytes.NewBufferString("[]") 192 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 193 | req.Header.Set("Content-Type", "application/json") 194 | 195 | rr := httptest.NewRecorder() 196 | m.handleRegisterValidator(rr, req) 197 | 198 | require.Equal(t, http.StatusOK, rr.Code) 199 | 200 | // No need to check badRelay request count, it might not be called if the 201 | // other relay responds first. This happens sometimes when running the tests. 202 | countSuccess := relaySuccess.GetRequestCount(params.PathRegisterValidator) 203 | require.Equal(t, 1, countSuccess) 204 | } 205 | 206 | // TestHandleRegisterValidator_AllFail verifies that if all relays fail the response is bad gateway 207 | func TestHandleRegisterValidator_AllFail(t *testing.T) { 208 | badRelay1 := mock.NewRelay(t) 209 | defer badRelay1.Server.Close() 210 | badRelay1.OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) { 211 | http.Error(w, "simulated failure 1", http.StatusInternalServerError) 212 | }) 213 | 214 | badRelay2 := mock.NewRelay(t) 215 | defer badRelay2.Server.Close() 216 | badRelay2.OverrideHandleRegisterValidator(func(w http.ResponseWriter, _ *http.Request) { 217 | http.Error(w, "simulated failure 2", http.StatusInternalServerError) 218 | }) 219 | 220 | m := &BoostService{ 221 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(badRelay1.RelayEntry), types.NewRelayConfig(badRelay2.RelayEntry)}, 222 | httpClientRegVal: *http.DefaultClient, 223 | log: logrus.NewEntry(logrus.New()), 224 | } 225 | 226 | reqBody := bytes.NewBufferString("[]") 227 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 228 | req.Header.Set("Content-Type", "application/json") 229 | 230 | rr := httptest.NewRecorder() 231 | m.handleRegisterValidator(rr, req) 232 | 233 | require.Equal(t, http.StatusBadGateway, rr.Code) 234 | 235 | countBadRelay1 := badRelay1.GetRequestCount(params.PathRegisterValidator) 236 | require.Equal(t, 1, countBadRelay1) 237 | countBadRelay2 := badRelay2.GetRequestCount(params.PathRegisterValidator) 238 | require.Equal(t, 1, countBadRelay2) 239 | } 240 | 241 | // TestHandleRegisterValidator_RelayNetworkError verifies that a network error results in bad gateway 242 | func TestHandleRegisterValidator_RelayNetworkError(t *testing.T) { 243 | relay := mock.NewRelay(t) 244 | relay.Server.Close() // simulate network error 245 | 246 | m := &BoostService{ 247 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 248 | httpClientRegVal: *http.DefaultClient, 249 | log: logrus.NewEntry(logrus.New()), 250 | } 251 | 252 | reqBody := bytes.NewBufferString("[]") 253 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 254 | req.Header.Set("Content-Type", "application/json") 255 | 256 | rr := httptest.NewRecorder() 257 | m.handleRegisterValidator(rr, req) 258 | 259 | require.Equal(t, http.StatusBadGateway, rr.Code) 260 | } 261 | 262 | // TestHandleRegisterValidator_HeaderPropagation verifies that headers from the request are forwarded 263 | func TestHandleRegisterValidator_HeaderPropagation(t *testing.T) { 264 | relay := mock.NewRelay(t) 265 | defer relay.Server.Close() 266 | 267 | headerChan := make(chan http.Header, 1) 268 | relay.OverrideHandleRegisterValidator(func(w http.ResponseWriter, req *http.Request) { 269 | headerChan <- req.Header 270 | w.Header().Set("Content-Type", "application/json") 271 | w.WriteHeader(http.StatusOK) 272 | }) 273 | 274 | m := &BoostService{ 275 | relayConfigs: []types.RelayConfig{types.NewRelayConfig(relay.RelayEntry)}, 276 | httpClientRegVal: *http.DefaultClient, 277 | log: logrus.NewEntry(logrus.New()), 278 | } 279 | 280 | reqBody := bytes.NewBufferString("[]") 281 | req := httptest.NewRequest(http.MethodPost, "https://example.com"+params.PathRegisterValidator, reqBody) 282 | req.Header.Set("Content-Type", "application/json") 283 | req.Header.Set("X-Custom-Header", "custom-value") 284 | 285 | rr := httptest.NewRecorder() 286 | m.handleRegisterValidator(rr, req) 287 | 288 | require.Equal(t, http.StatusOK, rr.Code) 289 | 290 | select { 291 | case capturedHeader := <-headerChan: 292 | require.Equal(t, "custom-value", capturedHeader.Get("X-Custom-Header")) 293 | case <-time.After(2 * time.Second): 294 | t.Fatal("timed out waiting for header capture") 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /testdata/signed-blinded-beacon-block-capella.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": { 3 | "slot": "1", 4 | "proposer_index": "1", 5 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 6 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 7 | "body": { 8 | "randao_reveal": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 9 | "eth1_data": { 10 | "deposit_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 11 | "deposit_count": "1", 12 | "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 13 | }, 14 | "graffiti": "0x035d6e107958843dc805e7094b9843a30a1afd56d0df2e2b8ad1a907d56961e3", 15 | "proposer_slashings": [ 16 | { 17 | "signed_header_1": { 18 | "message": { 19 | "slot": "1", 20 | "proposer_index": "1", 21 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 22 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 23 | "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 24 | }, 25 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 26 | }, 27 | "signed_header_2": { 28 | "message": { 29 | "slot": "1", 30 | "proposer_index": "1", 31 | "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 32 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 33 | "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 34 | }, 35 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 36 | } 37 | } 38 | ], 39 | "attester_slashings": [ 40 | { 41 | "attestation_1": { 42 | "attesting_indices": ["1"], 43 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 44 | "data": { 45 | "slot": "1", 46 | "index": "1", 47 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 48 | "source": { 49 | "epoch": "1", 50 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 51 | }, 52 | "target": { 53 | "epoch": "1", 54 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 55 | } 56 | } 57 | }, 58 | "attestation_2": { 59 | "attesting_indices": ["1"], 60 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 61 | "data": { 62 | "slot": "1", 63 | "index": "1", 64 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 65 | "source": { 66 | "epoch": "1", 67 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 68 | }, 69 | "target": { 70 | "epoch": "1", 71 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 72 | } 73 | } 74 | } 75 | } 76 | ], 77 | "attestations": [ 78 | { 79 | "aggregation_bits": "0x01", 80 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505", 81 | "data": { 82 | "slot": "1", 83 | "index": "1", 84 | "beacon_block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 85 | "source": { 86 | "epoch": "1", 87 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 88 | }, 89 | "target": { 90 | "epoch": "1", 91 | "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 92 | } 93 | } 94 | } 95 | ], 96 | "deposits": [ 97 | { 98 | "proof": [ 99 | "0xcd5a13caadcac56ae4acdabbe37bcd12348b3dbd728a703cc8c7fd641a765a23", 100 | "0x11e283b8e45f0864817a125f630afa79e57a7093ccabb9a7dfec9b7cfc50b5b0", 101 | "0x1c7dbcb2c3ff23e0867512b2f5cf4ff300fd95f6654c721a29e30a188d177acb", 102 | "0x26777a9e610b55549f1d0fec6bc47049073ebd03a74c3d308dd4633ac156db43", 103 | "0xd2253d9472594a826ac081ae7847d8ff776fccc8eccf044395fcdf4fe9357351", 104 | "0x95faeee7cf2b494d0808fb6abcea11e0fb98e92d268f30bb0295f56ef624de48", 105 | "0x128cb49cac29a390f096dd95b7ce97b0465bce89fd003f48e8b349cb7f70aeeb", 106 | "0x9335cff8f4423727a09c097611113bd2a9b94af5bde89254163ff30cb448bd59", 107 | "0x493e981118edb12e7fcb78ff0c90c8a365a813e272a96e648b2dd38a2424247e", 108 | "0x8792492a88df0902cb407250b0149e8389a7b1d3f772094669bd0a75a054c5ff", 109 | "0xbad3cf3f2f110d3af7ffd1157ca8d313f3564b208c854c24b9b8249371b00495", 110 | "0x9aa7d4e7a9fc2c9ceaf6b475f2975c580b53f5c076988cdc2dfe20798ff85df3", 111 | "0x67ee9b8565b885c424e7c0e17d4722e26baef4e086b863e7cb1a1eb50f95450e", 112 | "0xe19199724ac00c727d30773c6d2e310de0ea5298be1af27f931ff265873c54ea", 113 | "0x7ca1e29efdde8c316c55b06a4854df3f0e2c6061e9d37eaf59a5310b0fc63600", 114 | "0xfb048c607eafa7ce08251b7bad7f4cdc9671fad7bd32b9a879c9705a25367a33", 115 | "0xa545e0beb4b0e9f62b3e4718684ee7bb5b471ea1e6c00f4f39d464d819c789bb", 116 | "0xac2587b5e376276c0fe6fec5b99538c64cac6fd7178495dc83f76cb71a4e04a8", 117 | "0x7583c07a2c06f98d37f7e9f8c8a336a78814c96b85a5ca8e2b46d2ef42c3274a", 118 | "0xa5ea145810ea5cb3af54f5022127d86190d1c3812037a7475b63d1da36d0deca", 119 | "0x5e6ab651c5244c2701c655ab18a6098f6a6e0fab9c5ad68e8fd2fe40c4d74740", 120 | "0xde9fd756dbfece4ef84d7491a405ae1a955adb3cf00465a6ac102fc273ae6c2c", 121 | "0x2087c35f0207f9b2e2868314ad78b33b079e477b86b76ecbb007af9ac6f527a4", 122 | "0xba70fa1d351ad45fff13e571827e9abd07c16928c097eaab7b465c9a2c3962d7", 123 | "0x6b6ce93bbfa3c460dfb8d19feed44dfd8b27dc6f462161c341ce1a8e30c1e4f5", 124 | "0xa274b195993d5b91c9f1bba9e0ebedf11a5558e03d4f8b90eef701e043f95d11", 125 | "0xafc6eb3ff05c7254e544a201f7c47518c1cf0c7905e06c87648dab6a0cde515d", 126 | "0xb4ab300a00ef3654766c11fe8ab92a3af548a652e7785e9b315d688ab72d0d2e", 127 | "0xac00ad809a1a95d3585c776584f79949b556c87c894bb75dd57deac3da8dd786", 128 | "0x683e1d65ed20b4bd6895e2d7e33fcac13e7a38316f4cdc0ac793e0c10e846a26", 129 | "0xdaf19d17403f7067798652ef62326c928e7caae1dc65af119364cea9651847c5", 130 | "0x2f68b4a365337404e1fc611bbfc3e841d603af623630a82586c7a26fbd2335d7", 131 | "0x080cad38549e8609d5422fdf547c1dc12dccd045f369d8a4ca288bfeba35eeec" 132 | ], 133 | "data": { 134 | "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", 135 | "withdrawal_credentials": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 136 | "amount": "1", 137 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 138 | } 139 | } 140 | ], 141 | "voluntary_exits": [ 142 | { 143 | "message": { 144 | "epoch": "1", 145 | "validator_index": "1" 146 | }, 147 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 148 | } 149 | ], 150 | "sync_aggregate": { 151 | "sync_committee_bits": "0x41157a77520bb661f0763f70142fba246e4c46983310145ff9e0b417bbff29f3ede7f957eb00c50ef39dec4e9172f800b58e2b7d79e5757d47655699a19253d5", 152 | "sync_committee_signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 153 | }, 154 | "execution_payload_header": { 155 | "parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 156 | "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09", 157 | "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 158 | "receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 159 | "logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 160 | "prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 161 | "block_number": "1", 162 | "gas_limit": "1", 163 | "gas_used": "1", 164 | "timestamp": "1", 165 | "extra_data": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 166 | "base_fee_per_gas": "1", 167 | "block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 168 | "transactions_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", 169 | "withdrawals_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" 170 | }, 171 | "bls_to_execution_changes": [ 172 | { 173 | "message": { 174 | "validator_index": "1", 175 | "from_bls_pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", 176 | "to_execution_address": "0xabcf8e0d4e9587369b2301d0790347320302cc09" 177 | }, 178 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 179 | } 180 | ] 181 | } 182 | }, 183 | "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" 184 | } -------------------------------------------------------------------------------- /cmd/test-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "strconv" 10 | 11 | builderApi "github.com/attestantio/go-builder-client/api" 12 | builderSpec "github.com/attestantio/go-builder-client/spec" 13 | eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" 14 | "github.com/attestantio/go-eth2-client/spec/altair" 15 | "github.com/attestantio/go-eth2-client/spec/phase0" 16 | "github.com/ethereum/go-ethereum/common" 17 | "github.com/flashbots/go-boost-utils/ssz" 18 | "github.com/flashbots/mev-boost/server" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var log = logrus.NewEntry(logrus.New()) 23 | 24 | func doGenerateValidator(filePath string, gasLimit uint64, feeRecipient string) { 25 | v := newRandomValidator(gasLimit, feeRecipient) 26 | err := v.SaveValidator(filePath) 27 | if err != nil { 28 | log.WithError(err).Fatal("could not save validator data") 29 | } 30 | log.WithField("file", filePath).Info("saved validator data") 31 | } 32 | 33 | func doRegisterValidator(v validatorPrivateData, boostEndpoint string, builderSigningDomain phase0.Domain) { 34 | message, err := v.PrepareRegistrationMessage(builderSigningDomain) 35 | if err != nil { 36 | log.WithError(err).Fatal("could not prepare registration message") 37 | } 38 | _, err = server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, boostEndpoint+"/eth/v1/builder/validators", "test-cli", nil, message, nil) 39 | if err != nil { 40 | log.WithError(err).Fatal("validator registration not successful") 41 | } 42 | 43 | log.WithError(err).Info("registered validator") 44 | } 45 | 46 | func doGetHeader(v validatorPrivateData, boostEndpoint string, beaconNode Beacon, engineEndpoint string, builderSigningDomain phase0.Domain) builderSpec.VersionedSignedBuilderBid { 47 | // Mergemock needs to call forkchoice update before getHeader, for non-mergemock beacon node this is a no-op 48 | err := beaconNode.onGetHeader() 49 | if err != nil { 50 | log.WithError(err).Fatal("onGetHeader hook failed") 51 | } 52 | 53 | currentBlock, err := beaconNode.getCurrentBeaconBlock() 54 | if err != nil { 55 | log.WithError(err).Fatal("could not retrieve current block hash from beacon endpoint") 56 | } 57 | 58 | var currentBlockHash string 59 | var emptyHash common.Hash 60 | // Beacon does not return block hash pre-bellatrix, fetch it from the engine if that's the case 61 | if currentBlock.BlockHash != emptyHash { 62 | currentBlockHash = currentBlock.BlockHash.String() 63 | } else if currentBlockHash == "" { 64 | block, err := getLatestEngineBlock(engineEndpoint) 65 | if err != nil { 66 | log.WithError(err).Fatal("could not get current block hash") 67 | } 68 | currentBlockHash = block.Hash.String() 69 | } 70 | 71 | uri := fmt.Sprintf("%s/eth/v1/builder/header/%d/%s/%s", boostEndpoint, currentBlock.Slot+1, currentBlockHash, v.Pk.String()) 72 | 73 | var getHeaderResp builderSpec.VersionedSignedBuilderBid 74 | if _, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, uri, "test-cli", nil, nil, &getHeaderResp); err != nil { 75 | log.WithError(err).WithField("currentBlockHash", currentBlockHash).Fatal("could not get header") 76 | } 77 | 78 | if getHeaderResp.Deneb.Message == nil { 79 | log.Fatal("did not receive correct header") 80 | } 81 | log.WithField("header", *getHeaderResp.Deneb.Message).Info("got header from boost") 82 | 83 | ok, err := ssz.VerifySignature(getHeaderResp.Deneb.Message, builderSigningDomain, getHeaderResp.Deneb.Message.Pubkey[:], getHeaderResp.Deneb.Signature[:]) 84 | if err != nil { 85 | log.WithError(err).Fatal("could not verify builder bid signature") 86 | } 87 | if !ok { 88 | log.Fatal("incorrect builder bid signature") 89 | } 90 | 91 | return getHeaderResp 92 | } 93 | 94 | func doGetPayload(v validatorPrivateData, boostEndpoint string, beaconNode Beacon, engineEndpoint string, builderSigningDomain, proposerSigningDomain phase0.Domain) { 95 | header := doGetHeader(v, boostEndpoint, beaconNode, engineEndpoint, builderSigningDomain) 96 | 97 | blindedBeaconBlock := eth2ApiV1Deneb.BlindedBeaconBlock{ 98 | Slot: 0, 99 | ProposerIndex: 0, 100 | ParentRoot: phase0.Root{}, 101 | StateRoot: phase0.Root{}, 102 | Body: ð2ApiV1Deneb.BlindedBeaconBlockBody{ 103 | RANDAOReveal: phase0.BLSSignature{}, 104 | ETH1Data: &phase0.ETH1Data{}, 105 | Graffiti: phase0.Hash32{}, 106 | ProposerSlashings: []*phase0.ProposerSlashing{}, 107 | AttesterSlashings: []*phase0.AttesterSlashing{}, 108 | Attestations: []*phase0.Attestation{}, 109 | Deposits: []*phase0.Deposit{}, 110 | VoluntaryExits: []*phase0.SignedVoluntaryExit{}, 111 | SyncAggregate: &altair.SyncAggregate{}, 112 | ExecutionPayloadHeader: header.Deneb.Message.Header, 113 | }, 114 | } 115 | 116 | signature, err := v.Sign(&blindedBeaconBlock, proposerSigningDomain) 117 | if err != nil { 118 | log.WithError(err).Fatal("could not sign blinded beacon block") 119 | } 120 | 121 | payload := eth2ApiV1Deneb.SignedBlindedBeaconBlock{ 122 | Message: &blindedBeaconBlock, 123 | Signature: signature, 124 | } 125 | var respPayload builderApi.VersionedExecutionPayload 126 | if _, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, boostEndpoint+"/eth/v1/builder/blinded_blocks", "test-cli", nil, payload, &respPayload); err != nil { 127 | log.WithError(err).Fatal("could not get payload") 128 | } 129 | 130 | if respPayload.IsEmpty() { 131 | log.Fatal("did not receive correct payload") 132 | } 133 | log.WithField("payload", respPayload).Info("got payload from mev-boost") 134 | } 135 | 136 | func main() { 137 | generateCommand := flag.NewFlagSet("generate", flag.ExitOnError) 138 | registerCommand := flag.NewFlagSet("register", flag.ExitOnError) 139 | getHeaderCommand := flag.NewFlagSet("getHeader", flag.ExitOnError) 140 | getPayloadCommand := flag.NewFlagSet("getPayload", flag.ExitOnError) 141 | 142 | var validatorDataFile string 143 | envValidatorDataFile := getEnv("VALIDATOR_DATA_FILE", "./validator_data.json") 144 | generateCommand.StringVar(&validatorDataFile, "vd-file", envValidatorDataFile, "Path to validator data file") 145 | registerCommand.StringVar(&validatorDataFile, "vd-file", envValidatorDataFile, "Path to validator data file") 146 | getHeaderCommand.StringVar(&validatorDataFile, "vd-file", envValidatorDataFile, "Path to validator data file") 147 | getPayloadCommand.StringVar(&validatorDataFile, "vd-file", envValidatorDataFile, "Path to validator data file") 148 | 149 | var boostEndpoint string 150 | envBoostEndpoint := getEnv("MEV_BOOST_ENDPOINT", "http://127.0.0.1:18550") 151 | registerCommand.StringVar(&boostEndpoint, "mev-boost", envBoostEndpoint, "Mev boost endpoint") 152 | getHeaderCommand.StringVar(&boostEndpoint, "mev-boost", envBoostEndpoint, "Mev boost endpoint") 153 | getPayloadCommand.StringVar(&boostEndpoint, "mev-boost", envBoostEndpoint, "Mev boost endpoint") 154 | 155 | var beaconEndpoint string 156 | envBeaconEndpoint := getEnv("BEACON_ENDPOINT", "http://localhost:5052") 157 | getHeaderCommand.StringVar(&beaconEndpoint, "bn", envBeaconEndpoint, "Beacon node endpoint") 158 | getPayloadCommand.StringVar(&beaconEndpoint, "bn", envBeaconEndpoint, "Beacon node endpoint") 159 | 160 | var isMergemock bool 161 | getHeaderCommand.BoolVar(&isMergemock, "mm", false, "Use mergemock EL to fake beacon node") 162 | getPayloadCommand.BoolVar(&isMergemock, "mm", false, "Use mergemock EL to fake beacon node") 163 | 164 | var engineEndpoint string 165 | envEngineEndpoint := getEnv("ENGINE_ENDPOINT", "http://localhost:8545") 166 | getHeaderCommand.StringVar(&engineEndpoint, "en", envEngineEndpoint, "Engine endpoint") 167 | getPayloadCommand.StringVar(&engineEndpoint, "en", envEngineEndpoint, "Engine endpoint") 168 | 169 | var genesisValidatorsRootStr string 170 | envGenesisValidatorsRoot := getEnv("GENESIS_VALIDATORS_ROOT", "0x0000000000000000000000000000000000000000000000000000000000000000") 171 | getPayloadCommand.StringVar(&genesisValidatorsRootStr, "genesis-validators-root", envGenesisValidatorsRoot, "Root of genesis validators") 172 | 173 | var genesisForkVersionStr string 174 | envGenesisForkVersion := getEnv("GENESIS_FORK_VERSION", "0x00000000") 175 | registerCommand.StringVar(&genesisForkVersionStr, "genesis-fork-version", envGenesisForkVersion, "hex encoded genesis fork version") 176 | getHeaderCommand.StringVar(&genesisForkVersionStr, "genesis-fork-version", envGenesisForkVersion, "hex encoded genesis fork version") 177 | getPayloadCommand.StringVar(&genesisForkVersionStr, "genesis-fork-version", envGenesisForkVersion, "hex encoded genesis fork version") 178 | 179 | var bellatrixForkVersionStr string 180 | envBellatrixForkVersion := getEnv("BELLATRIX_FORK_VERSION", "0x02000000") 181 | getPayloadCommand.StringVar(&bellatrixForkVersionStr, "bellatrix-fork-version", envBellatrixForkVersion, "hex encoded bellatrix fork version") 182 | 183 | var gasLimit uint64 184 | envGasLimitStr := getEnv("VALIDATOR_GAS_LIMIT", "30000000") 185 | envGasLimit, err := strconv.ParseUint(envGasLimitStr, 10, 64) 186 | if err != nil { 187 | log.WithError(err).Fatal("invalid gas limit specified") 188 | } 189 | generateCommand.Uint64Var(&gasLimit, "gas-limit", envGasLimit, "Gas limit to register the validator with") 190 | 191 | var validatorFeeRecipient string 192 | envValidatorFeeRecipient := getEnv("VALIDATOR_FEE_RECIPIENT", "0x0000000000000000000000000000000000000000") 193 | generateCommand.StringVar(&validatorFeeRecipient, "feeRecipient", envValidatorFeeRecipient, "FeeRecipient to register the validator with") 194 | 195 | flag.Usage = func() { 196 | if _, err := fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [generate|register|getHeader|getPayload]:\n", os.Args[0]); err != nil { 197 | log.Fatal(err) 198 | } 199 | flag.PrintDefaults() 200 | } 201 | 202 | if len(os.Args) < 2 { 203 | flag.Usage() 204 | os.Exit(1) 205 | } 206 | 207 | switch os.Args[1] { 208 | case "generate": 209 | if err := generateCommand.Parse(os.Args[2:]); err != nil { 210 | log.Fatal(err) 211 | } 212 | doGenerateValidator(validatorDataFile, gasLimit, validatorFeeRecipient) 213 | case "register": 214 | if err := registerCommand.Parse(os.Args[2:]); err != nil { 215 | log.Fatal(err) 216 | } 217 | builderSigningDomain, err := server.ComputeDomain(ssz.DomainTypeAppBuilder, genesisForkVersionStr, phase0.Root{}.String()) 218 | if err != nil { 219 | log.WithError(err).Fatal("computing signing domain failed") 220 | } 221 | doRegisterValidator(mustLoadValidator(validatorDataFile), boostEndpoint, builderSigningDomain) 222 | case "getHeader": 223 | if err := getHeaderCommand.Parse(os.Args[2:]); err != nil { 224 | log.Fatal(err) 225 | } 226 | builderSigningDomain, err := server.ComputeDomain(ssz.DomainTypeAppBuilder, genesisForkVersionStr, phase0.Root{}.String()) 227 | if err != nil { 228 | log.WithError(err).Fatal("computing signing domain failed") 229 | } 230 | doGetHeader(mustLoadValidator(validatorDataFile), boostEndpoint, createBeacon(isMergemock, beaconEndpoint, engineEndpoint), engineEndpoint, builderSigningDomain) 231 | case "getPayload": 232 | if err := getPayloadCommand.Parse(os.Args[2:]); err != nil { 233 | log.Fatal(err) 234 | } 235 | builderSigningDomain, err := server.ComputeDomain(ssz.DomainTypeAppBuilder, genesisForkVersionStr, phase0.Root{}.String()) 236 | if err != nil { 237 | log.WithError(err).Fatal("computing signing domain failed") 238 | } 239 | proposerSigningDomain, err := server.ComputeDomain(ssz.DomainTypeBeaconProposer, bellatrixForkVersionStr, genesisValidatorsRootStr) 240 | if err != nil { 241 | log.WithError(err).Fatal("computing signing domain failed") 242 | } 243 | doGetPayload(mustLoadValidator(validatorDataFile), boostEndpoint, createBeacon(isMergemock, beaconEndpoint, engineEndpoint), engineEndpoint, builderSigningDomain, proposerSigningDomain) 244 | default: 245 | log.Info("expected generate|register|getHeader|getPayload subcommand") 246 | os.Exit(1) 247 | } 248 | } 249 | 250 | func getEnv(key, defaultValue string) string { 251 | if value, ok := os.LookupEnv(key); ok { 252 | return value 253 | } 254 | return defaultValue 255 | } 256 | -------------------------------------------------------------------------------- /docs/audit-20220620.md: -------------------------------------------------------------------------------- 1 | # MEV-Boost Security Assessment 2 | 3 | Auditor: [lotusbumi](https://github.com/lotusbumi) 4 | 5 | Start date: 2022-06-20 6 | 7 | MEV-Boost Security assessment for the Flashbots Collective 8 | --- 9 | 10 | ## System overview 11 | 12 | [MEV-Boost](https://github.com/flashbots/mev-boost) is a system created by the Flashbots Collective to allow the out-of-protocol Proposer/Builder separation on PoS Eth once the Merge takes place. By making use of MEV-Boost, solo validators are expected to be able to get a share of the MEV on Ethereum without relying on running MEV strategies by themselves, but rather, receive from different builders bids to include specific block contents on their proposing slot. 13 | 14 | The system consists in different actors: 15 | 16 | - Builders: Block builders receive from different searchers bundles that will be "glued" together to create blocks. After creating them, they send a block header together with a bid value to relayers so that these block headers are exposed to validators. Afterwards, if validators select their bid, builders will unblind the contents of the block so that validators can add them to the canonical chain. 17 | - Relayers: Relayers are actors that save registered validators and receive builders bid to make them available to validators. 18 | - Validator clients: Beacon nodes that run the consensus logic. They register themselves to relayers through MEV-Boost, ask for specific block headers of the slots they propose and sign the block header so that the builder can unblind the block contents to include these contents on the proposed block. 19 | - MEV-Boost: MEV-Boost is a software that receives HTTP requests from the validator client, and send HTTP requests to relayers, handling communications and the logic by which the block header is selected from the different relayers. In the current system, MEV-Boost will always select the block header with the highest bid. 20 | - Execution clients: PoW ETH nodes that run the execution logic. 21 | It is important to note that the specifications are still a work in progress and that both the builder and relay software are still under heavy development. 22 | 23 | ## Trust Assumptions 24 | 25 | Proof of Stake Ethereum roadmap presents a trustless proposer/builder separation protocol. In the meantime, the current system will depend on trust assumptions of the different actors: 26 | 27 | - Validators/MEV-Boost assume: 28 | - Relayers will not send blocks that could make validators get slashed. 29 | - Relayers will not send blocks bigger than the gas limit used in validator registration. 30 | - Relayers will not use a false bid value. 31 | - Relayers assume: 32 | - Validators will send periodically registration information. 33 | - Builders will be online to share the block contents with validators if auction is won. 34 | - Builders will not use a false bid value. 35 | - Builders assume: 36 | - Relayers will not share their blocks with other relayers/builders. 37 | - Relayers will verify that validators commit to a specific `blockhash`. 38 | 39 | --- 40 | 41 | ## Findings 42 | 43 | ### Critical 44 | 45 | None. 46 | 47 | ### High 48 | 49 | None. 50 | 51 | ### Medium 52 | 53 | #### Signing library skips membership check 54 | 55 | *Update: Fixed in [PR21](https://github.com/flashbots/go-boost-utils/pull/21)* 56 | 57 | In the `go-boost-utils` repository, the [`VerifySignature` function](https://github.com/flashbots/go-boost-utils/blob/cbdc1a5f5b7d8a32a41d89a4c1ce19600a6c691f/bls/bls.go#L92-L94) of the bls.go file makes use of the [`Verify` function](https://github.com/supranational/blst/blob/master/bindings/go/blst.go#L413-L424) of the upstream `blst` library but with the `sigGroupCheck` variable [set to `false`](https://github.com/flashbots/go-boost-utils/blob/cbdc1a5f5b7d8a32a41d89a4c1ce19600a6c691f/bls/bls.go#L93). 58 | 59 | The [IETF BLS Signature specification](https://tools.ietf.org/html/draft-irtf-cfrg-bls-signature) points outs about the usage of this variable in [Section 5.3](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature#section-5.3): 60 | 61 | ``` 62 | Some existing implementations skip the signature_subgroup_check 63 | invocation in CoreVerify (Section 2.7), whose purpose is ensuring 64 | that the signature is an element of a prime-order subgroup. This 65 | check is REQUIRED of conforming implementations, for two reasons. 66 | 67 | 1. For most pairing-friendly elliptic curves used in practice, the 68 | pairing operation e (Section 1.3) is undefined when its input 69 | points are not in the prime-order subgroups of E1 and E2. The 70 | resulting behavior is unpredictable, and may enable forgeries. 71 | 72 | 2. Even if the pairing operation behaves properly on inputs that are 73 | outside the correct subgroups, skipping the subgroup check breaks 74 | the strong unforgeability property. 75 | ``` 76 | 77 | Consider ensuring that the `Verify` function is called with the `sigGroupCheck` variable set to `true` to enforce unforgeability of signatures. 78 | 79 | ### Low 80 | 81 | #### Server-Side request forgery via unchecked redirects 82 | 83 | *Update: Fixed in [PR205](https://github.com/flashbots/mev-boost/pull/205)* 84 | 85 | A server side request forgery attack is the ability to force a system to create a new HTTP request by using the victim transport layer. 86 | 87 | A malicious relayer can take advantage of server redirects and trigger [`GET` or `HEAD` requests with empty body to any path or `POST` requests to any path with the initial payload body](https://pkg.go.dev/net/http#Client.Do). This vulnerability can only be exploited if in the local network where the MEV-Boost software runs there is a reachable software with an existing vulnerability that can be triggered with the types of requests that an attacker is allowed to send. 88 | 89 | As in most infrastructures MEV-Boost will have access to execution and validator clients, an underlying vulnerability in these critical systems could be triggered even though these clients are not exposed publicly. 90 | 91 | Consider modifying the [`http.Client.CheckRedirect`](https://pkg.go.dev/net/http#Client) policy to ensure that no redirects will be followed. 92 | 93 | #### Server timeouts and `MaxHeaderBytes` configuration allows denial of service attacks 94 | 95 | *Update: Fixed in [PR210](https://github.com/flashbots/mev-boost/pull/210)* 96 | 97 | Given the current [`http.Server`](https://pkg.go.dev/net/http#Server) timeouts configuration and the default [`MaxHeaderBytes` value in use](https://cs.opensource.google/go/go/+/refs/tags/go1.18.3:src/net/http/server.go;l=855), it is possible for a malicious individual to perform a denial of service attack through HTTP requests on MEV-Boost. 98 | 99 | By leveraging requests that asks for a path comprised of 1Mb of characters, it is possible to consume high amount of resources of the server running MEV-Boost. With the default configuration, we can slow other user's requests to the same MEV-Boost instance up to 15 seconds by only sending 100 requests via 10 threads with an attacker client. 100 | 101 | Consider modifying the [`ReadHeaderTimeout`](https://github.com/flashbots/mev-boost/blob/b93a9e3de1843dd284c9350ac9d2ca4a248e064c/server/service.go#L114) value or the `MaxHeaderBytes` value to ensure the quick return of malicious requests to quickly free resources. Otherwise, consider making these values configurable by a command line flag. Moreover, consider publishing documentation around how to set up a public facing MEV-Boost or strictly documenting that this system should be only reachable by trusted parties. 102 | 103 | ### Notes 104 | 105 | #### BLS library missing documentation 106 | 107 | *Update: Fixed in [PR205](https://github.com/flashbots/mev-boost/pull/205)* 108 | 109 | As explained in the [IETF BLS Signature specification](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-bls-signature#section-3), BLS signature schemes can be basic, message augmentation or proof of possession. Each one of these schemes work differently and requires different behavior for the client making use of the bslt library. 110 | 111 | The client built in the bls.go file of the go-boost-repository uses a [domain tag separator value](https://github.com/flashbots/go-boost-utils/blob/cbdc1a5f5b7d8a32a41d89a4c1ce19600a6c691f/bls/bls.go#L12) corresponding to the proof of possession scheme. However, the library never make use of the PopProve and PopVerify functions which are the ones used in the proof of possession scheme. 112 | 113 | Moreover, the bls client library does not support aggregated signatures, as the verification is only done with one message per verification. 114 | 115 | Consider clearly documenting the expected behavior of the bls client library, the deviations from the specification and the security properties of the used scheme. 116 | 117 | #### Client timeout can be set to insecure values 118 | 119 | *Update: Fixed in [PR210](https://github.com/flashbots/mev-boost/pull/210)* 120 | 121 | Even though MEV-Boost has a command line flag to set the timeout settings for the `http.Client` and that the default value is a "safe" value of two seconds, it is possible that a user sets this value to 0, disabling the client timeout and opening the door to being attacked by a malicious relayer that stalls the communication. 122 | 123 | This is specially important during [`handleGetHeader` function execution](https://github.com/flashbots/mev-boost/blob/9a701c1b4d625d2e7f83fef2971af54ca856facd/server/service.go#L233) as any of the relayers can [block the execution](https://github.com/flashbots/mev-boost/blob/c47fa3e4739cc858ce0eabbf31a2131fce96fbf6/server/service.go#L298) until every single request to the different relayers return. 124 | 125 | Given the [proposer boost](https://ethresear.ch/t/change-fork-choice-rule-to-mitigate-balancing-and-reorging-attacks/11127), it is recommended for validators to include the block before 4 seconds into the slot or they will be probably be reorganized out of the canonical chain. 126 | 127 | Consider checking that the timeout cannot be set to 0. Otherwise, consider documenting this behavior so that solo validators can understand the implications of this choice. 128 | 129 | #### JSON Decoder allows extra information to be loaded in memory 130 | 131 | *Update: Fixed in [PR211](https://github.com/flashbots/mev-boost/pull/211)* 132 | 133 | In the [`handleGetPayload`](https://github.com/flashbots/mev-boost/blob/9a701c1b4d625d2e7f83fef2971af54ca856facd/server/service.go#L343) and [`handleRegisterValidator`](https://github.com/flashbots/mev-boost/blob/9a701c1b4d625d2e7f83fef2971af54ca856facd/server/service.go#L188) functions of the `service.go` file, the request payloads are processed by a `Decoder` without making use of the [` DisallowUnknownFields` function](https://pkg.go.dev/encoding/json#Decoder.DisallowUnknownFields), which would allow the `Decoder` to return an error when the destination is a struct and the input contains object keys which do not match any non-ignored, exported fields in the destination. 134 | 135 | The usage of `DisallowUnknownFields` is recommended to avoid loading to memory and consuming resources decoding an invalid input. 136 | 137 | --- 138 | 139 | ## Appendix A: Specification improvements 140 | 141 | Even though this security audit was based on the MEV-Boost software, some security considerations were discovered for either the builder or relay software. A list of these is created to serve as base for further refinements and improvements of the specification of these actors to avoid these issues to trickle down on the implementations: 142 | 143 | #### Relayer `SignedBlindedBeaconBlock` validation 144 | 145 | To be slashed, a validator would need to sign two competing blocks for the block of the slot they are in charge of producing. 146 | 147 | As such, relayers need to validate the `SignedBlindedBeaconBlock` signed by validators to be of the correct `Slot` and `ProposerIndex` before showing the unblinded block to the validator. If this is not checked, validators will be able to access the block contents and use these transactions to craft a new block, where they extract the MEV for themselves. 148 | 149 | #### Validator `ExecutionPayloadHeader` validation 150 | 151 | Validators should propose their block as in the first 4 seconds of the slot. Failure in doing so will result in higher probabilities of the block being re-organized due to the new [proposer boost mechanism](https://ethresear.ch/t/change-fork-choice-rule-to-mitigate-balancing-and-reorging-attacks/11127) and the incentive of MEV extractors to create reorgs so that they can steal MEV extraction opportunities. 152 | 153 | The `GasLimit` becomes then an important value, given the fact that if this number is high enough to allow blocks that are too big for the validator hardware to timely process the block contents it will probably end up in a missing opportunity for creation of a block in the canonical chain. 154 | 155 | As such, validators should verify that the `GasLimit` value of the `ExecutionPayloadHeader` is lower than the gas limit value they used to register themselves to the relay network, which needs to be set to a sensible value that allows a validator hardware to timely create and validate the block. 156 | 157 | --- 158 | 159 | ## Known-Issues 160 | 161 | - [Validator privacy](https://hackmd.io/@paulhauner/H1XifIQ_t#Privacy-vs-Simplicity) 162 | - [Unconditional payments](https://github.com/flashbots/mev-boost/issues/109) 163 | - [Late block proposal](https://github.com/flashbots/mev-boost/issues/111) 164 | - [Header and bid publicly accessible](https://github.com/flashbots/mev-boost/issues/112) 165 | - [Transaction cancellation](https://github.com/flashbots/mev-boost/issues/113) 166 | - [Relays gossiping payloads between each other & multiple relays with the same payload](https://github.com/flashbots/mev-boost/issues/122) 167 | - [Clarify relationship b/t builder, relay and proposer](https://github.com/ethereum/builder-specs/issues/23) 168 | - [Value can be spoofed by relays](https://ethresear.ch/t/mev-boost-merge-ready-flashbots-architecture/11177#malicious-relay-6) 169 | - [Not validating relayer key](https://github.com/flashbots/mev-boost/pull/143/files) 170 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= 2 | github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= 3 | github.com/VictoriaMetrics/metrics v1.40.1 h1:FrF5uJRpIVj9fayWcn8xgiI+FYsKGMslzPuOXjdeyR4= 4 | github.com/VictoriaMetrics/metrics v1.40.1/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA= 5 | github.com/attestantio/go-builder-client v0.7.2 h1:bOrtysEIZd9bEM+mAeT6OtAo6LSAft/qylBLwFoFwZ0= 6 | github.com/attestantio/go-builder-client v0.7.2/go.mod h1:+NADxbaknI5yxl+0mCkMa/VciVsesxRMGNP/poDfV08= 7 | github.com/attestantio/go-eth2-client v0.27.1 h1:g7bm+gG/p+gfzYdEuxuAepVWYb8EO+2KojV5/Lo2BxM= 8 | github.com/attestantio/go-eth2-client v0.27.1/go.mod h1:fvULSL9WtNskkOB4i+Yyr6BKpNHXvmpGZj9969fCrfY= 9 | github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= 10 | github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 11 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 12 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/consensys/bavard v0.1.30 h1:wwAj9lSnMLFXjEclKwyhf7Oslg8EoaFz9u1QGgt0bsk= 14 | github.com/consensys/bavard v0.1.30/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= 15 | github.com/consensys/gnark-crypto v0.17.0 h1:vKDhZMOrySbpZDCvGMOELrHFv/A9mJ7+9I8HEfRZSkI= 16 | github.com/consensys/gnark-crypto v0.17.0/go.mod h1:A2URlMHUT81ifJ0UlLzSlm7TmnE3t7VxEThApdMukJw= 17 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 18 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 19 | github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= 20 | github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= 25 | github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 26 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= 27 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= 28 | github.com/emicklei/dot v1.8.0 h1:HnD60yAKFAevNeT+TPYr9pb8VB9bqdeSo0nzwIW6IOI= 29 | github.com/emicklei/dot v1.8.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= 30 | github.com/ethereum/c-kzg-4844 v1.0.3 h1:IEnbOHwjixW2cTvKRUlAAUOeleV7nNM/umJR+qy4WDs= 31 | github.com/ethereum/c-kzg-4844 v1.0.3/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 32 | github.com/ethereum/go-ethereum v1.15.9 h1:bRra1zi+/q+qyXZ6fylZOrlaF8kDdnlTtzNTmNHfX+g= 33 | github.com/ethereum/go-ethereum v1.15.9/go.mod h1:+S9k+jFzlyVTNcYGvqFhzN/SFhI6vA+aOY4T5tLSPL0= 34 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= 35 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 36 | github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= 37 | github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= 38 | github.com/flashbots/go-boost-utils v1.10.0 h1:AGihhYtOjGF/efaBoQefYfmqzKsba6Y7SlfEK2iEPGY= 39 | github.com/flashbots/go-boost-utils v1.10.0/go.mod h1:vCtklzlENAGLqDrf6JteivgANjzXFqVSQQ3LtoQxyV8= 40 | github.com/flashbots/go-utils v0.10.0 h1:75XWewRO5GIhdLn8+vqdzzuoqJh+j8wN54A++Id7W0Y= 41 | github.com/flashbots/go-utils v0.10.0/go.mod h1:i4xxEB6sHDFfNWEIfh+rP6nx3LxynEn8AOZa05EYgwA= 42 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 43 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 44 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 45 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 46 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 47 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 48 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 49 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 50 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 51 | github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY= 52 | github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 53 | github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= 54 | github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= 55 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 56 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 57 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 58 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 60 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 61 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 62 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 63 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 64 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 65 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 66 | github.com/huandu/go-clone v1.7.2 h1:3+Aq0Ed8XK+zKkLjE2dfHg0XrpIfcohBE1K+c8Usxoo= 67 | github.com/huandu/go-clone v1.7.2/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= 68 | github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= 69 | github.com/huandu/go-clone/generic v1.6.0/go.mod h1:xgd9ZebcMsBWWcBx5mVMCoqMX24gLWr5lQicr+nVXNs= 70 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 71 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 72 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 73 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 74 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 75 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 76 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 77 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 78 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= 79 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= 80 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 81 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 82 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 83 | github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 84 | github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 85 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 86 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 87 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 88 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 89 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 90 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 91 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 92 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 93 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 94 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 95 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15 h1:lC8kiphgdOBTcbTvo8MwkvpKjO0SlAgjv4xIK5FGJ94= 99 | github.com/prysmaticlabs/go-bitfield v0.0.0-20240618144021-706c95b2dd15/go.mod h1:8svFBIKKu31YriBG/pNizo9N0Jr9i5PQ+dFkxWg3x5k= 100 | github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= 101 | github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= 102 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 103 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 104 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 105 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 106 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 107 | github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= 108 | github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= 109 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 110 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 111 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 112 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 113 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= 114 | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= 115 | github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= 116 | github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= 117 | github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= 118 | github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 119 | github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 120 | github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 121 | github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= 122 | github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 123 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 124 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 125 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 126 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 127 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 128 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 129 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= 130 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 131 | github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09 h1:QVxbx5l/0pzciWYOynixQMtUhPYC3YKD6EcUlOsgGqw= 132 | github.com/timewasted/go-accept-headers v0.0.0-20130320203746-c78f304b1b09/go.mod h1:Uy/Rnv5WKuOO+PuDhuYLEpUiiKIZtss3z519uk67aF0= 133 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 134 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 135 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 136 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 137 | github.com/trailofbits/go-fuzz-utils v0.0.0-20240830175354-474de707d2aa h1:jXdW82tOv+Bvh6adpc4kqcV6yuy5KLw/xzJmZBtZIdw= 138 | github.com/trailofbits/go-fuzz-utils v0.0.0-20240830175354-474de707d2aa/go.mod h1:/7KgvY5ghyUsjocUh9dMkLCwKtNxqe0kWl5SIdpLtO8= 139 | github.com/urfave/cli/v3 v3.2.0 h1:m8WIXY0U9LCuUl5r+0fqLWDhNYWt6qvlW+GcF4EoXf8= 140 | github.com/urfave/cli/v3 v3.2.0/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 141 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 142 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 143 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 144 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 145 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 146 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 147 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 148 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 149 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 150 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 151 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 152 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 153 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 154 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 155 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 156 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 157 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 158 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 159 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 161 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 163 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 164 | golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= 165 | golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= 166 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 167 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 168 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 169 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 170 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 171 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 172 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 173 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 174 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 175 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 176 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 177 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 178 | --------------------------------------------------------------------------------