diff --git a/internal/ethapi/api_mev.go b/internal/ethapi/api_mev.go index d6e6d66e57..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. @@ -65,33 +62,18 @@ func (m *MevAPI) SendBid(ctx context.Context, args types.BidArgs) (common.Hash, 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 { + 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") - } - } - } else { - if len(args.PayBidTx) != 0 || args.PayBidTxGasUsed != 0 { - return common.Hash{}, types.NewInvalidPayBidTxError("payBidTx should be nil when builder fee is nil") - } + 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 5f9d2588b0..0831922501 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), @@ -305,6 +311,8 @@ func (b *bidSimulator) newBidLoop() { // commit aborts in-flight bid execution with given signal and resubmits a new one. commit := func(reason int32, bidRuntime *BidRuntime) { + log.Debug("BidSimulator: start", "bidHash", bidRuntime.bid.Hash().Hex()) + // 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 { @@ -312,6 +320,7 @@ func (b *bidSimulator) newBidLoop() { } if time.Until(b.bidMustBefore(bidRuntime.bid.ParentHash)) <= simDuration*leftOverTimeRate/leftOverTimeScale { + log.Debug("BidSimulator: abort commit, not enough time to simulate", "bidHash", bidRuntime.bid.Hash().Hex()) return } @@ -343,6 +352,7 @@ func (b *bidSimulator) newBidLoop() { if expectedValidatorReward.Cmp(big.NewInt(0)) < 0 { // damage self profit, ignore + log.Debug("BidSimulator: invalid bid, validator reward is less than 0, ignore", "bidHash", newBid.Hash().Hex()) continue } @@ -354,8 +364,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 { @@ -374,6 +382,7 @@ func (b *bidSimulator) newBidLoop() { continue } + log.Debug("BidSimulator: lower reward, ignore", "bidHash", newBid.Hash().Hex()) continue } @@ -385,6 +394,7 @@ func (b *bidSimulator) newBidLoop() { continue } + log.Debug("BidSimulator: lower reward, ignore", "bidHash", newBid.Hash().Hex()) case <-b.exitCh: return } @@ -500,8 +510,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 + bidTxLen = len(bidTxs) + payBidTx = bidTxs[bidTxLen-1] + + err error + success bool ) // ensure simulation exited then start next simulation @@ -526,7 +541,7 @@ func (b *bidSimulator) simBid(interruptCh chan int32, bidRuntime *BidRuntime) { if err != nil { logCtx = append(logCtx, "err", err) - log.Debug("bid simulation failed", logCtx...) + log.Info("BidSimulator: simulation failed", logCtx...) go b.reportIssue(bidRuntime, err) } @@ -541,9 +556,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 +567,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 +588,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 == bidTxLen-1 { + break + } err = bidRuntime.commitTransaction(b.chain, b.chainConfig, tx) if err != nil { @@ -581,8 +598,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 +608,38 @@ 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: greedy merge tx 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) + log.Info("BidSimulator: greedy merge tx fill transactions", "block", bidRuntime.env.header.Number, + "tx count", bidRuntime.env.tcount-bidTxLen+1, "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 { @@ -615,11 +662,16 @@ func (b *bidSimulator) reportIssue(bidRuntime *BidRuntime, err error) { cli := b.builders[bidRuntime.bid.Builder] if cli != nil { - cli.ReportIssue(context.Background(), &types.BidIssue{ + err = cli.ReportIssue(context.Background(), &types.BidIssue{ Validator: bidRuntime.env.header.Coinbase, Builder: bidRuntime.bid.Builder, + BidHash: bidRuntime.bid.Hash(), Message: err.Error(), }) + + if err != nil { + log.Error("BidSimulator: failed to report issue", "builder", bidRuntime.bid.Builder, "err", err) + } } } @@ -658,6 +710,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 { @@ -692,5 +747,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 09d7fedf03..2cc2ff00ab 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() @@ -1066,6 +1067,26 @@ func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stop filter.OnlyPlainTxs, filter.OnlyBlobTxs = false, true pendingBlobTxs := w.eth.TxPool().Pending(filter) + 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) { + if i == len(txs)-1 { + delete(commonTxs, acc) + } else { + commonTxs[acc] = txs[i+1:] + } + break + } + } + } + } + + filterBidTxs(pendingPlainTxs) + filterBidTxs(pendingBlobTxs) + } + // Split the pending transactions into locals and remotes. localPlainTxs, remotePlainTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingPlainTxs localBlobTxs, remoteBlobTxs := make(map[common.Address][]*txpool.LazyTransaction), pendingBlobTxs @@ -1080,6 +1101,7 @@ func (w *worker) fillTransactions(interruptCh chan int32, env *environment, stop localBlobTxs[account] = txs } } + // Fill the block with all available pending transactions. // we will abort when: // 1.new block was imported @@ -1116,7 +1138,7 @@ func (w *worker) generateWork(params *generateParams) *newPayloadResult { defer work.discard() if !params.noTxs { - err := w.fillTransactions(nil, work, nil) + err := w.fillTransactions(nil, work, nil, nil) if errors.Is(err, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) } @@ -1216,7 +1238,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): @@ -1307,15 +1329,27 @@ LOOP: if w.bidFetcher != nil && bestWork.header.Difficulty.Cmp(diffInTurn) == 0 { bestBid := w.bidFetcher.GetBestBid(bestWork.header.ParentHash) + if bestBid != nil { + log.Debug("BidSimulator: final compare", "block", bestWork.header.Number.Uint64(), + "localBlockReward", bestReward.String(), + "bidBlockReward", bestBid.packedBlockReward.String()) + } + 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)) + log.Debug("BidSimulator: final compare", "block", bestWork.header.Number.Uint64(), + "localValidatorReward", localValidatorReward.String(), + "bidValidatorReward", bestBid.packedValidatorReward.String()) + // blockReward(benefits delegators) and validatorReward(benefits the validator) are both optimal if localValidatorReward.CmpBig(bestBid.packedValidatorReward) < 0 { bestWork = bestBid.env from = bestBid.bid.Builder + + log.Debug("BidSimulator: bid win", "block", bestWork.header.Number.Uint64(), "bid", bestBid.bid.Hash()) } } } 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