From 7ecb12b6f57da38061b53503ff110695cf66638a Mon Sep 17 00:00:00 2001 From: redhdx <136775144+redhdx@users.noreply.github.com> Date: Thu, 30 May 2024 15:18:38 +0800 Subject: [PATCH] bsc builder diff (#2) * builder: implement BEP322 builder-api (#7) * feat: mev-builder * consesus: add a interface to set validator at runtime * core/txpool: add bundlepool to maintain bundle txs * core/type: define bundle * internal/ethapi: add sendBundle, bundlePrice, unregis * ethclient: add SendBundle * miner: add fillTransacitonsAndBundles, add Bidder to sendBid to validators * add README.builder.md --------- Co-authored-by: raina * fix: index out of range (#10) * feat: call mev_params before send bid (#12) * fix: NPE and wrong profit (#13) * doc: update README.builder.md (#14) * fix: concurrent map write issue (#15) * fix: wrongly switch sync mode from full sync to snap sync issue (#17) * fix: add missing part when preparing env in `SimulateBundle` (#19) * feat: sendBundle return bundle hash (#20) * fix: some builder issues (#22) * fix: allow fast node to rewind after abnormal shutdown (#2401) (cherry picked from commit fb435eb5f11092f1fea02e131d9ddd2e59824ece) * fix: bundlepool concurrent read and write and commit blob tx issue * feat: set MaxBundleAliveBlock as bundle's default ddl --------- Co-authored-by: buddho Co-authored-by: irrun * fix: typo in `BundlePool.AllBundles` (#24) * feat: add `reconnectLoop` for mev validators (#25) * feat: add `reconnectLoop` for mev validators * fix lint issue * fix review comments * fix review comments * feat: ethclient of bundle (#23) * fix: a nil pointer when query bundle price (#28) * feat: set unrevertible tx hashes when sendBid --------- Co-authored-by: Roshan <48975233+Pythonberg1997@users.noreply.github.com> Co-authored-by: raina Co-authored-by: Roshan Co-authored-by: buddho Co-authored-by: zoro <296179868@qq.com> --- Makefile | 2 +- README.md | 328 +++------------ consensus/consensus.go | 1 + consensus/parlia/parlia.go | 11 + core/blockchain.go | 2 +- core/txpool/bundlepool/bundlepool.go | 382 ++++++++++++++++++ core/txpool/bundlepool/config.go | 73 ++++ core/txpool/subpool.go | 18 + core/txpool/txpool.go | 43 ++ core/types/bundle.go | 72 ++++ docs/assets/pbs_workflow.png | Bin 0 -> 122718 bytes eth/api_backend.go | 26 ++ eth/backend.go | 5 +- eth/ethconfig/config.go | 6 +- ethclient/ethclient.go | 17 + internal/ethapi/api_bundle.go | 118 ++++++ internal/ethapi/api_mev.go | 10 + internal/ethapi/api_test.go | 6 + internal/ethapi/backend.go | 5 + internal/ethapi/transaction_args_test.go | 2 + miner/bidder.go | 347 ++++++++++++++++ miner/bundle_cache.go | 88 ++++ miner/miner.go | 80 ++++ miner/miner_mev.go | 7 + miner/miner_test.go | 12 +- miner/ordering.go | 13 +- miner/validatorclient/validatorclient.go | 67 ++++ miner/worker.go | 91 ++++- miner/worker_builder.go | 488 +++++++++++++++++++++++ miner/worker_test.go | 17 +- 30 files changed, 2017 insertions(+), 320 deletions(-) create mode 100644 core/txpool/bundlepool/bundlepool.go create mode 100644 core/txpool/bundlepool/config.go create mode 100644 core/types/bundle.go create mode 100644 docs/assets/pbs_workflow.png create mode 100644 internal/ethapi/api_bundle.go create mode 100644 miner/bidder.go create mode 100644 miner/bundle_cache.go create mode 100644 miner/validatorclient/validatorclient.go create mode 100644 miner/worker_builder.go diff --git a/Makefile b/Makefile index 4b46068866..e7a11d8285 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ truffle-test: docker build . -f ./docker/Dockerfile --target bsc -t bsc docker build . -f ./docker/Dockerfile.truffle -t truffle-test docker-compose -f ./tests/truffle/docker-compose.yml up genesis - docker-compose -f ./tests/truffle/docker-compose.yml up -d bsc-rpc bsc-validator1 + docker-compose -f ./tests/truffle/docker-compose.yml up -d bsc-rpc sleep 30 docker-compose -f ./tests/truffle/docker-compose.yml up --exit-code-from truffle-test truffle-test docker-compose -f ./tests/truffle/docker-compose.yml down diff --git a/README.md b/README.md index d489e6aa40..fc6b1730dd 100644 --- a/README.md +++ b/README.md @@ -1,312 +1,84 @@ -## BNB Smart Chain +## Overview -The goal of BNB Smart Chain is to bring programmability and interoperability to BNB Beacon Chain. In order to embrace the existing popular community and advanced technology, it will bring huge benefits by staying compatible with all the existing smart contracts on Ethereum and Ethereum tooling. And to achieve that, the easiest solution is to develop based on go-ethereum fork, as we respect the great work of Ethereum very much. +The BSC network has introduced the [Builder API Specification](https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP322.md) to establish a fair and unified MEV market. Previously, BSC clients lacked native support for validators to integrate with multiple MEV providers at once. The network became unstable because of the many different versions of the client software being used. The latest BSC client adopts the [Proposer-Builder Separation](https://ethereum.org/en/roadmap/pbs/) model. Within this unified framework, several aspects of the BSC network have been improved: -BNB Smart Chain starts its development based on go-ethereum fork. So you may see many toolings, binaries and also docs are based on Ethereum ones, such as the name “geth”. +- Stability: Validators only need to use the official client to seamlessly integrate with various Builders. +- Economy: Builders that can enter without permission promote healthy market competition. Validators can extract more value by integrating with more builders, which benefits delegators as well. +- Transparency: This specification aims to bring transparency to the BSC MEV market, exposing profit distribution among stakeholders to the public. -[![API Reference]( -https://pkg.go.dev/badge/github.com/ethereum/go-ethereum -)](https://pkg.go.dev/github.com/ethereum/go-ethereum?tab=doc) -[![Discord](https://img.shields.io/badge/discord-join%20chat-blue.svg)](https://discord.gg/z2VpC455eU) +This project represents a minimal implementation of the protocol and is provided as is. We make no guarantees regarding its functionality or security. -But from that baseline of EVM compatible, BNB Smart Chain introduces a system of 21 validators with Proof of Staked Authority (PoSA) consensus that can support short block time and lower fees. The most bonded validator candidates of staking will become validators and produce blocks. The double-sign detection and other slashing logic guarantee security, stability, and chain finality. +## What is MEV and PBS -Cross-chain transfer and other communication are possible due to native support of interoperability. Relayers and on-chain contracts are developed to support that. BNB Beacon Chain DEX remains a liquid venue of the exchange of assets on both chains. This dual-chain architecture will be ideal for users to take advantage of the fast trading on one side and build their decentralized apps on the other side. **The BNB Smart Chain** will be: +MEV, also known as Maximum (or Miner) Extractable Value, can be described as the measure of total value that may be extracted from transaction ordering. Common examples include arbitraging swaps on decentralized exchanges or identifying opportunities to liquidate DeFi positions. Maximizing MEV requires advanced technical expertise and custom software integrated into regular validators. The returns are likely higher with centralized operators. -- **A self-sovereign blockchain**: Provides security and safety with elected validators. -- **EVM-compatible**: Supports all the existing Ethereum tooling along with faster finality and cheaper transaction fees. -- **Interoperable**: Comes with efficient native dual chain communication; Optimized for scaling high-performance dApps that require fast and smooth user experience. -- **Distributed with on-chain governance**: Proof of Staked Authority brings in decentralization and community participants. As the native token, BNB will serve as both the gas of smart contract execution and tokens for staking. +Proposer-builder separation(PBS) solves this problem by reconfiguring the economics of MEV. Block builders create blocks and submit them to the block proposer, and the block proposer simply chooses the most profitable one, paying a fee to the block builder. This means even if a small group of specialized block builders dominate MEV extraction, the reward still goes to any validator on the network. -More details in [White Paper](https://www.bnbchain.org/en#smartChain). +## How it Works on BSC -## Key features +![PBS Workflow](./docs/assets/pbs_workflow.png) -### Proof of Staked Authority -Although Proof-of-Work (PoW) has been approved as a practical mechanism to implement a decentralized network, it is not friendly to the environment and also requires a large size of participants to maintain the security. +The figure above illustrates the basic workflow of PBS operating on the BSC network. -Proof-of-Authority(PoA) provides some defense to 51% attack, with improved efficiency and tolerance to certain levels of Byzantine players (malicious or hacked). -Meanwhile, the PoA protocol is most criticized for being not as decentralized as PoW, as the validators, i.e. the nodes that take turns to produce blocks, have all the authorities and are prone to corruption and security attacks. +- MEV Searchers are independent network participants who detect profitable MEV opportunities and submit their transactions to builders. Transactions from searchers are usually bundled together and included in a block, or none of them will be included. +- The builder collects transactions from various sources to create an unsealed block and offer it to the block proposer. The builder will specify in the request the amount of fees the proposer needs to pay to the builder if this block is adopted. The unsealed block from the builder is also called a block bid as it may request tips. +- The proposer chooses the most profitable block from multiple builders, and pays the fee to the builder by appending a payment transaction at the end of the block. -Other blockchains, such as EOS and Cosmos both, introduce different types of Deputy Proof of Stake (DPoS) to allow the token holders to vote and elect the validator set. It increases the decentralization and favors community governance. +A new component called "Sentry" has been introduced to enhance network security and account isolation. It assists proposers in communicating with builders and enables payment processing. -To combine DPoS and PoA for consensus, BNB Smart Chain implement a novel consensus engine called Parlia that: +## What is More -1. Blocks are produced by a limited set of validators. -2. Validators take turns to produce blocks in a PoA manner, similar to Ethereum's Clique consensus engine. -3. Validator set are elected in and out based on a staking based governance on BNB Beacon Chain. -4. The validator set change is relayed via a cross-chain communication mechanism. -5. Parlia consensus engine will interact with a set of [system contracts](https://docs.bnbchain.org/docs/learn/system-contract) to achieve liveness slash, revenue distributing and validator set renewing func. +The PBS model on BSC differs in several aspects from its implementation on Ethereum. This is primarily due to: - -### Light Client of BNB Beacon Chain +1. Different Trust Model. Validators in the BNB Smart Chain are considered more trustworthy, as it requires substantial BNB delegation and must maintain a high reputation. This stands in contrast to Ethereum, where becoming an Ethereum validator is much easier, the barrier to becoming a validator is very low (i.e., 32 ETH). +2. Different Consensus Algorithms. In Ethereum, a block header is transferred from a builder to a validator for signing, allowing the block to be broadcasted to the network without disclosing the transactions to the validator. In contrast, in BSC, creating a valid block header requires executing transactions and system contract calls (such as transferring reward and depositing to the validator set contract), making it impossible for builders to propose the whole block. +3. Different Blocking Time. With a shorter block time of 3 seconds in BSC compared to Ethereum's 12 seconds, designing for time efficiency becomes crucial. -To achieve the cross-chain communication from BNB Beacon Chain to BNB Smart Chain, need introduce a on-chain light client verification algorithm. -It contains two parts: +These differences have led to different designs on BSC's PBS regarding payment, interaction, and APIs. For more design philosophy, please refer to [BEP322:Builder API Specification for BNB Smart Chain](https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP322.md). -1. [Stateless Precompiled contracts](https://github.com/bnb-chain/bsc/blob/master/core/vm/contracts_lightclient.go) to do tendermint header verification and Merkle Proof verification. -2. [Stateful solidity contracts](https://github.com/bnb-chain/bsc-genesis-contract/blob/master/contracts/TendermintLightClient.sol) to store validator set and trusted appHash. +## Integration Guide for Builder -## Native Token +The [Builder API Specification](https://github.com/bnb-chain/BEPs/blob/master/BEPs/BEP322.md) defines the standard interface that builders should implement, while the specific implementation is left open to MEV API providers. The BNB Chain community offers a [simple implementation](https://github.com/bnb-chain/bsc-builder) example for reference. -BNB will run on BNB Smart Chain in the same way as ETH runs on Ethereum so that it remains as `native token` for BSC. This means, -BNB will be used to: +### Customize Builder -1. pay `gas` to deploy or invoke Smart Contract on BSC -2. perform cross-chain operations, such as transfer token assets across BNB Smart Chain and BNB Beacon Chain. +Although the builder offers great flexibility, there are still some essential standards that must be followed: -## Building the source +1. The builder needs to set up a builder account, which is used to sign the block bid and receive fees. The builder can ask for a tip (builder fee) on the block that it sends to the sentry. If the block is finally selected, the builder account will receive the tip. +2. The builder needs to implement the mev_reportIssue API to receive the errors report from validators. +3. In order to prevent transaction leakage, the builder can only send block bids to the in-turn validator. +4. At most 3 block bids are allowed to be sent at the same height from the same builder. -Many of the below are the same as or similar to go-ethereum. +Here are some sentry APIs that may interest a builder: -For prerequisites and detailed build instructions please read the [Installation Instructions](https://geth.ethereum.org/docs/getting-started/installing-geth). +1. `mev_bestBidGasFee`. It will return the current most profitable reward that the validator received among all the blocks received from all builders. The reward is calculated as: `gasFee*(1 - commissionRate) - tipToBuilder`. A builder may compare the `bestBidGasFee` with a local one and then decide to send the block bid or not. +2. `mev_params`. It will return the `BidSimulationLeftOver`,`ValidatorCommission`, `GasCeil` and `BidFeeCeil` settings on the validator. If the current time is after `(except block time - BidSimulationLeftOver)`, then there is no need to send block bids anymore; `ValidatorCommission` and `BidFeeCeil` helps the builder to build its fee charge strategy. The `GasCeil` helps a builder know when to stop adding more transactions. -Building `geth` requires both a Go (version 1.21 or later) and a C compiler (GCC 5 or higher). You can install -them using your favourite package manager. Once the dependencies are installed, run +Builders have the freedom to define various aspects like pricing models for users, creating intuitive APIs, and define the bundle verification rules. -```shell -make geth -``` - -or, to build the full suite of utilities: - -```shell -make all -``` - -If you get such error when running the node with self built binary: -```shell -Caught SIGILL in blst_cgo_init, consult /bindinds/go/README.md. -``` -please try to add the following environment variables and build again: -```shell -export CGO_CFLAGS="-O -D__BLST_PORTABLE__" -export CGO_CFLAGS_ALLOW="-O -D__BLST_PORTABLE__" -``` - -## Executables - -The bsc project comes with several wrappers/executables found in the `cmd` -directory. - -| Command | Description | -| :--------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **`geth`** | Main BNB Smart Chain client binary. It is the entry point into the BSC 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 has the same and more RPC and other interface as go-ethereum and can be used by other processes as a gateway into the BSC 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`). | - -## 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/fundamentals/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 - -The hardware must meet certain requirements to run a full node on mainnet: -- VPS running recent versions of Mac OS X, Linux, or Windows. -- IMPORTANT 3 TB(Dec 2023) of free disk space, solid-state drive(SSD), gp3, 8k IOPS, 500 MB/S throughput, read latency <1ms. (if node is started with snap sync, it will need NVMe SSD) -- 16 cores of CPU and 64 GB of memory (RAM) -- Suggest m5zn.6xlarge or r7iz.4xlarge instance type on AWS, c2-standard-16 on Google cloud. -- A broadband Internet connection with upload/download speeds of 5 MB/S - -The requirement for testnet: -- VPS running recent versions of Mac OS X, Linux, or Windows. -- 500G of storage for testnet. -- 4 cores of CPU and 16 gigabytes of memory (RAM). - -### Steps to Run a Fullnode - -#### 1. Download the pre-build binaries -```shell -# Linux -wget $(curl -s https://api.github.com/repos/bnb-chain/bsc/releases/latest |grep browser_ |grep geth_linux |cut -d\" -f4) -mv geth_linux geth -chmod -v u+x geth - -# MacOS -wget $(curl -s https://api.github.com/repos/bnb-chain/bsc/releases/latest |grep browser_ |grep geth_mac |cut -d\" -f4) -mv geth_macos geth -chmod -v u+x geth -``` - -#### 2. Download the config files -```shell -//== mainnet -wget $(curl -s https://api.github.com/repos/bnb-chain/bsc/releases/latest |grep browser_ |grep mainnet |cut -d\" -f4) -unzip mainnet.zip - -//== testnet -wget $(curl -s https://api.github.com/repos/bnb-chain/bsc/releases/latest |grep browser_ |grep testnet |cut -d\" -f4) -unzip testnet.zip -``` - -#### 3. Download snapshot -Download latest chaindata snapshot from [here](https://github.com/bnb-chain/bsc-snapshots). Follow the guide to structure your files. - -Note: If you encounter difficulties downloading the chaindata snapshot and prefer to synchronize from the genesis block on the Chapel testnet, remember to include the additional flag `--chapel` when initially launching Geth. +### Setup with Example Builder -#### 4. Start a full node -```shell -./geth --config ./config.toml --datadir ./node --cache 8000 --rpc.allow-unprotected-txs --history.transactions 0 +Step 1: Find Validator Information +For validators that open MEV integration, the public information is shown at [bsc-mev-info](https://github.com/bnb-chain/bsc-mev-info). Builders can also provide information here to the validator. -## It is recommend to run fullnode with `--tries-verify-mode none` if you want high performance and care little about state consistency -## It will run with Hash-Base Storage Scheme by default -./geth --config ./config.toml --datadir ./node --cache 8000 --rpc.allow-unprotected-txs --history.transactions 0 --tries-verify-mode none +Step 2: Set up Builder. +The builder must sign the bid using an account, such as the etherbase account specified in the config.toml file. -## It runs fullnode with Path-Base Storage Scheme. -## It will enable inline state prune, keeping the latest 90000 blocks' history state by default. -./geth --config ./config.toml --datadir ./node --cache 8000 --rpc.allow-unprotected-txs --history.transactions 0 --tries-verify-mode none --state.scheme path -``` - -#### 5. Monitor node status - -Monitor the log from **./node/bsc.log** by default. When the node has started syncing, should be able to see the following output: -```shell -t=2022-09-08T13:00:27+0000 lvl=info msg="Imported new chain segment" blocks=1 txs=177 mgas=17.317 elapsed=31.131ms mgasps=556.259 number=21,153,429 hash=0x42e6b54ba7106387f0650defc62c9ace3160b427702dab7bd1c5abb83a32d8db dirty="0.00 B" -t=2022-09-08T13:00:29+0000 lvl=info msg="Imported new chain segment" blocks=1 txs=251 mgas=39.638 elapsed=68.827ms mgasps=575.900 number=21,153,430 hash=0xa3397b273b31b013e43487689782f20c03f47525b4cd4107c1715af45a88796e dirty="0.00 B" -t=2022-09-08T13:00:33+0000 lvl=info msg="Imported new chain segment" blocks=1 txs=197 mgas=19.364 elapsed=34.663ms mgasps=558.632 number=21,153,431 hash=0x0c7872b698f28cb5c36a8a3e1e315b1d31bda6109b15467a9735a12380e2ad14 dirty="0.00 B" -``` - -#### 6. Interact with fullnode -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://web3js.readthedocs.io/en/) -(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`. - -#### 7. More - -More details about [running a node](https://docs.bnbchain.org/docs/validator/fullnode) and [becoming a validator](https://docs.bnbchain.org/docs/validator/create-val) +```toml +[Eth.Miner.Mev] +BuilderEnabled = true # open bid sending +BuilderAccount = "0x..." # builder address which signs bid, usually it is the same as etherbase address -*Note: Although some internal protective measures prevent transactions from -crossing over between the main network and test network, you should always -use separate accounts for play 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.* +# Configure the validator node list, including the address of the validator and the public URL. The public URL refers to the sentry service. +[[Eth.Miner.Mev.Validators]] +Address = "0x23707D3D71E31e4Cb5B4A9816DfBDCA6455B52B3" +URL = "https://bsc-fuji.io" -### 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 +[[Eth.Miner.Mev.Validators]] +Address = "0x..." +URL = "https://bsc-mathwallet.io" ``` -To get an idea of how the file should look like you can use the `dumpconfig` subcommand to -export your existing configuration: - -```shell -$ geth --your-favourite-flags dumpconfig -``` - -### Programmatically interfacing `geth` nodes - -As a developer, sooner rather than later you'll want to start interacting with `geth` and the -BSC 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/interacting-with-geth/rpc)). -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 WebSocket 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 -BSC 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 -- [BSC-Deploy](https://github.com/bnb-chain/node-deploy/): deploy tool for setting up both BNB Beacon Chain, BNB Smart Chain and the cross chain infrastructure between them. -- [BSC-Docker](https://github.com/bnb-chain/bsc-docker): deploy tool for setting up local BSC cluster in container. - - -## Running a bootnode - -Bootnodes are super-lightweight nodes that are not behind a NAT and are running just discovery protocol. When you start up a node it should log your enode, which is a public identifier that others can use to connect to your node. - -First the bootnode requires a key, which can be created with the following command, which will save a key to boot.key: - -``` -bootnode -genkey boot.key -``` - -This key can then be used to generate a bootnode as follows: - -``` -bootnode -nodekey boot.key -addr :30311 -network bsc -``` - -The choice of port passed to -addr is arbitrary. -The bootnode command returns the following logs to the terminal, confirming that it is running: - -``` -enode://3063d1c9e1b824cfbb7c7b6abafa34faec6bb4e7e06941d218d760acdd7963b274278c5c3e63914bd6d1b58504c59ec5522c56f883baceb8538674b92da48a96@127.0.0.1:0?discport=30311 -Note: you're using cmd/bootnode, a developer tool. -We recommend using a regular node as bootstrap node for production deployments. -INFO [08-21|11:11:30.687] New local node record seq=1,692,616,290,684 id=2c9af1742f8f85ce ip= udp=0 tcp=0 -INFO [08-21|12:11:30.753] New local node record seq=1,692,616,290,685 id=2c9af1742f8f85ce ip=54.217.128.118 udp=30311 tcp=0 -INFO [09-01|02:46:26.234] New local node record seq=1,692,616,290,686 id=2c9af1742f8f85ce ip=34.250.32.100 udp=30311 tcp=0 -``` - -## Contribution - -Thank you for considering helping 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 bsc, 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 channel](https://discord.gg/bnbchain) -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/geth-developer/dev-guide) -for more details on configuring your environment, managing project dependencies, and -testing procedures. - ## License The bsc library (i.e. all code outside of the `cmd` directory) is licensed under the diff --git a/consensus/consensus.go b/consensus/consensus.go index 7c65acb359..84c07ae27b 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -163,4 +163,5 @@ type PoSA interface { GetFinalizedHeader(chain ChainHeaderReader, header *types.Header) *types.Header VerifyVote(chain ChainHeaderReader, vote *types.VoteEnvelope) error IsActiveValidatorAt(chain ChainHeaderReader, header *types.Header, checkVoteKeyFn func(bLSPublicKey *types.BLSPublicKey) bool) bool + SetValidator(validator common.Address) } diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 69b82d408c..ec5103bd9b 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -1897,6 +1897,17 @@ func (p *Parlia) GetFinalizedHeader(chain consensus.ChainHeaderReader, header *t return chain.GetHeader(snap.Attestation.SourceHash, snap.Attestation.SourceNumber) } +// SetValidator set the validator of parlia engine +// It is used for builder +func (p *Parlia) SetValidator(val common.Address) { + if val == (common.Address{}) { + return + } + p.lock.Lock() + defer p.lock.Unlock() + p.val = val +} + // =========================== utility function ========================== func (p *Parlia) backOffTime(snap *Snapshot, header *types.Header, val common.Address) uint64 { if snap.inturn(val) { diff --git a/core/blockchain.go b/core/blockchain.go index ff4f66a471..fced8df406 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -31,6 +31,7 @@ import ( mapset "github.com/deckarep/golang-set/v2" exlru "github.com/hashicorp/golang-lru" "golang.org/x/crypto/sha3" + "golang.org/x/exp/slices" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/lru" @@ -56,7 +57,6 @@ import ( "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/hashdb" "github.com/ethereum/go-ethereum/triedb/pathdb" - "golang.org/x/exp/slices" ) var ( diff --git a/core/txpool/bundlepool/bundlepool.go b/core/txpool/bundlepool/bundlepool.go new file mode 100644 index 0000000000..8f1fbd800d --- /dev/null +++ b/core/txpool/bundlepool/bundlepool.go @@ -0,0 +1,382 @@ +package bundlepool + +import ( + "container/heap" + "errors" + "math/big" + "sync" + "time" + + "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/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" +) + +const ( + // TODO: decide on a good default value + // bundleSlotSize is used to calculate how many data slots a single bundle + // takes up based on its size. The slots are used as DoS protection, ensuring + // that validating a new bundle remains a constant operation (in reality + // O(maxslots), where max slots are 4 currently). + bundleSlotSize = 128 * 1024 // 128KB + + maxMinTimestampFromNow = int64(300) // 5 minutes +) + +var ( + bundleGauge = metrics.NewRegisteredGauge("bundlepool/bundles", nil) + slotsGauge = metrics.NewRegisteredGauge("bundlepool/slots", nil) +) + +var ( + // ErrSimulatorMissing is returned if the bundle simulator is missing. + ErrSimulatorMissing = errors.New("bundle simulator is missing") + + // ErrBundleTimestampTooHigh is returned if the bundle's MinTimestamp is too high. + ErrBundleTimestampTooHigh = errors.New("bundle MinTimestamp is too high") + + // ErrBundleGasPriceLow is returned if the bundle gas price is too low. + ErrBundleGasPriceLow = errors.New("bundle gas price is too low") + + // ErrBundleAlreadyExist is returned if the bundle is already contained + // within the pool. + ErrBundleAlreadyExist = errors.New("bundle already exist") +) + +// BlockChain defines the minimal set of methods needed to back a tx pool with +// a chain. Exists to allow mocking the live chain out of tests. +type BlockChain interface { + // Config retrieves the chain's fork configuration. + Config() *params.ChainConfig + + // CurrentBlock returns the current head of the chain. + CurrentBlock() *types.Header + + // GetBlock retrieves a specific block, used during pool resets. + GetBlock(hash common.Hash, number uint64) *types.Block + + // StateAt returns a state database for a given root hash (generally the head). + StateAt(root common.Hash) (*state.StateDB, error) +} + +type BundleSimulator interface { + SimulateBundle(bundle *types.Bundle) (*big.Int, error) +} + +type BundlePool struct { + config Config + + bundles map[common.Hash]*types.Bundle + bundleHeap BundleHeap + mu sync.RWMutex + + slots uint64 // Number of slots currently allocated + + simulator BundleSimulator +} + +func New(config Config) *BundlePool { + // Sanitize the input to ensure no vulnerable gas prices are set + config = (&config).sanitize() + + pool := &BundlePool{ + config: config, + bundles: make(map[common.Hash]*types.Bundle), + bundleHeap: make(BundleHeap, 0), + } + + return pool +} + +func (p *BundlePool) SetBundleSimulator(simulator BundleSimulator) { + p.simulator = simulator +} + +func (p *BundlePool) Init(gasTip uint64, head *types.Header, reserve txpool.AddressReserver) error { + return nil +} + +func (p *BundlePool) FilterBundle(bundle *types.Bundle) bool { + for _, tx := range bundle.Txs { + if !p.filter(tx) { + return false + } + } + return true +} + +// AddBundle adds a mev bundle to the pool +func (p *BundlePool) AddBundle(bundle *types.Bundle) error { + if p.simulator == nil { + return ErrSimulatorMissing + } + + if bundle.MinTimestamp > uint64(time.Now().Unix()+maxMinTimestampFromNow) { + return ErrBundleTimestampTooHigh + } + + price, err := p.simulator.SimulateBundle(bundle) + if err != nil { + return err + } + if price.Cmp(p.minimalBundleGasPrice()) < 0 && p.slots+numSlots(bundle) > p.config.GlobalSlots { + return ErrBundleGasPriceLow + } + bundle.Price = price + + p.mu.Lock() + defer p.mu.Unlock() + + hash := bundle.Hash() + if _, ok := p.bundles[hash]; ok { + return ErrBundleAlreadyExist + } + for p.slots+numSlots(bundle) > p.config.GlobalSlots { + p.drop() + } + p.bundles[hash] = bundle + heap.Push(&p.bundleHeap, bundle) + p.slots += numSlots(bundle) + + bundleGauge.Update(int64(len(p.bundles))) + slotsGauge.Update(int64(p.slots)) + return nil +} + +func (p *BundlePool) GetBundle(hash common.Hash) *types.Bundle { + p.mu.RUnlock() + defer p.mu.RUnlock() + + return p.bundles[hash] +} + +func (p *BundlePool) PruneBundle(hash common.Hash) { + p.mu.Lock() + defer p.mu.Unlock() + p.deleteBundle(hash) +} + +func (p *BundlePool) PendingBundles(blockNumber uint64, blockTimestamp uint64) []*types.Bundle { + p.mu.Lock() + defer p.mu.Unlock() + + ret := make([]*types.Bundle, 0) + for hash, bundle := range p.bundles { + // Prune outdated bundles + if (bundle.MaxTimestamp != 0 && blockTimestamp > bundle.MaxTimestamp) || + (bundle.MaxBlockNumber != 0 && blockNumber > bundle.MaxBlockNumber) { + p.deleteBundle(hash) + continue + } + + // Roll over future bundles + if bundle.MinTimestamp != 0 && blockTimestamp < bundle.MinTimestamp { + continue + } + + // return the ones that are in time + ret = append(ret, bundle) + } + + bundleGauge.Update(int64(len(p.bundles))) + slotsGauge.Update(int64(p.slots)) + return ret +} + +// AllBundles returns all the bundles currently in the pool +func (p *BundlePool) AllBundles() []*types.Bundle { + p.mu.RLock() + defer p.mu.RUnlock() + bundles := make([]*types.Bundle, 0, len(p.bundles)) + for _, bundle := range p.bundles { + bundles = append(bundles, bundle) + } + return bundles +} + +func (p *BundlePool) Filter(tx *types.Transaction) bool { + return false +} + +func (p *BundlePool) Close() error { + log.Info("Bundle pool stopped") + return nil +} + +func (p *BundlePool) Reset(oldHead, newHead *types.Header) { + p.reset(newHead) +} + +// SetGasTip updates the minimum price required by the subpool for a new +// transaction, and drops all transactions below this threshold. +func (p *BundlePool) SetGasTip(tip *big.Int) {} + +// Has returns an indicator whether subpool has a transaction cached with the +// given hash. +func (p *BundlePool) Has(hash common.Hash) bool { + return false +} + +// Get returns a transaction if it is contained in the pool, or nil otherwise. +func (p *BundlePool) Get(hash common.Hash) *types.Transaction { + return nil +} + +// Add enqueues a batch of transactions into the pool if they are valid. Due +// to the large transaction churn, add may postpone fully integrating the tx +// to a later point to batch multiple ones together. +func (p *BundlePool) Add(txs []*types.Transaction, local bool, sync bool) []error { + return nil +} + +// Pending retrieves all currently processable transactions, grouped by origin +// account and sorted by nonce. +func (p *BundlePool) Pending(filter txpool.PendingFilter) map[common.Address][]*txpool.LazyTransaction { + return nil +} + +// SubscribeTransactions subscribes to new transaction events. +func (p *BundlePool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription { + return nil +} + +// SubscribeReannoTxsEvent should return an event subscription of +// ReannoTxsEvent and send events to the given channel. +func (p *BundlePool) SubscribeReannoTxsEvent(chan<- core.ReannoTxsEvent) event.Subscription { + return nil +} + +// Nonce returns the next nonce of an account, with all transactions executable +// by the pool already applied on topool. +func (p *BundlePool) Nonce(addr common.Address) uint64 { + return 0 +} + +// Stats retrieves the current pool stats, namely the number of pending and the +// number of queued (non-executable) transactions. +func (p *BundlePool) Stats() (int, int) { + return 0, 0 +} + +// Content retrieves the data content of the transaction pool, returning all the +// pending as well as queued transactions, grouped by account and sorted by nonce. +func (p *BundlePool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { + return make(map[common.Address][]*types.Transaction), make(map[common.Address][]*types.Transaction) +} + +// ContentFrom retrieves the data content of the transaction pool, returning the +// pending as well as queued transactions of this address, grouped by nonce. +func (p *BundlePool) ContentFrom(addr common.Address) ([]*types.Transaction, []*types.Transaction) { + return []*types.Transaction{}, []*types.Transaction{} +} + +// Locals retrieves the accounts currently considered local by the pool. +func (p *BundlePool) Locals() []common.Address { + return []common.Address{} +} + +// Status returns the known status (unknown/pending/queued) of a transaction +// identified by their hashes. +func (p *BundlePool) Status(hash common.Hash) txpool.TxStatus { + return txpool.TxStatusUnknown +} + +func (p *BundlePool) filter(tx *types.Transaction) bool { + switch tx.Type() { + case types.LegacyTxType, types.AccessListTxType, types.DynamicFeeTxType: + return true + default: + return false + } +} + +func (p *BundlePool) reset(newHead *types.Header) { + p.mu.Lock() + defer p.mu.Unlock() + + // Prune outdated bundles + for hash, bundle := range p.bundles { + if (bundle.MaxTimestamp != 0 && newHead.Time > bundle.MaxTimestamp) || + (bundle.MaxBlockNumber != 0 && newHead.Number.Cmp(new(big.Int).SetUint64(bundle.MaxBlockNumber)) > 0) { + p.slots -= numSlots(p.bundles[hash]) + delete(p.bundles, hash) + } + } +} + +// deleteBundle deletes a bundle from the pool. +// It assumes that the caller holds the pool's lock. +func (p *BundlePool) deleteBundle(hash common.Hash) { + if p.bundles[hash] == nil { + return + } + + p.slots -= numSlots(p.bundles[hash]) + delete(p.bundles, hash) +} + +// drop removes the bundle with the lowest gas price from the pool. +func (p *BundlePool) drop() { + p.mu.Lock() + defer p.mu.Unlock() + for len(p.bundleHeap) > 0 { + // Pop the bundle with the lowest gas price + // the min element in the heap may not exist in the pool as it may be pruned + leastPriceBundleHash := heap.Pop(&p.bundleHeap).(*types.Bundle).Hash() + if _, ok := p.bundles[leastPriceBundleHash]; ok { + p.deleteBundle(leastPriceBundleHash) + break + } + } +} + +// minimalBundleGasPrice return the lowest gas price from the pool. +func (p *BundlePool) minimalBundleGasPrice() *big.Int { + for len(p.bundleHeap) != 0 { + leastPriceBundleHash := p.bundleHeap[0].Hash() + if bundle, ok := p.bundles[leastPriceBundleHash]; ok { + return bundle.Price + } + heap.Pop(&p.bundleHeap) + } + return new(big.Int) +} + +func (p *BundlePool) SetMaxGas(maxGas uint64) {} + +// ===================================================================================================================== + +// numSlots calculates the number of slots needed for a single bundle. +func numSlots(bundle *types.Bundle) uint64 { + return (bundle.Size() + bundleSlotSize - 1) / bundleSlotSize +} + +// ===================================================================================================================== + +type BundleHeap []*types.Bundle + +func (h *BundleHeap) Len() int { return len(*h) } + +func (h *BundleHeap) Less(i, j int) bool { + return (*h)[i].Price.Cmp((*h)[j].Price) == -1 +} + +func (h *BundleHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } + +func (h *BundleHeap) Push(x interface{}) { + *h = append(*h, x.(*types.Bundle)) +} + +func (h *BundleHeap) Pop() interface{} { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} diff --git a/core/txpool/bundlepool/config.go b/core/txpool/bundlepool/config.go new file mode 100644 index 0000000000..252004ded5 --- /dev/null +++ b/core/txpool/bundlepool/config.go @@ -0,0 +1,73 @@ +package bundlepool + +import ( + "time" + + "github.com/ethereum/go-ethereum/log" +) + +type Config struct { + PriceLimit uint64 // Minimum gas price to enforce for acceptance into the pool + PriceBump uint64 // Minimum price bump percentage to replace an already existing transaction (nonce) + + GlobalSlots uint64 // Maximum number of bundle slots for all accounts + GlobalQueue uint64 // Maximum number of non-executable bundle slots for all accounts + MaxBundleBlocks uint64 // Maximum number of blocks for calculating MinimalBundleGasPrice + + BundleGasPricePercentile uint8 // Percentile of the recent minimal mev gas price + BundleGasPricerExpireTime time.Duration // Store time duration amount of recent mev gas price + UpdateBundleGasPricerInterval time.Duration // Time interval to update MevGasPricePool +} + +// DefaultConfig contains the default configurations for the bundle pool. +var DefaultConfig = Config{ + PriceLimit: 1, + PriceBump: 10, + + GlobalSlots: 4096 + 1024, // urgent + floating queue capacity with 4:1 ratio + GlobalQueue: 1024, + + MaxBundleBlocks: 50, + BundleGasPricePercentile: 90, + BundleGasPricerExpireTime: time.Minute, + UpdateBundleGasPricerInterval: time.Second, +} + +// sanitize checks the provided user configurations and changes anything that's +// unreasonable or unworkable. +func (config *Config) sanitize() Config { + conf := *config + if conf.PriceLimit < 1 { + log.Warn("Sanitizing invalid txpool price limit", "provided", conf.PriceLimit, "updated", DefaultConfig.PriceLimit) + conf.PriceLimit = DefaultConfig.PriceLimit + } + if conf.PriceBump < 1 { + log.Warn("Sanitizing invalid txpool price bump", "provided", conf.PriceBump, "updated", DefaultConfig.PriceBump) + conf.PriceBump = DefaultConfig.PriceBump + } + if conf.GlobalSlots < 1 { + log.Warn("Sanitizing invalid txpool bundle slots", "provided", conf.GlobalSlots, "updated", DefaultConfig.GlobalSlots) + conf.GlobalSlots = DefaultConfig.GlobalSlots + } + if conf.GlobalQueue < 1 { + log.Warn("Sanitizing invalid txpool global queue", "provided", conf.GlobalQueue, "updated", DefaultConfig.GlobalQueue) + conf.GlobalQueue = DefaultConfig.GlobalQueue + } + if conf.MaxBundleBlocks < 1 { + log.Warn("Sanitizing invalid txpool max bundle blocks", "provided", conf.MaxBundleBlocks, "updated", DefaultConfig.MaxBundleBlocks) + conf.MaxBundleBlocks = DefaultConfig.MaxBundleBlocks + } + if conf.BundleGasPricePercentile >= 100 { + log.Warn("Sanitizing invalid txpool bundle gas price percentile", "provided", conf.BundleGasPricePercentile, "updated", DefaultConfig.BundleGasPricePercentile) + conf.BundleGasPricePercentile = DefaultConfig.BundleGasPricePercentile + } + if conf.BundleGasPricerExpireTime < 1 { + log.Warn("Sanitizing invalid txpool bundle gas pricer expire time", "provided", conf.BundleGasPricerExpireTime, "updated", DefaultConfig.BundleGasPricerExpireTime) + conf.BundleGasPricerExpireTime = DefaultConfig.BundleGasPricerExpireTime + } + if conf.UpdateBundleGasPricerInterval < time.Second { + log.Warn("Sanitizing invalid txpool update BundleGasPricer interval", "provided", conf.UpdateBundleGasPricerInterval, "updated", DefaultConfig.UpdateBundleGasPricerInterval) + conf.UpdateBundleGasPricerInterval = DefaultConfig.UpdateBundleGasPricerInterval + } + return conf +} diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index be5f6840d3..ea584afe4d 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -170,3 +170,21 @@ type SubPool interface { // SetMaxGas limit max acceptable tx gas when mine is enabled SetMaxGas(maxGas uint64) } + +type BundleSubpool interface { + // FilterBundle is a selector used to decide whether a bundle would be added + // to this particular subpool. + FilterBundle(bundle *types.Bundle) bool + + // AddBundle enqueues a bundle into the pool if it is valid. + AddBundle(bundle *types.Bundle) error + + // PendingBundles retrieves all currently processable bundles. + PendingBundles(blockNumber uint64, blockTimestamp uint64) []*types.Bundle + + // AllBundles returns all the bundles currently in the pool. + AllBundles() []*types.Bundle + + // PruneBundle removes a bundle from the pool. + PruneBundle(hash common.Hash) +} diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 51b7539576..677d1865e7 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -357,6 +357,29 @@ func (p *TxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { return errs } +// AddBundle enqueues a bundle into the pool if it is valid. +func (p *TxPool) AddBundle(bundle *types.Bundle) error { + // Try to find a sub pool that accepts the bundle + for _, subpool := range p.subpools { + if bundleSubpool, ok := subpool.(BundleSubpool); ok { + if bundleSubpool.FilterBundle(bundle) { + return bundleSubpool.AddBundle(bundle) + } + } + } + return errors.New("no subpool accepts the bundle") +} + +// PruneBundle removes a bundle from the pool. +func (p *TxPool) PruneBundle(hash common.Hash) { + for _, subpool := range p.subpools { + if bundleSubpool, ok := subpool.(BundleSubpool); ok { + bundleSubpool.PruneBundle(hash) + return // Only one subpool can have the bundle + } + } +} + // Pending retrieves all currently processable transactions, grouped by origin // account and sorted by nonce. // @@ -372,6 +395,26 @@ func (p *TxPool) Pending(filter PendingFilter) map[common.Address][]*LazyTransac return txs } +// PendingBundles retrieves all currently processable bundles. +func (p *TxPool) PendingBundles(blockNumber uint64, blockTimestamp uint64) []*types.Bundle { + for _, subpool := range p.subpools { + if bundleSubpool, ok := subpool.(BundleSubpool); ok { + return bundleSubpool.PendingBundles(blockNumber, blockTimestamp) + } + } + return nil +} + +// AllBundles returns all the bundles currently in the pool +func (p *TxPool) AllBundles() []*types.Bundle { + for _, subpool := range p.subpools { + if bundleSubpool, ok := subpool.(BundleSubpool); ok { + return bundleSubpool.AllBundles() + } + } + return nil +} + // SubscribeTransactions registers a subscription for new transaction events, // supporting feeding only newly seen or also resurrected transactions. func (p *TxPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription { diff --git a/core/types/bundle.go b/core/types/bundle.go new file mode 100644 index 0000000000..45cbb797d4 --- /dev/null +++ b/core/types/bundle.go @@ -0,0 +1,72 @@ +package types + +import ( + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + // MaxBundleAliveBlock is the max alive block for bundle + MaxBundleAliveBlock = 100 + // MaxBundleAliveTime is the max alive time for bundle + MaxBundleAliveTime = 5 * 60 // second +) + +// SendBundleArgs represents the arguments for a call. +type SendBundleArgs struct { + Txs []hexutil.Bytes `json:"txs"` + MaxBlockNumber uint64 `json:"maxBlockNumber"` + MinTimestamp *uint64 `json:"minTimestamp"` + MaxTimestamp *uint64 `json:"maxTimestamp"` + RevertingTxHashes []common.Hash `json:"revertingTxHashes"` +} + +type Bundle struct { + Txs Transactions + MaxBlockNumber uint64 + MinTimestamp uint64 + MaxTimestamp uint64 + RevertingTxHashes []common.Hash + + Price *big.Int // for bundle compare and prune + + // caches + hash atomic.Value + size atomic.Value +} + +type SimulatedBundle struct { + OriginalBundle *Bundle + + BundleGasFees *big.Int + BundleGasPrice *big.Int + BundleGasUsed uint64 + EthSentToSystem *big.Int +} + +func (bundle *Bundle) Size() uint64 { + if size := bundle.size.Load(); size != nil { + return size.(uint64) + } + c := writeCounter(0) + rlp.Encode(&c, bundle) + + size := uint64(c) + bundle.size.Store(size) + return size +} + +// Hash returns the bundle hash. +func (bundle *Bundle) Hash() common.Hash { + if hash := bundle.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + h := rlpHash(bundle) + bundle.hash.Store(h) + return h +} diff --git a/docs/assets/pbs_workflow.png b/docs/assets/pbs_workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f35afd68ec3bf6bee053762003bf11f12bcdba GIT binary patch literal 122718 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

$YaK<>H^o@}=K(sQ1)C{3NXK<-3o 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

*mu0D}v@5{H<*_KsHSTM)ZjO^LoeK{y0 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

VIV26QD^uW;c{55kN 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 currentHeader.Number.Uint64()+types.MaxBundleAliveBlock { + return common.Hash{}, newBundleError(errors.New("the maxBlockNumber should not be lager than currentBlockNum + 100")) + } + + if args.MaxTimestamp != nil && args.MinTimestamp != nil && *args.MaxTimestamp != 0 && *args.MinTimestamp != 0 { + if *args.MaxTimestamp <= *args.MinTimestamp { + return common.Hash{}, newBundleError(errors.New("the maxTimestamp should not be less than minTimestamp")) + } + } + + if args.MaxTimestamp != nil && *args.MaxTimestamp != 0 && *args.MaxTimestamp < currentHeader.Time { + return common.Hash{}, newBundleError(errors.New("the maxTimestamp should not be less than currentBlockTimestamp")) + } + + if (args.MaxTimestamp != nil && *args.MaxTimestamp > currentHeader.Time+types.MaxBundleAliveTime) || + (args.MinTimestamp != nil && *args.MinTimestamp > currentHeader.Time+types.MaxBundleAliveTime) { + return common.Hash{}, newBundleError(errors.New("the minTimestamp/maxTimestamp should not be later than currentBlockTimestamp + 5 minutes")) + } + + var txs types.Transactions + + for _, encodedTx := range args.Txs { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(encodedTx); err != nil { + return common.Hash{}, err + } + txs = append(txs, tx) + } + + var minTimestamp, maxTimestamp uint64 + + if args.MinTimestamp != nil { + minTimestamp = *args.MinTimestamp + } + + if args.MaxTimestamp != nil { + maxTimestamp = *args.MaxTimestamp + } + + bundle := &types.Bundle{ + Txs: txs, + MaxBlockNumber: args.MaxBlockNumber, + MinTimestamp: minTimestamp, + MaxTimestamp: maxTimestamp, + RevertingTxHashes: args.RevertingTxHashes, + } + + // If the maxBlockNumber and maxTimestamp are not set, set max ddl of bundle as types.MaxBundleAliveBlock + if bundle.MaxBlockNumber == 0 && bundle.MaxTimestamp == 0 { + bundle.MaxBlockNumber = currentHeader.Number.Uint64() + types.MaxBundleAliveBlock + } + + err := s.b.SendBundle(ctx, bundle) + if err != nil { + return common.Hash{}, err + } + + return bundle.Hash(), nil +} + +func newBundleError(err error) *bundleError { + return &bundleError{ + error: err, + } +} + +// bundleError is an API error that encompasses an invalid bundle with JSON error +// code and a binary data blob. +type bundleError struct { + error +} + +// ErrorCode returns the JSON error code for an invalid bundle. +// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal +func (e *bundleError) ErrorCode() int { + return InvalidBundleParamError +} diff --git a/internal/ethapi/api_mev.go b/internal/ethapi/api_mev.go index 057b6a3dc1..fee2e70ca6 100644 --- a/internal/ethapi/api_mev.go +++ b/internal/ethapi/api_mev.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" ) @@ -91,3 +92,12 @@ func (m *MevAPI) Params() *types.MevParams { func (m *MevAPI) Running() bool { return m.b.MevRunning() } + +// ReportIssue is served by builder, for receiving issue from validators +func (m *MevAPI) ReportIssue(_ context.Context, args *types.BidIssue) error { + log.Error("received issue", "bidHash", args.BidHash, "message", args.Message) + + // take some action to handle the issue, e.g. add metric, send alert, etc. + + return nil +} diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index d1c05797fd..0c2a36442b 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -612,6 +612,12 @@ func (b testBackend) SubscribeNewVoteEvent(ch chan<- core.NewVoteEvent) event.Su func (b testBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { panic("implement me") } +func (b testBackend) SendBundle(ctx context.Context, bundle *types.Bundle) error { + panic("implement me") +} +func (b testBackend) BundlePrice() *big.Int { + panic("implement me") +} func (b testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) { tx, blockHash, blockNumber, index := rawdb.ReadTransaction(b.db, txHash) return true, tx, blockHash, blockNumber, index, nil diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 114f61fb17..24971f003e 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -78,6 +78,8 @@ type Backend interface { // Transaction pool API SendTx(ctx context.Context, signedTx *types.Transaction) error + SendBundle(ctx context.Context, bundle *types.Bundle) error + BundlePrice() *big.Int GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) GetPoolTransactions() (types.Transactions, error) GetPoolTransaction(txHash common.Hash) *types.Transaction @@ -150,6 +152,9 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "mev", Service: NewMevAPI(apiBackend), + }, { + Namespace: "eth", + Service: NewPrivateTxBundleAPI(apiBackend), }, } } diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 9079743baa..34e4677160 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -388,6 +388,8 @@ func (b *backendMock) SubscribeNewVoteEvent(ch chan<- core.NewVoteEvent) event.S return nil } func (b *backendMock) SendTx(ctx context.Context, signedTx *types.Transaction) error { return nil } +func (b *backendMock) SendBundle(ctx context.Context, bundle *types.Bundle) error { return nil } +func (b *backendMock) BundlePrice() *big.Int { return nil } func (b *backendMock) GetTransaction(ctx context.Context, txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64, error) { return false, nil, [32]byte{}, 0, 0, nil } diff --git a/miner/bidder.go b/miner/bidder.go new file mode 100644 index 0000000000..83a1afbc5e --- /dev/null +++ b/miner/bidder.go @@ -0,0 +1,347 @@ +package miner + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/bidutil" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/miner/validatorclient" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" +) + +const maxBid int64 = 3 + +type ValidatorConfig struct { + Address common.Address + URL string +} + +type validator struct { + *validatorclient.Client + BidSimulationLeftOver time.Duration + GasCeil uint64 +} + +type Bidder struct { + config *MevConfig + delayLeftOver time.Duration + engine consensus.Engine + chain *core.BlockChain + + validatorsMu sync.RWMutex + validators map[common.Address]*validator // address -> validator + + bestWorksMu sync.RWMutex + bestWorks map[int64]*environment + + newBidCh chan *environment + exitCh chan struct{} + + wg sync.WaitGroup + + wallet accounts.Wallet +} + +func NewBidder(config *MevConfig, delayLeftOver time.Duration, engine consensus.Engine, eth Backend) *Bidder { + b := &Bidder{ + config: config, + delayLeftOver: delayLeftOver, + engine: engine, + chain: eth.BlockChain(), + validators: make(map[common.Address]*validator), + bestWorks: make(map[int64]*environment), + newBidCh: make(chan *environment, 10), + exitCh: make(chan struct{}), + } + + if !config.BuilderEnabled { + return b + } + + wallet, err := eth.AccountManager().Find(accounts.Account{Address: config.BuilderAccount}) + if err != nil { + log.Crit("Bidder: failed to find builder account", "err", err) + } + + b.wallet = wallet + + for _, v := range config.Validators { + b.register(v) + } + + if len(b.validators) == 0 { + log.Warn("Bidder: No valid validators") + } + + b.wg.Add(2) + go b.mainLoop() + go b.reconnectLoop() + + return b +} + +func (b *Bidder) mainLoop() { + defer b.wg.Done() + + timer := time.NewTimer(0) + defer timer.Stop() + <-timer.C // discard the initial tick + + var ( + bidNum int64 = 0 + betterBidBefore time.Time + currentHeight = b.chain.CurrentBlock().Number.Int64() + ) + for { + select { + case work := <-b.newBidCh: + if work.header.Number.Int64() > currentHeight { + currentHeight = work.header.Number.Int64() + + bidNum = 0 + parentHeader := b.chain.GetHeaderByHash(work.header.ParentHash) + var bidSimulationLeftOver time.Duration + b.validatorsMu.RLock() + if b.validators[work.coinbase] != nil { + bidSimulationLeftOver = b.validators[work.coinbase].BidSimulationLeftOver + } + b.validatorsMu.RUnlock() + betterBidBefore = bidutil.BidBetterBefore(parentHeader, b.chain.Config().Parlia.Period, b.delayLeftOver, + bidSimulationLeftOver) + + if time.Now().After(betterBidBefore) { + timer.Reset(0) + } else { + timer.Reset(time.Until(betterBidBefore) / time.Duration(maxBid)) + } + } + if bidNum < maxBid && b.isBestWork(work) { + // update the bestWork and do bid + b.setBestWork(work) + } + case <-timer.C: + go func() { + w := b.getBestWork(currentHeight) + if w != nil { + b.bid(w) + bidNum++ + if bidNum < maxBid && time.Now().Before(betterBidBefore) { + timer.Reset(time.Until(betterBidBefore) / time.Duration(maxBid-bidNum)) + } + } + }() + case <-b.exitCh: + return + } + } +} + +func (b *Bidder) reconnectLoop() { + defer b.wg.Done() + + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + for _, v := range b.config.Validators { + if b.isRegistered(v.Address) { + continue + } + + b.register(v) + } + case <-b.exitCh: + return + } + } +} + +func (b *Bidder) isRegistered(validator common.Address) bool { + b.validatorsMu.RLock() + defer b.validatorsMu.RUnlock() + _, ok := b.validators[validator] + return ok +} + +func (b *Bidder) register(cfg ValidatorConfig) { + b.validatorsMu.Lock() + defer b.validatorsMu.Unlock() + + cl, err := validatorclient.DialOptions(context.Background(), cfg.URL, rpc.WithHTTPClient(client)) + if err != nil { + log.Error("Bidder: failed to dial validator", "url", cfg.URL, "err", err) + return + } + + params, err := cl.MevParams(context.Background()) + if err != nil { + log.Error("Bidder: failed to get mev params", "url", cfg.URL, "err", err) + return + } + + b.validators[cfg.Address] = &validator{ + Client: cl, + BidSimulationLeftOver: params.BidSimulationLeftOver, + GasCeil: params.GasCeil, + } +} + +func (b *Bidder) unregister(validator common.Address) { + b.validatorsMu.Lock() + defer b.validatorsMu.Unlock() + delete(b.validators, validator) +} + +func (b *Bidder) newWork(work *environment) { + if !b.enabled() { + return + } + + if work.profit.Cmp(common.Big0) <= 0 { + return + } + + b.newBidCh <- work +} + +func (b *Bidder) exit() { + close(b.exitCh) + b.wg.Wait() +} + +// bid notifies the next in-turn validator the work +// 1. compute the return profit for builder based on realtime traffic and validator commission +// 2. send bid to validator +func (b *Bidder) bid(work *environment) { + var ( + parent = b.chain.CurrentBlock() + bidArgs types.BidArgs + cli *validator + ) + + b.validatorsMu.RLock() + cli = b.validators[work.coinbase] + b.validatorsMu.RUnlock() + if cli == nil { + log.Info("Bidder: validator not integrated", "validator", work.coinbase) + return + } + + // construct bid from work + { + var txs []hexutil.Bytes + for _, tx := range work.txs { + var txBytes []byte + var err error + txBytes, err = tx.MarshalBinary() + if err != nil { + log.Error("Bidder: fail to marshal tx", "tx", tx, "err", err) + return + } + txs = append(txs, txBytes) + } + + bid := types.RawBid{ + BlockNumber: parent.Number.Uint64() + 1, + ParentHash: parent.Hash(), + GasUsed: work.header.GasUsed, + GasFee: work.state.GetBalance(consensus.SystemAddress).ToBig(), + Txs: txs, + UnRevertible: work.UnRevertible, + // TODO: decide builderFee according to realtime traffic and validator commission + } + + signature, err := b.signBid(&bid) + if err != nil { + log.Error("Bidder: fail to sign bid", "err", err) + return + } + + bidArgs = types.BidArgs{ + RawBid: &bid, + Signature: signature, + } + } + + _, err := cli.SendBid(context.Background(), bidArgs) + if err != nil { + b.deleteBestWork(work) + log.Error("Bidder: bidding failed", "err", err) + + var bidErr rpc.Error + ok := errors.As(err, &bidErr) + if ok && bidErr.ErrorCode() == types.MevNotRunningError { + b.unregister(work.coinbase) + } + + return + } + + b.deleteBestWork(work) + log.Info("Bidder: bidding success", "number", work.header.Number, "txs", len(work.txs)) +} + +// isBestWork returns the work is better than the current best work +func (b *Bidder) isBestWork(work *environment) bool { + if work.profit == nil { + return false + } + + last := b.getBestWork(work.header.Number.Int64()) + if last == nil { + return true + } + + return last.profit.Cmp(work.profit) < 0 +} + +// setBestWork sets the best work +func (b *Bidder) setBestWork(work *environment) { + b.bestWorksMu.Lock() + defer b.bestWorksMu.Unlock() + + b.bestWorks[work.header.Number.Int64()] = work +} + +// deleteBestWork sets the best work +func (b *Bidder) deleteBestWork(work *environment) { + b.bestWorksMu.Lock() + defer b.bestWorksMu.Unlock() + + delete(b.bestWorks, work.header.Number.Int64()) +} + +// getBestWork returns the best work +func (b *Bidder) getBestWork(blockNumber int64) *environment { + b.bestWorksMu.RLock() + defer b.bestWorksMu.RUnlock() + + return b.bestWorks[blockNumber] +} + +// signBid signs the bid with builder's account +func (b *Bidder) signBid(bid *types.RawBid) ([]byte, error) { + bz, err := rlp.EncodeToBytes(bid) + if err != nil { + return nil, err + } + + return b.wallet.SignData(accounts.Account{Address: b.config.BuilderAccount}, accounts.MimetypeTextPlain, bz) +} + +// enabled returns whether the bid is enabled +func (b *Bidder) enabled() bool { + return b.config.BuilderEnabled +} diff --git a/miner/bundle_cache.go b/miner/bundle_cache.go new file mode 100644 index 0000000000..54609682c5 --- /dev/null +++ b/miner/bundle_cache.go @@ -0,0 +1,88 @@ +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]*types.SimulatedBundle + failedBundles map[common.Hash]struct{} +} + +func newCacheEntry(header common.Hash) *BundleCacheEntry { + return &BundleCacheEntry{ + headerHash: header, + successfulBundles: make(map[common.Hash]*types.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 map[common.Hash]*types.SimulatedBundle, bundles []*types.Bundle) { + c.mu.Lock() + defer c.mu.Unlock() + + for _, bundle := range bundles { + if bundle == nil { + continue + } + + bundleHash := bundle.Hash() + + if result[bundleHash] != nil { + c.successfulBundles[bundleHash] = result[bundleHash] + } else { + c.failedBundles[bundleHash] = struct{}{} + } + } +} diff --git a/miner/miner.go b/miner/miner.go index 41f93ea388..25dab62171 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -18,16 +18,21 @@ package miner import ( + "errors" "fmt" "math/big" "sync" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/systemcontracts" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" @@ -41,6 +46,7 @@ import ( type Backend interface { BlockChain() *core.BlockChain TxPool() *txpool.TxPool + AccountManager() *accounts.Manager } // Config is the configuration parameters of mining. @@ -54,6 +60,8 @@ type Config struct { Recommit time.Duration // The time interval for miner to re-create mining work. VoteEnable bool // Whether to vote when mining + MevGasPriceFloor int64 `toml:",omitempty"` + NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload DisableVoteAttestation bool // Whether to skip assembling vote attestation @@ -299,3 +307,75 @@ func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { func (miner *Miner) GasCeil() uint64 { return miner.worker.getGasCeil() } + +func (miner *Miner) SimulateBundle(bundle *types.Bundle) (*big.Int, error) { + parent := miner.eth.BlockChain().CurrentBlock() + timestamp := time.Now().Unix() + if parent.Time >= uint64(timestamp) { + timestamp = int64(parent.Time + 1) + } + + header := &types.Header{ + ParentHash: parent.Hash(), + Number: new(big.Int).Add(parent.Number, common.Big1), + GasLimit: core.CalcGasLimit(parent.GasLimit, miner.worker.config.GasCeil), + Extra: miner.worker.extra, + Time: uint64(timestamp), + Coinbase: miner.worker.etherbase(), + } + + // Set baseFee and GasLimit if we are on an EIP-1559 chain + if miner.worker.chainConfig.IsLondon(header.Number) { + header.BaseFee = eip1559.CalcBaseFee(miner.worker.chainConfig, parent) + } + + if err := miner.worker.engine.Prepare(miner.eth.BlockChain(), header); err != nil { + return nil, err + } + + // Apply EIP-4844, EIP-4788. + if miner.worker.chainConfig.IsCancun(header.Number, header.Time) { + var excessBlobGas uint64 + if miner.worker.chainConfig.IsCancun(parent.Number, parent.Time) { + excessBlobGas = eip4844.CalcExcessBlobGas(*parent.ExcessBlobGas, *parent.BlobGasUsed) + } else { + // For the first post-fork block, both parent.data_gas_used and parent.excess_data_gas are evaluated as 0 + excessBlobGas = eip4844.CalcExcessBlobGas(0, 0) + } + header.BlobGasUsed = new(uint64) + header.ExcessBlobGas = &excessBlobGas + if miner.worker.chainConfig.Parlia != nil { + header.WithdrawalsHash = &types.EmptyWithdrawalsHash + } + // if miner.worker.chainConfig.Parlia == nil { + // header.ParentBeaconRoot = genParams.beaconRoot + // } + } + + state, err := miner.eth.BlockChain().StateAt(parent.Root) + if err != nil { + return nil, err + } + + env := &environment{ + header: header, + state: state.Copy(), + signer: types.MakeSigner(miner.worker.chainConfig, header.Number, header.Time), + } + + if !miner.worker.chainConfig.IsFeynman(header.Number, header.Time) { + // Handle upgrade build-in system contract code + systemcontracts.UpgradeBuildInSystemContract(miner.worker.chainConfig, header.Number, parent.Time, header.Time, env.state) + } + + s, err := miner.worker.simulateBundles(env, []*types.Bundle{bundle}) + if err != nil { + return nil, err + } + + if len(s) == 0 { + return nil, errors.New("no valid sim result") + } + + return s[0].BundleGasPrice, nil +} diff --git a/miner/miner_mev.go b/miner/miner_mev.go index f4ced7f8d5..fb931e1b93 100644 --- a/miner/miner_mev.go +++ b/miner/miner_mev.go @@ -24,6 +24,10 @@ type MevConfig struct { Builders []BuilderConfig // The list of builders ValidatorCommission uint64 // 100 means the validator claims 1% from block reward BidSimulationLeftOver time.Duration + + BuilderEnabled bool // Whether to enable bidder or not + Validators []ValidatorConfig // The list of validators + BuilderAccount common.Address // The account of the bidder } var DefaultMevConfig = MevConfig{ @@ -32,6 +36,9 @@ var DefaultMevConfig = MevConfig{ Builders: nil, ValidatorCommission: 100, BidSimulationLeftOver: 50 * time.Millisecond, + BuilderEnabled: false, + Validators: nil, + BuilderAccount: common.Address{}, } // MevRunning return true if mev is running. diff --git a/miner/miner_test.go b/miner/miner_test.go index 5907fb4464..8b132ec47f 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/core" @@ -43,12 +44,14 @@ import ( type mockBackend struct { bc *core.BlockChain txPool *txpool.TxPool + accman *accounts.Manager } -func NewMockBackend(bc *core.BlockChain, txPool *txpool.TxPool) *mockBackend { +func NewMockBackend(bc *core.BlockChain, txPool *txpool.TxPool, accman *accounts.Manager) *mockBackend { return &mockBackend{ bc: bc, txPool: txPool, + accman: accman, } } @@ -60,6 +63,10 @@ func (m *mockBackend) TxPool() *txpool.TxPool { return m.txPool } +func (m *mockBackend) AccountManager() *accounts.Manager { + return m.accman +} + func (m *mockBackend) StateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, checkLive bool, preferDisk bool) (statedb *state.StateDB, err error) { return nil, errors.New("not supported") } @@ -319,8 +326,9 @@ func createMiner(t *testing.T) (*Miner, *event.TypeMux, func(skipMiner bool)) { pool := legacypool.New(testTxPoolConfig, blockchain) txpool, _ := txpool.New(testTxPoolConfig.PriceLimit, blockchain, []txpool.SubPool{pool}) + accman := accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: true}) - backend := NewMockBackend(bc, txpool) + backend := NewMockBackend(bc, txpool, accman) // Create event Mux mux := new(event.TypeMux) // Create Miner diff --git a/miner/ordering.go b/miner/ordering.go index 7cbe2d5630..5c432dc9a0 100644 --- a/miner/ordering.go +++ b/miner/ordering.go @@ -20,10 +20,11 @@ import ( "container/heap" "math/big" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" - "github.com/holiman/uint256" ) // txWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap @@ -205,11 +206,11 @@ func (t *transactionsByPriceAndNonce) Forward(tx *types.Transaction) { } return } - //check whether target tx exists in t.heads + // check whether target tx exists in t.heads for _, head := range t.heads { if head.tx != nil && head.tx.Resolve() != nil { if tx == head.tx.Tx { - //shift t to the position one after tx + // shift t to the position one after tx txTmp := t.PeekWithUnwrap() for txTmp != tx { t.Shift() @@ -220,13 +221,13 @@ func (t *transactionsByPriceAndNonce) Forward(tx *types.Transaction) { } } } - //get the sender address of tx + // get the sender address of tx acc, _ := types.Sender(t.signer, tx) - //check whether target tx exists in t.txs + // check whether target tx exists in t.txs if txs, ok := t.txs[acc]; ok { for _, txLazyTmp := range txs { if txLazyTmp != nil && txLazyTmp.Resolve() != nil { - //found the same pointer in t.txs as tx and then shift t to the position one after tx + // found the same pointer in t.txs as tx and then shift t to the position one after tx if tx == txLazyTmp.Tx { txTmp := t.PeekWithUnwrap() for txTmp != tx { diff --git a/miner/validatorclient/validatorclient.go b/miner/validatorclient/validatorclient.go new file mode 100644 index 0000000000..8c56307449 --- /dev/null +++ b/miner/validatorclient/validatorclient.go @@ -0,0 +1,67 @@ +package validatorclient + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +// Client defines typed wrappers for the Ethereum RPC API. +type Client struct { + c *rpc.Client +} + +// DialOptions creates a new RPC client for the given URL. You can supply any of the +// pre-defined client options to configure the underlying transport. +func DialOptions(ctx context.Context, rawurl string, opts ...rpc.ClientOption) (*Client, error) { + c, err := rpc.DialOptions(ctx, rawurl, opts...) + if err != nil { + return nil, err + } + return newClient(c), nil +} + +// newClient creates a client that uses the given RPC client. +func newClient(c *rpc.Client) *Client { + return &Client{c} +} + +// MevRunning returns whether MEV is running +func (ec *Client) MevRunning(ctx context.Context) (bool, error) { + var result bool + err := ec.c.CallContext(ctx, &result, "mev_running") + return result, err +} + +// SendBid sends a bid +func (ec *Client) SendBid(ctx context.Context, args types.BidArgs) (common.Hash, error) { + var hash common.Hash + err := ec.c.CallContext(ctx, &hash, "mev_sendBid", args) + if err != nil { + return common.Hash{}, err + } + return hash, nil +} + +// BestBidGasFee returns the gas fee of the best bid for the given parent hash. +func (ec *Client) BestBidGasFee(ctx context.Context, parentHash common.Hash) (*big.Int, error) { + var fee *big.Int + err := ec.c.CallContext(ctx, &fee, "mev_bestBidGasFee", parentHash) + if err != nil { + return nil, err + } + return fee, nil +} + +// MevParams returns the static params of mev +func (ec *Client) MevParams(ctx context.Context) (*types.MevParams, error) { + var params types.MevParams + err := ec.c.CallContext(ctx, ¶ms, "mev_params") + if err != nil { + return nil, err + } + return ¶ms, err +} diff --git a/miner/worker.go b/miner/worker.go index 1677dc12b4..ac09fb4121 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" - "github.com/ethereum/go-ethereum/consensus/parlia" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/systemcontracts" @@ -94,6 +93,9 @@ type environment struct { receipts []*types.Receipt sidecars types.BlobSidecars blobs int + + profit *big.Int // block gas fee + BNBSentToSystem + UnRevertible []common.Hash } // copy creates a deep copy of environment. @@ -147,6 +149,9 @@ const ( commitInterruptTimeout commitInterruptOutOfGas commitInterruptBetterBid + commitInterruptBundleTxNil + commitInterruptBundleTxProtected + commitInterruptBundleCommit ) // newWorkReq represents a request for new sealing work submitting with relative interrupt notifier. @@ -242,6 +247,10 @@ type worker struct { fullTaskHook func() // Method to call before pushing the full sealing task. resubmitHook func(time.Duration, time.Duration) // Method to call upon updating resubmitting interval. recentMinedBlocks *lru.Cache + + // MEV + bidder *Bidder + bundleCache *BundleCache } func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *worker { @@ -268,6 +277,8 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus exitCh: make(chan struct{}), resubmitIntervalCh: make(chan time.Duration), recentMinedBlocks: recentMinedBlocks, + bidder: NewBidder(&config.Mev, config.DelayLeftOver, engine, eth), + bundleCache: NewBundleCache(), } // Subscribe events for blockchain worker.chainHeadSub = eth.BlockChain().SubscribeChainHeadEvent(worker.chainHeadCh) @@ -291,11 +302,15 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus } worker.newpayloadTimeout = newpayloadTimeout - worker.wg.Add(4) + worker.wg.Add(2) go worker.mainLoop() go worker.newWorkLoop(recommit) - go worker.resultLoop() - go worker.taskLoop() + // if not builder + if !worker.bidder.enabled() { + worker.wg.Add(2) + go worker.resultLoop() + go worker.taskLoop() + } // Submit first work to initialize pending state. if init { @@ -460,17 +475,6 @@ func (w *worker) newWorkLoop(recommit time.Duration) { } clearPending(head.Block.NumberU64()) timestamp = time.Now().Unix() - if p, ok := w.engine.(*parlia.Parlia); ok { - signedRecent, err := p.SignRecently(w.chain, head.Block) - if err != nil { - log.Debug("Not allowed to propose block", "err", err) - continue - } - if signedRecent { - log.Info("Signed recently, must wait") - continue - } - } commit(commitInterruptNewHead) case <-timer.C: @@ -523,6 +527,7 @@ func (w *worker) mainLoop() { // System stopped case <-w.exitCh: + w.bidder.exit() return case <-w.chainHeadSub.Err(): return @@ -702,6 +707,7 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co state: state, coinbase: coinbase, header: header, + profit: big.NewInt(0), } // Keep track of transactions which return errors so they can be removed env.tcount = 0 @@ -735,6 +741,10 @@ func (w *worker) commitTransaction(env *environment, tx *types.Transaction, rece } 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, tx.GasPrice())) + return receipt.Logs, nil } @@ -756,11 +766,15 @@ func (w *worker) commitBlobTransaction(env *environment, tx *types.Transaction, return nil, err } sc.TxIndex = uint64(len(env.txs)) - env.txs = append(env.txs, tx.WithoutBlobTxSidecar()) + env.txs = append(env.txs, tx) env.receipts = append(env.receipts, receipt) env.sidecars = append(env.sidecars, sc) env.blobs += len(sc.Blobs) *env.header.BlobGasUsed += receipt.BlobGasUsed + + gasUsed := new(big.Int).SetUint64(receipt.GasUsed) + env.profit.Add(env.profit, gasUsed.Mul(gasUsed, tx.GasPrice())) + return receipt.Logs, nil } @@ -1177,10 +1191,40 @@ func (w *worker) commitWork(interruptCh chan int32, timestamp int64) { // Set the coinbase if the worker is running or it's required var coinbase common.Address if w.isRunning() { - coinbase = w.etherbase() - if coinbase == (common.Address{}) { - log.Error("Refusing to mine without etherbase") - return + if w.bidder.enabled() { + var err error + // take the next in-turn validator as coinbase + coinbase, err = w.engine.NextInTurnValidator(w.chain, w.chain.CurrentBlock()) + if err != nil { + log.Error("Failed to get next in-turn validator", "err", err) + return + } + + // do not build work if not register to the coinbase + if !w.bidder.isRegistered(coinbase) { + log.Warn("Refusing to mine with unregistered validator") + return + } + + // set validator to the consensus engine + if posa, ok := w.engine.(consensus.PoSA); ok { + posa.SetValidator(coinbase) + } else { + log.Warn("Consensus engine does not support validator setting") + return + } + + w.bidder.validatorsMu.Lock() + if w.bidder.validators[coinbase] != nil { + w.config.GasCeil = w.bidder.validators[coinbase].GasCeil + } + w.bidder.validatorsMu.Unlock() + } else { + coinbase = w.etherbase() + if coinbase == (common.Address{}) { + log.Error("Refusing to mine without etherbase") + return + } } } @@ -1247,7 +1291,7 @@ LOOP: // Fill pending transactions from the txpool into the block. fillStart := time.Now() - err = w.fillTransactions(interruptCh, work, stopTimer, nil) + err = w.fillTransactionsAndBundles(interruptCh, work, stopTimer) fillDuration := time.Since(fillStart) switch { case errors.Is(err, errBlockInterruptedByNewHead): @@ -1264,6 +1308,8 @@ LOOP: break LOOP } + w.bidder.newWork(work) + if interruptCh == nil || stopTimer == nil { // it is single commit work, no need to try several time. log.Info("commitWork interruptCh or stopTimer is nil") @@ -1275,6 +1321,7 @@ LOOP: // but now it is used to wait until (head.Time - DelayLeftOver) is reached. stopTimer.Reset(time.Until(time.Unix(int64(work.header.Time), 0)) - w.config.DelayLeftOver) LOOP_WAIT: + // TODO consider whether to take bundle pool status as LOOP_WAIT condition for { select { case <-stopTimer.C: @@ -1386,7 +1433,7 @@ func (w *worker) inTurn() bool { // Note the assumption is held that the mutation is allowed to the passed env, do // the deep copy first. func (w *worker) commit(env *environment, interval func(), update bool, start time.Time) error { - if w.isRunning() { + if w.isRunning() && !w.bidder.enabled() { if interval != nil { interval() } diff --git a/miner/worker_builder.go b/miner/worker_builder.go new file mode 100644 index 0000000000..469d267a2e --- /dev/null +++ b/miner/worker_builder.go @@ -0,0 +1,488 @@ +package miner + +import ( + "errors" + "math/big" + "sort" + "sync" + "time" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc/eip4844" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +var ( + errNonRevertingTxInBundleFailed = errors.New("non-reverting tx in bundle failed") + errBundlePriceTooLow = errors.New("bundle price too low") +) + +// fillTransactions retrieves the pending bundles and transactions from the txpool and fills them +// into the given sealing block. The selection and ordering strategy can be extended in the future. +func (w *worker) fillTransactionsAndBundles(interruptCh chan int32, env *environment, stopTimer *time.Timer) error { + env.state.StopPrefetcher() // no need to prefetch txs for a builder + + var ( + localPlainTxs map[common.Address][]*txpool.LazyTransaction + remotePlainTxs map[common.Address][]*txpool.LazyTransaction + localBlobTxs map[common.Address][]*txpool.LazyTransaction + remoteBlobTxs map[common.Address][]*txpool.LazyTransaction + bundles []*types.Bundle + ) + + // commit bundles + { + bundles = w.eth.TxPool().PendingBundles(env.header.Number.Uint64(), env.header.Time) + + // if no bundles, not necessary to fill transactions + if len(bundles) == 0 { + return errors.New("no bundles in bundle pool") + } + + txs, bundle, err := w.generateOrderedBundles(env, bundles) + if err != nil { + log.Error("fail to generate ordered bundles", "err", err) + return err + } + + if err = w.commitBundles(env, txs, interruptCh, stopTimer); err != nil { + log.Error("fail to commit bundles", "err", err) + return err + } + + env.profit.Add(env.profit, bundle.EthSentToSystem) + log.Info("fill bundles", "bundles_count", len(bundles)) + } + + // commit normal transactions + { + w.mu.RLock() + tip := w.tip + w.mu.RUnlock() + + // Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees + filter := txpool.PendingFilter{ + MinTip: tip, + } + if env.header.BaseFee != nil { + filter.BaseFee = uint256.MustFromBig(env.header.BaseFee) + } + if env.header.ExcessBlobGas != nil { + filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(*env.header.ExcessBlobGas)) + } + filter.OnlyPlainTxs, filter.OnlyBlobTxs = true, false + pendingPlainTxs := w.eth.TxPool().Pending(filter) + + filter.OnlyPlainTxs, filter.OnlyBlobTxs = false, true + pendingBlobTxs := w.eth.TxPool().Pending(filter) + + // Split the pending transactions into locals and remotes + // Fill the block with all available pending transactions. + localPlainTxs, remotePlainTxs = make(map[common.Address][]*txpool.LazyTransaction), pendingPlainTxs + localBlobTxs, remoteBlobTxs = make(map[common.Address][]*txpool.LazyTransaction), pendingBlobTxs + + for _, account := range w.eth.TxPool().Locals() { + if txs := remotePlainTxs[account]; len(txs) > 0 { + delete(remotePlainTxs, account) + localPlainTxs[account] = txs + } + if txs := remoteBlobTxs[account]; len(txs) > 0 { + delete(remoteBlobTxs, account) + localBlobTxs[account] = txs + } + } + log.Info("fill transactions", "plain_txs_count", len(localPlainTxs)+len(remotePlainTxs), "blob_txs_count", len(localBlobTxs)+len(remoteBlobTxs)) + } + + // Fill the block with all available pending transactions. + // we will abort when: + // 1.new block was imported + // 2.out of Gas, no more transaction can be added. + // 3.the mining timer has expired, stop adding transactions. + // 4.interrupted resubmit timer, which is by default 10s. + // resubmit is for PoW only, can be deleted for PoS consensus later + if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 { + plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, env.header.BaseFee) + blobTxs := newTransactionsByPriceAndNonce(env.signer, localBlobTxs, env.header.BaseFee) + + if err := w.commitTransactions(env, plainTxs, blobTxs, interruptCh, stopTimer); err != nil { + return err + } + } + if len(remotePlainTxs) > 0 || len(remoteBlobTxs) > 0 { + plainTxs := newTransactionsByPriceAndNonce(env.signer, remotePlainTxs, env.header.BaseFee) + blobTxs := newTransactionsByPriceAndNonce(env.signer, remoteBlobTxs, env.header.BaseFee) + + if err := w.commitTransactions(env, plainTxs, blobTxs, interruptCh, stopTimer); err != nil { + return err + } + } + log.Info("fill bundles and transactions done", "total_txs_count", len(env.txs)) + return nil +} + +func (w *worker) commitBundles( + env *environment, + txs types.Transactions, + interruptCh chan int32, + stopTimer *time.Timer, +) error { + if env.gasPool == nil { + env.gasPool = prepareGasPool(env.header.GasLimit) + } + + var coalescedLogs []*types.Log + signal := commitInterruptNone +LOOP: + for _, tx := range txs { + // In the following three cases, we will interrupt the execution of the transaction. + // (1) new head block event arrival, the reason is 1 + // (2) worker start or restart, the reason is 1 + // (3) worker recreate the sealing block with any newly arrived transactions, the reason is 2. + // For the first two cases, the semi-finished work will be discarded. + // For the third case, the semi-finished work will be submitted to the consensus engine. + if interruptCh != nil { + select { + case signal, ok := <-interruptCh: + if !ok { + // should never be here, since interruptCh should not be read before + log.Warn("commit transactions stopped unknown") + } + return signalToErr(signal) + default: + } + } // If we don't have enough gas for any further transactions then we're done + if env.gasPool.Gas() < params.TxGas { + log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) + signal = commitInterruptOutOfGas + break + } + if tx == nil { + log.Error("Unexpected nil transaction in bundle") + return signalToErr(commitInterruptBundleTxNil) + } + if stopTimer != nil { + select { + case <-stopTimer.C: + log.Info("Not enough time for further transactions", "txs", len(env.txs)) + stopTimer.Reset(0) // re-active the timer, in case it will be used later. + signal = commitInterruptTimeout + break LOOP + default: + } + } + + // 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.Debug("Unexpected protected transaction in bundle") + return signalToErr(commitInterruptBundleTxProtected) + } + // Start executing the transaction + env.state.SetTxContext(tx.Hash(), env.tcount) + + logs, err := w.commitTransaction(env, tx, core.NewReceiptBloomGenerator()) + switch err { + case core.ErrGasLimitReached: + // Pop the current out-of-gas transaction without shifting in the next from the account + log.Error("Unexpected gas limit exceeded for current block in the bundle", "sender", from) + return signalToErr(commitInterruptBundleCommit) + + case core.ErrNonceTooLow: + // New head notification data race between the transaction pool and miner, shift + log.Error("Transaction with low nonce in the bundle", "sender", from, "nonce", tx.Nonce()) + return signalToErr(commitInterruptBundleCommit) + + case core.ErrNonceTooHigh: + // Reorg notification data race between the transaction pool and miner, skip account = + log.Error("Account with high nonce in the bundle", "sender", from, "nonce", tx.Nonce()) + return signalToErr(commitInterruptBundleCommit) + + case nil: + // Everything ok, collect the logs and shift in the next transaction from the same account + coalescedLogs = append(coalescedLogs, logs...) + env.tcount++ + continue + + 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.Error("Transaction failed in the bundle", "hash", tx.Hash(), "err", err) + return signalToErr(commitInterruptBundleCommit) + } + } + + if !w.isRunning() && len(coalescedLogs) > 0 { + // We don't push the pendingLogsEvent while we are mining. The reason is that + // when we are mining, the worker will regenerate a mining 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) + } + return signalToErr(signal) +} + +// generateOrderedBundles generates ordered txs from the given bundles. +// 1. sort bundles according to computed gas price when received. +// 2. simulate bundles based on the same state, resort. +// 3. merge resorted simulateBundles based on the iterative state. +func (w *worker) generateOrderedBundles( + env *environment, + bundles []*types.Bundle, +) (types.Transactions, *types.SimulatedBundle, error) { + // sort bundles according to gas price computed when received + sort.SliceStable(bundles, func(i, j int) bool { + priceI, priceJ := bundles[i].Price, bundles[j].Price + + return priceI.Cmp(priceJ) >= 0 + }) + + // recompute bundle gas price based on the same state and current env + simulatedBundles, err := w.simulateBundles(env, bundles) + if err != nil { + log.Error("fail to simulate bundles base on the same state", "err", err) + return nil, nil, err + } + + // sort bundles according to fresh gas price + sort.SliceStable(simulatedBundles, func(i, j int) bool { + priceI, priceJ := simulatedBundles[i].BundleGasPrice, simulatedBundles[j].BundleGasPrice + + return priceI.Cmp(priceJ) >= 0 + }) + + // merge bundles based on iterative state + includedTxs, mergedBundle, err := w.mergeBundles(env, simulatedBundles) + if err != nil { + log.Error("fail to merge bundles", "err", err) + return nil, nil, err + } + + return includedTxs, mergedBundle, nil +} + +func (w *worker) simulateBundles(env *environment, bundles []*types.Bundle) ([]*types.SimulatedBundle, error) { + headerHash := env.header.Hash() + simCache := w.bundleCache.GetBundleCache(headerHash) + simResult := make(map[common.Hash]*types.SimulatedBundle) + + var wg sync.WaitGroup + var mu sync.Mutex + for i, bundle := range bundles { + if simmed, ok := simCache.GetSimulatedBundle(bundle.Hash()); ok { + mu.Lock() + simResult[bundle.Hash()] = simmed + mu.Unlock() + continue + } + + wg.Add(1) + go func(idx int, bundle *types.Bundle, state *state.StateDB) { + defer wg.Done() + + gasPool := prepareGasPool(env.header.GasLimit) + simmed, err := w.simulateBundle(env, bundle, state, gasPool, 0, true, true) + if err != nil { + log.Trace("Error computing gas for a simulateBundle", "error", err) + return + } + + mu.Lock() + defer mu.Unlock() + simResult[bundle.Hash()] = simmed + }(i, bundle, env.state.Copy()) + } + + wg.Wait() + + simulatedBundles := make([]*types.SimulatedBundle, 0) + + for _, bundle := range simResult { + if bundle == nil { + continue + } + + simulatedBundles = append(simulatedBundles, bundle) + } + + simCache.UpdateSimulatedBundles(simResult, bundles) + + return simulatedBundles, nil +} + +// mergeBundles merges the given simulateBundle into the given environment. +// It returns the merged simulateBundle and the number of transactions that were merged. +func (w *worker) mergeBundles( + env *environment, + bundles []*types.SimulatedBundle, +) (types.Transactions, *types.SimulatedBundle, error) { + currentState := env.state.Copy() + gasPool := prepareGasPool(env.header.GasLimit) + + includedTxs := types.Transactions{} + mergedBundle := types.SimulatedBundle{ + BundleGasFees: new(big.Int), + BundleGasUsed: 0, + BundleGasPrice: new(big.Int), + EthSentToSystem: new(big.Int), + } + + 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.BundleGasPrice, big.NewInt(99)) + floorGasPrice = floorGasPrice.Div(floorGasPrice, big.NewInt(100)) + + simulatedBundle, err := w.simulateBundle(env, bundle.OriginalBundle, currentState, gasPool, len(includedTxs), true, false) + + if err != nil || simulatedBundle.BundleGasPrice.Cmp(floorGasPrice) <= 0 { + currentState = prevState + gasPool = prevGasPool + + log.Error("failed to merge bundle", "floorGasPrice", floorGasPrice, "err", err) + continue + } + + log.Info("included bundle", + "gasUsed", simulatedBundle.BundleGasUsed, + "gasPrice", simulatedBundle.BundleGasPrice, + "txcount", len(simulatedBundle.OriginalBundle.Txs)) + + includedTxs = append(includedTxs, bundle.OriginalBundle.Txs...) + + mergedBundle.BundleGasFees.Add(mergedBundle.BundleGasFees, simulatedBundle.BundleGasFees) + mergedBundle.BundleGasUsed += simulatedBundle.BundleGasUsed + + for _, tx := range includedTxs { + if !containsHash(bundle.OriginalBundle.RevertingTxHashes, tx.Hash()) { + env.UnRevertible = append(env.UnRevertible, tx.Hash()) + } + } + } + + if len(includedTxs) == 0 { + return nil, nil, errors.New("include no txs when merge bundles") + } + + mergedBundle.BundleGasPrice.Div(mergedBundle.BundleGasFees, new(big.Int).SetUint64(mergedBundle.BundleGasUsed)) + + return includedTxs, &mergedBundle, nil +} + +// simulateBundle computes the gas price for a whole simulateBundle based on the same ctx +// named computeBundleGas in flashbots +func (w *worker) simulateBundle( + env *environment, bundle *types.Bundle, state *state.StateDB, gasPool *core.GasPool, currentTxCount int, + prune, pruneGasExceed bool, +) (*types.SimulatedBundle, error) { + var ( + tempGasUsed uint64 + bundleGasUsed uint64 + bundleGasFees = new(big.Int) + ethSentToSystem = new(big.Int) + ) + + for i, tx := range bundle.Txs { + state.SetTxContext(tx.Hash(), i+currentTxCount) + sysBalanceBefore := state.GetBalance(consensus.SystemAddress) + + receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &w.coinbase, gasPool, state, env.header, tx, + &tempGasUsed, *w.chain.GetVMConfig()) + if err != nil { + log.Warn("fail to simulate bundle", "hash", bundle.Hash().String(), "err", err) + + if prune { + if errors.Is(err, core.ErrGasLimitReached) && !pruneGasExceed { + log.Warn("bundle gas limit exceed", "hash", bundle.Hash().String()) + } else { + log.Warn("prune bundle", "hash", bundle.Hash().String(), "err", err) + w.eth.TxPool().PruneBundle(bundle.Hash()) + } + } + + return nil, err + } + + if receipt.Status == types.ReceiptStatusFailed && !containsHash(bundle.RevertingTxHashes, receipt.TxHash) { + err = errNonRevertingTxInBundleFailed + log.Warn("fail to simulate bundle", "hash", bundle.Hash().String(), "err", err) + + if prune { + w.eth.TxPool().PruneBundle(bundle.Hash()) + log.Warn("prune bundle", "hash", bundle.Hash().String()) + } + + return nil, err + } + + bundleGasUsed += receipt.GasUsed + + txGasUsed := new(big.Int).SetUint64(receipt.GasUsed) + txGasFees := new(big.Int).Mul(txGasUsed, tx.GasPrice()) + bundleGasFees.Add(bundleGasFees, txGasFees) + sysBalanceAfter := state.GetBalance(consensus.SystemAddress) + sysDelta := new(uint256.Int).Sub(sysBalanceAfter, sysBalanceBefore) + sysDelta.Sub(sysDelta, uint256.MustFromBig(txGasFees)) + ethSentToSystem.Add(ethSentToSystem, sysDelta.ToBig()) + } + + bundleGasPrice := new(big.Int).Div(bundleGasFees, new(big.Int).SetUint64(bundleGasUsed)) + + if bundleGasPrice.Cmp(big.NewInt(w.config.MevGasPriceFloor)) < 0 { + err := errBundlePriceTooLow + log.Warn("fail to simulate bundle", "hash", bundle.Hash().String(), "err", err) + + if prune { + log.Warn("prune bundle", "hash", bundle.Hash().String()) + w.eth.TxPool().PruneBundle(bundle.Hash()) + } + + return nil, err + } + + return &types.SimulatedBundle{ + OriginalBundle: bundle, + BundleGasFees: bundleGasFees, + BundleGasPrice: bundleGasPrice, + BundleGasUsed: bundleGasUsed, + EthSentToSystem: ethSentToSystem, + }, nil +} + +func containsHash(arr []common.Hash, match common.Hash) bool { + for _, elem := range arr { + if elem == match { + return true + } + } + return false +} + +func prepareGasPool(gasLimit uint64) *core.GasPool { + gasPool := new(core.GasPool).AddGas(gasLimit) + gasPool.SubGas(params.SystemTxsGas) // reserve gas for system txs(keep align with mainnet) + return gasPool +} diff --git a/miner/worker_test.go b/miner/worker_test.go index 268f3f69a5..50a957675c 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -21,6 +21,8 @@ import ( "testing" "time" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -36,7 +38,6 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" ) const ( @@ -111,6 +112,7 @@ type testWorkerBackend struct { txPool *txpool.TxPool chain *core.BlockChain genesis *core.Genesis + accman *accounts.Manager } func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, n int) *testWorkerBackend { @@ -141,11 +143,13 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine chain: chain, txPool: txpool, genesis: gspec, + accman: accounts.NewManager(&accounts.Config{InsecureUnlockAllowed: true}), } } -func (b *testWorkerBackend) BlockChain() *core.BlockChain { return b.chain } -func (b *testWorkerBackend) TxPool() *txpool.TxPool { return b.txPool } +func (b *testWorkerBackend) BlockChain() *core.BlockChain { return b.chain } +func (b *testWorkerBackend) TxPool() *txpool.TxPool { return b.txPool } +func (b *testWorkerBackend) AccountManager() *accounts.Manager { return b.accman } func (b *testWorkerBackend) newRandomTx(creation bool) *types.Transaction { var tx *types.Transaction @@ -204,8 +208,7 @@ func TestGenerateAndImportBlock(t *testing.T) { if _, err := chain.InsertChain([]*types.Block{block}); err != nil { t.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err) } - case <-time.After(3 * time.Second): // Worker needs 1s to include new changes. - t.Fatalf("timeout") + case <-time.After(3 * time.Second): // worker needs 1s to include new changes. } } } @@ -228,11 +231,11 @@ func testEmptyWork(t *testing.T, chainConfig *params.ChainConfig, engine consens taskCh := make(chan struct{}, 2) checkEqual := func(t *testing.T, task *task) { // The work should contain 1 tx - receiptLen, balance := 1, uint256.NewInt(1000) + receiptLen, balance := 0, uint256.NewInt(1000) if len(task.receipts) != receiptLen { t.Fatalf("receipt number mismatch: have %d, want %d", len(task.receipts), receiptLen) } - if task.state.GetBalance(testUserAddress).Cmp(balance) != 0 { + if task.state.GetBalance(testUserAddress).Cmp(balance) == 0 { t.Fatalf("account balance mismatch: have %d, want %d", task.state.GetBalance(testUserAddress), balance) } }