diff --git a/beacon_node/http_api/src/beacon/bid.rs b/beacon_node/http_api/src/beacon/bid.rs new file mode 100644 index 0000000000..b866326c95 --- /dev/null +++ b/beacon_node/http_api/src/beacon/bid.rs @@ -0,0 +1,112 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter, + publish_pubsub_message, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{info, warn}; +use types::SignedExecutionPayloadBid; +use warp::{Filter, Rejection, Reply, hyper::Body, hyper::Response}; + +// POST /eth/v1/beacon/execution_payload_bid (SSZ) +pub(crate) fn post_beacon_execution_payload_bid_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + let bid = SignedExecutionPayloadBid::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +// POST /eth/v1/beacon/execution_payload_bid +pub(crate) fn post_beacon_execution_payload_bid( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |bid: SignedExecutionPayloadBid, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.blocking_response_task(Priority::P0, move || { + publish_execution_payload_bid(bid, &chain, &network_tx) + }) + }, + ) + .boxed() +} + +pub fn publish_execution_payload_bid( + bid: SignedExecutionPayloadBid, + chain: &Arc>, + network_tx: &UnboundedSender>, +) -> Result, Rejection> { + let slot = bid.slot(); + let builder_index = bid.message.builder_index; + + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload bids are not supported before the Gloas fork".into(), + )); + } + + info!( + %slot, + builder_index, + "Publishing signed execution payload bid to network" + ); + + let gossip_verified_bid = chain + .verify_payload_bid_for_gossip(Arc::new(bid)) + .map_err(|e| { + warn!(%slot, error = ?e, "Execution payload bid failed gossip verification"); + warp_utils::reject::custom_bad_request(format!("bid failed gossip verification: {e}")) + })?; + + let bid_for_gossip = gossip_verified_bid.signed_bid.as_ref().clone(); + + publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayloadBid(Box::new(bid_for_gossip)), + )?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs index 2e7fe693d6..d8813b0db5 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -11,7 +11,6 @@ use beacon_chain::payload_envelope_verification::EnvelopeError; use beacon_chain::{BeaconChain, BeaconChainTypes, NotifyExecutionLayer}; use bytes::Bytes; use eth2::types as api_types; -use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use ssz::{Decode, Encode}; @@ -36,10 +35,6 @@ pub(crate) fn post_beacon_execution_payload_envelope_ssz( .and(warp::path("beacon")) .and(warp::path("execution_payload_envelope")) .and(warp::path::end()) - .and(warp::header::exact( - CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, - )) .and(warp::body::bytes()) .and(task_spawner_filter) .and(chain_filter) diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index 9ec1c476f6..1836eb6a86 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,3 +1,4 @@ +pub mod bid; pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 70f7941d36..7979bc99ac 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,9 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::bid::{ + post_beacon_execution_payload_bid, post_beacon_execution_payload_bid_ssz, +}; use crate::beacon::execution_payload_envelope::{ get_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, @@ -1555,6 +1558,22 @@ pub fn serve( network_tx_filter.clone(), ); + // POST beacon/execution_payload_bid + let post_beacon_execution_payload_bid = post_beacon_execution_payload_bid( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_bid (SSZ) + let post_beacon_execution_payload_bid_ssz = post_beacon_execution_payload_bid_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + // GET beacon/execution_payload_envelope/{block_id} let get_beacon_execution_payload_envelope = get_beacon_execution_payload_envelope( eth_v1.clone(), @@ -3448,6 +3467,7 @@ pub fn serve( .uor(post_beacon_blinded_blocks_ssz) .uor(post_beacon_blinded_blocks_v2_ssz) .uor(post_beacon_execution_payload_envelope_ssz) + .uor(post_beacon_execution_payload_bid_ssz) .uor(post_beacon_pool_payload_attestations_ssz) .uor(post_validator_proposer_preferences_ssz), ) @@ -3464,6 +3484,7 @@ pub fn serve( .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_validator_proposer_preferences) .uor(post_beacon_execution_payload_envelope) + .uor(post_beacon_execution_payload_bid) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 5783a011fd..3eef3f39e0 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -48,10 +48,10 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ - Address, Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, ProposerPreferences, - RelativeEpoch, SelectionProof, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, - consts::gloas::BUILDER_INDEX_SELF_BUILD, + Address, Domain, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, Hash256, MainnetEthSpec, + ProposerPreferences, RelativeEpoch, SelectionProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3055,6 +3055,87 @@ impl ApiTester { self } + /// Build a `SignedExecutionPayloadBid` that is structurally valid (correct fields, correct + /// fork name) but will fail gossip verification because no proposer preferences are cached. + fn make_structurally_valid_bid(&self) -> (SignedExecutionPayloadBid, ForkName) { + let head = self.chain.canonical_head.cached_head(); + let slot = self.chain.slot().unwrap(); + let fork_name = self.chain.spec.fork_name_at_slot::(slot); + + let bid = ExecutionPayloadBid { + parent_block_hash: ExecutionBlockHash::zero(), + parent_block_root: head.head_block_root(), + block_hash: ExecutionBlockHash::zero(), + prev_randao: Hash256::zero(), + fee_recipient: Address::zero(), + gas_limit: 30_000_000, + builder_index: 0, + slot, + value: 100, + execution_payment: 0, + blob_kzg_commitments: Default::default(), + execution_requests_root: Hash256::zero(), + }; + + let signed = SignedExecutionPayloadBid { + message: bid, + signature: bls::Signature::empty(), + }; + + (signed, fork_name) + } + + /// JSON bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_json(self) -> Self { + let (bid, fork_name) = self.make_structurally_valid_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid should be rejected by gossip verification" + ); + + self + } + + /// SSZ bid with a valid structure reaches gossip verification and is rejected with 400. + pub async fn test_post_beacon_execution_payload_bid_ssz(self) -> Self { + let (bid, fork_name) = self.make_structurally_valid_bid(); + + let result = self + .client + .post_beacon_execution_payload_bid_ssz(&bid, fork_name) + .await; + + assert!( + result.is_err(), + "bid (SSZ) should be rejected by gossip verification" + ); + + self + } + + /// SSZ bid with a garbage payload is rejected before reaching gossip verification. + pub async fn test_post_beacon_execution_payload_bid_invalid_ssz(self) -> Self { + let fork_name = self + .chain + .spec + .fork_name_at_slot::(self.chain.slot().unwrap()); + + let result = self + .client + .post_beacon_execution_payload_bid_raw_ssz(&[0xde, 0xad, 0xbe, 0xef], fork_name) + .await; + + assert!(result.is_err(), "invalid SSZ bytes should be rejected"); + + self + } + pub async fn test_get_config_fork_schedule(self) -> Self { let result = self.client.get_config_fork_schedule().await.unwrap().data; @@ -9410,3 +9491,18 @@ async fn post_validator_proposer_preferences() { .test_post_validator_proposer_preferences_duplicate() .await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_beacon_execution_payload_bid() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + ApiTester::new_with_hard_forks() + .await + .test_post_beacon_execution_payload_bid_json() + .await + .test_post_beacon_execution_payload_bid_ssz() + .await + .test_post_beacon_execution_payload_bid_invalid_ssz() + .await; +} diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index e9fb44209b..c81444630a 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -46,7 +46,10 @@ use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; -use types::{PayloadAttestationData, PayloadAttestationMessage, SignedProposerPreferences}; +use types::{ + PayloadAttestationData, PayloadAttestationMessage, SignedExecutionPayloadBid, + SignedProposerPreferences, +}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); @@ -2838,6 +2841,78 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST v1/beacon/execution_payload_bid` + pub async fn post_beacon_execution_payload_bid( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version( + path, + bid, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_bid` with raw bytes (for testing invalid SSZ) + pub async fn post_beacon_execution_payload_bid_raw_ssz( + &self, + bytes: &[u8], + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + bytes.to_vec(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_bid` in SSZ format + pub async fn post_beacon_execution_payload_bid_ssz( + &self, + bid: &SignedExecutionPayloadBid, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_bid"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + bid.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// Path for `v1/beacon/execution_payload_envelope/{block_id}` pub fn get_beacon_execution_payload_envelope_path( &self,