Add API for posting bid

This commit is contained in:
Eitan Seri-Levi
2026-05-24 13:18:27 +03:00
parent 5045e8dd85
commit e4f137dc04
6 changed files with 310 additions and 10 deletions

View File

@@ -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<T: BeaconChainTypes>(
eth_v1: EthV1Filter,
task_spawner_filter: TaskSpawnerFilter<T>,
chain_filter: ChainFilter<T>,
network_tx_filter: NetworkTxFilter<T>,
) -> 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<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| {
task_spawner.blocking_response_task(Priority::P0, move || {
let bid = SignedExecutionPayloadBid::<T::EthSpec>::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<T: BeaconChainTypes>(
eth_v1: EthV1Filter,
task_spawner_filter: TaskSpawnerFilter<T>,
chain_filter: ChainFilter<T>,
network_tx_filter: NetworkTxFilter<T>,
) -> 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<T::EthSpec>,
task_spawner: TaskSpawner<T::EthSpec>,
chain: Arc<BeaconChain<T>>,
network_tx: UnboundedSender<NetworkMessage<T::EthSpec>>| {
task_spawner.blocking_response_task(Priority::P0, move || {
publish_execution_payload_bid(bid, &chain, &network_tx)
})
},
)
.boxed()
}
pub fn publish_execution_payload_bid<T: BeaconChainTypes>(
bid: SignedExecutionPayloadBid<T::EthSpec>,
chain: &Arc<BeaconChain<T>>,
network_tx: &UnboundedSender<NetworkMessage<T::EthSpec>>,
) -> Result<Response<Body>, 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())
}

View File

@@ -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<T: BeaconChainTypes>(
.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)

View File

@@ -1,3 +1,4 @@
pub mod bid;
pub mod execution_payload_envelope;
pub mod pool;
pub mod states;

View File

@@ -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<T: BeaconChainTypes>(
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(),
@@ -3445,6 +3464,7 @@ pub fn serve<T: BeaconChainTypes>(
.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),
)
@@ -3461,6 +3481,7 @@ pub fn serve<T: BeaconChainTypes>(
.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)

View File

@@ -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<E>, 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::<E>(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::<E>(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;
}

View File

@@ -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<E: EthSpec>(
&self,
bid: &SignedExecutionPayloadBid<E>,
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<E: EthSpec>(
&self,
bid: &SignedExecutionPayloadBid<E>,
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,