├── .editorconfig ├── .github ├── CODEOWNERS ├── dependabot.yaml └── workflows │ ├── checks.yaml │ └── release.yaml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── SECURITY.md ├── build.rs ├── config-live-example.toml ├── rustfmt.toml └── src ├── best_bid_ws.rs ├── bidding_service_wrapper ├── bidding_service.rs ├── client │ ├── bidding_service_client_adapter.rs │ ├── mod.rs │ └── slot_bidder_client.rs ├── conversion.rs ├── mod.rs └── proto │ └── bidding_service.proto ├── bin ├── backtest-build-block.rs ├── backtest-build-range.rs └── rbuilder.rs ├── block_descriptor_bidding ├── bid_maker_adapter.rs ├── bidding_service_adapter.rs ├── block_registry.rs ├── mock_bidding_service.rs ├── mock_slot_bidder.rs ├── mod.rs ├── slot_bidder_adapter.rs └── traits.rs ├── blocks_processor.rs ├── build_info.rs ├── flashbots_config.rs ├── flashbots_signer.rs ├── lib.rs ├── metrics.rs ├── reconnect.rs ├── signed_http_client.rs └── true_block_value_push ├── best_true_value_observer.rs ├── best_true_value_pusher.rs ├── blocks_processor_backend.rs ├── mod.rs └── redis_backend.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.rs] 16 | max_line_length = 100 17 | 18 | [*.{yml,yaml}] 19 | indent_size = 2 20 | 21 | [*.md] 22 | # double whitespace at end of line 23 | # denotes a line break in Markdown 24 | trim_trailing_whitespace = false 25 | 26 | [Makefile] 27 | indent_style = tab 28 | 29 | [] 30 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # they will be requested for review when someone opens a pull request. 4 | * @dvush @ZanCorDX @metachris 5 | /crates/ @dvush @ZanCorDX 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | # ignore: 11 | # # These are peer deps of Cargo and should not be automatically bumped 12 | # - dependency-name: "semver" 13 | # - dependency-name: "crates-io" 14 | # rebase-strategy: "disabled" 15 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | merge_group: 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | 13 | jobs: 14 | lint_and_test: 15 | name: Lint and test 16 | runs-on: warp-ubuntu-latest-x64-16x 17 | strategy: 18 | matrix: 19 | toolchain: 20 | - stable 21 | 22 | steps: 23 | - name: Checkout sources 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup rust toolchain 27 | uses: dtolnay/rust-toolchain@stable 28 | with: 29 | toolchain: ${{ matrix.toolchain }} 30 | 31 | - name: Install Protoc 32 | uses: arduino/setup-protoc@v3 33 | 34 | - name: Run WarpBuilds/rust-cache 35 | uses: WarpBuilds/rust-cache@v2 36 | with: 37 | cache-on-failure: true 38 | 39 | - name: Run sccache-action 40 | uses: mozilla-actions/sccache-action@v0.0.9 41 | 42 | - name: Set sccache env vars 43 | run: | 44 | echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV 45 | echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV 46 | 47 | - name: Install native dependencies 48 | run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev 49 | 50 | - run: make lint 51 | - run: make test 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | inputs: 9 | build-binary: 10 | description: 'Build Binary' 11 | required: false 12 | type: boolean 13 | default: true 14 | build-docker: 15 | description: 'Build Docker' 16 | required: false 17 | type: boolean 18 | default: true 19 | draft-release: 20 | description: 'Draft Release' 21 | required: false 22 | type: boolean 23 | default: false 24 | 25 | jobs: 26 | extract-version: 27 | name: Extract version 28 | runs-on: warp-ubuntu-latest-x64-16x 29 | outputs: 30 | VERSION: ${{ steps.extract_version.outputs.VERSION }} 31 | steps: 32 | - name: Extract version 33 | id: extract_version 34 | run: | 35 | if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then 36 | VERSION="${GITHUB_REF#refs/tags/}" 37 | else 38 | SHA_SHORT="$(echo ${GITHUB_SHA} | cut -c1-7)" 39 | BRANCH_NAME_SAFE="${GITHUB_REF_NAME//\//-}" # replaces "/" in branch name with "-" 40 | VERSION="${BRANCH_NAME_SAFE}-${SHA_SHORT}" 41 | fi 42 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 43 | echo "${VERSION}" 44 | 45 | echo "### Version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY 46 | echo "| | |" >> $GITHUB_STEP_SUMMARY 47 | echo "| ------------------- | ---------------------- |" >> $GITHUB_STEP_SUMMARY 48 | echo "| \`GITHUB_REF_TYPE\` | \`${GITHUB_REF_TYPE}\` |" >> $GITHUB_STEP_SUMMARY 49 | echo "| \`GITHUB_REF_NAME\` | \`${GITHUB_REF_NAME}\` |" >> $GITHUB_STEP_SUMMARY 50 | echo "| \`GITHUB_REF\` | \`${GITHUB_REF}\` |" >> $GITHUB_STEP_SUMMARY 51 | echo "| \`GITHUB_SHA\` | \`${GITHUB_SHA}\` |" >> $GITHUB_STEP_SUMMARY 52 | echo "| \`VERSION\` | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY 53 | 54 | build-binary: 55 | name: Build binary 56 | needs: extract-version 57 | if: ${{ github.event.inputs.build-binary == 'true' || github.event_name == 'push'}} # when manually triggered or version tagged 58 | runs-on: ${{ matrix.configs.runner }} 59 | container: 60 | image: ubuntu:22.04 61 | env: 62 | VERSION: ${{ needs.extract-version.outputs.VERSION }} 63 | permissions: 64 | contents: write 65 | packages: write 66 | strategy: 67 | matrix: 68 | configs: 69 | - target: x86_64-unknown-linux-gnu 70 | runner: warp-ubuntu-latest-x64-16x 71 | 72 | steps: 73 | - name: Install dependencies 74 | run: | 75 | apt-get update 76 | apt-get install -y \ 77 | build-essential \ 78 | curl \ 79 | git \ 80 | libclang-dev \ 81 | libssl-dev \ 82 | pkg-config \ 83 | protobuf-compiler 84 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 85 | 86 | - uses: actions/checkout@v4 # must install git before checkout and set safe.directory after checkout because of container 87 | with: 88 | fetch-depth: 0 89 | 90 | - name: Prepare filename 91 | run: echo "OUTPUT_FILENAME=rbuilder-${VERSION}-${{ matrix.configs.target }}" >> $GITHUB_ENV 92 | 93 | - name: Build binary 94 | run: | 95 | git config --global --add safe.directory "$(pwd)" 96 | . $HOME/.cargo/env 97 | make build-reproducible TARGET=${{ matrix.configs.target }} 98 | ./target/${{ matrix.configs.target }}/release/rbuilder version 99 | 100 | - name: Upload artifact 101 | uses: actions/upload-artifact@v4 102 | with: 103 | name: ${{ env.OUTPUT_FILENAME }} 104 | path: target/${{ matrix.configs.target }}/release/rbuilder 105 | 106 | build-docker: 107 | name: Build and publish Docker image 108 | if: ${{ github.event.inputs.build-docker == 'true' || github.event_name == 'push'}} 109 | needs: extract-version 110 | runs-on: warp-ubuntu-latest-x64-16x 111 | env: 112 | VERSION: ${{ needs.extract-version.outputs.VERSION }} 113 | permissions: 114 | contents: read 115 | packages: write 116 | 117 | steps: 118 | - name: Checkout sources 119 | uses: actions/checkout@v4 120 | with: 121 | fetch-depth: 0 122 | 123 | - name: Set up Docker Buildx 124 | uses: docker/setup-buildx-action@v3 125 | 126 | - name: Docker metadata 127 | uses: docker/metadata-action@v5 128 | id: meta 129 | with: 130 | images: ghcr.io/${{ github.repository }} 131 | labels: org.opencontainers.image.source=${{ github.repositoryUrl }} 132 | tags: | 133 | type=sha 134 | type=semver,pattern={{version}},value=${{ env.VERSION }} 135 | type=semver,pattern={{major}}.{{minor}},value=${{ env.VERSION }} 136 | type=semver,pattern={{major}},value=${{ env.VERSION }} 137 | type=raw,value=latest,enable=${{ !contains(env.VERSION, '-') }} 138 | 139 | - name: Login to GHCR 140 | uses: docker/login-action@v3 141 | with: 142 | registry: ghcr.io 143 | username: ${{ github.actor }} 144 | password: ${{ secrets.GITHUB_TOKEN }} 145 | 146 | - name: Build and push Docker image 147 | uses: docker/build-push-action@v5 148 | with: 149 | context: . 150 | push: true 151 | tags: ${{ steps.meta.outputs.tags }} 152 | labels: ${{ steps.meta.outputs.labels }} 153 | platforms: linux/amd64 154 | provenance: false 155 | cache-from: type=gha 156 | cache-to: type=gha,mode=max 157 | build-args: | 158 | BUILD_PROFILE=release 159 | 160 | draft-release: 161 | name: Draft release 162 | if: ${{ github.event.inputs.draft-release == 'true' || github.event_name == 'push'}} # when manually triggered or version tagged 163 | needs: [extract-version, build-binary] 164 | runs-on: warp-ubuntu-latest-x64-16x 165 | env: 166 | VERSION: ${{ needs.extract-version.outputs.VERSION }} 167 | permissions: 168 | contents: write 169 | steps: 170 | - name: Checkout 171 | uses: actions/checkout@v4 172 | 173 | - name: Download artifacts 174 | uses: actions/download-artifact@v4 175 | with: 176 | merge-multiple: true 177 | path: artifacts 178 | 179 | - name: Record artifacts checksums 180 | working-directory: artifacts 181 | run: | 182 | find ./ || true 183 | for file in *; do sha256sum "$file" >> sha256sums.txt; done; 184 | cat sha256sums.txt 185 | 186 | - name: Create release draft 187 | uses: softprops/action-gh-release@v2.0.5 188 | id: create-release-draft 189 | with: 190 | draft: true 191 | files: artifacts/* 192 | generate_release_notes: true 193 | name: ${{ env.VERSION }} 194 | tag_name: ${{ env.VERSION }} 195 | 196 | - name: Write Github Step Summary 197 | run: | 198 | echo "---" 199 | echo "### Release Draft: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY 200 | echo "${{ steps.create-release-draft.outputs.url }}" >> $GITHUB_STEP_SUMMARY 201 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /cargo 2 | /data 3 | /target 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "innecesary" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rbuilder-operator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | reth = { git = "https://github.com/paradigmxyz/reth", tag = "v1.4.8" } 8 | reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.4.8" } 9 | reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.4.8" } 10 | 11 | 12 | alloy-primitives = { version = "1.1.0", default-features = false } 13 | alloy-provider = { version = "1.0.9", features = ["ipc", "pubsub"] } 14 | alloy-json-rpc = { version = "1.0.9" } 15 | alloy-transport-http = { version = "1.0.9" } 16 | alloy-transport = { version = "1.0.9" } 17 | alloy-rpc-types-beacon = { version = "1.0.9", features = ["ssz"] } 18 | alloy-signer-local = { version = "1.0.9" } 19 | 20 | alloy-signer = { version = "1.0.9" } 21 | alloy-rpc-client = { version = "1.0.9" } 22 | 23 | clap = { version = "4.4.3", features = ["derive", "env"] } 24 | tokio = "1.40.0" 25 | tokio-util = "0.7.12" 26 | eyre = "0.6.12" 27 | serde = "1.0.210" 28 | serde_json = "1.0.128" 29 | serde_with = { version = "3.9.0", features = ["time_0_3"] } 30 | toml = "0.8.8" 31 | jsonrpsee = { version = "0.20.3", features = ["full"] } 32 | tracing = "0.1.37" 33 | time = { version = "0.3.36", features = ["macros", "formatting", "parsing"] } 34 | thiserror = "1.0.64" 35 | ahash = "0.8.6" 36 | itertools = "0.11.0" 37 | rand = "0.8.5" 38 | crossbeam-queue = "0.3.10" 39 | lazy_static = "1.4.0" 40 | clickhouse = { version = "0.12.2", features = ["time", "uuid", "native-tls"] } 41 | uuid = { version = "1.10.0", features = ["serde", "v4", "v5"] } 42 | mockall = "0.12.1" 43 | prometheus = "0.13.4" 44 | ctor = "0.2" 45 | flume = "0.11.0" 46 | redis = "0.25.4" 47 | tonic = "0.8" 48 | prost = "0.11" 49 | tokio-stream = { version = "0.1", features = ["net"] } 50 | futures = "0.3.28" 51 | tower = "0.4" 52 | reqwest = { version = "0.11.20", features = ["blocking"] } 53 | secp256k1 = { version = "0.29" } 54 | url = "2.5.2" 55 | http = "0.2.9" 56 | hyper = "0.14" 57 | futures-util = "0.3.31" 58 | parking_lot = { version = "0.12.3" } 59 | 60 | #rbuilder = {path="./../rbuilder/crates/rbuilder"} 61 | rbuilder = { git = "https://github.com/flashbots/rbuilder.git", tag = "v1.2.3"} 62 | 63 | #metrics_macros = {path="./../rbuilder/crates/rbuilder/src/telemetry/metrics_macros"} 64 | metrics_macros = { git = "https://github.com/flashbots/rbuilder.git", tag = "v1.2.3"} 65 | 66 | tokio-tungstenite = "0.26.2" 67 | exponential-backoff = "1.2.0" 68 | 69 | [build-dependencies] 70 | built = { version = "0.7.1", features = ["git2", "chrono"] } 71 | tonic-build = "0.8" 72 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.87.0-bullseye@sha256:eb809362961259a30f540857c3cac8423c466d558bea0f55f32e3a6354654353 AS builder 2 | 3 | ARG BUILD_PROFILE=release 4 | ENV BUILD_PROFILE=$BUILD_PROFILE 5 | 6 | RUN apt-get update && apt-get install -y \ 7 | libclang-dev=1:11.0-51+nmu5 \ 8 | protobuf-compiler=3.12.4-1+deb11u1 9 | 10 | # Clone the repository at the specific branch 11 | WORKDIR /app 12 | COPY ./ /app 13 | 14 | # Build the project with the reproducible settings 15 | RUN make build-reproducible 16 | 17 | RUN mv /app/target/x86_64-unknown-linux-gnu/"${BUILD_PROFILE}"/rbuilder /rbuilder 18 | 19 | FROM gcr.io/distroless/cc-debian12:nonroot-6755e21ccd99ddead6edc8106ba03888cbeed41a 20 | COPY --from=builder /rbuilder /rbuilder 21 | ENTRYPOINT [ "/rbuilder" ] 22 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2023-2024 rbuilder Contributors 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 rbuilder Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /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 | GIT_VER ?= $(shell git describe --tags --always --dirty="-dev") 6 | GIT_TAG ?= $(shell git describe --tags --abbrev=0) 7 | 8 | ##@ Help 9 | 10 | .PHONY: help 11 | help: ## Display this help. 12 | @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) 13 | 14 | .PHONY: v 15 | v: ## Show the current version 16 | @echo "Version: ${GIT_VER}" 17 | 18 | ##@ Build 19 | 20 | .PHONY: clean 21 | clean: ## Clean up 22 | cargo clean 23 | 24 | .PHONY: build 25 | build: ## Build static binary for x86_64 26 | cargo build --release --target x86_64-unknown-linux-gnu 27 | 28 | # Environment variables for reproducible builds 29 | # Initialize RUSTFLAGS 30 | RUST_BUILD_FLAGS = 31 | 32 | # Remove build ID from the binary to ensure reproducibility across builds 33 | RUST_BUILD_FLAGS += -C link-arg=-Wl,--build-id=none 34 | 35 | # Remove metadata hash from symbol names to ensure reproducible builds 36 | RUST_BUILD_FLAGS += -C metadata='' 37 | 38 | # Set timestamp from last git commit for reproducible builds 39 | SOURCE_DATE ?= $(shell git log -1 --pretty=%ct) 40 | 41 | # Disable incremental compilation to avoid non-deterministic artifacts 42 | CARGO_INCREMENTAL_VAL = 0 43 | 44 | # Set C locale for consistent string handling and sorting 45 | LOCALE_VAL = C 46 | 47 | # Set UTC timezone for consistent time handling across builds 48 | TZ_VAL = UTC 49 | 50 | # Set the target for the build, default to x86_64 51 | TARGET ?= x86_64-unknown-linux-gnu 52 | 53 | .PHONY: build-reproducible 54 | build-reproducible: ## Build reproducible static binary for x86_64 55 | # Set timestamp from last git commit for reproducible builds 56 | SOURCE_DATE_EPOCH=$(SOURCE_DATE) \ 57 | RUSTFLAGS="${RUST_BUILD_FLAGS} --remap-path-prefix $$(pwd)=." \ 58 | CARGO_INCREMENTAL=${CARGO_INCREMENTAL_VAL} \ 59 | LC_ALL=${LOCALE_VAL} \ 60 | TZ=${TZ_VAL} \ 61 | cargo build --release --locked --target $(TARGET) 62 | 63 | .PHONY: docker-image 64 | docker-image: ## Build a rbuilder Docker image 65 | docker build --platform linux/amd64 . -t rbuilder 66 | 67 | ##@ Dev 68 | 69 | .PHONY: lint 70 | lint: ## Run the linters 71 | cargo fmt -- --check 72 | cargo clippy -- -D warnings 73 | 74 | .PHONY: test 75 | test: ## Run the tests 76 | cargo test --verbose 77 | 78 | .PHONY: lt 79 | lt: lint test ## Run "lint" and "test" 80 | 81 | fmt: ## Format the code 82 | cargo fmt 83 | cargo fix --allow-staged 84 | cargo clippy --fix --allow-staged 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rbuilder-operator 2 | Specific implementation (based on the public rbuilder) of a block builder to be used on a TDX context. 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Contact: security@flashbots.net -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | fn main() -> Result<(), Box> { 4 | built::write_built_file().expect("Failed to acquire build-time information"); 5 | let proto_file = "./src/bidding_service_wrapper/proto/bidding_service.proto"; 6 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 7 | tonic_build::configure() 8 | .protoc_arg("--experimental_allow_proto3_optional") // for older systems 9 | .build_client(true) 10 | .build_server(true) 11 | .file_descriptor_set_path(out_dir.join("bidding_service_descriptor.bin")) 12 | .out_dir("./src/bidding_service_wrapper") 13 | .compile(&[proto_file], &["proto"])?; 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /config-live-example.toml: -------------------------------------------------------------------------------- 1 | log_json = true 2 | log_level = "info,rbuilder=debug" 3 | redacted_telemetry_server_port = 6061 4 | redacted_telemetry_server_ip = "0.0.0.0" 5 | full_telemetry_server_port = 6060 6 | full_telemetry_server_ip = "0.0.0.0" 7 | 8 | chain = "mainnet" 9 | reth_datadir = "/mnt/data/reth" 10 | 11 | coinbase_secret_key = "env:COINBASE_SECRET_KEY" 12 | 13 | cl_node_url = ["http://localhost:3500"] 14 | jsonrpc_server_port = 8645 15 | jsonrpc_server_ip = "0.0.0.0" 16 | el_node_ipc_path = "/tmp/reth.ipc" 17 | extra_data = "⚡🤖" 18 | 19 | blocklist_file_path = "./blocklist.json" 20 | 21 | 22 | blocks_processor_url = "http://block_processor.internal" 23 | key_registration_url = "http://127.0.0.1:8090" 24 | ignore_cancellable_orders = true 25 | 26 | sbundle_mergeabe_signers = [] 27 | live_builders = ["mp-ordering", "mgp-ordering", "parallel"] 28 | 29 | top_bid_ws_url = "env:TOP_BID_WS_URL" 30 | top_bid_ws_basic_auth = "env:TOP_BID_WS_BASIC_AUTH" 31 | 32 | bidding_service_ipc_path = "/tmp/rpc_bidding_server.sock" 33 | 34 | [tbv_push_redis] 35 | url = "env:BIDDING_REDIS_URL" 36 | channel = "best_true_value" 37 | 38 | 39 | [[relays]] 40 | name = "flashbots" 41 | url = "https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net" 42 | priority = 0 43 | use_ssz_for_submit = false 44 | use_gzip_for_submit = false 45 | 46 | [[builders]] 47 | name = "mgp-ordering" 48 | algo = "ordering-builder" 49 | discard_txs = true 50 | sorting = "mev-gas-price" 51 | failed_order_retries = 1 52 | drop_failed_orders = true 53 | 54 | [[builders]] 55 | name = "mp-ordering" 56 | algo = "ordering-builder" 57 | discard_txs = true 58 | sorting = "max-profit" 59 | failed_order_retries = 1 60 | drop_failed_orders = true 61 | 62 | [[builders]] 63 | name = "parallel" 64 | algo = "parallel-builder" 65 | discard_txs = true 66 | num_threads = 5 67 | safe_sorting_only = false 68 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | imports_granularity = "Crate" 3 | -------------------------------------------------------------------------------- /src/best_bid_ws.rs: -------------------------------------------------------------------------------- 1 | //! Best bid websocket connector 2 | //! 3 | //! This module uses websocket connection to get best bid value from the relays. 4 | //! The best bid for the current block-slot is stored in the `BestBidCell` and can be read at any time. 5 | use std::ops::DerefMut; 6 | use std::sync::Arc; 7 | use std::time::Duration; 8 | 9 | use crate::reconnect::{run_async_loop_with_reconnect, RunCommand}; 10 | use alloy_primitives::utils::format_ether; 11 | use alloy_primitives::U256; 12 | use parking_lot::Mutex; 13 | use rbuilder::live_builder::block_output::bid_value_source::interfaces::{ 14 | BidValueObs, BidValueSource, CompetitionBid, 15 | }; 16 | use serde::Deserialize; 17 | use tokio::net::TcpStream; 18 | use tokio_stream::StreamExt; 19 | use tokio_tungstenite::tungstenite::client::IntoClientRequest; 20 | use tokio_tungstenite::tungstenite::handshake::client::Request; 21 | use tokio_tungstenite::tungstenite::Error; 22 | use tokio_tungstenite::{connect_async_with_config, tungstenite::protocol::Message}; 23 | use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; 24 | use tokio_util::sync::CancellationToken; 25 | use tracing::{error, trace, warn}; 26 | 27 | use crate::metrics::inc_non_0_competition_bids; 28 | 29 | type Connection = WebSocketStream>; 30 | 31 | const MAX_IO_ERRORS: usize = 5; 32 | 33 | // time that we wait for a new value before reconnecting 34 | const READ_TIMEOUT: Duration = Duration::from_secs(5); 35 | 36 | #[derive(Debug, Clone, Deserialize, Default)] 37 | #[serde(rename_all = "camelCase")] 38 | pub struct BestBidValue { 39 | pub block_number: u64, 40 | pub slot_number: u64, 41 | pub block_top_bid: U256, 42 | } 43 | 44 | #[derive(Debug)] 45 | pub struct Subscription { 46 | pub block_number: u64, 47 | pub slot_number: u64, 48 | pub obs: Arc, 49 | } 50 | 51 | /// Struct that connects to a websocket feed with best bids from the competition. 52 | /// Allows to subscribe so listen for changes on a particular slot. 53 | /// Usage: 54 | /// - call sub = subscribe 55 | /// - monitor the value as long as needed: 56 | /// - await wait_for_change. This will wake when a change happens, no need for polling. 57 | /// - Ask top_bid 58 | /// - call unsubscribe(sub) 59 | #[derive(Debug)] 60 | pub struct BestBidWSConnector { 61 | connection_request: Request, 62 | subscriptions: Arc>>, 63 | } 64 | 65 | impl BestBidWSConnector { 66 | pub fn new(url: &str, basic_auth: &str) -> eyre::Result { 67 | let mut connection_request = url.into_client_request()?; 68 | connection_request 69 | .headers_mut() 70 | .insert("Authorization", format!("Basic {}", basic_auth).parse()?); 71 | 72 | Ok(Self { 73 | connection_request, 74 | subscriptions: Default::default(), 75 | }) 76 | } 77 | 78 | pub async fn run_ws_stream( 79 | &self, 80 | // We must try_send on every non 0 bid or the process will be killed 81 | watch_dog_sender: flume::Sender<()>, 82 | cancellation_token: CancellationToken, 83 | ) { 84 | run_async_loop_with_reconnect( 85 | "ws_top_bid_connection", 86 | || connect(self.connection_request.clone()), 87 | |conn| { 88 | run_command( 89 | conn, 90 | cancellation_token.clone(), 91 | watch_dog_sender.clone(), 92 | self.subscriptions.clone(), 93 | ) 94 | }, 95 | None, 96 | cancellation_token.clone(), 97 | ) 98 | .await; 99 | } 100 | } 101 | 102 | async fn connect(request: R) -> Result 103 | where 104 | R: IntoClientRequest + Unpin, 105 | { 106 | connect_async_with_config( 107 | request, None, true, // TODO: naggle, decide 108 | ) 109 | .await 110 | .map(|(c, _)| c) 111 | } 112 | 113 | async fn run_command( 114 | mut conn: Connection, 115 | cancellation_token: CancellationToken, 116 | watch_dog_sender: flume::Sender<()>, 117 | subscriptions: Arc>>, 118 | ) -> RunCommand { 119 | let mut io_error_count = 0; 120 | loop { 121 | if cancellation_token.is_cancelled() { 122 | break; 123 | } 124 | if io_error_count >= MAX_IO_ERRORS { 125 | warn!("Too many read errors, reconnecting"); 126 | return RunCommand::Reconnect; 127 | } 128 | 129 | let next_message = tokio::time::timeout(READ_TIMEOUT, conn.next()); 130 | let res = match next_message.await { 131 | Ok(res) => res, 132 | Err(err) => { 133 | warn!(?err, "Timeout error"); 134 | return RunCommand::Reconnect; 135 | } 136 | }; 137 | let message = match res { 138 | Some(Ok(message)) => message, 139 | Some(Err(err)) => { 140 | warn!(?err, "Error reading WS stream"); 141 | io_error_count += 1; 142 | continue; 143 | } 144 | None => { 145 | warn!("Connection read stream is closed, reconnecting"); 146 | return RunCommand::Reconnect; 147 | } 148 | }; 149 | let data = match &message { 150 | Message::Text(msg) => msg.as_bytes(), 151 | Message::Binary(msg) => msg.as_ref(), 152 | Message::Ping(_) => { 153 | error!(ws_message = "ping", "Received unexpected message"); 154 | continue; 155 | } 156 | Message::Pong(_) => { 157 | error!(ws_message = "pong", "Received unexpected message"); 158 | continue; 159 | } 160 | Message::Frame(_) => { 161 | error!(ws_message = "frame", "Received unexpected message"); 162 | continue; 163 | } 164 | Message::Close(_) => { 165 | warn!("Connection closed, reconnecting"); 166 | return RunCommand::Reconnect; 167 | } 168 | }; 169 | 170 | let bid_value: BestBidValue = match serde_json::from_slice(data) { 171 | Ok(value) => value, 172 | Err(err) => { 173 | error!(?err, "Failed to parse best bid value"); 174 | continue; 175 | } 176 | }; 177 | 178 | if !bid_value.block_top_bid.is_zero() { 179 | inc_non_0_competition_bids(); 180 | let _ = watch_dog_sender.try_send(()); 181 | } 182 | 183 | trace!( 184 | block = bid_value.block_number, 185 | slot = bid_value.slot_number, 186 | value = format_ether(bid_value.block_top_bid), 187 | "Updated best bid value" 188 | ); 189 | 190 | for sub in subscriptions.lock().deref_mut() { 191 | if sub.block_number == bid_value.block_number 192 | && sub.slot_number == bid_value.slot_number 193 | { 194 | sub.obs 195 | .update_new_bid(CompetitionBid::new(bid_value.block_top_bid)); 196 | } 197 | } 198 | } 199 | RunCommand::Finish 200 | } 201 | 202 | impl BidValueSource for BestBidWSConnector { 203 | fn subscribe(&self, block_number: u64, slot_number: u64, obs: Arc) { 204 | self.subscriptions.lock().push(Subscription { 205 | block_number, 206 | slot_number, 207 | obs, 208 | }); 209 | } 210 | 211 | fn unsubscribe(&self, obs: Arc) { 212 | self.subscriptions 213 | .lock() 214 | .retain(|s| !Arc::ptr_eq(&s.obs, &obs)); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/bidding_service.rs: -------------------------------------------------------------------------------- 1 | /// Mapping of build_info::Version 2 | #[allow(clippy::derive_partial_eq_without_eq)] 3 | #[derive(Clone, PartialEq, ::prost::Message)] 4 | pub struct BidderVersionInfo { 5 | #[prost(string, tag = "1")] 6 | pub git_commit: ::prost::alloc::string::String, 7 | #[prost(string, tag = "2")] 8 | pub git_ref: ::prost::alloc::string::String, 9 | #[prost(string, tag = "3")] 10 | pub build_time_utc: ::prost::alloc::string::String, 11 | } 12 | #[allow(clippy::derive_partial_eq_without_eq)] 13 | #[derive(Clone, PartialEq, ::prost::Message)] 14 | pub struct Empty {} 15 | #[allow(clippy::derive_partial_eq_without_eq)] 16 | #[derive(Clone, PartialEq, ::prost::Message)] 17 | pub struct MustWinBlockParams { 18 | #[prost(uint64, tag = "1")] 19 | pub block: u64, 20 | } 21 | #[allow(clippy::derive_partial_eq_without_eq)] 22 | #[derive(Clone, PartialEq, ::prost::Message)] 23 | pub struct UpdateNewBidParams { 24 | #[prost(uint64, tag = "1")] 25 | pub session_id: u64, 26 | /// Array of 4 uint64 27 | #[prost(uint64, repeated, tag = "2")] 28 | pub bid: ::prost::alloc::vec::Vec, 29 | #[prost(uint64, tag = "3")] 30 | pub creation_time_us: u64, 31 | } 32 | #[allow(clippy::derive_partial_eq_without_eq)] 33 | #[derive(Clone, PartialEq, ::prost::Message)] 34 | pub struct NewBlockParams { 35 | #[prost(uint64, tag = "1")] 36 | pub session_id: u64, 37 | /// Array of 4 uint64 38 | #[prost(uint64, repeated, tag = "2")] 39 | pub true_block_value: ::prost::alloc::vec::Vec, 40 | #[prost(bool, tag = "3")] 41 | pub can_add_payout_tx: bool, 42 | #[prost(uint64, tag = "4")] 43 | pub block_id: u64, 44 | #[prost(uint64, tag = "5")] 45 | pub creation_time_us: u64, 46 | } 47 | #[allow(clippy::derive_partial_eq_without_eq)] 48 | #[derive(Clone, PartialEq, ::prost::Message)] 49 | pub struct DestroySlotBidderParams { 50 | #[prost(uint64, tag = "1")] 51 | pub session_id: u64, 52 | } 53 | #[allow(clippy::derive_partial_eq_without_eq)] 54 | #[derive(Clone, PartialEq, ::prost::Message)] 55 | pub struct CreateSlotBidderParams { 56 | #[prost(uint64, tag = "1")] 57 | pub block: u64, 58 | #[prost(uint64, tag = "2")] 59 | pub slot: u64, 60 | /// Id identifying the session. Used in all following calls. 61 | #[prost(uint64, tag = "3")] 62 | pub session_id: u64, 63 | /// unix ts 64 | #[prost(int64, tag = "4")] 65 | pub slot_timestamp: i64, 66 | } 67 | /// Info about a onchain block from reth. 68 | #[allow(clippy::derive_partial_eq_without_eq)] 69 | #[derive(Clone, PartialEq, ::prost::Message)] 70 | pub struct LandedBlockInfo { 71 | #[prost(uint64, tag = "1")] 72 | pub block_number: u64, 73 | #[prost(int64, tag = "2")] 74 | pub block_timestamp: i64, 75 | /// Array of 4 uint64 76 | #[prost(uint64, repeated, tag = "3")] 77 | pub builder_balance: ::prost::alloc::vec::Vec, 78 | /// true -> we landed this block. 79 | /// If false we could have landed it in coinbase == fee recipient mode but balance wouldn't change so we don't care. 80 | #[prost(bool, tag = "4")] 81 | pub beneficiary_is_builder: bool, 82 | } 83 | #[allow(clippy::derive_partial_eq_without_eq)] 84 | #[derive(Clone, PartialEq, ::prost::Message)] 85 | pub struct LandedBlocksParams { 86 | /// Added field name 87 | #[prost(message, repeated, tag = "1")] 88 | pub landed_block_info: ::prost::alloc::vec::Vec, 89 | } 90 | #[allow(clippy::derive_partial_eq_without_eq)] 91 | #[derive(Clone, PartialEq, ::prost::Message)] 92 | pub struct Bid { 93 | /// Optional implicitly by allowing empty 94 | /// 95 | /// Array of 4 uint64 96 | #[prost(uint64, repeated, tag = "1")] 97 | pub payout_tx_value: ::prost::alloc::vec::Vec, 98 | #[prost(uint64, tag = "2")] 99 | pub block_id: u64, 100 | /// Optional implicitly by allowing empty 101 | /// 102 | /// Array of 4 uint64 103 | #[prost(uint64, repeated, tag = "3")] 104 | pub seen_competition_bid: ::prost::alloc::vec::Vec, 105 | #[prost(uint64, optional, tag = "4")] 106 | pub trigger_creation_time_us: ::core::option::Option, 107 | } 108 | /// Exactly 1 member will be not null. 109 | /// Since this is not mapped to an enum we must be careful to manually update BiddingServiceClientAdapter. 110 | #[allow(clippy::derive_partial_eq_without_eq)] 111 | #[derive(Clone, PartialEq, ::prost::Message)] 112 | pub struct Callback { 113 | #[prost(message, optional, tag = "1")] 114 | pub bid: ::core::option::Option, 115 | #[prost(bool, optional, tag = "2")] 116 | pub can_use_suggested_fee_recipient_as_coinbase_change: ::core::option::Option, 117 | } 118 | /// Generated client implementations. 119 | pub mod bidding_service_client { 120 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 121 | use tonic::codegen::*; 122 | use tonic::codegen::http::Uri; 123 | /// Protocol for the bidding service. It's used to marshal all the traits in src/block_descriptor_bidding/traits.rs 124 | /// Usage: 125 | /// The client connects to the server and calls Initialize, this call should create the real BiddingService on the server side. 126 | /// Before calling Initialize any other call will fail. Initialize can be called again to recreate the BiddingService (eg: rbuilder reconnection). 127 | /// After that, for each slot the client should call CreateSlotBidder to create the SlotBidder on the server side and DestroySlotBidder when the SlotBidder is not needed anymore. 128 | /// Other calls are almost 1 to 1 with the original traits but for SlotBidder calls block/slot are added to identify the SlotBidder. 129 | /// Notice that CreateSlotBidder returns a stream of Callback. This stream is used for 2 things: 130 | /// - Send back bids made by the SlotBidder. 131 | /// - Notify changes on the state of SlotBidder's can_use_suggested_fee_recipient_as_coinbase. We use this methodology instead of a 132 | /// forward RPC call since can_use_suggested_fee_recipient_as_coinbase almost does not change and we want to avoid innecesary RPC calls during block building. 133 | #[derive(Debug, Clone)] 134 | pub struct BiddingServiceClient { 135 | inner: tonic::client::Grpc, 136 | } 137 | impl BiddingServiceClient { 138 | /// Attempt to create a new client by connecting to a given endpoint. 139 | pub async fn connect(dst: D) -> Result 140 | where 141 | D: std::convert::TryInto, 142 | D::Error: Into, 143 | { 144 | let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; 145 | Ok(Self::new(conn)) 146 | } 147 | } 148 | impl BiddingServiceClient 149 | where 150 | T: tonic::client::GrpcService, 151 | T::Error: Into, 152 | T::ResponseBody: Body + Send + 'static, 153 | ::Error: Into + Send, 154 | { 155 | pub fn new(inner: T) -> Self { 156 | let inner = tonic::client::Grpc::new(inner); 157 | Self { inner } 158 | } 159 | pub fn with_origin(inner: T, origin: Uri) -> Self { 160 | let inner = tonic::client::Grpc::with_origin(inner, origin); 161 | Self { inner } 162 | } 163 | pub fn with_interceptor( 164 | inner: T, 165 | interceptor: F, 166 | ) -> BiddingServiceClient> 167 | where 168 | F: tonic::service::Interceptor, 169 | T::ResponseBody: Default, 170 | T: tonic::codegen::Service< 171 | http::Request, 172 | Response = http::Response< 173 | >::ResponseBody, 174 | >, 175 | >, 176 | , 178 | >>::Error: Into + Send + Sync, 179 | { 180 | BiddingServiceClient::new(InterceptedService::new(inner, interceptor)) 181 | } 182 | /// Compress requests with the given encoding. 183 | /// 184 | /// This requires the server to support it otherwise it might respond with an 185 | /// error. 186 | #[must_use] 187 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 188 | self.inner = self.inner.send_compressed(encoding); 189 | self 190 | } 191 | /// Enable decompressing responses. 192 | #[must_use] 193 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 194 | self.inner = self.inner.accept_compressed(encoding); 195 | self 196 | } 197 | /// Call after connection before calling anything. This will really create the BiddingService on the server side. 198 | /// Returns the version info for the server side. 199 | pub async fn initialize( 200 | &mut self, 201 | request: impl tonic::IntoRequest, 202 | ) -> Result, tonic::Status> { 203 | self.inner 204 | .ready() 205 | .await 206 | .map_err(|e| { 207 | tonic::Status::new( 208 | tonic::Code::Unknown, 209 | format!("Service was not ready: {}", e.into()), 210 | ) 211 | })?; 212 | let codec = tonic::codec::ProstCodec::default(); 213 | let path = http::uri::PathAndQuery::from_static( 214 | "/bidding_service.BiddingService/Initialize", 215 | ); 216 | self.inner.unary(request.into_request(), path, codec).await 217 | } 218 | /// BiddingService 219 | pub async fn create_slot_bidder( 220 | &mut self, 221 | request: impl tonic::IntoRequest, 222 | ) -> Result< 223 | tonic::Response>, 224 | tonic::Status, 225 | > { 226 | self.inner 227 | .ready() 228 | .await 229 | .map_err(|e| { 230 | tonic::Status::new( 231 | tonic::Code::Unknown, 232 | format!("Service was not ready: {}", e.into()), 233 | ) 234 | })?; 235 | let codec = tonic::codec::ProstCodec::default(); 236 | let path = http::uri::PathAndQuery::from_static( 237 | "/bidding_service.BiddingService/CreateSlotBidder", 238 | ); 239 | self.inner.server_streaming(request.into_request(), path, codec).await 240 | } 241 | pub async fn destroy_slot_bidder( 242 | &mut self, 243 | request: impl tonic::IntoRequest, 244 | ) -> Result, tonic::Status> { 245 | self.inner 246 | .ready() 247 | .await 248 | .map_err(|e| { 249 | tonic::Status::new( 250 | tonic::Code::Unknown, 251 | format!("Service was not ready: {}", e.into()), 252 | ) 253 | })?; 254 | let codec = tonic::codec::ProstCodec::default(); 255 | let path = http::uri::PathAndQuery::from_static( 256 | "/bidding_service.BiddingService/DestroySlotBidder", 257 | ); 258 | self.inner.unary(request.into_request(), path, codec).await 259 | } 260 | pub async fn must_win_block( 261 | &mut self, 262 | request: impl tonic::IntoRequest, 263 | ) -> Result, tonic::Status> { 264 | self.inner 265 | .ready() 266 | .await 267 | .map_err(|e| { 268 | tonic::Status::new( 269 | tonic::Code::Unknown, 270 | format!("Service was not ready: {}", e.into()), 271 | ) 272 | })?; 273 | let codec = tonic::codec::ProstCodec::default(); 274 | let path = http::uri::PathAndQuery::from_static( 275 | "/bidding_service.BiddingService/MustWinBlock", 276 | ); 277 | self.inner.unary(request.into_request(), path, codec).await 278 | } 279 | pub async fn update_new_landed_blocks_detected( 280 | &mut self, 281 | request: impl tonic::IntoRequest, 282 | ) -> Result, tonic::Status> { 283 | self.inner 284 | .ready() 285 | .await 286 | .map_err(|e| { 287 | tonic::Status::new( 288 | tonic::Code::Unknown, 289 | format!("Service was not ready: {}", e.into()), 290 | ) 291 | })?; 292 | let codec = tonic::codec::ProstCodec::default(); 293 | let path = http::uri::PathAndQuery::from_static( 294 | "/bidding_service.BiddingService/UpdateNewLandedBlocksDetected", 295 | ); 296 | self.inner.unary(request.into_request(), path, codec).await 297 | } 298 | pub async fn update_failed_reading_new_landed_blocks( 299 | &mut self, 300 | request: impl tonic::IntoRequest, 301 | ) -> Result, tonic::Status> { 302 | self.inner 303 | .ready() 304 | .await 305 | .map_err(|e| { 306 | tonic::Status::new( 307 | tonic::Code::Unknown, 308 | format!("Service was not ready: {}", e.into()), 309 | ) 310 | })?; 311 | let codec = tonic::codec::ProstCodec::default(); 312 | let path = http::uri::PathAndQuery::from_static( 313 | "/bidding_service.BiddingService/UpdateFailedReadingNewLandedBlocks", 314 | ); 315 | self.inner.unary(request.into_request(), path, codec).await 316 | } 317 | /// SlotBidder->UnfinishedBlockBuildingSink 318 | pub async fn new_block( 319 | &mut self, 320 | request: impl tonic::IntoRequest, 321 | ) -> Result, tonic::Status> { 322 | self.inner 323 | .ready() 324 | .await 325 | .map_err(|e| { 326 | tonic::Status::new( 327 | tonic::Code::Unknown, 328 | format!("Service was not ready: {}", e.into()), 329 | ) 330 | })?; 331 | let codec = tonic::codec::ProstCodec::default(); 332 | let path = http::uri::PathAndQuery::from_static( 333 | "/bidding_service.BiddingService/NewBlock", 334 | ); 335 | self.inner.unary(request.into_request(), path, codec).await 336 | } 337 | /// SlotBidder->BidValueObs 338 | pub async fn update_new_bid( 339 | &mut self, 340 | request: impl tonic::IntoRequest, 341 | ) -> Result, tonic::Status> { 342 | self.inner 343 | .ready() 344 | .await 345 | .map_err(|e| { 346 | tonic::Status::new( 347 | tonic::Code::Unknown, 348 | format!("Service was not ready: {}", e.into()), 349 | ) 350 | })?; 351 | let codec = tonic::codec::ProstCodec::default(); 352 | let path = http::uri::PathAndQuery::from_static( 353 | "/bidding_service.BiddingService/UpdateNewBid", 354 | ); 355 | self.inner.unary(request.into_request(), path, codec).await 356 | } 357 | } 358 | } 359 | /// Generated server implementations. 360 | pub mod bidding_service_server { 361 | #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] 362 | use tonic::codegen::*; 363 | /// Generated trait containing gRPC methods that should be implemented for use with BiddingServiceServer. 364 | #[async_trait] 365 | pub trait BiddingService: Send + Sync + 'static { 366 | /// Call after connection before calling anything. This will really create the BiddingService on the server side. 367 | /// Returns the version info for the server side. 368 | async fn initialize( 369 | &self, 370 | request: tonic::Request, 371 | ) -> Result, tonic::Status>; 372 | /// Server streaming response type for the CreateSlotBidder method. 373 | type CreateSlotBidderStream: futures_core::Stream< 374 | Item = Result, 375 | > 376 | + Send 377 | + 'static; 378 | /// BiddingService 379 | async fn create_slot_bidder( 380 | &self, 381 | request: tonic::Request, 382 | ) -> Result, tonic::Status>; 383 | async fn destroy_slot_bidder( 384 | &self, 385 | request: tonic::Request, 386 | ) -> Result, tonic::Status>; 387 | async fn must_win_block( 388 | &self, 389 | request: tonic::Request, 390 | ) -> Result, tonic::Status>; 391 | async fn update_new_landed_blocks_detected( 392 | &self, 393 | request: tonic::Request, 394 | ) -> Result, tonic::Status>; 395 | async fn update_failed_reading_new_landed_blocks( 396 | &self, 397 | request: tonic::Request, 398 | ) -> Result, tonic::Status>; 399 | /// SlotBidder->UnfinishedBlockBuildingSink 400 | async fn new_block( 401 | &self, 402 | request: tonic::Request, 403 | ) -> Result, tonic::Status>; 404 | /// SlotBidder->BidValueObs 405 | async fn update_new_bid( 406 | &self, 407 | request: tonic::Request, 408 | ) -> Result, tonic::Status>; 409 | } 410 | /// Protocol for the bidding service. It's used to marshal all the traits in src/block_descriptor_bidding/traits.rs 411 | /// Usage: 412 | /// The client connects to the server and calls Initialize, this call should create the real BiddingService on the server side. 413 | /// Before calling Initialize any other call will fail. Initialize can be called again to recreate the BiddingService (eg: rbuilder reconnection). 414 | /// After that, for each slot the client should call CreateSlotBidder to create the SlotBidder on the server side and DestroySlotBidder when the SlotBidder is not needed anymore. 415 | /// Other calls are almost 1 to 1 with the original traits but for SlotBidder calls block/slot are added to identify the SlotBidder. 416 | /// Notice that CreateSlotBidder returns a stream of Callback. This stream is used for 2 things: 417 | /// - Send back bids made by the SlotBidder. 418 | /// - Notify changes on the state of SlotBidder's can_use_suggested_fee_recipient_as_coinbase. We use this methodology instead of a 419 | /// forward RPC call since can_use_suggested_fee_recipient_as_coinbase almost does not change and we want to avoid innecesary RPC calls during block building. 420 | #[derive(Debug)] 421 | pub struct BiddingServiceServer { 422 | inner: _Inner, 423 | accept_compression_encodings: EnabledCompressionEncodings, 424 | send_compression_encodings: EnabledCompressionEncodings, 425 | } 426 | struct _Inner(Arc); 427 | impl BiddingServiceServer { 428 | pub fn new(inner: T) -> Self { 429 | Self::from_arc(Arc::new(inner)) 430 | } 431 | pub fn from_arc(inner: Arc) -> Self { 432 | let inner = _Inner(inner); 433 | Self { 434 | inner, 435 | accept_compression_encodings: Default::default(), 436 | send_compression_encodings: Default::default(), 437 | } 438 | } 439 | pub fn with_interceptor( 440 | inner: T, 441 | interceptor: F, 442 | ) -> InterceptedService 443 | where 444 | F: tonic::service::Interceptor, 445 | { 446 | InterceptedService::new(Self::new(inner), interceptor) 447 | } 448 | /// Enable decompressing requests with the given encoding. 449 | #[must_use] 450 | pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { 451 | self.accept_compression_encodings.enable(encoding); 452 | self 453 | } 454 | /// Compress responses with the given encoding, if the client supports it. 455 | #[must_use] 456 | pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { 457 | self.send_compression_encodings.enable(encoding); 458 | self 459 | } 460 | } 461 | impl tonic::codegen::Service> for BiddingServiceServer 462 | where 463 | T: BiddingService, 464 | B: Body + Send + 'static, 465 | B::Error: Into + Send + 'static, 466 | { 467 | type Response = http::Response; 468 | type Error = std::convert::Infallible; 469 | type Future = BoxFuture; 470 | fn poll_ready( 471 | &mut self, 472 | _cx: &mut Context<'_>, 473 | ) -> Poll> { 474 | Poll::Ready(Ok(())) 475 | } 476 | fn call(&mut self, req: http::Request) -> Self::Future { 477 | let inner = self.inner.clone(); 478 | match req.uri().path() { 479 | "/bidding_service.BiddingService/Initialize" => { 480 | #[allow(non_camel_case_types)] 481 | struct InitializeSvc(pub Arc); 482 | impl< 483 | T: BiddingService, 484 | > tonic::server::UnaryService 485 | for InitializeSvc { 486 | type Response = super::BidderVersionInfo; 487 | type Future = BoxFuture< 488 | tonic::Response, 489 | tonic::Status, 490 | >; 491 | fn call( 492 | &mut self, 493 | request: tonic::Request, 494 | ) -> Self::Future { 495 | let inner = self.0.clone(); 496 | let fut = async move { (*inner).initialize(request).await }; 497 | Box::pin(fut) 498 | } 499 | } 500 | let accept_compression_encodings = self.accept_compression_encodings; 501 | let send_compression_encodings = self.send_compression_encodings; 502 | let inner = self.inner.clone(); 503 | let fut = async move { 504 | let inner = inner.0; 505 | let method = InitializeSvc(inner); 506 | let codec = tonic::codec::ProstCodec::default(); 507 | let mut grpc = tonic::server::Grpc::new(codec) 508 | .apply_compression_config( 509 | accept_compression_encodings, 510 | send_compression_encodings, 511 | ); 512 | let res = grpc.unary(method, req).await; 513 | Ok(res) 514 | }; 515 | Box::pin(fut) 516 | } 517 | "/bidding_service.BiddingService/CreateSlotBidder" => { 518 | #[allow(non_camel_case_types)] 519 | struct CreateSlotBidderSvc(pub Arc); 520 | impl< 521 | T: BiddingService, 522 | > tonic::server::ServerStreamingService< 523 | super::CreateSlotBidderParams, 524 | > for CreateSlotBidderSvc { 525 | type Response = super::Callback; 526 | type ResponseStream = T::CreateSlotBidderStream; 527 | type Future = BoxFuture< 528 | tonic::Response, 529 | tonic::Status, 530 | >; 531 | fn call( 532 | &mut self, 533 | request: tonic::Request, 534 | ) -> Self::Future { 535 | let inner = self.0.clone(); 536 | let fut = async move { 537 | (*inner).create_slot_bidder(request).await 538 | }; 539 | Box::pin(fut) 540 | } 541 | } 542 | let accept_compression_encodings = self.accept_compression_encodings; 543 | let send_compression_encodings = self.send_compression_encodings; 544 | let inner = self.inner.clone(); 545 | let fut = async move { 546 | let inner = inner.0; 547 | let method = CreateSlotBidderSvc(inner); 548 | let codec = tonic::codec::ProstCodec::default(); 549 | let mut grpc = tonic::server::Grpc::new(codec) 550 | .apply_compression_config( 551 | accept_compression_encodings, 552 | send_compression_encodings, 553 | ); 554 | let res = grpc.server_streaming(method, req).await; 555 | Ok(res) 556 | }; 557 | Box::pin(fut) 558 | } 559 | "/bidding_service.BiddingService/DestroySlotBidder" => { 560 | #[allow(non_camel_case_types)] 561 | struct DestroySlotBidderSvc(pub Arc); 562 | impl< 563 | T: BiddingService, 564 | > tonic::server::UnaryService 565 | for DestroySlotBidderSvc { 566 | type Response = super::Empty; 567 | type Future = BoxFuture< 568 | tonic::Response, 569 | tonic::Status, 570 | >; 571 | fn call( 572 | &mut self, 573 | request: tonic::Request, 574 | ) -> Self::Future { 575 | let inner = self.0.clone(); 576 | let fut = async move { 577 | (*inner).destroy_slot_bidder(request).await 578 | }; 579 | Box::pin(fut) 580 | } 581 | } 582 | let accept_compression_encodings = self.accept_compression_encodings; 583 | let send_compression_encodings = self.send_compression_encodings; 584 | let inner = self.inner.clone(); 585 | let fut = async move { 586 | let inner = inner.0; 587 | let method = DestroySlotBidderSvc(inner); 588 | let codec = tonic::codec::ProstCodec::default(); 589 | let mut grpc = tonic::server::Grpc::new(codec) 590 | .apply_compression_config( 591 | accept_compression_encodings, 592 | send_compression_encodings, 593 | ); 594 | let res = grpc.unary(method, req).await; 595 | Ok(res) 596 | }; 597 | Box::pin(fut) 598 | } 599 | "/bidding_service.BiddingService/MustWinBlock" => { 600 | #[allow(non_camel_case_types)] 601 | struct MustWinBlockSvc(pub Arc); 602 | impl< 603 | T: BiddingService, 604 | > tonic::server::UnaryService 605 | for MustWinBlockSvc { 606 | type Response = super::Empty; 607 | type Future = BoxFuture< 608 | tonic::Response, 609 | tonic::Status, 610 | >; 611 | fn call( 612 | &mut self, 613 | request: tonic::Request, 614 | ) -> Self::Future { 615 | let inner = self.0.clone(); 616 | let fut = async move { 617 | (*inner).must_win_block(request).await 618 | }; 619 | Box::pin(fut) 620 | } 621 | } 622 | let accept_compression_encodings = self.accept_compression_encodings; 623 | let send_compression_encodings = self.send_compression_encodings; 624 | let inner = self.inner.clone(); 625 | let fut = async move { 626 | let inner = inner.0; 627 | let method = MustWinBlockSvc(inner); 628 | let codec = tonic::codec::ProstCodec::default(); 629 | let mut grpc = tonic::server::Grpc::new(codec) 630 | .apply_compression_config( 631 | accept_compression_encodings, 632 | send_compression_encodings, 633 | ); 634 | let res = grpc.unary(method, req).await; 635 | Ok(res) 636 | }; 637 | Box::pin(fut) 638 | } 639 | "/bidding_service.BiddingService/UpdateNewLandedBlocksDetected" => { 640 | #[allow(non_camel_case_types)] 641 | struct UpdateNewLandedBlocksDetectedSvc( 642 | pub Arc, 643 | ); 644 | impl< 645 | T: BiddingService, 646 | > tonic::server::UnaryService 647 | for UpdateNewLandedBlocksDetectedSvc { 648 | type Response = super::Empty; 649 | type Future = BoxFuture< 650 | tonic::Response, 651 | tonic::Status, 652 | >; 653 | fn call( 654 | &mut self, 655 | request: tonic::Request, 656 | ) -> Self::Future { 657 | let inner = self.0.clone(); 658 | let fut = async move { 659 | (*inner).update_new_landed_blocks_detected(request).await 660 | }; 661 | Box::pin(fut) 662 | } 663 | } 664 | let accept_compression_encodings = self.accept_compression_encodings; 665 | let send_compression_encodings = self.send_compression_encodings; 666 | let inner = self.inner.clone(); 667 | let fut = async move { 668 | let inner = inner.0; 669 | let method = UpdateNewLandedBlocksDetectedSvc(inner); 670 | let codec = tonic::codec::ProstCodec::default(); 671 | let mut grpc = tonic::server::Grpc::new(codec) 672 | .apply_compression_config( 673 | accept_compression_encodings, 674 | send_compression_encodings, 675 | ); 676 | let res = grpc.unary(method, req).await; 677 | Ok(res) 678 | }; 679 | Box::pin(fut) 680 | } 681 | "/bidding_service.BiddingService/UpdateFailedReadingNewLandedBlocks" => { 682 | #[allow(non_camel_case_types)] 683 | struct UpdateFailedReadingNewLandedBlocksSvc( 684 | pub Arc, 685 | ); 686 | impl tonic::server::UnaryService 687 | for UpdateFailedReadingNewLandedBlocksSvc { 688 | type Response = super::Empty; 689 | type Future = BoxFuture< 690 | tonic::Response, 691 | tonic::Status, 692 | >; 693 | fn call( 694 | &mut self, 695 | request: tonic::Request, 696 | ) -> Self::Future { 697 | let inner = self.0.clone(); 698 | let fut = async move { 699 | (*inner) 700 | .update_failed_reading_new_landed_blocks(request) 701 | .await 702 | }; 703 | Box::pin(fut) 704 | } 705 | } 706 | let accept_compression_encodings = self.accept_compression_encodings; 707 | let send_compression_encodings = self.send_compression_encodings; 708 | let inner = self.inner.clone(); 709 | let fut = async move { 710 | let inner = inner.0; 711 | let method = UpdateFailedReadingNewLandedBlocksSvc(inner); 712 | let codec = tonic::codec::ProstCodec::default(); 713 | let mut grpc = tonic::server::Grpc::new(codec) 714 | .apply_compression_config( 715 | accept_compression_encodings, 716 | send_compression_encodings, 717 | ); 718 | let res = grpc.unary(method, req).await; 719 | Ok(res) 720 | }; 721 | Box::pin(fut) 722 | } 723 | "/bidding_service.BiddingService/NewBlock" => { 724 | #[allow(non_camel_case_types)] 725 | struct NewBlockSvc(pub Arc); 726 | impl< 727 | T: BiddingService, 728 | > tonic::server::UnaryService 729 | for NewBlockSvc { 730 | type Response = super::Empty; 731 | type Future = BoxFuture< 732 | tonic::Response, 733 | tonic::Status, 734 | >; 735 | fn call( 736 | &mut self, 737 | request: tonic::Request, 738 | ) -> Self::Future { 739 | let inner = self.0.clone(); 740 | let fut = async move { (*inner).new_block(request).await }; 741 | Box::pin(fut) 742 | } 743 | } 744 | let accept_compression_encodings = self.accept_compression_encodings; 745 | let send_compression_encodings = self.send_compression_encodings; 746 | let inner = self.inner.clone(); 747 | let fut = async move { 748 | let inner = inner.0; 749 | let method = NewBlockSvc(inner); 750 | let codec = tonic::codec::ProstCodec::default(); 751 | let mut grpc = tonic::server::Grpc::new(codec) 752 | .apply_compression_config( 753 | accept_compression_encodings, 754 | send_compression_encodings, 755 | ); 756 | let res = grpc.unary(method, req).await; 757 | Ok(res) 758 | }; 759 | Box::pin(fut) 760 | } 761 | "/bidding_service.BiddingService/UpdateNewBid" => { 762 | #[allow(non_camel_case_types)] 763 | struct UpdateNewBidSvc(pub Arc); 764 | impl< 765 | T: BiddingService, 766 | > tonic::server::UnaryService 767 | for UpdateNewBidSvc { 768 | type Response = super::Empty; 769 | type Future = BoxFuture< 770 | tonic::Response, 771 | tonic::Status, 772 | >; 773 | fn call( 774 | &mut self, 775 | request: tonic::Request, 776 | ) -> Self::Future { 777 | let inner = self.0.clone(); 778 | let fut = async move { 779 | (*inner).update_new_bid(request).await 780 | }; 781 | Box::pin(fut) 782 | } 783 | } 784 | let accept_compression_encodings = self.accept_compression_encodings; 785 | let send_compression_encodings = self.send_compression_encodings; 786 | let inner = self.inner.clone(); 787 | let fut = async move { 788 | let inner = inner.0; 789 | let method = UpdateNewBidSvc(inner); 790 | let codec = tonic::codec::ProstCodec::default(); 791 | let mut grpc = tonic::server::Grpc::new(codec) 792 | .apply_compression_config( 793 | accept_compression_encodings, 794 | send_compression_encodings, 795 | ); 796 | let res = grpc.unary(method, req).await; 797 | Ok(res) 798 | }; 799 | Box::pin(fut) 800 | } 801 | _ => { 802 | Box::pin(async move { 803 | Ok( 804 | http::Response::builder() 805 | .status(200) 806 | .header("grpc-status", "12") 807 | .header("content-type", "application/grpc") 808 | .body(empty_body()) 809 | .unwrap(), 810 | ) 811 | }) 812 | } 813 | } 814 | } 815 | } 816 | impl Clone for BiddingServiceServer { 817 | fn clone(&self) -> Self { 818 | let inner = self.inner.clone(); 819 | Self { 820 | inner, 821 | accept_compression_encodings: self.accept_compression_encodings, 822 | send_compression_encodings: self.send_compression_encodings, 823 | } 824 | } 825 | } 826 | impl Clone for _Inner { 827 | fn clone(&self) -> Self { 828 | Self(self.0.clone()) 829 | } 830 | } 831 | impl std::fmt::Debug for _Inner { 832 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 833 | write!(f, "{:?}", self.0) 834 | } 835 | } 836 | impl tonic::server::NamedService for BiddingServiceServer { 837 | const NAME: &'static str = "bidding_service.BiddingService"; 838 | } 839 | } 840 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/client/bidding_service_client_adapter.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::U256; 2 | use rbuilder::{ 3 | live_builder::block_output::bidding::interfaces::{ 4 | BiddingServiceWinControl, LandedBlockInfo as RealLandedBlockInfo, 5 | }, 6 | utils::{build_info::Version, timestamp_us_to_offset_datetime}, 7 | }; 8 | use std::{ 9 | path::PathBuf, 10 | sync::{atomic::AtomicBool, Arc}, 11 | }; 12 | use tokio::sync::mpsc; 13 | use tokio_stream::StreamExt; 14 | use tonic::transport::{Channel, Endpoint, Uri}; 15 | use tower::service_fn; 16 | use tracing::error; 17 | 18 | use crate::{ 19 | bidding_service_wrapper::{ 20 | bidding_service_client::BiddingServiceClient, conversion::real2rpc_landed_block_info, 21 | CreateSlotBidderParams, DestroySlotBidderParams, Empty, LandedBlocksParams, 22 | MustWinBlockParams, NewBlockParams, UpdateNewBidParams, 23 | }, 24 | block_descriptor_bidding::traits::{Bid, BidMaker, BiddingService, BlockId, SlotBidder}, 25 | metrics::set_bidding_service_version, 26 | }; 27 | 28 | use super::slot_bidder_client::SlotBidderClient; 29 | 30 | pub struct CreateSlotBidderCommandData { 31 | params: CreateSlotBidderParams, 32 | bid_maker: Box, 33 | cancel: tokio_util::sync::CancellationToken, 34 | can_use_suggested_fee_recipient_as_coinbase: Arc, 35 | } 36 | 37 | pub enum BiddingServiceClientCommand { 38 | CreateSlotBidder(CreateSlotBidderCommandData), 39 | NewBlock(NewBlockParams), 40 | UpdateNewBid(UpdateNewBidParams), 41 | MustWinBlock(MustWinBlockParams), 42 | UpdateNewLandedBlocksDetected(LandedBlocksParams), 43 | UpdateFailedReadingNewLandedBlocks, 44 | DestroySlotBidder(DestroySlotBidderParams), 45 | } 46 | 47 | /// Adapts [BiddingServiceClient] to [BiddingService]. 48 | /// To adapt sync world ([BiddingService]) to async ([BiddingServiceClient]) it receives commands via a channel (commands_sender) 49 | /// which is handled by a tokio task. 50 | /// It creates a SlotBidderClient implementing SlotBidder per create_slot_bidder call. 51 | /// For each SlotBidderClient created a task is created to poll callbacks (eg: bids and can_use_suggested_fee_recipient_as_coinbase updates). 52 | /// The created SlotBidderClient forwards all calls to the BiddingServiceClientAdapter as commands. 53 | #[derive(Debug)] 54 | pub struct BiddingServiceClientAdapter { 55 | commands_sender: mpsc::UnboundedSender, 56 | win_control: Arc, 57 | last_session_id: u64, 58 | } 59 | 60 | #[derive(thiserror::Error, Debug)] 61 | pub enum Error { 62 | #[error("Unable to connect : {0}")] 63 | TonicTrasport(#[from] tonic::transport::Error), 64 | #[error("RPC error : {0}")] 65 | TonicStatus(#[from] tonic::Status), 66 | #[error("Initialization failed : {0}")] 67 | InitFailed(tonic::Status), 68 | } 69 | 70 | pub type Result = core::result::Result; 71 | 72 | impl BiddingServiceClientAdapter { 73 | /// @Remove async and reconnect on all create_slot_bidder calls. 74 | pub async fn new( 75 | uds_path: &str, 76 | landed_blocks_history: &[RealLandedBlockInfo], 77 | ) -> Result { 78 | let commands_sender = Self::init_sender_task(uds_path, landed_blocks_history).await?; 79 | let win_control = Arc::new(BiddingServiceWinControlAdapter { 80 | commands_sender: commands_sender.clone(), 81 | }); 82 | Ok(Self { 83 | commands_sender, 84 | win_control, 85 | last_session_id: 0, 86 | }) 87 | } 88 | 89 | fn new_session_id(&mut self) -> u64 { 90 | self.last_session_id += 1; 91 | self.last_session_id 92 | } 93 | 94 | async fn init_sender_task( 95 | uds_path: &str, 96 | landed_blocks_history: &[RealLandedBlockInfo], 97 | ) -> Result> { 98 | let uds_path = uds_path.to_string(); 99 | // Url us dummy but needed to create the Endpoint. 100 | let channel = Endpoint::try_from("http://[::]:50051") 101 | .unwrap() 102 | .connect_with_connector(service_fn(move |_: Uri| { 103 | // Connect to a Uds socket 104 | let path = PathBuf::from(uds_path.clone()); 105 | tokio::net::UnixStream::connect(path) 106 | })) 107 | .await?; 108 | // Create a client 109 | let mut client = BiddingServiceClient::new(channel); 110 | let init_params = LandedBlocksParams { 111 | landed_block_info: landed_blocks_history 112 | .iter() 113 | .map(real2rpc_landed_block_info) 114 | .collect(), 115 | }; 116 | let bidding_service_version = client 117 | .initialize(init_params) 118 | .await 119 | .map_err(Error::InitFailed)?; 120 | let bidding_service_version = bidding_service_version.into_inner(); 121 | set_bidding_service_version(Version { 122 | git_commit: bidding_service_version.git_commit, 123 | git_ref: bidding_service_version.git_ref, 124 | build_time_utc: bidding_service_version.build_time_utc, 125 | }); 126 | let (commands_sender, mut rx) = mpsc::unbounded_channel::(); 127 | // Spawn a task to execute received futures 128 | tokio::spawn(async move { 129 | while let Some(command) = rx.recv().await { 130 | match command { 131 | BiddingServiceClientCommand::CreateSlotBidder(create_slot_data) => { 132 | Self::create_slot_bidder(&mut client, create_slot_data).await; 133 | } 134 | BiddingServiceClientCommand::NewBlock(new_block_params) => { 135 | Self::handle_error(client.new_block(new_block_params).await); 136 | } 137 | BiddingServiceClientCommand::UpdateNewBid(update_new_bid_params) => { 138 | Self::handle_error(client.update_new_bid(update_new_bid_params).await); 139 | } 140 | BiddingServiceClientCommand::MustWinBlock(must_win_block_params) => { 141 | Self::handle_error(client.must_win_block(must_win_block_params).await); 142 | } 143 | BiddingServiceClientCommand::UpdateNewLandedBlocksDetected(params) => { 144 | Self::handle_error(client.update_new_landed_blocks_detected(params).await); 145 | } 146 | BiddingServiceClientCommand::UpdateFailedReadingNewLandedBlocks => { 147 | Self::handle_error( 148 | client 149 | .update_failed_reading_new_landed_blocks(Empty {}) 150 | .await, 151 | ); 152 | } 153 | BiddingServiceClientCommand::DestroySlotBidder(destroy_slot_bidder_params) => { 154 | Self::handle_error( 155 | client.destroy_slot_bidder(destroy_slot_bidder_params).await, 156 | ); 157 | } 158 | } 159 | } 160 | }); 161 | Ok(commands_sender) 162 | } 163 | 164 | fn parse_option_u256(limbs: Vec) -> Option { 165 | if limbs.is_empty() { 166 | None 167 | } else { 168 | Some(U256::from_limbs_slice(&limbs)) 169 | } 170 | } 171 | 172 | /// Calls create_slot_bidder via RPC to init the bidder. 173 | async fn create_slot_bidder( 174 | client: &mut BiddingServiceClient, 175 | create_slot_bidder_data: CreateSlotBidderCommandData, 176 | ) { 177 | match client 178 | .create_slot_bidder(create_slot_bidder_data.params) 179 | .await 180 | { 181 | Ok(response) => { 182 | let mut stream = response.into_inner(); 183 | tokio::spawn(async move { 184 | loop { 185 | tokio::select! { 186 | _ = create_slot_bidder_data.cancel.cancelled() => { 187 | return; 188 | } 189 | callback = stream.next() => { 190 | if let Some(Ok(callback)) = callback { 191 | if let Some(bid) = callback.bid { 192 | let payout_tx_value = Self::parse_option_u256(bid.payout_tx_value); 193 | let seen_competition_bid = Self::parse_option_u256(bid.seen_competition_bid); 194 | let trigger_creation_time = bid.trigger_creation_time_us.map(timestamp_us_to_offset_datetime); 195 | create_slot_bidder_data.bid_maker.send_bid(Bid{block_id:BlockId(bid.block_id),payout_tx_value,seen_competition_bid, trigger_creation_time }); 196 | } else if let Some(can_use_suggested_fee_recipient_as_coinbase_change) = callback.can_use_suggested_fee_recipient_as_coinbase_change { 197 | create_slot_bidder_data.can_use_suggested_fee_recipient_as_coinbase.store(can_use_suggested_fee_recipient_as_coinbase_change,std::sync::atomic::Ordering::SeqCst); 198 | } 199 | } 200 | else { 201 | return; 202 | } 203 | } 204 | } 205 | } 206 | }); 207 | } 208 | Err(err) => { 209 | Self::handle_error(Err(err)); 210 | } 211 | }; 212 | } 213 | 214 | /// If error logs it. 215 | /// return result is error 216 | fn handle_error(result: tonic::Result>) -> bool { 217 | if let Err(error) = &result { 218 | error!(error=?error,"RPC call error, killing process so it reconnects"); 219 | std::process::exit(1); 220 | } else { 221 | false 222 | } 223 | } 224 | } 225 | 226 | impl BiddingService for BiddingServiceClientAdapter { 227 | fn create_slot_bidder( 228 | &mut self, 229 | block: u64, 230 | slot: u64, 231 | slot_timestamp: time::OffsetDateTime, 232 | bid_maker: Box, 233 | cancel: tokio_util::sync::CancellationToken, 234 | ) -> Arc { 235 | // This default will be immediately changed by a callback. 236 | let can_use_suggested_fee_recipient_as_coinbase = Arc::new(AtomicBool::new(false)); 237 | let session_id = self.new_session_id(); 238 | let _ = self 239 | .commands_sender 240 | .send(BiddingServiceClientCommand::CreateSlotBidder( 241 | CreateSlotBidderCommandData { 242 | params: CreateSlotBidderParams { 243 | block, 244 | slot, 245 | session_id, 246 | slot_timestamp: slot_timestamp.unix_timestamp(), 247 | }, 248 | bid_maker, 249 | cancel, 250 | can_use_suggested_fee_recipient_as_coinbase: 251 | can_use_suggested_fee_recipient_as_coinbase.clone(), 252 | }, 253 | )); 254 | Arc::new(SlotBidderClient::new( 255 | session_id, 256 | self.commands_sender.clone(), 257 | can_use_suggested_fee_recipient_as_coinbase, 258 | )) 259 | } 260 | 261 | fn win_control(&self) -> Arc { 262 | self.win_control.clone() 263 | } 264 | 265 | fn update_new_landed_blocks_detected(&mut self, landed_blocks: &[RealLandedBlockInfo]) { 266 | let param = LandedBlocksParams { 267 | landed_block_info: landed_blocks 268 | .iter() 269 | .map(real2rpc_landed_block_info) 270 | .collect(), 271 | }; 272 | let _ = 273 | self.commands_sender 274 | .send(BiddingServiceClientCommand::UpdateNewLandedBlocksDetected( 275 | param, 276 | )); 277 | } 278 | 279 | fn update_failed_reading_new_landed_blocks(&mut self) { 280 | let _ = self 281 | .commands_sender 282 | .send(BiddingServiceClientCommand::UpdateFailedReadingNewLandedBlocks); 283 | } 284 | } 285 | 286 | #[derive(Debug)] 287 | struct BiddingServiceWinControlAdapter { 288 | commands_sender: mpsc::UnboundedSender, 289 | } 290 | 291 | impl BiddingServiceWinControl for BiddingServiceWinControlAdapter { 292 | fn must_win_block(&self, block: u64) { 293 | let _ = self 294 | .commands_sender 295 | .send(BiddingServiceClientCommand::MustWinBlock( 296 | MustWinBlockParams { block }, 297 | )); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/client/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bidding_service_client_adapter; 2 | mod slot_bidder_client; 3 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/client/slot_bidder_client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{atomic::AtomicBool, Arc}; 2 | 3 | use rbuilder::{ 4 | live_builder::block_output::bid_value_source::interfaces::{BidValueObs, CompetitionBid}, 5 | utils::offset_datetime_to_timestamp_us, 6 | }; 7 | use tokio::sync::mpsc; 8 | 9 | use crate::{ 10 | bidding_service_wrapper::{DestroySlotBidderParams, NewBlockParams, UpdateNewBidParams}, 11 | block_descriptor_bidding::traits::{BlockDescriptor, SlotBidder, UnfinishedBlockBuildingSink}, 12 | }; 13 | 14 | use super::bidding_service_client_adapter::BiddingServiceClientCommand; 15 | 16 | /// Implementation of SlotBidder. 17 | /// Commands are forwarded everything to a UnboundedSender. 18 | /// BidMaker is wrapped with ... that contains a poling task that makes the bids. 19 | #[derive(Debug)] 20 | pub struct SlotBidderClient { 21 | session_id: u64, 22 | commands_sender: mpsc::UnboundedSender, 23 | can_use_suggested_fee_recipient_as_coinbase: Arc, 24 | } 25 | 26 | impl SlotBidderClient { 27 | pub fn new( 28 | session_id: u64, 29 | commands_sender: mpsc::UnboundedSender, 30 | can_use_suggested_fee_recipient_as_coinbase: Arc, 31 | ) -> Self { 32 | SlotBidderClient { 33 | commands_sender, 34 | can_use_suggested_fee_recipient_as_coinbase, 35 | session_id, 36 | } 37 | } 38 | } 39 | 40 | impl UnfinishedBlockBuildingSink for SlotBidderClient { 41 | fn new_block(&self, block_descriptor: BlockDescriptor) { 42 | let _ = self 43 | .commands_sender 44 | .send(BiddingServiceClientCommand::NewBlock(NewBlockParams { 45 | session_id: self.session_id, 46 | true_block_value: block_descriptor.true_block_value().as_limbs().to_vec(), 47 | can_add_payout_tx: block_descriptor.can_add_payout_tx(), 48 | block_id: block_descriptor.id().0, 49 | creation_time_us: offset_datetime_to_timestamp_us(block_descriptor.creation_time()), 50 | })); 51 | } 52 | 53 | fn can_use_suggested_fee_recipient_as_coinbase(&self) -> bool { 54 | self.can_use_suggested_fee_recipient_as_coinbase 55 | .load(std::sync::atomic::Ordering::SeqCst) 56 | } 57 | } 58 | 59 | impl BidValueObs for SlotBidderClient { 60 | fn update_new_bid(&self, bid: CompetitionBid) { 61 | let _ = self 62 | .commands_sender 63 | .send(BiddingServiceClientCommand::UpdateNewBid( 64 | UpdateNewBidParams { 65 | session_id: self.session_id, 66 | bid: bid.bid().as_limbs().to_vec(), 67 | creation_time_us: offset_datetime_to_timestamp_us(bid.creation_time()), 68 | }, 69 | )); 70 | } 71 | } 72 | 73 | impl SlotBidder for SlotBidderClient {} 74 | 75 | impl Drop for SlotBidderClient { 76 | fn drop(&mut self) { 77 | let _ = self 78 | .commands_sender 79 | .send(BiddingServiceClientCommand::DestroySlotBidder( 80 | DestroySlotBidderParams { 81 | session_id: self.session_id, 82 | }, 83 | )); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/conversion.rs: -------------------------------------------------------------------------------- 1 | //! Conversion real data <-> rpc data 2 | use crate::bidding_service_wrapper::LandedBlockInfo as RPCLandedBlockInfo; 3 | 4 | use alloy_primitives::U256; 5 | use rbuilder::live_builder::block_output::bidding::interfaces::LandedBlockInfo as RealLandedBlockInfo; 6 | use time::OffsetDateTime; 7 | use tonic::Status; 8 | 9 | pub fn real2rpc_landed_block_info(l: &RealLandedBlockInfo) -> RPCLandedBlockInfo { 10 | RPCLandedBlockInfo { 11 | block_number: l.block_number, 12 | block_timestamp: l.block_timestamp.unix_timestamp(), 13 | builder_balance: l.builder_balance.as_limbs().to_vec(), 14 | beneficiary_is_builder: l.beneficiary_is_builder, 15 | } 16 | } 17 | 18 | #[allow(clippy::result_large_err)] 19 | pub fn rpc2real_landed_block_info(l: &RPCLandedBlockInfo) -> Result { 20 | Ok(RealLandedBlockInfo { 21 | block_number: l.block_number, 22 | block_timestamp: OffsetDateTime::from_unix_timestamp(l.block_timestamp) 23 | .map_err(|_| Status::invalid_argument("block_timestamp"))?, 24 | builder_balance: U256::from_limbs_slice(&l.builder_balance), 25 | beneficiary_is_builder: l.beneficiary_is_builder, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/mod.rs: -------------------------------------------------------------------------------- 1 | #[rustfmt::skip] 2 | pub mod bidding_service; 3 | pub mod client; 4 | pub mod conversion; 5 | pub use bidding_service::*; 6 | -------------------------------------------------------------------------------- /src/bidding_service_wrapper/proto/bidding_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package bidding_service; 3 | 4 | // Protocol for the bidding service. It's used to marshal all the traits in src/block_descriptor_bidding/traits.rs 5 | // Usage: 6 | // The client connects to the server and calls Initialize, this call should create the real BiddingService on the server side. 7 | // Before calling Initialize any other call will fail. Initialize can be called again to recreate the BiddingService (eg: rbuilder reconnection). 8 | // After that, for each slot the client should call CreateSlotBidder to create the SlotBidder on the server side and DestroySlotBidder when the SlotBidder is not needed anymore. 9 | // Other calls are almost 1 to 1 with the original traits but for SlotBidder calls block/slot are added to identify the SlotBidder. 10 | // Notice that CreateSlotBidder returns a stream of Callback. This stream is used for 2 things: 11 | // - Send back bids made by the SlotBidder. 12 | // - Notify changes on the state of SlotBidder's can_use_suggested_fee_recipient_as_coinbase. We use this methodology instead of a 13 | // forward RPC call since can_use_suggested_fee_recipient_as_coinbase almost does not change and we want to avoid innecesary RPC calls during block building. 14 | service BiddingService { 15 | 16 | // Call after connection before calling anything. This will really create the BiddingService on the server side. 17 | // Returns the version info for the server side. 18 | rpc Initialize(LandedBlocksParams) returns (BidderVersionInfo); 19 | 20 | // BiddingService 21 | rpc CreateSlotBidder(CreateSlotBidderParams) returns (stream Callback); 22 | rpc DestroySlotBidder(DestroySlotBidderParams) returns (Empty); 23 | rpc MustWinBlock(MustWinBlockParams) returns (Empty); 24 | rpc UpdateNewLandedBlocksDetected(LandedBlocksParams) returns (Empty); 25 | rpc UpdateFailedReadingNewLandedBlocks(Empty) returns (Empty); 26 | 27 | 28 | // SlotBidder->UnfinishedBlockBuildingSink 29 | rpc NewBlock(NewBlockParams) returns (Empty); 30 | 31 | // SlotBidder->BidValueObs 32 | rpc UpdateNewBid(UpdateNewBidParams) returns (Empty); 33 | 34 | 35 | } 36 | // Not using sub messages to avoid the extra Option generated in rust code. 37 | // uint64 block + uint64 slot should be something like BidderId 38 | 39 | 40 | // Mapping of build_info::Version 41 | message BidderVersionInfo { 42 | string git_commit = 1; 43 | string git_ref = 2; 44 | string build_time_utc = 3; 45 | } 46 | 47 | message Empty { 48 | } 49 | 50 | 51 | message MustWinBlockParams { 52 | uint64 block = 1; 53 | } 54 | 55 | message UpdateNewBidParams { 56 | uint64 session_id = 1; 57 | repeated uint64 bid = 2; // Array of 4 uint64 58 | uint64 creation_time_us = 3; 59 | } 60 | 61 | message NewBlockParams{ 62 | uint64 session_id = 1; 63 | repeated uint64 true_block_value = 2; // Array of 4 uint64 64 | bool can_add_payout_tx = 3; 65 | uint64 block_id = 4; 66 | uint64 creation_time_us = 5; 67 | } 68 | 69 | message DestroySlotBidderParams { 70 | uint64 session_id = 1; 71 | } 72 | 73 | message CreateSlotBidderParams { 74 | uint64 block = 1; 75 | uint64 slot = 2; 76 | // Id identifying the session. Used in all following calls. 77 | uint64 session_id = 3; 78 | // unix ts 79 | int64 slot_timestamp = 4; 80 | } 81 | 82 | 83 | // Info about a onchain block from reth. 84 | message LandedBlockInfo { 85 | uint64 block_number = 1; 86 | int64 block_timestamp = 2; 87 | repeated uint64 builder_balance = 3; // Array of 4 uint64 88 | // true -> we landed this block. 89 | // If false we could have landed it in coinbase == fee recipient mode but balance wouldn't change so we don't care. 90 | bool beneficiary_is_builder = 4; 91 | } 92 | 93 | message LandedBlocksParams { 94 | repeated LandedBlockInfo landed_block_info = 1; // Added field name 95 | } 96 | 97 | message Bid { 98 | // Optional implicitly by allowing empty 99 | repeated uint64 payout_tx_value = 1; // Array of 4 uint64 100 | uint64 block_id = 2; 101 | // Optional implicitly by allowing empty 102 | repeated uint64 seen_competition_bid = 3; // Array of 4 uint64 103 | optional uint64 trigger_creation_time_us = 4; 104 | } 105 | 106 | 107 | // Exactly 1 member will be not null. 108 | // Since this is not mapped to an enum we must be careful to manually update BiddingServiceClientAdapter. 109 | message Callback { 110 | Bid bid = 1; 111 | optional bool can_use_suggested_fee_recipient_as_coinbase_change = 2; 112 | } 113 | -------------------------------------------------------------------------------- /src/bin/backtest-build-block.rs: -------------------------------------------------------------------------------- 1 | use rbuilder::backtest::build_block::landed_block_from_db::run_backtest; 2 | use rbuilder_operator::flashbots_config::FlashbotsConfig; 3 | 4 | #[tokio::main] 5 | async fn main() -> eyre::Result<()> { 6 | run_backtest::().await 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/backtest-build-range.rs: -------------------------------------------------------------------------------- 1 | use rbuilder::backtest::run_backtest_build_range; 2 | use rbuilder_operator::flashbots_config::FlashbotsConfig; 3 | 4 | #[tokio::main] 5 | async fn main() -> eyre::Result<()> { 6 | run_backtest_build_range::().await 7 | } 8 | -------------------------------------------------------------------------------- /src/bin/rbuilder.rs: -------------------------------------------------------------------------------- 1 | use rbuilder::live_builder::cli::{self}; 2 | use rbuilder_operator::{ 3 | build_info::{print_version_info, rbuilder_version}, 4 | flashbots_config::FlashbotsConfig, 5 | }; 6 | use tracing::info; 7 | 8 | fn on_run() { 9 | info!(version = ?rbuilder_version(), "Flashbots rbuilder version"); 10 | } 11 | 12 | #[tokio::main] 13 | async fn main() -> eyre::Result<()> { 14 | return cli::run::(print_version_info, Some(on_run)).await; 15 | } 16 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/bid_maker_adapter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::metrics::add_trigger_to_bid_round_trip_time; 4 | 5 | use super::{ 6 | block_registry::BlockRegistry, 7 | traits::{Bid, BidMaker}, 8 | }; 9 | use parking_lot::Mutex; 10 | use rbuilder::live_builder::block_output::bidding::interfaces::Bid as FullBid; 11 | use rbuilder::live_builder::block_output::bidding::interfaces::BidMaker as FullBidMaker; 12 | use time::OffsetDateTime; 13 | use tracing::error; 14 | 15 | /// Adapts by translating BlockId -> BlockBuildingHelper via a block_registry 16 | #[derive(Debug)] 17 | pub struct BidMakerAdapter { 18 | full_bid_maker: Box, 19 | block_registry: Arc>, 20 | } 21 | 22 | impl BidMakerAdapter { 23 | pub fn new( 24 | full_bid_maker: Box, 25 | block_registry: Arc>, 26 | ) -> Self { 27 | Self { 28 | full_bid_maker, 29 | block_registry, 30 | } 31 | } 32 | } 33 | 34 | impl BidMaker for BidMakerAdapter { 35 | fn send_bid(&self, bid: Bid) { 36 | match self.block_registry.lock().get_block_clon(bid.block_id) { 37 | Some(block) => { 38 | if let Some(trigger_creation_time) = &bid.trigger_creation_time { 39 | let network_round_trip = OffsetDateTime::now_utc() - *trigger_creation_time; 40 | add_trigger_to_bid_round_trip_time(network_round_trip); 41 | } 42 | self.full_bid_maker.send_bid(FullBid::new( 43 | block, 44 | bid.payout_tx_value, 45 | bid.seen_competition_bid, 46 | )) 47 | } 48 | None => error!("Tried to bid with lost block"), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/bidding_service_adapter.rs: -------------------------------------------------------------------------------- 1 | //! We use FullXXX aliases for the original versions using full block info. 2 | use std::sync::Arc; 3 | 4 | use parking_lot::Mutex; 5 | use rbuilder::live_builder::block_output::bidding::interfaces::{ 6 | BidMaker as FullBidMaker, BiddingService as FullBiddingService, BiddingServiceWinControl, 7 | LandedBlockInfo, SlotBidder as FullSlotBidder, 8 | }; 9 | use time::OffsetDateTime; 10 | use tokio_util::sync::CancellationToken; 11 | 12 | use crate::block_descriptor_bidding::{ 13 | bid_maker_adapter::BidMakerAdapter, block_registry::BlockRegistry, 14 | }; 15 | 16 | use super::{slot_bidder_adapter::SlotBidderAdapter, traits::BiddingService}; 17 | 18 | /// We need to make sure that a block is not deleted between the bidder sees it and tries to use it. 19 | /// We assume that after getting a better block the bidder will stop using the previews one so he can only use 20 | /// the previous one if he was already making the bid. 21 | /// Worst case is making a bid via RPC, that takes no more than .5ms (usually .15ms). 22 | /// Generating a new block usually takes way more than .5ms but let's assume 10 blocks in .5ms. 23 | /// Assuming 5 building algorithms worst case would be 10 x 5 = 50. To play it safe we go with 100. 24 | /// This shouldn't be a lot of mem, just a few MB. 25 | const MAX_ACTIVE_BLOCKS: usize = 100; 26 | 27 | /// Adapter from simplified world to full world. 28 | #[derive(Debug)] 29 | pub struct BiddingServiceAdapter { 30 | bidding_service: Box, 31 | } 32 | 33 | impl BiddingServiceAdapter { 34 | pub fn new(bidding_service: Box) -> Self { 35 | Self { bidding_service } 36 | } 37 | } 38 | 39 | impl FullBiddingService for BiddingServiceAdapter { 40 | fn create_slot_bidder( 41 | &mut self, 42 | block: u64, 43 | slot: u64, 44 | slot_timestamp: OffsetDateTime, 45 | bid_maker: Box, 46 | cancel: CancellationToken, 47 | ) -> Arc { 48 | let block_registry = Arc::new(Mutex::new(BlockRegistry::new(MAX_ACTIVE_BLOCKS))); 49 | let wrapped_bid_maker = Box::new(BidMakerAdapter::new(bid_maker, block_registry.clone())); 50 | 51 | let bidder = self.bidding_service.create_slot_bidder( 52 | block, 53 | slot, 54 | slot_timestamp, 55 | wrapped_bid_maker, 56 | cancel, 57 | ); 58 | Arc::new(SlotBidderAdapter::new(bidder, block_registry)) 59 | } 60 | 61 | fn win_control(&self) -> Arc { 62 | self.bidding_service.win_control() 63 | } 64 | 65 | fn update_new_landed_blocks_detected(&mut self, landed_blocks: &[LandedBlockInfo]) { 66 | self.bidding_service 67 | .update_new_landed_blocks_detected(landed_blocks) 68 | } 69 | 70 | fn update_failed_reading_new_landed_blocks(&mut self) { 71 | self.bidding_service 72 | .update_failed_reading_new_landed_blocks() 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/block_registry.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use rbuilder::building::builders::block_building_helper::BiddableUnfinishedBlock; 4 | 5 | use super::traits::BlockId; 6 | 7 | /// Maintains a map from BlockId -> BlockBuildingHelper. 8 | /// Auto generates unique BlockIds. 9 | /// To avoid (possible RPC) reference count handling if only keeps the last max_blocks_to_keep blocks. 10 | /// We externally assume that we'll never need more. 11 | /// Overflows after 2^64 blocks but it's impossible to get to that in a single slot. 12 | pub struct BlockRegistry { 13 | last_generated_block_id: u64, 14 | /// Id of first block in blocks. The following blocks have sequential ids. 15 | first_block_id: u64, 16 | blocks: VecDeque, 17 | max_blocks_to_keep: usize, 18 | } 19 | 20 | impl std::fmt::Debug for BlockRegistry { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | f.debug_struct("BlockRegistry") 23 | .field("max_blocks_to_keep", &self.max_blocks_to_keep) 24 | .field("first_block_id", &self.first_block_id) 25 | .field("last_generated_block_id", &self.last_generated_block_id) 26 | .field("blocks_len", &self.blocks.len()) 27 | .finish_non_exhaustive() 28 | } 29 | } 30 | 31 | impl BlockRegistry { 32 | pub fn new(max_blocks_to_keep: usize) -> Self { 33 | Self { 34 | max_blocks_to_keep, 35 | // Fist generated item will be 1 since we pre increment last_generated_block_id. 36 | first_block_id: 1, 37 | last_generated_block_id: 0, 38 | blocks: Default::default(), 39 | } 40 | } 41 | 42 | pub fn add_block(&mut self, block: BiddableUnfinishedBlock) -> BlockId { 43 | self.blocks.push_back(block); 44 | if self.blocks.len() > self.max_blocks_to_keep { 45 | self.blocks.pop_front(); 46 | self.first_block_id += 1; 47 | } 48 | self.last_generated_block_id += 1; 49 | BlockId(self.last_generated_block_id) 50 | } 51 | 52 | pub fn _remove_older_than(&mut self, id: BlockId) { 53 | while self.first_block_id < id.0 && !self.blocks.is_empty() { 54 | self.blocks.pop_front(); 55 | self.first_block_id += 1; 56 | } 57 | } 58 | 59 | pub fn get_block_clon(&self, id: BlockId) -> Option { 60 | if id.0 < self.first_block_id || id.0 >= self.first_block_id + self.blocks.len() as u64 { 61 | return None; 62 | } 63 | Some(self.blocks[(id.0 - self.first_block_id) as usize].clone()) 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod tests { 69 | use alloy_primitives::U256; 70 | use rbuilder::building::builders::{ 71 | block_building_helper::BiddableUnfinishedBlock, 72 | mock_block_building_helper::MockBlockBuildingHelper, 73 | }; 74 | 75 | use super::BlockRegistry; 76 | const MAX_BLOCKS: usize = 10; 77 | #[test] 78 | fn add_block() { 79 | let mut registry = BlockRegistry::new(MAX_BLOCKS); 80 | let initial_true_block_value = 0; 81 | let mut block_ids = Vec::new(); 82 | for i in 0..=MAX_BLOCKS { 83 | let true_block_value = U256::from(i + initial_true_block_value); 84 | let can_add_payout_tx = i % 2 == 0; 85 | let block_id = registry.add_block( 86 | BiddableUnfinishedBlock::new(Box::new(MockBlockBuildingHelper::new( 87 | true_block_value, 88 | can_add_payout_tx, 89 | ))) 90 | .unwrap(), 91 | ); 92 | block_ids.push(block_id.clone()); 93 | let block = registry.get_block_clon(block_id).unwrap(); 94 | assert_eq!(block.true_block_value(), true_block_value); 95 | assert_eq!(block.can_add_payout_tx(), can_add_payout_tx); 96 | } 97 | // it should remember the last MAX_BLOCKS 98 | #[allow(clippy::needless_range_loop)] 99 | for i in 1..=MAX_BLOCKS { 100 | let true_block_value = U256::from(i + initial_true_block_value); 101 | let can_add_payout_tx = i % 2 == 0; 102 | let block = registry.get_block_clon(block_ids[i].clone()).unwrap(); 103 | assert_eq!(block.true_block_value(), true_block_value); 104 | assert_eq!(block.can_add_payout_tx(), can_add_payout_tx); 105 | } 106 | // the oldest should not be stored. 107 | assert!(registry.get_block_clon(block_ids[0].clone()).is_none()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/mock_bidding_service.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use ahash::HashMap; 4 | use mockall::automock; 5 | use parking_lot::Mutex; 6 | use rbuilder::live_builder::block_output::bidding::interfaces::{ 7 | BiddingServiceWinControl, LandedBlockInfo, MockBiddingServiceWinControl, 8 | }; 9 | 10 | use super::{mock_slot_bidder::MockSlotBidder, traits::BiddingService}; 11 | 12 | #[automock] 13 | pub trait PartialBiddingService { 14 | fn update_new_landed_blocks_detected(&mut self, landed_blocks: &[LandedBlockInfo]); 15 | fn update_failed_reading_new_landed_blocks(&mut self); 16 | } 17 | 18 | /// Slot + block to use on maps 19 | #[derive(Debug, Eq, Hash, PartialEq, Clone)] 20 | pub struct SlotAndBlock { 21 | pub block: u64, 22 | pub slot: u64, 23 | } 24 | 25 | /// Custom mock for BiddingService. 26 | /// Usage: 27 | /// Create a MockBiddingService via new. 28 | /// Fill all the internal fields to make MockBiddingService return what you need. 29 | #[derive(Debug)] 30 | pub struct MockBiddingService { 31 | pub mock_partial_bidding_service: MockPartialBiddingService, 32 | pub mock_bidding_service_win_control: Arc, 33 | /// When create_slot_bidder is called the bidder from bidders will be returned. 34 | /// This is though as a single use thing, don't call create_slot_bidder twice for the same slot/block. 35 | pub bidders: HashMap>, 36 | /// BidMaker we received on create_slot_bidder. 37 | pub bid_makers: 38 | Arc>>>, 39 | } 40 | 41 | impl Default for MockBiddingService { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | 47 | impl MockBiddingService { 48 | pub fn new() -> Self { 49 | MockBiddingService { 50 | mock_partial_bidding_service: MockPartialBiddingService::new(), 51 | mock_bidding_service_win_control: Arc::new(MockBiddingServiceWinControl::new()), 52 | bidders: Default::default(), 53 | bid_makers: Default::default(), 54 | } 55 | } 56 | } 57 | 58 | impl BiddingService for MockBiddingService { 59 | fn create_slot_bidder( 60 | &mut self, 61 | block: u64, 62 | slot: u64, 63 | _slot_timestamp: time::OffsetDateTime, 64 | bid_maker: Box, 65 | _cancel: tokio_util::sync::CancellationToken, 66 | ) -> std::sync::Arc { 67 | let slot_block = SlotAndBlock { block, slot }; 68 | self.bid_makers.lock().insert(slot_block.clone(), bid_maker); 69 | self.bidders.get(&slot_block).unwrap().clone() 70 | } 71 | 72 | fn win_control(&self) -> Arc { 73 | self.mock_bidding_service_win_control.clone() 74 | } 75 | 76 | fn update_new_landed_blocks_detected(&mut self, landed_blocks: &[LandedBlockInfo]) { 77 | self.mock_partial_bidding_service 78 | .update_new_landed_blocks_detected(landed_blocks) 79 | } 80 | 81 | fn update_failed_reading_new_landed_blocks(&mut self) { 82 | self.mock_partial_bidding_service 83 | .update_failed_reading_new_landed_blocks() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/mock_slot_bidder.rs: -------------------------------------------------------------------------------- 1 | use rbuilder::live_builder::block_output::bid_value_source::interfaces::{ 2 | BidValueObs, CompetitionBid, MockBidValueObs, 3 | }; 4 | 5 | use super::traits::{ 6 | BlockDescriptor, MockUnfinishedBlockBuildingSink, SlotBidder, UnfinishedBlockBuildingSink, 7 | }; 8 | 9 | /// Manually implemented since #[automock] seems to have a problem with trait inherence. 10 | #[derive(Debug)] 11 | pub struct MockSlotBidder { 12 | pub unfinished_sink: MockUnfinishedBlockBuildingSink, 13 | pub bid_value_obs: MockBidValueObs, 14 | } 15 | 16 | impl MockSlotBidder { 17 | pub fn new() -> Self { 18 | Self { 19 | unfinished_sink: MockUnfinishedBlockBuildingSink::new(), 20 | bid_value_obs: MockBidValueObs::new(), 21 | } 22 | } 23 | } 24 | 25 | impl Default for MockSlotBidder { 26 | fn default() -> Self { 27 | Self::new() 28 | } 29 | } 30 | 31 | impl UnfinishedBlockBuildingSink for MockSlotBidder { 32 | fn new_block(&self, block_descriptor: BlockDescriptor) { 33 | self.unfinished_sink.new_block(block_descriptor) 34 | } 35 | 36 | fn can_use_suggested_fee_recipient_as_coinbase(&self) -> bool { 37 | self.unfinished_sink 38 | .can_use_suggested_fee_recipient_as_coinbase() 39 | } 40 | } 41 | 42 | impl BidValueObs for MockSlotBidder { 43 | fn update_new_bid(&self, bid: CompetitionBid) { 44 | self.bid_value_obs.update_new_bid(bid) 45 | } 46 | } 47 | 48 | impl SlotBidder for MockSlotBidder {} 49 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a simplified version of the bidding world that passes a BlockDescriptor instead of a full BlockBuildingHelper which is a 2 | //! much heavier object. 3 | //! BlockDescriptor contains all the information that is needed for most simple bidding services that don't really look inside the block to bid. 4 | //! A wrapper between both worlds if provided here. 5 | 6 | mod bid_maker_adapter; 7 | pub mod bidding_service_adapter; 8 | mod block_registry; 9 | pub mod mock_bidding_service; 10 | pub mod mock_slot_bidder; 11 | mod slot_bidder_adapter; 12 | pub mod traits; 13 | 14 | pub type SlotBidderId = u64; 15 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/slot_bidder_adapter.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use parking_lot::Mutex; 4 | use rbuilder::{ 5 | building::builders::{ 6 | block_building_helper::BiddableUnfinishedBlock, 7 | UnfinishedBlockBuildingSink as FullUnfinishedBlockBuildingSink, 8 | }, 9 | live_builder::block_output::{ 10 | bid_value_source::interfaces::{BidValueObs, CompetitionBid}, 11 | bidding::interfaces::SlotBidder as FullSlotBidder, 12 | }, 13 | }; 14 | 15 | use super::{ 16 | block_registry::BlockRegistry, 17 | traits::{BlockDescriptor, SlotBidder}, 18 | }; 19 | 20 | /// Adapter from SlotBidder to FullSlotBidder. 21 | /// It uses a block_registry to go from BlockBuildingHelper->BlockId. 22 | #[derive(Debug)] 23 | pub struct SlotBidderAdapter { 24 | bidder: Arc, 25 | block_registry: Arc>, 26 | } 27 | 28 | impl SlotBidderAdapter { 29 | pub fn new(bidder: Arc, block_registry: Arc>) -> Self { 30 | Self { 31 | bidder, 32 | block_registry, 33 | } 34 | } 35 | } 36 | 37 | impl FullUnfinishedBlockBuildingSink for SlotBidderAdapter { 38 | fn new_block(&self, block: BiddableUnfinishedBlock) { 39 | let true_block_value = block.true_block_value(); 40 | let can_add_payout_tx = block.can_add_payout_tx(); 41 | let block_id = self.block_registry.lock().add_block(block); 42 | self.bidder.new_block(BlockDescriptor::new( 43 | true_block_value, 44 | can_add_payout_tx, 45 | block_id, 46 | )); 47 | } 48 | 49 | fn can_use_suggested_fee_recipient_as_coinbase(&self) -> bool { 50 | self.bidder.can_use_suggested_fee_recipient_as_coinbase() 51 | } 52 | } 53 | 54 | impl BidValueObs for SlotBidderAdapter { 55 | fn update_new_bid(&self, bid: CompetitionBid) { 56 | self.bidder.update_new_bid(bid) 57 | } 58 | } 59 | 60 | impl FullSlotBidder for SlotBidderAdapter {} 61 | -------------------------------------------------------------------------------- /src/block_descriptor_bidding/traits.rs: -------------------------------------------------------------------------------- 1 | //! To keep comment consistency comments from the original elements are not copied. 2 | use mockall::automock; 3 | use std::sync::Arc; 4 | 5 | use alloy_primitives::U256; 6 | use rbuilder::live_builder::block_output::{ 7 | bid_value_source::interfaces::BidValueObs, 8 | bidding::interfaces::{BiddingServiceWinControl, LandedBlockInfo}, 9 | }; 10 | use time::OffsetDateTime; 11 | use tokio_util::sync::CancellationToken; 12 | 13 | /// This design abuses of the fact that the u64 is going to come handy on RPC serialization. 14 | #[derive(Clone, Eq, PartialEq, Debug)] 15 | pub struct BlockId(pub u64); 16 | 17 | /// Selected information coming from a BlockBuildingHelper. 18 | #[derive(Clone, Eq, PartialEq, Debug)] 19 | pub struct BlockDescriptor { 20 | true_block_value: U256, 21 | can_add_payout_tx: bool, 22 | id: BlockId, 23 | /// For metrics 24 | creation_time: OffsetDateTime, 25 | } 26 | 27 | impl BlockDescriptor { 28 | pub fn new(true_block_value: U256, can_add_payout_tx: bool, id: BlockId) -> Self { 29 | Self { 30 | true_block_value, 31 | can_add_payout_tx, 32 | id, 33 | creation_time: OffsetDateTime::now_utc(), 34 | } 35 | } 36 | 37 | pub fn new_for_deserialization( 38 | true_block_value: U256, 39 | can_add_payout_tx: bool, 40 | id: BlockId, 41 | creation_time: OffsetDateTime, 42 | ) -> Self { 43 | Self { 44 | true_block_value, 45 | can_add_payout_tx, 46 | id, 47 | creation_time, 48 | } 49 | } 50 | 51 | pub fn true_block_value(&self) -> U256 { 52 | self.true_block_value 53 | } 54 | 55 | pub fn can_add_payout_tx(&self) -> bool { 56 | self.can_add_payout_tx 57 | } 58 | 59 | pub fn id(&self) -> &BlockId { 60 | &self.id 61 | } 62 | 63 | pub fn creation_time(&self) -> OffsetDateTime { 64 | self.creation_time 65 | } 66 | } 67 | /// Simplified version of [rbuilder::live_builder::block_output::bidding::interfaces::Bid] 68 | #[derive(Clone, Eq, PartialEq, Debug)] 69 | pub struct Bid { 70 | pub block_id: BlockId, 71 | pub payout_tx_value: Option, 72 | pub seen_competition_bid: Option, 73 | /// When this bid is a reaction so some event (eg: new block, new competition bid) we put here 74 | /// the creation time of that event so we can measure our reaction time. 75 | pub trigger_creation_time: Option, 76 | } 77 | 78 | /// Simplified version of [rbuilder::live_builder::block_output::bidding::interfaces::BidMaker] 79 | #[automock] 80 | pub trait BidMaker: std::fmt::Debug { 81 | fn send_bid(&self, bid: Bid); 82 | } 83 | 84 | /// Simplified version of [rbuilder::building::builders::UnfinishedBlockBuildingSink] 85 | #[automock] 86 | pub trait UnfinishedBlockBuildingSink: std::fmt::Debug + Send + Sync { 87 | fn new_block(&self, block_descriptor: BlockDescriptor); 88 | fn can_use_suggested_fee_recipient_as_coinbase(&self) -> bool; 89 | } 90 | 91 | /// Simplified version of [rbuilder::live_builder::block_output::bidding::interfaces::SlotBidder] 92 | pub trait SlotBidder: UnfinishedBlockBuildingSink + BidValueObs {} 93 | 94 | /// Simplified version of [rbuilder::live_builder::block_output::bidding::interfaces::BiddingService] 95 | pub trait BiddingService: std::fmt::Debug + Send + Sync { 96 | fn create_slot_bidder( 97 | &mut self, 98 | block: u64, 99 | slot: u64, 100 | slot_timestamp: OffsetDateTime, 101 | bid_maker: Box, 102 | cancel: CancellationToken, 103 | ) -> Arc; 104 | 105 | // Consider moving these 3 func could be on a parent trait (I didn't want to modify the original BiddingService yet). 106 | 107 | fn win_control(&self) -> Arc; 108 | 109 | fn update_new_landed_blocks_detected(&mut self, landed_blocks: &[LandedBlockInfo]); 110 | 111 | fn update_failed_reading_new_landed_blocks(&mut self); 112 | } 113 | -------------------------------------------------------------------------------- /src/blocks_processor.rs: -------------------------------------------------------------------------------- 1 | use alloy_primitives::{BlockHash, U256}; 2 | use exponential_backoff::Backoff; 3 | use jsonrpsee::{ 4 | core::{client::ClientT, traits::ToRpcParams}, 5 | http_client::{HttpClient, HttpClientBuilder}, 6 | }; 7 | use rbuilder::{ 8 | building::BuiltBlockTrace, 9 | live_builder::{block_output::bid_observer::BidObserver, payload_events::MevBoostSlotData}, 10 | mev_boost::submission::SubmitBlockRequest, 11 | primitives::{ 12 | serialize::{RawBundle, RawShareBundle}, 13 | Bundle, Order, 14 | }, 15 | utils::error_storage::store_error_event, 16 | }; 17 | use reth::primitives::SealedBlock; 18 | use serde::{Deserialize, Serialize}; 19 | use serde_json::value::RawValue; 20 | use serde_with::{serde_as, DisplayFromStr}; 21 | use std::{sync::Arc, time::Duration}; 22 | use time::format_description::well_known; 23 | use tracing::{error, trace, warn, Span}; 24 | 25 | use crate::metrics::inc_submit_block_errors; 26 | 27 | const BLOCK_PROCESSOR_ERROR_CATEGORY: &str = "block_processor"; 28 | const DEFAULT_BLOCK_CONSUME_BUILT_BLOCK_METHOD: &str = "block_consumeBuiltBlockV2"; 29 | pub const SIGNED_BLOCK_CONSUME_BUILT_BLOCK_METHOD: &str = "flashbots_consumeBuiltBlockV2"; 30 | 31 | #[derive(Debug, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | struct UsedSbundle { 34 | bundle: RawShareBundle, 35 | success: bool, 36 | } 37 | 38 | #[serde_as] 39 | #[derive(Debug, Clone, Serialize, Deserialize)] 40 | #[serde(rename_all = "camelCase")] 41 | struct UsedBundle { 42 | #[serde_as(as = "DisplayFromStr")] 43 | mev_gas_price: U256, 44 | #[serde_as(as = "DisplayFromStr")] 45 | total_eth: U256, 46 | #[serde_as(as = "DisplayFromStr")] 47 | eth_send_to_coinbase: U256, 48 | #[serde_as(as = "DisplayFromStr")] 49 | total_gas_used: u64, 50 | original_bundle: RawBundle, 51 | } 52 | 53 | /// Header used by block_consumeBuiltBlockV2. Since docs are not up to date I copied RbuilderHeader from block-processor/ports/models.go (commit b341b35) 54 | /// Based on alloy_primitives::Block 55 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] 56 | #[serde(rename_all = "camelCase")] 57 | struct BlocksProcessorHeader { 58 | pub hash: BlockHash, 59 | pub gas_limit: U256, 60 | pub gas_used: U256, 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | pub base_fee_per_gas: Option, 63 | pub parent_hash: BlockHash, 64 | pub timestamp: U256, 65 | pub number: Option, 66 | } 67 | 68 | type ConsumeBuiltBlockRequest = ( 69 | BlocksProcessorHeader, 70 | String, 71 | String, 72 | Vec, 73 | Vec, 74 | Vec, 75 | alloy_rpc_types_beacon::relay::BidTrace, 76 | String, 77 | U256, 78 | U256, 79 | ); 80 | 81 | /// Struct to avoid copying ConsumeBuiltBlockRequest since HttpClient::request eats the parameter. 82 | #[derive(Clone)] 83 | struct ConsumeBuiltBlockRequestArc { 84 | inner: Arc, 85 | } 86 | 87 | impl ConsumeBuiltBlockRequestArc { 88 | fn new(request: ConsumeBuiltBlockRequest) -> Self { 89 | Self { 90 | inner: Arc::new(request), 91 | } 92 | } 93 | fn as_ref(&self) -> &ConsumeBuiltBlockRequest { 94 | self.inner.as_ref() 95 | } 96 | } 97 | 98 | impl ToRpcParams for ConsumeBuiltBlockRequestArc { 99 | fn to_rpc_params(self) -> Result>, jsonrpsee::core::Error> { 100 | let json = serde_json::to_string(self.inner.as_ref()) 101 | .map_err(jsonrpsee::core::Error::ParseError)?; 102 | RawValue::from_string(json) 103 | .map(Some) 104 | .map_err(jsonrpsee::core::Error::ParseError) 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone)] 109 | pub struct BlocksProcessorClient { 110 | client: HttpClientType, 111 | consume_built_block_method: &'static str, 112 | } 113 | 114 | impl BlocksProcessorClient { 115 | pub fn try_from(url: &str) -> eyre::Result { 116 | Ok(Self { 117 | client: HttpClientBuilder::default().build(url)?, 118 | consume_built_block_method: DEFAULT_BLOCK_CONSUME_BUILT_BLOCK_METHOD, 119 | }) 120 | } 121 | } 122 | 123 | /// RawBundle::encode_no_blobs but more compatible. 124 | fn encode_bundle_for_blocks_processor(mut bundle: Bundle) -> RawBundle { 125 | // set to 0 when none 126 | bundle.block = bundle.block.or(Some(0)); 127 | RawBundle::encode_no_blobs(bundle.clone()) 128 | } 129 | 130 | impl BlocksProcessorClient { 131 | pub fn new(client: HttpClientType, consume_built_block_method: &'static str) -> Self { 132 | Self { 133 | client, 134 | consume_built_block_method, 135 | } 136 | } 137 | pub async fn submit_built_block( 138 | &self, 139 | sealed_block: &SealedBlock, 140 | submit_block_request: &SubmitBlockRequest, 141 | built_block_trace: &BuiltBlockTrace, 142 | builder_name: String, 143 | best_bid_value: U256, 144 | ) -> eyre::Result<()> { 145 | let header = BlocksProcessorHeader { 146 | hash: sealed_block.hash(), 147 | gas_limit: U256::from(sealed_block.gas_limit), 148 | gas_used: U256::from(sealed_block.gas_used), 149 | base_fee_per_gas: sealed_block.base_fee_per_gas.map(U256::from), 150 | parent_hash: sealed_block.parent_hash, 151 | timestamp: U256::from(sealed_block.timestamp), 152 | number: Some(U256::from(sealed_block.number)), 153 | }; 154 | let closed_at = built_block_trace 155 | .orders_closed_at 156 | .format(&well_known::Iso8601::DEFAULT)?; 157 | let sealed_at = built_block_trace 158 | .orders_sealed_at 159 | .format(&well_known::Iso8601::DEFAULT)?; 160 | 161 | let committed_bundles = built_block_trace 162 | .included_orders 163 | .iter() 164 | .filter_map(|res| { 165 | if let Order::Bundle(bundle) = &res.order { 166 | Some(UsedBundle { 167 | mev_gas_price: res.inplace_sim.full_profit_info().mev_gas_price(), 168 | total_eth: res.inplace_sim.full_profit_info().coinbase_profit(), 169 | eth_send_to_coinbase: U256::ZERO, 170 | total_gas_used: res.inplace_sim.gas_used(), 171 | original_bundle: encode_bundle_for_blocks_processor(bundle.clone()), 172 | }) 173 | } else { 174 | None 175 | } 176 | }) 177 | .collect::>(); 178 | 179 | let used_share_bundles = Self::get_used_sbundles(built_block_trace); 180 | 181 | let params: ConsumeBuiltBlockRequest = ( 182 | header, 183 | closed_at, 184 | sealed_at, 185 | committed_bundles, 186 | Vec::::new(), 187 | used_share_bundles, 188 | submit_block_request.bid_trace().clone(), 189 | builder_name, 190 | built_block_trace.true_bid_value, 191 | best_bid_value, 192 | ); 193 | let request = ConsumeBuiltBlockRequestArc::new(params); 194 | let backoff = backoff(); 195 | let mut backoff_iter = backoff.iter(); 196 | loop { 197 | let sleep_time = backoff_iter.next(); 198 | match self 199 | .client 200 | .request(self.consume_built_block_method, request.clone()) 201 | .await 202 | { 203 | Ok(()) => { 204 | return Ok(()); 205 | } 206 | Err(err) => match sleep_time { 207 | Some(time) => { 208 | trace!(?err, "Block processor returned error, retrying."); 209 | tokio::time::sleep(time).await; 210 | } 211 | None => { 212 | Self::handle_rpc_error(&err, request.as_ref()); 213 | return Err(err.into()); 214 | } 215 | }, 216 | } 217 | } 218 | } 219 | 220 | fn handle_rpc_error(err: &jsonrpsee::core::Error, request: &ConsumeBuiltBlockRequest) { 221 | const RPC_ERROR_TEXT: &str = "Block processor RPC"; 222 | match err { 223 | jsonrpsee::core::Error::Call(error_object) => { 224 | error!(err = ?error_object, kind = "error_returned", RPC_ERROR_TEXT); 225 | store_error_event(BLOCK_PROCESSOR_ERROR_CATEGORY, &err.to_string(), request); 226 | } 227 | jsonrpsee::core::Error::Transport(_) => { 228 | error!(err = ?err, kind = "transport", RPC_ERROR_TEXT); 229 | store_error_event(BLOCK_PROCESSOR_ERROR_CATEGORY, &err.to_string(), request); 230 | } 231 | jsonrpsee::core::Error::ParseError(error) => { 232 | error!(err = ?err, kind = "deserialize", RPC_ERROR_TEXT); 233 | let error_txt = error.to_string(); 234 | if !(error_txt.contains("504 Gateway Time-out") 235 | || error_txt.contains("502 Bad Gateway")) 236 | { 237 | store_error_event(BLOCK_PROCESSOR_ERROR_CATEGORY, &err.to_string(), request); 238 | } 239 | } 240 | _ => { 241 | error!(err = ?err, kind = "other", RPC_ERROR_TEXT); 242 | } 243 | } 244 | } 245 | 246 | /// Gets the UsedSbundle carefully considering virtual orders formed by other original orders. 247 | fn get_used_sbundles(built_block_trace: &BuiltBlockTrace) -> Vec { 248 | built_block_trace 249 | .included_orders 250 | .iter() 251 | .flat_map(|exec_result| { 252 | if let Order::ShareBundle(sbundle) = &exec_result.order { 253 | // don't like having special cases (merged vs not merged), can we improve this? 254 | let filtered_sbundles = if sbundle.is_merged_order() { 255 | // We include only original orders that are contained in original_order_ids. 256 | // If not contained in original_order_ids then the sub sbundle failed or was an empty execution. 257 | sbundle 258 | .original_orders 259 | .iter() 260 | .filter_map(|sub_order| { 261 | if let Order::ShareBundle(sbundle) = sub_order { 262 | if exec_result.original_order_ids.contains(&sub_order.id()) { 263 | Some(sbundle) 264 | } else { 265 | None 266 | } 267 | } else { 268 | None 269 | } 270 | }) 271 | .collect() 272 | } else if exec_result.tx_infos.is_empty() { 273 | // non merged empty execution sbundle 274 | vec![] 275 | } else { 276 | // non merged non empty execution sbundle 277 | vec![sbundle] 278 | }; 279 | filtered_sbundles 280 | .into_iter() 281 | .map(|sbundle| UsedSbundle { 282 | bundle: RawShareBundle::encode_no_blobs(sbundle.clone()), 283 | success: true, 284 | }) 285 | .collect() 286 | } else { 287 | Vec::new() 288 | } 289 | }) 290 | .collect::>() 291 | } 292 | } 293 | 294 | /// BidObserver sending all data to a BlocksProcessorClient 295 | #[derive(Debug)] 296 | pub struct BlocksProcessorClientBidObserver { 297 | client: BlocksProcessorClient, 298 | } 299 | 300 | impl BlocksProcessorClientBidObserver { 301 | pub fn new(client: BlocksProcessorClient) -> Self { 302 | Self { client } 303 | } 304 | } 305 | 306 | impl BidObserver 307 | for BlocksProcessorClientBidObserver 308 | { 309 | fn block_submitted( 310 | &self, 311 | _slot_data: &MevBoostSlotData, 312 | sealed_block: &SealedBlock, 313 | submit_block_request: &SubmitBlockRequest, 314 | built_block_trace: &BuiltBlockTrace, 315 | builder_name: String, 316 | best_bid_value: U256, 317 | ) { 318 | let client = self.client.clone(); 319 | let parent_span = Span::current(); 320 | let sealed_block = sealed_block.clone(); 321 | let submit_block_request = submit_block_request.clone(); 322 | let built_block_trace = built_block_trace.clone(); 323 | tokio::spawn(async move { 324 | let block_processor_result = client 325 | .submit_built_block( 326 | &sealed_block, 327 | &submit_block_request, 328 | &built_block_trace, 329 | builder_name, 330 | best_bid_value, 331 | ) 332 | .await; 333 | if let Err(err) = block_processor_result { 334 | inc_submit_block_errors(); 335 | warn!(parent: &parent_span, ?err, "Failed to submit block to the blocks api"); 336 | } 337 | }); 338 | } 339 | } 340 | 341 | // backoff is around 1 minute and total number of requests per payload will be 4 342 | // assuming 200 blocks per slot and if API is down we will max at around 1k of blocks in memory 343 | fn backoff() -> Backoff { 344 | let mut backoff = Backoff::new(3, Duration::from_secs(5), None); 345 | backoff.set_factor(2); 346 | backoff.set_jitter(0.1); 347 | backoff 348 | } 349 | 350 | #[cfg(test)] 351 | mod tests { 352 | use super::*; 353 | 354 | #[test] 355 | fn backoff_total_time_assert() { 356 | let mut requests = 0; 357 | let mut total_sleep_time = Duration::default(); 358 | let backoff = backoff(); 359 | let backoff_iter = backoff.iter(); 360 | for duration in backoff_iter { 361 | requests += 1; 362 | total_sleep_time += duration; 363 | } 364 | assert_eq!(requests, 4); 365 | let total_sleep_time = total_sleep_time.as_secs(); 366 | dbg!(total_sleep_time); 367 | assert!(total_sleep_time > 40 && total_sleep_time < 90); 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/build_info.rs: -------------------------------------------------------------------------------- 1 | // The file has been placed there by the build script. 2 | 3 | mod internal { 4 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 5 | } 6 | 7 | use internal::{ 8 | BUILT_TIME_UTC, CI_PLATFORM, FEATURES, GIT_COMMIT_HASH_SHORT, GIT_HEAD_REF, PROFILE, 9 | RUSTC_VERSION, 10 | }; 11 | use rbuilder::utils::build_info::Version; 12 | 13 | pub fn print_version_info() { 14 | println!( 15 | "commit: {}", 16 | GIT_COMMIT_HASH_SHORT.unwrap_or_default() 17 | ); 18 | println!("branch: {}", GIT_HEAD_REF.unwrap_or_default()); 19 | println!("build_platform: {:?}", CI_PLATFORM.unwrap_or_default()); 20 | println!("build_time: {}", BUILT_TIME_UTC); 21 | println!("features: {:?}", FEATURES); 22 | println!("profile: {}", PROFILE); 23 | println!("rustc: {}", RUSTC_VERSION); 24 | } 25 | 26 | pub fn rbuilder_version() -> Version { 27 | let git_commit = { 28 | let mut commit = String::new(); 29 | if let Some(hash) = GIT_COMMIT_HASH_SHORT { 30 | commit.push_str(hash); 31 | } 32 | if commit.is_empty() { 33 | commit.push_str("unknown"); 34 | } 35 | commit 36 | }; 37 | 38 | let git_ref = GIT_HEAD_REF.unwrap_or("unknown").to_string(); 39 | 40 | Version { 41 | git_commit, 42 | git_ref, 43 | build_time_utc: BUILT_TIME_UTC.to_string(), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/flashbots_config.rs: -------------------------------------------------------------------------------- 1 | //! Config should always be deserializable, default values should be used 2 | //! This code has lots of copy/paste from the example config but it's not really copy/paste since we use our own private types. 3 | //! @Pending make this copy/paste generic code on the library 4 | use alloy_signer_local::PrivateKeySigner; 5 | use eyre::Context; 6 | use http::StatusCode; 7 | use jsonrpsee::RpcModule; 8 | use rbuilder::building::builders::parallel_builder::parallel_build_backtest; 9 | use rbuilder::building::builders::UnfinishedBlockBuildingSinkFactory; 10 | use rbuilder::building::order_priority::{FullProfitInfoGetter, NonMempoolProfitInfoGetter}; 11 | use rbuilder::live_builder::base_config::EnvOrValue; 12 | use rbuilder::live_builder::block_output::bid_observer::BidObserver; 13 | use rbuilder::live_builder::block_output::bid_observer_multiplexer::BidObserverMultiplexer; 14 | use rbuilder::live_builder::block_output::bid_value_source::interfaces::BidValueSource; 15 | use rbuilder::live_builder::block_output::bid_value_source::null_bid_value_source::NullBidValueSource; 16 | use rbuilder::live_builder::block_output::bidding::interfaces::{ 17 | BiddingService, BiddingServiceWinControl, LandedBlockInfo, 18 | }; 19 | use rbuilder::live_builder::block_output::bidding::wallet_balance_watcher::WalletBalanceWatcher; 20 | use rbuilder::live_builder::block_output::block_sealing_bidder_factory::BlockSealingBidderFactory; 21 | use rbuilder::live_builder::config::{ 22 | build_backtest_block_ordering_builder, create_builders, BuilderConfig, SpecificBuilderConfig, 23 | WALLET_INIT_HISTORY_SIZE, 24 | }; 25 | use rbuilder::live_builder::watchdog::spawn_watchdog_thread; 26 | use rbuilder::primitives::mev_boost::MevBoostRelaySlotInfoProvider; 27 | use rbuilder::provider::StateProviderFactory; 28 | use rbuilder::{ 29 | building::builders::{BacktestSimulateBlockInput, Block}, 30 | live_builder::{ 31 | base_config::BaseConfig, cli::LiveBuilderConfig, config::L1Config, 32 | payload_events::MevBoostSlotDataGenerator, LiveBuilder, 33 | }, 34 | utils::build_info::Version, 35 | }; 36 | use serde::Deserialize; 37 | use serde_with::serde_as; 38 | use tokio_util::sync::CancellationToken; 39 | use tracing::{error, warn}; 40 | use url::Url; 41 | 42 | use crate::best_bid_ws::BestBidWSConnector; 43 | use crate::bidding_service_wrapper::client::bidding_service_client_adapter::BiddingServiceClientAdapter; 44 | use crate::block_descriptor_bidding::bidding_service_adapter::BiddingServiceAdapter; 45 | use crate::blocks_processor::{ 46 | BlocksProcessorClient, BlocksProcessorClientBidObserver, 47 | SIGNED_BLOCK_CONSUME_BUILT_BLOCK_METHOD, 48 | }; 49 | use crate::build_info::rbuilder_version; 50 | use crate::true_block_value_push::best_true_value_observer::BestTrueValueObserver; 51 | 52 | use clickhouse::Client; 53 | use std::sync::Arc; 54 | 55 | #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)] 56 | pub struct ClickhouseConfig { 57 | /// clickhouse host url (starts with http/https) 58 | pub clickhouse_host_url: Option>, 59 | pub clickhouse_user: Option>, 60 | pub clickhouse_password: Option>, 61 | } 62 | 63 | #[serde_as] 64 | #[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)] 65 | #[serde(default, deny_unknown_fields)] 66 | /// Config to push TBV to a redis channel. 67 | struct TBVPushRedisConfig { 68 | /// redis connection string for pushing best bid value 69 | /// Option so we can have Default for Deserialize but always required. 70 | pub url: Option>, 71 | 72 | /// redis channel name for syncing best bid value 73 | pub channel: String, 74 | } 75 | 76 | #[serde_as] 77 | #[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)] 78 | #[serde(default, deny_unknown_fields)] 79 | pub struct FlashbotsConfig { 80 | #[serde(flatten)] 81 | pub base_config: BaseConfig, 82 | 83 | #[serde(flatten)] 84 | pub l1_config: L1Config, 85 | 86 | #[serde(flatten)] 87 | clickhouse: ClickhouseConfig, 88 | 89 | #[serde(default)] 90 | pub flashbots_builder_pubkeys: Vec, 91 | 92 | /// ws stream url for getting best relay bid info 93 | pub top_bid_ws_url: Option>, 94 | /// ws stream url authentication 95 | pub top_bid_ws_basic_auth: Option>, 96 | 97 | // bidding server ipc path config. 98 | bidding_service_ipc_path: String, 99 | 100 | /// selected builder configurations 101 | pub builders: Vec, 102 | 103 | /// If this is Some then blocks_processor_url MUST be some and: 104 | /// - signed mode is used for blocks_processor. 105 | /// - tbv_push is done via blocks_processor_url (signed block-processor also handles flashbots_reportBestTrueValue). 106 | pub key_registration_url: Option, 107 | 108 | pub blocks_processor_url: Option, 109 | 110 | /// Cfg to push tbv to redis. 111 | /// For production we always need some tbv push (since it's used by smart-multiplexing.) so: 112 | /// !Some(key_registration_url) => Some(tbv_push_redis) 113 | tbv_push_redis: Option, 114 | } 115 | 116 | impl LiveBuilderConfig for FlashbotsConfig { 117 | fn base_config(&self) -> &BaseConfig { 118 | &self.base_config 119 | } 120 | 121 | async fn new_builder

( 122 | &self, 123 | provider: P, 124 | cancellation_token: CancellationToken, 125 | ) -> eyre::Result> 126 | where 127 | P: StateProviderFactory + Clone + 'static, 128 | { 129 | let (sink_factory, slot_info_provider, bidding_service_win_control) = self 130 | .create_sink_factory_and_relays(provider.clone(), cancellation_token.clone()) 131 | .await?; 132 | 133 | let blocklist_provider = self 134 | .base_config 135 | .blocklist_provider(cancellation_token.clone()) 136 | .await?; 137 | let payload_event = MevBoostSlotDataGenerator::new( 138 | self.l1_config.beacon_clients()?, 139 | slot_info_provider, 140 | blocklist_provider.clone(), 141 | cancellation_token.clone(), 142 | ); 143 | 144 | let mut res = self 145 | .base_config 146 | .create_builder_with_provider_factory( 147 | cancellation_token.clone(), 148 | sink_factory, 149 | payload_event, 150 | provider, 151 | blocklist_provider, 152 | ) 153 | .await?; 154 | 155 | let mut module = RpcModule::new(()); 156 | module.register_async_method("bid_subsidiseBlock", move |params, _| { 157 | handle_subsidise_block(bidding_service_win_control.clone(), params) 158 | })?; 159 | res = res.with_extra_rpc(module); 160 | let builders = create_builders(self.live_builders()?); 161 | res = res.with_builders(builders); 162 | Ok(res) 163 | } 164 | 165 | fn version_for_telemetry(&self) -> Version { 166 | rbuilder_version() 167 | } 168 | 169 | /// @Pending fix this ugly copy/paste 170 | fn build_backtest_block

( 171 | &self, 172 | building_algorithm_name: &str, 173 | input: BacktestSimulateBlockInput<'_, P>, 174 | ) -> eyre::Result 175 | where 176 | P: StateProviderFactory + Clone + 'static, 177 | { 178 | let builder_cfg = self.builder(building_algorithm_name)?; 179 | match builder_cfg.builder { 180 | SpecificBuilderConfig::OrderingBuilder(config) => { 181 | if config.ignore_mempool_profit_on_bundles { 182 | build_backtest_block_ordering_builder::( 183 | config, input, 184 | ) 185 | } else { 186 | build_backtest_block_ordering_builder::(config, input) 187 | } 188 | } 189 | SpecificBuilderConfig::ParallelBuilder(config) => { 190 | parallel_build_backtest::

(input, config) 191 | } 192 | } 193 | } 194 | } 195 | 196 | async fn handle_subsidise_block( 197 | win_control: Arc, 198 | params: jsonrpsee::types::Params<'static>, 199 | ) { 200 | match params.one() { 201 | Ok(block_number) => win_control.must_win_block(block_number), 202 | Err(err) => warn!(?err, "Failed to parse block_number"), 203 | }; 204 | } 205 | 206 | #[derive(thiserror::Error, Debug)] 207 | enum RegisterKeyError { 208 | #[error("Register key error parsing url: {0:?}")] 209 | UrlParse(#[from] url::ParseError), 210 | #[error("Register key network error: {0:?}")] 211 | Network(#[from] reqwest::Error), 212 | #[error("Register key service error: {0:?}")] 213 | Service(StatusCode), 214 | } 215 | 216 | impl FlashbotsConfig { 217 | /// Returns the BiddingService + an optional FlashbotsBlockSubsidySelector so smart multiplexing can force blocks. 218 | /// FlashbotsBlockSubsidySelector can be None if subcidy is disabled. 219 | pub async fn create_bidding_service( 220 | &self, 221 | landed_blocks_history: &[LandedBlockInfo], 222 | _cancellation_token: CancellationToken, 223 | ) -> eyre::Result> { 224 | let client = Box::new( 225 | BiddingServiceClientAdapter::new(&self.bidding_service_ipc_path, landed_blocks_history) 226 | .await 227 | .map_err(|e| eyre::Report::new(e).wrap_err("Unable to connect to remote bidder"))?, 228 | ); 229 | Ok(Box::new(BiddingServiceAdapter::new(client))) 230 | } 231 | 232 | /// Creates a new PrivateKeySigner and registers the associated address on key_registration_url 233 | async fn register_key( 234 | &self, 235 | key_registration_url: &str, 236 | ) -> Result { 237 | let signer = PrivateKeySigner::random(); 238 | let client = reqwest::Client::new(); 239 | let url = { 240 | let mut url = Url::parse(key_registration_url)?; 241 | url.set_path("/api/l1-builder/v1/register_credentials/rbuilder"); 242 | url 243 | }; 244 | let body = format!("{{ \"ecdsa_pubkey_address\": \"{}\" }}", signer.address()); 245 | let res = client.post(url).body(body).send().await?; 246 | if res.status().is_success() { 247 | Ok(signer) 248 | } else { 249 | Err(RegisterKeyError::Service(res.status())) 250 | } 251 | } 252 | 253 | /// Depending on the cfg may create: 254 | /// - Dummy sink (no blocks_processor_url) 255 | /// - Standard block processor client 256 | /// - Secure block processor client (using block_processor_key to sign) 257 | fn create_block_processor_client( 258 | &self, 259 | block_processor_key: Option, 260 | ) -> eyre::Result>> { 261 | if let Some(url) = &self.blocks_processor_url { 262 | let bid_observer: Box = 263 | if let Some(block_processor_key) = block_processor_key { 264 | let client = 265 | crate::signed_http_client::create_client(url, block_processor_key)?; 266 | let block_processor = 267 | BlocksProcessorClient::new(client, SIGNED_BLOCK_CONSUME_BUILT_BLOCK_METHOD); 268 | Box::new(BlocksProcessorClientBidObserver::new(block_processor)) 269 | } else { 270 | let client = BlocksProcessorClient::try_from(url)?; 271 | Box::new(BlocksProcessorClientBidObserver::new(client)) 272 | }; 273 | Ok(Some(bid_observer)) 274 | } else { 275 | if block_processor_key.is_some() { 276 | return Self::bail_blocks_processor_url_not_set(); 277 | } 278 | Ok(None) 279 | } 280 | } 281 | 282 | fn bail_blocks_processor_url_not_set() -> Result { 283 | eyre::bail!("blocks_processor_url should always be set if key_registration_url is set"); 284 | } 285 | 286 | /// Connects (UnfinishedBlockBuildingSinkFactoryWrapper->BlockSealingBidderFactory)->RelaySubmitSinkFactory 287 | /// RelaySubmitSinkFactory: submits final blocks to relays 288 | /// BlockSealingBidderFactory: performs sealing/bidding. Sends bids to the RelaySubmitSinkFactory 289 | /// UnfinishedBlockBuildingSinkFactoryWrapper: sends all the tbv info via redis and forwards to BlockSealingBidderFactory 290 | #[allow(clippy::type_complexity)] 291 | async fn create_sink_factory_and_relays

( 292 | &self, 293 | provider: P, 294 | cancellation_token: CancellationToken, 295 | ) -> eyre::Result<( 296 | Box, 297 | Vec, 298 | Arc, 299 | )> 300 | where 301 | P: StateProviderFactory + Clone + 'static, 302 | { 303 | let block_processor_key = if let Some(key_registration_url) = &self.key_registration_url { 304 | if self.blocks_processor_url.is_none() { 305 | return Self::bail_blocks_processor_url_not_set(); 306 | } 307 | Some(self.register_key(key_registration_url).await?) 308 | } else { 309 | None 310 | }; 311 | 312 | let bid_observer = 313 | self.create_bid_observer(block_processor_key.clone(), &cancellation_token)?; 314 | 315 | let (sink_sealed_factory, slot_info_provider) = self 316 | .l1_config 317 | .create_relays_sealed_sink_factory(self.base_config.chain_spec()?, bid_observer)?; 318 | 319 | // BlockSealingBidderFactory 320 | let (wallet_balance_watcher, wallet_history) = WalletBalanceWatcher::new( 321 | provider, 322 | self.base_config.coinbase_signer()?.address, 323 | WALLET_INIT_HISTORY_SIZE, 324 | )?; 325 | let bid_value_source = self.create_bid_value_source(cancellation_token.clone())?; 326 | let bidding_service: Box = self 327 | .create_bidding_service(&wallet_history, cancellation_token.clone()) 328 | .await?; 329 | let bidding_service_win_control = bidding_service.win_control(); 330 | 331 | let sink_factory = Box::new(BlockSealingBidderFactory::new( 332 | bidding_service, 333 | sink_sealed_factory, 334 | bid_value_source.clone(), 335 | wallet_balance_watcher, 336 | )); 337 | 338 | Ok(( 339 | sink_factory, 340 | slot_info_provider, 341 | bidding_service_win_control, 342 | )) 343 | } 344 | 345 | /// Depending on the cfg add a BlocksProcessorClientBidObserver and/or a true value pusher. 346 | fn create_bid_observer( 347 | &self, 348 | block_processor_key: Option, 349 | cancellation_token: &CancellationToken, 350 | ) -> eyre::Result> { 351 | let mut bid_observer_multiplexer = BidObserverMultiplexer::default(); 352 | if let Some(bid_observer) = 353 | self.create_block_processor_client(block_processor_key.clone())? 354 | { 355 | bid_observer_multiplexer.push(bid_observer); 356 | } 357 | if let Some(bid_observer) = 358 | self.create_tbv_pusher(block_processor_key, cancellation_token)? 359 | { 360 | bid_observer_multiplexer.push(bid_observer); 361 | } 362 | Ok(Box::new(bid_observer_multiplexer)) 363 | } 364 | 365 | fn create_tbv_pusher( 366 | &self, 367 | block_processor_key: Option, 368 | cancellation_token: &CancellationToken, 369 | ) -> eyre::Result>> { 370 | // Avoid sending TBV is we are not on buildernet 371 | if self.key_registration_url.is_none() { 372 | return Ok(None); 373 | } 374 | 375 | if let Some(block_processor_key) = block_processor_key { 376 | if let Some(blocks_processor_url) = &self.blocks_processor_url { 377 | Ok(Some(Box::new(BestTrueValueObserver::new_block_processor( 378 | blocks_processor_url.clone(), 379 | block_processor_key, 380 | cancellation_token.clone(), 381 | )?))) 382 | } else { 383 | Self::bail_blocks_processor_url_not_set() 384 | } 385 | } else if let Some(cfg) = &self.tbv_push_redis { 386 | let tbv_push_redis_url_value = cfg 387 | .url 388 | .as_ref() 389 | .ok_or(eyre::Report::msg("Missing tbv_push_redis_url"))? 390 | .value() 391 | .context("tbv_push_redis_url")?; 392 | Ok(Some(Box::new(BestTrueValueObserver::new_redis( 393 | tbv_push_redis_url_value, 394 | cfg.channel.clone(), 395 | cancellation_token.clone(), 396 | )?))) 397 | } else { 398 | Ok(None) 399 | } 400 | } 401 | 402 | fn create_bid_value_source( 403 | &self, 404 | cancellation_token: CancellationToken, 405 | ) -> eyre::Result> { 406 | if let (Some(top_bid_ws_url), Some(top_bid_ws_basic_auth)) = 407 | (&self.top_bid_ws_url, &self.top_bid_ws_basic_auth) 408 | { 409 | let ws_url = top_bid_ws_url.value().context("top_bid_ws_url")?; 410 | let ws_auth = top_bid_ws_basic_auth 411 | .value() 412 | .context("top_bid_ws_basic_auth")?; 413 | let connector = Arc::new(BestBidWSConnector::new(&ws_url, &ws_auth)?); 414 | let connector_clone = connector.clone(); 415 | 416 | let watchdog_sender = match self.base_config().watchdog_timeout() { 417 | Some(duration) => spawn_watchdog_thread(duration, "got bid source".to_string())?, 418 | None => { 419 | eyre::bail!("Watchdog not enabled. Needed for bid source"); 420 | } 421 | }; 422 | tokio::spawn(async move { 423 | connector_clone 424 | .run_ws_stream(watchdog_sender, cancellation_token) 425 | .await 426 | }); 427 | Ok(connector) 428 | } else { 429 | error!("No BidValueSource configured, using NullBidValueSource"); 430 | Ok(Arc::new(NullBidValueSource {})) 431 | } 432 | } 433 | 434 | fn live_builders(&self) -> eyre::Result> { 435 | self.base_config 436 | .live_builders 437 | .iter() 438 | .map(|cfg_name| self.builder(cfg_name)) 439 | .collect() 440 | } 441 | 442 | fn builder(&self, name: &str) -> eyre::Result { 443 | self.builders 444 | .iter() 445 | .find(|b| b.name == name) 446 | .cloned() 447 | .ok_or_else(|| eyre::eyre!("Builder {} not found in builders list", name)) 448 | } 449 | 450 | pub fn clickhouse_client(&self) -> eyre::Result> { 451 | let host_url = if let Some(host) = &self.clickhouse.clickhouse_host_url { 452 | host.value()? 453 | } else { 454 | return Ok(None); 455 | }; 456 | let user = self 457 | .clickhouse 458 | .clickhouse_user 459 | .as_ref() 460 | .ok_or(eyre::eyre!("clickhouse_user not found"))? 461 | .value()?; 462 | let password = self 463 | .clickhouse 464 | .clickhouse_password 465 | .as_ref() 466 | .ok_or(eyre::eyre!("clickhouse_password not found"))? 467 | .value()?; 468 | 469 | let client = Client::default() 470 | .with_url(host_url) 471 | .with_user(user) 472 | .with_password(password); 473 | Ok(Some(client)) 474 | } 475 | } 476 | 477 | #[cfg(test)] 478 | mod test { 479 | use rbuilder::live_builder::base_config::load_config_toml_and_env; 480 | 481 | use super::*; 482 | use std::{env, path::PathBuf}; 483 | 484 | #[test] 485 | fn test_default_config() { 486 | let config: FlashbotsConfig = serde_json::from_str("{}").unwrap(); 487 | let config_default = FlashbotsConfig::default(); 488 | 489 | assert_eq!(config, config_default); 490 | } 491 | 492 | #[test] 493 | fn test_parse_example_config() { 494 | let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 495 | p.push("config-live-example.toml"); 496 | 497 | load_config_toml_and_env::(p.clone()).expect("Config load"); 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/flashbots_signer.rs: -------------------------------------------------------------------------------- 1 | //! A layer responsible for implementing flashbots-style authentication 2 | //! by signing the request body with a private key and adding the signature 3 | //! to the request headers. 4 | //! Based on https://github.com/paradigmxyz/mev-share-rs/tree/a75c5959e98a79031a89f8893c97528e8f726826 but upgraded to alloy 5 | 6 | use std::{ 7 | error::Error, 8 | task::{Context, Poll}, 9 | }; 10 | 11 | use alloy_primitives::{hex, keccak256}; 12 | use alloy_signer::Signer; 13 | use futures_util::future::BoxFuture; 14 | 15 | use http::{header::HeaderValue, HeaderName, Request}; 16 | use hyper::Body; 17 | 18 | use tower::{Layer, Service}; 19 | 20 | static FLASHBOTS_HEADER: HeaderName = HeaderName::from_static("x-flashbots-signature"); 21 | 22 | /// Layer that applies [`FlashbotsSigner`] which adds a request header with a signed payload. 23 | #[derive(Clone, Debug)] 24 | pub struct FlashbotsSignerLayer { 25 | signer: S, 26 | } 27 | 28 | impl FlashbotsSignerLayer { 29 | /// Creates a new [`FlashbotsSignerLayer`] with the given signer. 30 | pub fn new(signer: S) -> Self { 31 | FlashbotsSignerLayer { signer } 32 | } 33 | } 34 | 35 | impl Layer for FlashbotsSignerLayer { 36 | type Service = FlashbotsSigner; 37 | 38 | fn layer(&self, inner: I) -> Self::Service { 39 | FlashbotsSigner { 40 | signer: self.signer.clone(), 41 | inner, 42 | } 43 | } 44 | } 45 | 46 | /// Middleware that signs the request body and adds the signature to the x-flashbots-signature 47 | /// header. For more info, see 48 | #[derive(Clone, Debug)] 49 | pub struct FlashbotsSigner { 50 | signer: S, 51 | inner: I, 52 | } 53 | 54 | impl Service> for FlashbotsSigner 55 | where 56 | I: Service> + Clone + Send + 'static, 57 | I::Future: Send, 58 | I::Error: Into> + 'static, 59 | S: Signer + Clone + Send + Sync + 'static, 60 | { 61 | type Response = I::Response; 62 | type Error = Box; 63 | type Future = BoxFuture<'static, Result>; 64 | 65 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 66 | self.inner.poll_ready(cx).map_err(Into::into) 67 | } 68 | 69 | fn call(&mut self, request: Request) -> Self::Future { 70 | let clone = self.inner.clone(); 71 | // wait for service to be ready 72 | let mut inner = std::mem::replace(&mut self.inner, clone); 73 | let signer = self.signer.clone(); 74 | 75 | let (mut parts, body) = request.into_parts(); 76 | 77 | // if method is not POST, return an error. 78 | if parts.method != http::Method::POST { 79 | return Box::pin(async move { 80 | Err(format!("Invalid method: {}", parts.method.as_str()).into()) 81 | }); 82 | } 83 | 84 | // if content-type is not json, or signature already exists, just pass through the request 85 | let is_json = parts 86 | .headers 87 | .get(http::header::CONTENT_TYPE) 88 | .map(|v| v == HeaderValue::from_static("application/json")) 89 | .unwrap_or(false); 90 | let has_sig = parts.headers.contains_key(FLASHBOTS_HEADER.clone()); 91 | 92 | if !is_json || has_sig { 93 | return Box::pin(async move { 94 | let request = Request::from_parts(parts, body); 95 | inner.call(request).await.map_err(Into::into) 96 | }); 97 | } 98 | 99 | // otherwise, sign the request body and add the signature to the header 100 | Box::pin(async move { 101 | let body_bytes = hyper::body::to_bytes(body).await?; 102 | 103 | // sign request body and insert header 104 | let signature = signer 105 | .sign_message(format!("{:?}", keccak256(&body_bytes)).as_bytes()) 106 | .await?; 107 | 108 | let header_val = HeaderValue::from_str(&format!( 109 | "{:?}:0x{}", 110 | signer.address(), 111 | hex::encode(signature.as_bytes()) 112 | ))?; 113 | parts.headers.insert(FLASHBOTS_HEADER.clone(), header_val); 114 | 115 | let request = Request::from_parts(parts, Body::from(body_bytes.clone())); 116 | inner.call(request).await.map_err(Into::into) 117 | }) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | use alloy_signer_local::PrivateKeySigner; 125 | use http::Response; 126 | use hyper::Body; 127 | use std::convert::Infallible; 128 | use tower::{service_fn, ServiceExt}; 129 | 130 | #[tokio::test] 131 | async fn test_signature() { 132 | let fb_signer = PrivateKeySigner::random(); 133 | 134 | // mock service that returns the request headers 135 | let svc = FlashbotsSigner { 136 | signer: fb_signer.clone(), 137 | inner: service_fn(|_req: Request| async { 138 | let (parts, _) = _req.into_parts(); 139 | 140 | let mut res = Response::builder(); 141 | for (k, v) in parts.headers.iter() { 142 | res = res.header(k, v); 143 | } 144 | let res = res.body(Body::empty()).unwrap(); 145 | Ok::<_, Infallible>(res) 146 | }), 147 | }; 148 | 149 | // build request 150 | let bytes = vec![1u8; 32]; 151 | let req = Request::builder() 152 | .method(http::Method::POST) 153 | .header(http::header::CONTENT_TYPE, "application/json") 154 | .body(Body::from(bytes.clone())) 155 | .unwrap(); 156 | 157 | let res = svc.oneshot(req).await.unwrap(); 158 | 159 | let header = res.headers().get("x-flashbots-signature").unwrap(); 160 | let header = header.to_str().unwrap(); 161 | let header = header.split(":0x").collect::>(); 162 | let header_address = header[0]; 163 | let header_signature = header[1]; 164 | 165 | let signer_address = format!("{:?}", fb_signer.address()); 166 | let expected_signature = fb_signer 167 | .sign_message(format!("{:?}", keccak256(bytes.clone())).as_bytes()) 168 | .await 169 | .unwrap(); 170 | let expected_signature = hex::encode(expected_signature.as_bytes()); 171 | // verify that the header contains expected address and signature 172 | assert_eq!(header_address, signer_address); 173 | assert_eq!(header_signature, expected_signature); 174 | } 175 | 176 | #[tokio::test] 177 | async fn test_skips_non_json() { 178 | let fb_signer = PrivateKeySigner::random(); 179 | 180 | // mock service that returns the request headers 181 | let svc = FlashbotsSigner { 182 | signer: fb_signer.clone(), 183 | inner: service_fn(|_req: Request| async { 184 | let (parts, _) = _req.into_parts(); 185 | 186 | let mut res = Response::builder(); 187 | for (k, v) in parts.headers.iter() { 188 | res = res.header(k, v); 189 | } 190 | let res = res.body(Body::empty()).unwrap(); 191 | Ok::<_, Infallible>(res) 192 | }), 193 | }; 194 | 195 | // build plain text request 196 | let bytes = vec![1u8; 32]; 197 | let req = Request::builder() 198 | .method(http::Method::POST) 199 | .header(http::header::CONTENT_TYPE, "text/plain") 200 | .body(Body::from(bytes.clone())) 201 | .unwrap(); 202 | 203 | let res = svc.oneshot(req).await.unwrap(); 204 | 205 | // response should not contain a signature header 206 | let header = res.headers().get("x-flashbots-signature"); 207 | assert!(header.is_none()); 208 | } 209 | 210 | #[tokio::test] 211 | async fn test_returns_error_when_not_post() { 212 | let fb_signer = PrivateKeySigner::random(); 213 | 214 | // mock service that returns the request headers 215 | let svc = FlashbotsSigner { 216 | signer: fb_signer.clone(), 217 | inner: service_fn(|_req: Request| async { 218 | let (parts, _) = _req.into_parts(); 219 | 220 | let mut res = Response::builder(); 221 | for (k, v) in parts.headers.iter() { 222 | res = res.header(k, v); 223 | } 224 | let res = res.body(Body::empty()).unwrap(); 225 | Ok::<_, Infallible>(res) 226 | }), 227 | }; 228 | 229 | // build plain text request 230 | let bytes = vec![1u8; 32]; 231 | let req = Request::builder() 232 | .method(http::Method::GET) 233 | .header(http::header::CONTENT_TYPE, "application/json") 234 | .body(Body::from(bytes.clone())) 235 | .unwrap(); 236 | 237 | let res = svc.oneshot(req).await; 238 | 239 | // should be an error 240 | assert!(res.is_err()); 241 | } 242 | 243 | /// Uses a static private key and compares the signature generated by this package to the signature 244 | /// generated by the `cast` CLI. 245 | /// Test copied from https://github.com/flashbots/go-utils/blob/main/signature/signature_test.go#L146 501d395be6a9802494ef1ef25a755acaa4448c17 (TestSignatureCreateCompareToCastAndEthers) 246 | #[tokio::test] 247 | async fn test_signature_cast() { 248 | let fb_signer: PrivateKeySigner = 249 | "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19" 250 | .parse() 251 | .unwrap(); 252 | // mock service that returns the request headers 253 | let svc = FlashbotsSigner { 254 | signer: fb_signer.clone(), 255 | inner: service_fn(|_req: Request| async { 256 | let (parts, _) = _req.into_parts(); 257 | 258 | let mut res = Response::builder(); 259 | for (k, v) in parts.headers.iter() { 260 | res = res.header(k, v); 261 | } 262 | let res = res.body(Body::empty()).unwrap(); 263 | Ok::<_, Infallible>(res) 264 | }), 265 | }; 266 | 267 | // build request 268 | let bytes = "Hello".as_bytes(); 269 | let req = Request::builder() 270 | .method(http::Method::POST) 271 | .header(http::header::CONTENT_TYPE, "application/json") 272 | .body(Body::from(bytes)) 273 | .unwrap(); 274 | 275 | let res = svc.oneshot(req).await.unwrap(); 276 | 277 | let header = res.headers().get("x-flashbots-signature").unwrap(); 278 | let header = header.to_str().unwrap(); 279 | let header = header.split(":0x").collect::>(); 280 | let header_address = header[0]; 281 | let header_signature = header[1]; 282 | // I generated the signature using the cast CLI: 283 | // cast wallet sign --private-key fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19 $(cast from-utf8 $(cast keccak Hello)) 284 | let signer_address = "0x96216849c49358B10257cb55b28eA603c874b05E".to_lowercase(); 285 | let expected_signature = "1446053488f02d460c012c84c4091cd5054d98c6cfca01b65f6c1a72773e80e60b8a4931aeee7ed18ce3adb45b2107e8c59e25556c1f871a8334e30e5bddbed21c"; 286 | // verify that the header contains expected address and signature 287 | assert_eq!(header_address, signer_address); 288 | assert_eq!(header_signature, expected_signature); 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod best_bid_ws; 2 | pub mod bidding_service_wrapper; 3 | pub mod block_descriptor_bidding; 4 | pub mod blocks_processor; 5 | pub mod build_info; 6 | pub mod flashbots_config; 7 | pub mod flashbots_signer; 8 | pub mod metrics; 9 | pub mod reconnect; 10 | pub mod signed_http_client; 11 | mod true_block_value_push; 12 | -------------------------------------------------------------------------------- /src/metrics.rs: -------------------------------------------------------------------------------- 1 | #![allow(unexpected_cfgs)] 2 | 3 | use ctor::ctor; 4 | use lazy_static::lazy_static; 5 | use metrics_macros::register_metrics; 6 | use prometheus::{HistogramOpts, HistogramVec, IntCounter, IntCounterVec, IntGaugeVec, Opts}; 7 | use rbuilder::{ 8 | telemetry::{linear_buckets_range, REGISTRY}, 9 | utils::build_info::Version, 10 | }; 11 | use time::Duration; 12 | 13 | register_metrics! { 14 | pub static BLOCK_API_ERRORS: IntCounterVec = IntCounterVec::new( 15 | Opts::new("block_api_errors", "counter of the block processor errors"), 16 | &["api_name"] 17 | ) 18 | .unwrap(); 19 | 20 | pub static NON_0_COMPETITION_BIDS: IntCounter = IntCounter::new( 21 | "non_0_competition_bids", 22 | "Counter of non 0 bids seen on SCP stream" 23 | ) 24 | .unwrap(); 25 | 26 | pub static BIDDING_SERVICE_VERSION: IntGaugeVec = IntGaugeVec::new( 27 | Opts::new("bidding_service_version", "Version of the bidding service"), 28 | &["git", "git_ref", "build_time_utc"] 29 | ) 30 | .unwrap(); 31 | 32 | pub static TRIGGER_TO_BID_ROUND_TRIP_TIME: HistogramVec = HistogramVec::new( 33 | HistogramOpts::new("trigger_to_bid_round_trip_time_us", "Time (in microseconds) it takes from a trigger (new block or competition bid) to get a new bid to make") 34 | .buckets(linear_buckets_range(50.0, 4000.0, 200)), 35 | &[] 36 | ) 37 | .unwrap(); 38 | 39 | } 40 | 41 | pub fn add_trigger_to_bid_round_trip_time(duration: Duration) { 42 | TRIGGER_TO_BID_ROUND_TRIP_TIME 43 | .with_label_values(&[]) 44 | .observe(duration.as_seconds_f64() * 1_000_000.0); 45 | } 46 | 47 | pub fn inc_submit_block_errors() { 48 | BLOCK_API_ERRORS.with_label_values(&["submit_block"]).inc() 49 | } 50 | 51 | pub fn inc_publish_tbv_errors() { 52 | BLOCK_API_ERRORS.with_label_values(&["publish_tbv"]).inc() 53 | } 54 | 55 | pub fn inc_non_0_competition_bids() { 56 | NON_0_COMPETITION_BIDS.inc(); 57 | } 58 | 59 | pub(super) fn set_bidding_service_version(version: Version) { 60 | BIDDING_SERVICE_VERSION 61 | .with_label_values(&[ 62 | &version.git_commit, 63 | &version.git_ref, 64 | &version.build_time_utc, 65 | ]) 66 | .set(1); 67 | } 68 | -------------------------------------------------------------------------------- /src/reconnect.rs: -------------------------------------------------------------------------------- 1 | use exponential_backoff::Backoff; 2 | use std::{future::Future, time::Duration}; 3 | use tokio_util::sync::CancellationToken; 4 | use tracing::{debug, error, info, info_span, warn, Instrument}; 5 | 6 | #[derive(Debug)] 7 | pub enum RunCommand { 8 | Reconnect, 9 | Finish, 10 | } 11 | 12 | fn default_backoff() -> Backoff { 13 | Backoff::new(u32::MAX, Duration::from_secs(1), Duration::from_secs(12)) 14 | } 15 | 16 | pub async fn run_async_loop_with_reconnect< 17 | Connection, 18 | ConnectErr: std::error::Error, 19 | ConnectFut: Future>, 20 | RunFut: Future, 21 | Connect: Fn() -> ConnectFut, 22 | Run: Fn(Connection) -> RunFut, 23 | >( 24 | context: &str, 25 | connect: Connect, 26 | run: Run, 27 | backoff: Option, 28 | cancellation_token: CancellationToken, 29 | ) { 30 | let span = info_span!("connect_loop_context", context); 31 | 32 | 'reconnect: loop { 33 | if cancellation_token.is_cancelled() { 34 | break 'reconnect; 35 | } 36 | let backoff = backoff.clone().unwrap_or_else(default_backoff); 37 | let mut backoff_iter = backoff.iter(); 38 | 39 | let connection = 'backoff: loop { 40 | let timeout = if let Some(timeout) = backoff_iter.next() { 41 | timeout 42 | } else { 43 | warn!(parent: &span, "Backoff for connection reached max retries"); 44 | break 'reconnect; 45 | }; 46 | 47 | match connect().instrument(span.clone()).await { 48 | Ok(conn) => { 49 | debug!(parent: &span, "Established connection"); 50 | break 'backoff conn; 51 | } 52 | Err(err) => { 53 | error!(parent: &span, ?err, "Failed to establish connection"); 54 | tokio::time::sleep(timeout).await; 55 | } 56 | } 57 | }; 58 | 59 | match run(connection).instrument(span.clone()).await { 60 | RunCommand::Reconnect => continue 'reconnect, 61 | RunCommand::Finish => break 'reconnect, 62 | } 63 | } 64 | info!("Exiting connect loop"); 65 | } 66 | -------------------------------------------------------------------------------- /src/signed_http_client.rs: -------------------------------------------------------------------------------- 1 | use crate::flashbots_signer::{FlashbotsSigner, FlashbotsSignerLayer}; 2 | use alloy_signer_local::PrivateKeySigner; 3 | use jsonrpsee::http_client::transport::Error as JsonError; 4 | use jsonrpsee::http_client::HttpClientBuilder; 5 | use jsonrpsee::http_client::{transport::HttpBackend, HttpClient}; 6 | use tower::ServiceBuilder; 7 | type MapErrorFn = fn(Box) -> JsonError; 8 | 9 | const fn map_error(err: Box) -> JsonError { 10 | JsonError::Http(err) 11 | } 12 | 13 | pub type SignedHttpClient = 14 | HttpClient, MapErrorFn>>; 15 | 16 | pub fn create_client( 17 | url: &str, 18 | signer: PrivateKeySigner, 19 | ) -> Result { 20 | let signing_middleware = FlashbotsSignerLayer::new(signer); 21 | let service_builder = ServiceBuilder::new() 22 | // Coerce to function pointer and remove the + 'static added to the closure 23 | .map_err(map_error as MapErrorFn) 24 | .layer(signing_middleware); 25 | let client = HttpClientBuilder::default() 26 | .set_middleware(service_builder) 27 | .build(url)?; 28 | Ok(client) 29 | } 30 | -------------------------------------------------------------------------------- /src/true_block_value_push/best_true_value_observer.rs: -------------------------------------------------------------------------------- 1 | use alloy_signer_local::PrivateKeySigner; 2 | use rbuilder::{ 3 | building::BuiltBlockTrace, 4 | live_builder::{block_output::bid_observer::BidObserver, payload_events::MevBoostSlotData}, 5 | mev_boost::submission::SubmitBlockRequest, 6 | }; 7 | use redis::RedisError; 8 | use reth::primitives::SealedBlock; 9 | use tokio_util::sync::CancellationToken; 10 | 11 | use super::{ 12 | best_true_value_pusher::{ 13 | Backend, BuiltBlockInfo, BuiltBlockInfoPusher, LastBuiltBlockInfoCell, 14 | }, 15 | blocks_processor_backend::BlocksProcessorBackend, 16 | redis_backend::RedisBackend, 17 | }; 18 | 19 | #[derive(thiserror::Error, Debug)] 20 | pub enum Error { 21 | #[error("Unable to init redis connection : {0}")] 22 | Redis(#[from] RedisError), 23 | #[error("BlocksProcessor backend error: {0}")] 24 | BlocksProcessor(#[from] super::blocks_processor_backend::Error), 25 | } 26 | 27 | pub type Result = core::result::Result; 28 | 29 | #[derive(Debug)] 30 | pub struct BestTrueValueObserver { 31 | best_local_value: LastBuiltBlockInfoCell, 32 | } 33 | 34 | impl BestTrueValueObserver { 35 | /// Constructor using a redis channel backend 36 | pub fn new_redis( 37 | tbv_push_redis_url: String, 38 | tbv_push_redis_channel: String, 39 | cancellation_token: CancellationToken, 40 | ) -> Result { 41 | let best_true_value_redis = redis::Client::open(tbv_push_redis_url)?; 42 | let redis_backend = RedisBackend::new(best_true_value_redis, tbv_push_redis_channel); 43 | Self::new(redis_backend, cancellation_token) 44 | } 45 | 46 | /// Constructor using signed JSON-RPC block-processor API 47 | pub fn new_block_processor( 48 | url: String, 49 | signer: PrivateKeySigner, 50 | cancellation_token: CancellationToken, 51 | ) -> Result { 52 | let backend = BlocksProcessorBackend::new(url, signer)?; 53 | Self::new(backend, cancellation_token) 54 | } 55 | 56 | fn new( 57 | backend: BackendType, 58 | cancellation_token: CancellationToken, 59 | ) -> Result { 60 | let last_local_value = LastBuiltBlockInfoCell::default(); 61 | let pusher = 62 | BuiltBlockInfoPusher::new(last_local_value.clone(), backend, cancellation_token); 63 | std::thread::spawn(move || pusher.run_push_task()); 64 | Ok(BestTrueValueObserver { 65 | best_local_value: last_local_value, 66 | }) 67 | } 68 | } 69 | 70 | impl BidObserver for BestTrueValueObserver { 71 | fn block_submitted( 72 | &self, 73 | slot_data: &MevBoostSlotData, 74 | _sealed_block: &SealedBlock, 75 | _submit_block_request: &SubmitBlockRequest, 76 | built_block_trace: &BuiltBlockTrace, 77 | builder_name: String, 78 | _best_bid_value: alloy_primitives::U256, 79 | ) { 80 | let block_info = BuiltBlockInfo::new( 81 | slot_data.block(), 82 | slot_data.slot(), 83 | built_block_trace.true_bid_value, 84 | built_block_trace.bid_value, 85 | builder_name, 86 | slot_data.timestamp().unix_timestamp() as u64, 87 | ); 88 | self.best_local_value.update_value_safe(block_info); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/true_block_value_push/best_true_value_pusher.rs: -------------------------------------------------------------------------------- 1 | //! This module is responsible for syncing the best true value bid between the local state and redis. 2 | 3 | use alloy_primitives::U256; 4 | 5 | use parking_lot::Mutex; 6 | use rbuilder::utils::{ 7 | reconnect::{run_loop_with_reconnect, RunCommand}, 8 | u256decimal_serde_helper, 9 | }; 10 | use serde::{Deserialize, Serialize}; 11 | use std::{sync::Arc, thread::sleep, time::Duration}; 12 | use time::OffsetDateTime; 13 | use tokio_util::sync::CancellationToken; 14 | use tracing::{error, trace}; 15 | 16 | use crate::metrics::inc_publish_tbv_errors; 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct BuiltBlockInfo { 21 | pub timestamp_ms: u64, 22 | pub block_number: u64, 23 | pub slot_number: u64, 24 | /// Best true value of submitted block (has subtracted the payout tx cost) 25 | #[serde(with = "u256decimal_serde_helper")] 26 | pub best_true_value: U256, 27 | /// Bid we made to the relay. 28 | #[serde(with = "u256decimal_serde_helper")] 29 | pub bid: U256, 30 | pub builder: String, 31 | pub slot_end_timestamp: u64, 32 | } 33 | 34 | impl BuiltBlockInfo { 35 | pub fn new( 36 | block_number: u64, 37 | slot_number: u64, 38 | best_true_value: U256, 39 | bid: U256, 40 | builder: String, 41 | slot_end_timestamp: u64, 42 | ) -> Self { 43 | BuiltBlockInfo { 44 | timestamp_ms: (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000) as u64, 45 | block_number, 46 | slot_number, 47 | best_true_value, 48 | bid, 49 | builder, 50 | slot_end_timestamp, 51 | } 52 | } 53 | 54 | /// Compares things related to bidding: block_number,slot_number,best_true_value and best_relay_value 55 | pub fn is_same_bid_info(&self, other: &Self) -> bool { 56 | self.block_number == other.block_number 57 | && self.slot_number == other.slot_number 58 | && self.best_true_value == other.best_true_value 59 | && self.bid == other.bid 60 | } 61 | } 62 | 63 | #[derive(Debug, Default, Clone)] 64 | pub struct LastBuiltBlockInfoCell { 65 | data: Arc>, 66 | } 67 | 68 | impl LastBuiltBlockInfoCell { 69 | pub fn update_value_safe(&self, value: BuiltBlockInfo) { 70 | let mut best_value = self.data.lock(); 71 | if value.slot_number < best_value.slot_number { 72 | // don't update value for the past slot 73 | return; 74 | } 75 | *best_value = value; 76 | } 77 | 78 | pub fn read(&self) -> BuiltBlockInfo { 79 | self.data.lock().clone() 80 | } 81 | } 82 | 83 | /// BuiltBlockInfoPusher periodically sends last BuiltBlockInfo via a configurable backend. 84 | #[derive(Debug, Clone)] 85 | pub struct BuiltBlockInfoPusher { 86 | /// Best value we got from our building algorithms. 87 | last_local_value: LastBuiltBlockInfoCell, 88 | backend: BackendType, 89 | 90 | cancellation_token: CancellationToken, 91 | } 92 | 93 | const PUSH_INTERVAL: Duration = Duration::from_millis(50); 94 | const MAX_IO_ERRORS: usize = 5; 95 | 96 | /// Trait to connect and publish new BuiltBlockInfo data (as a &str) 97 | /// For simplification mixes a little the factory role and the publish role. 98 | pub trait Backend { 99 | type Connection; 100 | type BackendError: std::error::Error; 101 | /// Creates a new connection to the sink of tbv info. 102 | fn connect(&self) -> Result; 103 | /// Call with the connection obtained by connect() 104 | fn publish( 105 | &self, 106 | connection: &mut Self::Connection, 107 | best_true_value: &BuiltBlockInfo, 108 | ) -> Result<(), Self::BackendError>; 109 | } 110 | 111 | impl BuiltBlockInfoPusher { 112 | pub fn new( 113 | last_local_value: LastBuiltBlockInfoCell, 114 | backend: BackendType, 115 | cancellation_token: CancellationToken, 116 | ) -> Self { 117 | Self { 118 | last_local_value, 119 | backend, 120 | cancellation_token, 121 | } 122 | } 123 | 124 | /// Run the task that pushes the last BuiltBlockInfo. 125 | /// The value is read from last_local_value and pushed to redis. 126 | pub fn run_push_task(self) { 127 | run_loop_with_reconnect( 128 | "push_best_bid", 129 | || -> Result { 130 | self.backend.connect() 131 | }, 132 | |mut conn| -> RunCommand { 133 | let mut io_errors = 0; 134 | let mut last_pushed_value: Option = None; 135 | loop { 136 | if self.cancellation_token.is_cancelled() { 137 | break; 138 | } 139 | 140 | if io_errors > MAX_IO_ERRORS { 141 | return RunCommand::Reconnect; 142 | } 143 | 144 | sleep(PUSH_INTERVAL); 145 | let last_local_value = self.last_local_value.read(); 146 | if last_pushed_value 147 | .as_ref() 148 | .is_none_or(|value| !value.is_same_bid_info(&last_local_value)) 149 | { 150 | last_pushed_value = Some(last_local_value.clone()); 151 | match self.backend.publish(&mut conn, &last_local_value) { 152 | Ok(()) => { 153 | trace!(?last_local_value, "Pushed last local value"); 154 | } 155 | Err(err) => { 156 | error!(?err, "Failed to publish last true value bid"); 157 | // inc_publish_tbv_errors is supposed to be called for block_processor errors but I added the metric here so 158 | // it logs for al backends. 159 | inc_publish_tbv_errors(); 160 | io_errors += 1; 161 | } 162 | } 163 | } 164 | } 165 | RunCommand::Finish 166 | }, 167 | self.cancellation_token.clone(), 168 | ) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/true_block_value_push/blocks_processor_backend.rs: -------------------------------------------------------------------------------- 1 | use crate::signed_http_client::SignedHttpClient; 2 | use alloy_signer_local::PrivateKeySigner; 3 | use jsonrpsee::core::client::ClientT; 4 | use tokio::runtime::Runtime; 5 | use tracing::error; 6 | 7 | use super::best_true_value_pusher::{Backend, BuiltBlockInfo}; 8 | 9 | const REPORT_BEST_TRUE_VALUE_METHOD: &str = "flashbots_reportBestTrueValue"; 10 | 11 | #[derive(thiserror::Error, Debug)] 12 | pub enum Error { 13 | #[error("Unable to build http client {0}")] 14 | BuildHttpClient(#[from] jsonrpsee::core::Error), 15 | #[error("Tokio runtime creation error {0}")] 16 | TokioRuntimeCreation(#[from] std::io::Error), 17 | } 18 | 19 | /// Backend for BestTrueValuePusher that sends signed JSON RPC to BlocksProcessor service. 20 | pub struct BlocksProcessorBackend { 21 | url: String, 22 | signer: PrivateKeySigner, 23 | /// A `current_thread` runtime for executing operations on the 24 | /// asynchronous client in a blocking manner. For more info: https://tokio.rs/tokio/topics/bridging 25 | runtime: Runtime, 26 | } 27 | 28 | impl BlocksProcessorBackend { 29 | pub fn new(url: String, signer: PrivateKeySigner) -> Result { 30 | let runtime = tokio::runtime::Builder::new_current_thread() 31 | .enable_all() 32 | .build()?; 33 | Ok(Self { 34 | url, 35 | signer, 36 | runtime, 37 | }) 38 | } 39 | } 40 | 41 | impl Backend for BlocksProcessorBackend { 42 | type Connection = SignedHttpClient; 43 | type BackendError = Error; 44 | 45 | fn connect(&self) -> Result { 46 | Ok(crate::signed_http_client::create_client( 47 | &self.url, 48 | self.signer.clone(), 49 | )?) 50 | } 51 | 52 | fn publish( 53 | &self, 54 | connection: &mut Self::Connection, 55 | best_true_value: &BuiltBlockInfo, 56 | ) -> Result<(), Self::BackendError> { 57 | let params = [best_true_value]; 58 | Ok(self 59 | .runtime 60 | .block_on(connection.request(REPORT_BEST_TRUE_VALUE_METHOD, params))?) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/true_block_value_push/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module handles the push of the best block (true block value) to a redis channel. 2 | //! This information is used by the smart-multiplexing core to decide when to stop multiplexing order flow. 3 | //! We use a redis channel for historical reasons but it could be changed to a direct streaming. 4 | //! Could be improved but this is just a refactoring resuscitating the old code. 5 | 6 | pub mod best_true_value_observer; 7 | pub mod best_true_value_pusher; 8 | mod blocks_processor_backend; 9 | mod redis_backend; 10 | -------------------------------------------------------------------------------- /src/true_block_value_push/redis_backend.rs: -------------------------------------------------------------------------------- 1 | use redis::Commands; 2 | use tracing::error; 3 | 4 | use super::best_true_value_pusher::{Backend, BuiltBlockInfo}; 5 | 6 | #[derive(thiserror::Error, Debug)] 7 | pub enum Error { 8 | #[error("Redis error {0}")] 9 | Redis(#[from] redis::RedisError), 10 | #[error("Json serialization error {0}")] 11 | JsonSerialization(#[from] serde_json::Error), 12 | } 13 | 14 | /// Backend for BestTrueValuePusher that publish data on a redis channel. 15 | pub struct RedisBackend { 16 | redis: redis::Client, 17 | channel_name: String, 18 | } 19 | 20 | impl RedisBackend { 21 | pub fn new(redis: redis::Client, channel_name: String) -> Self { 22 | Self { 23 | redis, 24 | channel_name, 25 | } 26 | } 27 | } 28 | 29 | impl Backend for RedisBackend { 30 | type Connection = redis::Connection; 31 | type BackendError = Error; 32 | 33 | fn connect(&self) -> Result { 34 | Ok(self.redis.get_connection()?) 35 | } 36 | 37 | fn publish( 38 | &self, 39 | connection: &mut Self::Connection, 40 | best_true_value: &BuiltBlockInfo, 41 | ) -> Result<(), Self::BackendError> { 42 | let best_true_value = serde_json::to_string(&best_true_value)?; 43 | Ok(connection.publish(&self.channel_name, &best_true_value)?) 44 | } 45 | } 46 | --------------------------------------------------------------------------------