From b1925ca8b8b8d78fc3b78043c2fa2a616743d045 Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Thu, 8 Oct 2026 05:31:53 +0530 Subject: [PATCH 1/2] Build NewPayloadRequest::Heze for Heze envelopes --- beacon_node/beacon_chain/src/beacon_chain.rs | 20 +++++ .../payload_notifier.rs | 39 ++++++-- beacon_node/beacon_chain/src/test_utils.rs | 31 +++++-- .../tests/envelope_verification.rs | 89 ++++++++++++++++++- .../execution_layer/src/engine_api/http.rs | 2 +- .../src/engine_api/new_payload_request.rs | 11 +-- .../src/test_utils/handle_rpc.rs | 5 +- .../execution_layer/src/test_utils/hook.rs | 14 +++ 8 files changed, 181 insertions(+), 30 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 31746fcabaf..3a8090e1af2 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -7460,6 +7460,26 @@ impl BeaconChain { .map_err(Into::into) } + /// Provides the timely inclusion list transactions for a payload built on `parent_root` at + /// `slot + 1`, falling back to an empty list if they can't be read. + pub fn payload_inclusion_list_transactions( + &self, + parent_root: Hash256, + slot: Slot, + ) -> ProgressiveTransactions { + self.get_inclusion_list_transactions(parent_root, slot, true) + .and_then(|transactions| ProgressiveTransactions::new(transactions).map_err(Into::into)) + .unwrap_or_else(|e| { + warn!( + ?parent_root, + %slot, + error = ?e, + "Failed to read inclusion lists, verifying payload without them" + ); + ProgressiveTransactions::empty() + }) + } + /// Dumps the entire canonical chain, from the head to genesis to a vector for analysis. /// /// This could be a very expensive operation and should only be done in testing/analysis diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs index ce5667fcece..40bd1e7e34a 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/payload_notifier.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas}; +use execution_layer::{NewPayloadRequest, NewPayloadRequestGloas, NewPayloadRequestHeze}; use fork_choice::PayloadVerificationStatus; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use tracing::warn; @@ -31,7 +31,8 @@ impl PayloadNotifier { match notify_execution_layer { NotifyExecutionLayer::No if chain.config.optimistic_finalized_sync => { - let new_payload_request = Self::build_new_payload_request(&envelope, &block)?; + let new_payload_request = + Self::build_new_payload_request(&chain, &envelope, &block)?; // TODO(gloas): check and test RLP block hash calculation post-Gloas if let Err(e) = new_payload_request.perform_optimistic_sync_verifications() { warn!( @@ -63,12 +64,14 @@ impl PayloadNotifier { if let Some(precomputed_status) = self.payload_verification_status { Ok(precomputed_status) } else { - let request = Self::build_new_payload_request(&self.envelope, &self.block)?; + let request = + Self::build_new_payload_request(&self.chain, &self.envelope, &self.block)?; notify_new_payload(&self.chain, self.envelope.slot(), request).await } } fn build_new_payload_request<'a>( + chain: &BeaconChain, envelope: &'a SignedExecutionPayloadEnvelope, block: &'a SignedBeaconBlock, ) -> Result, PayloadVerificationError> { @@ -85,11 +88,29 @@ impl PayloadNotifier { .map(kzg_commitment_to_versioned_hash) .collect(); - Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { - execution_payload: &envelope.message.payload, - versioned_hashes, - parent_beacon_block_root: envelope.message.parent_beacon_block_root, - execution_requests: &envelope.message.execution_requests, - })) + if chain + .spec + .fork_name_at_slot::(block.slot()) + .heze_enabled() + { + let inclusion_list_transactions = chain.payload_inclusion_list_transactions( + block.parent_root(), + block.slot().saturating_sub(1_u64), + ); + Ok(NewPayloadRequest::Heze(NewPayloadRequestHeze { + execution_payload: &envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: envelope.message.parent_beacon_block_root, + execution_requests: &envelope.message.execution_requests, + inclusion_list_transactions, + })) + } else { + Ok(NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: envelope.message.parent_beacon_block_root, + execution_requests: &envelope.message.execution_requests, + })) + } } } diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 33651b85fcc..7618669f98c 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -29,7 +29,7 @@ use bls::{ use eth2::types::{GraffitiPolicy, SignedBlockContentsTuple}; use execution_layer::test_utils::generate_genesis_header; use execution_layer::{ - ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas, + ExecutionLayer, NewPayloadRequest, NewPayloadRequestGloas, NewPayloadRequestHeze, auth::JwtKey, test_utils::{DEFAULT_JWT_SECRET, ExecutionBlockGenerator, MockBuilder, MockExecutionLayer}, }; @@ -3113,12 +3113,29 @@ where .map(kzg_commitment_to_versioned_hash) .collect(); - let request = NewPayloadRequest::Gloas(NewPayloadRequestGloas { - execution_payload: &signed_envelope.message.payload, - versioned_hashes, - parent_beacon_block_root: block.message().parent_root(), - execution_requests: &signed_envelope.message.execution_requests, - }); + let request = if self + .spec + .fork_name_at_slot::(block.slot()) + .heze_enabled() + { + NewPayloadRequest::Heze(NewPayloadRequestHeze { + execution_payload: &signed_envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: block.message().parent_root(), + execution_requests: &signed_envelope.message.execution_requests, + inclusion_list_transactions: self.chain.payload_inclusion_list_transactions( + block.parent_root(), + block.slot().saturating_sub(1_u64), + ), + }) + } else { + NewPayloadRequest::Gloas(NewPayloadRequestGloas { + execution_payload: &signed_envelope.message.payload, + versioned_hashes, + parent_beacon_block_root: block.message().parent_root(), + execution_requests: &signed_envelope.message.execution_requests, + }) + }; self.chain .execution_layer diff --git a/beacon_node/beacon_chain/tests/envelope_verification.rs b/beacon_node/beacon_chain/tests/envelope_verification.rs index 04fcdd025e4..4b5e6ac0bc3 100644 --- a/beacon_node/beacon_chain/tests/envelope_verification.rs +++ b/beacon_node/beacon_chain/tests/envelope_verification.rs @@ -4,13 +4,16 @@ use beacon_chain::payload_envelope_verification::{EnvelopeError, EnvelopeSource} use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; -use bls::PublicKeyBytes; +use bls::{PublicKeyBytes, Signature}; use eth2::types::EventKind; +use execution_layer::http::ENGINE_NEW_PAYLOAD_V6; +use parking_lot::Mutex; use proto_array::ExecutionStatus; +use ssz_types::ProgressiveVariableList; use std::sync::Arc; use types::{ - Address, BlockImportSource, Epoch, ExecPayload, ForkName, Hash256, MinimalEthSpec, Slot, - WithdrawalRequest, + Address, BlockImportSource, Epoch, EthSpec, ExecPayload, ForkName, Hash256, InclusionList, + MinimalEthSpec, ProgressiveTransactions, SignedInclusionList, Slot, WithdrawalRequest, }; type E = MinimalEthSpec; @@ -735,3 +738,83 @@ fn is_valid_and_post_bellatrix(status: ExecutionStatus) -> bool { fn is_optimistic(status: ExecutionStatus) -> bool { matches!(status, ExecutionStatus::Optimistic(_)) } + +/// A Heze payload is sent over `engine_newPayloadV6` with the timely inclusion lists from the +/// previous slot, which is in the previous epoch here. +#[tokio::test] +async fn heze_payload_is_sent_with_the_timely_inclusion_lists() { + let mut spec = test_spec::(); + if !spec.fork_name_at_slot::(Slot::new(0)).gloas_enabled() { + return; + } + spec.heze_fork_epoch = Some(Epoch::new(1)); + let harness = BeaconChainHarness::builder(E::default()) + .spec(Arc::new(spec)) + .deterministic_keypairs(64) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + let slot = Epoch::new(2).start_slot(E::slots_per_epoch()); + let inclusion_list_slot = slot - 1; + harness.extend_to_slot(inclusion_list_slot).await; + + let (committee, dependent_root) = harness + .chain + .inclusion_list_committee(harness.head_block_root(), inclusion_list_slot) + .unwrap(); + let timely_submitter = committee[0]; + let late_submitter = *committee + .iter() + .find(|index| **index != timely_submitter) + .unwrap(); + for (validator_index, tx_byte, is_timely) in [ + (timely_submitter, 0xaa, true), + (late_submitter, 0xbb, false), + ] { + harness + .chain + .inclusion_list_store + .write() + .process_inclusion_list( + SignedInclusionList { + message: InclusionList { + slot: inclusion_list_slot, + validator_index, + dependent_root, + transactions: ProgressiveTransactions::new(vec![ + ProgressiveVariableList::new(vec![tx_byte]).unwrap(), + ]) + .unwrap(), + }, + signature: Signature::empty(), + }, + is_timely, + ); + } + + let new_payload_calls = Arc::new(Mutex::new(vec![])); + let new_payload_calls_inner = new_payload_calls.clone(); + harness + .mock_execution_layer + .as_ref() + .unwrap() + .server + .ctx + .hook + .lock() + .set_new_payload_hook(Box::new(move |method, params| { + new_payload_calls_inner + .lock() + .push((method.to_string(), params.clone())); + })); + + import_block_and_envelope(&harness, slot).await; + + let new_payload_calls = new_payload_calls.lock(); + let [(method, params)] = new_payload_calls.as_slice() else { + panic!("expected one newPayload call, got {new_payload_calls:?}"); + }; + assert_eq!(method, ENGINE_NEW_PAYLOAD_V6); + assert_eq!(params[4], serde_json::json!(["0xaa"])); +} diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 40a79d2f552..0bc4d7bb019 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -953,7 +953,7 @@ impl HttpJsonRpc { new_payload_request_heze: NewPayloadRequestHeze<'_, E>, ) -> Result { let params = json!([ - JsonExecutionPayload::Heze( + JsonExecutionPayload::Gloas( new_payload_request_heze .execution_payload .clone() diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index 57c1a006aea..d8a09fb32d6 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -9,9 +9,8 @@ use types::{ }; use types::{ ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb, - ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionPayloadHeze, - ExecutionRequestsElectra, ExecutionRequestsGloas, ExecutionRequestsRef, - ProgressiveTransactions, + ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequestsElectra, + ExecutionRequestsGloas, ExecutionRequestsRef, ProgressiveTransactions, }; #[superstruct( @@ -43,10 +42,8 @@ pub struct NewPayloadRequest<'block, E: EthSpec> { pub execution_payload: &'block ExecutionPayloadElectra, #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))] pub execution_payload: &'block ExecutionPayloadFulu, - #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))] + #[superstruct(only(Gloas, Heze), partial_getter(rename = "execution_payload_gloas"))] pub execution_payload: &'block ExecutionPayloadGloas, - #[superstruct(only(Heze), partial_getter(rename = "execution_payload_heze"))] - pub execution_payload: &'block ExecutionPayloadHeze, #[superstruct(only(Deneb, Electra, Fulu, Gloas, Heze))] pub versioned_hashes: Vec, #[superstruct(only(Deneb, Electra, Fulu, Gloas, Heze))] @@ -83,7 +80,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload), Self::Fulu(request) => ExecutionPayloadRef::Fulu(request.execution_payload), Self::Gloas(request) => ExecutionPayloadRef::Gloas(request.execution_payload), - Self::Heze(request) => ExecutionPayloadRef::Heze(request.execution_payload), + Self::Heze(request) => ExecutionPayloadRef::Gloas(request.execution_payload), } } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 9e443ae30a8..764516c783a 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -71,6 +71,7 @@ pub async fn handle_rpc( | ENGINE_NEW_PAYLOAD_V4 | ENGINE_NEW_PAYLOAD_V5 | ENGINE_NEW_PAYLOAD_V6 => { + ctx.hook.lock().on_new_payload(method, params); let request = match method { ENGINE_NEW_PAYLOAD_V1 => JsonExecutionPayload::Bellatrix( get_param::>(params, 0) @@ -209,9 +210,7 @@ pub async fn handle_rpc( } } ForkName::Heze => { - // TODO(heze): drop V5 here once `NewPayloadRequest::Heze` is constructed. The - // payload notifier still builds the Gloas request, so Heze blocks send V5. - if method != ENGINE_NEW_PAYLOAD_V5 && method != ENGINE_NEW_PAYLOAD_V6 { + if method != ENGINE_NEW_PAYLOAD_V6 { return Err(( format!("{} called after Heze fork!", method), GENERIC_ERROR_CODE, diff --git a/beacon_node/execution_layer/src/test_utils/hook.rs b/beacon_node/execution_layer/src/test_utils/hook.rs index 4653811ac90..e0fc5cd9768 100644 --- a/beacon_node/execution_layer/src/test_utils/hook.rs +++ b/beacon_node/execution_layer/src/test_utils/hook.rs @@ -1,4 +1,5 @@ use crate::json_structures::*; +use serde_json::Value as JsonValue; type ForkChoiceUpdatedHook = dyn Fn( JsonForkchoiceStateV1, @@ -7,9 +8,12 @@ type ForkChoiceUpdatedHook = dyn Fn( + Send + Sync; +type NewPayloadHook = dyn Fn(&str, &JsonValue) + Send + Sync; + #[derive(Default)] pub struct Hook { forkchoice_updated: Option>, + new_payload: Option>, } impl Hook { @@ -24,4 +28,14 @@ impl Hook { pub fn set_forkchoice_updated_hook(&mut self, f: Box) { self.forkchoice_updated = Some(f); } + + pub fn on_new_payload(&self, method: &str, params: &JsonValue) { + if let Some(f) = self.new_payload.as_ref() { + f(method, params) + } + } + + pub fn set_new_payload_hook(&mut self, f: Box) { + self.new_payload = Some(f); + } } From f63854fbbcebeb97a3aa7ea58dc0fb4b9f069e2d Mon Sep 17 00:00:00 2001 From: rahulbarman Date: Thu, 8 Oct 2026 23:04:29 +0530 Subject: [PATCH 2/2] Explain the Gloas payload in the Heze new payload request --- .../execution_layer/src/engine_api/new_payload_request.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs index d8a09fb32d6..76c71d17b52 100644 --- a/beacon_node/execution_layer/src/engine_api/new_payload_request.rs +++ b/beacon_node/execution_layer/src/engine_api/new_payload_request.rs @@ -80,6 +80,7 @@ impl<'block, E: EthSpec> NewPayloadRequest<'block, E> { Self::Electra(request) => ExecutionPayloadRef::Electra(request.execution_payload), Self::Fulu(request) => ExecutionPayloadRef::Fulu(request.execution_payload), Self::Gloas(request) => ExecutionPayloadRef::Gloas(request.execution_payload), + // The envelope payload is an `ExecutionPayloadGloas` for both Gloas and Heze. Self::Heze(request) => ExecutionPayloadRef::Gloas(request.execution_payload), } }