diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 728a09a475a..90c70fa501a 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2225,6 +2225,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 { + // 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 195be342846..2964daeee7e 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -150,6 +150,8 @@ pub enum BeaconChainError { BlockVariantLacksExecutionPayload(Hash256), ExecutionLayerErrorPayloadReconstruction(ExecutionBlockHash, Box), ExecutionLayerGetBlockByNumberFailed(Box), + ExecutionLayerGetInclusionListFailed(Box), + ExecutionHashMissingFromHead(Hash256), BlockHashMissingFromExecutionLayer(ExecutionBlockHash), InconsistentPayloadReconstructed { slot: Slot, diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 630e9d92118..f2ffc41067e 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(), @@ -3426,6 +3434,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 7b904260b8e..abe24abf6aa 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}; @@ -412,6 +413,63 @@ 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/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 38bcd05a77c..6056bbc3daf 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6007,6 +6007,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(); @@ -10065,6 +10077,23 @@ async fn get_validator_payload_attestation_data_pre_gloas() { .await; } +// TODO(heze): add IL fetching tests for: +// - happy-path +// - bad-slot +// - 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)] +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()) { diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index a02b948a909..a42f76f337d 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -3716,6 +3716,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 08dc2e00c5f..7a587dad087 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -896,6 +896,18 @@ impl TryFrom> for SkipRandaoVerification { } } +#[derive(Clone, Serialize, Deserialize)] +pub struct ValidatorInclusionListQuery { + pub slot: Slot, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct InclusionListTransactions { + #[serde(with = "ssz_types::serde_utils::prog_list_of_hex_prog_var_list")] + pub transactions: ProgressiveTransactions, +} + #[derive(Clone, Serialize, Deserialize)] pub struct ValidatorAttestationDataQuery { pub slot: Slot,