Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

txmgr: Add min basefee and tip cap parameters to enforce fee minima #8799

Merged
merged 2 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions op-service/eth/ether.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package eth

import (
"errors"
"fmt"
"math"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/params"
)

func GweiToWei(gwei float64) (*big.Int, error) {
if math.IsNaN(gwei) || math.IsInf(gwei, 0) {
return nil, fmt.Errorf("invalid gwei value: %v", gwei)
}

// convert float GWei value into integer Wei value
wei, _ := new(big.Float).Mul(
big.NewFloat(gwei),
big.NewFloat(params.GWei)).
Int(nil)

if wei.Cmp(abi.MaxUint256) == 1 {
return nil, errors.New("gwei value larger than max uint256")
}

return wei, nil
}
71 changes: 71 additions & 0 deletions op-service/eth/ether_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package eth

import (
"math"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/stretchr/testify/require"
)

func TestGweiToWei(t *testing.T) {
maxUint256p1, _ := new(big.Int).Add(abi.MaxUint256, big.NewInt(1)).Float64()
for _, tt := range []struct {
desc string
gwei float64
wei *big.Int
err bool
}{
{
desc: "zero",
gwei: 0,
wei: new(big.Int),
},
{
desc: "one-wei",
gwei: 0.000000001,
wei: big.NewInt(1),
},
{
desc: "one-gwei",
gwei: 1.0,
wei: big.NewInt(1e9),
},
{
desc: "one-ether",
gwei: 1e9,
wei: big.NewInt(1e18),
},
{
desc: "err-pos-inf",
gwei: math.Inf(1),
err: true,
},
{
desc: "err-neg-inf",
gwei: math.Inf(-1),
err: true,
},
{
desc: "err-nan",
gwei: math.NaN(),
err: true,
},
{
desc: "err-too-large",
gwei: maxUint256p1,
err: true,
},
} {
t.Run(tt.desc, func(t *testing.T) {
wei, err := GweiToWei(tt.gwei)
if tt.err {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.wei, wei)
}
})
}
}
54 changes: 45 additions & 9 deletions op-service/txmgr/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import (
"context"
"errors"
"fmt"
"math"
"math/big"
"time"

opservice "github.com/ethereum-optimism/optimism/op-service"
opcrypto "github.com/ethereum-optimism/optimism/op-service/crypto"
"github.com/ethereum-optimism/optimism/op-service/eth"
opsigner "github.com/ethereum-optimism/optimism/op-service/signer"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/urfave/cli/v2"
)

Expand All @@ -30,6 +29,8 @@ const (
SafeAbortNonceTooLowCountFlagName = "safe-abort-nonce-too-low-count"
FeeLimitMultiplierFlagName = "fee-limit-multiplier"
FeeLimitThresholdFlagName = "txmgr.fee-limit-threshold"
MinBasefeeFlagName = "txmgr.min-basefee"
MinTipCapFlagName = "txmgr.min-tip-cap"
ResubmissionTimeoutFlagName = "resubmission-timeout"
NetworkTimeoutFlagName = "network-timeout"
TxSendTimeoutFlagName = "txmgr.send-timeout"
Expand Down Expand Up @@ -137,6 +138,16 @@ func CLIFlagsWithDefaults(envPrefix string, defaults DefaultFlagValues) []cli.Fl
Value: defaults.FeeLimitThresholdGwei,
EnvVars: prefixEnvVars("TXMGR_FEE_LIMIT_THRESHOLD"),
},
&cli.Float64Flag{
Name: MinBasefeeFlagName,
Usage: "Enforces a minimum basefee (in GWei) to assume when determining tx fees. Off by default.",
EnvVars: prefixEnvVars("TXMGR_MIN_BASEFEE"),
},
&cli.Float64Flag{
Name: MinTipCapFlagName,
Usage: "Enforces a minimum tip cap (in GWei) to use when determining tx fees. Off by default.",
EnvVars: prefixEnvVars("TXMGR_MIN_TIP_CAP"),
},
&cli.DurationFlag{
Name: ResubmissionTimeoutFlagName,
Usage: "Duration we will wait before resubmitting a transaction to L1",
Expand Down Expand Up @@ -182,6 +193,8 @@ type CLIConfig struct {
SafeAbortNonceTooLowCount uint64
FeeLimitMultiplier uint64
FeeLimitThresholdGwei float64
MinBasefeeGwei float64
MinTipCapGwei float64
ResubmissionTimeout time.Duration
ReceiptQueryInterval time.Duration
NetworkTimeout time.Duration
Expand Down Expand Up @@ -218,6 +231,10 @@ func (m CLIConfig) Check() error {
if m.FeeLimitMultiplier == 0 {
return errors.New("must provide FeeLimitMultiplier")
}
if m.MinBasefeeGwei < m.MinTipCapGwei {
return fmt.Errorf("minBasefee smaller than minTipCap, have %f < %f",
m.MinBasefeeGwei, m.MinTipCapGwei)
}
if m.ResubmissionTimeout == 0 {
return errors.New("must provide ResubmissionTimeout")
}
Expand Down Expand Up @@ -249,6 +266,8 @@ func ReadCLIConfig(ctx *cli.Context) CLIConfig {
SafeAbortNonceTooLowCount: ctx.Uint64(SafeAbortNonceTooLowCountFlagName),
FeeLimitMultiplier: ctx.Uint64(FeeLimitMultiplierFlagName),
FeeLimitThresholdGwei: ctx.Float64(FeeLimitThresholdFlagName),
MinBasefeeGwei: ctx.Float64(MinBasefeeFlagName),
MinTipCapGwei: ctx.Float64(MinTipCapFlagName),
ResubmissionTimeout: ctx.Duration(ResubmissionTimeoutFlagName),
ReceiptQueryInterval: ctx.Duration(ReceiptQueryIntervalFlagName),
NetworkTimeout: ctx.Duration(NetworkTimeoutFlagName),
Expand Down Expand Up @@ -289,21 +308,28 @@ func NewConfig(cfg CLIConfig, l log.Logger) (Config, error) {
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)
feeLimitThreshold, err := eth.GweiToWei(cfg.FeeLimitThresholdGwei)
if err != nil {
return Config{}, fmt.Errorf("invalid fee limit threshold: %w", err)
}

minBasefee, err := eth.GweiToWei(cfg.MinBasefeeGwei)
if err != nil {
return Config{}, fmt.Errorf("invalid min basefee: %w", err)
}

// convert float GWei value into integer Wei value
feeLimitThreshold, _ := new(big.Float).Mul(
big.NewFloat(cfg.FeeLimitThresholdGwei),
big.NewFloat(params.GWei)).
Int(nil)
minTipCap, err := eth.GweiToWei(cfg.MinTipCapGwei)
if err != nil {
return Config{}, fmt.Errorf("invalid min tip cap: %w", err)
}

return Config{
Backend: l1,
ResubmissionTimeout: cfg.ResubmissionTimeout,
FeeLimitMultiplier: cfg.FeeLimitMultiplier,
FeeLimitThreshold: feeLimitThreshold,
MinBasefee: minBasefee,
MinTipCap: minTipCap,
ChainID: chainID,
TxSendTimeout: cfg.TxSendTimeout,
TxNotInMempoolTimeout: cfg.TxNotInMempoolTimeout,
Expand Down Expand Up @@ -333,6 +359,12 @@ type Config struct {
// below this threshold.
FeeLimitThreshold *big.Int

// Minimum basefee (in Wei) to assume when determining tx fees.
MinBasefee *big.Int

// Minimum tip cap (in Wei) to enforce when determining tx fees.
MinTipCap *big.Int

// ChainID is the chain ID of the L1 chain.
ChainID *big.Int

Expand Down Expand Up @@ -380,6 +412,10 @@ func (m Config) Check() error {
if m.FeeLimitMultiplier == 0 {
return errors.New("must provide FeeLimitMultiplier")
}
if m.MinBasefee != nil && m.MinTipCap != nil && m.MinBasefee.Cmp(m.MinTipCap) == -1 {
return fmt.Errorf("minBasefee smaller than minTipCap, have %v < %v",
m.MinBasefee, m.MinTipCap)
}
if m.ResubmissionTimeout == 0 {
return errors.New("must provide ResubmissionTimeout")
}
Expand Down
16 changes: 14 additions & 2 deletions op-service/txmgr/txmgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,9 +594,21 @@ func (m *SimpleTxManager) suggestGasPriceCaps(ctx context.Context) (*big.Int, *b
} else if head.BaseFee == nil {
return nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a basefee")
}
m.metr.RecordBasefee(head.BaseFee)
basefee := head.BaseFee
m.metr.RecordBasefee(basefee)
m.metr.RecordTipCap(tip)
return tip, head.BaseFee, nil

// Enforce minimum basefee and tip cap
if minTipCap := m.cfg.MinTipCap; minTipCap != nil && tip.Cmp(minTipCap) == -1 {
m.l.Debug("Enforcing min tip cap", "minTipCap", m.cfg.MinTipCap, "origTipCap", tip)
tip = new(big.Int).Set(m.cfg.MinTipCap)
}
if minBasefee := m.cfg.MinBasefee; minBasefee != nil && basefee.Cmp(minBasefee) == -1 {
m.l.Debug("Enforcing min basefee", "minBasefee", m.cfg.MinBasefee, "origBasefee", basefee)
basefee = new(big.Int).Set(m.cfg.MinBasefee)
}

return tip, basefee, nil
}

func (m *SimpleTxManager) checkLimits(tip, basefee, bumpedTip, bumpedFee *big.Int) error {
Expand Down
71 changes: 69 additions & 2 deletions op-service/txmgr/txmgr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -871,15 +871,15 @@ func TestIncreaseGasPrice(t *testing.T) {
},
},
{
name: "enforces min bump on only tip incrase",
name: "enforces min bump on only tip increase",
run: func(t *testing.T) {
tx, newTx := doGasPriceIncrease(t, 100, 1000, 101, 440)
require.True(t, newTx.GasFeeCap().Cmp(tx.GasFeeCap()) > 0, "new tx fee cap must be larger")
require.True(t, newTx.GasTipCap().Cmp(tx.GasTipCap()) > 0, "new tx tip must be larger")
},
},
{
name: "enforces min bump on only basefee incrase",
name: "enforces min bump on only basefee increase",
run: func(t *testing.T) {
tx, newTx := doGasPriceIncrease(t, 100, 1000, 99, 460)
require.True(t, newTx.GasFeeCap().Cmp(tx.GasFeeCap()) > 0, "new tx fee cap must be larger")
Expand Down Expand Up @@ -1053,3 +1053,70 @@ func TestNonceReset(t *testing.T) {
// internal nonce tracking should be reset every 3rd tx
require.Equal(t, []uint64{0, 0, 1, 2, 0, 1, 2, 0}, nonces)
}

func TestMinFees(t *testing.T) {
for _, tt := range []struct {
desc string
minBasefee *big.Int
minTipCap *big.Int
expectMinBasefee bool
expectMinTipCap bool
}{
{
desc: "no-mins",
},
{
desc: "high-min-basefee",
minBasefee: big.NewInt(10_000_000),
expectMinBasefee: true,
},
{
desc: "high-min-tipcap",
minTipCap: big.NewInt(1_000_000),
expectMinTipCap: true,
},
{
desc: "high-mins",
minBasefee: big.NewInt(10_000_000),
minTipCap: big.NewInt(1_000_000),
expectMinBasefee: true,
expectMinTipCap: true,
},
{
desc: "low-min-basefee",
minBasefee: big.NewInt(1),
},
{
desc: "low-min-tipcap",
minTipCap: big.NewInt(1),
},
{
desc: "low-mins",
minBasefee: big.NewInt(1),
minTipCap: big.NewInt(1),
},
} {
t.Run(tt.desc, func(t *testing.T) {
require := require.New(t)
conf := configWithNumConfs(1)
conf.MinBasefee = tt.minBasefee
conf.MinTipCap = tt.minTipCap
h := newTestHarnessWithConfig(t, conf)

tip, basefee, err := h.mgr.suggestGasPriceCaps(context.TODO())
require.NoError(err)

if tt.expectMinBasefee {
require.Equal(tt.minBasefee, basefee, "expect suggested basefee to equal MinBasefee")
} else {
require.Equal(h.gasPricer.baseBaseFee, basefee, "expect suggested basefee to equal mock basefee")
}

if tt.expectMinTipCap {
require.Equal(tt.minTipCap, tip, "expect suggested tip to equal MinTipCap")
} else {
require.Equal(h.gasPricer.baseGasTipFee, tip, "expect suggested tip to equal mock tip")
}
})
}
}