├── .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 |
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 |
--------------------------------------------------------------------------------