diff --git a/internal/ethapi/api_mev.go b/internal/ethapi/api_mev.go index 0fbd1d008f..057b6a3dc1 100644 --- a/internal/ethapi/api_mev.go +++ b/internal/ethapi/api_mev.go @@ -7,10 +7,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" -) - -const ( - TransferTxGasLimit = 25000 + "github.com/ethereum/go-ethereum/params" ) // MevAPI implements the interfaces that defined in the BEP-322. @@ -68,20 +65,15 @@ func (m *MevAPI) SendBid(ctx context.Context, args types.BidArgs) (common.Hash, 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 { + return common.Hash{}, types.NewInvalidPayBidTxError("payBidTx and payBidTxGasUsed are must-have") } - 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") + if args.PayBidTxGasUsed > params.PayBidTxGasLimit { + return common.Hash{}, types.NewInvalidBidError( + fmt.Sprintf("transfer tx gas used must be no more than %v", params.PayBidTxGasLimit)) } return m.b.SendBid(ctx, &args) diff --git a/miner/bid_simulator.go b/miner/bid_simulator.go index b87c3054fc..32869f78cb 100644 --- a/miner/bid_simulator.go +++ b/miner/bid_simulator.go @@ -11,6 +11,8 @@ import ( "sync/atomic" "time" + mapset "github.com/deckarep/golang-set/v2" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/bidutil" "github.com/ethereum/go-ethereum/consensus" @@ -61,9 +63,10 @@ var ( } ) -type WorkPreparer interface { +type bidWorker interface { prepareWork(params *generateParams) (*environment, error) etherbase() common.Address + fillTransactions(interruptCh chan int32, env *environment, stopTimer *time.Timer, bidTxs mapset.Set[common.Hash]) (err error) } // simBidReq is the request for simulating a bid @@ -79,7 +82,8 @@ type bidSimulator struct { delayLeftOver time.Duration chain *core.BlockChain chainConfig *params.ChainConfig - workPreparer WorkPreparer + engine consensus.Engine + bidWorker bidWorker running atomic.Bool // controlled by miner exitCh chan struct{} @@ -112,16 +116,18 @@ type bidSimulator struct { func newBidSimulator( config *MevConfig, delayLeftOver time.Duration, - chainConfig *params.ChainConfig, chain *core.BlockChain, - workPreparer WorkPreparer, + chainConfig *params.ChainConfig, + engine consensus.Engine, + bidWorker bidWorker, ) *bidSimulator { b := &bidSimulator{ config: config, delayLeftOver: delayLeftOver, - chainConfig: chainConfig, chain: chain, - workPreparer: workPreparer, + chainConfig: chainConfig, + engine: engine, + bidWorker: bidWorker, exitCh: make(chan struct{}), chainHeadCh: make(chan core.ChainHeadEvent, chainHeadChanSize), builders: make(map[common.Address]*builderclient.Client), @@ -354,8 +360,6 @@ func (b *bidSimulator) newBidLoop() { 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 { @@ -500,8 +504,13 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { blockNumber = bidRuntime.bid.BlockNumber parentHash = bidRuntime.bid.ParentHash builder = bidRuntime.bid.Builder - err error - success bool + + bidTxs = bidRuntime.bid.Txs + bidLen = len(bidTxs) + payBidTx = bidTxs[bidLen-1] + + err error + success bool ) // ensure simulation exited then start next simulation @@ -541,9 +550,9 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { // prepareWork will configure header with a suitable time according to consensus // prepareWork will start trie prefetching - if bidRuntime.env, err = b.workPreparer.prepareWork(&generateParams{ + if bidRuntime.env, err = b.bidWorker.prepareWork(&generateParams{ parentHash: bidRuntime.bid.ParentHash, - coinbase: b.workPreparer.etherbase(), + coinbase: b.bidWorker.etherbase(), }); err != nil { return } @@ -552,6 +561,7 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { if bidRuntime.env.gasPool == nil { bidRuntime.env.gasPool = new(core.GasPool).AddGas(gasLimit) bidRuntime.env.gasPool.SubGas(params.SystemTxsGas) + bidRuntime.env.gasPool.SubGas(params.PayBidTxGasLimit) } if bidRuntime.bid.GasUsed > bidRuntime.env.gasPool.Gas() { @@ -572,8 +582,9 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { default: } - // Start executing the transaction - bidRuntime.env.state.SetTxContext(tx.Hash(), bidRuntime.env.tcount) + if bidRuntime.env.tcount == bidLen-1 { + break + } err = bidRuntime.commitTransaction(b.chain, b.chainConfig, tx) if err != nil { @@ -581,8 +592,6 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { err = fmt.Errorf("invalid tx in bid, %v", err) return } - - bidRuntime.env.tcount++ } bidRuntime.packReward(b.config.ValidatorCommission) @@ -593,6 +602,39 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { return } + // fill transactions from mempool + if b.config.GreedyMergeTx { + delay := b.engine.Delay(b.chain, bidRuntime.env.header, &b.delayLeftOver) + if delay != nil && *delay > 0 { + log.Debug("BidSimulator: GreedyMergeTx stopTimer", "block", bidRuntime.env.header.Number, + "header time", time.Until(time.Unix(int64(bidRuntime.env.header.Time), 0)), + "commit delay", *delay, "DelayLeftOver", b.delayLeftOver) + + stopTimer := time.NewTimer(*delay) + + bidTxsSet := mapset.NewSet[common.Hash]() + for _, tx := range bidRuntime.bid.Txs { + bidTxsSet.Add(tx.Hash()) + } + + fillErr := b.bidWorker.fillTransactions(interruptCh, bidRuntime.env, stopTimer, bidTxsSet) + if fillErr != nil { + log.Debug("BidSimulator: GreedyMergeTx fillTransactions", "block", bidRuntime.env.header.Number, "err", fillErr) + } + + // recalculate the packed reward + bidRuntime.packReward(b.config.ValidatorCommission) + } + } + + bidRuntime.env.gasPool.AddGas(params.PayBidTxGasLimit) + err = bidRuntime.commitTransaction(b.chain, b.chainConfig, payBidTx) + if err != nil { + log.Error("BidSimulator: failed to commit tx", "bidHash", bidRuntime.bid.Hash(), "tx", payBidTx.Hash(), "err", err) + err = fmt.Errorf("invalid tx in bid, %v", err) + return + } + bestBid := b.GetBestBid(parentHash) if bestBid == nil { @@ -663,6 +705,9 @@ func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *para sc *types.BlobSidecar ) + // Start executing the transaction + r.env.state.SetTxContext(tx.Hash(), r.env.tcount) + if tx.Type() == types.BlobTxType { sc := types.NewBlobSidecarFromTx(tx) if sc == nil { @@ -697,5 +742,7 @@ func (r *BidRuntime) commitTransaction(chain *core.BlockChain, chainConfig *para env.receipts = append(env.receipts, receipt) } + r.env.tcount++ + return nil } diff --git a/miner/miner.go b/miner/miner.go index 18a267ab83..665e409462 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -102,7 +102,7 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even worker: newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, false), } - miner.bidSimulator = newBidSimulator(&config.Mev, config.DelayLeftOver, chainConfig, eth.BlockChain(), miner.worker) + miner.bidSimulator = newBidSimulator(&config.Mev, config.DelayLeftOver, eth.BlockChain(), chainConfig, engine, miner.worker) miner.worker.setBestBidFetcher(miner.bidSimulator) miner.wg.Add(1) diff --git a/miner/miner_mev.go b/miner/miner_mev.go index 8f5315d178..f4ced7f8d5 100644 --- a/miner/miner_mev.go +++ b/miner/miner_mev.go @@ -18,10 +18,11 @@ type BuilderConfig struct { type MevConfig struct { Enabled bool // Whether to enable Mev or not + GreedyMergeTx bool // Whether to merge local transactions to the bid BuilderFeeCeil string // The maximum builder fee of a bid SentryURL string // The url of Mev sentry Builders []BuilderConfig // The list of builders - ValidatorCommission uint64 // 100 means 1% + ValidatorCommission uint64 // 100 means the validator claims 1% from block reward BidSimulationLeftOver time.Duration } diff --git a/miner/worker.go b/miner/worker.go index cdd47e2bc6..3ec2fe5c03 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -24,6 +24,7 @@ import ( "sync/atomic" "time" + mapset "github.com/deckarep/golang-set/v2" lru "github.com/hashicorp/golang-lru" "github.com/holiman/uint256" @@ -1045,7 +1046,7 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { // fillTransactions retrieves the pending transactions from the txpool and fills them // into the given sealing block. The transaction selection and ordering strategy can // be customized with the plugin in the future. -func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stopTimer *time.Timer) (err error) { +func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stopTimer *time.Timer, bidTxs mapset.Set[common.Hash]) (err error) { w.mu.RLock() tip := w.tip w.mu.RUnlock() @@ -1080,6 +1081,30 @@ func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stop localBlobTxs[account] = txs } } + + if bidTxs != nil { + filterBidTxs := func(commonTxs map[common.Address][]*txpool.LazyTransaction) { + for acc, txs := range commonTxs { + for i := len(txs) - 1; i >= 0; i-- { + if bidTxs.Contains(txs[i].Hash) { + txs = txs[i+1:] + break + } + } + if len(txs) == 0 { + delete(commonTxs, acc) + } else { + commonTxs[acc] = txs + } + } + } + + filterBidTxs(localPlainTxs) + filterBidTxs(localBlobTxs) + filterBidTxs(remotePlainTxs) + filterBidTxs(remoteBlobTxs) + } + // Fill the block with all available pending transactions. // we will abort when: // 1.new block was imported @@ -1108,21 +1133,21 @@ func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stop } // generateWork generates a sealing block based on the given parameters. -func (w *worker) generateWork(params *generateParams) *newPayloadResult { - work, err := w.prepareWork(params) +func (w *worker) generateWork(geParams *generateParams) *newPayloadResult { + work, err := w.prepareWork(geParams) if err != nil { return &newPayloadResult{err: err} } defer work.discard() - if !params.noTxs { - err := w.fillTransactions(nil, work, nil) + if !geParams.noTxs { + err := w.fillTransactions(nil, work, nil, nil) if errors.Is(err, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) } } fees := work.state.GetBalance(consensus.SystemAddress) - block, _, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, params.withdrawals) + block, _, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, geParams.withdrawals) if err != nil { return &newPayloadResult{err: err} } @@ -1216,7 +1241,7 @@ LOOP: // Fill pending transactions from the txpool into the block. fillStart := time.Now() - err = w.fillTransactions(interruptCh, work, stopTimer) + err = w.fillTransactions(interruptCh, work, stopTimer, nil) fillDuration := time.Since(fillStart) switch { case errors.Is(err, errBlockInterruptedByNewHead): diff --git a/params/protocol_params.go b/params/protocol_params.go index 95c1eb266d..0ab8b6a396 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -27,6 +27,7 @@ const ( MinGasLimit uint64 = 5000 // Minimum the gas limit may ever be. MaxGasLimit uint64 = 0x7fffffffffffffff // Maximum the gas limit (2^63-1). GenesisGasLimit uint64 = 4712388 // Gas limit of the Genesis block. + PayBidTxGasLimit uint64 = 25000 // Gas limit of the PayBidTx in the types.BidArgs. MaximumExtraDataSize uint64 = 32 // Maximum size extra data may be after Genesis. ForkIDSize uint64 = 4 // The length of fork id