Skip to content

Add config for block selection #134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ RPC_PORT=8081
DEBUG_HOST=127.0.0.1
DEBUG_SERVER_PORT=5555

# Block Selection Args
BLOCK_SELECTION_STRATEGY=builder

# Extra Args
TRACING=false
LOG_LEVEL=info
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ cargo run -- [OPTIONS]
- `--no-boost-sync`: Disables using the proposer to sync the builder node (default: true)
- `--debug-host <HOST>`: Host to run the server on (default: 127.0.0.1)
- `--debug-server-port <PORT>`: Port to run the debug server on (default: 5555)
- `--execution-mode <EXECUTION_MODE>`: Initial execution mode to run (default: enabled)
- `--block_selection_strategy`: Selection strategy between the builder and l2 block (default: builder)

### Environment Variables

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

## Block Selection Strategy

There are several block selection configurations for rollup-boost if both the builder and local block payloads returned are valid:

- `builder`: the builder payload will always be chosen
- `l2`: the local l2 block will always be chosen
- `gas-used`: a percentage threshold will be used to decide between the builder and local block
- `no-empty-blocks`: if the builder block has no user transactions while the local l2 block does, use the local block

By default rollup-boost will always choose the builder payload.

### Gas Usage Block Selection Strategy

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.

## Maintainers

- [@avalonche](https://github.com/avalonche)
Expand Down
11 changes: 10 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use tokio::signal::unix::{SignalKind, signal as unix_signal};
use tracing::{Level, info};

use crate::{
DebugClient, PayloadSource, ProxyLayer, RollupBoostServer, RpcClient,
BlockSelectionArgs, BlockSelectionConfig, BlockSelector, DebugClient, PayloadSource,
ProxyLayer, RollupBoostServer, RpcClient,
client::rpc::{BuilderArgs, L2ClientArgs},
init_metrics, init_tracing,
server::ExecutionMode,
Expand Down Expand Up @@ -81,6 +82,10 @@ pub struct Args {
/// Execution mode to start rollup boost with
#[arg(long, env, default_value = "enabled")]
pub execution_mode: ExecutionMode,

/// Block selection config
#[clap(flatten)]
pub block_selection: BlockSelectionArgs,
}

impl Args {
Expand Down Expand Up @@ -155,11 +160,15 @@ impl Args {
info!("Boost sync enabled");
}

let block_selection_config = BlockSelectionConfig::from_args(self.block_selection);
let block_selector = BlockSelector::new(block_selection_config);

let rollup_boost = RollupBoostServer::new(
l2_client,
builder_client,
boost_sync_enabled,
self.execution_mode,
block_selector,
);

// Spawn the debug server
Expand Down
184 changes: 176 additions & 8 deletions src/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::client::rpc::RpcClient;
use crate::debug_api::DebugServer;
use alloy_primitives::{B256, Bytes};
use clap::Parser;
use metrics::counter;
use moka::sync::Cache;
use opentelemetry::trace::SpanKind;
Expand Down Expand Up @@ -29,7 +30,7 @@ const CACHE_SIZE: u64 = 100;

pub struct PayloadTraceContext {
block_hash_to_payload_ids: Cache<B256, Vec<PayloadId>>,
payload_id: Cache<PayloadId, (bool, Option<tracing::Id>)>,
payload_id: Cache<PayloadId, (bool, bool, Option<tracing::Id>)>,
}

impl PayloadTraceContext {
Expand All @@ -45,10 +46,11 @@ impl PayloadTraceContext {
payload_id: PayloadId,
parent_hash: B256,
has_attributes: bool,
no_tx_pool: bool,
trace_id: Option<tracing::Id>,
) {
self.payload_id
.insert(payload_id, (has_attributes, trace_id));
.insert(payload_id, (has_attributes, no_tx_pool, trace_id));
self.block_hash_to_payload_ids
.entry(parent_hash)
.and_upsert_with(|o| match o {
Expand All @@ -69,13 +71,13 @@ impl PayloadTraceContext {
.map(|payload_ids| {
payload_ids
.iter()
.filter_map(|payload_id| self.payload_id.get(payload_id).and_then(|x| x.1))
.filter_map(|payload_id| self.payload_id.get(payload_id).and_then(|x| x.2))
.collect()
})
}

fn trace_id(&self, payload_id: &PayloadId) -> Option<tracing::Id> {
self.payload_id.get(payload_id).and_then(|x| x.1)
self.payload_id.get(payload_id).and_then(|x| x.2)
}

fn has_attributes(&self, payload_id: &PayloadId) -> bool {
Expand All @@ -85,6 +87,13 @@ impl PayloadTraceContext {
.unwrap_or_default()
}

fn no_tx_pool(&self, payload_id: &PayloadId) -> bool {
self.payload_id
.get(payload_id)
.map(|x| x.1)
.unwrap_or_default()
}

fn remove_by_parent_hash(&self, block_hash: &B256) {
if let Some(payload_ids) = self.block_hash_to_payload_ids.remove(block_hash) {
for payload_id in payload_ids.iter() {
Expand All @@ -107,6 +116,47 @@ pub enum ExecutionMode {
Fallback,
}

#[derive(Clone, Parser, Debug)]
pub struct BlockSelectionArgs {
#[arg(long, env, default_value = "builder")]
pub block_selection_strategy: BlockSelectionStrategy,
#[arg(long, env)]
pub required_builder_gas_pct: Option<u64>,
}

#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, clap::ValueEnum)]
pub enum BlockSelectionStrategy {
Builder,
L2,
GasUsed,
NoEmptyBlocks,
}

#[derive(Debug, Clone)]
pub enum BlockSelectionConfig {
// Always use the builder's payload if valid
Builder,
// Always use the L2's payload if valid
L2,
// Percentage of L2 gas used the builder payload should be within to use the builder payload
GasUsed { pct: u64 },
// Do not use builder payloads if they are empty
NoEmptyBlocks,
}

impl BlockSelectionConfig {
pub fn from_args(args: BlockSelectionArgs) -> Self {
match args.block_selection_strategy {
BlockSelectionStrategy::Builder => BlockSelectionConfig::Builder,
BlockSelectionStrategy::L2 => BlockSelectionConfig::L2,
BlockSelectionStrategy::GasUsed => BlockSelectionConfig::GasUsed {
pct: args.required_builder_gas_pct.unwrap_or(100),
},
BlockSelectionStrategy::NoEmptyBlocks => BlockSelectionConfig::NoEmptyBlocks,
}
}
}

impl ExecutionMode {
fn is_get_payload_enabled(&self) -> bool {
// get payload is only enabled in 'enabled' mode
Expand All @@ -122,13 +172,73 @@ impl ExecutionMode {
}
}

#[derive(Debug, Clone)]
pub struct BlockSelector {
config: BlockSelectionConfig,
payload_id_to_tx_count: Cache<PayloadId, usize>,
}

impl BlockSelector {
pub fn new(config: BlockSelectionConfig) -> Self {
BlockSelector {
config,
payload_id_to_tx_count: Cache::new(CACHE_SIZE),
}
}

fn store_tx_count(&self, payload_id: PayloadId, tx_count: usize) {
if matches!(self.config, BlockSelectionConfig::NoEmptyBlocks) {
self.payload_id_to_tx_count.insert(payload_id, tx_count);
}
}

// Returns the payload and payload source
fn select_block(
&self,
payload_id: PayloadId,
l2_payload: OpExecutionPayloadEnvelope,
builder_payload: OpExecutionPayloadEnvelope,
) -> (OpExecutionPayloadEnvelope, PayloadSource) {
match self.config {
BlockSelectionConfig::Builder => (builder_payload, PayloadSource::Builder),
BlockSelectionConfig::L2 => (l2_payload, PayloadSource::L2),
BlockSelectionConfig::GasUsed { pct } => {
// If the builder payload gas used is more than or equal to the L2 payload gas used * the percentage, we use the builder payload
if builder_payload.gas_used() * 100 >= l2_payload.gas_used() * pct {
(builder_payload, PayloadSource::Builder)
} else {
(l2_payload, PayloadSource::L2)
}
}
BlockSelectionConfig::NoEmptyBlocks => {
let tx_count = self.payload_id_to_tx_count.get(&payload_id);
if let Some(tx_count) = tx_count {
let builder_tx_count = builder_payload.transactions().len();
let l2_tx_count = l2_payload.transactions().len();
// Builder payload only contains the transactions from the sequencer and the builder transaction
// and considered an empty block as there are no user transactions.
// We use the l2 payload if the builder payload is empty and the l2 payload has user transactions
if builder_tx_count == tx_count + 1 && builder_tx_count < l2_tx_count + 1 {
return (l2_payload, PayloadSource::L2);
}
(builder_payload, PayloadSource::Builder)
} else {
// If payload attributes are not present, we default to the l2 payload
(l2_payload, PayloadSource::L2)
}
}
}
}
}

#[derive(Clone)]
pub struct RollupBoostServer {
pub l2_client: Arc<RpcClient>,
pub builder_client: Arc<RpcClient>,
pub boost_sync: bool,
pub payload_trace_context: Arc<PayloadTraceContext>,
execution_mode: Arc<Mutex<ExecutionMode>>,
block_selector: BlockSelector,
}

impl RollupBoostServer {
Expand All @@ -137,13 +247,15 @@ impl RollupBoostServer {
builder_client: RpcClient,
boost_sync: bool,
initial_execution_mode: ExecutionMode,
block_selector: BlockSelector,
) -> Self {
Self {
l2_client: Arc::new(l2_client),
builder_client: Arc::new(builder_client),
boost_sync,
payload_trace_context: Arc::new(PayloadTraceContext::new()),
execution_mode: Arc::new(Mutex::new(initial_execution_mode)),
block_selector,
}
}

Expand Down Expand Up @@ -284,13 +396,25 @@ impl EngineApiServer for RollupBoostServer {

let execution_mode = self.execution_mode();
let trace_id = span.id();

let has_attributes = payload_attributes.is_some();
let no_tx_pool = payload_attributes
.as_ref()
.is_some_and(|attr| attr.no_tx_pool.unwrap_or_default());
let tx_count = payload_attributes
.as_ref()
.map_or(0, |attr| attr.transactions.as_ref().map_or(0, |t| t.len()));
if let Some(payload_id) = l2_response.payload_id {
self.payload_trace_context.store(
payload_id,
fork_choice_state.head_block_hash,
payload_attributes.is_some(),
has_attributes,
no_tx_pool,
trace_id,
);
if has_attributes {
self.block_selector.store_tx_count(payload_id, tx_count);
}
}

if execution_mode.is_disabled() {
Expand Down Expand Up @@ -421,6 +545,39 @@ impl OpExecutionPayloadEnvelope {
OpExecutionPayloadEnvelope::V4(_) => Version::V4,
}
}

pub fn transactions(&self) -> &[Bytes] {
match self {
OpExecutionPayloadEnvelope::V3(v3) => {
&v3.execution_payload
.payload_inner
.payload_inner
.transactions
}
OpExecutionPayloadEnvelope::V4(v4) => {
&v4.execution_payload
.payload_inner
.payload_inner
.payload_inner
.transactions
}
}
}

pub fn gas_used(&self) -> u64 {
match self {
OpExecutionPayloadEnvelope::V3(v3) => {
v3.execution_payload.payload_inner.payload_inner.gas_used
}
OpExecutionPayloadEnvelope::V4(v4) => {
v4.execution_payload
.payload_inner
.payload_inner
.payload_inner
.gas_used
}
}
}
}

impl From<OpExecutionPayloadEnvelope> for ExecutionPayload {
Expand Down Expand Up @@ -560,8 +717,11 @@ impl RollupBoostServer {
tracing::Span::current().follows_from(cause);
}

if !self.payload_trace_context.has_attributes(&payload_id) {
// block builder won't build a block without attributes
if (!self.payload_trace_context.has_attributes(&payload_id))
|| (self.payload_trace_context.has_attributes(&payload_id)
&& self.payload_trace_context.no_tx_pool(&payload_id))
{
// block builder won't build a block without attributes or if no_tx_pool is true
info!(message = "no attributes found, skipping get_payload call to builder");
return Ok(None);
}
Expand All @@ -587,7 +747,9 @@ impl RollupBoostServer {
// Default to op-geth's payload
Ok((l2_payload, PayloadSource::L2))
} else {
Ok((builder, PayloadSource::Builder))
Ok(self
.block_selector
.select_block(payload_id, l2_payload, builder))
}
}
(_, Ok(l2)) => Ok((l2, PayloadSource::L2)),
Expand Down Expand Up @@ -728,12 +890,18 @@ mod tests {
Uri::from_str(&format!("http://{}:{}", HOST, BUILDER_PORT)).unwrap();
let builder_client =
RpcClient::new(builder_auth_rpc, jwt_secret, 2000, PayloadSource::Builder).unwrap();
let block_selection_config = BlockSelectionConfig::from_args(BlockSelectionArgs {
block_selection_strategy: BlockSelectionStrategy::Builder,
required_builder_gas_pct: None,
});
let block_selector = BlockSelector::new(block_selection_config);

let rollup_boost_client = RollupBoostServer::new(
l2_client,
builder_client,
boost_sync,
ExecutionMode::Enabled,
block_selector,
);

let module: RpcModule<()> = rollup_boost_client.try_into().unwrap();
Expand Down
Loading