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());