Commit 79e62bf5 authored by Sebastian Stammler's avatar Sebastian Stammler Committed by GitHub

op-service/txmgr: Add threshold to gas price increase limiter (#8699)

parent c04cefe0
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"math/big" "math/big"
"time" "time"
...@@ -13,6 +14,7 @@ import ( ...@@ -13,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
...@@ -27,6 +29,7 @@ const ( ...@@ -27,6 +29,7 @@ const (
NumConfirmationsFlagName = "num-confirmations" NumConfirmationsFlagName = "num-confirmations"
SafeAbortNonceTooLowCountFlagName = "safe-abort-nonce-too-low-count" SafeAbortNonceTooLowCountFlagName = "safe-abort-nonce-too-low-count"
FeeLimitMultiplierFlagName = "fee-limit-multiplier" FeeLimitMultiplierFlagName = "fee-limit-multiplier"
FeeLimitThresholdFlagName = "txmgr.fee-limit-threshold"
ResubmissionTimeoutFlagName = "resubmission-timeout" ResubmissionTimeoutFlagName = "resubmission-timeout"
NetworkTimeoutFlagName = "network-timeout" NetworkTimeoutFlagName = "network-timeout"
TxSendTimeoutFlagName = "txmgr.send-timeout" TxSendTimeoutFlagName = "txmgr.send-timeout"
...@@ -53,6 +56,7 @@ type DefaultFlagValues struct { ...@@ -53,6 +56,7 @@ type DefaultFlagValues struct {
NumConfirmations uint64 NumConfirmations uint64
SafeAbortNonceTooLowCount uint64 SafeAbortNonceTooLowCount uint64
FeeLimitMultiplier uint64 FeeLimitMultiplier uint64
FeeLimitThresholdGwei float64
ResubmissionTimeout time.Duration ResubmissionTimeout time.Duration
NetworkTimeout time.Duration NetworkTimeout time.Duration
TxSendTimeout time.Duration TxSendTimeout time.Duration
...@@ -65,6 +69,7 @@ var ( ...@@ -65,6 +69,7 @@ var (
NumConfirmations: uint64(10), NumConfirmations: uint64(10),
SafeAbortNonceTooLowCount: uint64(3), SafeAbortNonceTooLowCount: uint64(3),
FeeLimitMultiplier: uint64(5), FeeLimitMultiplier: uint64(5),
FeeLimitThresholdGwei: 100.0,
ResubmissionTimeout: 48 * time.Second, ResubmissionTimeout: 48 * time.Second,
NetworkTimeout: 10 * time.Second, NetworkTimeout: 10 * time.Second,
TxSendTimeout: 0 * time.Second, TxSendTimeout: 0 * time.Second,
...@@ -75,6 +80,7 @@ var ( ...@@ -75,6 +80,7 @@ var (
NumConfirmations: uint64(3), NumConfirmations: uint64(3),
SafeAbortNonceTooLowCount: uint64(3), SafeAbortNonceTooLowCount: uint64(3),
FeeLimitMultiplier: uint64(5), FeeLimitMultiplier: uint64(5),
FeeLimitThresholdGwei: 100.0,
ResubmissionTimeout: 24 * time.Second, ResubmissionTimeout: 24 * time.Second,
NetworkTimeout: 10 * time.Second, NetworkTimeout: 10 * time.Second,
TxSendTimeout: 2 * time.Minute, TxSendTimeout: 2 * time.Minute,
...@@ -125,6 +131,12 @@ func CLIFlagsWithDefaults(envPrefix string, defaults DefaultFlagValues) []cli.Fl ...@@ -125,6 +131,12 @@ func CLIFlagsWithDefaults(envPrefix string, defaults DefaultFlagValues) []cli.Fl
Value: defaults.FeeLimitMultiplier, Value: defaults.FeeLimitMultiplier,
EnvVars: prefixEnvVars("TXMGR_FEE_LIMIT_MULTIPLIER"), EnvVars: prefixEnvVars("TXMGR_FEE_LIMIT_MULTIPLIER"),
}, },
&cli.Float64Flag{
Name: FeeLimitThresholdFlagName,
Usage: "The minimum threshold (in GWei) at which fee bumping starts to be capped. Allows arbitrary fee bumps below this threshold.",
Value: defaults.FeeLimitThresholdGwei,
EnvVars: prefixEnvVars("TXMGR_FEE_LIMIT_THRESHOLD"),
},
&cli.DurationFlag{ &cli.DurationFlag{
Name: ResubmissionTimeoutFlagName, Name: ResubmissionTimeoutFlagName,
Usage: "Duration we will wait before resubmitting a transaction to L1", Usage: "Duration we will wait before resubmitting a transaction to L1",
...@@ -169,6 +181,7 @@ type CLIConfig struct { ...@@ -169,6 +181,7 @@ type CLIConfig struct {
NumConfirmations uint64 NumConfirmations uint64
SafeAbortNonceTooLowCount uint64 SafeAbortNonceTooLowCount uint64
FeeLimitMultiplier uint64 FeeLimitMultiplier uint64
FeeLimitThresholdGwei float64
ResubmissionTimeout time.Duration ResubmissionTimeout time.Duration
ReceiptQueryInterval time.Duration ReceiptQueryInterval time.Duration
NetworkTimeout time.Duration NetworkTimeout time.Duration
...@@ -182,6 +195,7 @@ func NewCLIConfig(l1RPCURL string, defaults DefaultFlagValues) CLIConfig { ...@@ -182,6 +195,7 @@ func NewCLIConfig(l1RPCURL string, defaults DefaultFlagValues) CLIConfig {
NumConfirmations: defaults.NumConfirmations, NumConfirmations: defaults.NumConfirmations,
SafeAbortNonceTooLowCount: defaults.SafeAbortNonceTooLowCount, SafeAbortNonceTooLowCount: defaults.SafeAbortNonceTooLowCount,
FeeLimitMultiplier: defaults.FeeLimitMultiplier, FeeLimitMultiplier: defaults.FeeLimitMultiplier,
FeeLimitThresholdGwei: defaults.FeeLimitThresholdGwei,
ResubmissionTimeout: defaults.ResubmissionTimeout, ResubmissionTimeout: defaults.ResubmissionTimeout,
NetworkTimeout: defaults.NetworkTimeout, NetworkTimeout: defaults.NetworkTimeout,
TxSendTimeout: defaults.TxSendTimeout, TxSendTimeout: defaults.TxSendTimeout,
...@@ -234,6 +248,7 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig { ...@@ -234,6 +248,7 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig {
NumConfirmations: ctx.Uint64(NumConfirmationsFlagName), NumConfirmations: ctx.Uint64(NumConfirmationsFlagName),
SafeAbortNonceTooLowCount: ctx.Uint64(SafeAbortNonceTooLowCountFlagName), SafeAbortNonceTooLowCount: ctx.Uint64(SafeAbortNonceTooLowCountFlagName),
FeeLimitMultiplier: ctx.Uint64(FeeLimitMultiplierFlagName), FeeLimitMultiplier: ctx.Uint64(FeeLimitMultiplierFlagName),
FeeLimitThresholdGwei: ctx.Float64(FeeLimitThresholdFlagName),
ResubmissionTimeout: ctx.Duration(ResubmissionTimeoutFlagName), ResubmissionTimeout: ctx.Duration(ResubmissionTimeoutFlagName),
ReceiptQueryInterval: ctx.Duration(ReceiptQueryIntervalFlagName), ReceiptQueryInterval: ctx.Duration(ReceiptQueryIntervalFlagName),
NetworkTimeout: ctx.Duration(NetworkTimeoutFlagName), NetworkTimeout: ctx.Duration(NetworkTimeoutFlagName),
...@@ -274,10 +289,21 @@ func NewConfig(cfg CLIConfig, l log.Logger) (Config, error) { ...@@ -274,10 +289,21 @@ func NewConfig(cfg CLIConfig, l log.Logger) (Config, error) {
return Config{}, fmt.Errorf("could not init signer: %w", err) return Config{}, fmt.Errorf("could not init signer: %w", err)
} }
if thr := cfg.FeeLimitThresholdGwei; math.IsNaN(thr) || math.IsInf(thr, 0) {
return Config{}, fmt.Errorf("invalid fee limit threshold: %v", thr)
}
// convert float GWei value into integer Wei value
feeLimitThreshold, _ := new(big.Float).Mul(
big.NewFloat(cfg.FeeLimitThresholdGwei),
big.NewFloat(params.GWei)).
Int(nil)
return Config{ return Config{
Backend: l1, Backend: l1,
ResubmissionTimeout: cfg.ResubmissionTimeout, ResubmissionTimeout: cfg.ResubmissionTimeout,
FeeLimitMultiplier: cfg.FeeLimitMultiplier, FeeLimitMultiplier: cfg.FeeLimitMultiplier,
FeeLimitThreshold: feeLimitThreshold,
ChainID: chainID, ChainID: chainID,
TxSendTimeout: cfg.TxSendTimeout, TxSendTimeout: cfg.TxSendTimeout,
TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout, TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout,
...@@ -302,6 +328,11 @@ type Config struct { ...@@ -302,6 +328,11 @@ type Config struct {
// The multiplier applied to fee suggestions to put a hard limit on fee increases. // The multiplier applied to fee suggestions to put a hard limit on fee increases.
FeeLimitMultiplier uint64 FeeLimitMultiplier uint64
// Minimum threshold (in Wei) at which the FeeLimitMultiplier takes effect.
// On low-fee networks, like test networks, this allows for arbitrary fee bumps
// below this threshold.
FeeLimitThreshold *big.Int
// ChainID is the chain ID of the L1 chain. // ChainID is the chain ID of the L1 chain.
ChainID *big.Int ChainID *big.Int
......
...@@ -513,15 +513,10 @@ func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transa ...@@ -513,15 +513,10 @@ func (m *SimpleTxManager) increaseGasPrice(ctx context.Context, tx *types.Transa
} }
bumpedTip, bumpedFee := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, m.l) bumpedTip, bumpedFee := updateFees(tx.GasTipCap(), tx.GasFeeCap(), tip, basefee, m.l)
// Make sure increase is at most [FeeLimitMultiplier] the suggested values if err := m.checkLimits(tip, basefee, bumpedTip, bumpedFee); err != nil {
maxTip := new(big.Int).Mul(tip, big.NewInt(int64(m.cfg.FeeLimitMultiplier))) return nil, err
if bumpedTip.Cmp(maxTip) > 0 {
return nil, fmt.Errorf("bumped tip cap %v is over %dx multiple of the suggested value", bumpedTip, m.cfg.FeeLimitMultiplier)
}
maxFee := calcGasFeeCap(new(big.Int).Mul(basefee, big.NewInt(int64(m.cfg.FeeLimitMultiplier))), maxTip)
if bumpedFee.Cmp(maxFee) > 0 {
return nil, fmt.Errorf("bumped fee cap %v is over %dx multiple of the suggested value", bumpedFee, m.cfg.FeeLimitMultiplier)
} }
rawTx := &types.DynamicFeeTx{ rawTx := &types.DynamicFeeTx{
ChainID: tx.ChainId(), ChainID: tx.ChainId(),
Nonce: tx.Nonce(), Nonce: tx.Nonce(),
...@@ -589,6 +584,25 @@ func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *b ...@@ -589,6 +584,25 @@ func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *b
return tip, head.BaseFee, nil return tip, head.BaseFee, nil
} }
func (m *SimpleTxManager) checkLimits(tip, basefee, bumpedTip, bumpedFee *big.Int) error {
// If below threshold, don't apply multiplier limit
if thr := m.cfg.FeeLimitThreshold; thr != nil && thr.Cmp(bumpedFee) == 1 {
return nil
}
// Make sure increase is at most [FeeLimitMultiplier] the suggested values
feeLimitMult := big.NewInt(int64(m.cfg.FeeLimitMultiplier))
maxTip := new(big.Int).Mul(tip, feeLimitMult)
if bumpedTip.Cmp(maxTip) > 0 {
return fmt.Errorf("bumped tip cap %v is over %dx multiple of the suggested value", bumpedTip, m.cfg.FeeLimitMultiplier)
}
maxFee := calcGasFeeCap(new(big.Int).Mul(basefee, feeLimitMult), maxTip)
if bumpedFee.Cmp(maxFee) > 0 {
return fmt.Errorf("bumped fee cap %v is over %dx multiple of the suggested value", bumpedFee, m.cfg.FeeLimitMultiplier)
}
return nil
}
// calcThresholdValue returns x * priceBumpPercent / 100 // calcThresholdValue returns x * priceBumpPercent / 100
func calcThresholdValue(x *big.Int) *big.Int { func calcThresholdValue(x *big.Int) *big.Int {
threshold := new(big.Int).Mul(priceBumpPercent, x) threshold := new(big.Int).Mul(priceBumpPercent, x)
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"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/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
) )
type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error type sendTransactionFunc func(ctx context.Context, tx *types.Transaction) error
...@@ -246,7 +247,6 @@ func (*mockBackend) ChainID(ctx context.Context) (*big.Int, error) { ...@@ -246,7 +247,6 @@ func (*mockBackend) ChainID(ctx context.Context) (*big.Int, error) {
// receipt containing the txHash and the gasFeeCap used in the GasUsed to make // receipt containing the txHash and the gasFeeCap used in the GasUsed to make
// the value accessible from our test framework. // the value accessible from our test framework.
func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { func (b *mockBackend) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) {
b.mu.RLock() b.mu.RLock()
defer b.mu.RUnlock() defer b.mu.RUnlock()
...@@ -714,8 +714,8 @@ func (b *failingBackend) BlockNumber(ctx context.Context) (uint64, error) { ...@@ -714,8 +714,8 @@ func (b *failingBackend) BlockNumber(ctx context.Context) (uint64, error) {
// TransactionReceipt for the failingBackend returns errRpcFailure on the first // TransactionReceipt for the failingBackend returns errRpcFailure on the first
// invocation, and a receipt containing the passed TxHash on the second. // invocation, and a receipt containing the passed TxHash on the second.
func (b *failingBackend) TransactionReceipt( func (b *failingBackend) TransactionReceipt(
ctx context.Context, txHash common.Hash) (*types.Receipt, error) { ctx context.Context, txHash common.Hash,
) (*types.Receipt, error) {
if !b.returnSuccessReceipt { if !b.returnSuccessReceipt {
b.returnSuccessReceipt = true b.returnSuccessReceipt = true
return nil, errRpcFailure return nil, errRpcFailure
...@@ -894,9 +894,32 @@ func TestIncreaseGasPrice(t *testing.T) { ...@@ -894,9 +894,32 @@ func TestIncreaseGasPrice(t *testing.T) {
} }
} }
// TestIncreaseGasPriceNotExponential asserts that if the L1 basefee & tip remain the // TestIncreaseGasPriceLimits asserts that if the L1 basefee & tip remain the
// same, repeated calls to IncreaseGasPrice do not continually increase the gas price. // same, repeated calls to IncreaseGasPrice eventually hit a limit.
func TestIncreaseGasPriceNotExponential(t *testing.T) { func TestIncreaseGasPriceLimits(t *testing.T) {
t.Run("no-threshold", func(t *testing.T) {
testIncreaseGasPriceLimit(t, gasPriceLimitTest{
expTipCap: 36,
expFeeCap: 493, // just below 5*100
})
})
t.Run("with-threshold", func(t *testing.T) {
testIncreaseGasPriceLimit(t, gasPriceLimitTest{
thr: big.NewInt(params.GWei),
expTipCap: 61_265_017,
expFeeCap: 957_582_949, // just below 1 gwei
})
})
}
type gasPriceLimitTest struct {
thr *big.Int
expTipCap, expFeeCap int64
}
// testIncreaseGasPriceLimit runs a gas bumping test that increases the gas price until it hits an error.
// It starts with a tx that has a tip cap of 10 wei and fee cap of 100 wei.
func testIncreaseGasPriceLimit(t *testing.T, lt gasPriceLimitTest) {
t.Parallel() t.Parallel()
borkedTip := int64(10) borkedTip := int64(10)
...@@ -913,6 +936,7 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) { ...@@ -913,6 +936,7 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) {
NumConfirmations: 1, NumConfirmations: 1,
SafeAbortNonceTooLowCount: 3, SafeAbortNonceTooLowCount: 3,
FeeLimitMultiplier: 5, FeeLimitMultiplier: 5,
FeeLimitThreshold: lt.thr,
Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) { Signer: func(ctx context.Context, from common.Address, tx *types.Transaction) (*types.Transaction, error) {
return tx, nil return tx, nil
}, },
...@@ -937,10 +961,11 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) { ...@@ -937,10 +961,11 @@ func TestIncreaseGasPriceNotExponential(t *testing.T) {
} }
tx = newTx tx = newTx
} }
lastTip, lastFee := tx.GasTipCap(), tx.GasFeeCap() lastTip, lastFee := tx.GasTipCap(), tx.GasFeeCap()
require.Equal(t, lastTip.Int64(), int64(36)) // Confirm that fees only rose until expected threshold
require.Equal(t, lastFee.Int64(), int64(493)) require.Equal(t, lt.expTipCap, lastTip.Int64())
// Confirm that fees stop rising require.Equal(t, lt.expFeeCap, lastFee.Int64())
_, err := mgr.increaseGasPrice(ctx, tx) _, err := mgr.increaseGasPrice(ctx, tx)
require.Error(t, err) require.Error(t, err)
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment