From 4db0830196abc1b97254274cd6a528ac5ad9d04b Mon Sep 17 00:00:00 2001 From: owen Date: Thu, 16 Jul 2026 13:49:01 +0100 Subject: [PATCH 1/2] adds header ts for merged blocks --- crates/common/src/local_cache.rs | 4 ++++ .../migrations/V48__add_header_served_time_ns.sql | 2 ++ crates/database/src/postgres/postgres_db_service.rs | 6 +++++- crates/relay/src/api/proposer/get_header.rs | 2 +- crates/relay/src/auctioneer/block_merger.rs | 9 ++++++++- crates/relay/src/auctioneer/get_header.rs | 6 ++++-- crates/relay/src/auctioneer/handle.rs | 3 ++- crates/relay/src/auctioneer/mod.rs | 4 ++-- crates/relay/src/auctioneer/types.rs | 1 + crates/relay/src/block_merging/mod.rs | 1 + crates/types/src/block_merging.rs | 1 + 11 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 crates/database/src/postgres/migrations/V48__add_header_served_time_ns.sql diff --git a/crates/common/src/local_cache.rs b/crates/common/src/local_cache.rs index ee3ae355..11d74d79 100644 --- a/crates/common/src/local_cache.rs +++ b/crates/common/src/local_cache.rs @@ -255,6 +255,10 @@ impl LocalCache { CACHE_SIZE.with_label_values(&["merged_blocks"]).set(self.merged_blocks.len() as f64); } + pub fn update_merged_block(&self, merged_block: MergedBlock) { + self.merged_blocks.insert(merged_block.block_hash(), merged_block); + } + pub fn get_merged_block(&self, block_hash: &B256) -> Option { self.merged_blocks.get(block_hash).map(|b| b.value().clone()) } diff --git a/crates/database/src/postgres/migrations/V48__add_header_served_time_ns.sql b/crates/database/src/postgres/migrations/V48__add_header_served_time_ns.sql new file mode 100644 index 00000000..c4b4383e --- /dev/null +++ b/crates/database/src/postgres/migrations/V48__add_header_served_time_ns.sql @@ -0,0 +1,2 @@ +ALTER TABLE merged_blocks + ADD COLUMN header_served_time_ns BIGINT; \ No newline at end of file diff --git a/crates/database/src/postgres/postgres_db_service.rs b/crates/database/src/postgres/postgres_db_service.rs index e7e478f1..b6f3ec24 100644 --- a/crates/database/src/postgres/postgres_db_service.rs +++ b/crates/database/src/postgres/postgres_db_service.rs @@ -2770,6 +2770,7 @@ impl PostgresDatabaseService { sim_start_time_ns: i64, sim_end_time_ns: i64, finalize_time_ns: i64, + header_served_time_ns: Option, inserted_at: SystemTime, } @@ -2794,12 +2795,13 @@ impl PostgresDatabaseService { sim_start_time_ns: block.trace.sim_start_time_ns as i64, sim_end_time_ns: block.trace.sim_end_time_ns as i64, finalize_time_ns: block.trace.finalize_time_ns as i64, + header_served_time_ns: block.trace.header_served_time_ns.map(|v| v as i64), inserted_at: SystemTime::now(), }); } // Flatten into SQL params - const FIELD_COUNT: usize = 16; + const FIELD_COUNT: usize = 17; let mut params: Vec<&(dyn ToSql + Sync)> = Vec::with_capacity(structured_blocks.len() * FIELD_COUNT); for block in &structured_blocks { @@ -2818,6 +2820,7 @@ impl PostgresDatabaseService { params.push(&block.sim_start_time_ns); params.push(&block.sim_end_time_ns); params.push(&block.finalize_time_ns); + params.push(&block.header_served_time_ns); params.push(&block.inserted_at); } @@ -2838,6 +2841,7 @@ impl PostgresDatabaseService { sim_start_time_ns, sim_end_time_ns, finalize_time_ns, + header_served_time_ns, inserted_at ) VALUES ", ); diff --git a/crates/relay/src/api/proposer/get_header.rs b/crates/relay/src/api/proposer/get_header.rs index 6023078f..e85370af 100644 --- a/crates/relay/src/api/proposer/get_header.rs +++ b/crates/relay/src/api/proposer/get_header.rs @@ -98,7 +98,7 @@ impl ProposerApi { trace!("done sleep"); - let Ok(rx) = proposer_api.auctioneer_handle.get_header(params) else { + let Ok(rx) = proposer_api.auctioneer_handle.get_header(params, is_mev_boost) else { error!("failed to send get_header to auctioneer"); return Err(ProposerApiError::InternalServerError); }; diff --git a/crates/relay/src/auctioneer/block_merger.rs b/crates/relay/src/auctioneer/block_merger.rs index 7b53fe75..7d080cab 100644 --- a/crates/relay/src/auctioneer/block_merger.rs +++ b/crates/relay/src/auctioneer/block_merger.rs @@ -169,7 +169,7 @@ impl BlockMerger { } #[timed] - pub fn get_header(&self, original_bid: &PayloadEntry) -> Option { + pub fn get_header(&self, original_bid: &PayloadEntry, is_mev_boost: bool) -> Option { trace!("fetching merged header"); let start_time = Instant::now(); let entry = self.best_merged_block.as_ref()?; @@ -191,6 +191,12 @@ impl BlockMerger { record_step("get_header", start_time.elapsed()); trace!("fetched merged header"); + let merged_bid = self.local_cache.get_merged_block(entry.bid.block_hash()); + if is_mev_boost && let Some(mut merged_bid) = merged_bid { + merged_bid.trace.header_served_time_ns = Some(utcnow_ns()); + self.local_cache.update_merged_block(merged_bid); + } + if self.config.block_merging_config.is_dry_run { info!("dry run mode enabled, not returning merged header"); return None; @@ -307,6 +313,7 @@ impl BlockMerger { sim_start_time_ns: 0, sim_end_time_ns: 0, finalize_time_ns: 0, + header_served_time_ns: None, }, }), block_hash: base_block_hash, diff --git a/crates/relay/src/auctioneer/get_header.rs b/crates/relay/src/auctioneer/get_header.rs index 1d195606..97912e84 100644 --- a/crates/relay/src/auctioneer/get_header.rs +++ b/crates/relay/src/auctioneer/get_header.rs @@ -23,9 +23,10 @@ impl Context { slot_data: &SlotData, res_tx: oneshot::Sender, producers: &mut HelixSpineProducers, + is_mev_boost: bool, ) { assert_eq!(params.slot, self.bid_slot.as_u64(), "params should already be validated!"); - let _ = res_tx.send(self.get_header(params.parent_hash, slot_data, producers)); + let _ = res_tx.send(self.get_header(params.parent_hash, slot_data, producers, is_mev_boost)); } #[timed] @@ -34,6 +35,7 @@ impl Context { parent_hash: B256, slot_data: &SlotData, producers: &mut HelixSpineProducers, + is_mev_boost: bool, ) -> GetHeaderResult { let Some(best_block_hash) = self.bid_sorter.get_header(&parent_hash) else { warn!(%parent_hash, "no bids for this fork"); @@ -45,7 +47,7 @@ impl Context { return Err(ProposerApiError::NoBidPrepared); }; - if let Some(merged_bid) = self.block_merger.get_header(original_bid) { + if let Some(merged_bid) = self.block_merger.get_header(original_bid, is_mev_boost) { return Ok(merged_bid); }; diff --git a/crates/relay/src/auctioneer/handle.rs b/crates/relay/src/auctioneer/handle.rs index 279a550b..9a5a63f9 100644 --- a/crates/relay/src/auctioneer/handle.rs +++ b/crates/relay/src/auctioneer/handle.rs @@ -50,11 +50,12 @@ impl AuctioneerHandle { pub fn get_header( &self, params: GetHeaderParams, + is_mev_boost: bool, ) -> Result, ChannelFull> { let (tx, rx) = oneshot::channel(); trace!("sending to auctioneer"); self.auctioneer - .try_send(Event::GetHeader { params, res_tx: tx, span: tracing::Span::current() }) + .try_send(Event::GetHeader { params, res_tx: tx, span: tracing::Span::current(), is_mev_boost }) .map_err(|_| ChannelFull)?; Ok(rx) } diff --git a/crates/relay/src/auctioneer/mod.rs b/crates/relay/src/auctioneer/mod.rs index 429f16ee..0f7cd4b4 100644 --- a/crates/relay/src/auctioneer/mod.rs +++ b/crates/relay/src/auctioneer/mod.rs @@ -406,7 +406,7 @@ impl State { } // get_header - (State::Sorting(slot_data), Event::GetHeader { params, res_tx, span }) => { + (State::Sorting(slot_data), Event::GetHeader { params, res_tx, span, is_mev_boost }) => { let _guard = span.enter(); trace!("received in auctioneer"); @@ -425,7 +425,7 @@ impl State { warn!(req =% params.pubkey, this =% slot_data.registration_data.entry.registration.message.pubkey, "get header for mismatched proposer"); let _ = res_tx.send(Err(ProposerApiError::NoBidPrepared)); } else { - ctx.handle_get_header(params, slot_data, res_tx, producers) + ctx.handle_get_header(params, slot_data, res_tx, producers, is_mev_boost) } trace!("finished processing"); diff --git a/crates/relay/src/auctioneer/types.rs b/crates/relay/src/auctioneer/types.rs index 1a71a52c..cefa0891 100644 --- a/crates/relay/src/auctioneer/types.rs +++ b/crates/relay/src/auctioneer/types.rs @@ -454,6 +454,7 @@ pub enum Event { params: GetHeaderParams, res_tx: oneshot::Sender, span: tracing::Span, + is_mev_boost: bool, }, // Receive multiple of these potentially, assume some light validation GetPayload { diff --git a/crates/relay/src/block_merging/mod.rs b/crates/relay/src/block_merging/mod.rs index 0f12e354..8b1c7b77 100644 --- a/crates/relay/src/block_merging/mod.rs +++ b/crates/relay/src/block_merging/mod.rs @@ -64,6 +64,7 @@ fn merged_block_to_response(m: MergedBlockV1) -> BlockMergeResponse { sim_start_time_ns: m.trace.sim_start_ns, sim_end_time_ns: m.trace.sim_end_ns, finalize_time_ns: m.trace.finalize_ns, + header_served_time_ns: None, // filled in by the auctioneer when it sends the header to the proposer }, } } diff --git a/crates/types/src/block_merging.rs b/crates/types/src/block_merging.rs index 4bfd4e52..29cc4342 100644 --- a/crates/types/src/block_merging.rs +++ b/crates/types/src/block_merging.rs @@ -269,6 +269,7 @@ pub struct MergedBlockTrace { pub sim_start_time_ns: u64, pub sim_end_time_ns: u64, pub finalize_time_ns: u64, + pub header_served_time_ns: Option, } #[cfg(test)] From bb58a21262289479dfb9bddebbcafa5488d317ec Mon Sep 17 00:00:00 2001 From: owen Date: Thu, 16 Jul 2026 14:04:15 +0100 Subject: [PATCH 2/2] fix up --- crates/common/src/local_cache.rs | 6 ++++-- crates/relay/src/auctioneer/block_merger.rs | 12 +++++++----- crates/relay/src/auctioneer/get_header.rs | 3 ++- crates/relay/src/auctioneer/handle.rs | 7 ++++++- crates/relay/src/auctioneer/mod.rs | 5 ++++- crates/relay/src/block_merging/mod.rs | 3 ++- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/common/src/local_cache.rs b/crates/common/src/local_cache.rs index 11d74d79..8d6e8587 100644 --- a/crates/common/src/local_cache.rs +++ b/crates/common/src/local_cache.rs @@ -255,8 +255,10 @@ impl LocalCache { CACHE_SIZE.with_label_values(&["merged_blocks"]).set(self.merged_blocks.len() as f64); } - pub fn update_merged_block(&self, merged_block: MergedBlock) { - self.merged_blocks.insert(merged_block.block_hash(), merged_block); + pub fn set_merged_block_header_served(&self, block_hash: &B256, time_ns: u64) { + if let Some(mut b) = self.merged_blocks.get_mut(block_hash) { + b.trace.header_served_time_ns = Some(time_ns); + } } pub fn get_merged_block(&self, block_hash: &B256) -> Option { diff --git a/crates/relay/src/auctioneer/block_merger.rs b/crates/relay/src/auctioneer/block_merger.rs index 7d080cab..8b4b191f 100644 --- a/crates/relay/src/auctioneer/block_merger.rs +++ b/crates/relay/src/auctioneer/block_merger.rs @@ -169,7 +169,11 @@ impl BlockMerger { } #[timed] - pub fn get_header(&self, original_bid: &PayloadEntry, is_mev_boost: bool) -> Option { + pub fn get_header( + &self, + original_bid: &PayloadEntry, + is_mev_boost: bool, + ) -> Option { trace!("fetching merged header"); let start_time = Instant::now(); let entry = self.best_merged_block.as_ref()?; @@ -191,10 +195,8 @@ impl BlockMerger { record_step("get_header", start_time.elapsed()); trace!("fetched merged header"); - let merged_bid = self.local_cache.get_merged_block(entry.bid.block_hash()); - if is_mev_boost && let Some(mut merged_bid) = merged_bid { - merged_bid.trace.header_served_time_ns = Some(utcnow_ns()); - self.local_cache.update_merged_block(merged_bid); + if is_mev_boost { + self.local_cache.set_merged_block_header_served(entry.bid.block_hash(), utcnow_ns()); } if self.config.block_merging_config.is_dry_run { diff --git a/crates/relay/src/auctioneer/get_header.rs b/crates/relay/src/auctioneer/get_header.rs index 97912e84..164e3f5d 100644 --- a/crates/relay/src/auctioneer/get_header.rs +++ b/crates/relay/src/auctioneer/get_header.rs @@ -26,7 +26,8 @@ impl Context { is_mev_boost: bool, ) { assert_eq!(params.slot, self.bid_slot.as_u64(), "params should already be validated!"); - let _ = res_tx.send(self.get_header(params.parent_hash, slot_data, producers, is_mev_boost)); + let _ = + res_tx.send(self.get_header(params.parent_hash, slot_data, producers, is_mev_boost)); } #[timed] diff --git a/crates/relay/src/auctioneer/handle.rs b/crates/relay/src/auctioneer/handle.rs index 9a5a63f9..be2135c6 100644 --- a/crates/relay/src/auctioneer/handle.rs +++ b/crates/relay/src/auctioneer/handle.rs @@ -55,7 +55,12 @@ impl AuctioneerHandle { let (tx, rx) = oneshot::channel(); trace!("sending to auctioneer"); self.auctioneer - .try_send(Event::GetHeader { params, res_tx: tx, span: tracing::Span::current(), is_mev_boost }) + .try_send(Event::GetHeader { + params, + res_tx: tx, + span: tracing::Span::current(), + is_mev_boost, + }) .map_err(|_| ChannelFull)?; Ok(rx) } diff --git a/crates/relay/src/auctioneer/mod.rs b/crates/relay/src/auctioneer/mod.rs index 0f7cd4b4..c1afa644 100644 --- a/crates/relay/src/auctioneer/mod.rs +++ b/crates/relay/src/auctioneer/mod.rs @@ -406,7 +406,10 @@ impl State { } // get_header - (State::Sorting(slot_data), Event::GetHeader { params, res_tx, span, is_mev_boost }) => { + ( + State::Sorting(slot_data), + Event::GetHeader { params, res_tx, span, is_mev_boost }, + ) => { let _guard = span.enter(); trace!("received in auctioneer"); diff --git a/crates/relay/src/block_merging/mod.rs b/crates/relay/src/block_merging/mod.rs index 8b1c7b77..f2f65cdd 100644 --- a/crates/relay/src/block_merging/mod.rs +++ b/crates/relay/src/block_merging/mod.rs @@ -64,7 +64,8 @@ fn merged_block_to_response(m: MergedBlockV1) -> BlockMergeResponse { sim_start_time_ns: m.trace.sim_start_ns, sim_end_time_ns: m.trace.sim_end_ns, finalize_time_ns: m.trace.finalize_ns, - header_served_time_ns: None, // filled in by the auctioneer when it sends the header to the proposer + header_served_time_ns: None, /* filled in by the auctioneer when it sends the header + * to the proposer */ }, } }