## Issue Addressed https://github.com/sigp/lighthouse/issues/3091 Extends https://github.com/sigp/lighthouse/pull/3062, adding pre-bellatrix block support on blinded endpoints and allowing the normal proposal flow (local payload construction) on blinded endpoints. This resulted in better fallback logic because the VC will not have to switch endpoints on failure in the BN <> Builder API, the BN can just fallback immediately and without repeating block processing that it shouldn't need to. We can also keep VC fallback from the VC<>BN API's blinded endpoint to full endpoint. ## Proposed Changes - Pre-bellatrix blocks on blinded endpoints - Add a new `PayloadCache` to the execution layer - Better fallback-from-builder logic ## Todos - [x] Remove VC transition logic - [x] Add logic to only enable builder flow after Merge transition finalization - [x] Tests - [x] Fix metrics - [x] Rustdocs Co-authored-by: Mac L <mjladson@pm.me> Co-authored-by: realbigsean <sean@sigmaprime.io>
205 lines
6.2 KiB
Rust
205 lines
6.2 KiB
Rust
use eth2::types::builder_bid::SignedBuilderBid;
|
|
use eth2::types::{
|
|
BlindedPayload, EthSpec, ExecPayload, ExecutionBlockHash, ExecutionPayload,
|
|
ForkVersionedResponse, PublicKeyBytes, SignedBeaconBlock, SignedValidatorRegistrationData,
|
|
Slot,
|
|
};
|
|
pub use eth2::Error;
|
|
use eth2::{ok_or_error, StatusCode};
|
|
use reqwest::{IntoUrl, Response};
|
|
use sensitive_url::SensitiveUrl;
|
|
use serde::de::DeserializeOwned;
|
|
use serde::Serialize;
|
|
use std::time::Duration;
|
|
|
|
pub const DEFAULT_TIMEOUT_MILLIS: u64 = 15000;
|
|
|
|
/// This timeout is in accordance with v0.2.0 of the [builder specs](https://github.com/flashbots/mev-boost/pull/20).
|
|
pub const DEFAULT_GET_HEADER_TIMEOUT_MILLIS: u64 = 1000;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Timeouts {
|
|
get_header: Duration,
|
|
post_validators: Duration,
|
|
post_blinded_blocks: Duration,
|
|
get_builder_status: Duration,
|
|
}
|
|
|
|
impl Default for Timeouts {
|
|
fn default() -> Self {
|
|
Self {
|
|
get_header: Duration::from_millis(DEFAULT_GET_HEADER_TIMEOUT_MILLIS),
|
|
post_validators: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS),
|
|
post_blinded_blocks: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS),
|
|
get_builder_status: Duration::from_millis(DEFAULT_TIMEOUT_MILLIS),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct BuilderHttpClient {
|
|
client: reqwest::Client,
|
|
server: SensitiveUrl,
|
|
timeouts: Timeouts,
|
|
}
|
|
|
|
impl BuilderHttpClient {
|
|
pub fn new(server: SensitiveUrl) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
client: reqwest::Client::new(),
|
|
server,
|
|
timeouts: Timeouts::default(),
|
|
})
|
|
}
|
|
|
|
pub fn new_with_timeouts(server: SensitiveUrl, timeouts: Timeouts) -> Result<Self, Error> {
|
|
Ok(Self {
|
|
client: reqwest::Client::new(),
|
|
server,
|
|
timeouts,
|
|
})
|
|
}
|
|
|
|
async fn get_with_timeout<T: DeserializeOwned, U: IntoUrl>(
|
|
&self,
|
|
url: U,
|
|
timeout: Duration,
|
|
) -> Result<T, Error> {
|
|
self.get_response_with_timeout(url, Some(timeout))
|
|
.await?
|
|
.json()
|
|
.await
|
|
.map_err(Error::Reqwest)
|
|
}
|
|
|
|
/// Perform a HTTP GET request, returning the `Response` for further processing.
|
|
async fn get_response_with_timeout<U: IntoUrl>(
|
|
&self,
|
|
url: U,
|
|
timeout: Option<Duration>,
|
|
) -> Result<Response, Error> {
|
|
let mut builder = self.client.get(url);
|
|
if let Some(timeout) = timeout {
|
|
builder = builder.timeout(timeout);
|
|
}
|
|
let response = builder.send().await.map_err(Error::Reqwest)?;
|
|
ok_or_error(response).await
|
|
}
|
|
|
|
/// Generic POST function supporting arbitrary responses and timeouts.
|
|
async fn post_generic<T: Serialize, U: IntoUrl>(
|
|
&self,
|
|
url: U,
|
|
body: &T,
|
|
timeout: Option<Duration>,
|
|
) -> Result<Response, Error> {
|
|
let mut builder = self.client.post(url);
|
|
if let Some(timeout) = timeout {
|
|
builder = builder.timeout(timeout);
|
|
}
|
|
let response = builder.json(body).send().await?;
|
|
ok_or_error(response).await
|
|
}
|
|
|
|
async fn post_with_raw_response<T: Serialize, U: IntoUrl>(
|
|
&self,
|
|
url: U,
|
|
body: &T,
|
|
timeout: Option<Duration>,
|
|
) -> Result<Response, Error> {
|
|
let mut builder = self.client.post(url);
|
|
if let Some(timeout) = timeout {
|
|
builder = builder.timeout(timeout);
|
|
}
|
|
let response = builder.json(body).send().await.map_err(Error::Reqwest)?;
|
|
ok_or_error(response).await
|
|
}
|
|
|
|
/// `POST /eth/v1/builder/validators`
|
|
pub async fn post_builder_validators(
|
|
&self,
|
|
validator: &[SignedValidatorRegistrationData],
|
|
) -> Result<(), Error> {
|
|
let mut path = self.server.full.clone();
|
|
|
|
path.path_segments_mut()
|
|
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
|
.push("eth")
|
|
.push("v1")
|
|
.push("builder")
|
|
.push("validators");
|
|
|
|
self.post_generic(path, &validator, Some(self.timeouts.post_validators))
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// `POST /eth/v1/builder/blinded_blocks`
|
|
pub async fn post_builder_blinded_blocks<E: EthSpec>(
|
|
&self,
|
|
blinded_block: &SignedBeaconBlock<E, BlindedPayload<E>>,
|
|
) -> Result<ForkVersionedResponse<ExecutionPayload<E>>, Error> {
|
|
let mut path = self.server.full.clone();
|
|
|
|
path.path_segments_mut()
|
|
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
|
.push("eth")
|
|
.push("v1")
|
|
.push("builder")
|
|
.push("blinded_blocks");
|
|
|
|
Ok(self
|
|
.post_with_raw_response(
|
|
path,
|
|
&blinded_block,
|
|
Some(self.timeouts.post_blinded_blocks),
|
|
)
|
|
.await?
|
|
.json()
|
|
.await?)
|
|
}
|
|
|
|
/// `GET /eth/v1/builder/header`
|
|
pub async fn get_builder_header<E: EthSpec, Payload: ExecPayload<E>>(
|
|
&self,
|
|
slot: Slot,
|
|
parent_hash: ExecutionBlockHash,
|
|
pubkey: &PublicKeyBytes,
|
|
) -> Result<Option<ForkVersionedResponse<SignedBuilderBid<E, Payload>>>, Error> {
|
|
let mut path = self.server.full.clone();
|
|
|
|
path.path_segments_mut()
|
|
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
|
.push("eth")
|
|
.push("v1")
|
|
.push("builder")
|
|
.push("header")
|
|
.push(slot.to_string().as_str())
|
|
.push(format!("{parent_hash:?}").as_str())
|
|
.push(pubkey.as_hex_string().as_str());
|
|
|
|
let resp = self.get_with_timeout(path, self.timeouts.get_header).await;
|
|
|
|
if matches!(resp, Err(Error::StatusCode(StatusCode::NO_CONTENT))) {
|
|
Ok(None)
|
|
} else {
|
|
resp.map(Some)
|
|
}
|
|
}
|
|
|
|
/// `GET /eth/v1/builder/status`
|
|
pub async fn get_builder_status<E: EthSpec>(&self) -> Result<(), Error> {
|
|
let mut path = self.server.full.clone();
|
|
|
|
path.path_segments_mut()
|
|
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
|
.push("eth")
|
|
.push("v1")
|
|
.push("builder")
|
|
.push("status");
|
|
|
|
self.get_with_timeout(path, self.timeouts.get_builder_status)
|
|
.await
|
|
}
|
|
}
|