diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..15f903e765
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,11 @@
+## 📝 Summary
+
+<!--- A general summary of your changes -->
+
+## 📚 References
+
+<!-- Any interesting external links to documentation, articles, tweets which add value to the PR -->
+
+---
+
+* [ ] I have seen and agree to [`CONTRIBUTING.md`](https://github.com/flashbots/builder/blob/main/CONTRIBUTING.md)
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
new file mode 100644
index 0000000000..ced95a5dc9
--- /dev/null
+++ b/.github/workflows/go.yml
@@ -0,0 +1,84 @@
+name: Go
+
+on:
+  push:
+  pull_request:
+    branches: [ master ]
+
+env:
+  CGO_CFLAGS_ALLOW: "-O -D__BLST_PORTABLE__"
+  CGO_CFLAGS: "-O -D__BLST_PORTABLE__"
+
+jobs:
+
+  build:
+    name: Build
+    runs-on: ubuntu-latest
+    steps:
+
+    - name: Set up Go 1.x
+      uses: actions/setup-go@v2
+      with:
+        go-version: ^1.13
+      id: go
+
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+
+    - name: Test
+      run: go test ./core ./miner/... ./internal/ethapi/... ./les/...
+
+    - name: Build
+      run: make geth
+
+  e2e:
+    name: End to End
+    runs-on: ubuntu-latest
+    timeout-minutes: 20
+    steps:
+
+    - name: Set up Go 1.x
+      uses: actions/setup-go@v2
+      with:
+        go-version: ^1.13
+      id: go
+
+    - name: Use Node.js 12.x
+      uses: actions/setup-node@v1
+      with:
+        node-version: 12.x
+
+    - name: Check out code into the Go module directory
+      uses: actions/checkout@v2
+
+    - name: Build
+      run: make geth
+
+    - name: Check out the e2e code repo
+      uses: actions/checkout@v2
+      with:
+        repository: flashbots/mev-geth-demo
+        ref: no-megabundles
+        path: e2e
+
+    - run: cd e2e && yarn install
+    - name: Run single node e2e
+      run: |
+        cd e2e
+        GETH=`pwd`/../build/bin/geth ./run.sh &
+        sleep 15
+        yarn run demo-simple
+        yarn run e2e-reverting-bundles
+        yarn run demo-contract
+        pkill -9 geth || true
+    - name: Run private tx with two nodes
+      run: |
+        cd e2e
+        GETH=`pwd`/../build/bin/geth ./run.sh &
+        # Second node, not mining
+        P2P_PORT=30302 DATADIR=datadir2 HTTP_PORT=8546 AUTH_PORT=8552 MINER_ARGS='--nodiscover' GETH=`pwd`/../build/bin/geth ./run.sh &
+        sleep 15
+        DATADIR1=datadir DATADIR2=datadir2 GETH=`pwd`/../build/bin/geth ./peer_nodes.sh
+        sleep 15
+        yarn run demo-private-tx
+        pkill -9 geth || true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..4e9f4bb971
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,39 @@
+# Contributing guide
+
+Welcome to the Flashbots collective!
+
+Thanks for your help improving the project! We are so happy to have you! We just ask you to be nice when you play with us.
+
+Please start by reading our [license agreement](#individual-contributor-license-agreement) below, and our [code of conduct](CODE_OF_CONDUCT.md).
+
+## Code style
+
+Start by making sure that your code is readable, consistent, and pretty.
+Follow the [Clean Code](https://flashbots.notion.site/Clean-Code-13016c5c7ca649fba31ae19d797d7304) recommendations.
+
+## Send a pull request
+
+- Your proposed changes should be first described and discussed in an issue.
+- Open the branch in a personal fork, not in the team repository.
+- Every pull request should be small and represent a single change. If the problem is complicated, split it in multiple issues and pull requests.
+- Every pull request should be covered by unit tests.
+
+We appreciate you, friend <3.
+
+---
+
+# Individual Contributor License Agreement
+
+This text is adapted from Google's contributors license agreement: https://cla.developers.google.com/about/google-individual
+
+You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Flashbots. Except for the license granted herein to Flashbots and recipients of software distributed by Flashbots, You reserve all right, title, and interest in and to Your Contributions.
+1. Definitions.
+   * "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Flashbots. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+   * "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Flashbots for inclusion in, or documentation of, any of the products owned or managed by Flashbots (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Flashbots or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Flashbots for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
+2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Flashbots and to recipients of software distributed by Flashbots a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works, under the terms of the license which the project is using on the Submission Date.
+3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Flashbots and to recipients of software distributed by Flashbots a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
+4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Flashbots, or that your employer has executed a separate Corporate CLA with Flashbots.
+5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions.
+6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON- INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
+7. Should You wish to submit work that is not Your original creation, You may submit it to Flashbots separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
+8. You agree to notify Flashbots of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
\ No newline at end of file
diff --git a/README.md b/README.md
index b20eb5b748..185b702ecb 100644
--- a/README.md
+++ b/README.md
@@ -1,379 +1,127 @@
-## Go Ethereum
+[geth readme](README.original.md)
 
-Official Golang implementation of the Ethereum protocol.
+# Builder API
 
-[![API Reference](
-https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667
-)](https://pkg.go.dev/github.com/ethereum/go-ethereum?tab=doc)
-[![Go Report Card](https://goreportcard.com/badge/github.com/ethereum/go-ethereum)](https://goreportcard.com/report/github.com/ethereum/go-ethereum)
-[![Travis](https://travis-ci.com/ethereum/go-ethereum.svg?branch=master)](https://travis-ci.com/ethereum/go-ethereum)
-[![Discord](https://img.shields.io/badge/discord-join%20chat-blue.svg)](https://discord.gg/nthXNEv)
+Builder API implementing [builder spec](https://github.com/ethereum/builder-specs), making geth into a standalone block builder. 
 
-Automated builds are available for stable releases and the unstable master branch. Binary
-archives are published at https://geth.ethereum.org/downloads/.
+Run on your favorite network, including Mainnet, Goerli, Sepolia and local devnet.
 
-## Building the source
+Requires forkchoice update to be sent for block building, on public testnets run beacon node modified to send forkchoice update on every slot [example modified beacon client (lighthouse)](https://github.com/flashbots/lighthouse)
 
-For prerequisites and detailed build instructions please read the [Installation Instructions](https://geth.ethereum.org/docs/install-and-build/installing-geth).
+Test with [mev-boost](https://github.com/flashbots/mev-boost) and [mev-boost test cli](https://github.com/flashbots/mev-boost/tree/main/cmd/test-cli).
 
-Building `geth` requires both a Go (version 1.16 or later) and a C compiler. You can install
-them using your favourite package manager. Once the dependencies are installed, run
+Provides summary page at the listening address' root (http://localhost:28545 by default).
 
-```shell
-make geth
-```
-
-or, to build the full suite of utilities:
-
-```shell
-make all
-```
-
-## Executables
-
-The go-ethereum project comes with several wrappers/executables found in the `cmd`
-directory.
-
-|    Command    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
-| :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-|  **`geth`**   | Our main Ethereum CLI client. It is the entry point into the Ethereum network (main-, test- or private net), capable of running as a full node (default), archive node (retaining all historical state) or a light node (retrieving data live). It can be used by other processes as a gateway into the Ethereum network via JSON RPC endpoints exposed on top of HTTP, WebSocket and/or IPC transports. `geth --help` and the [CLI page](https://geth.ethereum.org/docs/interface/command-line-options) for command line options.          |
-|   `clef`    | Stand-alone signing tool, which can be used as a backend signer for `geth`.  |
-|   `devp2p`    | Utilities to interact with nodes on the networking layer, without running a full blockchain. |
-|   `abigen`    | Source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages. It operates on plain [Ethereum contract ABIs](https://docs.soliditylang.org/en/develop/abi-spec.html) with expanded functionality if the contract bytecode is also available. However, it also accepts Solidity source files, making development much more streamlined. Please see our [Native DApps](https://geth.ethereum.org/docs/dapp/native-bindings) page for details. |
-|  `bootnode`   | Stripped down version of our Ethereum client implementation that only takes part in the network node discovery protocol, but does not run any of the higher level application protocols. It can be used as a lightweight bootstrap node to aid in finding peers in private networks.                                                                                                                                                                                                                                                                 |
-|     `evm`     | Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode. Its purpose is to allow isolated, fine-grained debugging of EVM opcodes (e.g. `evm --code 60ff60ff --debug run`).                                                                                                                                                                                                                                                                     |
-|   `rlpdump`   | Developer utility tool to convert binary RLP ([Recursive Length Prefix](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp)) dumps (data encoding used by the Ethereum protocol both network as well as consensus wise) to user-friendlier hierarchical representation (e.g. `rlpdump --hex CE0183FFFFFFC4C304050583616263`).                                                                                                                                                                                                                                 |
-|   `puppeth`   | a CLI wizard that aids in creating a new Ethereum network.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
-
-## Running `geth`
-
-Going through all the possible command line flags is out of scope here (please consult our
-[CLI Wiki page](https://geth.ethereum.org/docs/interface/command-line-options)),
-but we've enumerated a few common parameter combos to get you up to speed quickly
-on how you can run your own `geth` instance.
-
-### Hardware Requirements
-
-Minimum:
-
-* CPU with 2+ cores
-* 4GB RAM
-* 1TB free storage space to sync the Mainnet
-* 8 MBit/sec download Internet service
-
-Recommended:
-
-* Fast CPU with 4+ cores
-* 16GB+ RAM
-* High Performance SSD with at least 1TB free space
-* 25+ MBit/sec download Internet service
-
-### Full node on the main Ethereum network
-
-By far the most common scenario is people wanting to simply interact with the Ethereum
-network: create accounts; transfer funds; deploy and interact with contracts. For this
-particular use-case the user doesn't care about years-old historical data, so we can
-sync quickly to the current state of the network. To do so:
-
-```shell
-$ geth console
-```
-
-This command will:
- * Start `geth` in snap sync mode (default, can be changed with the `--syncmode` flag),
-   causing it to download more data in exchange for avoiding processing the entire history
-   of the Ethereum network, which is very CPU intensive.
- * Start up `geth`'s built-in interactive [JavaScript console](https://geth.ethereum.org/docs/interface/javascript-console),
-   (via the trailing `console` subcommand) through which you can interact using [`web3` methods](https://github.com/ChainSafe/web3.js/blob/0.20.7/DOCUMENTATION.md) 
-   (note: the `web3` version bundled within `geth` is very old, and not up to date with official docs),
-   as well as `geth`'s own [management APIs](https://geth.ethereum.org/docs/rpc/server).
-   This tool is optional and if you leave it out you can always attach to an already running
-   `geth` instance with `geth attach`.
-
-### A Full node on the Görli test network
-
-Transitioning towards developers, if you'd like to play around with creating Ethereum
-contracts, you almost certainly would like to do that without any real money involved until
-you get the hang of the entire system. In other words, instead of attaching to the main
-network, you want to join the **test** network with your node, which is fully equivalent to
-the main network, but with play-Ether only.
-
-```shell
-$ geth --goerli console
-```
-
-The `console` subcommand has the exact same meaning as above and they are equally
-useful on the testnet too. Please, see above for their explanations if you've skipped here.
-
-Specifying the `--goerli` flag, however, will reconfigure your `geth` instance a bit:
-
- * Instead of connecting the main Ethereum network, the client will connect to the Görli
-   test network, which uses different P2P bootnodes, different network IDs and genesis
-   states.
- * Instead of using the default data directory (`~/.ethereum` on Linux for example), `geth`
-   will nest itself one level deeper into a `goerli` subfolder (`~/.ethereum/goerli` on
-   Linux). Note, on OSX and Linux this also means that attaching to a running testnet node
-   requires the use of a custom endpoint since `geth attach` will try to attach to a
-   production node endpoint by default, e.g.,
-   `geth attach <datadir>/goerli/geth.ipc`. Windows users are not affected by
-   this.
-
-*Note: Although there are some internal protective measures to prevent transactions from
-crossing over between the main network and test network, you should make sure to always
-use separate accounts for play-money and real-money. Unless you manually move
-accounts, `geth` will by default correctly separate the two networks and will not make any
-accounts available between them.*
+## How it works
 
-### Full node on the Rinkeby test network
+* Builder polls relay for the proposer registrations for the next epoch
 
-Go Ethereum also supports connecting to the older proof-of-authority based test network
-called [*Rinkeby*](https://www.rinkeby.io) which is operated by members of the community.
+Builder has two hooks into geth:
+* On forkchoice update, changing the payload attributes feeRecipient to the one registered for next slot's validator
+* On new sealed block, consuming the block as the next slot's proposed payload and submits it to the relay
 
-```shell
-$ geth --rinkeby console
-```
-
-### Full node on the Ropsten test network
-
-In addition to Görli and Rinkeby, Geth also supports the ancient Ropsten testnet. The
-Ropsten test network is based on the Ethash proof-of-work consensus algorithm. As such,
-it has certain extra overhead and is more susceptible to reorganization attacks due to the
-network's low difficulty/security.
-
-```shell
-$ geth --ropsten console
-```
-
-*Note: Older Geth configurations store the Ropsten database in the `testnet` subdirectory.*
-
-### Configuration
-
-As an alternative to passing the numerous flags to the `geth` binary, you can also pass a
-configuration file via:
-
-```shell
-$ geth --config /path/to/your_config.toml
-```
-
-To get an idea how the file should look like you can use the `dumpconfig` subcommand to
-export your existing configuration:
-
-```shell
-$ geth --your-favourite-flags dumpconfig
-```
+Local relay is enabled by default and overwrites remote relay data. This is only meant for the testnets!
 
-*Note: This works only with `geth` v1.6.0 and above.*
+## Limitations
 
-#### Docker quick start
+* Blocks are only built on forkchoice update call from beacon node
+* Does not accept external blocks
+* Does not have payload cache, only the latest block is available
 
-One of the quickest ways to get Ethereum up and running on your machine is by using
-Docker:
+# Usage
 
-```shell
-docker run -d --name ethereum-node -v /Users/alice/ethereum:/root \
-           -p 8545:8545 -p 30303:30303 \
-           ethereum/client-go
-```
+Configure geth for your network, it will become the block builder.
 
-This will start `geth` in snap-sync mode with a DB memory allowance of 1GB just as the
-above command does.  It will also create a persistent volume in your home directory for
-saving your blockchain as well as map the default ports. There is also an `alpine` tag
-available for a slim version of the image.
-
-Do not forget `--http.addr 0.0.0.0`, if you want to access RPC from other containers
-and/or hosts. By default, `geth` binds to the local interface and RPC endpoints are not
-accessible from the outside.
-
-### Programmatically interfacing `geth` nodes
-
-As a developer, sooner rather than later you'll want to start interacting with `geth` and the
-Ethereum network via your own programs and not manually through the console. To aid
-this, `geth` has built-in support for a JSON-RPC based APIs ([standard APIs](https://ethereum.github.io/execution-apis/api-documentation/)
-and [`geth` specific APIs](https://geth.ethereum.org/docs/rpc/server)).
-These can be exposed via HTTP, WebSockets and IPC (UNIX sockets on UNIX based
-platforms, and named pipes on Windows).
-
-The IPC interface is enabled by default and exposes all the APIs supported by `geth`,
-whereas the HTTP and WS interfaces need to manually be enabled and only expose a
-subset of APIs due to security reasons. These can be turned on/off and configured as
-you'd expect.
-
-HTTP based JSON-RPC API options:
-
-  * `--http` Enable the HTTP-RPC server
-  * `--http.addr` HTTP-RPC server listening interface (default: `localhost`)
-  * `--http.port` HTTP-RPC server listening port (default: `8545`)
-  * `--http.api` API's offered over the HTTP-RPC interface (default: `eth,net,web3`)
-  * `--http.corsdomain` Comma separated list of domains from which to accept cross origin requests (browser enforced)
-  * `--ws` Enable the WS-RPC server
-  * `--ws.addr` WS-RPC server listening interface (default: `localhost`)
-  * `--ws.port` WS-RPC server listening port (default: `8546`)
-  * `--ws.api` API's offered over the WS-RPC interface (default: `eth,net,web3`)
-  * `--ws.origins` Origins from which to accept websockets requests
-  * `--ipcdisable` Disable the IPC-RPC server
-  * `--ipcapi` API's offered over the IPC-RPC interface (default: `admin,debug,eth,miner,net,personal,txpool,web3`)
-  * `--ipcpath` Filename for IPC socket/pipe within the datadir (explicit paths escape it)
-
-You'll need to use your own programming environments' capabilities (libraries, tools, etc) to
-connect via HTTP, WS or IPC to a `geth` node configured with the above flags and you'll
-need to speak [JSON-RPC](https://www.jsonrpc.org/specification) on all transports. You
-can reuse the same connection for multiple requests!
-
-**Note: Please understand the security implications of opening up an HTTP/WS based
-transport before doing so! Hackers on the internet are actively trying to subvert
-Ethereum nodes with exposed APIs! Further, all browser tabs can access locally
-running web servers, so malicious web pages could try to subvert locally available
-APIs!**
-
-### Operating a private network
-
-Maintaining your own private network is more involved as a lot of configurations taken for
-granted in the official networks need to be manually set up.
-
-#### Defining the private genesis state
-
-First, you'll need to create the genesis state of your networks, which all nodes need to be
-aware of and agree upon. This consists of a small JSON file (e.g. call it `genesis.json`):
-
-```json
-{
-  "config": {
-    "chainId": <arbitrary positive integer>,
-    "homesteadBlock": 0,
-    "eip150Block": 0,
-    "eip155Block": 0,
-    "eip158Block": 0,
-    "byzantiumBlock": 0,
-    "constantinopleBlock": 0,
-    "petersburgBlock": 0,
-    "istanbulBlock": 0,
-    "berlinBlock": 0,
-    "londonBlock": 0
-  },
-  "alloc": {},
-  "coinbase": "0x0000000000000000000000000000000000000000",
-  "difficulty": "0x20000",
-  "extraData": "",
-  "gasLimit": "0x2fefd8",
-  "nonce": "0x0000000000000042",
-  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
-  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
-  "timestamp": "0x00"
-}
+Builder-related options:
 ```
-
-The above fields should be fine for most purposes, although we'd recommend changing
-the `nonce` to some random value so you prevent unknown remote nodes from being able
-to connect to you. If you'd like to pre-fund some accounts for easier testing, create
-the accounts and populate the `alloc` field with their addresses.
-
-```json
-"alloc": {
-  "0x0000000000000000000000000000000000000001": {
-    "balance": "111111111"
-  },
-  "0x0000000000000000000000000000000000000002": {
-    "balance": "222222222"
-  }
-}
+$ geth --help
+BUILDER API OPTIONS:
+    --builder                      (default: false)
+          Enable the builder
+    --builder.beacon_endpoint value (default: "http://127.0.0.1:5052")
+    --builder.bellatrix_fork_version value (default: "0x02000000")
+    --builder.dry-run              (default: false)
+    --builder.genesis_fork_version value (default: "0x00000000")
+    --builder.genesis_validators_root value (default: "0x0000000000000000000000000000000000000000000000000000000000000000")
+    --builder.listen_addr value    (default: ":28545")
+          Listening address for builder endpoint [$BUILDER_LISTEN_ADDR]
+    --builder.local_relay          (default: false)
+    --builder.no_bundle_fetcher    (default: false)
+    --builder.relay_secret_key value (default: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11")
+    --builder.remote_relay_endpoint value
+    --builder.secret_key value     (default: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11")
+    --builder.validator_checks     (default: false)
+    --builder.validation_blacklist value
+    --miner.algotype value         (default: "mev-geth")
+    --miner.blocklist value
+    --miner.extradata value
 ```
 
-With the genesis state defined in the above JSON file, you'll need to initialize **every**
-`geth` node with it prior to starting it up to ensure all blockchain parameters are correctly
-set:
-
-```shell
-$ geth init path/to/genesis.json
+Environment variables:
 ```
-
-#### Creating the rendezvous point
-
-With all nodes that you want to run initialized to the desired genesis state, you'll need to
-start a bootstrap node that others can use to find each other in your network and/or over
-the internet. The clean way is to configure and run a dedicated bootnode:
-
-```shell
-$ bootnode --genkey=boot.key
-$ bootnode --nodekey=boot.key
+BUILDER_TX_SIGNING_KEY - private key of the builder used to sign payment transaction
 ```
 
-With the bootnode online, it will display an [`enode` URL](https://ethereum.org/en/developers/docs/networking-layer/network-addresses/#enode)
-that other nodes can use to connect to it and exchange peer information. Make sure to
-replace the displayed IP address information (most probably `[::]`) with your externally
-accessible IP to get the actual `enode` URL.
-
-*Note: You could also use a full-fledged `geth` node as a bootnode, but it's the less
-recommended way.*
-
-#### Starting up your member nodes
+## Details of the implementation
 
-With the bootnode operational and externally reachable (you can try
-`telnet <ip> <port>` to ensure it's indeed reachable), start every subsequent `geth`
-node pointed to the bootnode for peer discovery via the `--bootnodes` flag. It will
-probably also be desirable to keep the data directory of your private network separated, so
-do also specify a custom `--datadir` flag.
+There are two parts of the builder.
 
-```shell
-$ geth --datadir=path/to/custom/data/folder --bootnodes=<bootnode-enode-url-from-above>
-```
-
-*Note: Since your network will be completely cut off from the main and test networks, you'll
-also need to configure a miner to process transactions and create new blocks for you.*
+1. `./builder` responsible for communicating with the relay
+2. `./miner` responsible for producing blocks
 
-#### Running a private miner
+### `builder` module
 
-Mining on the public Ethereum network is a complex task as it's only feasible using GPUs,
-requiring an OpenCL or CUDA enabled `ethminer` instance. For information on such a
-setup, please consult the [EtherMining subreddit](https://www.reddit.com/r/EtherMining/)
-and the [ethminer](https://github.com/ethereum-mining/ethminer) repository.
+Main logic of the builder is in the `builder.go` file.
 
-In a private network setting, however a single CPU miner instance is more than enough for
-practical purposes as it can produce a stable stream of blocks at the correct intervals
-without needing heavy resources (consider running on a single thread, no need for multiple
-ones either). To start a `geth` instance for mining, run it with all your usual flags, extended
-by:
+Builder is driven by the modified consensus client that calls `OnPayloadAttribute` indicating that block should be produced.
+After requesting additional validator data from the relay builder starts building job with `runBuildingJob`.
+Building job continuously makes a request to the `miner` with the correct parameters and submits produced block.
 
-```shell
-$ geth <usual-flags> --mine --miner.threads=1 --miner.etherbase=0x0000000000000000000000000000000000000000
-```
+* Builder retries build block requests every second on average.
+* If the job is running but a new one is submitted for a different slot we cancel previous job.
+* All jobs have 12s deadline.
+* If new request is submitted for the same slot as before but with different parameters, we run these jobs in parallel.
+  It is possible to receive multiple requests from CL for the same slot but for different parent blocks if there is a possibility
+  of a missed block.
+* All submissions to the relay are rate limited at 2 req/s
+* Only blocks that have more profit than the previous best submissions for the particular job are submitted.
 
-Which will start mining blocks and transactions on a single CPU thread, crediting all
-proceedings to the account specified by `--miner.etherbase`. You can further tune the mining
-by changing the default gas limit blocks converge to (`--miner.targetgaslimit`) and the price
-transactions are accepted at (`--miner.gasprice`).
+Additional features of the builder:
+* Builder can submit data about build blocks to the database. It stores block data, included bundles, and all considered bundles.
+  Implemented in `flashbotsextra.IDatabaseService`.
+* It's possible to run local relay in the same process
+* It can validate blocks instead of submitting them to the relay. (see `--builder.dry-run`)
 
-## Contribution
+### `miner` module
 
-Thank you for considering to help out with the source code! We welcome contributions
-from anyone on the internet, and are grateful for even the smallest of fixes!
+Miner is responsible for block creation. Request from the `builder` is routed to the `worker.go` where
+`generateWork` does the job of creating a block.
 
-If you'd like to contribute to go-ethereum, please fork, fix, commit and send a pull request
-for the maintainers to review and merge into the main code base. If you wish to submit
-more complex changes though, please check up with the core devs first on [our Discord Server](https://discord.gg/invite/nthXNEv)
-to ensure those changes are in line with the general philosophy of the project and/or get
-some early feedback which can make both your efforts much lighter as well as our review
-and merge procedures quick and simple.
+* Coinbase of the block is set to the address of the block proposer, fee recipient of the validator receives its eth
+  in the last tx in the block.
+* We reserve gas for the proposer payment using `proposerTxPrepare` and commit proposer payment after txs are added with
+  `proposerTxCommit`. We do it in a way so all fees received by the block builder are sent to the fee recipient.
+* Transaction insertion is done in `fillTransactionsAlgoWorker` \ `fillTransactions`. Depending on the algorithm selected.
+  Algo worker (greedy) inserts bundles whenever they belong in the block by effective gas price but default method inserts bundles on top of the block.
+  (see `--miner.algo`)
+* Worker is also responsible for simulating bundles. Bundles are simulated in parallel and results are cached for the particular parent block.
+* `algo_greedy.go` implements logic of the block building. Bundles and transactions are sorted in the order of effective gas price then
+  we try to insert everything into to block until gas limit is reached. Failing bundles are reverted during the insertion but txs are not.
+* Builder can filter transactions touching a particular set of addresses.
+  If a bundle or transaction touches one of the addresses it is skipped. (see `--miner.blocklist` flag)
 
-Please make sure your contributions adhere to our coding guidelines:
+## Bundle Movement 
 
- * Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting)
-   guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
- * Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary)
-   guidelines.
- * Pull requests need to be based on and opened against the `master` branch.
- * Commit messages should be prefixed with the package(s) they modify.
-   * E.g. "eth, rpc: make trace configs optional"
+There are two ways bundles are moved to builders 
 
-Please see the [Developers' Guide](https://geth.ethereum.org/docs/developers/devguide)
-for more details on configuring your environment, managing project dependencies, and
-testing procedures.
+1. via API -`sendBundle` 
+2. via Data Base - `flashbotsextra.IDatabaseService`
 
-## License
+### `fetcher` service 
+* Fetcher service is part of `flashbotsextra.IDatabaseService` which is responsible for fetching the bundles from db and pushing into mev bundles queue which will be processed by builder. 
+* Fetcher is a background process which fetches high priority and low priority bundles from db. 
+* Fetcher fetches `500` high priority bundles on every head change, and `100` low priority bundles in the interval of every `2 seconds`.  
 
-The go-ethereum library (i.e. all code outside of the `cmd` directory) is licensed under the
-[GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html),
-also included in our repository in the `COPYING.LESSER` file.
+## Block builder diagram
 
-The go-ethereum binaries (i.e. all code inside of the `cmd` directory) is licensed under the
-[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), also
-included in our repository in the `COPYING` file.
+![block builder diagram](docs/builder/builder-diagram.png "Block builder diagram")
diff --git a/README.original.md b/README.original.md
new file mode 100644
index 0000000000..b20eb5b748
--- /dev/null
+++ b/README.original.md
@@ -0,0 +1,379 @@
+## Go Ethereum
+
+Official Golang implementation of the Ethereum protocol.
+
+[![API Reference](
+https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667
+)](https://pkg.go.dev/github.com/ethereum/go-ethereum?tab=doc)
+[![Go Report Card](https://goreportcard.com/badge/github.com/ethereum/go-ethereum)](https://goreportcard.com/report/github.com/ethereum/go-ethereum)
+[![Travis](https://travis-ci.com/ethereum/go-ethereum.svg?branch=master)](https://travis-ci.com/ethereum/go-ethereum)
+[![Discord](https://img.shields.io/badge/discord-join%20chat-blue.svg)](https://discord.gg/nthXNEv)
+
+Automated builds are available for stable releases and the unstable master branch. Binary
+archives are published at https://geth.ethereum.org/downloads/.
+
+## Building the source
+
+For prerequisites and detailed build instructions please read the [Installation Instructions](https://geth.ethereum.org/docs/install-and-build/installing-geth).
+
+Building `geth` requires both a Go (version 1.16 or later) and a C compiler. You can install
+them using your favourite package manager. Once the dependencies are installed, run
+
+```shell
+make geth
+```
+
+or, to build the full suite of utilities:
+
+```shell
+make all
+```
+
+## Executables
+
+The go-ethereum project comes with several wrappers/executables found in the `cmd`
+directory.
+
+|    Command    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
+| :-----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|  **`geth`**   | Our main Ethereum CLI client. It is the entry point into the Ethereum network (main-, test- or private net), capable of running as a full node (default), archive node (retaining all historical state) or a light node (retrieving data live). It can be used by other processes as a gateway into the Ethereum network via JSON RPC endpoints exposed on top of HTTP, WebSocket and/or IPC transports. `geth --help` and the [CLI page](https://geth.ethereum.org/docs/interface/command-line-options) for command line options.          |
+|   `clef`    | Stand-alone signing tool, which can be used as a backend signer for `geth`.  |
+|   `devp2p`    | Utilities to interact with nodes on the networking layer, without running a full blockchain. |
+|   `abigen`    | Source code generator to convert Ethereum contract definitions into easy to use, compile-time type-safe Go packages. It operates on plain [Ethereum contract ABIs](https://docs.soliditylang.org/en/develop/abi-spec.html) with expanded functionality if the contract bytecode is also available. However, it also accepts Solidity source files, making development much more streamlined. Please see our [Native DApps](https://geth.ethereum.org/docs/dapp/native-bindings) page for details. |
+|  `bootnode`   | Stripped down version of our Ethereum client implementation that only takes part in the network node discovery protocol, but does not run any of the higher level application protocols. It can be used as a lightweight bootstrap node to aid in finding peers in private networks.                                                                                                                                                                                                                                                                 |
+|     `evm`     | Developer utility version of the EVM (Ethereum Virtual Machine) that is capable of running bytecode snippets within a configurable environment and execution mode. Its purpose is to allow isolated, fine-grained debugging of EVM opcodes (e.g. `evm --code 60ff60ff --debug run`).                                                                                                                                                                                                                                                                     |
+|   `rlpdump`   | Developer utility tool to convert binary RLP ([Recursive Length Prefix](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp)) dumps (data encoding used by the Ethereum protocol both network as well as consensus wise) to user-friendlier hierarchical representation (e.g. `rlpdump --hex CE0183FFFFFFC4C304050583616263`).                                                                                                                                                                                                                                 |
+|   `puppeth`   | a CLI wizard that aids in creating a new Ethereum network.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
+
+## Running `geth`
+
+Going through all the possible command line flags is out of scope here (please consult our
+[CLI Wiki page](https://geth.ethereum.org/docs/interface/command-line-options)),
+but we've enumerated a few common parameter combos to get you up to speed quickly
+on how you can run your own `geth` instance.
+
+### Hardware Requirements
+
+Minimum:
+
+* CPU with 2+ cores
+* 4GB RAM
+* 1TB free storage space to sync the Mainnet
+* 8 MBit/sec download Internet service
+
+Recommended:
+
+* Fast CPU with 4+ cores
+* 16GB+ RAM
+* High Performance SSD with at least 1TB free space
+* 25+ MBit/sec download Internet service
+
+### Full node on the main Ethereum network
+
+By far the most common scenario is people wanting to simply interact with the Ethereum
+network: create accounts; transfer funds; deploy and interact with contracts. For this
+particular use-case the user doesn't care about years-old historical data, so we can
+sync quickly to the current state of the network. To do so:
+
+```shell
+$ geth console
+```
+
+This command will:
+ * Start `geth` in snap sync mode (default, can be changed with the `--syncmode` flag),
+   causing it to download more data in exchange for avoiding processing the entire history
+   of the Ethereum network, which is very CPU intensive.
+ * Start up `geth`'s built-in interactive [JavaScript console](https://geth.ethereum.org/docs/interface/javascript-console),
+   (via the trailing `console` subcommand) through which you can interact using [`web3` methods](https://github.com/ChainSafe/web3.js/blob/0.20.7/DOCUMENTATION.md) 
+   (note: the `web3` version bundled within `geth` is very old, and not up to date with official docs),
+   as well as `geth`'s own [management APIs](https://geth.ethereum.org/docs/rpc/server).
+   This tool is optional and if you leave it out you can always attach to an already running
+   `geth` instance with `geth attach`.
+
+### A Full node on the Görli test network
+
+Transitioning towards developers, if you'd like to play around with creating Ethereum
+contracts, you almost certainly would like to do that without any real money involved until
+you get the hang of the entire system. In other words, instead of attaching to the main
+network, you want to join the **test** network with your node, which is fully equivalent to
+the main network, but with play-Ether only.
+
+```shell
+$ geth --goerli console
+```
+
+The `console` subcommand has the exact same meaning as above and they are equally
+useful on the testnet too. Please, see above for their explanations if you've skipped here.
+
+Specifying the `--goerli` flag, however, will reconfigure your `geth` instance a bit:
+
+ * Instead of connecting the main Ethereum network, the client will connect to the Görli
+   test network, which uses different P2P bootnodes, different network IDs and genesis
+   states.
+ * Instead of using the default data directory (`~/.ethereum` on Linux for example), `geth`
+   will nest itself one level deeper into a `goerli` subfolder (`~/.ethereum/goerli` on
+   Linux). Note, on OSX and Linux this also means that attaching to a running testnet node
+   requires the use of a custom endpoint since `geth attach` will try to attach to a
+   production node endpoint by default, e.g.,
+   `geth attach <datadir>/goerli/geth.ipc`. Windows users are not affected by
+   this.
+
+*Note: Although there are some internal protective measures to prevent transactions from
+crossing over between the main network and test network, you should make sure to always
+use separate accounts for play-money and real-money. Unless you manually move
+accounts, `geth` will by default correctly separate the two networks and will not make any
+accounts available between them.*
+
+### Full node on the Rinkeby test network
+
+Go Ethereum also supports connecting to the older proof-of-authority based test network
+called [*Rinkeby*](https://www.rinkeby.io) which is operated by members of the community.
+
+```shell
+$ geth --rinkeby console
+```
+
+### Full node on the Ropsten test network
+
+In addition to Görli and Rinkeby, Geth also supports the ancient Ropsten testnet. The
+Ropsten test network is based on the Ethash proof-of-work consensus algorithm. As such,
+it has certain extra overhead and is more susceptible to reorganization attacks due to the
+network's low difficulty/security.
+
+```shell
+$ geth --ropsten console
+```
+
+*Note: Older Geth configurations store the Ropsten database in the `testnet` subdirectory.*
+
+### Configuration
+
+As an alternative to passing the numerous flags to the `geth` binary, you can also pass a
+configuration file via:
+
+```shell
+$ geth --config /path/to/your_config.toml
+```
+
+To get an idea how the file should look like you can use the `dumpconfig` subcommand to
+export your existing configuration:
+
+```shell
+$ geth --your-favourite-flags dumpconfig
+```
+
+*Note: This works only with `geth` v1.6.0 and above.*
+
+#### Docker quick start
+
+One of the quickest ways to get Ethereum up and running on your machine is by using
+Docker:
+
+```shell
+docker run -d --name ethereum-node -v /Users/alice/ethereum:/root \
+           -p 8545:8545 -p 30303:30303 \
+           ethereum/client-go
+```
+
+This will start `geth` in snap-sync mode with a DB memory allowance of 1GB just as the
+above command does.  It will also create a persistent volume in your home directory for
+saving your blockchain as well as map the default ports. There is also an `alpine` tag
+available for a slim version of the image.
+
+Do not forget `--http.addr 0.0.0.0`, if you want to access RPC from other containers
+and/or hosts. By default, `geth` binds to the local interface and RPC endpoints are not
+accessible from the outside.
+
+### Programmatically interfacing `geth` nodes
+
+As a developer, sooner rather than later you'll want to start interacting with `geth` and the
+Ethereum network via your own programs and not manually through the console. To aid
+this, `geth` has built-in support for a JSON-RPC based APIs ([standard APIs](https://ethereum.github.io/execution-apis/api-documentation/)
+and [`geth` specific APIs](https://geth.ethereum.org/docs/rpc/server)).
+These can be exposed via HTTP, WebSockets and IPC (UNIX sockets on UNIX based
+platforms, and named pipes on Windows).
+
+The IPC interface is enabled by default and exposes all the APIs supported by `geth`,
+whereas the HTTP and WS interfaces need to manually be enabled and only expose a
+subset of APIs due to security reasons. These can be turned on/off and configured as
+you'd expect.
+
+HTTP based JSON-RPC API options:
+
+  * `--http` Enable the HTTP-RPC server
+  * `--http.addr` HTTP-RPC server listening interface (default: `localhost`)
+  * `--http.port` HTTP-RPC server listening port (default: `8545`)
+  * `--http.api` API's offered over the HTTP-RPC interface (default: `eth,net,web3`)
+  * `--http.corsdomain` Comma separated list of domains from which to accept cross origin requests (browser enforced)
+  * `--ws` Enable the WS-RPC server
+  * `--ws.addr` WS-RPC server listening interface (default: `localhost`)
+  * `--ws.port` WS-RPC server listening port (default: `8546`)
+  * `--ws.api` API's offered over the WS-RPC interface (default: `eth,net,web3`)
+  * `--ws.origins` Origins from which to accept websockets requests
+  * `--ipcdisable` Disable the IPC-RPC server
+  * `--ipcapi` API's offered over the IPC-RPC interface (default: `admin,debug,eth,miner,net,personal,txpool,web3`)
+  * `--ipcpath` Filename for IPC socket/pipe within the datadir (explicit paths escape it)
+
+You'll need to use your own programming environments' capabilities (libraries, tools, etc) to
+connect via HTTP, WS or IPC to a `geth` node configured with the above flags and you'll
+need to speak [JSON-RPC](https://www.jsonrpc.org/specification) on all transports. You
+can reuse the same connection for multiple requests!
+
+**Note: Please understand the security implications of opening up an HTTP/WS based
+transport before doing so! Hackers on the internet are actively trying to subvert
+Ethereum nodes with exposed APIs! Further, all browser tabs can access locally
+running web servers, so malicious web pages could try to subvert locally available
+APIs!**
+
+### Operating a private network
+
+Maintaining your own private network is more involved as a lot of configurations taken for
+granted in the official networks need to be manually set up.
+
+#### Defining the private genesis state
+
+First, you'll need to create the genesis state of your networks, which all nodes need to be
+aware of and agree upon. This consists of a small JSON file (e.g. call it `genesis.json`):
+
+```json
+{
+  "config": {
+    "chainId": <arbitrary positive integer>,
+    "homesteadBlock": 0,
+    "eip150Block": 0,
+    "eip155Block": 0,
+    "eip158Block": 0,
+    "byzantiumBlock": 0,
+    "constantinopleBlock": 0,
+    "petersburgBlock": 0,
+    "istanbulBlock": 0,
+    "berlinBlock": 0,
+    "londonBlock": 0
+  },
+  "alloc": {},
+  "coinbase": "0x0000000000000000000000000000000000000000",
+  "difficulty": "0x20000",
+  "extraData": "",
+  "gasLimit": "0x2fefd8",
+  "nonce": "0x0000000000000042",
+  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+  "timestamp": "0x00"
+}
+```
+
+The above fields should be fine for most purposes, although we'd recommend changing
+the `nonce` to some random value so you prevent unknown remote nodes from being able
+to connect to you. If you'd like to pre-fund some accounts for easier testing, create
+the accounts and populate the `alloc` field with their addresses.
+
+```json
+"alloc": {
+  "0x0000000000000000000000000000000000000001": {
+    "balance": "111111111"
+  },
+  "0x0000000000000000000000000000000000000002": {
+    "balance": "222222222"
+  }
+}
+```
+
+With the genesis state defined in the above JSON file, you'll need to initialize **every**
+`geth` node with it prior to starting it up to ensure all blockchain parameters are correctly
+set:
+
+```shell
+$ geth init path/to/genesis.json
+```
+
+#### Creating the rendezvous point
+
+With all nodes that you want to run initialized to the desired genesis state, you'll need to
+start a bootstrap node that others can use to find each other in your network and/or over
+the internet. The clean way is to configure and run a dedicated bootnode:
+
+```shell
+$ bootnode --genkey=boot.key
+$ bootnode --nodekey=boot.key
+```
+
+With the bootnode online, it will display an [`enode` URL](https://ethereum.org/en/developers/docs/networking-layer/network-addresses/#enode)
+that other nodes can use to connect to it and exchange peer information. Make sure to
+replace the displayed IP address information (most probably `[::]`) with your externally
+accessible IP to get the actual `enode` URL.
+
+*Note: You could also use a full-fledged `geth` node as a bootnode, but it's the less
+recommended way.*
+
+#### Starting up your member nodes
+
+With the bootnode operational and externally reachable (you can try
+`telnet <ip> <port>` to ensure it's indeed reachable), start every subsequent `geth`
+node pointed to the bootnode for peer discovery via the `--bootnodes` flag. It will
+probably also be desirable to keep the data directory of your private network separated, so
+do also specify a custom `--datadir` flag.
+
+```shell
+$ geth --datadir=path/to/custom/data/folder --bootnodes=<bootnode-enode-url-from-above>
+```
+
+*Note: Since your network will be completely cut off from the main and test networks, you'll
+also need to configure a miner to process transactions and create new blocks for you.*
+
+#### Running a private miner
+
+Mining on the public Ethereum network is a complex task as it's only feasible using GPUs,
+requiring an OpenCL or CUDA enabled `ethminer` instance. For information on such a
+setup, please consult the [EtherMining subreddit](https://www.reddit.com/r/EtherMining/)
+and the [ethminer](https://github.com/ethereum-mining/ethminer) repository.
+
+In a private network setting, however a single CPU miner instance is more than enough for
+practical purposes as it can produce a stable stream of blocks at the correct intervals
+without needing heavy resources (consider running on a single thread, no need for multiple
+ones either). To start a `geth` instance for mining, run it with all your usual flags, extended
+by:
+
+```shell
+$ geth <usual-flags> --mine --miner.threads=1 --miner.etherbase=0x0000000000000000000000000000000000000000
+```
+
+Which will start mining blocks and transactions on a single CPU thread, crediting all
+proceedings to the account specified by `--miner.etherbase`. You can further tune the mining
+by changing the default gas limit blocks converge to (`--miner.targetgaslimit`) and the price
+transactions are accepted at (`--miner.gasprice`).
+
+## Contribution
+
+Thank you for considering to help out with the source code! We welcome contributions
+from anyone on the internet, and are grateful for even the smallest of fixes!
+
+If you'd like to contribute to go-ethereum, please fork, fix, commit and send a pull request
+for the maintainers to review and merge into the main code base. If you wish to submit
+more complex changes though, please check up with the core devs first on [our Discord Server](https://discord.gg/invite/nthXNEv)
+to ensure those changes are in line with the general philosophy of the project and/or get
+some early feedback which can make both your efforts much lighter as well as our review
+and merge procedures quick and simple.
+
+Please make sure your contributions adhere to our coding guidelines:
+
+ * Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting)
+   guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)).
+ * Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary)
+   guidelines.
+ * Pull requests need to be based on and opened against the `master` branch.
+ * Commit messages should be prefixed with the package(s) they modify.
+   * E.g. "eth, rpc: make trace configs optional"
+
+Please see the [Developers' Guide](https://geth.ethereum.org/docs/developers/devguide)
+for more details on configuring your environment, managing project dependencies, and
+testing procedures.
+
+## License
+
+The go-ethereum library (i.e. all code outside of the `cmd` directory) is licensed under the
+[GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html),
+also included in our repository in the `COPYING.LESSER` file.
+
+The go-ethereum binaries (i.e. all code inside of the `cmd` directory) is licensed under the
+[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), also
+included in our repository in the `COPYING` file.
diff --git a/builder/beacon_client.go b/builder/beacon_client.go
new file mode 100644
index 0000000000..769a5674c4
--- /dev/null
+++ b/builder/beacon_client.go
@@ -0,0 +1,201 @@
+package builder
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"net/http"
+	"strconv"
+	"sync"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+type testBeaconClient struct {
+	validator *ValidatorPrivateData
+	slot      uint64
+}
+
+func (b *testBeaconClient) isValidator(pubkey PubkeyHex) bool {
+	return true
+}
+func (b *testBeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) {
+	return PubkeyHex(hexutil.Encode(b.validator.Pk)), nil
+}
+func (b *testBeaconClient) onForkchoiceUpdate() (uint64, error) {
+	return b.slot, nil
+}
+
+type BeaconClient struct {
+	endpoint string
+
+	mu               sync.Mutex
+	currentEpoch     uint64
+	currentSlot      uint64
+	nextSlotProposer PubkeyHex
+	slotProposerMap  map[uint64]PubkeyHex
+}
+
+func NewBeaconClient(endpoint string) *BeaconClient {
+	return &BeaconClient{
+		endpoint:        endpoint,
+		slotProposerMap: make(map[uint64]PubkeyHex),
+	}
+}
+
+func (b *BeaconClient) isValidator(pubkey PubkeyHex) bool {
+	return true
+}
+
+func (b *BeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) {
+	/* Only returns proposer if requestedSlot is currentSlot + 1, would be a race otherwise */
+	b.mu.Lock()
+	defer b.mu.Unlock()
+
+	if b.currentSlot+1 != requestedSlot {
+		return PubkeyHex(""), errors.New("slot out of sync")
+	}
+	return b.nextSlotProposer, nil
+}
+
+/* Returns next slot's proposer pubkey */
+// TODO: what happens if no block for previous slot - should still get next slot
+func (b *BeaconClient) onForkchoiceUpdate() (uint64, error) {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+
+	currentSlot, err := fetchCurrentSlot(b.endpoint)
+	if err != nil {
+		return 0, err
+	}
+
+	nextSlot := currentSlot + 1
+
+	b.currentSlot = currentSlot
+	nextSlotEpoch := nextSlot / 32
+
+	if nextSlotEpoch != b.currentEpoch {
+		// TODO: this should be prepared in advance, possibly just fetch for next epoch in advance
+		slotProposerMap, err := fetchEpochProposersMap(b.endpoint, nextSlotEpoch)
+		if err != nil {
+			return 0, err
+		}
+
+		b.currentEpoch = nextSlotEpoch
+		b.slotProposerMap = slotProposerMap
+	}
+
+	nextSlotProposer, found := b.slotProposerMap[nextSlot]
+	if !found {
+		log.Error("inconsistent proposer mapping", "currentSlot", currentSlot, "slotProposerMap", b.slotProposerMap)
+		return 0, errors.New("inconsistent proposer mapping")
+	}
+	b.nextSlotProposer = nextSlotProposer
+	return nextSlot, nil
+}
+
+func fetchCurrentSlot(endpoint string) (uint64, error) {
+	headerRes := &struct {
+		Data []struct {
+			Root      common.Hash `json:"root"`
+			Canonical bool        `json:"canonical"`
+			Header    struct {
+				Message struct {
+					Slot          string      `json:"slot"`
+					ProposerIndex string      `json:"proposer_index"`
+					ParentRoot    common.Hash `json:"parent_root"`
+					StateRoot     common.Hash `json:"state_root"`
+					BodyRoot      common.Hash `json:"body_root"`
+				} `json:"message"`
+				Signature hexutil.Bytes `json:"signature"`
+			} `json:"header"`
+		} `json:"data"`
+	}{}
+
+	err := fetchBeacon(endpoint+"/eth/v1/beacon/headers", headerRes)
+	if err != nil {
+		return uint64(0), err
+	}
+
+	if len(headerRes.Data) != 1 {
+		return uint64(0), errors.New("invalid response")
+	}
+
+	slot, err := strconv.Atoi(headerRes.Data[0].Header.Message.Slot)
+	if err != nil {
+		log.Error("could not parse slot", "Slot", headerRes.Data[0].Header.Message.Slot, "err", err)
+		return uint64(0), errors.New("invalid response")
+	}
+	return uint64(slot), nil
+}
+
+func fetchEpochProposersMap(endpoint string, epoch uint64) (map[uint64]PubkeyHex, error) {
+	proposerDutiesResponse := &struct {
+		Data []struct {
+			PubkeyHex string `json:"pubkey"`
+			Slot      string `json:"slot"`
+		} `json:"data"`
+	}{}
+
+	err := fetchBeacon(fmt.Sprintf("%s/eth/v1/validator/duties/proposer/%d", endpoint, epoch), proposerDutiesResponse)
+	if err != nil {
+		return nil, err
+	}
+
+	proposersMap := make(map[uint64]PubkeyHex)
+	for _, proposerDuty := range proposerDutiesResponse.Data {
+		slot, err := strconv.Atoi(proposerDuty.Slot)
+		if err != nil {
+			log.Error("could not parse slot", "Slot", proposerDuty.Slot, "err", err)
+			continue
+		}
+		proposersMap[uint64(slot)] = PubkeyHex(proposerDuty.PubkeyHex)
+	}
+	return proposersMap, nil
+}
+
+func fetchBeacon(url string, dst any) error {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		log.Error("invalid request", "url", url, "err", err)
+		return err
+	}
+	req.Header.Set("accept", "application/json")
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Error("client refused", "url", url, "err", err)
+		return err
+	}
+	defer resp.Body.Close()
+
+	bodyBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		log.Error("could not read response body", "url", url, "err", err)
+		return err
+	}
+
+	if resp.StatusCode >= 300 {
+		ec := &struct {
+			Code    int    `json:"code"`
+			Message string `json:"message"`
+		}{}
+		if err = json.Unmarshal(bodyBytes, ec); err != nil {
+			log.Error("Couldn't unmarshal error from beacon node", "url", url, "body", string(bodyBytes))
+			return errors.New("could not unmarshal error response from beacon node")
+		}
+		return errors.New(ec.Message)
+	}
+
+	err = json.Unmarshal(bodyBytes, dst)
+	if err != nil {
+		log.Error("could not unmarshal response", "url", url, "resp", string(bodyBytes), "dst", dst, "err", err)
+		return err
+	}
+
+	log.Info("fetched", "url", url, "res", dst)
+	return nil
+}
diff --git a/builder/beacon_client_test.go b/builder/beacon_client_test.go
new file mode 100644
index 0000000000..564275e5ad
--- /dev/null
+++ b/builder/beacon_client_test.go
@@ -0,0 +1,268 @@
+package builder
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"strconv"
+	"testing"
+
+	"github.com/gorilla/mux"
+	"github.com/stretchr/testify/require"
+)
+
+type mockBeaconNode struct {
+	srv *httptest.Server
+
+	proposerDuties map[int][]byte
+	forkResp       map[int][]byte
+	headersCode    int
+	headersResp    []byte
+}
+
+func newMockBeaconNode() *mockBeaconNode {
+	r := mux.NewRouter()
+	srv := httptest.NewServer(r)
+
+	mbn := &mockBeaconNode{
+		srv: srv,
+
+		proposerDuties: make(map[int][]byte),
+		forkResp:       make(map[int][]byte),
+		headersCode:    200,
+		headersResp:    []byte{},
+	}
+
+	r.HandleFunc("/eth/v1/validator/duties/proposer/{epoch}", func(w http.ResponseWriter, r *http.Request) {
+		vars := mux.Vars(r)
+		epochStr, ok := vars["epoch"]
+		if !ok {
+			http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400)
+			return
+		}
+		epoch, err := strconv.Atoi(epochStr)
+		if err != nil {
+			http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400)
+			return
+		}
+
+		resp, found := mbn.proposerDuties[epoch]
+		if !found {
+			http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400)
+			return
+		}
+
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(resp)
+	})
+
+	r.HandleFunc("/eth/v1/beacon/headers", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(mbn.headersCode)
+		w.Write(mbn.headersResp)
+	})
+
+	return mbn
+}
+
+func TestFetchBeacon(t *testing.T) {
+	mbn := newMockBeaconNode()
+	defer mbn.srv.Close()
+
+	mbn.headersCode = 200
+	mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "10", "proposer_index": "1" } } } ] }`)
+
+	// Green path
+	headersResp := struct {
+		Data []struct {
+			Header struct {
+				Message struct {
+					Slot string `json:"slot"`
+				} `json:"message"`
+			} `json:"header"`
+		} `json:"data"`
+	}{}
+	err := fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &headersResp)
+	require.NoError(t, err)
+	require.Equal(t, "10", headersResp.Data[0].Header.Message.Slot)
+
+	// Wrong dst
+	wrongForkResp := struct {
+		Data []struct {
+			Slot string `json:"slot"`
+		}
+	}{}
+	err = fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &wrongForkResp)
+	require.NoError(t, err)
+	require.Equal(t, wrongForkResp.Data[0].Slot, "")
+
+	mbn.headersCode = 400
+	mbn.headersResp = []byte(`{ "code": 400, "message": "Invalid call" }`)
+	err = fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &headersResp)
+	require.EqualError(t, err, "Invalid call")
+}
+
+func TestFetchCurrentSlot(t *testing.T) {
+	mbn := newMockBeaconNode()
+	defer mbn.srv.Close()
+
+	mbn.headersResp = []byte(`{
+  "execution_optimistic": false,
+  "data": [
+    {
+      "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+      "canonical": true,
+      "header": {
+        "message": {
+          "slot": "101",
+          "proposer_index": "1",
+          "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+          "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+          "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
+        },
+        "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
+      }
+    }
+  ]
+}`)
+
+	slot, err := fetchCurrentSlot(mbn.srv.URL)
+	require.NoError(t, err)
+	require.Equal(t, uint64(101), slot)
+
+	mbn.headersResp = []byte(`{
+  "execution_optimistic": false,
+  "data": [
+    {
+      "header": {
+        "message": {
+          "slot": "xxx"
+        }
+      }
+    }
+  ]
+}`)
+
+	slot, err = fetchCurrentSlot(mbn.srv.URL)
+	require.EqualError(t, err, "invalid response")
+	require.Equal(t, uint64(0), slot)
+}
+
+func TestFetchEpochProposersMap(t *testing.T) {
+	mbn := newMockBeaconNode()
+	defer mbn.srv.Close()
+
+	mbn.proposerDuties[10] = []byte(`{
+  "dependent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+  "execution_optimistic": false,
+  "data": [
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+      "validator_index": "1",
+      "slot": "1"
+    },
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b",
+      "validator_index": "2",
+      "slot": "2"
+    }
+  ]
+}`)
+
+	proposersMap, err := fetchEpochProposersMap(mbn.srv.URL, 10)
+	require.NoError(t, err)
+	require.Equal(t, 2, len(proposersMap))
+	require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"), proposersMap[1])
+	require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), proposersMap[2])
+}
+
+func TestOnForkchoiceUpdate(t *testing.T) {
+	mbn := newMockBeaconNode()
+	defer mbn.srv.Close()
+
+	mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "31", "proposer_index": "1" } } } ] }`)
+
+	mbn.proposerDuties[1] = []byte(`{
+  "dependent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
+  "execution_optimistic": false,
+  "data": [
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+      "validator_index": "1",
+      "slot": "31"
+    },
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b",
+      "validator_index": "2",
+      "slot": "32"
+    },
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74c",
+      "validator_index": "3",
+      "slot": "33"
+    }
+  ]
+}`)
+
+	bc := NewBeaconClient(mbn.srv.URL)
+	slot, err := bc.onForkchoiceUpdate()
+	require.NoError(t, err)
+	require.Equal(t, slot, uint64(32))
+
+	pubkeyHex, err := bc.getProposerForNextSlot(32)
+	require.NoError(t, err)
+	require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), pubkeyHex)
+
+	_, err = bc.getProposerForNextSlot(31)
+	require.EqualError(t, err, "slot out of sync")
+
+	_, err = bc.getProposerForNextSlot(33)
+	require.EqualError(t, err, "slot out of sync")
+
+	mbn.headersCode = 404
+	mbn.headersResp = []byte(`{ "code": 404, "message": "State not found" }`)
+
+	slot, err = NewBeaconClient(mbn.srv.URL).onForkchoiceUpdate()
+	require.EqualError(t, err, "State not found")
+	require.Equal(t, slot, uint64(0))
+
+	// Check that client does not fetch new proposers if epoch did not change
+	mbn.headersCode = 200
+	mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "31", "proposer_index": "1" } } } ] }`)
+	mbn.proposerDuties[1] = []byte(`{
+  "data": [
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74d",
+      "validator_index": "4",
+      "slot": "32"
+    }
+  ]
+}`)
+
+	slot, err = bc.onForkchoiceUpdate()
+	require.NoError(t, err, "")
+	require.Equal(t, slot, uint64(32))
+
+	mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "63", "proposer_index": "1" } } } ] }`)
+	mbn.proposerDuties[2] = []byte(`{
+  "data": [
+    {
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74d",
+      "validator_index": "4",
+      "slot": "64"
+    }
+  ]
+}`)
+
+	slot, err = bc.onForkchoiceUpdate()
+	require.NoError(t, err, "")
+	require.Equal(t, slot, uint64(64))
+
+	pubkeyHex, err = bc.getProposerForNextSlot(64)
+	require.NoError(t, err)
+	require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74d"), pubkeyHex)
+
+	// Check proposers map error is routed out
+	mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "65", "proposer_index": "1" } } } ] }`)
+	_, err = bc.onForkchoiceUpdate()
+	require.EqualError(t, err, "inconsistent proposer mapping")
+}
diff --git a/builder/builder.go b/builder/builder.go
new file mode 100644
index 0000000000..7dde6fe325
--- /dev/null
+++ b/builder/builder.go
@@ -0,0 +1,327 @@
+package builder
+
+import (
+	"context"
+	"errors"
+	"math/big"
+	_ "os"
+	"sync"
+	"time"
+
+	blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation"
+	"golang.org/x/time/rate"
+
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/flashbotsextra"
+	"github.com/ethereum/go-ethereum/log"
+
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+)
+
+type PubkeyHex string
+
+type ValidatorData struct {
+	Pubkey       PubkeyHex
+	FeeRecipient boostTypes.Address `json:"feeRecipient"`
+	GasLimit     uint64             `json:"gasLimit"`
+	Timestamp    uint64             `json:"timestamp"`
+}
+
+type IBeaconClient interface {
+	isValidator(pubkey PubkeyHex) bool
+	getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error)
+	onForkchoiceUpdate() (uint64, error)
+}
+
+type IRelay interface {
+	SubmitBlock(msg *boostTypes.BuilderSubmitBlockRequest) error
+	GetValidatorForSlot(nextSlot uint64) (ValidatorData, error)
+}
+
+type IBuilder interface {
+	OnPayloadAttribute(attrs *BuilderPayloadAttributes) error
+	Start() error
+	Stop() error
+}
+
+type Builder struct {
+	ds                   flashbotsextra.IDatabaseService
+	relay                IRelay
+	eth                  IEthereumService
+	dryRun               bool
+	validator            *blockvalidation.BlockValidationAPI
+	builderSecretKey     *bls.SecretKey
+	builderPublicKey     boostTypes.PublicKey
+	builderSigningDomain boostTypes.Domain
+
+	limiter *rate.Limiter
+
+	slotMu        sync.Mutex
+	slot          uint64
+	slotAttrs     []BuilderPayloadAttributes
+	slotCtx       context.Context
+	slotCtxCancel context.CancelFunc
+}
+
+func NewBuilder(sk *bls.SecretKey, ds flashbotsextra.IDatabaseService, relay IRelay, builderSigningDomain boostTypes.Domain, eth IEthereumService, dryRun bool, validator *blockvalidation.BlockValidationAPI) *Builder {
+	pkBytes := bls.PublicKeyFromSecretKey(sk).Compress()
+	pk := boostTypes.PublicKey{}
+	pk.FromSlice(pkBytes)
+
+	slotCtx, slotCtxCancel := context.WithCancel(context.Background())
+	return &Builder{
+		ds:                   ds,
+		relay:                relay,
+		eth:                  eth,
+		dryRun:               dryRun,
+		validator:            validator,
+		builderSecretKey:     sk,
+		builderPublicKey:     pk,
+		builderSigningDomain: builderSigningDomain,
+
+		limiter:       rate.NewLimiter(rate.Every(time.Millisecond), 510),
+		slot:          0,
+		slotCtx:       slotCtx,
+		slotCtxCancel: slotCtxCancel,
+	}
+}
+
+func (b *Builder) Start() error {
+	return nil
+}
+
+func (b *Builder) Stop() error {
+	return nil
+}
+
+func (b *Builder) onSealedBlock(block *types.Block, ordersClosedAt time.Time, sealedAt time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, proposerPubkey boostTypes.PublicKey, proposerFeeRecipient boostTypes.Address, proposerRegisteredGasLimit uint64, attrs *BuilderPayloadAttributes) error {
+	executableData := beacon.BlockToExecutableData(block)
+	payload, err := executableDataToExecutionPayload(executableData)
+	if err != nil {
+		log.Error("could not format execution payload", "err", err)
+		return err
+	}
+
+	value := new(boostTypes.U256Str)
+	err = value.FromBig(block.Profit)
+	if err != nil {
+		log.Error("could not set block value", "err", err)
+		return err
+	}
+
+	blockBidMsg := boostTypes.BidTrace{
+		Slot:                 attrs.Slot,
+		ParentHash:           payload.ParentHash,
+		BlockHash:            payload.BlockHash,
+		BuilderPubkey:        b.builderPublicKey,
+		ProposerPubkey:       proposerPubkey,
+		ProposerFeeRecipient: proposerFeeRecipient,
+		GasLimit:             executableData.GasLimit,
+		GasUsed:              executableData.GasUsed,
+		Value:                *value,
+	}
+
+	signature, err := boostTypes.SignMessage(&blockBidMsg, b.builderSigningDomain, b.builderSecretKey)
+	if err != nil {
+		log.Error("could not sign builder bid", "err", err)
+		return err
+	}
+
+	blockSubmitReq := boostTypes.BuilderSubmitBlockRequest{
+		Signature:        signature,
+		Message:          &blockBidMsg,
+		ExecutionPayload: payload,
+	}
+
+	if b.dryRun {
+		err = b.validator.ValidateBuilderSubmissionV1(&blockvalidation.BuilderBlockValidationRequest{blockSubmitReq, proposerRegisteredGasLimit})
+		if err != nil {
+			log.Error("could not validate block", "err", err)
+		}
+	} else {
+		go b.ds.ConsumeBuiltBlock(block, ordersClosedAt, sealedAt, commitedBundles, allBundles, &blockBidMsg)
+		err = b.relay.SubmitBlock(&blockSubmitReq)
+		if err != nil {
+			log.Error("could not submit block", "err", err, "#commitedBundles", len(commitedBundles))
+			return err
+		}
+	}
+
+	log.Info("submitted block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles))
+
+	return nil
+}
+
+func (b *Builder) OnPayloadAttribute(attrs *BuilderPayloadAttributes) error {
+	if attrs == nil {
+		return nil
+	}
+
+	vd, err := b.relay.GetValidatorForSlot(attrs.Slot)
+	if err != nil {
+		log.Info("could not get validator while submitting block", "err", err, "slot", attrs.Slot)
+		return err
+	}
+
+	attrs.SuggestedFeeRecipient = [20]byte(vd.FeeRecipient)
+	attrs.GasLimit = vd.GasLimit
+
+	proposerPubkey, err := boostTypes.HexToPubkey(string(vd.Pubkey))
+	if err != nil {
+		log.Error("could not parse pubkey", "err", err, "pubkey", vd.Pubkey)
+		return err
+	}
+
+	if !b.eth.Synced() {
+		return errors.New("backend not Synced")
+	}
+
+	parentBlock := b.eth.GetBlockByHash(attrs.HeadHash)
+	if parentBlock == nil {
+		log.Warn("Block hash not found in blocktree", "head block hash", attrs.HeadHash)
+		return errors.New("parent block not found in blocktree")
+	}
+
+	b.slotMu.Lock()
+	defer b.slotMu.Unlock()
+
+	if b.slot != attrs.Slot {
+		if b.slotCtxCancel != nil {
+			b.slotCtxCancel()
+		}
+
+		slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 12*time.Second)
+		b.slot = attrs.Slot
+		b.slotAttrs = nil
+		b.slotCtx = slotCtx
+		b.slotCtxCancel = slotCtxCancel
+	}
+
+	for _, currentAttrs := range b.slotAttrs {
+		if *attrs == currentAttrs {
+			log.Debug("ignoring known payload attribute", "slot", attrs.Slot, "hash", attrs.HeadHash)
+			return nil
+		}
+	}
+	b.slotAttrs = append(b.slotAttrs, *attrs)
+
+	go b.runBuildingJob(b.slotCtx, proposerPubkey, vd.FeeRecipient, vd.GasLimit, attrs)
+	return nil
+}
+
+type blockQueueEntry struct {
+	block           *types.Block
+	ordersCloseTime time.Time
+	sealedAt        time.Time
+	commitedBundles []types.SimulatedBundle
+	allBundles      []types.SimulatedBundle
+}
+
+func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey boostTypes.PublicKey, feeRecipient boostTypes.Address, proposerRegisteredGasLimit uint64, attrs *BuilderPayloadAttributes) {
+	ctx, cancel := context.WithTimeout(slotCtx, 12*time.Second)
+	defer cancel()
+
+	// Submission queue for the given payload attributes
+	// multiple jobs can run for different attributes fot the given slot
+	// 1. When new block is ready we check if its profit is higher than profit of last best block
+	//    if it is we set queueBest* to values of the new block and notify queueSignal channel.
+	// 2. Submission goroutine waits for queueSignal and submits queueBest* if its more valuable than
+	//    queueLastSubmittedProfit keeping queueLastSubmittedProfit to be the profit of the last submission.
+	//    Submission goroutine is globally rate limited to have fixed rate of submissions for all jobs.
+	var (
+		queueSignal = make(chan struct{}, 1)
+
+		queueMu                  sync.Mutex
+		queueLastSubmittedProfit = new(big.Int)
+		queueBestProfit          = new(big.Int)
+		queueBestEntry           blockQueueEntry
+	)
+
+	log.Debug("runBuildingJob", "slot", attrs.Slot, "parent", attrs.HeadHash)
+
+	submitBestBlock := func() {
+		queueMu.Lock()
+		if queueLastSubmittedProfit.Cmp(queueBestProfit) < 0 {
+			err := b.onSealedBlock(queueBestEntry.block, queueBestEntry.ordersCloseTime, queueBestEntry.sealedAt, queueBestEntry.commitedBundles, queueBestEntry.allBundles, proposerPubkey, feeRecipient, proposerRegisteredGasLimit, attrs)
+
+			if err != nil {
+				log.Error("could not run sealed block hook", "err", err)
+			} else {
+				queueLastSubmittedProfit.Set(queueBestProfit)
+			}
+		}
+		queueMu.Unlock()
+	}
+
+	// Empties queue, submits the best block for current job with rate limit (global for all jobs)
+	go runResubmitLoop(ctx, b.limiter, queueSignal, submitBestBlock)
+
+	// Populates queue with submissions that increase block profit
+	blockHook := func(block *types.Block, ordersCloseTime time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle) {
+		if ctx.Err() != nil {
+			return
+		}
+
+		sealedAt := time.Now()
+
+		queueMu.Lock()
+		defer queueMu.Unlock()
+		if block.Profit.Cmp(queueBestProfit) > 0 {
+			queueBestEntry = blockQueueEntry{
+				block:           block,
+				ordersCloseTime: ordersCloseTime,
+				sealedAt:        sealedAt,
+				commitedBundles: commitedBundles,
+				allBundles:      allBundles,
+			}
+			queueBestProfit.Set(block.Profit)
+
+			select {
+			case queueSignal <- struct{}{}:
+			default:
+			}
+		}
+	}
+
+	// resubmits block builder requests every second
+	runRetryLoop(ctx, 500*time.Millisecond, func() {
+		log.Debug("retrying BuildBlock", "slot", attrs.Slot, "parent", attrs.HeadHash)
+		err := b.eth.BuildBlock(attrs, blockHook)
+		if err != nil {
+			log.Warn("Failed to build block", "err", err)
+		}
+	})
+}
+
+func executableDataToExecutionPayload(data *beacon.ExecutableDataV1) (*boostTypes.ExecutionPayload, error) {
+	transactionData := make([]hexutil.Bytes, len(data.Transactions))
+	for i, tx := range data.Transactions {
+		transactionData[i] = hexutil.Bytes(tx)
+	}
+
+	baseFeePerGas := new(boostTypes.U256Str)
+	err := baseFeePerGas.FromBig(data.BaseFeePerGas)
+	if err != nil {
+		return nil, err
+	}
+
+	return &boostTypes.ExecutionPayload{
+		ParentHash:    [32]byte(data.ParentHash),
+		FeeRecipient:  [20]byte(data.FeeRecipient),
+		StateRoot:     [32]byte(data.StateRoot),
+		ReceiptsRoot:  [32]byte(data.ReceiptsRoot),
+		LogsBloom:     boostTypes.Bloom(types.BytesToBloom(data.LogsBloom)),
+		Random:        [32]byte(data.Random),
+		BlockNumber:   data.Number,
+		GasLimit:      data.GasLimit,
+		GasUsed:       data.GasUsed,
+		Timestamp:     data.Timestamp,
+		ExtraData:     data.ExtraData,
+		BaseFeePerGas: *baseFeePerGas,
+		BlockHash:     [32]byte(data.BlockHash),
+		Transactions:  transactionData,
+	}, nil
+}
diff --git a/builder/builder_test.go b/builder/builder_test.go
new file mode 100644
index 0000000000..09d4c5b728
--- /dev/null
+++ b/builder/builder_test.go
@@ -0,0 +1,139 @@
+package builder
+
+import (
+	"math/big"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/flashbotsextra"
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/stretchr/testify/require"
+)
+
+func TestOnPayloadAttributes(t *testing.T) {
+	vsk, err := bls.SecretKeyFromBytes(hexutil.MustDecode("0x370bb8c1a6e62b2882f6ec76762a67b39609002076b95aae5b023997cf9b2dc9"))
+	require.NoError(t, err)
+	validator := &ValidatorPrivateData{
+		sk: vsk,
+		Pk: hexutil.MustDecode("0xb67d2c11bcab8c4394fc2faa9601d0b99c7f4b37e14911101da7d97077917862eed4563203d34b91b5cf0aa44d6cfa05"),
+	}
+
+	testBeacon := testBeaconClient{
+		validator: validator,
+		slot:      56,
+	}
+
+	feeRecipient, _ := boostTypes.HexToAddress("0xabcf8e0d4e9587369b2301d0790347320302cc00")
+	testRelay := testRelay{
+		validator: ValidatorData{
+			Pubkey:       PubkeyHex(testBeacon.validator.Pk.String()),
+			FeeRecipient: feeRecipient,
+			GasLimit:     10,
+			Timestamp:    15,
+		},
+	}
+
+	sk, err := bls.SecretKeyFromBytes(hexutil.MustDecode("0x31ee185dad1220a8c88ca5275e64cf5a5cb09cb621cb30df52c9bee8fbaaf8d7"))
+	require.NoError(t, err)
+
+	bDomain := boostTypes.ComputeDomain(boostTypes.DomainTypeAppBuilder, [4]byte{0x02, 0x0, 0x0, 0x0}, boostTypes.Hash{})
+
+	testExecutableData := &beacon.ExecutableDataV1{
+		ParentHash:   common.Hash{0x02, 0x03},
+		FeeRecipient: common.Address(feeRecipient),
+		StateRoot:    common.Hash{0x07, 0x16},
+		ReceiptsRoot: common.Hash{0x08, 0x20},
+		LogsBloom:    types.Bloom{}.Bytes(),
+		Number:       uint64(10),
+		GasLimit:     uint64(50),
+		GasUsed:      uint64(100),
+		Timestamp:    uint64(105),
+		ExtraData:    hexutil.MustDecode("0x0042fafc"),
+
+		BaseFeePerGas: big.NewInt(16),
+
+		BlockHash:    common.HexToHash("0xca4147f0d4150183ece9155068f34ee3c375448814e4ca557d482b1d40ee5407"),
+		Transactions: [][]byte{},
+	}
+
+	testBlock, err := beacon.ExecutableDataToBlock(*testExecutableData)
+	require.NoError(t, err)
+	testBlock.Profit = big.NewInt(10)
+
+	testPayloadAttributes := &BuilderPayloadAttributes{
+		Timestamp:             hexutil.Uint64(104),
+		Random:                common.Hash{0x05, 0x10},
+		SuggestedFeeRecipient: common.Address{0x04, 0x10},
+		GasLimit:              uint64(21),
+		Slot:                  uint64(25),
+	}
+
+	testEthService := &testEthereumService{synced: true, testExecutableData: testExecutableData, testBlock: testBlock}
+
+	builder := NewBuilder(sk, flashbotsextra.NilDbService{}, &testRelay, bDomain, testEthService, false, nil)
+	builder.Start()
+	defer builder.Stop()
+
+	err = builder.OnPayloadAttribute(testPayloadAttributes)
+	require.NoError(t, err)
+	time.Sleep(time.Second * 3)
+
+	require.NotNil(t, testRelay.submittedMsg)
+	expectedProposerPubkey, err := boostTypes.HexToPubkey(testBeacon.validator.Pk.String())
+	require.NoError(t, err)
+
+	expectedMessage := boostTypes.BidTrace{
+		Slot:                 uint64(25),
+		ParentHash:           boostTypes.Hash{0x02, 0x03},
+		BuilderPubkey:        builder.builderPublicKey,
+		ProposerPubkey:       expectedProposerPubkey,
+		ProposerFeeRecipient: feeRecipient,
+		GasLimit:             uint64(50),
+		GasUsed:              uint64(100),
+		Value:                boostTypes.U256Str{0x0a},
+	}
+	expectedMessage.BlockHash.FromSlice(hexutil.MustDecode("0xca4147f0d4150183ece9155068f34ee3c375448814e4ca557d482b1d40ee5407")[:])
+
+	require.Equal(t, expectedMessage, *testRelay.submittedMsg.Message)
+
+	expectedExecutionPayload := boostTypes.ExecutionPayload{
+		ParentHash:    [32]byte(testExecutableData.ParentHash),
+		FeeRecipient:  feeRecipient,
+		StateRoot:     [32]byte(testExecutableData.StateRoot),
+		ReceiptsRoot:  [32]byte(testExecutableData.ReceiptsRoot),
+		LogsBloom:     [256]byte{},
+		Random:        [32]byte(testExecutableData.Random),
+		BlockNumber:   testExecutableData.Number,
+		GasLimit:      testExecutableData.GasLimit,
+		GasUsed:       testExecutableData.GasUsed,
+		Timestamp:     testExecutableData.Timestamp,
+		ExtraData:     hexutil.MustDecode("0x0042fafc"),
+		BaseFeePerGas: boostTypes.U256Str{0x10},
+		BlockHash:     expectedMessage.BlockHash,
+		Transactions:  []hexutil.Bytes{},
+	}
+
+	require.Equal(t, expectedExecutionPayload, *testRelay.submittedMsg.ExecutionPayload)
+
+	expectedSignature, err := boostTypes.HexToSignature("0xad09f171b1da05636acfc86778c319af69e39c79515d44bdfed616ba2ef677ffd4d155d87b3363c6bae651ce1e92786216b75f1ac91dd65f3b1d1902bf8485e742170732dd82ffdf4decb0151eeb7926dd053efa9794b2ebed1a203e62bb13e9")
+
+	require.NoError(t, err)
+	require.Equal(t, expectedSignature, testRelay.submittedMsg.Signature)
+
+	require.Equal(t, uint64(25), testRelay.requestedSlot)
+
+	// Clear the submitted message and check that the job will be ran again and but a new message will not be submitted since the profit is the same
+	testRelay.submittedMsg = nil
+	time.Sleep(2200 * time.Millisecond)
+	require.Nil(t, testRelay.submittedMsg)
+
+	// Up the profit, expect to get the block
+	testEthService.testBlock.Profit.SetInt64(11)
+	time.Sleep(2200 * time.Millisecond)
+	require.NotNil(t, testRelay.submittedMsg)
+}
diff --git a/builder/eth_service.go b/builder/eth_service.go
new file mode 100644
index 0000000000..47ef090d5c
--- /dev/null
+++ b/builder/eth_service.go
@@ -0,0 +1,76 @@
+package builder
+
+import (
+	"errors"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/miner"
+)
+
+type IEthereumService interface {
+	BuildBlock(attrs *BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error
+	GetBlockByHash(hash common.Hash) *types.Block
+	Synced() bool
+}
+
+type testEthereumService struct {
+	synced             bool
+	testExecutableData *beacon.ExecutableDataV1
+	testBlock          *types.Block
+	testBundlesMerged  []types.SimulatedBundle
+	testAllBundles     []types.SimulatedBundle
+}
+
+func (t *testEthereumService) BuildBlock(attrs *BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error {
+	sealedBlockCallback(t.testBlock, time.Now(), t.testBundlesMerged, t.testAllBundles)
+	return nil
+}
+
+func (t *testEthereumService) GetBlockByHash(hash common.Hash) *types.Block { return t.testBlock }
+
+func (t *testEthereumService) Synced() bool { return t.synced }
+
+type EthereumService struct {
+	eth *eth.Ethereum
+}
+
+func NewEthereumService(eth *eth.Ethereum) *EthereumService {
+	return &EthereumService{eth: eth}
+}
+
+func (s *EthereumService) BuildBlock(attrs *BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error {
+	// Send a request to generate a full block in the background.
+	// The result can be obtained via the returned channel.
+	resCh, err := s.eth.Miner().GetSealingBlockAsync(attrs.HeadHash, uint64(attrs.Timestamp), attrs.SuggestedFeeRecipient, attrs.GasLimit, attrs.Random, false, sealedBlockCallback)
+	if err != nil {
+		log.Error("Failed to create async sealing payload", "err", err)
+		return err
+	}
+
+	timer := time.NewTimer(4 * time.Second)
+	defer timer.Stop()
+
+	select {
+	case block := <-resCh:
+		if block == nil {
+			return errors.New("received nil block from sealing work")
+		}
+		return nil
+	case <-timer.C:
+		log.Error("timeout waiting for block", "parent hash", attrs.HeadHash, "slot", attrs.Slot)
+		return errors.New("timeout waiting for block result")
+	}
+}
+
+func (s *EthereumService) GetBlockByHash(hash common.Hash) *types.Block {
+	return s.eth.BlockChain().GetBlockByHash(hash)
+}
+
+func (s *EthereumService) Synced() bool {
+	return s.eth.Synced()
+}
diff --git a/builder/eth_service_test.go b/builder/eth_service_test.go
new file mode 100644
index 0000000000..e13a05e288
--- /dev/null
+++ b/builder/eth_service_test.go
@@ -0,0 +1,108 @@
+package builder
+
+import (
+	"math/big"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/eth/downloader"
+	"github.com/ethereum/go-ethereum/eth/ethconfig"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/p2p"
+	"github.com/ethereum/go-ethereum/params"
+	"github.com/stretchr/testify/require"
+)
+
+func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) {
+	db := rawdb.NewMemoryDatabase()
+	config := params.AllEthashProtocolChanges
+	genesis := &core.Genesis{
+		Config:     config,
+		Alloc:      core.GenesisAlloc{},
+		ExtraData:  []byte("test genesis"),
+		Timestamp:  9000,
+		BaseFee:    big.NewInt(params.InitialBaseFee),
+		Difficulty: big.NewInt(0),
+	}
+	gblock := genesis.ToBlock()
+	engine := ethash.NewFaker()
+	blocks, _ := core.GenerateChain(config, gblock, engine, db, n, nil)
+	totalDifficulty := big.NewInt(0)
+	for _, b := range blocks {
+		totalDifficulty.Add(totalDifficulty, b.Difficulty())
+	}
+	config.TerminalTotalDifficulty = totalDifficulty
+	return genesis, blocks
+}
+
+// startEthService creates a full node instance for testing.
+func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) {
+	t.Helper()
+
+	n, err := node.New(&node.Config{
+		P2P: p2p.Config{
+			ListenAddr:  "0.0.0.0:0",
+			NoDiscovery: true,
+			MaxPeers:    25,
+		}})
+	if err != nil {
+		t.Fatal("can't create node:", err)
+	}
+
+	ethcfg := &ethconfig.Config{Genesis: genesis, Ethash: ethash.Config{PowMode: ethash.ModeFake}, SyncMode: downloader.SnapSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
+	ethservice, err := eth.New(n, ethcfg)
+	if err != nil {
+		t.Fatal("can't create eth service:", err)
+	}
+	if err := n.Start(); err != nil {
+		t.Fatal("can't start node:", err)
+	}
+	if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil {
+		n.Close()
+		t.Fatal("can't import test blocks:", err)
+	}
+	time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event
+
+	ethservice.SetSynced()
+	return n, ethservice
+}
+
+func TestBuildBlock(t *testing.T) {
+	genesis, blocks := generatePreMergeChain(10)
+	n, ethservice := startEthService(t, genesis, blocks)
+	defer n.Close()
+
+	parent := ethservice.BlockChain().CurrentBlock()
+
+	testPayloadAttributes := &BuilderPayloadAttributes{
+		Timestamp:             hexutil.Uint64(parent.Time() + 1),
+		Random:                common.Hash{0x05, 0x10},
+		SuggestedFeeRecipient: common.Address{0x04, 0x10},
+		GasLimit:              uint64(4800000),
+		Slot:                  uint64(25),
+	}
+
+	service := NewEthereumService(ethservice)
+	service.eth.APIBackend.Miner().SetEtherbase(common.Address{0x05, 0x11})
+
+	err := service.BuildBlock(testPayloadAttributes, func(block *types.Block, _ time.Time, _ []types.SimulatedBundle, _ []types.SimulatedBundle) {
+		executableData := beacon.BlockToExecutableData(block)
+		require.Equal(t, common.Address{0x05, 0x11}, executableData.FeeRecipient)
+		require.Equal(t, common.Hash{0x05, 0x10}, executableData.Random)
+		require.Equal(t, parent.Hash(), executableData.ParentHash)
+		require.Equal(t, parent.Time()+1, executableData.Timestamp)
+		require.Equal(t, block.ParentHash(), parent.Hash())
+		require.Equal(t, block.Hash(), executableData.BlockHash)
+		require.Equal(t, block.Profit.Uint64(), uint64(0))
+	})
+
+	require.NoError(t, err)
+}
diff --git a/builder/index.go b/builder/index.go
new file mode 100644
index 0000000000..8a096dab3f
--- /dev/null
+++ b/builder/index.go
@@ -0,0 +1,106 @@
+package builder
+
+import (
+	"html/template"
+)
+
+func parseIndexTemplate() (*template.Template, error) {
+	return template.New("index").Parse(`
+<!DOCTYPE html>
+<html lang="en" class="no-js">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width">
+
+    <title>Boost Block Builder</title>
+
+    <meta name="description" content="MEV builder API">
+
+    <link rel="stylesheet" href="https://unpkg.com/purecss@2.1.0/build/pure-min.css" integrity="sha384-yHIFVG6ClnONEA5yB5DJXfW2/KC173DIQrYoZMEtBvGzmf0PKiGyNEqe9N6BNDBH" crossorigin="anonymous">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <style type="text/css">
+        body {
+            padding: 10px 40px;
+        }
+
+        pre {
+            text-align: left;
+        }
+
+        hr {
+            border-top: 1px solid #e5e5e5;
+            margin: 40px 0;
+        }
+    </style>
+</head>
+
+<body>
+
+
+    <div class="grids">
+        <div class="content">
+            <p>
+                <img style="float:right;" src="https://d33wubrfki0l68.cloudfront.net/ae8530415158fbbbbe17fb033855452f792606c7/fe19f/img/logo.png" />
+            <h1>
+                Boost Block Builder
+            </h1>
+            <h2>
+                Pubkey {{ .Pubkey }}
+            </h2>
+            <p>
+            <ul>
+                <li>Genesis fork version {{ .GenesisForkVersion }}</li>
+                <li>Bellatrix fork version {{ .BellatrixForkVersion }}</li>
+                <li>Genesis validators root {{ .GenesisValidatorsRoot }}</li>
+            </ul>
+            </p>
+            <p>
+            <ul>
+                <li>Builder signing domain {{ .BuilderSigningDomain }}</li>
+                <li>Proposer signing domain {{ .ProposerSigningDomain }}</li>
+            </ul>
+            </p>
+
+            <p>
+            <ul>
+                <li>More details: <a href="https://github.com/flashbots/mev-boost/wiki">github.com/flashbots/mev-boost/wiki</a></li>
+                <li>Issues & feedback: <a href="https://github.com/flashbots/boost-geth-builder/issues">github.com/flashbots/boost-geth-builder/issues</a> <a href="https://github.com/flashbots/mev-boost/issues">github.com/flashbots/mev-boost/issues</a></li>
+            </ul>
+
+            </p>
+
+            <hr>
+
+            <p>
+            <h2>
+				{{ .ValidatorsStats }}
+            </h2>
+            </p>
+
+            <hr>
+
+            <p>
+            <h2>
+                Best Header
+            </h2>
+            <pre>{{ .Header }}</pre>
+            </p>
+
+            <hr>
+
+            <p>
+            <h2>
+                Best Payload
+            </h2>
+            <pre>{{ .Blocks }}</pre>
+            </p>
+
+        </div>
+    </div>
+</body>
+
+</html>
+`)
+}
diff --git a/builder/local_relay.go b/builder/local_relay.go
new file mode 100644
index 0000000000..68ec8f91f1
--- /dev/null
+++ b/builder/local_relay.go
@@ -0,0 +1,378 @@
+package builder
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"html/template"
+	"net/http"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/gorilla/mux"
+)
+
+type ForkData struct {
+	GenesisForkVersion    string
+	BellatrixForkVersion  string
+	GenesisValidatorsRoot string
+}
+
+type LocalRelay struct {
+	beaconClient IBeaconClient
+
+	relaySecretKey        *bls.SecretKey
+	relayPublicKey        boostTypes.PublicKey
+	serializedRelayPubkey hexutil.Bytes
+
+	builderSigningDomain  boostTypes.Domain
+	proposerSigningDomain boostTypes.Domain
+
+	validatorsLock sync.RWMutex
+	validators     map[PubkeyHex]ValidatorData
+
+	enableBeaconChecks bool
+
+	bestDataLock sync.Mutex
+	bestHeader   *boostTypes.ExecutionPayloadHeader
+	bestPayload  *boostTypes.ExecutionPayload
+	profit       boostTypes.U256Str
+
+	indexTemplate *template.Template
+	fd            ForkData
+}
+
+func NewLocalRelay(sk *bls.SecretKey, beaconClient IBeaconClient, builderSigningDomain boostTypes.Domain, proposerSigningDomain boostTypes.Domain, fd ForkData, enableBeaconChecks bool) *LocalRelay {
+	pkBytes := bls.PublicKeyFromSecretKey(sk).Compress()
+	pk := boostTypes.PublicKey{}
+	pk.FromSlice(pkBytes)
+
+	indexTemplate, err := parseIndexTemplate()
+	if err != nil {
+		log.Error("could not parse index template", "err", err)
+		indexTemplate = nil
+	}
+
+	return &LocalRelay{
+		beaconClient: beaconClient,
+
+		relaySecretKey: sk,
+		relayPublicKey: pk,
+
+		builderSigningDomain:  builderSigningDomain,
+		proposerSigningDomain: proposerSigningDomain,
+		serializedRelayPubkey: pkBytes,
+
+		validators: make(map[PubkeyHex]ValidatorData),
+
+		enableBeaconChecks: enableBeaconChecks,
+
+		indexTemplate: indexTemplate,
+		fd:            fd,
+	}
+}
+
+func (r *LocalRelay) SubmitBlock(msg *boostTypes.BuilderSubmitBlockRequest) error {
+	payloadHeader, err := boostTypes.PayloadToPayloadHeader(msg.ExecutionPayload)
+	if err != nil {
+		log.Error("could not convert payload to header", "err", err)
+		return err
+	}
+
+	r.bestDataLock.Lock()
+	r.bestHeader = payloadHeader
+	r.bestPayload = msg.ExecutionPayload
+	r.profit = msg.Message.Value
+	r.bestDataLock.Unlock()
+
+	return nil
+}
+
+func (r *LocalRelay) handleRegisterValidator(w http.ResponseWriter, req *http.Request) {
+	payload := []boostTypes.SignedValidatorRegistration{}
+	if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
+		log.Error("could not decode payload", "err", err)
+		respondError(w, http.StatusBadRequest, "invalid payload")
+		return
+	}
+
+	for _, registerRequest := range payload {
+		if len(registerRequest.Message.Pubkey) != 48 {
+			respondError(w, http.StatusBadRequest, "invalid pubkey")
+			return
+		}
+
+		if len(registerRequest.Signature) != 96 {
+			respondError(w, http.StatusBadRequest, "invalid signature")
+			return
+		}
+
+		ok, err := boostTypes.VerifySignature(registerRequest.Message, r.builderSigningDomain, registerRequest.Message.Pubkey[:], registerRequest.Signature[:])
+		if !ok || err != nil {
+			log.Error("error verifying signature", "err", err)
+			respondError(w, http.StatusBadRequest, "invalid signature")
+			return
+		}
+
+		// Do not check timestamp before signature, as it would leak validator data
+		if registerRequest.Message.Timestamp > uint64(time.Now().Add(10*time.Second).Unix()) {
+			respondError(w, http.StatusBadRequest, "invalid payload")
+			return
+		}
+	}
+
+	for _, registerRequest := range payload {
+		pubkeyHex := PubkeyHex(registerRequest.Message.Pubkey.String())
+		if !r.beaconClient.isValidator(pubkeyHex) {
+			respondError(w, http.StatusBadRequest, "not a validator")
+			return
+		}
+	}
+
+	r.validatorsLock.Lock()
+	defer r.validatorsLock.Unlock()
+
+	for _, registerRequest := range payload {
+		pubkeyHex := PubkeyHex(registerRequest.Message.Pubkey.String())
+		if previousValidatorData, ok := r.validators[pubkeyHex]; ok {
+			if registerRequest.Message.Timestamp < previousValidatorData.Timestamp {
+				respondError(w, http.StatusBadRequest, "invalid timestamp")
+				return
+			}
+
+			if registerRequest.Message.Timestamp == previousValidatorData.Timestamp && (registerRequest.Message.FeeRecipient != previousValidatorData.FeeRecipient || registerRequest.Message.GasLimit != previousValidatorData.GasLimit) {
+				respondError(w, http.StatusBadRequest, "invalid timestamp")
+				return
+			}
+		}
+	}
+
+	for _, registerRequest := range payload {
+		pubkeyHex := PubkeyHex(strings.ToLower(registerRequest.Message.Pubkey.String()))
+		r.validators[pubkeyHex] = ValidatorData{
+			Pubkey:       pubkeyHex,
+			FeeRecipient: registerRequest.Message.FeeRecipient,
+			GasLimit:     registerRequest.Message.GasLimit,
+			Timestamp:    registerRequest.Message.Timestamp,
+		}
+
+		log.Info("registered validator", "pubkey", pubkeyHex, "data", r.validators[pubkeyHex])
+	}
+
+	w.WriteHeader(http.StatusOK)
+}
+
+func (r *LocalRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) {
+	pubkeyHex, err := r.beaconClient.getProposerForNextSlot(nextSlot)
+	if err != nil {
+		return ValidatorData{}, err
+	}
+
+	r.validatorsLock.RLock()
+	if vd, ok := r.validators[pubkeyHex]; ok {
+		r.validatorsLock.RUnlock()
+		return vd, nil
+	}
+	r.validatorsLock.RUnlock()
+	log.Info("no local entry for validator", "validator", pubkeyHex)
+	return ValidatorData{}, errors.New("missing validator")
+}
+
+func (r *LocalRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) {
+	vars := mux.Vars(req)
+	slot, err := strconv.Atoi(vars["slot"])
+	if err != nil {
+		respondError(w, http.StatusBadRequest, "incorrect slot")
+		return
+	}
+	parentHashHex := vars["parent_hash"]
+	pubkeyHex := PubkeyHex(strings.ToLower(vars["pubkey"]))
+
+	// Do not validate slot separately, it will create a race between slot update and proposer key
+	if nextSlotProposer, err := r.beaconClient.getProposerForNextSlot(uint64(slot)); err != nil || nextSlotProposer != pubkeyHex {
+		log.Error("getHeader requested for public key other than next slots proposer", "requested", pubkeyHex, "expected", nextSlotProposer)
+		w.WriteHeader(http.StatusNoContent)
+		return
+	}
+
+	// Only check if slot is within a couple of the expected one, otherwise will force validators resync
+	vd, err := r.GetValidatorForSlot(uint64(slot))
+	if err != nil {
+		respondError(w, http.StatusBadRequest, "unknown validator")
+		return
+	}
+	if vd.Pubkey != pubkeyHex {
+		respondError(w, http.StatusBadRequest, "unknown validator")
+		return
+	}
+
+	r.bestDataLock.Lock()
+	bestHeader := r.bestHeader
+	profit := r.profit
+	r.bestDataLock.Unlock()
+
+	if bestHeader == nil || bestHeader.ParentHash.String() != parentHashHex {
+		respondError(w, http.StatusBadRequest, "unknown payload")
+		return
+	}
+
+	bid := boostTypes.BuilderBid{
+		Header: bestHeader,
+		Value:  profit,
+		Pubkey: r.relayPublicKey,
+	}
+	signature, err := boostTypes.SignMessage(&bid, r.builderSigningDomain, r.relaySecretKey)
+	if err != nil {
+		respondError(w, http.StatusInternalServerError, "internal server error")
+		return
+	}
+
+	response := &boostTypes.GetHeaderResponse{
+		Version: "bellatrix",
+		Data:    &boostTypes.SignedBuilderBid{Message: &bid, Signature: signature},
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		respondError(w, http.StatusInternalServerError, "internal server error")
+		return
+	}
+}
+
+func (r *LocalRelay) handleGetPayload(w http.ResponseWriter, req *http.Request) {
+	payload := new(boostTypes.SignedBlindedBeaconBlock)
+	if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
+		respondError(w, http.StatusBadRequest, "invalid payload")
+		return
+	}
+
+	if len(payload.Signature) != 96 {
+		respondError(w, http.StatusBadRequest, "invalid signature")
+		return
+	}
+
+	nextSlotProposerPubkeyHex, err := r.beaconClient.getProposerForNextSlot(payload.Message.Slot)
+	if err != nil {
+		if r.enableBeaconChecks {
+			respondError(w, http.StatusBadRequest, "unknown validator")
+			return
+		}
+	}
+
+	nextSlotProposerPubkeyBytes, err := hexutil.Decode(string(nextSlotProposerPubkeyHex))
+	if err != nil {
+		if r.enableBeaconChecks {
+			respondError(w, http.StatusBadRequest, "unknown validator")
+			return
+		}
+	}
+
+	ok, err := boostTypes.VerifySignature(payload.Message, r.proposerSigningDomain, nextSlotProposerPubkeyBytes[:], payload.Signature[:])
+	if !ok || err != nil {
+		if r.enableBeaconChecks {
+			respondError(w, http.StatusBadRequest, "invalid signature")
+			return
+		}
+	}
+
+	r.bestDataLock.Lock()
+	bestHeader := r.bestHeader
+	bestPayload := r.bestPayload
+	r.bestDataLock.Unlock()
+
+	log.Info("Received blinded block", "payload", payload, "bestHeader", bestHeader)
+
+	if bestHeader == nil || bestPayload == nil {
+		respondError(w, http.StatusInternalServerError, "no payloads")
+		return
+	}
+
+	if !ExecutionPayloadHeaderEqual(bestHeader, payload.Message.Body.ExecutionPayloadHeader) {
+		respondError(w, http.StatusBadRequest, "unknown payload")
+		return
+	}
+
+	response := boostTypes.GetPayloadResponse{
+		Version: "bellatrix",
+		Data:    bestPayload,
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	if err := json.NewEncoder(w).Encode(response); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		respondError(w, http.StatusInternalServerError, "internal server error")
+		return
+	}
+}
+
+func (r *LocalRelay) handleIndex(w http.ResponseWriter, req *http.Request) {
+	if r.indexTemplate == nil {
+		http.Error(w, "not available", http.StatusInternalServerError)
+	}
+
+	r.validatorsLock.RLock()
+	noValidators := len(r.validators)
+	r.validatorsLock.RUnlock()
+	validatorsStats := fmt.Sprint(noValidators) + " validators registered"
+
+	r.bestDataLock.Lock()
+	header := r.bestHeader
+	payload := r.bestPayload
+	r.bestDataLock.Lock()
+
+	headerData, err := json.MarshalIndent(header, "", "  ")
+	if err != nil {
+		headerData = []byte{}
+	}
+
+	payloadData, err := json.MarshalIndent(payload, "", "  ")
+	if err != nil {
+		payloadData = []byte{}
+	}
+
+	statusData := struct {
+		Pubkey                string
+		ValidatorsStats       string
+		GenesisForkVersion    string
+		BellatrixForkVersion  string
+		GenesisValidatorsRoot string
+		BuilderSigningDomain  string
+		ProposerSigningDomain string
+		Header                string
+		Blocks                string
+	}{hexutil.Encode(r.serializedRelayPubkey), validatorsStats, r.fd.GenesisForkVersion, r.fd.BellatrixForkVersion, r.fd.GenesisValidatorsRoot, hexutil.Encode(r.builderSigningDomain[:]), hexutil.Encode(r.proposerSigningDomain[:]), string(headerData), string(payloadData)}
+
+	if err := r.indexTemplate.Execute(w, statusData); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+	}
+}
+
+type httpErrorResp struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+func respondError(w http.ResponseWriter, code int, message string) {
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(code)
+	if err := json.NewEncoder(w).Encode(httpErrorResp{code, message}); err != nil {
+		http.Error(w, message, code)
+	}
+}
+
+func (r *LocalRelay) handleStatus(w http.ResponseWriter, req *http.Request) {
+	w.WriteHeader(http.StatusOK)
+}
+
+func ExecutionPayloadHeaderEqual(l *boostTypes.ExecutionPayloadHeader, r *boostTypes.ExecutionPayloadHeader) bool {
+	return l.ParentHash == r.ParentHash && l.FeeRecipient == r.FeeRecipient && l.StateRoot == r.StateRoot && l.ReceiptsRoot == r.ReceiptsRoot && l.LogsBloom == r.LogsBloom && l.Random == r.Random && l.BlockNumber == r.BlockNumber && l.GasLimit == r.GasLimit && l.GasUsed == r.GasUsed && l.Timestamp == r.Timestamp && l.BaseFeePerGas == r.BaseFeePerGas && bytes.Equal(l.ExtraData, r.ExtraData) && l.BlockHash == r.BlockHash && l.TransactionsRoot == r.TransactionsRoot
+}
diff --git a/builder/local_relay_test.go b/builder/local_relay_test.go
new file mode 100644
index 0000000000..53db30392a
--- /dev/null
+++ b/builder/local_relay_test.go
@@ -0,0 +1,257 @@
+package builder
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"math/big"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	"golang.org/x/time/rate"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/flashbotsextra"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/stretchr/testify/require"
+)
+
+func newTestBackend(t *testing.T, forkchoiceData *beacon.ExecutableDataV1, block *types.Block) (*Builder, *LocalRelay, *ValidatorPrivateData) {
+	validator := NewRandomValidator()
+	sk, _ := bls.GenerateRandomSecretKey()
+	bDomain := boostTypes.ComputeDomain(boostTypes.DomainTypeAppBuilder, [4]byte{0x02, 0x0, 0x0, 0x0}, boostTypes.Hash{})
+	genesisValidatorsRoot := boostTypes.Hash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"))
+	cDomain := boostTypes.ComputeDomain(boostTypes.DomainTypeBeaconProposer, [4]byte{0x02, 0x0, 0x0, 0x0}, genesisValidatorsRoot)
+	beaconClient := &testBeaconClient{validator: validator}
+	localRelay := NewLocalRelay(sk, beaconClient, bDomain, cDomain, ForkData{}, true)
+	ethService := &testEthereumService{synced: true, testExecutableData: forkchoiceData, testBlock: block}
+	backend := NewBuilder(sk, flashbotsextra.NilDbService{}, localRelay, bDomain, ethService, false, nil)
+	// service := NewService("127.0.0.1:31545", backend)
+
+	backend.limiter = rate.NewLimiter(rate.Inf, 0)
+
+	return backend, localRelay, validator
+}
+
+func testRequest(t *testing.T, localRelay *LocalRelay, method string, path string, payload any) *httptest.ResponseRecorder {
+	var req *http.Request
+	var err error
+
+	if payload == nil {
+		req, err = http.NewRequest(method, path, nil)
+	} else {
+		payloadBytes, err2 := json.Marshal(payload)
+		require.NoError(t, err2)
+		req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes))
+	}
+
+	require.NoError(t, err)
+	rr := httptest.NewRecorder()
+	getRouter(localRelay).ServeHTTP(rr, req)
+	return rr
+}
+
+func TestValidatorRegistration(t *testing.T) {
+	_, relay, _ := newTestBackend(t, nil, nil)
+	log.Error("rsk", "sk", hexutil.Encode(relay.relaySecretKey.Serialize()))
+
+	v := NewRandomValidator()
+	payload, err := prepareRegistrationMessage(t, relay.builderSigningDomain, v)
+	require.NoError(t, err)
+
+	rr := testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload)
+	require.Equal(t, http.StatusOK, rr.Code)
+	require.Contains(t, relay.validators, PubkeyHex(v.Pk.String()))
+	require.Equal(t, ValidatorData{Pubkey: PubkeyHex(v.Pk.String()), FeeRecipient: payload[0].Message.FeeRecipient, GasLimit: payload[0].Message.GasLimit, Timestamp: payload[0].Message.Timestamp}, relay.validators[PubkeyHex(v.Pk.String())])
+
+	rr = testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload)
+	require.Equal(t, http.StatusOK, rr.Code)
+
+	payload[0].Message.Timestamp += 1
+	// Invalid signature
+	payload[0].Signature[len(payload[0].Signature)-1] = 0x00
+	rr = testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload)
+	require.Equal(t, http.StatusBadRequest, rr.Code)
+	require.Equal(t, `{"code":400,"message":"invalid signature"}`+"\n", rr.Body.String())
+
+	// TODO: cover all errors
+}
+
+func prepareRegistrationMessage(t *testing.T, domain boostTypes.Domain, v *ValidatorPrivateData) ([]boostTypes.SignedValidatorRegistration, error) {
+	var pubkey boostTypes.PublicKey
+	pubkey.FromSlice(v.Pk)
+	require.Equal(t, []byte(v.Pk), pubkey[:])
+
+	msg := boostTypes.RegisterValidatorRequestMessage{
+		FeeRecipient: boostTypes.Address{0x42},
+		GasLimit:     15_000_000,
+		Timestamp:    uint64(time.Now().Unix()),
+		Pubkey:       pubkey,
+	}
+
+	signature, err := v.Sign(&msg, domain)
+	require.NoError(t, err)
+
+	return []boostTypes.SignedValidatorRegistration{{
+		Message:   &msg,
+		Signature: signature,
+	}}, nil
+}
+
+func registerValidator(t *testing.T, v *ValidatorPrivateData, relay *LocalRelay) {
+	payload, err := prepareRegistrationMessage(t, relay.builderSigningDomain, v)
+	require.NoError(t, err)
+
+	log.Info("Registering", "payload", payload[0].Message)
+	rr := testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload)
+	require.Equal(t, http.StatusOK, rr.Code)
+	require.Contains(t, relay.validators, PubkeyHex(v.Pk.String()))
+	require.Equal(t, ValidatorData{Pubkey: PubkeyHex(v.Pk.String()), FeeRecipient: payload[0].Message.FeeRecipient, GasLimit: payload[0].Message.GasLimit, Timestamp: payload[0].Message.Timestamp}, relay.validators[PubkeyHex(v.Pk.String())])
+}
+
+func TestGetHeader(t *testing.T) {
+	forkchoiceData := &beacon.ExecutableDataV1{
+		ParentHash:    common.HexToHash("0xafafafa"),
+		FeeRecipient:  common.Address{0x01},
+		LogsBloom:     types.Bloom{0x00, 0x05, 0x10}.Bytes(),
+		BlockHash:     common.HexToHash("0x24e6998e4d2b4fd85f7f0d27ac4b87aaf0ec18e156e4b6e194ab5dadee0cd004"),
+		BaseFeePerGas: big.NewInt(12),
+		ExtraData:     []byte{},
+	}
+
+	forkchoiceBlock, err := beacon.ExecutableDataToBlock(*forkchoiceData)
+	require.NoError(t, err)
+	forkchoiceBlock.Profit = big.NewInt(10)
+
+	backend, relay, validator := newTestBackend(t, forkchoiceData, forkchoiceBlock)
+
+	path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String())
+	rr := testRequest(t, relay, "GET", path, nil)
+	require.Equal(t, `{"code":400,"message":"unknown validator"}`+"\n", rr.Body.String())
+
+	registerValidator(t, validator, relay)
+
+	rr = testRequest(t, relay, "GET", path, nil)
+	require.Equal(t, `{"code":400,"message":"unknown payload"}`+"\n", rr.Body.String())
+
+	path = fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), NewRandomValidator().Pk.String())
+	rr = testRequest(t, relay, "GET", path, nil)
+	require.Equal(t, ``, rr.Body.String())
+	require.Equal(t, 204, rr.Code)
+
+	backend.OnPayloadAttribute(&BuilderPayloadAttributes{})
+	time.Sleep(2 * time.Second)
+
+	path = fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String())
+	rr = testRequest(t, relay, "GET", path, nil)
+	require.Equal(t, http.StatusOK, rr.Code)
+
+	bid := new(boostTypes.GetHeaderResponse)
+	err = json.Unmarshal(rr.Body.Bytes(), bid)
+	require.NoError(t, err)
+
+	executionPayload, err := executableDataToExecutionPayload(forkchoiceData)
+	require.NoError(t, err)
+	expectedHeader, err := boostTypes.PayloadToPayloadHeader(executionPayload)
+	require.NoError(t, err)
+	expectedValue := new(boostTypes.U256Str)
+	err = expectedValue.FromBig(forkchoiceBlock.Profit)
+	require.NoError(t, err)
+	require.EqualValues(t, &boostTypes.BuilderBid{
+		Header: expectedHeader,
+		Value:  *expectedValue,
+		Pubkey: backend.builderPublicKey,
+	}, bid.Data.Message)
+
+	require.Equal(t, forkchoiceData.ParentHash.Bytes(), bid.Data.Message.Header.ParentHash[:], "didn't build on expected parent")
+	ok, err := boostTypes.VerifySignature(bid.Data.Message, backend.builderSigningDomain, backend.builderPublicKey[:], bid.Data.Signature[:])
+
+	require.NoError(t, err)
+	require.True(t, ok)
+}
+
+func TestGetPayload(t *testing.T) {
+	forkchoiceData := &beacon.ExecutableDataV1{
+		ParentHash:    common.HexToHash("0xafafafa"),
+		FeeRecipient:  common.Address{0x01},
+		LogsBloom:     types.Bloom{}.Bytes(),
+		BlockHash:     common.HexToHash("0xc4a012b67027b3ab6c00acd31aeee24aa1515d6a5d7e81b0ee2e69517fdc387f"),
+		BaseFeePerGas: big.NewInt(12),
+		ExtraData:     []byte{},
+	}
+
+	forkchoiceBlock, err := beacon.ExecutableDataToBlock(*forkchoiceData)
+	require.NoError(t, err)
+	forkchoiceBlock.Profit = big.NewInt(10)
+
+	backend, relay, validator := newTestBackend(t, forkchoiceData, forkchoiceBlock)
+
+	registerValidator(t, validator, relay)
+	backend.OnPayloadAttribute(&BuilderPayloadAttributes{})
+	time.Sleep(2 * time.Second)
+
+	path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String())
+	rr := testRequest(t, relay, "GET", path, nil)
+	require.Equal(t, http.StatusOK, rr.Code)
+
+	bid := new(boostTypes.GetHeaderResponse)
+	err = json.Unmarshal(rr.Body.Bytes(), bid)
+	require.NoError(t, err)
+
+	// Create request payload
+	msg := &boostTypes.BlindedBeaconBlock{
+		Slot:          1,
+		ProposerIndex: 2,
+		ParentRoot:    boostTypes.Root{0x03},
+		StateRoot:     boostTypes.Root{0x04},
+		Body: &boostTypes.BlindedBeaconBlockBody{
+			Eth1Data: &boostTypes.Eth1Data{
+				DepositRoot:  boostTypes.Root{0x05},
+				DepositCount: 5,
+				BlockHash:    boostTypes.Hash{0x06},
+			},
+			SyncAggregate: &boostTypes.SyncAggregate{
+				CommitteeBits:      boostTypes.CommitteeBits{0x07},
+				CommitteeSignature: boostTypes.Signature{0x08},
+			},
+			ExecutionPayloadHeader: bid.Data.Message.Header,
+		},
+	}
+
+	// TODO: test wrong signing domain
+	signature, err := validator.Sign(msg, relay.proposerSigningDomain)
+	require.NoError(t, err)
+
+	// Call getPayload with invalid signature
+	rr = testRequest(t, relay, "POST", "/eth/v1/builder/blinded_blocks", boostTypes.SignedBlindedBeaconBlock{
+		Message:   msg,
+		Signature: boostTypes.Signature{0x09},
+	})
+	require.Equal(t, http.StatusBadRequest, rr.Code)
+	require.Equal(t, `{"code":400,"message":"invalid signature"}`+"\n", rr.Body.String())
+
+	// Call getPayload with correct signature
+	rr = testRequest(t, relay, "POST", "/eth/v1/builder/blinded_blocks", boostTypes.SignedBlindedBeaconBlock{
+		Message:   msg,
+		Signature: signature,
+	})
+
+	// Verify getPayload response
+	require.Equal(t, http.StatusOK, rr.Code)
+	getPayloadResponse := new(boostTypes.GetPayloadResponse)
+	err = json.Unmarshal(rr.Body.Bytes(), getPayloadResponse)
+	require.NoError(t, err)
+	require.Equal(t, bid.Data.Message.Header.BlockHash, getPayloadResponse.Data.BlockHash)
+}
+
+func TestXxx(t *testing.T) {
+	sk, _ := bls.GenerateRandomSecretKey()
+	fmt.Println(hexutil.Encode(sk.Serialize()))
+}
diff --git a/builder/relay.go b/builder/relay.go
new file mode 100644
index 0000000000..f5414ae280
--- /dev/null
+++ b/builder/relay.go
@@ -0,0 +1,189 @@
+package builder
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/log"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/flashbots/mev-boost/server"
+)
+
+type testRelay struct {
+	validator     ValidatorData
+	requestedSlot uint64
+	submittedMsg  *boostTypes.BuilderSubmitBlockRequest
+}
+
+func (r *testRelay) SubmitBlock(msg *boostTypes.BuilderSubmitBlockRequest) error {
+	r.submittedMsg = msg
+	return nil
+}
+func (r *testRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) {
+	r.requestedSlot = nextSlot
+	return r.validator, nil
+}
+
+type RemoteRelay struct {
+	endpoint string
+	client   http.Client
+
+	localRelay *LocalRelay
+
+	validatorsLock       sync.RWMutex
+	validatorSyncOngoing bool
+	lastRequestedSlot    uint64
+	validatorSlotMap     map[uint64]ValidatorData
+}
+
+func NewRemoteRelay(endpoint string, localRelay *LocalRelay) *RemoteRelay {
+	r := &RemoteRelay{
+		endpoint:             endpoint,
+		client:               http.Client{Timeout: time.Second},
+		localRelay:           localRelay,
+		validatorSyncOngoing: false,
+		lastRequestedSlot:    0,
+		validatorSlotMap:     make(map[uint64]ValidatorData),
+	}
+
+	err := r.updateValidatorsMap(0, 3)
+	if err != nil {
+		log.Error("could not connect to remote relay, continuing anyway", "err", err)
+	}
+	return r
+}
+
+type GetValidatorRelayResponse []struct {
+	Slot  uint64 `json:"slot,string"`
+	Entry struct {
+		Message struct {
+			FeeRecipient string `json:"fee_recipient"`
+			GasLimit     uint64 `json:"gas_limit,string"`
+			Timestamp    uint64 `json:"timestamp,string"`
+			Pubkey       string `json:"pubkey"`
+		} `json:"message"`
+		Signature string `json:"signature"`
+	} `json:"entry"`
+}
+
+func (r *RemoteRelay) updateValidatorsMap(currentSlot uint64, retries int) error {
+	r.validatorsLock.Lock()
+	if r.validatorSyncOngoing {
+		r.validatorsLock.Unlock()
+		return errors.New("sync is ongoing")
+	}
+	r.validatorSyncOngoing = true
+	r.validatorsLock.Unlock()
+
+	log.Info("requesting ", "currentSlot", currentSlot)
+	newMap, err := r.getSlotValidatorMapFromRelay()
+	for err != nil && retries > 0 {
+		log.Error("could not get validators map from relay, retrying", "err", err)
+		time.Sleep(time.Second)
+		newMap, err = r.getSlotValidatorMapFromRelay()
+		retries -= 1
+	}
+	r.validatorsLock.Lock()
+	r.validatorSyncOngoing = false
+	if err != nil {
+		r.validatorsLock.Unlock()
+		log.Error("could not get validators map from relay", "err", err)
+		return err
+	}
+
+	r.validatorSlotMap = newMap
+	r.lastRequestedSlot = currentSlot
+	r.validatorsLock.Unlock()
+
+	log.Info("Updated validators", "count", len(newMap), "slot", currentSlot)
+	return nil
+}
+
+func (r *RemoteRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) {
+	// next slot is expected to be the actual chain's next slot, not something requested by the user!
+	// if not sanitized it will force resync of validator data and possibly is a DoS vector
+
+	r.validatorsLock.RLock()
+	if r.lastRequestedSlot == 0 || nextSlot/32 > r.lastRequestedSlot/32 {
+		// Every epoch request validators map
+		go func() {
+			err := r.updateValidatorsMap(nextSlot, 1)
+			if err != nil {
+				log.Error("could not update validators map", "err", err)
+			}
+		}()
+	}
+
+	vd, found := r.validatorSlotMap[nextSlot]
+	r.validatorsLock.RUnlock()
+
+	if r.localRelay != nil {
+		localValidator, err := r.localRelay.GetValidatorForSlot(nextSlot)
+		if err == nil {
+			log.Info("Validator registration overwritten by local data", "slot", nextSlot, "validator", localValidator)
+			return localValidator, nil
+		}
+	}
+
+	if found {
+		return vd, nil
+	}
+
+	return ValidatorData{}, errors.New("validator not found")
+}
+
+func (r *RemoteRelay) SubmitBlock(msg *boostTypes.BuilderSubmitBlockRequest) error {
+	code, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, r.endpoint+"/relay/v1/builder/blocks", msg, nil)
+	if err != nil {
+		return err
+	}
+	if code > 299 {
+		return fmt.Errorf("non-ok response code %d from relay ", code)
+	}
+
+	if r.localRelay != nil {
+		r.localRelay.SubmitBlock(msg)
+	}
+
+	return nil
+}
+
+func (r *RemoteRelay) getSlotValidatorMapFromRelay() (map[uint64]ValidatorData, error) {
+	var dst GetValidatorRelayResponse
+	code, err := server.SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, r.endpoint+"/relay/v1/builder/validators", nil, &dst)
+	if err != nil {
+		return nil, err
+	}
+
+	if code > 299 {
+		return nil, fmt.Errorf("non-ok response code %d from relay", code)
+	}
+
+	res := make(map[uint64]ValidatorData)
+	for _, data := range dst {
+		feeRecipientBytes, err := hexutil.Decode(data.Entry.Message.FeeRecipient)
+		if err != nil {
+			log.Error("Ill-formatted fee_recipient from relay", "data", data)
+			continue
+		}
+		var feeRecipient boostTypes.Address
+		feeRecipient.FromSlice(feeRecipientBytes[:])
+
+		pubkeyHex := PubkeyHex(strings.ToLower(data.Entry.Message.Pubkey))
+
+		res[data.Slot] = ValidatorData{
+			Pubkey:       pubkeyHex,
+			FeeRecipient: feeRecipient,
+			GasLimit:     data.Entry.Message.GasLimit,
+			Timestamp:    data.Entry.Message.Timestamp,
+		}
+	}
+
+	return res, nil
+}
diff --git a/builder/relay_test.go b/builder/relay_test.go
new file mode 100644
index 0000000000..adc0f9aaba
--- /dev/null
+++ b/builder/relay_test.go
@@ -0,0 +1,127 @@
+package builder
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/gorilla/mux"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRemoteRelay(t *testing.T) {
+	r := mux.NewRouter()
+	var validatorsHandler func(w http.ResponseWriter, r *http.Request)
+	r.HandleFunc("/relay/v1/builder/validators", func(w http.ResponseWriter, r *http.Request) { validatorsHandler(w, r) })
+
+	validatorsHandler = func(w http.ResponseWriter, r *http.Request) {
+		resp := `[{
+  "slot": "123",
+  "entry": {
+    "message": {
+      "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
+      "gas_limit": "1",
+      "timestamp": "1",
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
+    },
+    "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
+  }}, {
+  "slot": "155",
+  "entry": {
+    "message": {
+      "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc10",
+      "gas_limit": "1",
+      "timestamp": "1",
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
+    },
+    "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
+  }
+}]`
+
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte(resp))
+	}
+
+	srv := httptest.NewServer(r)
+	relay := NewRemoteRelay(srv.URL, nil)
+	vd, found := relay.validatorSlotMap[123]
+	require.True(t, found)
+	expectedValidator_123 := ValidatorData{
+		Pubkey:       "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+		FeeRecipient: boostTypes.Address{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x9},
+		GasLimit:     uint64(1),
+		Timestamp:    uint64(1),
+	}
+	require.Equal(t, expectedValidator_123, vd)
+
+	vd, err := relay.GetValidatorForSlot(123)
+	require.NoError(t, err)
+	require.Equal(t, expectedValidator_123, vd)
+
+	vd, err = relay.GetValidatorForSlot(124)
+	require.Error(t, err)
+	require.Equal(t, vd, ValidatorData{})
+
+	validatorsRequested := make(chan struct{})
+	validatorsHandler = func(w http.ResponseWriter, r *http.Request) {
+		resp := `[{
+  "slot": "155",
+  "entry": {
+    "message": {
+      "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc10",
+      "gas_limit": "1",
+      "timestamp": "1",
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
+    },
+    "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
+  }}, {
+  "slot": "156",
+  "entry": {
+    "message": {
+      "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc11",
+      "gas_limit": "1",
+      "timestamp": "1",
+      "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
+    },
+    "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
+  }
+}]`
+
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte(resp))
+		validatorsRequested <- struct{}{}
+	}
+
+	expectedValidator_155 := ValidatorData{
+		Pubkey:       "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+		FeeRecipient: boostTypes.Address{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x10},
+		GasLimit:     uint64(1),
+		Timestamp:    uint64(1),
+	}
+
+	expectedValidator_156 := ValidatorData{
+		Pubkey:       "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a",
+		FeeRecipient: boostTypes.Address{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x11},
+		GasLimit:     uint64(1),
+		Timestamp:    uint64(1),
+	}
+
+	vd, err = relay.GetValidatorForSlot(155)
+	require.NoError(t, err)
+	require.Equal(t, expectedValidator_155, vd)
+
+	select {
+	case <-validatorsRequested:
+		for i := 0; i < 10 && relay.lastRequestedSlot != 155; i++ {
+			time.Sleep(time.Millisecond)
+		}
+	case <-time.After(time.Second):
+		t.Error("timeout waiting for validator registration request")
+	}
+
+	vd, err = relay.GetValidatorForSlot(156)
+	require.NoError(t, err)
+	require.Equal(t, expectedValidator_156, vd)
+}
diff --git a/builder/resubmit_utils.go b/builder/resubmit_utils.go
new file mode 100644
index 0000000000..4d95dab93a
--- /dev/null
+++ b/builder/resubmit_utils.go
@@ -0,0 +1,65 @@
+package builder
+
+import (
+	"context"
+	"time"
+
+	"github.com/ethereum/go-ethereum/log"
+	"golang.org/x/time/rate"
+)
+
+// runResubmitLoop checks for update signal and calls submit respecting provided rate limiter and context
+func runResubmitLoop(ctx context.Context, limiter *rate.Limiter, updateSignal chan struct{}, submit func()) {
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-updateSignal:
+			res := limiter.Reserve()
+			if !res.OK() {
+				log.Warn("resubmit loop failed to make limiter reservation")
+				return
+			}
+
+			// check if we could make submission before context ctxDeadline
+			if ctxDeadline, ok := ctx.Deadline(); ok {
+				delayDeadline := time.Now().Add(res.Delay())
+				if delayDeadline.After(ctxDeadline) {
+					res.Cancel()
+					return
+				}
+			}
+
+			delay := res.Delay()
+			if delay == 0 {
+				submit()
+				continue
+			}
+
+			t := time.NewTimer(delay)
+			select {
+			case <-t.C:
+				submit()
+				continue
+			case <-ctx.Done():
+				res.Cancel()
+				t.Stop()
+				return
+			}
+		}
+	}
+}
+
+// runRetryLoop calls retry periodically with the provided interval respecting context cancellation
+func runRetryLoop(ctx context.Context, interval time.Duration, retry func()) {
+	t := time.NewTicker(interval)
+	defer t.Stop()
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case <-t.C:
+			retry()
+		}
+	}
+}
diff --git a/builder/resubmit_utils_test.go b/builder/resubmit_utils_test.go
new file mode 100644
index 0000000000..f612c5a292
--- /dev/null
+++ b/builder/resubmit_utils_test.go
@@ -0,0 +1,76 @@
+package builder
+
+import (
+	"context"
+	"math/rand"
+	"sort"
+	"sync"
+	"testing"
+	"time"
+
+	"golang.org/x/time/rate"
+)
+
+type submission struct {
+	t time.Time
+	v int
+}
+
+func TestResubmitUtils(t *testing.T) {
+	const (
+		totalTime        = time.Second
+		rateLimitTime    = 100 * time.Millisecond
+		resubmitInterval = 10 * time.Millisecond
+	)
+
+	ctx, cancel := context.WithTimeout(context.Background(), totalTime)
+	defer cancel()
+	limiter := rate.NewLimiter(rate.Every(rateLimitTime), 1)
+
+	var (
+		signal  = make(chan struct{}, 1)
+		subMu   sync.Mutex
+		subLast int
+		subBest int
+		subAll  []submission
+	)
+
+	go runResubmitLoop(ctx, limiter, signal, func() {
+		subMu.Lock()
+		defer subMu.Unlock()
+
+		if subBest > subLast {
+			subAll = append(subAll, submission{time.Now(), subBest})
+			subLast = subBest
+		}
+	})
+
+	runRetryLoop(ctx, resubmitInterval, func() {
+		subMu.Lock()
+		defer subMu.Unlock()
+
+		value := rand.Int()
+		if value > subBest {
+			subBest = value
+
+			select {
+			case signal <- struct{}{}:
+			default:
+			}
+		}
+	})
+
+	sorted := sort.SliceIsSorted(subAll, func(i, j int) bool {
+		return subAll[i].v < subAll[j].v
+	})
+	if !sorted {
+		t.Error("submissions are not sorted")
+	}
+
+	for i := 0; i < len(subAll)-1; i++ {
+		interval := subAll[i+1].t.Sub(subAll[i].t)
+		if interval+10*time.Millisecond < rateLimitTime {
+			t.Errorf("submissions are not rate limited: interval %s, limit %s", interval, rateLimitTime)
+		}
+	}
+}
diff --git a/builder/service.go b/builder/service.go
new file mode 100644
index 0000000000..2c311b1b33
--- /dev/null
+++ b/builder/service.go
@@ -0,0 +1,231 @@
+package builder
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+
+	blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/flashbotsextra"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/rpc"
+	"github.com/gorilla/mux"
+
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+
+	"github.com/flashbots/go-utils/httplogger"
+)
+
+const (
+	_PathStatus            = "/eth/v1/builder/status"
+	_PathRegisterValidator = "/eth/v1/builder/validators"
+	_PathGetHeader         = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}"
+	_PathGetPayload        = "/eth/v1/builder/blinded_blocks"
+)
+
+type BuilderPayloadAttributes struct {
+	Timestamp             hexutil.Uint64 `json:"timestamp"`
+	Random                common.Hash    `json:"prevRandao"`
+	SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient,omitempty"`
+	Slot                  uint64         `json:"slot"`
+	HeadHash              common.Hash    `json:"blockHash"`
+	GasLimit              uint64
+}
+
+type Service struct {
+	srv     *http.Server
+	builder IBuilder
+}
+
+func (s *Service) Start() error {
+	if s.srv != nil {
+		log.Info("Service started")
+		go s.srv.ListenAndServe()
+	}
+
+	s.builder.Start()
+
+	return nil
+}
+
+func (s *Service) Stop() error {
+	if s.srv != nil {
+		s.srv.Close()
+	}
+	s.builder.Stop()
+	return nil
+}
+
+func (s *Service) PayloadAttributes(payloadAttributes *BuilderPayloadAttributes) error {
+	return s.builder.OnPayloadAttribute(payloadAttributes)
+}
+
+func getRouter(localRelay *LocalRelay) http.Handler {
+	router := mux.NewRouter()
+
+	// Add routes
+	router.HandleFunc("/", localRelay.handleIndex).Methods(http.MethodGet)
+	router.HandleFunc(_PathStatus, localRelay.handleStatus).Methods(http.MethodGet)
+	router.HandleFunc(_PathRegisterValidator, localRelay.handleRegisterValidator).Methods(http.MethodPost)
+	router.HandleFunc(_PathGetHeader, localRelay.handleGetHeader).Methods(http.MethodGet)
+	router.HandleFunc(_PathGetPayload, localRelay.handleGetPayload).Methods(http.MethodPost)
+
+	// Add logging and return router
+	loggedRouter := httplogger.LoggingMiddleware(router)
+	return loggedRouter
+}
+
+func NewService(listenAddr string, localRelay *LocalRelay, builder *Builder) *Service {
+	var srv *http.Server
+	if localRelay != nil {
+		srv = &http.Server{
+			Addr:    listenAddr,
+			Handler: getRouter(localRelay),
+			/*
+			   ReadTimeout:
+			   ReadHeaderTimeout:
+			   WriteTimeout:
+			   IdleTimeout:
+			*/
+		}
+	}
+
+	return &Service{
+		srv:     srv,
+		builder: builder,
+	}
+}
+
+type BuilderConfig struct {
+	Enabled               bool
+	EnableValidatorChecks bool
+	EnableLocalRelay      bool
+	DisableBundleFetcher  bool
+	DryRun                bool
+	BuilderSecretKey      string
+	RelaySecretKey        string
+	ListenAddr            string
+	GenesisForkVersion    string
+	BellatrixForkVersion  string
+	GenesisValidatorsRoot string
+	BeaconEndpoint        string
+	RemoteRelayEndpoint   string
+	ValidationBlocklist   string
+}
+
+func Register(stack *node.Node, backend *eth.Ethereum, cfg *BuilderConfig) error {
+	envRelaySkBytes, err := hexutil.Decode(cfg.RelaySecretKey)
+	if err != nil {
+		return errors.New("incorrect builder API secret key provided")
+	}
+
+	relaySk, err := bls.SecretKeyFromBytes(envRelaySkBytes[:])
+	if err != nil {
+		return errors.New("incorrect builder API secret key provided")
+	}
+
+	envBuilderSkBytes, err := hexutil.Decode(cfg.BuilderSecretKey)
+	if err != nil {
+		return errors.New("incorrect builder API secret key provided")
+	}
+
+	builderSk, err := bls.SecretKeyFromBytes(envBuilderSkBytes[:])
+	if err != nil {
+		return errors.New("incorrect builder API secret key provided")
+	}
+
+	genesisForkVersionBytes, err := hexutil.Decode(cfg.GenesisForkVersion)
+	if err != nil {
+		return fmt.Errorf("invalid genesisForkVersion: %w", err)
+	}
+
+	var genesisForkVersion [4]byte
+	copy(genesisForkVersion[:], genesisForkVersionBytes[:4])
+	builderSigningDomain := boostTypes.ComputeDomain(boostTypes.DomainTypeAppBuilder, genesisForkVersion, boostTypes.Root{})
+
+	genesisValidatorsRoot := boostTypes.Root(common.HexToHash(cfg.GenesisValidatorsRoot))
+	bellatrixForkVersionBytes, err := hexutil.Decode(cfg.BellatrixForkVersion)
+	if err != nil {
+		return fmt.Errorf("invalid bellatrixForkVersion: %w", err)
+	}
+
+	var bellatrixForkVersion [4]byte
+	copy(bellatrixForkVersion[:], bellatrixForkVersionBytes[:4])
+	proposerSigningDomain := boostTypes.ComputeDomain(boostTypes.DomainTypeBeaconProposer, bellatrixForkVersion, genesisValidatorsRoot)
+
+	beaconClient := NewBeaconClient(cfg.BeaconEndpoint)
+
+	var localRelay *LocalRelay
+	if cfg.EnableLocalRelay {
+		localRelay = NewLocalRelay(relaySk, beaconClient, builderSigningDomain, proposerSigningDomain, ForkData{cfg.GenesisForkVersion, cfg.BellatrixForkVersion, cfg.GenesisValidatorsRoot}, cfg.EnableValidatorChecks)
+	}
+
+	var relay IRelay
+	if cfg.RemoteRelayEndpoint != "" {
+		relay = NewRemoteRelay(cfg.RemoteRelayEndpoint, localRelay)
+	} else if localRelay != nil {
+		relay = localRelay
+	} else {
+		return errors.New("neither local nor remote relay specified")
+	}
+
+	var validator *blockvalidation.BlockValidationAPI
+	if cfg.DryRun {
+		var accessVerifier *blockvalidation.AccessVerifier
+		if cfg.ValidationBlocklist != "" {
+			accessVerifier, err = blockvalidation.NewAccessVerifierFromFile(cfg.ValidationBlocklist)
+			if err != nil {
+				return fmt.Errorf("failed to load validation blocklist %w", err)
+			}
+		}
+		validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier)
+	}
+
+	// TODO: move to proper flags
+	var ds flashbotsextra.IDatabaseService
+	dbDSN := os.Getenv("FLASHBOTS_POSTGRES_DSN")
+	if dbDSN != "" {
+		ds, err = flashbotsextra.NewDatabaseService(dbDSN)
+		if err != nil {
+			log.Error("could not connect to the DB", "err", err)
+			ds = flashbotsextra.NilDbService{}
+		}
+	} else {
+		log.Info("db dsn is not provided, starting nil db svc")
+		ds = flashbotsextra.NilDbService{}
+	}
+
+	// Bundle fetcher
+	if !cfg.DisableBundleFetcher {
+		mevBundleCh := make(chan []types.MevBundle)
+		blockNumCh := make(chan int64)
+		bundleFetcher := flashbotsextra.NewBundleFetcher(backend, ds, blockNumCh, mevBundleCh, true)
+		go bundleFetcher.Run()
+	}
+
+	ethereumService := NewEthereumService(backend)
+	builderBackend := NewBuilder(builderSk, ds, relay, builderSigningDomain, ethereumService, cfg.DryRun, validator)
+	builderService := NewService(cfg.ListenAddr, localRelay, builderBackend)
+
+	stack.RegisterAPIs([]rpc.API{
+		{
+			Namespace:     "builder",
+			Version:       "1.0",
+			Service:       builderService,
+			Public:        true,
+			Authenticated: true,
+		},
+	})
+
+	stack.RegisterLifecycle(builderService)
+
+	return nil
+}
diff --git a/builder/validator.go b/builder/validator.go
new file mode 100644
index 0000000000..2a27d44b92
--- /dev/null
+++ b/builder/validator.go
@@ -0,0 +1,49 @@
+package builder
+
+import (
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/hexutil"
+
+	"github.com/flashbots/go-boost-utils/bls"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+)
+
+type ValidatorPrivateData struct {
+	sk *bls.SecretKey
+	Pk hexutil.Bytes
+}
+
+func NewRandomValidator() *ValidatorPrivateData {
+	sk, pk, err := bls.GenerateNewKeypair()
+	if err != nil {
+		return nil
+	}
+	return &ValidatorPrivateData{sk, pk.Compress()}
+}
+
+func (v *ValidatorPrivateData) Sign(msg boostTypes.HashTreeRoot, d boostTypes.Domain) (boostTypes.Signature, error) {
+	return boostTypes.SignMessage(msg, d, v.sk)
+}
+
+func (v *ValidatorPrivateData) PrepareRegistrationMessage(feeRecipientHex string) (boostTypes.SignedValidatorRegistration, error) {
+	address, err := boostTypes.HexToAddress(feeRecipientHex)
+	if err != nil {
+		return boostTypes.SignedValidatorRegistration{}, err
+	}
+
+	pubkey := boostTypes.PublicKey{}
+	pubkey.FromSlice(v.Pk)
+
+	msg := &boostTypes.RegisterValidatorRequestMessage{
+		FeeRecipient: address,
+		GasLimit:     1000,
+		Timestamp:    uint64(time.Now().UnixMilli()),
+		Pubkey:       pubkey,
+	}
+	signature, err := v.Sign(msg, boostTypes.DomainBuilder)
+	if err != nil {
+		return boostTypes.SignedValidatorRegistration{}, err
+	}
+	return boostTypes.SignedValidatorRegistration{Message: msg, Signature: signature}, nil
+}
diff --git a/cmd/evm/internal/t8ntool/block.go b/cmd/evm/internal/t8ntool/block.go
index 4a070b6c71..d63d41e84a 100644
--- a/cmd/evm/internal/t8ntool/block.go
+++ b/cmd/evm/internal/t8ntool/block.go
@@ -188,7 +188,7 @@ func (i *bbInput) sealEthash(block *types.Block) (*types.Block, error) {
 	// If the testmode is used, the sealer will return quickly, and complain
 	// "Sealing result is not read by miner" if it cannot write the result.
 	results := make(chan *types.Block, 1)
-	if err := engine.Seal(nil, block, results, nil); err != nil {
+	if err := engine.Seal(nil, block, nil, results, nil); err != nil {
 		panic(fmt.Sprintf("failed to seal block: %v", err))
 	}
 	found := <-results
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index 30565fda61..490db0e319 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -30,8 +30,10 @@ import (
 	"github.com/ethereum/go-ethereum/accounts/keystore"
 	"github.com/ethereum/go-ethereum/accounts/scwallet"
 	"github.com/ethereum/go-ethereum/accounts/usbwallet"
+	builder "github.com/ethereum/go-ethereum/builder"
 	"github.com/ethereum/go-ethereum/cmd/utils"
 	"github.com/ethereum/go-ethereum/core/rawdb"
+	blockvalidationapi "github.com/ethereum/go-ethereum/eth/block-validation"
 	"github.com/ethereum/go-ethereum/eth/ethconfig"
 	"github.com/ethereum/go-ethereum/internal/ethapi"
 	"github.com/ethereum/go-ethereum/internal/flags"
@@ -159,12 +161,28 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
 	if ctx.IsSet(utils.OverrideTerminalTotalDifficulty.Name) {
 		cfg.Eth.OverrideTerminalTotalDifficulty = flags.GlobalBig(ctx, utils.OverrideTerminalTotalDifficulty.Name)
 	}
+
 	if ctx.IsSet(utils.OverrideTerminalTotalDifficultyPassed.Name) {
 		override := ctx.Bool(utils.OverrideTerminalTotalDifficultyPassed.Name)
 		cfg.Eth.OverrideTerminalTotalDifficultyPassed = &override
 	}
 
-	backend, eth := utils.RegisterEthService(stack, &cfg.Eth)
+	bpConfig := &builder.BuilderConfig{
+		Enabled:               ctx.IsSet(utils.BuilderEnabled.Name),
+		EnableValidatorChecks: ctx.IsSet(utils.BuilderEnableValidatorChecks.Name),
+		EnableLocalRelay:      ctx.IsSet(utils.BuilderEnableLocalRelay.Name),
+		DisableBundleFetcher:  ctx.IsSet(utils.BuilderDisableBundleFetcher.Name),
+		DryRun:                ctx.IsSet(utils.BuilderDryRun.Name),
+		BuilderSecretKey:      ctx.String(utils.BuilderSecretKey.Name),
+		RelaySecretKey:        ctx.String(utils.BuilderRelaySecretKey.Name),
+		ListenAddr:            ctx.String(utils.BuilderListenAddr.Name),
+		GenesisForkVersion:    ctx.String(utils.BuilderGenesisForkVersion.Name),
+		BellatrixForkVersion:  ctx.String(utils.BuilderBellatrixForkVersion.Name),
+		GenesisValidatorsRoot: ctx.String(utils.BuilderGenesisValidatorsRoot.Name),
+		BeaconEndpoint:        ctx.String(utils.BuilderBeaconEndpoint.Name),
+		RemoteRelayEndpoint:   ctx.String(utils.BuilderRemoteRelayEndpoint.Name),
+	}
+	backend, eth := utils.RegisterEthService(stack, &cfg.Eth, bpConfig)
 
 	// Warn users to migrate if they have a legacy freezer format.
 	if eth != nil && !ctx.IsSet(utils.IgnoreLegacyReceiptsFlag.Name) {
@@ -187,7 +205,11 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
 	// Configure log filter RPC API.
 	filterSystem := utils.RegisterFilterAPI(stack, backend, &cfg.Eth)
 
-	// Configure GraphQL if requested.
+	if err := blockvalidationapi.Register(stack, eth); err != nil {
+		utils.Fatalf("Failed to register the Block Validation API: %v", err)
+	}
+
+	// Configure GraphQL if requested
 	if ctx.IsSet(utils.GraphQLEnabledFlag.Name) {
 		utils.RegisterGraphQLService(stack, backend, filterSystem, &cfg.Node)
 	}
diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go
index 442b82df0b..c290b65ecc 100644
--- a/cmd/geth/consolecmd_test.go
+++ b/cmd/geth/consolecmd_test.go
@@ -30,7 +30,7 @@ import (
 )
 
 const (
-	ipcAPIs  = "admin:1.0 debug:1.0 engine:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0"
+	ipcAPIs  = "admin:1.0 debug:1.0 engine:1.0 eth:1.0 ethash:1.0 flashbots:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0"
 	httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0"
 )
 
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index b9e3ed31e8..78b62cb8db 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -90,6 +90,7 @@ var (
 		utils.TxPoolAccountQueueFlag,
 		utils.TxPoolGlobalQueueFlag,
 		utils.TxPoolLifetimeFlag,
+		utils.TxPoolPrivateLifetimeFlag,
 		utils.SyncModeFlag,
 		utils.ExitWhenSyncedFlag,
 		utils.GCModeFlag,
@@ -129,10 +130,13 @@ var (
 		utils.LegacyMinerGasTargetFlag,
 		utils.MinerGasLimitFlag,
 		utils.MinerGasPriceFlag,
+		utils.MinerAlgoTypeFlag,
 		utils.MinerEtherbaseFlag,
 		utils.MinerExtraDataFlag,
 		utils.MinerRecommitIntervalFlag,
 		utils.MinerNoVerifyFlag,
+		utils.MinerMaxMergedBundlesFlag,
+		utils.MinerBlocklistFileFlag,
 		utils.NATFlag,
 		utils.NoDiscoverFlag,
 		utils.DiscoveryV5Flag,
@@ -157,6 +161,22 @@ var (
 		configFileFlag,
 	}, utils.NetworkFlags, utils.DatabasePathFlags)
 
+	builderApiFlags = []cli.Flag{
+		utils.BuilderEnabled,
+		utils.BuilderEnableValidatorChecks,
+		utils.BuilderEnableLocalRelay,
+		utils.BuilderDisableBundleFetcher,
+		utils.BuilderDryRun,
+		utils.BuilderSecretKey,
+		utils.BuilderRelaySecretKey,
+		utils.BuilderListenAddr,
+		utils.BuilderGenesisForkVersion,
+		utils.BuilderBellatrixForkVersion,
+		utils.BuilderGenesisValidatorsRoot,
+		utils.BuilderBeaconEndpoint,
+		utils.BuilderRemoteRelayEndpoint,
+	}
+
 	rpcFlags = []cli.Flag{
 		utils.HTTPEnabledFlag,
 		utils.HTTPListenAddrFlag,
@@ -247,6 +267,7 @@ func init() {
 	app.Flags = flags.Merge(
 		nodeFlags,
 		rpcFlags,
+		builderApiFlags,
 		consoleFlags,
 		debug.Flags,
 		metricsFlags,
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 9e95193343..b5df47ac03 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -19,6 +19,7 @@ package utils
 
 import (
 	"crypto/ecdsa"
+	"encoding/json"
 	"fmt"
 	"math"
 	"math/big"
@@ -31,6 +32,7 @@ import (
 
 	"github.com/ethereum/go-ethereum/accounts"
 	"github.com/ethereum/go-ethereum/accounts/keystore"
+	builder "github.com/ethereum/go-ethereum/builder"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/fdlimit"
 	"github.com/ethereum/go-ethereum/consensus"
@@ -440,6 +442,12 @@ var (
 		Category: flags.TxPoolCategory,
 	}
 
+	TxPoolPrivateLifetimeFlag = &cli.DurationFlag{
+		Name:     "txpool.privatelifetime",
+		Usage:    "Maximum amount of time private transactions are withheld from public broadcasting",
+		Value:    ethconfig.Defaults.TxPool.PrivateTxLifetime,
+		Category: flags.TxPoolCategory,
+	}
 	// Performance tuning settings
 	CacheFlag = &cli.IntFlag{
 		Name:     "cache",
@@ -539,6 +547,12 @@ var (
 		Value:    ethconfig.Defaults.Miner.GasPrice,
 		Category: flags.MinerCategory,
 	}
+	MinerAlgoTypeFlag = &cli.StringFlag{
+		Name:     "miner.algotype",
+		Usage:    "Block building algorithm to use [=mev-geth] (mev-geth, greedy)",
+		Value:    "mev-geth",
+		Category: flags.MinerCategory,
+	}
 	MinerEtherbaseFlag = &cli.StringFlag{
 		Name:     "miner.etherbase",
 		Usage:    "Public address for block mining rewards (default = first account)",
@@ -561,6 +575,18 @@ var (
 		Usage:    "Disable remote sealing verification",
 		Category: flags.MinerCategory,
 	}
+	MinerMaxMergedBundlesFlag = &cli.IntFlag{
+		Name:     "miner.maxmergedbundles",
+		Usage:    "flashbots - The maximum amount of bundles to merge. The miner will run this many workers in parallel to calculate if the full block is more profitable with these additional bundles.",
+		Value:    3,
+		Category: flags.MinerCategory,
+	}
+	MinerBlocklistFileFlag = &cli.StringFlag{
+		Name:     "miner.blocklist",
+		Usage:    "flashbots - Path to JSON file with list of blocked addresses. Miner will ignore txs that touch mentioned addresses.",
+		Value:    "",
+		Category: flags.MinerCategory,
+	}
 
 	// Account settings
 	UnlockedAccountFlag = &cli.StringFlag{
@@ -660,7 +686,75 @@ var (
 		Usage:    "Geth will start up even if there are legacy receipts in freezer",
 		Category: flags.MiscCategory,
 	}
-
+	// Builder API settings
+	BuilderEnabled = &cli.BoolFlag{
+		Name:  "builder",
+		Usage: "Enable the builder",
+	}
+	BuilderEnableValidatorChecks = &cli.BoolFlag{
+		Name:  "builder.validator_checks",
+		Usage: "Enable the validator checks",
+	}
+	BuilderEnableLocalRelay = &cli.BoolFlag{
+		Name:  "builder.local_relay",
+		Usage: "Enable the local relay",
+	}
+	BuilderDisableBundleFetcher = &cli.BoolFlag{
+		Name:  "builder.no_bundle_fetcher",
+		Usage: "Disable the bundle fetcher",
+	}
+	BuilderDryRun = &cli.BoolFlag{
+		Name:  "builder.dry-run",
+		Usage: "Builder only validates blocks without submission to the relay",
+	}
+	BuilderSecretKey = &cli.StringFlag{
+		Name:    "builder.secret_key",
+		Usage:   "Builder key used for signing blocks",
+		EnvVars: []string{"BUILDER_SECRET_KEY"},
+		Value:   "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11",
+	}
+	BuilderRelaySecretKey = &cli.StringFlag{
+		Name:    "builder.relay_secret_key",
+		Usage:   "Builder local relay API key used for signing headers",
+		EnvVars: []string{"BUILDER_RELAY_SECRET_KEY"},
+		Value:   "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11",
+	}
+	BuilderListenAddr = &cli.StringFlag{
+		Name:    "builder.listen_addr",
+		Usage:   "Listening address for builder endpoint",
+		EnvVars: []string{"BUILDER_LISTEN_ADDR"},
+		Value:   ":28545",
+	}
+	BuilderGenesisForkVersion = &cli.StringFlag{
+		Name:    "builder.genesis_fork_version",
+		Usage:   "Gensis fork version. For kiln use 0x70000069",
+		EnvVars: []string{"BUILDER_GENESIS_FORK_VERSION"},
+		Value:   "0x00000000",
+	}
+	BuilderBellatrixForkVersion = &cli.StringFlag{
+		Name:    "builder.bellatrix_fork_version",
+		Usage:   "Bellatrix fork version. For kiln use 0x70000071",
+		EnvVars: []string{"BUILDER_BELLATRIX_FORK_VERSION"},
+		Value:   "0x02000000",
+	}
+	BuilderGenesisValidatorsRoot = &cli.StringFlag{
+		Name:    "builder.genesis_validators_root",
+		Usage:   "Genesis validators root of the network. For kiln use 0x99b09fcd43e5905236c370f184056bec6e6638cfc31a323b304fc4aa789cb4ad",
+		EnvVars: []string{"BUILDER_GENESIS_VALIDATORS_ROOT"},
+		Value:   "0x0000000000000000000000000000000000000000000000000000000000000000",
+	}
+	BuilderBeaconEndpoint = &cli.StringFlag{
+		Name:    "builder.beacon_endpoint",
+		Usage:   "Beacon endpoint to connect to for beacon chain data",
+		EnvVars: []string{"BUILDER_BEACON_ENDPOINT"},
+		Value:   "http://127.0.0.1:5052",
+	}
+	BuilderRemoteRelayEndpoint = &cli.StringFlag{
+		Name:    "builder.remote_relay_endpoint",
+		Usage:   "Relay endpoint to connect to for validator registration data, if not provided will expose validator registration locally",
+		EnvVars: []string{"BUILDER_REMOTE_RELAY_ENDPOINT"},
+		Value:   "",
+	}
 	// RPC settings
 	IPCDisabledFlag = &cli.BoolFlag{
 		Name:     "ipcdisable",
@@ -1601,6 +1695,9 @@ func setTxPool(ctx *cli.Context, cfg *core.TxPoolConfig) {
 	if ctx.IsSet(TxPoolLifetimeFlag.Name) {
 		cfg.Lifetime = ctx.Duration(TxPoolLifetimeFlag.Name)
 	}
+	if ctx.IsSet(TxPoolPrivateLifetimeFlag.Name) {
+		cfg.PrivateTxLifetime = ctx.Duration(TxPoolPrivateLifetimeFlag.Name)
+	}
 }
 
 func setEthash(ctx *cli.Context, cfg *ethconfig.Config) {
@@ -1644,6 +1741,13 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
 	if ctx.IsSet(MinerGasPriceFlag.Name) {
 		cfg.GasPrice = flags.GlobalBig(ctx, MinerGasPriceFlag.Name)
 	}
+	if ctx.IsSet(MinerAlgoTypeFlag.Name) {
+		algoType, err := miner.AlgoTypeFlagToEnum(ctx.String(MinerAlgoTypeFlag.Name))
+		if err != nil {
+			Fatalf("Invalid algo in --miner.algotype: %s", ctx.String(MinerAlgoTypeFlag.Name))
+		}
+		cfg.AlgoType = algoType
+	}
 	if ctx.IsSet(MinerRecommitIntervalFlag.Name) {
 		cfg.Recommit = ctx.Duration(MinerRecommitIntervalFlag.Name)
 	}
@@ -1653,6 +1757,19 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) {
 	if ctx.IsSet(LegacyMinerGasTargetFlag.Name) {
 		log.Warn("The generic --miner.gastarget flag is deprecated and will be removed in the future!")
 	}
+
+	cfg.MaxMergedBundles = ctx.Int(MinerMaxMergedBundlesFlag.Name)
+
+	if ctx.IsSet(MinerBlocklistFileFlag.Name) {
+		bytes, err := os.ReadFile(ctx.String(MinerBlocklistFileFlag.Name))
+		if err != nil {
+			Fatalf("Failed to read blocklist file: %s", err)
+		}
+
+		if err := json.Unmarshal(bytes, &cfg.Blocklist); err != nil {
+			Fatalf("Failed to parse blocklist: %s", err)
+		}
+	}
 }
 
 func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) {
@@ -1987,7 +2104,7 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) {
 // RegisterEthService adds an Ethereum client to the stack.
 // The second return value is the full node instance, which may be nil if the
 // node is running as a light client.
-func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) {
+func RegisterEthService(stack *node.Node, cfg *ethconfig.Config, bpCfg *builder.BuilderConfig) (ethapi.Backend, *eth.Ethereum) {
 	if cfg.SyncMode == downloader.LightSync {
 		backend, err := les.New(stack, cfg)
 		if err != nil {
@@ -2012,6 +2129,13 @@ func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend
 	if err := ethcatalyst.Register(stack, backend); err != nil {
 		Fatalf("Failed to register the Engine API service: %v", err)
 	}
+
+	if bpCfg.Enabled {
+		if err := builder.Register(stack, backend, bpCfg); err != nil {
+			Fatalf("Failed to register the builder service: %v", err)
+		}
+	}
+
 	stack.RegisterAPIs(tracers.APIs(backend.APIBackend))
 	return backend.APIBackend, backend
 }
diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go
index 7e4d657413..92e2de99f4 100644
--- a/consensus/beacon/consensus.go
+++ b/consensus/beacon/consensus.go
@@ -352,9 +352,9 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea
 //
 // Note, the method returns immediately and will send the result async. More
 // than one result may also be returned depending on the consensus algorithm.
-func (beacon *Beacon) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
+func (beacon *Beacon) Seal(chain consensus.ChainHeaderReader, block *types.Block, profit *big.Int, results chan<- *types.Block, stop <-chan struct{}) error {
 	if !beacon.IsPoSHeader(block.Header()) {
-		return beacon.ethone.Seal(chain, block, results, stop)
+		return beacon.ethone.Seal(chain, block, profit, results, stop)
 	}
 	// The seal verification is done by the external consensus engine,
 	// return directly without pushing any block back. In another word
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go
index dcdfb20c63..aae7ce0fcc 100644
--- a/consensus/clique/clique.go
+++ b/consensus/clique/clique.go
@@ -592,7 +592,7 @@ func (c *Clique) Authorize(signer common.Address, signFn SignerFn) {
 
 // Seal implements consensus.Engine, attempting to create a sealed block using
 // the local signing credentials.
-func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
+func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, profit *big.Int, results chan<- *types.Block, stop <-chan struct{}) error {
 	header := block.Header()
 
 	// Sealing the genesis block is not supported
diff --git a/consensus/consensus.go b/consensus/consensus.go
index af8ce98ff3..540c78209f 100644
--- a/consensus/consensus.go
+++ b/consensus/consensus.go
@@ -105,7 +105,7 @@ type Engine interface {
 	//
 	// Note, the method returns immediately and will send the result async. More
 	// than one result may also be returned depending on the consensus algorithm.
-	Seal(chain ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error
+	Seal(chain ChainHeaderReader, block *types.Block, profit *big.Int, results chan<- *types.Block, stop <-chan struct{}) error
 
 	// SealHash returns the hash of a block prior to it being sealed.
 	SealHash(header *types.Header) common.Hash
diff --git a/consensus/ethash/api.go b/consensus/ethash/api.go
index f4d3802e0b..8aece9c7bb 100644
--- a/consensus/ethash/api.go
+++ b/consensus/ethash/api.go
@@ -44,7 +44,7 @@ func (api *API) GetWork() ([4]string, error) {
 	}
 
 	var (
-		workCh = make(chan [4]string, 1)
+		workCh = make(chan [5]string, 1)
 		errc   = make(chan error, 1)
 	)
 	select {
@@ -53,7 +53,10 @@ func (api *API) GetWork() ([4]string, error) {
 		return [4]string{}, errEthashStopped
 	}
 	select {
-	case work := <-workCh:
+	case fullWork := <-workCh:
+		var work [4]string
+		copy(work[:], fullWork[:4])
+
 		return work, nil
 	case err := <-errc:
 		return [4]string{}, err
diff --git a/consensus/ethash/ethash.go b/consensus/ethash/ethash.go
index dfe00d4b93..69686f5bc3 100644
--- a/consensus/ethash/ethash.go
+++ b/consensus/ethash/ethash.go
@@ -687,6 +687,12 @@ func (ethash *Ethash) APIs(chain consensus.ChainHeaderReader) []rpc.API {
 			Namespace: "ethash",
 			Service:   &API{ethash},
 		},
+		{
+			Namespace: "flashbots",
+			Version:   "1.0",
+			Service:   &FlashbotsAPI{ethash},
+			Public:    true,
+		},
 	}
 }
 
diff --git a/consensus/ethash/ethash_test.go b/consensus/ethash/ethash_test.go
index eb6bad9622..6e3f3bb59f 100644
--- a/consensus/ethash/ethash_test.go
+++ b/consensus/ethash/ethash_test.go
@@ -37,7 +37,7 @@ func TestTestMode(t *testing.T) {
 	defer ethash.Close()
 
 	results := make(chan *types.Block)
-	err := ethash.Seal(nil, types.NewBlockWithHeader(header), results, nil)
+	err := ethash.Seal(nil, types.NewBlockWithHeader(header), nil, results, nil)
 	if err != nil {
 		t.Fatalf("failed to seal block: %v", err)
 	}
@@ -112,7 +112,7 @@ func TestRemoteSealer(t *testing.T) {
 
 	// Push new work.
 	results := make(chan *types.Block)
-	ethash.Seal(nil, block, results, nil)
+	ethash.Seal(nil, block, nil, results, nil)
 
 	var (
 		work [4]string
@@ -129,7 +129,7 @@ func TestRemoteSealer(t *testing.T) {
 	header = &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(1000)}
 	block = types.NewBlockWithHeader(header)
 	sealhash = ethash.SealHash(header)
-	ethash.Seal(nil, block, results, nil)
+	ethash.Seal(nil, block, nil, results, nil)
 
 	if work, err = api.GetWork(); err != nil || work[0] != sealhash.Hex() {
 		t.Error("expect to return the latest pushed work")
diff --git a/consensus/ethash/flashbots_api.go b/consensus/ethash/flashbots_api.go
new file mode 100644
index 0000000000..527d2a4435
--- /dev/null
+++ b/consensus/ethash/flashbots_api.go
@@ -0,0 +1,38 @@
+package ethash
+
+import "errors"
+
+// FlashbotsAPI exposes Flashbots related methods for the RPC interface.
+type FlashbotsAPI struct {
+	ethash *Ethash
+}
+
+// GetWork returns a work package for external miner.
+//
+// The work package consists of 5 strings:
+//   result[0] - 32 bytes hex encoded current block header pow-hash
+//   result[1] - 32 bytes hex encoded seed hash used for DAG
+//   result[2] - 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty
+//   result[3] - hex encoded block number
+//   result[4] - hex encoded profit generated from this block
+func (api *FlashbotsAPI) GetWork() ([5]string, error) {
+	if api.ethash.remote == nil {
+		return [5]string{}, errors.New("not supported")
+	}
+
+	var (
+		workCh = make(chan [5]string, 1)
+		errc   = make(chan error, 1)
+	)
+	select {
+	case api.ethash.remote.fetchWorkCh <- &sealWork{errc: errc, res: workCh}:
+	case <-api.ethash.remote.exitCh:
+		return [5]string{}, errEthashStopped
+	}
+	select {
+	case work := <-workCh:
+		return work, nil
+	case err := <-errc:
+		return [5]string{}, err
+	}
+}
diff --git a/consensus/ethash/sealer.go b/consensus/ethash/sealer.go
index 6fa60ef6a8..d2b9253e5c 100644
--- a/consensus/ethash/sealer.go
+++ b/consensus/ethash/sealer.go
@@ -48,7 +48,7 @@ var (
 
 // Seal implements consensus.Engine, attempting to find a nonce that satisfies
 // the block's difficulty requirements.
-func (ethash *Ethash) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error {
+func (ethash *Ethash) Seal(chain consensus.ChainHeaderReader, block *types.Block, profit *big.Int, results chan<- *types.Block, stop <-chan struct{}) error {
 	// If we're running a fake PoW, simply return a 0 nonce immediately
 	if ethash.config.PowMode == ModeFake || ethash.config.PowMode == ModeFullFake {
 		header := block.Header()
@@ -62,7 +62,7 @@ func (ethash *Ethash) Seal(chain consensus.ChainHeaderReader, block *types.Block
 	}
 	// If we're running a shared PoW, delegate sealing to it
 	if ethash.shared != nil {
-		return ethash.shared.Seal(chain, block, results, stop)
+		return ethash.shared.Seal(chain, block, profit, results, stop)
 	}
 	// Create a runner and the multiple search threads it directs
 	abort := make(chan struct{})
@@ -86,7 +86,7 @@ func (ethash *Ethash) Seal(chain consensus.ChainHeaderReader, block *types.Block
 	}
 	// Push new work to remote sealer
 	if ethash.remote != nil {
-		ethash.remote.workCh <- &sealTask{block: block, results: results}
+		ethash.remote.workCh <- &sealTask{block: block, profit: profit, results: results}
 	}
 	var (
 		pend   sync.WaitGroup
@@ -117,7 +117,7 @@ func (ethash *Ethash) Seal(chain consensus.ChainHeaderReader, block *types.Block
 		case <-ethash.update:
 			// Thread count was changed on user request, restart
 			close(abort)
-			if err := ethash.Seal(chain, block, results, stop); err != nil {
+			if err := ethash.Seal(chain, block, profit, results, stop); err != nil {
 				ethash.config.Log.Error("Failed to restart sealing after update", "err", err)
 			}
 		}
@@ -194,7 +194,7 @@ type remoteSealer struct {
 	works        map[common.Hash]*types.Block
 	rates        map[common.Hash]hashrate
 	currentBlock *types.Block
-	currentWork  [4]string
+	currentWork  [5]string
 	notifyCtx    context.Context
 	cancelNotify context.CancelFunc // cancels all notification requests
 	reqWG        sync.WaitGroup     // tracks notification request goroutines
@@ -215,6 +215,7 @@ type remoteSealer struct {
 // sealTask wraps a seal block with relative result channel for remote sealer thread.
 type sealTask struct {
 	block   *types.Block
+	profit  *big.Int
 	results chan<- *types.Block
 }
 
@@ -239,7 +240,7 @@ type hashrate struct {
 // sealWork wraps a seal work package for remote sealer.
 type sealWork struct {
 	errc chan error
-	res  chan [4]string
+	res  chan [5]string
 }
 
 func startRemoteSealer(ethash *Ethash, urls []string, noverify bool) *remoteSealer {
@@ -281,7 +282,7 @@ func (s *remoteSealer) loop() {
 			// Update current work with new received block.
 			// Note same work can be past twice, happens when changing CPU threads.
 			s.results = work.results
-			s.makeWork(work.block)
+			s.makeWork(work.block, work.profit)
 			s.notifyWork()
 
 		case work := <-s.fetchWorkCh:
@@ -338,18 +339,23 @@ func (s *remoteSealer) loop() {
 
 // makeWork creates a work package for external miner.
 //
-// The work package consists of 3 strings:
+// The work package consists of 5 strings:
 //   result[0], 32 bytes hex encoded current block header pow-hash
 //   result[1], 32 bytes hex encoded seed hash used for DAG
 //   result[2], 32 bytes hex encoded boundary condition ("target"), 2^256/difficulty
 //   result[3], hex encoded block number
-func (s *remoteSealer) makeWork(block *types.Block) {
+//   result[4], hex encoded profit generated from this block, if present
+func (s *remoteSealer) makeWork(block *types.Block, profit *big.Int) {
 	hash := s.ethash.SealHash(block.Header())
 	s.currentWork[0] = hash.Hex()
 	s.currentWork[1] = common.BytesToHash(SeedHash(block.NumberU64())).Hex()
 	s.currentWork[2] = common.BytesToHash(new(big.Int).Div(two256, block.Difficulty()).Bytes()).Hex()
 	s.currentWork[3] = hexutil.EncodeBig(block.Number())
 
+	if profit != nil {
+		s.currentWork[4] = hexutil.EncodeBig(profit)
+	}
+
 	// Trace the seal work fetched by remote sealer.
 	s.currentBlock = block
 	s.works[hash] = block
@@ -375,7 +381,7 @@ func (s *remoteSealer) notifyWork() {
 	}
 }
 
-func (s *remoteSealer) sendNotification(ctx context.Context, url string, json []byte, work [4]string) {
+func (s *remoteSealer) sendNotification(ctx context.Context, url string, json []byte, work [5]string) {
 	defer s.reqWG.Done()
 
 	req, err := http.NewRequest("POST", url, bytes.NewReader(json))
diff --git a/consensus/ethash/sealer_test.go b/consensus/ethash/sealer_test.go
index e338f75290..4957788f21 100644
--- a/consensus/ethash/sealer_test.go
+++ b/consensus/ethash/sealer_test.go
@@ -57,7 +57,7 @@ func TestRemoteNotify(t *testing.T) {
 	header := &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(100)}
 	block := types.NewBlockWithHeader(header)
 
-	ethash.Seal(nil, block, nil, nil)
+	ethash.Seal(nil, block, nil, nil, nil)
 	select {
 	case work := <-sink:
 		if want := ethash.SealHash(header).Hex(); work[0] != want {
@@ -105,7 +105,7 @@ func TestRemoteNotifyFull(t *testing.T) {
 	header := &types.Header{Number: big.NewInt(1), Difficulty: big.NewInt(100)}
 	block := types.NewBlockWithHeader(header)
 
-	ethash.Seal(nil, block, nil, nil)
+	ethash.Seal(nil, block, nil, nil, nil)
 	select {
 	case work := <-sink:
 		if want := "0x" + strconv.FormatUint(header.Number.Uint64(), 16); work["number"] != want {
@@ -151,7 +151,7 @@ func TestRemoteMultiNotify(t *testing.T) {
 	for i := 0; i < cap(sink); i++ {
 		header := &types.Header{Number: big.NewInt(int64(i)), Difficulty: big.NewInt(100)}
 		block := types.NewBlockWithHeader(header)
-		ethash.Seal(nil, block, results, nil)
+		ethash.Seal(nil, block, nil, results, nil)
 	}
 
 	for i := 0; i < cap(sink); i++ {
@@ -200,7 +200,7 @@ func TestRemoteMultiNotifyFull(t *testing.T) {
 	for i := 0; i < cap(sink); i++ {
 		header := &types.Header{Number: big.NewInt(int64(i)), Difficulty: big.NewInt(100)}
 		block := types.NewBlockWithHeader(header)
-		ethash.Seal(nil, block, results, nil)
+		ethash.Seal(nil, block, nil, results, nil)
 	}
 
 	for i := 0; i < cap(sink); i++ {
@@ -266,7 +266,7 @@ func TestStaleSubmission(t *testing.T) {
 
 	for id, c := range testcases {
 		for _, h := range c.headers {
-			ethash.Seal(nil, types.NewBlockWithHeader(h), results, nil)
+			ethash.Seal(nil, types.NewBlockWithHeader(h), nil, results, nil)
 		}
 		if res := api.SubmitWork(fakeNonce, ethash.SealHash(c.headers[c.submitIndex]), fakeDigest); res != c.submitRes {
 			t.Errorf("case %d submit result mismatch, want %t, get %t", id+1, c.submitRes, res)
diff --git a/core/beacon/types.go b/core/beacon/types.go
index e25d724c0d..cf02850324 100644
--- a/core/beacon/types.go
+++ b/core/beacon/types.go
@@ -24,6 +24,8 @@ import (
 	"github.com/ethereum/go-ethereum/common/hexutil"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/trie"
+
+	boostTypes "github.com/flashbots/go-boost-utils/types"
 )
 
 //go:generate go run github.com/fjl/gencodec -type PayloadAttributesV1 -field-override payloadAttributesMarshaling -out gen_blockparams.go
@@ -33,6 +35,8 @@ type PayloadAttributesV1 struct {
 	Timestamp             uint64         `json:"timestamp"     gencodec:"required"`
 	Random                common.Hash    `json:"prevRandao"        gencodec:"required"`
 	SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient"  gencodec:"required"`
+	GasLimit              uint64
+	Slot                  uint64
 }
 
 // JSON type overrides for PayloadAttributesV1.
@@ -179,6 +183,38 @@ func ExecutableDataToBlock(params ExecutableDataV1) (*types.Block, error) {
 	return block, nil
 }
 
+func ExecutionPayloadToBlock(payload *boostTypes.ExecutionPayload) (*types.Block, error) {
+	// TODO: separate decode function to avoid allocating twice
+	transactionBytes := make([][]byte, len(payload.Transactions))
+	for i, txHexBytes := range payload.Transactions {
+		transactionBytes[i] = txHexBytes[:]
+	}
+	txs, err := decodeTransactions(transactionBytes)
+	if err != nil {
+		return nil, err
+	}
+
+	header := &types.Header{
+		ParentHash:  common.Hash(payload.ParentHash),
+		UncleHash:   types.EmptyUncleHash,
+		Coinbase:    common.Address(payload.FeeRecipient),
+		Root:        common.Hash(payload.StateRoot),
+		TxHash:      types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)),
+		ReceiptHash: common.Hash(payload.ReceiptsRoot),
+		Bloom:       types.BytesToBloom(payload.LogsBloom[:]),
+		Difficulty:  common.Big0,
+		Number:      new(big.Int).SetUint64(payload.BlockNumber),
+		GasLimit:    payload.GasLimit,
+		GasUsed:     payload.GasUsed,
+		Time:        payload.Timestamp,
+		BaseFee:     payload.BaseFeePerGas.BigInt(),
+		Extra:       payload.ExtraData,
+		MixDigest:   common.Hash(payload.Random),
+	}
+	block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */)
+	return block, nil
+}
+
 // BlockToExecutableData constructs the executableDataV1 structure by filling the
 // fields from the given block. It assumes the given block is post-merge block.
 func BlockToExecutableData(block *types.Block) *ExecutableDataV1 {
diff --git a/core/block_validator.go b/core/block_validator.go
index 3763be0be0..3950bfe2d7 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -22,6 +22,7 @@ import (
 	"github.com/ethereum/go-ethereum/consensus"
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/core/utils"
 	"github.com/ethereum/go-ethereum/params"
 	"github.com/ethereum/go-ethereum/trie"
 )
@@ -102,28 +103,6 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
 	return nil
 }
 
-// CalcGasLimit computes the gas limit of the next block after parent. It aims
-// to keep the baseline gas close to the provided target, and increase it towards
-// the target if the baseline gas is lower.
 func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 {
-	delta := parentGasLimit/params.GasLimitBoundDivisor - 1
-	limit := parentGasLimit
-	if desiredLimit < params.MinGasLimit {
-		desiredLimit = params.MinGasLimit
-	}
-	// If we're outside our allowed gas range, we try to hone towards them
-	if limit < desiredLimit {
-		limit = parentGasLimit + delta
-		if limit > desiredLimit {
-			limit = desiredLimit
-		}
-		return limit
-	}
-	if limit > desiredLimit {
-		limit = parentGasLimit - delta
-		if limit < desiredLimit {
-			limit = desiredLimit
-		}
-	}
-	return limit
+	return utils.CalcGasLimit(parentGasLimit, desiredLimit)
 }
diff --git a/core/blockchain.go b/core/blockchain.go
index a98c3b4dbe..59479190b1 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -35,6 +35,7 @@ import (
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/state/snapshot"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/core/utils"
 	"github.com/ethereum/go-ethereum/core/vm"
 	"github.com/ethereum/go-ethereum/ethdb"
 	"github.com/ethereum/go-ethereum/event"
@@ -2421,3 +2422,95 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro
 	bc.validator = v
 	bc.processor = p
 }
+
+func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config) error {
+	header := block.Header()
+	if err := bc.engine.VerifyHeader(bc, header, true); err != nil {
+		return err
+	}
+
+	current := bc.CurrentBlock()
+	reorg, err := bc.forker.ReorgNeeded(current.Header(), header)
+	if err == nil && reorg {
+		return errors.New("block requires a reorg")
+	}
+
+	parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1)
+	if parent == nil {
+		return errors.New("parent not found")
+	}
+
+	calculatedGasLimit := utils.CalcGasLimit(parent.GasLimit, registeredGasLimit)
+	if calculatedGasLimit != header.GasLimit {
+		return errors.New("incorrect gas limit set")
+	}
+
+	statedb, err := bc.StateAt(parent.Root)
+	if err != nil {
+		return err
+	}
+
+	// The chain importer is starting and stopping trie prefetchers. If a bad
+	// block or other error is hit however, an early return may not properly
+	// terminate the background threads. This defer ensures that we clean up
+	// and dangling prefetcher, without defering each and holding on live refs.
+	defer statedb.StopPrefetcher()
+
+	receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig)
+	if err != nil {
+		return err
+	}
+
+	if err := bc.validator.ValidateBody(block); err != nil {
+		return err
+	}
+
+	if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil {
+		return err
+	}
+
+	if len(receipts) == 0 {
+		return errors.New("no proposer payment receipt")
+	}
+
+	lastReceipt := receipts[len(receipts)-1]
+	if lastReceipt.Status != types.ReceiptStatusSuccessful {
+		return errors.New("proposer payment not successful")
+	}
+	txIndex := lastReceipt.TransactionIndex
+	if txIndex+1 != uint(len(block.Transactions())) {
+		return fmt.Errorf("proposer payment index not last transaction in the block (%d of %d)", txIndex, len(block.Transactions())-1)
+	}
+
+	paymentTx := block.Transaction(lastReceipt.TxHash)
+	if paymentTx == nil {
+		return errors.New("payment tx not in the block")
+	}
+
+	paymentTo := paymentTx.To()
+	if paymentTo == nil || *paymentTo != feeRecipient {
+		return fmt.Errorf("payment tx not to the proposers fee recipient (%v)", paymentTo)
+	}
+
+	if paymentTx.Value().Cmp(expectedProfit) != 0 {
+		return fmt.Errorf("inaccurate payment %s, expected %s", paymentTx.Value().String(), expectedProfit.String())
+	}
+
+	if len(paymentTx.Data()) != 0 {
+		return fmt.Errorf("malformed proposer payment, contains calldata")
+	}
+
+	if paymentTx.GasPrice().Cmp(block.BaseFee()) != 0 {
+		return fmt.Errorf("malformed proposer payment, gas price not equal to base fee")
+	}
+
+	if paymentTx.GasTipCap().Cmp(block.BaseFee()) != 0 && paymentTx.GasTipCap().Sign() != 0 {
+		return fmt.Errorf("malformed proposer payment, unexpected gas tip cap")
+	}
+
+	if paymentTx.GasFeeCap().Cmp(block.BaseFee()) != 0 {
+		return fmt.Errorf("malformed proposer payment, unexpected gas fee cap")
+	}
+
+	return nil
+}
diff --git a/core/chain_makers.go b/core/chain_makers.go
index c7bf60a4b0..62686414b8 100644
--- a/core/chain_makers.go
+++ b/core/chain_makers.go
@@ -103,7 +103,7 @@ func (b *BlockGen) AddTxWithChain(bc *BlockChain, tx *types.Transaction) {
 		b.SetCoinbase(common.Address{})
 	}
 	b.statedb.Prepare(tx.Hash(), len(b.txs))
-	receipt, err := ApplyTransaction(b.config, bc, &b.header.Coinbase, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed, vm.Config{})
+	receipt, err := ApplyTransaction(b.config, bc, &b.header.Coinbase, b.gasPool, b.statedb, b.header, tx, &b.header.GasUsed, vm.Config{}, nil)
 	if err != nil {
 		panic(err)
 	}
diff --git a/core/state_processor.go b/core/state_processor.go
index e511697c5f..84192d7b0a 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -79,7 +79,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
 			return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
 		}
 		statedb.Prepare(tx.Hash(), i)
-		receipt, err := applyTransaction(msg, p.config, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv)
+		receipt, err := applyTransaction(msg, p.config, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, nil)
 		if err != nil {
 			return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err)
 		}
@@ -92,16 +92,24 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
 	return receipts, allLogs, *usedGas, nil
 }
 
-func applyTransaction(msg types.Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) {
+func applyTransaction(msg types.Message, config *params.ChainConfig, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, preFinalizeHook func() error) (*types.Receipt, error) {
 	// Create a new context to be used in the EVM environment.
 	txContext := NewEVMTxContext(msg)
 	evm.Reset(txContext, statedb)
 
+	snapshot := statedb.Snapshot()
 	// Apply the transaction to the current state (included in the env).
 	result, err := ApplyMessage(evm, msg, gp)
 	if err != nil {
 		return nil, err
 	}
+	if preFinalizeHook != nil {
+		err = preFinalizeHook()
+		if err != nil {
+			statedb.RevertToSnapshot(snapshot)
+			return nil, err
+		}
+	}
 
 	// Update the state with pending changes.
 	var root []byte
@@ -137,11 +145,56 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, author *com
 	return receipt, err
 }
 
+func applyTransactionWithResult(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, *ExecutionResult, error) {
+	// Create a new context to be used in the EVM environment.
+	txContext := NewEVMTxContext(msg)
+	evm.Reset(txContext, statedb)
+
+	// Apply the transaction to the current state (included in the env).
+	result, err := ApplyMessage(evm, msg, gp)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Update the state with pending changes.
+	var root []byte
+	if config.IsByzantium(header.Number) {
+		statedb.Finalise(true)
+	} else {
+		root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
+	}
+	*usedGas += result.UsedGas
+
+	// Create a new receipt for the transaction, storing the intermediate root and gas used
+	// by the tx.
+	receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas}
+	if result.Failed() {
+		receipt.Status = types.ReceiptStatusFailed
+	} else {
+		receipt.Status = types.ReceiptStatusSuccessful
+	}
+	receipt.TxHash = tx.Hash()
+	receipt.GasUsed = result.UsedGas
+
+	// If the transaction created a contract, store the creation address in the receipt.
+	if msg.To() == nil {
+		receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce())
+	}
+
+	// Set the receipt logs and create the bloom filter.
+	receipt.Logs = statedb.GetLogs(tx.Hash(), header.Hash())
+	receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
+	receipt.BlockHash = header.Hash()
+	receipt.BlockNumber = header.Number
+	receipt.TransactionIndex = uint(statedb.TxIndex())
+	return receipt, result, err
+}
+
 // ApplyTransaction attempts to apply a transaction to the given state database
 // and uses the input parameters for its environment. It returns the receipt
 // for the transaction, gas used and an error if the transaction failed,
 // indicating the block was invalid.
-func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) {
+func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, preFinalizeHook func() error) (*types.Receipt, error) {
 	msg, err := tx.AsMessage(types.MakeSigner(config, header.Number), header.BaseFee)
 	if err != nil {
 		return nil, err
@@ -149,5 +202,16 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
 	// Create a new context to be used in the EVM environment
 	blockContext := NewEVMBlockContext(header, bc, author)
 	vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)
-	return applyTransaction(msg, config, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv)
+	return applyTransaction(msg, config, author, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv, preFinalizeHook)
+}
+
+func ApplyTransactionWithResult(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, *ExecutionResult, error) {
+	msg, err := tx.AsMessage(types.MakeSigner(config, header.Number), header.BaseFee)
+	if err != nil {
+		return nil, nil, err
+	}
+	// Create a new context to be used in the EVM environment
+	blockContext := NewEVMBlockContext(header, bc, author)
+	vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg)
+	return applyTransactionWithResult(msg, config, bc, author, gp, statedb, header, tx, usedGas, vmenv)
 }
diff --git a/core/tx_pool.go b/core/tx_pool.go
index 1c25442dd9..d53bb4559f 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -34,6 +34,7 @@ import (
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/metrics"
 	"github.com/ethereum/go-ethereum/params"
+	"golang.org/x/crypto/sha3"
 )
 
 const (
@@ -88,8 +89,9 @@ var (
 )
 
 var (
-	evictionInterval    = time.Minute     // Time interval to check for evictable transactions
-	statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats
+	evictionInterval         = time.Minute     // Time interval to check for evictable transactions
+	statsReportInterval      = 8 * time.Second // Time interval to report transaction pool stats
+	privateTxCleanupInterval = 1 * time.Hour
 )
 
 var (
@@ -164,7 +166,10 @@ type TxPoolConfig struct {
 	AccountQueue uint64 // Maximum number of non-executable transaction slots permitted per account
 	GlobalQueue  uint64 // Maximum number of non-executable transaction slots for all accounts
 
-	Lifetime time.Duration // Maximum amount of time non-executable transaction are queued
+	Lifetime          time.Duration // Maximum amount of time non-executable transaction are queued
+	PrivateTxLifetime time.Duration // Maximum amount of time to keep private transactions private
+
+	TrustedRelays []common.Address // Trusted relay addresses. Duplicated from the miner config.
 }
 
 // DefaultTxPoolConfig contains the default configurations for the transaction
@@ -181,7 +186,8 @@ var DefaultTxPoolConfig = TxPoolConfig{
 	AccountQueue: 64,
 	GlobalQueue:  1024,
 
-	Lifetime: 3 * time.Hour,
+	Lifetime:          3 * time.Hour,
+	PrivateTxLifetime: 3 * 24 * time.Hour,
 }
 
 // sanitize checks the provided user configurations and changes anything that's
@@ -220,6 +226,10 @@ func (config *TxPoolConfig) sanitize() TxPoolConfig {
 		log.Warn("Sanitizing invalid txpool lifetime", "provided", conf.Lifetime, "updated", DefaultTxPoolConfig.Lifetime)
 		conf.Lifetime = DefaultTxPoolConfig.Lifetime
 	}
+	if conf.PrivateTxLifetime < 1 {
+		log.Warn("Sanitizing invalid txpool private tx lifetime", "provided", conf.PrivateTxLifetime, "updated", DefaultTxPoolConfig.PrivateTxLifetime)
+		conf.PrivateTxLifetime = DefaultTxPoolConfig.PrivateTxLifetime
+	}
 	return conf
 }
 
@@ -251,11 +261,13 @@ type TxPool struct {
 	locals  *accountSet // Set of local transaction to exempt from eviction rules
 	journal *txJournal  // Journal of local transaction to back up to disk
 
-	pending map[common.Address]*txList   // All currently processable transactions
-	queue   map[common.Address]*txList   // Queued but non-processable transactions
-	beats   map[common.Address]time.Time // Last heartbeat from each known account
-	all     *txLookup                    // All transactions to allow lookups
-	priced  *txPricedList                // All transactions sorted by price
+	pending    map[common.Address]*txList   // All currently processable transactions
+	queue      map[common.Address]*txList   // Queued but non-processable transactions
+	beats      map[common.Address]time.Time // Last heartbeat from each known account
+	mevBundles []types.MevBundle
+	all        *txLookup     // All transactions to allow lookups
+	priced     *txPricedList // All transactions sorted by price
+	privateTxs *timestampedTxHashSet
 
 	chainHeadCh     chan ChainHeadEvent
 	chainHeadSub    event.Subscription
@@ -290,6 +302,7 @@ func NewTxPool(config TxPoolConfig, chainconfig *params.ChainConfig, chain block
 		queue:           make(map[common.Address]*txList),
 		beats:           make(map[common.Address]time.Time),
 		all:             newTxLookup(),
+		privateTxs:      newExpiringTxHashSet(config.PrivateTxLifetime),
 		chainHeadCh:     make(chan ChainHeadEvent, chainHeadChanSize),
 		reqResetCh:      make(chan *txpoolResetRequest),
 		reqPromoteCh:    make(chan *accountSet),
@@ -340,15 +353,17 @@ func (pool *TxPool) loop() {
 	var (
 		prevPending, prevQueued, prevStales int
 		// Start the stats reporting and transaction eviction tickers
-		report  = time.NewTicker(statsReportInterval)
-		evict   = time.NewTicker(evictionInterval)
-		journal = time.NewTicker(pool.config.Rejournal)
+		report    = time.NewTicker(statsReportInterval)
+		evict     = time.NewTicker(evictionInterval)
+		journal   = time.NewTicker(pool.config.Rejournal)
+		privateTx = time.NewTicker(privateTxCleanupInterval)
 		// Track the previous head headers for transaction reorgs
 		head = pool.chain.CurrentBlock()
 	)
 	defer report.Stop()
 	defer evict.Stop()
 	defer journal.Stop()
+	defer privateTx.Stop()
 
 	// Notify tests that the init phase is done
 	close(pool.initDoneCh)
@@ -406,6 +421,10 @@ func (pool *TxPool) loop() {
 				}
 				pool.mu.Unlock()
 			}
+
+			// Remove stale hashes that must be kept private
+		case <-privateTx.C:
+			pool.privateTxs.prune()
 		}
 	}
 }
@@ -526,6 +545,11 @@ func (pool *TxPool) ContentFrom(addr common.Address) (types.Transactions, types.
 	return pending, queued
 }
 
+// IsPrivateTxHash indicates whether the transaction should be shared with peers
+func (pool *TxPool) IsPrivateTxHash(hash common.Hash) bool {
+	return pool.privateTxs.Contains(hash)
+}
+
 // Pending retrieves all currently processable transactions, grouped by origin
 // account and sorted by nonce. The returned transaction set is a copy and can be
 // freely modified by calling code.
@@ -557,6 +581,75 @@ func (pool *TxPool) Pending(enforceTips bool) map[common.Address]types.Transacti
 	return pending
 }
 
+/// AllMevBundles returns all the MEV Bundles currently in the pool
+func (pool *TxPool) AllMevBundles() []types.MevBundle {
+	return pool.mevBundles
+}
+
+// MevBundles returns a list of bundles valid for the given blockNumber/blockTimestamp
+// also prunes bundles that are outdated
+func (pool *TxPool) MevBundles(blockNumber *big.Int, blockTimestamp uint64) []types.MevBundle {
+	pool.mu.Lock()
+	defer pool.mu.Unlock()
+
+	// returned values
+	var ret []types.MevBundle
+	// rolled over values
+	var bundles []types.MevBundle
+
+	for _, bundle := range pool.mevBundles {
+		// Prune outdated bundles
+		if (bundle.MaxTimestamp != 0 && blockTimestamp > bundle.MaxTimestamp) || blockNumber.Cmp(bundle.BlockNumber) > 0 {
+			continue
+		}
+
+		// Roll over future bundles
+		if (bundle.MinTimestamp != 0 && blockTimestamp < bundle.MinTimestamp) || blockNumber.Cmp(bundle.BlockNumber) < 0 {
+			bundles = append(bundles, bundle)
+			continue
+		}
+
+		// return the ones which are in time
+		ret = append(ret, bundle)
+		// keep the bundles around internally until they need to be pruned
+		bundles = append(bundles, bundle)
+	}
+
+	pool.mevBundles = bundles
+	return ret
+}
+
+// AddMevBundles adds a mev bundles to the pool
+func (pool *TxPool) AddMevBundles(mevBundles []types.MevBundle) error {
+	pool.mu.Lock()
+	defer pool.mu.Unlock()
+
+	pool.mevBundles = append(pool.mevBundles, mevBundles...)
+	return nil
+}
+
+// AddMevBundle adds a mev bundle to the pool
+func (pool *TxPool) AddMevBundle(txs types.Transactions, blockNumber *big.Int, minTimestamp, maxTimestamp uint64, revertingTxHashes []common.Hash) error {
+	bundleHasher := sha3.NewLegacyKeccak256()
+	for _, tx := range txs {
+		bundleHasher.Write(tx.Hash().Bytes())
+	}
+	bundleHash := common.BytesToHash(bundleHasher.Sum(nil))
+
+	pool.mu.Lock()
+	defer pool.mu.Unlock()
+
+	pool.mevBundles = append(pool.mevBundles, types.MevBundle{
+		Txs:               txs,
+		BlockNumber:       blockNumber,
+		MinTimestamp:      minTimestamp,
+		MaxTimestamp:      maxTimestamp,
+		RevertingTxHashes: revertingTxHashes,
+		Hash:              bundleHash,
+	})
+	return nil
+}
+
 // Locals retrieves the accounts currently considered local by the pool.
 func (pool *TxPool) Locals() []common.Address {
 	pool.mu.Lock()
@@ -846,7 +939,7 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T
 // This method is used to add transactions from the RPC API and performs synchronous pool
 // reorganization and event propagation.
 func (pool *TxPool) AddLocals(txs []*types.Transaction) []error {
-	return pool.addTxs(txs, !pool.config.NoLocals, true)
+	return pool.addTxs(txs, !pool.config.NoLocals, true, false)
 }
 
 // AddLocal enqueues a single local transaction into the pool if it is valid. This is
@@ -862,12 +955,18 @@ func (pool *TxPool) AddLocal(tx *types.Transaction) error {
 // This method is used to add transactions from the p2p network and does not wait for pool
 // reorganization and internal event propagation.
 func (pool *TxPool) AddRemotes(txs []*types.Transaction) []error {
-	return pool.addTxs(txs, false, false)
+	return pool.addTxs(txs, false, false, false)
+}
+
+// AddPrivateRemote adds transactions to the pool, but does not broadcast these transactions to any peers.
+func (pool *TxPool) AddPrivateRemote(tx *types.Transaction) error {
+	errs := pool.addTxs([]*types.Transaction{tx}, false, false, true)
+	return errs[0]
 }
 
 // This is like AddRemotes, but waits for pool reorganization. Tests use this method.
 func (pool *TxPool) AddRemotesSync(txs []*types.Transaction) []error {
-	return pool.addTxs(txs, false, true)
+	return pool.addTxs(txs, false, true, false)
 }
 
 // This is like AddRemotes with a single transaction, but waits for pool reorganization. Tests use this method.
@@ -886,7 +985,7 @@ func (pool *TxPool) AddRemote(tx *types.Transaction) error {
 }
 
 // addTxs attempts to queue a batch of transactions if they are valid.
-func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error {
+func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync, private bool) []error {
 	// Filter out known ones without obtaining the pool lock or recovering signatures
 	var (
 		errs = make([]error, len(txs))
@@ -915,6 +1014,13 @@ func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error {
 		return errs
 	}
 
+	// Track private transactions, so they don't get leaked to the public mempool
+	if private {
+		for _, tx := range news {
+			pool.privateTxs.Add(tx.Hash())
+		}
+	}
+
 	// Process all the new transaction and merge any errors into the original slice
 	pool.mu.Lock()
 	newErrs, dirtyAddrs := pool.addTxsLocked(news, local)
@@ -1209,7 +1315,11 @@ func (pool *TxPool) runReorg(done chan struct{}, reset *txpoolResetRequest, dirt
 	if len(events) > 0 {
 		var txs []*types.Transaction
 		for _, set := range events {
-			txs = append(txs, set.Flatten()...)
+			for _, tx := range set.Flatten() {
+				if !pool.IsPrivateTxHash(tx.Hash()) {
+					txs = append(txs, tx)
+				}
+			}
 		}
 		pool.txFeed.Send(NewTxsEvent{txs})
 	}
@@ -1521,6 +1631,7 @@ func (pool *TxPool) demoteUnexecutables() {
 		for _, tx := range olds {
 			hash := tx.Hash()
 			pool.all.Remove(hash)
+			pool.privateTxs.Remove(hash)
 			log.Trace("Removed old pending transaction", "hash", hash)
 		}
 		// Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later
@@ -1819,6 +1930,60 @@ func (t *txLookup) RemotesBelowTip(threshold *big.Int) types.Transactions {
 	return found
 }
 
+type timestampedTxHashSet struct {
+	lock       sync.RWMutex
+	timestamps map[common.Hash]time.Time
+	ttl        time.Duration
+}
+
+func newExpiringTxHashSet(ttl time.Duration) *timestampedTxHashSet {
+	s := &timestampedTxHashSet{
+		timestamps: make(map[common.Hash]time.Time),
+		ttl:        ttl,
+	}
+
+	return s
+}
+
+func (s *timestampedTxHashSet) Add(hash common.Hash) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	_, ok := s.timestamps[hash]
+	if !ok {
+		s.timestamps[hash] = time.Now().Add(s.ttl)
+	}
+}
+
+func (s *timestampedTxHashSet) Contains(hash common.Hash) bool {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+	_, ok := s.timestamps[hash]
+	return ok
+}
+
+func (s *timestampedTxHashSet) Remove(hash common.Hash) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	_, ok := s.timestamps[hash]
+	if ok {
+		delete(s.timestamps, hash)
+	}
+}
+
+func (s *timestampedTxHashSet) prune() {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	now := time.Now()
+	for hash, ts := range s.timestamps {
+		if ts.Before(now) {
+			delete(s.timestamps, hash)
+		}
+	}
+}
+
 // numSlots calculates the number of slots needed for a single transaction.
 func numSlots(tx *types.Transaction) int {
 	return int((tx.Size() + txSlotSize - 1) / txSlotSize)
diff --git a/core/types/block.go b/core/types/block.go
index 7525a88f5a..c5076f1142 100644
--- a/core/types/block.go
+++ b/core/types/block.go
@@ -168,6 +168,8 @@ type Block struct {
 	uncles       []*Header
 	transactions Transactions
 
+	Profit *big.Int
+
 	// caches
 	hash atomic.Value
 	size atomic.Value
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 715ede15db..b87549ac1f 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -461,7 +461,8 @@ func (s TxByNonce) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
 
 // TxWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap
 type TxWithMinerFee struct {
-	tx       *Transaction
+	Tx       *Transaction
+	Bundle   *SimulatedBundle
 	minerFee *big.Int
 }
 
@@ -474,7 +475,16 @@ func NewTxWithMinerFee(tx *Transaction, baseFee *big.Int) (*TxWithMinerFee, erro
 		return nil, err
 	}
 	return &TxWithMinerFee{
-		tx:       tx,
+		Tx:       tx,
+		minerFee: minerFee,
+	}, nil
+}
+
+// NewBundleWithMinerFee creates a wrapped bundle.
+func NewBundleWithMinerFee(bundle *SimulatedBundle, baseFee *big.Int) (*TxWithMinerFee, error) {
+	minerFee := bundle.MevGasPrice
+	return &TxWithMinerFee{
+		Bundle:   bundle,
 		minerFee: minerFee,
 	}, nil
 }
@@ -489,7 +499,14 @@ func (s TxByPriceAndTime) Less(i, j int) bool {
 	// deterministic sorting
 	cmp := s[i].minerFee.Cmp(s[j].minerFee)
 	if cmp == 0 {
-		return s[i].tx.time.Before(s[j].tx.time)
+		if s[i].Tx != nil && s[j].Tx != nil {
+			return s[i].Tx.time.Before(s[j].Tx.time)
+		} else if s[i].Bundle != nil && s[j].Bundle != nil {
+			return s[i].Bundle.TotalGasUsed <= s[j].Bundle.TotalGasUsed
+		} else if s[i].Bundle != nil {
+			return false
+		}
+		return true
 	}
 	return cmp > 0
 }
@@ -522,9 +539,16 @@ type TransactionsByPriceAndNonce struct {
 //
 // Note, the input map is reowned so the caller should not interact any more with
 // if after providing it to the constructor.
-func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, baseFee *big.Int) *TransactionsByPriceAndNonce {
+func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, bundles []SimulatedBundle, baseFee *big.Int) *TransactionsByPriceAndNonce {
 	// Initialize a price and received time based heap with the head transactions
-	heads := make(TxByPriceAndTime, 0, len(txs))
+	heads := make(TxByPriceAndTime, 0, len(txs)+len(bundles))
+	for i := range bundles {
+		wrapped, err := NewBundleWithMinerFee(&bundles[i], baseFee)
+		if err != nil {
+			continue
+		}
+		heads = append(heads, wrapped)
+	}
 	for from, accTxs := range txs {
 		acc, _ := Sender(signer, accTxs[0])
 		wrapped, err := NewTxWithMinerFee(accTxs[0], baseFee)
@@ -547,22 +571,37 @@ func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transa
 	}
 }
 
+func (t *TransactionsByPriceAndNonce) DeepCopy() *TransactionsByPriceAndNonce {
+	newT := &TransactionsByPriceAndNonce{
+		txs:     make(map[common.Address]Transactions),
+		heads:   append(TxByPriceAndTime{}, t.heads...),
+		signer:  t.signer,
+		baseFee: new(big.Int).Set(t.baseFee),
+	}
+	for k, v := range t.txs {
+		newT.txs[k] = v
+	}
+	return newT
+}
+
 // Peek returns the next transaction by price.
-func (t *TransactionsByPriceAndNonce) Peek() *Transaction {
+func (t *TransactionsByPriceAndNonce) Peek() *TxWithMinerFee {
 	if len(t.heads) == 0 {
 		return nil
 	}
-	return t.heads[0].tx
+	return t.heads[0]
 }
 
 // Shift replaces the current best head with the next one from the same account.
 func (t *TransactionsByPriceAndNonce) Shift() {
-	acc, _ := Sender(t.signer, t.heads[0].tx)
-	if txs, ok := t.txs[acc]; ok && len(txs) > 0 {
-		if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil {
-			t.heads[0], t.txs[acc] = wrapped, txs[1:]
-			heap.Fix(&t.heads, 0)
-			return
+	if t.heads[0].Tx != nil {
+		acc, _ := Sender(t.signer, t.heads[0].Tx)
+		if txs, ok := t.txs[acc]; ok && len(txs) > 0 {
+			if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil {
+				t.heads[0], t.txs[acc] = wrapped, txs[1:]
+				heap.Fix(&t.heads, 0)
+				return
+			}
 		}
 	}
 	heap.Pop(&t.heads)
@@ -651,3 +690,29 @@ func copyAddressPtr(a *common.Address) *common.Address {
 	cpy := *a
 	return &cpy
 }
+
+type MevBundle struct {
+	Txs               Transactions
+	BlockNumber       *big.Int
+	MinTimestamp      uint64
+	MaxTimestamp      uint64
+	RevertingTxHashes []common.Hash
+	Hash              common.Hash
+}
+
+func (b *MevBundle) RevertingHash(hash common.Hash) bool {
+	for _, revHash := range b.RevertingTxHashes {
+		if revHash == hash {
+			return true
+		}
+	}
+	return false
+}
+
+type SimulatedBundle struct {
+	MevGasPrice       *big.Int
+	TotalEth          *big.Int
+	EthSentToCoinbase *big.Int
+	TotalGasUsed      uint64
+	OriginalBundle    MevBundle
+}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 67e5b3cce3..47a6bc638e 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -320,11 +320,11 @@ func testTransactionPriceNonceSort(t *testing.T, baseFee *big.Int) {
 		expectedCount += count
 	}
 	// Sort the transactions and cross check the nonce ordering
-	txset := NewTransactionsByPriceAndNonce(signer, groups, baseFee)
+	txset := NewTransactionsByPriceAndNonce(signer, groups, nil, baseFee)
 
 	txs := Transactions{}
 	for tx := txset.Peek(); tx != nil; tx = txset.Peek() {
-		txs = append(txs, tx)
+		txs = append(txs, tx.Tx)
 		txset.Shift()
 	}
 	if len(txs) != expectedCount {
@@ -377,11 +377,11 @@ func TestTransactionTimeSort(t *testing.T) {
 		groups[addr] = append(groups[addr], tx)
 	}
 	// Sort the transactions and cross check the nonce ordering
-	txset := NewTransactionsByPriceAndNonce(signer, groups, nil)
+	txset := NewTransactionsByPriceAndNonce(signer, groups, nil, nil)
 
 	txs := Transactions{}
 	for tx := txset.Peek(); tx != nil; tx = txset.Peek() {
-		txs = append(txs, tx)
+		txs = append(txs, tx.Tx)
 		txset.Shift()
 	}
 	if len(txs) != len(keys) {
diff --git a/core/utils/gas_limit.go b/core/utils/gas_limit.go
new file mode 100644
index 0000000000..8be0a6a43e
--- /dev/null
+++ b/core/utils/gas_limit.go
@@ -0,0 +1,29 @@
+package utils
+
+import "github.com/ethereum/go-ethereum/params"
+
+// CalcGasLimit computes the gas limit of the next block after parent. It aims
+// to keep the baseline gas close to the provided target, and increase it towards
+// the target if the baseline gas is lower.
+func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 {
+	delta := parentGasLimit/params.GasLimitBoundDivisor - 1
+	limit := parentGasLimit
+	if desiredLimit < params.MinGasLimit {
+		desiredLimit = params.MinGasLimit
+	}
+	// If we're outside our allowed gas range, we try to hone towards them
+	if limit < desiredLimit {
+		limit = parentGasLimit + delta
+		if limit > desiredLimit {
+			limit = desiredLimit
+		}
+		return limit
+	}
+	if limit > desiredLimit {
+		limit = parentGasLimit - delta
+		if limit < desiredLimit {
+			limit = desiredLimit
+		}
+	}
+	return limit
+}
diff --git a/docs/builder/builder-diagram.png b/docs/builder/builder-diagram.png
new file mode 100644
index 0000000000..d1b4e66f75
Binary files /dev/null and b/docs/builder/builder-diagram.png differ
diff --git a/eth/api_backend.go b/eth/api_backend.go
index 00ecacc31d..709d3a808f 100644
--- a/eth/api_backend.go
+++ b/eth/api_backend.go
@@ -246,8 +246,16 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri
 	return b.eth.BlockChain().SubscribeLogsEvent(ch)
 }
 
-func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
-	return b.eth.txPool.AddLocal(signedTx)
+func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction, private bool) error {
+	if private {
+		return b.eth.txPool.AddPrivateRemote(signedTx)
+	} else {
+		return b.eth.txPool.AddLocal(signedTx)
+	}
+}
+
+func (b *EthAPIBackend) SendBundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error {
+	return b.eth.txPool.AddMevBundle(txs, big.NewInt(blockNumber.Int64()), minTimestamp, maxTimestamp, revertingTxHashes)
 }
 
 func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) {
diff --git a/eth/backend.go b/eth/backend.go
index 7782076363..fbb75d89b6 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -297,7 +297,7 @@ func makeExtraData(extra []byte) []byte {
 // APIs return the collection of RPC services the ethereum package offers.
 // NOTE, some of these services probably need to be moved to somewhere else.
 func (s *Ethereum) APIs() []rpc.API {
-	apis := ethapi.GetAPIs(s.APIBackend)
+	apis := ethapi.GetAPIs(s.APIBackend, s.BlockChain())
 
 	// Append any APIs exposed explicitly by the consensus engine
 	apis = append(apis, s.engine.APIs(s.BlockChain())...)
diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go
new file mode 100644
index 0000000000..fb4d0582da
--- /dev/null
+++ b/eth/block-validation/api.go
@@ -0,0 +1,173 @@
+package blockvalidation
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"math/big"
+	"os"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/core/vm"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/eth/tracers/logger"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/rpc"
+
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+)
+
+type BlacklistedAddresses []common.Address
+
+type AccessVerifier struct {
+	blacklistedAddresses map[common.Address]struct{}
+}
+
+func (a *AccessVerifier) verifyTraces(tracer *logger.AccessListTracer) error {
+	log.Trace("x", "tracer.AccessList()", tracer.AccessList())
+	for _, accessTuple := range tracer.AccessList() {
+		// TODO: should we ignore common.Address{}?
+		if _, found := a.blacklistedAddresses[accessTuple.Address]; found {
+			log.Info("bundle accesses blacklisted address", "address", accessTuple.Address)
+			return fmt.Errorf("blacklisted address %s in execution trace", accessTuple.Address.String())
+		}
+	}
+
+	return nil
+}
+
+func (a *AccessVerifier) isBlacklisted(addr common.Address) error {
+	if _, present := a.blacklistedAddresses[addr]; present {
+		return fmt.Errorf("transaction from blacklisted address %s", addr.String())
+	}
+	return nil
+}
+
+func (a *AccessVerifier) verifyTransactions(signer types.Signer, txs types.Transactions) error {
+	for _, tx := range txs {
+		from, err := signer.Sender(tx)
+		if err == nil {
+			if _, present := a.blacklistedAddresses[from]; present {
+				return fmt.Errorf("transaction from blacklisted address %s", from.String())
+			}
+		}
+		to := tx.To()
+		if to != nil {
+			if _, present := a.blacklistedAddresses[*to]; present {
+				return fmt.Errorf("transaction to blacklisted address %s", to.String())
+			}
+		}
+	}
+	return nil
+}
+
+func NewAccessVerifierFromFile(path string) (*AccessVerifier, error) {
+	bytes, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	var ba BlacklistedAddresses
+	if err := json.Unmarshal(bytes, &ba); err != nil {
+		return nil, err
+	}
+
+	blacklistedAddresses := make(map[common.Address]struct{}, len(ba))
+	for _, address := range ba {
+		blacklistedAddresses[address] = struct{}{}
+	}
+
+	return &AccessVerifier{
+		blacklistedAddresses: blacklistedAddresses,
+	}, nil
+}
+
+// Register adds catalyst APIs to the full node.
+func Register(stack *node.Node, backend *eth.Ethereum) error {
+	stack.RegisterAPIs([]rpc.API{
+		{
+			Namespace: "flashbots",
+			Service:   BlockValidationAPI(backend),
+		},
+	})
+	return nil
+}
+
+type BlockValidationAPI struct {
+	eth            *eth.Ethereum
+	accessVerifier *AccessVerifier
+}
+
+type BuilderBlockValidationRequest struct {
+	boostTypes.BuilderSubmitBlockRequest
+	RegisteredGasLimit uint64 `json:"registered_gas_limit,string"`
+}
+
+func (api *BlockValidationAPI) ValidateBuilderSubmissionV1(params *BuilderBlockValidationRequest) error {
+	// TODO: fuzztest, make sure the validation is sound
+	// TODO: handle context!
+
+	if params.ExecutionPayload == nil {
+		return errors.New("nil execution payload")
+	}
+	payload := params.ExecutionPayload
+	block, err := beacon.ExecutionPayloadToBlock(payload)
+	if err != nil {
+		return err
+	}
+
+	if params.Message.ParentHash != boostTypes.Hash(block.ParentHash()) {
+		return fmt.Errorf("incorrect ParentHash %s, expected %s", params.Message.ParentHash.String(), block.ParentHash().String())
+	}
+
+	if params.Message.BlockHash != boostTypes.Hash(block.Hash()) {
+		return fmt.Errorf("incorrect BlockHash %s, expected %s", params.Message.BlockHash.String(), block.Hash().String())
+	}
+
+	if params.Message.GasLimit != block.GasLimit() {
+		return fmt.Errorf("incorrect GasLimit %d, expected %d", params.Message.GasLimit, block.GasLimit())
+	}
+
+	if params.Message.GasUsed != block.GasUsed() {
+		return fmt.Errorf("incorrect GasUsed %d, expected %d", params.Message.GasUsed, block.GasUsed())
+	}
+
+	feeRecipient := common.BytesToAddress(params.Message.ProposerFeeRecipient[:])
+	expectedProfit := params.Message.Value.BigInt()
+
+	var vmconfig vm.Config
+	var tracer *logger.AccessListTracer = nil
+	if api.accessVerifier != nil {
+		if err := api.accessVerifier.isBlacklisted(block.Coinbase()); err != nil {
+			return err
+		}
+		if err := api.accessVerifier.isBlacklisted(feeRecipient); err != nil {
+			return err
+		}
+		if err := api.accessVerifier.verifyTransactions(types.LatestSigner(api.eth.BlockChain().Config()), block.Transactions()); err != nil {
+			return err
+		}
+		isPostMerge := true // the call is PoS-native
+		precompiles := vm.ActivePrecompiles(api.eth.APIBackend.ChainConfig().Rules(new(big.Int).SetUint64(params.ExecutionPayload.BlockNumber), isPostMerge))
+		tracer = logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, precompiles)
+		vmconfig = vm.Config{Tracer: tracer, Debug: true}
+	}
+
+	err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig)
+	if err != nil {
+		log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err)
+		return err
+	}
+
+	if api.accessVerifier != nil && tracer != nil {
+		if err := api.accessVerifier.verifyTraces(tracer); err != nil {
+			return err
+		}
+	}
+
+	log.Info("validated block", "hash", block.Hash(), "number", block.NumberU64(), "parentHash", block.ParentHash())
+	return nil
+}
diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go
new file mode 100644
index 0000000000..fca96454e8
--- /dev/null
+++ b/eth/block-validation/api_test.go
@@ -0,0 +1,301 @@
+package blockvalidation
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"math/big"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/consensus/misc"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/beacon"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/eth/downloader"
+	"github.com/ethereum/go-ethereum/eth/ethconfig"
+	"github.com/ethereum/go-ethereum/eth/tracers/logger"
+	"github.com/ethereum/go-ethereum/node"
+	"github.com/ethereum/go-ethereum/p2p"
+	"github.com/ethereum/go-ethereum/params"
+	"github.com/stretchr/testify/require"
+
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+)
+
+/* Based on catalyst API tests */
+
+var (
+	// testKey is a private key to use for funding a tester account.
+	testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+
+	// testAddr is the Ethereum address of the tester account.
+	testAddr = crypto.PubkeyToAddress(testKey.PublicKey)
+
+	testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0")
+	testValidatorAddr   = crypto.PubkeyToAddress(testValidatorKey.PublicKey)
+
+	testBalance = big.NewInt(2e18)
+)
+
+func TestValidateBuilderSubmissionV1(t *testing.T) {
+	genesis, preMergeBlocks := generatePreMergeChain(20)
+	os.Setenv("BUILDER_TX_SIGNING_KEY", "0x28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0")
+	n, ethservice := startEthService(t, genesis, preMergeBlocks)
+	ethservice.Merger().ReachTTD()
+	defer n.Close()
+
+	api := NewBlockValidationAPI(ethservice, nil)
+	parent := preMergeBlocks[len(preMergeBlocks)-1]
+
+	api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr)
+
+	// This EVM code generates a log when the contract is created.
+	logCode := common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00")
+
+	statedb, _ := ethservice.BlockChain().StateAt(parent.Root())
+	nonce := statedb.GetNonce(testAddr)
+
+	tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	ethservice.TxPool().AddLocal(tx1)
+
+	cc, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	ethservice.TxPool().AddLocal(cc)
+
+	baseFee := misc.CalcBaseFee(params.AllEthashProtocolChanges, preMergeBlocks[len(preMergeBlocks)-1].Header())
+	tx2, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey)
+	ethservice.TxPool().AddLocal(tx2)
+
+	execData, err := assembleBlock(api, parent.Hash(), &beacon.PayloadAttributesV1{
+		Timestamp:             parent.Time() + 5,
+		SuggestedFeeRecipient: testValidatorAddr,
+	})
+	require.EqualValues(t, len(execData.Transactions), 4)
+	require.NoError(t, err)
+
+	payload, err := ExecutableDataToExecutionPayload(execData)
+	require.NoError(t, err)
+
+	proposerAddr := boostTypes.Address{}
+	proposerAddr.FromSlice(testValidatorAddr[:])
+
+	blockRequest := &BuilderBlockValidationRequest{
+		BuilderSubmitBlockRequest: boostTypes.BuilderSubmitBlockRequest{
+			Signature: boostTypes.Signature{},
+			Message: &boostTypes.BidTrace{
+				ParentHash:           boostTypes.Hash(execData.ParentHash),
+				BlockHash:            boostTypes.Hash(execData.BlockHash),
+				ProposerFeeRecipient: proposerAddr,
+				GasLimit:             execData.GasLimit,
+				GasUsed:              execData.GasUsed,
+			},
+			ExecutionPayload: payload,
+		},
+		RegisteredGasLimit: execData.GasLimit,
+	}
+
+	blockRequest.Message.Value = boostTypes.IntToU256(190526394825529)
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "inaccurate payment")
+	blockRequest.Message.Value = boostTypes.IntToU256(149830884438530)
+	require.NoError(t, api.ValidateBuilderSubmissionV1(blockRequest))
+
+	blockRequest.Message.GasLimit += 1
+	blockRequest.ExecutionPayload.GasLimit += 1
+
+	oldHash := blockRequest.Message.BlockHash
+	copy(blockRequest.Message.BlockHash[:], hexutil.MustDecode("0x56cbdd508966f89cfb6ba16535e3676b59ae3ac3774478b631466bc99c1033c9")[:32])
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "incorrect gas limit set")
+
+	blockRequest.Message.GasLimit -= 1
+	blockRequest.ExecutionPayload.GasLimit -= 1
+	blockRequest.Message.BlockHash = oldHash
+
+	// TODO: test with contract calling blacklisted address
+	// Test tx from blacklisted address
+	api.accessVerifier = &AccessVerifier{
+		blacklistedAddresses: map[common.Address]struct{}{
+			testAddr: struct{}{},
+		},
+	}
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7")
+
+	// Test tx to blacklisted address
+	api.accessVerifier = &AccessVerifier{
+		blacklistedAddresses: map[common.Address]struct{}{
+			common.Address{0x16}: struct{}{},
+		},
+	}
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "transaction to blacklisted address 0x1600000000000000000000000000000000000000")
+
+	api.accessVerifier = nil
+
+	blockRequest.Message.GasUsed = 10
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "incorrect GasUsed 10, expected 119990")
+	blockRequest.Message.GasUsed = execData.GasUsed
+
+	newTestKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f290")
+	invalidTx, err := types.SignTx(types.NewTransaction(0, common.Address{}, new(big.Int).Mul(big.NewInt(2e18), big.NewInt(10)), 19000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), newTestKey)
+	require.NoError(t, err)
+
+	txData, err := invalidTx.MarshalBinary()
+	require.NoError(t, err)
+	execData.Transactions = append(execData.Transactions, txData)
+
+	invalidPayload, err := ExecutableDataToExecutionPayload(execData)
+	require.NoError(t, err)
+	invalidPayload.GasUsed = execData.GasUsed
+	invalidPayload.LogsBloom = boostTypes.Bloom{}
+	copy(invalidPayload.ReceiptsRoot[:], hexutil.MustDecode("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")[:32])
+	blockRequest.ExecutionPayload = invalidPayload
+	copy(blockRequest.Message.BlockHash[:], hexutil.MustDecode("0x595cba7ab70a18b7e11ae7541661cb6692909a0acd3eba3f1cf6ae694f85a8bd")[:32])
+	require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value")
+}
+
+func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) {
+	db := rawdb.NewMemoryDatabase()
+	config := params.AllEthashProtocolChanges
+	genesis := &core.Genesis{
+		Config:     config,
+		Alloc:      core.GenesisAlloc{testAddr: {Balance: testBalance}},
+		ExtraData:  []byte("test genesis"),
+		Timestamp:  9000,
+		BaseFee:    big.NewInt(params.InitialBaseFee),
+		Difficulty: big.NewInt(0),
+	}
+	testNonce := uint64(0)
+	generate := func(i int, g *core.BlockGen) {
+		g.OffsetTime(5)
+		g.SetExtra([]byte("test"))
+		tx, _ := types.SignTx(types.NewTransaction(testNonce, common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(config), testKey)
+		g.AddTx(tx)
+		testNonce++
+	}
+	gblock := genesis.MustCommit(db)
+	engine := ethash.NewFaker()
+	blocks, _ := core.GenerateChain(config, gblock, engine, db, n, generate)
+	totalDifficulty := big.NewInt(0)
+	for _, b := range blocks {
+		totalDifficulty.Add(totalDifficulty, b.Difficulty())
+	}
+	config.TerminalTotalDifficulty = totalDifficulty
+	return genesis, blocks
+}
+
+// startEthService creates a full node instance for testing.
+func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) {
+	t.Helper()
+
+	n, err := node.New(&node.Config{
+		P2P: p2p.Config{
+			ListenAddr:  "0.0.0.0:0",
+			NoDiscovery: true,
+			MaxPeers:    25,
+		}})
+	if err != nil {
+		t.Fatal("can't create node:", err)
+	}
+
+	ethcfg := &ethconfig.Config{Genesis: genesis, Ethash: ethash.Config{PowMode: ethash.ModeFake}, SyncMode: downloader.SnapSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256}
+	ethservice, err := eth.New(n, ethcfg)
+	if err != nil {
+		t.Fatal("can't create eth service:", err)
+	}
+	if err := n.Start(); err != nil {
+		t.Fatal("can't start node:", err)
+	}
+	if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil {
+		n.Close()
+		t.Fatal("can't import test blocks:", err)
+	}
+	time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event
+
+	ethservice.SetEtherbase(testAddr)
+	ethservice.SetSynced()
+	return n, ethservice
+}
+
+func assembleBlock(api *BlockValidationAPI, parentHash common.Hash, params *beacon.PayloadAttributesV1) (*beacon.ExecutableDataV1, error) {
+	block, err := api.eth.Miner().GetSealingBlockSync(parentHash, params.Timestamp, params.SuggestedFeeRecipient, 0, params.Random, false, nil)
+	if err != nil {
+		return nil, err
+	}
+	return beacon.BlockToExecutableData(block), nil
+}
+
+func TestBlacklistLoad(t *testing.T) {
+	file, err := os.CreateTemp(".", "blacklist")
+	require.NoError(t, err)
+	defer os.Remove(file.Name())
+
+	av, err := NewAccessVerifierFromFile(file.Name())
+	require.Error(t, err)
+	require.Nil(t, av)
+
+	ba := BlacklistedAddresses{common.Address{0x13}, common.Address{0x14}}
+	bytes, err := json.MarshalIndent(ba, "", " ")
+	require.NoError(t, err)
+	err = ioutil.WriteFile(file.Name(), bytes, 0644)
+	require.NoError(t, err)
+
+	av, err = NewAccessVerifierFromFile(file.Name())
+	require.NoError(t, err)
+	require.NotNil(t, av)
+	require.EqualValues(t, av.blacklistedAddresses, map[common.Address]struct{}{
+		common.Address{0x13}: struct{}{},
+		common.Address{0x14}: struct{}{},
+	})
+
+	require.NoError(t, av.verifyTraces(logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, nil)))
+
+	acl := types.AccessList{
+		types.AccessTuple{
+			Address: common.Address{0x14},
+		},
+	}
+	tracer := logger.NewAccessListTracer(acl, common.Address{}, common.Address{}, nil)
+	require.ErrorContains(t, av.verifyTraces(tracer), "blacklisted address 0x1400000000000000000000000000000000000000 in execution trace")
+
+	acl = types.AccessList{
+		types.AccessTuple{
+			Address: common.Address{0x15},
+		},
+	}
+	tracer = logger.NewAccessListTracer(acl, common.Address{}, common.Address{}, nil)
+	require.NoError(t, av.verifyTraces(tracer))
+}
+
+func ExecutableDataToExecutionPayload(data *beacon.ExecutableDataV1) (*boostTypes.ExecutionPayload, error) {
+	transactionData := make([]hexutil.Bytes, len(data.Transactions))
+	for i, tx := range data.Transactions {
+		transactionData[i] = hexutil.Bytes(tx)
+	}
+
+	baseFeePerGas := new(boostTypes.U256Str)
+	err := baseFeePerGas.FromBig(data.BaseFeePerGas)
+	if err != nil {
+		return nil, err
+	}
+
+	return &boostTypes.ExecutionPayload{
+		ParentHash:    [32]byte(data.ParentHash),
+		FeeRecipient:  [20]byte(data.FeeRecipient),
+		StateRoot:     [32]byte(data.StateRoot),
+		ReceiptsRoot:  [32]byte(data.ReceiptsRoot),
+		LogsBloom:     boostTypes.Bloom(types.BytesToBloom(data.LogsBloom)),
+		Random:        [32]byte(data.Random),
+		BlockNumber:   data.Number,
+		GasLimit:      data.GasLimit,
+		GasUsed:       data.GasUsed,
+		Timestamp:     data.Timestamp,
+		ExtraData:     data.ExtraData,
+		BaseFeePerGas: *baseFeePerGas,
+		BlockHash:     [32]byte(data.BlockHash),
+		Transactions:  transactionData,
+	}, nil
+}
diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go
index b159f34e64..c7fbbacb6f 100644
--- a/eth/catalyst/api.go
+++ b/eth/catalyst/api.go
@@ -277,14 +277,14 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV1(update beacon.ForkchoiceStateV1, pa
 	// might replace it arbitrarily many times in between.
 	if payloadAttributes != nil {
 		// Create an empty block first which can be used as a fallback
-		empty, err := api.eth.Miner().GetSealingBlockSync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, true)
+		empty, err := api.eth.Miner().GetSealingBlockSync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.GasLimit, payloadAttributes.Random, true, nil)
 		if err != nil {
 			log.Error("Failed to create empty sealing payload", "err", err)
 			return valid(nil), beacon.InvalidPayloadAttributes.With(err)
 		}
 		// Send a request to generate a full block in the background.
 		// The result can be obtained via the returned channel.
-		resCh, err := api.eth.Miner().GetSealingBlockAsync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.Random, false)
+		resCh, err := api.eth.Miner().GetSealingBlockAsync(update.HeadBlockHash, payloadAttributes.Timestamp, payloadAttributes.SuggestedFeeRecipient, payloadAttributes.GasLimit, payloadAttributes.Random, false, nil)
 		if err != nil {
 			log.Error("Failed to create async sealing payload", "err", err)
 			return valid(nil), beacon.InvalidPayloadAttributes.With(err)
diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go
index 0d945993eb..d4d475742e 100644
--- a/eth/catalyst/api_test.go
+++ b/eth/catalyst/api_test.go
@@ -603,7 +603,7 @@ func TestNewPayloadOnInvalidChain(t *testing.T) {
 }
 
 func assembleBlock(api *ConsensusAPI, parentHash common.Hash, params *beacon.PayloadAttributesV1) (*beacon.ExecutableDataV1, error) {
-	block, err := api.eth.Miner().GetSealingBlockSync(parentHash, params.Timestamp, params.SuggestedFeeRecipient, params.Random, false)
+	block, err := api.eth.Miner().GetSealingBlockSync(parentHash, params.Timestamp, params.SuggestedFeeRecipient, 0, params.Random, false, nil)
 	if err != nil {
 		return nil, err
 	}
@@ -846,7 +846,7 @@ func TestNewPayloadOnInvalidTerminalBlock(t *testing.T) {
 		Random:                crypto.Keccak256Hash([]byte{byte(1)}),
 		SuggestedFeeRecipient: parent.Coinbase(),
 	}
-	empty, err := api.eth.Miner().GetSealingBlockSync(parent.Hash(), params.Timestamp, params.SuggestedFeeRecipient, params.Random, true)
+	empty, err := api.eth.Miner().GetSealingBlockSync(parent.Hash(), params.Timestamp, params.SuggestedFeeRecipient, 0, params.Random, true, nil)
 	if err != nil {
 		t.Fatalf("error preparing payload, err=%v", err)
 	}
diff --git a/eth/catalyst/queue.go b/eth/catalyst/queue.go
index ff8edc1201..2995ff4860 100644
--- a/eth/catalyst/queue.go
+++ b/eth/catalyst/queue.go
@@ -47,7 +47,7 @@ type payload struct {
 
 // resolve extracts the generated full block from the given channel if possible
 // or fallback to empty block as an alternative.
-func (req *payload) resolve() *beacon.ExecutableDataV1 {
+func (req *payload) resolve() (*beacon.ExecutableDataV1, *types.Block) {
 	// this function can be called concurrently, prevent any
 	// concurrency issue in the first place.
 	req.lock.Lock()
@@ -71,9 +71,9 @@ func (req *payload) resolve() *beacon.ExecutableDataV1 {
 	}
 
 	if req.block != nil {
-		return beacon.BlockToExecutableData(req.block)
+		return beacon.BlockToExecutableData(req.block), req.block
 	}
-	return beacon.BlockToExecutableData(req.empty)
+	return beacon.BlockToExecutableData(req.empty), req.empty
 }
 
 // payloadQueueItem represents an id->payload tuple to store until it's retrieved
@@ -120,7 +120,8 @@ func (q *payloadQueue) get(id beacon.PayloadID) *beacon.ExecutableDataV1 {
 			return nil // no more items
 		}
 		if item.id == id {
-			return item.data.resolve()
+			data, _ := item.data.resolve()
+			return data
 		}
 	}
 	return nil
diff --git a/eth/handler.go b/eth/handler.go
index 4224a9f33a..3539856c01 100644
--- a/eth/handler.go
+++ b/eth/handler.go
@@ -72,6 +72,10 @@ type txPool interface {
 	// SubscribeNewTxsEvent should return an event subscription of
 	// NewTxsEvent and send events to the given channel.
 	SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription
+
+	// IsPrivateTxHash indicates if the transaction hash should not
+	// be broadcast on public channels
+	IsPrivateTxHash(hash common.Hash) bool
 }
 
 // handlerConfig is the collection of initialization parameters to create a full
diff --git a/eth/handler_test.go b/eth/handler_test.go
index d967b6df93..382bed491b 100644
--- a/eth/handler_test.go
+++ b/eth/handler_test.go
@@ -113,6 +113,11 @@ func (p *testTxPool) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subs
 	return p.txFeed.Subscribe(ch)
 }
 
+// IsPrivateTxHash always returns false in tests
+func (p *testTxPool) IsPrivateTxHash(hash common.Hash) bool {
+	return false
+}
+
 // testHandler is a live implementation of the Ethereum protocol handler, just
 // preinitialized with some sane testing defaults and the transaction pool mocked
 // out.
diff --git a/eth/protocols/eth/handler.go b/eth/protocols/eth/handler.go
index 3a0b21c30b..b655e1e6d0 100644
--- a/eth/protocols/eth/handler.go
+++ b/eth/protocols/eth/handler.go
@@ -91,6 +91,10 @@ type Backend interface {
 type TxPool interface {
 	// Get retrieves the transaction from the local txpool with the given hash.
 	Get(hash common.Hash) *types.Transaction
+
+	// IsPrivateTxHash indicates if the transaction hash should not
+	// be broadcast on public channels
+	IsPrivateTxHash(hash common.Hash) bool
 }
 
 // MakeProtocols constructs the P2P protocol definitions for `eth`.
diff --git a/eth/sync.go b/eth/sync.go
index 8fd86b578c..2edb7a731c 100644
--- a/eth/sync.go
+++ b/eth/sync.go
@@ -46,7 +46,12 @@ func (h *handler) syncTransactions(p *eth.Peer) {
 	var txs types.Transactions
 	pending := h.txpool.Pending(false)
 	for _, batch := range pending {
-		txs = append(txs, batch...)
+		for _, tx := range batch {
+			// don't share any transactions marked as private
+			if !h.txpool.IsPrivateTxHash(tx.Hash()) {
+				txs = append(txs, tx)
+			}
+		}
 	}
 	if len(txs) == 0 {
 		return
diff --git a/eth/tracers/logger/account_touch_tracer.go b/eth/tracers/logger/account_touch_tracer.go
new file mode 100644
index 0000000000..4c24779637
--- /dev/null
+++ b/eth/tracers/logger/account_touch_tracer.go
@@ -0,0 +1,77 @@
+// Copyright 2022 flashbots
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package logger
+
+import (
+	"math/big"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/vm"
+)
+
+type AccountTouchTracer struct {
+	touched map[common.Address]struct{}
+}
+
+// NewAccountTouchTracer creates new AccountTouchTracer
+// that collect all addresses touched in the given tx
+// including tx sender and tx.to from the top level call
+func NewAccountTouchTracer() *AccountTouchTracer {
+	return &AccountTouchTracer{
+		touched: map[common.Address]struct{}{},
+	}
+}
+
+func (t *AccountTouchTracer) TouchedAddresses() []common.Address {
+	result := make([]common.Address, 0, len(t.touched))
+
+	for address := range t.touched {
+		result = append(result, address)
+	}
+	return result
+}
+
+func (t *AccountTouchTracer) CaptureTxStart(uint64) {}
+
+func (t *AccountTouchTracer) CaptureTxEnd(uint64) {}
+
+func (t *AccountTouchTracer) CaptureStart(_ *vm.EVM, from common.Address, to common.Address, _ bool, _ []byte, _ uint64, _ *big.Int) {
+	t.touched[from] = struct{}{}
+	t.touched[to] = struct{}{}
+}
+
+func (t *AccountTouchTracer) CaptureEnd([]byte, uint64, time.Duration, error) {}
+
+func (t *AccountTouchTracer) CaptureEnter(_ vm.OpCode, _ common.Address, to common.Address, _ []byte, _ uint64, _ *big.Int) {
+	t.touched[to] = struct{}{}
+}
+
+func (t *AccountTouchTracer) CaptureExit([]byte, uint64, error) {}
+
+func (t *AccountTouchTracer) CaptureState(_ uint64, op vm.OpCode, _, _ uint64, scope *vm.ScopeContext, _ []byte, _ int, _ error) {
+	stack := scope.Stack
+	stackData := stack.Data()
+	stackLen := len(stackData)
+	if (op == vm.EXTCODECOPY || op == vm.EXTCODEHASH || op == vm.EXTCODESIZE || op == vm.BALANCE || op == vm.SELFDESTRUCT) && stackLen >= 1 {
+		addr := common.Address(stackData[stackLen-1].Bytes20())
+		t.touched[addr] = struct{}{}
+	}
+}
+
+func (t *AccountTouchTracer) CaptureFault(uint64, vm.OpCode, uint64, uint64, *vm.ScopeContext, int, error) {
+}
diff --git a/flashbotsextra/cmd/bundle_fetcher.go b/flashbotsextra/cmd/bundle_fetcher.go
new file mode 100644
index 0000000000..ff6e78be93
--- /dev/null
+++ b/flashbotsextra/cmd/bundle_fetcher.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"os"
+	"time"
+
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/flashbotsextra"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+func main() {
+	// Test bundle fetcher
+	log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StreamHandler(os.Stderr, log.TerminalFormat(true))))
+	mevBundleCh := make(chan []types.MevBundle)
+	blockNumCh := make(chan int64)
+	db, err := flashbotsextra.NewDatabaseService("postgres://postgres:postgres@localhost:5432/test?sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+	bundleFetcher := flashbotsextra.NewBundleFetcher(nil, db, blockNumCh, mevBundleCh, false)
+
+	go bundleFetcher.Run()
+	log.Info("waiting for mev bundles")
+	go func() {
+		blockNum := []int64{15232009, 15232008, 15232010}
+		for _, num := range blockNum {
+			<-time.After(time.Second)
+			blockNumCh <- num
+		}
+	}()
+	for bundles := range mevBundleCh {
+		for _, bundle := range bundles {
+			log.Info("bundle info", "blockNum", bundle.BlockNumber, "txsLength", len(bundle.Txs))
+		}
+	}
+}
diff --git a/flashbotsextra/database.go b/flashbotsextra/database.go
new file mode 100644
index 0000000000..1375e9214a
--- /dev/null
+++ b/flashbotsextra/database.go
@@ -0,0 +1,280 @@
+package flashbotsextra
+
+import (
+	"context"
+	"database/sql"
+	"math/big"
+	"time"
+
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/jmoiron/sqlx"
+	_ "github.com/lib/pq"
+)
+
+const (
+	highPrioLimitSize = 500
+	lowPrioLimitSize  = 100
+)
+
+type IDatabaseService interface {
+	ConsumeBuiltBlock(block *types.Block, OrdersClosedAt time.Time, sealedAt time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, bidTrace *boostTypes.BidTrace)
+	GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error)
+}
+
+type NilDbService struct{}
+
+func (NilDbService) ConsumeBuiltBlock(block *types.Block, _ time.Time, _ time.Time, _ []types.SimulatedBundle, _ []types.SimulatedBundle, _ *boostTypes.BidTrace) {
+}
+
+func (NilDbService) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) {
+	return []DbBundle{}, nil
+}
+
+type DatabaseService struct {
+	db *sqlx.DB
+
+	insertBuiltBlockStmt    *sqlx.NamedStmt
+	insertMissingBundleStmt *sqlx.NamedStmt
+	fetchPrioBundlesStmt    *sqlx.NamedStmt
+}
+
+func NewDatabaseService(postgresDSN string) (*DatabaseService, error) {
+	db, err := sqlx.Connect("postgres", postgresDSN)
+	if err != nil {
+		return nil, err
+	}
+
+	insertBuiltBlockStmt, err := db.PrepareNamed("insert into built_blocks (block_number, profit, slot, hash, gas_limit, gas_used, base_fee, parent_hash, proposer_pubkey, proposer_fee_recipient, builder_pubkey, timestamp, timestamp_datetime, orders_closed_at, sealed_at) values (:block_number, :profit, :slot, :hash, :gas_limit, :gas_used, :base_fee, :parent_hash, :proposer_pubkey, :proposer_fee_recipient, :builder_pubkey, :timestamp, to_timestamp(:timestamp), :orders_closed_at, :sealed_at) returning block_id")
+	if err != nil {
+		return nil, err
+	}
+
+	insertMissingBundleStmt, err := db.PrepareNamed("insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id")
+	if err != nil {
+		return nil, err
+	}
+
+	fetchPrioBundlesStmt, err := db.PrepareNamed("select bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase from bundles where is_high_prio = :is_high_prio and coinbase_diff*1e18/total_gas_used > 1000000000 and param_block_number = :param_block_number order by coinbase_diff/total_gas_used DESC limit :limit")
+	if err != nil {
+		return nil, err
+	}
+	return &DatabaseService{
+		db:                      db,
+		insertBuiltBlockStmt:    insertBuiltBlockStmt,
+		insertMissingBundleStmt: insertMissingBundleStmt,
+		fetchPrioBundlesStmt:    fetchPrioBundlesStmt,
+	}, nil
+}
+
+func Min(l int, r int) int {
+	if l < r {
+		return l
+	}
+	return r
+}
+
+func (ds *DatabaseService) getBundleIds(ctx context.Context, blockNumber uint64, bundles []types.SimulatedBundle) (map[string]uint64, error) {
+	if len(bundles) == 0 {
+		return nil, nil
+	}
+
+	bundleIdsMap := make(map[string]uint64, len(bundles))
+
+	// Batch by 500
+	requestsToMake := [][]string{make([]string, 0, Min(500, len(bundles)))}
+	cRequestInd := 0
+	for i, bundle := range bundles {
+		if i != 0 && i%500 == 0 {
+			cRequestInd += 1
+			requestsToMake = append(requestsToMake, make([]string, 0, Min(500, len(bundles)-i)))
+		}
+		requestsToMake[cRequestInd] = append(requestsToMake[cRequestInd], bundle.OriginalBundle.Hash.String())
+	}
+
+	for _, request := range requestsToMake {
+		query, args, err := sqlx.In("select id, bundle_hash from bundles where param_block_number = ? and bundle_hash in (?)", blockNumber, request)
+		if err != nil {
+			return nil, err
+		}
+		query = ds.db.Rebind(query)
+
+		queryRes := []struct {
+			Id         uint64 `db:"id"`
+			BundleHash string `db:"bundle_hash"`
+		}{}
+		err = ds.db.SelectContext(ctx, &queryRes, query, args...)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, row := range queryRes {
+			bundleIdsMap[row.BundleHash] = row.Id
+		}
+	}
+
+	return bundleIdsMap, nil
+}
+
+// TODO: cache locally for current block!
+func (ds *DatabaseService) getBundleIdsAndInsertMissingBundles(ctx context.Context, blockNumber uint64, bundles []types.SimulatedBundle) (map[string]uint64, error) {
+	bundleIdsMap, err := ds.getBundleIds(ctx, blockNumber, bundles)
+	if err != nil {
+		return nil, err
+	}
+
+	toRetry := []types.SimulatedBundle{}
+	for _, bundle := range bundles {
+		bundleHashString := bundle.OriginalBundle.Hash.String()
+		if _, found := bundleIdsMap[bundleHashString]; found {
+			continue
+		}
+
+		var bundleId uint64
+		missingBundleData := SimulatedBundleToDbBundle(&bundle)                        // nolint: gosec
+		err = ds.insertMissingBundleStmt.GetContext(ctx, &bundleId, missingBundleData) // not using the tx as it relies on the unique constraint!
+		if err == nil {
+			bundleIdsMap[bundleHashString] = bundleId
+		} else if err == sql.ErrNoRows /* conflict, someone else inserted the bundle before we could */ {
+			toRetry = append(toRetry, bundle)
+		} else {
+			log.Error("could not insert missing bundle", "err", err)
+		}
+	}
+
+	retriedBundleIds, err := ds.getBundleIds(ctx, blockNumber, toRetry)
+	if err != nil {
+		return nil, err
+	}
+
+	for hash, id := range retriedBundleIds {
+		bundleIdsMap[hash] = id
+	}
+
+	return bundleIdsMap, nil
+}
+
+func (ds *DatabaseService) insertBuildBlock(tx *sqlx.Tx, ctx context.Context, block *types.Block, bidTrace *boostTypes.BidTrace, ordersClosedAt time.Time, sealedAt time.Time) (uint64, error) {
+	blockData := BuiltBlock{
+		BlockNumber:          block.NumberU64(),
+		Profit:               new(big.Rat).SetFrac(block.Profit, big.NewInt(1e18)).FloatString(18),
+		Slot:                 bidTrace.Slot,
+		Hash:                 block.Hash().String(),
+		GasLimit:             block.GasLimit(),
+		GasUsed:              block.GasUsed(),
+		BaseFee:              block.BaseFee().Uint64(),
+		ParentHash:           block.ParentHash().String(),
+		ProposerPubkey:       bidTrace.ProposerPubkey.String(),
+		ProposerFeeRecipient: bidTrace.ProposerFeeRecipient.String(),
+		BuilderPubkey:        bidTrace.BuilderPubkey.String(),
+		Timestamp:            block.Time(),
+		OrdersClosedAt:       ordersClosedAt.UTC(),
+		SealedAt:             sealedAt.UTC(),
+	}
+
+	var blockId uint64
+	if err := tx.NamedStmtContext(ctx, ds.insertBuiltBlockStmt).GetContext(ctx, &blockId, blockData); err != nil {
+		log.Error("could not insert built block", "err", err)
+		return 0, err
+	}
+
+	return blockId, nil
+}
+
+func (ds *DatabaseService) insertBuildBlockBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, bundleIds []uint64) error {
+	if len(bundleIds) == 0 {
+		return nil
+	}
+
+	toInsert := make([]blockAndBundleId, len(bundleIds))
+	for i, bundleId := range bundleIds {
+		toInsert[i] = blockAndBundleId{blockId, bundleId}
+	}
+
+	_, err := tx.NamedExecContext(ctx, "insert into built_blocks_bundles (block_id, bundle_id) values (:block_id, :bundle_id)", toInsert)
+	return err
+}
+
+func (ds *DatabaseService) insertAllBlockBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, bundleIdsMap map[string]uint64) error {
+	if len(bundleIdsMap) == 0 {
+		return nil
+	}
+
+	toInsert := make([]blockAndBundleId, 0, len(bundleIdsMap))
+	for _, bundleId := range bundleIdsMap {
+		toInsert = append(toInsert, blockAndBundleId{blockId, bundleId})
+	}
+
+	_, err := tx.NamedExecContext(ctx, "insert into built_blocks_all_bundles (block_id, bundle_id) values (:block_id, :bundle_id)", toInsert)
+	return err
+}
+
+func (ds *DatabaseService) ConsumeBuiltBlock(block *types.Block, ordersClosedAt time.Time, sealedAt time.Time, commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, bidTrace *boostTypes.BidTrace) {
+	ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
+	defer cancel()
+
+	bundleIdsMap, err := ds.getBundleIdsAndInsertMissingBundles(ctx, block.NumberU64(), allBundles)
+	if err != nil {
+		log.Error("could not insert bundles", "err", err)
+	}
+
+	tx, err := ds.db.Beginx()
+	if err != nil {
+		log.Error("could not open DB transaction", "err", err)
+		return
+	}
+
+	blockId, err := ds.insertBuildBlock(tx, ctx, block, bidTrace, ordersClosedAt, sealedAt)
+	if err != nil {
+		tx.Rollback()
+		log.Error("could not insert built block", "err", err)
+		return
+	}
+
+	commitedBundlesIds := make([]uint64, 0, len(commitedBundles))
+	for _, bundle := range commitedBundles {
+		if id, found := bundleIdsMap[bundle.OriginalBundle.Hash.String()]; found {
+			commitedBundlesIds = append(commitedBundlesIds, id)
+		}
+	}
+
+	err = ds.insertBuildBlockBundleIds(tx, ctx, blockId, commitedBundlesIds)
+	if err != nil {
+		tx.Rollback()
+		log.Error("could not insert built block bundles", "err", err)
+		return
+	}
+
+	err = ds.insertAllBlockBundleIds(tx, ctx, blockId, bundleIdsMap)
+	if err != nil {
+		tx.Rollback()
+		log.Error("could not insert built block all bundles", "err", err)
+		return
+	}
+
+	err = tx.Commit()
+	if err != nil {
+		log.Error("could not commit DB trasnaction", "err", err)
+	}
+}
+func (ds *DatabaseService) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) {
+	var bundles []DbBundle
+	tx, err := ds.db.Beginx()
+	if err != nil {
+		log.Error("failed to begin db tx for get priority bundles", "err", err)
+		return nil, err
+	}
+	arg := map[string]interface{}{"param_block_number": uint64(blockNum), "is_high_prio": isHighPrio, "limit": lowPrioLimitSize}
+	if isHighPrio {
+		arg["limit"] = highPrioLimitSize
+	}
+	if err = tx.NamedStmtContext(ctx, ds.fetchPrioBundlesStmt).SelectContext(ctx, &bundles, arg); err != nil {
+		return nil, err
+	}
+	err = tx.Commit()
+	if err != nil {
+		log.Error("could not commit GetPriorityBundles transaction", "err", err)
+	}
+	return bundles, nil
+}
diff --git a/flashbotsextra/database_test.go b/flashbotsextra/database_test.go
new file mode 100644
index 0000000000..7b9efaf1a0
--- /dev/null
+++ b/flashbotsextra/database_test.go
@@ -0,0 +1,151 @@
+package flashbotsextra
+
+import (
+	"math/big"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	boostTypes "github.com/flashbots/go-boost-utils/types"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDatabaseBlockInsertion(t *testing.T) {
+	dsn := os.Getenv("FLASHBOTS_TEST_POSTGRES_DSN")
+	if dsn == "" {
+		return
+	}
+
+	ds, err := NewDatabaseService(dsn)
+	require.NoError(t, err)
+
+	_, err = ds.db.Exec("delete from built_blocks_bundles where block_id = (select block_id from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab')")
+	require.NoError(t, err)
+
+	_, err = ds.db.Exec("delete from built_blocks_all_bundles where block_id = (select block_id from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab')")
+	require.NoError(t, err)
+
+	_, err = ds.db.Exec("delete from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab'")
+	require.NoError(t, err)
+
+	_, err = ds.db.Exec("delete from bundles where bundle_hash in ('0x0978000000000000000000000000000000000000000000000000000000000000', '0x1078000000000000000000000000000000000000000000000000000000000000', '0x0979000000000000000000000000000000000000000000000000000000000000', '0x1080000000000000000000000000000000000000000000000000000000000000')")
+	require.NoError(t, err)
+
+	block := types.NewBlock(
+		&types.Header{
+			ParentHash: common.HexToHash("0xafafafa"),
+			Number:     big.NewInt(12),
+			GasLimit:   uint64(10000),
+			GasUsed:    uint64(1000),
+			Time:       16000000,
+			BaseFee:    big.NewInt(7),
+		}, nil, nil, nil, nil)
+	block.Profit = big.NewInt(10)
+
+	simBundle1 := types.SimulatedBundle{
+		MevGasPrice:       big.NewInt(9),
+		TotalEth:          big.NewInt(11),
+		EthSentToCoinbase: big.NewInt(10),
+		TotalGasUsed:      uint64(100),
+		OriginalBundle: types.MevBundle{
+			Txs:               types.Transactions{types.NewTransaction(uint64(50), common.Address{0x60}, big.NewInt(19), uint64(67), big.NewInt(43), []byte{})},
+			BlockNumber:       big.NewInt(12),
+			MinTimestamp:      uint64(1000000),
+			RevertingTxHashes: []common.Hash{common.Hash{0x10, 0x17}},
+			Hash:              common.Hash{0x09, 0x78},
+		},
+	}
+
+	simBundle2 := types.SimulatedBundle{
+		MevGasPrice:       big.NewInt(90),
+		TotalEth:          big.NewInt(110),
+		EthSentToCoinbase: big.NewInt(100),
+		TotalGasUsed:      uint64(1000),
+		OriginalBundle: types.MevBundle{
+			Txs:               types.Transactions{types.NewTransaction(uint64(51), common.Address{0x61}, big.NewInt(109), uint64(167), big.NewInt(433), []byte{})},
+			BlockNumber:       big.NewInt(12),
+			MinTimestamp:      uint64(1000020),
+			RevertingTxHashes: []common.Hash{common.Hash{0x11, 0x17}},
+			Hash:              common.Hash{0x10, 0x78},
+		},
+	}
+
+	var bundle2Id uint64
+	ds.db.Get(&bundle2Id, "insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id", SimulatedBundleToDbBundle(&simBundle2))
+
+	simBundle3 := types.SimulatedBundle{
+		MevGasPrice:       big.NewInt(91),
+		TotalEth:          big.NewInt(111),
+		EthSentToCoinbase: big.NewInt(101),
+		TotalGasUsed:      uint64(101),
+		OriginalBundle: types.MevBundle{
+			Txs:               types.Transactions{types.NewTransaction(uint64(51), common.Address{0x62}, big.NewInt(20), uint64(68), big.NewInt(44), []byte{})},
+			BlockNumber:       big.NewInt(12),
+			MinTimestamp:      uint64(1000021),
+			RevertingTxHashes: []common.Hash{common.Hash{0x10, 0x18}},
+			Hash:              common.Hash{0x09, 0x79},
+		},
+	}
+
+	simBundle4 := types.SimulatedBundle{
+		MevGasPrice:       big.NewInt(92),
+		TotalEth:          big.NewInt(112),
+		EthSentToCoinbase: big.NewInt(102),
+		TotalGasUsed:      uint64(1002),
+		OriginalBundle: types.MevBundle{
+			Txs:               types.Transactions{types.NewTransaction(uint64(52), common.Address{0x62}, big.NewInt(110), uint64(168), big.NewInt(434), []byte{})},
+			BlockNumber:       big.NewInt(12),
+			MinTimestamp:      uint64(1000022),
+			RevertingTxHashes: []common.Hash{common.Hash{0x11, 0x19}},
+			Hash:              common.Hash{0x10, 0x80},
+		},
+	}
+
+	var bundle4Id uint64
+	ds.db.Get(&bundle4Id, "insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id", SimulatedBundleToDbBundle(&simBundle4))
+
+	bidTrace := &boostTypes.BidTrace{}
+
+	ocAt := time.Now().Add(-time.Hour).UTC()
+	sealedAt := time.Now().Add(-30 * time.Minute).UTC()
+	ds.ConsumeBuiltBlock(block, ocAt, sealedAt, []types.SimulatedBundle{simBundle1, simBundle2}, []types.SimulatedBundle{simBundle1, simBundle2, simBundle3, simBundle4}, bidTrace)
+
+	var dbBlock BuiltBlock
+	require.NoError(t, ds.db.Get(&dbBlock, "select block_id, block_number, profit, slot, hash, gas_limit, gas_used, base_fee, parent_hash, timestamp, timestamp_datetime, orders_closed_at, sealed_at from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab'"))
+	require.Equal(t, BuiltBlock{
+		BlockId:           dbBlock.BlockId,
+		BlockNumber:       12,
+		Profit:            "0.000000000000000010",
+		Slot:              0,
+		Hash:              block.Hash().String(),
+		GasLimit:          block.GasLimit(),
+		GasUsed:           block.GasUsed(),
+		BaseFee:           7,
+		ParentHash:        "0x000000000000000000000000000000000000000000000000000000000afafafa",
+		Timestamp:         16000000,
+		TimestampDatetime: dbBlock.TimestampDatetime,
+		OrdersClosedAt:    dbBlock.OrdersClosedAt,
+		SealedAt:          dbBlock.SealedAt,
+	}, dbBlock)
+
+	require.True(t, dbBlock.TimestampDatetime.Equal(time.Unix(16000000, 0)))
+	require.Equal(t, ocAt.Truncate(time.Millisecond), dbBlock.OrdersClosedAt.UTC().Truncate(time.Millisecond))
+	require.Equal(t, sealedAt.Truncate(time.Millisecond), dbBlock.SealedAt.UTC().Truncate(time.Millisecond))
+
+	var bundles []DbBundle
+	ds.db.Select(&bundles, "select bundle_hash, param_signed_txs, param_block_number, param_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase from bundles order by param_timestamp")
+	require.Len(t, bundles, 4)
+	require.Equal(t, []DbBundle{SimulatedBundleToDbBundle(&simBundle1), SimulatedBundleToDbBundle(&simBundle2), SimulatedBundleToDbBundle(&simBundle3), SimulatedBundleToDbBundle(&simBundle4)}, bundles)
+
+	var commitedBundles []string
+	require.NoError(t, ds.db.Select(&commitedBundles, "select b.bundle_hash as bundle_hash from built_blocks_bundles bbb inner join bundles b on b.id = bbb.bundle_id where bbb.block_id = $1 order by b.param_timestamp", dbBlock.BlockId))
+	require.Len(t, commitedBundles, 2)
+	require.Equal(t, []string{simBundle1.OriginalBundle.Hash.String(), simBundle2.OriginalBundle.Hash.String()}, commitedBundles)
+
+	var allBundles []string
+	require.NoError(t, ds.db.Select(&allBundles, "select b.bundle_hash as bundle_hash from built_blocks_all_bundles bbb inner join bundles b on b.id = bbb.bundle_id where bbb.block_id = $1 order by b.param_timestamp", dbBlock.BlockId))
+	require.Len(t, allBundles, 4)
+	require.Equal(t, []string{simBundle1.OriginalBundle.Hash.String(), simBundle2.OriginalBundle.Hash.String(), simBundle3.OriginalBundle.Hash.String(), simBundle4.OriginalBundle.Hash.String()}, allBundles)
+}
diff --git a/flashbotsextra/database_types.go b/flashbotsextra/database_types.go
new file mode 100644
index 0000000000..560d616996
--- /dev/null
+++ b/flashbotsextra/database_types.go
@@ -0,0 +1,84 @@
+package flashbotsextra
+
+import (
+	"math/big"
+	"strings"
+	"time"
+
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+type BuiltBlock struct {
+	BlockId              uint64    `db:"block_id"`
+	BlockNumber          uint64    `db:"block_number"`
+	Profit               string    `db:"profit"`
+	Slot                 uint64    `db:"slot"`
+	Hash                 string    `db:"hash"`
+	GasLimit             uint64    `db:"gas_limit"`
+	GasUsed              uint64    `db:"gas_used"`
+	BaseFee              uint64    `db:"base_fee"`
+	ParentHash           string    `db:"parent_hash"`
+	ProposerPubkey       string    `db:"proposer_pubkey"`
+	ProposerFeeRecipient string    `db:"proposer_fee_recipient"`
+	BuilderPubkey        string    `db:"builder_pubkey"`
+	Timestamp            uint64    `db:"timestamp"`
+	TimestampDatetime    time.Time `db:"timestamp_datetime"`
+	OrdersClosedAt       time.Time `db:"orders_closed_at"`
+	SealedAt             time.Time `db:"sealed_at"`
+}
+
+type BuiltBlockBundle struct {
+	BlockId     uint64  `db:"block_id"`
+	BundleId    *uint64 `db:"bundle_id"`
+	BlockNumber uint64  `db:"block_number"`
+	BundleHash  string  `db:"bundle_hash"`
+}
+
+type DbBundle struct {
+	DbId       uint64 `db:"id"`
+	BundleHash string `db:"bundle_hash"`
+
+	ParamSignedTxs         string    `db:"param_signed_txs"`
+	ParamBlockNumber       uint64    `db:"param_block_number"`
+	ParamTimestamp         *uint64   `db:"param_timestamp"`
+	ReceivedTimestamp      time.Time `db:"received_timestamp"`
+	ParamRevertingTxHashes *string   `db:"param_reverting_tx_hashes"`
+
+	CoinbaseDiff      string `db:"coinbase_diff"`
+	TotalGasUsed      uint64 `db:"total_gas_used"`
+	StateBlockNumber  uint64 `db:"state_block_number"`
+	GasFees           string `db:"gas_fees"`
+	EthSentToCoinbase string `db:"eth_sent_to_coinbase"`
+}
+
+type blockAndBundleId struct {
+	BlockId  uint64 `db:"block_id"`
+	BundleId uint64 `db:"bundle_id"`
+}
+
+func SimulatedBundleToDbBundle(bundle *types.SimulatedBundle) DbBundle {
+	revertingTxHashes := make([]string, len(bundle.OriginalBundle.RevertingTxHashes))
+	for i, rTxHash := range bundle.OriginalBundle.RevertingTxHashes {
+		revertingTxHashes[i] = rTxHash.String()
+	}
+	paramRevertingTxHashes := strings.Join(revertingTxHashes, ",")
+	signedTxsStrings := make([]string, len(bundle.OriginalBundle.Txs))
+	for i, tx := range bundle.OriginalBundle.Txs {
+		signedTxsStrings[i] = tx.Hash().String()
+	}
+
+	return DbBundle{
+		BundleHash: bundle.OriginalBundle.Hash.String(),
+
+		ParamSignedTxs:         strings.Join(signedTxsStrings, ","),
+		ParamBlockNumber:       bundle.OriginalBundle.BlockNumber.Uint64(),
+		ParamTimestamp:         &bundle.OriginalBundle.MinTimestamp,
+		ParamRevertingTxHashes: &paramRevertingTxHashes,
+
+		CoinbaseDiff:      new(big.Rat).SetFrac(bundle.TotalEth, big.NewInt(1e18)).FloatString(18),
+		TotalGasUsed:      bundle.TotalGasUsed,
+		StateBlockNumber:  bundle.OriginalBundle.BlockNumber.Uint64(),
+		GasFees:           new(big.Int).Mul(big.NewInt(int64(bundle.TotalGasUsed)), bundle.MevGasPrice).String(),
+		EthSentToCoinbase: new(big.Rat).SetFrac(bundle.EthSentToCoinbase, big.NewInt(1e18)).FloatString(18),
+	}
+}
diff --git a/flashbotsextra/fetcher.go b/flashbotsextra/fetcher.go
new file mode 100644
index 0000000000..484aa1a4ee
--- /dev/null
+++ b/flashbotsextra/fetcher.go
@@ -0,0 +1,167 @@
+package flashbotsextra
+
+import (
+	"context"
+	"errors"
+	"math/big"
+	"strings"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/eth"
+	"github.com/ethereum/go-ethereum/log"
+	"golang.org/x/crypto/sha3"
+)
+
+type Fetcher interface {
+	Run() error
+}
+
+type bundleFetcher struct {
+	db                 IDatabaseService
+	backend            *eth.Ethereum
+	blockNumCh         chan int64
+	bundlesCh          chan []types.MevBundle
+	shouldPushToTxPool bool // Added for testing
+}
+
+func NewBundleFetcher(backend *eth.Ethereum, db IDatabaseService, blockNumCh chan int64, bundlesCh chan []types.MevBundle, shouldPushToTxPool bool) *bundleFetcher {
+	return &bundleFetcher{
+		db:                 db,
+		backend:            backend,
+		blockNumCh:         blockNumCh,
+		bundlesCh:          bundlesCh,
+		shouldPushToTxPool: shouldPushToTxPool,
+	}
+}
+
+func (b *bundleFetcher) Run() {
+	log.Info("Start bundle fetcher")
+	if b.shouldPushToTxPool {
+		eventCh := make(chan core.ChainHeadEvent)
+		b.backend.BlockChain().SubscribeChainHeadEvent(eventCh)
+		pushBlockNum := func() {
+			for currentBlockNum := range eventCh {
+				b.blockNumCh <- currentBlockNum.Block.Header().Number.Int64()
+			}
+		}
+		addMevBundle := func() {
+			log.Info("Start receiving mev bundles")
+			for bundles := range b.bundlesCh {
+				b.backend.TxPool().AddMevBundles(bundles)
+			}
+		}
+		go pushBlockNum()
+		go addMevBundle()
+	}
+	pushMevBundles := func(bundles []DbBundle) {
+		mevBundles := make([]types.MevBundle, 0)
+		for _, bundle := range bundles {
+			mevBundle, err := b.dbBundleToMevBundle(bundle)
+			if err != nil {
+				log.Error("failed to convert db bundle to mev bundle", "err", err)
+				continue
+			}
+			mevBundles = append(mevBundles, *mevBundle)
+		}
+		if len(mevBundles) > 0 {
+			b.bundlesCh <- mevBundles
+		}
+	}
+	go b.fetchAndPush(context.Background(), pushMevBundles)
+}
+
+func (b *bundleFetcher) fetchAndPush(ctx context.Context, pushMevBundles func(bundles []DbBundle)) {
+	var currentBlockNum int64
+	lowPrioBundleTicker := time.NewTicker(time.Second * 2)
+	defer lowPrioBundleTicker.Stop()
+
+	for {
+		select {
+		case currentBlockNum = <-b.blockNumCh:
+			ctxH, cancelH := context.WithTimeout(ctx, time.Second*3)
+			bundles, err := b.db.GetPriorityBundles(ctxH, currentBlockNum+1, true)
+			cancelH()
+			if err != nil {
+				log.Error("failed to fetch high prio bundles", "err", err)
+				continue
+			}
+			log.Debug("Fetching High prio bundles", "size", len(bundles), "currentlyBuiltBlockNum", currentBlockNum+1)
+			if len(bundles) != 0 {
+				pushMevBundles(bundles)
+			}
+
+		case <-lowPrioBundleTicker.C:
+			if currentBlockNum == 0 {
+				continue
+			}
+			ctxL, cancelL := context.WithTimeout(ctx, time.Second*3)
+			bundles, err := b.db.GetPriorityBundles(ctxL, currentBlockNum+1, false)
+			cancelL()
+			if err != nil {
+				log.Error("failed to fetch low prio bundles", "err", err)
+				continue
+			}
+			log.Debug("Fetching low prio bundles", "len", len(bundles), "currentlyBuiltBlockNum", currentBlockNum+1)
+			if len(bundles) != 0 {
+				pushMevBundles(bundles)
+			}
+		case <-ctx.Done():
+			close(b.bundlesCh)
+			return
+		}
+	}
+}
+
+func (b *bundleFetcher) dbBundleToMevBundle(arg DbBundle) (*types.MevBundle, error) {
+	signedTxsStr := strings.Split(arg.ParamSignedTxs, ",")
+	if len(signedTxsStr) == 0 {
+		return nil, errors.New("bundle missing txs")
+	}
+	if arg.ParamBlockNumber == 0 {
+		return nil, errors.New("bundle missing blockNumber")
+	}
+
+	var txs types.Transactions
+	for _, txStr := range signedTxsStr {
+		decodedTx, err := hexutil.Decode(txStr)
+		if err != nil {
+			log.Error("could not decode bundle tx", "id", arg.DbId, "err", err)
+			continue
+		}
+		tx := new(types.Transaction)
+		if err := tx.UnmarshalBinary(decodedTx); err != nil {
+			log.Error("could not unmarshal bundle decoded tx", "id", arg.DbId, "err", err)
+			continue
+		}
+		txs = append(txs, tx)
+	}
+	var paramRevertingTxHashes []string
+	if arg.ParamRevertingTxHashes != nil {
+		paramRevertingTxHashes = strings.Split(*arg.ParamRevertingTxHashes, ",")
+	}
+	revertingTxHashesStrings := paramRevertingTxHashes
+	revertingTxHashes := make([]common.Hash, len(revertingTxHashesStrings))
+	for _, rTxHashStr := range revertingTxHashesStrings {
+		revertingTxHashes = append(revertingTxHashes, common.HexToHash(rTxHashStr))
+	}
+	var minTimestamp uint64
+	if arg.ParamTimestamp != nil {
+		minTimestamp = *arg.ParamTimestamp
+	}
+	bundleHasher := sha3.NewLegacyKeccak256()
+	for _, tx := range txs {
+		bundleHasher.Write(tx.Hash().Bytes())
+	}
+	bundleHash := common.BytesToHash(bundleHasher.Sum(nil))
+	return &types.MevBundle{
+		Txs:               txs,
+		BlockNumber:       new(big.Int).SetUint64(arg.ParamBlockNumber),
+		MinTimestamp:      minTimestamp,
+		RevertingTxHashes: revertingTxHashes,
+		Hash:              bundleHash,
+	}, nil
+}
diff --git a/go.mod b/go.mod
index 4a769c7a2d..3b0f931132 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/ethereum/go-ethereum
 
-go 1.17
+go 1.18
 
 require (
 	github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0
@@ -9,8 +9,8 @@ require (
 	github.com/aws/aws-sdk-go-v2/config v1.1.1
 	github.com/aws/aws-sdk-go-v2/credentials v1.1.1
 	github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1
-	github.com/btcsuite/btcd/btcec/v2 v2.2.0
-	github.com/cespare/cp v0.1.0
+	github.com/btcsuite/btcd/btcec/v2 v2.2.1
+	github.com/cespare/cp v1.1.1
 	github.com/cloudflare/cloudflare-go v0.14.0
 	github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f
 	github.com/davecgh/go-spew v1.1.1
@@ -18,35 +18,41 @@ require (
 	github.com/docker/docker v1.6.2
 	github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf
 	github.com/edsrzf/mmap-go v1.0.0
-	github.com/fatih/color v1.7.0
+	github.com/fatih/color v1.9.0
 	github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c
 	github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5
-	github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
+	github.com/flashbots/go-boost-utils v0.3.5
+	github.com/flashbots/go-utils v0.4.5
+	github.com/flashbots/mev-boost v0.7.3
+	github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08
 	github.com/go-stack/stack v1.8.0
 	github.com/golang-jwt/jwt/v4 v4.3.0
 	github.com/golang/protobuf v1.5.2
 	github.com/golang/snappy v0.0.4
-	github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
-	github.com/google/uuid v1.2.0
-	github.com/gorilla/websocket v1.4.2
+	github.com/google/gofuzz v1.2.0
+	github.com/google/uuid v1.3.0
+	github.com/gorilla/mux v1.8.0
+	github.com/gorilla/websocket v1.5.0
 	github.com/graph-gophers/graphql-go v1.3.0
 	github.com/hashicorp/go-bexpr v0.1.10
 	github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
 	github.com/holiman/bloomfilter/v2 v2.0.3
 	github.com/holiman/uint256 v1.2.0
-	github.com/huin/goupnp v1.0.3
+	github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204
 	github.com/influxdata/influxdb v1.8.3
 	github.com/influxdata/influxdb-client-go/v2 v2.4.0
 	github.com/jackpal/go-nat-pmp v1.0.2
 	github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e
-	github.com/julienschmidt/httprouter v1.2.0
+	github.com/jmoiron/sqlx v1.3.5
+	github.com/julienschmidt/httprouter v1.3.0
 	github.com/karalabe/usb v0.0.2
-	github.com/mattn/go-colorable v0.1.8
-	github.com/mattn/go-isatty v0.0.12
+	github.com/lib/pq v1.2.0
+	github.com/mattn/go-colorable v0.1.9
+	github.com/mattn/go-isatty v0.0.14
 	github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416
 	github.com/olekukonko/tablewriter v0.0.5
-	github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
-	github.com/prometheus/tsdb v0.7.1
+	github.com/peterh/liner v1.2.0
+	github.com/prometheus/tsdb v0.10.0
 	github.com/rjeczalik/notify v0.9.1
 	github.com/rs/cors v1.7.0
 	github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible
@@ -56,53 +62,57 @@ require (
 	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
 	github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef
 	github.com/urfave/cli/v2 v2.10.2
-	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
+	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
 	golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
+	golang.org/x/sys v0.0.0-20220829200755-d48e67d00261
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
 	golang.org/x/text v0.3.7
 	golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
-	golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023
+	golang.org/x/tools v0.1.10
 	gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
 )
 
 require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3 // indirect
-	github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
+	github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
+	github.com/allegro/bigcache v1.2.1 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.1.1 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.1.1 // indirect
 	github.com/aws/smithy-go v1.1.0 // indirect
-	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
-	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
 	github.com/deepmap/oapi-codegen v1.8.2 // indirect
 	github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
+	github.com/ferranbt/fastssz v0.1.2-0.20220723134332-b3d3034a4575 // indirect
 	github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect
-	github.com/go-logfmt/logfmt v0.4.0 // indirect
 	github.com/go-ole/go-ole v1.2.1 // indirect
 	github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
 	github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect
-	github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.12 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
 	github.com/mattn/go-runewidth v0.0.9 // indirect
+	github.com/minio/sha256-simd v1.0.0 // indirect
 	github.com/mitchellh/mapstructure v1.4.1 // indirect
 	github.com/mitchellh/pointerstructure v1.2.0 // indirect
 	github.com/naoina/go-stringutil v0.1.0 // indirect
-	github.com/opentracing/opentracing-go v1.1.0 // indirect
+	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/tklauser/go-sysconf v0.3.5 // indirect
 	github.com/tklauser/numcpus v0.2.2 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
+	golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
 	golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
 	golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
 	google.golang.org/protobuf v1.26.0 // indirect
+	gopkg.in/urfave/cli.v1 v1.20.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 4b27867fbc..2f8816c40b 100644
--- a/go.sum
+++ b/go.sum
@@ -25,19 +25,19 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSu
 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 h1:Px2UA+2RvSSvv+RvJNuUB6n7rs5Wsel4dXLe90Um2n4=
 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
-github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 h1:5sXbqlSomvdjlRbWyNqkPsJ3Fg+tQZCbgeX1VGljbQY=
+github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o=
 github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw=
 github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
 github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
+github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
+github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
 github.com/aws/aws-sdk-go-v2 v1.2.0 h1:BS+UYpbsElC82gB+2E2jiCBg36i8HlubTB/dO/moQ9c=
@@ -62,18 +62,17 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
-github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
-github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
+github.com/btcsuite/btcd/btcec/v2 v2.2.1 h1:xP60mv8fvp+0khmrN0zTdPC3cNm24rfeE6lh2R/Yv3E=
+github.com/btcsuite/btcd/btcec/v2 v2.2.1/go.mod h1:9/CSmJxmuvqzX9Wh2fXMWToLOHhPd11lSPuIupwTkI8=
 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
-github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
 github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
-github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
-github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
+github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU=
+github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -95,9 +94,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
 github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
 github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
 github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
 github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
 github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
@@ -119,21 +117,28 @@ github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/ferranbt/fastssz v0.1.2-0.20220723134332-b3d3034a4575 h1:56lKKtcqQZ5sGjeuyBAeFwzcYuk32d8oqDvxQ9FUERA=
+github.com/ferranbt/fastssz v0.1.2-0.20220723134332-b3d3034a4575/go.mod h1:U2ZsxlYyvGeQGmadhz8PlEqwkBzDIhHwd3xuKrg2JIs=
 github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c h1:CndMRAH4JIwxbW8KYq6Q+cGWcGHz0FjGR3QqcInWcW0=
 github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY=
 github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c=
 github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
+github.com/flashbots/go-boost-utils v0.3.5 h1:RApwo1+8SGKhEGWH/WFIhg21pkCCW+RpO7MkZwiMI6A=
+github.com/flashbots/go-boost-utils v0.3.5/go.mod h1:vEfLpq6MLgdRtfWUG0fGDBbHXu0SyRInkw6ekLJeRIU=
+github.com/flashbots/go-utils v0.4.5 h1:xTrVcfxQ+qpVVPyRBWUllwZAxbAijE06d9N7e7mmmlM=
+github.com/flashbots/go-utils v0.4.5/go.mod h1:3YKyfbtetVIXuWKbZ9WmK8bSF20hSFXk0wCWDNHYFvE=
+github.com/flashbots/mev-boost v0.7.3 h1:AwJQACv6/CJuMQe4oGNvIgW00oKFCzzDBrb4DlJsBKE=
+github.com/flashbots/mev-boost v0.7.3/go.mod h1:DOqKZloyaZnfr6eGbnQb8XGH3gtjts19ATxI1LnfcNM=
 github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
-github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 h1:IZqZOB2fydHte3kUgxrzK5E1fW7RQGeDwE8F/ZZnUYc=
 github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61/go.mod h1:Q0X6pkwTILDlzrGEckF6HKjXe48EgsY/l7K7vhY4MW8=
-github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
-github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
+github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays=
+github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
 github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
 github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -154,9 +159,10 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
 github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
 github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
 github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
@@ -183,6 +189,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -198,22 +205,22 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa h1:Q75Upo5UN4JbPFURXZ8nLKYUvF85dyFRop/vQ0Rv+64=
-github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
 github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
 github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
 github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
@@ -227,11 +234,10 @@ github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iU
 github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
 github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ=
-github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
+github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204 h1:+EYBkW+dbi3F/atB+LSQZSWh7+HNrV3A/N0y6DSoy9k=
+github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y=
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY=
 github.com/influxdata/influxdb v1.8.3 h1:WEypI1BQFTT4teLM+1qkEcvUi0dAvopAI/ir0vAiBg8=
@@ -253,13 +259,16 @@ github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e h1:UvSe12bq+U
 github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
+github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
 github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
 github.com/karalabe/usb v0.0.2 h1:M6QQBNxF+CQ8OFvxrT90BA0qBOXymndZnk5q235mFc4=
@@ -267,20 +276,23 @@ github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio=
 github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE=
+github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
 github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg=
 github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kylelemons/godebug v0.0.0-20170224010052-a616ab194758/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
@@ -288,25 +300,35 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL
 github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c=
 github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8=
 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
 github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
+github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
+github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
+github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
 github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A=
@@ -320,37 +342,27 @@ github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hz
 github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0=
 github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
+github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
 github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.3 h1:OoxbjfXVZyod1fmWYhI7SEyaD8B00ynP3T+D5GiyHOY=
-github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
-github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
-github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1 h1:K0jcRCwNQM3vFGh1ppMtDh/+7ApJrjldlX8fA0jDTLQ=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
-github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE=
 github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
-github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
-github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
+github.com/peterh/liner v1.2.0 h1:w/UPXyl5GfahFxcTOz2j9wCIHNI+pUPr2laqpojKNCg=
+github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -365,17 +377,17 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/prometheus/tsdb v0.10.0 h1:If5rVCMTp6W2SiRAQFlbpJNgVlgMEd+U2GZckwK38ic=
+github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4=
 github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
 github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE=
 github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -388,6 +400,8 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1
 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@@ -408,19 +422,14 @@ github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8
 github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 github.com/supranational/blst v0.3.8-0.20220526154634-513d2456b344 h1:m+8fKfQwCAy1QjzINvKe/pYtLjo2dl59x2w9YSEJxuY=
 github.com/supranational/blst v0.3.8-0.20220526154634-513d2456b344/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
-github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
-github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
-github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
-github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
-github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
-github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
 github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
 github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4=
 github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI=
 github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA=
 github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM=
+github.com/trailofbits/go-fuzz-utils v0.0.0-20210901195358-9657fcfd256c h1:4WU+p200eLYtBsx3M5CKXvkjVdf5SC3W9nMg37y0TFI=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef h1:wHSqTBrZW24CsNJDfeh9Ex6Pm0Rcpc7qrgKBiL44vF4=
 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@@ -434,7 +443,6 @@ github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6Ut
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -451,8 +459,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
+golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -463,7 +471,6 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0
 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
 golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -483,9 +490,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 h1:LQmS1nU0twXLA96Kt7U9qtHJEbBk3z6Q0V4UXjZkpr4=
-golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
+golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -509,11 +515,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
 golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
 golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -533,7 +536,6 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -560,19 +562,15 @@ golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
+golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
@@ -614,14 +612,12 @@ golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191126055441-b0650ceb63d9/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 h1:0c3L82FDQ5rt1bjTBlchS8t6RQ6299/+5bWMnRLh+uI=
-golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
+golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
+golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -685,6 +681,8 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7
 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0=
+gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/graphql/graphql.go b/graphql/graphql.go
index 97b460c205..dc2f7a56ac 100644
--- a/graphql/graphql.go
+++ b/graphql/graphql.go
@@ -1266,7 +1266,7 @@ func (r *Resolver) SendRawTransaction(ctx context.Context, args struct{ Data hex
 	if err := tx.UnmarshalBinary(args.Data); err != nil {
 		return common.Hash{}, err
 	}
-	hash, err := ethapi.SubmitTransaction(ctx, r.backend, tx)
+	hash, err := ethapi.SubmitTransaction(ctx, r.backend, tx, false)
 	return hash, err
 }
 
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index e6740942d8..5d25203e51 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -18,6 +18,8 @@ package ethapi
 
 import (
 	"context"
+	"crypto/rand"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"math/big"
@@ -46,6 +48,7 @@ import (
 	"github.com/ethereum/go-ethereum/rlp"
 	"github.com/ethereum/go-ethereum/rpc"
 	"github.com/tyler-smith/go-bip39"
+	"golang.org/x/crypto/sha3"
 )
 
 // EthereumAPI provides an API to access Ethereum related information.
@@ -465,7 +468,7 @@ func (s *PersonalAccountAPI) SendTransaction(ctx context.Context, args Transacti
 		log.Warn("Failed transaction send attempt", "from", args.from(), "to", args.To, "value", args.Value.ToInt(), "err", err)
 		return common.Hash{}, err
 	}
-	return SubmitTransaction(ctx, s.b, signed)
+	return SubmitTransaction(ctx, s.b, signed, false)
 }
 
 // SignTransaction will create a transaction from the given arguments and
@@ -1650,7 +1653,7 @@ func (s *TransactionAPI) sign(addr common.Address, tx *types.Transaction) (*type
 }
 
 // SubmitTransaction is a helper function that submits tx to txPool and logs a message.
-func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) {
+func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction, private bool) (common.Hash, error) {
 	// If the transaction fee cap is already specified, ensure the
 	// fee of the given transaction is _reasonable_.
 	if err := checkTxFee(tx.GasPrice(), tx.Gas(), b.RPCTxFeeCap()); err != nil {
@@ -1660,7 +1663,7 @@ func SubmitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (c
 		// Ensure only eip155 signed transactions are submitted if EIP155Required is set.
 		return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC")
 	}
-	if err := b.SendTx(ctx, tx); err != nil {
+	if err := b.SendTx(ctx, tx, private); err != nil {
 		return common.Hash{}, err
 	}
 	// Print a log with full tx details for manual investigations and interventions
@@ -1708,7 +1711,7 @@ func (s *TransactionAPI) SendTransaction(ctx context.Context, args TransactionAr
 	if err != nil {
 		return common.Hash{}, err
 	}
-	return SubmitTransaction(ctx, s.b, signed)
+	return SubmitTransaction(ctx, s.b, signed, false)
 }
 
 // FillTransaction fills the defaults (nonce, gas, gasPrice or 1559 fields)
@@ -1735,7 +1738,20 @@ func (s *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil.B
 	if err := tx.UnmarshalBinary(input); err != nil {
 		return common.Hash{}, err
 	}
-	return SubmitTransaction(ctx, s.b, tx)
+	return SubmitTransaction(ctx, s.b, tx, false)
+}
+
+// SendPrivateRawTransaction will add the signed transaction to the transaction pool,
+// without broadcasting the transaction to its peers, and mark the transaction to avoid
+// future syncs.
+//
+// See SendRawTransaction.
+func (s *TransactionAPI) SendPrivateRawTransaction(ctx context.Context, input hexutil.Bytes) (common.Hash, error) {
+	tx := new(types.Transaction)
+	if err := tx.UnmarshalBinary(input); err != nil {
+		return common.Hash{}, err
+	}
+	return SubmitTransaction(ctx, s.b, tx, true)
 }
 
 // Sign calculates an ECDSA signature for:
@@ -1868,7 +1884,7 @@ func (s *TransactionAPI) Resend(ctx context.Context, sendArgs TransactionArgs, g
 			if err != nil {
 				return common.Hash{}, err
 			}
-			if err = s.b.SendTx(ctx, signedTx); err != nil {
+			if err = s.b.SendTx(ctx, signedTx, false); err != nil {
 				return common.Hash{}, err
 			}
 			return signedTx.Hash(), nil
@@ -2028,3 +2044,372 @@ func toHexSlice(b [][]byte) []string {
 	}
 	return r
 }
+
+// ---------------------------------------------------------------- FlashBots ----------------------------------------------------------------
+
+// PrivateTxBundleAPI offers an API for accepting bundled transactions
+type PrivateTxBundleAPI struct {
+	b Backend
+}
+
+// NewPrivateTxBundleAPI creates a new Tx Bundle API instance.
+func NewPrivateTxBundleAPI(b Backend) *PrivateTxBundleAPI {
+	return &PrivateTxBundleAPI{b}
+}
+
+// SendBundleArgs represents the arguments for a SendBundle call.
+type SendBundleArgs struct {
+	Txs               []hexutil.Bytes `json:"txs"`
+	BlockNumber       rpc.BlockNumber `json:"blockNumber"`
+	MinTimestamp      *uint64         `json:"minTimestamp"`
+	MaxTimestamp      *uint64         `json:"maxTimestamp"`
+	RevertingTxHashes []common.Hash   `json:"revertingTxHashes"`
+}
+
+// SendBundle will add the signed transaction to the transaction pool.
+// The sender is responsible for signing the transaction and using the correct nonce and ensuring validity
+func (s *PrivateTxBundleAPI) SendBundle(ctx context.Context, args SendBundleArgs) error {
+	var txs types.Transactions
+	if len(args.Txs) == 0 {
+		return errors.New("bundle missing txs")
+	}
+	if args.BlockNumber == 0 {
+		return errors.New("bundle missing blockNumber")
+	}
+
+	for _, encodedTx := range args.Txs {
+		tx := new(types.Transaction)
+		if err := tx.UnmarshalBinary(encodedTx); err != nil {
+			return err
+		}
+		txs = append(txs, tx)
+	}
+
+	var minTimestamp, maxTimestamp uint64
+	if args.MinTimestamp != nil {
+		minTimestamp = *args.MinTimestamp
+	}
+	if args.MaxTimestamp != nil {
+		maxTimestamp = *args.MaxTimestamp
+	}
+
+	return s.b.SendBundle(ctx, txs, args.BlockNumber, minTimestamp, maxTimestamp, args.RevertingTxHashes)
+}
+
+// BundleAPI offers an API for accepting bundled transactions
+type BundleAPI struct {
+	b     Backend
+	chain *core.BlockChain
+}
+
+// NewBundleAPI creates a new Tx Bundle API instance.
+func NewBundleAPI(b Backend, chain *core.BlockChain) *BundleAPI {
+	return &BundleAPI{b, chain}
+}
+
+// CallBundleArgs represents the arguments for a call.
+type CallBundleArgs struct {
+	Txs                    []hexutil.Bytes       `json:"txs"`
+	BlockNumber            rpc.BlockNumber       `json:"blockNumber"`
+	StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"`
+	Coinbase               *string               `json:"coinbase"`
+	Timestamp              *uint64               `json:"timestamp"`
+	Timeout                *int64                `json:"timeout"`
+	GasLimit               *uint64               `json:"gasLimit"`
+	Difficulty             *big.Int              `json:"difficulty"`
+	BaseFee                *big.Int              `json:"baseFee"`
+}
+
+// CallBundle will simulate a bundle of transactions at the top of a given block
+// number with the state of another (or the same) block. This can be used to
+// simulate future blocks with the current state, or it can be used to simulate
+// a past block.
+// The sender is responsible for signing the transactions and using the correct
+// nonce and ensuring validity
+func (s *BundleAPI) CallBundle(ctx context.Context, args CallBundleArgs) (map[string]interface{}, error) {
+	if len(args.Txs) == 0 {
+		return nil, errors.New("bundle missing txs")
+	}
+	if args.BlockNumber == 0 {
+		return nil, errors.New("bundle missing blockNumber")
+	}
+
+	var txs types.Transactions
+
+	for _, encodedTx := range args.Txs {
+		tx := new(types.Transaction)
+		if err := tx.UnmarshalBinary(encodedTx); err != nil {
+			return nil, err
+		}
+		txs = append(txs, tx)
+	}
+	defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
+
+	timeoutMilliSeconds := int64(5000)
+	if args.Timeout != nil {
+		timeoutMilliSeconds = *args.Timeout
+	}
+	timeout := time.Millisecond * time.Duration(timeoutMilliSeconds)
+
+	// Setup context so it may be cancelled the call has completed
+	// or, in case of unmetered gas, setup a context with a timeout.
+	var cancel context.CancelFunc
+	if timeout > 0 {
+		ctx, cancel = context.WithTimeout(ctx, timeout)
+	} else {
+		ctx, cancel = context.WithCancel(ctx)
+	}
+	// Make sure the context is cancelled when the call has completed
+	// this makes sure resources are cleaned up.
+	defer cancel()
+
+	state, parent, err := s.b.StateAndHeaderByNumberOrHash(ctx, args.StateBlockNumberOrHash)
+	if state == nil || err != nil {
+		return nil, err
+	}
+	blockNumber := big.NewInt(int64(args.BlockNumber))
+
+	timestamp := parent.Time + 1
+	if args.Timestamp != nil {
+		timestamp = *args.Timestamp
+	}
+	coinbase := parent.Coinbase
+	if args.Coinbase != nil {
+		coinbase = common.HexToAddress(*args.Coinbase)
+	}
+	difficulty := parent.Difficulty
+	if args.Difficulty != nil {
+		difficulty = args.Difficulty
+	}
+	gasLimit := parent.GasLimit
+	if args.GasLimit != nil {
+		gasLimit = *args.GasLimit
+	}
+	var baseFee *big.Int
+	if args.BaseFee != nil {
+		baseFee = args.BaseFee
+	} else if s.b.ChainConfig().IsLondon(big.NewInt(args.BlockNumber.Int64())) {
+		baseFee = misc.CalcBaseFee(s.b.ChainConfig(), parent)
+	}
+	header := &types.Header{
+		ParentHash: parent.Hash(),
+		Number:     blockNumber,
+		GasLimit:   gasLimit,
+		Time:       timestamp,
+		Difficulty: difficulty,
+		Coinbase:   coinbase,
+		BaseFee:    baseFee,
+	}
+
+	vmconfig := vm.Config{}
+
+	// Setup the gas pool (also for unmetered requests)
+	// and apply the message.
+	gp := new(core.GasPool).AddGas(math.MaxUint64)
+
+	results := []map[string]interface{}{}
+	coinbaseBalanceBefore := state.GetBalance(coinbase)
+
+	bundleHash := sha3.NewLegacyKeccak256()
+	signer := types.MakeSigner(s.b.ChainConfig(), blockNumber)
+	var totalGasUsed uint64
+	gasFees := new(big.Int)
+	for i, tx := range txs {
+		// Check if the context was cancelled (eg. timed-out)
+		if err := ctx.Err(); err != nil {
+			return nil, err
+		}
+
+		coinbaseBalanceBeforeTx := state.GetBalance(coinbase)
+		state.Prepare(tx.Hash(), i)
+
+		receipt, result, err := core.ApplyTransactionWithResult(s.b.ChainConfig(), s.chain, &coinbase, gp, state, header, tx, &header.GasUsed, vmconfig)
+		if err != nil {
+			return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
+		}
+
+		txHash := tx.Hash().String()
+		from, err := types.Sender(signer, tx)
+		if err != nil {
+			return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
+		}
+		to := "0x"
+		if tx.To() != nil {
+			to = tx.To().String()
+		}
+		jsonResult := map[string]interface{}{
+			"txHash":      txHash,
+			"gasUsed":     receipt.GasUsed,
+			"fromAddress": from.String(),
+			"toAddress":   to,
+		}
+		totalGasUsed += receipt.GasUsed
+		gasPrice, err := tx.EffectiveGasTip(header.BaseFee)
+		if err != nil {
+			return nil, fmt.Errorf("err: %w; txhash %s", err, tx.Hash())
+		}
+		gasFeesTx := new(big.Int).Mul(big.NewInt(int64(receipt.GasUsed)), gasPrice)
+		gasFees.Add(gasFees, gasFeesTx)
+		bundleHash.Write(tx.Hash().Bytes())
+		if result.Err != nil {
+			jsonResult["error"] = result.Err.Error()
+			revert := result.Revert()
+			if len(revert) > 0 {
+				jsonResult["revert"] = string(revert)
+			}
+		} else {
+			dst := make([]byte, hex.EncodedLen(len(result.Return())))
+			hex.Encode(dst, result.Return())
+			jsonResult["value"] = "0x" + string(dst)
+		}
+		coinbaseDiffTx := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBeforeTx)
+		jsonResult["coinbaseDiff"] = coinbaseDiffTx.String()
+		jsonResult["gasFees"] = gasFeesTx.String()
+		jsonResult["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiffTx, gasFeesTx).String()
+		jsonResult["gasPrice"] = new(big.Int).Div(coinbaseDiffTx, big.NewInt(int64(receipt.GasUsed))).String()
+		jsonResult["gasUsed"] = receipt.GasUsed
+		results = append(results, jsonResult)
+	}
+
+	ret := map[string]interface{}{}
+	ret["results"] = results
+	coinbaseDiff := new(big.Int).Sub(state.GetBalance(coinbase), coinbaseBalanceBefore)
+	ret["coinbaseDiff"] = coinbaseDiff.String()
+	ret["gasFees"] = gasFees.String()
+	ret["ethSentToCoinbase"] = new(big.Int).Sub(coinbaseDiff, gasFees).String()
+	ret["bundleGasPrice"] = new(big.Int).Div(coinbaseDiff, big.NewInt(int64(totalGasUsed))).String()
+	ret["totalGasUsed"] = totalGasUsed
+	ret["stateBlockNumber"] = parent.Number.Int64()
+
+	ret["bundleHash"] = "0x" + common.Bytes2Hex(bundleHash.Sum(nil))
+	return ret, nil
+}
+
+// EstimateGasBundleArgs represents the arguments for a call
+type EstimateGasBundleArgs struct {
+	Txs                    []TransactionArgs     `json:"txs"`
+	BlockNumber            rpc.BlockNumber       `json:"blockNumber"`
+	StateBlockNumberOrHash rpc.BlockNumberOrHash `json:"stateBlockNumber"`
+	Coinbase               *string               `json:"coinbase"`
+	Timestamp              *uint64               `json:"timestamp"`
+	Timeout                *int64                `json:"timeout"`
+}
+
+func (s *BundleAPI) EstimateGasBundle(ctx context.Context, args EstimateGasBundleArgs) (map[string]interface{}, error) {
+	if len(args.Txs) == 0 {
+		return nil, errors.New("bundle missing txs")
+	}
+	if args.BlockNumber == 0 {
+		return nil, errors.New("bundle missing blockNumber")
+	}
+
+	timeoutMS := int64(5000)
+	if args.Timeout != nil {
+		timeoutMS = *args.Timeout
+	}
+	timeout := time.Millisecond * time.Duration(timeoutMS)
+
+	// Setup context so it may be cancelled when the call
+	// has completed or, in case of unmetered gas, setup
+	// a context with a timeout
+	var cancel context.CancelFunc
+	if timeout > 0 {
+		ctx, cancel = context.WithTimeout(ctx, timeout)
+	} else {
+		ctx, cancel = context.WithCancel(ctx)
+	}
+
+	// Make sure the context is cancelled when the call has completed
+	// This makes sure resources are cleaned up
+	defer cancel()
+
+	state, parent, err := s.b.StateAndHeaderByNumberOrHash(ctx, args.StateBlockNumberOrHash)
+	if state == nil || err != nil {
+		return nil, err
+	}
+	blockNumber := big.NewInt(int64(args.BlockNumber))
+	timestamp := parent.Time + 1
+	if args.Timestamp != nil {
+		timestamp = *args.Timestamp
+	}
+	coinbase := parent.Coinbase
+	if args.Coinbase != nil {
+		coinbase = common.HexToAddress(*args.Coinbase)
+	}
+
+	header := &types.Header{
+		ParentHash: parent.Hash(),
+		Number:     blockNumber,
+		GasLimit:   parent.GasLimit,
+		Time:       timestamp,
+		Difficulty: parent.Difficulty,
+		Coinbase:   coinbase,
+		BaseFee:    parent.BaseFee,
+	}
+
+	// RPC Call gas cap
+	globalGasCap := s.b.RPCGasCap()
+
+	// Results
+	results := []map[string]interface{}{}
+
+	// Copy the original db so we don't modify it
+	statedb := state.Copy()
+
+	// Gas pool
+	gp := new(core.GasPool).AddGas(math.MaxUint64)
+
+	// Block context
+	blockContext := core.NewEVMBlockContext(header, s.chain, &coinbase)
+
+	// Feed each of the transactions into the VM ctx
+	// And try and estimate the gas used
+	for i, txArgs := range args.Txs {
+		// Check if the context was cancelled (eg. timed-out)
+		if err := ctx.Err(); err != nil {
+			return nil, err
+		}
+
+		// Since its a txCall we'll just prepare the
+		// state with a random hash
+		var randomHash common.Hash
+		rand.Read(randomHash[:])
+
+		// New random hash since its a call
+		statedb.Prepare(randomHash, i)
+
+		// Convert tx args to msg to apply state transition
+		msg, err := txArgs.ToMessage(globalGasCap, header.BaseFee)
+		if err != nil {
+			return nil, err
+		}
+
+		// Prepare the hashes
+		txContext := core.NewEVMTxContext(msg)
+
+		// Get EVM Environment
+		vmenv := vm.NewEVM(blockContext, txContext, statedb, s.b.ChainConfig(), vm.Config{NoBaseFee: true})
+
+		// Apply state transition
+		result, err := core.ApplyMessage(vmenv, msg, gp)
+		if err != nil {
+			return nil, err
+		}
+
+		// Modifications are committed to the state
+		// Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect
+		statedb.Finalise(vmenv.ChainConfig().IsEIP158(blockNumber))
+
+		// Append result
+		jsonResult := map[string]interface{}{
+			"gasUsed": result.UsedGas,
+		}
+		results = append(results, jsonResult)
+	}
+
+	// Return results
+	ret := map[string]interface{}{}
+	ret["results"] = results
+
+	return ret, nil
+}
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index 5b4ceb6310..f5ab88915b 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -74,7 +74,8 @@ type Backend interface {
 	SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription
 
 	// Transaction pool API
-	SendTx(ctx context.Context, signedTx *types.Transaction) error
+	SendTx(ctx context.Context, signedTx *types.Transaction, private bool) error
+	SendBundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error
 	GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error)
 	GetPoolTransactions() (types.Transactions, error)
 	GetPoolTransaction(txHash common.Hash) *types.Transaction
@@ -92,7 +93,7 @@ type Backend interface {
 	filters.Backend
 }
 
-func GetAPIs(apiBackend Backend) []rpc.API {
+func GetAPIs(apiBackend Backend, chain *core.BlockChain) []rpc.API {
 	nonceLock := new(AddrLocker)
 	return []rpc.API{
 		{
@@ -116,6 +117,12 @@ func GetAPIs(apiBackend Backend) []rpc.API {
 		}, {
 			Namespace: "personal",
 			Service:   NewPersonalAccountAPI(apiBackend, nonceLock),
+		}, {
+			Namespace: "eth",
+			Service:   NewPrivateTxBundleAPI(apiBackend),
+		}, {
+			Namespace: "eth",
+			Service:   NewBundleAPI(apiBackend, chain),
 		},
 	}
 }
diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go
index 28dc561c36..d10014a52c 100644
--- a/internal/ethapi/transaction_args_test.go
+++ b/internal/ethapi/transaction_args_test.go
@@ -212,6 +212,14 @@ type backendMock struct {
 	config  *params.ChainConfig
 }
 
+func (b *backendMock) SendMegabundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash, relayAddr common.Address) error {
+	return nil
+}
+
+func (b *backendMock) SendBundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error {
+	return nil
+}
+
 func newBackendMock() *backendMock {
 	config := &params.ChainConfig{
 		ChainID:             big.NewInt(42),
@@ -312,7 +320,9 @@ func (b *backendMock) SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) eve
 func (b *backendMock) SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription {
 	return nil
 }
-func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction) error { return nil }
+func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction, private bool) error {
+	return nil
+}
 func (b *backendMock) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) {
 	return nil, [32]byte{}, 0, 0, nil
 }
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index 88c31c04da..b3a7e5fb53 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -530,6 +530,12 @@ web3._extend({
 			params: 1,
 			inputFormatter: [web3._extend.formatters.inputTransactionFormatter]
 		}),
+		new web3._extend.Method({
+ 			name: 'sendPrivateRawTransaction',
+ 			call: 'eth_sendPrivateRawTransaction',
+ 			params: 1,
+ 			inputFormatter: [null]
+ 		}),
 		new web3._extend.Method({
 			name: 'fillTransaction',
 			call: 'eth_fillTransaction',
@@ -595,6 +601,16 @@ web3._extend({
 			call: 'eth_getLogs',
 			params: 1,
 		}),
+		new web3._extend.Method({
+			name: 'sendBundle',
+			call: 'eth_sendBundle',
+			params: 1,
+		}),
+		new web3._extend.Method({
+			name: 'callBundle',
+			call: 'eth_callBundle',
+			params: 6
+		}),
 	],
 	properties: [
 		new web3._extend.Property({
diff --git a/les/api_backend.go b/les/api_backend.go
index 5b4213134b..a0c2234fc7 100644
--- a/les/api_backend.go
+++ b/les/api_backend.go
@@ -188,7 +188,7 @@ func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *sta
 	return vm.NewEVM(context, txContext, state, b.eth.chainConfig, *vmConfig), state.Error, nil
 }
 
-func (b *LesApiBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error {
+func (b *LesApiBackend) SendTx(ctx context.Context, signedTx *types.Transaction, private bool) error {
 	return b.eth.txPool.Add(ctx, signedTx)
 }
 
@@ -196,6 +196,14 @@ func (b *LesApiBackend) RemoveTx(txHash common.Hash) {
 	b.eth.txPool.RemoveTx(txHash)
 }
 
+func (b *LesApiBackend) SendBundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error {
+	return b.eth.txPool.AddMevBundle(txs, big.NewInt(blockNumber.Int64()), minTimestamp, maxTimestamp, revertingTxHashes)
+}
+
+func (b *LesApiBackend) SendMegabundle(ctx context.Context, txs types.Transactions, blockNumber rpc.BlockNumber, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash, relayAddr common.Address) error {
+	return nil
+}
+
 func (b *LesApiBackend) GetPoolTransactions() (types.Transactions, error) {
 	return b.eth.txPool.GetTransactions()
 }
diff --git a/les/client.go b/les/client.go
index 6504fe2af8..9b98df7235 100644
--- a/les/client.go
+++ b/les/client.go
@@ -288,7 +288,7 @@ func (s *LightDummyAPI) Mining() bool {
 // APIs returns the collection of RPC services the ethereum package offers.
 // NOTE, some of these services probably need to be moved to somewhere else.
 func (s *LightEthereum) APIs() []rpc.API {
-	apis := ethapi.GetAPIs(s.ApiBackend)
+	apis := ethapi.GetAPIs(s.ApiBackend, nil)
 	apis = append(apis, s.engine.APIs(s.BlockChain().HeaderChain())...)
 	return append(apis, []rpc.API{
 		{
diff --git a/light/txpool.go b/light/txpool.go
index b3e1a62e18..7f74b9091e 100644
--- a/light/txpool.go
+++ b/light/txpool.go
@@ -550,3 +550,14 @@ func (pool *TxPool) RemoveTx(hash common.Hash) {
 	pool.chainDb.Delete(hash[:])
 	pool.relay.Discard([]common.Hash{hash})
 }
+
+// MevBundles returns a list of bundles valid for the given blockNumber/blockTimestamp
+// also prunes bundles that are outdated
+func (pool *TxPool) MevBundles(blockNumber *big.Int, blockTimestamp uint64) ([]types.Transactions, error) {
+	return nil, nil
+}
+
+// AddMevBundle adds a mev bundle to the pool
+func (pool *TxPool) AddMevBundle(txs types.Transactions, blockNumber *big.Int, minTimestamp uint64, maxTimestamp uint64, revertingTxHashes []common.Hash) error {
+	return nil
+}
diff --git a/miner/algo_common.go b/miner/algo_common.go
new file mode 100644
index 0000000000..e1e8503d6a
--- /dev/null
+++ b/miner/algo_common.go
@@ -0,0 +1,393 @@
+package miner
+
+import (
+	"crypto/ecdsa"
+	"errors"
+	"fmt"
+	"math/big"
+	"sync/atomic"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/state"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/core/vm"
+	"github.com/ethereum/go-ethereum/eth/tracers/logger"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+const (
+	shiftTx = 1
+	popTx   = 2
+)
+
+var emptyCodeHash = common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
+
+var errInterrupt = errors.New("miner worker interrupted")
+
+type chainData struct {
+	chainConfig *params.ChainConfig
+	chain       *core.BlockChain
+	blacklist   map[common.Address]struct{}
+}
+
+type environmentDiff struct {
+	baseEnvironment *environment
+	header          *types.Header
+	gasPool         *core.GasPool  // available gas used to pack transactions
+	state           *state.StateDB // apply state changes here
+	newProfit       *big.Int
+	newTxs          []*types.Transaction
+	newReceipts     []*types.Receipt
+}
+
+func newEnvironmentDiff(env *environment) *environmentDiff {
+	gasPool := new(core.GasPool).AddGas(env.gasPool.Gas())
+	return &environmentDiff{
+		baseEnvironment: env,
+		header:          types.CopyHeader(env.header),
+		gasPool:         gasPool,
+		state:           env.state.Copy(),
+		newProfit:       new(big.Int),
+	}
+}
+
+func (e *environmentDiff) copy() *environmentDiff {
+	gasPool := new(core.GasPool).AddGas(e.gasPool.Gas())
+
+	return &environmentDiff{
+		baseEnvironment: e.baseEnvironment.copy(),
+		header:          types.CopyHeader(e.header),
+		gasPool:         gasPool,
+		state:           e.state.Copy(),
+		newProfit:       new(big.Int).Set(e.newProfit),
+		newTxs:          e.newTxs[:],
+		newReceipts:     e.newReceipts[:],
+	}
+}
+
+func (e *environmentDiff) applyToBaseEnv() {
+	env := e.baseEnvironment
+	env.gasPool = new(core.GasPool).AddGas(e.gasPool.Gas())
+	env.header = e.header
+	env.state = e.state
+	env.profit.Add(env.profit, e.newProfit)
+	env.tcount += len(e.newTxs)
+	env.txs = append(env.txs, e.newTxs...)
+	env.receipts = append(env.receipts, e.newReceipts...)
+}
+
+func checkInterrupt(i *int32) bool {
+	return i != nil && atomic.LoadInt32(i) != commitInterruptNone
+}
+
+// Simulate bundle on top of current state without modifying it
+// pending txs used to track if bundle tx is part of the mempool
+func applyTransactionWithBlacklist(signer types.Signer, config *params.ChainConfig, bc core.ChainContext, author *common.Address, gp *core.GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, blacklist map[common.Address]struct{}) (*types.Receipt, *state.StateDB, error) {
+	// short circuit if blacklist is empty
+	if len(blacklist) == 0 {
+		snap := statedb.Snapshot()
+		receipt, err := core.ApplyTransaction(config, bc, author, gp, statedb, header, tx, usedGas, cfg, nil)
+		if err != nil {
+			statedb.RevertToSnapshot(snap)
+		}
+		return receipt, statedb, err
+	}
+
+	sender, err := signer.Sender(tx)
+	if err != nil {
+		return nil, statedb, err
+	}
+
+	if _, in := blacklist[sender]; in {
+		return nil, statedb, errors.New("blacklist violation, tx.sender")
+	}
+
+	if to := tx.To(); to != nil {
+		if _, in := blacklist[*to]; in {
+			return nil, statedb, errors.New("blacklist violation, tx.to")
+		}
+	}
+
+	touchTracer := logger.NewAccountTouchTracer()
+	cfg.Tracer = touchTracer
+	cfg.Debug = true
+
+	hook := func() error {
+		for _, address := range touchTracer.TouchedAddresses() {
+			if _, in := blacklist[address]; in {
+				return errors.New("blacklist violation, tx trace")
+			}
+		}
+		return nil
+	}
+
+	usedGasTmp := *usedGas
+	gasPoolTmp := new(core.GasPool).AddGas(gp.Gas())
+	snap := statedb.Snapshot()
+
+	receipt, err := core.ApplyTransaction(config, bc, author, gasPoolTmp, statedb, header, tx, &usedGasTmp, cfg, hook)
+	if err != nil {
+		statedb.RevertToSnapshot(snap)
+		return receipt, statedb, err
+	}
+
+	*usedGas = usedGasTmp
+	*gp = *gasPoolTmp
+	return receipt, statedb, err
+}
+
+// commit tx to envDiff
+func (envDiff *environmentDiff) commitTx(tx *types.Transaction, chData chainData) (*types.Receipt, int, error) {
+	header := envDiff.header
+	coinbase := &envDiff.baseEnvironment.coinbase
+	signer := envDiff.baseEnvironment.signer
+
+	gasPrice, err := tx.EffectiveGasTip(header.BaseFee)
+	if err != nil {
+		return nil, shiftTx, err
+	}
+
+	envDiff.state.Prepare(tx.Hash(), envDiff.baseEnvironment.tcount+len(envDiff.newTxs))
+
+	receipt, newState, err := applyTransactionWithBlacklist(signer, chData.chainConfig, chData.chain, coinbase,
+		envDiff.gasPool, envDiff.state, header, tx, &header.GasUsed, *chData.chain.GetVMConfig(), chData.blacklist)
+	envDiff.state = newState
+	if err != nil {
+		switch {
+		case errors.Is(err, core.ErrGasLimitReached):
+			// Pop the current out-of-gas transaction without shifting in the next from the account
+			from, _ := types.Sender(signer, tx)
+			log.Trace("Gas limit exceeded for current block", "sender", from)
+			return nil, popTx, err
+
+		case errors.Is(err, core.ErrNonceTooLow):
+			// New head notification data race between the transaction pool and miner, shift
+			from, _ := types.Sender(signer, tx)
+			log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
+			return nil, shiftTx, err
+
+		case errors.Is(err, core.ErrNonceTooHigh):
+			// Reorg notification data race between the transaction pool and miner, skip account =
+			from, _ := types.Sender(signer, tx)
+			log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
+			return nil, popTx, err
+
+		case errors.Is(err, core.ErrTxTypeNotSupported):
+			// Pop the unsupported transaction without shifting in the next from the account
+			from, _ := types.Sender(signer, tx)
+			log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type())
+			return nil, popTx, err
+
+		default:
+			// Strange error, discard the transaction and get the next in line (note, the
+			// nonce-too-high clause will prevent us from executing in vain).
+			log.Trace("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
+			return nil, shiftTx, err
+		}
+	}
+
+	envDiff.newProfit = envDiff.newProfit.Add(envDiff.newProfit, gasPrice.Mul(gasPrice, big.NewInt(int64(receipt.GasUsed))))
+	envDiff.newTxs = append(envDiff.newTxs, tx)
+	envDiff.newReceipts = append(envDiff.newReceipts, receipt)
+	return receipt, shiftTx, nil
+}
+
+// Commit Bundle to env diff
+func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chData chainData, interrupt *int32) error {
+	coinbase := envDiff.baseEnvironment.coinbase
+	tmpEnvDiff := envDiff.copy()
+
+	coinbaseBalanceBefore := tmpEnvDiff.state.GetBalance(coinbase)
+
+	profitBefore := new(big.Int).Set(tmpEnvDiff.newProfit)
+	var gasUsed uint64
+
+	for _, tx := range bundle.OriginalBundle.Txs {
+		if tmpEnvDiff.header.BaseFee != nil && tx.Type() == 2 {
+			// Sanity check for extremely large numbers
+			if tx.GasFeeCap().BitLen() > 256 {
+				return core.ErrFeeCapVeryHigh
+			}
+			if tx.GasTipCap().BitLen() > 256 {
+				return core.ErrTipVeryHigh
+			}
+			// Ensure gasFeeCap is greater than or equal to gasTipCap.
+			if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 {
+				return core.ErrTipAboveFeeCap
+			}
+		}
+
+		if tx.Value().Sign() == -1 {
+			return core.ErrNegativeValue
+		}
+
+		_, err := tx.EffectiveGasTip(envDiff.header.BaseFee)
+		if err != nil {
+			return err
+		}
+
+		_, err = types.Sender(envDiff.baseEnvironment.signer, tx)
+		if err != nil {
+			return err
+		}
+
+		if checkInterrupt(interrupt) {
+			return errInterrupt
+		}
+
+		receipt, _, err := tmpEnvDiff.commitTx(tx, chData)
+
+		if err != nil {
+			log.Trace("Bundle tx error", "bundle", bundle.OriginalBundle.Hash, "tx", tx.Hash(), "err", err)
+			return err
+		}
+
+		if receipt.Status != types.ReceiptStatusSuccessful && !bundle.OriginalBundle.RevertingHash(tx.Hash()) {
+			log.Trace("Bundle tx failed", "bundle", bundle.OriginalBundle.Hash, "tx", tx.Hash(), "err", err)
+			return errors.New("bundle tx revert")
+		}
+
+		gasUsed += receipt.GasUsed
+	}
+	coinbaseBalanceAfter := tmpEnvDiff.state.GetBalance(coinbase)
+	coinbaseBalanceDelta := new(big.Int).Sub(coinbaseBalanceAfter, coinbaseBalanceBefore)
+	tmpEnvDiff.newProfit.Add(profitBefore, coinbaseBalanceDelta)
+
+	bundleProfit := coinbaseBalanceDelta
+
+	bundleActualEffGP := bundleProfit.Div(bundleProfit, big.NewInt(int64(gasUsed)))
+	bundleSimEffGP := new(big.Int).Set(bundle.MevGasPrice)
+
+	// allow >-1% divergence
+	bundleActualEffGP.Mul(bundleActualEffGP, big.NewInt(100))
+	bundleSimEffGP.Mul(bundleSimEffGP, big.NewInt(99))
+
+	if bundleSimEffGP.Cmp(bundleActualEffGP) == 1 {
+		log.Trace("Bundle underpays after inclusion", "bundle", bundle.OriginalBundle.Hash)
+		return errors.New("bundle underpays")
+	}
+
+	*envDiff = *tmpEnvDiff
+	return nil
+}
+
+func estimatePayoutTxGas(env *environment, sender, receiver common.Address, prv *ecdsa.PrivateKey, chData chainData) (uint64, bool, error) {
+	if codeHash := env.state.GetCodeHash(receiver); codeHash == (common.Hash{}) || codeHash == emptyCodeHash {
+		return params.TxGas, true, nil
+	}
+	gasLimit := env.gasPool.Gas()
+
+	balance := new(big.Int).SetBytes([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})
+	value := new(big.Int).SetBytes([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})
+
+	diff := newEnvironmentDiff(env)
+	diff.state.SetBalance(sender, balance)
+	receipt, err := diff.commitPayoutTx(value, sender, receiver, gasLimit, prv, chData)
+	if err != nil {
+		return 0, false, err
+	}
+	return receipt.GasUsed, false, nil
+}
+
+func insertPayoutTx(env *environment, sender, receiver common.Address, gas uint64, isEOA bool, availableFunds *big.Int, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) {
+	applyTx := func(envDiff *environmentDiff, gas uint64) (*types.Receipt, error) {
+		fee := new(big.Int).Mul(env.header.BaseFee, new(big.Int).SetUint64(gas))
+		amount := new(big.Int).Sub(availableFunds, fee)
+		if amount.Sign() < 0 {
+			return nil, errors.New("not enough funds available")
+		}
+		rec, err := envDiff.commitPayoutTx(amount, sender, receiver, gas, prv, chData)
+		if err != nil {
+			return nil, fmt.Errorf("failed to commit payment tx: %w", err)
+		} else if rec.Status != types.ReceiptStatusSuccessful {
+			return nil, fmt.Errorf("payment tx failed")
+		}
+		return rec, nil
+	}
+
+	if isEOA {
+		diff := newEnvironmentDiff(env)
+		rec, err := applyTx(diff, gas)
+		if err != nil {
+			return nil, err
+		}
+		diff.applyToBaseEnv()
+		return rec, nil
+	}
+
+	var err error
+	for i := 0; i < 6; i++ {
+		diff := newEnvironmentDiff(env)
+		var rec *types.Receipt
+		rec, err = applyTx(diff, gas)
+		if err != nil {
+			gas += 1000
+			continue
+		}
+
+		if gas == rec.GasUsed {
+			diff.applyToBaseEnv()
+			return rec, nil
+		}
+
+		exactEnvDiff := newEnvironmentDiff(env)
+		exactRec, err := applyTx(exactEnvDiff, rec.GasUsed)
+		if err != nil {
+			diff.applyToBaseEnv()
+			return rec, nil
+		}
+		exactEnvDiff.applyToBaseEnv()
+		return exactRec, nil
+	}
+
+	if err == nil {
+		return nil, errors.New("could not estimate gas")
+	}
+
+	return nil, err
+}
+
+func (envDiff *environmentDiff) commitPayoutTx(amount *big.Int, sender, receiver common.Address, gas uint64, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) {
+	senderBalance := envDiff.state.GetBalance(sender)
+
+	if gas < params.TxGas {
+		return nil, errors.New("not enough gas for intrinsic gas cost")
+	}
+
+	requiredBalance := new(big.Int).Mul(envDiff.header.BaseFee, new(big.Int).SetUint64(gas))
+	requiredBalance = requiredBalance.Add(requiredBalance, amount)
+	if requiredBalance.Cmp(senderBalance) > 0 {
+		return nil, errors.New("not enough balance")
+	}
+
+	signer := envDiff.baseEnvironment.signer
+	tx, err := types.SignNewTx(prv, signer, &types.DynamicFeeTx{
+		ChainID:   chData.chainConfig.ChainID,
+		Nonce:     envDiff.state.GetNonce(sender),
+		GasTipCap: new(big.Int),
+		GasFeeCap: envDiff.header.BaseFee,
+		Gas:       gas,
+		To:        &receiver,
+		Value:     amount,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	txSender, err := signer.Sender(tx)
+	if err != nil {
+		return nil, err
+	}
+	if txSender != sender {
+		return nil, errors.New("incorrect sender private key")
+	}
+
+	receipt, _, err := envDiff.commitTx(tx, chData)
+	if err != nil {
+		return nil, err
+	}
+
+	return receipt, nil
+}
diff --git a/miner/algo_common_test.go b/miner/algo_common_test.go
new file mode 100644
index 0000000000..2dd43e62ef
--- /dev/null
+++ b/miner/algo_common_test.go
@@ -0,0 +1,605 @@
+package miner
+
+import (
+	"crypto/ecdsa"
+	"errors"
+	"fmt"
+	"math/big"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	mapset "github.com/deckarep/golang-set"
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/hexutil"
+	"github.com/ethereum/go-ethereum/consensus/ethash"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"github.com/ethereum/go-ethereum/core/state"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/core/vm"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+const GasLimit uint64 = 30000000
+
+var (
+	// Pay proxy is a contract that sends msg.value to address specified in calldata[0..32]
+	payProxyAddress = common.HexToAddress("0x1100000000000000000000000000000000000000")
+	payProxyCode    = hexutil.MustDecode("0x6000600060006000346000356000f1")
+	// log contract logs value that it receives
+	logContractAddress = common.HexToAddress("0x2200000000000000000000000000000000000000")
+	logContractCode    = hexutil.MustDecode("0x346000523460206000a1")
+)
+
+type signerList struct {
+	config    *params.ChainConfig
+	signers   []*ecdsa.PrivateKey
+	addresses []common.Address
+	nonces    []uint64
+}
+
+func simulateBundle(env *environment, bundle types.MevBundle, chData chainData, interrupt *int32) (types.SimulatedBundle, error) {
+	stateDB := env.state.Copy()
+	gasPool := new(core.GasPool).AddGas(env.header.GasLimit)
+
+	var totalGasUsed uint64
+	gasFees := big.NewInt(0)
+	ethSentToCoinbase := big.NewInt(0)
+
+	for i, tx := range bundle.Txs {
+		if checkInterrupt(interrupt) {
+			return types.SimulatedBundle{}, errInterrupt
+		}
+
+		if env.header.BaseFee != nil && tx.Type() == 2 {
+			// Sanity check for extremely large numbers
+			if tx.GasFeeCap().BitLen() > 256 {
+				return types.SimulatedBundle{}, core.ErrFeeCapVeryHigh
+			}
+			if tx.GasTipCap().BitLen() > 256 {
+				return types.SimulatedBundle{}, core.ErrTipVeryHigh
+			}
+			// Ensure gasFeeCap is greater than or equal to gasTipCap.
+			if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 {
+				return types.SimulatedBundle{}, core.ErrTipAboveFeeCap
+			}
+		}
+
+		stateDB.Prepare(tx.Hash(), i+env.tcount)
+		coinbaseBalanceBefore := stateDB.GetBalance(env.coinbase)
+
+		var tempGasUsed uint64
+		receipt, err := core.ApplyTransaction(chData.chainConfig, chData.chain, &env.coinbase, gasPool, stateDB, env.header, tx, &tempGasUsed, *chData.chain.GetVMConfig(), nil)
+		if err != nil {
+			return types.SimulatedBundle{}, err
+		}
+		if receipt.Status == types.ReceiptStatusFailed && !containsHash(bundle.RevertingTxHashes, receipt.TxHash) {
+			return types.SimulatedBundle{}, errors.New("failed tx")
+		}
+
+		totalGasUsed += receipt.GasUsed
+
+		_, err = types.Sender(env.signer, tx)
+		if err != nil {
+			return types.SimulatedBundle{}, err
+		}
+
+		// see NOTE below
+		//txInPendingPool := false
+		//if accountTxs, ok := pendingTxs[from]; ok {
+		//	// check if tx is in pending pool
+		//	txNonce := tx.Nonce()
+		//
+		//	for _, accountTx := range accountTxs {
+		//		if accountTx.Nonce() == txNonce {
+		//			txInPendingPool = true
+		//			break
+		//		}
+		//	}
+		//}
+
+		gasUsed := new(big.Int).SetUint64(receipt.GasUsed)
+		gasPrice, err := tx.EffectiveGasTip(env.header.BaseFee)
+		if err != nil {
+			return types.SimulatedBundle{}, err
+		}
+		gasFeesTx := gasUsed.Mul(gasUsed, gasPrice)
+		coinbaseBalanceAfter := stateDB.GetBalance(env.coinbase)
+		coinbaseDelta := big.NewInt(0).Sub(coinbaseBalanceAfter, coinbaseBalanceBefore)
+		coinbaseDelta.Sub(coinbaseDelta, gasFeesTx)
+		ethSentToCoinbase.Add(ethSentToCoinbase, coinbaseDelta)
+
+		// NOTE - it differs from prod!, if changed - change in commit bundle too
+		//if !txInPendingPool {
+		//	// If tx is not in pending pool, count the gas fees
+		//	gasFees.Add(gasFees, gasFeesTx)
+		//}
+		gasFees.Add(gasFees, gasFeesTx)
+	}
+
+	totalEth := new(big.Int).Add(ethSentToCoinbase, gasFees)
+
+	return types.SimulatedBundle{
+		MevGasPrice:       new(big.Int).Div(totalEth, new(big.Int).SetUint64(totalGasUsed)),
+		TotalEth:          totalEth,
+		EthSentToCoinbase: ethSentToCoinbase,
+		TotalGasUsed:      totalGasUsed,
+		OriginalBundle:    bundle,
+	}, nil
+}
+
+func (sig signerList) signTx(i int, gas uint64, gasTipCap *big.Int, gasFeeCap *big.Int, to common.Address, value *big.Int, data []byte) *types.Transaction {
+	txData := &types.DynamicFeeTx{
+		ChainID:   sig.config.ChainID,
+		Nonce:     sig.nonces[i],
+		GasTipCap: gasTipCap,
+		GasFeeCap: gasFeeCap,
+		Gas:       gas,
+		To:        &to,
+		Value:     value,
+		Data:      data,
+	}
+	sig.nonces[i] += 1
+
+	return types.MustSignNewTx(sig.signers[i], types.LatestSigner(sig.config), txData)
+}
+
+func genSignerList(len int, config *params.ChainConfig) signerList {
+	res := signerList{
+		config:    config,
+		signers:   make([]*ecdsa.PrivateKey, len),
+		addresses: make([]common.Address, len),
+		nonces:    make([]uint64, len),
+	}
+
+	for i := 0; i < len; i++ {
+		privKey, err := crypto.ToECDSA(crypto.Keccak256(big.NewInt(int64(i)).Bytes()))
+		if err != nil {
+			panic(fmt.Sprint("cant create priv key", err))
+		}
+		res.signers[i] = privKey
+		res.addresses[i] = crypto.PubkeyToAddress(privKey.PublicKey)
+	}
+	return res
+}
+
+func genGenesisAlloc(sign signerList, contractAddr []common.Address, contractCode [][]byte) core.GenesisAlloc {
+	genesisAlloc := make(core.GenesisAlloc)
+	for i := 0; i < len(sign.signers); i++ {
+		genesisAlloc[sign.addresses[i]] = core.GenesisAccount{
+			Balance: big.NewInt(1000000000000000000), // 1 ether
+			Nonce:   sign.nonces[i],
+		}
+	}
+
+	for i, address := range contractAddr {
+		genesisAlloc[address] = core.GenesisAccount{
+			Balance: new(big.Int),
+			Code:    contractCode[i],
+		}
+	}
+
+	return genesisAlloc
+}
+
+func genTestSetup() (*state.StateDB, chainData, signerList) {
+	config := params.AllEthashProtocolChanges
+	db := rawdb.NewMemoryDatabase()
+	signerList := genSignerList(10, config)
+
+	genesisAlloc := genGenesisAlloc(signerList, []common.Address{payProxyAddress, logContractAddress}, [][]byte{payProxyCode, logContractCode})
+
+	gspec := &core.Genesis{
+		Config: config,
+		Alloc:  genesisAlloc,
+	}
+	_ = gspec.MustCommit(db)
+
+	chain, _ := core.NewBlockChain(db, &core.CacheConfig{TrieDirtyDisabled: true}, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil)
+
+	stateDB, _ := state.New(chain.CurrentHeader().Root, state.NewDatabase(db), nil)
+
+	return stateDB, chainData{config, chain, nil}, signerList
+}
+
+func newEnvironment(data chainData, state *state.StateDB, coinbase common.Address, gasLimit uint64, baseFee *big.Int) *environment {
+	currentBlock := data.chain.CurrentBlock()
+	// Note the passed coinbase may be different with header.Coinbase.
+	return &environment{
+		signer:    types.MakeSigner(data.chainConfig, currentBlock.Number()),
+		state:     state,
+		gasPool:   new(core.GasPool).AddGas(gasLimit),
+		coinbase:  coinbase,
+		ancestors: mapset.NewSet(),
+		family:    mapset.NewSet(),
+		header: &types.Header{
+			Coinbase:   coinbase,
+			ParentHash: currentBlock.Hash(),
+			Number:     new(big.Int).Add(currentBlock.Number(), big.NewInt(1)),
+			GasLimit:   gasLimit,
+			GasUsed:    0,
+			BaseFee:    baseFee,
+			Difficulty: big.NewInt(0),
+		},
+		uncles: make(map[common.Hash]*types.Header),
+		profit: new(big.Int),
+	}
+}
+
+func TestTxCommit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	tx := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	receipt, i, err := envDiff.commitTx(tx, chData)
+	if err != nil {
+		t.Fatal("can't commit transaction:", err)
+	}
+	if receipt.Status != 1 {
+		t.Fatal("tx failed", receipt)
+	}
+	if i != shiftTx {
+		t.Fatal("incorrect shift value")
+	}
+
+	if env.tcount != 0 {
+		t.Fatal("env tcount modified")
+	}
+	if len(env.receipts) != 0 {
+		t.Fatal("env receipts modified")
+	}
+	if len(env.txs) != 0 {
+		t.Fatal("env txs modified")
+	}
+	if env.gasPool.Gas() != GasLimit {
+		t.Fatal("env gas pool modified")
+	}
+
+	if envDiff.gasPool.AddGas(receipt.GasUsed).Gas() != GasLimit {
+		t.Fatal("envDiff gas pool incorrect")
+	}
+	if envDiff.header.GasUsed != receipt.GasUsed {
+		t.Fatal("envDiff gas used is incorrect")
+	}
+	if len(envDiff.newReceipts) != 1 {
+		t.Fatal("envDiff receipts incorrect")
+	}
+	if len(envDiff.newTxs) != 1 {
+		t.Fatal("envDiff txs incorrect")
+	}
+}
+
+func TestBundleCommit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	tx1 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+	tx2 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	bundle := types.MevBundle{
+		Txs:         types.Transactions{tx1, tx2},
+		BlockNumber: env.header.Number,
+	}
+
+	simBundle, err := simulateBundle(env, bundle, chData, nil)
+	if err != nil {
+		t.Fatal("Failed to simulate bundle", err)
+	}
+
+	err = envDiff.commitBundle(&simBundle, chData, nil)
+	if err != nil {
+		t.Fatal("Failed to commit bundle", err)
+	}
+
+	if len(envDiff.newTxs) != 2 {
+		t.Fatal("Incorrect new txs")
+	}
+	if len(envDiff.newReceipts) != 2 {
+		t.Fatal("Incorrect receipts txs")
+	}
+	if envDiff.gasPool.AddGas(21000*2).Gas() != GasLimit {
+		t.Fatal("Gas pool incorrect update")
+	}
+}
+
+func TestErrorTxCommit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	signers.nonces[1] = 10
+	tx := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	_, i, err := envDiff.commitTx(tx, chData)
+	if err == nil {
+		t.Fatal("committed incorrect transaction:", err)
+	}
+	if i != popTx {
+		t.Fatal("incorrect shift value")
+	}
+
+	if envDiff.gasPool.Gas() != GasLimit {
+		t.Fatal("envDiff gas pool incorrect")
+	}
+	if envDiff.header.GasUsed != 0 {
+		t.Fatal("envDiff gas used incorrect")
+	}
+	if envDiff.newProfit.Sign() != 0 {
+		t.Fatal("envDiff new profit incorrect")
+	}
+	if len(envDiff.newReceipts) != 0 {
+		t.Fatal("envDiff receipts incorrect")
+	}
+	if len(envDiff.newTxs) != 0 {
+		t.Fatal("envDiff txs incorrect")
+	}
+}
+
+func TestCommitTxOverGasLimit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], 21000, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	tx1 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+	tx2 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	receipt, i, err := envDiff.commitTx(tx1, chData)
+	if err != nil {
+		t.Fatal("can't commit transaction:", err)
+	}
+	if receipt.Status != 1 {
+		t.Fatal("tx failed", receipt)
+	}
+	if i != shiftTx {
+		t.Fatal("incorrect shift value")
+	}
+
+	if envDiff.gasPool.Gas() != 0 {
+		t.Fatal("Env diff gas pool is not drained")
+	}
+
+	_, _, err = envDiff.commitTx(tx2, chData)
+	require.Error(t, err, "committed tx over gas limit")
+}
+
+func TestErrorBundleCommit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], 21000*2, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	// This tx will be included before bundle so bundle will fail because of gas limit
+	tx0 := signers.signTx(4, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	tx1 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+	tx2 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{})
+
+	bundle := types.MevBundle{
+		Txs:         types.Transactions{tx1, tx2},
+		BlockNumber: env.header.Number,
+	}
+
+	simBundle, err := simulateBundle(env, bundle, chData, nil)
+	if err != nil {
+		t.Fatal("Failed to simulate bundle", err)
+	}
+
+	_, _, err = envDiff.commitTx(tx0, chData)
+	if err != nil {
+		t.Fatal("Failed to commit tx0", err)
+	}
+
+	gasPoolBefore := *envDiff.gasPool
+	gasUsedBefore := envDiff.header.GasUsed
+	newProfitBefore := new(big.Int).Set(envDiff.newProfit)
+	balanceBefore := envDiff.state.GetBalance(signers.addresses[2])
+
+	err = envDiff.commitBundle(&simBundle, chData, nil)
+	if err == nil {
+		t.Fatal("Committed failed bundle", err)
+	}
+
+	if *envDiff.gasPool != gasPoolBefore {
+		t.Fatal("gasPool changed")
+	}
+
+	if envDiff.header.GasUsed != gasUsedBefore {
+		t.Fatal("gasUsed changed")
+	}
+
+	balanceAfter := envDiff.state.GetBalance(signers.addresses[2])
+	if balanceAfter.Cmp(balanceBefore) != 0 {
+		t.Fatal("balance changed")
+	}
+
+	if envDiff.newProfit.Cmp(newProfitBefore) != 0 {
+		t.Fatal("newProfit changed")
+	}
+
+	if len(envDiff.newTxs) != 1 {
+		t.Fatal("Incorrect new txs")
+	}
+	if len(envDiff.newReceipts) != 1 {
+		t.Fatal("Incorrect receipts txs")
+	}
+}
+
+func TestBlacklist(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1))
+	envDiff := newEnvironmentDiff(env)
+
+	beforeRoot := statedb.IntermediateRoot(true)
+
+	blacklist := map[common.Address]struct{}{
+		signers.addresses[3]: {},
+	}
+	chData.blacklist = blacklist
+
+	gasPoolBefore := *envDiff.gasPool
+	gasUsedBefore := envDiff.header.GasUsed
+	balanceBefore := envDiff.state.GetBalance(signers.addresses[3])
+
+	tx := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[3], big.NewInt(77), []byte{})
+	_, _, err := envDiff.commitTx(tx, chData)
+	if err == nil {
+		t.Fatal("committed blacklisted transaction: to")
+	}
+
+	tx = signers.signTx(3, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[1], big.NewInt(88), []byte{})
+	_, _, err = envDiff.commitTx(tx, chData)
+	if err == nil {
+		t.Fatal("committed blacklisted transaction: sender")
+	}
+
+	calldata := make([]byte, 32-20, 20)
+	calldata = append(calldata, signers.addresses[3].Bytes()...)
+
+	tx = signers.signTx(4, 40000, big.NewInt(0), big.NewInt(1), payProxyAddress, big.NewInt(99), calldata)
+	_, _, err = envDiff.commitTx(tx, chData)
+	fmt.Println("balance", envDiff.state.GetBalance(signers.addresses[3]))
+
+	if err == nil {
+		t.Fatal("committed blacklisted transaction: trace")
+	}
+
+	if *envDiff.gasPool != gasPoolBefore {
+		t.Fatal("gasPool changed")
+	}
+
+	if envDiff.header.GasUsed != gasUsedBefore {
+		t.Fatal("gasUsed changed")
+	}
+
+	if envDiff.newProfit.Sign() != 0 {
+		t.Fatal("newProfit changed")
+	}
+
+	if envDiff.state.GetBalance(signers.addresses[3]).Cmp(balanceBefore) != 0 {
+		t.Fatal("blacklisted balance changed")
+	}
+
+	if len(envDiff.newTxs) != 0 {
+		t.Fatal("newTxs changed")
+	}
+
+	if len(envDiff.newReceipts) != 0 {
+		t.Fatal("newReceipts changed")
+	}
+
+	afterRoot := statedb.IntermediateRoot(true)
+	if beforeRoot != afterRoot {
+		t.Fatal("statedb root changed")
+	}
+}
+
+func TestGetSealingWorkAlgos(t *testing.T) {
+	t.Cleanup(func() {
+		testConfig.AlgoType = ALGO_MEV_GETH
+	})
+
+	for _, algoType := range []AlgoType{ALGO_MEV_GETH, ALGO_GREEDY} {
+		local := new(params.ChainConfig)
+		*local = *ethashChainConfig
+		local.TerminalTotalDifficulty = big.NewInt(0)
+		testConfig.AlgoType = algoType
+		testGetSealingWork(t, local, ethash.NewFaker(), true)
+	}
+}
+
+func TestGetSealingWorkAlgosWithProfit(t *testing.T) {
+	t.Cleanup(func() {
+		testConfig.AlgoType = ALGO_MEV_GETH
+		testConfig.BuilderTxSigningKey = nil
+	})
+
+	for _, algoType := range []AlgoType{ALGO_GREEDY} {
+		var err error
+		testConfig.BuilderTxSigningKey, err = crypto.GenerateKey()
+		require.NoError(t, err)
+		testConfig.AlgoType = algoType
+		t.Logf("running for %d", algoType)
+		testBundles(t)
+	}
+}
+
+func TestPayoutTxUtils(t *testing.T) {
+	availableFunds := big.NewInt(50000000000000000) // 0.05 eth
+
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1))
+
+	// Sending payment to the plain EOA
+	gas, isEOA, err := estimatePayoutTxGas(env, signers.addresses[1], signers.addresses[2], signers.signers[1], chData)
+	require.Equal(t, uint64(21000), gas)
+	require.True(t, isEOA)
+	require.NoError(t, err)
+
+	expectedPayment := new(big.Int).Sub(availableFunds, big.NewInt(21000))
+	balanceBefore := env.state.GetBalance(signers.addresses[2])
+	rec, err := insertPayoutTx(env, signers.addresses[1], signers.addresses[2], gas, isEOA, availableFunds, signers.signers[1], chData)
+	balanceAfter := env.state.GetBalance(signers.addresses[2])
+	require.NoError(t, err)
+	require.NotNil(t, rec)
+	require.Equal(t, types.ReceiptStatusSuccessful, rec.Status)
+	require.Equal(t, uint64(21000), rec.GasUsed)
+	require.True(t, new(big.Int).Sub(balanceAfter, balanceBefore).Cmp(expectedPayment) == 0)
+	require.Equal(t, env.state.GetNonce(signers.addresses[1]), uint64(1))
+
+	// Sending payment to the contract that logs event of the amount
+	gas, isEOA, err = estimatePayoutTxGas(env, signers.addresses[1], logContractAddress, signers.signers[1], chData)
+	require.Equal(t, uint64(22025), gas)
+	require.False(t, isEOA)
+	require.NoError(t, err)
+
+	expectedPayment = new(big.Int).Sub(availableFunds, big.NewInt(22025))
+	balanceBefore = env.state.GetBalance(logContractAddress)
+	rec, err = insertPayoutTx(env, signers.addresses[1], logContractAddress, gas, isEOA, availableFunds, signers.signers[1], chData)
+	balanceAfter = env.state.GetBalance(logContractAddress)
+	require.NoError(t, err)
+	require.NotNil(t, rec)
+	require.Equal(t, types.ReceiptStatusSuccessful, rec.Status)
+	require.Equal(t, uint64(22025), rec.GasUsed)
+	require.True(t, new(big.Int).Sub(balanceAfter, balanceBefore).Cmp(expectedPayment) == 0)
+	require.Equal(t, env.state.GetNonce(signers.addresses[1]), uint64(2))
+
+	// Try requesting less gas for contract tx. We request 21k gas, but we must pay 22025
+	expectedPayment = new(big.Int).Sub(availableFunds, big.NewInt(22025))
+	balanceBefore = env.state.GetBalance(logContractAddress)
+	rec, err = insertPayoutTx(env, signers.addresses[1], logContractAddress, 21000, isEOA, availableFunds, signers.signers[1], chData)
+	balanceAfter = env.state.GetBalance(logContractAddress)
+	require.NoError(t, err)
+	require.NotNil(t, rec)
+	require.Equal(t, types.ReceiptStatusSuccessful, rec.Status)
+	require.Equal(t, uint64(22025), rec.GasUsed)
+	require.True(t, new(big.Int).Sub(balanceAfter, balanceBefore).Cmp(expectedPayment) == 0)
+	require.Equal(t, env.state.GetNonce(signers.addresses[1]), uint64(3))
+
+	// errors
+
+	_, err = insertPayoutTx(env, signers.addresses[1], signers.addresses[2], 21000, true, availableFunds, signers.signers[2], chData)
+	require.ErrorContains(t, err, "incorrect sender private key")
+	_, err = insertPayoutTx(env, signers.addresses[1], logContractAddress, 23000, false, availableFunds, signers.signers[2], chData)
+	require.ErrorContains(t, err, "incorrect sender private key")
+
+	_, err = insertPayoutTx(env, signers.addresses[1], signers.addresses[2], 21000, true, big.NewInt(21000-1), signers.signers[1], chData)
+	require.ErrorContains(t, err, "not enough funds available")
+	_, err = insertPayoutTx(env, signers.addresses[1], logContractAddress, 23000, false, big.NewInt(23000-1), signers.signers[1], chData)
+	require.ErrorContains(t, err, "not enough funds available")
+
+	_, err = insertPayoutTx(env, signers.addresses[1], signers.addresses[2], 20000, true, availableFunds, signers.signers[1], chData)
+	require.ErrorContains(t, err, "not enough gas")
+
+	require.Equal(t, env.state.GetNonce(signers.addresses[1]), uint64(3))
+}
diff --git a/miner/algo_greedy.go b/miner/algo_greedy.go
new file mode 100644
index 0000000000..51b76a76cc
--- /dev/null
+++ b/miner/algo_greedy.go
@@ -0,0 +1,80 @@
+package miner
+
+import (
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+// / To use it:
+// / 1. Copy relevant data from the worker
+// / 2. Call buildBlock
+// / 2. If new bundles, txs arrive, call buildBlock again
+// / This struct lifecycle is tied to 1 block-building task
+type greedyBuilder struct {
+	inputEnvironment *environment
+	chainData        chainData
+	interrupt        *int32
+}
+
+func newGreedyBuilder(chain *core.BlockChain, chainConfig *params.ChainConfig, blacklist map[common.Address]struct{}, env *environment, interrupt *int32) *greedyBuilder {
+	return &greedyBuilder{
+		inputEnvironment: env,
+		chainData:        chainData{chainConfig, chain, blacklist},
+		interrupt:        interrupt,
+	}
+}
+
+func (b *greedyBuilder) mergeOrdersIntoEnvDiff(envDiff *environmentDiff, orders *types.TransactionsByPriceAndNonce) []types.SimulatedBundle {
+	usedBundles := []types.SimulatedBundle{}
+
+	for {
+		order := orders.Peek()
+		if order == nil {
+			break
+		}
+
+		if order.Tx != nil {
+			receipt, skip, err := envDiff.commitTx(order.Tx, b.chainData)
+			switch skip {
+			case shiftTx:
+				orders.Shift()
+			case popTx:
+				orders.Pop()
+			}
+
+			if err != nil {
+				log.Trace("could not apply tx", "hash", order.Tx.Hash(), "err", err)
+				continue
+			}
+			effGapPrice, err := order.Tx.EffectiveGasTip(envDiff.baseEnvironment.header.BaseFee)
+			if err == nil {
+				log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed)
+			}
+		} else if order.Bundle != nil {
+			bundle := order.Bundle
+			//log.Debug("buildBlock considering bundle", "egp", bundle.MevGasPrice.String(), "hash", bundle.OriginalBundle.Hash)
+			err := envDiff.commitBundle(bundle, b.chainData, b.interrupt)
+			orders.Pop()
+			if err != nil {
+				log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err)
+				continue
+			}
+
+			log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.TotalEth))
+			usedBundles = append(usedBundles, *bundle)
+		}
+	}
+
+	return usedBundles
+}
+
+func (b *greedyBuilder) buildBlock(simBundles []types.SimulatedBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle) {
+	orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, b.inputEnvironment.header.BaseFee)
+	envDiff := newEnvironmentDiff(b.inputEnvironment.copy())
+	usedBundles := b.mergeOrdersIntoEnvDiff(envDiff, orders)
+	envDiff.applyToBaseEnv()
+	return envDiff.baseEnvironment, usedBundles
+}
diff --git a/miner/algo_greedy_test.go b/miner/algo_greedy_test.go
new file mode 100644
index 0000000000..8f7f99d7ba
--- /dev/null
+++ b/miner/algo_greedy_test.go
@@ -0,0 +1,69 @@
+package miner
+
+import (
+	"fmt"
+	"math/big"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/log"
+)
+
+func TestBuildBlockGasLimit(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], 21000, big.NewInt(1))
+
+	txs := make(map[common.Address]types.Transactions)
+
+	txs[signers.addresses[1]] = types.Transactions{
+		signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{}),
+	}
+	txs[signers.addresses[2]] = types.Transactions{
+		signers.signTx(2, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{}),
+	}
+
+	builder := newGreedyBuilder(chData.chain, chData.chainConfig, nil, env, nil)
+
+	result, _ := builder.buildBlock([]types.SimulatedBundle{}, txs)
+	log.Info("block built", "txs", len(result.txs), "gasPool", result.gasPool.Gas())
+	if result.tcount != 1 {
+		t.Fatal("Incorrect tx count")
+	}
+}
+
+func TestTxWithMinerFeeHeap(t *testing.T) {
+	statedb, chData, signers := genTestSetup()
+
+	env := newEnvironment(chData, statedb, signers.addresses[0], 21000, big.NewInt(1))
+
+	txs := make(map[common.Address]types.Transactions)
+
+	txs[signers.addresses[1]] = types.Transactions{
+		signers.signTx(1, 21000, big.NewInt(1), big.NewInt(5), signers.addresses[2], big.NewInt(0), []byte{}),
+	}
+	txs[signers.addresses[2]] = types.Transactions{
+		signers.signTx(2, 21000, big.NewInt(4), big.NewInt(5), signers.addresses[2], big.NewInt(0), []byte{}),
+	}
+
+	bundle1 := types.SimulatedBundle{MevGasPrice: big.NewInt(3), OriginalBundle: types.MevBundle{Hash: common.HexToHash("0xb1")}}
+	bundle2 := types.SimulatedBundle{MevGasPrice: big.NewInt(2), OriginalBundle: types.MevBundle{Hash: common.HexToHash("0xb2")}}
+
+	orders := types.NewTransactionsByPriceAndNonce(env.signer, txs, []types.SimulatedBundle{bundle2, bundle1}, env.header.BaseFee)
+
+	for {
+		order := orders.Peek()
+		if order == nil {
+			return
+		}
+
+		if order.Tx != nil {
+			fmt.Println("tx", order.Tx.Hash())
+			orders.Shift()
+		} else if order.Bundle != nil {
+			fmt.Println("bundle", order.Bundle.OriginalBundle.Hash)
+			orders.Pop()
+		}
+	}
+}
diff --git a/miner/bundle_cache.go b/miner/bundle_cache.go
new file mode 100644
index 0000000000..2d6bf18537
--- /dev/null
+++ b/miner/bundle_cache.go
@@ -0,0 +1,83 @@
+package miner
+
+import (
+	"sync"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+const (
+	maxHeaders = 3
+)
+
+type BundleCache struct {
+	mu      sync.Mutex
+	entries []*BundleCacheEntry
+}
+
+func NewBundleCache() *BundleCache {
+	return &BundleCache{
+		entries: make([]*BundleCacheEntry, maxHeaders),
+	}
+}
+
+func (b *BundleCache) GetBundleCache(header common.Hash) *BundleCacheEntry {
+	b.mu.Lock()
+	defer b.mu.Unlock()
+
+	for _, entry := range b.entries {
+		if entry != nil && entry.headerHash == header {
+			return entry
+		}
+	}
+	newEntry := newCacheEntry(header)
+	b.entries = b.entries[1:]
+	b.entries = append(b.entries, newEntry)
+
+	return newEntry
+}
+
+type BundleCacheEntry struct {
+	mu                sync.Mutex
+	headerHash        common.Hash
+	successfulBundles map[common.Hash]*simulatedBundle
+	failedBundles     map[common.Hash]struct{}
+}
+
+func newCacheEntry(header common.Hash) *BundleCacheEntry {
+	return &BundleCacheEntry{
+		headerHash:        header,
+		successfulBundles: make(map[common.Hash]*simulatedBundle),
+		failedBundles:     make(map[common.Hash]struct{}),
+	}
+}
+
+func (c *BundleCacheEntry) GetSimulatedBundle(bundle common.Hash) (*types.SimulatedBundle, bool) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if simmed, ok := c.successfulBundles[bundle]; ok {
+		return simmed, true
+	}
+
+	if _, ok := c.failedBundles[bundle]; ok {
+		return nil, true
+	}
+
+	return nil, false
+}
+
+func (c *BundleCacheEntry) UpdateSimulatedBundles(result []*types.SimulatedBundle, bundles []types.MevBundle) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	for i, simBundle := range result {
+		bundleHash := bundles[i].Hash
+		if simBundle != nil {
+			c.successfulBundles[bundleHash] = simBundle
+		} else {
+			c.failedBundles[bundleHash] = struct{}{}
+		}
+	}
+}
diff --git a/miner/bundle_cache_test.go b/miner/bundle_cache_test.go
new file mode 100644
index 0000000000..77de101af6
--- /dev/null
+++ b/miner/bundle_cache_test.go
@@ -0,0 +1,69 @@
+package miner
+
+import (
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/core/types"
+)
+
+func TestBundleCacheEntry(t *testing.T) {
+	entry := newCacheEntry(common.HexToHash("0x01"))
+
+	failingBundle := common.HexToHash("0xff")
+	successBundle := common.HexToHash("0xaa")
+
+	sim, found := entry.GetSimulatedBundle(failingBundle)
+	if sim != nil || found {
+		t.Errorf("found bundle in empty cache: %s", failingBundle)
+	}
+	sim, found = entry.GetSimulatedBundle(successBundle)
+	if sim != nil || found {
+		t.Errorf("found bundle in empty cache: %s", successBundle)
+	}
+
+	bundles := []types.MevBundle{types.MevBundle{Hash: failingBundle}, types.MevBundle{Hash: successBundle}}
+	simResult := []*types.SimulatedBundle{nil, &types.SimulatedBundle{OriginalBundle: bundles[1]}}
+	entry.UpdateSimulatedBundles(simResult, bundles)
+
+	sim, found = entry.GetSimulatedBundle(failingBundle)
+	if sim != nil || !found {
+		t.Error("incorrect failing bundle result")
+	}
+	sim, found = entry.GetSimulatedBundle(successBundle)
+	if sim != simResult[1] || !found {
+		t.Error("incorrect successful bundle result")
+	}
+}
+
+func TestBundleCache(t *testing.T) {
+	cache := NewBundleCache()
+
+	header1 := common.HexToHash("0x01")
+	header2 := common.HexToHash("0x02")
+	header3 := common.HexToHash("0x03")
+	header4 := common.HexToHash("0x04")
+
+	cache1 := cache.GetBundleCache(header1)
+	if cache1.headerHash != header1 {
+		t.Error("incorrect header cache")
+	}
+
+	cache2 := cache.GetBundleCache(header2)
+	if cache2.headerHash != header2 {
+		t.Error("incorrect header cache")
+	}
+
+	cache2Again := cache.GetBundleCache(header2)
+	if cache2 != cache2Again {
+		t.Error("header cache is not reused")
+	}
+
+	cache.GetBundleCache(header3)
+	cache.GetBundleCache(header4)
+
+	cache1Again := cache.GetBundleCache(header1)
+	if cache1 == cache1Again {
+		t.Error("cache1 should be removed after insertions")
+	}
+}
diff --git a/miner/miner.go b/miner/miner.go
index 1e9607a76a..4eb06a213d 100644
--- a/miner/miner.go
+++ b/miner/miner.go
@@ -18,8 +18,12 @@
 package miner
 
 import (
+	"crypto/ecdsa"
+	"errors"
 	"fmt"
 	"math/big"
+	"os"
+	"strings"
 	"sync"
 	"time"
 
@@ -29,6 +33,7 @@ import (
 	"github.com/ethereum/go-ethereum/core"
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
 	"github.com/ethereum/go-ethereum/eth/downloader"
 	"github.com/ethereum/go-ethereum/event"
 	"github.com/ethereum/go-ethereum/log"
@@ -42,23 +47,45 @@ type Backend interface {
 	TxPool() *core.TxPool
 }
 
+type AlgoType int
+
+const (
+	ALGO_MEV_GETH AlgoType = iota
+	ALGO_GREEDY
+)
+
+func AlgoTypeFlagToEnum(algoString string) (AlgoType, error) {
+	switch algoString {
+	case "mev-geth":
+		return ALGO_MEV_GETH, nil
+	case "greedy":
+		return ALGO_GREEDY, nil
+	default:
+		return ALGO_MEV_GETH, errors.New("algo not recognized")
+	}
+}
+
 // Config is the configuration parameters of mining.
 type Config struct {
-	Etherbase  common.Address `toml:",omitempty"` // Public address for block mining rewards (default = first account)
-	Notify     []string       `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash).
-	NotifyFull bool           `toml:",omitempty"` // Notify with pending block headers instead of work packages
-	ExtraData  hexutil.Bytes  `toml:",omitempty"` // Block extra data set by the miner
-	GasFloor   uint64         // Target gas floor for mined blocks.
-	GasCeil    uint64         // Target gas ceiling for mined blocks.
-	GasPrice   *big.Int       // Minimum gas price for mining a transaction
-	Recommit   time.Duration  // The time interval for miner to re-create mining work.
-	Noverify   bool           // Disable remote mining solution verification(only useful in ethash).
+	Etherbase           common.Address    `toml:",omitempty"` // Public address for block mining rewards (default = first account)
+	Notify              []string          `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash).
+	NotifyFull          bool              `toml:",omitempty"` // Notify with pending block headers instead of work packages
+	ExtraData           hexutil.Bytes     `toml:",omitempty"` // Block extra data set by the miner
+	GasFloor            uint64            // Target gas floor for mined blocks.
+	GasCeil             uint64            // Target gas ceiling for mined blocks.
+	GasPrice            *big.Int          // Minimum gas price for mining a transaction
+	AlgoType            AlgoType          // Algorithm to use for block building
+	Recommit            time.Duration     // The time interval for miner to re-create mining work.
+	Noverify            bool              // Disable remote mining solution verification(only useful in ethash).
+	BuilderTxSigningKey *ecdsa.PrivateKey // Signing key of builder coinbase to make transaction to validator
+	MaxMergedBundles    int
+	Blocklist           []common.Address `toml:",omitempty"`
 }
 
 // Miner creates blocks and searches for proof-of-work values.
 type Miner struct {
 	mux      *event.TypeMux
-	worker   *worker
+	worker   *multiWorker
 	coinbase common.Address
 	eth      Backend
 	engine   consensus.Engine
@@ -70,6 +97,15 @@ type Miner struct {
 }
 
 func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, isLocalBlock func(header *types.Header) bool) *Miner {
+	if config.BuilderTxSigningKey == nil {
+		key := os.Getenv("BUILDER_TX_SIGNING_KEY")
+		if key, err := crypto.HexToECDSA(strings.TrimPrefix(key, "0x")); err != nil {
+			log.Error("Error parsing builder signing key from env", "err", err)
+		} else {
+			config.BuilderTxSigningKey = key
+		}
+	}
+
 	miner := &Miner{
 		eth:     eth,
 		mux:     mux,
@@ -77,7 +113,7 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even
 		exitCh:  make(chan struct{}),
 		startCh: make(chan common.Address),
 		stopCh:  make(chan struct{}),
-		worker:  newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, true),
+		worker:  newMultiWorker(config, chainConfig, engine, eth, mux, isLocalBlock, true),
 	}
 	miner.wg.Add(1)
 	go miner.update()
@@ -189,7 +225,7 @@ func (miner *Miner) SetRecommitInterval(interval time.Duration) {
 
 // Pending returns the currently pending block and associated state.
 func (miner *Miner) Pending() (*types.Block, *state.StateDB) {
-	return miner.worker.pending()
+	return miner.worker.regularWorker.pending()
 }
 
 // PendingBlock returns the currently pending block.
@@ -198,7 +234,7 @@ func (miner *Miner) Pending() (*types.Block, *state.StateDB) {
 // simultaneously, please use Pending(), as the pending state can
 // change between multiple method calls
 func (miner *Miner) PendingBlock() *types.Block {
-	return miner.worker.pendingBlock()
+	return miner.worker.regularWorker.pendingBlock()
 }
 
 // PendingBlockAndReceipts returns the currently pending block and corresponding receipts.
@@ -237,29 +273,24 @@ func (miner *Miner) DisablePreseal() {
 // SubscribePendingLogs starts delivering logs from pending transactions
 // to the given channel.
 func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscription {
-	return miner.worker.pendingLogsFeed.Subscribe(ch)
+	return miner.worker.regularWorker.pendingLogsFeed.Subscribe(ch)
 }
 
+// Accepts the block, time at which orders were taken, bundles which were used to build the block and all bundles that were considered for the block
+type BlockHookFn = func(*types.Block, time.Time, []types.SimulatedBundle, []types.SimulatedBundle)
+
 // GetSealingBlockAsync requests to generate a sealing block according to the
 // given parameters. Regardless of whether the generation is successful or not,
 // there is always a result that will be returned through the result channel.
 // The difference is that if the execution fails, the returned result is nil
 // and the concrete error is dropped silently.
-func (miner *Miner) GetSealingBlockAsync(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, noTxs bool) (chan *types.Block, error) {
-	resCh, _, err := miner.worker.getSealingBlock(parent, timestamp, coinbase, random, noTxs)
-	if err != nil {
-		return nil, err
-	}
-	return resCh, nil
+func (miner *Miner) GetSealingBlockAsync(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, noTxs bool, blockHook BlockHookFn) (chan *types.Block, error) {
+	return miner.worker.GetSealingBlockAsync(parent, timestamp, coinbase, gasLimit, random, noTxs, false, blockHook)
 }
 
 // GetSealingBlockSync creates a sealing block according to the given parameters.
 // If the generation is failed or the underlying work is already closed, an error
 // will be returned.
-func (miner *Miner) GetSealingBlockSync(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, noTxs bool) (*types.Block, error) {
-	resCh, errCh, err := miner.worker.getSealingBlock(parent, timestamp, coinbase, random, noTxs)
-	if err != nil {
-		return nil, err
-	}
-	return <-resCh, <-errCh
+func (miner *Miner) GetSealingBlockSync(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, noTxs bool, blockHook BlockHookFn) (*types.Block, error) {
+	return miner.worker.GetSealingBlockSync(parent, timestamp, coinbase, gasLimit, random, noTxs, false, blockHook)
 }
diff --git a/miner/multi_worker.go b/miner/multi_worker.go
new file mode 100644
index 0000000000..45ffb67f07
--- /dev/null
+++ b/miner/multi_worker.go
@@ -0,0 +1,209 @@
+package miner
+
+import (
+	"errors"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/consensus"
+	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/event"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/params"
+)
+
+type multiWorker struct {
+	workers       []*worker
+	regularWorker *worker
+}
+
+func (w *multiWorker) stop() {
+	for _, worker := range w.workers {
+		worker.stop()
+	}
+}
+
+func (w *multiWorker) start() {
+	for _, worker := range w.workers {
+		worker.start()
+	}
+}
+
+func (w *multiWorker) close() {
+	for _, worker := range w.workers {
+		worker.close()
+	}
+}
+
+func (w *multiWorker) isRunning() bool {
+	for _, worker := range w.workers {
+		if worker.isRunning() {
+			return true
+		}
+	}
+	return false
+}
+
+// pendingBlockAndReceipts returns pending block and corresponding receipts from the `regularWorker`
+func (w *multiWorker) pendingBlockAndReceipts() (*types.Block, types.Receipts) {
+	// return a snapshot to avoid contention on currentMu mutex
+	return w.regularWorker.pendingBlockAndReceipts()
+}
+
+func (w *multiWorker) setGasCeil(ceil uint64) {
+	for _, worker := range w.workers {
+		worker.setGasCeil(ceil)
+	}
+}
+
+func (w *multiWorker) setExtra(extra []byte) {
+	for _, worker := range w.workers {
+		worker.setExtra(extra)
+	}
+}
+
+func (w *multiWorker) setRecommitInterval(interval time.Duration) {
+	for _, worker := range w.workers {
+		worker.setRecommitInterval(interval)
+	}
+}
+
+func (w *multiWorker) setEtherbase(addr common.Address) {
+	for _, worker := range w.workers {
+		worker.setEtherbase(addr)
+	}
+}
+
+func (w *multiWorker) enablePreseal() {
+	for _, worker := range w.workers {
+		worker.enablePreseal()
+	}
+}
+
+func (w *multiWorker) disablePreseal() {
+	for _, worker := range w.workers {
+		worker.disablePreseal()
+	}
+}
+
+type resChPair struct {
+	resCh chan *types.Block
+	errCh chan error
+}
+
+func (w *multiWorker) GetSealingBlockAsync(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, noTxs bool, noExtra bool, blockHook BlockHookFn) (chan *types.Block, error) {
+	resChans := []resChPair{}
+
+	for _, worker := range w.workers {
+		resCh, errCh, err := worker.getSealingBlock(parent, timestamp, coinbase, gasLimit, random, noTxs, noExtra, blockHook)
+		if err != nil {
+			log.Error("could not start async block construction", "isFlashbotsWorker", worker.flashbots.isFlashbots, "#bundles", worker.flashbots.maxMergedBundles)
+			continue
+		}
+		resChans = append(resChans, resChPair{resCh, errCh})
+	}
+
+	if len(resChans) == 0 {
+		return nil, errors.New("no worker could start async block construction")
+	}
+
+	resCh := make(chan *types.Block)
+
+	go func(resCh chan *types.Block) {
+		var res *types.Block = nil
+		for _, chPair := range resChans {
+			err := <-chPair.errCh
+			if err != nil {
+				log.Error("could not generate block", "err", err)
+				continue
+			}
+			newBlock := <-chPair.resCh
+			if res == nil || (newBlock != nil && newBlock.Profit.Cmp(res.Profit) > 0) {
+				res = newBlock
+			}
+		}
+		resCh <- res
+	}(resCh)
+
+	return resCh, nil
+}
+
+func (w *multiWorker) GetSealingBlockSync(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, noTxs bool, noExtra bool, blockHook BlockHookFn) (*types.Block, error) {
+	resCh, err := w.GetSealingBlockAsync(parent, timestamp, coinbase, gasLimit, random, noTxs, noExtra, blockHook)
+	if err != nil {
+		return nil, err
+	}
+	res := <-resCh
+	if res == nil {
+		return nil, errors.New("no viable blocks created")
+	}
+	return res, nil
+}
+
+func newMultiWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker {
+	if config.AlgoType != ALGO_MEV_GETH {
+		return newMultiWorkerGreedy(config, chainConfig, engine, eth, mux, isLocalBlock, init)
+	} else {
+		return newMultiWorkerMevGeth(config, chainConfig, engine, eth, mux, isLocalBlock, init)
+	}
+}
+
+func newMultiWorkerGreedy(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker {
+	queue := make(chan *task)
+
+	greedyWorker := newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{
+		isFlashbots:      true,
+		queue:            queue,
+		algoType:         config.AlgoType,
+		maxMergedBundles: config.MaxMergedBundles,
+		bundleCache:      NewBundleCache(),
+	})
+
+	log.Info("creating new greedy worker")
+	return &multiWorker{
+		regularWorker: greedyWorker,
+		workers:       []*worker{greedyWorker},
+	}
+}
+
+func newMultiWorkerMevGeth(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker {
+	queue := make(chan *task)
+
+	bundleCache := NewBundleCache()
+
+	regularWorker := newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{
+		isFlashbots:      false,
+		queue:            queue,
+		algoType:         ALGO_MEV_GETH,
+		maxMergedBundles: config.MaxMergedBundles,
+		bundleCache:      bundleCache,
+	})
+
+	workers := []*worker{regularWorker}
+	if config.AlgoType == ALGO_MEV_GETH {
+		for i := 1; i <= config.MaxMergedBundles; i++ {
+			workers = append(workers,
+				newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{
+					isFlashbots:      true,
+					queue:            queue,
+					algoType:         ALGO_MEV_GETH,
+					maxMergedBundles: i,
+					bundleCache:      bundleCache,
+				}))
+		}
+	}
+
+	log.Info("creating multi worker", "config.MaxMergedBundles", config.MaxMergedBundles, "workers", len(workers))
+	return &multiWorker{
+		regularWorker: regularWorker,
+		workers:       workers,
+	}
+}
+
+type flashbotsData struct {
+	isFlashbots      bool
+	queue            chan *task
+	maxMergedBundles int
+	algoType         AlgoType
+	bundleCache      *BundleCache
+}
diff --git a/miner/stress/beacon/main.go b/miner/stress/beacon/main.go
index 88af84c7fc..29e84b0c6f 100644
--- a/miner/stress/beacon/main.go
+++ b/miner/stress/beacon/main.go
@@ -141,9 +141,9 @@ func newNode(typ nodetype, genesis *core.Genesis, enodes []*enode.Node) *ethNode
 	}
 }
 
-func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) (*beacon.ExecutableDataV1, error) {
+func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64) (*beacon.ExecutableDataV1, *types.Block, error) {
 	if n.typ != eth2MiningNode {
-		return nil, errors.New("invalid node type")
+		return nil, nil, errors.New("invalid node type")
 	}
 	timestamp := uint64(time.Now().Unix())
 	if timestamp <= parentTimestamp {
@@ -161,9 +161,10 @@ func (n *ethNode) assembleBlock(parentHash common.Hash, parentTimestamp uint64)
 	}
 	payload, err := n.api.ForkchoiceUpdatedV1(fcState, &payloadAttribute)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
-	return n.api.GetPayloadV1(*payload.PayloadID)
+	data, err := n.api.GetPayloadV1(*payload.PayloadID)
+	return data, nil, err
 }
 
 func (n *ethNode) insertBlock(eb beacon.ExecutableDataV1) error {
@@ -358,7 +359,7 @@ func (mgr *nodeManager) run() {
 			if parentBlock.NumberU64() == 0 {
 				timestamp = uint64(time.Now().Unix()) - uint64(blockIntervalInt)
 			}
-			ed, err := producers[0].assembleBlock(hash, timestamp)
+			ed, _, err := producers[0].assembleBlock(hash, timestamp)
 			if err != nil {
 				log.Error("Failed to assemble the block", "err", err)
 				continue
diff --git a/miner/worker.go b/miner/worker.go
index 93fb6288bb..b0b032a225 100644
--- a/miner/worker.go
+++ b/miner/worker.go
@@ -20,6 +20,8 @@ import (
 	"errors"
 	"fmt"
 	"math/big"
+
+	"sort"
 	"sync"
 	"sync/atomic"
 	"time"
@@ -31,6 +33,8 @@ import (
 	"github.com/ethereum/go-ethereum/core"
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/types"
+	"github.com/ethereum/go-ethereum/crypto"
+	"github.com/ethereum/go-ethereum/eth/tracers/logger"
 	"github.com/ethereum/go-ethereum/event"
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/params"
@@ -39,7 +43,7 @@ import (
 
 const (
 	// resultQueueSize is the size of channel listening to sealing result.
-	resultQueueSize = 10
+	resultQueueSize = 20
 
 	// txChanSize is the size of channel listening to NewTxsEvent.
 	// The number is referenced from the size of tx pool.
@@ -78,8 +82,11 @@ const (
 )
 
 var (
+	errCouldNotApplyTransaction   = errors.New("could not apply transaction")
+	errBundleInterrupted          = errors.New("interrupt while applying bundles")
 	errBlockInterruptedByNewHead  = errors.New("new head arrived while building block")
 	errBlockInterruptedByRecommit = errors.New("recommit interrupt while building block")
+	errBlocklistViolation         = errors.New("blocklist violation")
 )
 
 // environment is the worker's current environment and holds all
@@ -93,6 +100,7 @@ type environment struct {
 	tcount    int            // tx count in cycle
 	gasPool   *core.GasPool  // available gas used to pack transactions
 	coinbase  common.Address
+	profit    *big.Int
 
 	header   *types.Header
 	txs      []*types.Transaction
@@ -109,6 +117,7 @@ func (env *environment) copy() *environment {
 		family:    env.family.Clone(),
 		tcount:    env.tcount,
 		coinbase:  env.coinbase,
+		profit:    new(big.Int).Set(env.profit),
 		header:    types.CopyHeader(env.header),
 		receipts:  copyReceipts(env.receipts),
 	}
@@ -152,6 +161,10 @@ type task struct {
 	state     *state.StateDB
 	block     *types.Block
 	createdAt time.Time
+
+	profit      *big.Int
+	isFlashbots bool
+	worker      int
 }
 
 const (
@@ -188,6 +201,7 @@ type worker struct {
 	engine      consensus.Engine
 	eth         Backend
 	chain       *core.BlockChain
+	blockList   map[common.Address]struct{}
 
 	// Feeds
 	pendingLogsFeed event.Feed
@@ -244,6 +258,8 @@ type worker struct {
 	// External functions
 	isLocalBlock func(header *types.Header) bool // Function used to determine whether the specified block is mined by local miner.
 
+	flashbots *flashbotsData
+
 	// Test hooks
 	newTaskHook  func(*task)                        // Method to call upon receiving a new sealing task.
 	skipSealHook func(*task) bool                   // Method to decide whether skipping the sealing.
@@ -251,7 +267,45 @@ type worker struct {
 	resubmitHook func(time.Duration, time.Duration) // Method to call upon updating resubmitting interval.
 }
 
-func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *worker {
+func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool, flashbots *flashbotsData) *worker {
+	var builderCoinbase common.Address
+	if config.BuilderTxSigningKey == nil {
+		log.Error("Builder tx signing key is not set")
+	} else {
+		builderCoinbase = crypto.PubkeyToAddress(config.BuilderTxSigningKey.PublicKey)
+	}
+
+	log.Info("new worker", "builderCoinbase", builderCoinbase.String())
+	exitCh := make(chan struct{})
+	taskCh := make(chan *task)
+	if flashbots.algoType == ALGO_MEV_GETH {
+		if flashbots.isFlashbots {
+			// publish to the flashbots queue
+			taskCh = flashbots.queue
+		} else {
+			// read from the flashbots queue
+			go func() {
+				for {
+					select {
+					case flashbotsTask := <-flashbots.queue:
+						select {
+						case taskCh <- flashbotsTask:
+						case <-exitCh:
+							return
+						}
+					case <-exitCh:
+						return
+					}
+				}
+			}()
+		}
+	}
+
+	blockList := make(map[common.Address]struct{})
+	for _, address := range config.Blocklist {
+		blockList[address] = struct{}{}
+	}
+
 	worker := &worker{
 		config:             config,
 		chainConfig:        chainConfig,
@@ -259,6 +313,7 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus
 		eth:                eth,
 		mux:                mux,
 		chain:              eth.BlockChain(),
+		blockList:          blockList,
 		isLocalBlock:       isLocalBlock,
 		localUncles:        make(map[common.Hash]*types.Block),
 		remoteUncles:       make(map[common.Hash]*types.Block),
@@ -267,15 +322,18 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus
 		txsCh:              make(chan core.NewTxsEvent, txChanSize),
 		chainHeadCh:        make(chan core.ChainHeadEvent, chainHeadChanSize),
 		chainSideCh:        make(chan core.ChainSideEvent, chainSideChanSize),
-		newWorkCh:          make(chan *newWorkReq),
+		newWorkCh:          make(chan *newWorkReq, 1),
 		getWorkCh:          make(chan *getWorkReq),
-		taskCh:             make(chan *task),
+		taskCh:             taskCh,
 		resultCh:           make(chan *types.Block, resultQueueSize),
-		exitCh:             make(chan struct{}),
+		exitCh:             exitCh,
 		startCh:            make(chan struct{}, 1),
 		resubmitIntervalCh: make(chan time.Duration),
 		resubmitAdjustCh:   make(chan *intervalAdjust, resubmitAdjustChanSize),
+		coinbase:           builderCoinbase,
+		flashbots:          flashbots,
 	}
+
 	// Subscribe NewTxsEvent for tx pool
 	worker.txsSub = eth.TxPool().SubscribeNewTxsEvent(worker.txsCh)
 	// Subscribe events for blockchain
@@ -289,11 +347,15 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus
 		recommit = minRecommitInterval
 	}
 
-	worker.wg.Add(4)
+	worker.wg.Add(2)
 	go worker.mainLoop()
 	go worker.newWorkLoop(recommit)
-	go worker.resultLoop()
-	go worker.taskLoop()
+	if flashbots.algoType != ALGO_MEV_GETH || !flashbots.isFlashbots {
+		// only mine if not flashbots
+		worker.wg.Add(2)
+		go worker.resultLoop()
+		go worker.taskLoop()
+	}
 
 	// Submit first work to initialize pending state.
 	if init {
@@ -417,26 +479,38 @@ func recalcRecommit(minRecommit, prev time.Duration, target float64, inc bool) t
 func (w *worker) newWorkLoop(recommit time.Duration) {
 	defer w.wg.Done()
 	var (
-		interrupt   *int32
-		minRecommit = recommit // minimal resubmit interval specified by user.
-		timestamp   int64      // timestamp for each round of sealing.
+		runningInterrupt *int32     // Running task interrupt
+		queuedInterrupt  *int32     // Queued task interrupt
+		minRecommit      = recommit // minimal resubmit interval specified by user.
+		timestamp        int64      // timestamp for each round of sealing.
 	)
 
 	timer := time.NewTimer(0)
 	defer timer.Stop()
 	<-timer.C // discard the initial tick
 
-	// commit aborts in-flight transaction execution with given signal and resubmits a new one.
+	// commit aborts in-flight transaction execution with highest seen signal and resubmits a new one
 	commit := func(noempty bool, s int32) {
-		if interrupt != nil {
-			atomic.StoreInt32(interrupt, s)
-		}
-		interrupt = new(int32)
 		select {
-		case w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp}:
 		case <-w.exitCh:
 			return
+		case queuedRequest := <-w.newWorkCh:
+			// Previously queued request wasn't started yet, update the request and resubmit
+			queuedRequest.noempty = queuedRequest.noempty || noempty
+			queuedRequest.timestamp = timestamp
+			w.newWorkCh <- queuedRequest // guaranteed to be nonblocking
+		default:
+			// Previously queued request has already started, cycle interrupt pointer and submit new work
+			runningInterrupt = queuedInterrupt
+			queuedInterrupt = new(int32)
+
+			w.newWorkCh <- &newWorkReq{interrupt: queuedInterrupt, noempty: noempty, timestamp: timestamp} // guaranteed to be nonblocking
+		}
+
+		if runningInterrupt != nil && s > atomic.LoadInt32(runningInterrupt) {
+			atomic.StoreInt32(runningInterrupt, s)
 		}
+
 		timer.Reset(recommit)
 		atomic.StoreInt32(&w.newTxs, 0)
 	}
@@ -531,17 +605,22 @@ func (w *worker) mainLoop() {
 	for {
 		select {
 		case req := <-w.newWorkCh:
-			w.commitWork(req.interrupt, req.noempty, req.timestamp)
+			// Don't start if the work has already been interrupted
+			if req.interrupt == nil || atomic.LoadInt32(req.interrupt) == commitInterruptNone {
+				w.commitWork(req.interrupt, req.noempty, req.timestamp)
+			}
 
 		case req := <-w.getWorkCh:
-			block, err := w.generateWork(req.params)
-			if err != nil {
-				req.err <- err
-				req.result <- nil
-			} else {
-				req.err <- nil
-				req.result <- block
-			}
+			go func() {
+				block, err := w.generateWork(req.params)
+				if err != nil {
+					req.err <- err
+					req.result <- nil
+				} else {
+					req.err <- nil
+					req.result <- block
+				}
+			}()
 		case ev := <-w.chainSideCh:
 			// Short circuit for duplicate side blocks
 			if _, exist := w.localUncles[ev.Block.Hash()]; exist {
@@ -595,7 +674,7 @@ func (w *worker) mainLoop() {
 					acc, _ := types.Sender(w.current.signer, tx)
 					txs[acc] = append(txs[acc], tx)
 				}
-				txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, w.current.header.BaseFee)
+				txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, nil, w.current.header.BaseFee)
 				tcount := w.current.tcount
 				w.commitTransactions(w.current, txset, nil)
 
@@ -634,6 +713,9 @@ func (w *worker) taskLoop() {
 	var (
 		stopCh chan struct{}
 		prev   common.Hash
+
+		prevParentHash common.Hash
+		prevProfit     *big.Int
 	)
 
 	// interrupt aborts the in-flight sealing task.
@@ -654,10 +736,20 @@ func (w *worker) taskLoop() {
 			if sealHash == prev {
 				continue
 			}
+
+			taskParentHash := task.block.Header().ParentHash
+			// reject new tasks which don't profit
+			if taskParentHash == prevParentHash &&
+				prevProfit != nil && task.profit.Cmp(prevProfit) < 0 {
+				continue
+			}
+			prevParentHash = taskParentHash
+			prevProfit = task.profit
+
 			// Interrupt previous sealing operation
 			interrupt()
 			stopCh, prev = make(chan struct{}), sealHash
-
+			log.Info("Proposed miner block", "blockNumber", task.block.Number(), "profit", ethIntToFloat(prevProfit), "isFlashbots", task.isFlashbots, "sealhash", sealHash, "parentHash", prevParentHash, "worker", task.worker)
 			if w.skipSealHook != nil && w.skipSealHook(task) {
 				continue
 			}
@@ -665,7 +757,7 @@ func (w *worker) taskLoop() {
 			w.pendingTasks[sealHash] = task
 			w.pendingMu.Unlock()
 
-			if err := w.engine.Seal(w.chain, task.block, w.resultCh, stopCh); err != nil {
+			if err := w.engine.Seal(w.chain, task.block, task.profit, w.resultCh, stopCh); err != nil {
 				log.Warn("Block sealing failed", "err", err)
 				w.pendingMu.Lock()
 				delete(w.pendingTasks, sealHash)
@@ -770,6 +862,7 @@ func (w *worker) makeEnv(parent *types.Block, header *types.Header, coinbase com
 		family:    mapset.NewSet(),
 		header:    header,
 		uncles:    make(map[common.Hash]*types.Header),
+		profit:    new(big.Int),
 	}
 	// when 08 is processed ancestors contain 07 (quick block)
 	for _, ancestor := range w.chain.GetBlocksFromHash(parent.Hash(), 7) {
@@ -781,6 +874,7 @@ func (w *worker) makeEnv(parent *types.Block, header *types.Header, coinbase com
 	}
 	// Keep track of transactions which return errors so they can be removed
 	env.tcount = 0
+	env.gasPool = new(core.GasPool).AddGas(header.GasLimit)
 	return env, nil
 }
 
@@ -823,19 +917,162 @@ func (w *worker) updateSnapshot(env *environment) {
 }
 
 func (w *worker) commitTransaction(env *environment, tx *types.Transaction) ([]*types.Log, error) {
-	snap := env.state.Snapshot()
+	gasPool := *env.gasPool
+	envGasUsed := env.header.GasUsed
+	stateDB := env.state
 
-	receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, *w.chain.GetVMConfig())
+	// It's important to copy then .Prepare() - don't reorder.
+	stateDB.Prepare(tx.Hash(), env.tcount)
+
+	snapshot := stateDB.Snapshot()
+
+	gasPrice, err := tx.EffectiveGasTip(env.header.BaseFee)
 	if err != nil {
-		env.state.RevertToSnapshot(snap)
 		return nil, err
 	}
+
+	var tracer *logger.AccountTouchTracer
+	var hook func() error
+	config := *w.chain.GetVMConfig()
+	if len(w.blockList) != 0 {
+		tracer = logger.NewAccountTouchTracer()
+		config.Tracer = tracer
+		config.Debug = true
+		hook = func() error {
+			for _, address := range tracer.TouchedAddresses() {
+				if _, in := w.blockList[address]; in {
+					return errBlocklistViolation
+				}
+			}
+			return nil
+		}
+	}
+
+	receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, &gasPool, stateDB, env.header, tx, &envGasUsed, config, hook)
+	if err != nil {
+		stateDB.RevertToSnapshot(snapshot)
+		return nil, err
+	}
+
+	*env.gasPool = gasPool
+	env.header.GasUsed = envGasUsed
+	env.state = stateDB
+
 	env.txs = append(env.txs, tx)
 	env.receipts = append(env.receipts, receipt)
 
+	gasUsed := new(big.Int).SetUint64(receipt.GasUsed)
+	env.profit.Add(env.profit, gasUsed.Mul(gasUsed, gasPrice))
+
 	return receipt.Logs, nil
 }
 
+func (w *worker) commitBundle(env *environment, txs types.Transactions, interrupt *int32) error {
+	gasLimit := env.header.GasLimit
+	if env.gasPool == nil {
+		env.gasPool = new(core.GasPool).AddGas(gasLimit)
+	}
+
+	var coalescedLogs []*types.Log
+
+	for _, tx := range txs {
+		// In the following three cases, we will interrupt the execution of the transaction.
+		// (1) new head block event arrival, the interrupt signal is 1
+		// (2) worker start or restart, the interrupt signal is 1
+		// (3) worker recreate the sealing block with any newly arrived transactions, the interrupt signal is 2.
+		// Discard the interrupted work, since it is incomplete and contains partial bundles
+		if interrupt != nil && atomic.LoadInt32(interrupt) != commitInterruptNone {
+			// Notify resubmit loop to increase resubmitting interval due to too frequent commits.
+			if atomic.LoadInt32(interrupt) == commitInterruptResubmit {
+				ratio := float64(gasLimit-env.gasPool.Gas()) / float64(gasLimit)
+				if ratio < 0.1 {
+					ratio = 0.1
+				}
+				w.resubmitAdjustCh <- &intervalAdjust{
+					ratio: ratio,
+					inc:   true,
+				}
+			}
+			return errBundleInterrupted
+		}
+		// If we don't have enough gas for any further transactions discard the block
+		// since not all bundles of the were applied
+		if env.gasPool.Gas() < params.TxGas {
+			log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas)
+			return errCouldNotApplyTransaction
+		}
+
+		// Error may be ignored here. The error has already been checked
+		// during transaction acceptance is the transaction pool.
+		//
+		// We use the eip155 signer regardless of the current hf.
+		from, _ := types.Sender(env.signer, tx)
+		// Check whether the tx is replay protected. If we're not in the EIP155 hf
+		// phase, start ignoring the sender until we do.
+		if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) {
+			log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block)
+			return errCouldNotApplyTransaction
+		}
+
+		logs, err := w.commitTransaction(env, tx)
+		switch {
+		case errors.Is(err, core.ErrGasLimitReached):
+			// Pop the current out-of-gas transaction without shifting in the next from the account
+			log.Trace("Gas limit exceeded for current block", "sender", from)
+			return errCouldNotApplyTransaction
+
+		case errors.Is(err, core.ErrNonceTooLow):
+			// New head notification data race between the transaction pool and miner, shift
+			log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce())
+			return errCouldNotApplyTransaction
+
+		case errors.Is(err, core.ErrNonceTooHigh):
+			// Reorg notification data race between the transaction pool and miner, skip account =
+			log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce())
+			return errCouldNotApplyTransaction
+
+		case errors.Is(err, nil):
+			// Everything ok, collect the logs and shift in the next transaction from the same account
+			coalescedLogs = append(coalescedLogs, logs...)
+			env.tcount++
+			continue
+
+		case errors.Is(err, core.ErrTxTypeNotSupported):
+			// Pop the unsupported transaction without shifting in the next from the account
+			log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type())
+			return errCouldNotApplyTransaction
+
+		default:
+			// Strange error, discard the transaction and get the next in line (note, the
+			// nonce-too-high clause will prevent us from executing in vain).
+			log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err)
+			return errCouldNotApplyTransaction
+		}
+	}
+
+	if !w.isRunning() && len(coalescedLogs) > 0 {
+		// We don't push the pendingLogsEvent while we are sealing. The reason is that
+		// when we are sealing, the worker will regenerate a sealing block every 3 seconds.
+		// In order to avoid pushing the repeated pendingLog, we disable the pending log pushing.
+
+		// make a copy, the state caches the logs and these logs get "upgraded" from pending to mined
+		// logs by filling in the block hash when the block was mined by the local miner. This can
+		// cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed.
+		cpy := make([]*types.Log, len(coalescedLogs))
+		for i, l := range coalescedLogs {
+			cpy[i] = new(types.Log)
+			*cpy[i] = *l
+		}
+		w.pendingLogsFeed.Send(cpy)
+	}
+	// Notify resubmit loop to decrease resubmitting interval if current interval is larger
+	// than the user-specified one.
+	if interrupt != nil {
+		w.resubmitAdjustCh <- &intervalAdjust{inc: false}
+	}
+	return nil
+}
+
 func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByPriceAndNonce, interrupt *int32) error {
 	gasLimit := env.header.GasLimit
 	if env.gasPool == nil {
@@ -871,10 +1108,14 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP
 			break
 		}
 		// Retrieve the next transaction and abort if all done
-		tx := txs.Peek()
-		if tx == nil {
+		order := txs.Peek()
+		if order == nil {
 			break
 		}
+		tx := order.Tx
+		if tx == nil {
+			continue
+		}
 		// Error may be ignored here. The error has already been checked
 		// during transaction acceptance is the transaction pool.
 		//
@@ -888,8 +1129,6 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP
 			txs.Pop()
 			continue
 		}
-		// Start executing the transaction
-		env.state.Prepare(tx.Hash(), env.tcount)
 
 		logs, err := w.commitTransaction(env, tx)
 		switch {
@@ -956,10 +1195,12 @@ type generateParams struct {
 	forceTime  bool           // Flag whether the given timestamp is immutable or not
 	parentHash common.Hash    // Parent block hash, empty means the latest chain head
 	coinbase   common.Address // The fee recipient address for including transaction
+	gasLimit   uint64         // The validator's requested gas limit target
 	random     common.Hash    // The randomness generated by beacon chain, empty before the merge
 	noUncle    bool           // Flag whether the uncle block inclusion is allowed
 	noExtra    bool           // Flag whether the extra field assignment is allowed
 	noTxs      bool           // Flag whether an empty block without any transaction is expected
+	onBlock    BlockHookFn    // Callback to call for each produced block
 }
 
 // prepareWork constructs the sealing task according to the given parameters,
@@ -987,11 +1228,15 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) {
 		timestamp = parent.Time() + 1
 	}
 	// Construct the sealing block header, set the extra field if it's allowed
+	gasTarget := genParams.gasLimit
+	if gasTarget == 0 {
+		gasTarget = w.config.GasCeil
+	}
 	num := parent.Number()
 	header := &types.Header{
 		ParentHash: parent.Hash(),
 		Number:     num.Add(num, common.Big1),
-		GasLimit:   core.CalcGasLimit(parent.GasLimit(), w.config.GasCeil),
+		GasLimit:   core.CalcGasLimit(parent.GasLimit(), gasTarget),
 		Time:       timestamp,
 		Coinbase:   genParams.coinbase,
 	}
@@ -1007,7 +1252,7 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) {
 		header.BaseFee = misc.CalcBaseFee(w.chainConfig, parent.Header())
 		if !w.chainConfig.IsLondon(parent.Number()) {
 			parentGasLimit := parent.GasLimit() * params.ElasticityMultiplier
-			header.GasLimit = core.CalcGasLimit(parentGasLimit, w.config.GasCeil)
+			header.GasLimit = core.CalcGasLimit(parentGasLimit, gasTarget)
 		}
 	}
 	// Run the consensus preparation with the default or customized consensus engine.
@@ -1047,7 +1292,8 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) {
 // fillTransactions retrieves the pending transactions from the txpool and fills them
 // into the given sealing block. The transaction selection and ordering strategy can
 // be customized with the plugin in the future.
-func (w *worker) fillTransactions(interrupt *int32, env *environment) error {
+// Returns error if any, otherwise the bundles that made it into the block and all bundles that passed simulation
+func (w *worker) fillTransactions(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle) {
 	// Split the pending transactions into locals and remotes
 	// Fill the block with all available pending transactions.
 	pending := w.eth.TxPool().Pending(true)
@@ -1058,33 +1304,187 @@ func (w *worker) fillTransactions(interrupt *int32, env *environment) error {
 			localTxs[account] = txs
 		}
 	}
+
+	var blockBundles []types.SimulatedBundle
+	var allBundles []types.SimulatedBundle
+	if w.flashbots.isFlashbots {
+		bundles := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time)
+
+		var bundleTxs types.Transactions
+		var resultingBundle simulatedBundle
+		var mergedBundles []types.SimulatedBundle
+		var numBundles int
+		var err error
+		bundleTxs, resultingBundle, mergedBundles, numBundles, allBundles, err = w.generateFlashbotsBundle(env, bundles, pending)
+		if err != nil {
+			log.Error("Failed to generate flashbots bundle", "err", err)
+			return err, nil, nil
+		}
+		log.Info("Flashbots bundle", "ethToCoinbase", ethIntToFloat(resultingBundle.TotalEth), "gasUsed", resultingBundle.TotalGasUsed, "bundleScore", resultingBundle.MevGasPrice, "bundleLength", len(bundleTxs), "numBundles", numBundles, "worker", w.flashbots.maxMergedBundles)
+		if len(bundleTxs) == 0 {
+			return errors.New("no bundles to apply"), nil, nil
+		}
+		if err := w.commitBundle(env, bundleTxs, interrupt); err != nil {
+			return err, nil, nil
+		}
+		blockBundles = mergedBundles
+		env.profit.Add(env.profit, resultingBundle.EthSentToCoinbase)
+	}
+
 	if len(localTxs) > 0 {
-		txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, env.header.BaseFee)
+		txs := types.NewTransactionsByPriceAndNonce(env.signer, localTxs, nil, env.header.BaseFee)
 		if err := w.commitTransactions(env, txs, interrupt); err != nil {
-			return err
+			return err, nil, nil
 		}
 	}
 	if len(remoteTxs) > 0 {
-		txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, env.header.BaseFee)
+		txs := types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, nil, env.header.BaseFee)
 		if err := w.commitTransactions(env, txs, interrupt); err != nil {
-			return err
+			return err, nil, nil
 		}
 	}
-	return nil
+
+	return nil, blockBundles, allBundles
+}
+
+// fillTransactionsAlgoWorker retrieves the pending transactions and bundles from the txpool and fills them
+// into the given sealing block.
+// Returns error if any, otherwise the bundles that made it into the block and all bundles that passed simulation
+func (w *worker) fillTransactionsAlgoWorker(interrupt *int32, env *environment) (error, []types.SimulatedBundle, []types.SimulatedBundle) {
+	// Split the pending transactions into locals and remotes
+	// Fill the block with all available pending transactions.
+	pending := w.eth.TxPool().Pending(true)
+	bundlesToConsider, err := w.getSimulatedBundles(env)
+	if err != nil {
+		return err, nil, nil
+	}
+
+	builder := newGreedyBuilder(w.chain, w.chainConfig, w.blockList, env, interrupt)
+	newEnv, blockBundles := builder.buildBlock(bundlesToConsider, pending)
+	*env = *newEnv
+
+	return nil, blockBundles, bundlesToConsider
+}
+
+func (w *worker) getSimulatedBundles(env *environment) ([]types.SimulatedBundle, error) {
+	if !w.flashbots.isFlashbots {
+		return nil, nil
+	}
+
+	bundles := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time)
+
+	// TODO: consider interrupt
+	simBundles, err := w.simulateBundles(env, bundles, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */
+	if err != nil {
+		log.Error("Failed to simulate flashbots bundles", "err", err)
+		return nil, err
+	}
+
+	return simBundles, nil
 }
 
 // generateWork generates a sealing block based on the given parameters.
 func (w *worker) generateWork(params *generateParams) (*types.Block, error) {
+	start := time.Now()
+	validatorCoinbase := params.coinbase
+	// Set builder coinbase to be passed to beacon header
+	params.coinbase = w.coinbase
+
 	work, err := w.prepareWork(params)
 	if err != nil {
 		return nil, err
 	}
 	defer work.discard()
 
-	if !params.noTxs {
-		w.fillTransactions(nil, work)
+	finalizeFn := func(env *environment, orderCloseTime time.Time, blockBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle) (*types.Block, error) {
+		block, err := w.finalizeBlock(env, validatorCoinbase)
+		if err != nil {
+			log.Error("could not finalize block", "err", err)
+			return nil, err
+		}
+
+		log.Info("Block finalized and assembled", "blockProfit", ethIntToFloat(block.Profit), "txs", len(env.txs), "bundles", len(blockBundles), "gasUsed", block.GasUsed(), "time", time.Since(start))
+		if params.onBlock != nil {
+			go params.onBlock(block, orderCloseTime, blockBundles, allBundles)
+		}
+
+		return block, nil
+	}
+
+	if params.noTxs {
+		return finalizeFn(work, time.Now(), nil, nil)
+	}
+
+	paymentTxReserve, err := w.proposerTxPrepare(work, &validatorCoinbase)
+	if err != nil {
+		return nil, err
+	}
+
+	var blockBundles []types.SimulatedBundle
+	var allBundles []types.SimulatedBundle
+	orderCloseTime := time.Now()
+	switch w.flashbots.algoType {
+	case ALGO_GREEDY:
+		err, blockBundles, allBundles = w.fillTransactionsAlgoWorker(nil, work)
+	case ALGO_MEV_GETH:
+		err, blockBundles, allBundles = w.fillTransactions(nil, work)
+	default:
+		err, blockBundles, allBundles = w.fillTransactions(nil, work)
+	}
+
+	if err != nil {
+		return nil, err
 	}
-	return w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts)
+
+	err = w.proposerTxCommit(work, &validatorCoinbase, paymentTxReserve)
+	if err != nil {
+		return nil, err
+	}
+
+	return finalizeFn(work, orderCloseTime, blockBundles, allBundles)
+}
+
+func (w *worker) finalizeBlock(work *environment, validatorCoinbase common.Address) (*types.Block, error) {
+	block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts)
+	if err != nil {
+		return nil, err
+	}
+
+	block.Profit = big.NewInt(0)
+
+	if w.config.BuilderTxSigningKey == nil {
+		return block, nil
+	}
+
+	blockProfit, err := w.checkProposerPayment(work, validatorCoinbase)
+	if err != nil {
+		return nil, err
+	}
+
+	block.Profit = blockProfit
+	return block, nil
+}
+
+func (w *worker) checkProposerPayment(work *environment, validatorCoinbase common.Address) (*big.Int, error) {
+	if len(work.txs) == 0 {
+		return nil, errors.New("no proposer payment tx")
+	} else if len(work.receipts) == 0 {
+		return nil, errors.New("no proposer payment receipt")
+	}
+
+	lastTx := work.txs[len(work.txs)-1]
+	receipt := work.receipts[len(work.receipts)-1]
+	if receipt.TxHash != lastTx.Hash() || receipt.Status != types.ReceiptStatusSuccessful {
+		log.Error("proposer payment not successful!", "lastTx", lastTx, "receipt", receipt)
+		return nil, errors.New("last transaction is not proposer payment")
+	}
+	lastTxTo := lastTx.To()
+	if lastTxTo == nil || *lastTxTo != validatorCoinbase {
+		log.Error("last transaction is not to the proposer!", "lastTx", lastTx)
+		return nil, errors.New("last transaction is not proposer payment")
+	}
+
+	return new(big.Int).Set(lastTx.Value()), nil
 }
 
 // commitWork generates several new sealing tasks based on the parent block
@@ -1115,8 +1515,8 @@ func (w *worker) commitWork(interrupt *int32, noempty bool, timestamp int64) {
 	}
 
 	// Fill pending transactions from the txpool
-	err = w.fillTransactions(interrupt, work)
-	if errors.Is(err, errBlockInterruptedByNewHead) {
+	err, _, _ = w.fillTransactions(interrupt, work)
+	if err != nil && !errors.Is(err, errBlockInterruptedByRecommit) {
 		work.discard()
 		return
 	}
@@ -1149,13 +1549,12 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti
 		// If we're post merge, just ignore
 		if !w.isTTDReached(block.Header()) {
 			select {
-			case w.taskCh <- &task{receipts: env.receipts, state: env.state, block: block, createdAt: time.Now()}:
+			case w.taskCh <- &task{receipts: env.receipts, state: env.state, block: block, createdAt: time.Now(), profit: env.profit, isFlashbots: w.flashbots.isFlashbots, worker: w.flashbots.maxMergedBundles}:
 				w.unconfirmed.Shift(block.NumberU64() - 1)
 				log.Info("Commit new sealing work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()),
-					"uncles", len(env.uncles), "txs", env.tcount,
-					"gas", block.GasUsed(), "fees", totalFees(block, env.receipts),
-					"elapsed", common.PrettyDuration(time.Since(start)))
-
+					"uncles", len(env.uncles), "txs", env.tcount, "gas", block.GasUsed(), "fees", totalFees(block, env.receipts),
+					"profit", ethIntToFloat(env.profit), "elapsed", common.PrettyDuration(time.Since(start)),
+					"isFlashbots", w.flashbots.isFlashbots, "worker", w.flashbots.maxMergedBundles)
 			case <-w.exitCh:
 				log.Info("Worker has exited")
 			}
@@ -1170,7 +1569,7 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti
 // getSealingBlock generates the sealing block based on the given parameters.
 // The generation result will be passed back via the given channel no matter
 // the generation itself succeeds or not.
-func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, noTxs bool) (chan *types.Block, chan error, error) {
+func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, noTxs bool, noExtra bool, blockHook BlockHookFn) (chan *types.Block, chan error, error) {
 	var (
 		resCh = make(chan *types.Block, 1)
 		errCh = make(chan error, 1)
@@ -1181,10 +1580,12 @@ func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase
 			forceTime:  true,
 			parentHash: parent,
 			coinbase:   coinbase,
+			gasLimit:   gasLimit,
 			random:     random,
 			noUncle:    true,
-			noExtra:    true,
+			noExtra:    noExtra,
 			noTxs:      noTxs,
+			onBlock:    blockHook,
 		},
 		result: resCh,
 		err:    errCh,
@@ -1204,6 +1605,230 @@ func (w *worker) isTTDReached(header *types.Header) bool {
 	return td != nil && ttd != nil && td.Cmp(ttd) >= 0
 }
 
+type simulatedBundle = types.SimulatedBundle
+
+func (w *worker) generateFlashbotsBundle(env *environment, bundles []types.MevBundle, pendingTxs map[common.Address]types.Transactions) (types.Transactions, simulatedBundle, []types.SimulatedBundle, int, []types.SimulatedBundle, error) {
+	simulatedBundles, err := w.simulateBundles(env, bundles, pendingTxs)
+	if err != nil {
+		return nil, simulatedBundle{}, nil, 0, nil, err
+	}
+
+	sort.SliceStable(simulatedBundles, func(i, j int) bool {
+		return simulatedBundles[j].MevGasPrice.Cmp(simulatedBundles[i].MevGasPrice) < 0
+	})
+
+	bundleTxs, bundle, mergedBundles, numBundles, err := w.mergeBundles(env, simulatedBundles, pendingTxs)
+	return bundleTxs, bundle, mergedBundles, numBundles, simulatedBundles, err
+}
+
+func (w *worker) mergeBundles(env *environment, bundles []simulatedBundle, pendingTxs map[common.Address]types.Transactions) (types.Transactions, simulatedBundle, []types.SimulatedBundle, int, error) {
+	mergedBundles := []types.SimulatedBundle{}
+	finalBundle := types.Transactions{}
+
+	currentState := env.state.Copy()
+	gasPool := new(core.GasPool).AddGas(env.header.GasLimit)
+
+	var prevState *state.StateDB
+	var prevGasPool *core.GasPool
+
+	mergedBundle := simulatedBundle{
+		TotalEth:          new(big.Int),
+		EthSentToCoinbase: new(big.Int),
+	}
+
+	count := 0
+	for _, bundle := range bundles {
+		prevState = currentState.Copy()
+		prevGasPool = new(core.GasPool).AddGas(gasPool.Gas())
+
+		// the floor gas price is 99/100 what was simulated at the top of the block
+		floorGasPrice := new(big.Int).Mul(bundle.MevGasPrice, big.NewInt(99))
+		floorGasPrice = floorGasPrice.Div(floorGasPrice, big.NewInt(100))
+
+		simmed, err := w.computeBundleGas(env, bundle.OriginalBundle, currentState, gasPool, pendingTxs, len(finalBundle))
+		if err != nil || simmed.MevGasPrice.Cmp(floorGasPrice) <= 0 {
+			currentState = prevState
+			gasPool = prevGasPool
+			continue
+		}
+
+		log.Info("Included bundle", "ethToCoinbase", ethIntToFloat(simmed.TotalEth), "gasUsed", simmed.TotalGasUsed, "bundleScore", simmed.MevGasPrice, "bundleLength", len(simmed.OriginalBundle.Txs), "worker", w.flashbots.maxMergedBundles)
+		mergedBundles = append(mergedBundles, simmed)
+		finalBundle = append(finalBundle, bundle.OriginalBundle.Txs...)
+		mergedBundle.TotalEth.Add(mergedBundle.TotalEth, simmed.TotalEth)
+		mergedBundle.EthSentToCoinbase.Add(mergedBundle.EthSentToCoinbase, simmed.EthSentToCoinbase)
+		mergedBundle.TotalGasUsed += simmed.TotalGasUsed
+		count++
+
+		if count >= w.flashbots.maxMergedBundles {
+			break
+		}
+	}
+
+	if len(finalBundle) == 0 || count != w.flashbots.maxMergedBundles {
+		return nil, simulatedBundle{}, nil, count, nil
+	}
+
+	return finalBundle, simulatedBundle{
+		MevGasPrice:       new(big.Int).Div(mergedBundle.TotalEth, new(big.Int).SetUint64(mergedBundle.TotalGasUsed)),
+		TotalEth:          mergedBundle.TotalEth,
+		EthSentToCoinbase: mergedBundle.EthSentToCoinbase,
+		TotalGasUsed:      mergedBundle.TotalGasUsed,
+	}, mergedBundles, count, nil
+}
+
+func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, pendingTxs map[common.Address]types.Transactions) ([]simulatedBundle, error) {
+	start := time.Now()
+	headerHash := env.header.Hash()
+	simCache := w.flashbots.bundleCache.GetBundleCache(headerHash)
+
+	simResult := make([]*simulatedBundle, len(bundles))
+
+	var wg sync.WaitGroup
+	for i, bundle := range bundles {
+		if simmed, ok := simCache.GetSimulatedBundle(bundle.Hash); ok {
+			simResult[i] = simmed
+			continue
+		}
+
+		wg.Add(1)
+		go func(idx int, bundle types.MevBundle, state *state.StateDB) {
+			defer wg.Done()
+			if len(bundle.Txs) == 0 {
+				return
+			}
+			gasPool := new(core.GasPool).AddGas(env.header.GasLimit)
+			simmed, err := w.computeBundleGas(env, bundle, state, gasPool, pendingTxs, 0)
+
+			if err != nil {
+				log.Trace("Error computing gas for a bundle", "error", err)
+				return
+			}
+			simResult[idx] = &simmed
+		}(i, bundle, env.state.Copy())
+	}
+
+	wg.Wait()
+
+	simCache.UpdateSimulatedBundles(simResult, bundles)
+
+	simulatedBundles := make([]simulatedBundle, 0, len(bundles))
+	for _, bundle := range simResult {
+		if bundle != nil {
+			simulatedBundles = append(simulatedBundles, *bundle)
+		}
+	}
+
+	log.Debug("Simulated bundles", "block", env.header.Number, "allBundles", len(bundles), "okBundles", len(simulatedBundles), "time", time.Since(start))
+	return simulatedBundles, nil
+}
+
+func containsHash(arr []common.Hash, match common.Hash) bool {
+	for _, elem := range arr {
+		if elem == match {
+			return true
+		}
+	}
+	return false
+}
+
+// Compute the adjusted gas price for a whole bundle
+// Done by calculating all gas spent, adding transfers to the coinbase, and then dividing by gas used
+func (w *worker) computeBundleGas(env *environment, bundle types.MevBundle, state *state.StateDB, gasPool *core.GasPool, pendingTxs map[common.Address]types.Transactions, currentTxCount int) (simulatedBundle, error) {
+	var totalGasUsed uint64 = 0
+	var tempGasUsed uint64
+	gasFees := new(big.Int)
+
+	ethSentToCoinbase := new(big.Int)
+
+	for i, tx := range bundle.Txs {
+		if env.header.BaseFee != nil && tx.Type() == 2 {
+			// Sanity check for extremely large numbers
+			if tx.GasFeeCap().BitLen() > 256 {
+				return simulatedBundle{}, core.ErrFeeCapVeryHigh
+			}
+			if tx.GasTipCap().BitLen() > 256 {
+				return simulatedBundle{}, core.ErrTipVeryHigh
+			}
+			// Ensure gasFeeCap is greater than or equal to gasTipCap.
+			if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 {
+				return simulatedBundle{}, core.ErrTipAboveFeeCap
+			}
+		}
+
+		state.Prepare(tx.Hash(), i+currentTxCount)
+		coinbaseBalanceBefore := state.GetBalance(env.coinbase)
+
+		config := *w.chain.GetVMConfig()
+		var tracer *logger.AccountTouchTracer
+		if len(w.blockList) != 0 {
+			tracer = logger.NewAccountTouchTracer()
+			config.Tracer = tracer
+			config.Debug = true
+		}
+		receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, gasPool, state, env.header, tx, &tempGasUsed, config, nil)
+		if err != nil {
+			return simulatedBundle{}, err
+		}
+		if receipt.Status == types.ReceiptStatusFailed && !containsHash(bundle.RevertingTxHashes, receipt.TxHash) {
+			return simulatedBundle{}, errors.New("failed tx")
+		}
+		if len(w.blockList) != 0 {
+			for _, address := range tracer.TouchedAddresses() {
+				if _, in := w.blockList[address]; in {
+					return simulatedBundle{}, errBlocklistViolation
+				}
+			}
+		}
+
+		totalGasUsed += receipt.GasUsed
+
+		from, err := types.Sender(env.signer, tx)
+		if err != nil {
+			return simulatedBundle{}, err
+		}
+
+		txInPendingPool := false
+		if accountTxs, ok := pendingTxs[from]; ok {
+			// check if tx is in pending pool
+			txNonce := tx.Nonce()
+
+			for _, accountTx := range accountTxs {
+				if accountTx.Nonce() == txNonce {
+					txInPendingPool = true
+					break
+				}
+			}
+		}
+
+		gasUsed := new(big.Int).SetUint64(receipt.GasUsed)
+		gasPrice, err := tx.EffectiveGasTip(env.header.BaseFee)
+		if err != nil {
+			return simulatedBundle{}, err
+		}
+		gasFeesTx := gasUsed.Mul(gasUsed, gasPrice)
+		coinbaseBalanceAfter := state.GetBalance(env.coinbase)
+		coinbaseDelta := big.NewInt(0).Sub(coinbaseBalanceAfter, coinbaseBalanceBefore)
+		coinbaseDelta.Sub(coinbaseDelta, gasFeesTx)
+		ethSentToCoinbase.Add(ethSentToCoinbase, coinbaseDelta)
+
+		if !txInPendingPool {
+			// If tx is not in pending pool, count the gas fees
+			gasFees.Add(gasFees, gasFeesTx)
+		}
+	}
+
+	totalEth := new(big.Int).Add(ethSentToCoinbase, gasFees)
+
+	return simulatedBundle{
+		MevGasPrice:       new(big.Int).Div(totalEth, new(big.Int).SetUint64(totalGasUsed)),
+		TotalEth:          totalEth,
+		EthSentToCoinbase: ethSentToCoinbase,
+		TotalGasUsed:      totalGasUsed,
+		OriginalBundle:    bundle,
+	}, nil
+}
+
 // copyReceipts makes a deep copy of the given receipts.
 func copyReceipts(receipts []*types.Receipt) []*types.Receipt {
 	result := make([]*types.Receipt, len(receipts))
@@ -1222,6 +1847,14 @@ func (w *worker) postSideBlock(event core.ChainSideEvent) {
 	}
 }
 
+// ethIntToFloat is for formatting a big.Int in wei to eth
+func ethIntToFloat(eth *big.Int) *big.Float {
+	if eth == nil {
+		return big.NewFloat(0)
+	}
+	return new(big.Float).Quo(new(big.Float).SetInt(eth), new(big.Float).SetInt(big.NewInt(params.Ether)))
+}
+
 // totalFees computes total consumed miner fees in ETH. Block transactions and receipts have to have the same order.
 func totalFees(block *types.Block, receipts []*types.Receipt) *big.Float {
 	feesWei := new(big.Int)
@@ -1229,5 +1862,62 @@ func totalFees(block *types.Block, receipts []*types.Receipt) *big.Float {
 		minerFee, _ := tx.EffectiveGasTip(block.BaseFee())
 		feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee))
 	}
-	return new(big.Float).Quo(new(big.Float).SetInt(feesWei), new(big.Float).SetInt(big.NewInt(params.Ether)))
+	return ethIntToFloat(feesWei)
+}
+
+type proposerTxReservation struct {
+	builderBalance *big.Int
+	reservedGas    uint64
+	isEOA          bool
+}
+
+func (w *worker) proposerTxPrepare(env *environment, validatorCoinbase *common.Address) (*proposerTxReservation, error) {
+	if validatorCoinbase == nil || w.config.BuilderTxSigningKey == nil {
+		return nil, nil
+	}
+
+	w.mu.Lock()
+	sender := w.coinbase
+	w.mu.Unlock()
+	builderBalance := env.state.GetBalance(sender)
+
+	chainData := chainData{w.chainConfig, w.chain, w.blockList}
+	gas, isEOA, err := estimatePayoutTxGas(env, sender, *validatorCoinbase, w.config.BuilderTxSigningKey, chainData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to estimate proposer payout gas: %w", err)
+	}
+
+	if err := env.gasPool.SubGas(gas); err != nil {
+		return nil, err
+	}
+
+	return &proposerTxReservation{
+		builderBalance: builderBalance,
+		reservedGas:    gas,
+		isEOA:          isEOA,
+	}, nil
+}
+
+func (w *worker) proposerTxCommit(env *environment, validatorCoinbase *common.Address, reserve *proposerTxReservation) error {
+	if reserve == nil || validatorCoinbase == nil {
+		return nil
+	}
+
+	w.mu.Lock()
+	sender := w.coinbase
+	w.mu.Unlock()
+	builderBalance := env.state.GetBalance(sender)
+
+	availableFunds := new(big.Int).Sub(builderBalance, reserve.builderBalance)
+	if availableFunds.Sign() <= 0 {
+		return errors.New("builder balance decreased")
+	}
+
+	env.gasPool.AddGas(reserve.reservedGas)
+	chainData := chainData{w.chainConfig, w.chain, w.blockList}
+	_, err := insertPayoutTx(env, sender, *validatorCoinbase, reserve.reservedGas, reserve.isEOA, availableFunds, w.config.BuilderTxSigningKey, chainData)
+	if err != nil {
+		return err
+	}
+	return nil
 }
diff --git a/miner/worker_test.go b/miner/worker_test.go
index ec5ba67e1c..6baeea4b86 100644
--- a/miner/worker_test.go
+++ b/miner/worker_test.go
@@ -17,6 +17,7 @@
 package miner
 
 import (
+	"crypto/ecdsa"
 	"errors"
 	"math/big"
 	"math/rand"
@@ -38,6 +39,7 @@ import (
 	"github.com/ethereum/go-ethereum/ethdb"
 	"github.com/ethereum/go-ethereum/event"
 	"github.com/ethereum/go-ethereum/params"
+	"github.com/stretchr/testify/require"
 )
 
 const (
@@ -60,6 +62,13 @@ var (
 	testBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey)
 	testBankFunds   = big.NewInt(1000000000000000000)
 
+	testAddress1Key, _ = crypto.GenerateKey()
+	testAddress1       = crypto.PubkeyToAddress(testAddress1Key.PublicKey)
+	testAddress2Key, _ = crypto.GenerateKey()
+	testAddress2       = crypto.PubkeyToAddress(testAddress2Key.PublicKey)
+	testAddress3Key, _ = crypto.GenerateKey()
+	testAddress3       = crypto.PubkeyToAddress(testAddress3Key.PublicKey)
+
 	testUserKey, _  = crypto.GenerateKey()
 	testUserAddress = crypto.PubkeyToAddress(testUserKey.PublicKey)
 
@@ -71,6 +80,8 @@ var (
 		Recommit: time.Second,
 		GasCeil:  params.GenesisGasLimit,
 	}
+
+	defaultGenesisAlloc = core.GenesisAlloc{testBankAddress: {Balance: testBankFunds}}
 )
 
 func init() {
@@ -117,10 +128,10 @@ type testWorkerBackend struct {
 	uncleBlock *types.Block
 }
 
-func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, n int) *testWorkerBackend {
+func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, alloc core.GenesisAlloc, n int) *testWorkerBackend {
 	var gspec = core.Genesis{
 		Config: chainConfig,
-		Alloc:  core.GenesisAlloc{testBankAddress: {Balance: testBankFunds}},
+		Alloc:  alloc,
 	}
 
 	switch e := engine.(type) {
@@ -187,21 +198,25 @@ func (b *testWorkerBackend) newRandomUncle() *types.Block {
 	return blocks[0]
 }
 
-func (b *testWorkerBackend) newRandomTx(creation bool) *types.Transaction {
+func (b *testWorkerBackend) newRandomTx(creation bool, to common.Address, amt int64, key *ecdsa.PrivateKey, additionalGasLimit uint64, gasPrice *big.Int) *types.Transaction {
 	var tx *types.Transaction
-	gasPrice := big.NewInt(10 * params.InitialBaseFee)
 	if creation {
-		tx, _ = types.SignTx(types.NewContractCreation(b.txPool.Nonce(testBankAddress), big.NewInt(0), testGas, gasPrice, common.FromHex(testCode)), types.HomesteadSigner{}, testBankKey)
+		tx, _ = types.SignTx(types.NewContractCreation(b.txPool.Nonce(crypto.PubkeyToAddress(key.PublicKey)), big.NewInt(0), testGas, gasPrice, common.FromHex(testCode)), types.HomesteadSigner{}, key)
 	} else {
-		tx, _ = types.SignTx(types.NewTransaction(b.txPool.Nonce(testBankAddress), testUserAddress, big.NewInt(1000), params.TxGas, gasPrice, nil), types.HomesteadSigner{}, testBankKey)
+		tx, _ = types.SignTx(types.NewTransaction(b.txPool.Nonce(crypto.PubkeyToAddress(key.PublicKey)), to, big.NewInt(amt), params.TxGas+additionalGasLimit, gasPrice, nil), types.HomesteadSigner{}, key)
 	}
 	return tx
 }
 
-func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*worker, *testWorkerBackend) {
-	backend := newTestWorkerBackend(t, chainConfig, engine, db, blocks)
+func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, alloc core.GenesisAlloc, blocks int) (*worker, *testWorkerBackend) {
+	backend := newTestWorkerBackend(t, chainConfig, engine, db, alloc, blocks)
 	backend.txPool.AddLocals(pendingTxs)
-	w := newWorker(testConfig, chainConfig, engine, backend, new(event.TypeMux), nil, false)
+	w := newWorker(testConfig, chainConfig, engine, backend, new(event.TypeMux), nil, false, &flashbotsData{
+		isFlashbots: testConfig.AlgoType != ALGO_MEV_GETH,
+		queue:       nil,
+		bundleCache: NewBundleCache(),
+		algoType:    testConfig.AlgoType,
+	})
 	w.setEtherbase(testBankAddress)
 	return w, backend
 }
@@ -230,7 +245,7 @@ func testGenerateBlockAndImport(t *testing.T, isClique bool) {
 	}
 
 	chainConfig.LondonBlock = big.NewInt(0)
-	w, b := newTestWorker(t, chainConfig, engine, db, 0)
+	w, b := newTestWorker(t, chainConfig, engine, db, defaultGenesisAlloc, 0)
 	defer w.close()
 
 	// This test chain imports the mined blocks.
@@ -252,8 +267,8 @@ func testGenerateBlockAndImport(t *testing.T, isClique bool) {
 	w.start()
 
 	for i := 0; i < 5; i++ {
-		b.txPool.AddLocal(b.newRandomTx(true))
-		b.txPool.AddLocal(b.newRandomTx(false))
+		b.txPool.AddLocal(b.newRandomTx(true, testUserAddress, 0, testBankKey, 0, big.NewInt(10*params.InitialBaseFee)))
+		b.txPool.AddLocal(b.newRandomTx(false, testUserAddress, 1000, testBankKey, 0, big.NewInt(10*params.InitialBaseFee)))
 		w.postSideBlock(core.ChainSideEvent{Block: b.newRandomUncle()})
 		w.postSideBlock(core.ChainSideEvent{Block: b.newRandomUncle()})
 
@@ -279,7 +294,7 @@ func TestEmptyWorkClique(t *testing.T) {
 func testEmptyWork(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine) {
 	defer engine.Close()
 
-	w, _ := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), 0)
+	w, _ := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 0)
 	defer w.close()
 
 	var (
@@ -325,7 +340,7 @@ func TestStreamUncleBlock(t *testing.T) {
 	ethash := ethash.NewFaker()
 	defer ethash.Close()
 
-	w, b := newTestWorker(t, ethashChainConfig, ethash, rawdb.NewMemoryDatabase(), 1)
+	w, b := newTestWorker(t, ethashChainConfig, ethash, rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 1)
 	defer w.close()
 
 	var taskCh = make(chan struct{})
@@ -383,7 +398,7 @@ func TestRegenerateMiningBlockClique(t *testing.T) {
 func testRegenerateMiningBlock(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine) {
 	defer engine.Close()
 
-	w, b := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), 0)
+	w, b := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 0)
 	defer w.close()
 
 	var taskCh = make(chan struct{}, 3)
@@ -443,7 +458,7 @@ func TestAdjustIntervalClique(t *testing.T) {
 func testAdjustInterval(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine) {
 	defer engine.Close()
 
-	w, _ := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), 0)
+	w, _ := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 0)
 	defer w.close()
 
 	w.skipSealHook = func(task *task) bool {
@@ -543,8 +558,7 @@ func TestGetSealingWorkPostMerge(t *testing.T) {
 
 func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, postMerge bool) {
 	defer engine.Close()
-
-	w, b := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), 0)
+	w, b := newTestWorker(t, chainConfig, engine, rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 0)
 	defer w.close()
 
 	w.setExtra([]byte{0x01, 0x02})
@@ -557,7 +571,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co
 		time.Sleep(100 * time.Millisecond)
 	}
 	timestamp := uint64(time.Now().Unix())
-	assertBlock := func(block *types.Block, number uint64, coinbase common.Address, random common.Hash) {
+	assertBlock := func(block *types.Block, number uint64, coinbase common.Address, random common.Hash, noExtra bool) {
 		if block.Time() != timestamp {
 			// Sometime the timestamp will be mutated if the timestamp
 			// is even smaller than parent block's. It's OK.
@@ -568,12 +582,12 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co
 		}
 		_, isClique := engine.(*clique.Clique)
 		if !isClique {
-			if len(block.Extra()) != 0 {
+			if noExtra && len(block.Extra()) != 0 {
 				t.Error("Unexpected extra field")
 			}
-			if block.Coinbase() != coinbase {
-				t.Errorf("Unexpected coinbase got %x want %x", block.Coinbase(), coinbase)
-			}
+			//if block.Coinbase() != coinbase {
+			//	t.Errorf("Unexpected coinbase got %x want %x", block.Coinbase(), coinbase)
+			//}
 		} else {
 			if block.Coinbase() != (common.Address{}) {
 				t.Error("Unexpected coinbase")
@@ -637,7 +651,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co
 
 	// This API should work even when the automatic sealing is not enabled
 	for _, c := range cases {
-		resChan, errChan, _ := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, false)
+		resChan, errChan, _ := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, false, true, nil)
 		block := <-resChan
 		err := <-errChan
 		if c.expectErr {
@@ -648,14 +662,14 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co
 			if err != nil {
 				t.Errorf("Unexpected error %v", err)
 			}
-			assertBlock(block, c.expectNumber, c.coinbase, c.random)
+			assertBlock(block, c.expectNumber, c.coinbase, c.random, true)
 		}
 	}
 
 	// This API should work even when the automatic sealing is enabled
 	w.start()
 	for _, c := range cases {
-		resChan, errChan, _ := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, false)
+		resChan, errChan, _ := w.getSealingBlock(c.parent, timestamp, c.coinbase, 0, c.random, false, false, nil)
 		block := <-resChan
 		err := <-errChan
 		if c.expectErr {
@@ -666,7 +680,172 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co
 			if err != nil {
 				t.Errorf("Unexpected error %v", err)
 			}
-			assertBlock(block, c.expectNumber, c.coinbase, c.random)
+			assertBlock(block, c.expectNumber, c.coinbase, c.random, false)
+		}
+	}
+}
+
+func TestSimulateBundles(t *testing.T) {
+	w, _ := newTestWorker(t, ethashChainConfig, ethash.NewFaker(), rawdb.NewMemoryDatabase(), defaultGenesisAlloc, 0)
+	defer w.close()
+
+	env, err := w.prepareWork(&generateParams{gasLimit: 30000000})
+	if err != nil {
+		t.Fatalf("Failed to prepare work: %s", err)
+	}
+
+	signTx := func(nonce uint64) *types.Transaction {
+		tx, err := types.SignTx(types.NewTransaction(nonce, testUserAddress, big.NewInt(1000), params.TxGas, env.header.BaseFee, nil), types.HomesteadSigner{}, testBankKey)
+		if err != nil {
+			t.Fatalf("Failed to sign tx")
+		}
+		return tx
+	}
+
+	bundle1 := types.MevBundle{Txs: types.Transactions{signTx(0)}, Hash: common.HexToHash("0x01")}
+	// this bundle will fail
+	bundle2 := types.MevBundle{Txs: types.Transactions{signTx(1)}, Hash: common.HexToHash("0x02")}
+	bundle3 := types.MevBundle{Txs: types.Transactions{signTx(0)}, Hash: common.HexToHash("0x03")}
+
+	simBundles, err := w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil)
+	require.NoError(t, err)
+
+	if len(simBundles) != 2 {
+		t.Fatalf("Incorrect amount of sim bundles")
+	}
+
+	for _, simBundle := range simBundles {
+		if simBundle.OriginalBundle.Hash == common.HexToHash("0x02") {
+			t.Fatalf("bundle2 should fail")
+		}
+	}
+
+	// simulate 2 times to check cache
+	simBundles, err = w.simulateBundles(env, []types.MevBundle{bundle1, bundle2, bundle3}, nil)
+	require.NoError(t, err)
+
+	if len(simBundles) != 2 {
+		t.Fatalf("Incorrect amount of sim bundles(cache)")
+	}
+
+	for _, simBundle := range simBundles {
+		if simBundle.OriginalBundle.Hash == common.HexToHash("0x02") {
+			t.Fatalf("bundle2 should fail(cache)")
+		}
+	}
+}
+
+func testBundles(t *testing.T) {
+	db := rawdb.NewMemoryDatabase()
+	chainConfig := params.AllEthashProtocolChanges
+	engine := ethash.NewFaker()
+
+	chainConfig.LondonBlock = big.NewInt(0)
+
+	genesisAlloc := core.GenesisAlloc{testBankAddress: {Balance: testBankFunds}}
+
+	nExtraKeys := 5
+	extraKeys := make([]*ecdsa.PrivateKey, nExtraKeys)
+	for i := 0; i < nExtraKeys; i++ {
+		pk, _ := crypto.GenerateKey()
+		address := crypto.PubkeyToAddress(pk.PublicKey)
+		extraKeys[i] = pk
+		genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds}
+	}
+
+	nSearchers := 5
+	searcherPrivateKeys := make([]*ecdsa.PrivateKey, nSearchers)
+	for i := 0; i < nSearchers; i++ {
+		pk, _ := crypto.GenerateKey()
+		address := crypto.PubkeyToAddress(pk.PublicKey)
+		searcherPrivateKeys[i] = pk
+		genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds}
+	}
+
+	for _, address := range []common.Address{testAddress1, testAddress2, testAddress3} {
+		genesisAlloc[address] = core.GenesisAccount{Balance: testBankFunds}
+	}
+
+	w, b := newTestWorker(t, chainConfig, engine, db, genesisAlloc, 0)
+	w.setEtherbase(crypto.PubkeyToAddress(testConfig.BuilderTxSigningKey.PublicKey))
+	defer w.close()
+
+	// This test chain imports the mined blocks.
+	db2 := rawdb.NewMemoryDatabase()
+	b.genesis.MustCommit(db2)
+	chain, _ := core.NewBlockChain(db2, nil, b.chain.Config(), engine, vm.Config{}, nil, nil)
+	defer chain.Stop()
+
+	// Ignore empty commit here for less noise.
+	w.skipSealHook = func(task *task) bool {
+		return len(task.receipts) == 0
+	}
+
+	// Wait for mined blocks.
+	sub := w.mux.Subscribe(core.NewMinedBlockEvent{})
+	defer sub.Unsubscribe()
+
+	rand.Seed(10)
+
+	for i := 0; i < 2; i++ {
+		commonTxs := []*types.Transaction{
+			b.newRandomTx(false, testBankAddress, 1e15, testAddress1Key, 0, big.NewInt(100*params.InitialBaseFee)),
+			b.newRandomTx(false, testBankAddress, 1e15, testAddress2Key, 0, big.NewInt(110*params.InitialBaseFee)),
+			b.newRandomTx(false, testBankAddress, 1e15, testAddress3Key, 0, big.NewInt(120*params.InitialBaseFee)),
+		}
+
+		searcherTxs := make([]*types.Transaction, len(searcherPrivateKeys)*2)
+		for i, pk := range searcherPrivateKeys {
+			searcherTxs[2*i] = b.newRandomTx(false, testBankAddress, 1, pk, 0, big.NewInt(150*params.InitialBaseFee))
+			searcherTxs[2*i+1] = b.newRandomTx(false, testBankAddress, 1+1, pk, 0, big.NewInt(150*params.InitialBaseFee))
+		}
+
+		nBundles := 2 * len(searcherPrivateKeys)
+		// two bundles per searcher, i and i+1
+		bundles := make([]*types.MevBundle, nBundles)
+		for i := 0; i < nBundles; i++ {
+			bundles[i] = new(types.MevBundle)
+			bundles[i].Txs = append(bundles[i].Txs, searcherTxs[i])
+		}
+
+		// common transactions in 10% of the bundles, randomly
+		for i := 0; i < nBundles/10; i++ {
+			randomCommonIndex := rand.Intn(len(commonTxs))
+			randomBundleIndex := rand.Intn(nBundles)
+			bundles[randomBundleIndex].Txs = append(bundles[randomBundleIndex].Txs, commonTxs[randomCommonIndex])
+		}
+
+		// additional lower profit transactions in 10% of the bundles, randomly
+		for _, extraKey := range extraKeys {
+			tx := b.newRandomTx(false, testBankAddress, 1, extraKey, 0, big.NewInt(20*params.InitialBaseFee))
+			randomBundleIndex := rand.Intn(nBundles)
+			bundles[randomBundleIndex].Txs = append(bundles[randomBundleIndex].Txs, tx)
+		}
+
+		blockNumber := big.NewInt(0).Add(chain.CurrentBlock().Number(), big.NewInt(1))
+		for _, bundle := range bundles {
+			err := b.txPool.AddMevBundle(bundle.Txs, blockNumber, 0, 0, nil)
+			require.NoError(t, err)
+		}
+
+		blockCh, errCh, err := w.getSealingBlock(chain.CurrentBlock().Hash(), chain.CurrentHeader().Time+12, testUserAddress, 0, common.Hash{}, false, false, nil)
+		require.NoError(t, err)
+		select {
+		case block := <-blockCh:
+			state, err := chain.State()
+			require.NoError(t, err)
+			balancePre := state.GetBalance(testUserAddress)
+			if _, err := chain.InsertChain([]*types.Block{block}); err != nil {
+				t.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err)
+			}
+			state, err = chain.StateAt(block.Root())
+			require.NoError(t, err)
+			balancePost := state.GetBalance(testUserAddress)
+			t.Log("Balances", balancePre, balancePost)
+		case err := <-errCh:
+			require.NoError(t, err)
+		case <-time.After(3 * time.Second): // Worker needs 1s to include new changes.
+			t.Fatalf("timeout")
 		}
 	}
 }
diff --git a/node/defaults.go b/node/defaults.go
index fd0277e29d..0b254fcb3c 100644
--- a/node/defaults.go
+++ b/node/defaults.go
@@ -43,7 +43,7 @@ var (
 	DefaultAuthVhosts  = []string{"localhost"} // Default virtual hosts for the authenticated apis
 	DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
 	DefaultAuthPrefix  = ""                    // Default prefix for the authenticated apis
-	DefaultAuthModules = []string{"eth", "engine"}
+	DefaultAuthModules = []string{"eth", "engine", "builder"}
 )
 
 // DefaultConfig contains reasonable default settings.
diff --git a/node/endpoints.go b/node/endpoints.go
index 14c12fd1f1..0f33d4be06 100644
--- a/node/endpoints.go
+++ b/node/endpoints.go
@@ -62,7 +62,7 @@ func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available [
 	}
 	for _, name := range modules {
 		if _, ok := availableSet[name]; !ok {
-			if name != rpc.MetadataApi && name != rpc.EngineApi {
+			if name != rpc.MetadataApi && name != rpc.EngineApi && name != rpc.BuilderApi {
 				bad = append(bad, name)
 			}
 		}
diff --git a/ofac_blacklist.json b/ofac_blacklist.json
new file mode 100644
index 0000000000..5467935254
--- /dev/null
+++ b/ofac_blacklist.json
@@ -0,0 +1,71 @@
+[
+	"0x03893a7c7463ae47d46bc7f091665f1893656003",
+	"0x07687e702b410fa43f4cb4af7fa097918ffd2730",
+	"0x0836222f2b2b24a3f36f98668ed8f0b38d1a872f",
+	"0x08723392ed15743cc38513c4925f5e6be5c17243",
+	"0x098b716b8aaf21512996dc57eb0615e2383e2f96",
+	"0x12d66f87a04a9e220743712ce6d9bb1b5616b8fc",
+	"0x1356c899d8c9467c7f71c195612f8a395abf2f0a",
+	"0x169ad27a470d064dede56a2d3ff727986b15d52b",
+	"0x178169b423a011fff22b9e3f3abea13414ddd0f1",
+	"0x19aa5fe80d33a56d56c78e82ea5e50e5d80b4dff",
+	"0x1da5821544e25c636c1417ba96ade4cf6d2f9b5a",
+	"0x22aaa7720ddd5388a3c0a3333430953c68f1849b",
+	"0x23773e65ed146a459791799d01336db287f25334",
+	"0x2717c5e28cf931547b621a5dddb772ab6a35b701",
+	"0x2f389ce8bd8ff92de3402ffce4691d17fc4f6535",
+	"0x308ed4b7b49797e1a98d3818bff6fe5385410370",
+	"0x35fb6f6db4fb05e6a4ce86f2c93691425626d4b1",
+	"0x3cbded43efdaf0fc77b9c55f6fc9988fcc9b757d",
+	"0x3cffd56b47b7b41c56258d9c7731abadc360e073",
+	"0x3e37627deaa754090fbfbb8bd226c1ce66d255e9",
+	"0x4736dcf1b7a3d580672cce6e7c65cd5cc9cfba9d",
+	"0x47ce0c6ed5b0ce3d3a51fdb1c52dc66a7c3c2936",
+	"0x48549a34ae37b12f6a30566245176994e17c6b4a",
+	"0x527653ea119f3e6a1f5bd18fbf4714081d7b31ce",
+	"0x53b6936513e738f44fb50d2b9476730c0ab3bfc1",
+	"0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0",
+	"0x58e8dcc13be9780fc42e8723d8ead4cf46943df2",
+	"0x610b717796ad172b316836ac95a2ffad065ceab4",
+	"0x67d40ee1a85bf4a4bb7ffae16de985e8427b6b45",
+	"0x6acdfba02d390b97ac2b2d42a63e85293bcc160e",
+	"0x6f1ca141a28907f78ebaa64fb83a9088b02a8352",
+	"0x722122df12d4e14e13ac3b6895a86e84145b6967",
+	"0x72a5843cc08275c8171e582972aa4fda8c397b2a",
+	"0x7db418b5d567a4e0e8c59ad71be1fce48f3e6107",
+	"0x7f19720a857f834887fc9a7bc0a0fbe7fc7f8102",
+	"0x7f367cc41522ce07553e823bf3be79a889debe1b",
+	"0x7ff9cfad3877f21d41da833e2f775db0569ee3d9",
+	"0x8576acc5c05d6ce88f4e49bf65bdf0c62f91353c",
+	"0x8589427373d6d84e98730d7795d8f6f8731fda16",
+	"0x901bb9583b24d97e995513c6778dc6888ab6870e",
+	"0x905b63fff465b9ffbf41dea908ceb12478ec7601",
+	"0x910cbd523d972eb0a6f4cae4618ad62622b39dbf",
+	"0x94a1b5cdb22c43faab4abeb5c74999895464ddaf",
+	"0x9ad122c22b14202b4490edaf288fdb3c7cb3ff5e",
+	"0x9f4cda013e354b8fc285bf4b9a60460cee7f7ea9",
+	"0xa0e1c89ef1a489c9c7de96311ed5ce5d32c20e4b",
+	"0xa160cdab225685da1d56aa342ad8841c3b53f291",
+	"0xa60c772958a3ed56c1f15dd055ba37ac8e523a0d",
+	"0xa7e5d5a720f06526557c513402f2e6b5fa20b008",
+	"0xaeaac358560e11f52454d997aaff2c5731b6f8a6",
+	"0xb1c8094b234dce6e03f10a5b673c1d8c69739a00",
+	"0xb541fc07bc7619fd4062a54d96268525cbc6ffef",
+	"0xba214c1c1928a32bffe790263e38b4af9bfcd659",
+	"0xbb93e510bbcd0b7beb5a853875f9ec60275cf498",
+	"0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a",
+	"0xca0840578f57fe71599d29375e16783424023357",
+	"0xd21be7248e0197ee08e0c20d4a96debdac3d20af",
+	"0xd4b88df4d29f5cedd6857912842cff3b20c8cfa3",
+	"0xd691f27f38b395864ea86cfc7253969b409c362d",
+	"0xd882cfc20f52f2599d84b8e8d58c7fb62cfe344b",
+	"0xd90e2f925da726b50c4ed8d0fb90ad053324f31b",
+	"0xd96f2b1c14db8458374d9aca76e26c3d18364307",
+	"0xdd4c48c0b24039969fc16d1cdf626eab821d3384",
+	"0xe7aa314c77f4233c18c6cc84384a9247c0cf367b",
+	"0xf60dd140cff0706bae9cd734ac3ae76ad9ebc32a",
+	"0xf67721a2d8f736e75a49fdd7fad2e31d8676542a",
+	"0xf7b31119c2682c88d88d455dbb9d5932c65cf1be",
+	"0xfd8610d20aa15b7b2e3be39b396a1bc3516c7144",
+	"0xfec8a60023265364d066a1212fde3930f6ae8da7"
+]
diff --git a/rpc/server.go b/rpc/server.go
index bf1f71a28e..4f37dca63d 100644
--- a/rpc/server.go
+++ b/rpc/server.go
@@ -27,6 +27,7 @@ import (
 
 const MetadataApi = "rpc"
 const EngineApi = "engine"
+const BuilderApi = "builder"
 
 // CodecOption specifies which type of messages a codec supports.
 //