diff --git a/CONFIG.md b/CONFIG.md index 2396777fb1..d6e8ed5dd6 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -393,6 +393,7 @@ DualBroadcast = false # Example ReadRequestsToMultipleNodes = false # Example Bundles = false # Example FastlaneAuctionRequestTimeout = '5s' # Example +FeeBoost = false # Example ``` @@ -438,6 +439,12 @@ FastlaneAuctionRequestTimeout = '5s' # Example ``` FastlaneAuctionRequestTimeout configures the HTTP request timeout for Fastlane Atlas auction requests. Defaults to 5s if not set. +### FeeBoost +```toml +FeeBoost = false # Example +``` +FeeBoost enables using GetMaxFee instead of GetFee for all TxM v2 transaction attempts. This makes SVR transactions as aggressive as possible with gas pricing. Only active when DualBroadcast is also enabled. + ## BalanceMonitor ```toml [BalanceMonitor] diff --git a/pkg/config/chain_scoped_transactions.go b/pkg/config/chain_scoped_transactions.go index 5206909b12..76eed8db8b 100644 --- a/pkg/config/chain_scoped_transactions.go +++ b/pkg/config/chain_scoped_transactions.go @@ -80,6 +80,10 @@ func (t *transactionManagerV2Config) FastlaneAuctionRequestTimeout() *time.Durat return &d } +func (t *transactionManagerV2Config) FeeBoost() bool { + return t.c.FeeBoost != nil && *t.c.FeeBoost +} + func (t *transactionsConfig) AutoPurge() AutoPurgeConfig { return &autoPurgeConfig{c: t.c.AutoPurge} } diff --git a/pkg/config/config.go b/pkg/config/config.go index b3fd2799d3..d40e457d5d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -136,6 +136,7 @@ type TransactionManagerV2 interface { ReadRequestsToMultipleNodes() *bool Bundles() *bool FastlaneAuctionRequestTimeout() *time.Duration + FeeBoost() bool } type GasEstimator interface { diff --git a/pkg/config/toml/config.go b/pkg/config/toml/config.go index ca83e4c297..931ec40218 100644 --- a/pkg/config/toml/config.go +++ b/pkg/config/toml/config.go @@ -592,6 +592,7 @@ type TransactionManagerV2Config struct { ReadRequestsToMultipleNodes *bool `toml:",omitempty"` Bundles *bool `toml:",omitempty"` FastlaneAuctionRequestTimeout *commonconfig.Duration `toml:",omitempty"` + FeeBoost *bool `toml:",omitempty"` } func (t *TransactionManagerV2Config) setFrom(f *TransactionManagerV2Config) { @@ -616,6 +617,9 @@ func (t *TransactionManagerV2Config) setFrom(f *TransactionManagerV2Config) { if v := f.FastlaneAuctionRequestTimeout; v != nil { t.FastlaneAuctionRequestTimeout = f.FastlaneAuctionRequestTimeout } + if v := f.FeeBoost; v != nil { + t.FeeBoost = f.FeeBoost + } } func (t *TransactionManagerV2Config) ValidateConfig() (err error) { diff --git a/pkg/config/toml/config_test.go b/pkg/config/toml/config_test.go index c833ff9fb0..c5ddfb2717 100644 --- a/pkg/config/toml/config_test.go +++ b/pkg/config/toml/config_test.go @@ -67,6 +67,7 @@ func TestDefaults_fieldsNotNil(t *testing.T) { unknown.Transactions.TransactionManagerV2.ReadRequestsToMultipleNodes = ptr(false) unknown.Transactions.TransactionManagerV2.Bundles = ptr(false) unknown.Transactions.TransactionManagerV2.FastlaneAuctionRequestTimeout = new(config.Duration) + unknown.Transactions.TransactionManagerV2.FeeBoost = ptr(false) unknown.Transactions.AutoPurge.Threshold = ptr(uint32(0)) unknown.Transactions.AutoPurge.MinAttempts = ptr(uint32(0)) unknown.Transactions.AutoPurge.DetectionApiUrl = new(config.URL) @@ -165,6 +166,7 @@ func TestDocs(t *testing.T) { docDefaults.Transactions.TransactionManagerV2.ReadRequestsToMultipleNodes = nil docDefaults.Transactions.TransactionManagerV2.Bundles = nil docDefaults.Transactions.TransactionManagerV2.FastlaneAuctionRequestTimeout = nil + docDefaults.Transactions.TransactionManagerV2.FeeBoost = nil // Fallback DA oracle is not set docDefaults.GasEstimator.DAOracle = DAOracle{} @@ -294,6 +296,7 @@ var fullConfig = EVMConfig{ BlockTime: config.MustNewDuration(42 * time.Second), CustomURL: config.MustParseURL("http://txs.org"), FastlaneAuctionRequestTimeout: config.MustNewDuration(15 * time.Second), + FeeBoost: ptr(true), }, }, diff --git a/pkg/config/toml/docs.toml b/pkg/config/toml/docs.toml index 67eabcec61..5edf5ac4d6 100644 --- a/pkg/config/toml/docs.toml +++ b/pkg/config/toml/docs.toml @@ -177,6 +177,8 @@ ReadRequestsToMultipleNodes = false # Example Bundles = false # Example # FastlaneAuctionRequestTimeout configures the HTTP request timeout for Fastlane Atlas auction requests. Defaults to 5s if not set. FastlaneAuctionRequestTimeout = '5s' # Example +# FeeBoost enables using GetMaxFee instead of GetFee for all TxM v2 transaction attempts. This makes SVR transactions as aggressive as possible with gas pricing. Only active when DualBroadcast is also enabled. +FeeBoost = false # Example [BalanceMonitor] # Enabled balance monitoring for all keys. diff --git a/pkg/config/toml/testdata/config-full.toml b/pkg/config/toml/testdata/config-full.toml index af219ae27e..e812226903 100644 --- a/pkg/config/toml/testdata/config-full.toml +++ b/pkg/config/toml/testdata/config-full.toml @@ -50,6 +50,7 @@ DualBroadcast = true ReadRequestsToMultipleNodes = false Bundles = false FastlaneAuctionRequestTimeout = '15s' +FeeBoost = true [BalanceMonitor] Enabled = true diff --git a/pkg/txm/attempt_builder.go b/pkg/txm/attempt_builder.go index 0fbf1da3bf..b50de9c0da 100644 --- a/pkg/txm/attempt_builder.go +++ b/pkg/txm/attempt_builder.go @@ -23,14 +23,16 @@ type attemptBuilder struct { priceMaxKey func(common.Address) *assets.Wei keystore keys.TxSigner emptyTxLimitDefault uint64 + feeBoost bool } -func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner, emptyTxLimitDefault uint64) *attemptBuilder { +func NewAttemptBuilder(priceMaxKey func(common.Address) *assets.Wei, estimator gas.EvmFeeEstimator, keystore keys.TxSigner, emptyTxLimitDefault uint64, feeBoost bool) *attemptBuilder { return &attemptBuilder{ priceMaxKey: priceMaxKey, EvmFeeEstimator: estimator, keystore: keystore, emptyTxLimitDefault: emptyTxLimitDefault, + feeBoost: feeBoost, } } @@ -38,13 +40,17 @@ func (a *attemptBuilder) NewAttempt(ctx context.Context, lggr logger.Logger, tx var fee gas.EvmFee var estimatedGasLimit uint64 var err error - if tx.IsPurgeable { - fee, estimatedGasLimit, err = a.EvmFeeEstimator.GetMaxFee(ctx, tx.Data, a.emptyTxLimitDefault, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) + if tx.IsPurgeable || a.feeBoost { + gasLimit := tx.SpecifiedGasLimit + if tx.IsPurgeable { + gasLimit = a.emptyTxLimitDefault + } + fee, estimatedGasLimit, err = a.GetMaxFee(ctx, tx.Data, gasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) if err != nil { return nil, err } } else { - fee, estimatedGasLimit, err = a.EvmFeeEstimator.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) + fee, estimatedGasLimit, err = a.GetFee(ctx, tx.Data, tx.SpecifiedGasLimit, a.priceMaxKey(tx.FromAddress), &tx.FromAddress, &tx.ToAddress) if err != nil { return nil, err } @@ -69,9 +75,14 @@ func (a *attemptBuilder) NewBumpAttempt(ctx context.Context, lggr logger.Logger, } func (a *attemptBuilder) NewAgnosticBumpAttempt(ctx context.Context, lggr logger.Logger, tx *types.Transaction, dynamic bool) (attempt *types.Attempt, err error) { - // if the transaction is purgeable, NewAttempt will return the max fee instantly, so there is no need to bump + // if the transaction is purgeable or feeBoost is enabled, NewAttempt will return the max fee instantly, so there is no need to bump attempt, err = a.NewAttempt(ctx, lggr, tx, dynamic) - if tx.IsPurgeable || err != nil { + + if err != nil { + return + } + + if tx.IsPurgeable || a.feeBoost { return } diff --git a/pkg/txm/attempt_builder_test.go b/pkg/txm/attempt_builder_test.go index 5c55af8dc4..d387595ea1 100644 --- a/pkg/txm/attempt_builder_test.go +++ b/pkg/txm/attempt_builder_test.go @@ -22,7 +22,7 @@ import ( ) func TestAttemptBuilder_newLegacyAttempt(t *testing.T) { - ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100, false) address := testutils.NewAddress() lggr := logger.Test(t) var gasLimit uint64 = 100 @@ -57,7 +57,7 @@ func TestAttemptBuilder_newLegacyAttempt(t *testing.T) { } func TestAttemptBuilder_newDynamicFeeAttempt(t *testing.T) { - ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(nil, nil, keystest.TxSigner(nil), 100, false) address := testutils.NewAddress() lggr := logger.Test(t) @@ -100,7 +100,7 @@ func TestAttemptBuilder_NewAttempt(t *testing.T) { var nonce uint64 = 1 var specifiedGasLimit uint64 = 200 var emptyGasLimit uint64 = 100 - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), emptyGasLimit) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), emptyGasLimit, false) address := testutils.NewAddress() lggr := logger.Test(t) @@ -160,6 +160,30 @@ func TestAttemptBuilder_NewAttempt(t *testing.T) { assert.Contains(t, err.Error(), "estimator error") mockEstimator.AssertExpectations(t) }) + + t.Run("uses SpecifiedGasLimit when feeBoost is enabled and tx is not purgeable", func(t *testing.T) { + boostEstimator := mocks.NewEvmFeeEstimator(t) + boostAb := NewAttemptBuilder(priceMaxKey, boostEstimator, keystest.TxSigner(nil), emptyGasLimit, true) + tx := &types.Transaction{ID: 10, FromAddress: address, Nonce: &nonce, SpecifiedGasLimit: specifiedGasLimit} + boostEstimator.On("GetMaxFee", mock.Anything, mock.Anything, specifiedGasLimit, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, specifiedGasLimit, nil).Once() + a, err := boostAb.NewAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, specifiedGasLimit, a.GasLimit) + boostEstimator.AssertExpectations(t) + }) + + t.Run("uses emptyTxLimitDefault when feeBoost is enabled and tx is purgeable", func(t *testing.T) { + boostEstimator := mocks.NewEvmFeeEstimator(t) + boostAb := NewAttemptBuilder(priceMaxKey, boostEstimator, keystest.TxSigner(nil), emptyGasLimit, true) + tx := &types.Transaction{ID: 10, FromAddress: address, IsPurgeable: true, Nonce: &nonce, SpecifiedGasLimit: specifiedGasLimit} + boostEstimator.On("GetMaxFee", mock.Anything, mock.Anything, emptyGasLimit, mock.Anything, mock.Anything, mock.Anything). + Return(gas.EvmFee{GasPrice: assets.NewWeiI(100)}, emptyGasLimit, nil).Once() + a, err := boostAb.NewAttempt(t.Context(), lggr, tx, false) + require.NoError(t, err) + assert.Equal(t, emptyGasLimit, a.GasLimit) + boostEstimator.AssertExpectations(t) + }) } func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { @@ -172,7 +196,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("returns original attempt when AttemptCount is 0", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, @@ -196,7 +220,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("bumps once when AttemptCount is 1", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, @@ -222,7 +246,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("bumps N times when AttemptCount is N", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, @@ -253,7 +277,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("returns last valid attempt when BumpFee fails", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, @@ -282,7 +306,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("caps bumps at maxBumpThreshold", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, @@ -307,7 +331,7 @@ func TestAttemptBuilder_NewAgnosticBumpAttempt(t *testing.T) { t.Run("returns max percentile attempt when transaction is purgeable", func(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) - ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100) + ab := NewAttemptBuilder(priceMaxKey, mockEstimator, keystest.TxSigner(nil), 100, false) tx := &types.Transaction{ ID: 10, diff --git a/pkg/txm/integration-tests/integration_test.go b/pkg/txm/integration-tests/integration_test.go index 80e0991de5..c8cba9a6b8 100644 --- a/pkg/txm/integration-tests/integration_test.go +++ b/pkg/txm/integration-tests/integration_test.go @@ -99,7 +99,7 @@ func setupTestnetTXM( require.NoError(t, err, "failed to add private key to keystore") // AttemptBuilder - ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitTransfer()) + ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitTransfer(), false) // InMemory storage store := storage.NewInMemoryStoreManager(lggr, chainID) @@ -158,7 +158,7 @@ func setupDevnetTXM( require.NoError(t, keystore.Add(privateKeyHex), "failed to add private key to keystore") // AttemptBuilder - ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitDefault()) + ab := txm.NewAttemptBuilder(configs.PriceMaxKey, estimator, keystore, configs.LimitDefault(), false) // InMemory storage store := storage.NewInMemoryStoreManager(lggr, chainID) diff --git a/pkg/txm/txm_test.go b/pkg/txm/txm_test.go index 68876a87d0..e45c32a901 100644 --- a/pkg/txm/txm_test.go +++ b/pkg/txm/txm_test.go @@ -427,7 +427,7 @@ func TestFlow_ResendTransaction(t *testing.T) { mockEstimator := mocks.NewEvmFeeEstimator(t) defaultGasLimit := uint64(100000) keystore := &keystest.FakeChainStore{} - attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000) + attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000, false) stuckTxDetector := txm.NewStuckTxDetector(logger.Test(t), "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)}) tm := txm.NewTxm(logger.Test(t), testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, nil, txm.NewNoopTxmMetrics()) initialNonce := uint64(0) @@ -501,7 +501,7 @@ func TestFlow_ErrorHandler(t *testing.T) { config := txm.Config{EIP1559: true, EmptyTxLimitDefault: 22000, RetryBlockThreshold: 0, BlockTime: 2 * time.Second} mockEstimator := mocks.NewEvmFeeEstimator(t) keystore := &keystest.FakeChainStore{} - attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000) + attemptBuilder := txm.NewAttemptBuilder(func(address common.Address) *assets.Wei { return assets.NewWeiI(1) }, mockEstimator, keystore, 22000, false) stuckTxDetector := txm.NewStuckTxDetector(lggr, "", txm.StuckTxDetectorConfig{BlockTime: config.BlockTime, StuckTxBlockThreshold: uint32(config.RetryBlockThreshold + 1)}) errorHandler := dualbroadcast.NewErrorHandler() tm := txm.NewTxm(lggr, testutils.FixtureChainID, client, attemptBuilder, txStoreManager, stuckTxDetector, config, keystore, errorHandler, txm.NewNoopTxmMetrics()) diff --git a/pkg/txmgr/builder.go b/pkg/txmgr/builder.go index 2ea2aa3ebd..bcffa5c321 100644 --- a/pkg/txmgr/builder.go +++ b/pkg/txmgr/builder.go @@ -141,7 +141,8 @@ func NewTxmV2( stuckTxDetector = txm.NewStuckTxDetector(lggr, chainConfig.ChainType(), stuckTxDetectorConfig) } - attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore, gasEstimatorConfig.LimitTransfer()) + feeBoost := txmV2Config.DualBroadcast() != nil && *txmV2Config.DualBroadcast() && txmV2Config.FeeBoost() + attemptBuilder := txm.NewAttemptBuilder(fCfg.PriceMaxKey, estimator, keyStore, gasEstimatorConfig.LimitTransfer(), feeBoost) inMemoryStoreManager := storage.NewInMemoryStoreManager(lggr, chainID) readRequestsToMultipleNodes := false if txmV2Config.ReadRequestsToMultipleNodes() != nil && *txmV2Config.ReadRequestsToMultipleNodes() {