Repository: scroll-tech/scroll
Pull request #1659: perf(relayer): add sequencer submission strategy with blob‐fee history and target price
Author: login: ranchalp
Labels: name: bump-version
description: Bump the version tag for deployment
Created: 2025-05-14T16:17:38Z | Merged: 2025-05-19T20:32:28Z

### Purpose or design rationale of this PR

Implements a new sequencer submission strategy in the L2 relayer that decides when to publish batches based on recent blob‐fee conditions rather than immediately. It consists of:

- `fetchBlobFeeHistory(windowSec uint64)` – pulls the last `windowSec` seconds of blob‐fee samples from the `l1_block` table.

- `calculateTargetPrice(strat StrategyParams, firstTime time.Time, history []*big.Int)` – computes a dynamic “target” blob fee by applying either a percentile or EWMA baseline, then a relaxation curve (exponential or sigmoid) that increases toward the window deadline. Estimates per `windowSec` obtained from tests performed on traces.

- Integration into `ProcessPendingBatches` – before sending a commit transaction, the relayer compares the most recent blob fee to the target and only proceeds if the price is at or below the target, or if the configured time window has elapsed.

Resulting behavior: 

- Cost optimization: by delaying submission until blob fees are favorable, the protocol can reduce L1 data‐fee costs.

- Flexibility & safety: the relaxation curve ensures that batches aren’t starved indefinitely and will be submitted by the deadline even if prices remain high.

### Deployment tag versioning

Has `tag` in `common/version.go` been updated or have you added `bump-version` label to this PR?

- [ ] No, this PR doesn't involve a new deployment, git tag, docker image tag
- [x] Yes


### Breaking change label

Does this PR have the `breaking-change` label?

- [x] No, this PR is not a breaking change
- [ ] Yes


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a dynamic fee-submission strategy for batch processing, adapting submissions based on recent fee history and backlog size.
  - Added a method to retrieve counts of failed and pending batches to improve backlog monitoring.
  - Added querying of blob fee history over block ranges to support fee-based submission control.
- **Improvements**
  - Increased batch submission timeout from 5 minutes to 2 hours.
  - Added a configurable maximum backlog threshold to control batch submission flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/common/version/version.go b/common/version/version.go
index 1a52f93d65..56cd403002 100644
--- a/common/version/version.go
+++ b/common/version/version.go
@@ -5,7 +5,7 @@ import (
 	"runtime/debug"
 )
 
-var tag = "v4.5.10"
+var tag = "v4.5.11"
 
 var commit = func() string {
 	if info, ok := debug.ReadBuildInfo(); ok {
diff --git a/rollup/conf/config.json b/rollup/conf/config.json
index 941be1b006..8424f75f79 100644
--- a/rollup/conf/config.json
+++ b/rollup/conf/config.json
@@ -54,7 +54,8 @@
       "batch_submission": {
         "min_batches": 1,
         "max_batches": 6,
-        "timeout": 300
+        "timeout": 7200,
+        "backlog_max": 75
       },
       "gas_oracle_config": {
         "min_gas_price": 0,
diff --git a/rollup/internal/config/relayer.go b/rollup/internal/config/relayer.go
index a691076961..4ec853d1be 100644
--- a/rollup/internal/config/relayer.go
+++ b/rollup/internal/config/relayer.go
@@ -38,6 +38,8 @@ type BatchSubmission struct {
 	MaxBatches int `json:"max_batches"`
 	// The time in seconds after which a batch is considered stale and should be submitted ignoring the min batch count.
 	TimeoutSec int64 `json:"timeout"`
+	// The maximum number of pending batches to keep in the backlog.
+	BacklogMax int64 `json:"backlog_max"`
 }
 
 // ChainMonitor this config is used to get batch status from chain_monitor API.
diff --git a/rollup/internal/controller/relayer/l2_relayer.go b/rollup/internal/controller/relayer/l2_relayer.go
index 9accf38353..4680c97625 100644
--- a/rollup/internal/controller/relayer/l2_relayer.go
+++ b/rollup/internal/controller/relayer/l2_relayer.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math"
 	"math/big"
 	"sort"
 	"strings"
@@ -33,6 +34,32 @@ import (
 	rutils "scroll-tech/rollup/internal/utils"
 )
 
+// RelaxType enumerates the relaxation functions we support when
+// turning a baseline fee into a “target” fee.
+type RelaxType int
+
+const (
+	// NoRelaxation means “don’t touch the baseline” (i.e. fallback/default).
+	NoRelaxation RelaxType = iota
+	Exponential
+	Sigmoid
+)
+
+const secondsPerBlock = 12
+
+// BaselineType enumerates the baseline types we support when
+// turning a baseline fee into a “target” fee.
+type BaselineType int
+
+const (
+	// PctMin means “take the minimum of the last N blocks’ fees, then
+	// take the PCT of that”.
+	PctMin BaselineType = iota
+	// EWMA means “take the exponentially‐weighted moving average of
+	// the last N blocks’ fees”.
+	EWMA
+)
+
 // Layer2Relayer is responsible for:
 // i. committing and finalizing L2 blocks on L1.
 // ii. updating L2 gas price oracle contract on L1.
@@ -46,6 +73,7 @@ type Layer2Relayer struct {
 	batchOrm   *orm.Batch
 	chunkOrm   *orm.Chunk
 	l2BlockOrm *orm.L2Block
+	l1BlockOrm *orm.L1Block
 
 	cfg *config.RelayerConfig
 
@@ -61,6 +89,26 @@ type Layer2Relayer struct {
 	metrics *l2RelayerMetrics
 
 	chainCfg *params.ChainConfig
+
+	lastFetchedBlock uint64     // highest block number ever pulled
+	feeHistory       []*big.Int // sliding window of blob fees
+	batchStrategy    StrategyParams
+}
+
+// StrategyParams holds the per‐window fee‐submission rules.
+type StrategyParams struct {
+	BaselineType  BaselineType // "pct_min" or "ewma"
+	BaselineParam float64      // percentile (0–1) or α for EWMA
+	Gamma         float64      // relaxation γ
+	Beta          float64      // relaxation β
+	RelaxType     RelaxType    // Exponential or Sigmoid
+}
+
+// bestParams maps your 2h/5h/12h windows to their best rules.
+var bestParams = map[uint64]StrategyParams{
+	2 * 3600:  {BaselineType: PctMin, BaselineParam: 0.10, Gamma: 0.4, Beta: 8, RelaxType: Exponential},
+	5 * 3600:  {BaselineType: PctMin, BaselineParam: 0.30, Gamma: 0.6, Beta: 20, RelaxType: Sigmoid},
+	12 * 3600: {BaselineType: PctMin, BaselineParam: 0.50, Gamma: 0.5, Beta: 20, RelaxType: Sigmoid},
 }
 
 // NewLayer2Relayer will return a new instance of Layer2RelayerClient
@@ -106,6 +154,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 
 		bundleOrm:  orm.NewBundle(db),
 		batchOrm:   orm.NewBatch(db),
+		l1BlockOrm: orm.NewL1Block(db),
 		l2BlockOrm: orm.NewL2Block(db),
 		chunkOrm:   orm.NewChunk(db),
 
@@ -116,9 +165,9 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 		l1RollupABI:    bridgeAbi.ScrollChainABI,
 
 		l2GasOracleABI: bridgeAbi.L2GasPriceOracleABI,
-
-		cfg:      cfg,
-		chainCfg: chainCfg,
+		batchStrategy:  bestParams[uint64(cfg.BatchSubmission.TimeoutSec)],
+		cfg:            cfg,
+		chainCfg:       chainCfg,
 	}
 
 	// chain_monitor client
@@ -266,6 +315,10 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 }
 
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
+// Pending batchess are submitted if one of the following conditions is met:
+// - the first batch is too old -> forceSubmit
+// - backlogCount > r.cfg.BatchSubmission.BacklogMax -> forceSubmit
+// - we have at least minBatches AND price hits a desired target price
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
 	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
@@ -274,8 +327,40 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 		return
 	}
 
-	var batchesToSubmit []*dbBatchWithChunksAndParent
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
 	var forceSubmit bool
+
+	oldestBatchTimestamp := dbBatches[0].CreatedAt
+	// if the batch with the oldest index is too old, we force submit all batches that we have so far in the next step
+	if r.cfg.BatchSubmission.TimeoutSec > 0 && time.Since(oldestBatchTimestamp) > time.Duration(r.cfg.BatchSubmission.TimeoutSec)*time.Second {
+		forceSubmit = true
+	}
+
+	// force submit if backlog is too big
+	if backlogCount > r.cfg.BatchSubmission.BacklogMax {
+		forceSubmit = true
+	}
+
+	if !forceSubmit {
+		// check if we should skip submitting the batch based on the fee target
+		skip, err := r.skipSubmitByFee(oldestBatchTimestamp)
+		// return if not hitting target price
+		if skip {
+			log.Debug("Skipping batch submission", "reason", err)
+			return
+		}
+		if err != nil {
+			log.Warn("Failed to check if we should skip batch submission, fallback to immediate submission", "err", err)
+		}
+	}
+
+	var batchesToSubmit []*dbBatchWithChunksAndParent
 	for i, dbBatch := range dbBatches {
 		if i == 0 && encoding.CodecVersion(dbBatch.CodecVersion) < encoding.CodecV7 {
 			// if the first batch is not >= V7 then we need to submit batches one by one
@@ -336,11 +421,6 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 			break
 		}
 
-		// if one of the batches is too old, we force submit all batches that we have so far in the next step
-		if r.cfg.BatchSubmission.TimeoutSec > 0 && !forceSubmit && time.Since(dbBatch.CreatedAt) > time.Duration(r.cfg.BatchSubmission.TimeoutSec)*time.Second {
-			forceSubmit = true
-		}
-
 		if batchesToSubmitLen < r.cfg.BatchSubmission.MaxBatches {
 			batchesToSubmit = append(batchesToSubmit, &dbBatchWithChunksAndParent{
 				Batch:       dbBatch,
@@ -1118,6 +1198,136 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	// bootstrap on first call
+	if r.lastFetchedBlock == 0 {
+		// start window
+		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / secondsPerBlock)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+
+	return r.feeHistory, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strategy StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	var baseline float64 // baseline in Gwei (converting to float, small loss of precision)
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	switch strategy.BaselineType {
+	case PctMin:
+		// make a copy, sort by big.Int.Cmp, then pick the percentile element
+		sorted := make([]*big.Int, n)
+		copy(sorted, history)
+		sort.Slice(sorted, func(i, j int) bool {
+			return sorted[i].Cmp(sorted[j]) < 0
+		})
+		idx := int(strategy.BaselineParam * float64(n-1))
+		if idx < 0 {
+			idx = 0
+		}
+		baseline, _ = new(big.Float).
+			Quo(new(big.Float).SetInt(sorted[idx]), big.NewFloat(1e9)).
+			Float64()
+
+	case EWMA:
+		one := big.NewFloat(1)
+		alpha := big.NewFloat(strategy.BaselineParam)
+		oneMinusAlpha := new(big.Float).Sub(one, alpha)
+
+		// start from first history point
+		ewma := new(big.Float).
+			Quo(new(big.Float).SetInt(history[0]), big.NewFloat(1e9))
+
+		for i := 1; i < n; i++ {
+			curr := new(big.Float).
+				Quo(new(big.Float).SetInt(history[i]), big.NewFloat(1e9))
+			term1 := new(big.Float).Mul(alpha, curr)
+			term2 := new(big.Float).Mul(oneMinusAlpha, ewma)
+			ewma = new(big.Float).Add(term1, term2)
+		}
+		baseline, _ = ewma.Float64()
+
+	default:
+		// fallback to last element
+		baseline, _ = new(big.Float).
+			Quo(new(big.Float).SetInt(history[n-1]), big.NewFloat(1e9)).
+			Float64()
+	} // now baseline holds our baseline in float64 Gwei
+
+	// relaxation
+	age := time.Since(firstTime).Seconds()
+	frac := age / float64(windowSec)
+	var adjusted float64
+	switch strategy.RelaxType {
+	case Exponential:
+		adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	case Sigmoid:
+		adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	default:
+		adjusted = baseline
+	}
+	// back to wei
+	f := new(big.Float).Mul(big.NewFloat(adjusted), big.NewFloat(1e9))
+	out, _ := f.Int(nil)
+	return out
+}
+
+// skipSubmitByFee returns (true, nil) when submission should be skipped right now
+// because the blob‐fee is above target and the timeout window hasn’t yet elapsed.
+// Otherwise returns (false, err)
+func (r *Layer2Relayer) skipSubmitByFee(oldest time.Time) (bool, error) {
+	windowSec := uint64(r.cfg.BatchSubmission.TimeoutSec)
+
+	hist, err := r.fetchBlobFeeHistory(windowSec)
+	if err != nil || len(hist) == 0 {
+		return false, fmt.Errorf(
+			"blob-fee history unavailable or empty: %w (history_length=%d)",
+			err, len(hist),
+		)
+	}
+
+	// calculate target & get current (in wei)
+	target := calculateTargetPrice(windowSec, r.batchStrategy, oldest, hist)
+	current := hist[len(hist)-1]
+
+	// if current fee > target and still inside the timeout window, skip
+	if current.Cmp(target) > 0 && time.Since(oldest) < time.Duration(windowSec)*time.Second {
+		return true, fmt.Errorf(
+			"blob-fee above target & window not yet passed; current=%s target=%s age=%s",
+			current.String(), target.String(), time.Since(oldest),
+		)
+	}
+
+	// otherwise proceed with submission
+	return false, nil
+}
+
 func addrFromSignerConfig(config *config.SignerConfig) (common.Address, error) {
 	switch config.SignerType {
 	case sender.PrivateKeySignerType:
diff --git a/rollup/internal/orm/batch.go b/rollup/internal/orm/batch.go
index 0fb4ee0127..3630ca2770 100644
--- a/rollup/internal/orm/batch.go
+++ b/rollup/internal/orm/batch.go
@@ -218,6 +218,18 @@ func (o *Batch) GetRollupStatusByHashList(ctx context.Context, hashes []string)
 	return statuses, nil
 }
 
+func (o *Batch) GetFailedAndPendingBatchesCount(ctx context.Context) (int64, error) {
+	db := o.db.WithContext(ctx)
+	db = db.Model(&Batch{})
+	db = db.Where("rollup_status = ? OR rollup_status = ?", types.RollupCommitFailed, types.RollupPending)
+
+	var count int64
+	if err := db.Count(&count).Error; err != nil {
+		return 0, fmt.Errorf("Batch.GetFailedAndPendingBatchesCount error: %w", err)
+	}
+	return count, nil
+}
+
 // GetFailedAndPendingBatches retrieves batches with failed or pending status up to the specified limit.
 // The returned batches are sorted in ascending order by their index.
 func (o *Batch) GetFailedAndPendingBatches(ctx context.Context, limit int) ([]*Batch, error) {
diff --git a/rollup/internal/orm/l1_block.go b/rollup/internal/orm/l1_block.go
index edaa10c1ac..5d47dc5e04 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
 }
 
+// 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) {
+	var fees []uint64
+	db := o.db.WithContext(ctx).
+		Model(&L1Block{}).
+		Where("number >= ? AND number <= ?", startBlock, endBlock).
+		Order("number ASC")
+	if err := db.Pluck("blob_base_fee", &fees).Error; err != nil {
+		return nil, fmt.Errorf("L1Block.GetBlobFeesInRange error: %w", err)
+	}
+	return fees, nil
+}
+
 // InsertL1Blocks batch inserts l1 blocks.
 // If there's a block number conflict (e.g., due to reorg), soft deletes the existing block and inserts the new one.
 func (o *L1Block) InsertL1Blocks(ctx context.Context, blocks []L1Block) error {


Reviews:
user: login: coderabbitai[bot]
body: > [!CAUTION]
> Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.  If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

**Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1188-1216`: **Potential slice-building inefficiency & range query correctness in `fetchBlobFeeHistory`.**

*Minor, non-blocking observations*  
• `blocksAgo := windowSec / 12` silently floors – for window < 12 s the query span becomes **0**. A guard could return early to avoid a full-table scan on very small windows.  
• The `filters` map uses two separate `number >= ?` / `<= ?` entries; GORM will treat duplicate keys oddly. Prefer a single `Where("number BETWEEN ? AND ?", start, latest)` or two chained `Where`s for clarity.  
• You allocate `hist := make([]*big.Int, len(recs))`; if the result is large this double-stores all fees (`uint64` in record + `big.Int` object). Consider returning `[]uint64` unless arbitrary precision is strictly required.

No functional bug here, but tightening these points will improve robustness.

</blockquote></details>
<details>
<summary>rollup/conf/config.json (1)</summary><blockquote>

`57-58`: **Timeout raised to 2 h – verify dependent services & dashboards.**

Doubling the submission window changes alerting expectations:

• Grafana/Prometheus alerts that fired after 5 min of no-submission must be updated.  
• Any automation relying on fast batch availability (e.g. testnets) may need tuning.

Please audit downstream tooling before merging.

</blockquote></details>

</blockquote></details>

<details>
<summary>🛑 Comments failed to post (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

290-336: _🛠️ Refactor suggestion_

**Unbounded query + hard-coded backlog limits – consider configurability & safety.**

1. `GetFailedAndPendingBatches(r.ctx, 0)` now issues an *unlimited* query. In a pathological backlog this could fetch tens of thousands of rows into memory in one go.  
   • Either keep a *large, but bounded* limit (e.g. `backlogMax+cfg.BatchSubmission.MaxBatches`) or use a paginated cursor.

2. `backlogMax := 75` is an in-code constant while the comment points at `cfg.BatchSubmission.BacklogMax`.  
   • Promote it to configuration so operators can tune it without recompilation.

Failing to address these may cause memory spikes and reduce observability in production.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 golangci-lint (1.64.8)</summary>

313-313: strat.WindowSec undefined (type StrategyParams has no field or method WindowSec)

(typecheck)

---

327-327: strat.WindowSec undefined (type StrategyParams has no field or method WindowSec)

(typecheck)

</details>
<details>
<summary>🪛 GitHub Actions: Rollup</summary>

[error] 313-313: Build error: 'strat.WindowSec' undefined (type StrategyParams has no field or method WindowSec)

</details>

</details>

---

68-82: _⚠️ Potential issue_

**Compilation breaks – `WindowSec` field missing from `StrategyParams`.**

`ProcessPendingBatches`, `fetchBlobFeeHistory`, and `calculateTargetPrice` all reference `strat.WindowSec`, yet the struct declared here does not define that field.  
The build currently fails (`typecheck: strat.WindowSec undefined`).  
Add the field (and initialise it in `bestParams`) or stop referencing it and pass the window size explicitly.

```diff
 type StrategyParams struct {
-	BaselineType  string  // "pct_min" or "ewma"
-	BaselineParam float64 // percentile (0–1) or α for EWMA
-	Gamma         float64 // relaxation γ
-	Beta          float64 // relaxation β
-	RelaxType     string  // "exponential" or "sigmoid"
+	WindowSec     uint64  // sampling window (seconds)
+	BaselineType  string  // "pct_min" or "ewma"
+	BaselineParam float64 // percentile (0–1) or α for EWMA
+	Gamma         float64 // relaxation γ
+	Beta          float64 // relaxation β
+	RelaxType     string  // "exponential" or "sigmoid"
 }
 
 var bestParams = map[uint64]StrategyParams{
-	2 * 3600:  {BaselineType: "pct_min", BaselineParam: 0.10, Gamma: 0.4, Beta: 8, RelaxType: "exponential"},
-	5 * 3600:  {BaselineType: "pct_min", BaselineParam: 0.30, Gamma: 0.6, Beta: 20, RelaxType: "sigmoid"},
-	12 * 3600: {BaselineType: "pct_min", BaselineParam: 0.50, Gamma: 0.5, Beta: 20, RelaxType: "sigmoid"},
+	2 * 3600:  {WindowSec: 2 * 3600,  BaselineType: "pct_min", BaselineParam: 0.10, Gamma: 0.4, Beta: 8,  RelaxType: "exponential"},
+	5 * 3600:  {WindowSec: 5 * 3600,  BaselineType: "pct_min", BaselineParam: 0.30, Gamma: 0.6, Beta: 20, RelaxType: "sigmoid"},
+	12 * 3600: {WindowSec: 12 * 3600, BaselineType: "pct_min", BaselineParam: 0.50, Gamma: 0.5, Beta: 20, RelaxType: "sigmoid"},
 }
```

This restores type-checking, keeps the single source-of-truth for the window size and allows downstream code to compile.  
Be sure to run `go test ./...` or the CI pipeline after applying the patch.

</blockquote></details>

</blockquote></details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-14T16:23:33Z
user: login: Thegaram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T09:44:04Z
user: login: jonastheis
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T12:23:34Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:41:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (6)</summary><blockquote>

`97-104`: **Consider adding validation for strategy parameters**

The `StrategyParams` struct holds important configuration that impacts batch submission behavior, but there's no validation to ensure the parameters are within reasonable bounds.

Consider adding a validation method to check that parameters like BaselineParam (0-1), Gamma, and Beta are within expected ranges:

```diff
 type StrategyParams struct {
 	BaselineType  BaselineType // "pct_min" or "ewma"
 	BaselineParam float64      // percentile (0–1) or α for EWMA
 	Gamma         float64      // relaxation γ
 	Beta          float64      // relaxation β
 	RelaxType     RelaxType    // Exponential or Sigmoid
+
+	// Validate checks if the parameters are within valid ranges
+	func (s StrategyParams) Validate() error {
+		if s.BaselineType == PctMin && (s.BaselineParam < 0 || s.BaselineParam > 1) {
+			return fmt.Errorf("BaselineParam must be between 0 and 1 for PctMin, got %f", s.BaselineParam)
+		}
+		if s.BaselineType == EWMA && (s.BaselineParam <= 0 || s.BaselineParam >= 1) {
+			return fmt.Errorf("BaselineParam (alpha) must be between 0 and 1 for EWMA, got %f", s.BaselineParam)
+		}
+		if s.Gamma < 0 {
+			return fmt.Errorf("Gamma must be non-negative, got %f", s.Gamma)
+		}
+		if s.Beta < 0 {
+			return fmt.Errorf("Beta must be non-negative, got %f", s.Beta)
+		}
+		return nil
+	}
 }
```

---

`106-111`: **Hard-coded strategy parameters could be made configurable**

The strategy parameters for different time windows are hard-coded in the `bestParams` map, which might limit flexibility.

Consider making these settings configurable through configuration files or environment variables, allowing for easier tuning without code changes:

```diff
-// bestParams maps your 2h/5h/12h windows to their best rules.
-var bestParams = map[uint64]StrategyParams{
-	2 * 3600:  {BaselineType: PctMin, BaselineParam: 0.10, Gamma: 0.4, Beta: 8, RelaxType: Exponential},
-	5 * 3600:  {BaselineType: PctMin, BaselineParam: 0.30, Gamma: 0.6, Beta: 20, RelaxType: Sigmoid},
-	12 * 3600: {BaselineType: PctMin, BaselineParam: 0.50, Gamma: 0.5, Beta: 20, RelaxType: Sigmoid},
-}
```

Add to the configuration structure instead, and load default values if not specified.

---

`192-211`: **Add error logging for initialization failures**

The strategy selection and fee history initialization are crucial for the relayer's operation, but errors during these steps could be more informative.

Consider adding more detailed logging before returning errors to aid in troubleshooting:

```diff
 	// pick and validate our submission strategy
 	windowSec := uint64(cfg.BatchSubmission.TimeoutSec)
 	strategy, ok := bestParams[windowSec]
 	if !ok {
+		log.Error("Unsupported batch submission timeout", "windowSec", windowSec, "supported", maps.Keys(bestParams))
 		return nil, fmt.Errorf(
 			"unsupported BatchSubmission.TimeoutSec: %d (must be one of %v)",
 			windowSec, maps.Keys(bestParams),
 		)
 	}
 	layer2Relayer.batchStrategy = strategy

 	latest, err := layer2Relayer.l1BlockOrm.GetLatestL1BlockHeight(ctx)
 	if err != nil {
+		log.Error("Failed to get latest L1 block height", "err", err)
 		return nil, fmt.Errorf("failed to get latest L1 block height: %v", err)
 	}
 	layer2Relayer.lastFetchedBlock = latest - uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)/12 // start ~window seconds ago
 	if _, err = layer2Relayer.fetchBlobFeeHistory(uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)); err != nil {
+		log.Error("Initial blob fee load failed", "err", err)
 		return nil, fmt.Errorf("initial blob‐fee load failed: %w", err)
 	}
```

---

`351-360`: **Consider refactoring skipSubmitByFee logic into a separate function**

The fee-skipping logic in ProcessPendingBatches could be more readable and maintainable if refactored.

Consider extracting the decision logic into a more descriptive method:

```diff
-	if backlogCount <= backlogMax {
-		oldest := dbBatches[0].CreatedAt
-		if skip, msg := r.skipSubmitByFee(oldest); skip {
-			log.Debug(msg)
-			return
-		}
-		// if !skip, we fall through and submit immediately
-	}
+	if backlogCount <= backlogMax && r.shouldDelaySubmissionForBetterFee(dbBatches[0].CreatedAt) {
+		return
+	}
+	// If backlog exceeds max, or we shouldn't delay, we fall through and submit immediately

// Add a new method:
+func (r *Layer2Relayer) shouldDelaySubmissionForBetterFee(oldestBatchTime time.Time) bool {
+	skip, msg := r.skipSubmitByFee(oldestBatchTime)
+	if skip {
+		log.Debug(msg)
+		return true
+	}
+	return false
+}
```

---

`1209-1238`: **Optimize blob fee history storage and retrieval**

The current implementation rebuilds the fee history on each call, which could be inefficient for frequent calls with large window sizes.

Consider implementing a time-based cache or a more efficient data structure like a circular buffer:

```diff
 func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
 	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
 	if err != nil {
 		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
 	}
 	from := r.lastFetchedBlock + 1
 	//if new blocks
 	if from <= latest {
 		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
 		if err != nil {
 			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
 		}
 		// 2) append them
 		for _, v := range raw {
 			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
 			r.lastFetchedBlock++
 		}
 	}

 	maxLen := int(windowSec / 12)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
 	// return a copy
 	out := make([]*big.Int, len(r.feeHistory))
 	copy(out, r.feeHistory)
 	return out, nil
 }
```

Also, consider adding a timestamp or block number to each fee entry to better track the age of each sample.

---

`1289-1317`: **Add retry logic for blob fee history retrieval**

The `skipSubmitByFee` method immediately falls back to immediate submission if blob fee history is unavailable, which could be temporary.

Consider adding retry logic for fetching the blob fee history:

```diff
 func (r *Layer2Relayer) skipSubmitByFee(oldest time.Time) (bool, string) {
 	windowSec := uint64(r.cfg.BatchSubmission.TimeoutSec)
 
-	hist, err := r.fetchBlobFeeHistory(windowSec)
+	var hist []*big.Int
+	var err error
+	
+	// Try fetching the blob fee history with retries
+	maxRetries := 3
+	for i := 0; i < maxRetries; i++ {
+		hist, err = r.fetchBlobFeeHistory(windowSec)
+		if err == nil && len(hist) > 0 {
+			break
+		}
+		
+		if i < maxRetries-1 {
+			log.Debug("Retrying blob fee history fetch", "attempt", i+1, "err", err)
+			time.Sleep(time.Second)
+		}
+	}
+	
 	if err != nil || len(hist) == 0 {
 		return false, fmt.Sprintf(
 			"blob‐fee history unavailable or empty; fallback to immediate batch submission – err=%v, history_length=%d",
 			err, len(hist),
 		)
 	}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 57e1f9a20d94b4f408ef141866ee6fa0d6afb8e2 and 4f84e9e5e8fa46ddd04df6898f4dd52b8cc44e85.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `rollup/internal/controller/relayer/l2_relayer.go` (8 hunks)
* `rollup/internal/orm/batch.go` (1 hunks)
* `rollup/internal/orm/l1_block.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* rollup/internal/orm/l1_block.go
* rollup/internal/orm/batch.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (5)</summary>

`38-47`: **Well-structured enum implementation for relaxation functions**

Good use of iota for enum values with proper documentation. This follows Go best practices for defining constants.

---

`49-60`: **Clear enum implementation for baseline types**

The enum is well-documented and follows Go best practices using iota.

---

`92-95`: **Good field additions to support the fee strategy**

These new fields appropriately store the needed state for the fee-based submission strategy.

---

`156-157`: **LGTM: Added L1Block ORM initialization**

The L1Block ORM is properly initialized to allow for fetching blob fee data.

---

`1240-1287`: <details>
<summary>❓ Verification inconclusive</summary>

**Add error checking for float64 conversions**

The `calculateTargetPrice` function converts between big.Int and float64 without checking for potential loss of precision or overflow.

Check for potential precision issues in the float64 conversions:



Consider adding error handling for these conversions and potentially using big.Float for the entire calculation to maintain precision.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any extremely large blob fee values in the codebase that might cause precision issues
rg -A 2 -B 2 "BlobBaseFee" 
rg -A 2 -B 2 "blobFee"
```

Length of output: 14319

---



</details>

**Verify float64 conversion precision in calculateTargetPrice**

The `calculateTargetPrice` function in  
`rollup/internal/controller/relayer/l2_relayer.go`  
converts `*big.Int` → `*big.Float` → `float64` (ignoring the `Accuracy` return) and later back to `*big.Int` via `Int(nil)`. In edge cases (values >2⁵³ or requiring sub-gwei precision) this can silently round or overflow.

Please verify that:

- Blob-fee history values (in Gwei) always stay below the 2⁵³ threshold so conversion is exact.  
- You’re comfortable discarding the `Accuracy` flag returned by `Float64()`.  

If higher precision is needed, consider:  
- Checking the `Accuracy` value from `Float64()` and handling “Below”/“Above” cases.  
- Performing the entire relaxation and scaling using `big.Float`, only calling `Int(nil)` on the final result.

Location for review:  
- rollup/internal/controller/relayer/l2_relayer.go → `calculateTargetPrice` (lines 1240–1287)

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T16:44:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1272-1280`: _⚠️ Potential issue_

**Add bounds checking for relaxation formulas.**

The exponential and sigmoid formulas could lead to extremely large values without proper bounds checking.


Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (6)</summary><blockquote>

`107-111`: **Consider adding validation for strategy parameters.**

While the map of predefined parameters looks good, consider adding validation logic to ensure the parameters are within reasonable bounds (e.g., BaselineParam should be between 0-1 for percentiles).

```diff
 var bestParams = map[uint64]StrategyParams{
 	2 * 3600:  {BaselineType: PctMin, BaselineParam: 0.10, Gamma: 0.4, Beta: 8, RelaxType: Exponential},
 	5 * 3600:  {BaselineType: PctMin, BaselineParam: 0.30, Gamma: 0.6, Beta: 20, RelaxType: Sigmoid},
 	12 * 3600: {BaselineType: PctMin, BaselineParam: 0.50, Gamma: 0.5, Beta: 20, RelaxType: Sigmoid},
 }
+
+// validateStrategyParams ensures that strategy parameters are within reasonable bounds
+func validateStrategyParams(s StrategyParams) error {
+	if s.BaselineType == PctMin && (s.BaselineParam < 0 || s.BaselineParam > 1) {
+		return fmt.Errorf("PctMin baseline parameter must be between 0 and 1, got %f", s.BaselineParam)
+	}
+	if s.BaselineType == EWMA && (s.BaselineParam <= 0 || s.BaselineParam >= 1) {
+		return fmt.Errorf("EWMA alpha parameter must be between 0 and 1 exclusive, got %f", s.BaselineParam)
+	}
+	return nil
+}
```

---

`344-359`: **Hardcoded backlog threshold should be configurable.**

The backlog threshold is using the configuration value `r.cfg.BatchSubmission.BacklogMax` which is good, but consider documenting in the comments what this threshold represents and how it affects the batch submission behavior.

```diff
-	// if backlog outgrow max size, force‐submit enough oldest batches
+	// If backlog exceeds the maximum allowed size (BacklogMax), we force-submit 
+	// batches regardless of the current blob fee, to prevent excessive batch buildup.
+	// This ensures the system can recover from prolonged high-fee periods.
 	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
```

---

`1243-1248`: **Consider optimizing big.Float to float64 conversion.**

The conversion from `big.Int` to `float64` via `big.Float` is correct but could be optimized for large batches.


```diff
 	data := make([]float64, n)
+	divider := big.NewFloat(1e9)
 	for i, v := range history {
-		f, _ := new(big.Float).Quo(new(big.Float).SetInt(v), big.NewFloat(1e9)).Float64()
+		f, _ := new(big.Float).Quo(new(big.Float).SetInt(v), divider).Float64()
 		data[i] = f
 	}
```

---

`1306-1311`: **Add metric for submission skipping due to fees.**

It would be valuable to track how often batch submissions are being skipped due to high fees. This would help monitor the effectiveness of the fee-based submission strategy.


```diff
 	// if current fee > target and still inside the timeout window, skip
 	if current.Cmp(target) > 0 && time.Since(oldest) < time.Duration(windowSec)*time.Second {
+		r.metrics.rollupL2RelayerBatchSubmissionSkippedForFee.Inc()
 		return true, fmt.Sprintf(
 			"blob‐fee above target & window not yet passed; current=%s target=%s age=%s",
 			current.String(), target.String(), time.Since(oldest),
 		)
 	}
```

---

`1180-1180`: **Remove debug print statement.**

The `fmt.Println` statement should be removed or replaced with proper logging.


```diff
-	fmt.Println("packing finalizeBundlePostEuclidV2NoProof", len(dbBatch.BatchHeader), dbBatch.CodecVersion, dbBatch.BatchHeader, new(big.Int).SetUint64(endChunk.TotalL1MessagesPoppedBefore+endChunk.TotalL1MessagesPoppedInChunk), common.HexToHash(dbBatch.StateRoot), common.HexToHash(dbBatch.WithdrawRoot))
+	log.Debug("Packing finalizeBundlePostEuclidV2NoProof", 
+		"header_length", len(dbBatch.BatchHeader), 
+		"codec_version", dbBatch.CodecVersion,
+		"messages_popped", endChunk.TotalL1MessagesPoppedBefore+endChunk.TotalL1MessagesPoppedInChunk,
+		"state_root", dbBatch.StateRoot,
+		"withdraw_root", dbBatch.WithdrawRoot)
```

---

`1207-1236`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider optimizing blob fee history retrieval.**

The current implementation queries the database frequently to update the fee history. This could become a performance bottleneck as the system scales. Consider:

1. Using a time-based cache with periodic updates instead of querying on every check
2. Adding batch size limits to prevent fetching too many records at once
3. Implementing a more efficient data structure like a circular buffer for the fee history



Let's check how frequently this method might be called:

---

🏁 Script executed:

```shell
#!/bin/bash
# Find where fetchBlobFeeHistory is called
rg -A 2 "fetchBlobFeeHistory" --no-ignore
```

Length of output: 1170

---

I’ll pull the exact call sites and surrounding context to see how often and where this is invoked.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate all calls and show 5 lines of context around them
rg -n "fetchBlobFeeHistory" rollup/internal/controller/relayer/l2_relayer.go
rg -C 5 "fetchBlobFeeHistory" rollup/internal/controller/relayer/l2_relayer.go
```

Length of output: 1992

---



</details>

**Optimize blob fee history retrieval**

The `fetchBlobFeeHistory` method (rollup/internal/controller/relayer/l2_relayer.go:1207–1236) is invoked twice:  
- During relayer initialization (line 208)  
- Inside `skipSubmitByFee` (around line 1293) on each batch‐submission check  

Each call performs two DB queries (latest block height + fee range), which may become a bottleneck as submissions scale. Consider:

• Caching the recent base-fee history and refreshing on a timer rather than querying every check  
• Imposing a hard limit on fetched rows or time window to bound each query  
• Switching to a fixed-size ring (circular buffer) in memory to cap history growth and reduce slicing overhead

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4f84e9e5e8fa46ddd04df6898f4dd52b8cc44e85 and d5af194c895caa5abc3b3f4d69be11f02ef8e954.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `rollup/conf/config.json` (1 hunks)
* `rollup/internal/config/relayer.go` (1 hunks)
* `rollup/internal/controller/relayer/l2_relayer.go` (8 hunks)
* `rollup/internal/orm/batch.go` (1 hunks)
* `rollup/internal/orm/l1_block.go` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* rollup/internal/config/relayer.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* rollup/conf/config.json
* rollup/internal/orm/l1_block.go
* rollup/internal/orm/batch.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

<details>
<summary>rollup/internal/orm/l1_block.go (3)</summary>

* `L1Block` (14-31)
* `L1Block` (39-41)
* `NewL1Block` (34-36)

</details>
<details>
<summary>rollup/internal/config/relayer.go (1)</summary>

* `BatchSubmission` (34-43)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: check
* GitHub Check: tests
* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (6)</summary>

`38-47`: **Good use of Golang iota for enumeration.**

The enum implementation for `RelaxType` using `iota` is clean and follows Go best practices for creating type-safe enumeration constants.

---

`49-60`: **Good documentation of enum types.**

The explanation comments for each baseline type clearly communicate the purpose and behavior of these constants. This improves code readability and maintainability.

---

`97-104`: **Clear structure definition for strategy parameters.**

The `StrategyParams` struct provides a well-organized way to encapsulate the fee submission strategy configuration with appropriate field types.

---

`192-210`: **Good strategy parameter validation in constructor.**

The constructor properly validates that the configured timeout window is one of the supported values and returns a helpful error message if not. This is a good practice to fail fast on invalid configuration.

---

`1287-1315`: **Good fallback behavior in skipSubmitByFee.**

The method properly handles error cases by falling back to immediate submission if fee history is unavailable or empty. This ensures the system remains operational even when fee data is missing.

---

`1252-1268`: **Robust implementation of baseline calculation algorithms.**

The implementation of the percentile minimum (PctMin) and exponentially weighted moving average (EWMA) calculations is well structured and follows standard algorithmic approaches.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-15T17:05:40Z
user: login: jonastheis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T02:01:33Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:21:06Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:26:31Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:39:46Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:45:43Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:45:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

`1229-1276`: _⚠️ Potential issue_

**Add bounds checking for relaxation formulas**

The exponential and sigmoid formulas could lead to extremely large values without proper bounds checking.


Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```

---

`1195-1227`: _🛠️ Refactor suggestion_

**Add minimum history length check**

The fee history window could be too small to make meaningful decisions if there aren't enough samples.


```diff
 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+	
+	// Ensure we have sufficient data for meaningful analysis
+	if len(r.feeHistory) < int(math.Max(3, float64(maxLen)*0.1)) {
+		log.Warn("Insufficient blob fee history samples for reliable decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", maxLen)
+	}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

`108-112`: **Consider extracting best parameters to configuration**

While the bestParams map is a good approach for providing default strategies, consider making these values configurable through the configuration file rather than hardcoding them in the source code.

This would allow tuning the parameters without requiring a code change and redeployment.

---

`1278-1306`: **Consider caching calculated target prices**

The target price calculation is potentially expensive, especially with larger fee histories. Consider caching the result for a short period to reduce computational overhead when `skipSubmitByFee` is called frequently.


Add a cache for the target price with a short expiration to avoid recalculating too frequently:

```go
type targetPriceCache struct {
    price      *big.Int
    oldest     time.Time
    calculated time.Time
    expiry     time.Duration
}

// Cache the target price calculation result
if time.Since(r.targetCache.calculated) < r.targetCache.expiry && 
   r.targetCache.oldest == oldest {
    target = r.targetCache.price
} else {
    target = calculateTargetPrice(windowSec, r.batchStrategy, oldest, hist)
    r.targetCache = targetPriceCache{
        price:      target,
        oldest:     oldest,
        calculated: time.Now(),
        expiry:     5 * time.Second, // Configurable cache duration
    }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d5af194c895caa5abc3b3f4d69be11f02ef8e954 and 0f8a816acdd82a13435907723de7f95fa8fbcac0.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `rollup/internal/controller/relayer/l2_relayer.go` (8 hunks)
* `rollup/internal/orm/batch.go` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* rollup/internal/orm/batch.go

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: tests
* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (10)</summary>

`37-46`: **Good use of iota for enum definition**

Using iota for RelaxType enum values follows Go convention and makes the code more maintainable.

---

`48-48`: **Use constant instead of magic number**

Good use of a named constant for secondsPerBlock rather than hardcoding the value throughout the code.

---

`50-61`: **Well-documented enum definition**

Clear documentation for the BaselineType enum that explains the purpose of each option.

---

`93-96`: **New fields added to support fee-based submission strategy**

The new fields properly track the state needed for implementing the fee-based batch submission strategy.

---

`98-106`: **Well-structured strategy parameters**

The StrategyParams struct is well organized with appropriate fields for fee baseline calculation and relaxation functions.

---

`157-157`: **New ORM field initialization**

The l1BlockOrm field is properly initialized in the constructor.

---

`193-195`: **Proper initialization of fee history**

The code correctly preloads the blob fee history during initialization and returns an error if it fails.

---

`330-334`: **Backlog check for forcing submission**

Good approach to check the backlog count for determining whether to force-submit batches. This prevents excessive queue buildup.

---

`339-353`: **Well-structured batch submission decision logic**

The code clearly separates the conditions for deciding when to submit batches:
1. Check backlog against the maximum threshold
2. Check if the oldest batch exceeds the timeout window
3. Use price-based strategy if neither of the above conditions are met

This provides a balanced approach between cost optimization and ensuring timely submissions.

---

`1297-1302`: **Good debugging information in error message**

The error message provides useful debugging information including the current fee, target fee, and time since the oldest batch, which will help with troubleshooting.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T08:49:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1229-1276`: _🛠️ Refactor suggestion_

**Implement target price calculation with baseline and relaxation.**

The implementation correctly:
- Handles empty history array
- Applies either percentile or EWMA baseline calculation
- Applies relaxation curves (exponential or sigmoid)
- Converts between Gwei and Wei for calculations

Consider adding bounds checking to prevent potential numerical instability with the exponential and sigmoid functions.

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoidTerm := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoidTerm > 10 {
+		sigmoidTerm = 10
+	}
+	adjusted = baseline * (1 + sigmoidTerm)
 default:
 	adjusted = baseline
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1195-1227`: **Implement fetchBlobFeeHistory to maintain sliding window of blob fees.**

The method:
- Correctly bootstraps on first call
- Fetches only new blocks since last call
- Maintains a sliding window of appropriate size

You might consider adding a minimum history length check to ensure enough samples for reliable decision-making.

```diff
 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+	
+	// Ensure we have sufficient data for meaningful analysis
+	if len(r.feeHistory) < int(math.Max(3, float64(maxLen)*0.1)) {
+		log.Debug("Limited blob fee history samples for decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", maxLen)
+	}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 0f8a816acdd82a13435907723de7f95fa8fbcac0 and a306b53b442b72c3a1d136b5e0f92ce24a8505d7.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `rollup/internal/controller/relayer/l2_relayer.go` (8 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

<details>
<summary>rollup/internal/orm/l1_block.go (3)</summary>

* `L1Block` (14-31)
* `L1Block` (39-41)
* `NewL1Block` (34-36)

</details>
<details>
<summary>rollup/internal/config/relayer.go (1)</summary>

* `BatchSubmission` (34-43)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: tests
* GitHub Check: check
* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (11)</summary>

`37-46`: **Good job defining the RelaxType enumeration clearly.**

The enum is well-documented with descriptive comments for each relaxation function type.

---

`48-48`: **Good addition of secondsPerBlock constant.**

Extracting this as a constant improves readability and maintainability.

---

`50-61`: **BaselineType enumeration is well-defined.**

Clear structure with descriptive comments explaining each baseline type's purpose.

---

`76-76`: **Adding l1BlockOrm field is appropriate.**

This field is necessary to fetch blob fee data for the new strategy.

---

`93-96`: **New Layer2Relayer fields support the fee history strategy.**

The fields are well named and appropriately scoped for the new functionality.

---

`98-105`: **StrategyParams struct is well-designed.**

The struct provides a clear structure for fee-submission rules with appropriate fields for each parameter.

---

`107-112`: **The bestParams map provides good defaults for different windows.**

Presets for 2h, 5h, and 12h windows allow for flexible configuration based on timeout duration.

---

`157-157`: **Properly initializing l1BlockOrm in the Layer2Relayer constructor.**

The ORM field is initialized correctly using the provided database connection.

---

`193-195`: **Initial blob-fee history load is a good practice.**

Loading initial fee history at startup ensures the relayer has data available for the first batch submission decision.

---

`329-352`: **Check batches backlog and implement fee-based submission strategy.**

The code correctly implements the logic to:
1. Check backlog count against configurable threshold
2. Enforce submission when the oldest batch exceeds the timeout
3. Skip submission when current blob fees are above target and timeout hasn't elapsed

There's a seamless integration with the existing batch submission logic.

---

`1278-1306`: **Implement skipSubmitByFee for batch submission control.**

The function:
- Correctly fetches blob fee history
- Calculates target price based on strategy
- Compares current fee to target price
- Allows submission when either fee conditions are favorable or timeout window has elapsed

The error reporting is detailed and will help with debugging submission decisions.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-16T09:00:11Z
user: login: jonastheis
body: Would be good to add a high level comment to `ProcessPendingBatches`: 

We submit batches if:
- the first batch is too old -> forceSubmit
- backlogCount > r.cfg.BatchSubmission.BacklogMax -> forceSubmit
- we have at least min count AND price is good

I committed a small refactor as I wanted to try it locally first
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T01:42:18Z
user: login: jonastheis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T01:51:31Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T06:35:38Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:05:45Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:06:49Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:06:53Z
user: login: ranchalp
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:07:06Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

`1228-1232`: **Consider adding minimum history length check.**

The fee history window could be too small to make meaningful decisions if there aren't enough samples.


```diff
 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+	
+	// Ensure we have sufficient data for meaningful analysis
+	if len(r.feeHistory) < int(math.Max(3, float64(maxLen)*0.1)) {
+		log.Warn("Insufficient blob fee history samples for reliable decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", maxLen)
+	}
```

---

`1237-1301`: _🛠️ Refactor suggestion_

**Add bounds checking for relaxation formulas.**

The exponential and sigmoid formulas could lead to extremely large values without proper bounds checking.


Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

`1203-1235`: **Consider caching intermediate results in fee history fetching.**

The `fetchBlobFeeHistory` method queries the database for new blocks on each call. While you do maintain a sliding window, there's potential for optimization by caching the results of expensive calculations derived from this history.

Consider implementing a time-based or update-based cache refreshing strategy to avoid redundant calculations, especially since this method might be called frequently:

```diff
 func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	// Only update if it's been more than X seconds since last update
+	if r.lastFeeHistoryUpdate != nil && time.Since(*r.lastFeeHistoryUpdate) < 5*time.Second {
+		return r.feeHistory, nil
+	}
+
 	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
 	if err != nil {
 		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
 	}
 	// bootstrap on first call
 	if r.lastFetchedBlock == 0 {
 		// start window
 		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
 	}
 	from := r.lastFetchedBlock + 1
 	//if new blocks
 	if from <= latest {
 		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
 		if err != nil {
 			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
 		}
 		// append them
 		for _, v := range raw {
 			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
 			r.lastFetchedBlock++
 		}
+		// Update timestamp of last update
+		now := time.Now()
+		r.lastFeeHistoryUpdate = &now
 	}

 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}

 	return r.feeHistory, nil
 }
```

---

`1238-1250`: **PctMin implementation could be more efficient.**

The PctMin calculation creates a full copy of the history array and sorts it on every call, which could be inefficient for large history windows.


Consider optimizing this by using a selection algorithm (like quickselect) to find the k-th smallest element without full sorting:

```diff
 case PctMin:
-	// make a copy, sort by big.Int.Cmp, then pick the percentile element
-	sorted := make([]*big.Int, n)
-	copy(sorted, history)
-	sort.Slice(sorted, func(i, j int) bool {
-		return sorted[i].Cmp(sorted[j]) < 0
-	})
-	idx := int(strategy.BaselineParam * float64(n-1))
-	if idx < 0 {
-		idx = 0
-	}
-	baseline, _ = new(big.Float).
-		Quo(new(big.Float).SetInt(sorted[idx]), big.NewFloat(1e9)).
-		Float64()
+	// Use a more efficient selection algorithm for finding the percentile element
+	idx := int(strategy.BaselineParam * float64(n-1))
+	if idx < 0 {
+		idx = 0
+	}
+	
+	// For small arrays, simple sort is fine
+	if n <= 100 {
+		sorted := make([]*big.Int, n)
+		copy(sorted, history)
+		sort.Slice(sorted, func(i, j int) bool {
+			return sorted[i].Cmp(sorted[j]) < 0
+		})
+		baseline, _ = new(big.Float).
+			Quo(new(big.Float).SetInt(sorted[idx]), big.NewFloat(1e9)).
+			Float64()
+	} else {
+		// For larger arrays, use selection algorithm to find k-th smallest
+		// Implementation of quickselect or similar algorithm
+		percentileValue := findKthSmallest(history, idx)
+		baseline, _ = new(big.Float).
+			Quo(new(big.Float).SetInt(percentileValue), big.NewFloat(1e9)).
+			Float64()
+	}
```

This optimization would be helpful for the long windows (like 12h) where the history could contain thousands of elements.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f268068d87f5bab40d86e12f8993edf4c713cd00 and c444b5f64e9f4e70ef61e29d9f76f29c2249cb66.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `rollup/internal/controller/relayer/l2_relayer.go` (8 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

<details>
<summary>rollup/internal/orm/l1_block.go (3)</summary>

* `L1Block` (14-31)
* `L1Block` (39-41)
* `NewL1Block` (34-36)

</details>
<details>
<summary>rollup/internal/config/relayer.go (1)</summary>

* `BatchSubmission` (34-43)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (12)</summary>

`37-46`: **Good implementation of the relaxation function enum.**

The enum implementation using iota provides a clean way to define different relaxation functions, with clear documentation of each type's purpose. This makes the code more maintainable and follows Go best practices.

---

`48-48`: **Good use of a named constant for block time.**

Defining `secondsPerBlock` as a constant improves code readability and maintainability. This value is used throughout the calculation logic, making it easy to update if the network parameters change.

---

`50-61`: **Well-defined baseline type enumeration.**

The `BaselineType` enumeration clearly defines the supported baseline calculation methods with good documentation. Using Go's iota for this enum is the correct approach.

---

`93-96`: **Good storage structure for fee history data.**

The added fields provide proper state management for the fee submission strategy, with clear separation of concerns between storing history data and strategy parameters.

---

`98-105`: **Well-designed strategy parameter structure.**

The `StrategyParams` struct provides a clean way to encapsulate the different parameters needed for the fee submission strategy. This approach makes it easy to create and manage different strategy configurations.

---

`108-112`: **Appreciate the pre-configured strategy parameters for different time windows.**

The use of a map to store optimal parameters for different time windows is a good approach. These parameters appear to be derived from trace tests as mentioned in the PR description, which is a data-driven way to optimize the strategy.

---

`157-157`: **Good integration of L1Block ORM.**

The addition of the L1BlockOrm initialization in the constructor enables querying L1 block data including blob fees, which is essential for the new submission strategy.

---

`318-322`: **Clear documentation of the extended batch submission policy.**

The updated comments accurately describe the multiple conditions under which batches are submitted, making the implementation intent clear.

---

`330-336`: **Good implementation of backlog management.**

The code properly fetches the backlog count and handles potential database errors. This ensures the relayer can make informed decisions based on the backlog size.

---

`339-349`: **Good implementation of force submission conditions.**

The code defines two clear conditions for forcing batch submission:
1. When the oldest batch exceeds the timeout window
2. When the backlog exceeds the configured maximum

This ensures the system doesn't indefinitely delay batch submissions during sustained high fee periods.

---

`350-361`: **Good implementation of fee-based submission gating.**

The code properly calls `skipSubmitByFee` to determine if the current blob fee is favorable for submission. The error handling is well-designed, with appropriate logging and a fallback to immediate submission if the fee check fails.

---

`1303-1331`: **Well-implemented fee-based submission decision logic.**

The `skipSubmitByFee` function properly implements the strategy described in the PR objectives. It fetches the latest blob fee history, calculates a target price based on the batch age, and makes a decision based on the current fee compared to the target. 

The error messages are informative, including the actual values that influenced the decision.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T08:16:17Z
user: login: jonastheis
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:31:14Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1237-1301`: _🛠️ Refactor suggestion_

**Add bounds checking for relaxation formulas to prevent extreme values.**

The exponential and sigmoid relaxation formulas could produce extremely large values for certain parameter combinations, potentially causing numerical instability.


Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```

Additionally, be aware of potential precision loss in the float64 conversions. This is likely negligible for the expected fee ranges but could become an issue if blob fees grow to extreme values.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1203-1235`: **Consider adding a minimum history check for reliable fee analysis.**

The function maintains a sliding window of blob fees, but there's no check to ensure there's sufficient data for making reliable fee decisions. If the window is too small or empty, the analysis may not be representative.


Add a minimum history length check and warning:

```diff
 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+
+	// Ensure we have sufficient data for meaningful analysis
+	minSamples := int(math.Max(3, float64(maxLen)*0.1))
+	if len(r.feeHistory) < minSamples {
+		log.Warn("Insufficient blob fee history samples for reliable decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", minSamples)
+	}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c444b5f64e9f4e70ef61e29d9f76f29c2249cb66 and bfc60b5e3235d278d7d4a63ddb2088f4527cb18f.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `rollup/internal/controller/relayer/l2_relayer.go` (9 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

<details>
<summary>rollup/internal/orm/l1_block.go (3)</summary>

* `L1Block` (14-31)
* `L1Block` (39-41)
* `NewL1Block` (34-36)

</details>
<details>
<summary>rollup/internal/config/relayer.go (1)</summary>

* `BatchSubmission` (34-43)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: check
* GitHub Check: tests
* GitHub Check: tests

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (10)</summary>

`37-61`: **Well-structured enums for the relaxation and baseline types.**

The enums are properly defined using Go's iota with clear documentation for each type. This provides a solid foundation for the fee submission strategy implementation.

---

`63-96`: **LGTM for the Layer2Relayer struct extensions.**

The additions to the Layer2Relayer struct are well-organized and properly support the new blob fee tracking functionality with clear purpose for each field.

---

`98-105`: **Good design for the StrategyParams struct.**

The StrategyParams struct provides a clean way to encapsulate all the parameters needed for the fee submission strategy, making it easily configurable.

---

`107-112`: **Well-tuned preset parameters for different time windows.**

The bestParams map provides sensible defaults for various timeout windows based on trace testing as mentioned in the PR description.

---

`156-158`: **LGTM for ORM initialization.**

The l1BlockOrm is properly initialized in the constructor to provide access to the L1 block data.

---

`168-171`: **Clear strategy initialization based on configured timeout.**

The batchStrategy is properly initialized from the bestParams map based on the configured timeout, ensuring that the optimal strategy is applied.

---

`318-322`: **Clear documentation of the batch submission conditions.**

The updated documentation clearly explains the conditions under which batches are submitted, making the code more maintainable.

---

`331-348`: **Well-structured backlog handling logic.**

The code properly checks the backlog count and forces submission if it exceeds the configured maximum, helping to prevent excessive delays in batch processing.

---

`350-361`: **Good implementation of fee-based submission gating.**

The code properly integrates the fee-based submission strategy by calling skipSubmitByFee and handling the result appropriately, including detailed logging when skipping submission.

---

`1303-1331`: **Well-designed fee skipping logic with good error reporting.**

The skipSubmitByFee function provides a clear implementation of the fee-based gating mechanism with descriptive error messages when skipping submission, making debugging easier.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T08:36:48Z
user: login: jonastheis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:49:30Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (2)</summary><blockquote>

`1227-1231`: **Consider adding minimum history length check.**

The fee history window could be too small to make meaningful decisions if there aren't enough samples.


Add a check for minimum history length:

```diff
 	maxLen := int(windowSec / secondsPerBlock)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+	
+	// Ensure we have sufficient data for meaningful analysis
+	minSamples := int(math.Max(3, float64(maxLen)*0.1))
+	if len(r.feeHistory) < minSamples {
+		log.Warn("Insufficient blob fee history samples for reliable decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", maxLen,
+			"minimum", minSamples)
+	}
```

---

`1235-1299`: _🛠️ Refactor suggestion_

**Add bounds checking for relaxation formulas.**

The exponential and sigmoid formulas could lead to extremely large values without proper bounds checking.


Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (1)</summary><blockquote>

`1258-1275`: **Consider optimizing the EWMA calculation.**

The EWMA calculation creates many temporary `big.Float` objects, which could be optimized.


Reuse float objects where possible:

```diff
 	case EWMA:
 		one := big.NewFloat(1)
 		alpha := big.NewFloat(strategy.BaselineParam)
 		oneMinusAlpha := new(big.Float).Sub(one, alpha)
+		curr := new(big.Float)
+		term1 := new(big.Float)
+		term2 := new(big.Float)
 
 		// start from first history point
 		ewma := new(big.Float).
 			Quo(new(big.Float).SetInt(history[0]), big.NewFloat(1e9))
 
 		for i := 1; i < n; i++ {
-			curr := new(big.Float).
+			curr.
 				Quo(new(big.Float).SetInt(history[i]), big.NewFloat(1e9))
-			term1 := new(big.Float).Mul(alpha, curr)
-			term2 := new(big.Float).Mul(oneMinusAlpha, ewma)
-			ewma = new(big.Float).Add(term1, term2)
+			term1.Mul(alpha, curr)
+			term2.Mul(oneMinusAlpha, ewma)
+			ewma.Add(term1, term2)
 		}
 		baseline, _ = ewma.Float64()
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**
**Cache: Disabled due to data retention organization setting**
**Knowledge Base: Disabled due to data retention organization setting**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bfc60b5e3235d278d7d4a63ddb2088f4527cb18f and f7be7c4301b1395664187eb22473d6de6707bcf1.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `common/version/version.go` (1 hunks)
* `rollup/internal/controller/relayer/l2_relayer.go` (9 hunks)
* `rollup/internal/orm/batch.go` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (1)</summary>

* common/version/version.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* rollup/internal/orm/batch.go

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>rollup/internal/controller/relayer/l2_relayer.go (10)</summary>

`37-46`: **Good use of enumeration for relaxation functions.**

The enum `RelaxType` is well structured with clear comments explaining the purpose of each constant. Using iota for auto-incrementing enum values is a Go best practice.

---

`48-48`: **Good use of a constant for seconds per block.**

Extracting the value to a named constant improves readability and maintenance.

---

`50-61`: **Well-documented enum for baseline types.**

The `BaselineType` enum is clearly defined with detailed comments that explain each strategy, making the code more maintainable.

---

`93-96`: **Good structure additions to Layer2Relayer.**

The new fields are well-named and have clear purposes:
- `lastFetchedBlock` tracks progress
- `feeHistory` stores the sliding window of blob fees
- `batchStrategy` holds the configured submission strategy parameters

---

`98-105`: **Well-designed strategy parameters struct.**

The `StrategyParams` struct encapsulates all the parameters needed for fee-based submission strategies with clear field names and appropriate types.

---

`107-112`: **Strategy tuning parameters are well-organized.**

Using a map to associate different window durations with their optimal strategy parameters is a clean approach. The values appear to be derived from empirical testing, which is good for performance optimization.

---

`157-169`: **Constructor initialization looks good.**

The initialization of the new fields in the constructor is appropriate, with the `batchStrategy` selected based on the configured timeout window.

---

`331-348`: **Backlog and timeout-based forcing is well implemented.**

The code properly checks for two conditions to force submission:
1. If the oldest batch exceeds the timeout window
2. If the backlog count exceeds the configured maximum

This ensures that batches are eventually processed even if fee conditions remain unfavorable.

---

`350-361`: **Good fee-gating implementation with error handling.**

The fee-gating logic with `skipSubmitByFee` is well integrated, with appropriate error handling and logging. If the fee check fails, it falls back to immediate submission, which is a safe approach.

---

`1301-1329`: **Well-structured skipSubmitByFee implementation.**

The method clearly handles the logic for determining when to skip batch submission based on fee conditions. The error message provides useful information for debugging and monitoring.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-19T08:57:24Z
user: login: Thegaram
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T16:48:41Z

Review comments:
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: Thegaram
body: Better to limit at `r.cfg.BatchSubmission.MaxBatches`, otherwise we might need to read too many records.

If you just need it to check backlog size, you can add another count query for that.
created_at: 2025-05-15T09:31:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: Thegaram
body: The current batch submission trigger is "either collect `MinBatches` or timeout". Here you change it, what's the rationale for that? Is your point that with such a large window we should never have `<MinBatches` after timeout?
created_at: 2025-05-15T09:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= backlogMax {
+		windowSec := uint64(r.cfg.BatchSubmission.TimeoutSec)
+		strat, ok := bestParams[windowSec]
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: Thegaram
body: Can rename to `strategy` to make the linter happy.
created_at: 2025-05-15T09:35:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1186,85 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	// how many blocks ago? ~12s per block
+	blocksAgo := windowSec / 12
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	start := int64(latest) - int64(blocksAgo)
+	if start < 0 {
+		start = 0
+	}
+
+	// pull all L1Blocks in [start .. latest]
+	filters := map[string]interface{}{
+		"number >= ?": start,
+		"number <= ?": latest,
+	}
+	recs, err := r.l1BlockOrm.GetL1Blocks(r.ctx, filters)
+	if err != nil {
+		return nil, fmt.Errorf("GetL1Blocks: %w", err)
+	}
+	hist := make([]*big.Int, len(recs))
+	for i, b := range recs {
+		hist[i] = new(big.Int).SetUint64(b.BlobBaseFee)
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: Thegaram
body: Better to just query the field (`BlobBaseFee`) and not the whole block.

Also, performance-wise this might be problematic (e.g. query 3600 blocks every couple of seconds). You might need to add a db index. Or better, maintain the fee history (or the moving average) in memory instead of querying blocks again every time.
created_at: 2025-05-15T09:44:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: ```suggestion
	if len(allBatches) < r.cfg.BatchSubmission.MinBatches {
```
I think the second check is not necessary. Also with Peter's suggestion to do a count query you can do this check at the very beginning (and keep the `backlogMax` stuff below it for better separation and readability. 
created_at: 2025-05-15T11:42:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: My understanding is: we don't necessarily want to commit even if we reach `MinBatches`. We still want to wait and commit at the best time. Or after a timeout we force the submission. The timeout is variable though, influenced by the timeout window (max) and backlog of batches. Worst case, we commit at an expensive price towards the end of the window. 
created_at: 2025-05-15T11:46:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1186,85 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	// how many blocks ago? ~12s per block
+	blocksAgo := windowSec / 12
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	start := int64(latest) - int64(blocksAgo)
+	if start < 0 {
+		start = 0
+	}
+
+	// pull all L1Blocks in [start .. latest]
+	filters := map[string]interface{}{
+		"number >= ?": start,
+		"number <= ?": latest,
+	}
+	recs, err := r.l1BlockOrm.GetL1Blocks(r.ctx, filters)
+	if err != nil {
+		return nil, fmt.Errorf("GetL1Blocks: %w", err)
+	}
+	hist := make([]*big.Int, len(recs))
+	for i, b := range recs {
+		hist[i] = new(big.Int).SetUint64(b.BlobBaseFee)
+	}
+	return hist, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strat StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	// convert to float64 Gwei
+	data := make([]float64, n)
+	for i, v := range history {
+		f, _ := new(big.Float).Quo(new(big.Float).SetInt(v), big.NewFloat(1e9)).Float64()
+		data[i] = f
+	}
+	var baseline float64
+	switch strat.BaselineType {
+	case "pct_min":
+		sort.Float64s(data)
+		idx := int(strat.BaselineParam * float64(n-1))
+		if idx < 0 {
+			idx = 0
+		}
+		baseline = data[idx]
+	case "ewma":
+		alpha := strat.BaselineParam
+		ewma := data[0]
+		for i := 1; i < n; i++ {
+			ewma = alpha*data[i] + (1-alpha)*ewma
+		}
+		baseline = ewma
+	default:
+		baseline = data[n-1]
+	}
+	// relaxation
+	age := time.Since(firstTime).Seconds()
+	frac := age / float64(windowSec)
+	var adjusted float64
+	switch strat.RelaxType {
+	case "exponential":
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: better to use constants for this. can be either string constants or better with `iota` modifier a Go style enum
created_at: 2025-05-15T11:57:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= backlogMax {
+		windowSec := uint64(r.cfg.BatchSubmission.TimeoutSec)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: Since this parameter can only change by restart, I think it would be better to assign the strategy in the constructor instead of this clunky few lines being here. Either startup can fail or set a reasonable default like you do here. 
created_at: 2025-05-15T11:59:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= backlogMax {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: somehow this code block is very ugly. Maybe you can put the logic into a function and then just return with the error message that you want to log. In that case we can continue and submit immediately (and log the error message). this way we can use the Go typical guard style
created_at: 2025-05-15T12:09:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1186,85 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	// how many blocks ago? ~12s per block
+	blocksAgo := windowSec / 12
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	start := int64(latest) - int64(blocksAgo)
+	if start < 0 {
+		start = 0
+	}
+
+	// pull all L1Blocks in [start .. latest]
+	filters := map[string]interface{}{
+		"number >= ?": start,
+		"number <= ?": latest,
+	}
+	recs, err := r.l1BlockOrm.GetL1Blocks(r.ctx, filters)
+	if err != nil {
+		return nil, fmt.Errorf("GetL1Blocks: %w", err)
+	}
+	hist := make([]*big.Int, len(recs))
+	for i, b := range recs {
+		hist[i] = new(big.Int).SetUint64(b.BlobBaseFee)
+	}
+	return hist, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strat StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	// convert to float64 Gwei
+	data := make([]float64, n)
+	for i, v := range history {
+		f, _ := new(big.Float).Quo(new(big.Float).SetInt(v), big.NewFloat(1e9)).Float64()
+		data[i] = f
+	}
+	var baseline float64
+	switch strat.BaselineType {
+	case "pct_min":
+		sort.Float64s(data)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: This is probably also relatively expensive after the repeated DB query. Like Peter said would be better to keep this in memory.

Maybe something like this construction https://github.com/scroll-tech/go-ethereum/blob/e62c6f08e2cb233ca0e14d01bc3f41f2edaea369/rollup/da_syncer/batch_queue.go#L20
with a sorted heap and a shrinkingmap in memory could work.

Every time you check you would only need to verify which min/max blocknumbers you expect to be in the map. if they are not there then you query them from DB and add them (this way also after startup should be handled automatically). if an entry is too old you delete it out of the heap and map. 
created_at: 2025-05-15T12:23:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: I removed this to keep the original semantic of "either collect MinBatches or timeout" (adding extra conditions if not in the timeout on top of having collected enough batches.
created_at: 2025-05-15T14:41:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1206,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// 2) append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+	// return a copy
+	out := make([]*big.Int, len(r.feeHistory))
+	copy(out, r.feeHistory)
+	return out, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strategy StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	// convert to float64 Gwei
+	data := make([]float64, n)
+	for i, v := range history {
+		f, _ := new(big.Float).Quo(new(big.Float).SetInt(v), big.NewFloat(1e9)).Float64()
+		data[i] = f
+	}
+	var baseline float64
+	switch strategy.BaselineType {
+	case PctMin:
+		sort.Float64s(data)
+		idx := int(strategy.BaselineParam * float64(n-1))
+		if idx < 0 {
+			idx = 0
+		}
+		baseline = data[idx]
+	case EWMA:
+		alpha := strategy.BaselineParam
+		ewma := data[0]
+		for i := 1; i < n; i++ {
+			ewma = alpha*data[i] + (1-alpha)*ewma
+		}
+		baseline = ewma
+	default:
+		baseline = data[n-1]
+	}
+	// relaxation
+	age := time.Since(firstTime).Seconds()
+	frac := age / float64(windowSec)
+	var adjusted float64
+	switch strategy.RelaxType {
+	case Exponential:
+		adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	case Sigmoid:
+		adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	default:
+		adjusted = baseline
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add bounds checking for relaxation formulas**

The exponential and sigmoid formulas could lead to extremely large values without proper bounds checking.

Add protection against potential numerical issues:

```diff
 switch strategy.RelaxType {
 case Exponential:
-	adjusted = baseline * (1 + strategy.Gamma*math.Exp(strategy.Beta*(frac-1)))
+	exponent := strategy.Beta * (frac - 1)
+	// Prevent extreme values that could cause overflow
+	if exponent > 20 {
+		exponent = 20
+	}
+	adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
 case Sigmoid:
-	adjusted = baseline * (1 + strategy.Gamma/(1+math.Exp(-strategy.Beta*(frac-0.5))))
+	sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
+	// Ensure the adjustment stays within reasonable bounds
+	if sigmoid > 10 {
+		sigmoid = 10
+	}
+	adjusted = baseline * (1 + sigmoid)
 default:
 	adjusted = baseline
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
	var adjusted float64
	switch strategy.RelaxType {
	case Exponential:
		exponent := strategy.Beta * (frac - 1)
		// Prevent extreme values that could cause overflow
		if exponent > 20 {
			exponent = 20
		}
		adjusted = baseline * (1 + strategy.Gamma*math.Exp(exponent))
	case Sigmoid:
		sigmoid := strategy.Gamma / (1 + math.Exp(-strategy.Beta*(frac-0.5)))
		// Ensure the adjustment stays within reasonable bounds
		if sigmoid > 10 {
			sigmoid = 10
		}
		adjusted = baseline * (1 + sigmoid)
	default:
		adjusted = baseline
	}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In rollup/internal/controller/relayer/l2_relayer.go around lines 1274 to 1282,
the exponential and sigmoid relaxation formulas can produce extremely large
values that may cause numerical instability. Add bounds checking to limit the
maximum and minimum values of the computed adjusted variable after applying the
formulas. Implement safeguards such as clamping the result within a reasonable
range to prevent overflow or underflow issues.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-15T16:44:33Z
author_association: NONE
diff_hunk: @@ -274,6 +341,25 @@
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
+	backlogMax := int64(75) //r.cfg.BatchSubmission.BacklogMax
+
+	// return if not hitting target price
+	if backlogCount <= backlogMax {
+		oldest := dbBatches[0].CreatedAt
+		if skip, msg := r.skipSubmitByFee(oldest); skip {
+			log.Debug(msg)
+			return
+		}
+		// if !skip, we fall through and submit immediately
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Hardcoded backlog threshold should be configurable**

The backlog threshold of 75 is hardcoded but there's a commented-out reference to a configuration value `r.cfg.BatchSubmission.BacklogMax`.

Make the backlog threshold configurable by uncommenting the reference to the configuration value:

```diff
-	backlogMax := int64(75) //r.cfg.BatchSubmission.BacklogMax
+	backlogMax := r.cfg.BatchSubmission.BacklogMax
```

Ensure this configuration field is properly defined in the config struct. This would allow for easier tuning of the system without code changes.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
  // if backlog outgrow max size, force‐submit enough oldest batches
  backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
  if err != nil {
    log.Error("Failed to fetch pending L2 batches", "err", err)
    return
  }

- backlogMax := int64(75) //r.cfg.BatchSubmission.BacklogMax
+ backlogMax := r.cfg.BatchSubmission.BacklogMax

  // return if not hitting target price
  if backlogCount <= backlogMax {
    oldest := dbBatches[0].CreatedAt
    if skip, msg := r.skipSubmitByFee(oldest); skip {
      log.Debug(msg)
      return
    }
    // if !skip, we fall through and submit immediately
  }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In rollup/internal/controller/relayer/l2_relayer.go around lines 344 to 361, the
backlog threshold is hardcoded to 75 instead of using the configurable value
r.cfg.BatchSubmission.BacklogMax. To fix this, uncomment the reference to
r.cfg.BatchSubmission.BacklogMax and ensure that the BatchSubmission.BacklogMax
field is properly defined and loaded in the configuration struct so the backlog
threshold can be adjusted without code changes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit d5af194
created_at: 2025-05-15T16:44:34Z
author_association: NONE
diff_hunk: @@ -1120,6 +1204,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding minimum history length check.**

The fee history window could be too small to make meaningful decisions if there aren't enough samples.


```diff
 	maxLen := int(windowSec / 12)
 	if len(r.feeHistory) > maxLen {
 		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
 	}
+	
+	// Ensure we have sufficient data for meaningful analysis
+	if len(r.feeHistory) < int(math.Max(3, float64(maxLen)*0.1)) {
+		log.Warn("Insufficient blob fee history samples for reliable decision making", 
+			"samples", len(r.feeHistory), 
+			"desired", maxLen)
+	}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
    maxLen := int(windowSec / 12)
    if len(r.feeHistory) > maxLen {
        r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
    }

    // Ensure we have sufficient data for meaningful analysis
    if len(r.feeHistory) < int(math.Max(3, float64(maxLen)*0.1)) {
        log.Warn("Insufficient blob fee history samples for reliable decision making",
            "samples", len(r.feeHistory),
            "desired", maxLen)
    }
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In rollup/internal/controller/relayer/l2_relayer.go around lines 1228 to 1231,
add a minimum length check for r.feeHistory before trimming it to maxLen. Ensure
that the feeHistory slice retains at least a certain minimum number of samples
to maintain meaningful data for decision-making. Implement logic to compare
maxLen with this minimum threshold and use the larger value to slice feeHistory
accordingly.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-15T17:05:39Z
author_association: NONE
diff_hunk: @@ -218,18 +218,32 @@ func (o *Batch) GetRollupStatusByHashList(ctx context.Context, hashes []string)
 	return statuses, nil
 }
 
+func (o *Batch) GetFailedAndPendingBatchesCount(ctx context.Context) (int64, error) {
+	db := o.db.WithContext(ctx)
+	db = db.Model(&Batch{})
+	db = db.Where("rollup_status = ? OR rollup_status = ?", types.RollupCommitFailed, types.RollupPending)
+
+	var count int64
+	if err := db.Count(&count).Error; err != nil {
+		return 0, fmt.Errorf("Batch.GetFailedAndPendingBatchesCount error: %w", err)
+	}
+	return count, nil
+}
+
 // GetFailedAndPendingBatches retrieves batches with failed or pending status up to the specified limit.
 // The returned batches are sorted in ascending order by their index.
 func (o *Batch) GetFailedAndPendingBatches(ctx context.Context, limit int) ([]*Batch, error) {
-	if limit <= 0 {
-		return nil, errors.New("limit must be greater than zero")
+	if limit < 0 {
path: rollup/internal/orm/batch.go
user: login: jonastheis
body: Revert these changes if not necessary anymore
created_at: 2025-05-15T23:54:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +341,23 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= r.cfg.BatchSubmission.BacklogMax {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: Now this function is much easier to read and reason about. One thing that we might consider but is probably okay for now: we always do the calculation, even if we don't have at least `MinBatches`. Only to then afterwards exit bc we don't have enough batches. 
created_at: 2025-05-16T01:40:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +341,23 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= r.cfg.BatchSubmission.BacklogMax {
+		oldest := dbBatches[0].CreatedAt
+		if skip, msg := r.skipSubmitByFee(oldest); skip {
+			log.Debug(msg)
+			return
+		}
+		// if !skip, we fall through and submit immediately
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: Now we don't log the reason of `!skip`. Instead of a msg as a string I think it's better to use an error type and just extract the message
created_at: 2025-05-16T01:46:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1204,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: use a constant for 12
created_at: 2025-05-16T01:54:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +189,25 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 		return nil, fmt.Errorf("invalid service type for l2_relayer: %v", serviceType)
 	}
 
+	// pick and validate submission strategy
+	windowSec := uint64(cfg.BatchSubmission.TimeoutSec)
+	strategy, ok := bestParams[windowSec]
+	if !ok {
+		return nil, fmt.Errorf(
+			"unsupported BatchSubmission.TimeoutSec: %d (must be one of %v)",
+			windowSec, maps.Keys(bestParams),
+		)
+	}
+	layer2Relayer.batchStrategy = strategy
+
+	latest, err := layer2Relayer.l1BlockOrm.GetLatestL1BlockHeight(ctx)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: Instead of having duplicate code can't we simply make `fetchBlobFeeHistory` recover itself if there's no previous state?

As far as I can see we only need to check if `layer2Relayer.lastFetchedBlock == 0` and then set it accordinlgy. 
created_at: 2025-05-16T01:59:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1204,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+	// return a copy
+	out := make([]*big.Int, len(r.feeHistory))
+	copy(out, r.feeHistory)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: Why copy here?
created_at: 2025-05-16T02:00:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1204,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+	// return a copy
+	out := make([]*big.Int, len(r.feeHistory))
+	copy(out, r.feeHistory)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: You are right, this is not needed. I thought because of the sort in calculate target price when using PctMin but the sort already takes a different slice.
created_at: 2025-05-16T08:21:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -141,6 +189,25 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 		return nil, fmt.Errorf("invalid service type for l2_relayer: %v", serviceType)
 	}
 
+	// pick and validate submission strategy
+	windowSec := uint64(cfg.BatchSubmission.TimeoutSec)
+	strategy, ok := bestParams[windowSec]
+	if !ok {
+		return nil, fmt.Errorf(
+			"unsupported BatchSubmission.TimeoutSec: %d (must be one of %v)",
+			windowSec, maps.Keys(bestParams),
+		)
+	}
+	layer2Relayer.batchStrategy = strategy
+
+	latest, err := layer2Relayer.l1BlockOrm.GetLatestL1BlockHeight(ctx)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Fair enough. Done.
created_at: 2025-05-16T08:26:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +341,23 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= r.cfg.BatchSubmission.BacklogMax {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Good point. I changed the code to do this, but something that I did while doing that is only setting forceSubmit to true if the oldest batch by index must be force submitted, unlike now that this is check for all the batches in dbBatches. I argue this is ok as the smallest index must be the one that must be submitted next. What do you think? 
created_at: 2025-05-16T08:39:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -218,18 +218,32 @@ func (o *Batch) GetRollupStatusByHashList(ctx context.Context, hashes []string)
 	return statuses, nil
 }
 
+func (o *Batch) GetFailedAndPendingBatchesCount(ctx context.Context) (int64, error) {
+	db := o.db.WithContext(ctx)
+	db = db.Model(&Batch{})
+	db = db.Where("rollup_status = ? OR rollup_status = ?", types.RollupCommitFailed, types.RollupPending)
+
+	var count int64
+	if err := db.Count(&count).Error; err != nil {
+		return 0, fmt.Errorf("Batch.GetFailedAndPendingBatchesCount error: %w", err)
+	}
+	return count, nil
+}
+
 // GetFailedAndPendingBatches retrieves batches with failed or pending status up to the specified limit.
 // The returned batches are sorted in ascending order by their index.
 func (o *Batch) GetFailedAndPendingBatches(ctx context.Context, limit int) ([]*Batch, error) {
-	if limit <= 0 {
-		return nil, errors.New("limit must be greater than zero")
+	if limit < 0 {
path: rollup/internal/orm/batch.go
user: login: ranchalp
body: done.
created_at: 2025-05-16T08:45:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1204,116 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / 12)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Done.
created_at: 2025-05-16T08:45:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,23 +190,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 		return nil, fmt.Errorf("invalid service type for l2_relayer: %v", serviceType)
 	}
 
-	// pick and validate submission strategy
-	windowSec := uint64(cfg.BatchSubmission.TimeoutSec)
-	strategy, ok := bestParams[windowSec]
-	if !ok {
-		return nil, fmt.Errorf(
-			"unsupported BatchSubmission.TimeoutSec: %d (must be one of %v)",
-			windowSec, maps.Keys(bestParams),
-		)
-	}
-	layer2Relayer.batchStrategy = strategy
-
-	latest, err := layer2Relayer.l1BlockOrm.GetLatestL1BlockHeight(ctx)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get latest L1 block height: %v", err)
-	}
-	layer2Relayer.lastFetchedBlock = latest - uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)/12 // start ~window seconds ago
-	if _, err = layer2Relayer.fetchBlobFeeHistory(uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)); err != nil {
+	if _, err := layer2Relayer.fetchBlobFeeHistory(uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)); err != nil {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: This is exactly called as is in `skipSubmitByFee`. I think we can just remove it from the constructor no?
created_at: 2025-05-19T01:14:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -274,6 +341,23 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount, err := r.batchOrm.GetFailedAndPendingBatchesCount(r.ctx)
+	if err != nil {
+		log.Error("Failed to fetch pending L2 batches", "err", err)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= r.cfg.BatchSubmission.BacklogMax {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: I think this is okay
created_at: 2025-05-19T01:15:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1199,119 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	// bootstrap on first call
+	if r.lastFetchedBlock == 0 {
+		// start window
+		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / secondsPerBlock)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+
+	return r.feeHistory, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strategy StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	// convert to float64 Gwei
+	data := make([]float64, n)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: jonastheis
body: why do we copy from `[]big.Int` to `[]float64` here? Can't we run into precision and rounding issues then? Shouldn't it be possible to do all operations in `big.Int`? 

Also as described in [this comment](https://github.com/scroll-tech/scroll/pull/1659/files#r2091051562) we still sort the data array every time, which might be quite expensive. 

I think this is okay for now, we should address this in the next version though.
created_at: 2025-05-19T01:46:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,23 +190,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
 		return nil, fmt.Errorf("invalid service type for l2_relayer: %v", serviceType)
 	}
 
-	// pick and validate submission strategy
-	windowSec := uint64(cfg.BatchSubmission.TimeoutSec)
-	strategy, ok := bestParams[windowSec]
-	if !ok {
-		return nil, fmt.Errorf(
-			"unsupported BatchSubmission.TimeoutSec: %d (must be one of %v)",
-			windowSec, maps.Keys(bestParams),
-		)
-	}
-	layer2Relayer.batchStrategy = strategy
-
-	latest, err := layer2Relayer.l1BlockOrm.GetLatestL1BlockHeight(ctx)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get latest L1 block height: %v", err)
-	}
-	layer2Relayer.lastFetchedBlock = latest - uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)/12 // start ~window seconds ago
-	if _, err = layer2Relayer.fetchBlobFeeHistory(uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)); err != nil {
+	if _, err := layer2Relayer.fetchBlobFeeHistory(uint64(layer2Relayer.cfg.BatchSubmission.TimeoutSec)); err != nil {
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Yes, it can be removed. It would just prepare the big chunk of the history on construction instead of during the loop. Otherwise it is fine. 
created_at: 2025-05-19T06:35:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1199,119 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	// bootstrap on first call
+	if r.lastFetchedBlock == 0 {
+		// start window
+		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / secondsPerBlock)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+
+	return r.feeHistory, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strategy StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	// convert to float64 Gwei
+	data := make([]float64, n)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: I thought the mention was to keep in memory the history, not the call to sort. The sort operation should be of negligible performance impact (we are talking about 7200 elements to sort through for a 24 hour period). Do you think it makes sense to invest time in removing this? 

Similar argument goes with float64. Dealing with big.Int will make for a more convoluted code when doing the exponent for the exponential and sigmoid relaxations. The loss of precision would be negligible (a couple wei), unless the blob base fee grows beyond unless the blob base fee grows to the order of millions of Gwei (i.e. ≳10⁶ Gwei, or ≳10¹⁵ Wei), at which point you’d still only lose a handful of Wei to floating-point imprecision in comparison. 

I have thus for the moment limitted the conversion to float to the end of the calculation of the baseline price, but I can still change all to big.Int all around if desired (probably in a new PR not to block this one). Same goes for the sort function. 
created_at: 2025-05-19T08:05:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1120,6 +1186,85 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	// how many blocks ago? ~12s per block
+	blocksAgo := windowSec / 12
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	start := int64(latest) - int64(blocksAgo)
+	if start < 0 {
+		start = 0
+	}
+
+	// pull all L1Blocks in [start .. latest]
+	filters := map[string]interface{}{
+		"number >= ?": start,
+		"number <= ?": latest,
+	}
+	recs, err := r.l1BlockOrm.GetL1Blocks(r.ctx, filters)
+	if err != nil {
+		return nil, fmt.Errorf("GetL1Blocks: %w", err)
+	}
+	hist := make([]*big.Int, len(recs))
+	for i, b := range recs {
+		hist[i] = new(big.Int).SetUint64(b.BlobBaseFee)
+	}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: You are right, done. 
created_at: 2025-05-19T08:06:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
 	if err != nil {
 		log.Error("Failed to fetch pending L2 batches", "err", err)
 		return
 	}
 
+	// if backlog outgrow max size, force‐submit enough oldest batches
+	backlogCount := len(allBatches)
+	backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
+
+	if len(allBatches) < r.cfg.BatchSubmission.MinBatches || len(allBatches) == 0 {
+		log.Debug("Not enough pending batches to submit", "count", len(allBatches), "minBatches", r.cfg.BatchSubmission.MinBatches, "maxBatches", r.cfg.BatchSubmission.MaxBatches)
+		return
+	}
+
+	// return if not hitting target price
+	if backlogCount <= backlogMax {
+		windowSec := uint64(r.cfg.BatchSubmission.TimeoutSec)
+		strat, ok := bestParams[windowSec]
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Done, thanks.
created_at: 2025-05-19T08:06:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -268,12 +287,59 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
 // ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
 func (r *Layer2Relayer) ProcessPendingBatches() {
 	// get pending batches from database in ascending order by their index.
-	dbBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, r.cfg.BatchSubmission.MaxBatches)
+	allBatches, err := r.batchOrm.GetFailedAndPendingBatches(r.ctx, 0)
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: ranchalp
body: Thanks, done. 
created_at: 2025-05-19T08:07:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1118,6 +1198,136 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	// bootstrap on first call
+	if r.lastFetchedBlock == 0 {
+		// start window
+		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / secondsPerBlock)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+
+	return r.feeHistory, nil
+}
+
+// calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
+func calculateTargetPrice(windowSec uint64, strategy StrategyParams, firstTime time.Time, history []*big.Int) *big.Int {
+	var baseline float64 // baseline in Gwei (converting to float, small loss of precision)
+	n := len(history)
+	if n == 0 {
+		return big.NewInt(0)
+	}
+	switch strategy.BaselineType {
+	case PctMin:
+		// make a copy, sort by big.Int.Cmp, then pick the percentile element
+		sorted := make([]*big.Int, n)
+		copy(sorted, history)
+		sort.Slice(sorted, func(i, j int) bool {
+			return sorted[i].Cmp(sorted[j]) < 0
+		})
+		idx := int(strategy.BaselineParam * float64(n-1))
+		if idx < 0 {
+			idx = 0
+		}
+		baseline, _ = new(big.Float).
+			Quo(new(big.Float).SetInt(sorted[idx]), big.NewFloat(1e9)).
+			Float64()
+
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Ensure safe handling of empty arrays in PctMin strategy.**

The PctMin strategy could panic if the history array is empty or has few elements.


Add safety checks for corner cases:

```diff
 	switch strategy.BaselineType {
 	case PctMin:
+		// Handle empty or small arrays
+		if n == 0 {
+			return big.NewInt(0)
+		}
+		if n == 1 {
+			return new(big.Int).Set(history[0])
+		}
 		// make a copy, sort by big.Int.Cmp, then pick the percentile element
 		sorted := make([]*big.Int, n)
 		copy(sorted, history)
```


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In rollup/internal/controller/relayer/l2_relayer.go around lines 1242 to 1257,
the PctMin strategy does not handle empty or very small history arrays, which
can cause a panic. Add a check before processing to return a default value or
handle the case when the history slice is empty or has fewer elements than
expected. This prevents out-of-range errors when copying, sorting, or indexing
the array.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T08:57:23Z
author_association: NONE
diff_hunk: @@ -1118,6 +1198,136 @@ func (r *Layer2Relayer) StopSenders() {
 	}
 }
 
+// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
+// by reading L1Block table’s BlobBaseFee column.
+func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
+	latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
+	if err != nil {
+		return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
+	}
+	// bootstrap on first call
+	if r.lastFetchedBlock == 0 {
+		// start window
+		r.lastFetchedBlock = latest - windowSec/secondsPerBlock
+	}
+	from := r.lastFetchedBlock + 1
+	//if new blocks
+	if from <= latest {
+		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
+		if err != nil {
+			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
+		}
+		// append them
+		for _, v := range raw {
+			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
+			r.lastFetchedBlock++
+		}
+	}
+
+	maxLen := int(windowSec / secondsPerBlock)
+	if len(r.feeHistory) > maxLen {
+		r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
+	}
+
+	return r.feeHistory, nil
+}
path: rollup/internal/controller/relayer/l2_relayer.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding error handling for database consistency.**

The `fetchBlobFeeHistory` method does not verify if the blocks returned from `GetBlobFeesInRange` match the expected range. If the database returns fewer blocks than expected, the `lastFetchedBlock` could get out of sync.


Add validation to ensure database consistency:

```diff
 		raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
 		if err != nil {
 			return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
 		}
+		// Validate that we got the expected number of blocks
+		expectedCount := latest - from + 1
+		if uint64(len(raw)) < expectedCount {
+			log.Warn("Incomplete blob fee history", 
+				"expected", expectedCount, 
+				"received", len(raw),
+				"from", from,
+				"to", latest)
+		}
 		// append them
 		for _, v := range raw {
 			r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
 			r.lastFetchedBlock++
 		}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
// fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
// by reading L1Block table’s BlobBaseFee column.
func (r *Layer2Relayer) fetchBlobFeeHistory(windowSec uint64) ([]*big.Int, error) {
    latest, err := r.l1BlockOrm.GetLatestL1BlockHeight(r.ctx)
    if err != nil {
        return nil, fmt.Errorf("GetLatestL1BlockHeight: %w", err)
    }
    // bootstrap on first call
    if r.lastFetchedBlock == 0 {
        // start window
        r.lastFetchedBlock = latest - windowSec/secondsPerBlock
    }
    from := r.lastFetchedBlock + 1
    // if new blocks
    if from <= latest {
        raw, err := r.l1BlockOrm.GetBlobFeesInRange(r.ctx, from, latest)
        if err != nil {
            return nil, fmt.Errorf("GetBlobFeesInRange: %w", err)
        }
        // Validate that we got the expected number of blocks
        expectedCount := latest - from + 1
        if uint64(len(raw)) < expectedCount {
            log.Warn("Incomplete blob fee history",
                "expected", expectedCount,
                "received", len(raw),
                "from", from,
                "to", latest)
        }
        // append them
        for _, v := range raw {
            r.feeHistory = append(r.feeHistory, new(big.Int).SetUint64(v))
            r.lastFetchedBlock++
        }
    }

    maxLen := int(windowSec / secondsPerBlock)
    if len(r.feeHistory) > maxLen {
        r.feeHistory = r.feeHistory[len(r.feeHistory)-maxLen:]
    }

    return r.feeHistory, nil
}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In rollup/internal/controller/relayer/l2_relayer.go around lines 1201 to 1233,
the fetchBlobFeeHistory method lacks validation to ensure the number of blocks
returned by GetBlobFeesInRange matches the expected range, risking
lastFetchedBlock getting out of sync. Add a check after fetching raw data to
verify its length equals latest - from + 1; if not, return an error indicating
database inconsistency. This prevents updating lastFetchedBlock and feeHistory
with incomplete data.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-19T08:57:23Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-14T16:17:46Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

The changes introduce a configurable, strategy-driven fee submission mechanism for Layer2 batch relaying, including new fee gating logic based on historical L1 blob fees and backlog thresholds. Configuration and ORM logic are updated to support querying batch counts and blob fee histories. The Layer2Relayer is extended with new fields and methods to implement these features.

## Changes

| File(s)                                  | Change Summary                                                                                                                                                                                                                                                                                                                                                                  |
|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| rollup/conf/config.json                  | Increased the `timeout` value for `batch_submission` in the relayer's `l2_config` from 300 seconds to 7200 seconds and added a new `backlog_max` parameter with value 75.                                                                                                                                                                                                       |
| rollup/internal/controller/relayer/l2_relayer.go | Added fee gating mechanism for batch submission using enums `RelaxType`, `BaselineType`, and struct `StrategyParams`. Introduced new fields (`l1BlockOrm`, `lastFetchedBlock`, `feeHistory`, `batchStrategy`) in `Layer2Relayer`. Updated constructor and `ProcessPendingBatches` to incorporate backlog threshold and fee-based gating. Added helper methods for fee history fetching and target price calculation. |
| rollup/internal/orm/batch.go             | Added method `GetFailedAndPendingBatchesCount` to return the count of batches with statuses `RollupCommitFailed` or `RollupPending`.                                                                                                                                                                                                                                            |
| rollup/internal/orm/l1_block.go          | Added method `GetBlobFeesInRange` to query `blob_base_fee` values from L1 blocks within a specified block number range, ordered ascending.                                                                                                                                                                                                                                     |
| rollup/internal/config/relayer.go        | Added new field `BacklogMax` of type `int64` to the `BatchSubmission` struct to represent the maximum allowed backlog size.                                                                                                                                                                                                                                                    |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Relayer
    participant BatchORM
    participant L1BlockORM

    Relayer->>BatchORM: GetFailedAndPendingBatchesCount()
    BatchORM-->>Relayer: Return backlog count

    alt Backlog < BacklogMax
        Relayer->>L1BlockORM: GetBlobFeesInRange(lastFetchedBlock, latestBlock)
        L1BlockORM-->>Relayer: Return blob fee history

        Relayer->>Relayer: calculateTargetPrice(strategy, firstTime, feeHistory)

        alt CurrentFee > TargetPrice and OldestBatch < TimeoutWindow
            Relayer->>Relayer: Skip batch submission
        else
            Relayer->>Relayer: Submit batches
        end
    else Backlog >= BacklogMax
        Relayer->>Relayer: Force submit oldest batches
    end
```

## Poem

> A rabbit hops with code anew,  
> Fee strategies now guide the queue.  
> Backlogs watched and prices weighed,  
> Submission rules are freshly laid.  
> With blobs and batches, swift and keen,  
> The relayer’s logic runs pristine!  
> 🐇✨
"""

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 golangci-lint (1.64.8)</summary>
> 
> level=warning msg="[runner] Can't run linter goanalysis_metalinter: buildir: failed to load package zstd: could not load export data: no export data for \"github.com/scroll-tech/da-codec/encoding/zstd\""
> level=error msg="Running error: can't run linter goanalysis_metalinter\nbuildir: failed to load package zstd: could not load export data: no export data for \"github.com/scroll-tech/da-codec/encoding/zstd\""
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNxU3bABsvkCiQBHbGlcABpIcVwvOkgAIm5KADMACn8vNFlKAEouNFp6REDgjAZKe2wBZiREeHwsRFwqGiJ5AHd1WEgBL0FABAJEkhJIBAb8F3QMelxnUlweCnhS0NiUDEbFbFLkNEhyVvsislK+B0rq2vrG6hIWyESxiNghgBkAJj8SdMyKDRgn+yuzXkShoFCq5GQrSeWAIPAqXiQnQE1AYTxkaEK9DqH1Kay6PQEYAGQyYk3UF2QTSefFwsEw5TOuHEGCIKFYdHg1y8sl+AGkSPImMxuHV2Mh4CUvNglBwjFAAAYDXCogBCBIAYoMABJIAguZLtSb4VoAZVEkGwEtwADYACxZeVcGwkRrwEhSGQEomDexoYXRZCJCgsR5DeVeACMAH1uvgGABreURFTRfhSGn/dINewJBjwRJu+iG2jG1YHUm0RAaAwKhhoLwMbzXaAzF1WBalZINJqQE2Am6yKzOP2IcIFigNaDwNgRackDRTtjhEZ6+QAbQAugAqNREDQASTWDq4AGF6430jRtpBaLIMH7FsmKLN8YI7j7sDUWZASB0yjsEgoXFxFTB4AFEAHUAFkAEEugxT4JRIcIhTUcgiw6dAPnSAAPagLkYbAKCkSBkhIHCRXINZOV8B4aiIZh8HgWgskeahVgYfwEOQAhWmcKZ/mLUslDyBFyF+OAhmmZ8XXmRYhlQEJp2uehANDBQMALIgiJiIT9loIj8OxZFMX4GEqFKCIQirGtIEPZomgIq18EgeV2zjaRECsMhaAlIgVRRNFHUgFUSHufwDjJb8diFKo5iuDBEDQMQLnCWkhjSDIyiFbhnGkdTGwofw8VjAR3yklz0qfF9eHkiZVODUo6GQOpuRQRJ1OJFBtjmB4BE+Usqukl8HnzdTxBnPThgxH90m4TFqwMSSPgcLxmVZfq6QkWo+HwbgJvgAAvfKmAaGRgU+DI/Pg5VOlOKpEBqbFsGo3xSvK7YIsSNAJDGFNkI+Ay82/Z4Ixvag0G9IZyISRLpAk/5Mrw8RsUKkjeHdMVViUAsMHUIZkVugFnAkIysDkH9EqI677vOYz5CqkTaDE6GpCwMbaq2D5mG0LAECIWBFuW2wOKlJRtiwbBuFoa4Ikq/4lG4HpZDYPFplZdMnrxllwglXy63WrC9kgTWCPV7rVl+rwpFocJMHodQLYwfA5h2AQuPja7UUwUhFosSATxYVXcGQBwnBcNx/jQPBYDGcIqBKOkvG4cJnZoVT0kSuX0HyXYSH2F7HavEOyyq8NXijTLQSjYuqyIfAkwLVMBDwS38foJglA+bb866MKxik2PCiss7IFaSghk8RAnloRaYKwaOCB59OA8UUR8AkD4RQoOZ+dgBEBZX2l2Kq3KiaYdM0FIO4HlpVARdQNBuF4fAcOUmg2teW0NGtAAOABScI7RaSQAjBGa0kAWbihhP8b2LJ8oPS/KyC+yhfZ/FQCgqg19UD9R6PsU+wYRBiEAJgELVL4+AUJfa++AOq2gAAwaAAKyvH/vVLe/hChrGvIgA+sA5hKE4iQBC/AOoMPAawnKeUpjywJsI92mBUS/BNLmfMj5MFXyGEQJ+kIJ4oCUNRAsMQJQHHIZAeMAo7jwADLKWy8pgw+ClgAeitJQe8XhHGknWD4SgjjMrfEcV4CufjKAaHrkmaexpkCvF/hoAAzIwiRG9UFDGAZ0X+ABOSBSFkCIOuvbSAAB2HgzhxD1japgug1YFT2KlNwZxaxXH1kcWMZgjjCaKLCdNZADC6F0MSVQlJmEIx0KyRCSAuSWRVNcjUpxLiKBuOaWCAJ0ZYwJlCQ3dAXhEAuTpN0jQvT+nJLHkMkZUDxnnEmZHEkSSsFDF8okDqYcebjH8FIesMRj6uzWrHbAAtQG2liXnfYZzx4RTyEoW2Zk2qxNOdkseuiKn0AprXeO0hvCG2MfSCUgjhHGMQTEM5+Tco7xopQ5JCMhg3LKfMfARC5jqOvgIripkeHwEPm1IMIZ6E/wAdnblzD/6LWdD3UFkAhB1AxOlRSFEOGmS4r5CEPEXJsBkupEWdRLLUtiljcipR9pmSGIgFRBYGA3iQI2R6ZtYDyvhRFDh+BraVL+BYhwRBSANBiIkPAOk2QvykMHQMdEKiFCCOwE2lAtajlFtKPyNiIy/DAjhK4KVooUOJGAKunqXopQlQiXA8hnJYQhYsFSAIKCbHii5I42imw0AUP6ci6hZDhBhvgJBUL5A7HlIgT23ATQVDiiqWQmoSCN2zSjGELkaj+nzPIdpnQX5bHbT0IgixqyvF+DBfI11C7xWsjfDMqBjZdTYHApAzB62UTDbCdMs6UAl02j9HaZYkAOrJtWWJvxDxMAoNvfC34XFEEctiWuxy74wKGJ+bA9ZIBvGwllGkIRyj4zYnMadtbDXGsfKDcq9hcTOFqDZAwAA5FyLtqTjLQOKvgpJjhZ1Go9YIOi7XaFMsYxm5rPxawpTgWkYxSGrBoNREDLlwUWjxnu0eesS1XlQyPelVLsHIBhqII+Llpx+qGGgRIoJHhtq09uidVZnWrwERvLeYwvnbJpdtCFoz4wS2kQ6wNfAvU7wo5gessgaiOfKrQZECYzLybJbcxa+hjDgCgD5ERvHCCkHIE0GI2q1hcF4PwYQqnJD5Qpp3ZQqh1BaB0OFkwUB3DiglrF4gZBlAr2S5wPwaB9hPOcHOwUa8qD5c0NoXQYBDARdMAYGZdTSSJA8XULSGghDbIwLKWIc2DD+xgvuKrCXS3NfGDQxgdJ4GICuXECaJB8B4BWKTKUCkINxHnVGWmlq6grEKDmrAm2qqxECVGEbbKVjBJo+NtlhkJ1jxmti5lnrgwXphSMh7dRKykUYecjAeBpCsVhAU14vTyzQ+QMkd4PyJxZE3YZi4ZTm1G17h97SwGsDEr9C6MosQAvxhXVGHmOEVipKwqd4IMWClw74tsfIHyZG+hnA9idvxSP8HSjR7b7rbVDB5l3WEVVyf/YInRRoFadKLXMJYGCa0asUmzozUQ6RKctQ6q2neMQHieG6I+dg5JpBGFI+JAwc3YhygG0N+poIFmeJqT477ASgmXW+Os2b83FvLfizVmI635Cbbge6yOqApYyzreC686aTIxC0YbU923z1G/+PKZ4CHXjOi+JQJM86GS5OxCutddk5hWmDEDfKuBWguWNmQRwERZAJCjfKSvaAcLQAH6O0ipGR/I1SpARNV7qL1nCCaNljFmKsXyfKAKhQWbj4SEmZIVgxBQQlOESCsEsh20mFheUfZEstCHFQZgiBwka7EJTOs81a3XRzyzfvCQ4Qf+SExSz+tOFAqKuEZMkAWiTgQBLoaA1+9ASM0B+aCQm6rk/UDQT+I4SYPM3AN4YUcKGMhQaG/Ytw1ObAoIyA5imQSKDMc4R2cwekWOrwsA4QjC7BoCbB+Oe2pe5eI+3wb+5aH+0qQmdm7OOwxs4YEYaocY8YAA8mCI3G6F4PQOFLBmDKsvGODNMOgAwEukgZYp8DDuGJKpqLdHQHIQmPKOEIqNqLqGMLILYWwvKPOvftcC0EmErhZDoV1CuE4Wwt2J4fIJQeAcZstKSN2BWg8PKMRvnGXt8BXqHtXqsOSPWEdPlDIdYYocoeEKQcgG4YFB4UCI3GDpgSEDgS/jXghFiBdirv4FMIwXgEYRKOkQiMdIGD6AEeMMmp7JcgYIeK5O5Eut5FFP5IFNIC4f1OFIagOuoIbPOtIGlP8FQbHOoS6KiB3rAkdniJtt9FYjEESj5L/pMb5pIhwvetnG7MlIzvgKyLSBwrHGoaRNvrcSulBKPvKPjs3gelzDdlrO1OpAziuj+DhE1DDkNNatIM8ViHwGNFVA6uLHMLXqgEiWUMfPUb9hTh8s0cwXrMaF+h1GnH8XQLrPSmUoUT2vAH2vMbgEOiOkmFCIsJ0EqFsZSKIGGu9P4Y4RtumONHicckaK0ChOeBhteMNLJF1J+NdPKHWA2Bhi2DJLgO2PJIfsyaiJeojteMAeQLhpyhelVD0fIDKd+IBMBIcfDtOH3uBNBHBPkk/ErG6NeCgQDl6iUADjnnUbNNoriWwFfmwhcdsSSERMVHMF1Erv8JKZoHZB1MrqGWGl1LqoMJCVGa2K7DfoiWoUhqicgLILsaQBmPSENIKXpPkfMbduzKHL2gkLPHtk8MnGUGsYoK5GybAHIQICOjqKMC4EmKGgsMGZoa+IFtMN0EMBoe9Dnh9KXIjD7OOeUVVFmOGZsTPMOToZGUMJeCEHbM/D5MgMeoMEqlhKygqqyGWWwq6C/upMacXtcppH9o0UKSWE1pkTxnKWKVuUqbMKqaUGOh6QRKSOmCXFVDyT2QzC5IkD0NQHaJAAAOLjzwAoQsCeCyZVS6njk+hqTBFAg7lOmDmukETumPbwSmTYiXTzR+nznJQTpGGAWUDAWIxoprTwQjmiZdBsqCY3BlDGIIWLT7gdQgn3FgkQk8RRnQkRIvEPBdR555I+CRJAGBR150xU6NQpnbAyD9z+Di4uRJ75R5SUaK5VpFQPDbbMzXQPCNoNBex1BeJ3B4L8B4A1CK7/AHmaL/qsiN4MB+y6765m63mEEMCm5kzm5gnbwrw27wj27UTiBO62Rbp2a96GkT6uQz776T7s5RHTBcKuTT6XSz51AuHygL6ihL5eCFWr4MRMS0BJjGJ2IOq1I+6NLuL+71WB4pEUDB6VztXrLyhTLxUxCJUAFhg76ITkBpVMmYSZWYAlxuQn4SiFUX4wQ1VYB1UOJ1JzJ+42UB4dVB5vbfY9V9UC4FDv5zB37kGDjDjVFgasmqGmEjV77JX3VITjV2FPXkBVF2X4DQW2iFWwV+g8yfXfWFWhR6GQVfU2g/V2GpXJXQ0H5lirUNUbVNItXeI7XtWdX7VhKHV2akwLD/QVHYGXU5JPxriWhrB2gbglEDhVGv6UbPzXQHZMFPmRKkRsEcFcERg8HZzYUDigE07UHw3e5I3NVbWtVo1V4dV7XdVY1xVHXGEvHZE9AJhKEXpbgtIaCgw5HeFTonWuSJGUDJES3LXTL1WzINLzLI2i2o2+Lo1S0S0HWy12aGJqGFFLkWFbG0A5EWhWh2iFXEjdmriQCbg7hsoHhrAuFb7uHnW9jnU03a1loVp60CHtXG0I1m2+6W1rDbU20S0Y3S0NzY0xDNn0CKjZqkR8Bbj60UCG0IasRtkdldm8myAGgEmmjmhk0Q2sTJDB27hh1hA/jGUUAOiC2m3rXm2bVZ1i050IZ5320y1QD9XqHjoESl0lCMAfnNjpk/kkAt3Clmimod12j5HR1U2P5E1jjwATi4CLhSRzgLhzjLhN1B3bi932SsQh17j2Sp1C3j2Z1eLRDi0z120IYO0L1y3F2tll2pCQCV3J0S2sTUm0mMgMmDDJDonZgHb33+mkQCD4AOotqD3D21U/0Z0i2T3W27Uh5z0F22QACq0spamVIheokDa98RrQVdNd3wyQGgvDXdsDSRghlABDwYQ98dbRpSHRYYkYORKtLhpBmBxR51ZRIYbhlRRNSYtBMQOW2JOkTRbATBrREmNEmRuGN5xDo9jVFtZD/9bVudwDYe89kA9D6eRdLo6xrDpq0DAjBtQjQ91gqlj0Yxp5AUlhiAyQw9sI2KYwf6daglDx4lsJW2og/RrI+SEZ6ZMB7lfcsxN0mpAJTkK1JDTVY25DAD09/iDjISMt7unuYARgxT1jiyrS864ebukeuu0e1WiWBQjgzyCeHUulu2BgcExsED8osFLo6o2g0QtA88tAwTfkoT7JgcL0uATJM04Kgu6kbxt0whFaCMqAED/Zzp6k6eKgwisITAaz6kCOzA/Uu0AlZxY8Q8YYQ2120wuAn4NVymf4fAw+o9gcrA6g0zhx1V/A/zNgo9izLIvVvxn4+UsFChNgUEAA5IUas+HXcMvdiJIa5BBNSJPoFdHMPE3KCOZRBsPJ3i5ENsTF84UL5v4F8/MqJdcjc5sytT7T9ZsnUGeZhPSJQKI0CZgAngYURMgAZAsN+FVP2TyCZhA46QiIOec1OZ4uRBmfQIxEoL4LvplsZJpdDDhKIHgAzTsTc7K9pZLhRp5RC3zVQRGugBFBKFbDbN5ZAHrqCMFf5QIkFUZjFpbuFXwLbgiKag7jFcM1AFBO4y2Vs7QFwKvV4y5FuMs7AKxJM7gKC7M/MzCxMWE5i7gMkGIDhBpDQEmhoIHA0kml3VyyI2MEQ0U5Y8Lc020oFD1e0x7jWF7o27/e4i0ssjGErfGG07U1HitrHr0+HAM1tnOcM6M73OM+mw3YeYeDYHORs/znZhuXrbIYO/s2IIczktG/QCc8qxDFORodSm4QSDGAhFGMSEmJzvlAadu1rTiGMDDlCPpq5Hcw8yoSYXcJSddexv8NilKDUCRAnNfPKGuA0CUjkbwz5DkRuHC8tBwuip9JShQEoI+RTNobsI4A82WBiLiKeRC9hzxhA0y0RFnDsKypZJtvKIfdy8SC1HwAK4PUCTK8EOMAcVskAlQPTdK/8GMGyhKDBoKw8BlTZeq9Br4BKOFMvBcG6x6wbnUEecbsS35ZtgG9bkG5FaG9Fc6XKJAFG3xvQLG/G0RaREm5rYO2my6Eu9ICu3OYW7gMW2q2WxW6W/3bBzvDkS2pMF7Ux13ZuEx7W2IyPWtVYwsn25GAO/IW27U52/U4Nt26Q6U1pBU9U/gBHh7qOzHj0+UJOzFkM0YHO/sM7SXQFAmB8V8TFmgWGFy+uznJu0Lrs6iP2mcJWXuzGWVvLcgSQCQVjFVCztaRej+2UJtrDKR0sUeeYoNzObIjV/cZa+RtlDLvlKCvLnkBVOpNER/kGsZS9KR7ltrgtj5Z6361uz68OH69pxRJZrp3CHbgZ5EEZ7ZOqLdS13QPG9V3cUQJ8cW012WPt6dSm51/XhgN/WlyU+Tllz8GEu23U0YLFHUI4qbGjxjxgMOx0+6106tivPHiVxt8M8tFj8mKyN2NdHzhaAwyvM+7EBIN/IwhoMMisLCIz8z6zxGLEKt1LgoF3A8Da0M3LgZfOEYDru675V69dybrd4bvd2FU98G1FW97FS7uL0l+FgYCVpTFiB1IvHFt04w0HOwFwFQE1n0y1l0G1thyoGoF1kVr1jr5FvWnFFGMxIgF1SKnQB8yUj1n1rr3QhGOkrQBGEoHQmgHQhkgIMwowmgL/K8AILEokLaIwukqjr/LErEgwOn4wrEgUhGDCmVMVq7+kraNaLQKHwwOkuCgUgwLQAUgIHQhn6jmH7QIwgINaAwN9InyoHQjX9H7aAHy7xAJAIwgUiQBGIkLX2jlX7aAILaKn9H2FBGLaBGL/NaNaIMNaN9HQrQNaNpgIL/CQO8KX2P0v7/LaCQOkiQIwiQL/N9BX/kLQHQraLQIkH/Oko/+/x30n7/AYVtDX9f4cOc/lAA77aYQ+toBgBkkYR1g0A8fAQAwFiTJ9U+B/dJP1DASJA6ErwMKCf3SSMJh+YAyAHQkSC/wE+4CZKPkD75F8AU6fOhAUlRyxIlABSGfowl76JAkByUEZMQLQAwprQsfFAUAKT6o5kBaAcPkXwEH39SB6SV4KUC/gJ9GEdCRhI3xH669EgrwP+HQj/i0Bf4rAzvioHoS6Dt+nNMgeknSSxI6AqfBgIXxz6v8eBzvXXgwCAEL92BdoG/qnxIAFI6EYUa0BGFP6h90kiQApLv1eDpIGArwL+OEK74QJiBAgRIAwGtB0JY+JAWJK8HiS0BXgBSX+I31oC2g0A1oZgf5jRy/wf+zCevgIA34dRiBwQ/qPXwBTB9KhsSAgVv3X56CSAAgSIbaAKTMDK+JAa0N4Kb498wY2vJwUHHUAe9Kw3vN0OPFoBRhos+gIAA -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=scroll-tech/scroll&utm_content=1659):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: ranchalp
created_at: 2025-05-14T16:25:41Z
body: Will push new unit tests at `l2_relater_tests.go` tomorrow, but code can already be reviewed.
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-05-14T16:35:04Z
body: ## [Codecov](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) Report
Attention: Patch coverage is `24.67532%` with `116 lines` in your changes missing coverage. Please review.
> Project coverage is 40.52%. Comparing base [(`0d8b00c`)](https://app.codecov.io/gh/scroll-tech/scroll/commit/0d8b00c3de45340b2ade97c29a32170bc3c3abc4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) to head [(`bfc60b5`)](https://app.codecov.io/gh/scroll-tech/scroll/commit/bfc60b5e3235d278d7d4a63ddb2088f4527cb18f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech).
> Report is 1 commits behind head on develop.

| [Files with missing lines](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) | Patch % | Lines |
|---|---|---|
| [rollup/internal/controller/relayer/l2\_relayer.go](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&filepath=rollup%2Finternal%2Fcontroller%2Frelayer%2Fl2_relayer.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech#diff-cm9sbHVwL2ludGVybmFsL2NvbnRyb2xsZXIvcmVsYXllci9sMl9yZWxheWVyLmdv) | 28.35% | [89 Missing and 7 partials :warning: ](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) |
| [rollup/internal/orm/batch.go](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&filepath=rollup%2Finternal%2Form%2Fbatch.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech#diff-cm9sbHVwL2ludGVybmFsL29ybS9iYXRjaC5nbw==) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) |
| [rollup/internal/orm/l1\_block.go](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&filepath=rollup%2Finternal%2Form%2Fl1_block.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech#diff-cm9sbHVwL2ludGVybmFsL29ybS9sMV9ibG9jay5nbw==) | 0.00% | [10 Missing :warning: ](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #1659      +/-   ##
===========================================
- Coverage    40.68%   40.52%   -0.16%     
===========================================
  Files          225      225              
  Lines        18419    18562     +143     
===========================================
+ Hits          7493     7523      +30     
- Misses       10195    10301     +106     
- Partials       731      738       +7     
```

| [Flag](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) | Coverage Δ | |
|---|---|---|
| [rollup](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech) | `48.46% <24.67%> (-0.82%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scroll-tech/scroll/pull/1659?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scroll-tech).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: jonastheis
created_at: 2025-05-19T01:57:14Z
body: I'm okay with merging this once all comments except https://github.com/scroll-tech/scroll/pull/1659#discussion_r2094716744 are resolved. 

Other things we should consider in the next version:
1.  extracting all the fee related functionality into it's own struct to encapsulate the complexity better. The only thing that object needs to expose is the `skipSubmitByFee` function. Everything else can be abstracted/hidden away from the process of processing pending batches. 
2. unit tests for this new component to test the fee mechanism and its complexity in isolation
3. integration tests (the usual l2relayer tests) that incorporate some tests with specific L1 fee scenarios
author_association: CONTRIBUTOR