From 72f0a7b9c1990f9514408066aee5ec022b1a9d1c Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 12:59:52 -0800 Subject: [PATCH 01/37] move commitments to bid --- beacon_node/lighthouse_network/src/types/pubsub.rs | 2 +- .../src/network_beacon_processor/gossip_methods.rs | 2 +- .../network/src/network_beacon_processor/mod.rs | 2 +- consensus/types/src/block/beacon_block_body.rs | 2 +- .../types/src/execution/execution_payload_bid.rs | 12 +++++++----- .../src/execution/execution_payload_envelope.rs | 4 +--- .../src/execution/signed_execution_payload_bid.rs | 12 +++++++----- consensus/types/src/state/beacon_state.rs | 2 +- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index d1df7face7..12567907f6 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -49,7 +49,7 @@ pub enum PubsubMessage { /// Gossipsub message providing notification of a payload attestation message. PayloadAttestation(Box), /// Gossipsub message providing notification of a signed execution payload bid. - ExecutionPayloadBid(Box), + ExecutionPayloadBid(Box>), /// Gossipsub message providing notification of signed proposer preferences. ProposerPreferences(Box), /// Gossipsub message providing notification of a light client finality update. diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index fec557ec04..efbf7bfaef 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -3249,7 +3249,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - payload_bid: SignedExecutionPayloadBid, + payload_bid: SignedExecutionPayloadBid, ) { // TODO(EIP-7732): Implement proper payload bid gossip processing. // This should integrate with a payload execution bid verification module once it's implemented. diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index c326dfd597..fd67fcde82 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -448,7 +448,7 @@ impl NetworkBeaconProcessor { self: &Arc, message_id: MessageId, peer_id: PeerId, - execution_payload_bid: Box, + execution_payload_bid: Box>, ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { diff --git a/consensus/types/src/block/beacon_block_body.rs b/consensus/types/src/block/beacon_block_body.rs index a113f85fd3..fd5d976c9b 100644 --- a/consensus/types/src/block/beacon_block_body.rs +++ b/consensus/types/src/block/beacon_block_body.rs @@ -167,7 +167,7 @@ pub struct BeaconBlockBody = FullPay #[superstruct(only(Electra, Fulu))] pub execution_requests: ExecutionRequests, #[superstruct(only(Gloas))] - pub signed_execution_payload_bid: SignedExecutionPayloadBid, + pub signed_execution_payload_bid: SignedExecutionPayloadBid, #[superstruct(only(Gloas))] pub payload_attestations: VariableList, E::MaxPayloadAttestations>, #[superstruct(only(Base, Altair, Gloas))] diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index f0056463e9..7d80bb48a9 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{Address, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; +use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; @@ -12,9 +12,10 @@ use tree_hash_derive::TreeHash; )] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#executionpayloadbid -pub struct ExecutionPayloadBid { +pub struct ExecutionPayloadBid { pub parent_block_hash: ExecutionBlockHash, pub parent_block_root: Hash256, pub block_hash: ExecutionBlockHash, @@ -30,14 +31,15 @@ pub struct ExecutionPayloadBid { pub value: u64, #[serde(with = "serde_utils::quoted_u64")] pub execution_payment: u64, - pub blob_kzg_commitments_root: Hash256, + pub blob_kzg_commitments: KzgCommitments, } -impl SignedRoot for ExecutionPayloadBid {} +impl SignedRoot for ExecutionPayloadBid {} #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(ExecutionPayloadBid); + ssz_and_tree_hash_tests!(ExecutionPayloadBid); } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 64e03cec5a..cf3315a58a 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,7 +1,6 @@ use crate::test_utils::TestRandom; use crate::{ - EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, KzgCommitments, - SignedRoot, Slot, + EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, SignedRoot, Slot, }; use context_deserialize::context_deserialize; use educe::Educe; @@ -21,7 +20,6 @@ pub struct ExecutionPayloadEnvelope { pub builder_index: u64, pub beacon_block_root: Hash256, pub slot: Slot, - pub blob_kzg_commitments: KzgCommitments, pub state_root: Hash256, } diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 29dfd03ba0..1fe26ba1c6 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,5 @@ use crate::test_utils::TestRandom; -use crate::{ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ExecutionPayloadBid, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; @@ -11,14 +11,15 @@ use tree_hash_derive::TreeHash; #[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[educe(PartialEq, Hash)] +#[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/beacon-chain.md#signedexecutionpayloadbid -pub struct SignedExecutionPayloadBid { - pub message: ExecutionPayloadBid, +pub struct SignedExecutionPayloadBid { + pub message: ExecutionPayloadBid, pub signature: Signature, } -impl SignedExecutionPayloadBid { +impl SignedExecutionPayloadBid { pub fn empty() -> Self { Self { message: ExecutionPayloadBid::default(), @@ -30,6 +31,7 @@ impl SignedExecutionPayloadBid { #[cfg(test)] mod tests { use super::*; + use crate::MainnetEthSpec; - ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); + ssz_and_tree_hash_tests!(SignedExecutionPayloadBid); } diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 3f8fa4cfff..2c639160a4 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -547,7 +547,7 @@ where pub latest_execution_payload_header: ExecutionPayloadHeaderFulu, #[superstruct(only(Gloas))] #[metastruct(exclude_from(tree_lists))] - pub latest_execution_payload_bid: ExecutionPayloadBid, + pub latest_execution_payload_bid: ExecutionPayloadBid, #[superstruct(only(Capella, Deneb, Electra, Fulu, Gloas), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] #[metastruct(exclude_from(tree_lists))] From dee394cf0f6c2a5dc9d1637ea1801cd82211f575 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Thu, 29 Jan 2026 21:23:21 -0800 Subject: [PATCH 02/37] Lint fixes --- consensus/types/src/execution/execution_payload_bid.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 7d80bb48a9..e81add5177 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,5 +1,7 @@ use crate::test_utils::TestRandom; -use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot}; +use crate::{ + Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot, +}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; From e1439e61e05805f66418781a79554157fac41bf4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 11:08:11 -0800 Subject: [PATCH 03/37] Use module level imports --- consensus/types/src/execution/execution_payload_bid.rs | 5 ++--- consensus/types/src/execution/execution_payload_envelope.rs | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index e81add5177..6dfed9f6e9 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -1,7 +1,6 @@ +use crate::kzg_ext::KzgCommitments; use crate::test_utils::TestRandom; -use crate::{ - Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, KzgCommitments, SignedRoot, Slot, -}; +use crate::{Address, EthSpec, ExecutionBlockHash, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index cf3315a58a..7f68dae037 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -1,7 +1,6 @@ +use crate::execution::{ExecutionPayloadGloas, ExecutionRequests}; use crate::test_utils::TestRandom; -use crate::{ - EthSpec, ExecutionPayloadGloas, ExecutionRequests, ForkName, Hash256, SignedRoot, Slot, -}; +use crate::{EthSpec, ForkName, Hash256, SignedRoot, Slot}; use context_deserialize::context_deserialize; use educe::Educe; use serde::{Deserialize, Serialize}; From 047599aac9c7939124c55c0380db0ad5c8de4e58 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Fri, 30 Jan 2026 14:36:56 -0800 Subject: [PATCH 04/37] Fix CI --- consensus/types/src/execution/execution_payload_bid.rs | 6 +++++- .../types/src/execution/signed_execution_payload_bid.rs | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/consensus/types/src/execution/execution_payload_bid.rs b/consensus/types/src/execution/execution_payload_bid.rs index 6dfed9f6e9..5c8771993e 100644 --- a/consensus/types/src/execution/execution_payload_bid.rs +++ b/consensus/types/src/execution/execution_payload_bid.rs @@ -11,7 +11,11 @@ use tree_hash_derive::TreeHash; #[derive( Default, Debug, Clone, Serialize, Encode, Decode, Deserialize, TreeHash, Educe, TestRandom, )] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] #[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] diff --git a/consensus/types/src/execution/signed_execution_payload_bid.rs b/consensus/types/src/execution/signed_execution_payload_bid.rs index 1fe26ba1c6..48da445332 100644 --- a/consensus/types/src/execution/signed_execution_payload_bid.rs +++ b/consensus/types/src/execution/signed_execution_payload_bid.rs @@ -1,5 +1,6 @@ +use crate::execution::ExecutionPayloadBid; use crate::test_utils::TestRandom; -use crate::{EthSpec, ExecutionPayloadBid, ForkName}; +use crate::{EthSpec, ForkName}; use bls::Signature; use context_deserialize::context_deserialize; use educe::Educe; @@ -9,7 +10,11 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[derive(TestRandom, TreeHash, Debug, Clone, Encode, Decode, Serialize, Deserialize, Educe)] -#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[cfg_attr( + feature = "arbitrary", + derive(arbitrary::Arbitrary), + arbitrary(bound = "E: EthSpec") +)] #[educe(PartialEq, Hash)] #[serde(bound = "E: EthSpec")] #[context_deserialize(ForkName)] From bf1fb7e29f4069766d1a608ae29013f97e8e3ab8 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Feb 2026 11:29:31 +1100 Subject: [PATCH 05/37] Add testing for Gloas block body --- testing/ef_tests/src/type_name.rs | 2 ++ testing/ef_tests/tests/tests.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 87d56968cc..b24be6a943 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -55,6 +55,7 @@ type_name_generic!(BeaconBlockBodyCapella, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyDeneb, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyElectra, "BeaconBlockBody"); type_name_generic!(BeaconBlockBodyFulu, "BeaconBlockBody"); +type_name_generic!(BeaconBlockBodyGloas, "BeaconBlockBody"); type_name!(BeaconBlockHeader); type_name_generic!(BeaconState); type_name!(BlobIdentifier); @@ -78,6 +79,7 @@ type_name_generic!(ExecutionPayloadCapella, "ExecutionPayload"); type_name_generic!(ExecutionPayloadDeneb, "ExecutionPayload"); type_name_generic!(ExecutionPayloadElectra, "ExecutionPayload"); type_name_generic!(ExecutionPayloadFulu, "ExecutionPayload"); +type_name_generic!(ExecutionPayloadGloas, "ExecutionPayload"); type_name_generic!(FullPayload, "ExecutionPayload"); type_name_generic!(ExecutionPayloadHeader); type_name_generic!(ExecutionPayloadHeaderBellatrix, "ExecutionPayloadHeader"); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index e5c043e27b..b8c69285d9 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -369,6 +369,10 @@ mod ssz_static { .run(); SszStaticHandler::, MinimalEthSpec>::fulu_only().run(); SszStaticHandler::, MainnetEthSpec>::fulu_only().run(); + SszStaticHandler::, MinimalEthSpec>::gloas_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::gloas_only() + .run(); } // Altair and later From 7f51c7d20bf479d86b809bbae60ed9bc17dce4e6 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Tue, 3 Feb 2026 11:29:55 +1100 Subject: [PATCH 06/37] Pin nightly tests --- testing/ef_tests/Makefile | 2 +- testing/ef_tests/download_test_vectors.sh | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 0c6371f825..a881718e02 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.1 +CONSENSUS_SPECS_TEST_VERSION ?= nightly-21573464110 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh index 21f74e817f..f3cef96dde 100755 --- a/testing/ef_tests/download_test_vectors.sh +++ b/testing/ef_tests/download_test_vectors.sh @@ -4,7 +4,7 @@ set -Eeuo pipefail TESTS=("general" "minimal" "mainnet") version=${1} -if [[ "$version" == "nightly" ]]; then +if [[ "$version" == "nightly" || "$version" == nightly-* ]]; then if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "Error GITHUB_TOKEN is not set" exit 1 @@ -21,17 +21,22 @@ if [[ "$version" == "nightly" ]]; then api="https://api.github.com" auth_header="Authorization: token ${GITHUB_TOKEN}" - run_id=$(curl -s -H "${auth_header}" \ - "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | - jq -r '.workflow_runs[0].id') + if [[ "$version" == nightly-* ]]; then + # Extract run_id from nightly- format + run_id="${version#nightly-}" + else + run_id=$(curl -v -H "${auth_header}" \ + "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | + jq -r '.workflow_runs[0].id') - if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then - echo "No successful nightly workflow run found" - exit 1 + if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then + echo "No successful nightly workflow run found" + exit 1 + fi fi echo "Downloading nightly test vectors for run: ${run_id}" - curl -s -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | + curl -v -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | jq -c '.artifacts[] | {name, url: .archive_download_url}' | while read -r artifact; do name=$(echo "${artifact}" | jq -r .name) From 4c9fa245af16ca4e743d1d3a9b09d49d7307d916 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 2 Feb 2026 23:22:00 -0800 Subject: [PATCH 07/37] Block and bid production --- beacon_node/beacon_chain/src/beacon_block.rs | 530 ++++++++++++++++++ .../beacon_chain/src/execution_payload_bid.rs | 160 ++++++ beacon_node/beacon_chain/src/lib.rs | 2 + beacon_node/execution_layer/src/lib.rs | 8 + 4 files changed, 700 insertions(+) create mode 100644 beacon_node/beacon_chain/src/beacon_block.rs create mode 100644 beacon_node/beacon_chain/src/execution_payload_bid.rs diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs new file mode 100644 index 0000000000..c9c9d15f57 --- /dev/null +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -0,0 +1,530 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +use bls::Signature; +use execution_layer::BuilderParams; +use operation_pool::CompactAttestationRef; +use ssz::Encode; +use state_processing::common::get_attesting_indices_from_state; +use state_processing::epoch_cache::initialize_epoch_cache; +use state_processing::per_block_processing::verify_attestation_for_block_inclusion; +use state_processing::{ + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, +}; +use state_processing::{VerifyOperation, state_advance::complete_state_advance}; +use tracing::{Span, debug, debug_span, error, trace, warn}; +use types::{ + Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, + BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, FullPayload, + Graffiti, Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, Slot, + SyncAggregate, +}; + +use crate::BeaconBlockResponse; +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + ProduceBlockVerification, graffiti_calculator::GraffitiSettings, +}; + +pub struct PartialBeaconBlock { + state: BeaconState, + slot: Slot, + proposer_index: u64, + parent_root: Hash256, + randao_reveal: Signature, + eth1_data: Eth1Data, + graffiti: Graffiti, + proposer_slashings: Vec, + attester_slashings: Vec>, + attestations: Vec>, + payload_attestations: Vec>, + deposits: Vec, + voluntary_exits: Vec, + sync_aggregate: Option>, + bls_to_execution_changes: Vec, +} + +impl BeaconChain { + pub async fn produce_block_on_bid( + self: &Arc, + state: BeaconState, + execution_payload_bid: SignedExecutionPayloadBid, + state_root_opt: Option, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + builder_boost_factor: Option, + ) -> Result>, BlockProductionError> { + let chain = self.clone(); + let graffiti = self + .graffiti_calculator + .get_graffiti(graffiti_settings) + .await; + let span = Span::current(); + let mut partial_beacon_block = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "produce_partial_beacon_block").entered(); + chain.produce_partial_beacon_block_gloas( + state, + state_root_opt, + produce_at_slot, + randao_reveal, + graffiti, + builder_boost_factor, + ) + }, + "produce_partial_beacon_block", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + let chain = self.clone(); + let span = Span::current(); + let beacon_block_response = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block").entered(); + chain.complete_partial_beacon_block_gloas( + partial_beacon_block, + execution_payload_bid, + verification, + ) + }, + "complete_partial_beacon_block", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + todo!() + } + + #[allow(clippy::too_many_arguments)] + fn produce_partial_beacon_block_gloas( + self: &Arc, + mut state: BeaconState, + state_root_opt: Option, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti: Graffiti, + builder_boost_factor: Option, + ) -> Result, BlockProductionError> { + // It is invalid to try to produce a block using a state from a future slot. + if state.slot() > produce_at_slot { + return Err(BlockProductionError::StateSlotTooHigh { + produce_at_slot, + state_slot: state.slot(), + }); + } + + // TODO(gloas) + // let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); + + // Ensure the state has performed a complete transition into the required slot. + complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?; + + // TODO(gloas) + // drop(slot_timer); + + state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + state.apply_pending_mutations()?; + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); + let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = + self.op_pool.get_slashings_and_exits(&state, &self.spec); + + drop(slashings_and_exits_span); + + let eth1_data = state.eth1_data().clone(); + + let deposits = vec![]; + + let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered(); + let bls_to_execution_changes = self + .op_pool + .get_bls_to_execution_changes(&state, &self.spec); + drop(bls_changes_span); + + // Iterate through the naive aggregation pool and ensure all the attestations from there + // are included in the operation pool. + { + let _guard = debug_span!("import_naive_aggregation_pool").entered(); + // TODO(gloas) + // let _unagg_import_timer = + // metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + for attestation in self.naive_aggregation_pool.read().iter() { + let import = |attestation: &Attestation| { + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; + self.op_pool + .insert_attestation(attestation.clone(), attesting_indices) + }; + if let Err(e) = import(attestation) { + // Don't stop block production if there's an error, just create a log. + error!( + reason = ?e, + "Attestation did not transfer to op pool" + ); + } + } + }; + + let mut attestations = { + let _guard = debug_span!("pack_attestations").entered(); + // TODO(gloas) + // let _attestation_packing_timer = + // metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + + let mut prev_filter_cache = HashMap::new(); + let prev_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) + }; + let mut curr_filter_cache = HashMap::new(); + let curr_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) + }; + + self.op_pool + .get_attestations( + &state, + prev_attestation_filter, + curr_attestation_filter, + &self.spec, + ) + .map_err(BlockProductionError::OpPoolError)? + }; + + // If paranoid mode is enabled re-check the signatures of every included message. + // This will be a lot slower but guards against bugs in block production and can be + // quickly rolled out without a release. + if self.config.paranoid_block_proposal { + let mut tmp_ctxt = ConsensusContext::new(state.slot()); + attestations.retain(|att| { + verify_attestation_for_block_inclusion( + &state, + att.to_ref(), + &mut tmp_ctxt, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + attestation = ?att, + "Attempted to include an invalid attestation" + ); + }) + .is_ok() + }); + + proposer_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + attester_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid attester slashing" + ); + }) + .is_ok() + }); + + voluntary_exits.retain(|exit| { + exit.clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?exit, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + // TODO(gloas) verifiy payload attestation signature here as well + } + + let attester_slashings = attester_slashings + .into_iter() + .filter_map(|a| match a { + AttesterSlashing::Base(_) => None, + AttesterSlashing::Electra(a) => Some(a), + }) + .collect::>(); + + let attestations = attestations + .into_iter() + .filter_map(|a| match a { + Attestation::Base(_) => None, + Attestation::Electra(a) => Some(a), + }) + .collect::>(); + + let slot = state.slot(); + + let sync_aggregate = if matches!(&state, BeaconState::Base(_)) { + None + } else { + let sync_aggregate = self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + slot = %state.slot(), + "Producing block with no sync contributions" + ); + SyncAggregate::new() + }); + Some(sync_aggregate) + }; + + Ok(PartialBeaconBlock { + state, + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }) + } + + fn complete_partial_beacon_block_gloas( + &self, + partial_beacon_block: PartialBeaconBlock, + signed_execution_payload_bid: SignedExecutionPayloadBid, + verification: ProduceBlockVerification, + ) -> Result< + ( + BeaconBlock>, + BeaconState, + u64, + ), + BlockProductionError, + > { + let PartialBeaconBlock { + mut state, + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + payload_attestations, + bls_to_execution_changes, + } = partial_beacon_block; + + let beacon_block = match &state { + BeaconState::Base(_) => { + ( + // TODO(gloas) this should be an error + todo!() + ) + } + BeaconState::Altair(_) => { + ( + // TODO(gloas) this should be an error + todo!() + ) + } + BeaconState::Bellatrix(_) => { + // TODO(gloas) this should be an error + todo!() + } + BeaconState::Capella(_) => { + // TODO(gloas) this should be an error + todo!() + } + BeaconState::Deneb(_) => { + // TODO(gloas) this should be an error + todo!() + } + BeaconState::Electra(_) => { + // TODO(gloas) this should be an error + todo!() + } + BeaconState::Fulu(_) => { + // TODO(gloas) this should be an error + todo!() + } + BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate: sync_aggregate + .ok_or(BlockProductionError::MissingSyncAggregate)?, + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + signed_execution_payload_bid, + payload_attestations: payload_attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + _phantom: PhantomData::>, + }, + }), + }; + + let signed_beacon_block = SignedBeaconBlock::from_block( + beacon_block, + // The block is not signed here, that is the task of a validator client. + Signature::empty(), + ); + + // TODO(gloas) ensure block size is measured from the signed block + let block_size = signed_beacon_block.ssz_bytes_len(); + debug!(%block_size, "Produced block on state"); + + // TODO(gloas) + // metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); + + if block_size > self.config.max_network_size { + return Err(BlockProductionError::BlockTooLarge(block_size)); + } + + // TODO(gloas) + // let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); + let signature_strategy = match verification { + ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao, + ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification, + }; + + // Use a context without block root or proposer index so that both are checked. + let mut ctxt = ConsensusContext::new(signed_beacon_block.slot()); + + let consensus_block_value = self + .compute_beacon_block_reward(signed_beacon_block.message(), &mut state) + .map(|reward| reward.total) + .unwrap_or(0); + + state_processing::per_block_processing( + &mut state, + &signed_beacon_block, + signature_strategy, + VerifyBlockRoot::True, + &mut ctxt, + &self.spec, + )?; + // TODO(gloas) + // drop(process_timer); + + // TODO(gloas) + //let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); + + let state_root = state.update_tree_hash_cache()?; + + // TODO(gloas) + // drop(state_root_timer); + + let (mut block, _) = signed_beacon_block.deconstruct(); + *block.state_root_mut() = state_root; + + // TODO(gloas) + // metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); + + trace!( + parent = ?block.parent_root(), + attestations = block.body().attestations_len(), + slot = %block.slot(), + "Produced beacon block" + ); + + Ok((block, state, consensus_block_value)) + } +} diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs new file mode 100644 index 0000000000..bffbdcd16d --- /dev/null +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -0,0 +1,160 @@ +use std::sync::Arc; + +use execution_layer::{BlockProposalContentsType, BuilderParams}; +use ssz_types::VariableList; +use state_processing::state_advance::complete_state_advance; +use tracing::instrument; +use types::{ + Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayload, + ExecutionPayloadBid, Hash256, ProposerPreferences, Slot, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + execution_payload::get_execution_payload, +}; + +impl BeaconChain { + /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + #[instrument(level = "debug", skip_all)] + pub async fn produce_execution_payload_bid( + self: &Arc, + mut state: BeaconState, + state_root: Hash256, + execution_payload: ExecutionPayload, + produce_at_slot: Slot, + proposer_preferences: Option, + builder_index: BuilderIndex, + value: u64, + ) -> Result, BlockProductionError> { + // It is invalid to try to produce a block using a state from a future slot. + if state.slot() > produce_at_slot { + return Err(BlockProductionError::StateSlotTooHigh { + produce_at_slot, + state_slot: state.slot(), + }); + } + + // TODO(gloas) add sanity check on value + // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. + + // TODO(gloas) add metrics for execution payload bid production + + // Ensure the state has performed a complete transition into the required slot. + complete_state_advance(&mut state, Some(state_root), produce_at_slot, &self.spec)?; + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + // TODO(gloas) this should be BlockProductionVersion::V4 + // V3 is okay for now as long as we're not connected to a builder + // TODO(gloas) add builder boost factor + let prepare_payload_handle = get_execution_payload( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + None, + BlockProductionVersion::V3, + )?; + + let block_contents_type_option = Some( + prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??, + ); + + let blob_kzg_commitments = if let Some(block_contents_type) = block_contents_type_option { + match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let blob_kzg_commitments = + block_proposal_contents.blob_kzg_commitments().cloned(); + + if let Some(blob_kzg_commitments) = blob_kzg_commitments { + blob_kzg_commitments + } else { + return Err(BlockProductionError::MissingKzgCommitment( + "No KZG commitments from the payload".to_owned(), + )); + } + } + // TODO(gloas) we should never receive a blinded response. + // Should return some type of `Unexpected` error variant as this should never happen + // in the V4 block production flow + BlockProposalContentsType::Blinded(_) => { + return Err(BlockProductionError::GloasNotImplemented); + } + } + } else { + todo!() + }; + + let bid = if let Some(proposer_preferences) = proposer_preferences + && proposer_preferences.proposal_slot == produce_at_slot + { + // Trustless bid + ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: execution_payload.block_hash(), + prev_randao: execution_payload.prev_randao(), + fee_recipient: proposer_preferences.fee_recipient, + // TODO(gloas) payload construction should factor in the proposers gas limit preferences + gas_limit: execution_payload.gas_limit(), + builder_index, + slot: produce_at_slot, + value, + execution_payment: 0, + blob_kzg_commitments, + } + } else if builder_index == u64::MAX { + // Local bid + ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: execution_payload.block_hash(), + prev_randao: execution_payload.prev_randao(), + fee_recipient: Address::ZERO, + gas_limit: execution_payload.gas_limit(), + builder_index, + slot: produce_at_slot, + value, + execution_payment: 0, + blob_kzg_commitments, + } + } else { + // No proposer preferences and this isn't local building + // TODO(gloas) this should return a specific error type + // i.e if proposer prefs are missing and its a trustless bid + // return an error that communicates that. + return Err(BlockProductionError::GloasNotImplemented); + }; + + Ok(bid) + } +} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index e77739e2d5..4ddd075bb5 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -1,6 +1,7 @@ pub mod attestation_rewards; pub mod attestation_simulator; pub mod attestation_verification; +mod beacon_block; pub mod beacon_block_reward; mod beacon_block_streamer; mod beacon_chain; @@ -23,6 +24,7 @@ mod early_attester_cache; mod errors; pub mod events; pub mod execution_payload; +pub mod execution_payload_bid; pub mod fetch_blobs; pub mod fork_choice_signal; pub mod fork_revert; diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b83aab09..63402f6fa7 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -334,6 +334,14 @@ impl> BlockProposalContents block_value, } } + pub fn blob_kzg_commitments(&self) -> Option<&KzgCommitments> { + match self { + Self::Payload { .. } => None, + Self::PayloadAndBlobs { + kzg_commitments, .. + } => Some(kzg_commitments), + } + } } // This just groups together a bunch of parameters that commonly From 7cf4eb039618ae154ba45a103cab94242a3495d9 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 16:13:07 -0800 Subject: [PATCH 08/37] Add new block production endpoint --- beacon_node/beacon_chain/src/beacon_block.rs | 205 +++++++++++------- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/errors.rs | 1 + .../beacon_chain/src/execution_payload_bid.rs | 159 ++++++-------- beacon_node/http_api/src/produce_block.rs | 75 +++++++ beacon_node/http_api/src/validator/mod.rs | 51 ++++- beacon_node/http_api/src/version.rs | 1 + beacon_node/http_api/tests/tests.rs | 177 +++++++++++++++ common/eth2/src/lib.rs | 170 +++++++++++++++ common/eth2/src/types.rs | 36 +++ .../validator_services/src/block_service.rs | 198 +++++++++++------ 11 files changed, 844 insertions(+), 231 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index c9c9d15f57..1b53f229dc 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::marker::PhantomData; use std::sync::Arc; +use std::u64; use bls::Signature; -use execution_layer::BuilderParams; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::get_attesting_indices_from_state; @@ -22,14 +22,12 @@ use types::{ SyncAggregate, }; -use crate::BeaconBlockResponse; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, - ProduceBlockVerification, graffiti_calculator::GraffitiSettings, + BeaconChain, BeaconChainTypes, BlockProductionError, ProduceBlockVerification, + graffiti_calculator::GraffitiSettings, metrics, }; pub struct PartialBeaconBlock { - state: BeaconState, slot: Slot, proposer_index: u64, parent_root: Hash256, @@ -46,66 +44,136 @@ pub struct PartialBeaconBlock { bls_to_execution_changes: Vec, } +// We'll need to add that once we include trusted/trustless bids impl BeaconChain { - pub async fn produce_block_on_bid( + pub async fn produce_block_with_verification_gloas( + self: &Arc, + randao_reveal: Signature, + slot: Slot, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + _builder_boost_factor: Option, + ) -> Result< + ( + BeaconBlock>, + BeaconState, + u64, + ), + BlockProductionError, + > { + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); + let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); + // Part 1/2 (blocking) + // + // Load the parent state from disk. + let chain = self.clone(); + let span = Span::current(); + let (state, state_root_opt) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, + "load_state_for_block_production", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/2 (async, with some blocking components) + // + // Produce the block upon the state + self.produce_block_on_state_gloas( + state, + state_root_opt, + slot, + randao_reveal, + graffiti_settings, + verification, + ) + .await + } + + // TODO(gloas) need to implement builder boost factor logic + pub async fn produce_block_on_state_gloas( self: &Arc, state: BeaconState, - execution_payload_bid: SignedExecutionPayloadBid, state_root_opt: Option, produce_at_slot: Slot, randao_reveal: Signature, graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, - builder_boost_factor: Option, - ) -> Result>, BlockProductionError> { + ) -> Result< + ( + BeaconBlock>, + BeaconState, + u64, + ), + BlockProductionError, + > { + // Part 1/3 (blocking) + // + // Perform the state advance and block-packing functions. let chain = self.clone(); let graffiti = self .graffiti_calculator .get_graffiti(graffiti_settings) .await; let span = Span::current(); - let mut partial_beacon_block = self + let (partial_beacon_block, state) = self .task_executor .spawn_blocking_handle( move || { let _guard = - debug_span!(parent: span, "produce_partial_beacon_block").entered(); + debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered(); chain.produce_partial_beacon_block_gloas( state, state_root_opt, produce_at_slot, randao_reveal, graffiti, - builder_boost_factor, ) }, - "produce_partial_beacon_block", + "produce_partial_beacon_block_gloas", ) .ok_or(BlockProductionError::ShuttingDown)? .await .map_err(BlockProductionError::TokioJoin)??; + // Part 2/3 (async) + // + // Produce the execution payload bid. + // TODO(gloas) this is strictly for building local bids + // We'll need to build out trustless/trusted bid paths. + let (execution_payload_bid, state) = self + .clone() + .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX) + .await?; + + // Part 3/3 (blocking) + // + // Complete the block with the execution payload bid. let chain = self.clone(); let span = Span::current(); - let beacon_block_response = self - .task_executor + self.task_executor .spawn_blocking_handle( move || { let _guard = - debug_span!(parent: span, "complete_partial_beacon_block").entered(); + debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered(); chain.complete_partial_beacon_block_gloas( partial_beacon_block, execution_payload_bid, + state, verification, ) }, - "complete_partial_beacon_block", + "complete_partial_beacon_block_gloas", ) .ok_or(BlockProductionError::ShuttingDown)? .await - .map_err(BlockProductionError::TokioJoin)??; - - todo!() + .map_err(BlockProductionError::TokioJoin)? } #[allow(clippy::too_many_arguments)] @@ -116,8 +184,8 @@ impl BeaconChain { produce_at_slot: Slot, randao_reveal: Signature, graffiti: Graffiti, - builder_boost_factor: Option, - ) -> Result, BlockProductionError> { + ) -> Result<(PartialBeaconBlock, BeaconState), BlockProductionError> + { // It is invalid to try to produce a block using a state from a future slot. if state.slot() > produce_at_slot { return Err(BlockProductionError::StateSlotTooHigh { @@ -148,22 +216,6 @@ impl BeaconChain { let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; - let pubkey = state - .validators() - .get(proposer_index as usize) - .map(|v| v.pubkey) - .ok_or(BlockProductionError::BeaconChain(Box::new( - BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), - )))?; - - let builder_params = BuilderParams { - pubkey, - slot: state.slot(), - chain_health: self - .is_healthy(&parent_root) - .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, - }; - let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = self.op_pool.get_slashings_and_exits(&state, &self.spec); @@ -339,30 +391,33 @@ impl BeaconChain { Some(sync_aggregate) }; - Ok(PartialBeaconBlock { + Ok(( + PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }, state, - slot, - proposer_index, - parent_root, - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - // TODO(gloas) need to implement payload attestations - payload_attestations: vec![], - bls_to_execution_changes, - }) + )) } fn complete_partial_beacon_block_gloas( &self, partial_beacon_block: PartialBeaconBlock, signed_execution_payload_bid: SignedExecutionPayloadBid, + mut state: BeaconState, verification: ProduceBlockVerification, ) -> Result< ( @@ -373,7 +428,6 @@ impl BeaconChain { BlockProductionError, > { let PartialBeaconBlock { - mut state, slot, proposer_index, parent_root, @@ -392,36 +446,39 @@ impl BeaconChain { let beacon_block = match &state { BeaconState::Base(_) => { - ( - // TODO(gloas) this should be an error - todo!() - ) + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Altair(_) => { - ( - // TODO(gloas) this should be an error - todo!() - ) + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Bellatrix(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Capella(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Deneb(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Electra(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Fulu(_) => { - // TODO(gloas) this should be an error - todo!() + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); } BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { slot, diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index ec79153785..a12600141f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4601,7 +4601,7 @@ impl BeaconChain { /// Load a beacon state from the database for block production. This is a long-running process /// that should not be performed in an `async` context. - fn load_state_for_block_production( + pub fn load_state_for_block_production( self: &Arc, slot: Slot, ) -> Result<(BeaconState, Option), BlockProductionError> { diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 816e75fd24..bd6c13e364 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -310,6 +310,7 @@ pub enum BlockProductionError { MissingSyncAggregate, MissingExecutionPayload, MissingKzgCommitment(String), + MissingStateRoot, TokioJoin(JoinError), BeaconChain(Box), InvalidPayloadFork, diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs index bffbdcd16d..752337a7e8 100644 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; +use std::{sync::Arc, u64}; +use bls::Signature; use execution_layer::{BlockProposalContentsType, BuilderParams}; -use ssz_types::VariableList; -use state_processing::state_advance::complete_state_advance; use tracing::instrument; use types::{ - Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayload, - ExecutionPayloadBid, Hash256, ProposerPreferences, Slot, + Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, Hash256, + SignedExecutionPayloadBid, Slot, }; use crate::{ @@ -15,34 +14,30 @@ use crate::{ }; impl BeaconChain { + // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless + // bid building. Right now this only works for local building. /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + /// This function assumes we've already done the state advance. #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( - self: &Arc, - mut state: BeaconState, - state_root: Hash256, - execution_payload: ExecutionPayload, + self: Arc, + state: BeaconState, + state_root_opt: Option, produce_at_slot: Slot, - proposer_preferences: Option, + bid_value: u64, builder_index: BuilderIndex, - value: u64, - ) -> Result, BlockProductionError> { - // It is invalid to try to produce a block using a state from a future slot. - if state.slot() > produce_at_slot { - return Err(BlockProductionError::StateSlotTooHigh { - produce_at_slot, - state_slot: state.slot(), - }); - } - - // TODO(gloas) add sanity check on value + ) -> Result< + ( + SignedExecutionPayloadBid, + BeaconState, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. // TODO(gloas) add metrics for execution payload bid production - // Ensure the state has performed a complete transition into the required slot. - complete_state_advance(&mut state, Some(state_root), produce_at_slot, &self.spec)?; - let parent_root = if state.slot() > 0 { *state .get_block_root(state.slot() - 1) @@ -82,79 +77,63 @@ impl BeaconChain { BlockProductionVersion::V3, )?; - let block_contents_type_option = Some( - prepare_payload_handle - .await - .map_err(BlockProductionError::TokioJoin)? - .ok_or(BlockProductionError::ShuttingDown)??, - ); + let block_contents_type = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; - let blob_kzg_commitments = if let Some(block_contents_type) = block_contents_type_option { - match block_contents_type { - BlockProposalContentsType::Full(block_proposal_contents) => { - let blob_kzg_commitments = - block_proposal_contents.blob_kzg_commitments().cloned(); + let (execution_payload, blob_kzg_commitments) = match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let blob_kzg_commitments = + block_proposal_contents.blob_kzg_commitments().cloned(); - if let Some(blob_kzg_commitments) = blob_kzg_commitments { - blob_kzg_commitments - } else { - return Err(BlockProductionError::MissingKzgCommitment( - "No KZG commitments from the payload".to_owned(), - )); - } - } - // TODO(gloas) we should never receive a blinded response. - // Should return some type of `Unexpected` error variant as this should never happen - // in the V4 block production flow - BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::GloasNotImplemented); + if let Some(blob_kzg_commitments) = blob_kzg_commitments { + ( + block_proposal_contents.to_payload().execution_payload(), + blob_kzg_commitments, + ) + } else { + return Err(BlockProductionError::MissingKzgCommitment( + "No KZG commitments from the payload".to_owned(), + )); } } - } else { - todo!() + // TODO(gloas) we should never receive a blinded response. + // Should return some type of `Unexpected` error variant as this should never happen + // in the V4 block production flow + BlockProposalContentsType::Blinded(_) => { + return Err(BlockProductionError::GloasNotImplemented); + } }; - let bid = if let Some(proposer_preferences) = proposer_preferences - && proposer_preferences.proposal_slot == produce_at_slot - { - // Trustless bid - ExecutionPayloadBid:: { - parent_block_hash: state.latest_block_hash()?.to_owned(), - parent_block_root: state.get_latest_block_root(state_root), - block_hash: execution_payload.block_hash(), - prev_randao: execution_payload.prev_randao(), - fee_recipient: proposer_preferences.fee_recipient, - // TODO(gloas) payload construction should factor in the proposers gas limit preferences - gas_limit: execution_payload.gas_limit(), - builder_index, - slot: produce_at_slot, - value, - execution_payment: 0, - blob_kzg_commitments, - } - } else if builder_index == u64::MAX { - // Local bid - ExecutionPayloadBid:: { - parent_block_hash: state.latest_block_hash()?.to_owned(), - parent_block_root: state.get_latest_block_root(state_root), - block_hash: execution_payload.block_hash(), - prev_randao: execution_payload.prev_randao(), - fee_recipient: Address::ZERO, - gas_limit: execution_payload.gas_limit(), - builder_index, - slot: produce_at_slot, - value, - execution_payment: 0, - blob_kzg_commitments, - } - } else { - // No proposer preferences and this isn't local building - // TODO(gloas) this should return a specific error type - // i.e if proposer prefs are missing and its a trustless bid - // return an error that communicates that. - return Err(BlockProductionError::GloasNotImplemented); + let state_root = state_root_opt.ok_or_else(|| { + BlockProductionError::MissingStateRoot + })?; + + let bid = ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: execution_payload.block_hash(), + prev_randao: execution_payload.prev_randao(), + fee_recipient: Address::ZERO, + gas_limit: execution_payload.gas_limit(), + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: 0, + blob_kzg_commitments, }; - Ok(bid) + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + // TODO(gloas) return better error variant here + signature: Signature::infinity() + .map_err(|_| BlockProductionError::GloasNotImplemented)?, + }, + state, + )) } } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 6a549c91ef..8fdf57fe1b 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -43,6 +43,49 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = "lh_produce_block_v4", + skip_all, + fields(%slot) +)] +pub async fn produce_block_v4( + accept_header: Option, + chain: Arc>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; + + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + + let (block, _state, consensus_block_value) = chain + .produce_block_with_verification_gloas( + randao_reveal, + slot, + graffiti_settings, + randao_verification, + builder_boost_factor, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + build_response_v4(chain, block, consensus_block_value, accept_header) +} + #[instrument( name = "lh_produce_block_v3", skip_all, @@ -87,6 +130,38 @@ pub async fn produce_block_v3( build_response_v3(chain, block_response_type, accept_header) } +pub fn build_response_v4( + chain: Arc>, + block: BeaconBlock>, + consensus_block_value: u64, + accept_header: Option, +) -> Result, warp::Rejection> { + let fork_name = block + .to_ref() + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; + let consensus_block_value_wei = Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(block.as_ssz_bytes().into()) + .map(|res: Response| add_ssz_content_type_header(res)) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => Ok(warp::reply::json(&beacon_response( + ResponseIncludesVersion::Yes(fork_name), + block, + )) + .into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)), + } +} + pub fn build_response_v3( chain: Arc>, block_response: BeaconBlockResponseWrapper, diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b1ab4c648a..126a9e472f 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,10 +1,10 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, ValidatorSubscriptionTxFilter, publish_network_message, }; -use crate::version::V3; +use crate::version::{V3, V4}; use crate::{StateId, attester_duties, proposer_duties, sync_committees}; use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; @@ -316,7 +316,11 @@ pub fn get_validator_blocks( not_synced_filter?; - if endpoint_version == V3 { + // Use V4 block production for Gloas fork + let fork_name = chain.spec.fork_name_at_slot::(slot); + if fork_name.gloas_enabled() { + produce_block_v4(accept_header, chain, slot, query).await + } else if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { produce_block_v2(accept_header, chain, slot, query).await @@ -327,6 +331,47 @@ pub fn get_validator_blocks( .boxed() } +// GET validator/execution_payload_bid/ +pub fn get_validator_execution_payload_bid( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!( + ?slot, + "Execution paylaod bid production request from HTTP API" + ); + + not_synced_filter?; + + todo!() + }) + }, + ) + .boxed() +} + // POST validator/liveness/{epoch} pub fn post_validator_liveness_epoch( eth_v1: EthV1Filter, diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index 371064c886..e1ba628032 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -14,6 +14,7 @@ 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 bef9fe6acd..825d8ed8ba 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3736,6 +3736,167 @@ impl ApiTester { self } + /// Test V4 block production (JSON). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4(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 (response, metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + let block = response.data; + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + // Sign and publish the block + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + /// Test V4 block production (SSZ). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4_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() + }; + + let (block, metadata) = self + .client + .get_validator_blocks_v4_ssz::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + // Sign and publish the block + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + + self.client + .post_beacon_blocks_v2_ssz(&signed_block_request, None) + .await + .unwrap(); + + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -7469,6 +7630,22 @@ async fn block_production_v3_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4_ssz() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 8746e3c063..267173b527 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -50,6 +50,7 @@ use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; @@ -2399,6 +2400,175 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// returns `GET v4/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v4_path( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result { + let mut path = self.eth_path(V4)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + if skip_randao_verification == SkipRandaoVerification::Yes { + path.query_pairs_mut() + .append_pair("skip_randao_verification", ""); + } + + if let Some(builder_booster_factor) = builder_booster_factor { + path.query_pairs_mut() + .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); + } + + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + + Ok(path) + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4_modular( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_result = self + .get_response_with_response_headers( + path, + Accept::Json, + self.timeouts.get_validator_block, + |response, headers| async move { + let header_metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let block_response = response + .json::, ProduceBlockV4Metadata>>() + .await?; + Ok((block_response, header_metadata)) + }, + ) + .await?; + + opt_result.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_modular_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_response = self + .get_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block, + |response, headers| async move { + let metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let response_bytes = response.bytes().await?; + + let block = BeaconBlock::from_ssz_bytes_for_fork( + &response_bytes, + metadata.consensus_version, + ) + .map_err(Error::InvalidSsz)?; + + Ok((block, metadata)) + }, + ) + .await?; + + opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c1572ca354..fa211b9d77 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1742,6 +1742,21 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } + +/// Metadata about a `ProduceBlockV3Response` which is returned in the body & headers. +#[derive(Debug, Deserialize, Serialize)] +pub struct ProduceBlockV4Metadata { + // The consensus version is serialized & deserialized by `ForkVersionedResponse`. + #[serde( + skip_serializing, + skip_deserializing, + default = "dummy_consensus_version" + )] + pub consensus_version: ForkName, + #[serde(with = "serde_utils::u256_dec")] + pub consensus_block_value: Uint256, +} + impl FullBlockContents { pub fn new(block: BeaconBlock, blob_data: Option<(KzgProofs, BlobsList)>) -> Self { match blob_data { @@ -1898,6 +1913,27 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } } +impl TryFrom<&HeaderMap> for ProduceBlockV4Metadata { + type Error = String; + + fn try_from(headers: &HeaderMap) -> Result { + let consensus_version = parse_required_header(headers, CONSENSUS_VERSION_HEADER, |s| { + s.parse::() + .map_err(|e| format!("invalid {CONSENSUS_VERSION_HEADER}: {e:?}")) + })?; + let consensus_block_value = + parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { + Uint256::from_str_radix(s, 10) + .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) + })?; + + Ok(ProduceBlockV4Metadata { + consensus_version, + consensus_block_value, + }) + } +} + /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 625f8db7cb..dd1b938baf 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -459,73 +459,145 @@ impl BlockService { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request an SSZ block from all beacon nodes in order, returning on the first successful response. - // If all nodes fail, run a second pass falling back to JSON. - // - // Proposer nodes will always be tried last during each pass since it's likely that they don't have a - // great view of attestations on the network. - let ssz_block_response = proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - beacon_node - .get_validator_blocks_v3_ssz::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - }) - .await; + // Check if Gloas fork is active at this slot + let fork_name = self_ref.chain_spec.fork_name_at_slot::(slot); - let block_response = match ssz_block_response { - Ok((ssz_block_response, _metadata)) => ssz_block_response, - Err(e) => { - warn!( - slot = slot.as_u64(), - error = %e, - "SSZ block production failed, falling back to JSON" - ); + let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() { + // Use V4 block production for Gloas + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v4_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; - proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - let (json_block_response, _metadata) = beacon_node - .get_validator_blocks_v3::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ V4 block production failed, falling back to JSON" + ); - Ok(json_block_response.data) - }) - .await - .map_err(BlockError::from)? - } - }; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v4::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response { - eth2::types::ProduceBlockV3Response::Full(block) => { - (block.block().proposer_index(), UnsignedBlock::Full(block)) - } - eth2::types::ProduceBlockV3Response::Blinded(block) => { - (block.proposer_index(), UnsignedBlock::Blinded(block)) + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + // Gloas blocks don't have blobs (they're in the execution layer) + let block_contents = eth2::types::FullBlockContents::Block(block_response); + ( + block_contents.block().proposer_index(), + UnsignedBlock::Full(block_contents), + ) + } else { + // Use V3 block production for pre-Gloas forks + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + // + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a + // great view of attestations on the network. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v3_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; + + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" + ); + + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; + + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + match block_response { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } } }; From 50dde1585c1f4f21e4a95f27bbff747bdbaa4022 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 17:27:58 -0800 Subject: [PATCH 09/37] Add payload to a cache for later signing --- beacon_node/beacon_chain/src/beacon_block.rs | 41 ++++- beacon_node/beacon_chain/src/beacon_chain.rs | 4 + beacon_node/beacon_chain/src/builder.rs | 1 + .../beacon_chain/src/execution_payload_bid.rs | 89 ++++++++--- beacon_node/beacon_chain/src/lib.rs | 1 + .../src/pending_payload_envelopes.rs | 150 ++++++++++++++++++ beacon_node/http_api/src/produce_block.rs | 3 +- beacon_node/http_api/src/validator/mod.rs | 4 +- beacon_node/http_api/tests/tests.rs | 13 ++ common/eth2/src/lib.rs | 16 +- common/eth2/src/types.rs | 1 - 11 files changed, 286 insertions(+), 37 deletions(-) create mode 100644 beacon_node/beacon_chain/src/pending_payload_envelopes.rs diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index 1b53f229dc..10a9fbe7c5 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -14,17 +14,18 @@ use state_processing::{ }; use state_processing::{VerifyOperation, state_advance::complete_state_advance}; use tracing::{Span, debug, debug_span, error, trace, warn}; +use tree_hash::TreeHash; use types::{ Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, - BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, FullPayload, - Graffiti, Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, - SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedVoluntaryExit, Slot, - SyncAggregate, + BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, + ExecutionPayloadEnvelope, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, + RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, + SignedVoluntaryExit, Slot, SyncAggregate, }; use crate::{ BeaconChain, BeaconChainTypes, BlockProductionError, ProduceBlockVerification, - graffiti_calculator::GraffitiSettings, metrics, + execution_payload_bid::ExecutionPayloadData, graffiti_calculator::GraffitiSettings, metrics, }; pub struct PartialBeaconBlock { @@ -147,7 +148,7 @@ impl BeaconChain { // Produce the execution payload bid. // TODO(gloas) this is strictly for building local bids // We'll need to build out trustless/trusted bid paths. - let (execution_payload_bid, state) = self + let (execution_payload_bid, state, payload_data) = self .clone() .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX) .await?; @@ -165,6 +166,7 @@ impl BeaconChain { chain.complete_partial_beacon_block_gloas( partial_beacon_block, execution_payload_bid, + payload_data, state, verification, ) @@ -417,6 +419,7 @@ impl BeaconChain { &self, partial_beacon_block: PartialBeaconBlock, signed_execution_payload_bid: SignedExecutionPayloadBid, + payload_data: Option>, mut state: BeaconState, verification: ProduceBlockVerification, ) -> Result< @@ -572,6 +575,32 @@ impl BeaconChain { let (mut block, _) = signed_beacon_block.deconstruct(); *block.state_root_mut() = state_root; + // Construct and cache the ExecutionPayloadEnvelope if we have payload data. + // For local building, we always have payload data. + // For trustless building, the builder will provide the envelope separately. + if let Some(payload_data) = payload_data { + let beacon_block_root = block.tree_hash_root(); + let execution_payload_envelope = ExecutionPayloadEnvelope { + payload: payload_data.payload, + execution_requests: payload_data.execution_requests, + builder_index: payload_data.builder_index, + beacon_block_root, + slot: payload_data.slot, + state_root: payload_data.state_root, + }; + + // Cache the envelope for later retrieval for signing and publishing. + self.pending_payload_envelopes + .write() + .insert(beacon_block_root, execution_payload_envelope); + + debug!( + %beacon_block_root, + slot = %block.slot(), + "Cached pending execution payload envelope" + ); + } + // TODO(gloas) // metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a12600141f..4f9e3b950e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -56,6 +56,7 @@ use crate::observed_block_producers::ObservedBlockProducers; use crate::observed_data_sidecars::ObservedDataSidecars; use crate::observed_operations::{ObservationOutcome, ObservedOperations}; use crate::observed_slashable::ObservedSlashable; +use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; @@ -419,6 +420,9 @@ pub struct BeaconChain { RwLock, T::EthSpec>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, + /// Cache of pending execution payload envelopes for local block building. + /// Envelopes are stored here during block production and eventually published. + pub pending_payload_envelopes: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex>, /// Maintains a record of which validators we've seen proposer slashings for. diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 5dbe662b9b..0cbcc28819 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1009,6 +1009,7 @@ where observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), + pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs index 752337a7e8..213fb71d4e 100644 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ b/beacon_node/beacon_chain/src/execution_payload_bid.rs @@ -4,8 +4,8 @@ use bls::Signature; use execution_layer::{BlockProposalContentsType, BuilderParams}; use tracing::instrument; use types::{ - Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, Hash256, - SignedExecutionPayloadBid, Slot, + Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, + ExecutionPayloadGloas, ExecutionRequests, Hash256, SignedExecutionPayloadBid, Slot, }; use crate::{ @@ -13,11 +13,27 @@ use crate::{ execution_payload::get_execution_payload, }; +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData { + pub payload: ExecutionPayloadGloas, + pub execution_requests: ExecutionRequests, + pub builder_index: BuilderIndex, + pub slot: Slot, + pub state_root: Hash256, +} + impl BeaconChain { // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless // bid building. Right now this only works for local building. /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. /// This function assumes we've already done the state advance. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// For local building, payload data is always returned (`Some`). + /// For trustless building, the builder provides the envelope separately, so `None` is returned. #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, @@ -30,6 +46,7 @@ impl BeaconChain { ( SignedExecutionPayloadBid, BeaconState, + Option>, ), BlockProductionError, > { @@ -82,33 +99,41 @@ impl BeaconChain { .map_err(BlockProductionError::TokioJoin)? .ok_or(BlockProductionError::ShuttingDown)??; - let (execution_payload, blob_kzg_commitments) = match block_contents_type { - BlockProposalContentsType::Full(block_proposal_contents) => { - let blob_kzg_commitments = - block_proposal_contents.blob_kzg_commitments().cloned(); + let (execution_payload, blob_kzg_commitments, execution_requests) = + match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let (payload, blob_kzg_commitments, _, execution_requests, _) = + block_proposal_contents.deconstruct(); - if let Some(blob_kzg_commitments) = blob_kzg_commitments { - ( - block_proposal_contents.to_payload().execution_payload(), - blob_kzg_commitments, - ) - } else { - return Err(BlockProductionError::MissingKzgCommitment( - "No KZG commitments from the payload".to_owned(), - )); + if let Some(blob_kzg_commitments) = blob_kzg_commitments + && let Some(execution_requests) = execution_requests + { + ( + payload.execution_payload(), + blob_kzg_commitments, + execution_requests, + ) + } else { + return Err(BlockProductionError::MissingKzgCommitment( + "No KZG commitments from the payload".to_owned(), + )); + } } - } - // TODO(gloas) we should never receive a blinded response. - // Should return some type of `Unexpected` error variant as this should never happen - // in the V4 block production flow - BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::GloasNotImplemented); - } - }; + // TODO(gloas) we should never receive a blinded response. + // Should return some type of `Unexpected` error variant as this should never happen + // in the V4 block production flow + BlockProposalContentsType::Blinded(_) => { + return Err(BlockProductionError::GloasNotImplemented); + } + }; - let state_root = state_root_opt.ok_or_else(|| { - BlockProductionError::MissingStateRoot - })?; + let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?; + + // TODO(gloas) this is just a dummy error variant for now + let execution_payload_gloas = execution_payload + .as_gloas() + .map_err(|_| BlockProductionError::GloasNotImplemented)? + .to_owned(); let bid = ExecutionPayloadBid:: { parent_block_hash: state.latest_block_hash()?.to_owned(), @@ -124,6 +149,15 @@ impl BeaconChain { blob_kzg_commitments, }; + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload: execution_payload_gloas, + execution_requests, + builder_index, + slot: produce_at_slot, + state_root, + }; + // TODO(gloas) this is only local building // we'll need to implement builder signature for the trustless path Ok(( @@ -134,6 +168,9 @@ impl BeaconChain { .map_err(|_| BlockProductionError::GloasNotImplemented)?, }, state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), )) } } diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4ddd075bb5..78385cd226 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -44,6 +44,7 @@ pub mod observed_block_producers; pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; +pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs new file mode 100644 index 0000000000..353830f175 --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -0,0 +1,150 @@ +//! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes +//! that have been produced during local block production but not yet imported to fork choice. +//! +//! For local building, the envelope is created during block production. +//! This cache holds the envelopes temporarily until the proposer can sign and publish the payload. + +use std::collections::HashMap; +use types::{EthSpec, ExecutionPayloadEnvelope, Hash256, Slot}; + +/// Cache for pending execution payload envelopes awaiting publishing. +/// +/// Envelopes are keyed by beacon block root and pruned based on slot age. +pub struct PendingPayloadEnvelopes { + /// Maximum number of slots to keep envelopes before pruning. + max_slot_age: u64, + /// The envelopes, keyed by beacon block root. + envelopes: HashMap>, +} + +impl Default for PendingPayloadEnvelopes { + fn default() -> Self { + Self::new(Self::DEFAULT_MAX_SLOT_AGE) + } +} + +impl PendingPayloadEnvelopes { + /// Default maximum slot age before pruning (2 slots). + pub const DEFAULT_MAX_SLOT_AGE: u64 = 2; + + /// Create a new cache with the specified maximum slot age. + pub fn new(max_slot_age: u64) -> Self { + Self { + max_slot_age, + envelopes: HashMap::new(), + } + } + + /// Insert a pending envelope into the cache. + pub fn insert(&mut self, block_root: Hash256, envelope: ExecutionPayloadEnvelope) { + self.envelopes.insert(block_root, envelope); + } + + /// Get a pending envelope by block root. + pub fn get(&self, block_root: &Hash256) -> Option<&ExecutionPayloadEnvelope> { + self.envelopes.get(block_root) + } + + /// Remove and return a pending envelope by block root. + pub fn remove(&mut self, block_root: &Hash256) -> Option> { + self.envelopes.remove(block_root) + } + + /// Check if an envelope exists for the given block root. + pub fn contains(&self, block_root: &Hash256) -> bool { + self.envelopes.contains_key(block_root) + } + + /// Prune envelopes older than `current_slot - max_slot_age`. + /// + /// This removes stale envelopes from blocks that were never imported. + pub fn prune(&mut self, current_slot: Slot) { + let min_slot = current_slot.saturating_sub(self.max_slot_age); + self.envelopes + .retain(|_, envelope| envelope.slot >= min_slot); + } + + /// Returns the number of pending envelopes in the cache. + pub fn len(&self) -> usize { + self.envelopes.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.envelopes.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ExecutionPayloadGloas, ExecutionRequests, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_envelope(slot: Slot, block_root: Hash256) -> ExecutionPayloadEnvelope { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: block_root, + slot, + state_root: Hash256::ZERO, + } + } + + #[test] + fn insert_and_get() { + let mut cache = PendingPayloadEnvelopes::::default(); + let block_root = Hash256::repeat_byte(1); + let envelope = make_envelope(Slot::new(1), block_root); + + assert!(!cache.contains(&block_root)); + assert_eq!(cache.len(), 0); + + cache.insert(block_root, envelope.clone()); + + assert!(cache.contains(&block_root)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(&block_root), Some(&envelope)); + } + + #[test] + fn remove() { + let mut cache = PendingPayloadEnvelopes::::default(); + let block_root = Hash256::repeat_byte(1); + let envelope = make_envelope(Slot::new(1), block_root); + + cache.insert(block_root, envelope.clone()); + assert!(cache.contains(&block_root)); + + let removed = cache.remove(&block_root); + assert_eq!(removed, Some(envelope)); + assert!(!cache.contains(&block_root)); + assert_eq!(cache.len(), 0); + } + + #[test] + fn prune_old_envelopes() { + let mut cache = PendingPayloadEnvelopes::::new(2); + + // Insert envelope at slot 5 + let block_root_1 = Hash256::repeat_byte(1); + let envelope_1 = make_envelope(Slot::new(5), block_root_1); + cache.insert(block_root_1, envelope_1); + + // Insert envelope at slot 10 + let block_root_2 = Hash256::repeat_byte(2); + let envelope_2 = make_envelope(Slot::new(10), block_root_2); + cache.insert(block_root_2, envelope_2); + + assert_eq!(cache.len(), 2); + + // Prune at slot 10 with max_slot_age=2, should keep slots >= 8 + cache.prune(Slot::new(10)); + + assert_eq!(cache.len(), 1); + assert!(!cache.contains(&block_root_1)); // slot 5 < 8, pruned + assert!(cache.contains(&block_root_2)); // slot 10 >= 8, kept + } +} diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 8fdf57fe1b..c520a608a9 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -140,7 +140,8 @@ pub fn build_response_v4( .to_ref() .fork_name(&chain.spec) .map_err(inconsistent_fork_rejection)?; - let consensus_block_value_wei = Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + let consensus_block_value_wei = + Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); match accept_header { Some(api_types::Accept::Ssz) => Response::builder() diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 126a9e472f..b82905be52 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,4 +1,6 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4}; +use crate::produce_block::{ + produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4, +}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 825d8ed8ba..2e5ce12323 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3799,6 +3799,19 @@ impl ApiTester { ); assert!(!metadata.consensus_block_value.is_zero()); + // Verify that the execution payload envelope is cached for local building. + // The envelope is stored in the pending cache until publishing. + let block_root = block.tree_hash_root(); + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(&block_root) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + // Sign and publish the block let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); let signed_block_request = diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 267173b527..c9672f2221 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2452,7 +2452,13 @@ impl BeaconNodeHttpClient { graffiti: Option<&Graffiti>, builder_booster_factor: Option, graffiti_policy: Option, - ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { self.get_validator_blocks_v4_modular( slot, randao_reveal, @@ -2473,7 +2479,13 @@ impl BeaconNodeHttpClient { skip_randao_verification: SkipRandaoVerification, builder_booster_factor: Option, graffiti_policy: Option, - ) -> Result<(ForkVersionedResponse, ProduceBlockV4Metadata>, ProduceBlockV4Metadata), Error> { + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { let path = self .get_validator_blocks_v4_path( slot, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index fa211b9d77..149b342a8c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1742,7 +1742,6 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } - /// Metadata about a `ProduceBlockV3Response` which is returned in the body & headers. #[derive(Debug, Deserialize, Serialize)] pub struct ProduceBlockV4Metadata { From 2d321f60ebc82678aaa40b0f80b024be9f529360 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 18:51:53 -0800 Subject: [PATCH 10/37] Add get payload envelope route --- beacon_node/beacon_chain/src/beacon_block.rs | 7 +- .../src/pending_payload_envelopes.rs | 85 +++++---- beacon_node/http_api/src/lib.rs | 9 + beacon_node/http_api/src/validator/mod.rs | 95 ++++++++++ beacon_node/http_api/tests/tests.rs | 173 +++++++++++++++++- common/eth2/src/lib.rs | 44 ++++- 6 files changed, 364 insertions(+), 49 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index 10a9fbe7c5..3b1fa00027 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -589,14 +589,15 @@ impl BeaconChain { state_root: payload_data.state_root, }; - // Cache the envelope for later retrieval for signing and publishing. + // Cache the envelope for later retrieval by the validator for signing and publishing. + let envelope_slot = payload_data.slot; self.pending_payload_envelopes .write() - .insert(beacon_block_root, execution_payload_envelope); + .insert(envelope_slot, execution_payload_envelope); debug!( %beacon_block_root, - slot = %block.slot(), + slot = %envelope_slot, "Cached pending execution payload envelope" ); } diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs index 353830f175..14979972b8 100644 --- a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -1,20 +1,22 @@ //! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes -//! that have been produced during local block production but not yet imported to fork choice. +//! that have been produced during local block production. //! //! For local building, the envelope is created during block production. -//! This cache holds the envelopes temporarily until the proposer can sign and publish the payload. +//! This cache holds the envelopes temporarily until the validator fetches, signs, +//! and publishes the payload. use std::collections::HashMap; -use types::{EthSpec, ExecutionPayloadEnvelope, Hash256, Slot}; +use types::{EthSpec, ExecutionPayloadEnvelope, Slot}; /// Cache for pending execution payload envelopes awaiting publishing. /// -/// Envelopes are keyed by beacon block root and pruned based on slot age. +/// Envelopes are keyed by slot and pruned based on slot age. +/// This cache is only used for local building. pub struct PendingPayloadEnvelopes { /// Maximum number of slots to keep envelopes before pruning. max_slot_age: u64, - /// The envelopes, keyed by beacon block root. - envelopes: HashMap>, + /// The envelopes, keyed by slot. + envelopes: HashMap>, } impl Default for PendingPayloadEnvelopes { @@ -36,32 +38,31 @@ impl PendingPayloadEnvelopes { } /// Insert a pending envelope into the cache. - pub fn insert(&mut self, block_root: Hash256, envelope: ExecutionPayloadEnvelope) { - self.envelopes.insert(block_root, envelope); + pub fn insert(&mut self, slot: Slot, envelope: ExecutionPayloadEnvelope) { + self.envelopes.insert(slot, envelope); } - /// Get a pending envelope by block root. - pub fn get(&self, block_root: &Hash256) -> Option<&ExecutionPayloadEnvelope> { - self.envelopes.get(block_root) + /// Get a pending envelope by slot. + pub fn get(&self, slot: Slot) -> Option<&ExecutionPayloadEnvelope> { + self.envelopes.get(&slot) } - /// Remove and return a pending envelope by block root. - pub fn remove(&mut self, block_root: &Hash256) -> Option> { - self.envelopes.remove(block_root) + /// Remove and return a pending envelope by slot. + pub fn remove(&mut self, slot: Slot) -> Option> { + self.envelopes.remove(&slot) } - /// Check if an envelope exists for the given block root. - pub fn contains(&self, block_root: &Hash256) -> bool { - self.envelopes.contains_key(block_root) + /// Check if an envelope exists for the given slot. + pub fn contains(&self, slot: Slot) -> bool { + self.envelopes.contains_key(&slot) } /// Prune envelopes older than `current_slot - max_slot_age`. /// - /// This removes stale envelopes from blocks that were never imported. + /// This removes stale envelopes from blocks that were never published. pub fn prune(&mut self, current_slot: Slot) { let min_slot = current_slot.saturating_sub(self.max_slot_age); - self.envelopes - .retain(|_, envelope| envelope.slot >= min_slot); + self.envelopes.retain(|slot, _| *slot >= min_slot); } /// Returns the number of pending envelopes in the cache. @@ -78,16 +79,16 @@ impl PendingPayloadEnvelopes { #[cfg(test)] mod tests { use super::*; - use types::{ExecutionPayloadGloas, ExecutionRequests, MainnetEthSpec}; + use types::{ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec}; type E = MainnetEthSpec; - fn make_envelope(slot: Slot, block_root: Hash256) -> ExecutionPayloadEnvelope { + fn make_envelope(slot: Slot) -> ExecutionPayloadEnvelope { ExecutionPayloadEnvelope { payload: ExecutionPayloadGloas::default(), execution_requests: ExecutionRequests::default(), builder_index: 0, - beacon_block_root: block_root, + beacon_block_root: Hash256::ZERO, slot, state_root: Hash256::ZERO, } @@ -96,31 +97,31 @@ mod tests { #[test] fn insert_and_get() { let mut cache = PendingPayloadEnvelopes::::default(); - let block_root = Hash256::repeat_byte(1); - let envelope = make_envelope(Slot::new(1), block_root); + let slot = Slot::new(1); + let envelope = make_envelope(slot); - assert!(!cache.contains(&block_root)); + assert!(!cache.contains(slot)); assert_eq!(cache.len(), 0); - cache.insert(block_root, envelope.clone()); + cache.insert(slot, envelope.clone()); - assert!(cache.contains(&block_root)); + assert!(cache.contains(slot)); assert_eq!(cache.len(), 1); - assert_eq!(cache.get(&block_root), Some(&envelope)); + assert_eq!(cache.get(slot), Some(&envelope)); } #[test] fn remove() { let mut cache = PendingPayloadEnvelopes::::default(); - let block_root = Hash256::repeat_byte(1); - let envelope = make_envelope(Slot::new(1), block_root); + let slot = Slot::new(1); + let envelope = make_envelope(slot); - cache.insert(block_root, envelope.clone()); - assert!(cache.contains(&block_root)); + cache.insert(slot, envelope.clone()); + assert!(cache.contains(slot)); - let removed = cache.remove(&block_root); + let removed = cache.remove(slot); assert_eq!(removed, Some(envelope)); - assert!(!cache.contains(&block_root)); + assert!(!cache.contains(slot)); assert_eq!(cache.len(), 0); } @@ -129,14 +130,12 @@ mod tests { let mut cache = PendingPayloadEnvelopes::::new(2); // Insert envelope at slot 5 - let block_root_1 = Hash256::repeat_byte(1); - let envelope_1 = make_envelope(Slot::new(5), block_root_1); - cache.insert(block_root_1, envelope_1); + let slot_1 = Slot::new(5); + cache.insert(slot_1, make_envelope(slot_1)); // Insert envelope at slot 10 - let block_root_2 = Hash256::repeat_byte(2); - let envelope_2 = make_envelope(Slot::new(10), block_root_2); - cache.insert(block_root_2, envelope_2); + let slot_2 = Slot::new(10); + cache.insert(slot_2, make_envelope(slot_2)); assert_eq!(cache.len(), 2); @@ -144,7 +143,7 @@ mod tests { cache.prune(Slot::new(10)); assert_eq!(cache.len(), 1); - assert!(!cache.contains(&block_root_1)); // slot 5 < 8, pruned - assert!(cache.contains(&block_root_2)); // slot 10 >= 8, kept + assert!(!cache.contains(slot_1)); // slot 5 < 8, pruned + assert!(cache.contains(slot_2)); // slot 10 >= 8, kept } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 4d7c76eb20..3cb52d8224 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2469,6 +2469,14 @@ pub fn serve( task_spawner_filter.clone(), ); + // GET validator/execution_payload_envelope/{slot}/{builder_index} + let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + eth_v1.clone().clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( eth_v1.clone().clone(), @@ -3327,6 +3335,7 @@ pub fn serve( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) + .uor(get_validator_execution_payload_envelope) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b82905be52..e7efa9564f 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -21,6 +21,7 @@ use eth2::types::{ use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; use slot_clock::SlotClock; +use ssz::Encode; use std::sync::Arc; use tokio::sync::mpsc::{Sender, UnboundedSender}; use tokio::sync::oneshot; @@ -30,6 +31,7 @@ use types::{ SignedContributionAndProof, SignedValidatorRegistrationData, Slot, SyncContributionData, ValidatorSubscription, }; +use warp::http::Response; use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; @@ -374,6 +376,99 @@ pub fn get_validator_execution_payload_bid( .boxed() } +// GET validator/execution_payload_envelope/{slot}/{builder_index} +pub fn get_validator_execution_payload_envelope( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid builder_index".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. + _builder_index: u64, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Execution payload envelope request from HTTP API"); + + not_synced_filter?; + + // Get the envelope from the pending cache (local building only) + let envelope = chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "Execution payload envelope not available for slot {slot}" + )) + })?; + + let fork_name = chain.spec.fork_name_at_slot::(slot); + + match accept_header { + Some(Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body(envelope.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build SSZ response: {e}" + )) + }), + _ => { + let json_response = GenericResponse { data: envelope }; + Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body( + serde_json::to_string(&json_response) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to serialize response: {e}" + )) + })? + .into(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build JSON response: {e}" + )) + }) + } + } + }) + }, + ) + .boxed() +} + // POST validator/liveness/{epoch} pub fn post_validator_liveness_epoch( eth_v1: EthV1Filter, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 2e5ce12323..90712d5a6b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3800,13 +3800,13 @@ impl ApiTester { assert!(!metadata.consensus_block_value.is_zero()); // Verify that the execution payload envelope is cached for local building. - // The envelope is stored in the pending cache until publishing. + // 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(&block_root) + .get(slot) .cloned() .expect("envelope should exist in pending cache for local building"); assert_eq!(envelope.beacon_block_root, block_root); @@ -3910,6 +3910,159 @@ impl ApiTester { 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() + }; + + // 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 (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; + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + + 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(); + + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -7659,6 +7812,22 @@ 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() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index c9672f2221..35219ff924 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -42,7 +42,7 @@ use reqwest::{ #[cfg(feature = "events")] use reqwest_eventsource::{Event, EventSource}; use serde::{Serialize, de::DeserializeOwned}; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; @@ -2581,6 +2581,48 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` + pub async fn get_validator_execution_payload_envelope( + &self, + slot: Slot, + builder_index: u64, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + self.get(path).await + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` in SSZ format + pub async fn get_validator_execution_payload_envelope_ssz( + &self, + slot: Slot, + builder_index: u64, + ) -> Result, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + let opt_response = self + .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block) + .await?; + + let response_bytes = opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND))?; + + ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz( &self, From 1ed80fa35d32881900f955cdc5b1a13bd4ed02ce Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 19:37:09 -0800 Subject: [PATCH 11/37] Fetch and sign payload envelope --- beacon_node/http_api/src/validator/mod.rs | 6 +- beacon_node/http_api/tests/tests.rs | 43 +++++++++-- .../execution/execution_payload_envelope.rs | 43 +++++++++++ .../lighthouse_validator_store/src/lib.rs | 41 +++++++++-- validator_client/signing_method/src/lib.rs | 5 ++ .../signing_method/src/web3signer.rs | 3 + .../validator_services/src/block_service.rs | 71 ++++++++++++++++++- validator_client/validator_store/src/lib.rs | 12 +++- 8 files changed, 208 insertions(+), 16 deletions(-) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index e7efa9564f..1575ad3a5c 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -403,9 +403,9 @@ pub fn get_validator_execution_payload_envelope( .and(chain_filter) .then( |slot: Slot, - // TODO(gloas) we're only doing local building - // we'll need to implement builder index logic - // eventually. + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. _builder_index: u64, accept_header: Option, not_synced_filter: Result<(), Rejection>, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 90712d5a6b..f8d345e87c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -3978,8 +3978,26 @@ impl ApiTester { .unwrap(); let envelope = envelope_response.data; - assert_eq!(envelope.beacon_block_root, block_root); - assert_eq!(envelope.slot, slot); + + // 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); } @@ -4054,8 +4072,25 @@ impl ApiTester { .await .unwrap(); - assert_eq!(envelope.beacon_block_root, block_root); - assert_eq!(envelope.slot, slot); + // 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); } diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 7f68dae037..978a35dfb9 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -28,6 +28,49 @@ impl SignedRoot for ExecutionPayloadEnvelope {} mod tests { use super::*; use crate::MainnetEthSpec; + use crate::test_utils::TestRandom; + use rand::SeedableRng; + use rand_xorshift::XorShiftRng; ssz_and_tree_hash_tests!(ExecutionPayloadEnvelope); + + #[test] + fn signing_root_is_deterministic() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let domain = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope.signing_root(domain); + let signing_root_2 = envelope.signing_root(domain); + + assert_eq!(signing_root_1, signing_root_2); + } + + #[test] + fn signing_root_changes_with_domain() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let domain_1 = Hash256::random_for_test(&mut rng); + let domain_2 = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope.signing_root(domain_1); + let signing_root_2 = envelope.signing_root(domain_2); + + assert_ne!(signing_root_1, signing_root_2); + } + + #[test] + fn signing_root_changes_with_envelope_data() { + let mut rng = XorShiftRng::from_seed([0x42; 16]); + let envelope_1 = ExecutionPayloadEnvelope::::random_for_test(&mut rng); + let mut envelope_2 = envelope_1.clone(); + envelope_2.beacon_block_root = Hash256::random_for_test(&mut rng); + + let domain = Hash256::random_for_test(&mut rng); + + let signing_root_1 = envelope_1.signing_root(domain); + let signing_root_2 = envelope_2.signing_root(domain); + + assert_ne!(signing_root_1, signing_root_2); + } } diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7b6a582363..c3c61751d0 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -20,12 +20,12 @@ use task_executor::TaskExecutor; use tracing::{error, info, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, + FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -1242,4 +1242,33 @@ impl ValidatorStore for LighthouseValidatorS .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } + + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> Result, Error> { + let domain_hash = self.spec.get_builder_domain(); + let signing_root = envelope.signing_root(domain_hash); + + // Execution payload envelope signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature_from_root::>( + SignableMessage::ExecutionPayloadEnvelope(&envelope), + signing_root, + &self.task_executor, + None, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index bf3cc6a17d..c132d86c17 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP SignedContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl> SignableMessage<'_, E, Payload> { @@ -70,6 +71,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), } } } @@ -233,6 +235,9 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionPayloadEnvelope(e) => { + Web3SignerObject::ExecutionPayloadEnvelope(e) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 246d9e9e09..7bf953aaeb 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,7 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + ExecutionPayloadEnvelope, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -75,6 +76,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -140,6 +142,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, } } } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index dd1b938baf..243059702b 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -143,6 +143,7 @@ impl BlockServiceBuilder { // Combines a set of non-block-proposing `beacon_nodes` and only-block-proposing // `proposer_nodes`. +#[derive(Clone)] pub struct ProposerFallback { beacon_nodes: Arc>, proposer_nodes: Option>>, @@ -610,7 +611,7 @@ impl BlockService { self_ref .sign_and_publish_block( - proposer_fallback, + proposer_fallback.clone(), slot, graffiti, &validator_pubkey, @@ -618,6 +619,74 @@ impl BlockService { ) .await?; + // For Gloas, fetch the execution payload envelope, sign it, and publish it + if fork_name.gloas_enabled() { + self_ref + .fetch_sign_and_publish_payload_envelope(proposer_fallback, slot, &validator_pubkey) + .await?; + } + + Ok(()) + } + + /// Fetch, sign, and publish the execution payload envelope for Gloas. + /// This should be called after the block has been published. + #[instrument(skip_all, fields(%slot, ?validator_pubkey))] + async fn fetch_sign_and_publish_payload_envelope( + &self, + proposer_fallback: ProposerFallback, + slot: Slot, + validator_pubkey: &PublicKeyBytes, + ) -> Result<(), BlockError> { + info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + + // Fetch the envelope from the beacon node (builder_index=0 for local building) + let envelope = proposer_fallback + .request_proposers_last(|beacon_node| async move { + beacon_node + .get_validator_execution_payload_envelope::(slot, 0) + .await + .map(|response| response.data) + .map_err(|e| { + BlockError::Recoverable(format!( + "Error fetching execution payload envelope: {:?}", + e + )) + }) + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %envelope.beacon_block_root, + "Received execution payload envelope, signing" + ); + + // Sign the envelope + let signed_envelope = self + .validator_store + .sign_execution_payload_envelope(*validator_pubkey, envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error signing execution payload envelope: {:?}", + e + )) + })?; + + info!( + slot = slot.as_u64(), + "Signed execution payload envelope, publishing" + ); + + // TODO(gloas): Publish the signed envelope + // For now, just log that we would publish it + debug!( + slot = slot.as_u64(), + beacon_block_root = %signed_envelope.message.beacon_block_root, + "Would publish signed execution payload envelope (not yet implemented)" + ); + Ok(()) } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 4fdbb8064c..87ab669e8d 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -5,8 +5,9 @@ use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use types::{ - Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, + ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; @@ -178,6 +179,13 @@ pub trait ValidatorStore: Send + Sync { /// runs. fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + /// Sign an `ExecutionPayloadEnvelope` for Gloas. + fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> impl Future, Error>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. From 25853847ef43d384d35cc8d461f0bc670ba6e228 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 20:28:28 -0800 Subject: [PATCH 12/37] 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(()) From 1c1e6dda10561463426cfaf55e461038ca010989 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 20:28:41 -0800 Subject: [PATCH 13/37] Fmt --- beacon_node/http_api/src/lib.rs | 17 ++++++++++++----- .../src/publish_execution_payload_envelope.rs | 6 +++--- beacon_node/http_api/tests/tests.rs | 1 - 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 812d0244e5..35885e2091 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1504,7 +1504,9 @@ pub fn serve( 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, + envelope, + chain, + &network_tx, ) .await }) @@ -1517,7 +1519,10 @@ pub fn serve( .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::header::exact( + CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, + )) .and(warp::body::bytes()) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) @@ -1531,10 +1536,12 @@ pub fn serve( let envelope = SignedExecutionPayloadEnvelope::::from_ssz_bytes(&body_bytes) .map_err(|e| { - warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) - })?; + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; publish_execution_payload_envelope::publish_execution_payload_envelope( - envelope, chain, &network_tx, + envelope, + chain, + &network_tx, ) .await }) diff --git a/beacon_node/http_api/src/publish_execution_payload_envelope.rs b/beacon_node/http_api/src/publish_execution_payload_envelope.rs index 0496f94a92..1e4225505c 100644 --- a/beacon_node/http_api/src/publish_execution_payload_envelope.rs +++ b/beacon_node/http_api/src/publish_execution_payload_envelope.rs @@ -17,9 +17,9 @@ pub async fn publish_execution_payload_envelope( 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()))?; + 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 { diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 392f7c7869..df943b0e6c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -7721,7 +7721,6 @@ async fn block_production_v4_ssz() { .await; } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; From 96d02ad93d9ba33531c8a549f864a727235b696b Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 20:52:44 -0800 Subject: [PATCH 14/37] Resolve merge conflicts --- testing/ef_tests/Makefile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index ee0cebd478..fd8a3f6da0 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,10 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -<<<<<<< HEAD -CONSENSUS_SPECS_TEST_VERSION ?= nightly-21573464110 -======= CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.2 ->>>>>>> 1dd0f7bcbb9e476028ec9146a149e121ad28ff9e REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) From 75bb4288ffe85f896c165a3c7a4b6309624234f4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 3 Feb 2026 21:19:58 -0800 Subject: [PATCH 15/37] Resolve some TODOs --- beacon_node/beacon_chain/src/beacon_block.rs | 35 +++++++------------ .../validator_services/src/block_service.rs | 5 +++ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/beacon_block.rs index 3b69318427..98233cdf08 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/beacon_block.rs @@ -196,14 +196,12 @@ impl BeaconChain { }); } - // TODO(gloas) - // let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); + let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); // Ensure the state has performed a complete transition into the required slot. complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?; - // TODO(gloas) - // drop(slot_timer); + drop(slot_timer); state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; state.apply_pending_mutations()?; @@ -238,9 +236,8 @@ impl BeaconChain { // are included in the operation pool. { let _guard = debug_span!("import_naive_aggregation_pool").entered(); - // TODO(gloas) - // let _unagg_import_timer = - // metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + let _unagg_import_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); for attestation in self.naive_aggregation_pool.read().iter() { let import = |attestation: &Attestation| { let attesting_indices = @@ -260,9 +257,8 @@ impl BeaconChain { let mut attestations = { let _guard = debug_span!("pack_attestations").entered(); - // TODO(gloas) - // let _attestation_packing_timer = - // metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + let _attestation_packing_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); // Epoch cache and total balance cache are required for op pool packing. state.build_total_active_balance_cache(&self.spec)?; @@ -528,19 +524,16 @@ impl BeaconChain { Signature::empty(), ); - // TODO(gloas) ensure block size is measured from the signed block let block_size = signed_beacon_block.ssz_bytes_len(); debug!(%block_size, "Produced block on state"); - // TODO(gloas) - // metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); + metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); if block_size > self.config.max_network_size { return Err(BlockProductionError::BlockTooLarge(block_size)); } - // TODO(gloas) - // let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); + let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); let signature_strategy = match verification { ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao, ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification, @@ -562,16 +555,13 @@ impl BeaconChain { &mut ctxt, &self.spec, )?; - // TODO(gloas) - // drop(process_timer); + drop(process_timer); - // TODO(gloas) - //let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); + let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); let state_root = state.update_tree_hash_cache()?; - // TODO(gloas) - // drop(state_root_timer); + drop(state_root_timer); let (mut block, _) = signed_beacon_block.deconstruct(); *block.state_root_mut() = state_root; @@ -603,8 +593,7 @@ impl BeaconChain { ); } - // TODO(gloas) - // metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); trace!( parent = ?block.parent_root(), diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 1f1ccdb320..2e20ec736e 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -631,6 +631,11 @@ impl BlockService { /// Fetch, sign, and publish the execution payload envelope for Gloas. /// This should be called after the block has been published. + /// + /// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block + /// and fetch/publish the envelope from that same node. The envelope is cached per-BN, + /// so fetching from a different BN than the one that built the block will fail. + /// See: https://github.com/sigp/lighthouse/pull/8313 #[instrument(skip_all, fields(%slot, ?validator_pubkey))] async fn fetch_sign_and_publish_payload_envelope( &self, From f9bfaf9da69ff7643391ab0829bc6d88dff4874c Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 5 Feb 2026 10:53:13 +1100 Subject: [PATCH 16/37] Move block production to gloas file (no logic change). --- .../gloas.rs} | 210 +++++++++++++++--- .../beacon_chain/src/block_production/mod.rs | 1 + .../beacon_chain/src/execution_payload_bid.rs | 177 --------------- beacon_node/beacon_chain/src/lib.rs | 3 +- .../src/pending_payload_envelopes.rs | 1 + beacon_node/http_api/src/produce_block.rs | 8 +- 6 files changed, 183 insertions(+), 217 deletions(-) rename beacon_node/beacon_chain/src/{beacon_block.rs => block_production/gloas.rs} (73%) create mode 100644 beacon_node/beacon_chain/src/block_production/mod.rs delete mode 100644 beacon_node/beacon_chain/src/execution_payload_bid.rs diff --git a/beacon_node/beacon_chain/src/beacon_block.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs similarity index 73% rename from beacon_node/beacon_chain/src/beacon_block.rs rename to beacon_node/beacon_chain/src/block_production/gloas.rs index 98233cdf08..9b6285bbac 100644 --- a/beacon_node/beacon_chain/src/beacon_block.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use std::sync::Arc; use bls::Signature; +use execution_layer::{BlockProposalContentsType, BuilderParams}; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::get_attesting_indices_from_state; @@ -12,19 +13,21 @@ use state_processing::{ BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, }; use state_processing::{VerifyOperation, state_advance::complete_state_advance}; -use tracing::{Span, debug, debug_span, error, trace, warn}; +use tracing::{Span, debug, debug_span, error, instrument, trace, warn}; use tree_hash::TreeHash; use types::{ - Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, - BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, Deposit, Eth1Data, EthSpec, - ExecutionPayloadEnvelope, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, - RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, SignedExecutionPayloadBid, - SignedVoluntaryExit, Slot, SyncAggregate, + Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BlockProductionVersion, + BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, + ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, Hash256, PayloadAttestation, + ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, + SignedExecutionPayloadBid, SignedVoluntaryExit, Slot, SyncAggregate, }; +use crate::execution_payload::get_execution_payload; use crate::{ - BeaconChain, BeaconChainTypes, BlockProductionError, ProduceBlockVerification, - execution_payload_bid::ExecutionPayloadData, graffiti_calculator::GraffitiSettings, metrics, + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics, }; pub struct PartialBeaconBlock { @@ -44,7 +47,16 @@ pub struct PartialBeaconBlock { bls_to_execution_changes: Vec, } -// We'll need to add that once we include trusted/trustless bids +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData { + pub payload: ExecutionPayloadGloas, + pub execution_requests: ExecutionRequests, + pub builder_index: BuilderIndex, + pub slot: Slot, + pub state_root: Hash256, +} + impl BeaconChain { pub async fn produce_block_with_verification_gloas( self: &Arc, @@ -53,14 +65,7 @@ impl BeaconChain { graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, _builder_boost_factor: Option, - ) -> Result< - ( - BeaconBlock>, - BeaconState, - u64, - ), - BlockProductionError, - > { + ) -> Result<(BeaconBlock>, u64), BlockProductionError> { metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); // Part 1/2 (blocking) @@ -105,14 +110,7 @@ impl BeaconChain { randao_reveal: Signature, graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, - ) -> Result< - ( - BeaconBlock>, - BeaconState, - u64, - ), - BlockProductionError, - > { + ) -> Result<(BeaconBlock>, u64), BlockProductionError> { // Part 1/3 (blocking) // // Perform the state advance and block-packing functions. @@ -419,14 +417,7 @@ impl BeaconChain { payload_data: Option>, mut state: BeaconState, verification: ProduceBlockVerification, - ) -> Result< - ( - BeaconBlock>, - BeaconState, - u64, - ), - BlockProductionError, - > { + ) -> Result<(BeaconBlock>, u64), BlockProductionError> { let PartialBeaconBlock { slot, proposer_index, @@ -602,6 +593,157 @@ impl BeaconChain { "Produced beacon block" ); - Ok((block, state, consensus_block_value)) + Ok((block, consensus_block_value)) + } + + // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless + // bid building. Right now this only works for local building. + /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + /// This function assumes we've already done the state advance. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// 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, + state: BeaconState, + state_root_opt: Option, + produce_at_slot: Slot, + bid_value: u64, + builder_index: BuilderIndex, + ) -> Result< + ( + SignedExecutionPayloadBid, + BeaconState, + Option>, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value + // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. + + // TODO(gloas) add metrics for execution payload bid production + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + // TODO(gloas) this should be BlockProductionVersion::V4 + // V3 is okay for now as long as we're not connected to a builder + // TODO(gloas) add builder boost factor + let prepare_payload_handle = get_execution_payload( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + None, + BlockProductionVersion::V3, + )?; + + let block_contents_type = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; + + let (execution_payload, blob_kzg_commitments, execution_requests) = + match block_contents_type { + BlockProposalContentsType::Full(block_proposal_contents) => { + let (payload, blob_kzg_commitments, _, execution_requests, _) = + block_proposal_contents.deconstruct(); + + if let Some(blob_kzg_commitments) = blob_kzg_commitments + && let Some(execution_requests) = execution_requests + { + ( + payload.execution_payload(), + blob_kzg_commitments, + execution_requests, + ) + } else { + return Err(BlockProductionError::MissingKzgCommitment( + "No KZG commitments from the payload".to_owned(), + )); + } + } + // TODO(gloas) we should never receive a blinded response. + // Should return some type of `Unexpected` error variant as this should never happen + // in the V4 block production flow + BlockProposalContentsType::Blinded(_) => { + return Err(BlockProductionError::GloasNotImplemented); + } + }; + + let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?; + + // TODO(gloas) this is just a dummy error variant for now + let execution_payload_gloas = execution_payload + .as_gloas() + .map_err(|_| BlockProductionError::GloasNotImplemented)? + .to_owned(); + + let bid = ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: execution_payload.block_hash(), + prev_randao: execution_payload.prev_randao(), + fee_recipient: Address::ZERO, + gas_limit: execution_payload.gas_limit(), + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: 0, + blob_kzg_commitments, + }; + + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload: execution_payload_gloas, + execution_requests, + builder_index, + slot: produce_at_slot, + state_root, + }; + + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + // TODO(gloas) return better error variant here + signature: Signature::infinity() + .map_err(|_| BlockProductionError::GloasNotImplemented)?, + }, + state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), + )) } } diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs new file mode 100644 index 0000000000..37b62a181b --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -0,0 +1 @@ +mod gloas; diff --git a/beacon_node/beacon_chain/src/execution_payload_bid.rs b/beacon_node/beacon_chain/src/execution_payload_bid.rs deleted file mode 100644 index fe9974d93c..0000000000 --- a/beacon_node/beacon_chain/src/execution_payload_bid.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::sync::Arc; - -use bls::Signature; -use execution_layer::{BlockProposalContentsType, BuilderParams}; -use tracing::instrument; -use types::{ - Address, BeaconState, BlockProductionVersion, BuilderIndex, ExecutionPayloadBid, - ExecutionPayloadGloas, ExecutionRequests, Hash256, SignedExecutionPayloadBid, Slot, -}; - -use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, - execution_payload::get_execution_payload, -}; - -/// Data needed to construct an ExecutionPayloadEnvelope. -/// The envelope requires the beacon_block_root which can only be computed after the block exists. -pub struct ExecutionPayloadData { - pub payload: ExecutionPayloadGloas, - pub execution_requests: ExecutionRequests, - pub builder_index: BuilderIndex, - pub slot: Slot, - pub state_root: Hash256, -} - -impl BeaconChain { - // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless - // bid building. Right now this only works for local building. - /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. - /// This function assumes we've already done the state advance. - /// - /// Returns the signed bid, the state, and optionally the payload data needed to construct - /// the `ExecutionPayloadEnvelope` after the beacon block is created. - /// - /// 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, - state: BeaconState, - state_root_opt: Option, - produce_at_slot: Slot, - bid_value: u64, - builder_index: BuilderIndex, - ) -> Result< - ( - SignedExecutionPayloadBid, - BeaconState, - Option>, - ), - BlockProductionError, - > { - // TODO(gloas) For non local building, add sanity check on value - // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. - - // TODO(gloas) add metrics for execution payload bid production - - let parent_root = if state.slot() > 0 { - *state - .get_block_root(state.slot() - 1) - .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? - } else { - state.latest_block_header().canonical_root() - }; - - let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; - - let pubkey = state - .validators() - .get(proposer_index as usize) - .map(|v| v.pubkey) - .ok_or(BlockProductionError::BeaconChain(Box::new( - BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), - )))?; - - let builder_params = BuilderParams { - pubkey, - slot: state.slot(), - chain_health: self - .is_healthy(&parent_root) - .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, - }; - - // TODO(gloas) this should be BlockProductionVersion::V4 - // V3 is okay for now as long as we're not connected to a builder - // TODO(gloas) add builder boost factor - let prepare_payload_handle = get_execution_payload( - self.clone(), - &state, - parent_root, - proposer_index, - builder_params, - None, - BlockProductionVersion::V3, - )?; - - let block_contents_type = prepare_payload_handle - .await - .map_err(BlockProductionError::TokioJoin)? - .ok_or(BlockProductionError::ShuttingDown)??; - - let (execution_payload, blob_kzg_commitments, execution_requests) = - match block_contents_type { - BlockProposalContentsType::Full(block_proposal_contents) => { - let (payload, blob_kzg_commitments, _, execution_requests, _) = - block_proposal_contents.deconstruct(); - - if let Some(blob_kzg_commitments) = blob_kzg_commitments - && let Some(execution_requests) = execution_requests - { - ( - payload.execution_payload(), - blob_kzg_commitments, - execution_requests, - ) - } else { - return Err(BlockProductionError::MissingKzgCommitment( - "No KZG commitments from the payload".to_owned(), - )); - } - } - // TODO(gloas) we should never receive a blinded response. - // Should return some type of `Unexpected` error variant as this should never happen - // in the V4 block production flow - BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::GloasNotImplemented); - } - }; - - let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?; - - // TODO(gloas) this is just a dummy error variant for now - let execution_payload_gloas = execution_payload - .as_gloas() - .map_err(|_| BlockProductionError::GloasNotImplemented)? - .to_owned(); - - let bid = ExecutionPayloadBid:: { - parent_block_hash: state.latest_block_hash()?.to_owned(), - parent_block_root: state.get_latest_block_root(state_root), - block_hash: execution_payload.block_hash(), - prev_randao: execution_payload.prev_randao(), - fee_recipient: Address::ZERO, - gas_limit: execution_payload.gas_limit(), - builder_index, - slot: produce_at_slot, - value: bid_value, - execution_payment: 0, - blob_kzg_commitments, - }; - - // Store payload data for envelope construction after block is created - let payload_data = ExecutionPayloadData { - payload: execution_payload_gloas, - execution_requests, - builder_index, - slot: produce_at_slot, - state_root, - }; - - // TODO(gloas) this is only local building - // we'll need to implement builder signature for the trustless path - Ok(( - SignedExecutionPayloadBid { - message: bid, - // TODO(gloas) return better error variant here - signature: Signature::infinity() - .map_err(|_| BlockProductionError::GloasNotImplemented)?, - }, - state, - // Local building always returns payload data. - // Trustless building would return None here. - Some(payload_data), - )) - } -} diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 78385cd226..3b03395a66 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -1,7 +1,6 @@ pub mod attestation_rewards; pub mod attestation_simulator; pub mod attestation_verification; -mod beacon_block; pub mod beacon_block_reward; mod beacon_block_streamer; mod beacon_chain; @@ -10,6 +9,7 @@ pub mod beacon_proposer_cache; mod beacon_snapshot; pub mod bellatrix_readiness; pub mod blob_verification; +mod block_production; pub mod block_reward; mod block_times_cache; mod block_verification; @@ -24,7 +24,6 @@ mod early_attester_cache; mod errors; pub mod events; pub mod execution_payload; -pub mod execution_payload_bid; pub mod fetch_blobs; pub mod fork_choice_signal; pub mod fork_revert; diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs index 14979972b8..80e71b1178 100644 --- a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -39,6 +39,7 @@ impl PendingPayloadEnvelopes { /// Insert a pending envelope into the cache. pub fn insert(&mut self, slot: Slot, envelope: ExecutionPayloadEnvelope) { + // TODO(gloas): we may want to check for duplicates here, which shouldn't be allowed self.envelopes.insert(slot, envelope); } diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index c520a608a9..f97df399d7 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -70,7 +70,7 @@ pub async fn produce_block_v4( let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); - let (block, _state, consensus_block_value) = chain + let (block, consensus_block_value) = chain .produce_block_with_verification_gloas( randao_reveal, slot, @@ -83,7 +83,7 @@ pub async fn produce_block_v4( warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) })?; - build_response_v4(chain, block, consensus_block_value, accept_header) + build_response_v4::(block, consensus_block_value, accept_header, &chain.spec) } #[instrument( @@ -131,14 +131,14 @@ pub async fn produce_block_v3( } pub fn build_response_v4( - chain: Arc>, block: BeaconBlock>, consensus_block_value: u64, accept_header: Option, + spec: &ChainSpec, ) -> Result, warp::Rejection> { let fork_name = block .to_ref() - .fork_name(&chain.spec) + .fork_name(&spec) .map_err(inconsistent_fork_rejection)?; let consensus_block_value_wei = Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); From 339ba6e1435e538f756fd0929c76582ad0557195 Mon Sep 17 00:00:00 2001 From: Jimmy Chen Date: Thu, 5 Feb 2026 12:25:40 +1100 Subject: [PATCH 17/37] Move gloas http logic to modules. --- .../src/beacon/execution_payload_envelope.rs | 127 ++++++++++++++++ beacon_node/http_api/src/beacon/mod.rs | 1 + beacon_node/http_api/src/beacon/states.rs | 1 - beacon_node/http_api/src/lib.rs | 92 ++++-------- beacon_node/http_api/src/produce_block.rs | 2 +- .../src/publish_execution_payload_envelope.rs | 57 ------- .../src/validator/execution_payload_bid.rs | 52 +++++++ .../validator/execution_payload_envelope.rs | 105 +++++++++++++ beacon_node/http_api/src/validator/mod.rs | 140 +----------------- common/eth2/src/types.rs | 2 +- 10 files changed, 321 insertions(+), 258 deletions(-) create mode 100644 beacon_node/http_api/src/beacon/execution_payload_envelope.rs delete mode 100644 beacon_node/http_api/src/publish_execution_payload_envelope.rs create mode 100644 beacon_node/http_api/src/validator/execution_payload_bid.rs create mode 100644 beacon_node/http_api/src/validator/execution_payload_envelope.rs diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs new file mode 100644 index 0000000000..13495d4d6d --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -0,0 +1,127 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; +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::SignedExecutionPayloadEnvelope; +use warp::{Filter, Rejection, Reply, reply::Response}; + +// POST beacon/execution_payload_envelope (SSZ) +pub(crate) fn post_beacon_execution_payload_envelope_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_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) + .and(network_tx_filter) + .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(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} + +// POST beacon/execution_payload_envelope +pub(crate) fn post_beacon_execution_payload_envelope( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + 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(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} +/// 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/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index df5e6eee5c..9ec1c476f6 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,2 +1,3 @@ +pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 828efb86a7..50be7211d8 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -28,7 +28,6 @@ pub fn get_beacon_state_pending_consolidations( beacon_states_path: BeaconStatesPath, ) -> ResponseFilter { beacon_states_path - .clone() .and(warp::path("pending_consolidations")) .and(warp::path::end()) .then( diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 35885e2091..9e09ef65b7 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -23,7 +23,6 @@ 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; @@ -37,9 +36,15 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::execution_payload_envelope::{ + post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, +}; use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; -use crate::utils::{AnyVersionFilter, EthV1Filter}; +use crate::utils::{ + AnyVersionFilter, EthV1Filter, +}; +use crate::validator::execution_payload_bid::get_validator_execution_payload_bid; use crate::validator::post_validator_liveness_epoch; use crate::validator::*; use crate::version::beacon_response; @@ -72,7 +77,7 @@ pub use publish_blocks::{ }; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; -use ssz::{Decode, Encode}; +use ssz::Encode; pub use state_id::StateId; use std::future::Future; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -91,8 +96,9 @@ use tokio_stream::{ use tracing::{debug, info, warn}; use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, - SignedBlindedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, + SignedBlindedBeaconBlock, Slot, }; +use validator::execution_payload_envelope::get_validator_execution_payload_envelope; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1488,65 +1494,20 @@ pub fn serve( 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 - }) - }, - ); + let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); // 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 post_beacon_execution_payload_envelope_ssz = post_beacon_execution_payload_envelope_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); let beacon_rewards_path = eth_v1 .clone() @@ -2539,6 +2500,14 @@ pub fn serve( task_spawner_filter.clone(), ); + // GET validator/execution_payload_bid/ + let get_validator_execution_payload_bid = get_validator_execution_payload_bid( + eth_v1.clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( eth_v1.clone().clone(), @@ -3398,6 +3367,7 @@ pub fn serve( .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) .uor(get_validator_execution_payload_envelope) + .uor(get_validator_execution_payload_bid) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index f97df399d7..c5338475b4 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -138,7 +138,7 @@ pub fn build_response_v4( ) -> Result, warp::Rejection> { let fork_name = block .to_ref() - .fork_name(&spec) + .fork_name(spec) .map_err(inconsistent_fork_rejection)?; let consensus_block_value_wei = Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); diff --git a/beacon_node/http_api/src/publish_execution_payload_envelope.rs b/beacon_node/http_api/src/publish_execution_payload_envelope.rs deleted file mode 100644 index 1e4225505c..0000000000 --- a/beacon_node/http_api/src/publish_execution_payload_envelope.rs +++ /dev/null @@ -1,57 +0,0 @@ -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/execution_payload_bid.rs b/beacon_node/http_api/src/validator/execution_payload_bid.rs new file mode 100644 index 0000000000..8e1235d0b0 --- /dev/null +++ b/beacon_node/http_api/src/validator/execution_payload_bid.rs @@ -0,0 +1,52 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::Accept; +use std::sync::Arc; +use tracing::debug; +use types::Slot; +use warp::{Filter, Rejection}; + +// GET validator/execution_payload_bid/ +#[allow(dead_code)] +pub fn get_validator_execution_payload_bid( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_bid")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + _accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + _chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!( + ?slot, + "Execution paylaod bid production request from HTTP API" + ); + + not_synced_filter?; + + todo!() + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelope.rs new file mode 100644 index 0000000000..16df922781 --- /dev/null +++ b/beacon_node/http_api/src/validator/execution_payload_envelope.rs @@ -0,0 +1,105 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::types::{Accept, GenericResponse}; +use ssz::Encode; +use std::sync::Arc; +use tracing::debug; +use types::Slot; +use warp::http::Response; +use warp::{Filter, Rejection}; + +// GET validator/execution_payload_envelope/{slot}/{builder_index} +pub fn get_validator_execution_payload_envelope( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid builder_index".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. + _builder_index: u64, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Execution payload envelope request from HTTP API"); + + not_synced_filter?; + + // Get the envelope from the pending cache (local building only) + let envelope = chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "Execution payload envelope not available for slot {slot}" + )) + })?; + + let fork_name = chain.spec.fork_name_at_slot::(slot); + + match accept_header { + Some(Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body(envelope.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build SSZ response: {e}" + )) + }), + _ => { + let json_response = GenericResponse { data: envelope }; + Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body( + serde_json::to_string(&json_response) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to serialize response: {e}" + )) + })? + .into(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build JSON response: {e}" + )) + }) + } + } + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 612b1fafec..90cca33018 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -21,7 +21,6 @@ use eth2::types::{ use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; use slot_clock::SlotClock; -use ssz::Encode; use std::sync::Arc; use tokio::sync::mpsc::{Sender, UnboundedSender}; use tokio::sync::oneshot; @@ -31,10 +30,12 @@ use types::{ SignedContributionAndProof, SignedValidatorRegistrationData, Slot, SyncContributionData, ValidatorSubscription, }; -use warp::http::Response; use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; +pub mod execution_payload_bid; +pub mod execution_payload_envelope; + /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator /// index and then ensures that the validator exists in the given `state`. pub fn pubkey_to_validator_index( @@ -335,141 +336,6 @@ pub fn get_validator_blocks( .boxed() } -// GET validator/execution_payload_bid/ -#[allow(dead_code)] -pub fn get_validator_execution_payload_bid( - eth_v1: EthV1Filter, - chain_filter: ChainFilter, - not_while_syncing_filter: NotWhileSyncingFilter, - task_spawner_filter: TaskSpawnerFilter, -) -> ResponseFilter { - eth_v1 - .and(warp::path("validator")) - .and(warp::path("execution_payload_bid")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid slot".to_string(), - )) - })) - .and(warp::path::end()) - .and(warp::header::optional::("accept")) - .and(not_while_syncing_filter) - .and(task_spawner_filter) - .and(chain_filter) - .then( - |slot: Slot, - _accept_header: Option, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - _chain: Arc>| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!( - ?slot, - "Execution paylaod bid production request from HTTP API" - ); - - not_synced_filter?; - - todo!() - }) - }, - ) - .boxed() -} - -// GET validator/execution_payload_envelope/{slot}/{builder_index} -pub fn get_validator_execution_payload_envelope( - eth_v1: EthV1Filter, - chain_filter: ChainFilter, - not_while_syncing_filter: NotWhileSyncingFilter, - task_spawner_filter: TaskSpawnerFilter, -) -> ResponseFilter { - eth_v1 - .and(warp::path("validator")) - .and(warp::path("execution_payload_envelope")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid slot".to_string(), - )) - })) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid builder_index".to_string(), - )) - })) - .and(warp::path::end()) - .and(warp::header::optional::("accept")) - .and(not_while_syncing_filter) - .and(task_spawner_filter) - .and(chain_filter) - .then( - |slot: Slot, - // TODO(gloas) we're only doing local building - // we'll need to implement builder index logic - // eventually. - _builder_index: u64, - accept_header: Option, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - chain: Arc>| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!(?slot, "Execution payload envelope request from HTTP API"); - - not_synced_filter?; - - // Get the envelope from the pending cache (local building only) - let envelope = chain - .pending_payload_envelopes - .read() - .get(slot) - .cloned() - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "Execution payload envelope not available for slot {slot}" - )) - })?; - - let fork_name = chain.spec.fork_name_at_slot::(slot); - - match accept_header { - Some(Accept::Ssz) => Response::builder() - .status(200) - .header("Content-Type", "application/octet-stream") - .header("Eth-Consensus-Version", fork_name.to_string()) - .body(envelope.as_ssz_bytes().into()) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "Failed to build SSZ response: {e}" - )) - }), - _ => { - let json_response = GenericResponse { data: envelope }; - Response::builder() - .status(200) - .header("Content-Type", "application/json") - .header("Eth-Consensus-Version", fork_name.to_string()) - .body( - serde_json::to_string(&json_response) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "Failed to serialize response: {e}" - )) - })? - .into(), - ) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "Failed to build JSON response: {e}" - )) - }) - } - } - }) - }, - ) - .boxed() -} - // POST validator/liveness/{epoch} pub fn post_validator_liveness_epoch( eth_v1: EthV1Filter, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 853afe9ece..af29df42d0 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1718,7 +1718,7 @@ pub type JsonProduceBlockV3Response = pub enum FullBlockContents { /// This is a full deneb variant with block and blobs. BlockContents(BlockContents), - /// This variant is for all pre-deneb full blocks. + /// This variant is for all pre-deneb full blocks or post-gloas beacon block. Block(BeaconBlock), } From 62f9648d5c5b6589486664823164acede170bc6e Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Wed, 4 Feb 2026 18:47:40 -0800 Subject: [PATCH 18/37] FMT --- beacon_node/http_api/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 9e09ef65b7..db15884ad3 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -41,9 +41,7 @@ use crate::beacon::execution_payload_envelope::{ }; use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; -use crate::utils::{ - AnyVersionFilter, EthV1Filter, -}; +use crate::utils::{AnyVersionFilter, EthV1Filter}; use crate::validator::execution_payload_bid::get_validator_execution_payload_bid; use crate::validator::post_validator_liveness_epoch; use crate::validator::*; From 0a098f27df5d35db35297bb76b4d85aa8d6c0b13 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 19:33:12 -0800 Subject: [PATCH 19/37] Remove unecessary fields --- validator_client/validator_services/src/block_service.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 2e20ec736e..1b97e2342f 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -636,7 +636,7 @@ impl BlockService { /// and fetch/publish the envelope from that same node. The envelope is cached per-BN, /// so fetching from a different BN than the one that built the block will fail. /// See: https://github.com/sigp/lighthouse/pull/8313 - #[instrument(skip_all, fields(%slot, ?validator_pubkey))] + #[instrument(skip_all)] async fn fetch_sign_and_publish_payload_envelope( &self, proposer_fallback: ProposerFallback, From a311b1a482aa95c284eb6ddf6261479ed05a1b87 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 19:34:40 -0800 Subject: [PATCH 20/37] Use local builder index const --- validator_client/validator_services/src/block_service.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 1b97e2342f..649f0e09cc 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -5,6 +5,7 @@ use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; use slot_clock::SlotClock; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use std::fmt::Debug; use std::future::Future; use std::ops::Deref; @@ -645,11 +646,11 @@ impl BlockService { ) -> Result<(), BlockError> { info!(slot = slot.as_u64(), "Fetching execution payload envelope"); - // Fetch the envelope from the beacon node (builder_index=0 for local building) + // Fetch the envelope from the beacon node. Use builder_index=BUILDER_INDEX_SELF_BUILD for local building. let envelope = proposer_fallback .request_proposers_last(|beacon_node| async move { beacon_node - .get_validator_execution_payload_envelope::(slot, 0) + .get_validator_execution_payload_envelope::(slot, BUILDER_INDEX_SELF_BUILD) .await .map(|response| response.data) .map_err(|e| { From 081efc7940f4eeac0d3dc9a1ec8f555db80b96e3 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:42:47 -0800 Subject: [PATCH 21/37] Fix sig domain --- validator_client/lighthouse_validator_store/src/lib.rs | 6 +++++- validator_client/signing_method/src/web3signer.rs | 1 + validator_client/validator_services/src/block_service.rs | 7 +++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index c3c61751d0..5820dd89e6 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -1250,7 +1250,11 @@ impl ValidatorStore for LighthouseValidatorS validator_pubkey: PublicKeyBytes, envelope: ExecutionPayloadEnvelope, ) -> Result, Error> { - let domain_hash = self.spec.get_builder_domain(); + let signing_context = self.signing_context( + Domain::BeaconBuilder, + envelope.slot.epoch(E::slots_per_epoch()), + ); + let domain_hash = signing_context.domain_hash(&self.spec); let signing_root = envelope.signing_root(domain_hash); // Execution payload envelope signing is not slashable, bypass doppelganger protection. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 7bf953aaeb..e6fc8f3ba2 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,7 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + // TODO(gloas) verify w/ web3signer specs ExecutionPayloadEnvelope, } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 649f0e09cc..ec6391b7d4 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -5,7 +5,6 @@ use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; use slot_clock::SlotClock; -use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use std::fmt::Debug; use std::future::Future; use std::ops::Deref; @@ -14,6 +13,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; @@ -650,7 +650,10 @@ impl BlockService { let envelope = proposer_fallback .request_proposers_last(|beacon_node| async move { beacon_node - .get_validator_execution_payload_envelope::(slot, BUILDER_INDEX_SELF_BUILD) + .get_validator_execution_payload_envelope::( + slot, + BUILDER_INDEX_SELF_BUILD, + ) .await .map(|response| response.data) .map_err(|e| { From 08c46533125a772f185ec963da3666a00c9f3b7f Mon Sep 17 00:00:00 2001 From: Eitan Seri-Levi Date: Mon, 9 Feb 2026 20:43:27 -0800 Subject: [PATCH 22/37] Update beacon_node/http_api/src/validator/execution_payload_bid.rs Co-authored-by: Jimmy Chen --- beacon_node/http_api/src/validator/execution_payload_bid.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/http_api/src/validator/execution_payload_bid.rs b/beacon_node/http_api/src/validator/execution_payload_bid.rs index 8e1235d0b0..c1353c22b0 100644 --- a/beacon_node/http_api/src/validator/execution_payload_bid.rs +++ b/beacon_node/http_api/src/validator/execution_payload_bid.rs @@ -39,7 +39,7 @@ pub fn get_validator_execution_payload_bid( task_spawner.spawn_async_with_rejection(Priority::P0, async move { debug!( ?slot, - "Execution paylaod bid production request from HTTP API" + "Execution payload bid production request from HTTP API" ); not_synced_filter?; From 8a53da9906caa865dfb4bbe5fb6e1b5fa86e3438 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:46:24 -0800 Subject: [PATCH 23/37] Add unexpected error variant --- beacon_node/beacon_chain/src/block_production/gloas.rs | 5 +---- beacon_node/beacon_chain/src/errors.rs | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 9b6285bbac..6312885af2 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -692,11 +692,8 @@ impl BeaconChain { )); } } - // TODO(gloas) we should never receive a blinded response. - // Should return some type of `Unexpected` error variant as this should never happen - // in the V4 block production flow BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::GloasNotImplemented); + return Err(BlockProductionError::Unexpected("Should never produce a blinded block post-Gloas".to_owned())); } }; diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index bd6c13e364..36bc4d7a75 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -321,6 +321,7 @@ pub enum BlockProductionError { SszTypesError(ssz_types::Error), // TODO(gloas): Remove this once Gloas is implemented GloasNotImplemented, + Unexpected(String), } easy_from_to!(BlockProcessingError, BlockProductionError); From 525bed709f6e39eb0a5c8346240de1e08e56d2ea Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:52:21 -0800 Subject: [PATCH 24/37] add consts --- beacon_node/beacon_chain/src/block_production/gloas.rs | 3 ++- consensus/types/src/core/consts.rs | 1 + validator_client/validator_services/src/block_service.rs | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 6312885af2..52f01cbe75 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -15,6 +15,7 @@ use state_processing::{ use state_processing::{VerifyOperation, state_advance::complete_state_advance}; use tracing::{Span, debug, debug_span, error, instrument, trace, warn}; use tree_hash::TreeHash; +use types::consts::gloas::{BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD}; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BlockProductionVersion, @@ -147,7 +148,7 @@ impl BeaconChain { // We'll need to build out trustless/trusted bid paths. let (execution_payload_bid, state, payload_data) = self .clone() - .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, 0, u64::MAX) + .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD) .await?; // Part 3/3 (blocking) diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index 0d4c0591cb..c1e9b695fa 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -28,6 +28,7 @@ pub mod deneb { pub mod gloas { pub const BUILDER_INDEX_SELF_BUILD: u64 = u64::MAX; pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; + pub const BID_VALUE_SELF_BUILD: u64 = 0; // Fork choice constants pub type PayloadStatus = u8; diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index ec6391b7d4..b95c0fa2ec 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -620,7 +620,9 @@ impl BlockService { ) .await?; - // For Gloas, fetch the execution payload envelope, sign it, and publish it + // TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case. + // Right now we always default to local building. Once we implement trustless/trusted builder logic + // we should check the bid for index == BUILDER_INDEX_SELF_BUILD if fork_name.gloas_enabled() { self_ref .fetch_sign_and_publish_payload_envelope(proposer_fallback, slot, &validator_pubkey) @@ -634,7 +636,7 @@ impl BlockService { /// This should be called after the block has been published. /// /// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block - /// and fetch/publish the envelope from that same node. The envelope is cached per-BN, + /// and fetch the envelope from that same node. The envelope is cached per-BN, /// so fetching from a different BN than the one that built the block will fail. /// See: https://github.com/sigp/lighthouse/pull/8313 #[instrument(skip_all)] From ed5cc3b272a08c249cbb5a49ba118ac6bbd42b84 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:55:39 -0800 Subject: [PATCH 25/37] add const --- .../src/block_production/gloas.rs | 18 +++++++-- .../src/pending_payload_envelopes.rs | 1 + consensus/types/src/core/consts.rs | 1 + .../execution/execution_payload_envelope.rs | 40 ------------------- 4 files changed, 16 insertions(+), 44 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 52f01cbe75..615a74b233 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -15,7 +15,7 @@ use state_processing::{ use state_processing::{VerifyOperation, state_advance::complete_state_advance}; use tracing::{Span, debug, debug_span, error, instrument, trace, warn}; use tree_hash::TreeHash; -use types::consts::gloas::{BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD}; +use types::consts::gloas::{BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD, EXECUTION_PAYMENT_TRUSTLESS_BUILD}; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BlockProductionVersion, @@ -148,7 +148,13 @@ impl BeaconChain { // We'll need to build out trustless/trusted bid paths. let (execution_payload_bid, state, payload_data) = self .clone() - .produce_execution_payload_bid(state, state_root_opt, produce_at_slot, BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD) + .produce_execution_payload_bid( + state, + state_root_opt, + produce_at_slot, + BID_VALUE_SELF_BUILD, + BUILDER_INDEX_SELF_BUILD, + ) .await?; // Part 3/3 (blocking) @@ -694,7 +700,9 @@ impl BeaconChain { } } BlockProposalContentsType::Blinded(_) => { - return Err(BlockProductionError::Unexpected("Should never produce a blinded block post-Gloas".to_owned())); + return Err(BlockProductionError::Unexpected( + "Should never produce a blinded block post-Gloas".to_owned(), + )); } }; @@ -706,6 +714,8 @@ impl BeaconChain { .map_err(|_| BlockProductionError::GloasNotImplemented)? .to_owned(); + // TODO(gloas) since we are defaulting to local building, execution payment is 0 + // execution payment should only be set to > 0 for trusted building. let bid = ExecutionPayloadBid:: { parent_block_hash: state.latest_block_hash()?.to_owned(), parent_block_root: state.get_latest_block_root(state_root), @@ -716,7 +726,7 @@ impl BeaconChain { builder_index, slot: produce_at_slot, value: bid_value, - execution_payment: 0, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, blob_kzg_commitments, }; diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs index 80e71b1178..336ab5323f 100644 --- a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -61,6 +61,7 @@ impl PendingPayloadEnvelopes { /// Prune envelopes older than `current_slot - max_slot_age`. /// /// This removes stale envelopes from blocks that were never published. + // TODO(gloas) implement pruning pub fn prune(&mut self, current_slot: Slot) { let min_slot = current_slot.saturating_sub(self.max_slot_age); self.envelopes.retain(|slot, _| *slot >= min_slot); diff --git a/consensus/types/src/core/consts.rs b/consensus/types/src/core/consts.rs index c1e9b695fa..0e131f26ff 100644 --- a/consensus/types/src/core/consts.rs +++ b/consensus/types/src/core/consts.rs @@ -29,6 +29,7 @@ pub mod gloas { pub const BUILDER_INDEX_SELF_BUILD: u64 = u64::MAX; pub const BUILDER_INDEX_FLAG: u64 = 1 << 40; pub const BID_VALUE_SELF_BUILD: u64 = 0; + pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; // Fork choice constants pub type PayloadStatus = u8; diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 978a35dfb9..598a6ef525 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -33,44 +33,4 @@ mod tests { use rand_xorshift::XorShiftRng; ssz_and_tree_hash_tests!(ExecutionPayloadEnvelope); - - #[test] - fn signing_root_is_deterministic() { - let mut rng = XorShiftRng::from_seed([0x42; 16]); - let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); - let domain = Hash256::random_for_test(&mut rng); - - let signing_root_1 = envelope.signing_root(domain); - let signing_root_2 = envelope.signing_root(domain); - - assert_eq!(signing_root_1, signing_root_2); - } - - #[test] - fn signing_root_changes_with_domain() { - let mut rng = XorShiftRng::from_seed([0x42; 16]); - let envelope = ExecutionPayloadEnvelope::::random_for_test(&mut rng); - let domain_1 = Hash256::random_for_test(&mut rng); - let domain_2 = Hash256::random_for_test(&mut rng); - - let signing_root_1 = envelope.signing_root(domain_1); - let signing_root_2 = envelope.signing_root(domain_2); - - assert_ne!(signing_root_1, signing_root_2); - } - - #[test] - fn signing_root_changes_with_envelope_data() { - let mut rng = XorShiftRng::from_seed([0x42; 16]); - let envelope_1 = ExecutionPayloadEnvelope::::random_for_test(&mut rng); - let mut envelope_2 = envelope_1.clone(); - envelope_2.beacon_block_root = Hash256::random_for_test(&mut rng); - - let domain = Hash256::random_for_test(&mut rng); - - let signing_root_1 = envelope_1.signing_root(domain); - let signing_root_2 = envelope_2.signing_root(domain); - - assert_ne!(signing_root_1, signing_root_2); - } } From 3eb4db20226dbe2029a64f8d3a2d61c56d5e3089 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:56:47 -0800 Subject: [PATCH 26/37] Instrument --- beacon_node/beacon_chain/src/block_production/gloas.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 615a74b233..22273d90f9 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -15,7 +15,9 @@ use state_processing::{ use state_processing::{VerifyOperation, state_advance::complete_state_advance}; use tracing::{Span, debug, debug_span, error, instrument, trace, warn}; use tree_hash::TreeHash; -use types::consts::gloas::{BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD, EXECUTION_PAYMENT_TRUSTLESS_BUILD}; +use types::consts::gloas::{ + BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD, EXECUTION_PAYMENT_TRUSTLESS_BUILD, +}; use types::{ Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BlockProductionVersion, @@ -103,6 +105,7 @@ impl BeaconChain { } // TODO(gloas) need to implement builder boost factor logic + #[instrument(level = "debug", skip_all)] pub async fn produce_block_on_state_gloas( self: &Arc, state: BeaconState, From 850dea6e54b3a8d0e9011782843a2f2477280af0 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 20:57:08 -0800 Subject: [PATCH 27/37] Remove unused --- beacon_node/execution_layer/src/lib.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 63402f6fa7..33b83aab09 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -334,14 +334,6 @@ impl> BlockProposalContents block_value, } } - pub fn blob_kzg_commitments(&self) -> Option<&KzgCommitments> { - match self { - Self::Payload { .. } => None, - Self::PayloadAndBlobs { - kzg_commitments, .. - } => Some(kzg_commitments), - } - } } // This just groups together a bunch of parameters that commonly From fea43fb0c860ed2c32fff75ad6bd6f000839f1c2 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:05:25 -0800 Subject: [PATCH 28/37] Move block production specific stuff to block_production module --- beacon_node/beacon_chain/src/beacon_chain.rs | 214 +---------------- .../beacon_chain/src/block_production/mod.rs | 222 ++++++++++++++++++ 2 files changed, 227 insertions(+), 209 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4f9e3b950e..8685346c1a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4508,55 +4508,6 @@ impl BeaconChain { Ok(()) } - /// If configured, wait for the fork choice run at the start of the slot to complete. - #[instrument(level = "debug", skip_all)] - fn wait_for_fork_choice_before_block_production( - self: &Arc, - slot: Slot, - ) -> Result<(), BlockProductionError> { - if let Some(rx) = &self.fork_choice_signal_rx { - let current_slot = self - .slot() - .map_err(|_| BlockProductionError::UnableToReadSlot)?; - - let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); - - if slot == current_slot || slot == current_slot + 1 { - match rx.wait_for_fork_choice(slot, timeout) { - ForkChoiceWaitResult::Success(fc_slot) => { - debug!( - %slot, - fork_choice_slot = %fc_slot, - "Fork choice successfully updated before block production" - ); - } - ForkChoiceWaitResult::Behind(fc_slot) => { - warn!( - fork_choice_slot = %fc_slot, - %slot, - message = "this block may be orphaned", - "Fork choice notifier out of sync with block production" - ); - } - ForkChoiceWaitResult::TimeOut => { - warn!( - message = "this block may be orphaned", - "Timed out waiting for fork choice before proposal" - ); - } - } - } else { - error!( - %slot, - %current_slot, - message = "check clock sync, this block may be orphaned", - "Producing block at incorrect slot" - ); - } - } - Ok(()) - } - pub async fn produce_block_with_verification( self: &Arc, randao_reveal: Signature, @@ -4603,165 +4554,6 @@ impl BeaconChain { .await } - /// Load a beacon state from the database for block production. This is a long-running process - /// that should not be performed in an `async` context. - pub fn load_state_for_block_production( - self: &Arc, - slot: Slot, - ) -> Result<(BeaconState, Option), BlockProductionError> { - let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); - self.wait_for_fork_choice_before_block_production(slot)?; - drop(fork_choice_timer); - - let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); - - // Atomically read some values from the head whilst avoiding holding cached head `Arc` any - // longer than necessary. - let (head_slot, head_block_root, head_state_root) = { - let head = self.canonical_head.cached_head(); - ( - head.head_slot(), - head.head_block_root(), - head.head_state_root(), - ) - }; - let (state, state_root_opt) = if head_slot < slot { - // Attempt an aggressive re-org if configured and the conditions are right. - if let Some((re_org_state, re_org_state_root)) = - self.get_state_for_re_org(slot, head_slot, head_block_root) - { - info!( - %slot, - head_to_reorg = %head_block_root, - "Proposing block to re-org current head" - ); - (re_org_state, Some(re_org_state_root)) - } else { - // Fetch the head state advanced through to `slot`, which should be present in the - // state cache thanks to the state advance timer. - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, slot, head_state_root) - .map_err(BlockProductionError::FailedToLoadState)? - .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; - (state, Some(state_root)) - } - } else { - warn!( - message = "this block is more likely to be orphaned", - %slot, - "Producing block that conflicts with head" - ); - let state = self - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; - - (state, None) - }; - - drop(state_load_timer); - - Ok((state, state_root_opt)) - } - - /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. - /// - /// This function will return `None` if proposer re-orgs are disabled. - #[instrument(skip_all, level = "debug")] - fn get_state_for_re_org( - &self, - slot: Slot, - head_slot: Slot, - canonical_head: Hash256, - ) -> Option<(BeaconState, Hash256)> { - let re_org_head_threshold = self.config.re_org_head_threshold?; - let re_org_parent_threshold = self.config.re_org_parent_threshold?; - - if self.spec.proposer_score_boost.is_none() { - warn!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - - let slot_delay = self - .slot_clock - .seconds_from_current_slot_start() - .or_else(|| { - warn!(error = "unable to read slot clock", "Not attempting re-org"); - None - })?; - - // Attempt a proposer re-org if: - // - // 1. It seems we have time to propagate and still receive the proposer boost. - // 2. The current head block was seen late. - // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = - slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); - if !proposing_on_time { - debug!(reason = "not proposing on time", "Not attempting re-org"); - return None; - } - - let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); - if !head_late { - debug!(reason = "head not late", "Not attempting re-org"); - return None; - } - - // Is the current head weak and appropriate for re-orging? - let proposer_head_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); - let proposer_head = self - .canonical_head - .fork_choice_read_lock() - .get_proposer_head( - slot, - canonical_head, - re_org_head_threshold, - re_org_parent_threshold, - &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, - ) - .map_err(|e| match e { - ProposerHeadError::DoNotReOrg(reason) => { - debug!( - %reason, - "Not attempting re-org" - ); - } - ProposerHeadError::Error(e) => { - warn!( - error = ?e, - "Not attempting re-org" - ); - } - }) - .ok()?; - drop(proposer_head_timer); - let re_org_parent_block = proposer_head.parent_node.root; - - let (state_root, state) = self - .store - .get_advanced_hot_state_from_cache(re_org_parent_block, slot) - .or_else(|| { - warn!(reason = "no state in cache", "Not attempting re-org"); - None - })?; - - info!( - weak_head = ?canonical_head, - parent = ?re_org_parent_block, - head_weight = proposer_head.head_node.weight, - threshold_weight = proposer_head.re_org_head_weight_threshold, - "Attempting re-org due to weak head" - ); - - Some((state, state_root)) - } - /// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`. /// /// The `proposer_head` may be the head block of `cached_head` or its parent. An error will @@ -5097,7 +4889,11 @@ impl BeaconChain { } /// Check if the block with `block_root` was observed after the attestation deadline of `slot`. - fn block_observed_after_attestation_deadline(&self, block_root: Hash256, slot: Slot) -> bool { + pub fn block_observed_after_attestation_deadline( + &self, + block_root: Hash256, + slot: Slot, + ) -> bool { let block_delays = self.block_times_cache.read().get_block_delays( block_root, self.slot_clock diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index 37b62a181b..76c8b77e93 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -1 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use proto_array::ProposerHeadError; +use slot_clock::SlotClock; +use tracing::{debug, error, info, instrument, warn}; +use types::{BeaconState, Hash256, Slot}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, + fork_choice_signal::ForkChoiceWaitResult, metrics, +}; + mod gloas; + +impl BeaconChain { + /// Load a beacon state from the database for block production. This is a long-running process + /// that should not be performed in an `async` context. + pub(crate) fn load_state_for_block_production( + self: &Arc, + slot: Slot, + ) -> Result<(BeaconState, Option), BlockProductionError> { + let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); + self.wait_for_fork_choice_before_block_production(slot)?; + drop(fork_choice_timer); + + let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); + + // Atomically read some values from the head whilst avoiding holding cached head `Arc` any + // longer than necessary. + let (head_slot, head_block_root, head_state_root) = { + let head = self.canonical_head.cached_head(); + ( + head.head_slot(), + head.head_block_root(), + head.head_state_root(), + ) + }; + let (state, state_root_opt) = if head_slot < slot { + // Attempt an aggressive re-org if configured and the conditions are right. + if let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) + { + info!( + %slot, + head_to_reorg = %head_block_root, + "Proposing block to re-org current head" + ); + (re_org_state, Some(re_org_state_root)) + } else { + // Fetch the head state advanced through to `slot`, which should be present in the + // state cache thanks to the state advance timer. + let (state_root, state) = self + .store + .get_advanced_hot_state(head_block_root, slot, head_state_root) + .map_err(BlockProductionError::FailedToLoadState)? + .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; + (state, Some(state_root)) + } + } else { + warn!( + message = "this block is more likely to be orphaned", + %slot, + "Producing block that conflicts with head" + ); + let state = self + .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) + .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; + + (state, None) + }; + + drop(state_load_timer); + + Ok((state, state_root_opt)) + } + + /// If configured, wait for the fork choice run at the start of the slot to complete. + #[instrument(level = "debug", skip_all)] + fn wait_for_fork_choice_before_block_production( + self: &Arc, + slot: Slot, + ) -> Result<(), BlockProductionError> { + if let Some(rx) = &self.fork_choice_signal_rx { + let current_slot = self + .slot() + .map_err(|_| BlockProductionError::UnableToReadSlot)?; + + let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); + + if slot == current_slot || slot == current_slot + 1 { + match rx.wait_for_fork_choice(slot, timeout) { + ForkChoiceWaitResult::Success(fc_slot) => { + debug!( + %slot, + fork_choice_slot = %fc_slot, + "Fork choice successfully updated before block production" + ); + } + ForkChoiceWaitResult::Behind(fc_slot) => { + warn!( + fork_choice_slot = %fc_slot, + %slot, + message = "this block may be orphaned", + "Fork choice notifier out of sync with block production" + ); + } + ForkChoiceWaitResult::TimeOut => { + warn!( + message = "this block may be orphaned", + "Timed out waiting for fork choice before proposal" + ); + } + } + } else { + error!( + %slot, + %current_slot, + message = "check clock sync, this block may be orphaned", + "Producing block at incorrect slot" + ); + } + } + Ok(()) + } + + /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. + /// + /// This function will return `None` if proposer re-orgs are disabled. + #[instrument(skip_all, level = "debug")] + fn get_state_for_re_org( + &self, + slot: Slot, + head_slot: Slot, + canonical_head: Hash256, + ) -> Option<(BeaconState, Hash256)> { + let re_org_head_threshold = self.config.re_org_head_threshold?; + let re_org_parent_threshold = self.config.re_org_parent_threshold?; + + if self.spec.proposer_score_boost.is_none() { + warn!( + reason = "this network does not have proposer boost enabled", + "Ignoring proposer re-org configuration" + ); + return None; + } + + let slot_delay = self + .slot_clock + .seconds_from_current_slot_start() + .or_else(|| { + warn!(error = "unable to read slot clock", "Not attempting re-org"); + None + })?; + + // Attempt a proposer re-org if: + // + // 1. It seems we have time to propagate and still receive the proposer boost. + // 2. The current head block was seen late. + // 3. The `get_proposer_head` conditions from fork choice pass. + let proposing_on_time = + slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); + if !proposing_on_time { + debug!(reason = "not proposing on time", "Not attempting re-org"); + return None; + } + + let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); + if !head_late { + debug!(reason = "head not late", "Not attempting re-org"); + return None; + } + + // Is the current head weak and appropriate for re-orging? + let proposer_head_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); + let proposer_head = self + .canonical_head + .fork_choice_read_lock() + .get_proposer_head( + slot, + canonical_head, + re_org_head_threshold, + re_org_parent_threshold, + &self.config.re_org_disallowed_offsets, + self.config.re_org_max_epochs_since_finalization, + ) + .map_err(|e| match e { + ProposerHeadError::DoNotReOrg(reason) => { + debug!( + %reason, + "Not attempting re-org" + ); + } + ProposerHeadError::Error(e) => { + warn!( + error = ?e, + "Not attempting re-org" + ); + } + }) + .ok()?; + drop(proposer_head_timer); + let re_org_parent_block = proposer_head.parent_node.root; + + let (state_root, state) = self + .store + .get_advanced_hot_state_from_cache(re_org_parent_block, slot) + .or_else(|| { + warn!(reason = "no state in cache", "Not attempting re-org"); + None + })?; + + info!( + weak_head = ?canonical_head, + parent = ?re_org_parent_block, + head_weight = proposer_head.head_node.weight, + threshold_weight = proposer_head.re_org_head_weight_threshold, + "Attempting re-org due to weak head" + ); + + Some((state, state_root)) + } +} From 8eb409a73ab5eda3b09c8d6d163c0fdbac99e302 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:34:00 -0800 Subject: [PATCH 29/37] linting --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/beacon_chain/src/block_production/gloas.rs | 6 ++++-- consensus/types/src/execution/execution_payload_envelope.rs | 3 --- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 8685346c1a..2d5572db53 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -31,7 +31,7 @@ use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; -use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; +use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 22273d90f9..4481903671 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -59,6 +59,8 @@ pub struct ExecutionPayloadData { pub slot: Slot, pub state_root: Hash256, } +type ConsensusBlockValue = u64; +type BlockProductionResult = (BeaconBlock>, ConsensusBlockValue); impl BeaconChain { pub async fn produce_block_with_verification_gloas( @@ -68,7 +70,7 @@ impl BeaconChain { graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, _builder_boost_factor: Option, - ) -> Result<(BeaconBlock>, u64), BlockProductionError> { + ) -> Result, BlockProductionError> { metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); // Part 1/2 (blocking) @@ -114,7 +116,7 @@ impl BeaconChain { randao_reveal: Signature, graffiti_settings: GraffitiSettings, verification: ProduceBlockVerification, - ) -> Result<(BeaconBlock>, u64), BlockProductionError> { + ) -> Result, BlockProductionError> { // Part 1/3 (blocking) // // Perform the state advance and block-packing functions. diff --git a/consensus/types/src/execution/execution_payload_envelope.rs b/consensus/types/src/execution/execution_payload_envelope.rs index 598a6ef525..7f68dae037 100644 --- a/consensus/types/src/execution/execution_payload_envelope.rs +++ b/consensus/types/src/execution/execution_payload_envelope.rs @@ -28,9 +28,6 @@ impl SignedRoot for ExecutionPayloadEnvelope {} mod tests { use super::*; use crate::MainnetEthSpec; - use crate::test_utils::TestRandom; - use rand::SeedableRng; - use rand_xorshift::XorShiftRng; ssz_and_tree_hash_tests!(ExecutionPayloadEnvelope); } From 34b4c46e7585d2dffbbfa59bf92d89a79e0cdd25 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:37:22 -0800 Subject: [PATCH 30/37] Reorder --- beacon_node/beacon_chain/src/block_production/gloas.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 4481903671..a28a3c5693 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -33,6 +33,9 @@ use crate::{ ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics, }; +type ConsensusBlockValue = u64; +type BlockProductionResult = (BeaconBlock>, ConsensusBlockValue); + pub struct PartialBeaconBlock { slot: Slot, proposer_index: u64, @@ -59,8 +62,6 @@ pub struct ExecutionPayloadData { pub slot: Slot, pub state_root: Hash256, } -type ConsensusBlockValue = u64; -type BlockProductionResult = (BeaconBlock>, ConsensusBlockValue); impl BeaconChain { pub async fn produce_block_with_verification_gloas( From aa0795238ee04c7be4cb8e5c54de4498e52b2e7a Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:41:26 -0800 Subject: [PATCH 31/37] Add comment --- beacon_node/http_api/src/beacon/execution_payload_envelope.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 13495d4d6d..18d2e35fa4 100644 --- a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -99,10 +99,12 @@ pub async fn publish_execution_payload_envelope( ))); } - // TODO(gloas): Add more validation: + // TODO(gloas): Do we want to add more validation like: // - Verify the signature // - Check builder_index is valid // - Verify the envelope references a known block + // + // If we do, then we must post the signed execution payload envelope to the BN that originally produced it. info!( %slot, From 9917e22d6348ba355da9c5a7cc146d06e7ace1b7 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:43:53 -0800 Subject: [PATCH 32/37] Revert --- testing/ef_tests/download_test_vectors.sh | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/testing/ef_tests/download_test_vectors.sh b/testing/ef_tests/download_test_vectors.sh index f3cef96dde..21f74e817f 100755 --- a/testing/ef_tests/download_test_vectors.sh +++ b/testing/ef_tests/download_test_vectors.sh @@ -4,7 +4,7 @@ set -Eeuo pipefail TESTS=("general" "minimal" "mainnet") version=${1} -if [[ "$version" == "nightly" || "$version" == nightly-* ]]; then +if [[ "$version" == "nightly" ]]; then if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "Error GITHUB_TOKEN is not set" exit 1 @@ -21,22 +21,17 @@ if [[ "$version" == "nightly" || "$version" == nightly-* ]]; then api="https://api.github.com" auth_header="Authorization: token ${GITHUB_TOKEN}" - if [[ "$version" == nightly-* ]]; then - # Extract run_id from nightly- format - run_id="${version#nightly-}" - else - run_id=$(curl -v -H "${auth_header}" \ - "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | - jq -r '.workflow_runs[0].id') + run_id=$(curl -s -H "${auth_header}" \ + "${api}/repos/${repo}/actions/workflows/generate_vectors.yml/runs?branch=dev&status=success&per_page=1" | + jq -r '.workflow_runs[0].id') - if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then - echo "No successful nightly workflow run found" - exit 1 - fi + if [[ "${run_id}" == "null" || -z "${run_id}" ]]; then + echo "No successful nightly workflow run found" + exit 1 fi echo "Downloading nightly test vectors for run: ${run_id}" - curl -v -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | + curl -s -H "${auth_header}" "${api}/repos/${repo}/actions/runs/${run_id}/artifacts" | jq -c '.artifacts[] | {name, url: .archive_download_url}' | while read -r artifact; do name=$(echo "${artifact}" | jq -r .name) From b16da1414a97da475c43dee4fb77644d3e09360b Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Mon, 9 Feb 2026 21:55:00 -0800 Subject: [PATCH 33/37] Fix test --- beacon_node/http_api/tests/tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index df943b0e6c..07aa10fed5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -48,7 +48,7 @@ use types::ApplicationDomain; use types::{ Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedExecutionPayloadEnvelope, SignedRoot, SingleAttestation, Slot, - attestation::AttestationBase, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3818,7 +3818,7 @@ impl ApiTester { // Fetch the envelope via the HTTP API let envelope_response = self .client - .get_validator_execution_payload_envelope::(slot, 0) + .get_validator_execution_payload_envelope::(slot, BUILDER_INDEX_SELF_BUILD) .await .unwrap(); let envelope = envelope_response.data; @@ -3826,7 +3826,7 @@ impl ApiTester { // Verify envelope fields assert_eq!(envelope.beacon_block_root, block_root); assert_eq!(envelope.slot, slot); - assert_eq!(envelope.builder_index, u64::MAX); + assert_eq!(envelope.builder_index, BUILDER_INDEX_SELF_BUILD); assert_ne!(envelope.state_root, Hash256::ZERO); // Sign and publish the block @@ -3929,14 +3929,14 @@ impl ApiTester { // Fetch the envelope via the HTTP API (SSZ) let envelope = self .client - .get_validator_execution_payload_envelope_ssz::(slot, 0) + .get_validator_execution_payload_envelope_ssz::(slot, BUILDER_INDEX_SELF_BUILD) .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_eq!(envelope.builder_index, BUILDER_INDEX_SELF_BUILD); assert_ne!(envelope.state_root, Hash256::ZERO); // Sign and publish the block From 088e5bbb3acc2e3d8bf444aa17f1e95ce8308098 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 12:12:18 -0800 Subject: [PATCH 34/37] Remove unused endpoint --- beacon_node/http_api/src/lib.rs | 10 ---- .../src/validator/execution_payload_bid.rs | 52 ------------------- beacon_node/http_api/src/validator/mod.rs | 1 - 3 files changed, 63 deletions(-) delete mode 100644 beacon_node/http_api/src/validator/execution_payload_bid.rs diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index db15884ad3..c2a6bc1f9d 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -42,7 +42,6 @@ use crate::beacon::execution_payload_envelope::{ use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::utils::{AnyVersionFilter, EthV1Filter}; -use crate::validator::execution_payload_bid::get_validator_execution_payload_bid; use crate::validator::post_validator_liveness_epoch; use crate::validator::*; use crate::version::beacon_response; @@ -2498,14 +2497,6 @@ pub fn serve( task_spawner_filter.clone(), ); - // GET validator/execution_payload_bid/ - let get_validator_execution_payload_bid = get_validator_execution_payload_bid( - eth_v1.clone(), - chain_filter.clone(), - not_while_syncing_filter.clone(), - task_spawner_filter.clone(), - ); - // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( eth_v1.clone().clone(), @@ -3365,7 +3356,6 @@ pub fn serve( .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) .uor(get_validator_execution_payload_envelope) - .uor(get_validator_execution_payload_bid) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) diff --git a/beacon_node/http_api/src/validator/execution_payload_bid.rs b/beacon_node/http_api/src/validator/execution_payload_bid.rs deleted file mode 100644 index c1353c22b0..0000000000 --- a/beacon_node/http_api/src/validator/execution_payload_bid.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::task_spawner::{Priority, TaskSpawner}; -use crate::utils::{ - ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, -}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use eth2::types::Accept; -use std::sync::Arc; -use tracing::debug; -use types::Slot; -use warp::{Filter, Rejection}; - -// GET validator/execution_payload_bid/ -#[allow(dead_code)] -pub fn get_validator_execution_payload_bid( - eth_v1: EthV1Filter, - chain_filter: ChainFilter, - not_while_syncing_filter: NotWhileSyncingFilter, - task_spawner_filter: TaskSpawnerFilter, -) -> ResponseFilter { - eth_v1 - .and(warp::path("validator")) - .and(warp::path("execution_payload_bid")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid slot".to_string(), - )) - })) - .and(warp::path::end()) - .and(warp::header::optional::("accept")) - .and(not_while_syncing_filter) - .and(task_spawner_filter) - .and(chain_filter) - .then( - |slot: Slot, - _accept_header: Option, - not_synced_filter: Result<(), Rejection>, - task_spawner: TaskSpawner, - _chain: Arc>| { - task_spawner.spawn_async_with_rejection(Priority::P0, async move { - debug!( - ?slot, - "Execution payload bid production request from HTTP API" - ); - - not_synced_filter?; - - todo!() - }) - }, - ) - .boxed() -} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 90cca33018..c9688daf50 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -33,7 +33,6 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; -pub mod execution_payload_bid; pub mod execution_payload_envelope; /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator From 846b1ba02364425ab2e15a06be9abae614ff05f3 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 12:16:59 -0800 Subject: [PATCH 35/37] pub crate --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2d5572db53..81398ee282 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -4889,7 +4889,7 @@ impl BeaconChain { } /// Check if the block with `block_root` was observed after the attestation deadline of `slot`. - pub fn block_observed_after_attestation_deadline( + pub(crate) fn block_observed_after_attestation_deadline( &self, block_root: Hash256, slot: Slot, From ac510412908ed0621f61e15c3d699b313df7fda0 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 12:26:03 -0800 Subject: [PATCH 36/37] Fix state root --- beacon_node/beacon_chain/src/block_production/gloas.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index a28a3c5693..fedcdda671 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -156,7 +156,6 @@ impl BeaconChain { .clone() .produce_execution_payload_bid( state, - state_root_opt, produce_at_slot, BID_VALUE_SELF_BUILD, BUILDER_INDEX_SELF_BUILD, @@ -612,7 +611,7 @@ impl BeaconChain { // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless // bid building. Right now this only works for local building. /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. - /// This function assumes we've already done the state advance. + /// This function assumes we've already advanced `state`. /// /// Returns the signed bid, the state, and optionally the payload data needed to construct /// the `ExecutionPayloadEnvelope` after the beacon block is created. @@ -623,8 +622,7 @@ impl BeaconChain { #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, - state: BeaconState, - state_root_opt: Option, + mut state: BeaconState, produce_at_slot: Slot, bid_value: u64, builder_index: BuilderIndex, @@ -712,7 +710,6 @@ impl BeaconChain { } }; - let state_root = state_root_opt.ok_or_else(|| BlockProductionError::MissingStateRoot)?; // TODO(gloas) this is just a dummy error variant for now let execution_payload_gloas = execution_payload @@ -720,6 +717,8 @@ impl BeaconChain { .map_err(|_| BlockProductionError::GloasNotImplemented)? .to_owned(); + let state_root = state.update_tree_hash_cache()?; + // TODO(gloas) since we are defaulting to local building, execution payment is 0 // execution payment should only be set to > 0 for trusted building. let bid = ExecutionPayloadBid:: { From a08c1d956b0d6e9e216ae4f9a3a9cb5a0350c2c4 Mon Sep 17 00:00:00 2001 From: Eitan Seri- Levi Date: Tue, 10 Feb 2026 13:15:10 -0800 Subject: [PATCH 37/37] call process_envelope --- .../src/block_production/gloas.rs | 29 +++++++++++--- .../src/envelope_processing.rs | 38 ++++++++++++++----- testing/ef_tests/src/cases/operations.rs | 10 ++++- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index fedcdda671..306e1c549e 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -7,6 +7,7 @@ use execution_layer::{BlockProposalContentsType, BuilderParams}; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::get_attesting_indices_from_state; +use state_processing::envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::verify_attestation_for_block_inclusion; use state_processing::{ @@ -24,7 +25,8 @@ use types::{ BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionPayloadBid, ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, SignedBlsToExecutionChange, - SignedExecutionPayloadBid, SignedVoluntaryExit, Slot, SyncAggregate, + SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, Slot, + SyncAggregate, }; use crate::execution_payload::get_execution_payload; @@ -60,7 +62,6 @@ pub struct ExecutionPayloadData { pub execution_requests: ExecutionRequests, pub builder_index: BuilderIndex, pub slot: Slot, - pub state_root: Hash256, } impl BeaconChain { @@ -580,14 +581,32 @@ impl BeaconChain { builder_index: payload_data.builder_index, beacon_block_root, slot: payload_data.slot, - state_root: payload_data.state_root, + state_root: Hash256::ZERO, }; + let mut signed_envelope = SignedExecutionPayloadEnvelope { + message: execution_payload_envelope, + signature: Signature::empty(), + }; + + // TODO(gloas) add better error variant + process_execution_payload_envelope( + &mut state, + None, + &signed_envelope, + VerifySignatures::False, + VerifyStateRoot::False, + &self.spec, + ) + .map_err(|_| BlockProductionError::GloasNotImplemented)?; + + signed_envelope.message.state_root = state.update_tree_hash_cache()?; + // Cache the envelope for later retrieval by the validator for signing and publishing. let envelope_slot = payload_data.slot; self.pending_payload_envelopes .write() - .insert(envelope_slot, execution_payload_envelope); + .insert(envelope_slot, signed_envelope.message); debug!( %beacon_block_root, @@ -710,7 +729,6 @@ impl BeaconChain { } }; - // TODO(gloas) this is just a dummy error variant for now let execution_payload_gloas = execution_payload .as_gloas() @@ -741,7 +759,6 @@ impl BeaconChain { execution_requests, builder_index, slot: produce_at_slot, - state_root, }; // TODO(gloas) this is only local building diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index d46728dbbc..2076f0f836 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -20,6 +20,23 @@ macro_rules! envelope_verify { }; } +/// The strategy to be used when validating the payloads state root. +#[cfg_attr(feature = "arbitrary-fuzz", derive(Arbitrary))] +#[derive(PartialEq, Clone, Copy)] +pub enum VerifyStateRoot { + /// Validate state root. + True, + /// Do not validate state root. Use with caution. + /// This should only be used when first constructing the payload envelope. + False, +} + +impl VerifyStateRoot { + pub fn is_true(self) -> bool { + self == VerifyStateRoot::True + } +} + #[derive(Debug, Clone)] pub enum EnvelopeProcessingError { /// Bad Signature @@ -111,6 +128,7 @@ pub fn process_execution_payload_envelope( parent_state_root: Option, signed_envelope: &SignedExecutionPayloadEnvelope, verify_signatures: VerifySignatures, + verify_state_root: VerifyStateRoot, spec: &ChainSpec, ) -> Result<(), EnvelopeProcessingError> { if verify_signatures.is_true() { @@ -264,15 +282,17 @@ pub fn process_execution_payload_envelope( .map_err(EnvelopeProcessingError::BitFieldError)?; *state.latest_block_hash_mut()? = payload.block_hash; - // Verify the state root - let state_root = state.canonical_root()?; - envelope_verify!( - envelope.state_root == state_root, - EnvelopeProcessingError::InvalidStateRoot { - state: state_root, - envelope: envelope.state_root, - } - ); + if verify_state_root.is_true() { + // Verify the state root + let state_root = state.canonical_root()?; + envelope_verify!( + envelope.state_root == state_root, + EnvelopeProcessingError::InvalidStateRoot { + state: state_root, + envelope: envelope.state_root, + } + ); + } Ok(()) } diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index ef998a94ba..8605800b79 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -5,6 +5,7 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::envelope_processing::VerifyStateRoot; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ process_consolidation_requests, process_deposit_requests_post_gloas, @@ -452,7 +453,14 @@ impl Operation for SignedExecutionPayloadEnvelope { .as_ref() .is_some_and(|e| e.execution_valid); if valid { - process_execution_payload_envelope(state, None, self, VerifySignatures::True, spec) + process_execution_payload_envelope( + state, + None, + self, + VerifySignatures::True, + VerifyStateRoot::True, + spec, + ) } else { Err(EnvelopeProcessingError::ExecutionInvalid) }