├── .github ├── pull_request_template.md └── workflows │ ├── checks.yml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── adapters ├── database │ ├── service.go │ ├── service_test.go │ └── types.go └── secrets │ └── service.go ├── application ├── config_test.go ├── config_utils.go └── service.go ├── cmd └── httpserver │ └── main.go ├── common ├── logging.go ├── utils.go └── vars.go ├── docker ├── database │ └── Dockerfile ├── docker-compose.yaml ├── httpserver │ └── Dockerfile └── mock-proxy │ ├── Dockerfile │ └── nginx-default.conf ├── docs ├── api-docs │ ├── README.md │ ├── admin-api │ │ ├── Add measurements.bru │ │ ├── Add new builder.bru │ │ ├── Disable builder.bru │ │ ├── Enable builder.bru │ │ ├── Enable measurement.bru │ │ ├── Get Builders v2.bru │ │ ├── Get Builders.bru │ │ ├── Get configuration.bru │ │ ├── Update builder config.bru │ │ ├── Update secrets config.bru │ │ ├── bruno.json │ │ └── collection.bru │ └── instance-api │ │ ├── BuilderHub Instance API │ │ ├── Get measurements.bru │ │ ├── Get peers.bru │ │ ├── Register credentials.bru │ │ └── bruno.json │ │ └── Get measurements.bru └── devenv-setup.md ├── domain ├── inmemory_secret.go └── types.go ├── go.mod ├── go.sum ├── httpserver ├── doc.go ├── e2e_test.go ├── handler.go ├── handler_test.go ├── server.go └── vars.go ├── metrics ├── metrics.go ├── middleware.go └── server.go ├── ports ├── admin_handler.go ├── http_handler.go ├── types.go └── types_test.go ├── schema ├── 000_init.sql ├── 001_measurement_constraints.sql └── 002_network_builder.sql ├── staticcheck.conf └── testdata ├── get-builders.json ├── get-configuration.json └── get-measurements.json /.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 | services: 14 | # Label used to access the service container 15 | postgres: 16 | # Docker Hub image 17 | image: postgres 18 | env: 19 | POSTGRES_USER: postgres 20 | POSTGRES_PASSWORD: postgres 21 | options: >- 22 | --health-cmd pg_isready 23 | --health-interval 10s 24 | --health-timeout 5s 25 | --health-retries 5 26 | ports: 27 | - 5432:5432 28 | steps: 29 | - name: Set up Go 30 | uses: actions/setup-go@v3 31 | with: 32 | go-version: ^1.21 33 | id: go 34 | 35 | - name: Check out code into the Go module directory 36 | uses: actions/checkout@v2 37 | 38 | - name: Run migrations 39 | run: for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $file; done 40 | 41 | - name: Run unit tests and generate the coverage report 42 | run: RUN_DB_TESTS=1 make test-race 43 | 44 | lint: 45 | name: Lint 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Set up Go 49 | uses: actions/setup-go@v3 50 | with: 51 | go-version: ^1.22 52 | id: go 53 | 54 | - name: Check out code into the Go module directory 55 | uses: actions/checkout@v2 56 | 57 | - name: Install gofumpt 58 | run: go install mvdan.cc/gofumpt@v0.4.0 59 | 60 | - name: Install staticcheck 61 | run: go install honnef.co/go/tools/cmd/staticcheck@2024.1.1 62 | 63 | - name: Install golangci-lint 64 | run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 65 | 66 | - name: Install NilAway 67 | run: go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7 68 | 69 | - name: Lint 70 | run: make lint 71 | 72 | - name: Ensure go mod tidy runs without changes 73 | run: | 74 | go mod tidy 75 | git update-index -q --really-refresh 76 | git diff-index HEAD 77 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | docker-image-service: 11 | name: Publish Service Docker image 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Get tag and SHA version 22 | run: | 23 | GITHUB_REF=${GITHUB_REF#refs/*/} 24 | GIT_SHA=${GITHUB_SHA::7} 25 | BINARY_VERSION=${GITHUB_REF}/${GIT_SHA} 26 | echo "GITHUB_REF=${GITHUB_REF}" >> $GITHUB_ENV 27 | echo "GIT_SHA=${GIT_SHA}" >> $GITHUB_ENV 28 | echo "BINARY_VERSION=${BINARY_VERSION}" >> $GITHUB_ENV 29 | 30 | - name: Print versions 31 | run: | 32 | echo "GITHUB_REF: ${{ env.GITHUB_REF }}" 33 | echo "GIT_SHA: ${{ env.GIT_SHA }}" 34 | echo "BINARY_VERSION: ${{ env.BINARY_VERSION }}" 35 | 36 | - name: Generate Docker metadata 37 | id: meta 38 | uses: docker/metadata-action@v5 39 | with: 40 | images: | 41 | flashbots/builder-hub 42 | ghcr.io/${{ github.repository }}/builder-hub 43 | labels: org.opencontainers.image.source=${{ github.repositoryUrl }} 44 | tags: | 45 | type=sha 46 | type=semver,pattern={{version}} 47 | type=semver,pattern={{major}}.{{minor}} 48 | type=raw,value=latest 49 | 50 | - name: Login to GHCR 51 | uses: docker/login-action@v3 52 | with: 53 | registry: ghcr.io 54 | username: ${{ github.actor }} 55 | password: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - name: Login to Docker Hub 58 | uses: docker/login-action@v3 59 | with: 60 | username: ${{ secrets.FLASHBOTS_DOCKERHUB_USERNAME }} 61 | password: ${{ secrets.FLASHBOTS_DOCKERHUB_TOKEN }} 62 | 63 | - name: Set up QEMU (for multi-arch builds, optional) 64 | uses: docker/setup-qemu-action@v3 65 | 66 | - name: Set up Buildx 67 | uses: docker/setup-buildx-action@v3 68 | 69 | - name: Build and push image 70 | uses: docker/build-push-action@v5 71 | with: 72 | file: ./docker/httpserver/Dockerfile 73 | context: . 74 | push: true 75 | build-args: | 76 | VERSION=${{ env.BINARY_VERSION }} 77 | platforms: linux/amd64,linux/arm64 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | cache-from: type=gha 81 | cache-to: type=gha,mode=max 82 | 83 | docker-image-db: 84 | name: Publish Database Docker image 85 | runs-on: ubuntu-latest 86 | permissions: 87 | contents: read 88 | packages: write 89 | 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | 94 | - name: Generate Docker metadata 95 | id: meta 96 | uses: docker/metadata-action@v5 97 | with: 98 | images: flashbots/builder-hub-db 99 | tags: | 100 | type=sha 101 | type=semver,pattern={{version}} 102 | type=semver,pattern={{major}}.{{minor}} 103 | type=raw,value=latest 104 | 105 | - name: Login to Docker Hub 106 | uses: docker/login-action@v3 107 | with: 108 | username: ${{ secrets.FLASHBOTS_DOCKERHUB_USERNAME }} 109 | password: ${{ secrets.FLASHBOTS_DOCKERHUB_TOKEN }} 110 | 111 | - name: Set up QEMU (for multi-arch builds, optional) 112 | uses: docker/setup-qemu-action@v3 113 | 114 | - name: Set up Buildx 115 | uses: docker/setup-buildx-action@v3 116 | 117 | - name: Build and push image 118 | uses: docker/build-push-action@v5 119 | with: 120 | file: ./docker/database/Dockerfile 121 | context: . 122 | push: true 123 | platforms: linux/amd64,linux/arm64 124 | tags: ${{ steps.meta.outputs.tags }} 125 | 126 | docker-image-proxy: 127 | name: Publish Proxy Docker image 128 | runs-on: ubuntu-latest 129 | permissions: 130 | contents: read 131 | packages: write 132 | 133 | steps: 134 | - name: Checkout 135 | uses: actions/checkout@v4 136 | 137 | - name: Generate Docker metadata 138 | id: meta 139 | uses: docker/metadata-action@v5 140 | with: 141 | images: flashbots/builder-hub-mock-proxy 142 | tags: | 143 | type=sha 144 | type=semver,pattern={{version}} 145 | type=semver,pattern={{major}}.{{minor}} 146 | type=raw,value=latest 147 | 148 | - name: Login to Docker Hub 149 | uses: docker/login-action@v3 150 | with: 151 | username: ${{ secrets.FLASHBOTS_DOCKERHUB_USERNAME }} 152 | password: ${{ secrets.FLASHBOTS_DOCKERHUB_TOKEN }} 153 | 154 | - name: Set up QEMU (for multi-arch builds, optional) 155 | uses: docker/setup-qemu-action@v3 156 | 157 | - name: Set up Buildx 158 | uses: docker/setup-buildx-action@v3 159 | 160 | - name: Build and push image 161 | uses: docker/build-push-action@v5 162 | with: 163 | file: ./docker/mock-proxy/Dockerfile 164 | context: . 165 | push: true 166 | platforms: linux/amd64,linux/arm64 167 | tags: ${{ steps.meta.outputs.tags }} 168 | -------------------------------------------------------------------------------- /.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 | .aider* 23 | 24 | # Live configs 25 | testdata/*-config.json 26 | testdata/*-secrets.json 27 | /database.dump 28 | db.dockerfile 29 | 30 | 31 | docs/api-docs/admin-api/environments/ 32 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | linters: 4 | enable-all: true 5 | disable: 6 | - cyclop 7 | - forbidigo 8 | - funlen 9 | - gci 10 | - gochecknoglobals 11 | - gochecknoinits 12 | - gocritic 13 | - godot 14 | - godox 15 | - gomnd 16 | - lll 17 | - nestif 18 | - nilnil 19 | - nlreturn 20 | - noctx 21 | - nonamedreturns 22 | - paralleltest 23 | - revive 24 | - testpackage 25 | - unparam 26 | - varnamelen 27 | - wrapcheck 28 | - wsl 29 | - exhaustruct 30 | - depguard 31 | - prealloc 32 | - whitespace 33 | - musttag 34 | - mnd 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 | tagliatelle: 63 | case: 64 | rules: 65 | json: snake 66 | 67 | gofumpt: 68 | extra-rules: true 69 | 70 | exhaustruct: 71 | exclude: 72 | # 73 | # Because it's easier to read without the other fields. 74 | # 75 | - 'GetPayloadsFilters' 76 | 77 | # 78 | # Structures outside our control that have a ton of settings. It doesn't 79 | # make sense to specify all of the fields. 80 | # 81 | - 'cobra.Command' 82 | - 'database.*Entry' 83 | - 'http.Server' 84 | - 'logrus.*Formatter' 85 | - 'Options' # redis 86 | 87 | # 88 | # Excluded because there are private fields (not capitalized) that are 89 | # not initialized. If possible, I think these should be altered. 90 | # 91 | - 'Datastore' 92 | - 'Housekeeper' 93 | - 'MockBeaconClient' 94 | - 'RelayAPI' 95 | - 'Webserver' 96 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | docker/httpserver/Dockerfile -------------------------------------------------------------------------------- /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 | # A few colors 8 | RED:=\033[0;31m 9 | BLUE:=\033[0;34m 10 | GREEN:=\033[0;32m 11 | NC:=\033[0m 12 | 13 | ##@ Help 14 | 15 | .PHONY: help 16 | help: ## Display this help. 17 | @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) 18 | 19 | .PHONY: v 20 | v: ## Show the version 21 | @echo "Version: ${VERSION}" 22 | 23 | ##@ Build 24 | 25 | .PHONY: clean 26 | clean: ## Clean the build directory 27 | rm -rf build/ 28 | 29 | .PHONY: build 30 | build: ## Build the HTTP server 31 | @mkdir -p ./build 32 | go build -trimpath -ldflags "-X github.com/flashbots/builder-hub/common.Version=${VERSION}" -v -o ./build/builder-hub cmd/httpserver/main.go 33 | 34 | ##@ Test & Development 35 | 36 | .PHONY: lt 37 | lt: lint test ## Run linters and tests (always do this!) 38 | 39 | .PHONY: fmt 40 | fmt: ## Format the code (use this often) 41 | gofmt -s -w . 42 | gci write . 43 | gofumpt -w -extra . 44 | go mod tidy 45 | 46 | .PHONY: test 47 | test: ## Run tests 48 | go test ./... 49 | 50 | .PHONY: test-race 51 | test-race: ## Run tests with race detector 52 | go test -race ./... 53 | 54 | .PHONY: lint 55 | lint: ## Run linters 56 | gofmt -d -s . 57 | gofumpt -d -extra . 58 | go vet ./... 59 | staticcheck ./... 60 | golangci-lint run 61 | 62 | .PHONY: gofumpt 63 | gofumpt: ## Run gofumpt 64 | gofumpt -l -w -extra . 65 | 66 | .PHONY: cover 67 | cover: ## Run tests with coverage 68 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 69 | go tool cover -func /tmp/go-sim-lb.cover.tmp 70 | unlink /tmp/go-sim-lb.cover.tmp 71 | 72 | .PHONY: cover-html 73 | cover-html: ## Run tests with coverage and open the HTML report 74 | go test -coverprofile=/tmp/go-sim-lb.cover.tmp ./... 75 | go tool cover -html=/tmp/go-sim-lb.cover.tmp 76 | unlink /tmp/go-sim-lb.cover.tmp 77 | 78 | .PHONY: docker-httpserver 79 | docker-httpserver: ## Build the HTTP server Docker image 80 | DOCKER_BUILDKIT=1 docker build \ 81 | --file docker/httpserver/Dockerfile \ 82 | --platform linux/amd64 \ 83 | --build-arg VERSION=${VERSION} \ 84 | --tag builder-hub \ 85 | . 86 | 87 | .PHONY: db-dump 88 | db-dump: ## Dump the database contents to file 'database.dump' 89 | pg_dump -h localhost -U postgres --column-inserts --data-only postgres -f database.dump 90 | @printf "Database dumped to file: $(GREEN)database.dump$(NC) ✅\n" 91 | 92 | .PHONY: dev-db-setup 93 | dev-db-setup: ## Create the basic database entries for testing and development 94 | @printf "$(BLUE)Create the allow-all measurements $(NC)\n" 95 | curl -v --request POST --url http://localhost:8081/api/admin/v1/measurements --data '{"measurement_id": "test1","attestation_type": "test","measurements": {}}' 96 | 97 | @printf "$(BLUE)Enable the measurements $(NC)\n" 98 | curl -v --request POST --url http://localhost:8081/api/admin/v1/measurements/activation/test1 --data '{"enabled": true}' 99 | 100 | @printf "$(BLUE)Create the builder $(NC)\n" 101 | curl -v --request POST --url http://localhost:8081/api/admin/v1/builders --data '{"name": "test_builder","ip_address": "1.2.3.4", "network": "production"}' 102 | 103 | @printf "$(BLUE)Create the builder configuration $(NC)\n" 104 | curl -v --request POST --url http://localhost:8081/api/admin/v1/builders/configuration/test_builder --data '{"dns_name": "foobar-v1.a.b.c","rbuilder": {"extra_data": "FooBar"}}' 105 | 106 | @printf "$(BLUE)Enable the builder $(NC)\n" 107 | curl -v --request POST --url http://localhost:8081/api/admin/v1/builders/activation/test_builder --data '{"enabled": true}' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BuilderHub 2 | 3 | [![Goreport status](https://goreportcard.com/badge/github.com/flashbots/builder-hub)](https://goreportcard.com/report/github.com/flashbots/builder-hub) 4 | [![Test status](https://github.com/flashbots/builder-hub/actions/workflows/checks.yml/badge.svg?branch=main)](https://github.com/flashbots/builder-hub/actions?query=workflow%3A%22Checks%22) 5 | 6 | BuilderHub is the central data source for BuilderNet builder registration and configuration. 7 | 8 | Docs here: https://buildernet.org/docs/flashbots-infra 9 | 10 | BuilderHub has these responsibilities: 11 | 12 | 1. Builder identity management 13 | 2. Provisioning of secrets and configuration 14 | 3. Peer discovery 15 | 16 | --- 17 | 18 | ![Architecture](https://buildernet.org/assets/ideal-img/flashbots-infra-dataflow.7377b1f.3909.png) 19 | 20 | --- 21 | 22 | ## Getting started 23 | 24 | 25 | ### Manual setup 26 | 27 | **Start the database and the server:** 28 | 29 | ```bash 30 | # Start a Postgres database container 31 | docker run -d --name postgres-test -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=postgres postgres 32 | 33 | # Apply the DB migrations 34 | for file in schema/*.sql; do psql "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" -f $file; done 35 | 36 | # Start the server 37 | go run cmd/httpserver/main.go 38 | ``` 39 | 40 | ### Using Docker 41 | 42 | See instructions on using Docker to run the full stack at [`docs/devenv-setup.md`](./docs/devenv-setup.md) 43 | 44 | ### Example requests 45 | 46 | ```bash 47 | # Public endpoints 48 | curl localhost:8080/api/l1-builder/v1/measurements 49 | 50 | # client-aTLS secured endpoints 51 | curl localhost:8080/api/l1-builder/v1/builders 52 | curl localhost:8080/api/l1-builder/v1/configuration 53 | curl -X POST localhost:8080/api/l1-builder/v1/register_credentials/rbuilder 54 | ``` 55 | 56 | ### Testing 57 | 58 | Run test suite with database tests included: 59 | 60 | ```bash 61 | RUN_DB_TESTS=1 make test 62 | ``` 63 | 64 | --- 65 | 66 | ## Contributing 67 | 68 | **Install dev dependencies** 69 | 70 | ```bash 71 | go install mvdan.cc/gofumpt@v0.4.0 72 | go install honnef.co/go/tools/cmd/staticcheck@v0.4.2 73 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 74 | go install go.uber.org/nilaway/cmd/nilaway@v0.0.0-20240821220108-c91e71c080b7 75 | go install github.com/daixiang0/gci@v0.11.2 76 | ``` 77 | 78 | **Lint, test, format** 79 | 80 | ```bash 81 | make lint 82 | make test 83 | make fmt 84 | ``` 85 | 86 | --- 87 | 88 | ## API Documentation 89 | 90 | `BuilderHub` exposes a JSON+REST API with these methods: 91 | 92 | | API | Exposure | Authentication | Requested by | Served by | 93 | | ------------------------ | -------- | ---------------------------- | ----------------------------- | ---------------------- | 94 | | Get Secrets + Config | TDX Node | IP + Client-ATLS Attestation | Builder (via cvm-proxy) | cvm-proxy → BuilderHub | 95 | | Register Credentials | TDX Node | IP + Client-ATLS Attestation | Builder (via cvm-proxy) | cvm-proxy → BuilderHub | 96 | | Get Active Builders | TDX Node | IP + Client-ATLS Attestation | Builder (via cvm-proxy) | cvm-proxy → BuilderHub | 97 | | Get Active Builders | Internal | HTTP Basic Auth | MEV-Share, block processor, … | BuilderHub | 98 | | Get Allowed Measurements | Internal | HTTP Basic Auth | Users, Builders | nginx → BuilderHub | 99 | | Admin Endpoints | Internal | HTTP Basic Auth | | | 100 | 101 | See also a [Bruno collection](https://www.usebruno.com/) (Postman alternative) in [`docs/api-docs/`](./docs/api-docs/). 102 | 103 | --- 104 | 105 | ### Get Secrets + Configuration 106 | 107 | `GET /api/l1-builder/v1/configuration` 108 | 109 | Auth: 110 | 111 | - IP + Client-ATLS Attestation 112 | 113 | Response: [testdata/get-configuration.json](https://github.com/flashbots/builder-config-hub/blob/main/testdata/get-configuration.json) 114 | 115 | --- 116 | 117 | ### Register Credentials 118 | 119 | `POST /api/l1-builder/v1/register_credentials/` 120 | 121 | Auth: 122 | 123 | - IP + Client-ATLS Attestation 124 | 125 | Request: 126 | 127 | - [service: `orderflow_proxy`]: 128 | - TLS cert + ECDSA pubkey address (for orderflow) 129 | 130 | ```json 131 | { 132 | "tls_cert": string (\n instead of newlines) 133 | "ecdsa_pubkey_address": string 134 | } 135 | ``` 136 | 137 | - [service: `rbuilder`]: ECDSA pubkey address (for bids) 138 | 139 | ```json 140 | { 141 | "ecdsa_pubkey_address": string 142 | } 143 | ``` 144 | 145 | 146 | Response: 200 OK 147 | 148 | --- 149 | 150 | ### Get Active Builders 151 | 152 | `GET /api/l1-builder/v1/builders` (external, requests from builder via cvm-proxy) 153 | 154 | `GET /api/internal/l1-builder/v1/builders` (internal, no auth, uses `production` network by default) 155 | 156 | `GET /api/internal/l1-builder/v2/network/{network}/builders` (internal, no auth, for specific peer network) 157 | 158 | Response: [testdata/get-builders.json](https://github.com/flashbots/builder-config-hub/blob/main/testdata/get-builders.json) 159 | 160 | --- 161 | 162 | ### Get Allowed Measurements 163 | 164 | Auth: public 165 | 166 | `GET /api/l1-builder/v1/measurements` 167 | 168 | Response: Array with currently allowed measurement JSONs 169 | 170 | [testdata/get-measurements.json](https://github.com/flashbots/builder-config-hub/blob/main/testdata/get-measurements.json) 171 | 172 | --- 173 | 174 | ## Admin Endpoints 175 | 176 | ### Add measurements 177 | 178 | (created disabled by default) 179 | 180 | `POST /api/admin/v1/measurements` 181 | 182 | Payload 183 | 184 | ```json 185 | { 186 | "measurement_id": "v1.2.3-20241010-rc1", 187 | "attestation_type": "azure-tdx", 188 | "measurements": { 189 | "11": { 190 | "expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7" 191 | }, 192 | } 193 | } 194 | ``` 195 | 196 | Note that only the measurements given are expected, and any non-present will be ignored. 197 | 198 | To allow _any_ measurement, use an empty measurements field: 199 | `"measurements": {}`. 200 | 201 | ```json 202 | { 203 | "measurement_id": "test-blanket-allow", 204 | "attestation_type": "azure-tdx", 205 | "measurements": {} 206 | } 207 | ``` 208 | 209 | ### Enable/disable measurements 210 | 211 | `POST /api/admin/v1/measurements/activation/{measurement_id}` 212 | 213 | ```json 214 | { 215 | "enabled": true 216 | } 217 | ``` 218 | 219 | ### Adding a new builder instance 220 | 221 | (created inactive by default) 222 | 223 | `POST /api/admin/v1/builders/` 224 | 225 | Payload 226 | 227 | ```json 228 | { 229 | "name": string, 230 | "ip_address": string 231 | } 232 | ``` 233 | 234 | ### Enable/disable builder instance 235 | 236 | `POST /api/admin/v1/builders/activation/{builder_name}` 237 | 238 | ```json 239 | { 240 | "enabled": true 241 | } 242 | ``` 243 | 244 | Errors: 245 | 246 | - if no active configuration available 247 | 248 | ### Get builder configuration 249 | 250 | `GET /api/admin/v1/builders/configuration/{builder_name}/active` 251 | 252 | gets always the latest/active configuration 253 | 254 | ### Get builder configuration with secrets 255 | 256 | `GET /api/admin/v1/builders/configuration/{builder_name}/full` 257 | 258 | gets always the latest/active configuration 259 | 260 | ### Update builder configuration 261 | 262 | if valid JSON, will create a new active configuration and disable the old configuration 263 | 264 | `POST /api/admin/v1/builders/configuration/{builder_name}` 265 | 266 | ```json 267 | { 268 | ... 269 | } 270 | ``` 271 | 272 | ### Update secrets configuration 273 | 274 | POST `/api/admin/v1/builders/secrets/{builderName}` 275 | 276 | Payload: JSON with secrets, both flattened/unflattened 277 | 278 | ```json 279 | { 280 | ... 281 | } 282 | ``` -------------------------------------------------------------------------------- /adapters/database/service.go: -------------------------------------------------------------------------------- 1 | // Package database provides a database adapter for postgres 2 | package database 3 | 4 | import ( 5 | "context" 6 | "database/sql" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "net" 11 | 12 | "github.com/flashbots/builder-hub/domain" 13 | "github.com/jackc/pgtype" 14 | "github.com/jmoiron/sqlx" 15 | _ "github.com/lib/pq" 16 | ) 17 | 18 | type Service struct { 19 | DB *sqlx.DB 20 | } 21 | 22 | func NewDatabaseService(dsn string) (*Service, error) { 23 | db, err := sqlx.Connect("postgres", dsn) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | db.DB.SetMaxOpenConns(50) 29 | db.DB.SetMaxIdleConns(10) 30 | db.DB.SetConnMaxIdleTime(0) 31 | 32 | dbService := &Service{DB: db} //nolint:exhaustruct 33 | return dbService, err 34 | } 35 | 36 | func (s *Service) Close() error { 37 | return s.DB.Close() 38 | } 39 | 40 | func (s *Service) GetActiveMeasurementsByType(ctx context.Context, attestationType string) ([]domain.Measurement, error) { 41 | var measurements []Measurement 42 | err := s.DB.SelectContext(ctx, &measurements, `SELECT * FROM measurements_whitelist WHERE is_active=true AND attestation_type=$1`, attestationType) 43 | var domainMeasurements []domain.Measurement 44 | for _, m := range measurements { 45 | domainM, err := convertMeasurementToDomain(m) 46 | if err != nil { 47 | return nil, err 48 | } 49 | domainMeasurements = append(domainMeasurements, *domainM) 50 | } 51 | return domainMeasurements, err 52 | } 53 | 54 | // GetBuilderByIP retrieves a builder by IP address 55 | func (s *Service) GetBuilderByIP(ip net.IP) (*domain.Builder, error) { 56 | var paramIP pgtype.Inet 57 | err := paramIP.Set(ip) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | var b Builder 63 | err = s.DB.Get(&b, ` 64 | SELECT * FROM builders 65 | WHERE ip_address = $1 and is_active = true 66 | `, paramIP) 67 | if errors.Is(err, sql.ErrNoRows) { 68 | return nil, domain.ErrNotFound 69 | } 70 | if err != nil { 71 | return nil, err 72 | } 73 | return convertBuilderToDomain(b) 74 | } 75 | 76 | // GetActiveMeasurements retrieves all measurements 77 | func (s *Service) GetActiveMeasurements(ctx context.Context) ([]domain.Measurement, error) { 78 | var measurements []Measurement 79 | err := s.DB.SelectContext(ctx, &measurements, `SELECT * FROM measurements_whitelist WHERE is_active=true`) 80 | var domainMeasurements []domain.Measurement 81 | for _, m := range measurements { 82 | domainM, err := convertMeasurementToDomain(m) 83 | if err != nil { 84 | return nil, err 85 | } 86 | domainMeasurements = append(domainMeasurements, *domainM) 87 | } 88 | return domainMeasurements, err 89 | } 90 | 91 | // RegisterCredentialsForBuilder registers new credentials for a builder, deprecating all previous credentials 92 | // It uses hash and attestation_type to fetch the corresponding measurement_id via a subquery. 93 | func (s *Service) RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error { 94 | // Start a transaction 95 | tx, err := s.DB.BeginTx(ctx, nil) 96 | if err != nil { 97 | return err 98 | } 99 | defer func() { 100 | _ = tx.Rollback() 101 | }() // Rollback the transaction if it's not committed 102 | 103 | // Deprecate all previous credentials for this builder and service 104 | _, err = tx.Exec(` 105 | UPDATE service_credential_registrations 106 | SET is_active = false, deprecated_at = NOW() 107 | WHERE builder_name = $1 AND service = $2 108 | `, builderName, service) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // Insert new credentials with a subquery to fetch the measurement_id 114 | var nullableTLSCert sql.NullString 115 | if tlsCert != "" { 116 | nullableTLSCert = sql.NullString{String: tlsCert, Valid: true} 117 | } 118 | 119 | _, err = tx.Exec(` 120 | INSERT INTO service_credential_registrations 121 | (builder_name, service, tls_cert, ecdsa_pubkey, is_active, measurement_id) 122 | VALUES ($1, $2, $3, $4, true, 123 | (SELECT id FROM measurements_whitelist WHERE name = $5 AND attestation_type = $6) 124 | ) 125 | `, builderName, service, nullableTLSCert, ecdsaPubKey, measurementName, attestationType) 126 | if err != nil { 127 | return fmt.Errorf("failed to insert credentials for builder %s: %w", builderName, err) 128 | } 129 | 130 | // Commit the transaction 131 | if err = tx.Commit(); err != nil { 132 | return err 133 | } 134 | 135 | return nil 136 | } 137 | 138 | // GetActiveConfigForBuilder retrieves the active config for a builder by name 139 | func (s *Service) GetActiveConfigForBuilder(ctx context.Context, builderName string) (json.RawMessage, error) { 140 | var config BuilderConfig 141 | err := s.DB.GetContext(ctx, &config, ` 142 | SELECT * FROM builder_configs 143 | WHERE builder_name = $1 AND is_active = true 144 | `, builderName) 145 | if errors.Is(err, sql.ErrNoRows) { 146 | return nil, domain.ErrNotFound 147 | } 148 | return config.Config, err 149 | } 150 | 151 | func (s *Service) GetActiveBuildersWithServiceCredentials(ctx context.Context, network string) ([]domain.BuilderWithServices, error) { 152 | rows, err := s.DB.QueryContext(ctx, ` 153 | SELECT 154 | b.name, 155 | b.ip_address, 156 | scr.service, 157 | scr.tls_cert, 158 | scr.ecdsa_pubkey 159 | FROM 160 | builders b 161 | LEFT JOIN 162 | service_credential_registrations scr ON b.name = scr.builder_name 163 | WHERE 164 | b.is_active = true AND (scr.is_active = true OR scr.is_active IS NULL) AND b.network = $1 165 | ORDER BY 166 | b.name, scr.service 167 | `, network) 168 | if err != nil { 169 | return nil, err 170 | } 171 | defer rows.Close() 172 | 173 | buildersMap := make(map[string]*BuilderWithCredentials) 174 | 175 | for rows.Next() { 176 | var ipAddress pgtype.Inet 177 | var builderName string 178 | var service sql.NullString 179 | var tlsCert sql.NullString 180 | var ecdsaPubKey []byte 181 | 182 | err := rows.Scan(&builderName, &ipAddress, &service, &tlsCert, &ecdsaPubKey) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | builder, exists := buildersMap[builderName] 188 | if !exists { 189 | builder = &BuilderWithCredentials{ 190 | Name: builderName, 191 | IPAddress: ipAddress, 192 | } 193 | buildersMap[builderName] = builder 194 | } 195 | 196 | if service.Valid { 197 | builder.Credentials = append(builder.Credentials, ServiceCredential{ 198 | Service: service.String, 199 | TLSCert: tlsCert, 200 | ECDSAPubKey: ecdsaPubKey, 201 | }) 202 | } 203 | } 204 | 205 | if err = rows.Err(); err != nil { 206 | return nil, err 207 | } 208 | 209 | // Convert map to slice 210 | builders := make([]domain.BuilderWithServices, 0, len(buildersMap)) 211 | for _, builder := range buildersMap { 212 | dBuilder, err := toDomainBuilderWithCredentials(*builder) 213 | if err != nil { 214 | return nil, err 215 | } 216 | builders = append(builders, *dBuilder) 217 | } 218 | 219 | return builders, nil 220 | } 221 | 222 | // LogEvent creates a new log entry in the event_log table. 223 | // It uses hash and attestation_type to fetch the corresponding measurement_id via a subquery. 224 | func (s *Service) LogEvent(ctx context.Context, eventName, builderName, name string) error { 225 | // Insert new event log entry with a subquery to fetch the measurement_id 226 | _, err := s.DB.ExecContext(ctx, ` 227 | INSERT INTO event_log 228 | (event_name, builder_name, measurement_id) 229 | VALUES ($1, $2, 230 | (SELECT id FROM measurements_whitelist WHERE name = $3) 231 | ) 232 | `, eventName, builderName, name) 233 | if err != nil { 234 | return fmt.Errorf("failed to insert event log for builder %s: %w", builderName, err) 235 | } 236 | 237 | return nil 238 | } 239 | 240 | func (s *Service) AddMeasurement(ctx context.Context, measurement domain.Measurement, enabled bool) error { 241 | bts, err := json.Marshal(measurement.Measurement) 242 | if err != nil { 243 | return err 244 | } 245 | _, err = s.DB.ExecContext(ctx, ` 246 | INSERT INTO measurements_whitelist (name, attestation_type, measurement, is_active) 247 | VALUES ($1, $2, $3, $4) 248 | `, measurement.Name, measurement.AttestationType, bts, enabled) 249 | return err 250 | } 251 | 252 | func (s *Service) AddBuilder(ctx context.Context, builder domain.Builder) error { 253 | bIP := pgtype.Inet{} 254 | err := bIP.Set(builder.IPAddress) 255 | if err != nil { 256 | return err 257 | } 258 | _, err = s.DB.ExecContext(ctx, ` 259 | INSERT INTO builders (name, ip_address, is_active, network) 260 | VALUES ($1, $2, $3, $4) 261 | `, builder.Name, bIP, builder.IsActive, builder.Network) 262 | return err 263 | } 264 | 265 | func (s *Service) ChangeActiveStatusForBuilder(ctx context.Context, builderName string, isActive bool) error { 266 | _, err := s.DB.ExecContext(ctx, ` 267 | UPDATE builders 268 | SET is_active = $1 269 | WHERE name = $2 270 | `, isActive, builderName) 271 | return err 272 | } 273 | 274 | func (s *Service) ChangeActiveStatusForMeasurement(ctx context.Context, measurementName string, isActive bool) error { 275 | // NOTE: we currently enforce uniqueness per name and attestation type not just by name 276 | _, err := s.DB.ExecContext(ctx, ` 277 | UPDATE measurements_whitelist 278 | SET is_active = $1 279 | WHERE name = $2 280 | `, isActive, measurementName) 281 | return err 282 | } 283 | 284 | func (s *Service) AddBuilderConfig(ctx context.Context, builderName string, config json.RawMessage) error { 285 | // Start a transaction 286 | tx, err := s.DB.BeginTx(ctx, nil) 287 | if err != nil { 288 | return err 289 | } 290 | defer func() { 291 | _ = tx.Rollback() 292 | }() // Rollback the transaction if it's not committed 293 | 294 | // Deactivate any previous configurations for this builder 295 | _, err = tx.Exec(` 296 | UPDATE builder_configs 297 | SET is_active = false, updated_at = NOW() 298 | WHERE builder_name = $1 AND is_active = true 299 | `, builderName) 300 | if err != nil { 301 | return fmt.Errorf("failed to deactivate previous configs for builder %s: %w", builderName, err) 302 | } 303 | 304 | // Insert the new configuration as active 305 | _, err = tx.Exec(` 306 | INSERT INTO builder_configs (builder_name, config, is_active) 307 | VALUES ($1, $2, true) 308 | `, builderName, config) 309 | if err != nil { 310 | return fmt.Errorf("failed to insert new config for builder %s: %w", builderName, err) 311 | } 312 | 313 | // Commit the transaction 314 | if err = tx.Commit(); err != nil { 315 | return fmt.Errorf("failed to commit transaction for builder %s: %w", builderName, err) 316 | } 317 | 318 | return nil 319 | } 320 | -------------------------------------------------------------------------------- /adapters/database/service_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net" 7 | "os" 8 | "testing" 9 | 10 | "github.com/flashbots/builder-hub/domain" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestGetBuilder(t *testing.T) { 15 | if os.Getenv("RUN_DB_TESTS") != "1" { 16 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 17 | } 18 | serv, err := NewDatabaseService("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable") 19 | if err != nil { 20 | t.Errorf("NewDatabaseService() = %v; want nil", err) 21 | } 22 | _, err = serv.DB.Exec("TRUNCATE TABLE public.builders CASCADE") 23 | require.NoError(t, err) 24 | _, err = serv.DB.Exec("TRUNCATE TABLE public.measurements_whitelist CASCADE") 25 | require.NoError(t, err) 26 | 27 | t.Run("GetBuilder2", func(t *testing.T) { 28 | t.Run("should return a builder", func(t *testing.T) { 29 | _, err := serv.DB.Exec("INSERT INTO public.builders (name, ip_address, is_active, created_at, updated_at, network) VALUES ('flashbots-builder', '192.168.1.1', true, '2024-10-11 13:05:56.845615 +00:00', '2024-10-11 13:05:56.845615 +00:00', 'production');") 30 | require.NoError(t, err) 31 | whitelist, err := serv.GetBuilderByIP(net.ParseIP("192.168.1.1")) 32 | require.NoError(t, err) 33 | require.Equal(t, whitelist.Name, "flashbots-builder") 34 | }) 35 | t.Run("get all active builders", func(t *testing.T) { 36 | whitelist, err := serv.GetActiveBuildersWithServiceCredentials(context.Background(), domain.ProductionNetwork) 37 | require.Nil(t, err) 38 | require.Lenf(t, whitelist, 1, "expected 1 builder, got %d", len(whitelist)) 39 | require.Equal(t, whitelist[0].Builder.Name, "flashbots-builder") 40 | }) 41 | t.Run("get all active measurements", func(t *testing.T) { 42 | whitelist, err := serv.GetActiveMeasurements(context.Background()) 43 | if err != nil { 44 | t.Errorf("GetIPWhitelistByIP() = %v; want nil", err) 45 | } 46 | require.Len(t, whitelist, 0) 47 | }) 48 | }) 49 | } 50 | 51 | func TestAdminFlow(t *testing.T) { 52 | if os.Getenv("RUN_DB_TESTS") != "1" { 53 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 54 | } 55 | dbService, err := NewDatabaseService("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable") 56 | if err != nil { 57 | t.Errorf("NewDatabaseService() = %v; want nil", err) 58 | } 59 | _, err = dbService.DB.Exec("TRUNCATE TABLE public.builders CASCADE") 60 | require.NoError(t, err) 61 | _, err = dbService.DB.Exec("TRUNCATE TABLE public.measurements_whitelist CASCADE") 62 | require.NoError(t, err) 63 | 64 | t.Run("AdminFlow", func(t *testing.T) { 65 | t.Run("add measurement", func(t *testing.T) { 66 | err := dbService.AddMeasurement(context.Background(), *domain.NewMeasurement("test-measurement-1", "test-type", map[string]domain.SingleMeasurement{"test": {Expected: "0x1234"}}), false) 67 | require.NoError(t, err) 68 | }) 69 | t.Run("add builder", func(t *testing.T) { 70 | builder := domain.Builder{ 71 | Name: "test-builder", 72 | IPAddress: net.ParseIP("127.0.0.1"), 73 | Network: domain.ProductionNetwork, 74 | IsActive: false, 75 | } 76 | err := dbService.AddBuilder(context.Background(), builder) 77 | require.NoError(t, err) 78 | }) 79 | t.Run("add builder config", func(t *testing.T) { 80 | conf := `{"key1":"value1", "key2":"value2"}` 81 | err := dbService.AddBuilderConfig(context.Background(), "test-builder", json.RawMessage(conf)) 82 | require.NoError(t, err) 83 | }) 84 | t.Run("get builders", func(t *testing.T) { 85 | builders, err := dbService.GetActiveBuildersWithServiceCredentials(context.Background(), "") 86 | require.NoError(t, err) 87 | require.Len(t, builders, 0) 88 | }) 89 | t.Run("activate measurements", func(t *testing.T) { 90 | err := dbService.ChangeActiveStatusForMeasurement(context.Background(), "test-measurement-1", true) 91 | require.NoError(t, err) 92 | }) 93 | t.Run("activate builder", func(t *testing.T) { 94 | err := dbService.ChangeActiveStatusForBuilder(context.Background(), "test-builder", true) 95 | require.NoError(t, err) 96 | }) 97 | t.Run("get builders", func(t *testing.T) { 98 | builders, err := dbService.GetActiveBuildersWithServiceCredentials(context.Background(), domain.ProductionNetwork) 99 | require.NoError(t, err) 100 | require.Len(t, builders, 1) 101 | require.Equal(t, builders[0].Builder.Name, "test-builder") 102 | }) 103 | t.Run("get measurements", func(t *testing.T) { 104 | measurements, err := dbService.GetActiveMeasurements(context.Background()) 105 | require.NoError(t, err) 106 | require.Len(t, measurements, 1) 107 | require.Equal(t, measurements[0].Name, "test-measurement-1") 108 | }) 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /adapters/database/types.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/flashbots/builder-hub/domain" 9 | "github.com/jackc/pgtype" 10 | ) 11 | 12 | type ActiveBuilder struct { 13 | BuilderID int `db:"builder_id"` 14 | Name string `db:"name"` 15 | IPAddress pgtype.Inet `db:"ip_address"` 16 | TLSPubKey []byte `db:"tls_pubkey"` 17 | ECDSAPubKey []byte `db:"ecdsa_pubkey"` 18 | } 19 | 20 | type Measurement struct { 21 | ID int `db:"id"` 22 | Name string `db:"name"` 23 | AttestationType string `db:"attestation_type"` 24 | Measurement json.RawMessage `db:"measurement"` 25 | IsActive bool `db:"is_active"` 26 | CreatedAt time.Time `db:"created_at"` 27 | UpdatedAt time.Time `db:"updated_at"` 28 | DeprecatedAt *time.Time `db:"deprecated_at"` 29 | } 30 | 31 | func convertMeasurementToDomain(measurement Measurement) (*domain.Measurement, error) { 32 | var m domain.Measurement 33 | m.AttestationType = measurement.AttestationType 34 | m.Measurement = make(map[string]domain.SingleMeasurement) 35 | err := json.Unmarshal(measurement.Measurement, &m.Measurement) 36 | if err != nil { 37 | return nil, err 38 | } 39 | m.Name = measurement.Name 40 | return &m, nil 41 | } 42 | 43 | type Builder struct { 44 | Name string `db:"name"` 45 | IPAddress pgtype.Inet `db:"ip_address"` 46 | IsActive bool `db:"is_active"` 47 | Network string `db:"network"` 48 | CreatedAt time.Time `db:"created_at"` 49 | UpdatedAt time.Time `db:"updated_at"` 50 | DeprecatedAt *time.Time `db:"deprecated_at"` 51 | } 52 | 53 | func convertBuilderToDomain(builder Builder) (*domain.Builder, error) { 54 | if builder.IPAddress.IPNet == nil { 55 | return nil, domain.ErrIncorrectBuilder 56 | } 57 | return &domain.Builder{ 58 | Name: builder.Name, 59 | IPAddress: builder.IPAddress.IPNet.IP, 60 | IsActive: builder.IsActive, 61 | Network: builder.Network, 62 | }, nil 63 | } 64 | 65 | type ServiceCredentialRegistration struct { 66 | ID int `db:"id"` 67 | BuilderName string `db:"builder_name"` 68 | Service string `db:"service"` 69 | TLSCert string `db:"tls_cert"` 70 | ECDSAPubKey []byte `db:"ecdsa_pubkey"` 71 | CreatedAt time.Time `db:"created_at"` 72 | } 73 | 74 | type BuilderConfig struct { 75 | ID int `db:"id"` 76 | BuilderName string `db:"builder_name"` 77 | Config json.RawMessage `db:"config"` 78 | IsActive bool `db:"is_active"` 79 | CreatedAt time.Time `db:"created_at"` 80 | UpdatedAt time.Time `db:"updated_at"` 81 | } 82 | 83 | type BuilderWithCredentials struct { 84 | Name string 85 | IPAddress pgtype.Inet 86 | Credentials []ServiceCredential 87 | } 88 | type ServiceCredential struct { 89 | Service string 90 | TLSCert sql.NullString 91 | ECDSAPubKey []byte 92 | } 93 | 94 | func toDomainBuilderWithCredentials(builder BuilderWithCredentials) (*domain.BuilderWithServices, error) { 95 | if builder.IPAddress.IPNet == nil { 96 | return nil, domain.ErrIncorrectBuilder 97 | } 98 | s := domain.BuilderWithServices{ 99 | Builder: domain.Builder{ 100 | Name: builder.Name, 101 | IPAddress: builder.IPAddress.IPNet.IP, 102 | IsActive: true, 103 | }, 104 | Services: make([]domain.BuilderServices, 0, len(builder.Credentials)), 105 | } 106 | for _, cred := range builder.Credentials { 107 | s.Services = append(s.Services, domain.BuilderServices{ 108 | TLSCert: cred.TLSCert.String, 109 | ECDSAPubKey: domain.Bytes2Address(cred.ECDSAPubKey), 110 | Service: cred.Service, 111 | }) 112 | } 113 | return &s, nil 114 | } 115 | -------------------------------------------------------------------------------- /adapters/secrets/service.go: -------------------------------------------------------------------------------- 1 | // Package secrets contains logic for adapter to aws secrets manager 2 | package secrets 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/secretsmanager" 11 | ) 12 | 13 | type Service struct { 14 | sm *secretsmanager.SecretsManager 15 | secretName string 16 | } 17 | 18 | func NewService(secretName string) (*Service, error) { 19 | sess, err := session.NewSession(&aws.Config{ 20 | Region: aws.String("us-east-2"), 21 | }) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // Create a Secrets Manager client 27 | svc := secretsmanager.New(sess) 28 | 29 | return &Service{sm: svc, secretName: secretName}, nil 30 | } 31 | 32 | var ErrMissingSecret = errors.New("missing secret for builder") 33 | 34 | func (s *Service) GetSecretValues(builderName string) (json.RawMessage, error) { 35 | input := &secretsmanager.GetSecretValueInput{ 36 | SecretId: aws.String(s.secretName), 37 | } 38 | 39 | result, err := s.sm.GetSecretValue(input) 40 | if err != nil { 41 | return nil, err 42 | } 43 | secretData := make(map[string]json.RawMessage) 44 | err = json.Unmarshal([]byte(*result.SecretString), &secretData) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | builderSecret, ok := secretData[builderName] 50 | if !ok { 51 | return nil, ErrMissingSecret 52 | } 53 | 54 | return builderSecret, nil 55 | } 56 | 57 | func (s *Service) SetSecretValues(builderName string, values json.RawMessage) error { 58 | input := &secretsmanager.GetSecretValueInput{ 59 | SecretId: aws.String(s.secretName), 60 | } 61 | 62 | result, err := s.sm.GetSecretValue(input) 63 | if err != nil { 64 | return err 65 | } 66 | secretData := make(map[string]json.RawMessage) 67 | err = json.Unmarshal([]byte(*result.SecretString), &secretData) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | secretData[builderName] = values 73 | newSecretString, err := json.Marshal(secretData) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | sv := &secretsmanager.PutSecretValueInput{ 79 | SecretId: aws.String(s.secretName), 80 | SecretString: aws.String(string(newSecretString)), 81 | } 82 | _, err = s.sm.PutSecretValue(sv) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | func MergeSecrets(defaultSecrets, secrets map[string]string) map[string]string { 90 | // merge secrets 91 | res := make(map[string]string) 92 | for k, v := range defaultSecrets { 93 | res[k] = v 94 | } 95 | for k, v := range secrets { 96 | res[k] = v 97 | } 98 | return res 99 | } 100 | -------------------------------------------------------------------------------- /application/config_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type ExampleConfig struct { 12 | RethChainDownloader struct { 13 | AwsAccessKeyId string `json:"aws_access_key_id"` 14 | AwsSecretAccessKey string `json:"aws_secret_access_key"` 15 | RcloneS3Endpoint string `json:"rclone_s3_endpoint"` 16 | } `json:"reth_chain_downloader"` 17 | OrderflowProxy struct { 18 | FlashbotsOfSigningKey string `json:"flashbots_of_signing_key"` 19 | BuilderPublicIp string `json:"builder_public_ip"` 20 | TlsHosts []string `json:"tls_hosts"` 21 | } `json:"orderflow_proxy"` 22 | Rbuilder struct { 23 | ExtraData string `json:"extra_data"` 24 | RelaySecretKey string `json:"relay_secret_key"` 25 | OptimisticRelaySecretKey string `json:"optimistic_relay_secret_key"` 26 | CoinbaseSecretKey string `json:"coinbase_secret_key"` 27 | AlwaysSeal bool `json:"always_seal"` 28 | Relays []struct { 29 | Name string `json:"name"` 30 | Url string `json:"url"` 31 | UseSszForSubmit bool `json:"use_ssz_for_submit"` 32 | UseGzipForSubmit bool `json:"use_gzip_for_submit"` 33 | Priority int `json:"priority"` 34 | Optimistic bool `json:"optimistic"` 35 | } `json:"relays"` 36 | } `json:"rbuilder"` 37 | Prometheus struct { 38 | ScrapeInterval string `json:"scrape_interval"` 39 | StaticConfigsDefaultLabels []struct { 40 | LabelKey string `json:"label_key"` 41 | LabelValue string `json:"label_value"` 42 | } `json:"static_configs_default_labels"` 43 | LighthouseMetrics struct { 44 | Enabled bool `json:"enabled"` 45 | Targets []string `json:"targets"` 46 | } `json:"lighthouse_metrics"` 47 | RethMetrics struct { 48 | Enabled bool `json:"enabled"` 49 | Targets []string `json:"targets"` 50 | } `json:"reth_metrics"` 51 | RbuilderMetrics struct { 52 | Enabled bool `json:"enabled"` 53 | Targets []string `json:"targets"` 54 | } `json:"rbuilder_metrics"` 55 | RemoteWrite []struct { 56 | Name string `json:"name"` 57 | Url string `json:"url"` 58 | } `json:"remote_write"` 59 | } `json:"prometheus"` 60 | ProcessExporter struct { 61 | ProcessNames []struct { 62 | Name string `json:"name"` 63 | Cmdline []string `json:"cmdline"` 64 | } `json:"process_names"` 65 | } `json:"process_exporter"` 66 | Fluentbit struct { 67 | InputTags string `json:"input_tags"` 68 | OutputCwLogGroupName string `json:"output_cw_log_group_name"` 69 | OutputCwLogStreamPrefix string `json:"output_cw_log_stream_prefix"` 70 | } `json:"fluentbit"` 71 | } 72 | 73 | func TestMerge(t *testing.T) { 74 | exStr := `{ 75 | "reth_chain_downloader": { 76 | "aws_access_key_id": "string", 77 | "aws_secret_access_key": "string", 78 | "rclone_s3_endpoint": "string" 79 | }, 80 | "orderflow_proxy": { 81 | "flashbots_of_signing_key": "0x00", 82 | "builder_public_ip": "1.2.3.4", 83 | "tls_hosts": [ 84 | "1.2.3.4", 85 | "fundomain.builderx.io", 86 | "172.27.14.1", 87 | "2001:db8::123.123.123.123" 88 | ] 89 | }, 90 | "rbuilder": { 91 | "extra_data": "Illuminate Dmocratize Dstribute", 92 | "relay_secret_key": "0x00", 93 | "optimistic_relay_secret_key": "0x00", 94 | "coinbase_secret_key": "0x00", 95 | "always_seal": true, 96 | "relays": [ 97 | { 98 | "name": "flashbots", 99 | "url": "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net", 100 | "use_ssz_for_submit": true, 101 | "use_gzip_for_submit": false, 102 | "priority": 0, 103 | "optimistic": false 104 | }, 105 | { 106 | "name": "ultrasound", 107 | "url": "https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money", 108 | "use_ssz_for_submit": true, 109 | "use_gzip_for_submit": true, 110 | "priority": 1, 111 | "optimistic": true 112 | } 113 | ] 114 | }, 115 | "prometheus": { 116 | "scrape_interval": "10s", 117 | "static_configs_default_labels": [ 118 | { 119 | "label_key": "flashbots_net_vendor", 120 | "label_value": "azure" 121 | }, 122 | { 123 | "label_key": "flashbots_net_chain", 124 | "label_value": "mainnet" 125 | } 126 | ], 127 | "lighthouse_metrics": { 128 | "enabled": true, 129 | "targets": [ 130 | "localhost:5054" 131 | ] 132 | }, 133 | "reth_metrics": { 134 | "enabled": true, 135 | "targets": [ 136 | "localhost:9001" 137 | ] 138 | }, 139 | "rbuilder_metrics": { 140 | "enabled": true, 141 | "targets": [ 142 | "localhost:6069" 143 | ] 144 | }, 145 | "remote_write": [ 146 | { 147 | "name": "tdx-rbuilder-collector", 148 | "url": "https://aps-workspaces.us-east-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write" 149 | } 150 | ] 151 | }, 152 | "process_exporter": { 153 | "process_names": [ 154 | { 155 | "name": "lighthouse", 156 | "cmdline": [ 157 | "^\\/([-.0-9a-zA-Z]+\\/)*lighthouse[-.0-9a-zA-Z]* " 158 | ] 159 | }, 160 | { 161 | "name": "rbuilder", 162 | "cmdline": [ 163 | "^\\/([-.0-9a-zA-Z]+\\/)*rbuilder[-.0-9a-zA-Z]* " 164 | ] 165 | }, 166 | { 167 | "name": "reth", 168 | "cmdline": [ 169 | "^\\/([-.0-9a-zA-Z]+\\/)*reth[-.0-9a-zA-Z]* " 170 | ] 171 | } 172 | ] 173 | }, 174 | "fluentbit": { 175 | "input_tags": "tag-1 tag-2", 176 | "output_cw_log_group_name": "multioperator-builder", 177 | "output_cw_log_stream_prefix": "builder-01-" 178 | } 179 | } 180 | ` 181 | secrets := make(map[string]string) 182 | secrets["orderflow_proxy.flashbots_of_signing_key"] = "test_value_1" 183 | secrets["smb.smt.[0].url"] = "test_value_2" 184 | newC, err := MergeConfigSecrets([]byte(exStr), secrets) 185 | fmt.Println(string(newC)) 186 | require.NoError(t, err) 187 | 188 | cfg := ExampleConfig{} 189 | err = json.Unmarshal(newC, &cfg) 190 | require.NoError(t, err) 191 | require.Equal(t, "test_value_1", cfg.OrderflowProxy.FlashbotsOfSigningKey) 192 | } 193 | 194 | func TestFlatten(t *testing.T) { 195 | jsonBytes := []byte(`{ 196 | "user": { 197 | "name": "Alice", 198 | "details": { 199 | "age": "30", 200 | "address": { 201 | "city": "Wonderland" 202 | } 203 | }, 204 | "tags": ["admin", "user"] 205 | }, 206 | "smb":{"smt":[{"url":"test_value_2"}]} 207 | 208 | }`) 209 | flatMap, err := FlattenJSONFromBytes(jsonBytes) 210 | require.NoError(t, err) 211 | require.Equal(t, "Alice", flatMap["user.name"]) 212 | require.Equal(t, "test_value_2", flatMap["smb.smt.[0].url"]) 213 | } 214 | -------------------------------------------------------------------------------- /application/config_utils.go: -------------------------------------------------------------------------------- 1 | // Package application contains application logic for the builder-hub 2 | package application 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/buger/jsonparser" 11 | ) 12 | 13 | var ErrNonStringSecret = errors.New("secret value is not a string") 14 | 15 | func MergeConfigSecrets(config json.RawMessage, secrets map[string]string) (json.RawMessage, error) { 16 | // merge config and secrets 17 | bts := []byte(config) 18 | var err error 19 | for k, v := range secrets { 20 | tV := "\"" + v + "\"" 21 | bts, err = jsonparser.Set(bts, []byte(tV), strings.Split(k, ".")...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | } 27 | return bts, nil 28 | } 29 | 30 | // Recursive function to flatten JSON objects 31 | func flattenJSON(data map[string]interface{}, prefix string, flatMap map[string]string) error { 32 | for key, value := range data { 33 | newKey := key 34 | if prefix != "" { 35 | newKey = prefix + "." + key 36 | } 37 | 38 | switch v := value.(type) { 39 | case map[string]interface{}: 40 | err := flattenJSON(v, newKey, flatMap) 41 | if err != nil { 42 | return err 43 | } 44 | case []interface{}: 45 | err := flattenArray(v, newKey, flatMap) 46 | if err != nil { 47 | return err 48 | } 49 | case string: 50 | flatMap[newKey] = v 51 | default: 52 | // skipping 53 | return ErrNonStringSecret 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | // Recursive function to flatten JSON arrays 60 | func flattenArray(data []interface{}, prefix string, flatMap map[string]string) error { 61 | for i, value := range data { 62 | newKey := prefix + "." + "[" + strconv.Itoa(i) + "]" 63 | switch v := value.(type) { 64 | case map[string]interface{}: 65 | err := flattenJSON(v, newKey, flatMap) 66 | if err != nil { 67 | return err 68 | } 69 | case []interface{}: 70 | err := flattenArray(v, newKey, flatMap) 71 | if err != nil { 72 | return err 73 | } 74 | case string: 75 | flatMap[newKey] = v 76 | default: 77 | return ErrNonStringSecret 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | // FlattenJSONFromBytes is the main function to process JSON from []byte input 84 | func FlattenJSONFromBytes(jsonBytes []byte) (map[string]string, error) { 85 | var data map[string]interface{} 86 | if err := json.Unmarshal(jsonBytes, &data); err != nil { 87 | return nil, err 88 | } 89 | 90 | flatMap := make(map[string]string) 91 | err := flattenJSON(data, "", flatMap) 92 | if err != nil { 93 | return nil, err 94 | } 95 | return flatMap, nil 96 | } 97 | -------------------------------------------------------------------------------- /application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/flashbots/builder-hub/domain" 10 | ) 11 | 12 | type BuilderDataAccessor interface { 13 | GetActiveMeasurements(ctx context.Context) ([]domain.Measurement, error) 14 | GetActiveBuildersWithServiceCredentials(ctx context.Context, network string) ([]domain.BuilderWithServices, error) 15 | GetActiveMeasurementsByType(ctx context.Context, attestationType string) ([]domain.Measurement, error) 16 | GetBuilderByIP(ip net.IP) (*domain.Builder, error) 17 | GetActiveConfigForBuilder(ctx context.Context, builderName string) (json.RawMessage, error) 18 | RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error 19 | LogEvent(ctx context.Context, eventName, builderName, name string) error 20 | } 21 | 22 | type SecretAccessor interface { 23 | GetSecretValues(builderName string) (json.RawMessage, error) 24 | } 25 | 26 | type BuilderHub struct { 27 | dataAccessor BuilderDataAccessor 28 | secretAccessor SecretAccessor 29 | } 30 | 31 | func NewBuilderHub(dataAccessor BuilderDataAccessor, secretAccessor SecretAccessor) *BuilderHub { 32 | return &BuilderHub{dataAccessor: dataAccessor, secretAccessor: secretAccessor} 33 | } 34 | 35 | func (b *BuilderHub) GetAllowedMeasurements(ctx context.Context) ([]domain.Measurement, error) { 36 | return b.dataAccessor.GetActiveMeasurements(ctx) 37 | } 38 | 39 | func (b *BuilderHub) GetActiveBuilders(ctx context.Context, network string) ([]domain.BuilderWithServices, error) { 40 | return b.dataAccessor.GetActiveBuildersWithServiceCredentials(ctx, network) 41 | } 42 | 43 | func (b *BuilderHub) LogEvent(ctx context.Context, eventName, builderName, name string) error { 44 | return b.dataAccessor.LogEvent(ctx, eventName, builderName, name) 45 | } 46 | 47 | func (b *BuilderHub) RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error { 48 | return b.dataAccessor.RegisterCredentialsForBuilder(ctx, builderName, service, tlsCert, ecdsaPubKey, measurementName, attestationType) 49 | } 50 | 51 | func (b *BuilderHub) GetConfigWithSecrets(ctx context.Context, builderName string) ([]byte, error) { 52 | _, err := b.dataAccessor.GetActiveConfigForBuilder(ctx, builderName) 53 | if err != nil { 54 | return nil, fmt.Errorf("failing to fetch config for builder %s %w", builderName, err) 55 | } 56 | secr, err := b.secretAccessor.GetSecretValues(builderName) 57 | if err != nil { 58 | return nil, fmt.Errorf("failing to fetch secrets for builder %s %w", builderName, err) 59 | } 60 | return secr, nil 61 | } 62 | 63 | func (b *BuilderHub) VerifyIPAndMeasurements(ctx context.Context, ip net.IP, measurement map[string]string, attestationType string) (*domain.Builder, string, error) { 64 | measurements, err := b.dataAccessor.GetActiveMeasurementsByType(ctx, attestationType) 65 | if err != nil { 66 | return nil, "", fmt.Errorf("failing to fetch corresponding measurement data %s %w", attestationType, err) 67 | } 68 | measurementName, err := validateMeasurement(measurement, measurements) 69 | if err != nil { 70 | return nil, "", fmt.Errorf("failing to validate measurement %w", err) 71 | } 72 | 73 | builder, err := b.dataAccessor.GetBuilderByIP(ip) 74 | if err != nil { 75 | // TODO: might avoid logging ip though it should be ok, at least keep it for development state 76 | return nil, "", fmt.Errorf("failing to fetch builder by ip %s %w", ip.String(), err) 77 | } 78 | return builder, measurementName, nil 79 | } 80 | 81 | func validateMeasurement(measurement map[string]string, measurementTemplate []domain.Measurement) (string, error) { 82 | for _, m := range measurementTemplate { 83 | if checkMeasurement(measurement, m) { 84 | return m.Name, nil 85 | } 86 | } 87 | return "", domain.ErrNotFound 88 | } 89 | 90 | // validates that all fields from measurementTemplate are the same in measurement 91 | func checkMeasurement(measurement map[string]string, measurementTemplate domain.Measurement) bool { 92 | for k, v := range measurementTemplate.Measurement { 93 | if val, ok := measurement[k]; !ok || val != v.Expected { 94 | return false 95 | } 96 | } 97 | return true 98 | } 99 | -------------------------------------------------------------------------------- /cmd/httpserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/flashbots/builder-hub/adapters/database" 11 | "github.com/flashbots/builder-hub/adapters/secrets" 12 | "github.com/flashbots/builder-hub/application" 13 | "github.com/flashbots/builder-hub/common" 14 | "github.com/flashbots/builder-hub/domain" 15 | "github.com/flashbots/builder-hub/httpserver" 16 | "github.com/flashbots/builder-hub/ports" 17 | "github.com/google/uuid" 18 | "github.com/urfave/cli/v2" // imports as package "cli" 19 | ) 20 | 21 | var flags = []cli.Flag{ 22 | &cli.StringFlag{ 23 | Name: "listen-addr", 24 | Value: "127.0.0.1:8080", 25 | Usage: "address to serve API", 26 | EnvVars: []string{"LISTEN_ADDR"}, 27 | }, 28 | &cli.StringFlag{ 29 | Name: "admin-addr", 30 | Value: "127.0.0.1:8081", 31 | Usage: "address to serve admin API", 32 | EnvVars: []string{"ADMIN_ADDR"}, 33 | }, 34 | &cli.StringFlag{ 35 | Name: "internal-addr", 36 | Value: "127.0.0.1:8082", 37 | Usage: "address to serve internal API", 38 | EnvVars: []string{"INTERNAL_ADDR"}, 39 | }, 40 | &cli.StringFlag{ 41 | Name: "metrics-addr", 42 | Value: "127.0.0.1:8090", 43 | Usage: "address to serve Prometheus metrics", 44 | EnvVars: []string{"METRICS_ADDR"}, 45 | }, 46 | &cli.BoolFlag{ 47 | Name: "log-json", 48 | Value: false, 49 | Usage: "log in JSON format", 50 | EnvVars: []string{"LOG_JSON"}, 51 | }, 52 | &cli.BoolFlag{ 53 | Name: "log-debug", 54 | Value: false, 55 | Usage: "log debug messages", 56 | EnvVars: []string{"LOG_DEBUG"}, 57 | }, 58 | &cli.BoolFlag{ 59 | Name: "log-uid", 60 | Value: false, 61 | Usage: "generate a uuid and add to all log messages", 62 | EnvVars: []string{"LOG_UID"}, 63 | }, 64 | &cli.StringFlag{ 65 | Name: "log-service", 66 | Value: "httpserver", 67 | Usage: "add 'service' tag to logs", 68 | EnvVars: []string{"LOG_SERVICE"}, 69 | }, 70 | &cli.BoolFlag{ 71 | Name: "pprof", 72 | Value: false, 73 | Usage: "enable pprof debug endpoint", 74 | EnvVars: []string{"PPROF"}, 75 | }, 76 | &cli.Int64Flag{ 77 | Name: "drain-seconds", 78 | Value: 15, 79 | Usage: "seconds to wait in drain HTTP request", 80 | }, 81 | &cli.StringFlag{ 82 | Name: "postgres-dsn", 83 | Value: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", 84 | Usage: "Postgres DSN", 85 | EnvVars: []string{"POSTGRES_DSN"}, 86 | }, 87 | &cli.StringFlag{ 88 | Name: "secret-name", 89 | Value: "", 90 | Usage: "AWS Secret name", 91 | EnvVars: []string{"AWS_BUILDER_CONFIGS_SECRET_NAME"}, 92 | }, 93 | &cli.BoolFlag{ 94 | Name: "mock-secrets", 95 | Value: false, 96 | Usage: "Use inmemory secrets service for testing", 97 | EnvVars: []string{"MOCK_SECRETS"}, 98 | }, 99 | } 100 | 101 | func main() { 102 | app := &cli.App{ 103 | Name: "httpserver", 104 | Usage: "Serve API, and metrics", 105 | Flags: flags, 106 | Action: runCli, 107 | Version: common.Version, 108 | } 109 | 110 | if err := app.Run(os.Args); err != nil { 111 | log.Fatal(err) 112 | } 113 | } 114 | 115 | func runCli(cCtx *cli.Context) error { 116 | listenAddr := cCtx.String("listen-addr") 117 | adminAddr := cCtx.String("admin-addr") 118 | internalAddr := cCtx.String("internal-addr") 119 | metricsAddr := cCtx.String("metrics-addr") 120 | logJSON := cCtx.Bool("log-json") 121 | logDebug := cCtx.Bool("log-debug") 122 | logUID := cCtx.Bool("log-uid") 123 | logService := cCtx.String("log-service") 124 | enablePprof := cCtx.Bool("pprof") 125 | drainDuration := time.Duration(cCtx.Int64("drain-seconds")) * time.Second 126 | mockSecretsStorage := cCtx.Bool("mock-secrets") 127 | 128 | logTags := map[string]string{ 129 | "version": common.Version, 130 | } 131 | if logUID { 132 | logTags["uid"] = uuid.Must(uuid.NewRandom()).String() 133 | } 134 | 135 | log := common.SetupLogger(&common.LoggingOpts{ 136 | Service: logService, 137 | JSON: logJSON, 138 | Debug: logDebug, 139 | Concise: true, 140 | RequestHeaders: true, 141 | Tags: logTags, 142 | }) 143 | 144 | log.With("version", common.Version).Info("starting builder-hub") 145 | 146 | db, err := database.NewDatabaseService(cCtx.String("postgres-dsn")) 147 | if err != nil { 148 | log.Error("failed to create database", "err", err) 149 | return err 150 | } 151 | defer db.Close() 152 | 153 | var sm ports.AdminSecretService 154 | 155 | if mockSecretsStorage { 156 | log.Info("using mock secrets storage") 157 | sm = domain.NewMockSecretService() 158 | } else { 159 | sm, err = secrets.NewService(cCtx.String("secret-name")) 160 | if err != nil { 161 | log.Error("failed to create secrets manager", "err", err) 162 | return err 163 | } 164 | } 165 | 166 | builderHub := application.NewBuilderHub(db, sm) 167 | builderHandler := ports.NewBuilderHubHandler(builderHub, log) 168 | 169 | adminHandler := ports.NewAdminHandler(db, sm, log) 170 | cfg := &httpserver.HTTPServerConfig{ 171 | ListenAddr: listenAddr, 172 | MetricsAddr: metricsAddr, 173 | AdminAddr: adminAddr, 174 | InternalAddr: internalAddr, 175 | Log: log, 176 | EnablePprof: enablePprof, 177 | 178 | DrainDuration: drainDuration, 179 | GracefulShutdownDuration: 30 * time.Second, 180 | ReadTimeout: 60 * time.Second, 181 | WriteTimeout: 30 * time.Second, 182 | } 183 | 184 | srv, err := httpserver.NewHTTPServer(cfg, builderHandler, adminHandler) 185 | if err != nil { 186 | cfg.Log.Error("failed to create server", "err", err) 187 | return err 188 | } 189 | 190 | exit := make(chan os.Signal, 1) 191 | signal.Notify(exit, os.Interrupt, syscall.SIGTERM) 192 | srv.RunInBackground() 193 | <-exit 194 | 195 | // Shutdown server once termination signal is received 196 | srv.Shutdown() 197 | return nil 198 | } 199 | -------------------------------------------------------------------------------- /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 | 7 | "github.com/go-chi/httplog/v2" 8 | ) 9 | 10 | type LoggingOpts struct { 11 | Service string 12 | JSON bool 13 | Debug bool 14 | Concise bool 15 | RequestHeaders bool 16 | Tags map[string]string 17 | } 18 | 19 | func SetupLogger(opts *LoggingOpts) (log *httplog.Logger) { 20 | logLevel := slog.LevelInfo 21 | if opts.Debug { 22 | logLevel = slog.LevelDebug 23 | } 24 | 25 | logger := httplog.NewLogger(opts.Service, httplog.Options{ 26 | JSON: opts.JSON, 27 | LogLevel: logLevel, 28 | Concise: opts.Concise, 29 | RequestHeaders: opts.RequestHeaders, 30 | Tags: opts.Tags, 31 | }) 32 | return logger 33 | } 34 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func GetEnv(key, defaultValue string) string { 10 | if value, ok := os.LookupEnv(key); ok { 11 | return value 12 | } 13 | return defaultValue 14 | } 15 | 16 | func GetIPXForwardedFor(r *http.Request) string { 17 | forwarded := r.Header.Get("X-Forwarded-For") 18 | if forwarded != "" { 19 | if strings.Contains(forwarded, ",") { // return first entry of list of IPs 20 | return strings.Split(forwarded, ",")[0] 21 | } 22 | return forwarded 23 | } 24 | return r.RemoteAddr 25 | } 26 | -------------------------------------------------------------------------------- /common/vars.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | var Version = "dev" 4 | 5 | const ( 6 | PackageName = "github.com/flashbots/builder-hub" 7 | ) 8 | -------------------------------------------------------------------------------- /docker/database/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:15 2 | ENV POSTGRES_DB postgres 3 | ENV POSTGRES_HOST_AUTH_METHOD=trust 4 | COPY ./schema/*.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: flashbots/builder-hub-db 4 | ports: 5 | - 127.0.0.1:5432:5432 6 | environment: 7 | PGUSER: postgres 8 | POSTGRES_DB: postgres 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: postgres 11 | healthcheck: 12 | test: ["CMD-SHELL", "pg_isready"] 13 | interval: 5s 14 | retries: 5 15 | start_period: 2s 16 | timeout: 5s 17 | 18 | web: 19 | image: flashbots/builder-hub 20 | depends_on: 21 | db: 22 | condition: service_healthy 23 | restart: true 24 | links: 25 | - "db:database" 26 | ports: 27 | - 127.0.0.1:8080:8080 28 | - 127.0.0.1:8081:8081 29 | - 127.0.0.1:8082:8082 30 | - 127.0.0.1:8090:8090 31 | environment: 32 | MOCK_SECRETS: true 33 | POSTGRES_DSN: "postgres://postgres:postgres@db:5432/postgres?sslmode=disable" 34 | LISTEN_ADDR: "0.0.0.0:8080" 35 | ADMIN_ADDR: "0.0.0.0:8081" 36 | INTERNAL_ADDR: "0.0.0.0:8082" 37 | METRICS_ADDR: "0.0.0.0:8090" 38 | 39 | proxy: 40 | image: flashbots/builder-hub-mock-proxy 41 | links: 42 | - "web:web" 43 | ports: 44 | - 127.0.0.1:8888:8888 45 | environment: 46 | TARGET: "http://web:8080" 47 | -------------------------------------------------------------------------------- /docker/httpserver/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:1.23-alpine AS builder 3 | ARG VERSION 4 | RUN apk add --no-cache gcc sqlite-dev musl-dev 5 | WORKDIR /build 6 | # First only add go.mod and go.sum, then run go mod download to cache dependencies 7 | # in a separate layer. 8 | ADD go.mod go.sum /build/ 9 | RUN --mount=type=cache,target=/root/.cache/go-build go mod download 10 | # Now add the rest of the source code and build the application. 11 | ADD . /build/ 12 | RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=1 GOOS=linux \ 13 | go build \ 14 | -trimpath \ 15 | -ldflags "-s -X github.com/flashbots/builder-hub/common.Version=${VERSION} -w -extldflags \"-static\"" \ 16 | -v \ 17 | -o builder-hub \ 18 | cmd/httpserver/main.go 19 | 20 | FROM alpine:latest 21 | RUN apk update && apk upgrade 22 | RUN apk add --no-cache sqlite-dev 23 | # See http://stackoverflow.com/questions/34729748/installed-go-binary-not-found-in-path-on-alpine-linux-docker 24 | RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 25 | WORKDIR /app 26 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 27 | COPY --from=builder /build/builder-hub /app/builder-hub 28 | ADD testdata/ /app/testdata/ 29 | CMD ["/app/builder-hub"] 30 | -------------------------------------------------------------------------------- /docker/mock-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | # BuilderHub expects measurements headers. For testing purposes, we just mock them with 2 | # this nginx-based proxy container. 3 | FROM nginx:1.27 4 | COPY ./docker/mock-proxy/nginx-default.conf /etc/nginx/conf.d/default.conf 5 | 6 | -------------------------------------------------------------------------------- /docker/mock-proxy/nginx-default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen 8888; 4 | 5 | location / { 6 | proxy_pass http://web:8080; 7 | proxy_set_header X-Flashbots-Attestation-Type 'test'; 8 | proxy_set_header X-Flashbots-Measurement '{}'; 9 | proxy_set_header X-Forwarded-For '1.2.3.4'; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/README.md: -------------------------------------------------------------------------------- 1 | You can load the Bruno collection with https://www.usebruno.com 2 | 3 | The docs include these collections: 4 | - [Admin API](./admin-api/) 5 | - [Instance API](./instance-api/) -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Add measurements.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Add measurements 3 | type: http 4 | seq: 9 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/measurements 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "measurement_id": "buildernet-v1.2.1-rc1-azure-tdx-2fbda0c258c7a335fd5c89aeebb868cd6a3d6f26ed329638017884c17ef9ea97.wic.vhd", 16 | "attestation_type": "azure-tdx", 17 | "measurements": { 18 | "4": { 19 | "expected": "18e71bdf677a6138fce971ef56f069cedc9b312970cc5a174e57113617aa8738" 20 | }, 21 | "9": { 22 | "expected": "b1bef3012caf44508fd28194a379f625f622003298c81e9b04a9dc506760691d" 23 | }, 24 | "11": { 25 | "expected": "206b43acd2327a5731e5872f5052efb01f05c71d9d00634d8e38296fcfc18c0d" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Add new builder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Add new builder 3 | type: http 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/builders 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "name": "{builder}", 16 | "ip_address": "{ip_address}", 17 | "network": "{network}" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Disable builder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Disable builder 3 | type: http 4 | seq: 6 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/builders/activation/{builder} 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "enabled": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Enable builder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Enable builder 3 | type: http 4 | seq: 5 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/builders/activation/{builder} 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "enabled": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Enable measurement.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Enable measurement 3 | type: http 4 | seq: 10 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/measurements/activation/{measurement} 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "enabled": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Get Builders v2.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Builders v2 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: http://localhost:8080/api/internal/l1-builder/v2/network/{{network}}/builders 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Get Builders.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Builders 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: http://localhost:8080/api/internal/l1-builder/v1/builders 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Get configuration.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get configuration 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: http://localhost:8081/api/admin/v1/builders/configuration/{{builder}}/full 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Update builder config.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Update builder config 3 | type: http 4 | seq: 7 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/builders/configuration/{builder} 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "dns_name": "foobar-v1.a.b.c", 16 | "rbuilder": { 17 | "extra_data": "FooBar" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/Update secrets config.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Update secrets config 3 | type: http 4 | seq: 8 5 | } 6 | 7 | post { 8 | url: http://localhost:8081/api/admin/v1/builders/secrets/{builder} 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | {} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api-docs/admin-api/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "BuilderHub Admin API", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /docs/api-docs/admin-api/collection.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: BuilderHub 3 | } 4 | 5 | auth { 6 | mode: none 7 | } 8 | -------------------------------------------------------------------------------- /docs/api-docs/instance-api/BuilderHub Instance API/Get measurements.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get measurements 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: http://localhost:8888/api/l1-builder/v1/measurements 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/instance-api/BuilderHub Instance API/Get peers.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get peers 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: http://localhost:8888/api/l1-builder/v1/builders 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/api-docs/instance-api/BuilderHub Instance API/Register credentials.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Register credentials 3 | type: http 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: http://localhost:8888/api/l1-builder/v1/register_credentials/rbuilder 9 | body: json 10 | auth: none 11 | } 12 | 13 | body:json { 14 | { 15 | "ecdsa_pubkey_address": "0x321f3426eEc20DE1910af1CD595c4DD83BEA0BA5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/api-docs/instance-api/BuilderHub Instance API/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "BuilderHub Instance API", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /docs/api-docs/instance-api/Get measurements.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get measurements 3 | type: http 4 | seq: 11 5 | } 6 | 7 | get { 8 | url: http://localhost:8081/api/admin/v1/builders/configuration/{builder}/full 9 | body: none 10 | auth: none 11 | } 12 | -------------------------------------------------------------------------------- /docs/devenv-setup.md: -------------------------------------------------------------------------------- 1 | This is a quick guide on setting setting up and configuring a dev BuilderHub instance. 2 | 3 | --- 4 | 5 | ## Getting started 6 | 7 | First, use docker-compose to start the three containers: 8 | 9 | | Service | Ports & Notes | 10 | | ----------- | ---------------------------------------------------------------------------------------------------------------------- | 11 | | builder-hub | **8080** (Instance API), **8081** (Admin API), **8082** (Internal API) | 12 | | mock-proxy | **8888** (forwards to builder-hub Instance API on port 8080, adds dummy auth headers that in prod cvm-proxy would add) | 13 | | postgres | **5432** (with applied migrations) | 14 | 15 | See also [`docker-compose.yaml`](../docker/docker-compose.yaml) for more details. 16 | 17 | ```bash 18 | # Switch into the 'docker' directory 19 | cd docker 20 | 21 | # Update and start the services 22 | docker-compose pull 23 | docker-compose up 24 | ``` 25 | 26 | --- 27 | 28 | ## Public API 29 | 30 | There are some public API points, most notably the `get-measurements` endpoint, which is accessible without authentication: 31 | 32 | ```bash 33 | curl http://localhost:8080/api/l1-builder/v1/measurements | jq 34 | ``` 35 | 36 | --- 37 | 38 | ## Admin API 39 | 40 | For initial setup, use the [Admin API](https://github.com/flashbots/builder-hub?tab=readme-ov-file#admin-endpoints) (on port 8081) to: 41 | 1. Create and enable allowed measurements 42 | 2. Create a builder instance 43 | 3. Create a builder configuration 44 | 4. Enable the builder instance 45 | 46 | ```bash 47 | # 1a. Create a new measurements entry (empty allowed measurements will allow all client measurements) 48 | curl -v \ 49 | --url http://localhost:8081/api/admin/v1/measurements \ 50 | --data '{ 51 | "measurement_id": "test1", 52 | "attestation_type": "test", 53 | "measurements": {} 54 | }' 55 | 56 | # 1b. Enable the new measurements 57 | curl -v \ 58 | --request POST \ 59 | --url http://localhost:8081/api/admin/v1/measurements/activation/test1 \ 60 | --data '{ 61 | "enabled": true 62 | }' 63 | 64 | # 2. Create a new builder instance (with IP address 1.2.3.4, which is fixed in the mock-proxy) 65 | curl -v \ 66 | --url http://localhost:8081/api/admin/v1/builders \ 67 | --data '{ 68 | "name": "test_builder", 69 | "ip_address": "1.2.3.4", 70 | "network": "production" 71 | }' 72 | 73 | # 3. Create (and enable) a new builder configuration 74 | curl -v \ 75 | --url http://localhost:8081/api/admin/v1/builders/configuration/test_builder \ 76 | --data '{ 77 | "dns_name": "foobar-v1.a.b.c", 78 | "rbuilder": { 79 | "extra_data": "FooBar" 80 | } 81 | }' 82 | 83 | # 4. Enable the new builder instance 84 | curl -v \ 85 | --url http://localhost:8081/api/admin/v1/builders/activation/test_builder \ 86 | --data '{ 87 | "enabled": true 88 | }' 89 | ``` 90 | 91 | --- 92 | 93 | ## Instance API (authenticated) 94 | 95 | Now you can use the (authenticated) Instance API, like any production Yocto TDX instance would: 96 | 1. Get own instance configuration 97 | 2. Get the list of peers 98 | 3. Register credentials 99 | 100 | The API is authenticated through headers that are attached by the proxy, namely measurements, attestation type and IP address headers. 101 | The mock-proxy on port 8888 [adds these headers](https://github.com/flashbots/builder-hub/blob/main/docker/mock-proxy/nginx-default.conf), 102 | so you can use the API without any additional setup (or cvm-proxy instances). 103 | 104 | ```bash 105 | # 1. Get the instance configuration 106 | curl http://localhost:8888/api/l1-builder/v1/configuration | jq 107 | 108 | # 2. Get the list of peers 109 | curl http://localhost:8888/api/l1-builder/v1/builders | jq 110 | 111 | # 3. Register credentials for 'rbuilder' service 112 | curl -v \ 113 | --url http://localhost:8888/api/l1-builder/v1/register_credentials/rbuilder \ 114 | --data '{ 115 | "ecdsa_pubkey_address": "0x321f3426eEc20DE1910af1CD595c4DD83BEA0BA5" 116 | }' 117 | 118 | # If you now call get-peers again, it will contain the newly registered address: 119 | curl http://localhost:8888/api/l1-builder/v1/builders | jq 120 | ``` 121 | -------------------------------------------------------------------------------- /domain/inmemory_secret.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "encoding/json" 5 | "sync" 6 | ) 7 | 8 | type InmemorySecretService struct { 9 | mu *sync.RWMutex 10 | st map[string]json.RawMessage 11 | } 12 | 13 | func NewMockSecretService() *InmemorySecretService { 14 | return &InmemorySecretService{ 15 | st: make(map[string]json.RawMessage), 16 | mu: &sync.RWMutex{}, 17 | } 18 | } 19 | 20 | func (mss *InmemorySecretService) GetSecretValues(builderName string) (json.RawMessage, error) { 21 | mss.mu.RLock() 22 | defer mss.mu.RUnlock() 23 | return mss.st[builderName], nil 24 | } 25 | 26 | func (mss *InmemorySecretService) SetSecretValues(builderName string, values json.RawMessage) error { 27 | mss.mu.Lock() 28 | defer mss.mu.Unlock() 29 | mss.st[builderName] = values 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /domain/types.go: -------------------------------------------------------------------------------- 1 | // Package domain contains domain area types/functions for builder hub 2 | package domain 3 | 4 | import ( 5 | "errors" 6 | "net" 7 | 8 | "github.com/ethereum/go-ethereum/common" 9 | ) 10 | 11 | var ( 12 | ErrNotFound = errors.New("not found") 13 | ErrIncorrectBuilder = errors.New("incorrect builder") 14 | ErrInvalidMeasurement = errors.New("no such active measurement found") 15 | ) 16 | 17 | const ProductionNetwork = "production" 18 | 19 | const EventGetConfig = "GetConfig" 20 | 21 | type Measurement struct { 22 | Name string 23 | AttestationType string 24 | Measurement map[string]SingleMeasurement 25 | } 26 | 27 | type SingleMeasurement struct { 28 | Expected string `json:"expected"` 29 | } 30 | 31 | func NewMeasurement(name, attestationType string, measurements map[string]SingleMeasurement) *Measurement { 32 | return &Measurement{ 33 | AttestationType: attestationType, 34 | Measurement: measurements, 35 | Name: name, 36 | } 37 | } 38 | 39 | type Builder struct { 40 | Name string `json:"name"` 41 | IPAddress net.IP `json:"ip_address"` 42 | IsActive bool `json:"is_active"` 43 | Network string `json:"network"` 44 | } 45 | 46 | type BuilderWithServices struct { 47 | Builder Builder 48 | Services []BuilderServices 49 | } 50 | 51 | type BuilderServices struct { 52 | TLSCert string 53 | ECDSAPubKey *common.Address 54 | Service string 55 | } 56 | 57 | func Bytes2Address(b []byte) *common.Address { 58 | if len(b) == 0 { 59 | return nil 60 | } 61 | addr := common.BytesToAddress(b) 62 | return &addr 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/flashbots/builder-hub 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/VictoriaMetrics/metrics v1.35.1 7 | github.com/aws/aws-sdk-go v1.55.5 8 | github.com/buger/jsonparser v1.1.1 9 | github.com/ethereum/go-ethereum v1.14.11 10 | github.com/go-chi/chi/v5 v5.1.0 11 | github.com/go-chi/httplog/v2 v2.1.1 12 | github.com/google/uuid v1.6.0 13 | github.com/jackc/pgtype v1.14.3 14 | github.com/jmoiron/sqlx v1.4.0 15 | github.com/lib/pq v1.10.9 16 | github.com/stretchr/testify v1.9.0 17 | github.com/urfave/cli/v2 v2.27.2 18 | go.uber.org/atomic v1.11.0 19 | ) 20 | 21 | require ( 22 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/holiman/uint256 v1.3.1 // indirect 25 | github.com/jackc/pgio v1.0.0 // indirect 26 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 27 | github.com/jmespath/go-jmespath v0.4.0 // indirect 28 | github.com/pmezard/go-difflib v1.0.0 // indirect 29 | github.com/rogpeppe/go-internal v1.10.0 // indirect 30 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 31 | github.com/valyala/fastrand v1.1.0 // indirect 32 | github.com/valyala/histogram v1.2.0 // indirect 33 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 34 | golang.org/x/crypto v0.27.0 // indirect 35 | golang.org/x/sys v0.25.0 // indirect 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 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/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= 8 | github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= 9 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 10 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 11 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 12 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 13 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 15 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 16 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/ethereum/go-ethereum v1.14.11 h1:8nFDCUUE67rPc6AKxFj7JKaOa2W/W1Rse3oS6LvvxEY= 21 | github.com/ethereum/go-ethereum v1.14.11/go.mod h1:+l/fr42Mma+xBnhefL/+z11/hcmJ2egl+ScIVPjhc7E= 22 | github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 23 | github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 24 | github.com/go-chi/httplog/v2 v2.1.1 h1:ojojiu4PIaoeJ/qAO4GWUxJqvYUTobeo7zmuHQJAxRk= 25 | github.com/go-chi/httplog/v2 v2.1.1/go.mod h1:/XXdxicJsp4BA5fapgIC3VuTD+z0Z/VzukoB3VDc1YE= 26 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 27 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 28 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 29 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 30 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 31 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 32 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 33 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 34 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 35 | github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= 36 | github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 37 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 38 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 39 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 40 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 41 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 42 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 43 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 44 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 45 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 46 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 47 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 48 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 49 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 50 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 51 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 52 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 53 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 54 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 55 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 56 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 57 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 58 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 59 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 60 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 61 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 62 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 63 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 64 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 65 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 66 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 67 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 68 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 69 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 70 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 71 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 72 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 73 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 74 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 75 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 76 | github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= 77 | github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= 78 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 79 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 80 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 81 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 82 | github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= 83 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 84 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 85 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 86 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 87 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 88 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 89 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 90 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 91 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 92 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 93 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 94 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 95 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 96 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 98 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 99 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 100 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 101 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 102 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 103 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 104 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 105 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 106 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 107 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 108 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 109 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 110 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 111 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 112 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 113 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 114 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 115 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 116 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 117 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 118 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 119 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 120 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 121 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 122 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 123 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 124 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 125 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 126 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 127 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 128 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 129 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 130 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 131 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 132 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 133 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 134 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 135 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 136 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 137 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 138 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 139 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 140 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 142 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 143 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 144 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 145 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 146 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 147 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 148 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 149 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 150 | github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= 151 | github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= 152 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 153 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 154 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 155 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 156 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= 157 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= 158 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 159 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 160 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 161 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 162 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 163 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 164 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 165 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 166 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 167 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 168 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 169 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 170 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 171 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 172 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 173 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 174 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 175 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 176 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 177 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 178 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 179 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 180 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 181 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 182 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 183 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 184 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 185 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 186 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 187 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 188 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 189 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 190 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 191 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 192 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 193 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 194 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 195 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 196 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 197 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 198 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 199 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 200 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 201 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 216 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 218 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 219 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 220 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 221 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 222 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 223 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 224 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 225 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 226 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 227 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 228 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 229 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 230 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 231 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 232 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 233 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 234 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 235 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 236 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 237 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 238 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 239 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 240 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 241 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 242 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 243 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 244 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 245 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 246 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 247 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 248 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 249 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 250 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 251 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 252 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 253 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 254 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 255 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 256 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 257 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 258 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 259 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 260 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 261 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 262 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 263 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 264 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 265 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 266 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 267 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 268 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 269 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 270 | -------------------------------------------------------------------------------- /httpserver/doc.go: -------------------------------------------------------------------------------- 1 | // Package httpserver implements the core HTTP server 2 | package httpserver 3 | -------------------------------------------------------------------------------- /httpserver/e2e_test.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "slices" 12 | "testing" 13 | 14 | "github.com/ethereum/go-ethereum/common" 15 | "github.com/flashbots/builder-hub/adapters/database" 16 | "github.com/flashbots/builder-hub/application" 17 | "github.com/flashbots/builder-hub/domain" 18 | "github.com/flashbots/builder-hub/ports" 19 | "github.com/stretchr/testify/require" 20 | ) 21 | 22 | func TestCreateMultipleBuilders(t *testing.T) { 23 | if os.Getenv("RUN_DB_TESTS") != "1" { 24 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 25 | } 26 | s, _, _ := createServer(t) 27 | 28 | createBuilder(t, s, "test-builder-1", "127.0.0.1", domain.ProductionNetwork) 29 | createBuilder(t, s, "test-builder-2", "127.0.0.2", domain.ProductionNetwork) 30 | createBuilder(t, s, "test-builder-3", "127.0.0.3", domain.ProductionNetwork) 31 | } 32 | 33 | func TestCreateMultipleNetworkBuilders(t *testing.T) { 34 | if os.Getenv("RUN_DB_TESTS") != "1" { 35 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 36 | } 37 | s, _, _ := createServer(t) 38 | 39 | createBuilder(t, s, "production-1", "127.0.0.1", domain.ProductionNetwork) 40 | createBuilder(t, s, "production-2", "127.0.0.2", domain.ProductionNetwork) 41 | createBuilder(t, s, "production-3", "127.0.0.3", domain.ProductionNetwork) 42 | 43 | createBuilder(t, s, "test-1", "127.0.1.1", "test-network") 44 | createBuilder(t, s, "test-2", "127.0.1.2", "test-network") 45 | createBuilder(t, s, "test-3", "127.0.1.3", "test-network") 46 | createBuilder(t, s, "test-4", "127.0.1.4", "test-network") 47 | 48 | measurement := ports.Measurement{ 49 | Name: "test-measurement-1", 50 | AttestationType: "test-attestation-type-1", 51 | Measurements: map[string]domain.SingleMeasurement{ 52 | "8": { 53 | Expected: "0000000000000000000000000000000000000000000000000000000000000000", 54 | }, 55 | "11": { 56 | Expected: "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7", 57 | }, 58 | }, 59 | } 60 | createMeasurement(t, s, measurement) 61 | 62 | t.Run("NoAuthV1", func(t *testing.T) { 63 | var resp []ports.BuilderWithServiceCreds 64 | sc, _ := execRequestNoAuth(t, s.GetInternalRouter(), http.MethodGet, "/api/internal/l1-builder/v1/builders", nil, &resp) 65 | require.Equal(t, http.StatusOK, sc) 66 | require.Len(t, resp, 3) 67 | validNames := []string{"production-1", "production-2", "production-3"} 68 | for _, b := range resp { 69 | if !slices.Contains(validNames, b.Name) { 70 | require.Fail(t, "Builder not found") 71 | } 72 | } 73 | }) 74 | 75 | t.Run("NoAuthV2 Networked", func(t *testing.T) { 76 | var resp []ports.BuilderWithServiceCreds 77 | sc, _ := execRequestNoAuth(t, s.GetInternalRouter(), http.MethodGet, fmt.Sprintf("/api/internal/l1-builder/v2/network/%s/builders", domain.ProductionNetwork), nil, &resp) 78 | require.Equal(t, http.StatusOK, sc) 79 | require.Len(t, resp, 3) 80 | validNames := []string{"production-1", "production-2", "production-3"} 81 | for _, b := range resp { 82 | if !slices.Contains(validNames, b.Name) { 83 | require.Fail(t, "Builder not found") 84 | } 85 | } 86 | }) 87 | 88 | t.Run("NoAuthV2 Networked other network", func(t *testing.T) { 89 | var resp []ports.BuilderWithServiceCreds 90 | sc, _ := execRequestNoAuth(t, s.GetInternalRouter(), http.MethodGet, fmt.Sprintf("/api/internal/l1-builder/v2/network/%s/builders", "test-network"), nil, &resp) 91 | require.Equal(t, http.StatusOK, sc) 92 | require.Len(t, resp, 4) 93 | validNames := []string{"test-1", "test-2", "test-3", "test-4"} 94 | for _, b := range resp { 95 | if !slices.Contains(validNames, b.Name) { 96 | require.Fail(t, "Builder not found") 97 | } 98 | } 99 | }) 100 | 101 | t.Run("Auth active builders prod", func(t *testing.T) { 102 | resp := make([]ports.BuilderWithServiceCreds, 0) 103 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/builders", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.1") 104 | require.Equal(t, http.StatusOK, status) 105 | require.Len(t, resp, 3) 106 | validNames := []string{"production-1", "production-2", "production-3"} 107 | for _, b := range resp { 108 | if !slices.Contains(validNames, b.Name) { 109 | require.Fail(t, "Builder not found") 110 | } 111 | } 112 | }) 113 | 114 | t.Run("Auth active builders other network", func(t *testing.T) { 115 | resp := make([]ports.BuilderWithServiceCreds, 0) 116 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/builders", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.1.1") 117 | require.Equal(t, http.StatusOK, status) 118 | require.Len(t, resp, 4) 119 | validNames := []string{"test-1", "test-2", "test-3", "test-4"} 120 | for _, b := range resp { 121 | if !slices.Contains(validNames, b.Name) { 122 | require.Fail(t, "Builder not found") 123 | } 124 | } 125 | }) 126 | } 127 | 128 | func TestCreateMultipleMeasurements(t *testing.T) { 129 | if os.Getenv("RUN_DB_TESTS") != "1" { 130 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 131 | } 132 | s, _, _ := createServer(t) 133 | 134 | createMeasurement(t, s, ports.Measurement{ 135 | Name: "test-measurement-1", 136 | AttestationType: "test-attestation-type-1", 137 | Measurements: map[string]domain.SingleMeasurement{ 138 | "8": { 139 | Expected: "0000000000000000000000000000000000000000000000000000000000000000", 140 | }, 141 | "11": { 142 | Expected: "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7", 143 | }, 144 | }, 145 | }) 146 | createMeasurement(t, s, ports.Measurement{ 147 | Name: "test-measurement-2", 148 | AttestationType: "test-attestation-type-2", 149 | Measurements: map[string]domain.SingleMeasurement{ 150 | "8": { 151 | Expected: "0000000000000000000000000000000000000000000000000000000000000000", 152 | }, 153 | "11": { 154 | Expected: "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f8", 155 | }, 156 | }, 157 | }) 158 | } 159 | 160 | func TestAuthInteractionFlow(t *testing.T) { 161 | if os.Getenv("RUN_DB_TESTS") != "1" { 162 | t.Skip("skipping test; RUN_DB_TESTS is not set to 1") 163 | } 164 | 165 | s, _, _ := createServer(t) 166 | 167 | builderName := "test_builder_1" 168 | ip := "127.0.0.1" 169 | createBuilder(t, s, builderName, ip, domain.ProductionNetwork) 170 | measurement := ports.Measurement{ 171 | Name: "test-measurement-1", 172 | AttestationType: "test-attestation-type-1", 173 | Measurements: map[string]domain.SingleMeasurement{ 174 | "8": { 175 | Expected: "0000000000000000000000000000000000000000000000000000000000000000", 176 | }, 177 | "11": { 178 | Expected: "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7", 179 | }, 180 | }, 181 | } 182 | createMeasurement(t, s, measurement) 183 | 184 | t.Run("GetConfig", func(t *testing.T) { 185 | resp := make(map[string]string) 186 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/configuration", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.1") 187 | require.Equal(t, http.StatusOK, status) 188 | require.Equal(t, builderName+"_test_value_1", resp["test_key_1"]) 189 | require.Equal(t, builderName+"_test_secret_value", resp["test_secret_1"]) 190 | }) 191 | t.Run("GetConfigFailureWrongIP", func(t *testing.T) { 192 | resp := make(map[string]string) 193 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/configuration", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.2") 194 | require.Equal(t, http.StatusForbidden, status) 195 | }) 196 | t.Run("GetConfigFailureWrongMeasurement", func(t *testing.T) { 197 | resp := make(map[string]string) 198 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/configuration", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "abc"}, "127.0.0.1") 199 | require.Equal(t, http.StatusForbidden, status) 200 | }) 201 | t.Run("GetConfigFailureWrongMeasurementType", func(t *testing.T) { 202 | resp := make(map[string]string) 203 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/configuration", nil, &resp, "wrong-type", map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.1") 204 | require.Equal(t, http.StatusForbidden, status) 205 | }) 206 | 207 | t.Run("RegisterCredentials", func(t *testing.T) { 208 | addr := common.HexToAddress("0x1234567890123456789012345678901234567890") 209 | sc := ports.ServiceCred{ 210 | TLSCert: "test-cert-no-validation", 211 | ECDSAPubkey: &addr, 212 | } 213 | status, _ := execRequestAuth(t, s.GetRouter(), http.MethodPost, "/api/l1-builder/v1/register_credentials/rbuilder", sc, nil, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.1") 214 | require.Equal(t, http.StatusOK, status) 215 | }) 216 | t.Run("CheckCredentials", func(t *testing.T) { 217 | resp := make([]ports.BuilderWithServiceCreds, 0) 218 | sc, bts := execRequestAuth(t, s.GetRouter(), http.MethodGet, "/api/l1-builder/v1/builders", nil, &resp, measurement.AttestationType, map[string]string{"8": "0000000000000000000000000000000000000000000000000000000000000000", "11": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7"}, "127.0.0.1") 219 | c := string(bts) 220 | fmt.Println(c) 221 | require.Equal(t, http.StatusOK, sc) 222 | require.Equal(t, 1, len(resp)) 223 | require.Equal(t, builderName, resp[0].Name) 224 | require.Equal(t, "test-cert-no-validation", resp[0].ServiceCreds["rbuilder"].TLSCert) 225 | require.Equal(t, "0x1234567890123456789012345678901234567890", resp[0].ServiceCreds["rbuilder"].ECDSAPubkey.String()) 226 | }) 227 | } 228 | 229 | // createBuilder emulates admin flow to provision a builder 230 | func createBuilder(t *testing.T, s *Server, builderName, ip, network string) { 231 | builder := ports.Builder{ 232 | Name: builderName, 233 | IPAddress: ip, 234 | Network: network, 235 | } 236 | t.Run("CreateBuilder", func(t *testing.T) { 237 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/builders", builder, nil) 238 | require.Equal(t, http.StatusOK, sc) 239 | }) 240 | 241 | t.Run("CreateBuilderConfiguration", func(t *testing.T) { 242 | builderConf := map[string]string{} 243 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/builders/configuration/"+builderName, builderConf, nil) 244 | require.Equal(t, http.StatusOK, sc) 245 | }) 246 | t.Run("SetSecrets", func(t *testing.T) { 247 | rawJson := json.RawMessage(fmt.Sprintf(`{"test_key_1": "%s_test_value_1", "test_secret_1": "%s_test_secret_value"}`, builderName, builderName)) 248 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/builders/secrets/"+builderName, rawJson, nil) 249 | require.Equal(t, http.StatusOK, sc) 250 | }) 251 | 252 | t.Run("ActivateBuilder", func(t *testing.T) { 253 | activate := ports.ActivationRequest{Enabled: true} 254 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/builders/activation/"+builderName, activate, nil) 255 | require.Equal(t, http.StatusOK, sc) 256 | }) 257 | 258 | t.Run("CheckBuilder", func(t *testing.T) { 259 | // Check if the builder is created 260 | var resp []ports.BuilderWithServiceCreds 261 | sc, _ := execRequestNoAuth(t, s.GetInternalRouter(), http.MethodGet, fmt.Sprintf("/api/internal/l1-builder/v2/network/%s/builders", network), nil, &resp) 262 | require.Equal(t, http.StatusOK, sc) 263 | // require.Len(t, resp, 1) 264 | for _, b := range resp { 265 | if b.Name == builderName { 266 | require.Equal(t, ip, b.IP) 267 | return 268 | } 269 | } 270 | require.Fail(t, "Builder not found") 271 | }) 272 | t.Run("CheckBuilderConfiguration", func(t *testing.T) { 273 | // Check if the builder configuration is created 274 | var resp map[string]string 275 | url := fmt.Sprintf("/api/admin/v1/builders/configuration/%s/full", builderName) 276 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodGet, url, nil, &resp) 277 | require.Equal(t, http.StatusOK, sc) 278 | require.Equal(t, builderName+"_test_value_1", resp["test_key_1"]) 279 | require.Equal(t, builderName+"_test_secret_value", resp["test_secret_1"]) 280 | }) 281 | } 282 | 283 | func createMeasurement(t *testing.T, s *Server, measurement ports.Measurement) { 284 | t.Run("CreateMeasurement", func(t *testing.T) { 285 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/measurements", measurement, nil) 286 | require.Equal(t, http.StatusOK, sc) 287 | }) 288 | t.Run("ActivateMeasurement", func(t *testing.T) { 289 | activate := ports.ActivationRequest{Enabled: true} 290 | sc, _ := execRequestNoAuth(t, s.GetAdminRouter(), http.MethodPost, "/api/admin/v1/measurements/activation/"+measurement.Name, activate, nil) 291 | require.Equal(t, http.StatusOK, sc) 292 | }) 293 | t.Run("CheckMeasurement", func(t *testing.T) { 294 | // Check if the measurement is created 295 | var resp []ports.Measurement 296 | sc, _ := execRequestNoAuth(t, s.GetInternalRouter(), http.MethodGet, "/api/l1-builder/v1/measurements", nil, &resp) 297 | require.Equal(t, http.StatusOK, sc) 298 | for _, m := range resp { 299 | if m.Name == measurement.Name { 300 | require.Equal(t, measurement.Name, m.Name) 301 | return 302 | } 303 | } 304 | require.Fail(t, "Measurement not found") 305 | }) 306 | } 307 | 308 | func createDbService(t *testing.T) *database.Service { 309 | t.Helper() 310 | dbService, err := database.NewDatabaseService("postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable") 311 | if err != nil { 312 | t.Errorf("NewDatabaseService() = %v; want nil", err) 313 | } 314 | _, err = dbService.DB.Exec("TRUNCATE TABLE public.builders CASCADE") 315 | require.NoError(t, err) 316 | _, err = dbService.DB.Exec("TRUNCATE TABLE public.measurements_whitelist CASCADE") 317 | require.NoError(t, err) 318 | return dbService 319 | } 320 | 321 | func createServer(t *testing.T) (*Server, *database.Service, *domain.InmemorySecretService) { 322 | t.Helper() 323 | dbService := createDbService(t) 324 | mss := domain.NewMockSecretService() 325 | bhs := application.NewBuilderHub(dbService, mss) 326 | bhh := ports.NewBuilderHubHandler(bhs, getTestLogger()) 327 | _ = bhh 328 | ah := ports.NewAdminHandler(dbService, mss, getTestLogger()) 329 | _ = ah 330 | s, err := NewHTTPServer(&HTTPServerConfig{ 331 | DrainDuration: latency, 332 | ListenAddr: listenAddr, 333 | InternalAddr: internalAddr, 334 | AdminAddr: adminAddr, 335 | Log: getTestLogger(), 336 | }, bhh, ah) 337 | require.NoError(t, err) 338 | return s, dbService, mss 339 | } 340 | 341 | func execRequestNoAuth(t *testing.T, router http.Handler, method, url string, request, response any) (statusCode int, responsePayload []byte) { 342 | t.Helper() 343 | return execRequestAuth(t, router, method, url, request, response, "", nil, "") 344 | } 345 | 346 | func execRequestAuth(t *testing.T, router http.Handler, method, url string, request, response any, attestationType string, measurement map[string]string, ip string) (statusCode int, responsePayload []byte) { 347 | t.Helper() 348 | rBody, err := json.Marshal(request) 349 | require.NoError(t, err) 350 | tr := httptest.NewRequest(method, url, bytes.NewBuffer(rBody)) 351 | require.NoError(t, err) 352 | if attestationType != "" { 353 | tr.Header.Set(ports.AttestationTypeHeader, attestationType) 354 | } 355 | if len(measurement) > 0 { 356 | measB, err := json.Marshal(measurement) 357 | require.NoError(t, err) 358 | tr.Header.Set(ports.MeasurementHeader, string(measB)) 359 | } 360 | if ip != "" { 361 | tr.Header.Set(ports.ForwardedHeader, ip) 362 | } 363 | 364 | rr := httptest.NewRecorder() 365 | router.ServeHTTP(rr, tr) 366 | 367 | responseBody, err := io.ReadAll(rr.Body) 368 | require.NoError(t, err) 369 | 370 | if response != nil && rr.Code >= 200 && rr.Code < 300 { 371 | err := json.Unmarshal(responseBody, response) 372 | require.NoErrorf(t, err, string(responseBody)) 373 | } 374 | return rr.Code, responseBody 375 | } 376 | -------------------------------------------------------------------------------- /httpserver/handler.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | func (srv *Server) handleLivenessCheck(w http.ResponseWriter, r *http.Request) { 9 | w.WriteHeader(http.StatusOK) 10 | } 11 | 12 | func (srv *Server) handleReadinessCheck(w http.ResponseWriter, r *http.Request) { 13 | if !srv.isReady.Load() { 14 | w.WriteHeader(http.StatusServiceUnavailable) 15 | return 16 | } 17 | 18 | w.WriteHeader(http.StatusOK) 19 | } 20 | 21 | func (srv *Server) handleDrain(w http.ResponseWriter, r *http.Request) { 22 | if wasReady := srv.isReady.Swap(false); !wasReady { 23 | return 24 | } 25 | // l := logutils.ZapFromRequest(r) 26 | srv.log.Info("Server marked as not ready") 27 | time.Sleep(srv.cfg.DrainDuration) // Give LB enough time to detect us not ready 28 | } 29 | 30 | func (srv *Server) handleUndrain(w http.ResponseWriter, r *http.Request) { 31 | if wasReady := srv.isReady.Swap(true); wasReady { 32 | return 33 | } 34 | // l := logutils.ZapFromRequest(r) 35 | srv.log.Info("Server marked as ready") 36 | } 37 | -------------------------------------------------------------------------------- /httpserver/handler_test.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/flashbots/builder-hub/common" 11 | "github.com/flashbots/builder-hub/ports" 12 | "github.com/go-chi/httplog/v2" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const ( 17 | latency = 200 * time.Millisecond 18 | listenAddr = ":8080" 19 | internalAddr = ":8081" 20 | adminAddr = ":8082" 21 | ) 22 | 23 | var testServerConfig = &HTTPServerConfig{ 24 | Log: getTestLogger(), 25 | } 26 | var _ = testServerConfig 27 | 28 | func getTestLogger() *httplog.Logger { 29 | return common.SetupLogger(&common.LoggingOpts{ 30 | Debug: true, 31 | JSON: false, 32 | Service: "test", 33 | }) 34 | } 35 | 36 | func Test_Handlers_Healthcheck_Drain_Undrain(t *testing.T) { 37 | //nolint: exhaustruct 38 | s, err := NewHTTPServer(&HTTPServerConfig{ 39 | DrainDuration: latency, 40 | ListenAddr: listenAddr, 41 | InternalAddr: internalAddr, 42 | AdminAddr: adminAddr, 43 | Log: getTestLogger(), 44 | }, ports.NewBuilderHubHandler(nil, getTestLogger()), ports.NewAdminHandler(nil, nil, getTestLogger())) 45 | require.NoError(t, err) 46 | 47 | { // Check health 48 | req := httptest.NewRequest(http.MethodGet, "http://localhost"+listenAddr+"/readyz", nil) //nolint:goconst,nolintlint 49 | w := httptest.NewRecorder() 50 | s.handleReadinessCheck(w, req) 51 | resp := w.Result() 52 | defer resp.Body.Close() 53 | _, err := io.ReadAll(resp.Body) 54 | require.NoError(t, err) 55 | require.Equal(t, http.StatusOK, resp.StatusCode, "Healthcheck must return `Ok` before draining") 56 | } 57 | 58 | { // Drain 59 | req := httptest.NewRequest(http.MethodGet, "http://localhost"+listenAddr+"/drain", nil) 60 | w := httptest.NewRecorder() 61 | start := time.Now() 62 | s.handleDrain(w, req) 63 | duration := time.Since(start) 64 | resp := w.Result() 65 | defer resp.Body.Close() 66 | _, err := io.ReadAll(resp.Body) 67 | require.NoError(t, err) 68 | require.Equal(t, http.StatusOK, resp.StatusCode, "Must return `Ok` for calls to `/drain`") 69 | require.GreaterOrEqual(t, duration, latency, "Must wait long enough during draining") 70 | } 71 | 72 | { // Check health 73 | req := httptest.NewRequest(http.MethodGet, "http://localhost"+listenAddr+"/readyz", nil) 74 | w := httptest.NewRecorder() 75 | s.handleReadinessCheck(w, req) 76 | resp := w.Result() 77 | defer resp.Body.Close() 78 | _, err := io.ReadAll(resp.Body) 79 | require.NoError(t, err) 80 | require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode, "Healthcheck must return `Service Unavailable` after draining") 81 | } 82 | 83 | { // Undrain 84 | req := httptest.NewRequest(http.MethodGet, "http://localhost"+listenAddr+"/undrain", nil) 85 | w := httptest.NewRecorder() 86 | s.handleUndrain(w, req) 87 | resp := w.Result() 88 | defer resp.Body.Close() 89 | _, err := io.ReadAll(resp.Body) 90 | require.NoError(t, err) 91 | require.Equal(t, http.StatusOK, resp.StatusCode, "Must return `Ok` for calls to `/undrain`") 92 | time.Sleep(latency) 93 | } 94 | 95 | { // Check health 96 | req := httptest.NewRequest(http.MethodGet, "http://localhost"+listenAddr+"/readyz", nil) 97 | w := httptest.NewRecorder() 98 | s.handleReadinessCheck(w, req) 99 | resp := w.Result() 100 | defer resp.Body.Close() 101 | _, err := io.ReadAll(resp.Body) 102 | require.NoError(t, err) 103 | require.Equal(t, http.StatusOK, resp.StatusCode, "Healthcheck must return `Ok` after undraining") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /httpserver/server.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/flashbots/builder-hub/metrics" 11 | "github.com/flashbots/builder-hub/ports" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/chi/v5/middleware" 14 | "github.com/go-chi/httplog/v2" 15 | "go.uber.org/atomic" 16 | ) 17 | 18 | type HTTPServerConfig struct { 19 | ListenAddr string 20 | MetricsAddr string 21 | AdminAddr string 22 | InternalAddr string 23 | EnablePprof bool 24 | Log *httplog.Logger 25 | 26 | DrainDuration time.Duration 27 | GracefulShutdownDuration time.Duration 28 | ReadTimeout time.Duration 29 | WriteTimeout time.Duration 30 | } 31 | 32 | type Server struct { 33 | cfg *HTTPServerConfig 34 | isReady atomic.Bool 35 | log *httplog.Logger 36 | appHandler *ports.BuilderHubHandler 37 | adminHandler *ports.AdminHandler 38 | 39 | srv *http.Server 40 | adminSrv *http.Server 41 | internalSrv *http.Server 42 | metricsSrv *metrics.MetricsServer 43 | 44 | mockGetConfigResponse string 45 | mockGetBuildersResponse string 46 | mockGetMeasurementsResponse string 47 | } 48 | 49 | func NewHTTPServer(cfg *HTTPServerConfig, appHandler *ports.BuilderHubHandler, adminHandler *ports.AdminHandler) (srv *Server, err error) { 50 | srv = &Server{ 51 | cfg: cfg, 52 | log: cfg.Log, 53 | appHandler: appHandler, 54 | adminHandler: adminHandler, 55 | srv: nil, 56 | metricsSrv: metrics.NewMetricsServer(cfg.MetricsAddr, nil), 57 | } 58 | srv.isReady.Swap(true) 59 | 60 | srv.srv = &http.Server{ 61 | Addr: cfg.ListenAddr, 62 | Handler: srv.GetRouter(), 63 | ReadTimeout: cfg.ReadTimeout, 64 | WriteTimeout: cfg.WriteTimeout, 65 | } 66 | srv.internalSrv = &http.Server{ 67 | Addr: cfg.InternalAddr, 68 | Handler: srv.GetInternalRouter(), 69 | ReadTimeout: cfg.ReadTimeout, 70 | WriteTimeout: cfg.WriteTimeout, 71 | } 72 | srv.adminSrv = &http.Server{ 73 | Addr: cfg.AdminAddr, 74 | Handler: srv.GetAdminRouter(), 75 | ReadTimeout: cfg.ReadTimeout, 76 | WriteTimeout: cfg.WriteTimeout, 77 | } 78 | 79 | return srv, nil 80 | } 81 | 82 | func (srv *Server) GetRouter() http.Handler { 83 | mux := chi.NewRouter() 84 | 85 | mux.Use(httplog.RequestLogger(srv.log)) 86 | mux.Use(middleware.Recoverer) 87 | mux.Use(metrics.Middleware) 88 | 89 | // System API 90 | mux.Get("/livez", srv.handleLivenessCheck) 91 | mux.Get("/readyz", srv.handleReadinessCheck) 92 | mux.Get("/drain", srv.handleDrain) 93 | mux.Get("/undrain", srv.handleUndrain) 94 | 95 | mux.Get("/api/l1-builder/v1/measurements", srv.appHandler.GetAllowedMeasurements) 96 | mux.Get("/api/l1-builder/v1/configuration", srv.appHandler.GetConfigSecrets) 97 | mux.Get("/api/l1-builder/v1/builders", srv.appHandler.GetActiveBuilders) 98 | mux.Post("/api/l1-builder/v1/register_credentials/{service}", srv.appHandler.RegisterCredentials) 99 | mux.Get("/api/internal/l1-builder/v1/builders", srv.appHandler.GetActiveBuildersNoAuth) 100 | mux.Get("/api/internal/l1-builder/v2/network/{network}/builders", srv.appHandler.GetActiveBuildersNoAuthNetworked) 101 | if srv.cfg.EnablePprof { 102 | srv.log.Info("pprof API enabled") 103 | mux.Mount("/debug", middleware.Profiler()) 104 | } 105 | 106 | return mux 107 | } 108 | 109 | func (srv *Server) GetAdminRouter() http.Handler { 110 | mux := chi.NewRouter() 111 | 112 | mux.Use(httplog.RequestLogger(srv.log)) 113 | mux.Use(middleware.Recoverer) 114 | mux.Use(metrics.Middleware) 115 | 116 | mux.Get("/api/admin/v1/builders/configuration/{builderName}/active", srv.adminHandler.GetActiveConfigForBuilder) 117 | mux.Get("/api/admin/v1/builders/configuration/{builderName}/full", srv.adminHandler.GetFullConfigForBuilder) 118 | mux.Post("/api/admin/v1/measurements", srv.adminHandler.AddMeasurement) 119 | mux.Post("/api/admin/v1/builders", srv.adminHandler.AddBuilder) 120 | mux.Post("/api/admin/v1/builders/activation/{builderName}", srv.adminHandler.ChangeActiveStatusForBuilder) 121 | mux.Post("/api/admin/v1/measurements/activation/{measurementName}", srv.adminHandler.ChangeActiveStatusForMeasurement) 122 | mux.Post("/api/admin/v1/builders/configuration/{builderName}", srv.adminHandler.AddBuilderConfig) 123 | mux.Post("/api/admin/v1/builders/secrets/{builderName}", srv.adminHandler.SetSecrets) 124 | 125 | return mux 126 | } 127 | 128 | func (srv *Server) GetInternalRouter() http.Handler { 129 | mux := chi.NewRouter() 130 | 131 | mux.Use(httplog.RequestLogger(srv.log)) 132 | mux.Use(middleware.Recoverer) 133 | mux.Use(metrics.Middleware) 134 | 135 | mux.Get("/api/l1-builder/v1/measurements", srv.appHandler.GetAllowedMeasurements) 136 | mux.Get("/api/internal/l1-builder/v1/builders", srv.appHandler.GetActiveBuildersNoAuth) 137 | mux.Get("/api/internal/l1-builder/v2/network/{network}/builders", srv.appHandler.GetActiveBuildersNoAuthNetworked) 138 | 139 | return mux 140 | } 141 | 142 | func (srv *Server) _stringFromFile(fn string) (string, error) { 143 | content, err := os.ReadFile(fn) 144 | if err != nil { 145 | srv.log.Error("Failed to read mock response", "file", fn, "err", err) 146 | return "", err 147 | } 148 | return string(content), nil 149 | } 150 | 151 | func (srv *Server) LoadMockResponses() (err error) { 152 | srv.mockGetConfigResponse, err = srv._stringFromFile("testdata/get-configuration.json") 153 | if err != nil { 154 | return err 155 | } 156 | srv.mockGetBuildersResponse, err = srv._stringFromFile("testdata/get-builders.json") 157 | if err != nil { 158 | return err 159 | } 160 | srv.mockGetMeasurementsResponse, err = srv._stringFromFile("testdata/get-measurements.json") 161 | return err 162 | } 163 | 164 | func (srv *Server) RunInBackground() { 165 | // metrics 166 | if srv.cfg.MetricsAddr != "" { 167 | go func() { 168 | srv.log.With("metricsAddress", srv.cfg.MetricsAddr).Info("Starting metrics server") 169 | err := srv.metricsSrv.Start() 170 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 171 | srv.log.Error("HTTP server failed", "err", err) 172 | } 173 | }() 174 | } 175 | 176 | // api 177 | go func() { 178 | srv.log.Info("Starting HTTP server", "listenAddress", srv.cfg.ListenAddr) 179 | if err := srv.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 180 | srv.log.Error("HTTP server failed", "err", err) 181 | } 182 | }() 183 | go func() { 184 | srv.log.Info("Starting internal HTTP server", "listenAddress", srv.cfg.InternalAddr) 185 | if err := srv.internalSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 186 | srv.log.Error("Internal HTTP server failed", "err", err) 187 | } 188 | }() 189 | go func() { 190 | srv.log.Info("Starting admin HTTP server", "listenAddress", srv.cfg.AdminAddr) 191 | if err := srv.adminSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 192 | srv.log.Error("Admin HTTP server failed", "err", err) 193 | } 194 | }() 195 | } 196 | 197 | func (srv *Server) Shutdown() { 198 | // api 199 | ctx, cancel := context.WithTimeout(context.Background(), srv.cfg.GracefulShutdownDuration) 200 | defer cancel() 201 | if err := srv.srv.Shutdown(ctx); err != nil { 202 | srv.log.Error("Graceful HTTP server shutdown failed", "err", err) 203 | } else { 204 | srv.log.Info("HTTP server gracefully stopped") 205 | } 206 | 207 | if err := srv.internalSrv.Shutdown(ctx); err != nil { 208 | srv.log.Error("Graceful HTTP server shutdown failed", "err", err) 209 | } else { 210 | srv.log.Info("HTTP server gracefully stopped") 211 | } 212 | 213 | if err := srv.adminSrv.Shutdown(ctx); err != nil { 214 | srv.log.Error("Graceful HTTP server shutdown failed", "err", err) 215 | } else { 216 | srv.log.Info("HTTP server gracefully stopped") 217 | } 218 | 219 | // metrics 220 | if len(srv.cfg.MetricsAddr) != 0 { 221 | ctx, cancel := context.WithTimeout(context.Background(), srv.cfg.GracefulShutdownDuration) 222 | defer cancel() 223 | 224 | if err := srv.metricsSrv.Shutdown(ctx); err != nil { 225 | srv.log.Error("Graceful metrics server shutdown failed", "err", err) 226 | } else { 227 | srv.log.Info("Metrics server gracefully stopped") 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /httpserver/vars.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Package metrics contains all application-logic metrics 2 | package metrics 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/VictoriaMetrics/metrics" 8 | ) 9 | 10 | const requestDurationLabel = `http_server_request_duration_milliseconds{route="%s"}` 11 | 12 | func recordRequestDuration(route string, duration int64) { 13 | l := fmt.Sprintf(requestDurationLabel, route) 14 | metrics.GetOrCreateSummary(l).Update(float64(duration)) 15 | } 16 | -------------------------------------------------------------------------------- /metrics/middleware.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/go-chi/chi/v5" 8 | ) 9 | 10 | func Middleware(next http.Handler) http.Handler { 11 | fn := func(w http.ResponseWriter, r *http.Request) { 12 | startAt := time.Now() 13 | defer func() { 14 | routePattern := chi.RouteContext(r.Context()).RoutePattern() 15 | recordRequestDuration(routePattern, time.Since(startAt).Milliseconds()) 16 | }() 17 | 18 | next.ServeHTTP(w, r) 19 | } 20 | return http.HandlerFunc(fn) 21 | } 22 | -------------------------------------------------------------------------------- /metrics/server.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/VictoriaMetrics/metrics" 10 | chi "github.com/go-chi/chi/v5" 11 | "github.com/go-chi/chi/v5/middleware" 12 | "github.com/go-chi/httplog/v2" 13 | ) 14 | 15 | type MetricsServer struct { 16 | listenAddr string 17 | log *httplog.Logger 18 | srv *http.Server 19 | } 20 | 21 | func NewMetricsServer(listenAddr string, log *httplog.Logger) *MetricsServer { 22 | server := &MetricsServer{ 23 | listenAddr: listenAddr, 24 | log: log, 25 | } 26 | return server 27 | } 28 | 29 | func (srv *MetricsServer) Start() error { 30 | srv.srv = &http.Server{ 31 | Addr: srv.listenAddr, 32 | Handler: srv.getRouter(), 33 | ReadHeaderTimeout: 5 * time.Second, 34 | } 35 | 36 | if err := srv.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | func (srv *MetricsServer) getRouter() http.Handler { 43 | mux := chi.NewRouter() 44 | 45 | if srv.log != nil { 46 | mux.Use(httplog.RequestLogger(srv.log)) 47 | } 48 | 49 | mux.Use(middleware.Recoverer) 50 | 51 | mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { 52 | metrics.WritePrometheus(w, true) 53 | }) 54 | 55 | return mux 56 | } 57 | 58 | func (srv *MetricsServer) Shutdown(ctx context.Context) error { 59 | return srv.srv.Shutdown(ctx) 60 | } 61 | -------------------------------------------------------------------------------- /ports/admin_handler.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | 10 | "github.com/flashbots/builder-hub/application" 11 | "github.com/flashbots/builder-hub/domain" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/go-chi/httplog/v2" 14 | ) 15 | 16 | type AdminBuilderService interface { 17 | GetActiveConfigForBuilder(ctx context.Context, builderName string) (json.RawMessage, error) 18 | AddMeasurement(ctx context.Context, measurement domain.Measurement, enabled bool) error 19 | AddBuilder(ctx context.Context, builder domain.Builder) error 20 | ChangeActiveStatusForBuilder(ctx context.Context, builderName string, isActive bool) error 21 | ChangeActiveStatusForMeasurement(ctx context.Context, measurementName string, isActive bool) error 22 | AddBuilderConfig(ctx context.Context, builderName string, config json.RawMessage) error 23 | } 24 | 25 | type AdminSecretService interface { 26 | SetSecretValues(builderName string, message json.RawMessage) error 27 | application.SecretAccessor 28 | } 29 | 30 | type AdminHandler struct { 31 | builderService AdminBuilderService 32 | secretService AdminSecretService 33 | log *httplog.Logger 34 | } 35 | 36 | func NewAdminHandler(service AdminBuilderService, secretService AdminSecretService, log *httplog.Logger) *AdminHandler { 37 | return &AdminHandler{builderService: service, secretService: secretService, log: log} 38 | } 39 | 40 | func (s *AdminHandler) GetActiveConfigForBuilder(w http.ResponseWriter, r *http.Request) { 41 | builderName := chi.URLParam(r, "builderName") 42 | bts, err := s.builderService.GetActiveConfigForBuilder(r.Context(), builderName) 43 | if errors.Is(err, domain.ErrNotFound) { 44 | s.log.Warn("no active config for builder found", "error", err) 45 | w.WriteHeader(http.StatusNotFound) 46 | return 47 | } 48 | if err != nil { 49 | s.log.Error("failed to fetch active config for builder", "error", err) 50 | w.WriteHeader(http.StatusInternalServerError) 51 | return 52 | } 53 | _, err = w.Write(bts) 54 | if err != nil { 55 | s.log.Error("failed to write response", "error", err) 56 | } 57 | } 58 | 59 | // GetFullConfigForBuilder returns the full config for a builder, including secrets 60 | // Note this copies logic from GetConfigWithSecrets in BuilderHubService 61 | // since we decided to avoid application layer here it probably makes sense unless 62 | // logic gets more complicated here 63 | func (s *AdminHandler) GetFullConfigForBuilder(w http.ResponseWriter, r *http.Request) { 64 | builderName := chi.URLParam(r, "builderName") 65 | _, err := s.builderService.GetActiveConfigForBuilder(r.Context(), builderName) 66 | if err != nil { 67 | s.log.Error("failed to get config with secrets", "error", err) 68 | w.WriteHeader(http.StatusInternalServerError) 69 | return 70 | } 71 | secr, err := s.secretService.GetSecretValues(builderName) 72 | if err != nil { 73 | s.log.Error("failed to get secrets", "error", err) 74 | w.WriteHeader(http.StatusInternalServerError) 75 | return 76 | } 77 | 78 | _, err = w.Write(secr) 79 | if err != nil { 80 | s.log.Error("failed to write response", "error", err) 81 | } 82 | } 83 | 84 | func (s *AdminHandler) AddMeasurement(w http.ResponseWriter, r *http.Request) { 85 | // read request body 86 | body, err := io.ReadAll(r.Body) 87 | if err != nil { 88 | s.log.Error("Failed to read request body", "err", err) 89 | w.WriteHeader(http.StatusInternalServerError) 90 | return 91 | } 92 | measurement := Measurement{} 93 | err = json.Unmarshal(body, &measurement) 94 | if err != nil { 95 | s.log.Error("Failed to unmarshal request body", "err", err) 96 | w.WriteHeader(http.StatusBadRequest) 97 | return 98 | } 99 | err = s.builderService.AddMeasurement(r.Context(), toDomainMeasurement(measurement), false) 100 | if err != nil { 101 | s.log.Error("failed to add measurement", "error", err) 102 | w.WriteHeader(http.StatusInternalServerError) 103 | return 104 | } 105 | } 106 | 107 | func (s *AdminHandler) AddBuilder(w http.ResponseWriter, r *http.Request) { 108 | // read request body 109 | body, err := io.ReadAll(r.Body) 110 | if err != nil { 111 | s.log.Error("Failed to read request body", "err", err) 112 | w.WriteHeader(http.StatusInternalServerError) 113 | return 114 | } 115 | builder := Builder{} 116 | err = json.Unmarshal(body, &builder) 117 | if err != nil { 118 | s.log.Error("Failed to unmarshal request body", "err", err) 119 | w.WriteHeader(http.StatusBadRequest) 120 | return 121 | } 122 | if builder.Network == "" { 123 | s.log.Error("network field is required") 124 | w.WriteHeader(http.StatusBadRequest) 125 | _, _ = w.Write([]byte("network field is required")) 126 | return 127 | } 128 | dBuilder, err := toDomainBuilder(builder, false) 129 | if err != nil { 130 | s.log.Error("Failed to convert builder to domain builder", "err", err) 131 | w.WriteHeader(http.StatusBadRequest) 132 | return 133 | } 134 | err = s.builderService.AddBuilder(r.Context(), dBuilder) 135 | if err != nil { 136 | s.log.Error("failed to add builder", "error", err) 137 | w.WriteHeader(http.StatusInternalServerError) 138 | return 139 | } 140 | } 141 | 142 | type ActivationRequest struct { 143 | Enabled bool `json:"enabled"` 144 | } 145 | 146 | func (s *AdminHandler) ChangeActiveStatusForBuilder(w http.ResponseWriter, r *http.Request) { 147 | builderName := chi.URLParam(r, "builderName") 148 | activationRequest := ActivationRequest{} 149 | err := json.NewDecoder(r.Body).Decode(&activationRequest) 150 | if err != nil { 151 | s.log.Error("failed to decode request body", "error", err) 152 | w.WriteHeader(http.StatusBadRequest) 153 | return 154 | } 155 | 156 | // we only ensure existence of active config for `activation` request 157 | // `deactivation` request must pass through for an ease of deactivation incorrect/rouge deployments 158 | if activationRequest.Enabled { 159 | _, err = s.builderService.GetActiveConfigForBuilder(r.Context(), builderName) 160 | if errors.Is(err, domain.ErrNotFound) { 161 | s.log.Warn("no active config for builder found", "error", err) 162 | w.WriteHeader(http.StatusNotFound) 163 | return 164 | } 165 | if err != nil { 166 | s.log.Error("failed to fetch active config for builder", "error", err) 167 | w.WriteHeader(http.StatusInternalServerError) 168 | return 169 | } 170 | } 171 | 172 | err = s.builderService.ChangeActiveStatusForBuilder(r.Context(), builderName, activationRequest.Enabled) 173 | if err != nil { 174 | s.log.Error("failed to change active status for builder", "error", err) 175 | w.WriteHeader(http.StatusInternalServerError) 176 | return 177 | } 178 | } 179 | 180 | func (s *AdminHandler) ChangeActiveStatusForMeasurement(w http.ResponseWriter, r *http.Request) { 181 | measurementName := chi.URLParam(r, "measurementName") 182 | activationRequest := ActivationRequest{} 183 | err := json.NewDecoder(r.Body).Decode(&activationRequest) 184 | if err != nil { 185 | s.log.Error("failed to decode request body", "error", err) 186 | w.WriteHeader(http.StatusBadRequest) 187 | return 188 | } 189 | err = s.builderService.ChangeActiveStatusForMeasurement(r.Context(), measurementName, activationRequest.Enabled) 190 | if err != nil { 191 | s.log.Error("failed to change active status for measurement", "error", err) 192 | w.WriteHeader(http.StatusInternalServerError) 193 | return 194 | } 195 | } 196 | 197 | func (s *AdminHandler) AddBuilderConfig(w http.ResponseWriter, r *http.Request) { 198 | builderName := chi.URLParam(r, "builderName") 199 | // read request body 200 | body, err := io.ReadAll(r.Body) 201 | if err != nil { 202 | s.log.Error("Failed to read request body", "err", err) 203 | w.WriteHeader(http.StatusInternalServerError) 204 | return 205 | } 206 | if !json.Valid(body) { 207 | s.log.Error("Invalid json", "err", err) 208 | w.WriteHeader(http.StatusBadRequest) 209 | return 210 | } 211 | 212 | // validate valid json 213 | err = s.builderService.AddBuilderConfig(r.Context(), builderName, body) 214 | if err != nil { 215 | s.log.Error("failed to add builder config", "error", err) 216 | w.WriteHeader(http.StatusInternalServerError) 217 | return 218 | } 219 | } 220 | 221 | func (s *AdminHandler) SetSecrets(w http.ResponseWriter, r *http.Request) { 222 | builderName := chi.URLParam(r, "builderName") 223 | 224 | body, err := io.ReadAll(r.Body) 225 | if err != nil { 226 | s.log.Error("Failed to read request body", "err", err) 227 | w.WriteHeader(http.StatusInternalServerError) 228 | return 229 | } 230 | 231 | err = s.secretService.SetSecretValues(builderName, body) 232 | if err != nil { 233 | s.log.Error("failed to set secret", "error", err) 234 | w.WriteHeader(http.StatusInternalServerError) 235 | return 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /ports/http_handler.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/flashbots/builder-hub/domain" 14 | "github.com/go-chi/chi/v5" 15 | "github.com/go-chi/httplog/v2" 16 | ) 17 | 18 | type BuilderHubService interface { 19 | GetAllowedMeasurements(ctx context.Context) ([]domain.Measurement, error) 20 | GetActiveBuilders(ctx context.Context, network string) ([]domain.BuilderWithServices, error) 21 | VerifyIPAndMeasurements(ctx context.Context, ip net.IP, measurement map[string]string, attestationType string) (*domain.Builder, string, error) 22 | GetConfigWithSecrets(ctx context.Context, builderName string) ([]byte, error) 23 | RegisterCredentialsForBuilder(ctx context.Context, builderName, service, tlsCert string, ecdsaPubKey []byte, measurementName, attestationType string) error 24 | LogEvent(ctx context.Context, eventName, builderName, name string) error 25 | } 26 | type BuilderHubHandler struct { 27 | builderHubService BuilderHubService 28 | log *httplog.Logger 29 | } 30 | 31 | func NewBuilderHubHandler(builderHubService BuilderHubService, log *httplog.Logger) *BuilderHubHandler { 32 | return &BuilderHubHandler{builderHubService: builderHubService, log: log} 33 | } 34 | 35 | type AuthData struct { 36 | AttestationType string 37 | MeasurementData map[string]string 38 | IP net.IP 39 | } 40 | 41 | func (bhs *BuilderHubHandler) getAuthData(r *http.Request) (*AuthData, error) { 42 | attestationType := r.Header.Get(AttestationTypeHeader) 43 | if attestationType == "" { 44 | return nil, fmt.Errorf("attestation type is empty %w", ErrInvalidAuthData) 45 | } 46 | measurementHeader := r.Header.Get(MeasurementHeader) 47 | measurementData := make(map[string]string) 48 | err := json.Unmarshal([]byte(measurementHeader), &measurementData) 49 | if err != nil { 50 | return nil, fmt.Errorf("failed to unmarshal measurement header %w", ErrInvalidAuthData) 51 | } 52 | ipHeaders := r.Header.Values(ForwardedHeader) 53 | if len(ipHeaders) == 0 { 54 | return nil, fmt.Errorf("ip header is empty %w", ErrInvalidAuthData) 55 | } 56 | // NOTE: we need this quite awkward logic since header comes not in the canonical format, with space. 57 | ipHeader := ipHeaders[len(ipHeaders)-1] 58 | ipHeaders = strings.Split(ipHeader, ",") 59 | ipHeader = ipHeaders[len(ipHeaders)-1] 60 | ipHeader = strings.TrimSpace(ipHeader) 61 | 62 | ip := net.ParseIP(ipHeader) 63 | if ip == nil { 64 | return nil, fmt.Errorf("failed to parse ip %s %w", ipHeader, ErrInvalidAuthData) 65 | } 66 | 67 | return &AuthData{ 68 | AttestationType: attestationType, 69 | MeasurementData: measurementData, 70 | IP: ip, 71 | }, nil 72 | } 73 | 74 | func (bhs *BuilderHubHandler) GetAllowedMeasurements(w http.ResponseWriter, r *http.Request) { 75 | _, err := io.ReadAll(r.Body) 76 | if err != nil { 77 | bhs.log.Error("failed to read request body", "error", err) 78 | w.WriteHeader(http.StatusInternalServerError) 79 | return 80 | } 81 | measurements, err := bhs.builderHubService.GetAllowedMeasurements(r.Context()) 82 | if err != nil { 83 | bhs.log.Error("failed to fetch allowed measurements from db", "error", err) 84 | w.WriteHeader(http.StatusInternalServerError) 85 | return 86 | } 87 | var pMeasurements []Measurement 88 | for _, m := range measurements { 89 | pMeasurements = append(pMeasurements, fromDomainMeasurement(m)) 90 | } 91 | 92 | btsM, err := json.Marshal(pMeasurements) 93 | if err != nil { 94 | bhs.log.Error("failed to marshal measurements", "error", err) 95 | w.WriteHeader(http.StatusInternalServerError) 96 | return 97 | } 98 | _, err = w.Write(btsM) 99 | if err != nil { 100 | bhs.log.Error("failed to write response", "error", err) 101 | } 102 | } 103 | 104 | func (bhs *BuilderHubHandler) GetActiveBuilders(w http.ResponseWriter, r *http.Request) { 105 | authData, err := bhs.getAuthData(r) 106 | if err != nil { 107 | bhs.log.Warn("malformed auth data", "error", err) 108 | w.WriteHeader(http.StatusForbidden) 109 | return 110 | } 111 | builder, _, err := bhs.builderHubService.VerifyIPAndMeasurements(r.Context(), authData.IP, authData.MeasurementData, authData.AttestationType) 112 | if errors.Is(err, domain.ErrNotFound) { 113 | bhs.log.Warn("invalid auth data", "error", err) 114 | w.WriteHeader(http.StatusForbidden) 115 | return 116 | } 117 | if err != nil { 118 | bhs.log.Error("failed to verify ip and measurements", "error", err) 119 | w.WriteHeader(http.StatusInternalServerError) 120 | return 121 | } 122 | 123 | builders, err := bhs.builderHubService.GetActiveBuilders(r.Context(), builder.Network) 124 | if err != nil { 125 | bhs.log.Error("failed to fetch active builders from db", "error", err) 126 | w.WriteHeader(http.StatusInternalServerError) 127 | return 128 | } 129 | var pBuilders []BuilderWithServiceCreds 130 | for _, b := range builders { 131 | pBuilders = append(pBuilders, fromDomainBuilderWithServices(b)) 132 | } 133 | bts, err := json.Marshal(pBuilders) 134 | if err != nil { 135 | bhs.log.Error("failed to marshal builders", "error", err) 136 | w.WriteHeader(http.StatusInternalServerError) 137 | return 138 | } 139 | _, err = w.Write(bts) 140 | if err != nil { 141 | bhs.log.Error("failed to write response", "error", err) 142 | } 143 | } 144 | 145 | func (bhs *BuilderHubHandler) GetActiveBuildersNoAuth(w http.ResponseWriter, r *http.Request) { 146 | builders, err := bhs.builderHubService.GetActiveBuilders(r.Context(), domain.ProductionNetwork) 147 | if err != nil { 148 | bhs.log.Error("failed to fetch active builders from db", "error", err) 149 | w.WriteHeader(http.StatusInternalServerError) 150 | return 151 | } 152 | var pBuilders []BuilderWithServiceCreds 153 | for _, b := range builders { 154 | pBuilders = append(pBuilders, fromDomainBuilderWithServices(b)) 155 | } 156 | bts, err := json.Marshal(pBuilders) 157 | if err != nil { 158 | bhs.log.Error("failed to marshal builders", "error", err) 159 | w.WriteHeader(http.StatusInternalServerError) 160 | return 161 | } 162 | _, err = w.Write(bts) 163 | if err != nil { 164 | bhs.log.Error("failed to write response", "error", err) 165 | } 166 | } 167 | 168 | func (bhs *BuilderHubHandler) GetActiveBuildersNoAuthNetworked(w http.ResponseWriter, r *http.Request) { 169 | network := chi.URLParam(r, "network") 170 | if network == "" { 171 | bhs.log.Warn("network is empty") 172 | w.WriteHeader(http.StatusBadRequest) 173 | return 174 | } 175 | 176 | builders, err := bhs.builderHubService.GetActiveBuilders(r.Context(), network) 177 | if err != nil { 178 | bhs.log.Error("failed to fetch active builders from db", "error", err) 179 | w.WriteHeader(http.StatusInternalServerError) 180 | return 181 | } 182 | var pBuilders []BuilderWithServiceCreds 183 | for _, b := range builders { 184 | pBuilders = append(pBuilders, fromDomainBuilderWithServices(b)) 185 | } 186 | bts, err := json.Marshal(pBuilders) 187 | if err != nil { 188 | bhs.log.Error("failed to marshal builders", "error", err) 189 | w.WriteHeader(http.StatusInternalServerError) 190 | return 191 | } 192 | _, err = w.Write(bts) 193 | if err != nil { 194 | bhs.log.Error("failed to write response", "error", err) 195 | } 196 | } 197 | 198 | func (bhs *BuilderHubHandler) GetConfigSecrets(w http.ResponseWriter, r *http.Request) { 199 | authData, err := bhs.getAuthData(r) 200 | if err != nil { 201 | bhs.log.Warn("malformed auth data", "error", err) 202 | w.WriteHeader(http.StatusForbidden) 203 | return 204 | } 205 | builder, measurementName, err := bhs.builderHubService.VerifyIPAndMeasurements(r.Context(), authData.IP, authData.MeasurementData, authData.AttestationType) 206 | if errors.Is(err, domain.ErrNotFound) { 207 | bhs.log.Warn("invalid auth data", "error", err) 208 | w.WriteHeader(http.StatusForbidden) 209 | return 210 | } 211 | bts, err := bhs.builderHubService.GetConfigWithSecrets(r.Context(), builder.Name) 212 | if err != nil { 213 | bhs.log.Error("failed to get config with secrets", "error", err) 214 | w.WriteHeader(http.StatusInternalServerError) 215 | return 216 | } 217 | // add event log 218 | err = bhs.builderHubService.LogEvent(r.Context(), domain.EventGetConfig, builder.Name, measurementName) 219 | if err != nil { 220 | bhs.log.Error("failed to get log event", "error", err) 221 | w.WriteHeader(http.StatusInternalServerError) 222 | return 223 | } 224 | 225 | _, err = w.Write(bts) 226 | if err != nil { 227 | bhs.log.Error("failed to write response", "error", err) 228 | } 229 | } 230 | 231 | func (bhs *BuilderHubHandler) RegisterCredentials(w http.ResponseWriter, r *http.Request) { 232 | authData, err := bhs.getAuthData(r) 233 | if err != nil { 234 | bhs.log.Warn("malformed auth data", "error", err) 235 | w.WriteHeader(http.StatusForbidden) 236 | return 237 | } 238 | builder, measurementName, err := bhs.builderHubService.VerifyIPAndMeasurements(r.Context(), authData.IP, authData.MeasurementData, authData.AttestationType) 239 | if errors.Is(err, domain.ErrNotFound) { 240 | bhs.log.Warn("invalid auth data", "error", err) 241 | w.WriteHeader(http.StatusForbidden) 242 | return 243 | } 244 | if err != nil { 245 | bhs.log.Error("failed to verify ip and measurements", "error", err) 246 | w.WriteHeader(http.StatusInternalServerError) 247 | return 248 | } 249 | 250 | service := chi.URLParam(r, "service") 251 | // TODO: validate service 252 | if service == "" { 253 | bhs.log.Warn("service is empty") 254 | w.WriteHeader(http.StatusBadRequest) 255 | return 256 | } 257 | 258 | // read request body 259 | body, err := io.ReadAll(r.Body) 260 | if err != nil { 261 | bhs.log.Error("Failed to read request body", "err", err) 262 | w.WriteHeader(http.StatusInternalServerError) 263 | return 264 | } 265 | sc := ServiceCred{} 266 | err = json.Unmarshal(body, &sc) 267 | if err != nil { 268 | bhs.log.Error("Failed to unmarshal request body", "err", err) 269 | w.WriteHeader(http.StatusBadRequest) 270 | return 271 | } 272 | if sc.TLSCert == "" && sc.ECDSAPubkey == nil { 273 | bhs.log.Error("No credentials provided") 274 | w.WriteHeader(http.StatusBadRequest) 275 | return 276 | } 277 | 278 | err = bhs.builderHubService.RegisterCredentialsForBuilder(r.Context(), builder.Name, service, sc.TLSCert, sc.ECDSAPubkey.Bytes(), measurementName, authData.AttestationType) 279 | if err != nil { 280 | bhs.log.Error("Failed to register credentials", "err", err) 281 | w.WriteHeader(http.StatusInternalServerError) 282 | return 283 | } 284 | 285 | w.WriteHeader(http.StatusOK) 286 | } 287 | -------------------------------------------------------------------------------- /ports/types.go: -------------------------------------------------------------------------------- 1 | // Package ports contains entry-point related logic for builder-hub. As of now only way to access builder-hub functionality is via http 2 | package ports 3 | 4 | import ( 5 | "encoding/json" 6 | "errors" 7 | "net" 8 | 9 | "github.com/ethereum/go-ethereum/common" 10 | "github.com/flashbots/builder-hub/domain" 11 | ) 12 | 13 | const ( 14 | AttestationTypeHeader string = "X-Flashbots-Attestation-Type" 15 | MeasurementHeader string = "X-Flashbots-Measurement" 16 | ForwardedHeader string = "X-Forwarded-For" 17 | ) 18 | 19 | var ( 20 | ErrInvalidAuthData = errors.New("invalid auth data") 21 | ErrInvalidIPAddress = errors.New("invalid ip address") 22 | ) 23 | 24 | type BuilderWithServiceCreds struct { 25 | IP string 26 | Name string 27 | ServiceCreds map[string]ServiceCred 28 | } 29 | 30 | type ServiceCred struct { 31 | TLSCert string `json:"tls_cert,omitempty"` 32 | ECDSAPubkey *common.Address `json:"ecdsa_pubkey_address,omitempty"` 33 | } 34 | 35 | // MarshalJSON is a custom json marshaller. Unfortunately, there seems to be no way to inline map[string]Service when marshalling 36 | // so we need to be careful when adding new fields, since custom json implementation will ignore it by default 37 | func (b BuilderWithServiceCreds) MarshalJSON() ([]byte, error) { 38 | // Create a map to hold all fields 39 | m := make(map[string]interface{}) 40 | 41 | // Add the IP field 42 | m["ip"] = b.IP 43 | m["name"] = b.Name 44 | 45 | // Add all services 46 | for k, v := range b.ServiceCreds { 47 | m[k] = v 48 | } 49 | 50 | // Marshal the map 51 | return json.Marshal(m) 52 | } 53 | 54 | func (b *BuilderWithServiceCreds) UnmarshalJSON(data []byte) error { 55 | // Define a temporary struct to unmarshal known fields 56 | type Alias struct { 57 | IP string `json:"ip"` 58 | Name string `json:"name"` 59 | } 60 | 61 | // Unmarshal known fields first 62 | var alias Alias 63 | if err := json.Unmarshal(data, &alias); err != nil { 64 | return err 65 | } 66 | 67 | // Copy known fields to the original struct 68 | b.IP = alias.IP 69 | b.Name = alias.Name 70 | 71 | // Unmarshal the remaining fields into a map 72 | var rawMap map[string]json.RawMessage 73 | if err := json.Unmarshal(data, &rawMap); err != nil { 74 | return err 75 | } 76 | 77 | // Remove known fields from the map 78 | delete(rawMap, "ip") 79 | delete(rawMap, "name") 80 | 81 | // Initialize the ServiceCreds map 82 | b.ServiceCreds = make(map[string]ServiceCred) 83 | 84 | // Unmarshal each remaining field into the ServiceCreds map 85 | for key, rawValue := range rawMap { 86 | var serviceCred ServiceCred 87 | if err := json.Unmarshal(rawValue, &serviceCred); err != nil { 88 | return err 89 | } 90 | b.ServiceCreds[key] = serviceCred 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func fromDomainBuilderWithServices(builder domain.BuilderWithServices) BuilderWithServiceCreds { 97 | b := BuilderWithServiceCreds{} 98 | 99 | b.IP = builder.Builder.IPAddress.String() 100 | b.Name = builder.Builder.Name 101 | b.ServiceCreds = make(map[string]ServiceCred) 102 | for _, v := range builder.Services { 103 | b.ServiceCreds[v.Service] = ServiceCred{ 104 | TLSCert: v.TLSCert, 105 | ECDSAPubkey: v.ECDSAPubKey, 106 | } 107 | } 108 | 109 | return b 110 | } 111 | 112 | type Measurement struct { 113 | Name string `json:"measurement_id"` 114 | AttestationType string `json:"attestation_type"` 115 | Measurements map[string]domain.SingleMeasurement `json:"measurements"` 116 | } 117 | 118 | func fromDomainMeasurement(measurement domain.Measurement) Measurement { 119 | m := Measurement{ 120 | Name: measurement.Name, 121 | AttestationType: measurement.AttestationType, 122 | Measurements: measurement.Measurement, 123 | } 124 | return m 125 | } 126 | 127 | func toDomainMeasurement(measurement Measurement) domain.Measurement { 128 | m := domain.NewMeasurement(measurement.Name, measurement.AttestationType, measurement.Measurements) 129 | return *m 130 | } 131 | 132 | type Builder struct { 133 | Name string `json:"name"` 134 | IPAddress string `json:"ip_address"` 135 | Network string `json:"network"` 136 | } 137 | 138 | func toDomainBuilder(builder Builder, enabled bool) (domain.Builder, error) { 139 | ip := net.ParseIP(builder.IPAddress) 140 | if ip == nil { 141 | return domain.Builder{}, ErrInvalidIPAddress 142 | } 143 | 144 | return domain.Builder{ 145 | Name: builder.Name, 146 | IPAddress: ip, 147 | IsActive: enabled, 148 | Network: builder.Network, 149 | }, nil 150 | } 151 | -------------------------------------------------------------------------------- /ports/types_test.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestServiceCreds(t *testing.T) { 9 | str := `{"tls_cert":"something", "ecdsa_pubkey_address":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"}` 10 | var sc ServiceCred 11 | err := json.Unmarshal([]byte(str), &sc) 12 | if err != nil { 13 | t.Error("Failed to unmarshal ServiceCred", err) 14 | } 15 | if sc.TLSCert != "something" { 16 | t.Error("Failed to unmarshal TLS cert") 17 | } 18 | if sc.ECDSAPubkey.Hex() != "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" { 19 | t.Error("Failed to unmarshal ECDSA pubkey") 20 | } 21 | } 22 | 23 | func TestServiceCredsNil(t *testing.T) { 24 | str := `{"tls_cert":"something"}` 25 | var sc ServiceCred 26 | err := json.Unmarshal([]byte(str), &sc) 27 | if err != nil { 28 | t.Error("Failed to unmarshal ServiceCred", err) 29 | } 30 | if sc.TLSCert != "something" { 31 | t.Error("Failed to unmarshal TLS cert") 32 | } 33 | if sc.ECDSAPubkey != nil { 34 | t.Error("Failed to unmarshal ECDSA pubkey") 35 | } 36 | } 37 | 38 | func TestUnmarshalBuilders(t *testing.T) { 39 | val := []byte(`[{"ip":"127.0.0.1","name":"test_builder_1","rbuilder":{"tls_cert":"test-cert-no-validation","ecdsa_pubkey_address":"0x1234567890123456789012345678901234567890"}}]`) 40 | var builders []BuilderWithServiceCreds 41 | err := json.Unmarshal(val, &builders) 42 | if err != nil { 43 | t.Error("Failed to unmarshal builders", err) 44 | } 45 | if len(builders) != 1 { 46 | t.Error("Failed to unmarshal builders") 47 | } 48 | if builders[0].ServiceCreds["rbuilder"].TLSCert != "test-cert-no-validation" { 49 | t.Error("Failed to unmarshal TLS cert") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /schema/000_init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE builders 2 | ( 3 | name VARCHAR(255) PRIMARY KEY, 4 | ip_address INET NOT NULL, 5 | is_active BOOLEAN NOT NULL DEFAULT true, 6 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | deprecated_at TIMESTAMP WITH TIME ZONE, 9 | CONSTRAINT active_only_when_not_deprecated CHECK ( 10 | (is_active = true AND deprecated_at IS NULL) OR 11 | (is_active = false) 12 | ) 13 | ); 14 | 15 | -- Add an index on ip_address for faster lookups 16 | CREATE INDEX idx_builders_ip_address ON builders (ip_address); 17 | 18 | -- Trigger to automatically update the updated_at timestamp 19 | CREATE OR REPLACE FUNCTION update_builders_updated_at() 20 | RETURNS TRIGGER AS 21 | $$ 22 | BEGIN 23 | NEW.updated_at = CURRENT_TIMESTAMP; 24 | RETURN NEW; 25 | END; 26 | $$ LANGUAGE plpgsql; 27 | 28 | CREATE TRIGGER trigger_update_builders_updated_at 29 | BEFORE UPDATE 30 | ON builders 31 | FOR EACH ROW 32 | EXECUTE FUNCTION update_builders_updated_at(); 33 | 34 | -- Measurements Whitelist table 35 | CREATE TABLE measurements_whitelist 36 | ( 37 | id SERIAL PRIMARY KEY, -- new serial primary key 38 | name TEXT NOT NULL, -- in code is referenced as measurement_id 39 | attestation_type TEXT NOT NULL, -- attestation type of the measurement 40 | measurement JSONB NOT NULL, 41 | is_active BOOLEAN NOT NULL DEFAULT true, 42 | 43 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 44 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 45 | deprecated_at TIMESTAMP WITH TIME ZONE, 46 | 47 | CONSTRAINT active_only_when_not_deprecated CHECK ( 48 | (is_active = true AND deprecated_at IS NULL) OR 49 | (is_active = false) 50 | ), 51 | 52 | CONSTRAINT unique_hash_attestation_type UNIQUE (name, attestation_type) 53 | ); 54 | 55 | 56 | CREATE TABLE service_credential_registrations 57 | ( 58 | id SERIAL PRIMARY KEY, 59 | builder_name VARCHAR(255) REFERENCES builders (name), 60 | service TEXT NOT NULL, 61 | tls_cert TEXT, 62 | ecdsa_pubkey BYTEA, 63 | is_active BOOLEAN NOT NULL DEFAULT true, 64 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 65 | deprecated_at TIMESTAMP WITH TIME ZONE, 66 | measurement_id INT REFERENCES measurements_whitelist (id), 67 | CONSTRAINT active_only_when_not_deprecated CHECK ( 68 | (is_active = true AND deprecated_at IS NULL) OR 69 | (is_active = false) 70 | ) 71 | ); 72 | 73 | CREATE UNIQUE INDEX idx_unique_active_credential_per_builder_service 74 | ON service_credential_registrations (builder_name, service) 75 | WHERE is_active = true; 76 | 77 | 78 | 79 | -- Updated builder_configs table 80 | CREATE TABLE builder_configs 81 | ( 82 | id SERIAL PRIMARY KEY, 83 | builder_name VARCHAR(255) REFERENCES builders (name), -- references name 84 | config JSONB NOT NULL, 85 | is_active BOOLEAN NOT NULL DEFAULT false, 86 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 87 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP 88 | ); 89 | 90 | 91 | -- Add a new constraint to ensure only one active config per builder 92 | ALTER TABLE builder_configs 93 | ADD CONSTRAINT unique_active_config_per_builder 94 | EXCLUDE (builder_name WITH =) 95 | WHERE (is_active = true); 96 | 97 | 98 | 99 | CREATE TABLE event_log 100 | ( 101 | id SERIAL PRIMARY KEY, -- Unique identifier for each event 102 | event_name TEXT NOT NULL, -- Name of the event 103 | builder_name VARCHAR(255) REFERENCES builders (name) ON DELETE CASCADE, -- Reference to builder 104 | measurement_id INT REFERENCES measurements_whitelist (id) ON DELETE SET NULL, -- Reference to used measurement 105 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP -- Timestamp when the event occurred 106 | ); 107 | -------------------------------------------------------------------------------- /schema/001_measurement_constraints.sql: -------------------------------------------------------------------------------- 1 | -- Drop the existing unique constraint 2 | ALTER TABLE measurements_whitelist 3 | DROP CONSTRAINT unique_hash_attestation_type; 4 | 5 | -- Create a new unique constraint on the 'name' column 6 | ALTER TABLE measurements_whitelist 7 | ADD CONSTRAINT unique_name UNIQUE (name); 8 | -------------------------------------------------------------------------------- /schema/002_network_builder.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE builders 2 | ADD COLUMN network TEXT NOT NULL DEFAULT 'production'; -------------------------------------------------------------------------------- /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all", "-ST1003"] 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 | -------------------------------------------------------------------------------- /testdata/get-builders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "flashbots-01", 4 | "ip": "1.2.3.4", 5 | "orderflow_proxy": { 6 | "tls_cert": "-----BEGIN CERTIFICATE-----\nMIIC+zCCAeOgAwIBAgIQK6x5/3P7RL0vvEpyZHzd+jANBgkqhkiG9w0BAQsFADAU\nMRIwEAYDVQQKEwlGbGFzaGJvdHMwHhcNMjQwOTIzMDkwMDI0WhcNMjUwOTIzMDkw\nMDI0WjAUMRIwEAYDVQQKEwlGbGFzaGJvdHMwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQDZs/VxZca7dyi+CxdDRahIgBoyrVX8TzuIB2F6fur8uTzP9Zqz\n2F+sDa5NzLj49MY0zgtwmPU33jjfUug/GfG1LMorqgvZ7mB8oXC2S+ufu6ze643m\nCqHiPY/dhhXC1j603VKzsHyPZtTupIu5Y0rrOBoeJCdJfhJ1oT7jpl6G8rnXRNup\n0mkrilHg8INlObiCJ/06Kd7FYRwc9mDtsT3euUEsDWvaA7Lk6tg78xEs1RAlslan\nWwmAbSaCtfxqx/IGP5cQUMrYpHykBU4GMGDgUwE0MazMUT5VW3wP9frvuqTHv6Ih\nkRQq8W6w5mk/QRnshQXSwTYLSG+eMzgE0c9zAgMBAAGjSTBHMA4GA1UdDwEB/wQE\nAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBIGA1UdEQQL\nMAmCB2Zvby5jb20wDQYJKoZIhvcNAQELBQADggEBAJPFTkkcVKX7/nu2BmYXKJKB\nINxc8L2A5brHv9gOyjmYoT1l5Bm87WABmhpTqulZ/hPz3LAYmrA9VSEoNh1LIWpE\nTNwLTc935qZNBec67dqyAqM8eoOqPUTszFGNEmO2SD5m9gd7sh8VVxwTWYH4mWpV\n3F6Jvre3t3km2ELRxdMocVxkKhx4Q1UD3FqQ3dX/9qrQW+5jWOU15i5kvkkIeEt7\nZRkT1y6G0HHT3tpS0BUOqK4Y1nrU5PwwW/yhIpPbQHruJdWNgNm99mpmQlaL+QMh\nCXCRUk5ndZ7utGez4/8vBfm/1kK83qmMYTCp/P1Rs+M91DIWlkunbMs+VdT1ahk=\n-----END CERTIFICATE-----", 7 | "ecdsa_pubkey_address": "0xf00" 8 | }, 9 | "rbuilder": { 10 | "ecdsa_pubkey_address": "0xf01" 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /testdata/get-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "instance_name": "tdx-multioperator-builder-01", 3 | "public_ip": "1.2.3.4", 4 | "dns_name": "multioperator-builder-01.builder.flashbots.net", 5 | "rclone": { 6 | "__version": "v1.66.0-DEV", 7 | "access_key_id": "string", 8 | "bucket_endpoint": "string", 9 | "secret_access_key": "string" 10 | }, 11 | "bidding_service": { 12 | "__version": "v0.4.3", 13 | "config#base64": "aGVsbG8Kd29ybGQhCg==", 14 | "github_token": "string" 15 | }, 16 | "orderflow_proxy": { 17 | "__version": "v0.2.8", 18 | "builder_confighub_endpoint": "http://127.0.0.1:7937", 19 | "builder_endpoint": "http://127.0.0.1:8645", 20 | "conn_per_peer": "50", 21 | "flashbots_orderflow_signing_address": "0x00", 22 | "user_listen_addr": "127.0.0.1:3443", 23 | "orderflow_archive_endpoint": "https://orderflow-archive.flashbots.net/api", 24 | "system_listen_addr": "0.0.0.0:5544", 25 | "max_user_rps": 1000 26 | }, 27 | "rbuilder": { 28 | "__version": "v0.1.6", 29 | "enabled": false, 30 | "blocklist": "", 31 | "builders": "[[builders]]builder1 config\n[[builders]]builder2 config", 32 | "coinbase_secret_key": "0x00", 33 | "extra_data": "BuilderNet (Flashbots)", 34 | "live_builders": "[\"mgp-ordering\", \"mp-ordering\", \"mp-ordering-cb\", \"mp-ordering-deadline\"]", 35 | "optimistic_relay_secret_key": "0x00", 36 | "relay_secret_key": "0x00", 37 | "relays": "[[relays]]relay1 config\n[[relays]]relay2 config", 38 | "require_non_empty_blocklist": false, 39 | "top_bid_stream_api_key": "0x00", 40 | "top_bid_ws_basic_auth": "Zmxhc2hib3RzOmRvbnRwZWVrb25tZQ==", 41 | "top_bid_ws_url": "ws://localhost:8546", 42 | "watchdog_timeout_sec": 145 43 | }, 44 | "disk_encryption": { 45 | "key": "string" 46 | }, 47 | "prometheus": { 48 | "__version": "2.54.0", 49 | "scrape_interval": "10s", 50 | "static_configs_default_labels": [ 51 | { 52 | "label_key": "flashbots_net_vendor", 53 | "label_value": "azure" 54 | }, 55 | { 56 | "label_key": "flashbots_net_chain", 57 | "label_value": "mainnet" 58 | } 59 | ], 60 | "lighthouse_metrics": { 61 | "enabled": true, 62 | "targets": [ 63 | "localhost:5054" 64 | ] 65 | }, 66 | "reth_metrics": { 67 | "enabled": true, 68 | "targets": [ 69 | "localhost:9001" 70 | ] 71 | }, 72 | "rbuilder_metrics": { 73 | "enabled": true, 74 | "targets": [ 75 | "localhost:6060" 76 | ] 77 | }, 78 | "orderflow_proxy_metrics": { 79 | "enabled": true, 80 | "targets": [ 81 | "localhost:8090" 82 | ] 83 | }, 84 | "haproxy_metrics": { 85 | "enabled": true, 86 | "targets": [ 87 | "localhost:8405" 88 | ] 89 | }, 90 | "remote_write": [ 91 | { 92 | "name": "tdx-rbuilder-collector", 93 | "url": "https://aps-workspaces.us-east-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write", 94 | "sigv4": { 95 | "access_key": "xxx", 96 | "secret_key": "xxx", 97 | "region": "us-east-2" 98 | } 99 | }, 100 | { 101 | "name": "basic-auth-collector", 102 | "url": "https://aps-workspaces.us-east-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write", 103 | "basic_auth": { 104 | "username": "xxx", 105 | "password": "xxx" 106 | } 107 | }, 108 | { 109 | "name": "authorization-collector", 110 | "url": "https://aps-workspaces.us-east-2.amazonaws.com/workspaces/ws-xxx/api/v1/remote_write", 111 | "authorization": { 112 | "type": "Bearer", 113 | "credentials": "xxx" 114 | } 115 | } 116 | ] 117 | }, 118 | "process_exporter": { 119 | "__version": "0.8.3", 120 | "process_names": [ 121 | { 122 | "name": "lighthouse", 123 | "cmdline": [ 124 | "^\\/([-.0-9a-zA-Z]+\\/)*lighthouse[-.0-9a-zA-Z]* " 125 | ] 126 | }, 127 | { 128 | "name": "rbuilder", 129 | "cmdline": [ 130 | "^\\/([-.0-9a-zA-Z]+\\/)*rbuilder[-.0-9a-zA-Z]* " 131 | ] 132 | }, 133 | { 134 | "name": "reth", 135 | "cmdline": [ 136 | "^\\/([-.0-9a-zA-Z]+\\/)*reth[-.0-9a-zA-Z]* " 137 | ] 138 | } 139 | ] 140 | }, 141 | "fluentbit": { 142 | "__version": "v1.9.7", 143 | "aws_access_key_id": "xxx", 144 | "aws_secret_access_key": "xxx", 145 | "input_tags": "tag-1 tag-2", 146 | "output_cw_log_group_name": "multioperator-builder" 147 | }, 148 | "haproxy": { 149 | "__version": "v3.0.6", 150 | "rate_limit_privileged_ips": "192.168.1.1 192.168.1.2", 151 | "rate_limit_conn_rate_regular": "3", 152 | "rate_limit_conn_rate_privileged": "1000", 153 | "rate_limit_total_conn_regular": "3", 154 | "rate_limit_total_conn_privileged": "100", 155 | "rate_limit_bytes_in_rate_regular": "600000000", 156 | "rate_limit_bytes_in_rate_privileged": "6000000000" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /testdata/get-measurements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "measurement_id": "v1.2.3-20241010-rc1", 4 | "attestation_type": "azure-tdx", 5 | "measurements": { 6 | "8": { 7 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 8 | }, 9 | "11": { 10 | "expected": "efa43e0beff151b0f251c4abf48152382b1452b4414dbd737b4127de05ca31f7" 11 | }, 12 | "12": { 13 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 14 | }, 15 | "13": { 16 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 17 | }, 18 | "15": { 19 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 20 | }, 21 | "4": { 22 | "expected": "ea92ff762767eae6316794f1641c485d4846bc2b9df2eab6ba7f630ce6f4d66f" 23 | }, 24 | "9": { 25 | "expected": "c9f429296634072d1063a03fb287bed0b2d177b0a504755ad9194cffd90b2489" 26 | } 27 | } 28 | }, 29 | { 30 | "measurement_id": "foobar123", 31 | "attestation_type": "azure-tdx", 32 | "measurements": { 33 | "8": { 34 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 35 | }, 36 | "11": { 37 | "expected": "efa43e0beff151b0f251c4abf48152382b14521f822222222222222222222222" 38 | }, 39 | "12": { 40 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 41 | }, 42 | "13": { 43 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 44 | }, 45 | "15": { 46 | "expected": "0000000000000000000000000000000000000000000000000000000000000000" 47 | }, 48 | "4": { 49 | "expected": "ea92ff762767eae6316794f1641c485d4846bc2b922222222222222222222222" 50 | }, 51 | "9": { 52 | "expected": "c9f429296634072d1063a03fb287bed0b2d177b0a22222222222222222222222" 53 | } 54 | } 55 | } 56 | ] --------------------------------------------------------------------------------