Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
40 changes: 36 additions & 4 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ use eth2::types::{
self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceExtraData,
ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, ValidatorId,
};
use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER};
use eth2::{
BUILDER_URL_HEADER, CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER,
};
use health_metrics::observe::Observe;
use lighthouse_network::Enr;
use lighthouse_network::NetworkGlobals;
Expand Down Expand Up @@ -106,7 +108,7 @@ use types::{
};
use validator::execution_payload_envelopes::get_validator_execution_payload_envelopes;
use version::{
ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header,
ResponseIncludesVersion, V1, V2, V4, add_consensus_version_header, add_ssz_content_type_header,
execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection,
unsupported_version_rejection,
};
Expand Down Expand Up @@ -384,6 +386,7 @@ pub async fn serve<T: BeaconChainTypes>(

let eth_v1 = single_version(any_version.clone(), V1);
let eth_v2 = single_version(any_version.clone(), V2);
let eth_v4 = single_version(any_version.clone(), V4);

// Create a `warp` filter that provides access to the network globals.
let inner_network_globals = ctx.network_globals.clone();
Expand Down Expand Up @@ -819,6 +822,9 @@ pub async fn serve<T: BeaconChainTypes>(
*/
let consensus_version_header_filter =
warp::header::header::<ForkName>(CONSENSUS_VERSION_HEADER).boxed();
// The winning builder's URL echoed by the VC on a Gloas block publish (beacon-APIs #630), so the
// node forwards the block to that builder. Optional: absent for self-build / p2p-won blocks.
let builder_url_header_filter = warp::header::optional::<String>(BUILDER_URL_HEADER).boxed();

let optional_consensus_version_header_filter =
warp::header::optional::<ForkName>(CONSENSUS_VERSION_HEADER).boxed();
Expand Down Expand Up @@ -855,6 +861,8 @@ pub async fn serve<T: BeaconChainTypes>(
&network_tx,
BroadcastValidation::default(),
duplicate_block_status_code,
// Legacy v1 publish: no builder-URL provenance (VC uses v2 for Gloas).
None,
)
.await
})
Expand Down Expand Up @@ -892,6 +900,8 @@ pub async fn serve<T: BeaconChainTypes>(
&network_tx,
BroadcastValidation::default(),
duplicate_block_status_code,
// Legacy v1 publish: no builder-URL provenance (VC uses v2 for Gloas).
None,
)
.await
})
Expand All @@ -909,13 +919,15 @@ pub async fn serve<T: BeaconChainTypes>(
.and(task_spawner_filter.clone())
.and(chain_filter.clone())
.and(network_tx_filter.clone())
.and(builder_url_header_filter.clone())
.then(
move |validation_level: api_types::BroadcastValidationQuery,
value: serde_json::Value,
consensus_version: ForkName,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| {
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
builder_url: Option<String>| {
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
let request = PublishBlockRequest::<T::EthSpec>::context_deserialize(
&value,
Expand All @@ -932,6 +944,7 @@ pub async fn serve<T: BeaconChainTypes>(
&network_tx,
validation_level.broadcast_validation,
duplicate_block_status_code,
builder_url,
)
.await
})
Expand All @@ -949,13 +962,15 @@ pub async fn serve<T: BeaconChainTypes>(
.and(task_spawner_filter.clone())
.and(chain_filter.clone())
.and(network_tx_filter.clone())
.and(builder_url_header_filter.clone())
.then(
move |validation_level: api_types::BroadcastValidationQuery,
block_bytes: Bytes,
consensus_version: ForkName,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| {
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>,
builder_url: Option<String>| {
task_spawner.spawn_async_with_rejection(Priority::P0, async move {
let block_contents = PublishBlockRequest::<T::EthSpec>::from_ssz_bytes(
&block_bytes,
Expand All @@ -971,6 +986,7 @@ pub async fn serve<T: BeaconChainTypes>(
&network_tx,
validation_level.broadcast_validation,
duplicate_block_status_code,
builder_url,
)
.await
})
Expand Down Expand Up @@ -2570,6 +2586,14 @@ pub async fn serve<T: BeaconChainTypes>(
task_spawner_filter.clone(),
);

// POST v4/validator/blocks/{slot}
let post_validator_blocks_v4 = post_validator_blocks_v4(
eth_v4.clone(),
chain_filter.clone(),
not_while_syncing_filter.clone(),
task_spawner_filter.clone(),
);

// GET validator/blinded_blocks/{slot}
let get_validator_blinded_blocks = get_validator_blinded_blocks(
eth_v1.clone(),
Expand Down Expand Up @@ -2683,6 +2707,12 @@ pub async fn serve<T: BeaconChainTypes>(
chain_filter.clone(),
task_spawner_filter.clone(),
);
// POST validator/builder_preferences
let post_validator_builder_preferences = post_validator_builder_preferences(
eth_v1.clone(),
chain_filter.clone(),
task_spawner_filter.clone(),
);
// POST validator/sync_committee_subscriptions
let post_validator_sync_committee_subscriptions = post_validator_sync_committee_subscriptions(
eth_v1.clone(),
Expand Down Expand Up @@ -3496,6 +3526,8 @@ pub async fn serve<T: BeaconChainTypes>(
.uor(post_validator_sync_committee_subscriptions)
.uor(post_validator_prepare_beacon_proposer)
.uor(post_validator_register_validator)
.uor(post_validator_builder_preferences)
.uor(post_validator_blocks_v4)
.uor(post_validator_liveness_epoch)
.uor(post_lighthouse_liveness)
.uor(post_lighthouse_database_reconstruct)
Expand Down
49 changes: 34 additions & 15 deletions beacon_node/http_api/src/produce_block.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::{
build_block_contents,
version::{
ResponseIncludesVersion, add_consensus_block_value_header, add_consensus_version_header,
add_execution_payload_blinded_header, add_execution_payload_included_header,
add_execution_payload_value_header, add_ssz_content_type_header, beacon_response,
inconsistent_fork_rejection,
ResponseIncludesVersion, add_builder_url_header, add_consensus_block_value_header,
add_consensus_version_header, add_execution_payload_blinded_header,
add_execution_payload_included_header, add_execution_payload_value_header,
add_ssz_content_type_header, beacon_response, inconsistent_fork_rejection,
},
};
use beacon_chain::graffiti_calculator::GraffitiSettings;
Expand All @@ -17,9 +17,10 @@ use eth2::{
beacon_response::ForkVersionedResponse,
types::{BlockAndEnvelope, ProduceBlockV4Metadata},
};
use sensitive_url::SensitiveUrl;
use ssz::Encode;
use std::sync::Arc;
use tracing::instrument;
use tracing::{debug, instrument};
use types::{execution::BlockProductionVersion, *};
use warp::{
http::response::Builder,
Expand Down Expand Up @@ -58,13 +59,30 @@ pub async fn produce_block_v4<T: BeaconChainTypes>(
chain: Arc<BeaconChain<T>>,
slot: Slot,
query: api_types::ValidatorBlocksQuery,
builder_config: api_types::BuilderConfig,
) -> Result<Response, warp::Rejection> {
// `produceBlockV4` is the Gloas block-production endpoint.
let fork_name = chain.spec.fork_name_at_slot::<T::EthSpec>(slot);
if !fork_name.gloas_enabled() {
return Err(warp_utils::reject::custom_bad_request(
"produceBlockV4 is only valid for Gloas and later".to_string(),
));
}

let include_payload = query.include_payload.ok_or_else(|| {
warp_utils::reject::custom_bad_request(
"include_payload query parameter is required".to_string(),
)
})?;

// The resolved builder config is threaded into block production, where it drives direct-builder
// bid requests and the gossip/direct bid policy (see `produce_block_on_state_gloas`).
debug!(
%slot,
builders = builder_config.builders.len(),
"Received produceBlockV4 request"
);

let randao_reveal = query.randao_reveal.decompress().map_err(|e| {
warp_utils::reject::custom_bad_request(format!(
"randao reveal is not a valid BLS signature: {:?}",
Expand All @@ -73,14 +91,9 @@ pub async fn produce_block_v4<T: BeaconChainTypes>(
})?;

let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?;
// The GET route carries only a boost factor; direct builders arrive with the `BuilderConfig`
// body once this route is converted to POST (later in this PR stack). Until then the winning
// bid's builder URL is unused (`Eth-Builder-Url` also lands with the POST conversion).
let builder_config = api_types::BuilderConfig {
builder_boost_factor: query.builder_boost_factor.unwrap_or(DEFAULT_BOOST_FACTOR),
..api_types::BuilderConfig::empty()
};

// Gloas takes its bid boost policy from `builder_config` (global for gossip, per-builder for
// direct), so the V3-style `builder_boost_factor` query param is not used on this path.
let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy);

let (
Expand All @@ -89,7 +102,7 @@ pub async fn produce_block_v4<T: BeaconChainTypes>(
consensus_block_value,
execution_payload_value,
payload_contents,
_builder_url,
builder_url,
) = chain
.produce_block_with_verification_gloas(
randao_reveal,
Expand All @@ -110,6 +123,7 @@ pub async fn produce_block_v4<T: BeaconChainTypes>(
consensus_block_value,
execution_payload_value,
payload_contents,
builder_url,
accept_header,
&chain.spec,
)
Expand Down Expand Up @@ -164,9 +178,13 @@ pub fn build_response_v4<T: BeaconChainTypes>(
consensus_block_value: u64,
execution_payload_value: Uint256,
payload_contents: Option<PayloadEnvelopeContents<T::EthSpec>>,
builder_url: Option<SensitiveUrl>,
accept_header: Option<api_types::Accept>,
spec: &ChainSpec,
) -> Result<Response, warp::Rejection> {
// Stringify the winning builder's URL only here, at the `Eth-Builder-Url` header boundary; it is
// kept as a redacted `SensitiveUrl` everywhere upstream.
let builder_url = builder_url.map(|url| url.expose_full().to_string());
let fork_name = block
.to_ref()
.fork_name(spec)
Expand All @@ -180,14 +198,15 @@ pub fn build_response_v4<T: BeaconChainTypes>(
consensus_block_value: consensus_block_value_wei,
execution_payload_value,
execution_payload_included,
builder_url: None,
builder_url: builder_url.clone(),
};

let add_v4_headers = |res: Response| {
let res = add_consensus_version_header(res, fork_name);
let res = add_consensus_block_value_header(res, consensus_block_value_wei);
let res = add_execution_payload_value_header(res, execution_payload_value);
add_execution_payload_included_header(res, execution_payload_included)
let res = add_execution_payload_included_header(res, execution_payload_included);
add_builder_url_header(res, builder_url.as_deref())
};

// When the payload is included, bundle the block with the execution payload envelope, blobs and
Expand Down
70 changes: 70 additions & 0 deletions beacon_node/http_api/src/publish_blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use logging::crit;
use network::NetworkMessage;
use rand::prelude::SliceRandom;
use reqwest::StatusCode;
use sensitive_url::SensitiveUrl;
use slot_clock::SlotClock;
use std::marker::PhantomData;
use std::sync::Arc;
Expand Down Expand Up @@ -73,6 +74,62 @@ impl<T: BeaconChainTypes> ProvenancedBlock<T, Arc<SignedBeaconBlock<T::EthSpec>>
}
}

/// If a direct builder won this block's payload bid, forward the signed block to that builder via
/// `submitSignedBeaconBlock` so it reveals the execution payload envelope.
///
/// The builder's URL is the `Eth-Builder-Url` request header the VC echoed on publish (beacon-APIs
/// #630), so this works even on a beacon node that did not produce the block. `None` (self-built or
/// p2p-won), no configured builders, or a malformed URL are all no-ops.
///
/// Fire-and-forget: the submission runs in a detached task; a failure is logged at high severity
/// (the validator has already signed the commitment) but never blocks the publish response. Runs
/// only once per block since it hangs off the single p2p-publish point.
fn forward_signed_block_to_winning_builder<T: BeaconChainTypes>(
chain: &Arc<BeaconChain<T>>,
block: Arc<SignedBeaconBlock<T::EthSpec>>,
builder_url: Option<&str>,
) {
// The VC echoes the winning builder's URL in the `Eth-Builder-Url` request header (beacon-APIs
// #630); absent for a self-built block or a p2p-won bid, in which case there's nothing to forward.
let Some(builder_url) = builder_url else {
return;
};
let Some(builders) = chain.builders.as_ref() else {
return;
};
let url = match SensitiveUrl::parse(builder_url) {
Ok(url) => url,
Err(e) => {
warn!(error = ?e, "Ignoring malformed Eth-Builder-Url header");
return;
}
};

let builders = builders.clone();
let slot = block.slot();
let block_root = block.canonical_root();

chain.task_executor.spawn(
async move {
match builders.forward_signed_block(&url, &block).await {
Ok(()) => info!(
%slot,
%block_root,
"Forwarded signed block to winning builder"
),
Err(e) => error!(
%slot,
%block_root,
builder_url = ?url,
error = ?e,
"Failed to forward signed block to winning builder"
),
}
},
"forward_signed_block_to_builder",
);
}

/// Handles a request from the HTTP API for full blocks.
#[allow(clippy::too_many_arguments)]
#[instrument(
Expand All @@ -88,6 +145,9 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
validation_level: BroadcastValidation,
duplicate_status_code: StatusCode,
// The `Eth-Builder-Url` request header (beacon-APIs #630): when a direct builder won the block's
// payload bid, its URL, so the block is forwarded there for envelope reveal.
builder_url: Option<String>,
) -> Result<Response, Rejection> {
let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default();
let block_publishing_delay_for_testing = chain.config.block_publishing_delay;
Expand Down Expand Up @@ -141,6 +201,14 @@ pub async fn publish_block<T: BeaconChainTypes, B: IntoGossipVerifiedBlock<T>>(
BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))
})?;

// If a direct builder won this block's payload bid, forward the signed block to it so it
// reveals the execution payload envelope.
forward_signed_block_to_winning_builder(
&publish_chain,
block.clone(),
builder_url.as_deref(),
);

Ok(())
};

Expand Down Expand Up @@ -570,6 +638,8 @@ pub async fn publish_blinded_block<T: BeaconChainTypes>(
network_tx,
validation_level,
duplicate_status_code,
// Blinded (mev-boost) publish predates the Gloas builder-URL round-trip.
None,
)
.await
} else {
Expand Down
Loading
Loading