From 0ab6f01a544c35d7c252d2fff4a624905b9ce813 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 16 Jul 2026 20:07:18 +0300 Subject: [PATCH 1/6] Add method to produce ILs for the current slot --- beacon_node/beacon_chain/src/beacon_chain.rs | 33 ++++++++++++++++++++ beacon_node/beacon_chain/src/errors.rs | 2 ++ 2 files changed, 35 insertions(+) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a0e3ac4ae2f..2a5d187bb2c 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2206,6 +2206,39 @@ impl BeaconChain { }) } + /// Produce the inclusion list transactions for `request_slot`. + /// + /// The transactions are requested from the execution layer via + /// `getInclusionListV1`, built on top of the current head. + pub async fn produce_inclusion_list( + &self, + request_slot: Slot, + ) -> Result, Error> { + // Inclusion lists are only produced for the current slot. + let current_slot = self.slot()?; + if request_slot != current_slot { + return Err(Error::InvalidSlot(request_slot)); + } + + let execution_layer = self + .execution_layer + .as_ref() + .ok_or(Error::ExecutionLayerMissing)?; + + let fcu_params = self + .canonical_head + .cached_head() + .forkchoice_update_parameters(); + let head_hash = fcu_params + .head_hash + .ok_or(Error::ExecutionHashMissingFromHead(fcu_params.head_root))?; + + execution_layer + .get_inclusion_list_v1(head_hash) + .await + .map_err(|e| Error::ExecutionLayerGetInclusionListFailed(Box::new(e))) + } + /// Performs the same validation as `Self::verify_unaggregated_attestation_for_gossip`, but for /// multiple attestations using batch BLS verification. Batch verification can provide /// significant CPU-time savings compared to individual verification. diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index a37d6703f9f..69ef4ee423d 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -152,6 +152,8 @@ pub enum BeaconChainError { EngineGetCapabilititesFailed(Box), ExecutionLayerGetBlockByNumberFailed(Box), ExecutionLayerGetBlockByHashFailed(Box), + ExecutionLayerGetInclusionListFailed(Box), + ExecutionHashMissingFromHead(Hash256), BlockHashMissingFromExecutionLayer(ExecutionBlockHash), InconsistentPayloadReconstructed { slot: Slot, From 0401d613425dcb9ddf0a372aa67be76e5ebcc9f3 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 20 Jul 2026 13:19:38 +0300 Subject: [PATCH 2/6] Complete http api endpoint support --- beacon_node/http_api/src/lib.rs | 9 ++++ beacon_node/http_api/src/validator/mod.rs | 63 ++++++++++++++++++++++- common/eth2/src/lib.rs | 18 +++++++ common/eth2/src/types.rs | 13 +++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 03d0450627f..eceffb209df 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -2601,6 +2601,14 @@ pub async fn serve( task_spawner_filter.clone(), ); + // GET validator/inclusion_list?slot + let get_validator_inclusion_list = get_validator_inclusion_list( + eth_v1.clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + // GET validator/aggregate_attestation?attestation_data_root,slot let get_validator_aggregate_attestation = get_validator_aggregate_attestation( any_version.clone(), @@ -3429,6 +3437,7 @@ pub async fn serve( .uor(get_validator_execution_payload_envelopes) .uor(get_validator_attestation_data) .uor(get_validator_payload_attestation_data) + .uor(get_validator_inclusion_list) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) .uor(get_lighthouse_health) diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index 5287a1a3974..c6e19080dfe 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -17,8 +17,9 @@ use context_deserialize::ContextDeserialize; use eth2::CONSENSUS_VERSION_HEADER; use eth2::types::{ Accept, BeaconCommitteeSubscription, EndpointVersion, Failure, GenericResponse, - StandardLivenessResponseData, StateId as CoreStateId, ValidatorAggregateAttestationQuery, - ValidatorAttestationDataQuery, ValidatorBlocksQuery, ValidatorIndexData, ValidatorStatus, + InclusionListTransactions, StandardLivenessResponseData, StateId as CoreStateId, + ValidatorAggregateAttestationQuery, ValidatorAttestationDataQuery, ValidatorBlocksQuery, + ValidatorInclusionListQuery, ValidatorIndexData, ValidatorStatus, }; use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; @@ -391,6 +392,64 @@ pub fn get_validator_payload_attestation_data( .boxed() } +// GET validator/inclusion_list?slot +pub fn get_validator_inclusion_list( + 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("inclusion_list")) + .and(warp::path::end()) + .and(warp::query::()) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |query: ValidatorInclusionListQuery, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + not_synced_filter?; + + let slot = query.slot; + let fork_name = chain.spec.fork_name_at_slot::(slot); + + // Inclusion lists are only valid for Heze and later forks. + if !fork_name.heze_enabled() { + return Err(warp_utils::reject::custom_bad_request(format!( + "Inclusion lists are not supported for fork: {fork_name}" + ))); + } + + let transactions = + chain + .produce_inclusion_list(slot) + .await + .map_err(|e| match e { + BeaconChainError::InvalidSlot(_) => { + warp_utils::reject::custom_bad_request(format!( + "Unable to produce inclusion list: {e:?}" + )) + } + _ => warp_utils::reject::custom_server_error(format!( + "Unable to produce inclusion list: {e:?}" + )), + })?; + + let response = GenericResponse::from(InclusionListTransactions:: { + transactions, + }); + Ok(warp::reply::json(&response).into_response()) + }) + }, + ) + .boxed() +} + // GET validator/blinded_blocks/{slot} pub fn get_validator_blinded_blocks( eth_v1: EthV1Filter, diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index b216362b389..df17c8228f1 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3288,6 +3288,24 @@ impl BeaconNodeHttpClient { .transpose() } + /// `GET validator/inclusion_list?slot` + pub async fn get_validator_inclusion_list( + &self, + slot: Slot, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("inclusion_list"); + + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + + self.get(path).await + } + /// `GET v1/validator/aggregate_attestation?slot,attestation_data_root` pub async fn get_validator_aggregate_attestation_v1( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index f941857d28f..e03736a7eba 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -895,6 +895,19 @@ impl TryFrom> for SkipRandaoVerification { } } +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorInclusionListQuery { + pub slot: Slot, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(bound = "E: EthSpec")] +#[serde(transparent)] +pub struct InclusionListTransactions { + #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] + pub transactions: Transactions, +} + #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorAttestationDataQuery { pub slot: Slot, From 1e7f9633e3b09a8d6c2ff1a3cfdb0fc3a00c553d Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 20 Jul 2026 13:47:14 +0300 Subject: [PATCH 3/6] Add pre-fork test --- beacon_node/http_api/tests/tests.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 6d71a1cdddc..b4d96cc9aab 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -5767,6 +5767,18 @@ impl ApiTester { self } + pub async fn test_get_validator_inclusion_list_pre_heze(self) -> Self { + let slot = self.chain.slot().unwrap(); + + // The endpoint should return a 400 error for pre-Heze forks. + match self.client.get_validator_inclusion_list::(slot).await { + Ok(result) => panic!("query for a pre-Heze slot should fail, got: {result:?}"), + Err(e) => assert_eq!(e.status().unwrap(), 400), + } + + self + } + pub async fn test_get_validator_payload_attestation_data_no_block(self) -> Self { // Advance the slot clock without producing a block self.harness.advance_slot(); @@ -9774,6 +9786,23 @@ async fn get_validator_payload_attestation_data_pre_gloas() { .await; } +// TODO(heze): add tests for: +// - happy-path +// - bad-slot +// - EL-failure +// +// The above tests should be added once the harness supports building a Heze chain. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_validator_inclusion_list_pre_heze() { + if fork_name_from_env().is_some_and(|f| f.heze_enabled()) { + return; + } + ApiTester::new() + .await + .test_get_validator_inclusion_list_pre_heze() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_validator_payload_attestation_data_no_block() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { From 23164013dde3e3de311f32573c1fde8bf82a5309 Mon Sep 17 00:00:00 2001 From: conache Date: Mon, 20 Jul 2026 15:35:00 +0300 Subject: [PATCH 4/6] Tweak todo --- beacon_node/http_api/tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index b4d96cc9aab..b68e71e0ff5 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -9786,10 +9786,10 @@ async fn get_validator_payload_attestation_data_pre_gloas() { .await; } -// TODO(heze): add tests for: +// TODO(heze): add IL fetching tests for: // - happy-path // - bad-slot -// - EL-failure +// - EL call failure // // The above tests should be added once the harness supports building a Heze chain. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] From 004f022e2f89677c43caa236a8b024667dedde26 Mon Sep 17 00:00:00 2001 From: conache Date: Thu, 6 Aug 2026 12:36:35 +0300 Subject: [PATCH 5/6] Use ProgressiveTransactions in the InclusionListTransactions struct --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/http_api/src/validator/mod.rs | 5 ++--- common/eth2/src/lib.rs | 4 ++-- common/eth2/src/types.rs | 7 +++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 2a5d187bb2c..4adbcf44c0a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2213,7 +2213,7 @@ impl BeaconChain { pub async fn produce_inclusion_list( &self, request_slot: Slot, - ) -> Result, Error> { + ) -> Result { // Inclusion lists are only produced for the current slot. let current_slot = self.slot()?; if request_slot != current_slot { diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index c6e19080dfe..c94d3ac7882 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -440,9 +440,8 @@ pub fn get_validator_inclusion_list( )), })?; - let response = GenericResponse::from(InclusionListTransactions:: { - transactions, - }); + let response = + GenericResponse::from(InclusionListTransactions { transactions }); Ok(warp::reply::json(&response).into_response()) }) }, diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index df17c8228f1..ef3ac9f0261 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3289,10 +3289,10 @@ impl BeaconNodeHttpClient { } /// `GET validator/inclusion_list?slot` - pub async fn get_validator_inclusion_list( + pub async fn get_validator_inclusion_list( &self, slot: Slot, - ) -> Result>, Error> { + ) -> Result, Error> { let mut path = self.eth_path(V1)?; path.path_segments_mut() diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index e03736a7eba..230a97b2a40 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -901,11 +901,10 @@ pub struct ValidatorInclusionListQuery { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(bound = "E: EthSpec")] #[serde(transparent)] -pub struct InclusionListTransactions { - #[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] - pub transactions: Transactions, +pub struct InclusionListTransactions { + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] + pub transactions: ProgressiveTransactions, } #[derive(Clone, Serialize, Deserialize)] From 985fc3d7a4ff917ee8b88e2e14dda68b4eea365e Mon Sep 17 00:00:00 2001 From: conache Date: Wed, 12 Aug 2026 13:23:11 +0300 Subject: [PATCH 6/6] Fix test in ci --- beacon_node/http_api/tests/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index b68e71e0ff5..bb0dbccc5d0 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -5771,7 +5771,7 @@ impl ApiTester { let slot = self.chain.slot().unwrap(); // The endpoint should return a 400 error for pre-Heze forks. - match self.client.get_validator_inclusion_list::(slot).await { + match self.client.get_validator_inclusion_list(slot).await { Ok(result) => panic!("query for a pre-Heze slot should fail, got: {result:?}"), Err(e) => assert_eq!(e.status().unwrap(), 400), }