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 0000000000..d6f35afd68 Binary files /dev/null and b/docs/assets/pbs_workflow.png differ diff --git a/eth/api_backend.go b/eth/api_backend.go index 8408240393..afec1190a8 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -20,6 +20,7 @@ import ( "context" "errors" "math/big" + "sort" "time" "github.com/ethereum/go-ethereum" @@ -299,6 +300,31 @@ func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) return b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0] } +func (b *EthAPIBackend) SendBundle(ctx context.Context, bundle *types.Bundle) error { + return b.eth.txPool.AddBundle(bundle) +} + +func (b *EthAPIBackend) BundlePrice() *big.Int { + bundles := b.eth.txPool.AllBundles() + gasFloor := big.NewInt(b.eth.config.Miner.MevGasPriceFloor) + + if len(bundles) == 0 { + return gasFloor + } + + sort.SliceStable(bundles, func(i, j int) bool { + return bundles[j].Price.Cmp(bundles[i].Price) < 0 + }) + + idx := len(bundles) / 2 + + if bundles[idx] == nil || bundles[idx].Price.Cmp(gasFloor) < 0 { + return gasFloor + } + + return bundles[idx].Price +} + func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { pending := b.eth.txPool.Pending(txpool.PendingFilter{}) var txs types.Transactions diff --git a/eth/backend.go b/eth/backend.go index 89ce49ad1d..9361a02fe8 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/core/state/pruner" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool/blobpool" + "github.com/ethereum/go-ethereum/core/txpool/bundlepool" "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -306,8 +307,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal) } legacyPool := legacypool.New(config.TxPool, eth.blockchain) + bundlePool := bundlepool.New(config.BundlePool) - eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool}) + eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool, bundlePool}) if err != nil { return nil, err } @@ -332,6 +334,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.miner = miner.New(eth, &config.Miner, eth.blockchain.Config(), eth.EventMux(), eth.engine, eth.isLocalBlock) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData)) + bundlePool.SetBundleSimulator(eth.miner) // Create voteManager instance if posa, ok := eth.engine.(consensus.PoSA); ok { diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 0b69b62219..3cd7693fbe 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/parlia" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/txpool/blobpool" + "github.com/ethereum/go-ethereum/core/txpool/bundlepool" "github.com/ethereum/go-ethereum/core/txpool/legacypool" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/gasprice" @@ -166,8 +167,9 @@ type Config struct { Miner miner.Config // Transaction pool options - TxPool legacypool.Config - BlobPool blobpool.Config + TxPool legacypool.Config + BlobPool blobpool.Config + BundlePool bundlepool.Config // Gas Price Oracle options GPO gasprice.Config diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 521add7e98..cbe21aa640 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -772,6 +772,23 @@ func (ec *Client) BestBidGasFee(ctx context.Context, parentHash common.Hash) (*b return fee, nil } +// SendBundle sends a bundle +func (ec *Client) SendBundle(ctx context.Context, args types.SendBundleArgs) (common.Hash, error) { + var hash common.Hash + err := ec.c.CallContext(ctx, &hash, "eth_sendBundle", args) + if err != nil { + return common.Hash{}, err + } + return hash, nil +} + +// BundlePrice returns the price of a bundle +func (ec *Client) BundlePrice(ctx context.Context) *big.Int { + var price *big.Int + _ = ec.c.CallContext(ctx, &price, "eth_bundlePrice") + return price +} + // MevParams returns the static params of mev func (ec *Client) MevParams(ctx context.Context) (*types.MevParams, error) { var params types.MevParams diff --git a/internal/ethapi/api_bundle.go b/internal/ethapi/api_bundle.go new file mode 100644 index 0000000000..cf9e5034e9 --- /dev/null +++ b/internal/ethapi/api_bundle.go @@ -0,0 +1,118 @@ +package ethapi + +import ( + "context" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +const InvalidBundleParamError = -38000 + +// PrivateTxBundleAPI offers an API for accepting bundled transactions +type PrivateTxBundleAPI struct { + b Backend +} + +// NewPrivateTxBundleAPI creates a new Tx Bundle API instance. +func NewPrivateTxBundleAPI(b Backend) *PrivateTxBundleAPI { + return &PrivateTxBundleAPI{b} +} + +func (s *PrivateTxBundleAPI) BundlePrice(ctx context.Context) *big.Int { + return s.b.BundlePrice() +} + +// SendBundle will add the signed transaction to the transaction pool. +// The sender is responsible for signing the transaction and using the correct nonce and ensuring validity +func (s *PrivateTxBundleAPI) SendBundle(ctx context.Context, args types.SendBundleArgs) (common.Hash, error) { + if len(args.Txs) == 0 { + return common.Hash{}, newBundleError(errors.New("bundle missing txs")) + } + + currentHeader := s.b.CurrentHeader() + + if args.MaxBlockNumber == 0 && (args.MaxTimestamp == nil || *args.MaxTimestamp == 0) { + maxTimeStamp := currentHeader.Time + types.MaxBundleAliveTime + args.MaxTimestamp = &maxTimeStamp + } + + if args.MaxBlockNumber != 0 && args.MaxBlockNumber > 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) } }