Skip to content

Commit 443d3db

Browse files
authored
feat: block selection policy (#242)
* chore: refactor into payload.rs and engine.rs * fix: fix tests * chore: clippy * chore: restructure RollupBoost impl * chore: remove unused import * chore: move engine api trait into server * fix: fix imports * fmt: cargo fmt * feat: add block selection policy * feat: implement gas selection policy * fix: block selection logic * feat: add block selection policy to cli args * docs: add comments detailing selection policy * fix: initialize rb server with block selection policy * test: test gas used policy * chore: clippy * fmt: cargo fmt * docs: fix comments * fmt: cargo fmt
1 parent da69eb6 commit 443d3db

File tree

5 files changed

+148
-4
lines changed

5 files changed

+148
-4
lines changed

src/cli.rs

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

1010
use crate::{
11-
DebugClient, ProxyLayer, RollupBoostServer, RpcClient,
11+
BlockSelectionPolicy, DebugClient, ProxyLayer, RollupBoostServer, RpcClient,
1212
client::rpc::{BuilderArgs, L2ClientArgs},
1313
debug_api::ExecutionMode,
1414
init_metrics, init_tracing,
@@ -87,6 +87,9 @@ pub struct Args {
8787
/// Execution mode to start rollup boost with
8888
#[arg(long, env, default_value = "enabled")]
8989
pub execution_mode: ExecutionMode,
90+
91+
#[arg(long, env)]
92+
pub block_selection_policy: Option<BlockSelectionPolicy>,
9093
}
9194

9295
impl Args {
@@ -160,6 +163,7 @@ impl Args {
160163
l2_client,
161164
builder_client,
162165
self.execution_mode,
166+
self.block_selection_policy,
163167
probes,
164168
self.health_check_interval,
165169
self.max_unsafe_interval,

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ pub use health::*;
2727

2828
mod payload;
2929
pub use payload::*;
30+
31+
mod selection;
32+
pub use selection::*;

src/payload.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ impl OpExecutionPayloadEnvelope {
2222
OpExecutionPayloadEnvelope::V4(_) => PayloadVersion::V4,
2323
}
2424
}
25+
26+
pub fn gas_used(&self) -> u64 {
27+
match self {
28+
OpExecutionPayloadEnvelope::V3(payload) => {
29+
payload
30+
.execution_payload
31+
.payload_inner
32+
.payload_inner
33+
.gas_used
34+
}
35+
36+
OpExecutionPayloadEnvelope::V4(payload) => {
37+
payload
38+
.execution_payload
39+
.payload_inner
40+
.payload_inner
41+
.payload_inner
42+
.gas_used
43+
}
44+
}
45+
}
2546
}
2647

2748
impl From<OpExecutionPayloadEnvelope> for ExecutionPayload {
@@ -107,7 +128,7 @@ impl PayloadVersion {
107128
}
108129
}
109130

110-
#[derive(Debug, Clone)]
131+
#[derive(Debug, Clone, PartialEq, Eq)]
111132
pub enum PayloadSource {
112133
L2,
113134
Builder,

src/selection.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use crate::{OpExecutionPayloadEnvelope, PayloadSource};
2+
use serde::{Deserialize, Serialize};
3+
4+
/// Defines the strategy for choosing between the builder block and the L2 client block
5+
/// during block production.
6+
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, clap::ValueEnum)]
7+
pub enum BlockSelectionPolicy {
8+
/// Selects the block based on gas usage.
9+
///
10+
/// If the builder block uses less than 10% of the gas used by the L2 client block,
11+
/// the L2 block is selected instead. This prevents propagation of valid but empty
12+
/// builder blocks and mitigates issues where the builder is not receiving enough
13+
/// transactions due to networking or peering failures.
14+
GasUsed,
15+
}
16+
17+
impl BlockSelectionPolicy {
18+
pub fn select_block(
19+
&self,
20+
builder_payload: OpExecutionPayloadEnvelope,
21+
l2_payload: OpExecutionPayloadEnvelope,
22+
) -> (OpExecutionPayloadEnvelope, PayloadSource) {
23+
match self {
24+
BlockSelectionPolicy::GasUsed => {
25+
let builder_gas = builder_payload.gas_used() as f64;
26+
let l2_gas = l2_payload.gas_used() as f64;
27+
28+
// Select the L2 block if the builder block uses less than 10% of the gas.
29+
// This avoids selecting empty or severely underfilled blocks,
30+
if builder_gas < l2_gas * 0.1 {
31+
(l2_payload, PayloadSource::L2)
32+
} else {
33+
(builder_payload, PayloadSource::Builder)
34+
}
35+
}
36+
}
37+
}
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
44+
use op_alloy_rpc_types_engine::OpExecutionPayloadEnvelopeV4;
45+
46+
#[test]
47+
fn test_gas_used_policy_select_l2_block() -> eyre::Result<()> {
48+
let execution_payload = r#"{"executionPayload":{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0","withdrawalsRoot":"0x123400000000000000000000000000000000000000000000000000000000babe"},"blockValue":"0x0","blobsBundle":{"commitments":[],"proofs":[],"blobs":[]},"shouldOverrideBuilder":false,"parentBeaconBlockRoot":"0xdead00000000000000000000000000000000000000000000000000000000beef","executionRequests":["0xdeadbeef"]}"#;
49+
let mut builder_payload: OpExecutionPayloadEnvelopeV4 =
50+
serde_json::from_str(execution_payload)?;
51+
let mut l2_payload = builder_payload.clone();
52+
53+
let gas_used = 1000000000;
54+
l2_payload
55+
.execution_payload
56+
.payload_inner
57+
.payload_inner
58+
.payload_inner
59+
.gas_used = gas_used;
60+
61+
builder_payload
62+
.execution_payload
63+
.payload_inner
64+
.payload_inner
65+
.payload_inner
66+
.gas_used = (gas_used as f64 * 0.09) as u64;
67+
68+
let builder_payload = OpExecutionPayloadEnvelope::V4(builder_payload);
69+
let l2_payload = OpExecutionPayloadEnvelope::V4(l2_payload);
70+
71+
let selected_payload =
72+
BlockSelectionPolicy::GasUsed.select_block(builder_payload, l2_payload);
73+
74+
assert_eq!(selected_payload.1, PayloadSource::L2);
75+
Ok(())
76+
}
77+
78+
#[test]
79+
fn test_gas_used_policy_select_builder_block() -> eyre::Result<()> {
80+
let execution_payload = r#"{"executionPayload":{"parentHash":"0xe927a1448525fb5d32cb50ee1408461a945ba6c39bd5cf5621407d500ecc8de9","feeRecipient":"0x0000000000000000000000000000000000000000","stateRoot":"0x10f8a0830000e8edef6d00cc727ff833f064b1950afd591ae41357f97e543119","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","prevRandao":"0xe0d8b4521a7da1582a713244ffb6a86aa1726932087386e2dc7973f43fc6cb24","blockNumber":"0x1","gasLimit":"0x2ffbd2","gasUsed":"0x0","timestamp":"0x1235","extraData":"0xd883010d00846765746888676f312e32312e30856c696e7578","baseFeePerGas":"0x342770c0","blockHash":"0x44d0fa5f2f73a938ebb96a2a21679eb8dea3e7b7dd8fd9f35aa756dda8bf0a8a","transactions":[],"withdrawals":[],"blobGasUsed":"0x0","excessBlobGas":"0x0","withdrawalsRoot":"0x123400000000000000000000000000000000000000000000000000000000babe"},"blockValue":"0x0","blobsBundle":{"commitments":[],"proofs":[],"blobs":[]},"shouldOverrideBuilder":false,"parentBeaconBlockRoot":"0xdead00000000000000000000000000000000000000000000000000000000beef","executionRequests":["0xdeadbeef"]}"#;
81+
let mut builder_payload: OpExecutionPayloadEnvelopeV4 =
82+
serde_json::from_str(execution_payload)?;
83+
let mut l2_payload = builder_payload.clone();
84+
85+
let gas_used = 1000000000;
86+
l2_payload
87+
.execution_payload
88+
.payload_inner
89+
.payload_inner
90+
.payload_inner
91+
.gas_used = gas_used;
92+
93+
builder_payload
94+
.execution_payload
95+
.payload_inner
96+
.payload_inner
97+
.payload_inner
98+
.gas_used = (gas_used as f64 * 0.1) as u64;
99+
100+
let builder_payload = OpExecutionPayloadEnvelope::V4(builder_payload);
101+
let l2_payload = OpExecutionPayloadEnvelope::V4(l2_payload);
102+
103+
let selected_payload =
104+
BlockSelectionPolicy::GasUsed.select_block(builder_payload, l2_payload);
105+
106+
assert_eq!(selected_payload.1, PayloadSource::Builder);
107+
Ok(())
108+
}
109+
}

src/server.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::BlockSelectionPolicy;
12
use crate::debug_api::ExecutionMode;
23
use crate::{
34
HealthHandle,
@@ -36,6 +37,7 @@ pub struct RollupBoostServer {
3637
pub l2_client: Arc<RpcClient>,
3738
pub builder_client: Arc<RpcClient>,
3839
pub payload_trace_context: Arc<PayloadTraceContext>,
40+
block_selection_policy: Option<BlockSelectionPolicy>,
3941
health_handle: JoinHandle<()>,
4042
execution_mode: Arc<Mutex<ExecutionMode>>,
4143
probes: Arc<Probes>,
@@ -46,6 +48,7 @@ impl RollupBoostServer {
4648
l2_client: RpcClient,
4749
builder_client: RpcClient,
4850
initial_execution_mode: ExecutionMode,
51+
block_selection_policy: Option<BlockSelectionPolicy>,
4952
probes: Arc<Probes>,
5053
health_check_interval: u64,
5154
max_unsafe_interval: u64,
@@ -61,6 +64,7 @@ impl RollupBoostServer {
6164
Self {
6265
l2_client: Arc::new(l2_client),
6366
builder_client: Arc::new(builder_client),
67+
block_selection_policy,
6468
payload_trace_context: Arc::new(PayloadTraceContext::new()),
6569
execution_mode: Arc::new(Mutex::new(initial_execution_mode)),
6670
probes,
@@ -179,13 +183,15 @@ impl RollupBoostServer {
179183
l2_payload.inspect_err(|_| self.probes.set_health(Health::ServiceUnavailable))?;
180184
self.probes.set_health(Health::Healthy);
181185

182-
if let Ok(Some(payload)) = builder_payload {
186+
if let Ok(Some(builder_payload)) = builder_payload {
183187
// If execution mode is set to DryRun, fallback to the l2_payload,
184188
// otherwise prefer the builder payload
185189
if self.execution_mode().is_dry_run() {
186190
(l2_payload, PayloadSource::L2)
191+
} else if let Some(selection_policy) = &self.block_selection_policy {
192+
selection_policy.select_block(builder_payload, l2_payload)
187193
} else {
188-
(payload, PayloadSource::Builder)
194+
(builder_payload, PayloadSource::Builder)
189195
}
190196
} else {
191197
// Only update the health status if the builder payload fails
@@ -601,6 +607,7 @@ mod tests {
601607
l2_client,
602608
builder_client,
603609
ExecutionMode::Enabled,
610+
None,
604611
probes,
605612
60,
606613
5,

0 commit comments

Comments
 (0)