Skip to content

Add builder blinded_blocks v2 #7778

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 3 commits into
base: unstable
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
102 changes: 100 additions & 2 deletions beacon_node/builder_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ impl BuilderHttpClient {
}

/// `POST /eth/v1/builder/blinded_blocks` with SSZ serialized request body
pub async fn post_builder_blinded_blocks_ssz<E: EthSpec>(
pub async fn post_builder_blinded_blocks_v1_ssz<E: EthSpec>(
&self,
blinded_block: &SignedBlindedBeaconBlock<E>,
) -> Result<FullPayloadContents<E>, Error> {
Expand Down Expand Up @@ -340,8 +340,58 @@ impl BuilderHttpClient {
.map_err(Error::InvalidSsz)
}

/// `POST /eth/v2/builder/blinded_blocks` with SSZ serialized request body
pub async fn post_builder_blinded_blocks_v2_ssz<E: EthSpec>(
&self,
blinded_block: &SignedBlindedBeaconBlock<E>,
) -> Result<(), Error> {
let mut path = self.server.full.clone();

let body = blinded_block.as_ssz_bytes();

path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("eth")
.push("v2")
.push("builder")
.push("blinded_blocks");

let mut headers = HeaderMap::new();
headers.insert(
CONSENSUS_VERSION_HEADER,
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);
headers.insert(
CONTENT_TYPE_HEADER,
HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER)
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);
headers.insert(
ACCEPT,
HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE)
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);

let result = self
.post_ssz_with_raw_response(
path,
body,
headers,
Some(self.timeouts.post_blinded_blocks),
)
.await?;

if result.status() == StatusCode::ACCEPTED {
Ok(())
} else {
// ACCEPTED is the only valid status code response
Err(Error::StatusCode(result.status()))
}
}

/// `POST /eth/v1/builder/blinded_blocks`
pub async fn post_builder_blinded_blocks<E: EthSpec>(
pub async fn post_builder_blinded_blocks_v1<E: EthSpec>(
&self,
blinded_block: &SignedBlindedBeaconBlock<E>,
) -> Result<ForkVersionedResponse<FullPayloadContents<E>>, Error> {
Expand Down Expand Up @@ -383,6 +433,54 @@ impl BuilderHttpClient {
.await?)
}

/// `POST /eth/v2/builder/blinded_blocks`
pub async fn post_builder_blinded_blocks_v2<E: EthSpec>(
&self,
blinded_block: &SignedBlindedBeaconBlock<E>,
) -> Result<(), Error> {
let mut path = self.server.full.clone();

path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("eth")
.push("v2")
.push("builder")
.push("blinded_blocks");

let mut headers = HeaderMap::new();
headers.insert(
CONSENSUS_VERSION_HEADER,
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);
headers.insert(
CONTENT_TYPE_HEADER,
HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER)
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);
headers.insert(
ACCEPT,
HeaderValue::from_str(JSON_ACCEPT_VALUE)
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
);

let result = self
.post_with_raw_response(
path,
&blinded_block,
headers,
Some(self.timeouts.post_blinded_blocks),
)
.await?;

if result.status() == StatusCode::ACCEPTED {
Ok(())
} else {
// ACCEPTED is the only valid status code response
Err(Error::StatusCode(result.status()))
}
}

/// `GET /eth/v1/builder/header`
pub async fn get_builder_header<E: EthSpec>(
&self,
Expand Down
89 changes: 85 additions & 4 deletions beacon_node/execution_layer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ pub enum FailedCondition {
EpochsSinceFinalization,
}

pub enum SubmitBlindedBlockResponse<E: EthSpec> {
V1(Box<FullPayloadContents<E>>),
V2,
}

type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle<E>>);

struct Inner<E: EthSpec> {
Expand Down Expand Up @@ -1893,26 +1898,42 @@ impl<E: EthSpec> ExecutionLayer<E> {
&self,
block_root: Hash256,
block: &SignedBlindedBeaconBlock<E>,
) -> Result<FullPayloadContents<E>, Error> {
spec: &ChainSpec,
) -> Result<SubmitBlindedBlockResponse<E>, Error> {
debug!(?block_root, "Sending block to builder");
if spec.is_fulu_scheduled() {
Copy link
Member

@jimmygchen jimmygchen Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raised a discussion here on whether to switch at the Fulu fork, or when running the node with Fulu scheduled:
https://discord.com/channels/595666850260713488/1395080509775937636/1397431503448969276

IMO it's safer if the switch happens before the fork.

self.post_builder_blinded_blocks_v2(block_root, block)
.await
.map(|()| SubmitBlindedBlockResponse::V2)
} else {
self.post_builder_blinded_blocks_v1(block_root, block)
.await
.map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload)))
}
}

async fn post_builder_blinded_blocks_v1(
&self,
block_root: Hash256,
block: &SignedBlindedBeaconBlock<E>,
) -> Result<FullPayloadContents<E>, Error> {
if let Some(builder) = self.builder() {
let (payload_result, duration) =
timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
let ssz_enabled = builder.is_ssz_available();
debug!(
?block_root,
ssz = ssz_enabled,
"Calling submit_blinded_block on builder"
"Calling submit_blinded_block v1 on builder"
);
if ssz_enabled {
builder
.post_builder_blinded_blocks_ssz(block)
.post_builder_blinded_blocks_v1_ssz(block)
.await
.map_err(Error::Builder)
} else {
builder
.post_builder_blinded_blocks(block)
.post_builder_blinded_blocks_v1(block)
.await
.map_err(Error::Builder)
.map(|d| d.data)
Expand Down Expand Up @@ -1961,6 +1982,66 @@ impl<E: EthSpec> ExecutionLayer<E> {
Err(Error::NoPayloadBuilder)
}
}

async fn post_builder_blinded_blocks_v2(
&self,
block_root: Hash256,
block: &SignedBlindedBeaconBlock<E>,
) -> Result<(), Error> {
if let Some(builder) = self.builder() {
let (result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
let ssz_enabled = builder.is_ssz_available();
debug!(
?block_root,
ssz = ssz_enabled,
"Calling submit_blinded_block v2 on builder"
);
if ssz_enabled {
builder
.post_builder_blinded_blocks_v2_ssz(block)
.await
.map_err(Error::Builder)
} else {
builder
.post_builder_blinded_blocks_v2(block)
.await
.map_err(Error::Builder)
}
})
.await;

match result {
Ok(()) => {
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME,
&[metrics::SUCCESS],
);
info!(
relay_response_ms = duration.as_millis(),
?block_root,
"Successfully submitted blinded block to the builder"
)
}
Err(e) => {
metrics::inc_counter_vec(
&metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME,
&[metrics::FAILURE],
);
error!(
info = "this may result in a missed block proposal",
error = ?e,
relay_response_ms = duration.as_millis(),
?block_root,
"Failed to submit blinded block to the builder"
)
}
}

Ok(())
} else {
Err(Error::NoPayloadBuilder)
}
}
}

#[derive(AsRefStr)]
Expand Down
54 changes: 36 additions & 18 deletions beacon_node/http_api/src/publish_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use eth2::types::{
BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents,
PublishBlockRequest, SignedBlockContents,
};
use execution_layer::ProvenancedPayload;
use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse};
use futures::TryFutureExt;
use lighthouse_network::{NetworkGlobals, PubsubMessage};
use network::NetworkMessage;
Expand Down Expand Up @@ -636,27 +636,37 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
network_globals: Arc<NetworkGlobals<T::EthSpec>>,
) -> Result<Response, Rejection> {
let block_root = blinded_block.canonical_root();
let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?;
publish_block::<T, _>(
Some(block_root),
full_block,
chain,
network_tx,
validation_level,
duplicate_status_code,
network_globals,
)
.await
let full_block_opt = reconstruct_block(chain.clone(), block_root, blinded_block).await?;

if let Some(full_block) = full_block_opt {
publish_block::<T, _>(
Some(block_root),
full_block,
chain,
network_tx,
validation_level,
duplicate_status_code,
network_globals,
)
.await
} else {
// From the fulu fork, builders are responsible for publishing and
// will no longer return the full payload and blobs.
Ok(warp::reply().into_response())
}
}

/// Deconstruct the given blinded block, and construct a full block. This attempts to use the
/// execution layer's payload cache, and if that misses, attempts a blind block proposal to retrieve
/// the full payload.
///
/// From the Fulu fork, external builders no longer return the full payload and blobs, and this
/// function will always return `Ok(None)` on successful submission of blinded block.
pub async fn reconstruct_block<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
block_root: Hash256,
block: Arc<SignedBlindedBeaconBlock<T::EthSpec>>,
) -> Result<ProvenancedBlock<T, Arc<SignedBeaconBlock<T::EthSpec>>>, Rejection> {
) -> Result<Option<ProvenancedBlock<T, Arc<SignedBeaconBlock<T::EthSpec>>>>, Rejection> {
let full_payload_opt = if let Ok(payload_header) = block.message().body().execution_payload() {
let el = chain.execution_layer.as_ref().ok_or_else(|| {
warp_utils::reject::custom_server_error("Missing execution layer".to_string())
Expand Down Expand Up @@ -696,17 +706,24 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
"builder",
);

let full_payload = el
.propose_blinded_beacon_block(block_root, &block)
match el
.propose_blinded_beacon_block(block_root, &block, &chain.spec)
.await
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"Blind block proposal failed: {:?}",
e
))
})?;
info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network");
ProvenancedPayload::Builder(full_payload)
})? {
SubmitBlindedBlockResponse::V1(full_payload) => {
info!(block_root = ?block_root, "Successfully published a block to the builder network");
ProvenancedPayload::Builder(*full_payload)
}
SubmitBlindedBlockResponse::V2 => {
info!(block_root = ?block_root, "Successfully published a block to the builder network");
return Ok(None);
}
}
};

Some(full_payload_contents)
Expand Down Expand Up @@ -734,6 +751,7 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
.map(|(block, blobs)| ProvenancedBlock::builder(block, blobs))
}
}
.map(Some)
.map_err(|e| {
warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}"))
})
Expand Down
Loading