Skip to content

Commit 2196def

Browse files
committed
Add builder blinded_blocks v2 (#7778)
Squashed commit of the following: commit 3b8d8e7 Author: Jimmy Chen <[email protected]> Date: Wed Jul 23 15:23:25 2025 +1000 Fix tests. commit 053d450 Author: Jimmy Chen <[email protected]> Date: Wed Jul 23 15:11:03 2025 +1000 Complete Fulu builder API changes. commit 661ee56 Author: Pawan Dhananjay <[email protected]> Date: Tue Jul 22 18:46:01 2025 -0700 Initial commit
1 parent 4daa015 commit 2196def

File tree

4 files changed

+226
-26
lines changed

4 files changed

+226
-26
lines changed

beacon_node/builder_client/src/lib.rs

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ impl BuilderHttpClient {
293293
}
294294

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

343+
/// `POST /eth/v2/builder/blinded_blocks` with SSZ serialized request body
344+
pub async fn post_builder_blinded_blocks_v2_ssz<E: EthSpec>(
345+
&self,
346+
blinded_block: &SignedBlindedBeaconBlock<E>,
347+
) -> Result<(), Error> {
348+
let mut path = self.server.full.clone();
349+
350+
let body = blinded_block.as_ssz_bytes();
351+
352+
path.path_segments_mut()
353+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
354+
.push("eth")
355+
.push("v2")
356+
.push("builder")
357+
.push("blinded_blocks");
358+
359+
let mut headers = HeaderMap::new();
360+
headers.insert(
361+
CONSENSUS_VERSION_HEADER,
362+
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
363+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
364+
);
365+
headers.insert(
366+
CONTENT_TYPE_HEADER,
367+
HeaderValue::from_str(SSZ_CONTENT_TYPE_HEADER)
368+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
369+
);
370+
headers.insert(
371+
ACCEPT,
372+
HeaderValue::from_str(PREFERENCE_ACCEPT_VALUE)
373+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
374+
);
375+
376+
let result = self
377+
.post_ssz_with_raw_response(
378+
path,
379+
body,
380+
headers,
381+
Some(self.timeouts.post_blinded_blocks),
382+
)
383+
.await?;
384+
385+
if result.status() == StatusCode::ACCEPTED {
386+
Ok(())
387+
} else {
388+
// ACCEPTED is the only valid status code response
389+
Err(Error::StatusCode(result.status()))
390+
}
391+
}
392+
343393
/// `POST /eth/v1/builder/blinded_blocks`
344-
pub async fn post_builder_blinded_blocks<E: EthSpec>(
394+
pub async fn post_builder_blinded_blocks_v1<E: EthSpec>(
345395
&self,
346396
blinded_block: &SignedBlindedBeaconBlock<E>,
347397
) -> Result<ForkVersionedResponse<FullPayloadContents<E>>, Error> {
@@ -383,6 +433,54 @@ impl BuilderHttpClient {
383433
.await?)
384434
}
385435

436+
/// `POST /eth/v2/builder/blinded_blocks`
437+
pub async fn post_builder_blinded_blocks_v2<E: EthSpec>(
438+
&self,
439+
blinded_block: &SignedBlindedBeaconBlock<E>,
440+
) -> Result<(), Error> {
441+
let mut path = self.server.full.clone();
442+
443+
path.path_segments_mut()
444+
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
445+
.push("eth")
446+
.push("v2")
447+
.push("builder")
448+
.push("blinded_blocks");
449+
450+
let mut headers = HeaderMap::new();
451+
headers.insert(
452+
CONSENSUS_VERSION_HEADER,
453+
HeaderValue::from_str(&blinded_block.fork_name_unchecked().to_string())
454+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
455+
);
456+
headers.insert(
457+
CONTENT_TYPE_HEADER,
458+
HeaderValue::from_str(JSON_CONTENT_TYPE_HEADER)
459+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
460+
);
461+
headers.insert(
462+
ACCEPT,
463+
HeaderValue::from_str(JSON_ACCEPT_VALUE)
464+
.map_err(|e| Error::InvalidHeaders(format!("{}", e)))?,
465+
);
466+
467+
let result = self
468+
.post_with_raw_response(
469+
path,
470+
&blinded_block,
471+
headers,
472+
Some(self.timeouts.post_blinded_blocks),
473+
)
474+
.await?;
475+
476+
if result.status() == StatusCode::ACCEPTED {
477+
Ok(())
478+
} else {
479+
// ACCEPTED is the only valid status code response
480+
Err(Error::StatusCode(result.status()))
481+
}
482+
}
483+
386484
/// `GET /eth/v1/builder/header`
387485
pub async fn get_builder_header<E: EthSpec>(
388486
&self,

beacon_node/execution_layer/src/lib.rs

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,11 @@ pub enum FailedCondition {
411411
EpochsSinceFinalization,
412412
}
413413

414+
pub enum SubmitBlindedBlockResponse<E: EthSpec> {
415+
V1(Box<FullPayloadContents<E>>),
416+
V2,
417+
}
418+
414419
type PayloadContentsRefTuple<'a, E> = (ExecutionPayloadRef<'a, E>, Option<&'a BlobsBundle<E>>);
415420

416421
struct Inner<E: EthSpec> {
@@ -1893,26 +1898,42 @@ impl<E: EthSpec> ExecutionLayer<E> {
18931898
&self,
18941899
block_root: Hash256,
18951900
block: &SignedBlindedBeaconBlock<E>,
1896-
) -> Result<FullPayloadContents<E>, Error> {
1901+
spec: &ChainSpec,
1902+
) -> Result<SubmitBlindedBlockResponse<E>, Error> {
18971903
debug!(?block_root, "Sending block to builder");
1904+
if spec.is_fulu_scheduled() {
1905+
self.post_builder_blinded_blocks_v2(block_root, block)
1906+
.await
1907+
.map(|()| SubmitBlindedBlockResponse::V2)
1908+
} else {
1909+
self.post_builder_blinded_blocks_v1(block_root, block)
1910+
.await
1911+
.map(|full_payload| SubmitBlindedBlockResponse::V1(Box::new(full_payload)))
1912+
}
1913+
}
18981914

1915+
async fn post_builder_blinded_blocks_v1(
1916+
&self,
1917+
block_root: Hash256,
1918+
block: &SignedBlindedBeaconBlock<E>,
1919+
) -> Result<FullPayloadContents<E>, Error> {
18991920
if let Some(builder) = self.builder() {
19001921
let (payload_result, duration) =
19011922
timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
19021923
let ssz_enabled = builder.is_ssz_available();
19031924
debug!(
19041925
?block_root,
19051926
ssz = ssz_enabled,
1906-
"Calling submit_blinded_block on builder"
1927+
"Calling submit_blinded_block v1 on builder"
19071928
);
19081929
if ssz_enabled {
19091930
builder
1910-
.post_builder_blinded_blocks_ssz(block)
1931+
.post_builder_blinded_blocks_v1_ssz(block)
19111932
.await
19121933
.map_err(Error::Builder)
19131934
} else {
19141935
builder
1915-
.post_builder_blinded_blocks(block)
1936+
.post_builder_blinded_blocks_v1(block)
19161937
.await
19171938
.map_err(Error::Builder)
19181939
.map(|d| d.data)
@@ -1961,6 +1982,66 @@ impl<E: EthSpec> ExecutionLayer<E> {
19611982
Err(Error::NoPayloadBuilder)
19621983
}
19631984
}
1985+
1986+
async fn post_builder_blinded_blocks_v2(
1987+
&self,
1988+
block_root: Hash256,
1989+
block: &SignedBlindedBeaconBlock<E>,
1990+
) -> Result<(), Error> {
1991+
if let Some(builder) = self.builder() {
1992+
let (result, duration) = timed_future(metrics::POST_BLINDED_PAYLOAD_BUILDER, async {
1993+
let ssz_enabled = builder.is_ssz_available();
1994+
debug!(
1995+
?block_root,
1996+
ssz = ssz_enabled,
1997+
"Calling submit_blinded_block v2 on builder"
1998+
);
1999+
if ssz_enabled {
2000+
builder
2001+
.post_builder_blinded_blocks_v2_ssz(block)
2002+
.await
2003+
.map_err(Error::Builder)
2004+
} else {
2005+
builder
2006+
.post_builder_blinded_blocks_v2(block)
2007+
.await
2008+
.map_err(Error::Builder)
2009+
}
2010+
})
2011+
.await;
2012+
2013+
match result {
2014+
Ok(()) => {
2015+
metrics::inc_counter_vec(
2016+
&metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME,
2017+
&[metrics::SUCCESS],
2018+
);
2019+
info!(
2020+
relay_response_ms = duration.as_millis(),
2021+
?block_root,
2022+
"Successfully submitted blinded block to the builder"
2023+
)
2024+
}
2025+
Err(e) => {
2026+
metrics::inc_counter_vec(
2027+
&metrics::EXECUTION_LAYER_BUILDER_REVEAL_PAYLOAD_OUTCOME,
2028+
&[metrics::FAILURE],
2029+
);
2030+
error!(
2031+
info = "this may result in a missed block proposal",
2032+
error = ?e,
2033+
relay_response_ms = duration.as_millis(),
2034+
?block_root,
2035+
"Failed to submit blinded block to the builder"
2036+
)
2037+
}
2038+
}
2039+
2040+
Ok(())
2041+
} else {
2042+
Err(Error::NoPayloadBuilder)
2043+
}
2044+
}
19642045
}
19652046

19662047
#[derive(AsRefStr)]

beacon_node/http_api/src/publish_blocks.rs

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use eth2::types::{
1313
BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents,
1414
PublishBlockRequest, SignedBlockContents,
1515
};
16-
use execution_layer::ProvenancedPayload;
16+
use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse};
1717
use futures::TryFutureExt;
1818
use lighthouse_network::{NetworkGlobals, PubsubMessage};
1919
use network::NetworkMessage;
@@ -636,27 +636,37 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
636636
network_globals: Arc<NetworkGlobals<T::EthSpec>>,
637637
) -> Result<Response, Rejection> {
638638
let block_root = blinded_block.canonical_root();
639-
let full_block = reconstruct_block(chain.clone(), block_root, blinded_block).await?;
640-
publish_block::<T, _>(
641-
Some(block_root),
642-
full_block,
643-
chain,
644-
network_tx,
645-
validation_level,
646-
duplicate_status_code,
647-
network_globals,
648-
)
649-
.await
639+
let full_block_opt = reconstruct_block(chain.clone(), block_root, blinded_block).await?;
640+
641+
if let Some(full_block) = full_block_opt {
642+
publish_block::<T, _>(
643+
Some(block_root),
644+
full_block,
645+
chain,
646+
network_tx,
647+
validation_level,
648+
duplicate_status_code,
649+
network_globals,
650+
)
651+
.await
652+
} else {
653+
// From the fulu fork, builders are responsible for publishing and
654+
// will no longer return the full payload and blobs.
655+
Ok(warp::reply().into_response())
656+
}
650657
}
651658

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

699-
let full_payload = el
700-
.propose_blinded_beacon_block(block_root, &block)
709+
match el
710+
.propose_blinded_beacon_block(block_root, &block, &chain.spec)
701711
.await
702712
.map_err(|e| {
703713
warp_utils::reject::custom_server_error(format!(
704714
"Blind block proposal failed: {:?}",
705715
e
706716
))
707-
})?;
708-
info!(block_hash = ?full_payload.block_hash(), "Successfully published a block to the builder network");
709-
ProvenancedPayload::Builder(full_payload)
717+
})? {
718+
SubmitBlindedBlockResponse::V1(full_payload) => {
719+
info!(block_root = ?block_root, "Successfully published a block to the builder network");
720+
ProvenancedPayload::Builder(*full_payload)
721+
}
722+
SubmitBlindedBlockResponse::V2 => {
723+
info!(block_root = ?block_root, "Successfully published a block to the builder network");
724+
return Ok(None);
725+
}
726+
}
710727
};
711728

712729
Some(full_payload_contents)
@@ -734,6 +751,7 @@ pub async fn reconstruct_block<T: BeaconChainTypes>(
734751
.map(|(block, blobs)| ProvenancedBlock::builder(block, blobs))
735752
}
736753
}
754+
.map(Some)
737755
.map_err(|e| {
738756
warp_utils::reject::custom_server_error(format!("Unable to add payload to block: {e:?}"))
739757
})

beacon_node/http_api/tests/broadcast_validation_tests.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,14 +1275,17 @@ pub async fn blinded_equivocation_consensus_late_equivocation() {
12751275
Arc::new(block_a),
12761276
)
12771277
.await
1278-
.unwrap();
1278+
.expect("failed to reconstruct block")
1279+
.expect("block expected");
1280+
12791281
let unblinded_block_b = reconstruct_block(
12801282
tester.harness.chain.clone(),
12811283
block_b.canonical_root(),
12821284
block_b.clone(),
12831285
)
12841286
.await
1285-
.unwrap();
1287+
.expect("failed to reconstruct block")
1288+
.expect("block expected");
12861289

12871290
let inner_block_a = match unblinded_block_a {
12881291
ProvenancedBlock::Local(a, _, _) => a,

0 commit comments

Comments
 (0)