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
48 changes: 36 additions & 12 deletions crates/relay/src/api/proposer/get_execution_payload_bid.rs
Original file line number Diff line number Diff line change
@@ -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<A: Api> ProposerApi<A> {
/// Serves a `SignedExecutionPayloadBid` for the given slot/parent_hash/parent_root to a
Expand All @@ -27,7 +30,7 @@ impl<A: Api> ProposerApi<A> {
headers: HeaderMap,
Path(params): Path<GetExecutionPayloadBidParams>,
body: bytes::Bytes,
) -> Result<StatusCode, ProposerApiError> {
) -> Result<impl IntoResponse, ProposerApiError> {
let fork = fork_name_from_header(&headers).ok().flatten();
if fork != Some(ForkName::Gloas) {
return Err(ProposerApiError::InvalidFork);
Expand Down Expand Up @@ -61,12 +64,33 @@ impl<A: Api> ProposerApi<A> {
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)
}
}
}
}
145 changes: 145 additions & 0 deletions crates/relay/src/auctioneer/get_execution_payload_bid.rs
Original file line number Diff line number Diff line change
@@ -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<B: BidAdjustor> Context<B> {
pub(super) fn handle_get_execution_payload_bid(
&self,
params: GetExecutionPayloadBidParams,
slot_data: &SlotData,
res_tx: oneshot::Sender<GetExecutionPayloadBidResult>,
) {
let _ = res_tx.send(get_execution_payload_bid(&params, 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(&params.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<B256, PayloadAttributesUpdate>) -> 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<B256>,
) -> 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(&params(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(&params(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(&params(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(&params(parent_hash, parent_root), &data);

assert!(matches!(result, Err(ProposerApiError::NoBidPrepared)));
}
}
24 changes: 22 additions & 2 deletions crates/relay/src/auctioneer/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};

Expand Down Expand Up @@ -65,6 +69,22 @@ impl AuctioneerHandle {
Ok(rx)
}

pub fn get_execution_payload_bid(
&self,
params: GetExecutionPayloadBidParams,
) -> Result<oneshot::Receiver<GetExecutionPayloadBidResult>, 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,
Expand Down
40 changes: 40 additions & 0 deletions crates/relay/src/auctioneer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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() {
Expand Down
14 changes: 11 additions & 3 deletions crates/relay/src/auctioneer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -51,6 +51,7 @@ pub enum SubmissionRef {

pub type GetHeaderResult = Result<PayloadEntry, ProposerApiError>;
pub type GetPayloadResult = Result<GetPayloadResultData, ProposerApiError>;
pub type GetExecutionPayloadBidResult = Result<SignedExecutionPayloadBid, ProposerApiError>;

#[derive(Debug, Clone, Copy)]
#[repr(C)]
Expand Down Expand Up @@ -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<GetExecutionPayloadBidResult>,
span: tracing::Span,
},
// Receive multiple of these potentially, assume some light validation
GetPayload {
block_hash: B256,
Expand All @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions crates/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub type ExecutionPayloadGloas = lh_types::ExecutionPayloadGloas<MainnetEthSpec>
pub type ExecutionRequestsGloas = lh_types::ExecutionRequestsGloas<MainnetEthSpec>;
pub type ExecutionPayloadEnvelope = lh_types::ExecutionPayloadEnvelope<MainnetEthSpec>;
pub type SignedExecutionPayloadEnvelope = lh_types::SignedExecutionPayloadEnvelope<MainnetEthSpec>;
pub type ExecutionPayloadBid = lh_types::ExecutionPayloadBid<MainnetEthSpec>;
pub type SignedExecutionPayloadBid = lh_types::SignedExecutionPayloadBid<MainnetEthSpec>;

// Beacon block
pub type BeaconBlockFulu = lh_types::BeaconBlockFulu<MainnetEthSpec>;
Expand Down
Loading