From da79ba0262bc6ae84d170bb3cdf917c952214b09 Mon Sep 17 00:00:00 2001 From: ironbeer <7997273+ironbeer@users.noreply.github.com> Date: Wed, 24 Jul 2024 01:57:36 +0900 Subject: [PATCH] op-batcher: Add threshold for L1 blob base fee --- op-batcher/batcher/config.go | 6 +++ op-batcher/batcher/driver.go | 24 ++++++++++++ op-batcher/batcher/driver_test.go | 49 ++++++++++++++++++++++--- op-batcher/batcher/service.go | 2 + op-batcher/flags/flags.go | 9 +++++ op-service/dial/active_l2_provider.go | 12 ++++++ op-service/dial/gpo_interface.go | 13 +++++++ op-service/dial/static_l2_provider.go | 8 ++++ op-service/testutils/mock_gpo_client.go | 21 +++++++++++ 9 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 op-service/dial/gpo_interface.go create mode 100644 op-service/testutils/mock_gpo_client.go diff --git a/op-batcher/batcher/config.go b/op-batcher/batcher/config.go index 5180d8434b2f..40c0abed3925 100644 --- a/op-batcher/batcher/config.go +++ b/op-batcher/batcher/config.go @@ -52,6 +52,11 @@ type CLIConfig struct { // transactions sent to the transaction manager (0 == no limit). MaxPendingTransactions uint64 + // If L1 blob base fee exceeds this value, batch tx will not be sent. + // The unit is Wei, and if 0 is specified, there is no fee cap. + // The base fee may not be accurate as it is obtained from the GasPriceOracle on L2. + MaxL1BlobBaseFee uint64 + // MaxL1TxSize is the maximum size of a batch tx submitted to L1. // If using blobs, this setting is ignored and the max blob size is used. MaxL1TxSize uint64 @@ -171,6 +176,7 @@ func NewConfig(ctx *cli.Context) *CLIConfig { /* Optional Flags */ MaxPendingTransactions: ctx.Uint64(flags.MaxPendingTransactionsFlag.Name), MaxChannelDuration: ctx.Uint64(flags.MaxChannelDurationFlag.Name), + MaxL1BlobBaseFee: ctx.Uint64(flags.MaxL1BlobBaseFeeFlag.Name), MaxL1TxSize: ctx.Uint64(flags.MaxL1TxSizeBytesFlag.Name), TargetNumFrames: ctx.Int(flags.TargetNumFramesFlag.Name), ApproxComprRatio: ctx.Float64(flags.ApproxComprRatioFlag.Name), diff --git a/op-batcher/batcher/driver.go b/op-batcher/batcher/driver.go index 8784b20119d9..da5151f2b218 100644 --- a/op-batcher/batcher/driver.go +++ b/op-batcher/batcher/driver.go @@ -18,6 +18,7 @@ import ( "github.com/ethereum-optimism/optimism/op-service/dial" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/txmgr" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/txpool" @@ -487,6 +488,12 @@ func (l *BatchSubmitter) publishTxToL1(ctx context.Context, queue *txmgr.Queue[t } l.recordL1Tip(l1tip) + if l.Config.UseBlobs { + if err := l.checkL1BlobBaseFee(ctx); err != nil { + return err + } + } + // Collect next transaction data txdata, err := l.state.TxData(l1tip.ID()) @@ -620,6 +627,23 @@ func (l *BatchSubmitter) calldataTxCandidate(data []byte) *txmgr.TxCandidate { } } +func (l *BatchSubmitter) checkL1BlobBaseFee(ctx context.Context) error { + if l.Config.MaxL1BlobBaseFee > 0 { + gpo, err := l.EndpointProvider.GasPriceOracle(ctx) + if err != nil { + return fmt.Errorf("could not get GasPriceOracle: %w", err) + } + blobBaseFee, err := gpo.BlobBaseFee(&bind.CallOpts{Context: ctx}) + if err != nil { + return fmt.Errorf("could not get L1 Blob base fee from GasPriceOracle: %w", err) + } + if blobBaseFee.Cmp(new(big.Int).SetUint64(l.Config.MaxL1BlobBaseFee)) == 1 { + return fmt.Errorf("L1 Blob base fee exceeds threshold, blob_base_fee = %s", blobBaseFee) + } + } + return nil +} + func (l *BatchSubmitter) handleReceipt(r txmgr.TxReceipt[txRef]) { // Record TX Status if r.Err != nil { diff --git a/op-batcher/batcher/driver_test.go b/op-batcher/batcher/driver_test.go index df72fa28d49a..217edc6514f3 100644 --- a/op-batcher/batcher/driver_test.go +++ b/op-batcher/batcher/driver_test.go @@ -3,6 +3,7 @@ package batcher import ( "context" "errors" + "math/big" "testing" "github.com/ethereum-optimism/optimism/op-batcher/metrics" @@ -15,16 +16,19 @@ import ( ) type mockL2EndpointProvider struct { - ethClient *testutils.MockL2Client - ethClientErr error - rollupClient *testutils.MockRollupClient - rollupClientErr error + ethClient *testutils.MockL2Client + ethClientErr error + rollupClient *testutils.MockRollupClient + rollupClientErr error + gasPriceOracle *testutils.MockGasPriceOracle + gasPriceOracleErr error } func newEndpointProvider() *mockL2EndpointProvider { return &mockL2EndpointProvider{ - ethClient: new(testutils.MockL2Client), - rollupClient: new(testutils.MockRollupClient), + ethClient: new(testutils.MockL2Client), + rollupClient: new(testutils.MockRollupClient), + gasPriceOracle: new(testutils.MockGasPriceOracle), } } @@ -36,6 +40,10 @@ func (p *mockL2EndpointProvider) RollupClient(context.Context) (dial.RollupClien return p.rollupClient, p.rollupClientErr } +func (p *mockL2EndpointProvider) GasPriceOracle(context.Context) (dial.GasPriceOracleInterface, error) { + return p.gasPriceOracle, p.gasPriceOracleErr +} + func (p *mockL2EndpointProvider) Close() {} const genesisL1Origin = uint64(123) @@ -117,3 +125,32 @@ func TestBatchSubmitter_SafeL1Origin_FailsToResolveRollupClient(t *testing.T) { _, err := bs.safeL1Origin(context.Background()) require.Error(t, err) } + +func TestBatchSubmitter_CheckL1BlobBaseFee_NotSet(t *testing.T) { + bs, ep := setup(t) + + ep.gasPriceOracle.ExpectBlobBaseFee(big.NewInt(1), nil) + + err := bs.checkL1BlobBaseFee(context.Background()) + require.NoError(t, err) +} + +func TestBatchSubmitter_CheckL1BlobBaseFee_Succeeds(t *testing.T) { + bs, ep := setup(t) + + bs.Config.MaxL1BlobBaseFee = 1 + ep.gasPriceOracle.ExpectBlobBaseFee(big.NewInt(1), nil) + + err := bs.checkL1BlobBaseFee(context.Background()) + require.NoError(t, err) +} + +func TestBatchSubmitter_CheckL1BlobBaseFee_Fails(t *testing.T) { + bs, ep := setup(t) + + bs.Config.MaxL1BlobBaseFee = 1 + ep.gasPriceOracle.ExpectBlobBaseFee(big.NewInt(2), nil) + + err := bs.checkL1BlobBaseFee(context.Background()) + require.ErrorContains(t, err, "L1 Blob base fee exceeds threshold") +} diff --git a/op-batcher/batcher/service.go b/op-batcher/batcher/service.go index 71dd7fa4b6d1..d9d009c27429 100644 --- a/op-batcher/batcher/service.go +++ b/op-batcher/batcher/service.go @@ -44,6 +44,7 @@ type BatcherConfig struct { WaitNodeSync bool CheckRecentTxsDepth int + MaxL1BlobBaseFee uint64 } // BatcherService represents a full batch-submitter instance and its resources, @@ -100,6 +101,7 @@ func (bs *BatcherService) initFromCLIConfig(ctx context.Context, version string, bs.NetworkTimeout = cfg.TxMgrConfig.NetworkTimeout bs.CheckRecentTxsDepth = cfg.CheckRecentTxsDepth bs.WaitNodeSync = cfg.WaitNodeSync + bs.MaxL1BlobBaseFee = cfg.MaxL1BlobBaseFee if err := bs.initRPCClients(ctx, cfg); err != nil { return err } diff --git a/op-batcher/flags/flags.go b/op-batcher/flags/flags.go index 5aa833b75bf7..ac7473d7c8d7 100644 --- a/op-batcher/flags/flags.go +++ b/op-batcher/flags/flags.go @@ -70,6 +70,14 @@ var ( Value: 0, EnvVars: prefixEnvVars("MAX_CHANNEL_DURATION"), } + MaxL1BlobBaseFeeFlag = &cli.Uint64Flag{ + Name: "max-l1-blob-base-fee", + Usage: "If L1 blob base fee exceeds this value, batch tx will not be sent. " + + "The unit is Wei, and if 0 is specified, there is no fee cap. " + + "The base fee may not be accurate as it is obtained from the GasPriceOracle on L2.", + Value: 0, + EnvVars: prefixEnvVars("MAX_L1_BLOB_BASE_FEE"), + } MaxL1TxSizeBytesFlag = &cli.Uint64Flag{ Name: "max-l1-tx-size-bytes", Usage: "The maximum size of a batch tx submitted to L1. Ignored for blobs, where max blob size will be used.", @@ -168,6 +176,7 @@ var optionalFlags = []cli.Flag{ PollIntervalFlag, MaxPendingTransactionsFlag, MaxChannelDurationFlag, + MaxL1BlobBaseFeeFlag, MaxL1TxSizeBytesFlag, TargetNumFramesFlag, ApproxComprRatioFlag, diff --git a/op-service/dial/active_l2_provider.go b/op-service/dial/active_l2_provider.go index 7d20d2c129c6..9475eaa08ef9 100644 --- a/op-service/dial/active_l2_provider.go +++ b/op-service/dial/active_l2_provider.go @@ -6,7 +6,9 @@ import ( "fmt" "time" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum-optimism/optimism/op-service/sources" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" @@ -115,6 +117,16 @@ func (p *ActiveL2EndpointProvider) EthClient(ctx context.Context) (EthClientInte return p.currentEthClient, nil } +func (p *ActiveL2EndpointProvider) GasPriceOracle(ctx context.Context) (GasPriceOracleInterface, error) { + if ec, err := p.EthClient(ctx); err != nil { + return nil, err + } else if t, ok := ec.(*ethclient.Client); !ok { + return nil, errors.New("not ethclient.Client") + } else { + return bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, t) + } +} + func (p *ActiveL2EndpointProvider) Close() { if p.currentEthClient != nil { p.currentEthClient.Close() diff --git a/op-service/dial/gpo_interface.go b/op-service/dial/gpo_interface.go new file mode 100644 index 000000000000..b9ef6cf470f9 --- /dev/null +++ b/op-service/dial/gpo_interface.go @@ -0,0 +1,13 @@ +package dial + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" +) + +// GasPriceOracleInterface is an interface for providing +// an GasPriceOracle pre-deployment contract +type GasPriceOracleInterface interface { + BlobBaseFee(opts *bind.CallOpts) (*big.Int, error) +} diff --git a/op-service/dial/static_l2_provider.go b/op-service/dial/static_l2_provider.go index 5a3a5f7ef814..e989314cd4d9 100644 --- a/op-service/dial/static_l2_provider.go +++ b/op-service/dial/static_l2_provider.go @@ -3,6 +3,8 @@ package dial import ( "context" + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-service/predeploys" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/log" ) @@ -16,6 +18,8 @@ type L2EndpointProvider interface { // Note: ctx should be a lifecycle context without an attached timeout as client selection may involve // multiple network operations, specifically in the case of failover. EthClient(ctx context.Context) (EthClientInterface, error) + // GasPriceOracle(ctx) returns the callable GasPriceOracle contract on the L2 + GasPriceOracle(ctx context.Context) (GasPriceOracleInterface, error) } // StaticL2EndpointProvider is a L2EndpointProvider that always returns the same static RollupClient and eth client @@ -44,6 +48,10 @@ func (p *StaticL2EndpointProvider) EthClient(context.Context) (EthClientInterfac return p.ethClient, nil } +func (p *StaticL2EndpointProvider) GasPriceOracle(context.Context) (GasPriceOracleInterface, error) { + return bindings.NewGasPriceOracle(predeploys.GasPriceOracleAddr, p.ethClient) +} + func (p *StaticL2EndpointProvider) Close() { if p.ethClient != nil { p.ethClient.Close() diff --git a/op-service/testutils/mock_gpo_client.go b/op-service/testutils/mock_gpo_client.go new file mode 100644 index 000000000000..446cfcc840f0 --- /dev/null +++ b/op-service/testutils/mock_gpo_client.go @@ -0,0 +1,21 @@ +package testutils + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/mock" +) + +type MockGasPriceOracle struct { + mock.Mock +} + +func (m *MockGasPriceOracle) BlobBaseFee(opts *bind.CallOpts) (*big.Int, error) { + out := m.Mock.Called(opts) + return out.Get(0).(*big.Int), out.Error(1) +} + +func (m *MockGasPriceOracle) ExpectBlobBaseFee(blobBaseFee *big.Int, err error) { + m.Mock.On("BlobBaseFee", mock.Anything).Once().Return(blobBaseFee, err) +}