Skip to content

Commit ce00dac

Browse files
committed
Add block selection config
1 parent ab842a1 commit ce00dac

File tree

4 files changed

+206
-9
lines changed

4 files changed

+206
-9
lines changed

.env.example

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ RPC_PORT=8081
1919
DEBUG_HOST=127.0.0.1
2020
DEBUG_SERVER_PORT=5555
2121

22+
# Block Selection Args
23+
BLOCK_SELECTION_STRATEGY=builder
24+
2225
# Extra Args
2326
TRACING=false
2427
LOG_LEVEL=info

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ cargo run -- [OPTIONS]
2727
- `--no-boost-sync`: Disables using the proposer to sync the builder node (default: true)
2828
- `--debug-host <HOST>`: Host to run the server on (default: 127.0.0.1)
2929
- `--debug-server-port <PORT>`: Port to run the debug server on (default: 5555)
30+
- `--execution-mode <EXECUTION_MODE>`: Initial execution mode to run (default: enabled)
31+
- `--block_selection_strategy`: Selection strategy between the builder and l2 block (default: builder)
3032

3133
### Environment Variables
3234

@@ -193,6 +195,21 @@ To run rollup-boost in debug mode with a specific execution mode, you can use th
193195
rollup-boost debug set-execution-mode [enabled|dry-run|disabled]
194196
```
195197

198+
## Block Selection Strategy
199+
200+
There are several block selection configurations for rollup-boost if both the builder and local block payloads returned are valid:
201+
202+
- `builder`: the builder payload will always be chosen
203+
- `l2`: the local l2 block will always be chosen
204+
- `gas-used`: a percentage threshold will be used to decide between the builder and local block
205+
- `no-empty-blocks`: if the builder block has no user transactions while the local l2 block does, use the local block
206+
207+
By default rollup-boost will always choose the builder payload.
208+
209+
### Gas Usage Block Selection Strategy
210+
211+
If `gas-used` is chosen as the block selection strategy, the `--required-builder-gas-pct` flag may optionally be specified. This is the percentage value of the local l2 block the builder payload should be within for the builder payload to be chosen. For instance, a value of 70 means that the gas used by the builder block should be at least 70% of the local l2 block, otherwise fallback to the local l2 block. By default it will be at 100, meaning that the local block will be chosen if it used more gas than the builder block.
212+
196213
## Maintainers
197214

198215
- [@avalonche](https://github.com/avalonche)

src/cli.rs

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use tokio::signal::unix::{SignalKind, signal as unix_signal};
88
use tracing::{Level, info};
99

1010
use crate::{
11-
DebugClient, PayloadSource, ProxyLayer, RollupBoostServer, RpcClient,
11+
BlockSelectionArgs, BlockSelectionConfig, BlockSelector, DebugClient, PayloadSource,
12+
ProxyLayer, RollupBoostServer, RpcClient,
1213
client::rpc::{BuilderArgs, L2ClientArgs},
1314
init_metrics, init_tracing,
1415
server::ExecutionMode,
@@ -81,6 +82,10 @@ pub struct Args {
8182
/// Execution mode to start rollup boost with
8283
#[arg(long, env, default_value = "enabled")]
8384
pub execution_mode: ExecutionMode,
85+
86+
/// Block selection config
87+
#[clap(flatten)]
88+
pub block_selection: BlockSelectionArgs,
8489
}
8590

8691
impl Args {
@@ -155,11 +160,15 @@ impl Args {
155160
info!("Boost sync enabled");
156161
}
157162

163+
let block_selection_config = BlockSelectionConfig::from_args(self.block_selection);
164+
let block_selector = BlockSelector::new(block_selection_config);
165+
158166
let rollup_boost = RollupBoostServer::new(
159167
l2_client,
160168
builder_client,
161169
boost_sync_enabled,
162170
self.execution_mode,
171+
block_selector,
163172
);
164173

165174
// Spawn the debug server

src/server.rs

+176-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::client::rpc::RpcClient;
22
use crate::debug_api::DebugServer;
33
use alloy_primitives::{B256, Bytes};
4+
use clap::Parser;
45
use metrics::counter;
56
use moka::sync::Cache;
67
use opentelemetry::trace::SpanKind;
@@ -29,7 +30,7 @@ const CACHE_SIZE: u64 = 100;
2930

3031
pub 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

3536
impl 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+
110160
impl 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)]
126235
pub 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

134244
impl 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

426583
impl 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

Comments
 (0)