feat: support MEV (#2224)

This commit is contained in:
Raina 2024-03-08 11:15:35 +08:00 committed by GitHub
parent 0dc4b1f119
commit 89c4ab2a05
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1459 additions and 6 deletions

@ -30,7 +30,7 @@ import (
) )
const ( const (
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 parlia:1.0 rpc:1.0 txpool:1.0 web3:1.0" ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 mev:1.0 miner:1.0 net:1.0 parlia:1.0 rpc:1.0 txpool:1.0 web3:1.0"
httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0" httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0"
) )

23
common/bidutil/bidutil.go Normal file

@ -0,0 +1,23 @@
package bidutil
import (
"time"
"github.com/ethereum/go-ethereum/core/types"
)
// BidBetterBefore returns the time when the next bid better be received, considering the delay and bid simulation.
// BidBetterBefore is earlier than BidMustBefore.
func BidBetterBefore(parentHeader *types.Header, blockPeriod uint64, delayLeftOver, simulationLeftOver time.Duration) time.Time {
nextHeaderTime := BidMustBefore(parentHeader, blockPeriod, delayLeftOver)
nextHeaderTime = nextHeaderTime.Add(-simulationLeftOver)
return nextHeaderTime
}
// BidMustBefore returns the time when the next bid must be received,
// only considering the consensus delay but not bid simulation duration.
func BidMustBefore(parentHeader *types.Header, blockPeriod uint64, delayLeftOver time.Duration) time.Time {
nextHeaderTime := time.Unix(int64(parentHeader.Time+blockPeriod), 0)
nextHeaderTime = nextHeaderTime.Add(-delayLeftOver)
return nextHeaderTime
}

@ -333,6 +333,11 @@ func (beacon *Beacon) verifyHeaders(chain consensus.ChainHeaderReader, headers [
return abort, results return abort, results
} }
// NextInTurnValidator return the next in-turn validator for header
func (beacon *Beacon) NextInTurnValidator(chain consensus.ChainHeaderReader, header *types.Header) (common.Address, error) {
return common.Address{}, errors.New("not implemented")
}
// Prepare implements consensus.Engine, initializing the difficulty field of a // Prepare implements consensus.Engine, initializing the difficulty field of a
// header to conform to the beacon protocol. The changes are done inline. // header to conform to the beacon protocol. The changes are done inline.
func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {

@ -511,6 +511,11 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parents []*typ
return nil return nil
} }
// NextInTurnValidator return the next in-turn validator for header
func (c *Clique) NextInTurnValidator(chain consensus.ChainHeaderReader, header *types.Header) (common.Address, error) {
return common.Address{}, errors.New("not implemented")
}
// Prepare implements consensus.Engine, preparing all the consensus fields of the // Prepare implements consensus.Engine, preparing all the consensus fields of the
// header for running the transactions on top. // header for running the transactions on top.
func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {

@ -94,6 +94,9 @@ type Engine interface {
// rules of a given engine. // rules of a given engine.
VerifyUncles(chain ChainReader, block *types.Block) error VerifyUncles(chain ChainReader, block *types.Block) error
// NextInTurnValidator return the next in-turn validator for header
NextInTurnValidator(chain ChainHeaderReader, header *types.Header) (common.Address, error)
// Prepare initializes the consensus fields of a block header according to the // Prepare initializes the consensus fields of a block header according to the
// rules of a particular engine. The changes are executed inline. // rules of a particular engine. The changes are executed inline.
Prepare(chain ChainHeaderReader, header *types.Header) error Prepare(chain ChainHeaderReader, header *types.Header) error

@ -489,6 +489,11 @@ var FrontierDifficultyCalculator = calcDifficultyFrontier
var HomesteadDifficultyCalculator = calcDifficultyHomestead var HomesteadDifficultyCalculator = calcDifficultyHomestead
var DynamicDifficultyCalculator = makeDifficultyCalculator var DynamicDifficultyCalculator = makeDifficultyCalculator
// NextInTurnValidator return the next in-turn validator for header
func (ethash *Ethash) NextInTurnValidator(chain consensus.ChainHeaderReader, header *types.Header) (common.Address, error) {
return common.Address{}, errors.New("not implemented")
}
// Prepare implements consensus.Engine, initializing the difficulty field of a // Prepare implements consensus.Engine, initializing the difficulty field of a
// header to conform to the ethash protocol. The changes are done inline. // header to conform to the ethash protocol. The changes are done inline.
func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {

@ -960,6 +960,16 @@ func (p *Parlia) assembleVoteAttestation(chain consensus.ChainHeaderReader, head
return nil return nil
} }
// NextInTurnValidator return the next in-turn validator for header
func (p *Parlia) NextInTurnValidator(chain consensus.ChainHeaderReader, header *types.Header) (common.Address, error) {
snap, err := p.snapshot(chain, header.Number.Uint64(), header.Hash(), nil)
if err != nil {
return common.Address{}, err
}
return snap.inturnValidator(), nil
}
// Prepare implements consensus.Engine, preparing all the consensus fields of the // Prepare implements consensus.Engine, preparing all the consensus fields of the
// header for running the transactions on top. // header for running the transactions on top.
func (p *Parlia) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { func (p *Parlia) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error {

@ -338,6 +338,13 @@ func (s *Snapshot) inturn(validator common.Address) bool {
return validators[offset] == validator return validators[offset] == validator
} }
// inturnValidator returns the validator at a given block height.
func (s *Snapshot) inturnValidator() common.Address {
validators := s.validators()
offset := (s.Number + 1) % uint64(len(validators))
return validators[offset]
}
func (s *Snapshot) enoughDistance(validator common.Address, header *types.Header) bool { func (s *Snapshot) enoughDistance(validator common.Address, header *types.Header) bool {
idx := s.indexOfVal(validator) idx := s.indexOfVal(validator)
if idx < 0 { if idx < 0 {

184
core/types/bid.go Normal file

@ -0,0 +1,184 @@
package types
import (
"fmt"
"math/big"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
)
const TxDecodeConcurrencyForPerBid = 5
// BidArgs represents the arguments to submit a bid.
type BidArgs struct {
// RawBid from builder directly
RawBid *RawBid
// Signature of the bid from builder
Signature hexutil.Bytes `json:"signature"`
// PayBidTx is a payment tx to builder from sentry, which is optional
PayBidTx hexutil.Bytes `json:"payBidTx"`
PayBidTxGasUsed uint64 `json:"payBidTxGasUsed"`
}
func (b *BidArgs) EcrecoverSender() (common.Address, error) {
pk, err := crypto.SigToPub(b.RawBid.Hash().Bytes(), b.Signature)
if err != nil {
return common.Address{}, err
}
return crypto.PubkeyToAddress(*pk), nil
}
func (b *BidArgs) ToBid(builder common.Address, signer Signer) (*Bid, error) {
txs, err := b.RawBid.DecodeTxs(signer)
if err != nil {
return nil, err
}
if len(b.PayBidTx) != 0 {
var payBidTx = new(Transaction)
err = payBidTx.UnmarshalBinary(b.PayBidTx)
if err != nil {
return nil, err
}
txs = append(txs, payBidTx)
}
bid := &Bid{
Builder: builder,
BlockNumber: b.RawBid.BlockNumber,
ParentHash: b.RawBid.ParentHash,
Txs: txs,
GasUsed: b.RawBid.GasUsed + b.PayBidTxGasUsed,
GasFee: b.RawBid.GasFee,
BuilderFee: b.RawBid.BuilderFee,
rawBid: *b.RawBid,
}
if bid.BuilderFee == nil {
bid.BuilderFee = big.NewInt(0)
}
return bid, nil
}
// RawBid represents a raw bid from builder directly.
type RawBid struct {
BlockNumber uint64 `json:"blockNumber"`
ParentHash common.Hash `json:"parentHash"`
Txs []hexutil.Bytes `json:"txs"`
GasUsed uint64 `json:"gasUsed"`
GasFee *big.Int `json:"gasFee"`
BuilderFee *big.Int `json:"builderFee"`
hash atomic.Value
}
func (b *RawBid) DecodeTxs(signer Signer) ([]*Transaction, error) {
if len(b.Txs) == 0 {
return []*Transaction{}, nil
}
txChan := make(chan int, len(b.Txs))
bidTxs := make([]*Transaction, len(b.Txs))
decode := func(txBytes hexutil.Bytes) (*Transaction, error) {
tx := new(Transaction)
err := tx.UnmarshalBinary(txBytes)
if err != nil {
return nil, err
}
_, err = Sender(signer, tx)
if err != nil {
return nil, err
}
return tx, nil
}
errChan := make(chan error, TxDecodeConcurrencyForPerBid)
for i := 0; i < TxDecodeConcurrencyForPerBid; i++ {
go func() {
for {
txIndex, ok := <-txChan
if !ok {
errChan <- nil
return
}
txBytes := b.Txs[txIndex]
tx, err := decode(txBytes)
if err != nil {
errChan <- err
return
}
bidTxs[txIndex] = tx
}
}()
}
for i := 0; i < len(b.Txs); i++ {
txChan <- i
}
close(txChan)
for i := 0; i < TxDecodeConcurrencyForPerBid; i++ {
err := <-errChan
if err != nil {
return nil, fmt.Errorf("failed to decode tx, %v", err)
}
}
return bidTxs, nil
}
// Hash returns the hash of the bid.
func (b *RawBid) Hash() common.Hash {
if hash := b.hash.Load(); hash != nil {
return hash.(common.Hash)
}
h := rlpHash(b)
b.hash.Store(h)
return h
}
// Bid represents a bid.
type Bid struct {
Builder common.Address
BlockNumber uint64
ParentHash common.Hash
Txs Transactions
GasUsed uint64
GasFee *big.Int
BuilderFee *big.Int
rawBid RawBid
}
// Hash returns the bid hash.
func (b *Bid) Hash() common.Hash {
return b.rawBid.Hash()
}
// BidIssue represents a bid issue.
type BidIssue struct {
Validator common.Address
Builder common.Address
BidHash common.Hash
Message string
}
type MevParams struct {
ValidatorCommission uint64 // 100 means 1%
BidSimulationLeftOver time.Duration
}

45
core/types/bid_error.go Normal file

@ -0,0 +1,45 @@
package types
import "errors"
const (
InvalidBidParamError = -38001
InvalidPayBidTxError = -38002
MevNotRunningError = -38003
MevBusyError = -38004
MevNotInTurnError = -38005
)
var (
ErrMevNotRunning = newBidError(errors.New("the validator stop accepting bids for now, try again later"), MevNotRunningError)
ErrMevBusy = newBidError(errors.New("the validator is working on too many bids, try again later"), MevBusyError)
ErrMevNotInTurn = newBidError(errors.New("the validator is not in-turn to propose currently, try again later"), MevNotInTurnError)
)
// bidError is an API error that encompasses an invalid bid with JSON error
// code and a binary data blob.
type bidError struct {
error
code int
}
// ErrorCode returns the JSON error code for an invalid bid.
// See: https://github.com/ethereum/wiki/wiki/JSON-RPC-Error-Codes-Improvement-Proposal
func (e *bidError) ErrorCode() int {
return e.code
}
func NewInvalidBidError(message string) *bidError {
return newBidError(errors.New(message), InvalidBidParamError)
}
func NewInvalidPayBidTxError(message string) *bidError {
return newBidError(errors.New(message), InvalidPayBidTxError)
}
func newBidError(err error, code int) *bidError {
return &bidError{
error: err,
code: code,
}
}

@ -24,6 +24,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
@ -141,3 +142,31 @@ func (api *AdminAPI) ImportChain(file string) (bool, error) {
} }
return true, nil return true, nil
} }
// MevRunning returns true if the validator accept bids from builder
func (api *AdminAPI) MevRunning() bool {
return api.eth.APIBackend.MevRunning()
}
// StartMev starts mev. It notifies the miner to start to receive bids.
func (api *AdminAPI) StartMev() {
api.eth.APIBackend.StartMev()
}
// StopMev stops mev. It notifies the miner to stop receiving bids from this moment,
// but the bids before this moment would still been taken into consideration by mev.
func (api *AdminAPI) StopMev() {
api.eth.APIBackend.StopMev()
}
// AddBuilder adds a builder to the bid simulator.
// url is the endpoint of the builder, for example, "https://mev-builder.amazonaws.com",
// if validator is equipped with sentry, ignore the url.
func (api *AdminAPI) AddBuilder(builder common.Address, url string) error {
return api.eth.APIBackend.AddBuilder(builder, url)
}
// RemoveBuilder removes a builder from the bid simulator.
func (api *AdminAPI) RemoveBuilder(builder common.Address) error {
return api.eth.APIBackend.RemoveBuilder(builder)
}

@ -456,3 +456,39 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re
func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) {
return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) return b.eth.stateAtTransaction(ctx, block, txIndex, reexec)
} }
func (b *EthAPIBackend) MevRunning() bool {
return b.Miner().MevRunning()
}
func (b *EthAPIBackend) MevParams() *types.MevParams {
return b.Miner().MevParams()
}
func (b *EthAPIBackend) StartMev() {
b.Miner().StartMev()
}
func (b *EthAPIBackend) StopMev() {
b.Miner().StopMev()
}
func (b *EthAPIBackend) AddBuilder(builder common.Address, url string) error {
return b.Miner().AddBuilder(builder, url)
}
func (b *EthAPIBackend) RemoveBuilder(builder common.Address) error {
return b.Miner().RemoveBuilder(builder)
}
func (b *EthAPIBackend) SendBid(ctx context.Context, bid *types.BidArgs) (common.Hash, error) {
return b.Miner().SendBid(ctx, bid)
}
func (b *EthAPIBackend) BestBidGasFee(parentHash common.Hash) *big.Int {
return b.Miner().BestPackedBlockReward(parentHash)
}
func (b *EthAPIBackend) MinerInTurn() bool {
return b.Miner().InTurn()
}

@ -52,6 +52,16 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
return NewClient(c), nil return NewClient(c), nil
} }
// 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. // NewClient creates a client that uses the given RPC client.
func NewClient(c *rpc.Client) *Client { func NewClient(c *rpc.Client) *Client {
return &Client{c} return &Client{c}
@ -715,6 +725,43 @@ func (ec *Client) SendTransactionConditional(ctx context.Context, tx *types.Tran
return ec.c.CallContext(ctx, nil, "eth_sendRawTransactionConditional", hexutil.Encode(data), opts) return ec.c.CallContext(ctx, nil, "eth_sendRawTransactionConditional", hexutil.Encode(data), opts)
} }
// 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, &params, "mev_params")
if err != nil {
return nil, err
}
return &params, err
}
func toBlockNumArg(number *big.Int) string { func toBlockNumArg(number *big.Int) string {
if number == nil { if number == nil {
return "latest" return "latest"

111
internal/ethapi/api_mev.go Normal file

@ -0,0 +1,111 @@
package ethapi
import (
"context"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
const (
TransferTxGasLimit = 25000
)
// MevAPI implements the interfaces that defined in the BEP-322.
// It offers methods for the interaction between builders and validators.
type MevAPI struct {
b Backend
}
// NewMevAPI creates a new MevAPI.
func NewMevAPI(b Backend) *MevAPI {
return &MevAPI{b}
}
// SendBid receives bid from the builders.
// If mev is not running or bid is invalid, return error.
// Otherwise, creates a builder bid for the given argument, submit it to the miner.
func (m *MevAPI) SendBid(ctx context.Context, args *types.BidArgs) (common.Hash, error) {
if !m.b.MevRunning() {
return common.Hash{}, types.ErrMevNotRunning
}
if !m.b.MinerInTurn() {
return common.Hash{}, types.ErrMevNotInTurn
}
var (
rawBid = args.RawBid
currentHeader = m.b.CurrentHeader()
)
if rawBid == nil {
return common.Hash{}, types.NewInvalidBidError("rawBid should not be nil")
}
// only support bidding for the next block not for the future block
if rawBid.BlockNumber != currentHeader.Number.Uint64()+1 {
return common.Hash{}, types.NewInvalidBidError("stale block number or block in future")
}
if rawBid.ParentHash != currentHeader.Hash() {
return common.Hash{}, types.NewInvalidBidError(
fmt.Sprintf("non-aligned parent hash: %v", currentHeader.Hash()))
}
if rawBid.GasFee == nil || rawBid.GasFee.Cmp(common.Big0) == 0 || rawBid.GasUsed == 0 {
return common.Hash{}, types.NewInvalidBidError("empty gasFee or empty gasUsed")
}
if rawBid.BuilderFee != nil {
builderFee := rawBid.BuilderFee
if builderFee.Cmp(common.Big0) < 0 {
return common.Hash{}, types.NewInvalidBidError("builder fee should not be less than 0")
}
if builderFee.Cmp(common.Big0) == 0 {
if len(args.PayBidTx) != 0 || args.PayBidTxGasUsed != 0 {
return common.Hash{}, types.NewInvalidPayBidTxError("payBidTx should be nil when builder fee is 0")
}
}
if builderFee.Cmp(rawBid.GasFee) >= 0 {
return common.Hash{}, types.NewInvalidBidError("builder fee must be less than gas fee")
}
if builderFee.Cmp(common.Big0) > 0 {
// payBidTx can be nil when validator and builder take some other settlement
if args.PayBidTxGasUsed > TransferTxGasLimit {
return common.Hash{}, types.NewInvalidBidError(
fmt.Sprintf("transfer tx gas used must be no more than %v", TransferTxGasLimit))
}
if (len(args.PayBidTx) == 0 && args.PayBidTxGasUsed != 0) ||
(len(args.PayBidTx) != 0 && args.PayBidTxGasUsed == 0) {
return common.Hash{}, types.NewInvalidPayBidTxError("non-aligned payBidTx and payBidTxGasUsed")
}
}
} else {
if len(args.PayBidTx) != 0 || args.PayBidTxGasUsed != 0 {
return common.Hash{}, types.NewInvalidPayBidTxError("payBidTx should be nil when builder fee is nil")
}
}
return m.b.SendBid(ctx, args)
}
func (m *MevAPI) BestBidGasFee(_ context.Context, parentHash common.Hash) *big.Int {
return m.b.BestBidGasFee(parentHash)
}
func (m *MevAPI) Params() *types.MevParams {
return m.b.MevParams()
}
// Running returns true if mev is running
func (m *MevAPI) Running() bool {
return m.b.MevRunning()
}

@ -30,6 +30,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
@ -50,9 +54,6 @@ import (
"github.com/ethereum/go-ethereum/internal/blocktest" "github.com/ethereum/go-ethereum/internal/blocktest"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
) )
func testTransactionMarshal(t *testing.T, tests []txData, config *params.ChainConfig) { func testTransactionMarshal(t *testing.T, tests []txData, config *params.ChainConfig) {
@ -633,6 +634,23 @@ func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.Match
panic("implement me") panic("implement me")
} }
func (b *testBackend) MevRunning() bool { return false }
func (b *testBackend) MevParams() *types.MevParams {
return &types.MevParams{}
}
func (b *testBackend) StartMev() {}
func (b *testBackend) StopMev() {}
func (b *testBackend) AddBuilder(builder common.Address, builderUrl string) error { return nil }
func (b *testBackend) RemoveBuilder(builder common.Address) error { return nil }
func (b *testBackend) SendBid(ctx context.Context, bid *types.BidArgs) (common.Hash, error) {
panic("implement me")
}
func (b *testBackend) MinerInTurn() bool { return false }
func (b *testBackend) BestBidGasFee(parentHash common.Hash) *big.Int {
//TODO implement me
panic("implement me")
}
func TestEstimateGas(t *testing.T) { func TestEstimateGas(t *testing.T) {
t.Parallel() t.Parallel()
// Initialize test accounts // Initialize test accounts

@ -101,6 +101,25 @@ type Backend interface {
ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession)
SubscribeFinalizedHeaderEvent(ch chan<- core.FinalizedHeaderEvent) event.Subscription SubscribeFinalizedHeaderEvent(ch chan<- core.FinalizedHeaderEvent) event.Subscription
SubscribeNewVoteEvent(chan<- core.NewVoteEvent) event.Subscription SubscribeNewVoteEvent(chan<- core.NewVoteEvent) event.Subscription
// MevRunning return true if mev is running
MevRunning() bool
// MevParams returns the static params of mev
MevParams() *types.MevParams
// StartMev starts mev
StartMev()
// StopMev stops mev
StopMev()
// AddBuilder adds a builder to the bid simulator.
AddBuilder(builder common.Address, builderUrl string) error
// RemoveBuilder removes a builder from the bid simulator.
RemoveBuilder(builder common.Address) error
// SendBid receives bid from the builders.
SendBid(ctx context.Context, bid *types.BidArgs) (common.Hash, error)
// BestBidGasFee returns the gas fee of the best bid for the given parent hash.
BestBidGasFee(parentHash common.Hash) *big.Int
// MinerInTurn returns true if the validator is in turn to propose the block.
MinerInTurn() bool
} }
func GetAPIs(apiBackend Backend) []rpc.API { func GetAPIs(apiBackend Backend) []rpc.API {
@ -127,6 +146,9 @@ func GetAPIs(apiBackend Backend) []rpc.API {
}, { }, {
Namespace: "personal", Namespace: "personal",
Service: NewPersonalAccountAPI(apiBackend, nonceLock), Service: NewPersonalAccountAPI(apiBackend, nonceLock),
}, {
Namespace: "mev",
Service: NewMevAPI(apiBackend),
}, },
} }
} }

@ -414,3 +414,19 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent)
} }
func (b *backendMock) Engine() consensus.Engine { return nil } func (b *backendMock) Engine() consensus.Engine { return nil }
func (b *backendMock) MevRunning() bool { return false }
func (b *backendMock) MevParams() *types.MevParams {
return &types.MevParams{}
}
func (b *backendMock) StartMev() {}
func (b *backendMock) StopMev() {}
func (b *backendMock) AddBuilder(builder common.Address, builderUrl string) error { return nil }
func (b *backendMock) RemoveBuilder(builder common.Address) error { return nil }
func (b *backendMock) SendBid(ctx context.Context, bid *types.BidArgs) (common.Hash, error) {
panic("implement me")
}
func (b *backendMock) MinerInTurn() bool { return false }
func (b *backendMock) BestBidGasFee(parentHash common.Hash) *big.Int {
panic("implement me")
}

688
miner/bid_simulator.go Normal file

@ -0,0 +1,688 @@
package miner
import (
"context"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/bidutil"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/miner/builderclient"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc"
)
const (
// maxBidPerBuilderPerBlock is the max bid number per builder
maxBidPerBuilderPerBlock = 3
commitInterruptBetterBid = 1
// leftOverTimeRate is the rate of left over time to simulate a bid
leftOverTimeRate = 11
// leftOverTimeScale is the scale of left over time to simulate a bid
leftOverTimeScale = 10
)
var (
diffInTurn = big.NewInt(2) // the difficulty of a block that proposed by an in-turn validator
)
var (
dialer = &net.Dialer{
Timeout: time.Second,
KeepAlive: 60 * time.Second,
}
transport = &http.Transport{
DialContext: dialer.DialContext,
MaxIdleConnsPerHost: 50,
MaxConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
}
client = &http.Client{
Timeout: 5 * time.Second,
Transport: transport,
}
)
type WorkPreparer interface {
prepareWork(params *generateParams) (*environment, error)
etherbase() common.Address
}
// simBidReq is the request for simulating a bid
type simBidReq struct {
bid *BidRuntime
interruptCh chan int32
}
// bidSimulator is in charge of receiving bid from builders, reporting issue to builders.
// And take care of bid simulation, rewards computing, best bid maintaining.
type bidSimulator struct {
config *MevConfig
delayLeftOver time.Duration
chain *core.BlockChain
chainConfig *params.ChainConfig
workPreparer WorkPreparer
running atomic.Bool // controlled by miner
exitCh chan struct{}
bidReceiving atomic.Bool // controlled by config and eth.AdminAPI
chainHeadCh chan core.ChainHeadEvent
chainHeadSub event.Subscription
sentryCli *builderclient.Client
// builder info (warning: only keep status in memory!)
buildersMu sync.RWMutex
builders map[common.Address]*builderclient.Client
// channels
simBidCh chan *simBidReq
newBidCh chan *types.Bid
pendingMu sync.RWMutex
pending map[uint64]map[common.Address]map[common.Hash]struct{} // blockNumber -> builder -> bidHash -> struct{}
bestBidMu sync.RWMutex
bestBid map[common.Hash]*BidRuntime // prevBlockHash -> bidRuntime
simBidMu sync.RWMutex
simulatingBid map[common.Hash]*BidRuntime // prevBlockHash -> bidRuntime, in the process of simulation
}
func newBidSimulator(
config *MevConfig,
delayLeftOver time.Duration,
chainConfig *params.ChainConfig,
chain *core.BlockChain,
workPreparer WorkPreparer,
) *bidSimulator {
b := &bidSimulator{
config: config,
delayLeftOver: delayLeftOver,
chainConfig: chainConfig,
chain: chain,
workPreparer: workPreparer,
exitCh: make(chan struct{}),
chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize),
builders: make(map[common.Address]*builderclient.Client),
simBidCh: make(chan *simBidReq),
newBidCh: make(chan *types.Bid, 100),
pending: make(map[uint64]map[common.Address]map[common.Hash]struct{}),
bestBid: make(map[common.Hash]*BidRuntime),
simulatingBid: make(map[common.Hash]*BidRuntime),
}
b.chainHeadSub = chain.SubscribeChainHeadEvent(b.chainHeadCh)
if config.Enabled {
b.bidReceiving.Store(true)
b.dialSentryAndBuilders()
if len(b.builders) == 0 {
log.Warn("BidSimulator: no valid builders")
}
}
go b.clearLoop()
go b.mainLoop()
go b.newBidLoop()
return b
}
func (b *bidSimulator) dialSentryAndBuilders() {
var sentryCli *builderclient.Client
var err error
if b.config.SentryURL != "" {
sentryCli, err = builderclient.DialOptions(context.Background(), b.config.SentryURL, rpc.WithHTTPClient(client))
if err != nil {
log.Error("BidSimulator: failed to dial sentry", "url", b.config.SentryURL, "err", err)
}
}
b.sentryCli = sentryCli
for _, v := range b.config.Builders {
_ = b.AddBuilder(v.Address, v.URL)
}
}
func (b *bidSimulator) start() {
b.running.Store(true)
}
func (b *bidSimulator) stop() {
b.running.Store(false)
}
func (b *bidSimulator) close() {
b.running.Store(false)
close(b.exitCh)
}
func (b *bidSimulator) isRunning() bool {
return b.running.Load()
}
func (b *bidSimulator) receivingBid() bool {
return b.bidReceiving.Load()
}
func (b *bidSimulator) startReceivingBid() {
b.dialSentryAndBuilders()
b.bidReceiving.Store(true)
}
func (b *bidSimulator) stopReceivingBid() {
b.bidReceiving.Store(false)
}
func (b *bidSimulator) AddBuilder(builder common.Address, url string) error {
b.buildersMu.Lock()
defer b.buildersMu.Unlock()
if b.sentryCli != nil {
b.builders[builder] = b.sentryCli
} else {
var builderCli *builderclient.Client
if url != "" {
var err error
builderCli, err = builderclient.DialOptions(context.Background(), url, rpc.WithHTTPClient(client))
if err != nil {
log.Error("BidSimulator: failed to dial builder", "url", url, "err", err)
return err
}
}
b.builders[builder] = builderCli
}
return nil
}
func (b *bidSimulator) RemoveBuilder(builder common.Address) error {
b.buildersMu.Lock()
defer b.buildersMu.Unlock()
delete(b.builders, builder)
return nil
}
func (b *bidSimulator) ExistBuilder(builder common.Address) bool {
b.buildersMu.RLock()
defer b.buildersMu.RUnlock()
_, ok := b.builders[builder]
return ok
}
func (b *bidSimulator) SetBestBid(prevBlockHash common.Hash, bid *BidRuntime) {
b.bestBidMu.Lock()
defer b.bestBidMu.Unlock()
b.bestBid[prevBlockHash] = bid
}
func (b *bidSimulator) GetBestBid(prevBlockHash common.Hash) *BidRuntime {
b.bestBidMu.RLock()
defer b.bestBidMu.RUnlock()
return b.bestBid[prevBlockHash]
}
func (b *bidSimulator) SetSimulatingBid(prevBlockHash common.Hash, bid *BidRuntime) {
b.simBidMu.Lock()
defer b.simBidMu.Unlock()
b.simulatingBid[prevBlockHash] = bid
}
func (b *bidSimulator) GetSimulatingBid(prevBlockHash common.Hash) *BidRuntime {
b.simBidMu.RLock()
defer b.simBidMu.RUnlock()
return b.simulatingBid[prevBlockHash]
}
func (b *bidSimulator) RemoveSimulatingBid(prevBlockHash common.Hash) {
b.simBidMu.Lock()
defer b.simBidMu.Unlock()
delete(b.simulatingBid, prevBlockHash)
}
func (b *bidSimulator) mainLoop() {
defer b.chainHeadSub.Unsubscribe()
for {
select {
case req := <-b.simBidCh:
if !b.isRunning() {
continue
}
b.simBid(req.interruptCh, req.bid)
// System stopped
case <-b.exitCh:
return
case <-b.chainHeadSub.Err():
return
}
}
}
func (b *bidSimulator) newBidLoop() {
var (
interruptCh chan int32
)
// commit aborts in-flight bid execution with given signal and resubmits a new one.
commit := func(reason int32, bidRuntime *BidRuntime) {
// if the left time is not enough to do simulation, return
var simDuration time.Duration
if lastBid := b.GetBestBid(bidRuntime.bid.ParentHash); lastBid != nil && lastBid.duration != 0 {
simDuration = lastBid.duration
}
if time.Until(b.bidMustBefore(bidRuntime.bid.ParentHash)) <= simDuration*leftOverTimeRate/leftOverTimeScale {
return
}
if interruptCh != nil {
// each commit work will have its own interruptCh to stop work with a reason
interruptCh <- reason
close(interruptCh)
}
interruptCh = make(chan int32, 1)
select {
case b.simBidCh <- &simBidReq{interruptCh: interruptCh, bid: bidRuntime}:
case <-b.exitCh:
return
}
}
for {
select {
case newBid := <-b.newBidCh:
if !b.isRunning() {
continue
}
// check the block reward and validator reward of the newBid
expectedBlockReward := newBid.GasFee
expectedValidatorReward := new(big.Int).Mul(expectedBlockReward, big.NewInt(int64(b.config.ValidatorCommission)))
expectedValidatorReward.Div(expectedValidatorReward, big.NewInt(10000))
expectedValidatorReward.Sub(expectedValidatorReward, newBid.BuilderFee)
if expectedValidatorReward.Cmp(big.NewInt(0)) < 0 {
// damage self profit, ignore
continue
}
bidRuntime := &BidRuntime{
bid: newBid,
expectedBlockReward: expectedBlockReward,
expectedValidatorReward: expectedValidatorReward,
packedBlockReward: big.NewInt(0),
packedValidatorReward: big.NewInt(0),
}
// TODO(renee-) opt bid comparation
simulatingBid := b.GetSimulatingBid(newBid.ParentHash)
// simulatingBid is nil means there is no bid in simulation
if simulatingBid == nil {
// bestBid is nil means bid is the first bid
bestBid := b.GetBestBid(newBid.ParentHash)
if bestBid == nil {
commit(commitInterruptBetterBid, bidRuntime)
continue
}
// if bestBid is not nil, check if newBid is better than bestBid
if bidRuntime.expectedBlockReward.Cmp(bestBid.expectedBlockReward) > 0 &&
bidRuntime.expectedValidatorReward.Cmp(bestBid.expectedValidatorReward) > 0 {
// if both reward are better than last simulating newBid, commit for simulation
commit(commitInterruptBetterBid, bidRuntime)
continue
}
continue
}
// simulatingBid must be better than bestBid, if newBid is better than simulatingBid, commit for simulation
if bidRuntime.expectedBlockReward.Cmp(simulatingBid.expectedBlockReward) > 0 &&
bidRuntime.expectedValidatorReward.Cmp(simulatingBid.expectedValidatorReward) > 0 {
// if both reward are better than last simulating newBid, commit for simulation
commit(commitInterruptBetterBid, bidRuntime)
continue
}
case <-b.exitCh:
return
}
}
}
func (b *bidSimulator) bidMustBefore(parentHash common.Hash) time.Time {
parentHeader := b.chain.GetHeaderByHash(parentHash)
return bidutil.BidMustBefore(parentHeader, b.chainConfig.Parlia.Period, b.delayLeftOver)
}
func (b *bidSimulator) bidBetterBefore(parentHash common.Hash) time.Time {
parentHeader := b.chain.GetHeaderByHash(parentHash)
return bidutil.BidBetterBefore(parentHeader, b.chainConfig.Parlia.Period, b.delayLeftOver, b.config.BidSimulationLeftOver)
}
func (b *bidSimulator) clearLoop() {
clearFn := func(parentHash common.Hash, blockNumber uint64) {
b.pendingMu.Lock()
delete(b.pending, blockNumber)
b.pendingMu.Unlock()
b.bestBidMu.Lock()
if bid, ok := b.bestBid[parentHash]; ok {
bid.env.discard()
}
delete(b.bestBid, parentHash)
for k, v := range b.bestBid {
if v.bid.BlockNumber <= blockNumber-core.TriesInMemory {
v.env.discard()
delete(b.bestBid, k)
}
}
b.bestBidMu.Unlock()
b.simBidMu.Lock()
if bid, ok := b.simulatingBid[parentHash]; ok {
bid.env.discard()
}
delete(b.simulatingBid, parentHash)
for k, v := range b.simulatingBid {
if v.bid.BlockNumber <= blockNumber-core.TriesInMemory {
v.env.discard()
delete(b.simulatingBid, k)
}
}
b.simBidMu.Unlock()
}
for head := range b.chainHeadCh {
if !b.isRunning() {
continue
}
clearFn(head.Block.ParentHash(), head.Block.NumberU64())
}
}
// sendBid checks if the bid is already exists or if the builder sends too many bids,
// if yes, return error, if not, add bid into newBid chan waiting for judge profit.
func (b *bidSimulator) sendBid(_ context.Context, bid *types.Bid) error {
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
select {
case b.newBidCh <- bid:
b.AddPending(bid.BlockNumber, bid.Builder, bid.Hash())
return nil
case <-timer.C:
return types.ErrMevBusy
}
}
func (b *bidSimulator) CheckPending(blockNumber uint64, builder common.Address, bidHash common.Hash) error {
b.pendingMu.Lock()
defer b.pendingMu.Unlock()
// check if bid exists or if builder sends too many bids
if _, ok := b.pending[blockNumber]; !ok {
b.pending[blockNumber] = make(map[common.Address]map[common.Hash]struct{})
}
if _, ok := b.pending[blockNumber][builder]; !ok {
b.pending[blockNumber][builder] = make(map[common.Hash]struct{})
}
if _, ok := b.pending[blockNumber][builder][bidHash]; ok {
return errors.New("bid already exists")
}
if len(b.pending[blockNumber][builder]) >= maxBidPerBuilderPerBlock {
return errors.New("too many bids")
}
return nil
}
func (b *bidSimulator) AddPending(blockNumber uint64, builder common.Address, bidHash common.Hash) {
b.pendingMu.Lock()
defer b.pendingMu.Unlock()
b.pending[blockNumber][builder][bidHash] = struct{}{}
}
// simBid simulates a newBid with txs.
// simBid does not enable state prefetching when commit transaction.
func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) {
// prevent from stopping happen in time interval from sendBid to simBid
if !b.isRunning() || !b.receivingBid() {
return
}
var (
blockNumber = bidRuntime.bid.BlockNumber
parentHash = bidRuntime.bid.ParentHash
builder = bidRuntime.bid.Builder
err error
success bool
)
// ensure simulation exited then start next simulation
b.SetSimulatingBid(parentHash, bidRuntime)
defer func(simStart time.Time) {
logCtx := []any{
"blockNumber", blockNumber,
"parentHash", parentHash,
"builder", builder,
"gasUsed", bidRuntime.bid.GasUsed,
}
if bidRuntime.env != nil {
logCtx = append(logCtx, "gasLimit", bidRuntime.env.header.GasLimit)
if err != nil || !success {
bidRuntime.env.discard()
}
}
if err != nil {
logCtx = append(logCtx, "err", err)
log.Debug("bid simulation failed", logCtx...)
go b.reportIssue(bidRuntime, err)
}
if success {
bidRuntime.duration = time.Since(simStart)
}
b.RemoveSimulatingBid(parentHash)
}(time.Now())
// prepareWork will configure header with a suitable time according to consensus
// prepareWork will start trie prefetching
if bidRuntime.env, err = b.workPreparer.prepareWork(&generateParams{
parentHash: bidRuntime.bid.ParentHash,
coinbase: b.workPreparer.etherbase(),
}); err != nil {
return
}
gasLimit := bidRuntime.env.header.GasLimit
if bidRuntime.env.gasPool == nil {
bidRuntime.env.gasPool = new(core.GasPool).AddGas(gasLimit)
bidRuntime.env.gasPool.SubGas(params.SystemTxsGas)
}
if bidRuntime.bid.GasUsed > bidRuntime.env.gasPool.Gas() {
err = errors.New("gas used exceeds gas limit")
return
}
for _, tx := range bidRuntime.bid.Txs {
select {
case <-interruptCh:
err = errors.New("simulation abort due to better bid arrived")
return
case <-b.exitCh:
err = errors.New("miner exit")
return
default:
}
// Start executing the transaction
bidRuntime.env.state.SetTxContext(tx.Hash(), bidRuntime.env.tcount)
err = bidRuntime.commitTransaction(b.chain, b.chainConfig, tx)
if err != nil {
log.Error("BidSimulator: failed to commit tx", "bidHash", bidRuntime.bid.Hash(), "tx", tx.Hash(), "err", err)
err = fmt.Errorf("invalid tx in bid, %v", err)
return
}
bidRuntime.env.tcount++
}
bidRuntime.packReward(b.config.ValidatorCommission)
// return if bid is invalid, reportIssue issue to mev-sentry/builder if simulation is fully done
if !bidRuntime.validReward() {
err = errors.New("reward does not achieve the expectation")
return
}
bestBid := b.GetBestBid(parentHash)
if bestBid == nil {
b.SetBestBid(bidRuntime.bid.ParentHash, bidRuntime)
success = true
return
}
// this is the simplest strategy: best for all the delegators.
if bidRuntime.packedBlockReward.Cmp(bestBid.packedBlockReward) > 0 {
b.SetBestBid(bidRuntime.bid.ParentHash, bidRuntime)
success = true
return
}
}
// reportIssue reports the issue to the mev-sentry
func (b *bidSimulator) reportIssue(bidRuntime *BidRuntime, err error) {
cli := b.builders[bidRuntime.bid.Builder]
if cli != nil {
cli.ReportIssue(context.Background(), &types.BidIssue{
Validator: bidRuntime.env.header.Coinbase,
Builder: bidRuntime.bid.Builder,
Message: err.Error(),
})
}
}
type BidRuntime struct {
bid *types.Bid
env *environment
expectedBlockReward *big.Int
expectedValidatorReward *big.Int
packedBlockReward *big.Int
packedValidatorReward *big.Int
duration time.Duration
}
func (r *BidRuntime) validReward() bool {
return r.packedBlockReward.Cmp(r.expectedBlockReward) >= 0 &&
r.packedValidatorReward.Cmp(r.expectedValidatorReward) >= 0
}
// packReward calculates packedBlockReward and packedValidatorReward
func (r *BidRuntime) packReward(validatorCommission uint64) {
r.packedBlockReward = r.env.state.GetBalance(consensus.SystemAddress).ToBig()
r.packedValidatorReward = new(big.Int).Mul(r.packedBlockReward, big.NewInt(int64(validatorCommission)))
r.packedValidatorReward.Div(r.packedValidatorReward, big.NewInt(10000))
r.packedValidatorReward.Sub(r.packedValidatorReward, r.bid.BuilderFee)
}
func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *params.ChainConfig, tx *types.Transaction) error {
var (
env = r.env
snap = env.state.Snapshot()
gp = env.gasPool.Gas()
sc *types.BlobTxSidecar
)
if tx.Type() == types.BlobTxType {
sc := tx.BlobTxSidecar()
if sc == nil {
return errors.New("blob transaction without blobs in miner")
}
// Checking against blob gas limit: It's kind of ugly to perform this check here, but there
// isn't really a better place right now. The blob gas limit is checked at block validation time
// and not during execution. This means core.ApplyTransaction will not return an error if the
// tx has too many blobs. So we have to explicitly check it here.
if (env.blobs+len(sc.Blobs))*params.BlobTxBlobGasPerBlob > params.MaxBlobGasPerBlock {
return errors.New("max data blobs reached")
}
}
receipt, err := core.ApplyTransaction(chainConfig, chain, &env.coinbase, env.gasPool, env.state, env.header, tx,
&env.header.GasUsed, *chain.GetVMConfig(), core.NewReceiptBloomGenerator())
if err != nil {
env.state.RevertToSnapshot(snap)
env.gasPool.SetGas(gp)
return err
}
if tx.Type() == types.BlobTxType {
env.txs = append(env.txs, tx.WithoutBlobTxSidecar())
env.receipts = append(env.receipts, receipt)
env.sidecars = append(env.sidecars, sc)
env.blobs += len(sc.Blobs)
*env.header.BlobGasUsed += receipt.BlobGasUsed
} else {
env.txs = append(env.txs, tx)
env.receipts = append(env.receipts, receipt)
}
return nil
}

@ -0,0 +1,33 @@
package builderclient
import (
"context"
"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}
}
// ReportIssue reports an issue
func (ec *Client) ReportIssue(ctx context.Context, args *types.BidIssue) error {
return ec.c.CallContext(ctx, nil, "mev_reportIssue", args)
}

@ -56,6 +56,8 @@ type Config struct {
NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload
DisableVoteAttestation bool // Whether to skip assembling vote attestation DisableVoteAttestation bool // Whether to skip assembling vote attestation
Mev MevConfig // Mev configuration
} }
// DefaultConfig contains default settings for miner. // DefaultConfig contains default settings for miner.
@ -70,6 +72,8 @@ var DefaultConfig = Config{
Recommit: 3 * time.Second, Recommit: 3 * time.Second,
NewPayloadTimeout: 2 * time.Second, NewPayloadTimeout: 2 * time.Second,
DelayLeftOver: 50 * time.Millisecond, DelayLeftOver: 50 * time.Millisecond,
Mev: DefaultMevConfig,
} }
// Miner creates blocks and searches for proof-of-work values. // Miner creates blocks and searches for proof-of-work values.
@ -82,6 +86,8 @@ type Miner struct {
stopCh chan struct{} stopCh chan struct{}
worker *worker worker *worker
bidSimulator *bidSimulator
wg sync.WaitGroup wg sync.WaitGroup
} }
@ -95,6 +101,10 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
worker: newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, false), worker: newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, false),
} }
miner.bidSimulator = newBidSimulator(&config.Mev, config.DelayLeftOver, chainConfig, eth.BlockChain(), miner.worker)
miner.worker.setBestBidFetcher(miner.bidSimulator)
miner.wg.Add(1) miner.wg.Add(1)
go miner.update() go miner.update()
return miner return miner
@ -129,6 +139,7 @@ func (miner *Miner) update() {
case downloader.StartEvent: case downloader.StartEvent:
wasMining := miner.Mining() wasMining := miner.Mining()
miner.worker.stop() miner.worker.stop()
miner.bidSimulator.stop()
canStart = false canStart = false
if wasMining { if wasMining {
// Resume mining after sync was finished // Resume mining after sync was finished
@ -141,6 +152,7 @@ func (miner *Miner) update() {
canStart = true canStart = true
if shouldStart { if shouldStart {
miner.worker.start() miner.worker.start()
miner.bidSimulator.start()
} }
miner.worker.syncing.Store(false) miner.worker.syncing.Store(false)
@ -148,6 +160,7 @@ func (miner *Miner) update() {
canStart = true canStart = true
if shouldStart { if shouldStart {
miner.worker.start() miner.worker.start()
miner.bidSimulator.start()
} }
miner.worker.syncing.Store(false) miner.worker.syncing.Store(false)
@ -157,13 +170,16 @@ func (miner *Miner) update() {
case <-miner.startCh: case <-miner.startCh:
if canStart { if canStart {
miner.worker.start() miner.worker.start()
miner.bidSimulator.start()
} }
shouldStart = true shouldStart = true
case <-miner.stopCh: case <-miner.stopCh:
shouldStart = false shouldStart = false
miner.worker.stop() miner.worker.stop()
miner.bidSimulator.stop()
case <-miner.exitCh: case <-miner.exitCh:
miner.worker.close() miner.worker.close()
miner.bidSimulator.close()
return return
} }
} }
@ -186,6 +202,10 @@ func (miner *Miner) Mining() bool {
return miner.worker.isRunning() return miner.worker.isRunning()
} }
func (miner *Miner) InTurn() bool {
return miner.worker.inTurn()
}
func (miner *Miner) Hashrate() uint64 { func (miner *Miner) Hashrate() uint64 {
if pow, ok := miner.engine.(consensus.PoW); ok { if pow, ok := miner.engine.(consensus.PoW); ok {
return uint64(pow.Hashrate()) return uint64(pow.Hashrate())

111
miner/miner_mev.go Normal file

@ -0,0 +1,111 @@
package miner
import (
"context"
"fmt"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
type BuilderConfig struct {
Address common.Address
URL string
}
type MevConfig struct {
Enabled bool // Whether to enable Mev or not
SentryURL string // The url of Mev sentry
Builders []BuilderConfig // The list of builders
ValidatorCommission uint64 // 100 means 1%
BidSimulationLeftOver time.Duration
}
var DefaultMevConfig = MevConfig{
Enabled: false,
SentryURL: "",
Builders: nil,
ValidatorCommission: 100,
BidSimulationLeftOver: 50 * time.Millisecond,
}
// MevRunning return true if mev is running.
func (miner *Miner) MevRunning() bool {
return miner.bidSimulator.isRunning() && miner.bidSimulator.receivingBid()
}
// StartMev starts mev.
func (miner *Miner) StartMev() {
miner.bidSimulator.startReceivingBid()
}
// StopMev stops mev.
func (miner *Miner) StopMev() {
miner.bidSimulator.stopReceivingBid()
}
// AddBuilder adds a builder to the bid simulator.
func (miner *Miner) AddBuilder(builder common.Address, url string) error {
return miner.bidSimulator.AddBuilder(builder, url)
}
// RemoveBuilder removes a builder from the bid simulator.
func (miner *Miner) RemoveBuilder(builderAddr common.Address) error {
return miner.bidSimulator.RemoveBuilder(builderAddr)
}
func (miner *Miner) SendBid(ctx context.Context, bidArgs *types.BidArgs) (common.Hash, error) {
builder, err := bidArgs.EcrecoverSender()
if err != nil {
return common.Hash{}, types.NewInvalidBidError(fmt.Sprintf("invalid signature:%v", err))
}
if !miner.bidSimulator.ExistBuilder(builder) {
return common.Hash{}, types.NewInvalidBidError("builder is not registered")
}
err = miner.bidSimulator.CheckPending(bidArgs.RawBid.BlockNumber, builder, bidArgs.RawBid.Hash())
if err != nil {
return common.Hash{}, err
}
signer := types.MakeSigner(miner.worker.chainConfig, big.NewInt(int64(bidArgs.RawBid.BlockNumber)), uint64(time.Now().Unix()))
bid, err := bidArgs.ToBid(builder, signer)
if err != nil {
return common.Hash{}, types.NewInvalidBidError(fmt.Sprintf("fail to convert bidArgs to bid, %v", err))
}
bidBetterBefore := miner.bidSimulator.bidBetterBefore(bidArgs.RawBid.ParentHash)
timeout := time.Until(bidBetterBefore)
if timeout <= 0 {
return common.Hash{}, fmt.Errorf("too late, expected befor %s, appeared %s later", bidBetterBefore,
common.PrettyDuration(timeout))
}
err = miner.bidSimulator.sendBid(ctx, bid)
if err != nil {
return common.Hash{}, err
}
return bid.Hash(), nil
}
func (miner *Miner) BestPackedBlockReward(parentHash common.Hash) *big.Int {
bidRuntime := miner.bidSimulator.GetBestBid(parentHash)
if bidRuntime == nil {
return big.NewInt(0)
}
return bidRuntime.packedBlockReward
}
func (miner *Miner) MevParams() *types.MevParams {
return &types.MevParams{
ValidatorCommission: miner.worker.config.Mev.ValidatorCommission,
BidSimulationLeftOver: miner.worker.config.Mev.BidSimulationLeftOver,
}
}

@ -24,6 +24,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
lru "github.com/hashicorp/golang-lru"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip1559"
@ -40,8 +43,6 @@ import (
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
lru "github.com/hashicorp/golang-lru"
"github.com/holiman/uint256"
) )
const ( const (
@ -162,9 +163,14 @@ type getWorkReq struct {
result chan *newPayloadResult // non-blocking channel result chan *newPayloadResult // non-blocking channel
} }
type bidFetcher interface {
GetBestBid(parentHash common.Hash) *BidRuntime
}
// worker is the main object which takes care of submitting new work to consensus engine // worker is the main object which takes care of submitting new work to consensus engine
// and gathering the sealing result. // and gathering the sealing result.
type worker struct { type worker struct {
bidFetcher bidFetcher
prefetcher core.Prefetcher prefetcher core.Prefetcher
config *Config config *Config
chainConfig *params.ChainConfig chainConfig *params.ChainConfig
@ -287,9 +293,14 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus
if init { if init {
worker.startCh <- struct{}{} worker.startCh <- struct{}{}
} }
return worker return worker
} }
func (w *worker) setBestBidFetcher(fetcher bidFetcher) {
w.bidFetcher = fetcher
}
// setEtherbase sets the etherbase used to initialize the block coinbase field. // setEtherbase sets the etherbase used to initialize the block coinbase field.
func (w *worker) setEtherbase(addr common.Address) { func (w *worker) setEtherbase(addr common.Address) {
w.mu.Lock() w.mu.Lock()
@ -1218,6 +1229,24 @@ LOOP:
bestReward = balance bestReward = balance
} }
} }
// when out-turn, use bestWork to prevent bundle leakage.
// when in-turn, compare with remote work.
if w.bidFetcher != nil && bestWork.header.Difficulty.Cmp(diffInTurn) == 0 {
bestBid := w.bidFetcher.GetBestBid(bestWork.header.ParentHash)
if bestBid != nil && bestReward.CmpBig(bestBid.packedBlockReward) < 0 {
// localValidatorReward is the reward for the validator self by the local block.
localValidatorReward := new(uint256.Int).Mul(bestReward, uint256.NewInt(w.config.Mev.ValidatorCommission))
localValidatorReward.Div(localValidatorReward, uint256.NewInt(10000))
// blockReward(benefits delegators) and validatorReward(benefits the validator) are both optimal
if localValidatorReward.CmpBig(bestBid.packedValidatorReward) < 0 {
bestWork = bestBid.env
}
}
}
w.commit(bestWork, w.fullTaskHook, true, start) w.commit(bestWork, w.fullTaskHook, true, start)
// Swap out the old work with the new one, terminating any leftover // Swap out the old work with the new one, terminating any leftover
@ -1228,6 +1257,12 @@ LOOP:
w.current = bestWork w.current = bestWork
} }
// inTurn return true if the current worker is in turn.
func (w *worker) inTurn() bool {
validator, _ := w.engine.NextInTurnValidator(w.chain, w.chain.CurrentBlock())
return validator != common.Address{} && validator == w.etherbase()
}
// commit runs any post-transaction state modifications, assembles the final block // commit runs any post-transaction state modifications, assembles the final block
// and commits new work if consensus engine is running. // and commits new work if consensus engine is running.
// Note the assumption is held that the mutation is allowed to the passed env, do // Note the assumption is held that the mutation is allowed to the passed env, do