diff --git a/tx-submitter/entry.go b/tx-submitter/entry.go index 2687ed8e3..aa639ea9d 100644 --- a/tx-submitter/entry.go +++ b/tx-submitter/entry.go @@ -69,6 +69,10 @@ func Main() func(ctx *cli.Context) error { "rough_estimate_per_l1_msg", cfg.RollupTxGasPerL1Msg, "log_level", cfg.LogLevel, "leveldb_pathname", cfg.LeveldbPathName, + "min_tip", cfg.MinTip, + "max_tip", cfg.MaxTip, + "max_base", cfg.MaxBaseFee, + "tip_bump", cfg.TipFeeBump, ) ctx, cancel := context.WithCancel(context.Background()) diff --git a/tx-submitter/flags/flags.go b/tx-submitter/flags/flags.go index 041cf2db5..e7de38ff6 100644 --- a/tx-submitter/flags/flags.go +++ b/tx-submitter/flags/flags.go @@ -214,11 +214,29 @@ var ( Value: "StakingEventStore.json", } - CalldataFeeBumpFlag = cli.Uint64Flag{ - Name: "call_data_fee_bump", - Usage: "The fee bump for call data", - Value: 100, //fee = x * origin_fee/100 - EnvVar: prefixEnvVar("CALL_DATA_FEE_BUMP"), + TipFeeBumpFlag = cli.Uint64Flag{ + Name: "TIP_FEE_BUMP", + Usage: "The fee bump for tip", + Value: 120, //bumpTip = tip * TipFeeBump/100 + EnvVar: prefixEnvVar("TIP_FEE_BUMP"), + } + MaxTipFlag = cli.Uint64Flag{ + Name: "max_tip", + Usage: "The maximum tip for a transaction", + Value: 10e9, //10gwei + EnvVar: prefixEnvVar("MAX_TIP"), + } + MinTipFlag = cli.Uint64Flag{ + Name: "min_tip", + Usage: "The minimum tip for a transaction", + Value: 5e8, //0.5gwei + EnvVar: prefixEnvVar("MIN_TIP"), + } + MaxBaseFeeFlag = cli.Uint64Flag{ + Name: "max_base_fee", + Usage: "The maximum base fee for a transaction", + Value: 100e9, //100gwei + EnvVar: prefixEnvVar("MAX_BASE_FEE"), } MaxTxsInPendingPoolFlag = cli.Uint64Flag{ @@ -345,7 +363,10 @@ var optionalFlags = []cli.Flag{ PrivateKeyFlag, L2SequencerAddressFlag, L2GovAddressFlag, - CalldataFeeBumpFlag, + TipFeeBumpFlag, + MaxTipFlag, + MinTipFlag, + MaxBaseFeeFlag, MaxTxsInPendingPoolFlag, // external sign diff --git a/tx-submitter/mock/l1mock.go b/tx-submitter/mock/l1mock.go new file mode 100644 index 000000000..10dccef62 --- /dev/null +++ b/tx-submitter/mock/l1mock.go @@ -0,0 +1,87 @@ +package mock + +import ( + "context" + "math/big" + + "github.com/morph-l2/go-ethereum" + "github.com/morph-l2/go-ethereum/common" + "github.com/morph-l2/go-ethereum/core/types" +) + +type L1ClientWrapper struct { + Block *types.Block + TipCap *big.Int +} + +func NewL1ClientWrapper() *L1ClientWrapper { + return &L1ClientWrapper{} +} + +func (c *L1ClientWrapper) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + return nil, nil +} + +func (c *L1ClientWrapper) BalanceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (*big.Int, error) { + return big.NewInt(0), nil +} + +func (c *L1ClientWrapper) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) { + return nil, false, nil +} + +func (c *L1ClientWrapper) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { + return 0, nil +} + +func (c *L1ClientWrapper) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + return nil, nil +} + +func (c *L1ClientWrapper) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + return c.Block.Header(), nil +} + +func (c *L1ClientWrapper) BlockNumber(ctx context.Context) (uint64, error) { + return c.Block.NumberU64(), nil +} + +func (c *L1ClientWrapper) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + ch <- c.Block.Header() + return nil, nil +} + +func (c *L1ClientWrapper) SetBlock(block *types.Block) { + c.Block = block +} +func (c *L1ClientWrapper) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { + return nil, nil +} + +func (c *L1ClientWrapper) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { + return nil, nil +} +func (c *L1ClientWrapper) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { + return nil, nil +} +func (c *L1ClientWrapper) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { + return 0, nil +} +func (c *L1ClientWrapper) SuggestGasPrice(ctx context.Context) (*big.Int, error) { + return big.NewInt(0), nil +} +func (c *L1ClientWrapper) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + return c.TipCap, nil +} +func (c *L1ClientWrapper) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { + return 0, nil +} +func (c *L1ClientWrapper) SendTransaction(ctx context.Context, tx *types.Transaction) error { + return nil +} +func (c *L1ClientWrapper) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error) { + return nil, nil +} +func (c *L1ClientWrapper) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { + return nil, nil +} diff --git a/tx-submitter/services/rollup.go b/tx-submitter/services/rollup.go index d944b5bd4..995906ec8 100644 --- a/tx-submitter/services/rollup.go +++ b/tx-submitter/services/rollup.go @@ -849,14 +849,32 @@ func (r *Rollup) buildSignatureInput(batch *eth.RPCRollupBatch) (*bindings.IRoll } func (r *Rollup) GetGasTipAndCap() (*big.Int, *big.Int, *big.Int, error) { - tip, err := r.L1Client.SuggestGasTipCap(context.Background()) + + head, err := r.L1Client.HeaderByNumber(context.Background(), nil) if err != nil { return nil, nil, nil, err } - head, err := r.L1Client.HeaderByNumber(context.Background(), nil) + if head.BaseFee != nil { + log.Info("market fee info", "feecap", head.BaseFee) + if r.cfg.MaxBaseFee > 0 && head.BaseFee.Cmp(big.NewInt(int64(r.cfg.MaxBaseFee))) > 0 { + return nil, nil, nil, fmt.Errorf("base fee is too high, base fee %v exceeds max %v", head.BaseFee, r.cfg.MaxBaseFee) + } + } + + tip, err := r.L1Client.SuggestGasTipCap(context.Background()) if err != nil { return nil, nil, nil, err } + log.Info("market fee info", "tip", tip) + + if r.cfg.TipFeeBump > 0 { + tip = new(big.Int).Mul(tip, big.NewInt(int64(r.cfg.TipFeeBump))) + tip = new(big.Int).Div(tip, big.NewInt(100)) + } + if r.cfg.MaxTip > 0 && tip.Cmp(big.NewInt(int64(r.cfg.MaxTip))) > 0 { + return nil, nil, nil, fmt.Errorf("tip is too high, tip %v exceeds max %v", tip, r.cfg.MaxTip) + } + var gasFeeCap *big.Int if head.BaseFee != nil { gasFeeCap = new(big.Int).Add( @@ -873,15 +891,11 @@ func (r *Rollup) GetGasTipAndCap() (*big.Int, *big.Int, *big.Int, error) { blobFee = eip4844.CalcBlobFee(*head.ExcessBlobGas) } - //calldata fee bump x*fee/100 - if r.cfg.CalldataFeeBump > 0 { - // feecap - gasFeeCap = new(big.Int).Mul(gasFeeCap, big.NewInt(int64(r.cfg.CalldataFeeBump))) - gasFeeCap = new(big.Int).Div(gasFeeCap, big.NewInt(100)) - // tip - tip = new(big.Int).Mul(tip, big.NewInt(int64(r.cfg.CalldataFeeBump))) - tip = new(big.Int).Div(tip, big.NewInt(100)) - } + log.Info("fee info after bump", + "tip", tip, + "feecap", gasFeeCap, + "blobfee", blobFee, + ) return tip, gasFeeCap, blobFee, nil } @@ -1099,7 +1113,7 @@ func (r *Rollup) SendTx(tx *types.Transaction) error { return errors.New("nil tx") } // l1 health check - if !r.bm.IsGrowth() { + if r.bm != nil && !r.bm.IsGrowth() { return fmt.Errorf("block not growth in %d blocks time", r.cfg.BlockNotIncreasedThreshold) } @@ -1110,7 +1124,9 @@ func (r *Rollup) SendTx(tx *types.Transaction) error { // after send tx // add to pending txs - r.pendingTxs.Add(tx) + if r.pendingTxs != nil { + r.pendingTxs.Add(tx) + } return nil @@ -1183,6 +1199,11 @@ func (r *Rollup) ReSubmitTx(resend bool, tx *types.Transaction) (*types.Transact blobFeeCap = bumpedBlobFeeCap } } + + if r.cfg.MinTip > 0 && tip.Cmp(big.NewInt(int64(r.cfg.MinTip))) < 0 { + log.Info("replace tip is too low, update tip to min tip ", "tip", tip, "min_tip", r.cfg.MinTip) + tip = big.NewInt(int64(r.cfg.MinTip)) + } } var newTx *types.Transaction diff --git a/tx-submitter/services/rollup_test.go b/tx-submitter/services/rollup_test.go index 3476d8f8f..390ddaec6 100644 --- a/tx-submitter/services/rollup_test.go +++ b/tx-submitter/services/rollup_test.go @@ -1,15 +1,19 @@ package services import ( + "context" "math/big" "testing" - "github.com/holiman/uint256" + "morph-l2/tx-submitter/mock" + "morph-l2/tx-submitter/utils" + "github.com/morph-l2/go-ethereum/common" "github.com/morph-l2/go-ethereum/core/types" - "github.com/stretchr/testify/require" + "github.com/morph-l2/go-ethereum/crypto" - "morph-l2/tx-submitter/utils" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" ) func TestSendTx(t *testing.T) { @@ -49,3 +53,103 @@ func TestSendTx(t *testing.T) { err = sendTx(nil, 1, blobTx) require.ErrorContains(t, err, utils.ErrExceedFeeLimit.Error()) } + +func TestGetGasTipAndCap(t *testing.T) { + l1Mock := mock.NewL1ClientWrapper() + initTip := big.NewInt(1e9) + + baseFee := big.NewInt(1e9) + excessBlobGas := uint64(1) + block := types.NewBlockWithHeader( + &types.Header{ + BaseFee: baseFee, + ExcessBlobGas: &excessBlobGas, + }, + ) + l1Mock.TipCap = initTip + l1Mock.Block = block + config := utils.Config{ + MaxTip: 10e9, + MaxBaseFee: 100e9, + MinTip: 1e9, + TipFeeBump: 100, + } + r := NewRollup(context.Background(), nil, nil, l1Mock, nil, nil, nil, nil, nil, common.Address{}, nil, config, nil, nil, nil, nil) + tip, feecap, blobfee, err := r.GetGasTipAndCap() + require.NoError(t, err) + require.NotNil(t, tip) + require.NotNil(t, feecap) + require.NotNil(t, blobfee) + require.Equal(t, initTip, tip) + + config = utils.Config{ + MaxTip: 10e9, + MaxBaseFee: 100e9, + MinTip: 1e9, + TipFeeBump: 200, + } + r = NewRollup(context.Background(), nil, nil, l1Mock, nil, nil, nil, nil, nil, common.Address{}, nil, config, nil, nil, nil, nil) + tip, feecap, blobfee, err = r.GetGasTipAndCap() + require.NoError(t, err) + require.NotNil(t, tip) + require.NotNil(t, feecap) + require.NotNil(t, blobfee) + require.Equal(t, tip, initTip.Mul(initTip, big.NewInt(2))) + + config = utils.Config{ + MaxTip: 10e9, + MaxBaseFee: baseFee.Uint64() - 1, + MinTip: 1e9, + TipFeeBump: 200, + } + r = NewRollup(context.Background(), nil, nil, l1Mock, nil, nil, nil, nil, nil, common.Address{}, nil, config, nil, nil, nil, nil) + _, _, _, err = r.GetGasTipAndCap() + require.ErrorContains(t, err, "base fee is too high") + + config = utils.Config{ + MaxTip: initTip.Uint64() - 1, + MaxBaseFee: 100e9, + MinTip: 1e9, + TipFeeBump: 200, + } + r = NewRollup(context.Background(), nil, nil, l1Mock, nil, nil, nil, nil, nil, common.Address{}, nil, config, nil, nil, nil, nil) + _, _, _, err = r.GetGasTipAndCap() + require.ErrorContains(t, err, "tip is too high") + +} + +func TestReSubmitTx(t *testing.T) { + l1Mock := mock.NewL1ClientWrapper() + initTip := big.NewInt(1e9) + + baseFee := big.NewInt(1e9) + excessBlobGas := uint64(1) + block := types.NewBlockWithHeader( + &types.Header{ + BaseFee: baseFee, + ExcessBlobGas: &excessBlobGas, + }, + ) + l1Mock.TipCap = initTip + l1Mock.Block = block + config := utils.Config{ + MaxTip: 10e12, + MaxBaseFee: 100e9, + MinTip: 1e10, + TipFeeBump: 100, + } + + priv, err := crypto.GenerateKey() + require.NoError(t, err) + + r := NewRollup(context.Background(), nil, nil, l1Mock, nil, nil, nil, big.NewInt(1), priv, common.Address{}, nil, config, nil, nil, nil, nil) + _, err = r.ReSubmitTx(false, nil) + require.ErrorContains(t, err, "nil tx") + oldTx := types.NewTx(&types.DynamicFeeTx{ + GasTipCap: initTip, + }) + tx, err := r.ReSubmitTx(false, oldTx) + require.NoError(t, err) + require.EqualValues(t, config.MinTip, tx.GasTipCap().Uint64()) + +} diff --git a/tx-submitter/utils/config.go b/tx-submitter/utils/config.go index 30decb206..31bd9c9ea 100644 --- a/tx-submitter/utils/config.go +++ b/tx-submitter/utils/config.go @@ -82,8 +82,11 @@ type Config struct { // journal file path JournalFilePath string - // calldata fee bump - CalldataFeeBump uint64 + // tip bump + TipFeeBump uint64 + MaxTip uint64 + MinTip uint64 + MaxBaseFee uint64 //max txs in pendingpool MaxTxsInPendingPool uint64 @@ -151,9 +154,11 @@ func NewConfig(ctx *cli.Context) (Config, error) { GasLimitBuffer: ctx.GlobalUint64(flags.GasLimitBuffer.Name), - JournalFilePath: ctx.GlobalString(flags.JournalFlag.Name), - // calldata fee bump - CalldataFeeBump: ctx.GlobalUint64(flags.CalldataFeeBumpFlag.Name), + JournalFilePath: ctx.GlobalString(flags.JournalFlag.Name), + TipFeeBump: ctx.GlobalUint64(flags.TipFeeBumpFlag.Name), + MaxTip: ctx.GlobalUint64(flags.MaxTipFlag.Name), + MinTip: ctx.GlobalUint64(flags.MinTipFlag.Name), + MaxBaseFee: ctx.GlobalUint64(flags.MaxBaseFeeFlag.Name), MaxTxsInPendingPool: ctx.GlobalUint64(flags.MaxTxsInPendingPoolFlag.Name), // external sign