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 { diff --git a/rollup/conf/config.json b/rollup/conf/config.json index 8055008cd6..a82ffaa577 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..e45d947a82 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.Warn("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 { @@ -154,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) { @@ -163,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 } @@ -175,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) 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,3 +300,44 @@ 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. +// Uses big.Int for intermediate calculations to avoid overflow. +func (r *Layer1Relayer) calculateAverageFees(blocks []orm.L1Block) (avgBaseFee uint64, avgBlobBaseFee uint64) { + if len(blocks) == 0 { + return 0, 0 + } + + // 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) + + // 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 avgBaseFee, avgBlobBaseFee +} diff --git a/rollup/internal/orm/l1_block.go b/rollup/internal/orm/l1_block.go index 5d47dc5e04..75bf3f9419 100644 --- a/rollup/internal/orm/l1_block.go +++ b/rollup/internal/orm/l1_block.go @@ -71,6 +71,20 @@ 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 +} + // 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) {