diff --git a/crates/relay/src/api/proposer/get_payload.rs b/crates/relay/src/api/proposer/get_payload.rs index 7a2cda4e..522c30c7 100644 --- a/crates/relay/src/api/proposer/get_payload.rs +++ b/crates/relay/src/api/proposer/get_payload.rs @@ -21,8 +21,8 @@ use tracing::{Instrument, error, info, warn}; use super::ProposerApi; use crate::{ api::{Api, proposer::error::ProposerApiError}, - auctioneer::{GetPayloadResultData, PayloadBidData}, - beacon::types::BroadcastValidation, + auctioneer::{Event, GetPayloadResultData, PayloadBidData}, + beacon::{error::BeaconClientError, types::BroadcastValidation}, database::SavePayloadParams, gossip::{BroadcastGetPayloadParams, BroadcastPayloadParams}, }; @@ -337,6 +337,25 @@ impl ProposerApi { ) .await { + if matches!(err, BeaconClientError::BlockValidationFailed(..)) { + let builder_pubkey = bid.builder_pubkey; + let reason = format!("Block validation failed: {err}"); + + let _ = self_clone.auctioneer_handle.send_event(Event::BuilderDemotion { + slot, + builder_pubkey, + block_hash, + reason, + }); + + warn!( + %builder_pubkey, + %block_hash, + slot = %slot, + "BuilderDemotion event sent due to BlockValidationFailed" + ); + } + error!(%err, "error publishing block"); failed_publishing = true; }; diff --git a/crates/relay/src/auctioneer/context.rs b/crates/relay/src/auctioneer/context.rs index 98362934..3922d618 100644 --- a/crates/relay/src/auctioneer/context.rs +++ b/crates/relay/src/auctioneer/context.rs @@ -210,6 +210,38 @@ impl Context { }; self.payloads.insert(block_hash, payload); } + + pub fn handle_builder_demotion( + &mut self, + slot: Slot, + builder_pubkey: BlsPublicKeyBytes, + block_hash: B256, + reason: String, + ) { + if self.cache.demote_builder(&builder_pubkey) { + warn!(%builder_pubkey, %block_hash, "builder demoted due to block validation failure"); + + let db = self.db.clone(); + let failsafe = self.sim_manager.failsafe_triggered.clone(); + let slot_u64 = slot.as_u64(); + + spawn_tracked!(async move { + if let Err(err) = + db.db_demote_builder(slot_u64, &builder_pubkey, &block_hash, reason).await + { + failsafe.store(true, Ordering::Relaxed); + error!( + %builder_pubkey, + %err, + %block_hash, + "failed to persist builder demotion in DB — pausing optimistic submissions" + ); + } + }); + } else { + warn!(%reason, %builder_pubkey, %block_hash, "builder already demoted, skipping demotion"); + } + } } impl Deref for Context { diff --git a/crates/relay/src/auctioneer/handle.rs b/crates/relay/src/auctioneer/handle.rs index 49c5925a..3e5ffd3e 100644 --- a/crates/relay/src/auctioneer/handle.rs +++ b/crates/relay/src/auctioneer/handle.rs @@ -84,6 +84,11 @@ impl AuctioneerHandle { trace!("sending to worker"); self.auctioneer.try_send(Event::GossipPayload(req)).map_err(|_| ChannelFull) } + + pub fn send_event(&self, event: Event) -> Result<(), ChannelFull> { + trace!("sending event to auctioneer: {:?}", event.as_str()); + self.auctioneer.try_send(event).map_err(|_| ChannelFull) + } } pub struct ChannelFull; diff --git a/crates/relay/src/auctioneer/mod.rs b/crates/relay/src/auctioneer/mod.rs index fd45dd9f..5fffeb9b 100644 --- a/crates/relay/src/auctioneer/mod.rs +++ b/crates/relay/src/auctioneer/mod.rs @@ -535,6 +535,14 @@ impl State { warn!(curr =% bid_slot, gossip_slot = payload.slot, "received early or late gossip payload"); } } + + // Builder demotion event + ( + State::Slot { .. } | State::Sorting(_) | State::Broadcasting { .. }, + Event::BuilderDemotion { slot, builder_pubkey, block_hash, reason }, + ) => { + ctx.handle_builder_demotion(slot, builder_pubkey, block_hash, reason); + } } } diff --git a/crates/relay/src/auctioneer/types.rs b/crates/relay/src/auctioneer/types.rs index 7158f24c..93e39e0a 100644 --- a/crates/relay/src/auctioneer/types.rs +++ b/crates/relay/src/auctioneer/types.rs @@ -291,6 +291,12 @@ pub enum Event { is_synced: bool, }, MergeResult(BlockMergeResult), + BuilderDemotion { + slot: Slot, + builder_pubkey: BlsPublicKeyBytes, + block_hash: B256, + reason: String, + }, } impl Event { @@ -304,6 +310,7 @@ impl Event { Event::SimResult(_) => "SimResult", Event::SimulatorSync { .. } => "SimulatorSync", Event::MergeResult(_) => "MergeResult", + Event::BuilderDemotion { .. } => "BuilderDemotion", } } } diff --git a/crates/relay/src/beacon/beacon_client.rs b/crates/relay/src/beacon/beacon_client.rs index 79172604..290c81da 100644 --- a/crates/relay/src/beacon/beacon_client.rs +++ b/crates/relay/src/beacon/beacon_client.rs @@ -170,6 +170,11 @@ impl BeaconClient { if body.contains("duplicate block") { return Ok(200); } + if body.contains("ExecutionPayloadError(RejectedByExecutionEngine") && + (body.contains("block hash mismatch") || body.contains("validation_error")) + { + return Err(BeaconClientError::BlockValidationFailed(body)); + } Ok(code) } _ => { diff --git a/crates/relay/src/beacon/error.rs b/crates/relay/src/beacon/error.rs index 6960ddd6..693321b8 100644 --- a/crates/relay/src/beacon/error.rs +++ b/crates/relay/src/beacon/error.rs @@ -29,6 +29,9 @@ pub enum BeaconClientError { #[error("block integration failed")] BlockIntegrationFailed, + + #[error("block validation failed: {0}")] + BlockValidationFailed(String), } impl IntoResponse for BeaconClientError { diff --git a/crates/relay/src/beacon/multi_beacon_client.rs b/crates/relay/src/beacon/multi_beacon_client.rs index a4ba7a60..43b3dc6a 100644 --- a/crates/relay/src/beacon/multi_beacon_client.rs +++ b/crates/relay/src/beacon/multi_beacon_client.rs @@ -233,6 +233,10 @@ impl MultiBeaconClient { return Ok(()); } + Err(BeaconClientError::BlockValidationFailed(details)) => { + last_error = Some(BeaconClientError::BlockValidationFailed(details)); + } + Err(err) => { last_error = Some(err); }