From 6738a3ebc46fa9d9ce92b0385596ab56f4132831 Mon Sep 17 00:00:00 2001 From: owen Date: Tue, 25 Aug 2026 15:16:17 +0100 Subject: [PATCH] Serve real Gloas bids and envelopes from the auctioneer's existing submissions getExecutionPayloadBid now reads the winning submission out of the same bid-sorter/payloads map get_header already reads, converts it via the prior step's Gloas conversion functions, and signs a real SignedExecutionPayloadBid under helix's own configured builder identity. submitSignedBeaconBlock's GloasPayloadStore placeholder is replaced by a real auctioneer round trip (Event::TakeHeldGloasPayload) that looks up the same payload by block hash and converts it for envelope construction. GloasBuilderIdentity is now constructed once in main.rs and shared between the proposer API and the auctioneer. execution_payment is set equal to value (no payment-split product need yet, per gattaca-com/helix#489). --- crates/relay/src/api/proposer/mod.rs | 5 +- .../proposer/submit_signed_beacon_block.rs | 100 ++++++------ crates/relay/src/api/service.rs | 8 +- crates/relay/src/auctioneer/context.rs | 7 +- .../auctioneer/get_execution_payload_bid.rs | 153 ++++++++++++++++-- crates/relay/src/auctioneer/gloas_payload.rs | 35 ++++ crates/relay/src/auctioneer/handle.rs | 18 ++- crates/relay/src/auctioneer/mod.rs | 18 ++- crates/relay/src/auctioneer/types.rs | 11 +- crates/relay/src/lib.rs | 2 +- crates/relay/src/main.rs | 17 +- crates/types/src/fields.rs | 23 +++ 12 files changed, 307 insertions(+), 90 deletions(-) create mode 100644 crates/relay/src/auctioneer/gloas_payload.rs diff --git a/crates/relay/src/api/proposer/mod.rs b/crates/relay/src/api/proposer/mod.rs index 7fee5c60e..4c52acf00 100644 --- a/crates/relay/src/api/proposer/mod.rs +++ b/crates/relay/src/api/proposer/mod.rs @@ -19,7 +19,7 @@ use helix_common::{ use helix_database::handle::DbHandle; use helix_operator::OperatorPubSub; use hyper::StatusCode; -pub use submit_signed_beacon_block::{GloasBuilderIdentity, GloasPayloadStore, NoHeldPayloads}; +pub use submit_signed_beacon_block::{GloasBuilderIdentity, HeldGloasPayload}; use crate::{ api::{Api, router::Terminating}, @@ -46,7 +46,6 @@ pub struct ProposerApi { pub reg_handle: RegWorkerHandle, pub operator_api: Option>, pub gloas_builder_identity: Arc, - pub gloas_payload_store: Arc, } impl ProposerApi { @@ -65,7 +64,6 @@ impl ProposerApi { reg_handle: RegWorkerHandle, alert_manager: Arc, operator_api: Option>, - gloas_payload_store: Arc, ) -> Self { let gloas_builder_identity = Arc::new(GloasBuilderIdentity { builder_index: relay_config.gloas_builder_index, @@ -87,7 +85,6 @@ impl ProposerApi { reg_handle, operator_api, gloas_builder_identity, - gloas_payload_store, } } } diff --git a/crates/relay/src/api/proposer/submit_signed_beacon_block.rs b/crates/relay/src/api/proposer/submit_signed_beacon_block.rs index 5bada9a2a..d67a1d77b 100644 --- a/crates/relay/src/api/proposer/submit_signed_beacon_block.rs +++ b/crates/relay/src/api/proposer/submit_signed_beacon_block.rs @@ -10,7 +10,7 @@ use helix_types::{ }; use hyper::StatusCode; use ssz::Decode; -use tracing::info; +use tracing::{info, warn}; use tree_hash::TreeHash; use super::{ProposerApi, get_payload::fork_name_from_header}; @@ -22,22 +22,6 @@ pub struct HeldGloasPayload { pub execution_requests: ExecutionRequestsGloas, } -/// Looks up and consumes the payload held for a bid's committed block hash. Must not return -/// the same payload twice. -pub trait GloasPayloadStore: Send + Sync { - fn take_held_payload(&self, block_hash: B256) -> Option; -} - -/// Placeholder `GloasPayloadStore`: nothing has held a payload yet. -// TODO(gloas): implement against the auctioneer; see gattaca-com/helix#489 step 3. -pub struct NoHeldPayloads; - -impl GloasPayloadStore for NoHeldPayloads { - fn take_held_payload(&self, _block_hash: B256) -> Option { - None - } -} - /// Helix's own on-chain Gloas builder identity: `builder_index` plus signing key. // TODO(gloas): support external builder-signed bids/envelopes; see gattaca-com/helix#489 step 5. pub struct GloasBuilderIdentity { @@ -64,12 +48,31 @@ impl GloasBuilderIdentity { let signature = self.keypair.sk.sign(message.signing_root(domain)); SignedExecutionPayloadEnvelope { message, signature } } + + /// Signs a `SignedExecutionPayloadBid` under the same domain as `sign_envelope`. + pub fn sign_bid( + &self, + message: helix_types::ExecutionPayloadBid, + chain_info: &ChainInfo, + ) -> helix_types::SignedExecutionPayloadBid { + let epoch = message.slot.epoch(MainnetEthSpec::slots_per_epoch()); + let fork = chain_info.spec.fork_at_epoch(epoch); + let domain = chain_info.spec.get_domain( + epoch, + Domain::BeaconBuilder, + &fork, + chain_info.genesis_validators_root, + ); + let signature = self.keypair.sk.sign(message.signing_root(domain)); + helix_types::SignedExecutionPayloadBid { message, signature } + } } /// Constructs and signs the `SignedExecutionPayloadEnvelope` fulfilling `block`'s committed bid. +/// `held` is the payload the auctioneer has stored for the bid's committed block hash, if any. pub(super) fn construct_signed_envelope( block: &SignedBeaconBlockGloas, - store: &dyn GloasPayloadStore, + held: Option, identity: &GloasBuilderIdentity, chain_info: &ChainInfo, ) -> Result { @@ -83,9 +86,7 @@ pub(super) fn construct_signed_envelope( }); } - let held = store - .take_held_payload(bid_block_hash) - .ok_or(ProposerApiError::NoHeldPayloadForBlock(bid_block_hash))?; + let held = held.ok_or(ProposerApiError::NoHeldPayloadForBlock(bid_block_hash))?; let held_block_hash: B256 = held.payload.block_hash.0; if held_block_hash != bid_block_hash { @@ -128,9 +129,25 @@ impl ProposerApi { info!(slot = block.message.slot.as_u64(), "accepted submitSignedBeaconBlock request"); + let bid_block_hash: B256 = + block.message.body.signed_execution_payload_bid.message.block_hash.0; + let Ok(rx) = proposer_api + .auctioneer_handle + .take_held_gloas_payload(bid_block_hash, block.message.slot) + else { + return Err(ProposerApiError::InternalServerError); + }; + let held = match rx.await { + Ok(held) => held, + Err(err) => { + warn!(%err, "failed to fetch held Gloas payload from auctioneer"); + return Err(ProposerApiError::InternalServerError); + } + }; + let signed_envelope = construct_signed_envelope( &block, - proposer_api.gloas_payload_store.as_ref(), + held, &proposer_api.gloas_builder_identity, &proposer_api.chain_info, )?; @@ -146,31 +163,11 @@ impl ProposerApi { #[cfg(test)] mod construct_signed_envelope_tests { - use std::sync::Mutex; - use helix_common::utils::install_default_crypto_provider; use helix_types::{BeaconBlockGloas, BlsSignature, EmptyBlock, ExecutionBlockHash}; use super::*; - struct StubStore(Mutex>); - - impl StubStore { - fn holding(payload: HeldGloasPayload) -> Self { - Self(Mutex::new(Some(payload))) - } - - fn empty() -> Self { - Self(Mutex::new(None)) - } - } - - impl GloasPayloadStore for StubStore { - fn take_held_payload(&self, _block_hash: B256) -> Option { - self.0.lock().unwrap().take() - } - } - fn held_payload(block_hash: B256) -> HeldGloasPayload { let mut payload = ExecutionPayloadGloas::default(); payload.block_hash = ExecutionBlockHash(block_hash); @@ -202,11 +199,11 @@ mod construct_signed_envelope_tests { let block_hash = B256::repeat_byte(0x11); let parent_root = B256::repeat_byte(0x22); let block = test_block(block_hash, 7, parent_root); - let store = StubStore::holding(held_payload(block_hash)); + let held = Some(held_payload(block_hash)); let identity = identity(7); let signed_envelope = - construct_signed_envelope(&block, &store, &identity, &chain_info).unwrap(); + construct_signed_envelope(&block, held, &identity, &chain_info).unwrap(); assert_eq!(signed_envelope.message.builder_index, 7); assert_eq!(signed_envelope.message.beacon_block_root, block.message.tree_hash_root()); @@ -219,11 +216,11 @@ mod construct_signed_envelope_tests { let chain_info = ChainInfo::default(); let block_hash = B256::repeat_byte(0x33); let block = test_block(block_hash, 3, B256::ZERO); - let store = StubStore::holding(held_payload(block_hash)); + let held = Some(held_payload(block_hash)); let identity = identity(3); let signed_envelope = - construct_signed_envelope(&block, &store, &identity, &chain_info).unwrap(); + construct_signed_envelope(&block, held, &identity, &chain_info).unwrap(); let epoch = signed_envelope.message.slot().epoch(MainnetEthSpec::slots_per_epoch()); let fork = chain_info.spec.fork_at_epoch(epoch); @@ -240,10 +237,9 @@ mod construct_signed_envelope_tests { let chain_info = ChainInfo::default(); let block_hash = B256::repeat_byte(0x44); let block = test_block(block_hash, 1, B256::ZERO); - let store = StubStore::empty(); let identity = identity(1); - let result = construct_signed_envelope(&block, &store, &identity, &chain_info); + let result = construct_signed_envelope(&block, None, &identity, &chain_info); assert!( matches!(result, Err(ProposerApiError::NoHeldPayloadForBlock(hash)) if hash == block_hash) @@ -256,10 +252,10 @@ mod construct_signed_envelope_tests { let bid_block_hash = B256::repeat_byte(0x55); let wrong_held_hash = B256::repeat_byte(0x66); let block = test_block(bid_block_hash, 1, B256::ZERO); - let store = StubStore::holding(held_payload(wrong_held_hash)); + let held = Some(held_payload(wrong_held_hash)); let identity = identity(1); - let result = construct_signed_envelope(&block, &store, &identity, &chain_info); + let result = construct_signed_envelope(&block, held, &identity, &chain_info); assert!(matches!( result, @@ -273,10 +269,10 @@ mod construct_signed_envelope_tests { let chain_info = ChainInfo::default(); let block_hash = B256::repeat_byte(0x77); let block = test_block(block_hash, 9, B256::ZERO); - let store = StubStore::holding(held_payload(block_hash)); + let held = Some(held_payload(block_hash)); let identity = identity(1); - let result = construct_signed_envelope(&block, &store, &identity, &chain_info); + let result = construct_signed_envelope(&block, held, &identity, &chain_info); assert!(matches!( result, diff --git a/crates/relay/src/api/service.rs b/crates/relay/src/api/service.rs index f5c333a40..752fea64a 100644 --- a/crates/relay/src/api/service.rs +++ b/crates/relay/src/api/service.rs @@ -26,11 +26,8 @@ use tracing::{error, info}; use crate::{ AuctioneerHandle, DbHandle, PostgresDatabaseService, RegWorkerHandle, api::{ - Api, FutureBidSubmissionResult, - builder::api::BuilderApi, - extract::raw_web_socket::RawWebSocket, - proposer::{NoHeldPayloads, ProposerApi}, - router::build_router, + Api, FutureBidSubmissionResult, builder::api::BuilderApi, + extract::raw_web_socket::RawWebSocket, proposer::ProposerApi, router::build_router, }, gossip::{GossipedMessage, GrpcGossiperClientManager, process_gossip_messages}, network::api::RelayNetworkApi, @@ -154,7 +151,6 @@ pub async fn run_api_service( registrations_handle, alert_manager, operator_api, - Arc::new(NoHeldPayloads), )); tokio::spawn(process_gossip_messages( diff --git a/crates/relay/src/auctioneer/context.rs b/crates/relay/src/auctioneer/context.rs index 07386d0db..94339f598 100644 --- a/crates/relay/src/auctioneer/context.rs +++ b/crates/relay/src/auctioneer/context.rs @@ -30,7 +30,9 @@ use tracing::{debug, info, warn}; use crate::{ SubmissionDataWithSpan, - api::{FutureBidSubmissionResult, builder::error::BuilderApiError}, + api::{ + FutureBidSubmissionResult, builder::error::BuilderApiError, proposer::GloasBuilderIdentity, + }, auctioneer::{ AuctioneerHandle, BlockMergeResponse, bid_adjustor::BidAdjustor, @@ -77,6 +79,7 @@ pub struct Context { pub alert_manager: Arc, pub operator_api: Option>, pub builder_preferences: BuilderPreferencesStore, + pub gloas_builder_identity: Arc, } const EXPECTED_PAYLOADS_PER_SLOT: usize = 5000; @@ -100,6 +103,7 @@ impl Context { auctioneer_handle: AuctioneerHandle, alert_manager: Arc, operator_api: Option>, + gloas_builder_identity: Arc, ) -> Self { let unknown_builder_info = BuilderInfo { collateral: U256::ZERO, @@ -146,6 +150,7 @@ impl Context { alert_manager, operator_api, builder_preferences: BuilderPreferencesStore::default(), + gloas_builder_identity, } } diff --git a/crates/relay/src/auctioneer/get_execution_payload_bid.rs b/crates/relay/src/auctioneer/get_execution_payload_bid.rs index 883db63aa..a47d501e4 100644 --- a/crates/relay/src/auctioneer/get_execution_payload_bid.rs +++ b/crates/relay/src/auctioneer/get_execution_payload_bid.rs @@ -1,13 +1,18 @@ -use helix_common::api::proposer_api::GetExecutionPayloadBidParams; +use helix_common::{api::proposer_api::GetExecutionPayloadBidParams, chain_info::ChainInfo}; +use helix_types::{ + ExecutionBlockHash, ExecutionPayloadBid, SignedExecutionPayloadBid, Slot, + convert_kzg_commitments_to_progressive, execution_requests_to_gloas, +}; use tokio::sync::oneshot; use tracing::warn; +use tree_hash::TreeHash; use crate::{ - api::proposer::ProposerApiError, + api::proposer::{GloasBuilderIdentity, ProposerApiError}, auctioneer::{ bid_adjustor::BidAdjustor, context::Context, - types::{GetExecutionPayloadBidResult, SlotData}, + types::{GetExecutionPayloadBidResult, PayloadEntry, SlotData}, }, }; @@ -18,17 +23,24 @@ impl Context { slot_data: &SlotData, res_tx: oneshot::Sender, ) { - let _ = res_tx.send(get_execution_payload_bid(¶ms, slot_data)); + let result = check_execution_payload_bid_liveness(¶ms, slot_data).and_then(|()| { + let best_block_hash = self + .bid_sorter + .get_header(¶ms.parent_hash) + .ok_or(ProposerApiError::NoBidPrepared)?; + let entry = + self.payloads.get(&best_block_hash).ok_or(ProposerApiError::NoBidPrepared)?; + build_signed_bid(entry, ¶ms, &self.gloas_builder_identity, &self.chain_info) + }); + let _ = res_tx.send(result); } } -/// 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( +/// Checks `params.parent_hash`/`params.parent_root` against currently-live payload attributes. +pub(super) fn check_execution_payload_bid_liveness( params: &GetExecutionPayloadBidParams, slot_data: &SlotData, -) -> GetExecutionPayloadBidResult { +) -> Result<(), ProposerApiError> { let Some(attrs) = slot_data.payload_attributes_map.get(¶ms.parent_hash) else { warn!( req =% params.parent_hash, @@ -47,14 +59,56 @@ pub(super) fn get_execution_payload_bid( return Err(ProposerApiError::NoBidPrepared); } - Err(ProposerApiError::NoBidPrepared) + Ok(()) +} + +/// Builds and signs the `SignedExecutionPayloadBid` for the winning submission held in `entry`, +/// under helix's own configured Gloas builder identity. +pub(super) fn build_signed_bid( + entry: &PayloadEntry, + params: &GetExecutionPayloadBidParams, + identity: &GloasBuilderIdentity, + chain_info: &ChainInfo, +) -> Result { + let slot = Slot::new(params.slot); + + let payload = entry.execution_payload().to_lighthouse_gloas_payload(slot).map_err(|err| { + warn!(%err, block_hash =% entry.block_hash(), "failed to convert held payload to Gloas shape for bid"); + ProposerApiError::InternalServerError + })?; + + let execution_requests = execution_requests_to_gloas(entry.bid_data_ref().execution_requests); + let execution_requests_root = execution_requests.tree_hash_root(); + + // Per gattaca-com/helix#489: no payment-split product need yet, so execution_payment = value. + let value = entry.value().saturating_to::(); + + let bid = ExecutionPayloadBid { + parent_block_hash: ExecutionBlockHash(params.parent_hash), + parent_block_root: params.parent_root, + block_hash: ExecutionBlockHash(*entry.block_hash()), + prev_randao: payload.prev_randao, + fee_recipient: payload.fee_recipient, + gas_limit: payload.gas_limit, + builder_index: identity.builder_index, + slot, + value, + execution_payment: value, + blob_kzg_commitments: convert_kzg_commitments_to_progressive( + &entry.payload_and_blobs().blobs_bundle.commitments, + ), + execution_requests_root, + _phantom: std::marker::PhantomData, + }; + + Ok(identity.sign_bid(bid, chain_info)) } #[cfg(test)] mod tests { use alloy_primitives::B256; use helix_common::PayloadAttributesUpdate; - use helix_types::ForkName; + use helix_types::{Domain, EthSpec, ForkName, SignedRoot, TestRandomSeed}; use rustc_hash::FxHashMap; use super::*; @@ -98,7 +152,7 @@ mod tests { 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); + let result = check_execution_payload_bid_liveness(¶ms(parent_hash, parent_root), &data); assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); } @@ -112,7 +166,8 @@ mod tests { 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); + let result = + check_execution_payload_bid_liveness(¶ms(parent_hash, requested_root), &data); assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); } @@ -125,21 +180,85 @@ mod tests { 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); + let result = + check_execution_payload_bid_liveness(¶ms(parent_hash, requested_root), &data); assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); } #[test] - fn matching_parent_still_reports_no_bid_until_step_5() { + fn matching_parent_passes_liveness_check() { 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); + let result = check_execution_payload_bid_liveness(¶ms(parent_hash, parent_root), &data); - assert!(matches!(result, Err(ProposerApiError::NoBidPrepared))); + assert!(result.is_ok()); + } + + fn payload_entry(block_hash: B256, value: u64) -> PayloadEntry { + use std::sync::Arc; + + use alloy_primitives::U256; + use helix_types::{BlobsBundle, ExecutionPayload, ExecutionRequests, PayloadAndBlobs}; + + let mut payload = ExecutionPayload::test_random(); + payload.block_hash = block_hash; + + PayloadEntry::new_gossip( + PayloadAndBlobs { + execution_payload: Arc::new(payload), + blobs_bundle: Arc::new(BlobsBundle::default()), + }, + helix_types::PayloadBidData { + withdrawals_root: B256::ZERO, + tx_root: None, + execution_requests: Arc::new(ExecutionRequests::default()), + value: U256::from(value), + builder_pubkey: Default::default(), + }, + ) + } + + fn bid_identity(builder_index: u64) -> GloasBuilderIdentity { + helix_common::utils::install_default_crypto_provider(); + GloasBuilderIdentity { builder_index, keypair: helix_types::BlsKeypair::random() } + } + + #[test] + fn build_signed_bid_uses_the_entrys_data_and_configured_identity() { + let chain_info = ChainInfo::default(); + let block_hash = B256::repeat_byte(0x99); + let parent_hash = B256::repeat_byte(0x11); + let parent_root = B256::repeat_byte(0x22); + let entry = payload_entry(block_hash, 42); + let identity = bid_identity(7); + let params = params(parent_hash, parent_root); + + let signed_bid = build_signed_bid(&entry, ¶ms, &identity, &chain_info).unwrap(); + + assert_eq!(signed_bid.message.block_hash.0, block_hash); + assert_eq!(signed_bid.message.parent_block_hash.0, parent_hash); + assert_eq!(signed_bid.message.parent_block_root, parent_root); + assert_eq!(signed_bid.message.builder_index, 7); + assert_eq!(signed_bid.message.value, 42); + assert_eq!(signed_bid.message.execution_payment, 42); + + let epoch = signed_bid.message.slot.epoch(helix_types::MainnetEthSpec::slots_per_epoch()); + let fork = chain_info.spec.fork_at_epoch(epoch); + let domain = chain_info.spec.get_domain( + epoch, + Domain::BeaconBuilder, + &fork, + chain_info.genesis_validators_root, + ); + assert!( + signed_bid + .signature + .verify(&identity.keypair.pk, signed_bid.message.signing_root(domain)) + ); } } diff --git a/crates/relay/src/auctioneer/gloas_payload.rs b/crates/relay/src/auctioneer/gloas_payload.rs new file mode 100644 index 000000000..91f6867ff --- /dev/null +++ b/crates/relay/src/auctioneer/gloas_payload.rs @@ -0,0 +1,35 @@ +use alloy_primitives::B256; +use helix_types::{Slot, execution_requests_to_gloas}; +use tokio::sync::oneshot; +use tracing::warn; + +use crate::{ + api::proposer::HeldGloasPayload, + auctioneer::{bid_adjustor::BidAdjustor, context::Context}, +}; + +impl Context { + /// Looks up the payload a submission held for `block_hash`, converting it to the real Gloas + /// consensus shape for `submitSignedBeaconBlock`'s envelope construction. + pub(super) fn handle_take_held_gloas_payload( + &self, + block_hash: B256, + slot: Slot, + res_tx: oneshot::Sender>, + ) { + let held = self.payloads.get(&block_hash).and_then(|entry| { + let payload = match entry.execution_payload().to_lighthouse_gloas_payload(slot) { + Ok(payload) => payload, + Err(err) => { + warn!(%block_hash, %err, "failed to convert held payload to Gloas shape"); + return None; + } + }; + let execution_requests = + execution_requests_to_gloas(entry.bid_data_ref().execution_requests); + Some(HeldGloasPayload { payload, execution_requests }) + }); + + let _ = res_tx.send(held); + } +} diff --git a/crates/relay/src/auctioneer/handle.rs b/crates/relay/src/auctioneer/handle.rs index 8db373629..47610510e 100644 --- a/crates/relay/src/auctioneer/handle.rs +++ b/crates/relay/src/auctioneer/handle.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use alloy_primitives::B256; use dashmap::DashMap; use futures::{FutureExt, future::Shared}; use helix_common::{ @@ -9,13 +10,13 @@ use helix_common::{ }; use helix_types::{ BlsPublicKey, BlsPublicKeyBytes, ExecPayload, GetPayloadResponse, SigError, - SignedBlindedBeaconBlock, + SignedBlindedBeaconBlock, Slot, }; use tokio::sync::oneshot::{self, Receiver}; use tracing::trace; use crate::{ - api::proposer::{ProposerApiError, get_payload::ProposerApiVersion}, + api::proposer::{HeldGloasPayload, ProposerApiError, get_payload::ProposerApiVersion}, auctioneer::types::{ Event, GetExecutionPayloadBidResult, GetHeaderResult, GetPayloadResult, SubmitBuilderPreferencesResult, @@ -107,6 +108,19 @@ impl AuctioneerHandle { Ok(rx) } + pub fn take_held_gloas_payload( + &self, + block_hash: B256, + slot: Slot, + ) -> Result>, ChannelFull> { + let (tx, rx) = oneshot::channel(); + trace!("sending to auctioneer"); + self.auctioneer + .try_send(Event::TakeHeldGloasPayload { block_hash, slot, res_tx: tx }) + .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 b9ed9aeb1..2424a5326 100644 --- a/crates/relay/src/auctioneer/mod.rs +++ b/crates/relay/src/auctioneer/mod.rs @@ -6,6 +6,7 @@ mod context; mod get_execution_payload_bid; mod get_header; mod get_payload; +mod gloas_payload; mod handle; mod submit_block; mod types; @@ -42,7 +43,11 @@ pub use types::{ use crate::{ HelixSpine, SubmissionDataWithSpan, - api::{FutureBidSubmissionResult, builder::error::BuilderApiError, proposer::ProposerApiError}, + api::{ + FutureBidSubmissionResult, + builder::error::BuilderApiError, + proposer::{GloasBuilderIdentity, ProposerApiError}, + }, auctioneer::types::PendingPayload, housekeeper::SlotUpdate, simulator::{SimRequest, SimResult}, @@ -93,6 +98,7 @@ impl Auctioneer { merged_blocks: Arc>, alert_manager: Arc, operator_api: Option>, + gloas_builder_identity: Arc, ) -> Self { let ctx = Context::new( chain_info, @@ -109,6 +115,7 @@ impl Auctioneer { auctioneer_handle, alert_manager, operator_api, + gloas_builder_identity, ); Self { ctx, @@ -687,6 +694,15 @@ impl State { let _ = res_tx.send(Ok(())); } } + + // take_held_gloas_payload (Gloas), valid regardless of state -- payloads are + // block-hash-keyed, so a lookup after the slot has moved on just misses. + ( + State::Slot { .. } | State::Sorting(_) | State::Broadcasting { .. }, + Event::TakeHeldGloasPayload { block_hash, slot, res_tx }, + ) => { + ctx.handle_take_held_gloas_payload(block_hash, slot, res_tx); + } } } diff --git a/crates/relay/src/auctioneer/types.rs b/crates/relay/src/auctioneer/types.rs index 9b9069053..e69bae17b 100644 --- a/crates/relay/src/auctioneer/types.rs +++ b/crates/relay/src/auctioneer/types.rs @@ -31,7 +31,8 @@ use crate::{ SubmissionDataWithSpan, api::{ HEADER_API_KEY, HEADER_API_TOKEN, HEADER_HYDRATE, HEADER_IS_MERGEABLE, HEADER_MERGE_TYPE, - HEADER_PESSIMISTIC, HEADER_SEQUENCE, HEADER_WITH_ADJUSTMENTS, proposer::ProposerApiError, + HEADER_PESSIMISTIC, HEADER_SEQUENCE, HEADER_WITH_ADJUSTMENTS, + proposer::{HeldGloasPayload, ProposerApiError}, }, auctioneer::MergeResult, gossip::BroadcastPayloadParams, @@ -450,6 +451,13 @@ pub enum Event { max_execution_payment: u64, res_tx: oneshot::Sender, }, + /// Looks up the execution payload a submission held for `block_hash`, so + /// `submitSignedBeaconBlock` can build the envelope fulfilling a proposer's committed bid. + TakeHeldGloasPayload { + block_hash: B256, + slot: Slot, + res_tx: oneshot::Sender>, + }, // Receive multiple of these potentially, assume some light validation GetPayload { block_hash: B256, @@ -477,6 +485,7 @@ impl Event { Event::GetHeader { .. } => "GetHeader", Event::GetExecutionPayloadBid { .. } => "GetExecutionPayloadBid", Event::SubmitBuilderPreferences { .. } => "SubmitBuilderPreferences", + Event::TakeHeldGloasPayload { .. } => "TakeHeldGloasPayload", Event::GetPayload { .. } => "GetPayload", Event::GossipPayload(_) => "GossipPayload", Event::SimResult(_) => "SimResult", diff --git a/crates/relay/src/lib.rs b/crates/relay/src/lib.rs index 70560dd66..027a33a35 100644 --- a/crates/relay/src/lib.rs +++ b/crates/relay/src/lib.rs @@ -28,7 +28,7 @@ pub use crate::block_merging::{OrderTxs, find_unbundled_txs}; pub use crate::{ api::{ Api, BidAdjustor, DefaultBidAdjustor, FutureBidSubmissionResult, builder::TopBidTile, - start_admin_service, start_api_service, + proposer::GloasBuilderIdentity, start_admin_service, start_api_service, }, auctioneer::{ Auctioneer, AuctioneerHandle, BidSorter, Context, Event, PayloadEntry, SimulatorClient, diff --git a/crates/relay/src/main.rs b/crates/relay/src/main.rs index d91e137d9..5db5a7c32 100644 --- a/crates/relay/src/main.rs +++ b/crates/relay/src/main.rs @@ -30,10 +30,11 @@ use helix_operator::spawn_operator_connection; use helix_relay::{ Api, Auctioneer, AuctioneerHandle, BidSorter, BidSubmissionTcpListener, BlockMergeResponse, BlockMergingTile, BroadcastPayloadParams, DataGatherer, DbHandle, DecoderTile, - DefaultBidAdjustor, FutureBidSubmissionResult, GossipedMessage, HelixSpine, HelixSpineConfig, - HousekeeperTile, NewBidSubmission, RegWorkerHandle, RegistrationTile, RelayNetworkManager, - SimRequest, SimResult, SimulatorTile, SlotUpdate, SubmissionDataWithSpan, TopBidTile, - spawn_tokio_monitoring, start_admin_service, start_api_service, start_db_service, + DefaultBidAdjustor, FutureBidSubmissionResult, GloasBuilderIdentity, GossipedMessage, + HelixSpine, HelixSpineConfig, HousekeeperTile, NewBidSubmission, RegWorkerHandle, + RegistrationTile, RelayNetworkManager, SimRequest, SimResult, SimulatorTile, SlotUpdate, + SubmissionDataWithSpan, TopBidTile, spawn_tokio_monitoring, start_admin_service, + start_api_service, start_db_service, }; use helix_types::BlsKeypair; use helix_website::WebsiteService; @@ -240,7 +241,7 @@ async fn run( local_cache.clone(), current_slot_info, chain_info.clone(), - relay_signing_context, + relay_signing_context.clone(), beacon_client, Arc::new(DefaultApiProvider {}), known_validators_loaded, @@ -367,6 +368,11 @@ async fn run( ); } + let gloas_builder_identity = Arc::new(GloasBuilderIdentity { + builder_index: config.gloas_builder_index, + keypair: relay_signing_context.keypair.clone(), + }); + let auctioneer_core = config.cores.auctioneer; let auctioneer = Auctioneer::new( chain_info.as_ref().clone(), @@ -388,6 +394,7 @@ async fn run( merged_blocks, alert_manager.clone(), operator_api.clone(), + gloas_builder_identity, ); attach_tile( auctioneer, diff --git a/crates/types/src/fields.rs b/crates/types/src/fields.rs index 7c1486568..90bc481ba 100644 --- a/crates/types/src/fields.rs +++ b/crates/types/src/fields.rs @@ -43,6 +43,13 @@ pub fn convert_transactions_to_progressive( ) } +/// Real, progressive-list Gloas KZG commitments shape, per EIP-7688. +pub fn convert_kzg_commitments_to_progressive( + commitments: &KzgCommitments, +) -> lh_types::ProgressiveKzgCommitments { + ProgressiveVariableList::new(commitments.iter().map(|c| lh_types::KzgCommitment(c.0)).collect()) +} + /// Converts helix's Electra-shaped builder-submission execution requests into the real, /// progressive-list Gloas shape. `builder_deposits`/`builder_exits` are left empty -- /// TODO(gloas): populate once EIP-8282 builder deposit/exit submission exists. @@ -162,6 +169,22 @@ mod tests { } } + #[test] + fn convert_kzg_commitments_to_progressive_preserves_bytes() { + let commitments = KzgCommitments::new(vec![ + KzgCommitment::repeat_byte(0x11), + KzgCommitment::repeat_byte(0x22), + ]) + .unwrap(); + + let progressive = convert_kzg_commitments_to_progressive(&commitments); + + assert_eq!(progressive.len(), commitments.len()); + for (converted, original) in progressive.as_slice().iter().zip(commitments.iter()) { + assert_eq!(converted.0, original.0); + } + } + #[test] fn execution_requests_to_gloas_preserves_lists_and_defaults_builder_requests() { let requests = ExecutionRequests::random_for_test(&mut rand::rng());