From 197b0a3dd85a7c4ab1dd46d3d29a3fbceb3b3d0e Mon Sep 17 00:00:00 2001 From: Morty Date: Mon, 8 Sep 2025 19:56:28 +0800 Subject: [PATCH 1/5] feat(gas-oracle): support moving average base fee --- rollup/conf/config.json | 3 +- rollup/internal/config/relayer.go | 3 ++ .../internal/controller/relayer/l1_relayer.go | 39 ++++++++++++------ rollup/internal/orm/l1_block.go | 40 +++++++++++++++++++ 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/rollup/conf/config.json b/rollup/conf/config.json index f6a634eda9..566fd2cefe 100644 --- a/rollup/conf/config.json +++ b/rollup/conf/config.json @@ -21,7 +21,8 @@ "check_committed_batches_window_minutes": 5, "l1_base_fee_default": 15000000000, "l1_blob_base_fee_default": 1, - "l1_blob_base_fee_threshold": 0 + "l1_blob_base_fee_threshold": 0, + "calculate_average_fees_window_size": 100 }, "gas_oracle_sender_signer_config": { "signer_type": "PrivateKey", diff --git a/rollup/internal/config/relayer.go b/rollup/internal/config/relayer.go index db2e274681..9f277fa903 100644 --- a/rollup/internal/config/relayer.go +++ b/rollup/internal/config/relayer.go @@ -103,6 +103,9 @@ type GasOracleConfig struct { // L1BlobBaseFeeThreshold the threshold of L1 blob base fee to enter the default gas price mode L1BlobBaseFeeThreshold uint64 `json:"l1_blob_base_fee_threshold"` + + // CalculateAverageFeesWindowSize the number of blocks used for average fee calculation + CalculateAverageFeesWindowSize int `json:"calculate_average_fees_window_size"` } // SignerConfig - config of signer, contains type and config corresponding to type diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index d9f0835d52..5bcc4b1e6a 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -105,23 +105,20 @@ func NewLayer1Relayer(ctx context.Context, db *gorm.DB, cfg *config.RelayerConfi // ProcessGasPriceOracle imports gas price to layer2 func (r *Layer1Relayer) ProcessGasPriceOracle() { r.metrics.rollupL1RelayerGasPriceOraclerRunTotal.Inc() - latestBlockHeight, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx) - if err != nil { - log.Warn("Failed to fetch latest L1 block height from db", "err", err) - return - } - blocks, err := r.l1BlockOrm.GetL1Blocks(r.ctx, map[string]interface{}{ - "number": latestBlockHeight, - }) + limit := r.cfg.GasOracleConfig.CalculateAverageFeesWindowSize + blocks, err := r.l1BlockOrm.GetLatestL1Blocks(r.ctx, limit) if err != nil { - log.Error("Failed to GetL1Blocks from db", "height", latestBlockHeight, "err", err) + log.Error("Failed to GetLatestL1Blocks from db", "limit", limit, "err", err) return } - if len(blocks) != 1 { - log.Error("Block not exist", "height", latestBlockHeight) + + // nothing to do if we don't have any l1 blocks + if len(blocks) == 0 { + log.Info("No l1 blocks to process", "limit", limit) return } + block := blocks[0] if types.GasOracleStatus(block.GasOracleStatus) == types.GasOraclePending { @@ -130,8 +127,8 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { return } - baseFee := block.BaseFee - blobBaseFee := block.BlobBaseFee + // calculate the average base fee and blob base fee of the last N blocks + baseFee, blobBaseFee := r.calculateAverageFees(blocks) // include the token exchange rate in the fee data if alternative gas token enabled if r.cfg.GasOracleConfig.AlternativeGasTokenConfig != nil && r.cfg.GasOracleConfig.AlternativeGasTokenConfig.Enabled { @@ -287,3 +284,19 @@ func (r *Layer1Relayer) commitBatchReachTimeout() (bool, error) { // Because batches[0].CommittedAt is nil in this case, this will only continue for a short time window. return len(batches) == 0 || (batches[0].Index != 0 && batches[0].CommittedAt != nil && utils.NowUTC().Sub(*batches[0].CommittedAt) > time.Duration(r.cfg.GasOracleConfig.CheckCommittedBatchesWindowMinutes)*time.Minute), nil } + +// calculateAverageFees returns the average base fee and blob base fee. +func (r *Layer1Relayer) calculateAverageFees(blocks []orm.L1Block) (avgBaseFee uint64, avgBlobBaseFee uint64) { + if len(blocks) == 0 { + return 0, 0 + } + + var totalBaseFee, totalBlobBaseFee uint64 + for _, b := range blocks { + totalBaseFee += b.BaseFee + totalBlobBaseFee += b.BlobBaseFee + } + + count := uint64(len(blocks)) + return totalBaseFee / count, totalBlobBaseFee / count +} diff --git a/rollup/internal/orm/l1_block.go b/rollup/internal/orm/l1_block.go index 5d47dc5e04..7a21c54c77 100644 --- a/rollup/internal/orm/l1_block.go +++ b/rollup/internal/orm/l1_block.go @@ -71,6 +71,46 @@ func (o *L1Block) GetL1Blocks(ctx context.Context, fields map[string]interface{} return l1Blocks, nil } +// GetLatestL1Blocks get the latest N l1 blocks ordered by block number descending +func (o *L1Block) GetLatestL1Blocks(ctx context.Context, limit int) ([]L1Block, error) { + db := o.db.WithContext(ctx) + db = db.Model(&L1Block{}) + db = db.Order("number DESC") + db = db.Limit(limit) + + var l1Blocks []L1Block + if err := db.Find(&l1Blocks).Error; err != nil { + return nil, fmt.Errorf("L1Block.GetLatestL1Blocks error: %w, limit: %d", err, limit) + } + return l1Blocks, nil +} + +// GetAverageFees returns the average base_fee and blob_base_fee of all blocks +func (o *L1Block) GetAverageFees(ctx context.Context) (avgBaseFee float64, avgBlobBaseFee float64, err error) { + db := o.db.WithContext(ctx) + + var l1Blocks []L1Block + if err := db.Model(&L1Block{}).Find(&l1Blocks).Error; err != nil { + return 0, 0, fmt.Errorf("L1Block.GetAverageFees error: %w", err) + } + + if len(l1Blocks) == 0 { + return 0, 0, nil + } + + var totalBaseFee, totalBlobBaseFee uint64 + for _, block := range l1Blocks { + totalBaseFee += block.BaseFee + totalBlobBaseFee += block.BlobBaseFee + } + + count := float64(len(l1Blocks)) + avgBaseFee = float64(totalBaseFee) / count + avgBlobBaseFee = float64(totalBlobBaseFee) / count + + return avgBaseFee, avgBlobBaseFee, nil +} + // GetBlobFeesInRange returns all blob_base_fee values for blocks // with number ∈ [startBlock..endBlock], ordered by block number ascending. func (o *L1Block) GetBlobFeesInRange(ctx context.Context, startBlock, endBlock uint64) ([]uint64, error) { From 277e71efec2ad541bc6d25e22f3a982b59aa106f Mon Sep 17 00:00:00 2001 From: yiweichi Date: Mon, 8 Sep 2025 12:00:53 +0000 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20auto=20version=20bump=E2=80=89[bot?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/version/version.go b/common/version/version.go index 13a7597d4b..df872f409f 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.5.45" +var tag = "v4.5.46" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { From 4d9fd5530ed65bb9ded1e428d4c0b3acd7b53c0e Mon Sep 17 00:00:00 2001 From: yiweichi Date: Wed, 10 Sep 2025 18:58:17 +0000 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20auto=20version=20bump=E2=80=89[bot?= =?UTF-8?q?]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/version/version.go b/common/version/version.go index df872f409f..592f969612 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -5,7 +5,7 @@ import ( "runtime/debug" ) -var tag = "v4.5.46" +var tag = "v4.5.47" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { From 500490206312123dd3bc19c8abd5c28371ee5084 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 11 Sep 2025 03:04:35 +0800 Subject: [PATCH 4/5] address comments --- .../internal/controller/relayer/l1_relayer.go | 50 +++++++++++++++---- rollup/internal/orm/l1_block.go | 26 ---------- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index 5bcc4b1e6a..f32aa1f33e 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -115,7 +115,7 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { // nothing to do if we don't have any l1 blocks if len(blocks) == 0 { - log.Info("No l1 blocks to process", "limit", limit) + log.Warn("No l1 blocks to process", "limit", limit) return } @@ -151,8 +151,24 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { log.Error("Invalid exchange rate", "exchangeRate", exchangeRate) return } - baseFee = uint64(math.Ceil(float64(baseFee) / exchangeRate)) - blobBaseFee = uint64(math.Ceil(float64(blobBaseFee) / exchangeRate)) + + // Check for overflow in exchange rate calculation + adjustedBaseFee := math.Ceil(float64(baseFee) / exchangeRate) + adjustedBlobBaseFee := math.Ceil(float64(blobBaseFee) / exchangeRate) + + if adjustedBaseFee > float64(^uint64(0)) { + log.Error("Base fee overflow after exchange rate adjustment", "originalBaseFee", baseFee, "exchangeRate", exchangeRate, "adjustedBaseFee", adjustedBaseFee) + baseFee = ^uint64(0) // Set to max uint64 + } else { + baseFee = uint64(adjustedBaseFee) + } + + if adjustedBlobBaseFee > float64(^uint64(0)) { + log.Error("Blob base fee overflow after exchange rate adjustment", "originalBlobBaseFee", blobBaseFee, "exchangeRate", exchangeRate, "adjustedBlobBaseFee", adjustedBlobBaseFee) + blobBaseFee = ^uint64(0) // Set to max uint64 + } else { + blobBaseFee = uint64(adjustedBlobBaseFee) + } } if r.shouldUpdateGasOracle(baseFee, blobBaseFee) { @@ -160,7 +176,7 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { // 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 && block.BlobBaseFee > r.cfg.GasOracleConfig.L1BlobBaseFeeThreshold && err == nil { + if reachTimeout, err = r.commitBatchReachTimeout(); reachTimeout && blobBaseFee > r.cfg.GasOracleConfig.L1BlobBaseFeeThreshold && err == nil { if r.lastBaseFee == r.cfg.GasOracleConfig.L1BaseFeeDefault && r.lastBlobBaseFee == r.cfg.GasOracleConfig.L1BlobBaseFeeDefault { return } @@ -172,13 +188,13 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { } data, err := r.l1GasOracleABI.Pack("setL1BaseFeeAndBlobBaseFee", new(big.Int).SetUint64(baseFee), new(big.Int).SetUint64(blobBaseFee)) if err != nil { - log.Error("Failed to pack setL1BaseFeeAndBlobBaseFee", "block.Hash", block.Hash, "block.Height", block.Number, "block.BaseFee", baseFee, "block.BlobBaseFee", blobBaseFee, "err", err) + log.Error("Failed to pack setL1BaseFeeAndBlobBaseFee", "block.Hash", block.Hash, "block.Height", block.Number, "baseFee", baseFee, "blobBaseFee", blobBaseFee, "err", err) return } - txHash, _, err := r.gasOracleSender.SendTransaction(block.Hash, &r.cfg.GasPriceOracleContractAddress, data, nil) + txHash, _, err := r.gasOracleSender.SendTransaction("updateL1GasOracle-"+block.Hash, &r.cfg.GasPriceOracleContractAddress, data, nil) if err != nil { - log.Error("Failed to send gas oracle update tx to layer2", "block.Hash", block.Hash, "block.Height", block.Number, "block.BaseFee", baseFee, "block.BlobBaseFee", blobBaseFee, "err", err) + log.Error("Failed to send gas oracle update tx to layer2", "block.Hash", block.Hash, "block.Height", block.Number, "baseFee", baseFee, "blobBaseFee", blobBaseFee, "err", err) return } @@ -287,16 +303,30 @@ func (r *Layer1Relayer) commitBatchReachTimeout() (bool, error) { // calculateAverageFees returns the average base fee and blob base fee. func (r *Layer1Relayer) calculateAverageFees(blocks []orm.L1Block) (avgBaseFee uint64, avgBlobBaseFee uint64) { - if len(blocks) == 0 { + count := uint64(len(blocks)) + if count == 0 { return 0, 0 } var totalBaseFee, totalBlobBaseFee uint64 - for _, b := range blocks { + for i, b := range blocks { + // Check for overflow before addition + if totalBaseFee > ^uint64(0)-b.BaseFee { + log.Error("Base fee overflow detected, using max uint64", "totalBaseFee", totalBaseFee, "blockBaseFee", b.BaseFee) + totalBaseFee = ^uint64(0) // Set to max uint64 + count = uint64(i + 1) // set the count to the index of the block that caused the overflow + break + } + if totalBlobBaseFee > ^uint64(0)-b.BlobBaseFee { + log.Error("Blob base fee overflow detected, using max uint64", "totalBlobBaseFee", totalBlobBaseFee, "blockBlobBaseFee", b.BlobBaseFee) + totalBlobBaseFee = ^uint64(0) // Set to max uint64 + count = uint64(i + 1) // set the count to the index of the block that caused the overflow + break + } + totalBaseFee += b.BaseFee totalBlobBaseFee += b.BlobBaseFee } - count := uint64(len(blocks)) return totalBaseFee / count, totalBlobBaseFee / count } diff --git a/rollup/internal/orm/l1_block.go b/rollup/internal/orm/l1_block.go index 7a21c54c77..75bf3f9419 100644 --- a/rollup/internal/orm/l1_block.go +++ b/rollup/internal/orm/l1_block.go @@ -85,32 +85,6 @@ func (o *L1Block) GetLatestL1Blocks(ctx context.Context, limit int) ([]L1Block, return l1Blocks, nil } -// GetAverageFees returns the average base_fee and blob_base_fee of all blocks -func (o *L1Block) GetAverageFees(ctx context.Context) (avgBaseFee float64, avgBlobBaseFee float64, err error) { - db := o.db.WithContext(ctx) - - var l1Blocks []L1Block - if err := db.Model(&L1Block{}).Find(&l1Blocks).Error; err != nil { - return 0, 0, fmt.Errorf("L1Block.GetAverageFees error: %w", err) - } - - if len(l1Blocks) == 0 { - return 0, 0, nil - } - - var totalBaseFee, totalBlobBaseFee uint64 - for _, block := range l1Blocks { - totalBaseFee += block.BaseFee - totalBlobBaseFee += block.BlobBaseFee - } - - count := float64(len(l1Blocks)) - avgBaseFee = float64(totalBaseFee) / count - avgBlobBaseFee = float64(totalBlobBaseFee) / count - - return avgBaseFee, avgBlobBaseFee, nil -} - // GetBlobFeesInRange returns all blob_base_fee values for blocks // with number ∈ [startBlock..endBlock], ordered by block number ascending. func (o *L1Block) GetBlobFeesInRange(ctx context.Context, startBlock, endBlock uint64) ([]uint64, error) { From 3dd9ab6c35ea49c6588bf6e2d6aaa785a6193e44 Mon Sep 17 00:00:00 2001 From: Morty Date: Thu, 11 Sep 2025 23:12:02 +0800 Subject: [PATCH 5/5] address comment --- .../internal/controller/relayer/l1_relayer.go | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/rollup/internal/controller/relayer/l1_relayer.go b/rollup/internal/controller/relayer/l1_relayer.go index f32aa1f33e..e45d947a82 100644 --- a/rollup/internal/controller/relayer/l1_relayer.go +++ b/rollup/internal/controller/relayer/l1_relayer.go @@ -192,7 +192,7 @@ func (r *Layer1Relayer) ProcessGasPriceOracle() { return } - txHash, _, err := r.gasOracleSender.SendTransaction("updateL1GasOracle-"+block.Hash, &r.cfg.GasPriceOracleContractAddress, data, nil) + txHash, _, err := r.gasOracleSender.SendTransaction(block.Hash, &r.cfg.GasPriceOracleContractAddress, data, nil) if err != nil { log.Error("Failed to send gas oracle update tx to layer2", "block.Hash", block.Hash, "block.Height", block.Number, "baseFee", baseFee, "blobBaseFee", blobBaseFee, "err", err) return @@ -302,31 +302,42 @@ func (r *Layer1Relayer) commitBatchReachTimeout() (bool, error) { } // calculateAverageFees returns the average base fee and blob base fee. +// Uses big.Int for intermediate calculations to avoid overflow. func (r *Layer1Relayer) calculateAverageFees(blocks []orm.L1Block) (avgBaseFee uint64, avgBlobBaseFee uint64) { - count := uint64(len(blocks)) - if count == 0 { + if len(blocks) == 0 { return 0, 0 } - var totalBaseFee, totalBlobBaseFee uint64 - for i, b := range blocks { - // Check for overflow before addition - if totalBaseFee > ^uint64(0)-b.BaseFee { - log.Error("Base fee overflow detected, using max uint64", "totalBaseFee", totalBaseFee, "blockBaseFee", b.BaseFee) - totalBaseFee = ^uint64(0) // Set to max uint64 - count = uint64(i + 1) // set the count to the index of the block that caused the overflow - break - } - if totalBlobBaseFee > ^uint64(0)-b.BlobBaseFee { - log.Error("Blob base fee overflow detected, using max uint64", "totalBlobBaseFee", totalBlobBaseFee, "blockBlobBaseFee", b.BlobBaseFee) - totalBlobBaseFee = ^uint64(0) // Set to max uint64 - count = uint64(i + 1) // set the count to the index of the block that caused the overflow - break - } + // Use big.Int to handle large sums without overflow + totalBaseFee := big.NewInt(0) + totalBlobBaseFee := big.NewInt(0) + count := big.NewInt(int64(len(blocks))) + + for _, b := range blocks { + totalBaseFee.Add(totalBaseFee, big.NewInt(0).SetUint64(b.BaseFee)) + totalBlobBaseFee.Add(totalBlobBaseFee, big.NewInt(0).SetUint64(b.BlobBaseFee)) + } + + // Calculate averages + avgBaseFeeBig := big.NewInt(0).Div(totalBaseFee, count) + avgBlobBaseFeeBig := big.NewInt(0).Div(totalBlobBaseFee, count) - totalBaseFee += b.BaseFee - totalBlobBaseFee += b.BlobBaseFee + // Check if results fit in uint64 + maxUint64 := big.NewInt(0).SetUint64(^uint64(0)) + + if avgBaseFeeBig.Cmp(maxUint64) > 0 { + log.Error("Average base fee exceeds uint64 max, capping at max value", "calculatedAvg", avgBaseFeeBig.String()) + avgBaseFee = ^uint64(0) + } else { + avgBaseFee = avgBaseFeeBig.Uint64() + } + + if avgBlobBaseFeeBig.Cmp(maxUint64) > 0 { + log.Error("Average blob base fee exceeds uint64 max, capping at max value", "calculatedAvg", avgBlobBaseFeeBig.String()) + avgBlobBaseFee = ^uint64(0) + } else { + avgBlobBaseFee = avgBlobBaseFeeBig.Uint64() } - return totalBaseFee / count, totalBlobBaseFee / count + return avgBaseFee, avgBlobBaseFee }