44 "context"
55 "errors"
66 "fmt"
7+ "math"
78 "math/big"
89 "sort"
910 "strings"
@@ -33,6 +34,32 @@ import (
3334 rutils "scroll-tech/rollup/internal/utils"
3435)
3536
37+ // RelaxType enumerates the relaxation functions we support when
38+ // turning a baseline fee into a “target” fee.
39+ type RelaxType int
40+
41+ const (
42+ // NoRelaxation means “don’t touch the baseline” (i.e. fallback/default).
43+ NoRelaxation RelaxType = iota
44+ Exponential
45+ Sigmoid
46+ )
47+
48+ const secondsPerBlock = 12
49+
50+ // BaselineType enumerates the baseline types we support when
51+ // turning a baseline fee into a “target” fee.
52+ type BaselineType int
53+
54+ const (
55+ // PctMin means “take the minimum of the last N blocks’ fees, then
56+ // take the PCT of that”.
57+ PctMin BaselineType = iota
58+ // EWMA means “take the exponentially‐weighted moving average of
59+ // the last N blocks’ fees”.
60+ EWMA
61+ )
62+
3663// Layer2Relayer is responsible for:
3764// i. committing and finalizing L2 blocks on L1.
3865// ii. updating L2 gas price oracle contract on L1.
@@ -46,6 +73,7 @@ type Layer2Relayer struct {
4673 batchOrm * orm.Batch
4774 chunkOrm * orm.Chunk
4875 l2BlockOrm * orm.L2Block
76+ l1BlockOrm * orm.L1Block
4977
5078 cfg * config.RelayerConfig
5179
@@ -61,6 +89,26 @@ type Layer2Relayer struct {
6189 metrics * l2RelayerMetrics
6290
6391 chainCfg * params.ChainConfig
92+
93+ lastFetchedBlock uint64 // highest block number ever pulled
94+ feeHistory []* big.Int // sliding window of blob fees
95+ batchStrategy StrategyParams
96+ }
97+
98+ // StrategyParams holds the per‐window fee‐submission rules.
99+ type StrategyParams struct {
100+ BaselineType BaselineType // "pct_min" or "ewma"
101+ BaselineParam float64 // percentile (0–1) or α for EWMA
102+ Gamma float64 // relaxation γ
103+ Beta float64 // relaxation β
104+ RelaxType RelaxType // Exponential or Sigmoid
105+ }
106+
107+ // bestParams maps your 2h/5h/12h windows to their best rules.
108+ var bestParams = map [uint64 ]StrategyParams {
109+ 2 * 3600 : {BaselineType : PctMin , BaselineParam : 0.10 , Gamma : 0.4 , Beta : 8 , RelaxType : Exponential },
110+ 5 * 3600 : {BaselineType : PctMin , BaselineParam : 0.30 , Gamma : 0.6 , Beta : 20 , RelaxType : Sigmoid },
111+ 12 * 3600 : {BaselineType : PctMin , BaselineParam : 0.50 , Gamma : 0.5 , Beta : 20 , RelaxType : Sigmoid },
64112}
65113
66114// NewLayer2Relayer will return a new instance of Layer2RelayerClient
@@ -106,6 +154,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
106154
107155 bundleOrm : orm .NewBundle (db ),
108156 batchOrm : orm .NewBatch (db ),
157+ l1BlockOrm : orm .NewL1Block (db ),
109158 l2BlockOrm : orm .NewL2Block (db ),
110159 chunkOrm : orm .NewChunk (db ),
111160
@@ -116,9 +165,9 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
116165 l1RollupABI : bridgeAbi .ScrollChainABI ,
117166
118167 l2GasOracleABI : bridgeAbi .L2GasPriceOracleABI ,
119-
120- cfg : cfg ,
121- chainCfg : chainCfg ,
168+ batchStrategy : bestParams [ uint64 ( cfg . BatchSubmission . TimeoutSec )],
169+ cfg : cfg ,
170+ chainCfg : chainCfg ,
122171 }
123172
124173 // chain_monitor client
@@ -266,6 +315,10 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
266315}
267316
268317// ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
318+ // Pending batchess are submitted if one of the following conditions is met:
319+ // - the first batch is too old -> forceSubmit
320+ // - backlogCount > r.cfg.BatchSubmission.BacklogMax -> forceSubmit
321+ // - we have at least minBatches AND price hits a desired target price
269322func (r * Layer2Relayer ) ProcessPendingBatches () {
270323 // get pending batches from database in ascending order by their index.
271324 dbBatches , err := r .batchOrm .GetFailedAndPendingBatches (r .ctx , r .cfg .BatchSubmission .MaxBatches )
@@ -274,8 +327,40 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
274327 return
275328 }
276329
277- var batchesToSubmit []* dbBatchWithChunksAndParent
330+ // if backlog outgrow max size, force‐submit enough oldest batches
331+ backlogCount , err := r .batchOrm .GetFailedAndPendingBatchesCount (r .ctx )
332+ if err != nil {
333+ log .Error ("Failed to fetch pending L2 batches" , "err" , err )
334+ return
335+ }
336+
278337 var forceSubmit bool
338+
339+ oldestBatchTimestamp := dbBatches [0 ].CreatedAt
340+ // if the batch with the oldest index is too old, we force submit all batches that we have so far in the next step
341+ if r .cfg .BatchSubmission .TimeoutSec > 0 && time .Since (oldestBatchTimestamp ) > time .Duration (r .cfg .BatchSubmission .TimeoutSec )* time .Second {
342+ forceSubmit = true
343+ }
344+
345+ // force submit if backlog is too big
346+ if backlogCount > r .cfg .BatchSubmission .BacklogMax {
347+ forceSubmit = true
348+ }
349+
350+ if ! forceSubmit {
351+ // check if we should skip submitting the batch based on the fee target
352+ skip , err := r .skipSubmitByFee (oldestBatchTimestamp )
353+ // return if not hitting target price
354+ if skip {
355+ log .Debug ("Skipping batch submission" , "reason" , err )
356+ return
357+ }
358+ if err != nil {
359+ log .Warn ("Failed to check if we should skip batch submission, fallback to immediate submission" , "err" , err )
360+ }
361+ }
362+
363+ var batchesToSubmit []* dbBatchWithChunksAndParent
279364 for i , dbBatch := range dbBatches {
280365 if i == 0 && encoding .CodecVersion (dbBatch .CodecVersion ) < encoding .CodecV7 {
281366 // if the first batch is not >= V7 then we need to submit batches one by one
@@ -336,11 +421,6 @@ func (r *Layer2Relayer) ProcessPendingBatches() {
336421 break
337422 }
338423
339- // if one of the batches is too old, we force submit all batches that we have so far in the next step
340- if r .cfg .BatchSubmission .TimeoutSec > 0 && ! forceSubmit && time .Since (dbBatch .CreatedAt ) > time .Duration (r .cfg .BatchSubmission .TimeoutSec )* time .Second {
341- forceSubmit = true
342- }
343-
344424 if batchesToSubmitLen < r .cfg .BatchSubmission .MaxBatches {
345425 batchesToSubmit = append (batchesToSubmit , & dbBatchWithChunksAndParent {
346426 Batch : dbBatch ,
@@ -1118,6 +1198,136 @@ func (r *Layer2Relayer) StopSenders() {
11181198 }
11191199}
11201200
1201+ // fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
1202+ // by reading L1Block table’s BlobBaseFee column.
1203+ func (r * Layer2Relayer ) fetchBlobFeeHistory (windowSec uint64 ) ([]* big.Int , error ) {
1204+ latest , err := r .l1BlockOrm .GetLatestL1BlockHeight (r .ctx )
1205+ if err != nil {
1206+ return nil , fmt .Errorf ("GetLatestL1BlockHeight: %w" , err )
1207+ }
1208+ // bootstrap on first call
1209+ if r .lastFetchedBlock == 0 {
1210+ // start window
1211+ r .lastFetchedBlock = latest - windowSec / secondsPerBlock
1212+ }
1213+ from := r .lastFetchedBlock + 1
1214+ //if new blocks
1215+ if from <= latest {
1216+ raw , err := r .l1BlockOrm .GetBlobFeesInRange (r .ctx , from , latest )
1217+ if err != nil {
1218+ return nil , fmt .Errorf ("GetBlobFeesInRange: %w" , err )
1219+ }
1220+ // append them
1221+ for _ , v := range raw {
1222+ r .feeHistory = append (r .feeHistory , new (big.Int ).SetUint64 (v ))
1223+ r .lastFetchedBlock ++
1224+ }
1225+ }
1226+
1227+ maxLen := int (windowSec / secondsPerBlock )
1228+ if len (r .feeHistory ) > maxLen {
1229+ r .feeHistory = r .feeHistory [len (r .feeHistory )- maxLen :]
1230+ }
1231+
1232+ return r .feeHistory , nil
1233+ }
1234+
1235+ // calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
1236+ func calculateTargetPrice (windowSec uint64 , strategy StrategyParams , firstTime time.Time , history []* big.Int ) * big.Int {
1237+ var baseline float64 // baseline in Gwei (converting to float, small loss of precision)
1238+ n := len (history )
1239+ if n == 0 {
1240+ return big .NewInt (0 )
1241+ }
1242+ switch strategy .BaselineType {
1243+ case PctMin :
1244+ // make a copy, sort by big.Int.Cmp, then pick the percentile element
1245+ sorted := make ([]* big.Int , n )
1246+ copy (sorted , history )
1247+ sort .Slice (sorted , func (i , j int ) bool {
1248+ return sorted [i ].Cmp (sorted [j ]) < 0
1249+ })
1250+ idx := int (strategy .BaselineParam * float64 (n - 1 ))
1251+ if idx < 0 {
1252+ idx = 0
1253+ }
1254+ baseline , _ = new (big.Float ).
1255+ Quo (new (big.Float ).SetInt (sorted [idx ]), big .NewFloat (1e9 )).
1256+ Float64 ()
1257+
1258+ case EWMA :
1259+ one := big .NewFloat (1 )
1260+ alpha := big .NewFloat (strategy .BaselineParam )
1261+ oneMinusAlpha := new (big.Float ).Sub (one , alpha )
1262+
1263+ // start from first history point
1264+ ewma := new (big.Float ).
1265+ Quo (new (big.Float ).SetInt (history [0 ]), big .NewFloat (1e9 ))
1266+
1267+ for i := 1 ; i < n ; i ++ {
1268+ curr := new (big.Float ).
1269+ Quo (new (big.Float ).SetInt (history [i ]), big .NewFloat (1e9 ))
1270+ term1 := new (big.Float ).Mul (alpha , curr )
1271+ term2 := new (big.Float ).Mul (oneMinusAlpha , ewma )
1272+ ewma = new (big.Float ).Add (term1 , term2 )
1273+ }
1274+ baseline , _ = ewma .Float64 ()
1275+
1276+ default :
1277+ // fallback to last element
1278+ baseline , _ = new (big.Float ).
1279+ Quo (new (big.Float ).SetInt (history [n - 1 ]), big .NewFloat (1e9 )).
1280+ Float64 ()
1281+ } // now baseline holds our baseline in float64 Gwei
1282+
1283+ // relaxation
1284+ age := time .Since (firstTime ).Seconds ()
1285+ frac := age / float64 (windowSec )
1286+ var adjusted float64
1287+ switch strategy .RelaxType {
1288+ case Exponential :
1289+ adjusted = baseline * (1 + strategy .Gamma * math .Exp (strategy .Beta * (frac - 1 )))
1290+ case Sigmoid :
1291+ adjusted = baseline * (1 + strategy .Gamma / (1 + math .Exp (- strategy .Beta * (frac - 0.5 ))))
1292+ default :
1293+ adjusted = baseline
1294+ }
1295+ // back to wei
1296+ f := new (big.Float ).Mul (big .NewFloat (adjusted ), big .NewFloat (1e9 ))
1297+ out , _ := f .Int (nil )
1298+ return out
1299+ }
1300+
1301+ // skipSubmitByFee returns (true, nil) when submission should be skipped right now
1302+ // because the blob‐fee is above target and the timeout window hasn’t yet elapsed.
1303+ // Otherwise returns (false, err)
1304+ func (r * Layer2Relayer ) skipSubmitByFee (oldest time.Time ) (bool , error ) {
1305+ windowSec := uint64 (r .cfg .BatchSubmission .TimeoutSec )
1306+
1307+ hist , err := r .fetchBlobFeeHistory (windowSec )
1308+ if err != nil || len (hist ) == 0 {
1309+ return false , fmt .Errorf (
1310+ "blob-fee history unavailable or empty: %w (history_length=%d)" ,
1311+ err , len (hist ),
1312+ )
1313+ }
1314+
1315+ // calculate target & get current (in wei)
1316+ target := calculateTargetPrice (windowSec , r .batchStrategy , oldest , hist )
1317+ current := hist [len (hist )- 1 ]
1318+
1319+ // if current fee > target and still inside the timeout window, skip
1320+ if current .Cmp (target ) > 0 && time .Since (oldest ) < time .Duration (windowSec )* time .Second {
1321+ return true , fmt .Errorf (
1322+ "blob-fee above target & window not yet passed; current=%s target=%s age=%s" ,
1323+ current .String (), target .String (), time .Since (oldest ),
1324+ )
1325+ }
1326+
1327+ // otherwise proceed with submission
1328+ return false , nil
1329+ }
1330+
11211331func addrFromSignerConfig (config * config.SignerConfig ) (common.Address , error ) {
11221332 switch config .SignerType {
11231333 case sender .PrivateKeySignerType :
0 commit comments