├── .github ├── pull_request_template.md └── workflows │ ├── checks.yml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cli.dockerfile ├── cmd ├── receiver-proxy │ └── main.go ├── sender-proxy │ └── main.go └── test-tx-sender │ └── main.go ├── common ├── logging.go └── vars.go ├── go.mod ├── go.sum ├── proxy ├── api_validate.go ├── archive.go ├── confighub.go ├── html │ └── index.html ├── metrics.go ├── receiver_api.go ├── receiver_proxy.go ├── receiver_proxy_test.go ├── receiver_servers.go ├── sender_proxy.go ├── sharing.go └── utils.go ├── receiver.dockerfile └── staticcheck.conf /.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` 19 | * [ ] `go mod tidy` 20 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 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: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | 23 | - name: Run unit tests and generate the coverage report 24 | run: make test-race 25 | 26 | lint: 27 | name: Lint 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Set up Go 31 | uses: actions/setup-go@v3 32 | with: 33 | go-version: ^1.24 34 | id: go 35 | 36 | - name: Check out code into the Go module directory 37 | uses: actions/checkout@v2 38 | 39 | - name: Install gofumpt 40 | run: go install mvdan.cc/gofumpt@v0.4.0 41 | 42 | - name: Install staticcheck 43 | run: go install honnef.co/go/tools/cmd/staticcheck@2025.1.1 44 | 45 | - name: Install golangci-lint 46 | run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8 47 | 48 | - name: Install NilAway 49 | run: go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7 50 | 51 | - name: Lint 52 | run: make lint 53 | 54 | - name: Ensure go mod tidy runs without changes 55 | run: | 56 | go mod tidy 57 | git update-index -q --really-refresh 58 | git diff-index HEAD 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - 'v*' 7 | workflow_dispatch: 8 | 9 | env: 10 | REGISTRY: ghcr.io 11 | IMAGE_NAME: ${{ github.repository }} 12 | KANIKO_VERSION: gcr.io/kaniko-project/executor@sha256:9e69fd4330ec887829c780f5126dd80edc663df6def362cd22e79bcdf00ac53f 13 | 14 | jobs: 15 | build-binary: 16 | name: Build binary 17 | runs-on: ubuntu-latest 18 | container: 19 | image: golang:1.24rc2-bullseye@sha256:236da40764c1bcf469fcaf6ca225ca881c3f06cbd1934e392d6e4af3484f6cac 20 | steps: 21 | - name: Checkout sources 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Build binaries 27 | run: make build 28 | 29 | - name: Upload artifacts 30 | uses: actions/upload-artifact@v4.5.0 31 | with: 32 | path: build/ 33 | 34 | build-receiver-image: 35 | name: Build Receiver Docker Image 36 | runs-on: ubuntu-latest 37 | permissions: 38 | contents: read 39 | packages: write 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 46 | 47 | - name: Extract metadata 48 | id: meta 49 | uses: docker/metadata-action@v5 50 | with: 51 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-receiver 52 | tags: | 53 | type=sha 54 | type=semver,pattern={{version}} 55 | type=semver,pattern={{major}}.{{minor}} 56 | 57 | - name: Login to GitHub Container Registry 58 | uses: docker/login-action@v3 59 | with: 60 | registry: ${{ env.REGISTRY }} 61 | username: ${{ github.actor }} 62 | password: ${{ secrets.GITHUB_TOKEN }} 63 | 64 | - name: Build and Push with Kaniko 65 | run: | 66 | mkdir -p /home/runner/.docker 67 | 68 | echo '{"auths":{"${{ env.REGISTRY }}":{"auth":"'$(echo -n "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" | base64)'"}}}'> /home/runner/.docker/config.json 69 | 70 | docker run \ 71 | -v ${{ github.workspace }}:/workspace \ 72 | -v /home/runner/.docker/config.json:/kaniko/.docker/config.json \ 73 | ${{ env.KANIKO_VERSION }} \ 74 | --context /workspace \ 75 | --dockerfile /workspace/receiver.dockerfile \ 76 | --reproducible \ 77 | --cache=true \ 78 | --cache-repo ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-cache \ 79 | --destination ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-receiver:${{ steps.meta.outputs.version }} \ 80 | ${{ steps.meta.outputs.tags }} 81 | 82 | github-release: 83 | runs-on: ubuntu-latest 84 | needs: [build-binary, build-receiver-image] 85 | steps: 86 | - name: Checkout sources 87 | uses: actions/checkout@v4 88 | 89 | - name: Create release 90 | id: create_release 91 | uses: actions/create-release@v1 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | with: 95 | tag_name: ${{ github.ref }} 96 | release_name: ${{ github.ref }} 97 | draft: false 98 | prerelease: false 99 | -------------------------------------------------------------------------------- /.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 | # IDE 18 | .vscode 19 | .idea 20 | # Builds 21 | /build 22 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - cyclop 5 | - forbidigo 6 | - funlen 7 | - gci 8 | - gochecknoglobals 9 | - gochecknoinits 10 | - gocritic 11 | - godot 12 | - godox 13 | - gomnd 14 | - mnd 15 | - lll 16 | - nestif 17 | - nilnil 18 | - nlreturn 19 | - noctx 20 | - nonamedreturns 21 | - paralleltest 22 | - revive 23 | - testpackage 24 | - unparam 25 | - varnamelen 26 | - wrapcheck 27 | - wsl 28 | - exhaustruct 29 | - depguard 30 | - err113 31 | - gocognit 32 | - nakedret 33 | - tagliatelle 34 | - usetesting 35 | 36 | # 37 | # Disabled because of generics: 38 | # 39 | - contextcheck 40 | - rowserrcheck 41 | - sqlclosecheck 42 | - wastedassign 43 | 44 | # 45 | # Disabled because deprecated: 46 | # 47 | - execinquery 48 | - exportloopref 49 | 50 | linters-settings: 51 | # 52 | # The G108 rule throws a false positive. We're not actually vulnerable. If 53 | # you're not careful the profiling endpoint is automatically exposed on 54 | # /debug/pprof if you import net/http/pprof. See this link: 55 | # 56 | # https://mmcloughlin.com/posts/your-pprof-is-showing 57 | # 58 | gosec: 59 | excludes: 60 | - G108 61 | 62 | gofumpt: 63 | extra-rules: true 64 | 65 | exhaustruct: 66 | exclude: 67 | # 68 | # Because it's easier to read without the other fields. 69 | # 70 | - 'GetPayloadsFilters' 71 | 72 | # 73 | # Structures outside our control that have a ton of settings. It doesn't 74 | # make sense to specify all of the fields. 75 | # 76 | - 'cobra.Command' 77 | - 'database.*Entry' 78 | - 'http.Server' 79 | - 'logrus.*Formatter' 80 | - 'Options' # redis 81 | 82 | # 83 | # Excluded because there are private fields (not capitalized) that are 84 | # not initialized. If possible, I think these should be altered. 85 | # 86 | - 'Datastore' 87 | - 'Housekeeper' 88 | - 'MockBeaconClient' 89 | - 'RelayAPI' 90 | - 'Webserver' 91 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.24 AS builder 3 | ARG VERSION 4 | WORKDIR /build 5 | ADD go.mod /build/ 6 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux \ 7 | go mod download 8 | ADD . /build/ 9 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux \ 10 | go build \ 11 | -trimpath \ 12 | -ldflags "-s -X main.version=${VERSION}" \ 13 | -v \ 14 | -o sender-proxy \ 15 | cmd/sender-proxy/main.go 16 | 17 | FROM alpine:latest 18 | WORKDIR /app 19 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 20 | COPY --from=builder /build/sender-proxy /app/sender-proxy 21 | ENV LISTEN_ADDR=":8080" 22 | EXPOSE 8080 23 | CMD ["/app/sender-proxy"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2024 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Heavily inspired by Lighthouse: https://github.com/sigp/lighthouse/blob/stable/Makefile 2 | # and Reth: https://github.com/paradigmxyz/reth/blob/main/Makefile 3 | .DEFAULT_GOAL := help 4 | 5 | VERSION := $(shell git describe --tags --always --dirty="-dev") 6 | 7 | ##@ Help 8 | 9 | .PHONY: help 10 | help: ## Display this help. 11 | @awk 'BEGIN {FS = ":.*##"; printf "Usage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 12 | 13 | .PHONY: v 14 | v: ## Show the version 15 | @echo "Version: ${VERSION}" 16 | 17 | ##@ Build 18 | 19 | .PHONY: clean 20 | clean: ## Clean the build directory 21 | rm -rf build/ 22 | 23 | .PHONY: build 24 | build: ## Build the HTTP server 25 | @mkdir -p ./build 26 | go build -trimpath -ldflags "-X github.com/flashbots/tdx-orderflow-proxy/common.Version=${VERSION}" -v -o ./build/sender-proxy cmd/sender-proxy/main.go 27 | go build -trimpath -ldflags "-X github.com/flashbots/tdx-orderflow-proxy/common.Version=${VERSION}" -v -o ./build/receiver-proxy cmd/receiver-proxy/main.go 28 | go build -trimpath -ldflags "-X github.com/flashbots/tdx-orderflow-proxy/common.Version=${VERSION}" -v -o ./build/test-orderflow-sender cmd/test-tx-sender/main.go 29 | 30 | .PHONY: build-receiver-proxy 31 | build-receiver-proxy: ## Build only the receiver-proxy 32 | @mkdir -p ./build 33 | CGO_ENABLED=0 GOOS=linux go build \ 34 | -trimpath \ 35 | -ldflags "-s -w -buildid= -X github.com/flashbots/tdx-orderflow-proxy/common.Version=${VERSION}" \ 36 | -v -o ./build/receiver-proxy \ 37 | cmd/receiver-proxy/main.go 38 | 39 | ##@ Test & Development 40 | 41 | .PHONY: test 42 | test: ## Run tests 43 | go test ./... 44 | 45 | .PHONY: test-race 46 | test-race: ## Run tests with race detector 47 | go test -race ./... 48 | 49 | .PHONY: lint 50 | lint: ## Run linters 51 | gofmt -d -s . 52 | gofumpt -d -extra . 53 | go vet ./... 54 | staticcheck ./... 55 | golangci-lint run 56 | # nilaway ./... 57 | 58 | .PHONY: fmt 59 | fmt: ## Format the code 60 | gofmt -s -w . 61 | gci write . 62 | gofumpt -w -extra . 63 | go mod tidy 64 | 65 | .PHONY: gofumpt 66 | gofumpt: ## Run gofumpt 67 | gofumpt -l -w -extra . 68 | 69 | .PHONY: lt 70 | lt: lint test ## Run linters and tests 71 | 72 | .PHONY: cover 73 | cover: ## Run tests with coverage 74 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 75 | go tool cover -func /tmp/go-sim-lb.cover.tmp 76 | unlink /tmp/go-sim-lb.cover.tmp 77 | 78 | .PHONY: cover-html 79 | cover-html: ## Run tests with coverage and open the HTML report 80 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 81 | go tool cover -html=/tmp/go-sim-lb.cover.tmp 82 | unlink /tmp/go-sim-lb.cover.tmp 83 | 84 | .PHONY: docker 85 | docker: 86 | DOCKER_BUILDKIT=1 docker build \ 87 | --platform linux/amd64 \ 88 | --build-arg VERSION=${VERSION} \ 89 | --file Dockerfile \ 90 | --tag tdx-orderflow-proxy-sender-proxy \ 91 | . 92 | 93 | DOCKER_BUILDKIT=1 docker build \ 94 | --platform linux/amd64 \ 95 | --build-arg VERSION=${VERSION} \ 96 | --file receiver.dockerfile \ 97 | --tag tdx-orderflow-proxy-receiver-proxy \ 98 | . 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # orderflow-proxy 2 | 3 | [![Goreport status](https://goreportcard.com/badge/github.com/flashbots/tdx-orderflow-proxy)](https://goreportcard.com/report/github.com/flashbots/go-template) 4 | [![Test status](https://github.com/flashbots/tdx-orderflow-proxy/actions/workflows/checks.yml/badge.svg?branch=main)](https://github.com/flashbots/go-template/actions?query=workflow%3A%22Checks%22) 5 | 6 | ## Getting started 7 | 8 | **Build** 9 | 10 | ```bash 11 | make build 12 | ``` 13 | 14 | There are two separate programs in this repo: 15 | * receiver proxy that should be part of tdx image 16 | * sender proxy that is part of infra that sends orderflow to all peers 17 | 18 | ## Run receiver proxy 19 | 20 | Receiver proxy will: 21 | 22 | * generate SSL certificate 23 | * generate orderflow signer 24 | * create 2 input servers serving TLS with that certificate (local-listen-addr, public-listen-addr) 25 | * create 1 local http server serving /cert (cert-listen-addr) 26 | * create metrics server (metric-addr) 27 | * proxy requests to local builder 28 | * proxy local request to other builders in the network 29 | * archive local requests by sending them to archive endpoint 30 | 31 | Flags for the receiver proxy 32 | 33 | ``` 34 | ./build/receiver-proxy -h 35 | NAME: 36 | receiver-proxy - Serve API, and metrics 37 | 38 | USAGE: 39 | receiver-proxy [global options] command [command options] 40 | 41 | COMMANDS: 42 | help, h Shows a list of commands or help for one command 43 | 44 | GLOBAL OPTIONS: 45 | --local-listen-addr value address to listen on for orderflow proxy API for external users and local operator (default: "127.0.0.1:443") [$LOCAL_LISTEN_ADDR] 46 | --public-listen-addr value address to listen on for orderflow proxy API for other network participants (default: "127.0.0.1:5544") [$PUBLIC_LISTEN_ADDR] 47 | --cert-listen-addr value address to listen on for orderflow proxy serving its SSL certificate on /cert (default: "127.0.0.1:14727") [$CERT_LISTEN_ADDR] 48 | --builder-endpoint value address to send local orderflow to (default: "http://127.0.0.1:8645") [$BUILDER_ENDPOINT] 49 | --rpc-endpoint value address of the node RPC that supports eth_blockNumber (default: "http://127.0.0.1:8545") [$RPC_ENDPOINT] 50 | --builder-confighub-endpoint value address of the builder config hub endpoint (directly or using the cvm-proxy) (default: "http://127.0.0.1:14892") [$BUILDER_CONFIGHUB_ENDPOINT] 51 | --orderflow-archive-endpoint value address of the orderflow archive endpoint (block-processor) (default: "http://127.0.0.1:14893") [$ORDERFLOW_ARCHIVE_ENDPOINT] 52 | --flashbots-orderflow-signer-address value orderflow from Flashbots will be signed with this address (default: "0x5015Fa72E34f75A9eC64f44a4Fcf0837919D1bB7") [$FLASHBOTS_ORDERFLOW_SIGNER_ADDRESS] 53 | --max-request-body-size-bytes value Maximum size of the request body, if 0 default will be used (default: 0) [$MAX_REQUEST_BODY_SIZE_BYTES] 54 | --connections-per-peer value Number of parallel connections for each peer and archival RPC (default: 10) [$CONN_PER_PEER] 55 | --max-local-requests-per-second value Maximum number of unique local requests per second (default: 100) [$MAX_LOCAL_RPS] 56 | --cert-duration value generated certificate duration (default: 8760h0m0s) [$CERT_DURATION] 57 | --cert-hosts value [ --cert-hosts value ] generated certificate hosts (default: "127.0.0.1", "localhost") [$CERT_HOSTS] 58 | --metrics-addr value address to listen on for Prometheus metrics (metrics are served on $metrics-addr/metrics) (default: "127.0.0.1:8090") [$METRICS_ADDR] 59 | --log-json log in JSON format (default: false) [$LOG_JSON] 60 | --log-debug log debug messages (default: false) [$LOG_DEBUG] 61 | --log-uid generate a uuid and add to all log messages (default: false) [$LOG_UID] 62 | --log-service value add 'service' tag to logs (default: "tdx-orderflow-proxy-receiver") [$LOG_SERVICE] 63 | --pprof enable pprof debug endpoint (pprof is served on $metrics-addr/debug/pprof/*) (default: false) [$PPROF] 64 | --help, -h show help 65 | ``` 66 | 67 | Additional receiver configuration environment variables: 68 | 69 | * `HTTP_READ_TIMEOUT_SEC` - timeout for reading the request, default 60 seconds 70 | * `HTTP_WRITE_TIMEOUT_SEC` - timeout for writing the response, default 30 seconds 71 | * `HTTP_IDLE_TIMEOUT_SEC` - timeout for idle connection, default 1 hour (3600 seconds) 72 | 73 | ## Run sender proxy 74 | 75 | Sender proxy will: 76 | * listen for http requests 77 | * sign request with `orderflow-signer-key` 78 | * proxy them to the peers received form builder config hub 79 | 80 | ``` 81 | ./build/sender-proxy -h 82 | NAME: 83 | sender-proxy - Serve API, and metrics 84 | 85 | USAGE: 86 | sender-proxy [global options] command [command options] 87 | 88 | COMMANDS: 89 | help, h Shows a list of commands or help for one command 90 | 91 | GLOBAL OPTIONS: 92 | --listen-address value address to listen on for requests (default: "127.0.0.1:8080") [$LISTEN_ADDRESS] 93 | --builder-confighub-endpoint value address of the builder config hub endpoint (directly or using the cvm-proxy) (default: "http://127.0.0.1:14892") [$BUILDER_CONFIGHUB_ENDPOINT] 94 | --orderflow-signer-key value orderflow will be signed with this address (default: "0xfb5ad18432422a84514f71d63b45edf51165d33bef9c2bd60957a48d4c4cb68e") [$ORDERFLOW_SIGNER_KEY] 95 | --max-request-body-size-bytes value Maximum size of the request body, if 0 default will be used (default: 0) [$MAX_REQUEST_BODY_SIZE_BYTES] 96 | --connections-per-peer value Number of parallel connections for each peer (default: 10) [$CONN_PER_PEER] 97 | --metrics-addr value address to listen on for Prometheus metrics (metrics are served on $metrics-addr/metrics) (default: "127.0.0.1:8090") [$METRICS_ADDR] 98 | --log-json log in JSON format (default: false) [$LOG_JSON] 99 | --log-debug log debug messages (default: false) [$LOG_DEBUG] 100 | --log-uid generate a uuid and add to all log messages (default: false) [$LOG_UID] 101 | --log-service value add 'service' tag to logs (default: "tdx-orderflow-proxy-sender") [$LOG_SERVICE] 102 | --pprof enable pprof debug endpoint (pprof is served on $metrics-addr/debug/pprof/*) (default: false) [$PPROF] 103 | --help, -h show help 104 | ``` 105 | -------------------------------------------------------------------------------- /cli.dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.23 AS builder 3 | ARG VERSION 4 | WORKDIR /build 5 | ADD go.mod /build/ 6 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux \ 7 | go mod download 8 | ADD . /build/ 9 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 GOOS=linux \ 10 | go build \ 11 | -trimpath \ 12 | -ldflags "-s -X main.version=${VERSION}" \ 13 | -v \ 14 | -o your-project \ 15 | cmd/cli/main.go 16 | 17 | FROM alpine:latest 18 | WORKDIR /app 19 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 20 | COPY --from=builder /build/your-project /app/your-project 21 | CMD ["/app/your-project"] 22 | -------------------------------------------------------------------------------- /cmd/receiver-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "net/http/pprof" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/VictoriaMetrics/metrics" 15 | eth "github.com/ethereum/go-ethereum/common" 16 | "github.com/flashbots/tdx-orderflow-proxy/common" 17 | "github.com/flashbots/tdx-orderflow-proxy/proxy" 18 | "github.com/google/uuid" 19 | "github.com/urfave/cli/v2" // imports as package "cli" 20 | ) 21 | 22 | const ( 23 | flagUserListenAddr = "user-listen-addr" 24 | flagSystemListenAddr = "system-listen-addr" 25 | flagCertListenAddr = "cert-listen-addr" 26 | flagMaxUserRPS = "max-user-requests-per-second" 27 | 28 | flagCertPath = "cert-path" 29 | flagCertKeyPath = "cert-key-path" 30 | ) 31 | 32 | var flags = []cli.Flag{ 33 | // Servers config (NEW) 34 | &cli.StringFlag{ 35 | Name: flagUserListenAddr, 36 | Value: "127.0.0.1:443", 37 | Usage: "server to receive orderflow from external users/public", 38 | EnvVars: []string{"USER_LISTEN_ADDR"}, 39 | }, 40 | &cli.StringFlag{ 41 | Name: flagSystemListenAddr, 42 | Value: "127.0.0.1:5544", 43 | Usage: "server to receive orderflow from BuilderNet network", 44 | EnvVars: []string{"SYSTEM_LISTEN_ADDR"}, 45 | }, 46 | &cli.StringFlag{ 47 | Name: flagCertListenAddr, 48 | Value: "127.0.0.1:14727", 49 | Usage: "address to listen on for orderflow proxy serving its SSL certificate on /cert", 50 | EnvVars: []string{"CERT_LISTEN_ADDR"}, 51 | }, 52 | 53 | // Connections to Builder, BuilderHub, RPC and block-processor 54 | &cli.StringFlag{ 55 | Name: "builder-endpoint", 56 | Value: "http://127.0.0.1:8645", 57 | Usage: "address to send local ordeflow to", 58 | EnvVars: []string{"BUILDER_ENDPOINT"}, 59 | }, 60 | &cli.StringFlag{ 61 | Name: "rpc-endpoint", 62 | Value: "http://127.0.0.1:8545", 63 | Usage: "address of the node RPC that supports eth_blockNumber", 64 | EnvVars: []string{"RPC_ENDPOINT"}, 65 | }, 66 | &cli.StringFlag{ 67 | Name: "builder-confighub-endpoint", 68 | Value: "http://127.0.0.1:14892", 69 | Usage: "address of the builder config hub endpoint (directly or using the cvm-proxy)", 70 | EnvVars: []string{"BUILDER_CONFIGHUB_ENDPOINT"}, 71 | }, 72 | &cli.StringFlag{ 73 | Name: "orderflow-archive-endpoint", 74 | Value: "http://127.0.0.1:14893", 75 | Usage: "address of the orderflow archive endpoint (block-processor)", 76 | EnvVars: []string{"ORDERFLOW_ARCHIVE_ENDPOINT"}, 77 | }, 78 | &cli.IntFlag{ 79 | Name: "archive-worker-count", 80 | Value: 5, 81 | Usage: "number of parallel workers sending orderflow to archive", 82 | EnvVars: []string{"ARCHIVE_WORKER_COUNT"}, 83 | }, 84 | 85 | // Various configs 86 | &cli.StringFlag{ 87 | Name: "flashbots-orderflow-signer-address", 88 | Value: "0x5015Fa72E34f75A9eC64f44a4Fcf0837919D1bB7", 89 | Usage: "orderflow from Flashbots will be signed with this address", 90 | EnvVars: []string{"FLASHBOTS_ORDERFLOW_SIGNER_ADDRESS"}, 91 | }, 92 | &cli.Int64Flag{ 93 | Name: "max-request-body-size-bytes", 94 | Value: 0, 95 | Usage: "Maximum size of the request body, if 0 default will be used", 96 | EnvVars: []string{"MAX_REQUEST_BODY_SIZE_BYTES"}, 97 | }, 98 | &cli.IntFlag{ 99 | Name: "connections-per-peer", 100 | Value: 10, 101 | Usage: "Number of parallel connections for each peer and archival RPC", 102 | EnvVars: []string{"CONN_PER_PEER"}, 103 | }, 104 | &cli.IntFlag{ 105 | Name: flagMaxUserRPS, 106 | Value: 0, 107 | Usage: "Maximum number of unique user requests per second (set 0 to disable)", 108 | EnvVars: []string{"MAX_USER_RPS"}, 109 | }, 110 | 111 | // Certificate config 112 | &cli.DurationFlag{ 113 | Name: "cert-duration", 114 | Value: time.Hour * 24 * 365, 115 | Usage: "generated certificate duration", 116 | EnvVars: []string{"CERT_DURATION"}, 117 | }, 118 | &cli.StringSliceFlag{ 119 | Name: "cert-hosts", 120 | Value: cli.NewStringSlice("127.0.0.1", "localhost"), 121 | Usage: "generated certificate hosts", 122 | EnvVars: []string{"CERT_HOSTS"}, 123 | }, 124 | &cli.StringFlag{ 125 | Name: flagCertPath, 126 | Usage: "path where to store the generated certificate", 127 | EnvVars: []string{"CERT_PATH"}, 128 | }, 129 | &cli.StringFlag{ 130 | Name: flagCertKeyPath, 131 | Usage: "path where to store the generated certificate key", 132 | EnvVars: []string{"CERT_KEY_PATH"}, 133 | }, 134 | 135 | // Logging, metrics and debug 136 | &cli.StringFlag{ 137 | Name: "metrics-addr", 138 | Value: "127.0.0.1:8090", 139 | Usage: "address to listen on for Prometheus metrics (metrics are served on $metrics-addr/metrics)", 140 | EnvVars: []string{"METRICS_ADDR"}, 141 | }, 142 | &cli.BoolFlag{ 143 | Name: "log-json", 144 | Value: false, 145 | Usage: "log in JSON format", 146 | EnvVars: []string{"LOG_JSON"}, 147 | }, 148 | &cli.BoolFlag{ 149 | Name: "log-debug", 150 | Value: false, 151 | Usage: "log debug messages", 152 | EnvVars: []string{"LOG_DEBUG"}, 153 | }, 154 | &cli.BoolFlag{ 155 | Name: "log-uid", 156 | Value: false, 157 | Usage: "generate a uuid and add to all log messages", 158 | EnvVars: []string{"LOG_UID"}, 159 | }, 160 | &cli.StringFlag{ 161 | Name: "log-service", 162 | Value: "tdx-orderflow-proxy-receiver", 163 | Usage: "add 'service' tag to logs", 164 | EnvVars: []string{"LOG_SERVICE"}, 165 | }, 166 | &cli.BoolFlag{ 167 | Name: "pprof", 168 | Value: false, 169 | Usage: "enable pprof debug endpoint (pprof is served on $metrics-addr/debug/pprof/*)", 170 | EnvVars: []string{"PPROF"}, 171 | }, 172 | } 173 | 174 | func main() { 175 | app := &cli.App{ 176 | Name: "receiver-proxy", 177 | Usage: "Serve API and metrics", 178 | Flags: flags, 179 | Version: common.Version, 180 | Action: runMain, 181 | } 182 | 183 | if err := app.Run(os.Args); err != nil { 184 | log.Fatal(err) 185 | } 186 | } 187 | 188 | func runMain(cCtx *cli.Context) error { 189 | logJSON := cCtx.Bool("log-json") 190 | logDebug := cCtx.Bool("log-debug") 191 | logUID := cCtx.Bool("log-uid") 192 | logService := cCtx.String("log-service") 193 | 194 | log := common.SetupLogger(&common.LoggingOpts{ 195 | Debug: logDebug, 196 | JSON: logJSON, 197 | Service: logService, 198 | Version: common.Version, 199 | }) 200 | 201 | if logUID { 202 | id := uuid.Must(uuid.NewRandom()) 203 | log = log.With("uid", id.String()) 204 | } 205 | 206 | exit := make(chan os.Signal, 1) 207 | signal.Notify(exit, os.Interrupt, syscall.SIGTERM) 208 | 209 | // metrics server 210 | go func() { 211 | metricsAddr := cCtx.String("metrics-addr") 212 | usePprof := cCtx.Bool("pprof") 213 | metricsMux := http.NewServeMux() 214 | metricsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { 215 | metrics.WritePrometheus(w, true) 216 | }) 217 | if usePprof { 218 | metricsMux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) 219 | metricsMux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) 220 | metricsMux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) 221 | metricsMux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) 222 | metricsMux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) 223 | } 224 | 225 | metricsServer := &http.Server{ 226 | Addr: metricsAddr, 227 | ReadHeaderTimeout: 5 * time.Second, 228 | Handler: metricsMux, 229 | } 230 | 231 | err := metricsServer.ListenAndServe() 232 | if err != nil { 233 | log.Error("Failed to start metrics server", "err", err) 234 | } 235 | }() 236 | 237 | builderEndpoint := cCtx.String("builder-endpoint") 238 | rpcEndpoint := cCtx.String("rpc-endpoint") 239 | 240 | builderConfigHubEndpoint := cCtx.String("builder-confighub-endpoint") 241 | archiveEndpoint := cCtx.String("orderflow-archive-endpoint") 242 | flashbotsSignerStr := cCtx.String("flashbots-orderflow-signer-address") 243 | flashbotsSignerAddress := eth.HexToAddress(flashbotsSignerStr) 244 | maxRequestBodySizeBytes := cCtx.Int64("max-request-body-size-bytes") 245 | connectionsPerPeer := cCtx.Int("connections-per-peer") 246 | archiveWorkerCount := cCtx.Int("archive-worker-count") 247 | maxUserRPS := cCtx.Int(flagMaxUserRPS) 248 | 249 | certDuration := cCtx.Duration("cert-duration") 250 | certHosts := cCtx.StringSlice("cert-hosts") 251 | certPath := cCtx.String(flagCertPath) 252 | certKeyPath := cCtx.String(flagCertKeyPath) 253 | if certPath == "" || certKeyPath == "" { 254 | return errors.New("cert-path and cert-key-path must be set") 255 | } 256 | 257 | proxyConfig := &proxy.ReceiverProxyConfig{ 258 | ReceiverProxyConstantConfig: proxy.ReceiverProxyConstantConfig{Log: log, FlashbotsSignerAddress: flashbotsSignerAddress}, 259 | CertValidDuration: certDuration, 260 | CertHosts: certHosts, 261 | CertPath: certPath, 262 | CertKeyPath: certKeyPath, 263 | BuilderConfigHubEndpoint: builderConfigHubEndpoint, 264 | ArchiveEndpoint: archiveEndpoint, 265 | ArchiveConnections: connectionsPerPeer, 266 | LocalBuilderEndpoint: builderEndpoint, 267 | EthRPC: rpcEndpoint, 268 | MaxRequestBodySizeBytes: maxRequestBodySizeBytes, 269 | ConnectionsPerPeer: connectionsPerPeer, 270 | MaxUserRPS: maxUserRPS, 271 | ArchiveWorkerCount: archiveWorkerCount, 272 | } 273 | 274 | instance, err := proxy.NewReceiverProxy(*proxyConfig) 275 | if err != nil { 276 | log.Error("Failed to create proxy server", "err", err) 277 | return err 278 | } 279 | 280 | registerContext, registerCancel := context.WithCancel(context.Background()) 281 | go func() { 282 | select { 283 | case <-exit: 284 | registerCancel() 285 | case <-registerContext.Done(): 286 | } 287 | }() 288 | 289 | err = instance.RegisterSecrets(registerContext) 290 | registerCancel() 291 | if err != nil { 292 | log.Error("Failed to generate and publish secrets", "err", err) 293 | return err 294 | } 295 | 296 | userListenAddr := cCtx.String(flagUserListenAddr) 297 | systemListenAddr := cCtx.String(flagSystemListenAddr) 298 | certListenAddr := cCtx.String(flagCertListenAddr) 299 | 300 | servers, err := proxy.StartReceiverServers(instance, userListenAddr, systemListenAddr, certListenAddr) 301 | if err != nil { 302 | log.Error("Failed to start proxy server", "err", err) 303 | return err 304 | } 305 | 306 | log.Info("Started receiver proxy", "userListenAddress", userListenAddr, "systemListenAddress", systemListenAddr, "certListenAddress", certListenAddr) 307 | 308 | <-exit 309 | servers.Stop() 310 | return nil 311 | } 312 | -------------------------------------------------------------------------------- /cmd/sender-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/pprof" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/VictoriaMetrics/metrics" 13 | "github.com/flashbots/go-utils/signature" 14 | "github.com/flashbots/tdx-orderflow-proxy/common" 15 | "github.com/flashbots/tdx-orderflow-proxy/proxy" 16 | "github.com/google/uuid" 17 | "github.com/urfave/cli/v2" // imports as package "cli" 18 | ) 19 | 20 | var flags []cli.Flag = []cli.Flag{ 21 | // input and output 22 | &cli.StringFlag{ 23 | Name: "listen-address", 24 | Value: "127.0.0.1:8080", 25 | Usage: "address to listen on for requests", 26 | EnvVars: []string{"LISTEN_ADDRESS"}, 27 | }, 28 | &cli.StringFlag{ 29 | Name: "builder-confighub-endpoint", 30 | Value: "http://127.0.0.1:14892", 31 | Usage: "address of the builder config hub endpoint (directly or using the cvm-proxy)", 32 | EnvVars: []string{"BUILDER_CONFIGHUB_ENDPOINT"}, 33 | }, 34 | &cli.StringFlag{ 35 | Name: "orderflow-signer-key", 36 | Value: "0xfb5ad18432422a84514f71d63b45edf51165d33bef9c2bd60957a48d4c4cb68e", 37 | Usage: "orderflow will be signed with this address", 38 | EnvVars: []string{"ORDERFLOW_SIGNER_KEY"}, 39 | }, 40 | &cli.Int64Flag{ 41 | Name: "max-request-body-size-bytes", 42 | Value: 0, 43 | Usage: "Maximum size of the request body, if 0 default will be used", 44 | EnvVars: []string{"MAX_REQUEST_BODY_SIZE_BYTES"}, 45 | }, 46 | &cli.IntFlag{ 47 | Name: "connections-per-peer", 48 | Value: 10, 49 | Usage: "Number of parallel connections for each peer", 50 | EnvVars: []string{"CONN_PER_PEER"}, 51 | }, 52 | 53 | // logging, metrics and debug 54 | &cli.StringFlag{ 55 | Name: "metrics-addr", 56 | Value: "127.0.0.1:8090", 57 | Usage: "address to listen on for Prometheus metrics (metrics are served on $metrics-addr/metrics)", 58 | EnvVars: []string{"METRICS_ADDR"}, 59 | }, 60 | &cli.BoolFlag{ 61 | Name: "log-json", 62 | Value: false, 63 | Usage: "log in JSON format", 64 | EnvVars: []string{"LOG_JSON"}, 65 | }, 66 | &cli.BoolFlag{ 67 | Name: "log-debug", 68 | Value: false, 69 | Usage: "log debug messages", 70 | EnvVars: []string{"LOG_DEBUG"}, 71 | }, 72 | &cli.BoolFlag{ 73 | Name: "log-uid", 74 | Value: false, 75 | Usage: "generate a uuid and add to all log messages", 76 | EnvVars: []string{"LOG_UID"}, 77 | }, 78 | &cli.StringFlag{ 79 | Name: "log-service", 80 | Value: "tdx-orderflow-proxy-sender", 81 | Usage: "add 'service' tag to logs", 82 | EnvVars: []string{"LOG_SERVICE"}, 83 | }, 84 | &cli.BoolFlag{ 85 | Name: "pprof", 86 | Value: false, 87 | Usage: "enable pprof debug endpoint (pprof is served on $metrics-addr/debug/pprof/*)", 88 | EnvVars: []string{"PPROF"}, 89 | }, 90 | } 91 | 92 | func main() { 93 | app := &cli.App{ 94 | Name: "sender-proxy", 95 | Usage: "Serve API, and metrics", 96 | Flags: flags, 97 | Action: func(cCtx *cli.Context) error { 98 | logJSON := cCtx.Bool("log-json") 99 | logDebug := cCtx.Bool("log-debug") 100 | logUID := cCtx.Bool("log-uid") 101 | logService := cCtx.String("log-service") 102 | 103 | log := common.SetupLogger(&common.LoggingOpts{ 104 | Debug: logDebug, 105 | JSON: logJSON, 106 | Service: logService, 107 | Version: common.Version, 108 | }) 109 | 110 | if logUID { 111 | id := uuid.Must(uuid.NewRandom()) 112 | log = log.With("uid", id.String()) 113 | } 114 | 115 | exit := make(chan os.Signal, 1) 116 | signal.Notify(exit, os.Interrupt, syscall.SIGTERM) 117 | 118 | builderConfigHubEndpoint := cCtx.String("builder-confighub-endpoint") 119 | orderflowSignerKeyStr := cCtx.String("orderflow-signer-key") 120 | orderflowSigner, err := signature.NewSignerFromHexPrivateKey(orderflowSignerKeyStr) 121 | if err != nil { 122 | log.Error("Failed to get signer from private key", "error", err) 123 | } 124 | log.Info("Ordeflow signing address", "address", orderflowSigner.Address()) 125 | maxRequestBodySizeBytes := cCtx.Int64("max-request-body-size-bytes") 126 | 127 | connectionsPerPeer := cCtx.Int("connections-per-peer") 128 | 129 | proxyConfig := &proxy.SenderProxyConfig{ 130 | SenderProxyConstantConfig: proxy.SenderProxyConstantConfig{ 131 | Log: log, 132 | OrderflowSigner: orderflowSigner, 133 | }, 134 | BuilderConfigHubEndpoint: builderConfigHubEndpoint, 135 | MaxRequestBodySizeBytes: maxRequestBodySizeBytes, 136 | ConnectionsPerPeer: connectionsPerPeer, 137 | } 138 | 139 | instance, err := proxy.NewSenderProxy(*proxyConfig) 140 | if err != nil { 141 | log.Error("Failed to create proxy server", "err", err) 142 | return err 143 | } 144 | 145 | listenAddr := cCtx.String("listen-address") 146 | servers, err := proxy.StartSenderServers(instance, listenAddr) 147 | if err != nil { 148 | log.Error("Failed to start proxy server", "err", err) 149 | return err 150 | } 151 | 152 | log.Info("Started sender proxy", "listenAddres", listenAddr) 153 | 154 | // metrics server 155 | go func() { 156 | metricsAddr := cCtx.String("metrics-addr") 157 | usePprof := cCtx.Bool("pprof") 158 | metricsMux := http.NewServeMux() 159 | metricsMux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { 160 | metrics.WritePrometheus(w, true) 161 | }) 162 | metricsMux.HandleFunc("/update_peers", func(w http.ResponseWriter, r *http.Request) { 163 | select { 164 | case instance.PeerUpdateForce <- struct{}{}: 165 | default: 166 | } 167 | w.WriteHeader(http.StatusOK) 168 | }) 169 | if usePprof { 170 | metricsMux.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) 171 | metricsMux.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) 172 | metricsMux.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) 173 | metricsMux.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) 174 | metricsMux.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) 175 | } 176 | 177 | metricsServer := &http.Server{ 178 | Addr: metricsAddr, 179 | ReadHeaderTimeout: 5 * time.Second, 180 | Handler: metricsMux, 181 | } 182 | 183 | err := metricsServer.ListenAndServe() 184 | if err != nil { 185 | log.Error("Failed to start metrics server", "err", err) 186 | } 187 | }() 188 | 189 | <-exit 190 | servers.Stop() 191 | return nil 192 | }, 193 | } 194 | 195 | if err := app.Run(os.Args); err != nil { 196 | log.Fatal(err) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /cmd/test-tx-sender/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "log" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | "github.com/flashbots/go-utils/rpctypes" 14 | "github.com/flashbots/go-utils/signature" 15 | "github.com/flashbots/tdx-orderflow-proxy/proxy" 16 | "github.com/google/uuid" 17 | "github.com/urfave/cli/v2" // imports as package "cli" 18 | ) 19 | 20 | var flags []cli.Flag = []cli.Flag{ 21 | // input and output 22 | &cli.StringFlag{ 23 | Name: "local-orderflow-endpoint", 24 | Value: "https://127.0.0.1:443", 25 | Usage: "address to send orderflow to", 26 | EnvVars: []string{"LOCAL_ORDERPLOW_ENDPOINT"}, 27 | }, 28 | &cli.StringFlag{ 29 | Name: "cert-endpoint", 30 | Value: "http://127.0.0.1:14727", 31 | Usage: "address that serves certifiate on /cert endpoint", 32 | EnvVars: []string{"CERT_ENDPOINT"}, 33 | }, 34 | &cli.StringFlag{ 35 | Name: "signer-private-key", 36 | Value: "0x52da2727dd1180b547258c9ca7deb7f9576b2768f3f293b67f36505c85b2ddd0", 37 | Usage: "signer of the requests", 38 | EnvVars: []string{"SIGNER_PRIVATE_KEY"}, 39 | }, 40 | &cli.StringFlag{ 41 | Name: "rpc-endpoint", 42 | Value: "http://127.0.0.1:8545", 43 | Usage: "address of the node RPC that supports eth_blockNumber", 44 | EnvVars: []string{"RPC_ENDPOINT"}, 45 | }, 46 | } 47 | 48 | // test tx 49 | 50 | // tx with hash 0x40614141bf0c512efcaa2e742f79ce5e654c6658d5de77ca4f1154b5b52ae13a 51 | var testTx = hexutil.MustDecode("0x1234") 52 | 53 | func main() { 54 | app := &cli.App{ 55 | Name: "test-tx-sender", 56 | Usage: "send test transactions", 57 | Flags: flags, 58 | Action: func(cCtx *cli.Context) error { 59 | localOrderflowEndpoint := cCtx.String("local-orderflow-endpoint") 60 | certEndpoint := cCtx.String("cert-endpoint") 61 | signerPrivateKey := cCtx.String("signer-private-key") 62 | 63 | orderflowSigner, err := signature.NewSignerFromHexPrivateKey(signerPrivateKey) 64 | if err != nil { 65 | return err 66 | } 67 | slog.Info("Ordeflow signing address", "address", orderflowSigner.Address()) 68 | 69 | cert, err := fetchCertificate(certEndpoint + "/cert") 70 | if err != nil { 71 | return err 72 | } 73 | slog.Info("Fetched certificate") 74 | 75 | client, err := proxy.RPCClientWithCertAndSigner(localOrderflowEndpoint, cert, orderflowSigner, 1) 76 | if err != nil { 77 | return err 78 | } 79 | slog.Info("Created client") 80 | 81 | rpcEndpoint := cCtx.String("rpc-endpoint") 82 | blockNumberSource := proxy.NewBlockNumberSource(rpcEndpoint) 83 | block, err := blockNumberSource.BlockNumber() 84 | if err != nil { 85 | return err 86 | } 87 | slog.Info("Current block number", "block", block) 88 | 89 | // send eth_sendRawTransactions 90 | resp, err := client.Call(context.Background(), "eth_sendRawTransaction", hexutil.Bytes(testTx)) 91 | if err != nil { 92 | return err 93 | } 94 | if resp.Error != nil { 95 | slog.Error("RPC returned error", "error", resp.Error) 96 | return errors.New("eth_sendRawTransaction failed") 97 | } 98 | slog.Info("Sent eth_sendRawTransaction") 99 | 100 | // send eth_sendBundle 101 | replacementUUIDTyped := uuid.New() 102 | repacementUUID := replacementUUIDTyped.String() 103 | slog.Info("Using the following replacement UUID", "value", repacementUUID) 104 | blockNumber := hexutil.Uint64(block) 105 | bundleArgs := rpctypes.EthSendBundleArgs{ 106 | Txs: []hexutil.Bytes{testTx}, 107 | ReplacementUUID: &repacementUUID, 108 | BlockNumber: &blockNumber, 109 | } 110 | 111 | bundleHash, bundleUUID, err := bundleArgs.Validate() 112 | if err != nil { 113 | return err 114 | } 115 | resp, err = client.Call(context.Background(), "eth_sendBundle", bundleArgs) 116 | if err != nil { 117 | return err 118 | } 119 | if resp.Error != nil { 120 | slog.Error("RPC returned error", "error", resp.Error) 121 | return errors.New("eth_sendBundle failed") 122 | } 123 | slog.Info("Sent eth_sendBundle", "bundleHash", bundleHash, "bundleUUID", bundleUUID) 124 | 125 | // send eth_cancelBundle 126 | 127 | resp, err = client.Call(context.Background(), "eth_cancelBundle", rpctypes.EthCancelBundleArgs{ 128 | ReplacementUUID: repacementUUID, 129 | }) 130 | if err != nil { 131 | return err 132 | } 133 | if resp.Error != nil { 134 | slog.Error("RPC returned error", "error", resp.Error) 135 | return errors.New("eth_cancelBundle failed") 136 | } 137 | slog.Info("Sent eth_cancelBundle") 138 | 139 | // send mev_sendBundle (normal bundle) 140 | sbundleArgs := rpctypes.MevSendBundleArgs{ 141 | Version: "v0.1", 142 | ReplacementUUID: repacementUUID, 143 | Inclusion: rpctypes.MevBundleInclusion{ 144 | BlockNumber: hexutil.Uint64(block), 145 | }, 146 | Body: []rpctypes.MevBundleBody{ 147 | { 148 | Tx: (*hexutil.Bytes)(&testTx), 149 | CanRevert: true, 150 | }, 151 | }, 152 | } 153 | sbundleHash, err := sbundleArgs.Validate() 154 | if err != nil { 155 | return err 156 | } 157 | 158 | resp, err = client.Call(context.Background(), "mev_sendBundle", sbundleArgs) 159 | if err != nil { 160 | return err 161 | } 162 | if resp.Error != nil { 163 | slog.Error("RPC returned error", "error", resp.Error) 164 | return errors.New("mev_sendBundle (normal bundle) failed") 165 | } 166 | slog.Info("Sent mev_sendBundle (normal bundle)", "hash", sbundleHash) 167 | 168 | // send mev_sendBundle (cancellation bundle) 169 | sbundleCancelArgs := rpctypes.MevSendBundleArgs{ 170 | Version: "v0.1", 171 | ReplacementUUID: repacementUUID, 172 | } 173 | resp, err = client.Call(context.Background(), "mev_sendBundle", sbundleCancelArgs) 174 | if err != nil { 175 | return err 176 | } 177 | if resp.Error != nil { 178 | slog.Error("RPC returned error", "error", resp.Error) 179 | return errors.New("mev_sendBundle (cancellation) failed") 180 | } 181 | slog.Info("Sent mev_sendBundle (cancellation)") 182 | 183 | return nil 184 | }, 185 | } 186 | 187 | if err := app.Run(os.Args); err != nil { 188 | log.Fatal(err) 189 | } 190 | } 191 | 192 | func fetchCertificate(endpoint string) ([]byte, error) { 193 | resp, err := http.Get(endpoint) //nolint:gosec 194 | if err != nil { 195 | return nil, err 196 | } 197 | defer resp.Body.Close() 198 | 199 | body, err := io.ReadAll(resp.Body) 200 | if err != nil { 201 | return nil, err 202 | } 203 | return body, nil 204 | } 205 | -------------------------------------------------------------------------------- /common/logging.go: -------------------------------------------------------------------------------- 1 | // Package common contains common utilities and functions used by the service. 2 | package common 3 | 4 | import ( 5 | "log/slog" 6 | "os" 7 | ) 8 | 9 | type LoggingOpts struct { 10 | Debug bool 11 | JSON bool 12 | Service string 13 | Version string 14 | } 15 | 16 | func SetupLogger(opts *LoggingOpts) (log *slog.Logger) { 17 | logLevel := slog.LevelInfo 18 | if opts.Debug { 19 | logLevel = slog.LevelDebug 20 | } 21 | 22 | if opts.JSON { 23 | log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})) 24 | } else { 25 | log = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel})) 26 | } 27 | 28 | if opts.Service != "" { 29 | log = log.With("service", opts.Service) 30 | } 31 | 32 | if opts.Version != "" { 33 | log = log.With("version", opts.Version) 34 | } 35 | 36 | slog.SetDefault(log) 37 | 38 | return log 39 | } 40 | -------------------------------------------------------------------------------- /common/vars.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | var Version = "dev" 4 | 5 | const ( 6 | PackageName = "github.com/flashbots/tdx-orderflow-proxy" 7 | ) 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flashbots/tdx-orderflow-proxy 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/VictoriaMetrics/metrics v1.35.1 9 | github.com/cenkalti/backoff v2.2.1+incompatible 10 | github.com/ethereum/go-ethereum v1.15.5 11 | github.com/flashbots/go-utils v0.14.0 12 | github.com/google/uuid v1.6.0 13 | github.com/hashicorp/golang-lru/v2 v2.0.7 14 | github.com/stretchr/testify v1.10.0 15 | github.com/urfave/cli/v2 v2.27.5 16 | golang.org/x/net v0.34.0 17 | golang.org/x/time v0.9.0 18 | ) 19 | 20 | require ( 21 | github.com/bits-and-blooms/bitset v1.17.0 // indirect 22 | github.com/consensys/bavard v0.1.22 // indirect 23 | github.com/consensys/gnark-crypto v0.14.0 // indirect 24 | github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 25 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect 26 | github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 29 | github.com/ethereum/c-kzg-4844 v1.0.0 // indirect 30 | github.com/ethereum/go-verkle v0.2.2 // indirect 31 | github.com/goccy/go-json v0.10.5 // indirect 32 | github.com/holiman/uint256 v1.3.2 // indirect 33 | github.com/mmcloughlin/addchain v0.4.0 // indirect 34 | github.com/pmezard/go-difflib v1.0.0 // indirect 35 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 36 | github.com/supranational/blst v0.3.14 // indirect 37 | github.com/valyala/fastrand v1.1.0 // indirect 38 | github.com/valyala/histogram v1.2.0 // indirect 39 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 40 | golang.org/x/crypto v0.32.0 // indirect 41 | golang.org/x/sync v0.10.0 // indirect 42 | golang.org/x/sys v0.29.0 // indirect 43 | golang.org/x/text v0.21.0 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | rsc.io/tmplfunc v0.0.3 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= 2 | github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= 3 | github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= 4 | github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= 5 | github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU= 6 | github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= 7 | github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= 8 | github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 9 | github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= 10 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 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.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= 14 | github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= 15 | github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= 16 | github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= 18 | github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 19 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= 20 | github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= 21 | github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= 22 | github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= 26 | github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 27 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 28 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 29 | github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= 30 | github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= 31 | github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo= 32 | github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo= 33 | github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= 34 | github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= 35 | github.com/flashbots/go-utils v0.14.0 h1:gg9UAGWhwMrnnhY/wV9W5TPRUhhM8GS6bFh4H2VqgsA= 36 | github.com/flashbots/go-utils v0.14.0/go.mod h1:vESXnJKP/r6WRrf8pr87Dr0gJdkGUpzOiMae8yangEc= 37 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 38 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 39 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 40 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 41 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 42 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 43 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= 44 | github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 45 | github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= 46 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 47 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 48 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 49 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 50 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 51 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 52 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 53 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 54 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 55 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 56 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 57 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 58 | github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= 59 | github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= 60 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 61 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 62 | github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= 63 | github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= 64 | github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= 65 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 66 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 70 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 71 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 72 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 73 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 74 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 75 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= 76 | github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 77 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 78 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 | github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= 80 | github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= 81 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 82 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 83 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 84 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 85 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 86 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 87 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 88 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 89 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 90 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 91 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 92 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 93 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 94 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 95 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 96 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 97 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 98 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 99 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 100 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 101 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 102 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 103 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 104 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 105 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 106 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 107 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 108 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 109 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 110 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 111 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= 113 | rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= 114 | -------------------------------------------------------------------------------- /proxy/api_validate.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/flashbots/go-utils/rpctypes" 7 | ) 8 | 9 | var ( 10 | errSigningAddress = errors.New("signing address field should not be set") 11 | 12 | errDroppingTxHashed = errors.New("dropping tx hashes field should not be set") 13 | errRefundPercent = errors.New("refund percent field should not be set") 14 | errRefundRecipient = errors.New("refund recipient field should not be set") 15 | errRefundTxHashes = errors.New("refund tx hashes field should not be set") 16 | errReplacementSet = errors.New("one of uuid or replacementUuid fields should be empty") 17 | errLocalEndpointSbundleMetadata = errors.New("mev share bundle should not contain metadata when sent to local endpoint") 18 | errVersionNotSet = errors.New("version field should be set") 19 | errInvalidVersion = errors.New("invalid version") 20 | ) 21 | 22 | // EnsureReplacementUUID updates bundle with consistent replacement uuid value (falling back to `uuid` field if needed) 23 | func EnsureReplacementUUID(args *rpctypes.EthSendBundleArgs) (*rpctypes.EthSendBundleArgs, error) { 24 | if args.UUID == nil { 25 | return args, nil 26 | } 27 | if args.ReplacementUUID != nil { 28 | return nil, errReplacementSet 29 | } 30 | 31 | args.ReplacementUUID = args.UUID 32 | args.UUID = nil 33 | return args, nil 34 | } 35 | 36 | func IsVersionValid(version *string) bool { 37 | return version == nil || *version == "" || *version == rpctypes.BundleVersionV1 || *version == rpctypes.BundleVersionV2 38 | } 39 | 40 | func ValidateEthSendBundle(args *rpctypes.EthSendBundleArgs, publicEndpoint bool) error { 41 | if !publicEndpoint { 42 | if args.SigningAddress != nil { 43 | return errSigningAddress 44 | } 45 | } 46 | 47 | valid := IsVersionValid(args.Version) 48 | if !valid { 49 | return errInvalidVersion 50 | } 51 | 52 | if publicEndpoint { 53 | // For now public (system) endpoint should receive version field preset. 54 | // For flashbots endpoint orderflowproxy-sender uses it, for direct orderflow 55 | // first orderflow-proxy receiver sets this field 56 | // We might later change it once we successfully finish transition to v2 bundle support 57 | if args.Version == nil || *args.Version == "" { 58 | return errVersionNotSet 59 | } 60 | version := *args.Version 61 | if version == rpctypes.BundleVersionV2 { 62 | return nil 63 | } 64 | 65 | // check that v1 bundle dosn't have unsupported fields 66 | if len(args.DroppingTxHashes) > 0 { 67 | return errDroppingTxHashed 68 | } 69 | 70 | if args.RefundPercent != nil { 71 | return errRefundPercent 72 | } 73 | if args.RefundRecipient != nil { 74 | return errRefundRecipient 75 | } 76 | if len(args.RefundTxHashes) > 0 { 77 | return errRefundTxHashes 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func ValidateEthCancelBundle(args *rpctypes.EthCancelBundleArgs, publicEndpoint bool) error { 85 | if !publicEndpoint { 86 | if args.SigningAddress != nil { 87 | return errSigningAddress 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | func ValidateMevSendBundle(args *rpctypes.MevSendBundleArgs, publicEndpoint bool) error { 94 | // @perf it calculates hash 95 | _, err := args.Validate() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if !publicEndpoint { 101 | if args.Metadata != nil { 102 | return errLocalEndpointSbundleMetadata 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /proxy/archive.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log/slog" 7 | "time" 8 | 9 | "github.com/cenkalti/backoff" 10 | "github.com/ethereum/go-ethereum/common/hexutil" 11 | "github.com/flashbots/go-utils/rpcclient" 12 | "github.com/flashbots/go-utils/rpctypes" 13 | ) 14 | 15 | const NewOrderEventsMethod = "flashbots_newOrderEvents" 16 | 17 | var ( 18 | // ArchiveBatchSize is a maximum size of the batch to send to the archive 19 | ArchiveBatchSize = 100 20 | // ArchiveBatchSizeFlushTimeout is a timeout to force flush the batch to the archive 21 | ArchiveBatchSizeFlushTimeout = time.Second * 6 22 | 23 | errArchivePublicRequest = errors.New("public RPC request should not reach archive") 24 | errArchiveReturnedError = errors.New("orderflow archive returned error") 25 | 26 | ArchiveRequestTimeout = time.Second * 15 27 | ArchiveRetryMaxTime = time.Second * 120 28 | 29 | ArchiveWorkerQueueSize = 20000 30 | ) 31 | 32 | type ArchiveQueue struct { 33 | log *slog.Logger 34 | queue chan *ParsedRequest 35 | flushQueue chan struct{} 36 | archiveClient rpcclient.RPCClient 37 | blockNumberSource *BlockNumberSource 38 | workerCount int 39 | } 40 | 41 | func (aq *ArchiveQueue) Run() { 42 | workerCount := 1 43 | if aq.workerCount > 0 { 44 | workerCount = aq.workerCount 45 | } 46 | workers := make([]*archiveQueueWorker, 0, workerCount) 47 | workersQueue := make(chan *ParsedRequest, ArchiveWorkerQueueSize) 48 | for w := range workerCount { 49 | worker := &archiveQueueWorker{ 50 | log: aq.log.With(slog.Int("worker", w)), 51 | archiveClient: aq.archiveClient, 52 | queue: workersQueue, 53 | flushQueue: make(chan struct{}), 54 | } 55 | go worker.runWorker() 56 | workers = append(workers, worker) 57 | } 58 | aq.log.Info("Started archival workers", slog.Int("workers", workerCount)) 59 | defer func() { 60 | for _, worker := range workers { 61 | worker.close() 62 | } 63 | aq.log.Info("Stopped archival workers", slog.Int("workers", workerCount)) 64 | }() 65 | 66 | var ( 67 | flushTimer = time.After(ArchiveBatchSizeFlushTimeout) 68 | needFlush = false 69 | ) 70 | for { 71 | if needFlush { 72 | for _, worker := range workers { 73 | select { 74 | case worker.flushQueue <- struct{}{}: 75 | default: 76 | } 77 | } 78 | needFlush = false 79 | flushTimer = time.After(ArchiveBatchSizeFlushTimeout) 80 | } 81 | select { 82 | case _, more := <-aq.flushQueue: 83 | if !more { 84 | return 85 | } 86 | needFlush = true 87 | case <-flushTimer: 88 | needFlush = true 89 | case req, more := <-aq.queue: 90 | if !more { 91 | return 92 | } 93 | archiveEventsProcessedTotalCounter.Inc() 94 | processedReq, err := aq.updateParsedRequest(req) 95 | if err != nil { 96 | aq.log.Error("Failed to prepare request for archive", slog.Any("error", err)) 97 | archiveEventsProcessedErrCounter.Inc() 98 | continue 99 | } 100 | if processedReq == nil { 101 | continue 102 | } 103 | select { 104 | case workersQueue <- processedReq: 105 | default: 106 | aq.log.Error("Archive workers are stalling") 107 | } 108 | } 109 | } 110 | } 111 | 112 | // updateParsedRequest will return updated request that can be used to send data to orderflow archive 113 | // result can be nil without error meaning we don't need to archive that 114 | func (aq *ArchiveQueue) updateParsedRequest(input *ParsedRequest) (*ParsedRequest, error) { 115 | if input.systemEndpoint { 116 | return nil, errArchivePublicRequest 117 | } 118 | if input.bidSubsidiseBlock != nil { 119 | return nil, nil 120 | } 121 | if input.ethSendRawTransaction != nil { 122 | var mevSendBundle rpctypes.MevSendBundleArgs 123 | block, err := aq.blockNumberSource.BlockNumber() 124 | if err != nil { 125 | return nil, err 126 | } 127 | mevSendBundle.Version = "v0.1" 128 | mevSendBundle.Inclusion.BlockNumber = hexutil.Uint64(block) 129 | // we set max block to be +5 here because thats roughly the amount of time that mempool tx lives inside the builder orderpool 130 | mevSendBundle.Inclusion.MaxBlock = hexutil.Uint64(block + 5) 131 | mevSendBundle.Body = []rpctypes.MevBundleBody{{Tx: (*hexutil.Bytes)(input.ethSendRawTransaction), CanRevert: true}} 132 | signer := input.signer 133 | mevSendBundle.Metadata = &rpctypes.MevBundleMetadata{ 134 | Signer: &signer, 135 | } 136 | 137 | input = &ParsedRequest{ 138 | systemEndpoint: input.systemEndpoint, 139 | signer: input.signer, 140 | method: input.method, 141 | receivedAt: input.receivedAt, 142 | mevSendBundle: &mevSendBundle, 143 | } 144 | } 145 | return input, nil 146 | } 147 | 148 | type archiveQueueWorker struct { 149 | log *slog.Logger 150 | archiveClient rpcclient.RPCClient 151 | queue chan *ParsedRequest 152 | flushQueue chan struct{} 153 | } 154 | 155 | func (aqw *archiveQueueWorker) close() { 156 | close(aqw.queue) 157 | close(aqw.flushQueue) 158 | } 159 | 160 | func (aqw *archiveQueueWorker) runWorker() { 161 | var ( 162 | pendingBatch []*ParsedRequest 163 | needFlush = false 164 | ) 165 | 166 | for { 167 | if needFlush { 168 | aqw.flush(pendingBatch) 169 | pendingBatch = nil 170 | needFlush = false 171 | } 172 | select { 173 | case req, more := <-aqw.queue: 174 | if !more { 175 | return 176 | } 177 | pendingBatch = append(pendingBatch, req) 178 | if len(pendingBatch) > ArchiveBatchSize { 179 | needFlush = true 180 | } 181 | case _, more := <-aqw.flushQueue: 182 | if !more { 183 | return 184 | } 185 | needFlush = true 186 | } 187 | } 188 | } 189 | 190 | func (aqw *archiveQueueWorker) flush(batch []*ParsedRequest) { 191 | args := FlashbotsNewOrderEventsArgs{} 192 | for _, request := range batch { 193 | event := ArchiveEvent{} 194 | metadata := ArchiveEventMetadata{ 195 | ReceivedAt: request.receivedAt.UnixMilli(), 196 | } 197 | if request.ethSendBundle != nil { 198 | event.EthSendBundle = &ArchiveEventEthSendBundle{ 199 | Params: request.ethSendBundle, 200 | Metadata: &metadata, 201 | } 202 | } else if request.mevSendBundle != nil { 203 | event.MevSendBundle = &ArchiveEventMevSendBundle{ 204 | Params: request.mevSendBundle, 205 | Metadata: &metadata, 206 | } 207 | } else if request.ethCancelBundle != nil { 208 | event.EthCancelBundle = &ArchiveEventEthCancelBundle{ 209 | Params: request.ethCancelBundle, 210 | Metadata: &metadata, 211 | } 212 | } else { 213 | aqw.log.Error("Incorrect request for orderflow archival", slog.String("method", request.method)) 214 | archiveEventsProcessedErrCounter.Inc() 215 | continue 216 | } 217 | args.OrderEvents = append(args.OrderEvents, event) 218 | } 219 | if len(args.OrderEvents) == 0 { 220 | return 221 | } 222 | 223 | aqw.log.Info("Sending batch to the archive", slog.Int("size", len(args.OrderEvents))) 224 | 225 | exp := backoff.NewExponentialBackOff() 226 | exp.MaxElapsedTime = ArchiveRetryMaxTime 227 | 228 | err := backoff.Retry(func() error { 229 | ctx, cancel := context.WithTimeout(context.Background(), ArchiveRequestTimeout) 230 | defer cancel() 231 | 232 | start := time.Now() 233 | res, err := aqw.archiveClient.Call(ctx, NewOrderEventsMethod, args) 234 | archiveEventsRPCDuration.Update(float64(time.Since(start).Milliseconds())) 235 | 236 | if err != nil { 237 | aqw.log.Error("Error while making RPC request to archive", slog.Any("error", err)) 238 | archiveEventsRPCErrors.Inc() 239 | return err 240 | } 241 | if res != nil && res.Error != nil { 242 | aqw.log.Error("Archive returned error", slog.Any("error", res.Error)) 243 | archiveEventsRPCErrors.Inc() 244 | return errArchiveReturnedError 245 | } 246 | return nil 247 | }, exp) 248 | 249 | if err != nil { 250 | aqw.log.Error("Failed to submit batch to the archive", slog.Any("error", err)) 251 | } else { 252 | aqw.log.Info("Successfully submitted batch to the archive") 253 | archiveEventsRPCSentCounter.AddInt64(int64(len(args.OrderEvents))) 254 | } 255 | } 256 | 257 | type FlashbotsNewOrderEventsArgs struct { 258 | OrderEvents []ArchiveEvent `json:"orderEvents"` 259 | } 260 | 261 | type ArchiveEvent struct { 262 | EthSendBundle *ArchiveEventEthSendBundle `json:"eth_sendBundle,omitempty"` 263 | MevSendBundle *ArchiveEventMevSendBundle `json:"mev_sendBundle,omitempty"` 264 | EthCancelBundle *ArchiveEventEthCancelBundle `json:"eth_cancelBundle,omitempty"` 265 | } 266 | 267 | type ArchiveEventMetadata struct { 268 | // ReceivedAt is a unix millisecond timestamp 269 | ReceivedAt int64 `json:"receivedAt"` 270 | } 271 | 272 | type ArchiveEventEthSendBundle struct { 273 | Params *rpctypes.EthSendBundleArgs `json:"params"` 274 | Metadata *ArchiveEventMetadata `json:"metadata"` 275 | } 276 | 277 | type ArchiveEventMevSendBundle struct { 278 | Params *rpctypes.MevSendBundleArgs `json:"params"` 279 | Metadata *ArchiveEventMetadata `json:"metadata"` 280 | } 281 | 282 | type ArchiveEventEthCancelBundle struct { 283 | Params *rpctypes.EthCancelBundleArgs `json:"params"` 284 | Metadata *ArchiveEventMetadata `json:"metadata"` 285 | } 286 | -------------------------------------------------------------------------------- /proxy/confighub.go: -------------------------------------------------------------------------------- 1 | // Package proxy provides the main proxy server. 2 | package proxy 3 | 4 | import ( 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net/http" 12 | 13 | "github.com/ethereum/go-ethereum/common" 14 | ) 15 | 16 | type ConfighubOrderflowProxyCredentials struct { 17 | TLSCert string `json:"tls_cert"` 18 | EcdsaPubkeyAddress common.Address `json:"ecdsa_pubkey_address"` 19 | } 20 | 21 | type ConfighubBuilder struct { 22 | Name string `json:"name"` 23 | IP string `json:"ip"` 24 | OrderflowProxy ConfighubOrderflowProxyCredentials `json:"orderflow_proxy"` 25 | } 26 | 27 | type BuilderConfigHub struct { 28 | log *slog.Logger 29 | endpoint string 30 | } 31 | 32 | func NewBuilderConfigHub(log *slog.Logger, endpoint string) *BuilderConfigHub { 33 | return &BuilderConfigHub{ 34 | log: log, 35 | endpoint: endpoint, 36 | } 37 | } 38 | 39 | func (b *BuilderConfigHub) RegisterCredentials(ctx context.Context, info ConfighubOrderflowProxyCredentials) error { 40 | body, err := json.Marshal(info) 41 | if err != nil { 42 | return err 43 | } 44 | req, err := http.NewRequest(http.MethodPost, b.endpoint+"/api/l1-builder/v1/register_credentials/orderflow_proxy", bytes.NewReader(body)) 45 | if err != nil { 46 | return err 47 | } 48 | req = req.WithContext(ctx) 49 | req.Header.Set("Content-Type", "application/json") 50 | resp, err := http.DefaultClient.Do(req) 51 | if err != nil { 52 | return err 53 | } 54 | defer resp.Body.Close() 55 | if resp.StatusCode != http.StatusOK { 56 | respBody, _ := io.ReadAll(resp.Body) 57 | return fmt.Errorf("builder config hub returned error, code: %d, body: %s", resp.StatusCode, string(respBody)) 58 | } 59 | return nil 60 | } 61 | 62 | func (b *BuilderConfigHub) Builders(internal bool) (result []ConfighubBuilder, err error) { 63 | defer func() { 64 | if err != nil { 65 | confighubErrorsCounter.Inc() 66 | b.log.Error("Failed to fetch peer list from config hub", slog.Any("error", err)) 67 | } 68 | }() 69 | 70 | var resp *http.Response 71 | url := b.endpoint 72 | if internal { 73 | url += "/api/internal/l1-builder/v1/builders" 74 | } else { 75 | url += "/api/l1-builder/v1/builders" 76 | } 77 | resp, err = http.Get(url) //nolint:gosec 78 | if err != nil { 79 | return 80 | } 81 | defer resp.Body.Close() 82 | var body []byte 83 | body, err = io.ReadAll(resp.Body) 84 | if err != nil { 85 | return 86 | } 87 | 88 | err = json.Unmarshal(body, &result) 89 | if err != nil { 90 | return nil, err 91 | } 92 | b.log.Info("Received list of peers from confighub", slog.Bool("internalEndpoint", internal), slog.Any("peers", result)) 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /proxy/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Welcome to BuilderNet 9 | 10 | 11 | 12 | 13 | 14 | 95 | 96 | 97 | 98 |
99 | 102 | 103 |
104 |

Welcome to BuilderNet

105 |
106 | 107 |
108 |

Documentation

109 | 114 |
115 |
116 | 117 |
118 |

Sending Orderflow to BuilderNet

119 | 120 |

Example curl Request

121 |
curl https://_BUILDERNET_INSTANCE_ \
122 |     --cacert builder-cert.pem \ # or using --insecure
123 |     --header 'Content-Type: application/json' \
124 |     --header 'X-Flashbots-Signature: _public_key_address_:_signature_' \
125 |     --data '{
126 |         "id": 1,
127 |         "jsonrpc": "2.0",
128 |         "method": "eth_sendBundle",
129 |         "params": [{
130 |             "txs": [
131 |                 "0x100000...", "0x200000..."
132 |             ],
133 |             "blockNumber": "0x1361bd3"
134 |         }]
135 |     }'
136 | 
137 |

See also the full documentation at 138 |

142 |

143 | 144 |

Downloading the TLS certificate

145 | 146 |

You can get the TLS certificate through a TEE-attested channel (see "TEE Proof Validation" below), or download it directly with curl:

147 |
curl -w %{certs} -k https://_BUILDERNET_INSTANCE_
148 | 149 |
150 | Note: Currently, requests are rate-limited to 3 requests / IP / second. This is expected to be raised soon. 151 |
152 | 153 |
154 | 155 |
156 | 157 |
158 |

Instance TLS Certificate

159 | 160 |

BuilderNet instances create their own unique TLS certificate, and use it to prove their identity and to encrypt incoming traffic.

161 |

This is the TLS certificate of this specific instance:

162 | 163 |
{{ .Cert }}
164 |
165 | 166 |
167 | 168 |
169 |

TEE Proof Validation

170 | 171 |

172 | You can retrieve the certificate over a TEE-attested channel, verifying the identity of the server, and 173 | then use it to verify the TLS connection: 174 |

175 | 176 |
# Install attested-get tool
177 | go install github.com/flashbots/cvm-reverse-proxy/cmd/attested-get
178 | 
179 | # Get the builder certificate over an attested channel
180 | attested-get \
181 |     --addr=https://_BUILDERNET_INSTANCE_:7936/cert \
182 |     --expected-measurements=https://measurements.builder.flashbots.net \
183 |     --out-response=builder-cert.pem
184 |

See more details about attested-get in the cvm-reverse-proxy repository.

185 |
186 |
187 | 188 | 189 | -------------------------------------------------------------------------------- /proxy/metrics.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/VictoriaMetrics/metrics" 8 | ) 9 | 10 | var ( 11 | archiveEventsProcessedTotalCounter = metrics.NewCounter("orderflow_proxy_archive_events_processed_total") 12 | archiveEventsProcessedErrCounter = metrics.NewCounter("orderflow_proxy_archive_events_processed_err") 13 | // number of events sent successfully to orderflow archive. i.e. if we batch 10 events into 1 request this would be increment by 10 14 | archiveEventsRPCSentCounter = metrics.NewCounter("orderflow_proxy_archive_events_sent_ok") 15 | archiveEventsRPCDuration = metrics.NewSummary("orderflow_proxy_archive_rpc_duration_milliseconds") 16 | archiveEventsRPCErrors = metrics.NewCounter("orderflow_proxy_archive_rpc_errors") 17 | 18 | confighubErrorsCounter = metrics.NewCounter("orderflow_proxy_confighub_errors") 19 | 20 | shareQueueInternalErrors = metrics.NewCounter("orderflow_proxy_share_queue_internal_errors") 21 | 22 | apiUserRateLimits = metrics.NewCounter("orderflow_proxy_api_user_rate_limits") 23 | ) 24 | 25 | const ( 26 | apiIncomingRequestsByPeer = `orderflow_proxy_api_incoming_requests_by_peer{peer="%s"}` 27 | apiDuplicateRequestsByPeer = `orderflow_proxy_api_duplicate_requests_by_peer{peer="%s"}` 28 | 29 | shareQueuePeerStallingErrorsLabel = `orderflow_proxy_share_queue_peer_stalling_errors{peer="%s"}` 30 | shareQueuePeerRPCErrorsLabel = `orderflow_proxy_share_queue_peer_rpc_errors{peer="%s"}` 31 | shareQueuePeerRPCDurationLabel = `orderflow_proxy_share_queue_peer_rpc_duration_milliseconds{peer="%s",is_big="%t"}` 32 | shareQueuePeerE2EDurationLabel = `orderflow_proxy_share_queue_peer_e2e_duration_milliseconds{peer="%s",method="%s",system_endpoint="%t",is_big="%t"}` 33 | shareQueuePeerQueueDurationLabel = `orderflow_proxy_share_queue_peer_queue_duration_milliseconds{peer="%s",method="%s",system_endpoint="%t",is_big="%t"}` 34 | 35 | requestDurationLabel = `orderflow_proxy_api_request_processing_duration_milliseconds{method="%s",server_name="%s",step="%s"}` 36 | ) 37 | 38 | func incAPIIncomingRequestsByPeer(peer string) { 39 | l := fmt.Sprintf(apiIncomingRequestsByPeer, peer) 40 | metrics.GetOrCreateCounter(l).Inc() 41 | } 42 | 43 | func incAPIDuplicateRequestsByPeer(peer string) { 44 | l := fmt.Sprintf(apiDuplicateRequestsByPeer, peer) 45 | metrics.GetOrCreateCounter(l).Inc() 46 | } 47 | 48 | func incAPIUserRateLimits() { 49 | apiUserRateLimits.Inc() 50 | } 51 | 52 | func incShareQueuePeerStallingErrors(peer string) { 53 | l := fmt.Sprintf(shareQueuePeerStallingErrorsLabel, peer) 54 | metrics.GetOrCreateCounter(l).Inc() 55 | } 56 | 57 | func incShareQueuePeerRPCErrors(peer string) { 58 | l := fmt.Sprintf(shareQueuePeerRPCErrorsLabel, peer) 59 | metrics.GetOrCreateCounter(l).Inc() 60 | } 61 | 62 | func timeShareQueuePeerRPCDuration(peer string, duration int64, bigRequest bool) { 63 | l := fmt.Sprintf(shareQueuePeerRPCDurationLabel, peer, bigRequest) 64 | metrics.GetOrCreateSummary(l).Update(float64(duration)) 65 | } 66 | 67 | func timeShareQueuePeerE2EDuration(peer string, duration time.Duration, method string, systemEndpoint, bigRequest bool) { 68 | millis := float64(duration.Microseconds()) / 1000.0 69 | l := fmt.Sprintf(shareQueuePeerE2EDurationLabel, peer, method, systemEndpoint, bigRequest) 70 | metrics.GetOrCreateSummary(l).Update(float64(millis)) 71 | } 72 | 73 | func timeShareQueuePeerQueueDuration(peer string, duration time.Duration, method string, systemEndpoint, bigRequest bool) { 74 | millis := float64(duration.Microseconds()) / 1000.0 75 | l := fmt.Sprintf(shareQueuePeerQueueDurationLabel, peer, method, systemEndpoint, bigRequest) 76 | metrics.GetOrCreateSummary(l).Update(float64(millis)) 77 | } 78 | 79 | func incRequestDurationStep(duration time.Duration, method, serverName, step string) { 80 | millis := float64(duration.Microseconds()) / 1000.0 81 | l := fmt.Sprintf(requestDurationLabel, method, serverName, step) 82 | metrics.GetOrCreateSummary(l).Update(millis) 83 | } 84 | -------------------------------------------------------------------------------- /proxy/receiver_api.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | _ "embed" 7 | "errors" 8 | "html/template" 9 | "log/slog" 10 | "time" 11 | 12 | "github.com/ethereum/go-ethereum/common" 13 | "github.com/flashbots/go-utils/rpcserver" 14 | "github.com/flashbots/go-utils/rpctypes" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | const DefaultMaxRequestBodySizeBytes = int64(30 * 1024 * 1024) // 30 MB 19 | 20 | const ( 21 | FlashbotsPeerName = "flashbots" 22 | 23 | EthSendBundleMethod = "eth_sendBundle" 24 | MevSendBundleMethod = "mev_sendBundle" 25 | EthCancelBundleMethod = "eth_cancelBundle" 26 | EthSendRawTransactionMethod = "eth_sendRawTransaction" 27 | BidSubsidiseBlockMethod = "bid_subsidiseBlock" 28 | ) 29 | 30 | //go:embed html/index.html 31 | var landingPageHTML string 32 | 33 | var ( 34 | errUnknownPeer = errors.New("unknown peers can't send to the system address") 35 | errSubsidyWrongEndpoint = errors.New("subsidy can only be called on system API") 36 | errSubsidyWrongCaller = errors.New("subsidy can only be called by Flashbots") 37 | errRateLimiting = errors.New("requests to user API are rate limited") 38 | 39 | errUUIDParse = errors.New("failed to parse UUID") 40 | 41 | apiNow = time.Now 42 | 43 | handleParsedRequestTimeout = time.Second * 1 44 | ) 45 | 46 | func (prx *ReceiverProxy) SystemJSONRPCHandler(maxRequestBodySizeBytes int64) (*rpcserver.JSONRPCHandler, error) { 47 | handler, err := rpcserver.NewJSONRPCHandler(rpcserver.Methods{ 48 | EthSendBundleMethod: prx.EthSendBundleSystem, 49 | MevSendBundleMethod: prx.MevSendBundleSystem, 50 | EthCancelBundleMethod: prx.EthCancelBundleSystem, 51 | EthSendRawTransactionMethod: prx.EthSendRawTransactionSystem, 52 | BidSubsidiseBlockMethod: prx.BidSubsidiseBlockSystem, 53 | }, 54 | rpcserver.JSONRPCHandlerOpts{ 55 | ServerName: "system_server", 56 | Log: prx.Log, 57 | MaxRequestBodySizeBytes: maxRequestBodySizeBytes, 58 | VerifyRequestSignatureFromHeader: true, 59 | }, 60 | ) 61 | 62 | return handler, err 63 | } 64 | 65 | func (prx *ReceiverProxy) UserJSONRPCHandler(maxRequestBodySizeBytes int64) (*rpcserver.JSONRPCHandler, error) { 66 | landingPageHTML, err := prx.prepHTML() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | handler, err := rpcserver.NewJSONRPCHandler(rpcserver.Methods{ 72 | EthSendBundleMethod: prx.EthSendBundleUser, 73 | MevSendBundleMethod: prx.MevSendBundleUser, 74 | EthCancelBundleMethod: prx.EthCancelBundleUser, 75 | EthSendRawTransactionMethod: prx.EthSendRawTransactionUser, 76 | BidSubsidiseBlockMethod: prx.BidSubsidiseBlockUser, 77 | }, 78 | rpcserver.JSONRPCHandlerOpts{ 79 | ServerName: "user_server", 80 | Log: prx.Log, 81 | MaxRequestBodySizeBytes: maxRequestBodySizeBytes, 82 | VerifyRequestSignatureFromHeader: true, 83 | GetResponseContent: landingPageHTML, 84 | }, 85 | ) 86 | 87 | return handler, err 88 | } 89 | 90 | func (prx *ReceiverProxy) ValidateSigner(ctx context.Context, req *ParsedRequest, systemEndpoint bool) error { 91 | req.signer = rpcserver.GetSigner(ctx) 92 | if !systemEndpoint { 93 | req.peerName = "user-request" 94 | return nil 95 | } 96 | 97 | prx.Log.Debug("Received signed request on a system endpoint", slog.Any("signer", req.signer)) 98 | 99 | if req.signer == prx.FlashbotsSignerAddress { 100 | req.peerName = FlashbotsPeerName 101 | return nil 102 | } 103 | 104 | prx.peersMu.RLock() 105 | defer prx.peersMu.RUnlock() 106 | found := false 107 | peerName := "" 108 | for _, peer := range prx.lastFetchedPeers { 109 | if req.signer == peer.OrderflowProxy.EcdsaPubkeyAddress { 110 | found = true 111 | peerName = peer.Name 112 | break 113 | } 114 | } 115 | if !found { 116 | return errUnknownPeer 117 | } 118 | req.peerName = peerName 119 | return nil 120 | } 121 | 122 | func (prx *ReceiverProxy) EthSendBundle(ctx context.Context, ethSendBundle rpctypes.EthSendBundleArgs, systemEndpoint bool) error { 123 | startAt := time.Now() 124 | parsedRequest := ParsedRequest{ 125 | systemEndpoint: systemEndpoint, 126 | ethSendBundle: ðSendBundle, 127 | method: EthSendBundleMethod, 128 | size: rpcserver.GetRequestSize(ctx), 129 | } 130 | 131 | err := prx.ValidateSigner(ctx, &parsedRequest, systemEndpoint) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | _, err = EnsureReplacementUUID(ðSendBundle) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | err = ValidateEthSendBundle(ðSendBundle, systemEndpoint) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "validation") 147 | startAt = time.Now() 148 | 149 | // For direct orderflow we extract signing address from header 150 | // We also default to v2 bundle version if it's not set 151 | if !systemEndpoint { 152 | ethSendBundle.SigningAddress = &parsedRequest.signer 153 | // when receiving orderflow directly we default to v2 (currently latest version) 154 | // for non-publicEndpoint we set v1 explicitly on sender_proxy, it might be a bit confusing 155 | if ethSendBundle.Version == nil || *ethSendBundle.Version == "" { 156 | version := rpctypes.BundleVersionV2 157 | ethSendBundle.Version = &version 158 | } 159 | if ethSendBundle.ReplacementUUID != nil { 160 | timestampInt := apiNow().UnixMicro() 161 | var timestamp uint64 162 | if timestampInt < 0 { 163 | timestamp = 0 164 | } else { 165 | timestamp = uint64(timestampInt) 166 | } 167 | 168 | if ethSendBundle.ReplacementNonce == nil { 169 | ethSendBundle.ReplacementNonce = ×tamp 170 | } 171 | } 172 | } 173 | 174 | uniqueKey := ethSendBundle.UniqueKey() 175 | parsedRequest.requestArgUniqueKey = &uniqueKey 176 | 177 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "add_fields") 178 | 179 | return prx.HandleParsedRequest(ctx, parsedRequest) 180 | } 181 | 182 | func (prx *ReceiverProxy) EthSendBundleSystem(ctx context.Context, ethSendBundle rpctypes.EthSendBundleArgs) error { 183 | return prx.EthSendBundle(ctx, ethSendBundle, true) 184 | } 185 | 186 | func (prx *ReceiverProxy) EthSendBundleUser(ctx context.Context, ethSendBundle rpctypes.EthSendBundleArgs) error { 187 | return prx.EthSendBundle(ctx, ethSendBundle, false) 188 | } 189 | 190 | func (prx *ReceiverProxy) MevSendBundle(ctx context.Context, mevSendBundle rpctypes.MevSendBundleArgs, systemEndpoint bool) error { 191 | startAt := time.Now() 192 | parsedRequest := ParsedRequest{ 193 | systemEndpoint: systemEndpoint, 194 | mevSendBundle: &mevSendBundle, 195 | method: MevSendBundleMethod, 196 | size: rpcserver.GetRequestSize(ctx), 197 | } 198 | 199 | err := prx.ValidateSigner(ctx, &parsedRequest, systemEndpoint) 200 | if err != nil { 201 | return err 202 | } 203 | 204 | err = ValidateMevSendBundle(&mevSendBundle, systemEndpoint) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "validation") 210 | startAt = time.Now() 211 | 212 | if !systemEndpoint { 213 | mevSendBundle.Metadata = &rpctypes.MevBundleMetadata{ 214 | Signer: &parsedRequest.signer, 215 | } 216 | if mevSendBundle.ReplacementUUID != "" { 217 | replUUID, err := uuid.Parse(mevSendBundle.ReplacementUUID) 218 | if err != nil { 219 | return errors.Join(errUUIDParse, err) 220 | } 221 | replacementKey := replacementNonceKey{ 222 | uuid: replUUID, 223 | signer: parsedRequest.signer, 224 | } 225 | // this is not atomic but the normal user will not send multiple replacements in parallel 226 | nonce, ok := prx.replacementNonceRLU.Peek(replacementKey) 227 | if ok { 228 | nonce += 1 229 | } else { 230 | nonce = 0 231 | } 232 | prx.replacementNonceRLU.Add(replacementKey, nonce) 233 | mevSendBundle.Metadata.ReplacementNonce = &nonce 234 | 235 | if len(mevSendBundle.Body) == 0 { 236 | cancelled := true 237 | mevSendBundle.Metadata.Cancelled = &cancelled 238 | } 239 | } 240 | } 241 | 242 | // @note: unique key filterst same requests and it can interact with cancellations (you can't cancel multiple times per block) 243 | uniqueKey := mevSendBundle.UniqueKey() 244 | parsedRequest.requestArgUniqueKey = &uniqueKey 245 | 246 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "add_fields") 247 | 248 | return prx.HandleParsedRequest(ctx, parsedRequest) 249 | } 250 | 251 | func (prx *ReceiverProxy) MevSendBundleSystem(ctx context.Context, mevSendBundle rpctypes.MevSendBundleArgs) error { 252 | return prx.MevSendBundle(ctx, mevSendBundle, true) 253 | } 254 | 255 | func (prx *ReceiverProxy) MevSendBundleUser(ctx context.Context, mevSendBundle rpctypes.MevSendBundleArgs) error { 256 | return prx.MevSendBundle(ctx, mevSendBundle, false) 257 | } 258 | 259 | func (prx *ReceiverProxy) EthCancelBundle(ctx context.Context, ethCancelBundle rpctypes.EthCancelBundleArgs, systemEndpoint bool) error { 260 | parsedRequest := ParsedRequest{ 261 | systemEndpoint: systemEndpoint, 262 | ethCancelBundle: ðCancelBundle, 263 | method: EthCancelBundleMethod, 264 | size: rpcserver.GetRequestSize(ctx), 265 | } 266 | 267 | err := prx.ValidateSigner(ctx, &parsedRequest, systemEndpoint) 268 | if err != nil { 269 | return err 270 | } 271 | 272 | err = ValidateEthCancelBundle(ðCancelBundle, systemEndpoint) 273 | if err != nil { 274 | return err 275 | } 276 | 277 | if !systemEndpoint { 278 | ethCancelBundle.SigningAddress = &parsedRequest.signer 279 | } 280 | return prx.HandleParsedRequest(ctx, parsedRequest) 281 | } 282 | 283 | func (prx *ReceiverProxy) EthCancelBundleSystem(ctx context.Context, ethCancelBundle rpctypes.EthCancelBundleArgs) error { 284 | return prx.EthCancelBundle(ctx, ethCancelBundle, true) 285 | } 286 | 287 | func (prx *ReceiverProxy) EthCancelBundleUser(ctx context.Context, ethCancelBundle rpctypes.EthCancelBundleArgs) error { 288 | return prx.EthCancelBundle(ctx, ethCancelBundle, false) 289 | } 290 | 291 | func (prx *ReceiverProxy) EthSendRawTransaction(ctx context.Context, ethSendRawTransaction rpctypes.EthSendRawTransactionArgs, systemEndpoint bool) error { 292 | parsedRequest := ParsedRequest{ 293 | systemEndpoint: systemEndpoint, 294 | ethSendRawTransaction: ðSendRawTransaction, 295 | method: EthSendRawTransactionMethod, 296 | size: rpcserver.GetRequestSize(ctx), 297 | } 298 | err := prx.ValidateSigner(ctx, &parsedRequest, systemEndpoint) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | uniqueKey := ethSendRawTransaction.UniqueKey() 304 | parsedRequest.requestArgUniqueKey = &uniqueKey 305 | 306 | return prx.HandleParsedRequest(ctx, parsedRequest) 307 | } 308 | 309 | func (prx *ReceiverProxy) EthSendRawTransactionSystem(ctx context.Context, ethSendRawTransaction rpctypes.EthSendRawTransactionArgs) error { 310 | return prx.EthSendRawTransaction(ctx, ethSendRawTransaction, true) 311 | } 312 | 313 | func (prx *ReceiverProxy) EthSendRawTransactionUser(ctx context.Context, ethSendRawTransaction rpctypes.EthSendRawTransactionArgs) error { 314 | return prx.EthSendRawTransaction(ctx, ethSendRawTransaction, false) 315 | } 316 | 317 | func (prx *ReceiverProxy) BidSubsidiseBlock(ctx context.Context, bidSubsidiseBlock rpctypes.BidSubsisideBlockArgs, systemEndpoint bool) error { 318 | if !systemEndpoint { 319 | return errSubsidyWrongEndpoint 320 | } 321 | 322 | parsedRequest := ParsedRequest{ 323 | systemEndpoint: systemEndpoint, 324 | bidSubsidiseBlock: &bidSubsidiseBlock, 325 | method: BidSubsidiseBlockMethod, 326 | size: rpcserver.GetRequestSize(ctx), 327 | } 328 | 329 | err := prx.ValidateSigner(ctx, &parsedRequest, systemEndpoint) 330 | if err != nil { 331 | return err 332 | } 333 | 334 | if parsedRequest.signer != prx.FlashbotsSignerAddress { 335 | return errSubsidyWrongCaller 336 | } 337 | 338 | uniqueKey := bidSubsidiseBlock.UniqueKey() 339 | parsedRequest.requestArgUniqueKey = &uniqueKey 340 | 341 | return prx.HandleParsedRequest(ctx, parsedRequest) 342 | } 343 | 344 | func (prx *ReceiverProxy) BidSubsidiseBlockSystem(ctx context.Context, bidSubsidiseBlock rpctypes.BidSubsisideBlockArgs) error { 345 | return prx.BidSubsidiseBlock(ctx, bidSubsidiseBlock, true) 346 | } 347 | 348 | func (prx *ReceiverProxy) BidSubsidiseBlockUser(ctx context.Context, bidSubsidiseBlock rpctypes.BidSubsisideBlockArgs) error { 349 | return prx.BidSubsidiseBlock(ctx, bidSubsidiseBlock, false) 350 | } 351 | 352 | type ParsedRequest struct { 353 | systemEndpoint bool 354 | signer common.Address 355 | method string 356 | peerName string 357 | size int 358 | receivedAt time.Time 359 | requestArgUniqueKey *uuid.UUID 360 | ethSendBundle *rpctypes.EthSendBundleArgs 361 | mevSendBundle *rpctypes.MevSendBundleArgs 362 | ethCancelBundle *rpctypes.EthCancelBundleArgs 363 | ethSendRawTransaction *rpctypes.EthSendRawTransactionArgs 364 | bidSubsidiseBlock *rpctypes.BidSubsisideBlockArgs 365 | } 366 | 367 | func (prx *ReceiverProxy) HandleParsedRequest(ctx context.Context, parsedRequest ParsedRequest) error { 368 | startAt := time.Now() 369 | ctx, cancel := context.WithTimeout(ctx, handleParsedRequestTimeout) 370 | defer cancel() 371 | 372 | parsedRequest.receivedAt = apiNow() 373 | prx.Log.Debug("Received request", slog.Bool("isSystemEndpoint", parsedRequest.systemEndpoint), slog.String("method", parsedRequest.method)) 374 | if parsedRequest.systemEndpoint { 375 | incAPIIncomingRequestsByPeer(parsedRequest.peerName) 376 | } 377 | if parsedRequest.requestArgUniqueKey != nil { 378 | if prx.requestUniqueKeysRLU.Contains(*parsedRequest.requestArgUniqueKey) { 379 | incAPIDuplicateRequestsByPeer(parsedRequest.peerName) 380 | return nil 381 | } 382 | prx.requestUniqueKeysRLU.Add(*parsedRequest.requestArgUniqueKey, struct{}{}) 383 | } 384 | 385 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "validation") 386 | startAt = time.Now() 387 | 388 | if !parsedRequest.systemEndpoint { 389 | err := prx.userAPIRateLimiter.Wait(ctx) 390 | if err != nil { 391 | incAPIUserRateLimits() 392 | return errors.Join(errRateLimiting, err) 393 | } 394 | } 395 | 396 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "rate_limiting") 397 | startAt = time.Now() 398 | 399 | select { 400 | case <-ctx.Done(): 401 | prx.Log.Error("Shared queue is stalling") 402 | case prx.shareQueue <- &parsedRequest: 403 | } 404 | 405 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "share_queue") 406 | startAt = time.Now() 407 | 408 | if !parsedRequest.systemEndpoint { 409 | select { 410 | case <-ctx.Done(): 411 | prx.Log.Error("Archive queue is stalling") 412 | case prx.archiveQueue <- &parsedRequest: 413 | } 414 | } 415 | 416 | incRequestDurationStep(time.Since(startAt), parsedRequest.method, "", "archive_queue") 417 | return nil 418 | } 419 | 420 | func (prx *ReceiverProxy) prepHTML() ([]byte, error) { 421 | templ, err := template.New("index").Parse(landingPageHTML) 422 | if err != nil { 423 | return nil, err 424 | } 425 | 426 | htmlData := struct { 427 | Cert string 428 | }{ 429 | Cert: string(prx.PublicCertPEM), 430 | } 431 | htmlBytes := bytes.Buffer{} 432 | err = templ.Execute(&htmlBytes, htmlData) 433 | if err != nil { 434 | return nil, err 435 | } 436 | 437 | return htmlBytes.Bytes(), nil 438 | } 439 | -------------------------------------------------------------------------------- /proxy/receiver_proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "log/slog" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/flashbots/go-utils/rpcclient" 13 | "github.com/flashbots/go-utils/signature" 14 | utils_tls "github.com/flashbots/go-utils/tls" 15 | "github.com/google/uuid" 16 | "github.com/hashicorp/golang-lru/v2/expirable" 17 | "golang.org/x/time/rate" 18 | ) 19 | 20 | var ( 21 | requestsRLUSize = 4096 22 | requestsRLUTTL = time.Second * 12 23 | 24 | peerUpdateTime = time.Second * 30 25 | 26 | replacementNonceSize = 4096 27 | replacementNonceTTL = time.Second * 5 * 12 28 | 29 | ReceiverProxyWorkerQueueSize = 10000 30 | ) 31 | 32 | type replacementNonceKey struct { 33 | uuid uuid.UUID 34 | signer common.Address 35 | } 36 | 37 | type ReceiverProxy struct { 38 | ReceiverProxyConstantConfig 39 | 40 | ConfigHub *BuilderConfigHub 41 | 42 | OrderflowSigner *signature.Signer 43 | PublicCertPEM []byte 44 | Certificate tls.Certificate 45 | 46 | localBuilder rpcclient.RPCClient 47 | 48 | UserHandler http.Handler 49 | SystemHandler http.Handler 50 | CertHandler http.Handler // this endpoint just returns generated certificate 51 | 52 | updatePeers chan []ConfighubBuilder 53 | shareQueue chan *ParsedRequest 54 | 55 | archiveQueue chan *ParsedRequest 56 | archiveFlushQueue chan struct{} 57 | 58 | peersMu sync.RWMutex 59 | lastFetchedPeers []ConfighubBuilder 60 | 61 | requestUniqueKeysRLU *expirable.LRU[uuid.UUID, struct{}] 62 | 63 | replacementNonceRLU *expirable.LRU[replacementNonceKey, int] 64 | 65 | peerUpdaterClose chan struct{} 66 | 67 | userAPIRateLimiter *rate.Limiter 68 | } 69 | 70 | type ReceiverProxyConstantConfig struct { 71 | Log *slog.Logger 72 | // Name is optional field and it used to distringuish multiple proxies when running in the same process in tests 73 | Name string 74 | FlashbotsSignerAddress common.Address 75 | } 76 | 77 | type ReceiverProxyConfig struct { 78 | ReceiverProxyConstantConfig 79 | 80 | CertValidDuration time.Duration 81 | CertHosts []string 82 | CertPath string 83 | CertKeyPath string 84 | 85 | BuilderConfigHubEndpoint string 86 | ArchiveEndpoint string 87 | ArchiveConnections int 88 | LocalBuilderEndpoint string 89 | 90 | // EthRPC should support eth_blockNumber API 91 | EthRPC string 92 | 93 | MaxRequestBodySizeBytes int64 94 | 95 | ConnectionsPerPeer int 96 | MaxUserRPS int 97 | ArchiveWorkerCount int 98 | } 99 | 100 | func NewReceiverProxy(config ReceiverProxyConfig) (*ReceiverProxy, error) { 101 | orderflowSigner, err := signature.NewRandomSigner() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | cert, key, err := utils_tls.GetOrGenerateTLS(config.CertPath, config.CertKeyPath, config.CertValidDuration, config.CertHosts) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | certificate, err := tls.X509KeyPair(cert, key) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | localCl := HTTPClientLocalhost(DefaultLocalhostMaxIdleConn) 117 | 118 | localBuilder := rpcclient.NewClientWithOpts(config.LocalBuilderEndpoint, &rpcclient.RPCClientOpts{ 119 | HTTPClient: localCl, 120 | }) 121 | 122 | limit := rate.Limit(config.MaxUserRPS) 123 | if config.MaxUserRPS == 0 { 124 | limit = rate.Inf 125 | } 126 | userAPIRateLimiter := rate.NewLimiter(limit, config.MaxUserRPS) 127 | prx := &ReceiverProxy{ 128 | ReceiverProxyConstantConfig: config.ReceiverProxyConstantConfig, 129 | ConfigHub: NewBuilderConfigHub(config.Log, config.BuilderConfigHubEndpoint), 130 | OrderflowSigner: orderflowSigner, 131 | PublicCertPEM: cert, 132 | Certificate: certificate, 133 | localBuilder: localBuilder, 134 | requestUniqueKeysRLU: expirable.NewLRU[uuid.UUID, struct{}](requestsRLUSize, nil, requestsRLUTTL), 135 | replacementNonceRLU: expirable.NewLRU[replacementNonceKey, int](replacementNonceSize, nil, replacementNonceTTL), 136 | userAPIRateLimiter: userAPIRateLimiter, 137 | } 138 | maxRequestBodySizeBytes := DefaultMaxRequestBodySizeBytes 139 | if config.MaxRequestBodySizeBytes != 0 { 140 | maxRequestBodySizeBytes = config.MaxRequestBodySizeBytes 141 | } 142 | 143 | systemHandler, err := prx.SystemJSONRPCHandler(maxRequestBodySizeBytes) 144 | if err != nil { 145 | return nil, err 146 | } 147 | prx.SystemHandler = systemHandler 148 | 149 | userHandler, err := prx.UserJSONRPCHandler(maxRequestBodySizeBytes) 150 | if err != nil { 151 | return nil, err 152 | } 153 | prx.UserHandler = userHandler 154 | 155 | prx.CertHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 156 | w.Header().Add("Content-Type", "application/octet-stream") 157 | _, err := w.Write(prx.PublicCertPEM) 158 | if err != nil { 159 | prx.Log.Warn("Failed to serve certificate", slog.Any("error", err)) 160 | } 161 | }) 162 | 163 | shareQeueuCh := make(chan *ParsedRequest, ReceiverProxyWorkerQueueSize) 164 | updatePeersCh := make(chan []ConfighubBuilder) 165 | prx.shareQueue = shareQeueuCh 166 | prx.updatePeers = updatePeersCh 167 | queue := ShareQueue{ 168 | name: prx.Name, 169 | log: prx.Log, 170 | queue: shareQeueuCh, 171 | updatePeers: updatePeersCh, 172 | localBuilder: prx.localBuilder, 173 | signer: prx.OrderflowSigner, 174 | workersPerPeer: config.ConnectionsPerPeer, 175 | } 176 | go queue.Run() 177 | 178 | archiveQueueCh := make(chan *ParsedRequest, ReceiverProxyWorkerQueueSize) 179 | archiveFlushCh := make(chan struct{}) 180 | prx.archiveQueue = archiveQueueCh 181 | prx.archiveFlushQueue = archiveFlushCh 182 | archiveHTTPClient := HTTPClientWithMaxConnections(config.ArchiveConnections) 183 | archiveClient := rpcclient.NewClientWithOpts(config.ArchiveEndpoint, &rpcclient.RPCClientOpts{ 184 | Signer: orderflowSigner, 185 | HTTPClient: archiveHTTPClient, 186 | }) 187 | archiveQueue := ArchiveQueue{ 188 | log: prx.Log, 189 | queue: archiveQueueCh, 190 | flushQueue: archiveFlushCh, 191 | archiveClient: archiveClient, 192 | blockNumberSource: NewBlockNumberSource(config.EthRPC), 193 | workerCount: config.ArchiveWorkerCount, 194 | } 195 | go archiveQueue.Run() 196 | 197 | prx.peerUpdaterClose = make(chan struct{}) 198 | go func() { 199 | for { 200 | select { 201 | case _, more := <-prx.peerUpdaterClose: 202 | if !more { 203 | return 204 | } 205 | case <-time.After(peerUpdateTime): 206 | err := prx.RequestNewPeers() 207 | if err != nil { 208 | prx.Log.Error("Failed to update peers", slog.Any("error", err)) 209 | } 210 | } 211 | } 212 | }() 213 | 214 | // request peers on the first start 215 | _ = prx.RequestNewPeers() 216 | 217 | return prx, nil 218 | } 219 | 220 | func (prx *ReceiverProxy) Stop() { 221 | close(prx.shareQueue) 222 | close(prx.updatePeers) 223 | close(prx.archiveQueue) 224 | close(prx.archiveFlushQueue) 225 | close(prx.peerUpdaterClose) 226 | } 227 | 228 | func (prx *ReceiverProxy) TLSConfig() *tls.Config { 229 | return &tls.Config{ 230 | Certificates: []tls.Certificate{prx.Certificate}, 231 | MinVersion: tls.VersionTLS13, 232 | } 233 | } 234 | 235 | func (prx *ReceiverProxy) RegisterSecrets(ctx context.Context) error { 236 | const maxRetries = 10 237 | const timeBetweenRetries = time.Second * 10 238 | 239 | retry := 0 240 | for { 241 | if ctx.Err() != nil { 242 | return ctx.Err() 243 | } 244 | err := prx.ConfigHub.RegisterCredentials(ctx, ConfighubOrderflowProxyCredentials{ 245 | TLSCert: string(prx.PublicCertPEM), 246 | EcdsaPubkeyAddress: prx.OrderflowSigner.Address(), 247 | }) 248 | if err == nil { 249 | prx.Log.Info("Credentials registered on config hub") 250 | return nil 251 | } 252 | 253 | retry += 1 254 | if retry >= maxRetries { 255 | return err 256 | } 257 | prx.Log.Error("Fail to register credentials", slog.Any("error", err)) 258 | select { 259 | case <-ctx.Done(): 260 | return ctx.Err() 261 | case <-time.After(timeBetweenRetries): 262 | } 263 | } 264 | } 265 | 266 | // RequestNewPeers updates currently available peers from the builder config hub 267 | func (prx *ReceiverProxy) RequestNewPeers() error { 268 | builders, err := prx.ConfigHub.Builders(false) 269 | if err != nil { 270 | return err 271 | } 272 | 273 | prx.peersMu.Lock() 274 | prx.lastFetchedPeers = builders 275 | prx.peersMu.Unlock() 276 | 277 | select { 278 | case prx.updatePeers <- builders: 279 | default: 280 | } 281 | return nil 282 | } 283 | 284 | // FlushArchiveQueue forces the archive queue to flush 285 | func (prx *ReceiverProxy) FlushArchiveQueue() { 286 | prx.archiveFlushQueue <- struct{}{} 287 | } 288 | -------------------------------------------------------------------------------- /proxy/receiver_proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "math/big" 10 | "net" 11 | "net/http" 12 | "net/http/httptest" 13 | "os" 14 | "path" 15 | "testing" 16 | "time" 17 | 18 | "github.com/ethereum/go-ethereum/common" 19 | "github.com/ethereum/go-ethereum/common/hexutil" 20 | "github.com/ethereum/go-ethereum/core/types" 21 | "github.com/ethereum/go-ethereum/crypto" 22 | "github.com/flashbots/go-utils/rpctypes" 23 | "github.com/flashbots/go-utils/signature" 24 | "github.com/stretchr/testify/require" 25 | ) 26 | 27 | type RequestData struct { 28 | request *http.Request 29 | body string 30 | } 31 | 32 | var ( 33 | builderHub *httptest.Server 34 | 35 | builderHubPeers []ConfighubBuilder 36 | 37 | archiveServer *httptest.Server 38 | archiveServerRequests chan *RequestData 39 | 40 | proxies []*OrderflowProxyTestSetup 41 | 42 | flashbotsSigner *signature.Signer 43 | ) 44 | 45 | func ServeHTTPRequestToChan(channel chan *RequestData) *httptest.Server { 46 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 | body, _ := io.ReadAll(r.Body) 48 | defer r.Body.Close() 49 | 50 | channel <- &RequestData{body: string(body), request: r} 51 | w.WriteHeader(http.StatusOK) 52 | _, _ = w.Write([]byte("{}")) 53 | })) 54 | } 55 | 56 | type OrderflowProxyTestSetup struct { 57 | proxy *ReceiverProxy 58 | publicServer *http.Server 59 | localServer *http.Server 60 | certServer *httptest.Server 61 | 62 | ip string 63 | publicServerEndpoint string 64 | localServerEndpoint string 65 | 66 | localBuilderServer *httptest.Server 67 | localBuilderRequests chan *RequestData 68 | } 69 | 70 | func (setup *OrderflowProxyTestSetup) Close() { 71 | _ = setup.publicServer.Close() 72 | _ = setup.localServer.Close() 73 | setup.certServer.Close() 74 | setup.localBuilderServer.Close() 75 | setup.proxy.Stop() 76 | } 77 | 78 | func StartTestOrderflowProxy(name, certPath, certKeyPath string) (*OrderflowProxyTestSetup, error) { 79 | localBuilderRequests := make(chan *RequestData, 1) 80 | localBuilderServer := ServeHTTPRequestToChan(localBuilderRequests) 81 | 82 | proxy := createProxy(localBuilderServer.URL, name, certPath, certKeyPath) 83 | publicProxyServer := &http.Server{ //nolint:gosec 84 | Handler: proxy.SystemHandler, 85 | TLSConfig: proxy.TLSConfig(), 86 | } 87 | publicListener, err := net.Listen("tcp", ":0") //nolint:gosec 88 | if err != nil { 89 | return nil, err 90 | } 91 | go publicProxyServer.ServeTLS(publicListener, "", "") //nolint: errcheck 92 | publicServerEndpoint := fmt.Sprintf("https://localhost:%d", publicListener.Addr().(*net.TCPAddr).Port) //nolint:forcetypeassert 93 | ip := fmt.Sprintf("127.0.0.1:%d", publicListener.Addr().(*net.TCPAddr).Port) //nolint:forcetypeassert 94 | 95 | localProxyServer := &http.Server{ //nolint:gosec 96 | Handler: proxy.UserHandler, 97 | TLSConfig: proxy.TLSConfig(), 98 | } 99 | localListener, err := net.Listen("tcp", ":0") //nolint:gosec 100 | if err != nil { 101 | return nil, err 102 | } 103 | go localProxyServer.ServeTLS(localListener, "", "") //nolint:errcheck 104 | localServerEndpoint := fmt.Sprintf("https://localhost:%d", localListener.Addr().(*net.TCPAddr).Port) //nolint:forcetypeassert 105 | 106 | certProxyServer := httptest.NewServer(proxy.CertHandler) 107 | 108 | return &OrderflowProxyTestSetup{ 109 | proxy: proxy, 110 | publicServer: publicProxyServer, 111 | localServer: localProxyServer, 112 | certServer: certProxyServer, 113 | publicServerEndpoint: publicServerEndpoint, 114 | localServerEndpoint: localServerEndpoint, 115 | localBuilderServer: localBuilderServer, 116 | localBuilderRequests: localBuilderRequests, 117 | ip: ip, 118 | }, nil 119 | } 120 | 121 | func check(err error) { 122 | if err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | func TestMain(m *testing.M) { 128 | signer, err := signature.NewRandomSigner() 129 | if err != nil { 130 | panic(err) 131 | } 132 | flashbotsSigner = signer 133 | 134 | archiveServerRequests = make(chan *RequestData) 135 | builderHub = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 136 | body, _ := io.ReadAll(r.Body) 137 | defer r.Body.Close() 138 | 139 | if r.URL.Path == "/api/l1-builder/v1/register_credentials/orderflow_proxy" { 140 | var req ConfighubOrderflowProxyCredentials 141 | err := json.Unmarshal(body, &req) 142 | if err != nil { 143 | w.WriteHeader(http.StatusBadRequest) 144 | _, _ = w.Write([]byte(err.Error())) 145 | } 146 | var ( 147 | ip string 148 | name string 149 | ) 150 | for _, proxy := range proxies { 151 | if proxy.proxy.OrderflowSigner.Address() == req.EcdsaPubkeyAddress { 152 | ip = proxy.ip 153 | name = proxy.proxy.Name 154 | break 155 | } 156 | } 157 | builderHubPeers = append(builderHubPeers, ConfighubBuilder{ 158 | Name: name, 159 | IP: ip, 160 | OrderflowProxy: req, 161 | }) 162 | } else if r.URL.Path == "/api/l1-builder/v1/builders" { 163 | res, err := json.Marshal(builderHubPeers) 164 | if err != nil { 165 | panic(err) 166 | } 167 | w.WriteHeader(http.StatusOK) 168 | _, _ = w.Write(res) 169 | } else { 170 | w.WriteHeader(http.StatusBadRequest) 171 | } 172 | })) 173 | defer builderHub.Close() 174 | 175 | archiveServer = ServeHTTPRequestToChan(archiveServerRequests) 176 | defer archiveServer.Close() 177 | 178 | tempDirs := make([]string, 0) 179 | for i := range 3 { 180 | tempDir, err := os.MkdirTemp("", "orderflow-proxy-test") 181 | check(err) 182 | tempDirs = append(tempDirs, tempDir) 183 | certPath := path.Join(tempDir, "cert") 184 | keyPath := path.Join(tempDir, "key") 185 | 186 | proxy, err := StartTestOrderflowProxy(fmt.Sprintf("proxy:%d", i), certPath, keyPath) 187 | proxies = append(proxies, proxy) 188 | check(err) 189 | } 190 | 191 | defer func() { 192 | for _, prx := range proxies { 193 | prx.Close() 194 | } 195 | 196 | for _, dir := range tempDirs { 197 | _ = os.RemoveAll(dir) 198 | } 199 | }() 200 | 201 | os.Exit(m.Run()) 202 | } 203 | 204 | func createProxy(localBuilder, name, certPath, certKeyPath string) *ReceiverProxy { 205 | log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) 206 | proxy, err := NewReceiverProxy(ReceiverProxyConfig{ 207 | ReceiverProxyConstantConfig: ReceiverProxyConstantConfig{ 208 | Log: log, 209 | Name: name, 210 | FlashbotsSignerAddress: flashbotsSigner.Address(), 211 | }, 212 | CertValidDuration: time.Hour * 24, 213 | CertHosts: []string{"localhost", "127.0.0.1"}, 214 | CertPath: certPath, 215 | CertKeyPath: certKeyPath, 216 | 217 | BuilderConfigHubEndpoint: builderHub.URL, 218 | ArchiveEndpoint: archiveServer.URL, 219 | LocalBuilderEndpoint: localBuilder, 220 | EthRPC: "eth-rpc-not-set", 221 | MaxUserRPS: 10, 222 | }) 223 | if err != nil { 224 | panic(err) 225 | } 226 | return proxy 227 | } 228 | 229 | func TestPublishSecrets(t *testing.T) { 230 | err := proxies[0].proxy.RegisterSecrets(context.Background()) 231 | require.NoError(t, err) 232 | } 233 | 234 | func TestServeCert(t *testing.T) { 235 | resp, err := http.Get(proxies[0].certServer.URL) 236 | require.NoError(t, err) 237 | defer resp.Body.Close() 238 | 239 | body, err := io.ReadAll(resp.Body) 240 | require.NoError(t, err) 241 | 242 | require.Equal(t, string(proxies[0].proxy.PublicCertPEM), string(body)) 243 | } 244 | 245 | func expectRequest(t *testing.T, ch chan *RequestData) *RequestData { 246 | t.Helper() 247 | select { 248 | case req := <-ch: 249 | return req 250 | case <-time.After(time.Millisecond * 100): 251 | t.Fatal("Timeout while waiting for request") 252 | return nil 253 | } 254 | } 255 | 256 | func expectNoRequest(t *testing.T, ch chan *RequestData) { 257 | t.Helper() 258 | select { 259 | case req := <-ch: 260 | t.Fatal("Unexpected request", req) 261 | case <-time.After(time.Millisecond * 100): 262 | } 263 | } 264 | 265 | func proxiesUpdatePeers(t *testing.T) { 266 | t.Helper() 267 | for _, instance := range proxies { 268 | err := instance.proxy.RequestNewPeers() 269 | require.NoError(t, err) 270 | } 271 | time.Sleep(time.Millisecond * 50) 272 | } 273 | 274 | func proxiesFlushQueue() { 275 | for _, instance := range proxies { 276 | instance.proxy.FlushArchiveQueue() 277 | } 278 | } 279 | 280 | func TestProxyBundleRequestWithPeerUpdate(t *testing.T) { 281 | defer func() { 282 | proxiesFlushQueue() 283 | for { 284 | select { 285 | case <-time.After(time.Millisecond * 100): 286 | expectNoRequest(t, archiveServerRequests) 287 | return 288 | case <-archiveServerRequests: 289 | } 290 | } 291 | }() 292 | apiNow = func() time.Time { 293 | return time.Unix(1730000000, 0) 294 | } 295 | defer func() { 296 | apiNow = time.Now 297 | }() 298 | 299 | signer, err := signature.NewSignerFromHexPrivateKey("0xd63b3c447fdea415a05e4c0b859474d14105a88178efdf350bc9f7b05be3cc58") 300 | require.NoError(t, err) 301 | client, err := RPCClientWithCertAndSigner(proxies[0].localServerEndpoint, proxies[0].proxy.PublicCertPEM, signer, 1) 302 | require.NoError(t, err) 303 | 304 | expectedRequest := `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x3e8","version":"v2","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"}],"id":0,"jsonrpc":"2.0"}` 305 | 306 | // we start with no peers 307 | builderHubPeers = nil 308 | err = proxies[0].proxy.RegisterSecrets(context.Background()) 309 | require.NoError(t, err) 310 | proxiesUpdatePeers(t) 311 | 312 | blockNumber := hexutil.Uint64(1000) 313 | resp, err := client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 314 | BlockNumber: &blockNumber, 315 | }) 316 | require.NoError(t, err) 317 | require.Nil(t, resp.Error) 318 | 319 | builderRequest := expectRequest(t, proxies[0].localBuilderRequests) 320 | require.Equal(t, expectedRequest, builderRequest.body) 321 | expectNoRequest(t, proxies[1].localBuilderRequests) 322 | expectNoRequest(t, proxies[2].localBuilderRequests) 323 | 324 | slog.Info("Adding first peer") 325 | 326 | // add one more peer 327 | err = proxies[1].proxy.RegisterSecrets(context.Background()) 328 | require.NoError(t, err) 329 | proxiesUpdatePeers(t) 330 | 331 | blockNumber = hexutil.Uint64(1001) 332 | _, err = client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 333 | BlockNumber: &blockNumber, 334 | }) 335 | require.NoError(t, err) 336 | 337 | expectedRequest = `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x3e9","version":"v2","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"}],"id":0,"jsonrpc":"2.0"}` 338 | builderRequest = expectRequest(t, proxies[0].localBuilderRequests) 339 | require.Equal(t, expectedRequest, builderRequest.body) 340 | builderRequest = expectRequest(t, proxies[1].localBuilderRequests) 341 | require.Equal(t, expectedRequest, builderRequest.body) 342 | expectNoRequest(t, proxies[2].localBuilderRequests) 343 | 344 | // add another peer 345 | slog.Info("Adding second peer") 346 | 347 | err = proxies[2].proxy.RegisterSecrets(context.Background()) 348 | require.NoError(t, err) 349 | proxiesUpdatePeers(t) 350 | 351 | blockNumber = hexutil.Uint64(1002) 352 | replacementUUID := "550e8400-e29b-41d4-a716-446655440000" 353 | _, err = client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 354 | BlockNumber: &blockNumber, 355 | ReplacementUUID: &replacementUUID, 356 | }) 357 | require.NoError(t, err) 358 | 359 | expectedRequest = `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x3ea","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","version":"v2","replacementNonce":1730000000000000,"signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"}],"id":0,"jsonrpc":"2.0"}` 360 | builderRequest = expectRequest(t, proxies[0].localBuilderRequests) 361 | require.Equal(t, expectedRequest, builderRequest.body) 362 | builderRequest = expectRequest(t, proxies[1].localBuilderRequests) 363 | require.Equal(t, expectedRequest, builderRequest.body) 364 | builderRequest = expectRequest(t, proxies[2].localBuilderRequests) 365 | require.Equal(t, expectedRequest, builderRequest.body) 366 | 367 | replacementNonce := uint64(8976) 368 | _, err = client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 369 | BlockNumber: &blockNumber, 370 | ReplacementUUID: &replacementUUID, 371 | ReplacementNonce: &replacementNonce, 372 | }) 373 | require.NoError(t, err) 374 | 375 | expectedRequest = `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x3ea","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","version":"v2","replacementNonce":8976,"signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"}],"id":0,"jsonrpc":"2.0"}` 376 | builderRequest = expectRequest(t, proxies[0].localBuilderRequests) 377 | require.Equal(t, expectedRequest, builderRequest.body) 378 | builderRequest = expectRequest(t, proxies[1].localBuilderRequests) 379 | require.Equal(t, expectedRequest, builderRequest.body) 380 | builderRequest = expectRequest(t, proxies[2].localBuilderRequests) 381 | require.Equal(t, expectedRequest, builderRequest.body) 382 | } 383 | 384 | func TestProxySendToArchive(t *testing.T) { 385 | signer, err := signature.NewSignerFromHexPrivateKey("0xd63b3c447fdea415a05e4c0b859474d14105a88178efdf350bc9f7b05be3cc58") 386 | require.NoError(t, err) 387 | client, err := RPCClientWithCertAndSigner(proxies[0].localServerEndpoint, proxies[0].proxy.PublicCertPEM, signer, 1) 388 | require.NoError(t, err) 389 | 390 | // we start with no peers 391 | builderHubPeers = nil 392 | err = proxies[0].proxy.RegisterSecrets(context.Background()) 393 | require.NoError(t, err) 394 | proxiesUpdatePeers(t) 395 | 396 | apiNow = func() time.Time { 397 | return time.Unix(1730000000, 0) 398 | } 399 | defer func() { 400 | apiNow = time.Now 401 | }() 402 | 403 | blockNumber := hexutil.Uint64(123) 404 | resp, err := client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 405 | BlockNumber: &blockNumber, 406 | }) 407 | require.NoError(t, err) 408 | require.Nil(t, resp.Error) 409 | _ = expectRequest(t, proxies[0].localBuilderRequests) 410 | 411 | blockNumber = hexutil.Uint64(456) 412 | resp, err = client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 413 | BlockNumber: &blockNumber, 414 | }) 415 | require.NoError(t, err) 416 | require.Nil(t, resp.Error) 417 | 418 | _ = expectRequest(t, proxies[0].localBuilderRequests) 419 | 420 | proxiesFlushQueue() 421 | archiveRequest := expectRequest(t, archiveServerRequests) 422 | expectedArchiveRequest := `{"method":"flashbots_newOrderEvents","params":[{"orderEvents":[{"eth_sendBundle":{"params":{"txs":null,"blockNumber":"0x7b","version":"v2","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"},"metadata":{"receivedAt":1730000000000}}},{"eth_sendBundle":{"params":{"txs":null,"blockNumber":"0x1c8","version":"v2","signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"},"metadata":{"receivedAt":1730000000000}}}]}],"id":0,"jsonrpc":"2.0"}` 423 | require.Equal(t, expectedArchiveRequest, archiveRequest.body) 424 | } 425 | 426 | func createTestTx(i int) *hexutil.Bytes { 427 | privateKey, err := crypto.HexToECDSA("c7589782d55a642c8ced7794ddcb24b62d4ebefbb81001034cb46545ff80e39e") 428 | if err != nil { 429 | panic(err) 430 | } 431 | 432 | chainID := big.NewInt(1) 433 | txData := &types.DynamicFeeTx{ 434 | ChainID: chainID, 435 | Nonce: uint64(i), //nolint:gosec 436 | GasTipCap: big.NewInt(1), 437 | GasFeeCap: big.NewInt(1), 438 | Gas: 21000, 439 | To: &common.Address{}, 440 | Value: big.NewInt(0), 441 | Data: nil, 442 | AccessList: nil, 443 | } 444 | tx, err := types.SignNewTx(privateKey, types.LatestSignerForChainID(big.NewInt(1)), txData) 445 | if err != nil { 446 | panic(err) 447 | } 448 | binary, err := tx.MarshalBinary() 449 | if err != nil { 450 | panic(err) 451 | } 452 | hexBytes := hexutil.Bytes(binary) 453 | return &hexBytes 454 | } 455 | 456 | func TestProxyShareBundleReplacementUUIDAndCancellation(t *testing.T) { 457 | defer func() { 458 | proxiesFlushQueue() 459 | for { 460 | select { 461 | case <-time.After(time.Millisecond * 100): 462 | expectNoRequest(t, archiveServerRequests) 463 | return 464 | case <-archiveServerRequests: 465 | } 466 | } 467 | }() 468 | 469 | signer, err := signature.NewSignerFromHexPrivateKey("0xd63b3c447fdea415a05e4c0b859474d14105a88178efdf350bc9f7b05be3cc58") 470 | require.NoError(t, err) 471 | client, err := RPCClientWithCertAndSigner(proxies[0].localServerEndpoint, proxies[0].proxy.PublicCertPEM, signer, 1) 472 | require.NoError(t, err) 473 | 474 | // we start with no peers 475 | builderHubPeers = nil 476 | err = proxies[0].proxy.RegisterSecrets(context.Background()) 477 | require.NoError(t, err) 478 | proxiesUpdatePeers(t) 479 | 480 | // first call 481 | resp, err := client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{ 482 | Version: "v0.1", 483 | ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000", 484 | Inclusion: rpctypes.MevBundleInclusion{ 485 | BlockNumber: 10, 486 | }, 487 | Body: []rpctypes.MevBundleBody{ 488 | { 489 | Tx: createTestTx(0), 490 | }, 491 | }, 492 | }) 493 | require.NoError(t, err) 494 | require.Nil(t, resp.Error) 495 | 496 | expectedRequest := `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0xa","maxBlock":"0x0"},"body":[{"tx":"0x02f862018001018252089400000000000000000000000000000000000000008080c001a05900a5ea3e4e07980b0d6276e2764b734be64d64c20f3eb87746c7ed1d72aa26a073f3a0877bd80098bd720afd1ba2c6d2d9c76d87cbc23f098d7be74902d9bfd4"}],"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":0}}],"id":0,"jsonrpc":"2.0"}` 497 | 498 | builderRequest := expectRequest(t, proxies[0].localBuilderRequests) 499 | require.Equal(t, expectedRequest, builderRequest.body) 500 | 501 | // second call 502 | resp, err = client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{ 503 | Version: "v0.1", 504 | ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000", 505 | Inclusion: rpctypes.MevBundleInclusion{ 506 | BlockNumber: 10, 507 | }, 508 | Body: []rpctypes.MevBundleBody{ 509 | { 510 | Tx: createTestTx(1), 511 | }, 512 | }, 513 | }) 514 | require.NoError(t, err) 515 | require.Nil(t, resp.Error) 516 | 517 | expectedRequest = `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0xa","maxBlock":"0x0"},"body":[{"tx":"0x02f862010101018252089400000000000000000000000000000000000000008080c001a03b5edc6a7fe16f7c7bf25c56281b86107e742a922f900ac94293225b380fd5bea00f2ea6392842711064ca5c0fe12d812a60e8936ec8dc13ca95ecde8b262fd1fe"}],"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":1}}],"id":0,"jsonrpc":"2.0"}` 518 | 519 | builderRequest = expectRequest(t, proxies[0].localBuilderRequests) 520 | require.Equal(t, expectedRequest, builderRequest.body) 521 | 522 | // cancell 523 | resp, err = client.Call(context.Background(), MevSendBundleMethod, &rpctypes.MevSendBundleArgs{ 524 | Version: "v0.1", 525 | ReplacementUUID: "550e8400-e29b-41d4-a716-446655440000", 526 | }) 527 | require.NoError(t, err) 528 | require.Nil(t, resp.Error) 529 | 530 | expectedRequest = `{"method":"mev_sendBundle","params":[{"version":"v0.1","replacementUuid":"550e8400-e29b-41d4-a716-446655440000","inclusion":{"block":"0x0","maxBlock":"0x0"},"body":null,"validity":{},"metadata":{"signer":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf","replacementNonce":2,"cancelled":true}}],"id":0,"jsonrpc":"2.0"}` 531 | 532 | builderRequest = expectRequest(t, proxies[0].localBuilderRequests) 533 | require.Equal(t, expectedRequest, builderRequest.body) 534 | } 535 | 536 | func TestProxyBidSubsidiseBlockCall(t *testing.T) { 537 | defer func() { 538 | proxiesFlushQueue() 539 | for { 540 | select { 541 | case <-time.After(time.Millisecond * 100): 542 | expectNoRequest(t, archiveServerRequests) 543 | return 544 | case <-archiveServerRequests: 545 | } 546 | } 547 | }() 548 | 549 | client, err := RPCClientWithCertAndSigner(proxies[0].publicServerEndpoint, proxies[0].proxy.PublicCertPEM, flashbotsSigner, 1) 550 | require.NoError(t, err) 551 | 552 | expectedRequest := `{"method":"bid_subsidiseBlock","params":[1000],"id":0,"jsonrpc":"2.0"}` 553 | 554 | // we add all proxies to the list of peers 555 | builderHubPeers = nil 556 | for _, proxy := range proxies { 557 | err = proxy.proxy.RegisterSecrets(context.Background()) 558 | require.NoError(t, err) 559 | } 560 | proxiesUpdatePeers(t) 561 | 562 | args := rpctypes.BidSubsisideBlockArgs(1000) 563 | resp, err := client.Call(context.Background(), BidSubsidiseBlockMethod, &args) 564 | require.NoError(t, err) 565 | require.Nil(t, resp.Error) 566 | 567 | builderRequest := expectRequest(t, proxies[0].localBuilderRequests) 568 | require.Equal(t, expectedRequest, builderRequest.body) 569 | expectNoRequest(t, proxies[1].localBuilderRequests) 570 | expectNoRequest(t, proxies[2].localBuilderRequests) 571 | } 572 | 573 | func TestBuilderNetRootCall(t *testing.T) { 574 | tempDir := t.TempDir() 575 | certPath := path.Join(tempDir, "cert") 576 | keyPath := path.Join(tempDir, "key") 577 | proxy, err := StartTestOrderflowProxy("1", certPath, keyPath) 578 | require.NoError(t, err) 579 | 580 | req := httptest.NewRequest(http.MethodGet, "/", nil) 581 | 582 | rr := httptest.NewRecorder() 583 | proxy.localServer.Handler.ServeHTTP(rr, req) 584 | respBody, err := io.ReadAll(rr.Body) 585 | require.NoError(t, err) 586 | require.Contains(t, string(respBody), "-----BEGIN CERTIFICATE-----") 587 | } 588 | 589 | func TestValidateLocalBundles(t *testing.T) { 590 | signer, err := signature.NewSignerFromHexPrivateKey("0xd63b3c447fdea415a05e4c0b859474d14105a88178efdf350bc9f7b05be3cc58") 591 | require.NoError(t, err) 592 | client, err := RPCClientWithCertAndSigner(proxies[0].localServerEndpoint, proxies[0].proxy.PublicCertPEM, signer, 1) 593 | require.NoError(t, err) 594 | 595 | pubClient, err := RPCClientWithCertAndSigner(proxies[0].publicServerEndpoint, proxies[0].proxy.PublicCertPEM, flashbotsSigner, 1) 596 | require.NoError(t, err) 597 | 598 | // we start with no peers 599 | builderHubPeers = nil 600 | err = proxies[0].proxy.RegisterSecrets(context.Background()) 601 | require.NoError(t, err) 602 | proxiesUpdatePeers(t) 603 | 604 | apiNow = func() time.Time { 605 | return time.Unix(1730000000, 0) 606 | } 607 | defer func() { 608 | apiNow = time.Now 609 | }() 610 | 611 | blockNumber := hexutil.Uint64(123) 612 | invalidVersion := "v14" 613 | resp, err := client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 614 | BlockNumber: &blockNumber, 615 | Version: &invalidVersion, 616 | }) 617 | require.NoError(t, err) 618 | require.Equal(t, "invalid version", resp.Error.Message) 619 | expectNoRequest(t, proxies[0].localBuilderRequests) 620 | 621 | blockNumber = hexutil.Uint64(124) 622 | uid := "52840671-89dc-484f-80f6-401753513ba9" 623 | resp, err = client.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 624 | BlockNumber: &blockNumber, 625 | UUID: &uid, 626 | }) 627 | require.NoError(t, err) 628 | require.Nil(t, resp.Error) 629 | rd := expectRequest(t, proxies[0].localBuilderRequests) 630 | require.JSONEq( 631 | t, 632 | `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x7c","replacementUuid":"52840671-89dc-484f-80f6-401753513ba9","version":"v2","replacementNonce":1730000000000000,"signingAddress":"0x9349365494be4f6205e5d44bdc7ec7dcd134becf"}],"id":0,"jsonrpc":"2.0"}`, 633 | rd.body, 634 | ) 635 | 636 | blockNumber = hexutil.Uint64(123) 637 | invalidVersion = "v14" 638 | resp, err = pubClient.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 639 | BlockNumber: &blockNumber, 640 | Version: &invalidVersion, 641 | }) 642 | require.NoError(t, err) 643 | require.Equal(t, "invalid version", resp.Error.Message) 644 | expectNoRequest(t, proxies[0].localBuilderRequests) 645 | 646 | blockNumber = hexutil.Uint64(123) 647 | resp, err = pubClient.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 648 | BlockNumber: &blockNumber, 649 | }) 650 | require.NoError(t, err) 651 | require.Equal(t, "version field should be set", resp.Error.Message) 652 | expectNoRequest(t, proxies[0].localBuilderRequests) 653 | 654 | blockNumber = hexutil.Uint64(125) 655 | uid = "4749af2a-1b99-45f2-a9bf-827cc0840964" 656 | version := rpctypes.BundleVersionV1 657 | resp, err = pubClient.Call(context.Background(), EthSendBundleMethod, &rpctypes.EthSendBundleArgs{ 658 | BlockNumber: &blockNumber, 659 | Version: &version, 660 | UUID: &uid, 661 | }) 662 | require.NoError(t, err) 663 | require.Nil(t, resp.Error) 664 | rd = expectRequest(t, proxies[0].localBuilderRequests) 665 | require.JSONEq( 666 | t, 667 | `{"method":"eth_sendBundle","params":[{"txs":null,"blockNumber":"0x7d","replacementUuid":"4749af2a-1b99-45f2-a9bf-827cc0840964","version":"v1"}],"id":0,"jsonrpc":"2.0"}`, 668 | rd.body, 669 | ) 670 | 671 | proxiesFlushQueue() 672 | } 673 | -------------------------------------------------------------------------------- /proxy/receiver_servers.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "errors" 5 | "log/slog" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/flashbots/go-utils/cli" 10 | "golang.org/x/net/http2" 11 | ) 12 | 13 | var ( 14 | HTTPDefaultReadTimeout = time.Duration(cli.GetEnvInt("HTTP_READ_TIMEOUT_SEC", 60)) * time.Second 15 | HTTPDefaultWriteTimeout = time.Duration(cli.GetEnvInt("HTTP_WRITE_TIMEOUT_SEC", 30)) * time.Second 16 | HTTPDefaultIdleTimeout = time.Duration(cli.GetEnvInt("HTTP_IDLE_TIMEOUT_SEC", 3600)) * time.Second 17 | HTTP2DefaultMaxUploadPerConnection = int32(cli.GetEnvInt("HTTP2_MAX_UPLOAD_PER_CONN", 32<<20)) // 32MiB 18 | HTTP2DefaultMaxUploadPerStream = int32(cli.GetEnvInt("HTTP2_MAX_UPLOAD_PER_STREAM", 8<<20)) // 8MiB 19 | HTTP2DefaultMaxConcurrentStreams = uint32(cli.GetEnvInt("HTTP2_MAX_CONCURRENT_STREAMS", 4096)) 20 | ) 21 | 22 | type ReceiverProxyServers struct { 23 | proxy *ReceiverProxy 24 | userServer *http.Server 25 | systemServer *http.Server 26 | certServer *http.Server 27 | } 28 | 29 | func StartReceiverServers(proxy *ReceiverProxy, userListenAddress, systemListenAddress, certListenAddress string) (*ReceiverProxyServers, error) { 30 | userServer := &http.Server{ 31 | Addr: userListenAddress, 32 | Handler: proxy.UserHandler, 33 | TLSConfig: proxy.TLSConfig(), 34 | ReadTimeout: HTTPDefaultReadTimeout, 35 | WriteTimeout: HTTPDefaultWriteTimeout, 36 | IdleTimeout: HTTPDefaultIdleTimeout, 37 | } 38 | userH2 := http2.Server{ 39 | MaxConcurrentStreams: HTTP2DefaultMaxConcurrentStreams, 40 | MaxUploadBufferPerConnection: HTTP2DefaultMaxUploadPerConnection, 41 | MaxUploadBufferPerStream: HTTP2DefaultMaxUploadPerStream, 42 | } 43 | 44 | // NOTE: as per https://github.com/golang/go/issues/67813 we still have to configure it like this 45 | // NOTE: this should be only meaningful for systemServer as these changes are to improve latencies whereas RTT is around 50-100ms 46 | err := http2.ConfigureServer(userServer, &userH2) 47 | if err != nil { 48 | return nil, err 49 | } 50 | systemServer := &http.Server{ 51 | Addr: systemListenAddress, 52 | Handler: proxy.SystemHandler, 53 | TLSConfig: proxy.TLSConfig(), 54 | ReadTimeout: HTTPDefaultReadTimeout, 55 | WriteTimeout: HTTPDefaultWriteTimeout, 56 | IdleTimeout: HTTPDefaultIdleTimeout, 57 | } 58 | systemH2 := http2.Server{ 59 | MaxConcurrentStreams: HTTP2DefaultMaxConcurrentStreams, 60 | MaxUploadBufferPerConnection: HTTP2DefaultMaxUploadPerConnection, 61 | MaxUploadBufferPerStream: HTTP2DefaultMaxUploadPerStream, 62 | } 63 | 64 | err = http2.ConfigureServer(systemServer, &systemH2) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | certServer := &http.Server{ 70 | Addr: certListenAddress, 71 | Handler: proxy.CertHandler, 72 | ReadTimeout: HTTPDefaultReadTimeout, 73 | WriteTimeout: HTTPDefaultWriteTimeout, 74 | IdleTimeout: HTTPDefaultIdleTimeout, 75 | } 76 | 77 | errCh := make(chan error) 78 | 79 | go func() { 80 | if err := systemServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { 81 | err = errors.Join(errors.New("system HTTP server failed"), err) 82 | errCh <- err 83 | } 84 | }() 85 | go func() { 86 | if err := userServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { 87 | err = errors.Join(errors.New("user HTTP server failed"), err) 88 | errCh <- err 89 | } 90 | }() 91 | go func() { 92 | if err := certServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 93 | err = errors.Join(errors.New("cert HTTP server failed"), err) 94 | errCh <- err 95 | } 96 | }() 97 | 98 | select { 99 | case err := <-errCh: 100 | return nil, err 101 | case <-time.After(time.Millisecond * 100): 102 | } 103 | 104 | go func() { 105 | for { 106 | err, more := <-errCh 107 | if !more { 108 | return 109 | } 110 | proxy.Log.Error("Error in HTTP server", slog.Any("error", err)) 111 | } 112 | }() 113 | 114 | return &ReceiverProxyServers{ 115 | proxy: proxy, 116 | userServer: userServer, 117 | systemServer: systemServer, 118 | certServer: certServer, 119 | }, nil 120 | } 121 | 122 | func (s *ReceiverProxyServers) Stop() { 123 | _ = s.userServer.Close() 124 | _ = s.systemServer.Close() 125 | _ = s.certServer.Close() 126 | s.proxy.Stop() 127 | } 128 | 129 | type SenderProxyServers struct { 130 | proxy *SenderProxy 131 | server *http.Server 132 | } 133 | 134 | func StartSenderServers(proxy *SenderProxy, listenAddress string) (*SenderProxyServers, error) { 135 | server := &http.Server{ 136 | Addr: listenAddress, 137 | Handler: proxy.Handler, 138 | ReadTimeout: HTTPDefaultReadTimeout, 139 | WriteTimeout: HTTPDefaultWriteTimeout, 140 | } 141 | 142 | errCh := make(chan error) 143 | 144 | go func() { 145 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 146 | err = errors.Join(errors.New("HTTP server failed"), err) 147 | errCh <- err 148 | } 149 | }() 150 | 151 | select { 152 | case err := <-errCh: 153 | return nil, err 154 | case <-time.After(time.Millisecond * 100): 155 | } 156 | 157 | go func() { 158 | for { 159 | err, more := <-errCh 160 | if !more { 161 | return 162 | } 163 | proxy.Log.Error("Error in HTTP server", slog.Any("error", err)) 164 | } 165 | }() 166 | 167 | return &SenderProxyServers{ 168 | proxy: proxy, 169 | server: server, 170 | }, nil 171 | } 172 | 173 | func (s *SenderProxyServers) Stop() { 174 | _ = s.server.Close() 175 | s.proxy.Stop() 176 | } 177 | -------------------------------------------------------------------------------- /proxy/sender_proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/flashbots/go-utils/rpcserver" 10 | "github.com/flashbots/go-utils/rpctypes" 11 | "github.com/flashbots/go-utils/signature" 12 | ) 13 | 14 | type SenderProxyConstantConfig struct { 15 | Log *slog.Logger 16 | OrderflowSigner *signature.Signer 17 | } 18 | 19 | type SenderProxyConfig struct { 20 | SenderProxyConstantConfig 21 | BuilderConfigHubEndpoint string 22 | MaxRequestBodySizeBytes int64 23 | ConnectionsPerPeer int 24 | } 25 | 26 | type SenderProxy struct { 27 | SenderProxyConstantConfig 28 | ConfigHub *BuilderConfigHub 29 | Handler http.Handler 30 | 31 | updatePeers chan []ConfighubBuilder 32 | shareQueue chan *ParsedRequest 33 | 34 | PeerUpdateForce chan struct{} 35 | } 36 | 37 | func NewSenderProxy(config SenderProxyConfig) (*SenderProxy, error) { 38 | maxRequestBodySizeBytes := DefaultMaxRequestBodySizeBytes 39 | if config.MaxRequestBodySizeBytes != 0 { 40 | maxRequestBodySizeBytes = config.MaxRequestBodySizeBytes 41 | } 42 | 43 | prx := &SenderProxy{ 44 | SenderProxyConstantConfig: config.SenderProxyConstantConfig, 45 | ConfigHub: NewBuilderConfigHub(config.Log, config.BuilderConfigHubEndpoint), 46 | Handler: nil, 47 | updatePeers: make(chan []ConfighubBuilder), 48 | shareQueue: make(chan *ParsedRequest), 49 | PeerUpdateForce: make(chan struct{}), 50 | } 51 | 52 | handler, err := rpcserver.NewJSONRPCHandler(rpcserver.Methods{ 53 | EthSendBundleMethod: prx.EthSendBundle, 54 | MevSendBundleMethod: prx.MevSendBundle, 55 | EthCancelBundleMethod: prx.EthCancelBundle, 56 | EthSendRawTransactionMethod: prx.EthSendRawTransaction, 57 | BidSubsidiseBlockMethod: prx.BidSubsidiseBlock, 58 | }, 59 | rpcserver.JSONRPCHandlerOpts{ 60 | Log: prx.Log, 61 | MaxRequestBodySizeBytes: maxRequestBodySizeBytes, 62 | }, 63 | ) 64 | if err != nil { 65 | return nil, err 66 | } 67 | prx.Handler = handler 68 | 69 | queue := ShareQueue{ 70 | log: prx.Log, 71 | queue: prx.shareQueue, 72 | updatePeers: prx.updatePeers, 73 | localBuilder: nil, 74 | signer: prx.OrderflowSigner, 75 | workersPerPeer: config.ConnectionsPerPeer, 76 | } 77 | go queue.Run() 78 | 79 | go func() { 80 | for { 81 | select { 82 | case _, more := <-prx.PeerUpdateForce: 83 | if !more { 84 | return 85 | } 86 | case <-time.After(peerUpdateTime): 87 | } 88 | builders, err := prx.ConfigHub.Builders(true) 89 | if err != nil { 90 | prx.Log.Error("Failed to update peers", slog.Any("error", err)) 91 | continue 92 | } 93 | 94 | prx.Log.Info("Updated peers", slog.Int("peerCount", len(builders))) 95 | 96 | select { 97 | case prx.updatePeers <- builders: 98 | default: 99 | } 100 | } 101 | }() 102 | 103 | prx.PeerUpdateForce <- struct{}{} 104 | 105 | return prx, nil 106 | } 107 | 108 | func (prx *SenderProxy) Stop() { 109 | close(prx.shareQueue) 110 | close(prx.updatePeers) 111 | close(prx.PeerUpdateForce) 112 | } 113 | 114 | func (prx *SenderProxy) EthSendBundle(ctx context.Context, ethSendBundle rpctypes.EthSendBundleArgs) error { 115 | parsedRequest := ParsedRequest{ 116 | ethSendBundle: ðSendBundle, 117 | method: EthSendBundleMethod, 118 | } 119 | 120 | err := ValidateEthSendBundle(ðSendBundle, true) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | // quick workaround for people setting timestamp to 0 126 | if ethSendBundle.MaxTimestamp != nil && *ethSendBundle.MaxTimestamp == 0 { 127 | ethSendBundle.MaxTimestamp = nil 128 | } 129 | if ethSendBundle.MinTimestamp != nil && *ethSendBundle.MinTimestamp == 0 { 130 | ethSendBundle.MinTimestamp = nil 131 | } 132 | // we set explicitly bundles to be v1 for all protect flow 133 | if ethSendBundle.Version == nil || *ethSendBundle.Version == "" { 134 | version := rpctypes.BundleVersionV1 135 | ethSendBundle.Version = &version 136 | } 137 | 138 | return prx.HandleParsedRequest(ctx, parsedRequest) 139 | } 140 | 141 | func (prx *SenderProxy) MevSendBundle(ctx context.Context, mevSendBundle rpctypes.MevSendBundleArgs) error { 142 | parsedRequest := ParsedRequest{ 143 | mevSendBundle: &mevSendBundle, 144 | method: MevSendBundleMethod, 145 | } 146 | 147 | err := ValidateMevSendBundle(&mevSendBundle, true) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return prx.HandleParsedRequest(ctx, parsedRequest) 153 | } 154 | 155 | func (prx *SenderProxy) EthCancelBundle(ctx context.Context, ethCancelBundle rpctypes.EthCancelBundleArgs) error { 156 | parsedRequest := ParsedRequest{ 157 | ethCancelBundle: ðCancelBundle, 158 | method: EthCancelBundleMethod, 159 | } 160 | 161 | err := ValidateEthCancelBundle(ðCancelBundle, true) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | return prx.HandleParsedRequest(ctx, parsedRequest) 167 | } 168 | 169 | func (prx *SenderProxy) EthSendRawTransaction(ctx context.Context, ethSendRawTransaction rpctypes.EthSendRawTransactionArgs) error { 170 | parsedRequest := ParsedRequest{ 171 | ethSendRawTransaction: ðSendRawTransaction, 172 | method: EthSendRawTransactionMethod, 173 | } 174 | return prx.HandleParsedRequest(ctx, parsedRequest) 175 | } 176 | 177 | func (prx *SenderProxy) BidSubsidiseBlock(ctx context.Context, bidSubsidiseBlock rpctypes.BidSubsisideBlockArgs) error { 178 | parsedRequest := ParsedRequest{ 179 | bidSubsidiseBlock: &bidSubsidiseBlock, 180 | method: BidSubsidiseBlockMethod, 181 | } 182 | 183 | return prx.HandleParsedRequest(ctx, parsedRequest) 184 | } 185 | 186 | func (prx *SenderProxy) HandleParsedRequest(ctx context.Context, parsedRequest ParsedRequest) error { 187 | parsedRequest.receivedAt = apiNow() 188 | // we set it explicitly to note that we need to proxy all calls to all peers 189 | parsedRequest.systemEndpoint = false 190 | prx.Log.Debug("Received request", slog.String("method", parsedRequest.method)) 191 | 192 | select { 193 | case <-ctx.Done(): 194 | case prx.shareQueue <- &parsedRequest: 195 | } 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /proxy/sharing.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/flashbots/go-utils/rpcclient" 9 | "github.com/flashbots/go-utils/signature" 10 | ) 11 | 12 | var ( 13 | ShareWorkerQueueSize = 10000 14 | requestTimeout = time.Second * 10 15 | ) 16 | 17 | const ( 18 | bigRequestSize = 50_000 19 | ) 20 | 21 | type ShareQueue struct { 22 | name string 23 | log *slog.Logger 24 | queue chan *ParsedRequest 25 | updatePeers chan []ConfighubBuilder 26 | localBuilder rpcclient.RPCClient 27 | signer *signature.Signer 28 | // if > 0 share queue will spawn multiple senders per peer 29 | workersPerPeer int 30 | } 31 | 32 | type shareQueuePeer struct { 33 | ch chan *ParsedRequest 34 | name string 35 | client rpcclient.RPCClient 36 | conf ConfighubBuilder 37 | } 38 | 39 | func newShareQueuePeer(name string, client rpcclient.RPCClient, conf ConfighubBuilder) shareQueuePeer { 40 | return shareQueuePeer{ 41 | ch: make(chan *ParsedRequest, ShareWorkerQueueSize), 42 | name: name, 43 | client: client, 44 | conf: conf, 45 | } 46 | } 47 | 48 | func (p *shareQueuePeer) Close() { 49 | close(p.ch) 50 | } 51 | 52 | func (p *shareQueuePeer) SendRequest(log *slog.Logger, request *ParsedRequest) { 53 | select { 54 | case p.ch <- request: 55 | default: 56 | log.Error("Peer is stalling on requests", slog.String("peer", p.name)) 57 | incShareQueuePeerStallingErrors(p.name) 58 | } 59 | } 60 | 61 | func (sq *ShareQueue) Run() { 62 | workersPerPeer := 1 63 | if sq.workersPerPeer > 0 { 64 | workersPerPeer = sq.workersPerPeer 65 | } 66 | var ( 67 | localBuilder *shareQueuePeer 68 | peers []shareQueuePeer 69 | ) 70 | if sq.localBuilder != nil { 71 | builderPeer := newShareQueuePeer("local-builder", sq.localBuilder, ConfighubBuilder{}) 72 | localBuilder = &builderPeer 73 | for worker := range workersPerPeer { 74 | go sq.proxyRequests(localBuilder, worker) 75 | } 76 | defer localBuilder.Close() 77 | } 78 | for { 79 | select { 80 | case req, more := <-sq.queue: 81 | sq.log.Debug("Share queue received a request", slog.String("name", sq.name), slog.String("method", req.method)) 82 | if !more { 83 | sq.log.Info("Share queue closing, queue channel closed") 84 | return 85 | } 86 | if localBuilder != nil { 87 | localBuilder.SendRequest(sq.log, req) 88 | } 89 | if !req.systemEndpoint { 90 | for _, peer := range peers { 91 | peer.SendRequest(sq.log, req) 92 | } 93 | } 94 | case newPeers, more := <-sq.updatePeers: 95 | if !more { 96 | sq.log.Info("Share queue closing, peer channel closed") 97 | return 98 | } 99 | 100 | var peersToKeep []shareQueuePeer 101 | var peersToClose []shareQueuePeer 102 | 103 | PeerLoop: 104 | for _, peer := range peers { 105 | for _, npi := range newPeers { 106 | if peer.conf == npi { 107 | // peer found do not close 108 | peersToKeep = append(peersToKeep, peer) 109 | continue PeerLoop 110 | } 111 | } 112 | peersToClose = append(peersToClose, peer) 113 | } 114 | 115 | var newPeersToOpen []ConfighubBuilder 116 | NewPeerLoop: 117 | for _, npi := range newPeers { 118 | for _, peer := range peersToKeep { 119 | if peer.conf == npi { 120 | continue NewPeerLoop 121 | } 122 | } 123 | newPeersToOpen = append(newPeersToOpen, npi) 124 | } 125 | 126 | for _, peer := range peersToClose { 127 | peer.Close() 128 | } 129 | 130 | peers = peersToKeep 131 | for _, info := range newPeersToOpen { 132 | // don't send to yourself 133 | if info.OrderflowProxy.EcdsaPubkeyAddress == sq.signer.Address() { 134 | continue 135 | } 136 | client, err := RPCClientWithCertAndSigner(OrderflowProxyURLFromIP(info.IP), []byte(info.OrderflowProxy.TLSCert), sq.signer, workersPerPeer) 137 | if err != nil { 138 | sq.log.Error("Failed to create a peer client", slog.Any("error", err)) 139 | shareQueueInternalErrors.Inc() 140 | continue 141 | } 142 | sq.log.Info("Created client for peer", slog.String("peer", info.Name), slog.String("name", sq.name)) 143 | newPeer := newShareQueuePeer(info.Name, client, info) 144 | peers = append(peers, newPeer) 145 | for worker := range workersPerPeer { 146 | go sq.proxyRequests(&newPeer, worker) 147 | } 148 | } 149 | } 150 | } 151 | } 152 | 153 | func (sq *ShareQueue) proxyRequests(peer *shareQueuePeer, worker int) { 154 | proxiedRequestCount := 0 155 | logger := sq.log.With(slog.String("peer", peer.name), slog.String("name", sq.name), slog.Int("worker", worker)) 156 | logger.Info("Started proxying requests to peer") 157 | defer func() { 158 | logger.Info("Stopped proxying requets to peer", slog.Int("proxiedRequestCount", proxiedRequestCount)) 159 | }() 160 | for { 161 | req, more := <-peer.ch 162 | if !more { 163 | return 164 | } 165 | var ( 166 | method string 167 | data any 168 | ) 169 | isBig := req.size > bigRequestSize 170 | if req.ethSendBundle != nil { 171 | method = EthSendBundleMethod 172 | data = req.ethSendBundle 173 | } else if req.mevSendBundle != nil { 174 | method = MevSendBundleMethod 175 | data = req.mevSendBundle 176 | } else if req.ethCancelBundle != nil { 177 | method = EthCancelBundleMethod 178 | data = req.ethCancelBundle 179 | } else if req.ethSendRawTransaction != nil { 180 | method = EthSendRawTransactionMethod 181 | data = req.ethSendRawTransaction 182 | } else if req.bidSubsidiseBlock != nil { 183 | method = BidSubsidiseBlockMethod 184 | data = req.bidSubsidiseBlock 185 | } else { 186 | logger.Error("Unknown request type", slog.String("method", req.method)) 187 | shareQueueInternalErrors.Inc() 188 | continue 189 | } 190 | timeShareQueuePeerQueueDuration(peer.name, time.Since(req.receivedAt), method, req.systemEndpoint, isBig) 191 | start := time.Now() 192 | ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) 193 | resp, err := peer.client.Call(ctx, method, data) 194 | cancel() 195 | timeShareQueuePeerRPCDuration(peer.name, time.Since(start).Milliseconds(), isBig) 196 | timeShareQueuePeerE2EDuration(peer.name, time.Since(req.receivedAt), method, req.systemEndpoint, isBig) 197 | logSendErrorLevel := slog.LevelDebug 198 | if peer.name == "local-builder" { 199 | logSendErrorLevel = slog.LevelWarn 200 | } 201 | if err != nil { 202 | logger.Log(context.Background(), logSendErrorLevel, "Error while proxying request", slog.Any("error", err)) 203 | incShareQueuePeerRPCErrors(peer.name) 204 | } 205 | if resp != nil && resp.Error != nil { 206 | logger.Log(context.Background(), logSendErrorLevel, "Error returned from target while proxying", slog.Any("error", resp.Error)) 207 | incShareQueuePeerRPCErrors(peer.name) 208 | } 209 | proxiedRequestCount += 1 210 | logger.Debug("Message proxied") 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /proxy/utils.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "errors" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/ethereum/go-ethereum/common/hexutil" 15 | "github.com/flashbots/go-utils/cli" 16 | "github.com/flashbots/go-utils/rpcclient" 17 | "github.com/flashbots/go-utils/signature" 18 | "golang.org/x/net/http2" 19 | ) 20 | 21 | var ( 22 | DefaultOrderflowProxyPublicPort = "5544" 23 | DefaultHTTPCLientWriteBuffer = cli.GetEnvInt("HTTP_CLIENT_WRITE_BUFFER", 64<<10) // 64 KiB 24 | ) 25 | 26 | const DefaultLocalhostMaxIdleConn = 1000 27 | 28 | var errCertificate = errors.New("failed to add certificate to pool") 29 | 30 | func createTransportForSelfSignedCert(certPEM []byte, maxOpenConnections int) (*http.Transport, error) { 31 | certPool := x509.NewCertPool() 32 | if ok := certPool.AppendCertsFromPEM(certPEM); !ok { 33 | return nil, errCertificate 34 | } 35 | tr := &http.Transport{ 36 | TLSClientConfig: &tls.Config{ 37 | RootCAs: certPool, 38 | MinVersion: tls.VersionTLS12, 39 | }, 40 | MaxConnsPerHost: maxOpenConnections * 2, 41 | } 42 | 43 | return tr, nil 44 | } 45 | 46 | func HTTPClientWithMaxConnections(maxOpenConnections int) *http.Client { 47 | return &http.Client{ 48 | Transport: &http.Transport{ 49 | MaxIdleConns: maxOpenConnections, 50 | MaxIdleConnsPerHost: maxOpenConnections, 51 | }, 52 | } 53 | } 54 | 55 | func HTTPClientLocalhost(maxOpenConnections int) *http.Client { 56 | localTransport := &http.Transport{ 57 | MaxIdleConnsPerHost: maxOpenConnections, 58 | MaxIdleConns: maxOpenConnections, 59 | DisableCompression: true, 60 | IdleConnTimeout: time.Minute * 2, 61 | // ---- kill delayed-ACK/Nagle ---- 62 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 63 | c, err := (&net.Dialer{ 64 | LocalAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1)}, 65 | KeepAlive: 30 * time.Second, 66 | Timeout: 1 * time.Second, 67 | }).DialContext(ctx, network, addr) 68 | if err == nil { 69 | tcp, ok := c.(*net.TCPConn) 70 | if ok { 71 | err = tcp.SetNoDelay(true) // <-- ACK immediately 72 | } 73 | } 74 | return c, err 75 | }, 76 | Proxy: nil, 77 | } 78 | _ = http2.ConfigureTransport(localTransport) 79 | localCl := http.Client{ 80 | Transport: localTransport, 81 | Timeout: 10 * time.Second, 82 | } 83 | return &localCl 84 | } 85 | 86 | //nolint:ireturn 87 | func RPCClientWithCertAndSigner(endpoint string, certPEM []byte, signer *signature.Signer, maxOpenConnections int) (rpcclient.RPCClient, error) { 88 | transport, err := createTransportForSelfSignedCert(certPEM, maxOpenConnections) 89 | if err != nil { 90 | return nil, err 91 | } 92 | transport.MaxIdleConns = maxOpenConnections * 2 93 | transport.MaxIdleConnsPerHost = maxOpenConnections * 2 94 | transport.WriteBufferSize = DefaultHTTPCLientWriteBuffer 95 | 96 | client := rpcclient.NewClientWithOpts(endpoint, &rpcclient.RPCClientOpts{ 97 | HTTPClient: &http.Client{ 98 | Transport: transport, 99 | }, 100 | Signer: signer, 101 | }) 102 | return client, nil 103 | } 104 | 105 | func OrderflowProxyURLFromIP(ip string) string { 106 | if strings.Contains(ip, ":") { 107 | return "https://" + ip 108 | } else { 109 | return "https://" + net.JoinHostPort(ip, DefaultOrderflowProxyPublicPort) 110 | } 111 | } 112 | 113 | type BlockNumberSource struct { 114 | client rpcclient.RPCClient 115 | cacheMu sync.RWMutex 116 | cacheTimestamp time.Time 117 | cachedNumber uint64 118 | } 119 | 120 | func NewBlockNumberSource(endpoint string) *BlockNumberSource { 121 | client := rpcclient.NewClient(endpoint) 122 | return &BlockNumberSource{ 123 | client: client, 124 | } 125 | } 126 | 127 | func (bs *BlockNumberSource) UpdateCachedBlockNumber() error { 128 | var numberHex hexutil.Uint64 129 | err := bs.client.CallFor(context.Background(), &numberHex, "eth_blockNumber") 130 | if err != nil { 131 | return err 132 | } 133 | bs.cacheMu.Lock() 134 | bs.cacheTimestamp = time.Now() 135 | bs.cachedNumber = uint64(numberHex) 136 | bs.cacheMu.Unlock() 137 | return nil 138 | } 139 | 140 | func (bs *BlockNumberSource) BlockNumber() (uint64, error) { 141 | bs.cacheMu.RLock() 142 | if time.Since(bs.cacheTimestamp) > time.Second*3 { 143 | bs.cacheMu.RUnlock() 144 | err := bs.UpdateCachedBlockNumber() 145 | if err != nil { 146 | return 0, err 147 | } 148 | bs.cacheMu.RLock() 149 | } 150 | res := bs.cachedNumber 151 | bs.cacheMu.RUnlock() 152 | return res, nil 153 | } 154 | -------------------------------------------------------------------------------- /receiver.dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24rc2-bullseye@sha256:236da40764c1bcf469fcaf6ca225ca881c3f06cbd1934e392d6e4af3484f6cac AS builder 2 | WORKDIR /app 3 | COPY . /app 4 | RUN make build-receiver-proxy 5 | 6 | FROM gcr.io/distroless/cc-debian12:nonroot-6755e21ccd99ddead6edc8106ba03888cbeed41a 7 | WORKDIR /app 8 | COPY --from=builder /app/build/receiver-proxy /app/receiver-proxy 9 | ENV LISTEN_ADDR=":8080" 10 | EXPOSE 8080 11 | ENTRYPOINT ["/app/receiver-proxy"] 12 | -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] 2 | # checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"] 3 | initialisms = ["ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS", "SIP", "RTP", "AMQP", "DB", "TS"] 4 | dot_import_whitelist = ["github.com/mmcloughlin/avo/build", "github.com/mmcloughlin/avo/operand", "github.com/mmcloughlin/avo/reg"] 5 | http_status_code_whitelist = ["200", "400", "404", "500"] 6 | --------------------------------------------------------------------------------