Skip to content

Commit a2ba050

Browse files
committed
Add support for multiple builder and block selection
1 parent bbdab15 commit a2ba050

File tree

5 files changed

+119
-73
lines changed

5 files changed

+119
-73
lines changed

Cargo.lock

+19-18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ hyper-util = { version = "0.1", features = ["full"] }
2828
serde_json = "1.0.96"
2929
reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.7" }
3030
reth-optimism-payload-builder = { git = "https://github.com/paradigmxyz/reth.git", tag = "v1.0.7", features = ["optimism"] }
31+
futures = "0.3.31"
3132

3233
[dev-dependencies]
3334
anyhow = "1.0"

src/main.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use tracing_subscriber::EnvFilter;
1818
mod error;
1919
mod proxy;
2020
mod server;
21+
mod selector;
2122

2223
#[derive(Parser, Debug)]
2324
#[clap(author, version, about)]
@@ -45,7 +46,7 @@ struct Args {
4546

4647
/// URL of the builder execution engine
4748
#[arg(long, env)]
48-
builder_url: String,
49+
builder_urls: Vec<String>,
4950

5051
/// Use the proposer to sync the builder node
5152
#[arg(long, env, default_value = "false")]
@@ -112,12 +113,12 @@ async fn main() -> Result<()> {
112113
// Initialize the l2 client
113114
let l2_client = create_client(&args.l2_url, jwt_secret)?;
114115

115-
// Initialize the builder client
116-
let builder_client = create_client(&args.builder_url, builder_jwt_secret)?;
116+
// Initialize the builder clients
117+
let builder_clients = args.builder_urls.iter().map(|url| create_client(url, builder_jwt_secret)).collect::<Result<Vec<_>>>()?;
117118

118119
let eth_engine_api = EthEngineApi::new(
119120
Arc::new(l2_client),
120-
Arc::new(builder_client),
121+
builder_clients.iter().map(|c| Arc::new(c.clone())).collect(),
121122
args.boost_sync,
122123
);
123124
let mut module: RpcModule<()> = RpcModule::new(());

src/selector.rs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use jsonrpsee::core::ClientError;
2+
use op_alloy_rpc_types_engine::OptimismExecutionPayloadEnvelopeV3;
3+
4+
// Define a trait for choosing a payload
5+
pub trait PayloadSelector {
6+
fn select_payload(
7+
&self,
8+
local_payload: Result<OptimismExecutionPayloadEnvelopeV3, ClientError>,
9+
builder_payloads: Vec<Result<OptimismExecutionPayloadEnvelopeV3, ClientError>>,
10+
) -> Result<OptimismExecutionPayloadEnvelopeV3, ClientError>;
11+
}
12+
13+
pub struct DefaultPayloadSelector;
14+
15+
impl PayloadSelector for DefaultPayloadSelector {
16+
fn select_payload(
17+
&self,
18+
local_payload: Result<OptimismExecutionPayloadEnvelopeV3, ClientError>,
19+
builder_payloads: Vec<Result<OptimismExecutionPayloadEnvelopeV3, ClientError>>,
20+
) -> Result<OptimismExecutionPayloadEnvelopeV3, ClientError> {
21+
builder_payloads
22+
.iter()
23+
.filter_map(|payload| payload.as_ref().ok())
24+
.max_by_key(|p| p.block_value)
25+
.map(|p| Ok(p.clone()))
26+
.unwrap_or(local_payload)
27+
}
28+
}

src/server.rs

+66-51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use alloy_rpc_types_engine::{
33
ExecutionPayload, ExecutionPayloadV3, ForkchoiceState, ForkchoiceUpdated, PayloadId,
44
PayloadStatus,
55
};
6+
use futures::future::join_all;
67
use jsonrpsee::core::{async_trait, ClientError, RpcResult};
78
use jsonrpsee::http_client::transport::HttpBackend;
89
use jsonrpsee::http_client::HttpClient;
@@ -16,6 +17,8 @@ use reth_rpc_layer::AuthClientService;
1617
use std::sync::Arc;
1718
use tracing::{error, info};
1819

20+
use crate::selector::{DefaultPayloadSelector, PayloadSelector};
21+
1922
#[rpc(server, client, namespace = "engine")]
2023
pub trait EngineApi {
2124
#[method(name = "forkchoiceUpdatedV3")]
@@ -42,20 +45,22 @@ pub trait EngineApi {
4245

4346
pub struct EthEngineApi<S = AuthClientService<HttpBackend>> {
4447
l2_client: Arc<HttpClient<S>>,
45-
builder_client: Arc<HttpClient<S>>,
48+
builder_clients: Vec<Arc<HttpClient<S>>>,
49+
payload_selector: Arc<dyn PayloadSelector + Send + Sync>,
4650
boost_sync: bool,
4751
}
4852

4953
impl<S> EthEngineApi<S> {
5054
pub fn new(
5155
l2_client: Arc<HttpClient<S>>,
52-
builder_client: Arc<HttpClient<S>>,
56+
builder_clients: Vec<Arc<HttpClient<S>>>,
5357
boost_sync: bool,
5458
) -> Self {
5559
Self {
5660
l2_client,
57-
builder_client,
61+
builder_clients,
5862
boost_sync,
63+
payload_selector: Arc::new(DefaultPayloadSelector),
5964
}
6065
}
6166
}
@@ -85,11 +90,12 @@ impl EngineApiServer for EthEngineApi {
8590
};
8691

8792
if should_send_to_builder {
88-
// async call to builder to trigger payload building and sync
89-
let builder = self.builder_client.clone();
90-
let attr = payload_attributes.clone();
91-
tokio::spawn(async move {
92-
builder.fork_choice_updated_v3(fork_choice_state, attr).await.map(|response| {
93+
// async call to each builder to trigger payload building and sync
94+
for builder in self.builder_clients.iter() {
95+
let builder = builder.clone();
96+
let attr = payload_attributes.clone();
97+
tokio::spawn(async move {
98+
builder.fork_choice_updated_v3(fork_choice_state, attr).await.map(|response| {
9399
let payload_id_str = response.payload_id.map(|id| id.to_string()).unwrap_or_default();
94100
if response.is_invalid() {
95101
error!(message = "builder rejected fork_choice_updated_v3 with attributes", "payload_id" = payload_id_str, "validation_error" = %response.payload_status.status);
@@ -99,7 +105,8 @@ impl EngineApiServer for EthEngineApi {
99105
}).map_err(|e| {
100106
error!(message = "error calling fork_choice_updated_v3 to builder", "error" = %e, "head_block_hash" = %fork_choice_state.head_block_hash);
101107
})
102-
});
108+
});
109+
}
103110
} else {
104111
info!(message = "no payload attributes provided or no_tx_pool is set", "head_block_hash" = %fork_choice_state.head_block_hash);
105112
}
@@ -126,47 +133,53 @@ impl EngineApiServer for EthEngineApi {
126133
) -> RpcResult<OptimismExecutionPayloadEnvelopeV3> {
127134
info!(message = "received get_payload_v3", "payload_id" = %payload_id);
128135
let l2_client_future = self.l2_client.get_payload_v3(payload_id);
129-
let builder_client_future = Box::pin(async {
130-
let payload = self.builder_client.get_payload_v3(payload_id).await.map_err(|e| {
131-
error!(message = "error calling get_payload_v3 from builder", "error" = %e, "payload_id" = %payload_id);
132-
e
133-
})?;
134-
135-
info!(message = "received payload from builder", "payload_id" = %payload_id, "block_hash" = %payload.as_v1_payload().block_hash);
136-
137-
// Send the payload to the local execution engine with engine_newPayload to validate the block from the builder.
138-
// Otherwise, we do not want to risk the network to a halt since op-node will not be able to propose the block.
139-
// If validation fails, return the local block since that one has already been validated.
140-
let payload_status = self.l2_client.new_payload_v3(payload.execution_payload.clone(), vec![], payload.parent_beacon_block_root).await.map_err(|e| {
141-
error!(message = "error calling new_payload_v3 to validate builder payload", "error" = %e, "payload_id" = %payload_id);
142-
e
143-
})?;
144-
if payload_status.is_invalid() {
136+
let builder_client_futures = self.builder_clients.iter().map(|builder| {
137+
let builder = builder.clone();
138+
Box::pin(async move {
139+
let payload = builder.get_payload_v3(payload_id).await.map_err(|e| {
140+
error!(message = "error calling get_payload_v3 from builder", "error" = %e, "payload_id" = %payload_id);
141+
e
142+
})?;
143+
144+
info!(message = "received payload from builder", "payload_id" = %payload_id, "block_hash" = %payload.as_v1_payload().block_hash);
145+
146+
// Send the payload to the local execution engine with engine_newPayload to validate the block from the builder.
147+
// Otherwise, we do not want to risk the network to a halt since op-node will not be able to propose the block.
148+
// If validation fails, return the local block since that one has already been validated.
149+
let payload_status = self.l2_client.new_payload_v3(payload.execution_payload.clone(), vec![], payload.parent_beacon_block_root).await.map_err(|e| {
150+
error!(message = "error calling new_payload_v3 to validate builder payload", "error" = %e, "payload_id" = %payload_id);
151+
e
152+
})?;
153+
if payload_status.is_invalid() {
145154
error!(message = "builder payload was not valid", "payload_status" = %payload_status.status, "payload_id" = %payload_id);
146155
Err(ClientError::Call(ErrorObject::owned(
147156
INVALID_REQUEST_CODE,
148157
"Builder payload was not valid",
149158
None::<String>,
150-
)))
151-
} else {
152-
info!(message = "received payload status from local execution engine validating builder payload", "payload_id" = %payload_id);
153-
Ok(payload)
154-
}
155-
});
156-
157-
let (l2_payload, builder_payload) = tokio::join!(l2_client_future, builder_client_future);
158-
159-
builder_payload.or(l2_payload).map_err(|e| match e {
160-
ClientError::Call(err) => err, // Already an ErrorObjectOwned, so just return it
161-
other_error => {
162-
error!(
163-
message = "error calling get_payload_v3",
164-
"error" = %other_error,
165-
"payload_id" = %payload_id
166-
);
167-
ErrorCode::InternalError.into()
168-
}
169-
})
159+
)))
160+
} else {
161+
info!(message = "received payload status from local execution engine validating builder payload", "payload_id" = %payload_id);
162+
Ok(payload)
163+
}
164+
})
165+
}).collect::<Vec<_>>();
166+
167+
let (l2_payload, builder_payloads) =
168+
tokio::join!(l2_client_future, join_all(builder_client_futures));
169+
170+
self.payload_selector
171+
.select_payload(l2_payload, builder_payloads)
172+
.map_err(|e| match e {
173+
ClientError::Call(err) => err, // Already an ErrorObjectOwned, so just return it
174+
other_error => {
175+
error!(
176+
message = "error calling get_payload_v3",
177+
"error" = %other_error,
178+
"payload_id" = %payload_id
179+
);
180+
ErrorCode::InternalError.into()
181+
}
182+
})
170183
}
171184

172185
async fn new_payload_v3(
@@ -180,11 +193,12 @@ impl EngineApiServer for EthEngineApi {
180193

181194
// async call to builder to sync the builder node
182195
if self.boost_sync {
183-
let builder = self.builder_client.clone();
184-
let builder_payload = payload.clone();
185-
let builder_versioned_hashes = versioned_hashes.clone();
186-
tokio::spawn(async move {
187-
builder.new_payload_v3(builder_payload, builder_versioned_hashes, parent_beacon_block_root).await
196+
for builder in self.builder_clients.iter() {
197+
let builder = builder.clone();
198+
let builder_payload = payload.clone();
199+
let builder_versioned_hashes = versioned_hashes.clone();
200+
tokio::spawn(async move {
201+
builder.new_payload_v3(builder_payload, builder_versioned_hashes, parent_beacon_block_root).await
188202
.map(|response: PayloadStatus| {
189203
if response.is_invalid() {
190204
error!(message = "builder rejected new_payload_v3", "block_hash" = %block_hash);
@@ -195,7 +209,8 @@ impl EngineApiServer for EthEngineApi {
195209
error!(message = "error calling new_payload_v3 to builder", "error" = %e, "block_hash" = %block_hash);
196210
e
197211
})
198-
});
212+
});
213+
}
199214
}
200215

201216
self.l2_client

0 commit comments

Comments
 (0)