feat: support MEV (#2224)
This commit is contained in:
parent
0dc4b1f119
commit
89c4ab2a05
@ -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
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
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
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, ¶ms, "mev_params")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ¶ms, 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
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
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
|
||||||
|
}
|
33
miner/builderclient/builderclient.go
Normal file
33
miner/builderclient/builderclient.go
Normal file
@ -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
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
|
||||||
|
Loading…
Reference in New Issue
Block a user