diff --git a/common/version/version.go b/common/version/version.go index 565adc20dd..ca5a810abf 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.4.44" +var tag = "v4.4.45" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { diff --git a/rollup/conf/config.json b/rollup/conf/config.json index e97522da16..457e42fe50 100644 --- a/rollup/conf/config.json +++ b/rollup/conf/config.json @@ -19,7 +19,10 @@ "min_gas_price": 0, "gas_price_diff": 50000, "l1_base_fee_weight": 0.132, - "l1_blob_base_fee_weight": 0.145 + "l1_blob_base_fee_weight": 0.145, + "check_committed_batches_window_minutes": 5, + "l1_base_fee_default": 15000000000, + "l1_blob_base_fee_default": 1 }, "gas_oracle_sender_private_key": "1313131313131313131313131313131313131313131313131313131313131313" } diff --git a/rollup/internal/config/relayer.go b/rollup/internal/config/relayer.go index be69d71314..da2a7562a4 100644 --- a/rollup/internal/config/relayer.go +++ b/rollup/internal/config/relayer.go @@ -84,6 +84,10 @@ type GasOracleConfig struct { L1BaseFeeWeight float64 `json:"l1_base_fee_weight"` // The weight for L1 blob base fee. L1BlobBaseFeeWeight float64 `json:"l1_blob_base_fee_weight"` + // CheckCommittedBatchesWindowMinutes the time frame to check if we committed batches to decide to update gas oracle or not in minutes + CheckCommittedBatchesWindowMinutes int `json:"check_committed_batches_window_minutes"` + L1BaseFeeDefault uint64 `json:"l1_base_fee_default"` + L1BlobBaseFeeDefault uint64 `json:"l1_blob_base_fee_default"` } // relayerConfigAlias RelayerConfig alias name diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index 6981dcda2f..6e98d28243 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "math/big" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/scroll-tech/go-ethereum/accounts/abi" @@ -15,6 +16,7 @@ import ( "gorm.io/gorm" "scroll-tech/common/types" + "scroll-tech/common/utils" bridgeAbi "scroll-tech/rollup/abi" "scroll-tech/rollup/internal/config" @@ -43,6 +45,7 @@ type Layer1Relayer struct { l1BlockOrm *orm.L1Block l2BlockOrm *orm.L2Block + batchOrm *orm.Batch metrics *l1RelayerMetrics } @@ -84,6 +87,7 @@ func NewLayer1Relayer(ctx context.Context, db *gorm.DB, cfg *config.RelayerConfi ctx: ctx, l1BlockOrm: orm.NewL1Block(db), l2BlockOrm: orm.NewL2Block(db), + batchOrm: orm.NewBatch(db), gasOracleSender: gasOracleSender, l1GasOracleABI: bridgeAbi.L1GasPriceOracleABI, @@ -150,6 +154,19 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { } if r.shouldUpdateGasOracle(baseFee, blobBaseFee, isCurie) { + // It indicates the committing batch has been stuck for a long time, it's likely that the L1 gas fee spiked. + // If we are not committing batches due to high fees then we shouldn't update fees to prevent users from paying high l1_data_fee + // Also, set fees to some default value, because we have already updated fees to some high values, probably + var reachTimeout bool + if reachTimeout, err = r.commitBatchReachTimeout(); reachTimeout && err == nil { + if r.lastBaseFee == r.cfg.GasOracleConfig.L1BaseFeeDefault && r.lastBlobBaseFee == r.cfg.GasOracleConfig.L1BlobBaseFeeDefault { + return + } + baseFee = r.cfg.GasOracleConfig.L1BaseFeeDefault + blobBaseFee = r.cfg.GasOracleConfig.L1BlobBaseFeeDefault + } else if err != nil { + return + } var data []byte if isCurie { data, err = r.l1GasOracleABI.Pack("setL1BaseFeeAndBlobBaseFee", new(big.Int).SetUint64(baseFee), new(big.Int).SetUint64(blobBaseFee)) @@ -260,3 +277,18 @@ func (r *Layer1Relayer) shouldUpdateGasOracle(baseFee uint64, blobBaseFee uint64 return false } + +func (r *Layer1Relayer) commitBatchReachTimeout() (bool, error) { + fields := map[string]interface{}{ + "rollup_status IN ?": []types.RollupStatus{types.RollupCommitted, types.RollupFinalizing, types.RollupFinalized}, + } + orderByList := []string{"index DESC"} + limit := 1 + batches, err := r.batchOrm.GetBatches(r.ctx, fields, orderByList, limit) + if err != nil { + log.Warn("failed to fetch latest committed, finalizing or finalized batch", "err", err) + return false, err + } + // len(batches) == 0 probably shouldn't ever happen, but need to check this + return len(batches) == 0 || utils.NowUTC().Sub(*batches[0].CommittedAt) > time.Duration(r.cfg.GasOracleConfig.CheckCommittedBatchesWindowMinutes)*time.Minute, nil +} diff --git a/rollup/tests/bridge_test.go b/rollup/tests/bridge_test.go index dc654f6cb0..8af6ac6b0f 100644 --- a/rollup/tests/bridge_test.go +++ b/rollup/tests/bridge_test.go @@ -210,5 +210,6 @@ func TestFunction(t *testing.T) { // l1/l2 gas oracle t.Run("TestImportL1GasPrice", testImportL1GasPrice) t.Run("TestImportL1GasPriceAfterCurie", testImportL1GasPriceAfterCurie) + t.Run("TestImportDefaultL1GasPriceDueToL1GasPriceSpike", testImportDefaultL1GasPriceDueToL1GasPriceSpike) t.Run("TestImportL2GasPrice", testImportL2GasPrice) } diff --git a/rollup/tests/gas_oracle_test.go b/rollup/tests/gas_oracle_test.go index 58e2362b3b..7496998c98 100644 --- a/rollup/tests/gas_oracle_test.go +++ b/rollup/tests/gas_oracle_test.go @@ -4,6 +4,7 @@ import ( "context" "math/big" "testing" + "time" "github.com/scroll-tech/da-codec/encoding" "github.com/scroll-tech/go-ethereum/common" @@ -56,6 +57,34 @@ func testImportL1GasPrice(t *testing.T) { assert.Empty(t, blocks[0].OracleTxHash) assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOraclePending) + // add fake batch to pass check for commit batch timeout + chunk := &encoding.Chunk{ + Blocks: []*encoding.Block{ + { + Header: &gethTypes.Header{ + Number: big.NewInt(1), + ParentHash: common.Hash{}, + Difficulty: big.NewInt(0), + BaseFee: big.NewInt(0), + }, + Transactions: nil, + WithdrawRoot: common.Hash{}, + RowConsumption: &gethTypes.RowConsumption{}, + }, + }, + } + batch := &encoding.Batch{ + Index: 0, + TotalL1MessagePoppedBefore: 0, + ParentBatchHash: common.Hash{}, + Chunks: []*encoding.Chunk{chunk}, + } + batchOrm := orm.NewBatch(db) + dbBatch, err := batchOrm.InsertBatch(context.Background(), batch, encoding.CodecV0, utils.BatchMetrics{}) + assert.NoError(t, err) + err = batchOrm.UpdateCommitTxHashAndRollupStatus(context.Background(), dbBatch.Hash, common.Hash{}.String(), types.RollupCommitted) + assert.NoError(t, err) + // relay gas price l1Relayer.ProcessGasPriceOracle() blocks, err = l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) @@ -101,13 +130,141 @@ func testImportL1GasPriceAfterCurie(t *testing.T) { assert.Empty(t, blocks[0].OracleTxHash) assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOraclePending) + // add fake batch to pass check for commit batch timeout + chunk := &encoding.Chunk{ + Blocks: []*encoding.Block{ + { + Header: &gethTypes.Header{ + Number: big.NewInt(1), + ParentHash: common.Hash{}, + Difficulty: big.NewInt(0), + BaseFee: big.NewInt(0), + }, + Transactions: nil, + WithdrawRoot: common.Hash{}, + RowConsumption: &gethTypes.RowConsumption{}, + }, + }, + } + batch := &encoding.Batch{ + Index: 0, + TotalL1MessagePoppedBefore: 0, + ParentBatchHash: common.Hash{}, + Chunks: []*encoding.Chunk{chunk}, + } + batchOrm := orm.NewBatch(db) + dbBatch, err := batchOrm.InsertBatch(context.Background(), batch, encoding.CodecV0, utils.BatchMetrics{}) + assert.NoError(t, err) + err = batchOrm.UpdateCommitTxHashAndRollupStatus(context.Background(), dbBatch.Hash, common.Hash{}.String(), types.RollupCommitted) + assert.NoError(t, err) + + // relay gas price + l1Relayer.ProcessGasPriceOracle() + blocks, err = l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) + assert.NoError(t, err) + assert.Equal(t, len(blocks), 1) + assert.NotEmpty(t, blocks[0].OracleTxHash) + assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOracleImporting) +} + +func testImportDefaultL1GasPriceDueToL1GasPriceSpike(t *testing.T) { + db := setupDB(t) + defer database.CloseDB(db) + + prepareContracts(t) + + l1Cfg := rollupApp.Config.L1Config + l1CfgCopy := *l1Cfg + // set CheckCommittedBatchesWindowMinutes to zero to not pass check for commit batch timeout + l1CfgCopy.RelayerConfig.GasOracleConfig.CheckCommittedBatchesWindowMinutes = 0 + // Create L1Relayer + l1Relayer, err := relayer.NewLayer1Relayer(context.Background(), db, l1CfgCopy.RelayerConfig, ¶ms.ChainConfig{BernoulliBlock: big.NewInt(0), CurieBlock: big.NewInt(0)}, relayer.ServiceTypeL1GasOracle, nil) + assert.NoError(t, err) + defer l1Relayer.StopSenders() + + // Create L1Watcher + startHeight, err := l1Client.BlockNumber(context.Background()) + assert.NoError(t, err) + l1Watcher := watcher.NewL1WatcherClient(context.Background(), l1Client, startHeight-2, db, nil) + + // fetch new blocks + number, err := l1Client.BlockNumber(context.Background()) + assert.Greater(t, number-1, startHeight-2) + assert.NoError(t, err) + err = l1Watcher.FetchBlockHeader(number - 1) + assert.NoError(t, err) + + l1BlockOrm := orm.NewL1Block(db) + // check db status + latestBlockHeight, err := l1BlockOrm.GetLatestL1BlockHeight(context.Background()) + assert.NoError(t, err) + assert.Equal(t, number-1, latestBlockHeight) + blocks, err := l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) + assert.NoError(t, err) + assert.Equal(t, len(blocks), 1) + assert.Empty(t, blocks[0].OracleTxHash) + assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOraclePending) + + // add fake batch + chunk := &encoding.Chunk{ + Blocks: []*encoding.Block{ + { + Header: &gethTypes.Header{ + Number: big.NewInt(1), + ParentHash: common.Hash{}, + Difficulty: big.NewInt(0), + BaseFee: big.NewInt(0), + }, + Transactions: nil, + WithdrawRoot: common.Hash{}, + RowConsumption: &gethTypes.RowConsumption{}, + }, + }, + } + batch := &encoding.Batch{ + Index: 0, + TotalL1MessagePoppedBefore: 0, + ParentBatchHash: common.Hash{}, + Chunks: []*encoding.Chunk{chunk}, + } + batchOrm := orm.NewBatch(db) + dbBatch, err := batchOrm.InsertBatch(context.Background(), batch, encoding.CodecV0, utils.BatchMetrics{}) + assert.NoError(t, err) + err = batchOrm.UpdateCommitTxHashAndRollupStatus(context.Background(), dbBatch.Hash, common.Hash{}.String(), types.RollupCommitted) + assert.NoError(t, err) + time.Sleep(1 * time.Second) + // relay gas price + // gas price will be relayed to some default value because we didn't commit batches for a l1CfgCopy.RelayerConfig.GasOracleConfig.CheckCommittedBatchesWindowMinutes = 0 minutes l1Relayer.ProcessGasPriceOracle() blocks, err = l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) assert.NoError(t, err) assert.Equal(t, len(blocks), 1) assert.NotEmpty(t, blocks[0].OracleTxHash) assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOracleImporting) + + // fetch new blocks + err = l1Watcher.FetchBlockHeader(number) + assert.NoError(t, err) + + // check db status + latestBlockHeight, err = l1BlockOrm.GetLatestL1BlockHeight(context.Background()) + assert.NoError(t, err) + assert.Equal(t, number, latestBlockHeight) + blocks, err = l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) + assert.NoError(t, err) + assert.Equal(t, len(blocks), 1) + assert.Empty(t, blocks[0].OracleTxHash) + assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOraclePending) + + // relay gas price + // gas price should not be relayed one more time because previously we already set it do default value + l1Relayer.ProcessGasPriceOracle() + blocks, err = l1BlockOrm.GetL1Blocks(context.Background(), map[string]interface{}{"number": latestBlockHeight}) + assert.NoError(t, err) + assert.Equal(t, len(blocks), 1) + assert.Empty(t, blocks[0].OracleTxHash) + assert.Equal(t, types.GasOracleStatus(blocks[0].GasOracleStatus), types.GasOraclePending) } func testImportL2GasPrice(t *testing.T) {