From c14c2997d283ffcf63369bb2556bea809390e67b Mon Sep 17 00:00:00 2001 From: Wasif Iqbal Date: Wed, 23 Aug 2023 16:28:01 -0500 Subject: [PATCH] Use different builders instead of configuration switch since major refactor required to handle more dynamic configurations. --- cmd/geth/main.go | 1 - cmd/utils/flags.go | 9 - miner/algo_common.go | 54 ------ miner/algo_common_test.go | 4 +- miner/algo_greedy.go | 26 +-- miner/algo_greedy_buckets.go | 23 +-- miner/algo_greedy_buckets_multisnap.go | 241 +++++++++++++++++++++++++ miner/algo_greedy_multisnap.go | 134 ++++++++++++++ miner/algo_greedy_test.go | 20 +- miner/algo_test.go | 81 ++++----- miner/env_changes_test.go | 2 - miner/miner.go | 50 ++--- miner/multi_worker.go | 2 +- miner/worker.go | 46 +++-- 14 files changed, 503 insertions(+), 190 deletions(-) create mode 100644 miner/algo_greedy_buckets_multisnap.go create mode 100644 miner/algo_greedy_multisnap.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d32b194681..7795f0b7d9 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -158,7 +158,6 @@ var ( builderApiFlags = []cli.Flag{ utils.BuilderEnabled, - utils.BuilderEnableMultiTxSnapshot, utils.BuilderAlgoTypeFlag, utils.BuilderPriceCutoffPercentFlag, utils.BuilderEnableValidatorChecks, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 62fd9dc8d8..d70ed9b12b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -697,14 +697,6 @@ var ( Usage: "Enable the builder", Category: flags.BuilderCategory, } - BuilderEnableMultiTxSnapshot = &cli.BoolFlag{ - Name: "builder.multi_tx_snapshot", - Usage: "Enable multi-transaction snapshots for block building, " + - "which decrease amount of state copying on bundle reverts (note: experimental)", - EnvVars: []string{"BUILDER_MULTI_TX_SNAPSHOT"}, - Value: ethconfig.Defaults.Miner.EnableMultiTransactionSnapshot, - Category: flags.BuilderCategory, - } // BuilderAlgoTypeFlag replaces MinerAlgoTypeFlag to move away from deprecated miner package // Note: builder.algotype was previously miner.algotype - this flag is still propagated to the miner configuration, @@ -1972,7 +1964,6 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { } } - cfg.EnableMultiTransactionSnapshot = ctx.Bool(BuilderEnableMultiTxSnapshot.Name) cfg.DiscardRevertibleTxOnErr = ctx.Bool(BuilderDiscardRevertibleTxOnErr.Name) cfg.PriceCutoffPercent = ctx.Int(BuilderPriceCutoffPercentFlag.Name) } diff --git a/miner/algo_common.go b/miner/algo_common.go index aea5602f94..e01ea6604a 100644 --- a/miner/algo_common.go +++ b/miner/algo_common.go @@ -38,7 +38,6 @@ var ( ExpectedProfit: nil, ProfitThresholdPercent: defaultProfitThresholdPercent, PriceCutoffPercent: defaultPriceCutoffPercent, - EnableMultiTxSnap: false, } ) @@ -84,9 +83,6 @@ type algorithmConfig struct { // is 10 (i.e. 10%), then the minimum effective gas price included in the same bucket as the top transaction // is (1000 * 10%) = 100 wei. PriceCutoffPercent int - // EnableMultiTxSnap is true if we want to use multi-transaction snapshot for committing transactions, - // which reduce state copies when reverting failed bundles (note: experimental) - EnableMultiTxSnap bool } type chainData struct { @@ -121,56 +117,6 @@ type ( CommitTxFunc func(*types.Transaction, chainData) (*types.Receipt, int, error) ) -func NewBuildBlockFunc( - inputEnvironment *environment, - builderKey *ecdsa.PrivateKey, - chData chainData, - algoConf algorithmConfig, - greedyBuckets *greedyBucketsBuilder, - greedy *greedyBuilder, -) BuildBlockFunc { - if algoConf.EnableMultiTxSnap { - return func(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { - orders := types.NewTransactionsByPriceAndNonce(inputEnvironment.signer, transactions, - simBundles, simSBundles, inputEnvironment.header.BaseFee) - - usedBundles, usedSbundles, err := BuildMultiTxSnapBlock( - inputEnvironment, - builderKey, - chData, - algoConf, - orders, - ) - if err != nil { - log.Trace("Error(s) building multi-tx snapshot block", "err", err) - } - return inputEnvironment, usedBundles, usedSbundles - } - } else if builder := greedyBuckets; builder != nil { - return func(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { - orders := types.NewTransactionsByPriceAndNonce(inputEnvironment.signer, transactions, - simBundles, simSBundles, inputEnvironment.header.BaseFee) - - envDiff := newEnvironmentDiff(inputEnvironment.copy()) - usedBundles, usedSbundles := builder.mergeOrdersIntoEnvDiff(envDiff, orders) - envDiff.applyToBaseEnv() - return envDiff.baseEnvironment, usedBundles, usedSbundles - } - } else if builder := greedy; builder != nil { - return func(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { - orders := types.NewTransactionsByPriceAndNonce(inputEnvironment.signer, transactions, - simBundles, simSBundles, inputEnvironment.header.BaseFee) - - envDiff := newEnvironmentDiff(inputEnvironment.copy()) - usedBundles, usedSbundles := builder.mergeOrdersIntoEnvDiff(envDiff, orders) - envDiff.applyToBaseEnv() - return envDiff.baseEnvironment, usedBundles, usedSbundles - } - } else { - panic("invalid call to build block function") - } -} - func ValidateGasPriceAndProfit(algoConf algorithmConfig, actualPrice, expectedPrice *big.Int, tolerablePriceDifferencePercent int, actualProfit, expectedProfit *big.Int) error { // allow tolerablePriceDifferencePercent % divergence diff --git a/miner/algo_common_test.go b/miner/algo_common_test.go index 6eb2984179..e3372626f0 100644 --- a/miner/algo_common_test.go +++ b/miner/algo_common_test.go @@ -525,7 +525,7 @@ func TestGetSealingWorkAlgos(t *testing.T) { testConfig.AlgoType = ALGO_MEV_GETH }) - for _, algoType := range []AlgoType{ALGO_MEV_GETH, ALGO_GREEDY, ALGO_GREEDY_BUCKETS} { + for _, algoType := range []AlgoType{ALGO_MEV_GETH, ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP} { local := new(params.ChainConfig) *local = *ethashChainConfig local.TerminalTotalDifficulty = big.NewInt(0) @@ -540,7 +540,7 @@ func TestGetSealingWorkAlgosWithProfit(t *testing.T) { testConfig.BuilderTxSigningKey = nil }) - for _, algoType := range []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS} { + for _, algoType := range []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP} { var err error testConfig.BuilderTxSigningKey, err = crypto.GenerateKey() require.NoError(t, err) diff --git a/miner/algo_greedy.go b/miner/algo_greedy.go index 4a712f03c3..f40f5ff872 100644 --- a/miner/algo_greedy.go +++ b/miner/algo_greedy.go @@ -20,36 +20,24 @@ type greedyBuilder struct { chainData chainData builderKey *ecdsa.PrivateKey interrupt *int32 - buildBlockFunc BuildBlockFunc algoConf algorithmConfig } func newGreedyBuilder( chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *int32, -) (*greedyBuilder, error) { +) *greedyBuilder { if algoConf == nil { - return nil, errNoAlgorithmConfig + panic("algoConf cannot be nil") } - builder := &greedyBuilder{ + return &greedyBuilder{ inputEnvironment: env, - chainData: chainData{chainConfig: chainConfig, chain: chain, blacklist: blacklist}, + chainData: chainData{chainConfig, chain, blacklist}, builderKey: key, interrupt: interrupt, algoConf: *algoConf, } - // Initialize block builder function - builder.buildBlockFunc = NewBuildBlockFunc( - builder.inputEnvironment, - builder.builderKey, - builder.chainData, - builder.algoConf, - nil, - builder, - ) - - return builder, nil } func (b *greedyBuilder) mergeOrdersIntoEnvDiff( @@ -115,5 +103,9 @@ func (b *greedyBuilder) mergeOrdersIntoEnvDiff( } func (b *greedyBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { - return b.buildBlockFunc(simBundles, simSBundles, transactions) + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, b.inputEnvironment.header.BaseFee) + envDiff := newEnvironmentDiff(b.inputEnvironment.copy()) + usedBundles, usedSbundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) + envDiff.applyToBaseEnv() + return envDiff.baseEnvironment, usedBundles, usedSbundles } diff --git a/miner/algo_greedy_buckets.go b/miner/algo_greedy_buckets.go index 63fad2989a..b3e410eb2a 100644 --- a/miner/algo_greedy_buckets.go +++ b/miner/algo_greedy_buckets.go @@ -25,18 +25,17 @@ type greedyBucketsBuilder struct { interrupt *int32 gasUsedMap map[*types.TxWithMinerFee]uint64 algoConf algorithmConfig - buildBlockFunc BuildBlockFunc } func newGreedyBucketsBuilder( chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *int32, -) (*greedyBucketsBuilder, error) { +) *greedyBucketsBuilder { if algoConf == nil { - return nil, errNoAlgorithmConfig + panic("algoConf cannot be nil") } - builder := &greedyBucketsBuilder{ + return &greedyBucketsBuilder{ inputEnvironment: env, chainData: chainData{chainConfig: chainConfig, chain: chain, blacklist: blacklist}, builderKey: key, @@ -44,10 +43,6 @@ func newGreedyBucketsBuilder( gasUsedMap: make(map[*types.TxWithMinerFee]uint64), algoConf: *algoConf, } - - // Initialize block builder function - builder.buildBlockFunc = NewBuildBlockFunc(builder.inputEnvironment, builder.builderKey, builder.chainData, builder.algoConf, builder, nil) - return builder, nil } // CutoffPriceFromOrder returns the cutoff price for a given order based on the cutoff percent. @@ -145,9 +140,6 @@ func (b *greedyBucketsBuilder) commit(envDiff *environmentDiff, usedEntry.Success = false usedSbundles = append(usedSbundles, usedEntry) } - } else { - usedEntry.Success = false - usedSbundles = append(usedSbundles, usedEntry) } continue } @@ -223,7 +215,10 @@ func (b *greedyBucketsBuilder) mergeOrdersIntoEnvDiff( return usedBundles, usedSbundles } -func (b *greedyBucketsBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, - transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { - return b.buildBlockFunc(simBundles, simSBundles, transactions) +func (b *greedyBucketsBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, b.inputEnvironment.header.BaseFee) + envDiff := newEnvironmentDiff(b.inputEnvironment.copy()) + usedBundles, usedSbundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) + envDiff.applyToBaseEnv() + return envDiff.baseEnvironment, usedBundles, usedSbundles } diff --git a/miner/algo_greedy_buckets_multisnap.go b/miner/algo_greedy_buckets_multisnap.go new file mode 100644 index 0000000000..ac95c4c8d9 --- /dev/null +++ b/miner/algo_greedy_buckets_multisnap.go @@ -0,0 +1,241 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyBucketsMultiSnapBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *int32 + gasUsedMap map[*types.TxWithMinerFee]uint64 + algoConf algorithmConfig +} + +func newGreedyBucketsMultiSnapBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *int32, +) *greedyBucketsMultiSnapBuilder { + if algoConf == nil { + panic("algoConf cannot be nil") + } + + return &greedyBucketsMultiSnapBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig: chainConfig, chain: chain, blacklist: blacklist}, + builderKey: key, + interrupt: interrupt, + gasUsedMap: make(map[*types.TxWithMinerFee]uint64), + algoConf: *algoConf, + } +} + +func (b *greedyBucketsMultiSnapBuilder) commit(changes *envChanges, + transactions []*types.TxWithMinerFee, + orders *types.TransactionsByPriceAndNonce, + gasUsedMap map[*types.TxWithMinerFee]uint64, retryMap map[*types.TxWithMinerFee]int, retryLimit int, +) ([]types.SimulatedBundle, []types.UsedSBundle) { + var ( + algoConf = b.algoConf + + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + + for _, order := range transactions { + if err := changes.env.state.NewMultiTxSnapshot(); err != nil { + log.Error("Failed to create new multi-tx snapshot", "err", err) + return usedBundles, usedSbundles + } + + orderFailed := false + + if tx := order.Tx(); tx != nil { + receipt, skip, err := changes.commitTx(tx, b.chainData) + orderFailed = err != nil + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + + // attempt to retry transaction commit up to retryLimit + // the gas used is set for the order to re-calculate profit of the transaction for subsequent retries + if receipt != nil { + // if the receipt is nil we don't attempt to retry the transaction - this is to mitigate abuse since + // without a receipt the default profit calculation for a transaction uses the gas limit which + // can cause the transaction to always be first in any profit-sorted transaction list + gasUsedMap[order] = receipt.GasUsed + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + } else { + if skip == shiftTx { + orders.ShiftAndPushByAccountForTx(tx) + } + // we don't check for error here because if EGP returns error, it would have been caught and returned by commitTx + effGapPrice, _ := tx.EffectiveGasTip(changes.env.header.BaseFee) + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + err := changes.commitBundle(bundle, b.chainData, algoConf) + orderFailed = err != nil + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + // if the bundle was not included due to low profit, we can retry the bundle + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + } else { + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), + "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.EthSentToCoinbase)) + usedBundles = append(usedBundles, *bundle) + } + } else if sbundle := order.SBundle(); sbundle != nil { + err := changes.CommitSBundle(sbundle, b.chainData, b.builderKey, algoConf) + orderFailed = err != nil + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + Success: err == nil, + } + + isValidOrNotRetried := true + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + + // if the sbundle was not included due to low profit, we can retry the bundle + if ok := CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit); ok { + isValidOrNotRetried = false + } + } + } else { + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + } + + if isValidOrNotRetried { + usedSbundles = append(usedSbundles, usedEntry) + } + } else { + // note: this should never happen because we should not be inserting invalid transaction types into + // the orders heap + panic("unsupported order type found") + } + + if orderFailed { + if err := changes.env.state.MultiTxSnapshotRevert(); err != nil { + log.Error("Failed to revert snapshot", "err", err) + return usedBundles, usedSbundles + } + } else { + if err := changes.env.state.MultiTxSnapshotCommit(); err != nil { + log.Error("Failed to commit snapshot", "err", err) + return usedBundles, usedSbundles + } + } + } + return usedBundles, usedSbundles +} + +func (b *greedyBucketsMultiSnapBuilder) mergeOrdersAndApplyToEnv( + orders *types.TransactionsByPriceAndNonce) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + if orders.Peek() == nil { + return b.inputEnvironment, nil, nil + } + + changes, err := newEnvChanges(b.inputEnvironment) + if err != nil { + log.Error("Failed to create new environment changes", "err", err) + return b.inputEnvironment, nil, nil + } + + const retryLimit = 1 + + var ( + baseFee = changes.env.header.BaseFee + retryMap = make(map[*types.TxWithMinerFee]int) + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + transactions []*types.TxWithMinerFee + priceCutoffPercent = b.algoConf.PriceCutoffPercent + + SortInPlaceByProfit = func(baseFee *big.Int, transactions []*types.TxWithMinerFee, gasUsedMap map[*types.TxWithMinerFee]uint64) { + sort.SliceStable(transactions, func(i, j int) bool { + return transactions[i].Profit(baseFee, gasUsedMap[transactions[i]]).Cmp(transactions[j].Profit(baseFee, gasUsedMap[transactions[j]])) > 0 + }) + } + ) + + minPrice := CutoffPriceFromOrder(orders.Peek(), priceCutoffPercent) + for { + order := orders.Peek() + if order == nil { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(changes, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + // re-run since committing transactions may have pushed higher nonce transactions, or previously + // failed transactions back into orders heap + continue + } + break + } + + if ok := IsOrderInPriceRange(order, minPrice); ok { + orders.Pop() + transactions = append(transactions, order) + } else { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(changes, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + } + minPrice = CutoffPriceFromOrder(order, priceCutoffPercent) + } + } + + if err := changes.apply(); err != nil { + log.Error("Failed to apply changes", "err", err) + return b.inputEnvironment, nil, nil + } + + return changes.env, usedBundles, usedSbundles +} + +func (b *greedyBucketsMultiSnapBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, b.inputEnvironment.header.BaseFee) + return b.mergeOrdersAndApplyToEnv(orders) +} diff --git a/miner/algo_greedy_multisnap.go b/miner/algo_greedy_multisnap.go new file mode 100644 index 0000000000..ca3ee3d3ed --- /dev/null +++ b/miner/algo_greedy_multisnap.go @@ -0,0 +1,134 @@ +package miner + +import ( + "crypto/ecdsa" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyMultiSnapBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *int32 + algoConf algorithmConfig +} + +func newGreedyMultiSnapBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *int32, +) *greedyMultiSnapBuilder { + if algoConf == nil { + algoConf = &defaultAlgorithmConfig + } + return &greedyMultiSnapBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig, chain, blacklist}, + builderKey: key, + interrupt: interrupt, + algoConf: *algoConf, + } +} + +func (b *greedyMultiSnapBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, b.inputEnvironment.header.BaseFee) + + var ( + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + + changes, err := newEnvChanges(b.inputEnvironment) + if err != nil { + log.Error("Failed to create new environment changes", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + for { + order := orders.Peek() + if order == nil { + break + } + + orderFailed := false + if err := changes.env.state.NewMultiTxSnapshot(); err != nil { + log.Error("Failed to create snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + if tx := order.Tx(); tx != nil { + receipt, skip, err := changes.commitTx(tx, b.chainData) + switch skip { + case shiftTx: + orders.Shift() + case popTx: + orders.Pop() + } + orderFailed = err != nil + + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + } else { + // we don't check for error here because if EGP returns error, it would have been caught and returned by commitTx + effGapPrice, _ := tx.EffectiveGasTip(changes.env.header.BaseFee) + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + err := changes.commitBundle(bundle, b.chainData, b.algoConf) + orders.Pop() + orderFailed = err != nil + + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + } else { + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), + "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.EthSentToCoinbase)) + usedBundles = append(usedBundles, *bundle) + } + } else if sbundle := order.SBundle(); sbundle != nil { + err := changes.CommitSBundle(sbundle, b.chainData, b.builderKey, b.algoConf) + orders.Pop() + orderFailed = err != nil + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + Success: err == nil, + } + + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + } else { + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + } + + usedSbundles = append(usedSbundles, usedEntry) + } + + if orderFailed { + if err := changes.env.state.MultiTxSnapshotRevert(); err != nil { + log.Error("Failed to revert snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + } else { + if err := changes.env.state.MultiTxSnapshotCommit(); err != nil { + log.Error("Failed to commit snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + } + } + + if err := changes.apply(); err != nil { + log.Error("Failed to apply changes", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + return changes.env, usedBundles, usedSbundles +} diff --git a/miner/algo_greedy_test.go b/miner/algo_greedy_test.go index c7d6aae682..ba680ec059 100644 --- a/miner/algo_greedy_test.go +++ b/miner/algo_greedy_test.go @@ -11,7 +11,7 @@ import ( ) func TestBuildBlockGasLimit(t *testing.T) { - algos := []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS} + algos := []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP} for _, algo := range algos { statedb, chData, signers := genTestSetup(GasLimit) env := newEnvironment(chData, statedb, signers.addresses[0], 21000, big.NewInt(1)) @@ -29,17 +29,17 @@ func TestBuildBlockGasLimit(t *testing.T) { var result *environment switch algo { + case ALGO_GREEDY: + builder := newGreedyBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) + result, _, _ = builder.buildBlock([]types.SimulatedBundle{}, nil, txs) + case ALGO_GREEDY_MULTISNAP: + builder := newGreedyMultiSnapBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) + result, _, _ = builder.buildBlock([]types.SimulatedBundle{}, nil, txs) case ALGO_GREEDY_BUCKETS: - builder, err := newGreedyBucketsBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) - if err != nil { - t.Fatalf("Error creating greedy buckets builder: %v", err) - } + builder := newGreedyBucketsBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) result, _, _ = builder.buildBlock([]types.SimulatedBundle{}, nil, txs) - case ALGO_GREEDY: - builder, err := newGreedyBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) - if err != nil { - t.Fatalf("Error creating greedy builder: %v", err) - } + case ALGO_GREEDY_BUCKETS_MULTISNAP: + builder := newGreedyBucketsMultiSnapBuilder(chData.chain, chData.chainConfig, &defaultAlgorithmConfig, nil, env, nil, nil) result, _, _ = builder.buildBlock([]types.SimulatedBundle{}, nil, txs) } diff --git a/miner/algo_test.go b/miner/algo_test.go index 5e3251467b..ab63031e48 100644 --- a/miner/algo_test.go +++ b/miner/algo_test.go @@ -38,7 +38,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(2 * 21_000), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: defaultAlgorithmConfig, }, { @@ -65,7 +65,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(4 * 21_000), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: defaultAlgorithmConfig, }, { @@ -84,7 +84,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(0), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: defaultAlgorithmConfig, }, { @@ -106,7 +106,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(50_000), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: defaultAlgorithmConfig, }, { @@ -128,7 +128,7 @@ var algoTests = []*algoTest{ } }, WantProfit: common.Big0, - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: algorithmConfig{ DropRevertibleTxOnErr: true, EnforceProfit: defaultAlgorithmConfig.EnforceProfit, @@ -160,7 +160,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(21_000), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: algorithmConfig{ DropRevertibleTxOnErr: true, EnforceProfit: defaultAlgorithmConfig.EnforceProfit, @@ -191,7 +191,7 @@ var algoTests = []*algoTest{ } }, WantProfit: big.NewInt(50_000), - SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS}, + SupportedAlgorithms: []AlgoType{ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP}, AlgorithmConfig: defaultAlgorithmConfig, }, } @@ -204,38 +204,25 @@ func TestAlgo(t *testing.T) { for _, test := range algoTests { for _, algo := range test.SupportedAlgorithms { - // test with multi-tx-snapshot enabled and disabled (default) - multiSnapDisabled := defaultAlgorithmConfig - multiSnapDisabled.EnableMultiTxSnap = false + testName := fmt.Sprintf("%s-%s", test.Name, algo.String()) - multiSnapEnabled := defaultAlgorithmConfig - multiSnapEnabled.EnableMultiTxSnap = true - - algoConfigs := []algorithmConfig{ - multiSnapEnabled, - multiSnapDisabled, - } - for _, algoConf := range algoConfigs { - testName := fmt.Sprintf("%s-%s-%t", test.Name, algo.String(), algoConf.EnableMultiTxSnap) - - t.Run(testName, func(t *testing.T) { - alloc, txPool, bundles, err := test.build(signer, 1) - if err != nil { - t.Fatalf("Build: %v", err) - } - simBundles, err := simulateBundles(config, test.Header, alloc, bundles) - if err != nil { - t.Fatalf("Simulate Bundles: %v", err) - } - gotProfit, err := runAlgoTest(algo, algoConf, config, alloc, txPool, simBundles, test.Header, 1) - if err != nil { - t.Fatal(err) - } - if test.WantProfit.Cmp(gotProfit) != 0 { - t.Fatalf("Profit: want %v, got %v", test.WantProfit, gotProfit) - } - }) - } + t.Run(testName, func(t *testing.T) { + alloc, txPool, bundles, err := test.build(signer, 1) + if err != nil { + t.Fatalf("Build: %v", err) + } + simBundles, err := simulateBundles(config, test.Header, alloc, bundles) + if err != nil { + t.Fatalf("Simulate Bundles: %v", err) + } + gotProfit, err := runAlgoTest(algo, test.AlgorithmConfig, config, alloc, txPool, simBundles, test.Header, 1) + if err != nil { + t.Fatal(err) + } + if test.WantProfit.Cmp(gotProfit) != 0 { + t.Fatalf("Profit: want %v, got %v", test.WantProfit, gotProfit) + } + }) } } } @@ -307,17 +294,17 @@ func runAlgoTest( // build block switch algo { + case ALGO_GREEDY: + builder := newGreedyBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) + resultEnv, _, _ = builder.buildBlock(bundles, nil, txPool) + case ALGO_GREEDY_MULTISNAP: + builder := newGreedyMultiSnapBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) + resultEnv, _, _ = builder.buildBlock(bundles, nil, txPool) case ALGO_GREEDY_BUCKETS: - builder, err := newGreedyBucketsBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) - if err != nil { - return nil, err - } + builder := newGreedyBucketsBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) resultEnv, _, _ = builder.buildBlock(bundles, nil, txPool) - case ALGO_GREEDY: - builder, err := newGreedyBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) - if err != nil { - return nil, err - } + case ALGO_GREEDY_BUCKETS_MULTISNAP: + builder := newGreedyBucketsMultiSnapBuilder(chData.chain, chData.chainConfig, &algoConf, nil, env, nil, nil) resultEnv, _, _ = builder.buildBlock(bundles, nil, txPool) } return resultEnv.profit, nil diff --git a/miner/env_changes_test.go b/miner/env_changes_test.go index e5b4fc2740..decb179dca 100644 --- a/miner/env_changes_test.go +++ b/miner/env_changes_test.go @@ -61,7 +61,6 @@ func TestBundleCommitSnaps(t *testing.T) { statedb, chData, signers := genTestSetup(GasLimit) algoConf := defaultAlgorithmConfig - algoConf.EnableMultiTxSnap = true env := newEnvironment(chData, statedb, signers.addresses[0], GasLimit, big.NewInt(1)) tx1 := signers.signTx(1, 21000, big.NewInt(0), big.NewInt(1), signers.addresses[2], big.NewInt(0), []byte{}) @@ -170,7 +169,6 @@ func TestErrorBundleCommitSnaps(t *testing.T) { statedb, chData, signers := genTestSetup(GasLimit) algoConf := defaultAlgorithmConfig - algoConf.EnableMultiTxSnap = true env := newEnvironment(chData, statedb, signers.addresses[0], 21000*2, big.NewInt(1)) // This tx will be included before bundle so bundle will fail because of gas limit diff --git a/miner/miner.go b/miner/miner.go index a6c8a1618b..203132fc7d 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -54,16 +54,22 @@ const ( ALGO_MEV_GETH AlgoType = iota ALGO_GREEDY ALGO_GREEDY_BUCKETS + ALGO_GREEDY_MULTISNAP + ALGO_GREEDY_BUCKETS_MULTISNAP ) func (a AlgoType) String() string { switch a { case ALGO_GREEDY: return "greedy" + case ALGO_GREEDY_MULTISNAP: + return "greedy-multi-snap" case ALGO_MEV_GETH: return "mev-geth" case ALGO_GREEDY_BUCKETS: return "greedy-buckets" + case ALGO_GREEDY_BUCKETS_MULTISNAP: + return "greedy-buckets-multi-snap" default: return "unsupported" } @@ -77,6 +83,10 @@ func AlgoTypeFlagToEnum(algoString string) (AlgoType, error) { return ALGO_GREEDY_BUCKETS, nil case ALGO_GREEDY.String(): return ALGO_GREEDY, nil + case ALGO_GREEDY_MULTISNAP.String(): + return ALGO_GREEDY_MULTISNAP, nil + case ALGO_GREEDY_BUCKETS_MULTISNAP.String(): + return ALGO_GREEDY_BUCKETS_MULTISNAP, nil default: return ALGO_MEV_GETH, errors.New("algo not recognized") } @@ -84,23 +94,22 @@ func AlgoTypeFlagToEnum(algoString string) (AlgoType, error) { // Config is the configuration parameters of mining. type Config struct { - Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards (default = first account) - Notify []string `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash). - NotifyFull bool `toml:",omitempty"` // Notify with pending block headers instead of work packages - ExtraData hexutil.Bytes `toml:",omitempty"` // Block extra data set by the miner - GasFloor uint64 // Target gas floor for mined blocks. - GasCeil uint64 // Target gas ceiling for mined blocks. - GasPrice *big.Int // Minimum gas price for mining a transaction - AlgoType AlgoType // Algorithm to use for block building - Recommit time.Duration // The time interval for miner to re-create mining work. - Noverify bool // Disable remote mining solution verification(only useful in ethash). - BuilderTxSigningKey *ecdsa.PrivateKey `toml:",omitempty"` // Signing key of builder coinbase to make transaction to validator - MaxMergedBundles int - Blocklist []common.Address `toml:",omitempty"` - NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload - PriceCutoffPercent int // Effective gas price cutoff % used for bucketing transactions by price (only useful in greedy-buckets AlgoType) - DiscardRevertibleTxOnErr bool // When enabled, if bundle revertible transaction has error on commit, builder will discard the transaction - EnableMultiTransactionSnapshot bool // Enable block building with multi-transaction snapshots to reduce state copying (note: experimental) + Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards (default = first account) + Notify []string `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash). + NotifyFull bool `toml:",omitempty"` // Notify with pending block headers instead of work packages + ExtraData hexutil.Bytes `toml:",omitempty"` // Block extra data set by the miner + GasFloor uint64 // Target gas floor for mined blocks. + GasCeil uint64 // Target gas ceiling for mined blocks. + GasPrice *big.Int // Minimum gas price for mining a transaction + AlgoType AlgoType // Algorithm to use for block building + Recommit time.Duration // The time interval for miner to re-create mining work. + Noverify bool // Disable remote mining solution verification(only useful in ethash). + BuilderTxSigningKey *ecdsa.PrivateKey `toml:",omitempty"` // Signing key of builder coinbase to make transaction to validator + MaxMergedBundles int + Blocklist []common.Address `toml:",omitempty"` + NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload + PriceCutoffPercent int // Effective gas price cutoff % used for bucketing transactions by price (only useful in greedy-buckets AlgoType) + DiscardRevertibleTxOnErr bool // When enabled, if bundle revertible transaction has error on commit, builder will discard the transaction } // DefaultConfig contains default settings for miner. @@ -112,10 +121,9 @@ var DefaultConfig = Config{ // consensus-layer usually will wait a half slot of time(6s) // for payload generation. It should be enough for Geth to // run 3 rounds. - Recommit: 2 * time.Second, - NewPayloadTimeout: 2 * time.Second, - PriceCutoffPercent: defaultPriceCutoffPercent, - EnableMultiTransactionSnapshot: defaultAlgorithmConfig.EnableMultiTxSnap, + Recommit: 2 * time.Second, + NewPayloadTimeout: 2 * time.Second, + PriceCutoffPercent: defaultPriceCutoffPercent, } // Miner creates blocks and searches for proof-of-work values. diff --git a/miner/multi_worker.go b/miner/multi_worker.go index 93cb8aadae..ab33a84ee8 100644 --- a/miner/multi_worker.go +++ b/miner/multi_worker.go @@ -141,7 +141,7 @@ func newMultiWorker(config *Config, chainConfig *params.ChainConfig, engine cons switch config.AlgoType { case ALGO_MEV_GETH: return newMultiWorkerMevGeth(config, chainConfig, engine, eth, mux, isLocalBlock, init) - case ALGO_GREEDY, ALGO_GREEDY_BUCKETS: + case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP: return newMultiWorkerGreedy(config, chainConfig, engine, eth, mux, isLocalBlock, init) default: panic("unsupported builder algorithm found") diff --git a/miner/worker.go b/miner/worker.go index 2453ec8395..ef31510ba2 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1312,7 +1312,7 @@ func (w *worker) fillTransactionsSelectAlgo(interrupt *int32, env *environment) err error ) switch w.flashbots.algoType { - case ALGO_GREEDY, ALGO_GREEDY_BUCKETS: + case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP: blockBundles, allBundles, usedSbundles, mempoolTxHashes, err = w.fillTransactionsAlgoWorker(interrupt, env) case ALGO_MEV_GETH: blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(interrupt, env) @@ -1426,37 +1426,59 @@ func (w *worker) fillTransactionsAlgoWorker(interrupt *int32, env *environment) EnforceProfit: true, ProfitThresholdPercent: defaultProfitThresholdPercent, PriceCutoffPercent: priceCutoffPercent, - EnableMultiTxSnap: w.config.EnableMultiTransactionSnapshot, } - builder, err := newGreedyBucketsBuilder( + builder := newGreedyBucketsBuilder( w.chain, w.chainConfig, algoConf, w.blockList, env, w.config.BuilderTxSigningKey, interrupt, ) - if err != nil { - return nil, nil, nil, nil, err + + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + case ALGO_GREEDY_BUCKETS_MULTISNAP: + priceCutoffPercent := w.config.PriceCutoffPercent + if !(priceCutoffPercent >= 0 && priceCutoffPercent <= 100) { + return nil, nil, nil, nil, errors.New("invalid price cutoff percent - must be between 0 and 100") + } + + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: true, + ProfitThresholdPercent: defaultProfitThresholdPercent, + PriceCutoffPercent: priceCutoffPercent, + } + builder := newGreedyBucketsMultiSnapBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, env, + w.config.BuilderTxSigningKey, interrupt, + ) + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + case ALGO_GREEDY_MULTISNAP: + // For greedy multi-snap builder, set algorithm configuration to default values, + // except DropRevertibleTxOnErr which is passed in from worker config + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: defaultAlgorithmConfig.EnforceProfit, + ProfitThresholdPercent: defaultAlgorithmConfig.ProfitThresholdPercent, } + builder := newGreedyMultiSnapBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, env, + w.config.BuilderTxSigningKey, interrupt, + ) newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) case ALGO_GREEDY: fallthrough default: // For default greedy builder, set algorithm configuration to default values, - // except DropRevertibleTxOnErr and EnableMultiTxSnap which are passed in from worker config + // except DropRevertibleTxOnErr which is passed in from worker config algoConf := &algorithmConfig{ - EnableMultiTxSnap: w.config.EnableMultiTransactionSnapshot, DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, EnforceProfit: defaultAlgorithmConfig.EnforceProfit, ProfitThresholdPercent: defaultAlgorithmConfig.ProfitThresholdPercent, } - builder, err := newGreedyBuilder( + builder := newGreedyBuilder( w.chain, w.chainConfig, algoConf, w.blockList, env, w.config.BuilderTxSigningKey, interrupt, ) - if err != nil { - return nil, nil, nil, nil, err - } - newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) }