4
4
"context"
5
5
"errors"
6
6
"fmt"
7
+ "math"
7
8
"math/big"
8
9
"sort"
9
10
"strings"
@@ -46,6 +47,7 @@ type Layer2Relayer struct {
46
47
batchOrm * orm.Batch
47
48
chunkOrm * orm.Chunk
48
49
l2BlockOrm * orm.L2Block
50
+ l1BlockOrm * orm.L1Block
49
51
50
52
cfg * config.RelayerConfig
51
53
@@ -63,6 +65,22 @@ type Layer2Relayer struct {
63
65
chainCfg * params.ChainConfig
64
66
}
65
67
68
+ // StrategyParams holds the per‐window fee‐submission rules.
69
+ type StrategyParams struct {
70
+ BaselineType string // "pct_min" or "ewma"
71
+ BaselineParam float64 // percentile (0–1) or α for EWMA
72
+ Gamma float64 // relaxation γ
73
+ Beta float64 // relaxation β
74
+ RelaxType string // "exponential" or "sigmoid"
75
+ }
76
+
77
+ // bestParams maps your 2h/5h/12h windows to their best rules.
78
+ var bestParams = map [uint64 ]StrategyParams {
79
+ 2 * 3600 : {BaselineType : "pct_min" , BaselineParam : 0.10 , Gamma : 0.4 , Beta : 8 , RelaxType : "exponential" },
80
+ 5 * 3600 : {BaselineType : "pct_min" , BaselineParam : 0.30 , Gamma : 0.6 , Beta : 20 , RelaxType : "sigmoid" },
81
+ 12 * 3600 : {BaselineType : "pct_min" , BaselineParam : 0.50 , Gamma : 0.5 , Beta : 20 , RelaxType : "sigmoid" },
82
+ }
83
+
66
84
// NewLayer2Relayer will return a new instance of Layer2RelayerClient
67
85
func NewLayer2Relayer (ctx context.Context , l2Client * ethclient.Client , db * gorm.DB , cfg * config.RelayerConfig , chainCfg * params.ChainConfig , serviceType ServiceType , reg prometheus.Registerer ) (* Layer2Relayer , error ) {
68
86
var commitSender , finalizeSender * sender.Sender
@@ -106,6 +124,7 @@ func NewLayer2Relayer(ctx context.Context, l2Client *ethclient.Client, db *gorm.
106
124
107
125
bundleOrm : orm .NewBundle (db ),
108
126
batchOrm : orm .NewBatch (db ),
127
+ l1BlockOrm : orm .NewL1Block (db ),
109
128
l2BlockOrm : orm .NewL2Block (db ),
110
129
chunkOrm : orm .NewChunk (db ),
111
130
@@ -268,12 +287,58 @@ func (r *Layer2Relayer) commitGenesisBatch(batchHash string, batchHeader []byte,
268
287
// ProcessPendingBatches processes the pending batches by sending commitBatch transactions to layer 1.
269
288
func (r * Layer2Relayer ) ProcessPendingBatches () {
270
289
// get pending batches from database in ascending order by their index.
271
- dbBatches , err := r .batchOrm .GetFailedAndPendingBatches (r .ctx , r . cfg . BatchSubmission . MaxBatches )
290
+ allBatches , err := r .batchOrm .GetFailedAndPendingBatches (r .ctx , 0 )
272
291
if err != nil {
273
292
log .Error ("Failed to fetch pending L2 batches" , "err" , err )
274
293
return
275
294
}
276
295
296
+ // if backlog outgrow max size, force‐submit enough oldest batches
297
+ backlogCount := len (allBatches )
298
+ backlogMax := 75 //r.cfg.BatchSubmission.BacklogMax
299
+
300
+ if len (allBatches ) < r .cfg .BatchSubmission .MinBatches || len (allBatches ) == 0 {
301
+ log .Debug ("Not enough pending batches to submit" , "count" , len (allBatches ), "minBatches" , r .cfg .BatchSubmission .MinBatches , "maxBatches" , r .cfg .BatchSubmission .MaxBatches )
302
+ return
303
+ }
304
+
305
+ // return if not hitting target price
306
+ if backlogCount <= backlogMax {
307
+ strat , ok := bestParams [uint64 (r .cfg .BatchSubmission .TimeoutSec )]
308
+ if ! ok {
309
+ log .Warn ("unknown timeoutSec in bestParams, falling back to immediate submit" ,
310
+ "windowSec" , r .cfg .BatchSubmission .TimeoutSec )
311
+ } else {
312
+ // pull the blob‐fee history
313
+ hist , err := r .fetchBlobFeeHistory (strat .WindowSec )
314
+ if err != nil || len (hist ) == 0 {
315
+ log .Warn ("blob-fee history unavailable or empty; fallback to immediate batch submission" ,
316
+ "err" , err , "history_length" , len (hist ))
317
+ // Proceed immediately with batch submission without further checks
318
+ } else {
319
+ // compute the target
320
+ oldest := allBatches [0 ].CreatedAt
321
+ target := calculateTargetPrice (strat , oldest , hist )
322
+
323
+ // current = most recent sample
324
+ current := hist [len (hist )- 1 ]
325
+
326
+ // deadline
327
+ deadline := time .Duration (strat .WindowSec ) * time .Second
328
+ if current .Cmp (target ) > 0 && time .Since (oldest ) < deadline {
329
+ log .Debug ("blob‐fee above target & window not yet passed; skipping submit" ,
330
+ "current" , current , "target" , target , "age" , time .Since (oldest ))
331
+ return
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ dbBatches := allBatches
338
+ if len (allBatches ) > r .cfg .BatchSubmission .MaxBatches {
339
+ dbBatches = allBatches [:r .cfg .BatchSubmission .MaxBatches ]
340
+ }
341
+
277
342
var batchesToSubmit []* dbBatchWithChunksAndParent
278
343
var forceSubmit bool
279
344
for i , dbBatch := range dbBatches {
@@ -1120,6 +1185,85 @@ func (r *Layer2Relayer) StopSenders() {
1120
1185
}
1121
1186
}
1122
1187
1188
+ // fetchBlobFeeHistory returns the last WindowSec seconds of blob‐fee samples,
1189
+ // by reading L1Block table’s BlobBaseFee column.
1190
+ func (r * Layer2Relayer ) fetchBlobFeeHistory (windowSec uint64 ) ([]* big.Int , error ) {
1191
+ // how many blocks ago? ~12s per block
1192
+ blocksAgo := windowSec / 12
1193
+ latest , err := r .l1BlockOrm .GetLatestL1BlockHeight (r .ctx )
1194
+ if err != nil {
1195
+ return nil , fmt .Errorf ("GetLatestL1BlockHeight: %w" , err )
1196
+ }
1197
+ start := int64 (latest ) - int64 (blocksAgo )
1198
+ if start < 0 {
1199
+ start = 0
1200
+ }
1201
+
1202
+ // pull all L1Blocks in [start .. latest]
1203
+ filters := map [string ]interface {}{
1204
+ "number >= ?" : start ,
1205
+ "number <= ?" : latest ,
1206
+ }
1207
+ recs , err := r .l1BlockOrm .GetL1Blocks (r .ctx , filters )
1208
+ if err != nil {
1209
+ return nil , fmt .Errorf ("GetL1Blocks: %w" , err )
1210
+ }
1211
+ hist := make ([]* big.Int , len (recs ))
1212
+ for i , b := range recs {
1213
+ hist [i ] = new (big.Int ).SetUint64 (b .BlobBaseFee )
1214
+ }
1215
+ return hist , nil
1216
+ }
1217
+
1218
+ // calculateTargetPrice applies pct_min/ewma + relaxation to get a BigInt target
1219
+ func calculateTargetPrice (strat StrategyParams , firstTime time.Time , history []* big.Int ) * big.Int {
1220
+ n := len (history )
1221
+ if n == 0 {
1222
+ return big .NewInt (0 )
1223
+ }
1224
+ // convert to float64 Gwei
1225
+ data := make ([]float64 , n )
1226
+ for i , v := range history {
1227
+ f , _ := new (big.Float ).Quo (new (big.Float ).SetInt (v ), big .NewFloat (1e9 )).Float64 ()
1228
+ data [i ] = f
1229
+ }
1230
+ var baseline float64
1231
+ switch strat .BaselineType {
1232
+ case "pct_min" :
1233
+ sort .Float64s (data )
1234
+ idx := int (strat .BaselineParam * float64 (n - 1 ))
1235
+ if idx < 0 {
1236
+ idx = 0
1237
+ }
1238
+ baseline = data [idx ]
1239
+ case "ewma" :
1240
+ alpha := strat .BaselineParam
1241
+ ewma := data [0 ]
1242
+ for i := 1 ; i < n ; i ++ {
1243
+ ewma = alpha * data [i ] + (1 - alpha )* ewma
1244
+ }
1245
+ baseline = ewma
1246
+ default :
1247
+ baseline = data [n - 1 ]
1248
+ }
1249
+ // relaxation
1250
+ age := time .Since (firstTime ).Seconds ()
1251
+ frac := age / float64 (strat .WindowSec )
1252
+ var adjusted float64
1253
+ switch strat .RelaxType {
1254
+ case "exponential" :
1255
+ adjusted = baseline * (1 + strat .Gamma * math .Exp (strat .Beta * (frac - 1 )))
1256
+ case "sigmoid" :
1257
+ adjusted = baseline * (1 + strat .Gamma / (1 + math .Exp (- strat .Beta * (frac - 0.5 ))))
1258
+ default :
1259
+ adjusted = baseline
1260
+ }
1261
+ // back to wei
1262
+ f := new (big.Float ).Mul (big .NewFloat (adjusted ), big .NewFloat (1e9 ))
1263
+ out , _ := f .Int (nil )
1264
+ return out
1265
+ }
1266
+
1123
1267
func addrFromSignerConfig (config * config.SignerConfig ) (common.Address , error ) {
1124
1268
switch config .SignerType {
1125
1269
case sender .PrivateKeySignerType :
0 commit comments