diff --git a/crates/api/src/builder/api.rs b/crates/api/src/builder/api.rs index 65de884..a7f20ba 100644 --- a/crates/api/src/builder/api.rs +++ b/crates/api/src/builder/api.rs @@ -679,42 +679,23 @@ where .await?; // Fetch constraints, and if available verify inclusion proofs and save them to cache - if let Some(constraints) = api.auctioneer.get_constraints(payload.slot()).await? { - let transactions_root: B256 = payload - .transactions() - .clone() - .hash_tree_root()? - .to_vec() - .as_slice() - .try_into() - .map_err(|e| { - error!(error = %e, "failed to convert root to hash32"); - BuilderApiError::InternalError - })?; - let proofs = payload.proofs().ok_or(BuilderApiError::InclusionProofsNotFound)?; - let constraints_proofs: Vec<_> = constraints.iter().map(|c| &c.proof_data).collect(); - - verify_multiproofs(constraints_proofs.as_slice(), proofs, transactions_root).map_err( - |e| { - error!(error = %e, "failed to verify inclusion proofs"); - BuilderApiError::InclusionProofVerificationFailed(e) - }, - )?; - - // Save inclusion proof to auctioneer. - api.save_inclusion_proof( - payload.slot(), - payload.proposer_public_key(), - payload.block_hash(), - proofs, - &request_id, - ) - .await?; - - info!(request_id = %request_id, head_slot, "inclusion proofs verified and saved to auctioneer"); + let skip_inclusion_proof_verify_and_save = api + .relay_config + .constraints_api_config + .max_block_value_to_verify_wei + .map_or(false, |max_block_value_to_verify| payload.value() > max_block_value_to_verify); + if !skip_inclusion_proof_verify_and_save { + if let Err(err) = api.verify_and_save_inclusion_proofs(&payload, &request_id).await { + warn!(request_id = %request_id, error = %err, "failed to verify and save inclusion proofs"); + return Err(err) + } } else { - info!(request_id = %request_id, "no constraints found for slot, proof verification is not needed"); - }; + info!( + request_id = %request_id, + block_value = %payload.value(), + "block value is greater than max value to verify, inclusion proof verification and saving is skipped", + ); + } // If cancellations are enabled, then abort now if there is a later submission if is_cancellations_enabled { @@ -2197,6 +2178,83 @@ where ); } } + + /// Fetch constraints, and if available verify inclusion proofs and save them to cache. + async fn verify_and_save_inclusion_proofs( + &self, + payload: &SignedBidSubmission, + request_id: &Uuid, + ) -> Result<(), BuilderApiError> { + if let Some(constraints) = self.auctioneer.get_constraints(payload.slot()).await? { + let transactions_root: B256 = payload + .transactions() + .clone() + .hash_tree_root()? + .to_vec() + .as_slice() + .try_into() + .map_err(|error| { + error!(?error, "failed to convert root to hash32"); + BuilderApiError::InternalError + })?; + + let proofs = match payload.proofs() { + Some(p) => p, + None => { + if self + .relay_config + .constraints_api_config + .allow_relay_side_constraint_validation + { + info!(%request_id, "no inclusion proofs found, but relay side constraint validation is allowed"); + // Validate by comparing transactions directly between payload and constraints. + // TODO: the relay should probably compute the inclusion proof here. + let flattened_contraints_txs: Vec<&_> = constraints + .iter() + .flat_map(|c| c.signed_constraints.message.transactions.iter()) + .collect(); + + if payload.transactions().len() != flattened_contraints_txs.len() { + return Err(BuilderApiError::RelaySideInclusionProofVerificationFailed) + } + if payload + .transactions() + .iter() + .zip(flattened_contraints_txs.iter()) + .any(|(a, b)| &a != b) + { + return Err(BuilderApiError::RelaySideInclusionProofVerificationFailed) + } + + return Ok(()) + } + return Err(BuilderApiError::InclusionProofsNotFound) + } + }; + + let constraints_proofs: Vec<_> = constraints.iter().map(|c| &c.proof_data).collect(); + verify_multiproofs(constraints_proofs.as_slice(), proofs, transactions_root).map_err( + |e| { + error!(error = %e, "failed to verify inclusion proofs"); + BuilderApiError::InclusionProofVerificationFailed(e) + }, + )?; + + // Save inclusion proof to auctioneer. + self.save_inclusion_proof( + payload.slot(), + payload.proposer_public_key(), + payload.block_hash(), + proofs, + request_id, + ) + .await?; + info!(%request_id, "inclusion proofs verified and saved to auctioneer"); + } else { + info!(%request_id, "no constraints found for slot, proof verification is not needed"); + }; + Ok(()) + } } // STATE SYNC diff --git a/crates/api/src/builder/error.rs b/crates/api/src/builder/error.rs index 464724c..d194b1c 100644 --- a/crates/api/src/builder/error.rs +++ b/crates/api/src/builder/error.rs @@ -166,6 +166,9 @@ pub enum BuilderApiError { #[error("incorrect slot for constraints request {0}")] IncorrectSlot(u64), + + #[error("relay side inclusion proof verification failed")] + RelaySideInclusionProofVerificationFailed, } impl IntoResponse for BuilderApiError { @@ -336,6 +339,9 @@ impl IntoResponse for BuilderApiError { BuilderApiError::IncorrectSlot(slot) => { (StatusCode::BAD_REQUEST, format!("incorrect slot for constraints request {slot}")).into_response() } + BuilderApiError::RelaySideInclusionProofVerificationFailed => { + (StatusCode::BAD_REQUEST, "relay side inclusion proof verification failed").into_response() + } } } } diff --git a/crates/api/src/proposer/api.rs b/crates/api/src/proposer/api.rs index f2ebb9c..2dc1fcf 100644 --- a/crates/api/src/proposer/api.rs +++ b/crates/api/src/proposer/api.rs @@ -45,7 +45,7 @@ use helix_common::{ signed_proposal::VersionedSignedProposal, try_execution_header_from_payload, versioned_payload::PayloadAndBlobs, - BidRequest, Filtering, GetHeaderTrace, GetPayloadTrace, RegisterValidatorsTrace, + BidRequest, Filtering, GetHeaderTrace, GetPayloadTrace, RegisterValidatorsTrace, RelayConfig, ValidatorPreferences, }; use helix_database::DatabaseService; @@ -88,7 +88,7 @@ where chain_info: Arc, validator_preferences: Arc, - target_get_payload_propagation_duration_ms: u64, + relay_config: RelayConfig, } impl ProposerApi @@ -107,8 +107,8 @@ where chain_info: Arc, slot_update_subscription: Sender>, validator_preferences: Arc, - target_get_payload_propagation_duration_ms: u64, gossip_receiver: Receiver, + relay_config: RelayConfig, ) -> Self { let api = Self { auctioneer, @@ -119,7 +119,7 @@ where curr_slot_info: Arc::new(RwLock::new((0, None))), chain_info, validator_preferences, - target_get_payload_propagation_duration_ms, + relay_config, }; // Spin up gossip processing task @@ -535,18 +535,12 @@ where return Err(ProposerApiError::BidValueZero) } - // Get inclusion proofs - let proofs = proposer_api - .auctioneer - .get_inclusion_proof(slot, &bid_request.public_key, bid.block_hash()) - .await?; - // Save trace to DB proposer_api .save_get_header_call( slot, bid_request.parent_hash, - bid_request.public_key, + bid_request.public_key.clone(), bid.block_hash().clone(), trace, request_id, @@ -554,6 +548,29 @@ where ) .await; + // If the block value is greater than the max value to verify, return the bid + // without proofs. + let value_above_max_to_verify = proposer_api + .relay_config + .constraints_api_config + .max_block_value_to_verify_wei + .map_or(false, |max| bid.value() > max); + if value_above_max_to_verify { + info!( + %request_id, + slot, + value = ?bid.value(), + "block value is greater than max value to verify, returning bid without proofs", + ); + return Ok(axum::Json(bid)) + } + + // Get inclusion proofs + let proofs = proposer_api + .auctioneer + .get_inclusion_proof(slot, &bid_request.public_key, bid.block_hash()) + .await?; + // Attach the proofs to the bid before sending it back if let Some(proofs) = proofs { bid.set_inclusion_proofs(proofs); @@ -937,6 +954,7 @@ where let elapsed_since_propagate_start_ms = (get_nanos_timestamp()?.saturating_sub(trace.beacon_client_broadcast)) / 1_000_000; let remaining_sleep_ms = self + .relay_config .target_get_payload_propagation_duration_ms .saturating_sub(elapsed_since_propagate_start_ms); if remaining_sleep_ms > 0 { diff --git a/crates/api/src/proposer/tests.rs b/crates/api/src/proposer/tests.rs index 15041ff..ef61bf3 100644 --- a/crates/api/src/proposer/tests.rs +++ b/crates/api/src/proposer/tests.rs @@ -997,8 +997,8 @@ mod proposer_api_tests { Arc::new(ChainInfo::for_holesky()), slot_update_sender.clone(), Arc::new(ValidatorPreferences::default()), - 0, gossip_receiver, + Arc::new(Default::default()), ); let mut x = gen_signed_vr(); diff --git a/crates/api/src/service.rs b/crates/api/src/service.rs index 085b249..39b84f9 100644 --- a/crates/api/src/service.rs +++ b/crates/api/src/service.rs @@ -182,8 +182,8 @@ impl ApiService { chain_info.clone(), slot_update_sender.clone(), validator_preferences.clone(), - config.target_get_payload_propagation_duration_ms, proposer_gossip_receiver, + config.clone(), )); let data_api = Arc::new(DataApiProd::new(validator_preferences.clone(), db.clone())); diff --git a/crates/api/src/test_utils.rs b/crates/api/src/test_utils.rs index 6012121..0e9107d 100644 --- a/crates/api/src/test_utils.rs +++ b/crates/api/src/test_utils.rs @@ -56,8 +56,8 @@ pub fn app() -> Router { Arc::new(ChainInfo::for_mainnet()), slot_update_sender, Arc::new(ValidatorPreferences::default()), - 0, gossip_receiver, + Arc::new(Default::default()), )); let data_api = Arc::new(DataApi::::new( @@ -218,8 +218,8 @@ pub fn proposer_api_app() -> ( Arc::new(ChainInfo::for_mainnet()), slot_update_sender.clone(), Arc::new(ValidatorPreferences::default()), - 0, gossip_receiver, + Arc::new(Default::default()), )); let router = Router::new() diff --git a/crates/common/src/config.rs b/crates/common/src/config.rs index a7a5434..edd3129 100644 --- a/crates/common/src/config.rs +++ b/crates/common/src/config.rs @@ -1,6 +1,9 @@ use crate::{api::*, BuilderInfo, ValidatorPreferences}; use clap::Parser; -use ethereum_consensus::{primitives::BlsPublicKey, ssz::prelude::Node}; +use ethereum_consensus::{ + primitives::BlsPublicKey, + ssz::prelude::{Node, U256}, +}; use helix_utils::{ request_encoding::Encoding, serde::{default_bool, deserialize_url, serialize_url}, @@ -136,11 +139,21 @@ pub struct ConstraintsApiConfig { /// [`/constraints/v1/builder/constraints`](https://docs.boltprotocol.xyz/technical-docs/api/builder#constraints) /// endpoint will not be checked. pub check_constraints_signature: bool, + /// Only verify and save inclusion proofs if the block value is less than this threshold. + /// We do this to ensure that high value blocks are not rejected. + pub max_block_value_to_verify_wei: Option, + /// If true, submissions for slots with constraints but, without inclusion proofs + /// will be allowed. Constraint validity will be verified in the relay. + pub allow_relay_side_constraint_validation: bool, } impl Default for ConstraintsApiConfig { fn default() -> Self { - ConstraintsApiConfig { check_constraints_signature: true } + ConstraintsApiConfig { + check_constraints_signature: true, + max_block_value_to_verify_wei: None, + allow_relay_side_constraint_validation: false, + } } }