diff --git a/cmd/geth/consolecmd_test.go b/cmd/geth/consolecmd_test.go index 9304ebcb3..59c0e0015 100644 --- a/cmd/geth/consolecmd_test.go +++ b/cmd/geth/consolecmd_test.go @@ -30,7 +30,7 @@ import ( ) const ( - ipcAPIs = "admin:1.0 debug:1.0 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" ) diff --git a/common/bidutil/bidutil.go b/common/bidutil/bidutil.go new file mode 100644 index 000000000..d2735808c --- /dev/null +++ b/common/bidutil/bidutil.go @@ -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 +} diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index 722f4b018..9e1c39b99 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -333,6 +333,11 @@ func (beacon *Beacon) verifyHeaders(chain consensus.ChainHeaderReader, headers [ 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 // header to conform to the beacon protocol. The changes are done inline. func (beacon *Beacon) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index fb1fa1b12..bad64b24a 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -511,6 +511,11 @@ func (c *Clique) verifySeal(snap *Snapshot, header *types.Header, parents []*typ 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 // header for running the transactions on top. func (c *Clique) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { diff --git a/consensus/consensus.go b/consensus/consensus.go index cb5f1841a..7b7648878 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -94,6 +94,9 @@ type Engine interface { // rules of a given engine. 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 // rules of a particular engine. The changes are executed inline. Prepare(chain ChainHeaderReader, header *types.Header) error diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 8b39b27dc..db730fab2 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -489,6 +489,11 @@ var FrontierDifficultyCalculator = calcDifficultyFrontier var HomesteadDifficultyCalculator = calcDifficultyHomestead 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 // header to conform to the ethash protocol. The changes are done inline. func (ethash *Ethash) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 78f758781..7a8a691c3 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -960,6 +960,16 @@ func (p *Parlia) assembleVoteAttestation(chain consensus.ChainHeaderReader, head 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 // header for running the transactions on top. func (p *Parlia) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { diff --git a/consensus/parlia/snapshot.go b/consensus/parlia/snapshot.go index ddfb1811f..0da0929e7 100644 --- a/consensus/parlia/snapshot.go +++ b/consensus/parlia/snapshot.go @@ -338,6 +338,13 @@ func (s *Snapshot) inturn(validator common.Address) bool { 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 { idx := s.indexOfVal(validator) if idx < 0 { diff --git a/core/types/bid.go b/core/types/bid.go new file mode 100644 index 000000000..6d7796acf --- /dev/null +++ b/core/types/bid.go @@ -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 +} diff --git a/core/types/bid_error.go b/core/types/bid_error.go new file mode 100644 index 000000000..6b543ae64 --- /dev/null +++ b/core/types/bid_error.go @@ -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, + } +} diff --git a/eth/api_admin.go b/eth/api_admin.go index 4a3ccb84e..76a0d087b 100644 --- a/eth/api_admin.go +++ b/eth/api_admin.go @@ -24,6 +24,7 @@ import ( "os" "strings" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rlp" @@ -141,3 +142,31 @@ func (api *AdminAPI) ImportChain(file string) (bool, error) { } 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) +} diff --git a/eth/api_backend.go b/eth/api_backend.go index bf66f078a..5df2b32f4 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -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) { 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() +} diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 2de369431..c5c6a391c 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -52,6 +52,16 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) { 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. func NewClient(c *rpc.Client) *Client { 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) } +// 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 +} + func toBlockNumArg(number *big.Int) string { if number == nil { return "latest" diff --git a/internal/ethapi/api_mev.go b/internal/ethapi/api_mev.go new file mode 100644 index 000000000..0fa92af1a --- /dev/null +++ b/internal/ethapi/api_mev.go @@ -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() +} diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 409a00e7d..654442f7c 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -30,6 +30,10 @@ import ( "testing" "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/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -50,9 +54,6 @@ import ( "github.com/ethereum/go-ethereum/internal/blocktest" "github.com/ethereum/go-ethereum/params" "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) { @@ -633,6 +634,23 @@ func (b testBackend) ServiceFilter(ctx context.Context, session *bloombits.Match 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) { t.Parallel() // Initialize test accounts diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index a304e5d9e..0f37e4f0f 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -101,6 +101,25 @@ type Backend interface { ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) SubscribeFinalizedHeaderEvent(ch chan<- core.FinalizedHeaderEvent) 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 { @@ -127,6 +146,9 @@ func GetAPIs(apiBackend Backend) []rpc.API { }, { Namespace: "personal", Service: NewPersonalAccountAPI(apiBackend, nonceLock), + }, { + Namespace: "mev", + Service: NewMevAPI(apiBackend), }, } } diff --git a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index 6c3402ec5..7d1461d82 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -414,3 +414,19 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) } 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") +} diff --git a/miner/bid_simulator.go b/miner/bid_simulator.go new file mode 100644 index 000000000..51dcf5835 --- /dev/null +++ b/miner/bid_simulator.go @@ -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 +} diff --git a/miner/builderclient/builderclient.go b/miner/builderclient/builderclient.go new file mode 100644 index 000000000..9606a92d9 --- /dev/null +++ b/miner/builderclient/builderclient.go @@ -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) +} diff --git a/miner/miner.go b/miner/miner.go index 4db614080..a05da63e5 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -56,6 +56,8 @@ type Config struct { NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload DisableVoteAttestation bool // Whether to skip assembling vote attestation + + Mev MevConfig // Mev configuration } // DefaultConfig contains default settings for miner. @@ -70,6 +72,8 @@ var DefaultConfig = Config{ Recommit: 3 * time.Second, NewPayloadTimeout: 2 * time.Second, DelayLeftOver: 50 * time.Millisecond, + + Mev: DefaultMevConfig, } // Miner creates blocks and searches for proof-of-work values. @@ -82,6 +86,8 @@ type Miner struct { stopCh chan struct{} worker *worker + bidSimulator *bidSimulator + wg sync.WaitGroup } @@ -95,6 +101,10 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even stopCh: make(chan struct{}), 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) go miner.update() return miner @@ -129,6 +139,7 @@ func (miner *Miner) update() { case downloader.StartEvent: wasMining := miner.Mining() miner.worker.stop() + miner.bidSimulator.stop() canStart = false if wasMining { // Resume mining after sync was finished @@ -141,6 +152,7 @@ func (miner *Miner) update() { canStart = true if shouldStart { miner.worker.start() + miner.bidSimulator.start() } miner.worker.syncing.Store(false) @@ -148,6 +160,7 @@ func (miner *Miner) update() { canStart = true if shouldStart { miner.worker.start() + miner.bidSimulator.start() } miner.worker.syncing.Store(false) @@ -157,13 +170,16 @@ func (miner *Miner) update() { case <-miner.startCh: if canStart { miner.worker.start() + miner.bidSimulator.start() } shouldStart = true case <-miner.stopCh: shouldStart = false miner.worker.stop() + miner.bidSimulator.stop() case <-miner.exitCh: miner.worker.close() + miner.bidSimulator.close() return } } @@ -186,6 +202,10 @@ func (miner *Miner) Mining() bool { return miner.worker.isRunning() } +func (miner *Miner) InTurn() bool { + return miner.worker.inTurn() +} + func (miner *Miner) Hashrate() uint64 { if pow, ok := miner.engine.(consensus.PoW); ok { return uint64(pow.Hashrate()) diff --git a/miner/miner_mev.go b/miner/miner_mev.go new file mode 100644 index 000000000..c499d0387 --- /dev/null +++ b/miner/miner_mev.go @@ -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, + } +} diff --git a/miner/worker.go b/miner/worker.go index c8232c294..4a473107d 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -24,6 +24,9 @@ import ( "sync/atomic" "time" + lru "github.com/hashicorp/golang-lru" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" @@ -40,8 +43,6 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" - lru "github.com/hashicorp/golang-lru" - "github.com/holiman/uint256" ) const ( @@ -162,9 +163,14 @@ type getWorkReq struct { 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 // and gathering the sealing result. type worker struct { + bidFetcher bidFetcher prefetcher core.Prefetcher config *Config chainConfig *params.ChainConfig @@ -287,9 +293,14 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus if init { worker.startCh <- struct{}{} } + return worker } +func (w *worker) setBestBidFetcher(fetcher bidFetcher) { + w.bidFetcher = fetcher +} + // setEtherbase sets the etherbase used to initialize the block coinbase field. func (w *worker) setEtherbase(addr common.Address) { w.mu.Lock() @@ -1218,6 +1229,24 @@ LOOP: 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) // Swap out the old work with the new one, terminating any leftover @@ -1228,6 +1257,12 @@ LOOP: 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 // and commits new work if consensus engine is running. // Note the assumption is held that the mutation is allowed to the passed env, do