diff --git a/crates/relay/src/api/proposer/get_execution_payload_bid.rs b/crates/relay/src/api/proposer/get_execution_payload_bid.rs index d10bc4548..3f6eac5dd 100644 --- a/crates/relay/src/api/proposer/get_execution_payload_bid.rs +++ b/crates/relay/src/api/proposer/get_execution_payload_bid.rs @@ -1,21 +1,24 @@ use std::sync::Arc; -use axum::{Extension, extract::Path, http::HeaderMap}; +use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse}; use helix_common::{ api::{ HEADER_START_TIME_UNIX_MS, HEADER_TIMEOUT_MS, proposer_api::GetExecutionPayloadBidParams, }, api_provider::header_u64, - decoder::Encoding, + decoder::{Encoding, HEADER_SSZ}, utils::extract_request_id, }; use helix_types::{ForkName, SignedBuilderRequestAuth}; -use hyper::StatusCode; -use ssz::Decode; -use tracing::info; +use http::{HeaderValue, header::CONTENT_TYPE}; +use ssz::{Decode, Encode}; +use tracing::{info, warn}; use super::{ProposerApi, get_payload::fork_name_from_header}; -use crate::api::{Api, proposer::error::ProposerApiError}; +use crate::api::{ + Api, + proposer::{CONSENSUS_VERSION_HEADER, error::ProposerApiError}, +}; impl ProposerApi { /// Serves a `SignedExecutionPayloadBid` for the given slot/parent_hash/parent_root to a @@ -27,7 +30,7 @@ impl ProposerApi { headers: HeaderMap, Path(params): Path, body: bytes::Bytes, - ) -> Result { + ) -> Result { let fork = fork_name_from_header(&headers).ok().flatten(); if fork != Some(ForkName::Gloas) { return Err(ProposerApiError::InvalidFork); @@ -61,12 +64,33 @@ impl ProposerApi { parent_hash = ?params.parent_hash, parent_root = ?params.parent_root, proposer_pubkey = ?params.proposer_pubkey, - "validated getExecutionPayloadBid request (not yet wired to the auctioneer)" + "validated getExecutionPayloadBid request" ); - // TODO(gloas): fetch/build the SignedExecutionPayloadBid from the auctioneer, honoring - // any stored max_execution_payment preference. Until then, "no bid available" is a - // valid response per spec. - Ok(StatusCode::NO_CONTENT) + let Ok(rx) = proposer_api.auctioneer_handle.get_execution_payload_bid(params) else { + return Err(ProposerApiError::InternalServerError); + }; + + let signed_bid = match rx.await { + Ok(res) => res?, + Err(err) => { + warn!(%err, "failed to get execution payload bid from auctioneer"); + return Err(ProposerApiError::InternalServerError); + } + }; + + match Encoding::from_accept(&headers) { + Encoding::Json => Ok(axum::Json(serde_json::to_value(&signed_bid)?).into_response()), + Encoding::Ssz => { + let mut response = signed_bid.as_ssz_bytes().into_response(); + let headers = response.headers_mut(); + headers.insert(CONTENT_TYPE, HeaderValue::from_str(HEADER_SSZ).unwrap()); + headers.insert( + CONSENSUS_VERSION_HEADER, + HeaderValue::from_str(&ForkName::Gloas.to_string()).unwrap(), + ); + Ok(response) + } + } } } diff --git a/crates/relay/src/auctioneer/get_execution_payload_bid.rs b/crates/relay/src/auctioneer/get_execution_payload_bid.rs new file mode 100644 index 000000000..883db63aa --- /dev/null +++ b/crates/relay/src/auctioneer/get_execution_payload_bid.rs @@ -0,0 +1,145 @@ +use helix_common::api::proposer_api::GetExecutionPayloadBidParams; +use tokio::sync::oneshot; +use tracing::warn; + +use crate::{ + api::proposer::ProposerApiError, + auctioneer::{ + bid_adjustor::BidAdjustor, + context::Context, + types::{GetExecutionPayloadBidResult, SlotData}, + }, +}; + +impl Context { + pub(super) fn handle_get_execution_payload_bid( + &self, + params: GetExecutionPayloadBidParams, + slot_data: &SlotData, + res_tx: oneshot::Sender, + ) { + let _ = res_tx.send(get_execution_payload_bid(¶ms, slot_data)); + } +} + +/// Checks `params.parent_hash`/`params.parent_root` against currently-live payload attributes, +/// then reports "no bid available" -- serving a real Gloas bid needs step 5's builder->relay +/// submission wire format, not landed yet. +pub(super) fn get_execution_payload_bid( + params: &GetExecutionPayloadBidParams, + slot_data: &SlotData, +) -> GetExecutionPayloadBidResult { + let Some(attrs) = slot_data.payload_attributes_map.get(¶ms.parent_hash) else { + warn!( + req =% params.parent_hash, + have =? slot_data.payload_attributes_map.keys(), + "get execution payload bid for unknown parent hash" + ); + return Err(ProposerApiError::NoBidPrepared); + }; + + if attrs.parent_beacon_block_root != Some(params.parent_root) { + warn!( + req =% params.parent_root, + have =? attrs.parent_beacon_block_root, + "get execution payload bid for mismatched parent root" + ); + return Err(ProposerApiError::NoBidPrepared); + } + + Err(ProposerApiError::NoBidPrepared) +} + +#[cfg(test)] +mod tests { + use alloy_primitives::B256; + use helix_common::PayloadAttributesUpdate; + use helix_types::ForkName; + use rustc_hash::FxHashMap; + + use super::*; + + fn slot_data(payload_attributes_map: FxHashMap) -> SlotData { + SlotData { + bid_slot: Default::default(), + registration_data: Default::default(), + current_fork: ForkName::Gloas, + payload_attributes_map, + il: Default::default(), + } + } + + fn attrs_update( + parent_hash: B256, + parent_beacon_block_root: Option, + ) -> PayloadAttributesUpdate { + let mut update = PayloadAttributesUpdate { + slot: Default::default(), + parent_hash, + withdrawals_root: Default::default(), + payload_attributes: Default::default(), + }; + update.payload_attributes.parent_beacon_block_root = parent_beacon_block_root; + update + } + + fn params(parent_hash: B256, parent_root: B256) -> GetExecutionPayloadBidParams { + GetExecutionPayloadBidParams { + slot: 1, + parent_hash, + parent_root, + proposer_pubkey: Default::default(), + } + } + + #[test] + fn unknown_parent_hash_is_no_bid() { + let parent_hash = B256::repeat_byte(0x11); + let parent_root = B256::repeat_byte(0x22); + let data = slot_data(FxHashMap::default()); + + let result = get_execution_payload_bid(¶ms(parent_hash, parent_root), &data); + + assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); + } + + #[test] + fn parent_root_mismatch_is_no_bid() { + let parent_hash = B256::repeat_byte(0x11); + let live_root = B256::repeat_byte(0x22); + let requested_root = B256::repeat_byte(0x33); + let mut map = FxHashMap::default(); + map.insert(parent_hash, attrs_update(parent_hash, Some(live_root))); + let data = slot_data(map); + + let result = get_execution_payload_bid(¶ms(parent_hash, requested_root), &data); + + assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); + } + + #[test] + fn missing_parent_beacon_block_root_is_no_bid() { + let parent_hash = B256::repeat_byte(0x11); + let requested_root = B256::repeat_byte(0x33); + let mut map = FxHashMap::default(); + map.insert(parent_hash, attrs_update(parent_hash, None)); + let data = slot_data(map); + + let result = get_execution_payload_bid(¶ms(parent_hash, requested_root), &data); + + assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); + } + + #[test] + fn matching_parent_still_reports_no_bid_until_step_5() { + let parent_hash = B256::repeat_byte(0x11); + let parent_root = B256::repeat_byte(0x22); + let mut map = FxHashMap::default(); + map.insert(parent_hash, attrs_update(parent_hash, Some(parent_root))); + let data = slot_data(map); + + let result = get_execution_payload_bid(¶ms(parent_hash, parent_root), &data); + + assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); + } +} diff --git a/crates/relay/src/auctioneer/handle.rs b/crates/relay/src/auctioneer/handle.rs index be2135c68..048840e12 100644 --- a/crates/relay/src/auctioneer/handle.rs +++ b/crates/relay/src/auctioneer/handle.rs @@ -2,7 +2,11 @@ use std::sync::Arc; use dashmap::DashMap; use futures::{FutureExt, future::Shared}; -use helix_common::{GetPayloadTrace, api::proposer_api::GetHeaderParams, chain_info::ChainInfo}; +use helix_common::{ + GetPayloadTrace, + api::proposer_api::{GetExecutionPayloadBidParams, GetHeaderParams}, + chain_info::ChainInfo, +}; use helix_types::{ BlsPublicKey, BlsPublicKeyBytes, ExecPayload, GetPayloadResponse, SigError, SignedBlindedBeaconBlock, @@ -12,7 +16,7 @@ use tracing::trace; use crate::{ api::proposer::{ProposerApiError, get_payload::ProposerApiVersion}, - auctioneer::types::{Event, GetHeaderResult, GetPayloadResult}, + auctioneer::types::{Event, GetExecutionPayloadBidResult, GetHeaderResult, GetPayloadResult}, gossip::BroadcastPayloadParams, }; @@ -65,6 +69,22 @@ impl AuctioneerHandle { Ok(rx) } + pub fn get_execution_payload_bid( + &self, + params: GetExecutionPayloadBidParams, + ) -> Result, ChannelFull> { + let (tx, rx) = oneshot::channel(); + trace!("sending to auctioneer"); + self.auctioneer + .try_send(Event::GetExecutionPayloadBid { + params, + res_tx: tx, + span: tracing::Span::current(), + }) + .map_err(|_| ChannelFull)?; + Ok(rx) + } + pub fn get_payload( &self, chain_info: &ChainInfo, diff --git a/crates/relay/src/auctioneer/mod.rs b/crates/relay/src/auctioneer/mod.rs index 8d707489d..a0b388b17 100644 --- a/crates/relay/src/auctioneer/mod.rs +++ b/crates/relay/src/auctioneer/mod.rs @@ -2,6 +2,7 @@ mod bid_adjustor; mod bid_sorter; mod block_merger; mod context; +mod get_execution_payload_bid; mod get_header; mod get_payload; mod handle; @@ -446,6 +447,24 @@ impl State { drop(_guard); } + // get_execution_payload_bid (Gloas) + (State::Sorting(slot_data), Event::GetExecutionPayloadBid { params, res_tx, span }) => { + let _guard = span.enter(); + trace!("received in auctioneer"); + + if slot_data.bid_slot != params.slot { + let _ = res_tx.send(Err(ProposerApiError::RequestWrongSlot { + request_slot: params.slot, + bid_slot: slot_data.bid_slot.into(), + })); + } else { + ctx.handle_get_execution_payload_bid(params, slot_data, res_tx) + } + + trace!("finished processing"); + drop(_guard); + } + // get_payload ( State::Sorting(slot_data), @@ -530,6 +549,11 @@ impl State { let _ = res_tx.send(Err(ProposerApiError::DeliveringPayload)); } + // late get_execution_payload_bid + (State::Broadcasting { .. }, Event::GetExecutionPayloadBid { res_tx, .. }) => { + let _ = res_tx.send(Err(ProposerApiError::DeliveringPayload)); + } + // duplicate get_payload, proposer equivocating? ( State::Broadcasting { slot_data: slot_ctx, block_hash }, @@ -602,6 +626,22 @@ impl State { } } + // get_execution_payload_bid not sorting + ( + State::Slot { bid_slot, .. }, + Event::GetExecutionPayloadBid { res_tx, params, .. }, + ) => { + if params.slot == bid_slot.as_u64() { + // either not registered or waiting for full data from housekepper + let _ = res_tx.send(Err(ProposerApiError::NoBidPrepared)); + } else { + let _ = res_tx.send(Err(ProposerApiError::RequestWrongSlot { + request_slot: params.slot, + bid_slot: bid_slot.as_u64(), + })); + } + } + // get_payload unregistered (State::Slot { bid_slot, .. }, Event::GetPayload { res_tx, blinded, .. }) => { if bid_slot.saturating_sub(Slot::from(1u64)) == blinded.slot() { diff --git a/crates/relay/src/auctioneer/types.rs b/crates/relay/src/auctioneer/types.rs index 28e02e629..3f43a41e0 100644 --- a/crates/relay/src/auctioneer/types.rs +++ b/crates/relay/src/auctioneer/types.rs @@ -6,7 +6,7 @@ use helix_common::{ GetPayloadTrace, PayloadAttributesUpdate, SubmissionTrace, api::{ builder_api::{BuilderGetValidatorsResponseEntry, InclusionListWithMetadata}, - proposer_api::GetHeaderParams, + proposer_api::{GetExecutionPayloadBidParams, GetHeaderParams}, }, decoder::{Encoding, SubmissionDecoderParams, SubmissionType}, metrics::BID_CREATION_LATENCY, @@ -15,8 +15,8 @@ use helix_tcp_types::{BidSubmissionFlags, BidSubmissionHeader}; use helix_types::{ BidAdjustmentData, BlockMergingData, BlsPublicKeyBytes, BuilderBid, Compression, ExecutionPayload, ForkName, GetPayloadResponse, MergeType, PayloadAndBlobs, PayloadBidData, - PayloadBidDataRef, SignedBidSubmission, SignedBlindedBeaconBlock, Slot, Submission, - SubmissionVersion, VersionedSignedProposal, mock_public_key_bytes, + PayloadBidDataRef, SignedBidSubmission, SignedBlindedBeaconBlock, SignedExecutionPayloadBid, + Slot, Submission, SubmissionVersion, VersionedSignedProposal, mock_public_key_bytes, }; use http::{ HeaderMap, HeaderValue, @@ -51,6 +51,7 @@ pub enum SubmissionRef { pub type GetHeaderResult = Result; pub type GetPayloadResult = Result; +pub type GetExecutionPayloadBidResult = Result; #[derive(Debug, Clone, Copy)] #[repr(C)] @@ -434,6 +435,12 @@ pub enum Event { span: tracing::Span, is_mev_boost: bool, }, + /// Gloas (ePBS) analogue of `GetHeader`. + GetExecutionPayloadBid { + params: GetExecutionPayloadBidParams, + res_tx: oneshot::Sender, + span: tracing::Span, + }, // Receive multiple of these potentially, assume some light validation GetPayload { block_hash: B256, @@ -459,6 +466,7 @@ impl Event { Event::SlotData { .. } => "SlotData", Event::Submission { .. } => "Submission", Event::GetHeader { .. } => "GetHeader", + Event::GetExecutionPayloadBid { .. } => "GetExecutionPayloadBid", Event::GetPayload { .. } => "GetPayload", Event::GossipPayload(_) => "GossipPayload", Event::SimResult(_) => "SimResult", diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 78840c762..78a37a036 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -72,6 +72,8 @@ pub type ExecutionPayloadGloas = lh_types::ExecutionPayloadGloas pub type ExecutionRequestsGloas = lh_types::ExecutionRequestsGloas; pub type ExecutionPayloadEnvelope = lh_types::ExecutionPayloadEnvelope; pub type SignedExecutionPayloadEnvelope = lh_types::SignedExecutionPayloadEnvelope; +pub type ExecutionPayloadBid = lh_types::ExecutionPayloadBid; +pub type SignedExecutionPayloadBid = lh_types::SignedExecutionPayloadBid; // Beacon block pub type BeaconBlockFulu = lh_types::BeaconBlockFulu;