1
1
use crate :: client:: rpc:: RpcClient ;
2
2
use crate :: debug_api:: DebugServer ;
3
3
use alloy_primitives:: { B256 , Bytes } ;
4
+ use clap:: Parser ;
4
5
use metrics:: counter;
5
6
use moka:: sync:: Cache ;
6
7
use opentelemetry:: trace:: SpanKind ;
@@ -29,7 +30,7 @@ const CACHE_SIZE: u64 = 100;
29
30
30
31
pub struct PayloadTraceContext {
31
32
block_hash_to_payload_ids : Cache < B256 , Vec < PayloadId > > ,
32
- payload_id : Cache < PayloadId , ( bool , Option < tracing:: Id > ) > ,
33
+ payload_id : Cache < PayloadId , ( bool , bool , Option < tracing:: Id > ) > ,
33
34
}
34
35
35
36
impl PayloadTraceContext {
@@ -45,10 +46,11 @@ impl PayloadTraceContext {
45
46
payload_id : PayloadId ,
46
47
parent_hash : B256 ,
47
48
has_attributes : bool ,
49
+ no_tx_pool : bool ,
48
50
trace_id : Option < tracing:: Id > ,
49
51
) {
50
52
self . payload_id
51
- . insert ( payload_id, ( has_attributes, trace_id) ) ;
53
+ . insert ( payload_id, ( has_attributes, no_tx_pool , trace_id) ) ;
52
54
self . block_hash_to_payload_ids
53
55
. entry ( parent_hash)
54
56
. and_upsert_with ( |o| match o {
@@ -69,13 +71,13 @@ impl PayloadTraceContext {
69
71
. map ( |payload_ids| {
70
72
payload_ids
71
73
. iter ( )
72
- . filter_map ( |payload_id| self . payload_id . get ( payload_id) . and_then ( |x| x. 1 ) )
74
+ . filter_map ( |payload_id| self . payload_id . get ( payload_id) . and_then ( |x| x. 2 ) )
73
75
. collect ( )
74
76
} )
75
77
}
76
78
77
79
fn trace_id ( & self , payload_id : & PayloadId ) -> Option < tracing:: Id > {
78
- self . payload_id . get ( payload_id) . and_then ( |x| x. 1 )
80
+ self . payload_id . get ( payload_id) . and_then ( |x| x. 2 )
79
81
}
80
82
81
83
fn has_attributes ( & self , payload_id : & PayloadId ) -> bool {
@@ -85,6 +87,13 @@ impl PayloadTraceContext {
85
87
. unwrap_or_default ( )
86
88
}
87
89
90
+ fn no_tx_pool ( & self , payload_id : & PayloadId ) -> bool {
91
+ self . payload_id
92
+ . get ( payload_id)
93
+ . map ( |x| x. 1 )
94
+ . unwrap_or_default ( )
95
+ }
96
+
88
97
fn remove_by_parent_hash ( & self , block_hash : & B256 ) {
89
98
if let Some ( payload_ids) = self . block_hash_to_payload_ids . remove ( block_hash) {
90
99
for payload_id in payload_ids. iter ( ) {
@@ -107,6 +116,47 @@ pub enum ExecutionMode {
107
116
Fallback ,
108
117
}
109
118
119
+ #[ derive( Clone , Parser , Debug ) ]
120
+ pub struct BlockSelectionArgs {
121
+ #[ arg( long, env, default_value = "builder" ) ]
122
+ pub block_selection_strategy : BlockSelectionStrategy ,
123
+ #[ arg( long, env) ]
124
+ pub required_builder_gas_pct : Option < u64 > ,
125
+ }
126
+
127
+ #[ derive( Serialize , Deserialize , Debug , Copy , Clone , PartialEq , clap:: ValueEnum ) ]
128
+ pub enum BlockSelectionStrategy {
129
+ Builder ,
130
+ L2 ,
131
+ GasUsed ,
132
+ NoEmptyBlocks ,
133
+ }
134
+
135
+ #[ derive( Debug , Clone ) ]
136
+ pub enum BlockSelectionConfig {
137
+ // Always use the builder's payload if valid
138
+ Builder ,
139
+ // Always use the L2's payload if valid
140
+ L2 ,
141
+ // Percentage of L2 gas used the builder payload should be within to use the builder payload
142
+ GasUsed { pct : u64 } ,
143
+ // Do not use builder payloads if they are empty
144
+ NoEmptyBlocks ,
145
+ }
146
+
147
+ impl BlockSelectionConfig {
148
+ pub fn from_args ( args : BlockSelectionArgs ) -> Self {
149
+ match args. block_selection_strategy {
150
+ BlockSelectionStrategy :: Builder => BlockSelectionConfig :: Builder ,
151
+ BlockSelectionStrategy :: L2 => BlockSelectionConfig :: L2 ,
152
+ BlockSelectionStrategy :: GasUsed => BlockSelectionConfig :: GasUsed {
153
+ pct : args. required_builder_gas_pct . unwrap_or ( 100 ) ,
154
+ } ,
155
+ BlockSelectionStrategy :: NoEmptyBlocks => BlockSelectionConfig :: NoEmptyBlocks ,
156
+ }
157
+ }
158
+ }
159
+
110
160
impl ExecutionMode {
111
161
fn is_get_payload_enabled ( & self ) -> bool {
112
162
// get payload is only enabled in 'enabled' mode
@@ -122,13 +172,73 @@ impl ExecutionMode {
122
172
}
123
173
}
124
174
175
+ #[ derive( Debug , Clone ) ]
176
+ pub struct BlockSelector {
177
+ config : BlockSelectionConfig ,
178
+ payload_id_to_tx_count : Cache < PayloadId , usize > ,
179
+ }
180
+
181
+ impl BlockSelector {
182
+ pub fn new ( config : BlockSelectionConfig ) -> Self {
183
+ BlockSelector {
184
+ config,
185
+ payload_id_to_tx_count : Cache :: new ( CACHE_SIZE ) ,
186
+ }
187
+ }
188
+
189
+ fn store_tx_count ( & self , payload_id : PayloadId , tx_count : usize ) {
190
+ if matches ! ( self . config, BlockSelectionConfig :: NoEmptyBlocks ) {
191
+ self . payload_id_to_tx_count . insert ( payload_id, tx_count) ;
192
+ }
193
+ }
194
+
195
+ // Returns the payload and payload source
196
+ fn select_block (
197
+ & self ,
198
+ payload_id : PayloadId ,
199
+ l2_payload : OpExecutionPayloadEnvelope ,
200
+ builder_payload : OpExecutionPayloadEnvelope ,
201
+ ) -> ( OpExecutionPayloadEnvelope , PayloadSource ) {
202
+ match self . config {
203
+ BlockSelectionConfig :: Builder => ( builder_payload, PayloadSource :: Builder ) ,
204
+ BlockSelectionConfig :: L2 => ( l2_payload, PayloadSource :: L2 ) ,
205
+ BlockSelectionConfig :: GasUsed { pct } => {
206
+ // If the builder payload gas used is more than or equal to the L2 payload gas used * the percentage, we use the builder payload
207
+ if builder_payload. gas_used ( ) * 100 >= l2_payload. gas_used ( ) * pct {
208
+ ( builder_payload, PayloadSource :: Builder )
209
+ } else {
210
+ ( l2_payload, PayloadSource :: L2 )
211
+ }
212
+ }
213
+ BlockSelectionConfig :: NoEmptyBlocks => {
214
+ let tx_count = self . payload_id_to_tx_count . get ( & payload_id) ;
215
+ if let Some ( tx_count) = tx_count {
216
+ let builder_tx_count = builder_payload. transactions ( ) . len ( ) ;
217
+ let l2_tx_count = l2_payload. transactions ( ) . len ( ) ;
218
+ // Builder payload only contains the transactions from the sequencer and the builder transaction
219
+ // and considered an empty block as there are no user transactions.
220
+ // We use the l2 payload if the builder payload is empty and the l2 payload has user transactions
221
+ if builder_tx_count == tx_count + 1 && builder_tx_count < l2_tx_count + 1 {
222
+ return ( l2_payload, PayloadSource :: L2 ) ;
223
+ }
224
+ ( builder_payload, PayloadSource :: Builder )
225
+ } else {
226
+ // If payload attributes are not present, we default to the l2 payload
227
+ ( l2_payload, PayloadSource :: L2 )
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
125
234
#[ derive( Clone ) ]
126
235
pub struct RollupBoostServer {
127
236
pub l2_client : Arc < RpcClient > ,
128
237
pub builder_client : Arc < RpcClient > ,
129
238
pub boost_sync : bool ,
130
239
pub payload_trace_context : Arc < PayloadTraceContext > ,
131
240
execution_mode : Arc < Mutex < ExecutionMode > > ,
241
+ block_selector : BlockSelector ,
132
242
}
133
243
134
244
impl RollupBoostServer {
@@ -137,13 +247,15 @@ impl RollupBoostServer {
137
247
builder_client : RpcClient ,
138
248
boost_sync : bool ,
139
249
initial_execution_mode : ExecutionMode ,
250
+ block_selector : BlockSelector ,
140
251
) -> Self {
141
252
Self {
142
253
l2_client : Arc :: new ( l2_client) ,
143
254
builder_client : Arc :: new ( builder_client) ,
144
255
boost_sync,
145
256
payload_trace_context : Arc :: new ( PayloadTraceContext :: new ( ) ) ,
146
257
execution_mode : Arc :: new ( Mutex :: new ( initial_execution_mode) ) ,
258
+ block_selector,
147
259
}
148
260
}
149
261
@@ -284,13 +396,25 @@ impl EngineApiServer for RollupBoostServer {
284
396
285
397
let execution_mode = self . execution_mode ( ) ;
286
398
let trace_id = span. id ( ) ;
399
+
400
+ let has_attributes = payload_attributes. is_some ( ) ;
401
+ let no_tx_pool = payload_attributes
402
+ . as_ref ( )
403
+ . is_some_and ( |attr| attr. no_tx_pool . unwrap_or_default ( ) ) ;
404
+ let tx_count = payload_attributes
405
+ . as_ref ( )
406
+ . map_or ( 0 , |attr| attr. transactions . as_ref ( ) . map_or ( 0 , |t| t. len ( ) ) ) ;
287
407
if let Some ( payload_id) = l2_response. payload_id {
288
408
self . payload_trace_context . store (
289
409
payload_id,
290
410
fork_choice_state. head_block_hash ,
291
- payload_attributes. is_some ( ) ,
411
+ has_attributes,
412
+ no_tx_pool,
292
413
trace_id,
293
414
) ;
415
+ if has_attributes {
416
+ self . block_selector . store_tx_count ( payload_id, tx_count) ;
417
+ }
294
418
}
295
419
296
420
if execution_mode. is_disabled ( ) {
@@ -421,6 +545,39 @@ impl OpExecutionPayloadEnvelope {
421
545
OpExecutionPayloadEnvelope :: V4 ( _) => Version :: V4 ,
422
546
}
423
547
}
548
+
549
+ pub fn transactions ( & self ) -> & [ Bytes ] {
550
+ match self {
551
+ OpExecutionPayloadEnvelope :: V3 ( v3) => {
552
+ & v3. execution_payload
553
+ . payload_inner
554
+ . payload_inner
555
+ . transactions
556
+ }
557
+ OpExecutionPayloadEnvelope :: V4 ( v4) => {
558
+ & v4. execution_payload
559
+ . payload_inner
560
+ . payload_inner
561
+ . payload_inner
562
+ . transactions
563
+ }
564
+ }
565
+ }
566
+
567
+ pub fn gas_used ( & self ) -> u64 {
568
+ match self {
569
+ OpExecutionPayloadEnvelope :: V3 ( v3) => {
570
+ v3. execution_payload . payload_inner . payload_inner . gas_used
571
+ }
572
+ OpExecutionPayloadEnvelope :: V4 ( v4) => {
573
+ v4. execution_payload
574
+ . payload_inner
575
+ . payload_inner
576
+ . payload_inner
577
+ . gas_used
578
+ }
579
+ }
580
+ }
424
581
}
425
582
426
583
impl From < OpExecutionPayloadEnvelope > for ExecutionPayload {
@@ -560,8 +717,11 @@ impl RollupBoostServer {
560
717
tracing:: Span :: current ( ) . follows_from ( cause) ;
561
718
}
562
719
563
- if !self . payload_trace_context . has_attributes ( & payload_id) {
564
- // block builder won't build a block without attributes
720
+ if ( !self . payload_trace_context . has_attributes ( & payload_id) )
721
+ || ( self . payload_trace_context . has_attributes ( & payload_id)
722
+ && self . payload_trace_context . no_tx_pool ( & payload_id) )
723
+ {
724
+ // block builder won't build a block without attributes or if no_tx_pool is true
565
725
info ! ( message = "no attributes found, skipping get_payload call to builder" ) ;
566
726
return Ok ( None ) ;
567
727
}
@@ -587,7 +747,9 @@ impl RollupBoostServer {
587
747
// Default to op-geth's payload
588
748
Ok ( ( l2_payload, PayloadSource :: L2 ) )
589
749
} else {
590
- Ok ( ( builder, PayloadSource :: Builder ) )
750
+ Ok ( self
751
+ . block_selector
752
+ . select_block ( payload_id, l2_payload, builder) )
591
753
}
592
754
}
593
755
( _, Ok ( l2) ) => Ok ( ( l2, PayloadSource :: L2 ) ) ,
@@ -728,12 +890,18 @@ mod tests {
728
890
Uri :: from_str ( & format ! ( "http://{}:{}" , HOST , BUILDER_PORT ) ) . unwrap ( ) ;
729
891
let builder_client =
730
892
RpcClient :: new ( builder_auth_rpc, jwt_secret, 2000 , PayloadSource :: Builder ) . unwrap ( ) ;
893
+ let block_selection_config = BlockSelectionConfig :: from_args ( BlockSelectionArgs {
894
+ block_selection_strategy : BlockSelectionStrategy :: Builder ,
895
+ required_builder_gas_pct : None ,
896
+ } ) ;
897
+ let block_selector = BlockSelector :: new ( block_selection_config) ;
731
898
732
899
let rollup_boost_client = RollupBoostServer :: new (
733
900
l2_client,
734
901
builder_client,
735
902
boost_sync,
736
903
ExecutionMode :: Enabled ,
904
+ block_selector,
737
905
) ;
738
906
739
907
let module: RpcModule < ( ) > = rollup_boost_client. try_into ( ) . unwrap ( ) ;
0 commit comments