11use crate :: client:: rpc:: RpcClient ;
22use crate :: debug_api:: DebugServer ;
33use alloy_primitives:: { B256 , Bytes } ;
4+ use clap:: Parser ;
45use metrics:: counter;
56use moka:: sync:: Cache ;
67use opentelemetry:: trace:: SpanKind ;
@@ -29,7 +30,7 @@ const CACHE_SIZE: u64 = 100;
2930
3031pub struct PayloadTraceContext {
3132 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 > ) > ,
3334}
3435
3536impl PayloadTraceContext {
@@ -45,10 +46,11 @@ impl PayloadTraceContext {
4546 payload_id : PayloadId ,
4647 parent_hash : B256 ,
4748 has_attributes : bool ,
49+ no_tx_pool : bool ,
4850 trace_id : Option < tracing:: Id > ,
4951 ) {
5052 self . payload_id
51- . insert ( payload_id, ( has_attributes, trace_id) ) ;
53+ . insert ( payload_id, ( has_attributes, no_tx_pool , trace_id) ) ;
5254 self . block_hash_to_payload_ids
5355 . entry ( parent_hash)
5456 . and_upsert_with ( |o| match o {
@@ -69,13 +71,13 @@ impl PayloadTraceContext {
6971 . map ( |payload_ids| {
7072 payload_ids
7173 . 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 ) )
7375 . collect ( )
7476 } )
7577 }
7678
7779 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 )
7981 }
8082
8183 fn has_attributes ( & self , payload_id : & PayloadId ) -> bool {
@@ -85,6 +87,13 @@ impl PayloadTraceContext {
8587 . unwrap_or_default ( )
8688 }
8789
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+
8897 fn remove_by_parent_hash ( & self , block_hash : & B256 ) {
8998 if let Some ( payload_ids) = self . block_hash_to_payload_ids . remove ( block_hash) {
9099 for payload_id in payload_ids. iter ( ) {
@@ -107,6 +116,47 @@ pub enum ExecutionMode {
107116 Fallback ,
108117}
109118
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+
110160impl ExecutionMode {
111161 fn is_get_payload_enabled ( & self ) -> bool {
112162 // get payload is only enabled in 'enabled' mode
@@ -122,13 +172,73 @@ impl ExecutionMode {
122172 }
123173}
124174
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+
125234#[ derive( Clone ) ]
126235pub struct RollupBoostServer {
127236 pub l2_client : Arc < RpcClient > ,
128237 pub builder_client : Arc < RpcClient > ,
129238 pub boost_sync : bool ,
130239 pub payload_trace_context : Arc < PayloadTraceContext > ,
131240 execution_mode : Arc < Mutex < ExecutionMode > > ,
241+ block_selector : BlockSelector ,
132242}
133243
134244impl RollupBoostServer {
@@ -137,13 +247,15 @@ impl RollupBoostServer {
137247 builder_client : RpcClient ,
138248 boost_sync : bool ,
139249 initial_execution_mode : ExecutionMode ,
250+ block_selector : BlockSelector ,
140251 ) -> Self {
141252 Self {
142253 l2_client : Arc :: new ( l2_client) ,
143254 builder_client : Arc :: new ( builder_client) ,
144255 boost_sync,
145256 payload_trace_context : Arc :: new ( PayloadTraceContext :: new ( ) ) ,
146257 execution_mode : Arc :: new ( Mutex :: new ( initial_execution_mode) ) ,
258+ block_selector,
147259 }
148260 }
149261
@@ -284,13 +396,25 @@ impl EngineApiServer for RollupBoostServer {
284396
285397 let execution_mode = self . execution_mode ( ) ;
286398 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 ( ) ) ) ;
287407 if let Some ( payload_id) = l2_response. payload_id {
288408 self . payload_trace_context . store (
289409 payload_id,
290410 fork_choice_state. head_block_hash ,
291- payload_attributes. is_some ( ) ,
411+ has_attributes,
412+ no_tx_pool,
292413 trace_id,
293414 ) ;
415+ if has_attributes {
416+ self . block_selector . store_tx_count ( payload_id, tx_count) ;
417+ }
294418 }
295419
296420 if execution_mode. is_disabled ( ) {
@@ -421,6 +545,39 @@ impl OpExecutionPayloadEnvelope {
421545 OpExecutionPayloadEnvelope :: V4 ( _) => Version :: V4 ,
422546 }
423547 }
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+ }
424581}
425582
426583impl From < OpExecutionPayloadEnvelope > for ExecutionPayload {
@@ -560,8 +717,11 @@ impl RollupBoostServer {
560717 tracing:: Span :: current ( ) . follows_from ( cause) ;
561718 }
562719
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
565725 info ! ( message = "no attributes found, skipping get_payload call to builder" ) ;
566726 return Ok ( None ) ;
567727 }
@@ -587,7 +747,9 @@ impl RollupBoostServer {
587747 // Default to op-geth's payload
588748 Ok ( ( l2_payload, PayloadSource :: L2 ) )
589749 } else {
590- Ok ( ( builder, PayloadSource :: Builder ) )
750+ Ok ( self
751+ . block_selector
752+ . select_block ( payload_id, l2_payload, builder) )
591753 }
592754 }
593755 ( _, Ok ( l2) ) => Ok ( ( l2, PayloadSource :: L2 ) ) ,
@@ -728,12 +890,18 @@ mod tests {
728890 Uri :: from_str ( & format ! ( "http://{}:{}" , HOST , BUILDER_PORT ) ) . unwrap ( ) ;
729891 let builder_client =
730892 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) ;
731898
732899 let rollup_boost_client = RollupBoostServer :: new (
733900 l2_client,
734901 builder_client,
735902 boost_sync,
736903 ExecutionMode :: Enabled ,
904+ block_selector,
737905 ) ;
738906
739907 let module: RpcModule < ( ) > = rollup_boost_client. try_into ( ) . unwrap ( ) ;
0 commit comments