Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion rollup/conf/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions rollup/internal/config/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 72 additions & 18 deletions rollup/internal/controller/relayer/l1_relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the oldest or newest block?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the newest block.


if types.GasOracleStatus(block.GasOracleStatus) == types.GasOraclePending {
Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many places below still use block.

  • Threshold should be checked against blobBaseFee, not block.BlobBaseFee
  • Send tx should probably not use block.Hash for ID (risk of conflict with other txs)
  • Need to consider DB updates, including gas oracle import status

Copy link
Member Author

@yiweichi yiweichi Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send tx should probably not use block.Hash for ID (risk of conflict with other txs)

I think we can add a prefix updateL1GasOracle- before block.Hash. btw, just want to check this is an already existing risk, instead of introducing by this PR right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to consider DB updates, including gas oracle import status

I'm not sure what does that mean, I think we already update import status here and here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we update the status to GasOracleImporting and then to GasOracleImported, ensuring that we import the same L1 block info at most once.

If your intention that we keep exactly the same logic, but now instead of meaning "this block is being imported / has been imported", it will mean that "the avg price with the window up to this block is being imported / has been imported"?

In that case, my points 2 and 3 above can be ignored. But please double check that this will work as intended.

Copy link
Member Author

@yiweichi yiweichi Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will mean that "the avg price with the window up to this block is being imported / has been imported"?

Yes, that's my intention. We only care about if the newest block of the window has been imported/importing or not. If it has, we just skip it, and waiting for the next new l1 block.

I think this will work as intended.


// 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 {
Expand All @@ -154,16 +151,32 @@ 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) {
// 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 && 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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}
14 changes: 14 additions & 0 deletions rollup/internal/orm/l1_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down