Skip to content

Commit

Permalink
feat: added extensible clients, refactored builder fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
merklefruit committed Dec 4, 2024
1 parent 60fcdf9 commit 1b25ad4
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 253 deletions.
9 changes: 9 additions & 0 deletions bolt-sidecar/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions bolt-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ clap = { version = "4.5.20", features = ["derive", "env"] }
tokio = { version = "1", features = ["full"] }
axum = { version = "0.7", features = ["macros"] }
tower-http = { version = "0.5.2", features = ["timeout"] }
tower = "0.5.1"
http-body-util = "0.1.2"
axum-extra = "0.9.3"
futures = "0.3"
tokio-retry = "0.3.0"
Expand All @@ -31,11 +33,10 @@ alloy = { version = "0.7.2", features = [
"full",
"provider-trace-api",
"rpc-types-beacon",
"rpc-types-engine",
] }
alloy-rpc-types-engine = { version = "0.7.2", default-features = false, features = [
"jwt",
] }
alloy-rpc-types-engine = { version = "0.7.2", default-features = false, features = ["jwt"] }
alloy-transport-http = { version = "0.7.2", default-features = false, features = ["jwt-auth"] }
alloy-provider = { version = "0.7.2", default-features = false, features = ["engine-api"] }

# reth
reth-primitives = { git = "https://github.com/paradigmxyz/reth", version = "1.1.2" }
Expand Down
15 changes: 5 additions & 10 deletions bolt-sidecar/src/builder/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,9 @@ use alloy::{
consensus::BlockHeader,
eips::{eip2718::Encodable2718, eip4895::Withdrawal},
primitives::{Address, Bloom, B256, U256},
rpc::types::{
engine::{
ExecutionPayload as AlloyExecutionPayload, ExecutionPayloadV1, ExecutionPayloadV2,
ExecutionPayloadV3,
},
Withdrawals,
},
rpc::types::Withdrawals,
};
use alloy_rpc_types_engine::{ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3};
use ethereum_consensus::{
bellatrix::mainnet::Transaction,
capella::spec,
Expand Down Expand Up @@ -81,7 +76,7 @@ pub(crate) fn to_execution_payload_header(
pub(crate) fn to_alloy_execution_payload(
block: &SealedBlock,
block_hash: B256,
) -> AlloyExecutionPayload {
) -> ExecutionPayloadV3 {
let alloy_withdrawals = block
.body
.withdrawals
Expand All @@ -99,7 +94,7 @@ pub(crate) fn to_alloy_execution_payload(
})
.unwrap_or_default();

AlloyExecutionPayload::V3(ExecutionPayloadV3 {
ExecutionPayloadV3 {
blob_gas_used: block.blob_gas_used().unwrap_or_default(),
excess_blob_gas: block.excess_blob_gas.unwrap_or_default(),
payload_inner: ExecutionPayloadV2 {
Expand All @@ -121,7 +116,7 @@ pub(crate) fn to_alloy_execution_payload(
},
withdrawals: alloy_withdrawals,
},
})
}
}

/// Compatibility: convert a sealed block into an ethereum-consensus execution payload
Expand Down
128 changes: 43 additions & 85 deletions bolt-sidecar/src/builder/fallback/engine_hinter.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,49 @@
use std::ops::Deref;

use alloy::{
consensus::EMPTY_OMMER_ROOT_HASH,
primitives::{Address, Bloom, Bytes, B256, B64, U256},
rpc::types::{Block, Withdrawal, Withdrawals},
};
use alloy_rpc_types_engine::{ClientCode, ClientVersionV1, ExecutionPayload, JwtSecret};
use alloy_provider::ext::EngineApi;
use alloy_rpc_types_engine::{ClientCode, ExecutionPayloadV3, JwtSecret, PayloadStatusEnum};
use reqwest::Url;
use reth_primitives::{
BlockBody, Header as RethHeader, SealedBlock, SealedHeader, TransactionSigned,
};
use tracing::debug;

use crate::builder::{compat::to_alloy_execution_payload, BuilderError};
use crate::{
builder::{compat::to_alloy_execution_payload, BuilderError},
client::EngineClient,
};

use super::{engine_hints::parse_hint_from_engine_response, secret_to_bearer_header};
use super::engine_hints::parse_hint_from_engine_response;

/// The [EngineHinter] is responsible for gathering "hints" from the
/// engine API error responses to complete the sealed block.
///
/// Since error messages are not overly standardized across execution clients,
/// we need to know which execution client is being used to properly parse the hints.
///
/// This can be done automatically by querying the EL `eth_g`
/// This can be done by querying the EL `engine_getClientVersionV1` method.
#[derive(Debug)]
pub struct EngineHinter {
client: reqwest::Client,
jwt_hex: String,
engine_rpc_url: Url,
engine_client: EngineClient,
}

impl Deref for EngineHinter {
type Target = EngineClient;

fn deref(&self) -> &Self::Target {
&self.engine_client
}
}

impl EngineHinter {
/// Create a new [EngineHinter] instance with the given JWT and engine RPC URL.
pub fn new(jwt_hex: String, engine_rpc_url: Url) -> Self {
Self { client: reqwest::Client::new(), jwt_hex, engine_rpc_url }
pub fn new(jwt_secret: JwtSecret, engine_rpc_url: Url) -> Self {
Self { engine_client: EngineClient::new_http(engine_rpc_url, jwt_secret) }
}

/// Collect hints from the engine API to complete the sealed block.
Expand All @@ -48,8 +61,10 @@ impl EngineHinter {
// Loop until we get a valid payload from the engine API. On each iteration,
// we build a new block header with the hints from the context and fetch the next hint.
let max_iterations = 20;
let mut iterations = 0;
let mut iteration = 0;
loop {
debug!(%iteration, "Fetching hint from engine API");

// Build a new block header using the hints from the context
let header = ctx.build_block_header_with_hints();

Expand All @@ -62,15 +77,15 @@ impl EngineHinter {
let exec_payload = to_alloy_execution_payload(&sealed_block, block_hash);

// fetch the next hint from the engine API and add it to the context
let hint = self.next_hint(&exec_payload, &ctx).await?;
let hint = self.next_hint(exec_payload, &ctx).await?;
ctx.hints.populate_new(hint);

if matches!(hint, EngineApiHint::ValidPayload) {
return Ok(sealed_block);
}

iterations += 1;
if iterations >= max_iterations {
iteration += 1;
if iteration >= max_iterations {
return Err(BuilderError::Custom(
"Failed to get a valid payload after 20 iterations".to_string(),
));
Expand All @@ -84,90 +99,33 @@ impl EngineHinter {
/// Returns Ok([EngineApiHint::ValidPayload]) if the payload is valid.
async fn next_hint(
&self,
exec_payload: &ExecutionPayload,
exec_payload: ExecutionPayloadV3,
ctx: &EngineHinterContext,
) -> Result<EngineApiHint, BuilderError> {
let raw_response = self
.engine_new_payload(
let payload_status = self
.engine_client
.new_payload_v3(
exec_payload,
&ctx.blob_versioned_hashes,
ctx.blob_versioned_hashes.clone(),
ctx.parent_beacon_block_root,
)
.await?;

// Parse the hint from the engine API response, based on the EL client code
let Some(hint) = parse_hint_from_engine_response(ctx.el_client_code, &raw_response)? else {
// Short-circuit if the payload is valid
if raw_response.contains("\"status\":\"VALID\"") {
return Ok(EngineApiHint::ValidPayload);
let validation_error = match payload_status.status {
PayloadStatusEnum::Valid => return Ok(EngineApiHint::ValidPayload),
PayloadStatusEnum::Invalid { validation_error } => validation_error,
PayloadStatusEnum::Syncing | PayloadStatusEnum::Accepted => {
return Err(BuilderError::UnexpectedPayloadStatus(payload_status.status))
}

return Err(BuilderError::Custom(
"Unexpected: failed to parse any hint from engine response".to_string(),
));
};

Ok(hint)
}

/// Perform an engine API `newPayloadV3` request and return the stringified response.
async fn engine_new_payload(
&self,
exec_payload: &ExecutionPayload,
versioned_hashes: &[B256],
parent_beacon_root: B256,
) -> Result<String, BuilderError> {
let auth_jwt = secret_to_bearer_header(&JwtSecret::from_hex(&self.jwt_hex)?);

let body = format!(
r#"{{"id":1,"jsonrpc":"2.0","method":"engine_newPayloadV3","params":[{}, {}, "{:?}"]}}"#,
serde_json::to_string(&exec_payload)?,
serde_json::to_string(&versioned_hashes)?,
parent_beacon_root
);

Ok(self
.client
.post(self.engine_rpc_url.as_str())
.header("Content-Type", "application/json")
.header("Authorization", auth_jwt)
.body(body)
.send()
.await?
.text()
.await?)
}

/// Fetch the client info from the engine API.
pub async fn engine_client_info(&self) -> Result<Vec<ClientVersionV1>, BuilderError> {
let auth_jwt = secret_to_bearer_header(&JwtSecret::from_hex(&self.jwt_hex)?);

// When calling the `engine_getClientVersionV1` method, the `params` field must contain
// a `ClientVersionV1` object containing the beacon client info to be shared with the EL.
// Ref: <https://github.com/ethereum/execution-apis/blob/main/src/engine/identification.md#clientversionv1>
// TODO: use accurate info from the CL connection instead of mocking this
let cl_info = ClientVersionV1 {
code: ClientCode::LH,
name: "BoltSidecar".to_string(),
version: format!("v{}", env!("CARGO_PKG_VERSION")),
commit: "deadbeef".to_string(),
// Parse the hint from the engine API response, based on the EL client code
let Some(hint) = parse_hint_from_engine_response(ctx.el_client_code, &validation_error)?
else {
return Err(BuilderError::FailedToParseHintsFromEngine);
};

let body = format!(
r#"{{"id":1,"jsonrpc":"2.0","method":"engine_getClientVersionV1","params":[{}]}}"#,
serde_json::to_string(&cl_info)?
);

Ok(self
.client
.post(self.engine_rpc_url.as_str())
.header("Content-Type", "application/json")
.header("Authorization", auth_jwt)
.body(body)
.send()
.await?
.json()
.await?)
Ok(hint)
}
}

Expand Down
5 changes: 5 additions & 0 deletions bolt-sidecar/src/builder/fallback/engine_hints/geth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,10 @@ pub fn parse_geth_engine_error_hint(error: &str) -> Result<Option<EngineApiHint>
return Ok(Some(EngineApiHint::LogsBloom(Bloom::from_hex(&raw_hint_value)?)));
};

// Match some error message that we don't know how to handle
if error.contains("could not apply tx") {
return Err(BuilderError::InvalidTransactions(error.to_string()));
}

Err(BuilderError::UnsupportedEngineHint(error.to_string()))
}
2 changes: 1 addition & 1 deletion bolt-sidecar/src/builder/fallback/engine_hints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub fn parse_hint_from_engine_response(
ClientCode::NM => nethermind::parse_nethermind_engine_error_hint(error),

_ => {
error!("No engine hint parser found for client {}", client.client_name());
error!("Unsupported fallback execution client: {}", client.client_name());
Err(BuilderError::UnsupportedEngineClient(client))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::builder::{fallback::engine_hinter::EngineApiHint, BuilderError};
pub fn parse_nethermind_engine_error_hint(
error: &str,
) -> Result<Option<EngineApiHint>, BuilderError> {
// TODO
dbg!(&error);

Ok(None)
}
Loading

0 comments on commit 1b25ad4

Please sign in to comment.