From 25853847ef43d384d35cc8d461f0bc670ba6e228 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 20:28:28 -0800 Subject: [PATCH] Publish payload --- beacon_node/beacon_chain/src/beacon_block.rs | 3 +- .../beacon_chain/src/execution_payload_bid.rs | 3 +- beacon_node/http_api/src/lib.rs | 63 +++- .../src/publish_execution_payload_envelope.rs | 57 ++++ beacon_node/http_api/src/validator/mod.rs | 7 +- beacon_node/http_api/src/version.rs | 1 - beacon_node/http_api/tests/tests.rs | 269 +++++------------- common/eth2/src/lib.rs | 23 ++ .../validator_services/src/block_service.rs | 24 +- 9 files changed, 232 insertions(+), 218 deletions(-) create mode 100644 beacon_node/http_api/src/publish_execution_payload_envelope.rs diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index 3b1fa00027..3b69318427 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; -use std::u64; use bls::Signature; use operation_pool::CompactAttestationRef; @@ -179,6 +178,7 @@ impl BeaconChain { } #[allow(clippy::too_many_arguments)] + #[allow(clippy::type_complexity)] fn produce_partial_beacon_block_gloas( self: &Arc, mut state: BeaconState, @@ -415,6 +415,7 @@ impl BeaconChain { )) } + #[allow(clippy::type_complexity)] fn complete_partial_beacon_block_gloas( &self, partial_beacon_block: PartialBeaconBlock, diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs index 213fb71d4e..fe9974d93c 100644 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, u64}; +use std::sync::Arc; use bls::Signature; use execution_layer::{BlockProposalContentsType, BuilderParams}; @@ -34,6 +34,7 @@ impl BeaconChain { /// /// For local building, payload data is always returned (`Some`). /// For trustless building, the builder provides the envelope separately, so `None` is returned. + #[allow(clippy::type_complexity)] #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 3cb52d8224..812d0244e5 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -23,6 +23,7 @@ mod produce_block; mod proposer_duties; mod publish_attestations; mod publish_blocks; +mod publish_execution_payload_envelope; mod standard_block_rewards; mod state_id; mod sync_committee_rewards; @@ -71,7 +72,7 @@ pub use publish_blocks::{ }; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; -use ssz::Encode; +use ssz::{Decode, Encode}; pub use state_id::StateId; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -90,7 +91,7 @@ use tokio_stream::{ use tracing::{debug, info, warn}; use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, - SignedBlindedBeaconBlock, Slot, + SignedBlindedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, }; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, @@ -1486,6 +1487,60 @@ pub fn serve( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/execution_payload_envelope + let post_beacon_execution_payload_envelope = eth_v1 + .clone() + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .then( + |envelope: SignedExecutionPayloadEnvelope, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_execution_payload_envelope::publish_execution_payload_envelope( + envelope, chain, &network_tx, + ) + .await + }) + }, + ); + + // POST beacon/execution_payload_envelope (SSZ) + let post_beacon_execution_payload_envelope_ssz = eth_v1 + .clone() + .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.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let envelope = + SignedExecutionPayloadEnvelope::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_envelope::publish_execution_payload_envelope( + envelope, chain, &network_tx, + ) + .await + }) + }, + ); + let beacon_rewards_path = eth_v1 .clone() .and(warp::path("beacon")) @@ -3374,7 +3429,8 @@ pub fn serve( post_beacon_blocks_ssz .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) - .uor(post_beacon_blinded_blocks_v2_ssz), + .uor(post_beacon_blinded_blocks_v2_ssz) + .uor(post_beacon_execution_payload_envelope_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) @@ -3386,6 +3442,7 @@ pub fn serve( .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_execution_payload_envelope) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/src/publish_execution_payload_envelope.rs b/beacon_node/http_api/src/publish_execution_payload_envelope.rs new file mode 100644 index 0000000000..0496f94a92 --- /dev/null +++ b/beacon_node/http_api/src/publish_execution_payload_envelope.rs @@ -0,0 +1,57 @@ +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{info, warn}; +use types::SignedExecutionPayloadEnvelope; +use warp::{Rejection, Reply, reply::Response}; + +/// Publishes a signed execution payload envelope to the network. +pub async fn publish_execution_payload_envelope( + envelope: SignedExecutionPayloadEnvelope, + chain: Arc>, + network_tx: &UnboundedSender>, +) -> Result { + let slot = envelope.message.slot; + let beacon_block_root = envelope.message.beacon_block_root; + + // Basic validation: check that the slot is reasonable + let current_slot = chain + .slot() + .map_err(|_| warp_utils::reject::custom_server_error("Unable to get current slot".into()))?; + + // Don't accept envelopes too far in the future + if slot > current_slot + 1 { + return Err(warp_utils::reject::custom_bad_request(format!( + "Envelope slot {} is too far in the future (current slot: {})", + slot, current_slot + ))); + } + + // TODO(gloas): Add more validation: + // - Verify the signature + // - Check builder_index is valid + // - Verify the envelope references a known block + + info!( + %slot, + %beacon_block_root, + builder_index = envelope.message.builder_index, + "Publishing signed execution payload envelope to network" + ); + + // Publish to the network + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayload(Box::new(envelope)), + ) + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + warp_utils::reject::custom_server_error( + "Unable to publish execution payload envelope to network".into(), + ) + })?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 1575ad3a5c..612b1fafec 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -6,7 +6,7 @@ use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message, }; -use crate::version::{V3, V4}; +use crate::version::V3; use crate::{StateId, attester_duties, proposer_duties, sync_committees}; use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; @@ -336,6 +336,7 @@ pub fn get_validator_blocks( } // GET validator/execution_payload_bid/ +#[allow(dead_code)] pub fn get_validator_execution_payload_bid( eth_v1: EthV1Filter, chain_filter: ChainFilter, @@ -357,10 +358,10 @@ pub fn get_validator_execution_payload_bid( .and(chain_filter) .then( |slot: Slot, - accept_header: Option, + _accept_header: Option, not_synced_filter: Result<(), Rejection>, task_spawner: TaskSpawner, - chain: Arc>| { + _chain: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { debug!( ?slot, diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index e1ba628032..371064c886 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -14,7 +14,6 @@ use warp::reply::{self, Reply, Response}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); -pub const V4: EndpointVersion = EndpointVersion(4); #[derive(Debug, PartialEq, Clone, Serialize)] pub enum ResponseIncludesVersion { diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index f8d345e87c..392f7c7869 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -47,7 +47,8 @@ use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + SignedExecutionPayloadEnvelope, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, }; type E = MainnetEthSpec; @@ -3802,15 +3803,31 @@ impl ApiTester { // Verify that the execution payload envelope is cached for local building. // The envelope is stored in the pending cache (keyed by slot) until publishing. let block_root = block.tree_hash_root(); - let envelope = self - .chain - .pending_payload_envelopes - .read() - .get(slot) - .cloned() - .expect("envelope should exist in pending cache for local building"); + { + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + } + + // Fetch the envelope via the HTTP API + let envelope_response = self + .client + .get_validator_execution_payload_envelope::(slot, 0) + .await + .unwrap(); + let envelope = envelope_response.data; + + // Verify envelope fields assert_eq!(envelope.beacon_block_root, block_root); assert_eq!(envelope.slot, slot); + assert_eq!(envelope.builder_index, u64::MAX); + assert_ne!(envelope.state_root, Hash256::ZERO); // Sign and publish the block let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); @@ -3824,6 +3841,21 @@ impl ApiTester { assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + // Sign and publish the execution payload envelope + let domain = self.chain.spec.get_builder_domain(); + let signing_root = envelope.signing_root(domain); + let signature = sk.sign(signing_root); + + let signed_envelope = SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }; + + self.client + .post_beacon_execution_payload_envelope(&signed_envelope) + .await + .unwrap(); + self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -3886,12 +3918,27 @@ impl ApiTester { .await .unwrap(); + let block_root = block.tree_hash_root(); + assert_eq!( metadata.consensus_version, block.to_ref().fork_name(&self.chain.spec).unwrap() ); assert!(!metadata.consensus_block_value.is_zero()); + // Fetch the envelope via the HTTP API (SSZ) + let envelope = self + .client + .get_validator_execution_payload_envelope_ssz::(slot, 0) + .await + .unwrap(); + + // Verify envelope fields + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + assert_eq!(envelope.builder_index, u64::MAX); + assert_ne!(envelope.state_root, Hash256::ZERO); + // Sign and publish the block let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); let signed_block_request = @@ -3904,194 +3951,21 @@ impl ApiTester { assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); - self.chain.slot_clock.set_slot(slot.as_u64() + 1); - } + // Sign and publish the execution payload envelope + let domain = self.chain.spec.get_builder_domain(); + let signing_root = envelope.signing_root(domain); + let signature = sk.sign(signing_root); - self - } - - /// Test fetching execution payload envelope via HTTP API (JSON). Only runs if Gloas is scheduled. - pub async fn test_get_execution_payload_envelope(self) -> Self { - if !self.chain.spec.is_gloas_scheduled() { - return self; - } - - let fork = self.chain.canonical_head.cached_head().head_fork(); - let genesis_validators_root = self.chain.genesis_validators_root; - - for _ in 0..E::slots_per_epoch() * 3 { - let slot = self.chain.slot().unwrap(); - let epoch = self.chain.epoch().unwrap(); - - // Skip if not in Gloas fork yet - let fork_name = self.chain.spec.fork_name_at_slot::(slot); - if !fork_name.gloas_enabled() { - self.chain.slot_clock.set_slot(slot.as_u64() + 1); - continue; - } - - let proposer_pubkey_bytes = self - .client - .get_validator_duties_proposer(epoch) - .await - .unwrap() - .data - .into_iter() - .find(|duty| duty.slot == slot) - .map(|duty| duty.pubkey) - .unwrap(); - let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); - - let sk = self - .validator_keypairs() - .iter() - .find(|kp| kp.pk == proposer_pubkey) - .map(|kp| kp.sk.clone()) - .unwrap(); - - let randao_reveal = { - let domain = self.chain.spec.get_domain( - epoch, - Domain::Randao, - &fork, - genesis_validators_root, - ); - let message = epoch.signing_root(domain); - sk.sign(message).into() + let signed_envelope = SignedExecutionPayloadEnvelope { + message: envelope, + signature, }; - // Produce a V4 block (which caches the envelope) - let (response, _metadata) = self - .client - .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) + self.client + .post_beacon_execution_payload_envelope(&signed_envelope) .await .unwrap(); - let block = response.data; - let block_root = block.tree_hash_root(); - - // Fetch the envelope via HTTP API (using builder_index=0 for local building) - let envelope_response = self - .client - .get_validator_execution_payload_envelope::(slot, 0) - .await - .unwrap(); - - let envelope = envelope_response.data; - - // Verify envelope fields match the produced block - assert_eq!( - envelope.beacon_block_root, block_root, - "Envelope beacon_block_root should match the produced block's root" - ); - assert_eq!( - envelope.slot, slot, - "Envelope slot should match the block's slot" - ); - assert_eq!( - envelope.builder_index, - u64::MAX, - "Builder index should be u64::MAX for local building" - ); - assert_ne!( - envelope.state_root, - Hash256::ZERO, - "State root should not be zero" - ); - - self.chain.slot_clock.set_slot(slot.as_u64() + 1); - } - - self - } - - /// Test fetching execution payload envelope via HTTP API (SSZ). Only runs if Gloas is scheduled. - pub async fn test_get_execution_payload_envelope_ssz(self) -> Self { - if !self.chain.spec.is_gloas_scheduled() { - return self; - } - - let fork = self.chain.canonical_head.cached_head().head_fork(); - let genesis_validators_root = self.chain.genesis_validators_root; - - for _ in 0..E::slots_per_epoch() * 3 { - let slot = self.chain.slot().unwrap(); - let epoch = self.chain.epoch().unwrap(); - - // Skip if not in Gloas fork yet - let fork_name = self.chain.spec.fork_name_at_slot::(slot); - if !fork_name.gloas_enabled() { - self.chain.slot_clock.set_slot(slot.as_u64() + 1); - continue; - } - - let proposer_pubkey_bytes = self - .client - .get_validator_duties_proposer(epoch) - .await - .unwrap() - .data - .into_iter() - .find(|duty| duty.slot == slot) - .map(|duty| duty.pubkey) - .unwrap(); - let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); - - let sk = self - .validator_keypairs() - .iter() - .find(|kp| kp.pk == proposer_pubkey) - .map(|kp| kp.sk.clone()) - .unwrap(); - - let randao_reveal = { - let domain = self.chain.spec.get_domain( - epoch, - Domain::Randao, - &fork, - genesis_validators_root, - ); - let message = epoch.signing_root(domain); - sk.sign(message).into() - }; - - // Produce a V4 block (which caches the envelope) - let (response, _metadata) = self - .client - .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) - .await - .unwrap(); - - let block = response.data; - let block_root = block.tree_hash_root(); - - // Fetch the envelope via HTTP API in SSZ format - let envelope = self - .client - .get_validator_execution_payload_envelope_ssz::(slot, 0) - .await - .unwrap(); - - // Verify envelope fields match the produced block - assert_eq!( - envelope.beacon_block_root, block_root, - "Envelope beacon_block_root should match the produced block's root" - ); - assert_eq!( - envelope.slot, slot, - "Envelope slot should match the block's slot" - ); - assert_eq!( - envelope.builder_index, - u64::MAX, - "Builder index should be u64::MAX for local building" - ); - assert_ne!( - envelope.state_root, - Hash256::ZERO, - "State root should not be zero" - ); - self.chain.slot_clock.set_slot(slot.as_u64() + 1); } @@ -7847,21 +7721,6 @@ async fn block_production_v4_ssz() { .await; } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn get_execution_payload_envelope() { - ApiTester::new_with_hard_forks() - .await - .test_get_execution_payload_envelope() - .await; -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn get_execution_payload_envelope_ssz() { - ApiTester::new_with_hard_forks() - .await - .test_get_execution_payload_envelope_ssz() - .await; -} #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 35219ff924..47440e9325 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2623,6 +2623,29 @@ impl BeaconNodeHttpClient { ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) } + /// `POST v1/beacon/execution_payload_envelope` + pub async fn post_beacon_execution_payload_envelope( + &self, + envelope: &SignedExecutionPayloadEnvelope, + ) -> 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_envelope"); + + self.post_generic_with_consensus_version( + path, + envelope, + Some(self.timeouts.proposal), + ForkName::Gloas, + ) + .await?; + + Ok(()) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz( &self, diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 243059702b..1f1ccdb320 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -679,12 +679,28 @@ impl BlockService { "Signed execution payload envelope, publishing" ); - // TODO(gloas): Publish the signed envelope - // For now, just log that we would publish it - debug!( + // Publish the signed envelope + proposer_fallback + .request_proposers_first(|beacon_node| { + let signed_envelope = signed_envelope.clone(); + async move { + beacon_node + .post_beacon_execution_payload_envelope(&signed_envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error publishing execution payload envelope: {:?}", + e + )) + }) + } + }) + .await?; + + info!( slot = slot.as_u64(), beacon_block_root = %signed_envelope.message.beacon_block_root, - "Would publish signed execution payload envelope (not yet implemented)" + "Successfully published signed execution payload envelope" ); Ok(())